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