Lorenz Brun | 1cf1795 | 2023-02-13 17:41:59 +0100 | [diff] [blame] | 1 | // Package bootparam implements encoding and decoding of Linux kernel command |
| 2 | // lines as documented in |
| 3 | // https://docs.kernel.org/admin-guide/kernel-parameters.html |
| 4 | // |
| 5 | // The format is quite quirky and thus the implementation is mostly based |
| 6 | // on the code in the Linux kernel implementing the decoder and not the |
| 7 | // specification. |
| 8 | package bootparam |
| 9 | |
| 10 | import ( |
| 11 | "errors" |
| 12 | "fmt" |
| 13 | "strings" |
| 14 | ) |
| 15 | |
| 16 | // Param represents a single boot parameter with or without a value |
| 17 | type Param struct { |
| 18 | Param, Value string |
| 19 | HasValue bool |
| 20 | } |
| 21 | |
| 22 | // Params represents a list of kernel boot parameters |
| 23 | type Params []Param |
| 24 | |
| 25 | // Linux has for historical reasons an unusual definition of this function |
| 26 | // Taken from @linux//lib:ctype.c |
| 27 | func isSpace(r byte) bool { |
| 28 | switch r { |
| 29 | case '\t', '\n', '\v', '\f', '\r', ' ', 0xa0: |
| 30 | return true |
| 31 | default: |
| 32 | return false |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | // Trim spaces as defined by Linux from the left of the string. |
| 37 | // This is only exported for tests, do not use this. Because of import loops |
| 38 | // as well as cgo restrictions this cannot be an internal function used by |
| 39 | // tests. |
| 40 | func TrimLeftSpace(s string) string { |
| 41 | start := 0 |
| 42 | for ; start < len(s); start++ { |
| 43 | c := s[start] |
| 44 | if !isSpace(c) { |
| 45 | break |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | return s[start:] |
| 50 | } |
| 51 | |
| 52 | func containsSpace(s string) bool { |
| 53 | for i := 0; i < len(s); i++ { |
| 54 | if isSpace(s[i]) { |
| 55 | return true |
| 56 | } |
| 57 | } |
| 58 | return false |
| 59 | } |
| 60 | |
| 61 | func parseToken(token string) (p Param, err error) { |
| 62 | if strings.HasPrefix(token, `=`) || strings.HasPrefix(token, `"=`) { |
| 63 | return Param{}, errors.New("param contains `=` at first position, this causes broken behavior") |
| 64 | } |
| 65 | param, value, hasValue := strings.Cut(token, "=") |
| 66 | |
| 67 | if strings.HasPrefix(param, `"`) { |
| 68 | p.Param = strings.TrimPrefix(param, `"`) |
| 69 | if !hasValue { |
| 70 | p.Param = strings.TrimSuffix(p.Param, `"`) |
| 71 | } |
| 72 | } else { |
| 73 | p.Param = param |
| 74 | } |
| 75 | if hasValue { |
| 76 | if strings.HasPrefix(value, `"`) { |
| 77 | p.Value = strings.TrimSuffix(strings.TrimPrefix(value, `"`), `"`) |
| 78 | } else if strings.HasPrefix(param, `"`) { |
| 79 | p.Value = strings.TrimSuffix(value, `"`) |
| 80 | } else { |
| 81 | p.Value = value |
| 82 | } |
| 83 | } |
| 84 | return |
| 85 | } |
| 86 | |
| 87 | // Unmarshal decodes a Linux kernel command line and returns a list of kernel |
| 88 | // parameters as well as a rest section after the "--" parsing terminator. |
| 89 | func Unmarshal(cmdline string) (params Params, rest string, err error) { |
| 90 | cmdline = TrimLeftSpace(cmdline) |
| 91 | if pos := strings.IndexByte(cmdline, 0x00); pos != -1 { |
| 92 | cmdline = cmdline[:pos] |
| 93 | } |
| 94 | var lastIdx int |
| 95 | var inQuote bool |
| 96 | var p Param |
| 97 | for i := 0; i < len(cmdline); i++ { |
| 98 | if isSpace(cmdline[i]) && !inQuote { |
| 99 | token := cmdline[lastIdx:i] |
| 100 | lastIdx = i + 1 |
| 101 | if TrimLeftSpace(token) == "" { |
| 102 | continue |
| 103 | } |
| 104 | p, err = parseToken(token) |
| 105 | if err != nil { |
| 106 | return |
| 107 | } |
| 108 | |
| 109 | // Stop processing and return everything left as rest |
| 110 | if p.Param == "--" { |
| 111 | rest = TrimLeftSpace(cmdline[lastIdx:]) |
| 112 | return |
| 113 | } |
| 114 | params = append(params, p) |
| 115 | } |
| 116 | if cmdline[i] == '"' { |
| 117 | inQuote = !inQuote |
| 118 | } |
| 119 | } |
| 120 | if len(cmdline)-lastIdx > 0 { |
| 121 | token := cmdline[lastIdx:] |
| 122 | if TrimLeftSpace(token) == "" { |
| 123 | return |
| 124 | } |
| 125 | p, err = parseToken(token) |
| 126 | if err != nil { |
| 127 | return |
| 128 | } |
| 129 | |
| 130 | // Stop processing, do not set rest as there is none |
| 131 | if p.Param == "--" { |
| 132 | return |
| 133 | } |
| 134 | params = append(params, p) |
| 135 | } |
| 136 | return |
| 137 | } |
| 138 | |
| 139 | // Marshal encodes a set of kernel parameters and an optional rest string into |
| 140 | // a Linux kernel command line. It rejects data which is not encodable, which |
| 141 | // includes null bytes, double quotes in params as well as characters which |
| 142 | // contain 0xa0 in their UTF-8 representation (historical Linux quirk of |
| 143 | // treating that as a space, inherited from Latin-1). |
| 144 | func Marshal(params Params, rest string) (string, error) { |
| 145 | if strings.IndexByte(rest, 0x00) != -1 { |
| 146 | return "", errors.New("rest contains 0x00 byte, this is disallowed") |
| 147 | } |
| 148 | var strb strings.Builder |
| 149 | for _, p := range params { |
| 150 | if strings.ContainsRune(p.Param, '=') { |
| 151 | return "", fmt.Errorf("invalid '=' character in param %q", p.Param) |
| 152 | } |
| 153 | // Technically a weird subset of double quotes can be encoded, but |
| 154 | // this should probably not be done so just reject them all. |
| 155 | if strings.ContainsRune(p.Param, '"') { |
| 156 | return "", fmt.Errorf("invalid '\"' character in param %q", p.Param) |
| 157 | } |
| 158 | if strings.ContainsRune(p.Value, '"') { |
| 159 | return "", fmt.Errorf("invalid '\"' character in value %q", p.Value) |
| 160 | } |
| 161 | if strings.IndexByte(p.Param, 0x00) != -1 { |
| 162 | return "", fmt.Errorf("invalid null byte in param %q", p.Param) |
| 163 | } |
| 164 | if strings.IndexByte(p.Value, 0x00) != -1 { |
| 165 | return "", fmt.Errorf("invalid null byte in value %q", p.Value) |
| 166 | } |
| 167 | // Linux treats 0xa0 as a space, even though it is a valid UTF-8 |
| 168 | // surrogate. This is unfortunate, but passing it through would |
| 169 | // break the whole command line. |
| 170 | if strings.IndexByte(p.Param, 0xa0) != -1 { |
| 171 | return "", fmt.Errorf("invalid 0xa0 byte in param %q", p.Param) |
| 172 | } |
| 173 | if strings.IndexByte(p.Value, 0xa0) != -1 { |
| 174 | return "", fmt.Errorf("invalid 0xa0 byte in value %q", p.Value) |
| 175 | } |
| 176 | if strings.ContainsRune(p.Param, '"') { |
| 177 | return "", fmt.Errorf("invalid '\"' character in value %q", p.Value) |
| 178 | } |
| 179 | // This should be allowed according to the docs, but is in fact broken. |
| 180 | if p.Value != "" && containsSpace(p.Param) { |
| 181 | return "", fmt.Errorf("param %q contains spaces and value, this is unsupported", p.Param) |
| 182 | } |
| 183 | if p.Param == "--" { |
| 184 | return "", errors.New("param '--' is reserved and cannot be used") |
| 185 | } |
| 186 | if p.Param == "" { |
| 187 | return "", errors.New("empty params are not supported") |
| 188 | } |
| 189 | if containsSpace(p.Param) { |
| 190 | strb.WriteRune('"') |
| 191 | strb.WriteString(p.Param) |
| 192 | strb.WriteRune('"') |
| 193 | } else { |
| 194 | strb.WriteString(p.Param) |
| 195 | } |
| 196 | if p.Value != "" { |
| 197 | strb.WriteRune('=') |
| 198 | if containsSpace(p.Value) { |
| 199 | strb.WriteRune('"') |
| 200 | strb.WriteString(p.Value) |
| 201 | strb.WriteRune('"') |
| 202 | } else { |
| 203 | strb.WriteString(p.Value) |
| 204 | } |
| 205 | } |
| 206 | strb.WriteRune(' ') |
| 207 | } |
| 208 | if len(rest) > 0 { |
| 209 | strb.WriteString("-- ") |
| 210 | // Starting whitespace will be dropped by the decoder anyways, do it |
| 211 | // here to make the resulting command line nicer. |
| 212 | strb.WriteString(TrimLeftSpace(rest)) |
| 213 | } |
| 214 | return strb.String(), nil |
| 215 | } |