blob: 3d6b7fa9f1c0aaaf49d6ade740e68b31eec79687 [file] [log] [blame]
Lorenz Brun1cf17952023-02-13 17:41:59 +01001// 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.
8package bootparam
9
10import (
11 "errors"
12 "fmt"
13 "strings"
14)
15
16// Param represents a single boot parameter with or without a value
17type Param struct {
18 Param, Value string
19 HasValue bool
20}
21
22// Params represents a list of kernel boot parameters
23type Params []Param
24
25// Linux has for historical reasons an unusual definition of this function
26// Taken from @linux//lib:ctype.c
27func 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.
40func 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
52func 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
61func 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.
89func 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).
144func 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}