| // 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 | 
 | } |