| // Package bootparam implements encoding and decoding of Linux kernel command |
| // lines as documented in |
| // https://docs.kernel.org/admin-guide/kernel-parameters.html |
| // |
| // The format is quite quirky and thus the implementation is mostly based |
| // on the code in the Linux kernel implementing the decoder and not the |
| // specification. |
| package bootparam |
| |
| import ( |
| "errors" |
| "fmt" |
| "strings" |
| ) |
| |
| // Param represents a single boot parameter with or without a value |
| type Param struct { |
| Param, Value string |
| HasValue bool |
| } |
| |
| // Params represents a list of kernel boot parameters |
| type Params []Param |
| |
| // Linux has for historical reasons an unusual definition of this function |
| // Taken from @linux//lib:ctype.c |
| func isSpace(r byte) bool { |
| switch r { |
| case '\t', '\n', '\v', '\f', '\r', ' ', 0xa0: |
| return true |
| default: |
| return false |
| } |
| } |
| |
| // Trim spaces as defined by Linux from the left of the string. |
| // This is only exported for tests, do not use this. Because of import loops |
| // as well as cgo restrictions this cannot be an internal function used by |
| // tests. |
| func TrimLeftSpace(s string) string { |
| start := 0 |
| for ; start < len(s); start++ { |
| c := s[start] |
| if !isSpace(c) { |
| break |
| } |
| } |
| |
| return s[start:] |
| } |
| |
| func containsSpace(s string) bool { |
| for i := 0; i < len(s); i++ { |
| if isSpace(s[i]) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func parseToken(token string) (p Param, err error) { |
| if strings.HasPrefix(token, `=`) || strings.HasPrefix(token, `"=`) { |
| return Param{}, errors.New("param contains `=` at first position, this causes broken behavior") |
| } |
| param, value, hasValue := strings.Cut(token, "=") |
| |
| if strings.HasPrefix(param, `"`) { |
| p.Param = strings.TrimPrefix(param, `"`) |
| if !hasValue { |
| p.Param = strings.TrimSuffix(p.Param, `"`) |
| } |
| } else { |
| p.Param = param |
| } |
| if hasValue { |
| if strings.HasPrefix(value, `"`) { |
| p.Value = strings.TrimSuffix(strings.TrimPrefix(value, `"`), `"`) |
| } else if strings.HasPrefix(param, `"`) { |
| p.Value = strings.TrimSuffix(value, `"`) |
| } else { |
| p.Value = value |
| } |
| } |
| return |
| } |
| |
| // Unmarshal decodes a Linux kernel command line and returns a list of kernel |
| // parameters as well as a rest section after the "--" parsing terminator. |
| func Unmarshal(cmdline string) (params Params, rest string, err error) { |
| cmdline = TrimLeftSpace(cmdline) |
| if pos := strings.IndexByte(cmdline, 0x00); pos != -1 { |
| cmdline = cmdline[:pos] |
| } |
| var lastIdx int |
| var inQuote bool |
| var p Param |
| for i := 0; i < len(cmdline); i++ { |
| if isSpace(cmdline[i]) && !inQuote { |
| token := cmdline[lastIdx:i] |
| lastIdx = i + 1 |
| if TrimLeftSpace(token) == "" { |
| continue |
| } |
| p, err = parseToken(token) |
| if err != nil { |
| return |
| } |
| |
| // Stop processing and return everything left as rest |
| if p.Param == "--" { |
| rest = TrimLeftSpace(cmdline[lastIdx:]) |
| return |
| } |
| params = append(params, p) |
| } |
| if cmdline[i] == '"' { |
| inQuote = !inQuote |
| } |
| } |
| if len(cmdline)-lastIdx > 0 { |
| token := cmdline[lastIdx:] |
| if TrimLeftSpace(token) == "" { |
| return |
| } |
| p, err = parseToken(token) |
| if err != nil { |
| return |
| } |
| |
| // Stop processing, do not set rest as there is none |
| if p.Param == "--" { |
| return |
| } |
| params = append(params, p) |
| } |
| return |
| } |
| |
| // Marshal encodes a set of kernel parameters and an optional rest string into |
| // a Linux kernel command line. It rejects data which is not encodable, which |
| // includes null bytes, double quotes in params as well as characters which |
| // contain 0xa0 in their UTF-8 representation (historical Linux quirk of |
| // treating that as a space, inherited from Latin-1). |
| func Marshal(params Params, rest string) (string, error) { |
| if strings.IndexByte(rest, 0x00) != -1 { |
| return "", errors.New("rest contains 0x00 byte, this is disallowed") |
| } |
| var strb strings.Builder |
| for _, p := range params { |
| if strings.ContainsRune(p.Param, '=') { |
| return "", fmt.Errorf("invalid '=' character in param %q", p.Param) |
| } |
| // Technically a weird subset of double quotes can be encoded, but |
| // this should probably not be done so just reject them all. |
| if strings.ContainsRune(p.Param, '"') { |
| return "", fmt.Errorf("invalid '\"' character in param %q", p.Param) |
| } |
| if strings.ContainsRune(p.Value, '"') { |
| return "", fmt.Errorf("invalid '\"' character in value %q", p.Value) |
| } |
| if strings.IndexByte(p.Param, 0x00) != -1 { |
| return "", fmt.Errorf("invalid null byte in param %q", p.Param) |
| } |
| if strings.IndexByte(p.Value, 0x00) != -1 { |
| return "", fmt.Errorf("invalid null byte in value %q", p.Value) |
| } |
| // Linux treats 0xa0 as a space, even though it is a valid UTF-8 |
| // surrogate. This is unfortunate, but passing it through would |
| // break the whole command line. |
| if strings.IndexByte(p.Param, 0xa0) != -1 { |
| return "", fmt.Errorf("invalid 0xa0 byte in param %q", p.Param) |
| } |
| if strings.IndexByte(p.Value, 0xa0) != -1 { |
| return "", fmt.Errorf("invalid 0xa0 byte in value %q", p.Value) |
| } |
| if strings.ContainsRune(p.Param, '"') { |
| return "", fmt.Errorf("invalid '\"' character in value %q", p.Value) |
| } |
| // This should be allowed according to the docs, but is in fact broken. |
| if p.Value != "" && containsSpace(p.Param) { |
| return "", fmt.Errorf("param %q contains spaces and value, this is unsupported", p.Param) |
| } |
| if p.Param == "--" { |
| return "", errors.New("param '--' is reserved and cannot be used") |
| } |
| if p.Param == "" { |
| return "", errors.New("empty params are not supported") |
| } |
| if containsSpace(p.Param) { |
| strb.WriteRune('"') |
| strb.WriteString(p.Param) |
| strb.WriteRune('"') |
| } else { |
| strb.WriteString(p.Param) |
| } |
| if p.Value != "" { |
| strb.WriteRune('=') |
| if containsSpace(p.Value) { |
| strb.WriteRune('"') |
| strb.WriteString(p.Value) |
| strb.WriteRune('"') |
| } else { |
| strb.WriteString(p.Value) |
| } |
| } |
| strb.WriteRune(' ') |
| } |
| if len(rest) > 0 { |
| strb.WriteString("-- ") |
| // Starting whitespace will be dropped by the decoder anyways, do it |
| // here to make the resulting command line nicer. |
| strb.WriteString(TrimLeftSpace(rest)) |
| } |
| return strb.String(), nil |
| } |