Created
November 18, 2020 02:30
-
-
Save anonymouse64/ab77e07ed9ef5406164646ec9042f617 to your computer and use it in GitHub Desktop.
pointless overly complicated and overly engineered kernel cmdline parsing utilities using SplitFunc supporting quotes (but not nested quotes!) in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// -*- Mode: Go; indent-tabs-mode: t -*- | |
/* | |
* Copyright (C) 2020 Canonical Ltd | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License version 3 as | |
* published by the Free Software Foundation. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
* | |
*/ | |
package osutil | |
import ( | |
"bufio" | |
"bytes" | |
"fmt" | |
"io/ioutil" | |
"strings" | |
"unicode" | |
"unicode/utf8" | |
) | |
var ( | |
procCmdline = "/proc/cmdline" | |
) | |
// scanWordsWithQuotes is a split function for a Scanner that returns each | |
// space-separated word of text, with surrounding spaces deleted. It also | |
// properly handles quotes, in that if there is an opening quote, then instead | |
// of space-separated words, quoted words are returned. If the quoted word is | |
// directly following non-spaces, then the whole word is returned, starting with | |
// the non-space characters, then the quote character, then the words within the | |
// quote, then the closing quote character. The quote character is either " or | |
// ', but must be consistent (i.e. an opening " will only be considered closed | |
// with another "). It does not support handling nested quotes of the same type, | |
// but does accept different quotes nested within such as in the string | |
// "this is 'nested'" will be returned as (verbatim) `"this is 'nested'"`. | |
func scanWordsWithQuotes(data []byte, atEOF bool) (advance int, token []byte, err error) { | |
// Skip leading spaces. | |
start := 0 | |
for width := 0; start < len(data); start += width { | |
var r rune | |
r, width = utf8.DecodeRune(data[start:]) | |
if !unicode.IsSpace(r) { | |
break | |
} | |
} | |
// handle the trivial case where we just have leading spaces in data | |
if len(data) <= start { | |
return start, nil, nil | |
} | |
// Scan until either a space (which is the end of an unquoted word) or a | |
// quote, which is the start of a quoted word. If we find an unquoted word, | |
// then we also | |
checkEndChar := unicode.IsSpace | |
startedQuote := false | |
finishedQuote := false | |
for width, i := 0, start; i < len(data); i += width { | |
var r rune | |
r, width = utf8.DecodeRune(data[i:]) | |
if r == '\'' { | |
if !startedQuote { | |
// we found the start of a quote | |
startedQuote = true | |
// change the end condition to look for the ' | |
checkEndChar = func(r rune) bool { | |
return r == '\'' | |
} | |
// skip the end check since we need to at least get one more | |
// character to end the quote | |
continue | |
} | |
// otherwise we already started a quote and will have set | |
// checkEndChar appropriately | |
} | |
if r == '"' { | |
if !startedQuote { | |
// we found the start of a quote | |
startedQuote = true | |
// change the end condition to look for the " | |
checkEndChar = func(r rune) bool { | |
return r == '"' | |
} | |
// skip the end check since we need to at least get one more | |
// character to end the quote | |
continue | |
} | |
// otherwise we already started a quote and will have set | |
// checkEndChar appropriately | |
} | |
if checkEndChar(r) { | |
if startedQuote && !finishedQuote { | |
// then we finished the quote and should continue until we hit | |
// a space to indicate the end of the "word" | |
checkEndChar = unicode.IsSpace | |
finishedQuote = true | |
continue | |
} | |
return i + width, data[start:i], nil | |
} | |
} | |
if startedQuote { | |
// if we started a quote but are not yet at EOF, then request more data | |
if !atEOF { | |
return start, nil, nil | |
} | |
// if we did start a quote but are not yet at EOF, check if we | |
// finished the quote - if we did then we just return what we got | |
// this case is when the EOF happens at the end of a quote | |
if finishedQuote { | |
return len(data), data[start:], nil | |
} | |
// otherwise we are at EOF, but we didn't finish a quote, so we have an | |
// invalid input and we should completely error - the quoting is | |
// incomplete | |
return 0, nil, fmt.Errorf("cannot scan words: incomplete quoting") | |
} | |
// otherwise if we didn't start a quote, and we are at EOF we have a final, | |
// non-empty, non-terminated word. Return it. | |
if atEOF && len(data) > start { | |
return len(data), data[start:], nil | |
} | |
// Request more data. | |
return start, nil, nil | |
} | |
// deQuote will delete the first specified quote character and the last one in | |
// the provided string. If the provided string does not contain exactly 2 | |
// of the provided quote character, then it does nothing and returns the string | |
// as-is. | |
// Suitable for use on text scanned with the SplitFunc scanWordsWithQuotes where | |
// the only valid words returned that contain quotes can only contain exactly 2 | |
// quotes of any one type. | |
func deQuote(s string, quoteChar rune) string { | |
if strings.Count(s, string(quoteChar)) != 2 { | |
// just don't do anything, we can't de-quote something that doesn't have | |
// multiple quotes | |
return s | |
} | |
startIndex := strings.IndexRune(s, quoteChar) | |
// drop the first | |
newStart := s[:startIndex] | |
newEnd := s[startIndex+1:] | |
newS := newStart + newEnd | |
// now do it again for the second quote | |
secondIndex := strings.IndexRune(newS, quoteChar) | |
// drop it again | |
finalStart := newS[:secondIndex] | |
finalEnd := newS[secondIndex+1:] | |
return finalStart + finalEnd | |
} | |
// charNotBetweenOtherChars returns whether or not the first rune is in between two | |
// instances of the second rune. | |
func charNotBetweenOtherChars(s string, s1, s2 rune) bool { | |
inBetweenRuneCount := strings.Count(s, string(s2)) | |
if inBetweenRuneCount == 1 || inBetweenRuneCount == 0 { | |
// can't be in between something if there's not at least 2 of it | |
return true | |
} | |
if inBetweenRuneCount > 2 { | |
// if there are more than 2 chars, we can't really say for sure, as it's | |
// unclear which one should be used as the, but the safer bet is that | |
// yes, there could be something fishy, so don't parse it as if there | |
// wasn't | |
return false | |
} | |
// otherwise we have two, and so we just make sure that the index of the | |
// first rune is greater than the first instance of the second rune, and the | |
// last instance of the second rune | |
firstInstanceFirstRune := strings.IndexRune(s, s1) | |
firstInstanceSecondRune := strings.IndexRune(s, s2) | |
secondInstanceSecondRune := strings.IndexRune(s[firstInstanceSecondRune:], s2) | |
return !(firstInstanceFirstRune > firstInstanceSecondRune && firstInstanceFirstRune < secondInstanceSecondRune) | |
} | |
// ParseKernelCommandLineParameters parses the kernel command line and separates | |
// the key value pairs (specifically, parameters that use "=" as the delimiter | |
// between the key and the value) from all non key value pair parameters and | |
// returns both sets of parameters. | |
// If there are duplicate key-value pairs, then the pair that appears last will | |
// be the one that appears in the map. | |
func ParseKernelCommandLineParameters() (map[string]string, []string, error) { | |
cmdline, err := ioutil.ReadFile(procCmdline) | |
if err != nil { | |
return nil, nil, err | |
} | |
scanner := bufio.NewScanner(bytes.NewBuffer(cmdline)) | |
// handle quotes when scanning | |
scanner.Split(scanWordsWithQuotes) | |
var keyValParams map[string]string | |
var plainParams []string | |
for scanner.Scan() { | |
w := scanner.Text() | |
// if there are quote characters in the text, then it might be something | |
// we need to de-quote, but if there are multiple kinds of quotes, then | |
// don't try anything fancy | |
switch { | |
case strings.Contains(w, "'") && strings.Contains(w, "\"") && strings.Contains(w, "="): | |
// we need a more complicated state machine to parse/handle nested | |
// quotes, so just skip de-quoting this one and trying to figure out | |
// where the equals sign and the quotes live relative to one another | |
// TODO: maybe we should error out here to make clear to callers we | |
// don't support nested quoting? | |
plainParams = append(plainParams, w) | |
continue | |
// in other cases, if there's a quote and the quoted part is not | |
// containing a "=", then there must be a single pair of somewhere | |
// and we should de-quote it to be able to parse out the key/value | |
case strings.Contains(w, "'") && charNotBetweenOtherChars(w, '=', '\''): | |
w = deQuote(w, '\'') | |
case strings.Contains(w, "\"") && charNotBetweenOtherChars(w, '=', '"'): | |
w = deQuote(w, '"') | |
// otherwise don't de-quote it | |
} | |
if strings.Contains(w, "=") { | |
pair := strings.SplitN(w, "=", 2) | |
// this has the side-effect that later keys override earlier ones | |
if keyValParams == nil { | |
keyValParams = make(map[string]string, 1) | |
} | |
keyValParams[pair[0]] = pair[1] | |
} else { | |
plainParams = append(plainParams, w) | |
} | |
} | |
// be robust and still return what we accumulated, some callers may not care | |
// if the end of the cmd line is malformed, as long as they find what | |
// they care about before that point | |
return keyValParams, plainParams, scanner.Err() | |
} | |
// MockProcCmdline overrides the path to /proc/cmdline. For use in tests. | |
func MockProcCmdline(newPath string) (restore func()) { | |
oldProcCmdline := procCmdline | |
procCmdline = newPath | |
return func() { | |
procCmdline = oldProcCmdline | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// -*- Mode: Go; indent-tabs-mode: t -*- | |
/* | |
* Copyright (C) 2020 Canonical Ltd | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License version 3 as | |
* published by the Free Software Foundation. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
* | |
*/ | |
package osutil_test | |
import ( | |
"io/ioutil" | |
"path/filepath" | |
"github.com/snapcore/snapd/dirs" | |
"github.com/snapcore/snapd/osutil" | |
"github.com/snapcore/snapd/testutil" | |
. "gopkg.in/check.v1" | |
) | |
type kernelCmdlineSuite struct { | |
testutil.BaseTest | |
} | |
var _ = Suite(&kernelCmdlineSuite{}) | |
func (s *kernelCmdlineSuite) SetUpTest(c *C) { | |
rootDir := c.MkDir() | |
dirs.SetRootDir(rootDir) | |
s.AddCleanup(func() { dirs.SetRootDir("") }) | |
} | |
func (s *kernelCmdlineSuite) TestKernelCommandline(c *C) { | |
tt := []struct { | |
procCmdline string | |
expPlain []string | |
expMap map[string]string | |
expErr string | |
comment string | |
}{ | |
{ | |
procCmdline: "", | |
comment: "empty", | |
}, | |
{ | |
procCmdline: "\"\"", | |
comment: "empty quotes is empty string", | |
expPlain: []string{ | |
"", | |
}, | |
}, | |
{ | |
procCmdline: `one two ""`, | |
comment: "empty quote is empty string with more before", | |
expPlain: []string{ | |
"one", | |
"two", | |
"", | |
}, | |
}, | |
{ | |
procCmdline: `one two "" another`, | |
comment: "empty quote is empty string with more before and after", | |
expPlain: []string{ | |
"one", | |
"two", | |
"", | |
"another", | |
}, | |
}, | |
{ | |
procCmdline: "key1=val1 key2=val2", | |
comment: "only key-val pairs", | |
expMap: map[string]string{ | |
"key1": "val1", | |
"key2": "val2", | |
}, | |
}, | |
{ | |
procCmdline: "empty-key=", | |
comment: "empty key value", | |
expMap: map[string]string{ | |
"empty-key": "", | |
}, | |
}, | |
{ | |
procCmdline: "before=something systemd.silliness=a=1 others=2", | |
comment: "multiple equals in value", | |
expMap: map[string]string{ | |
"before": "something", | |
"systemd.silliness": "a=1", | |
"others": "2", | |
}, | |
}, | |
{ | |
procCmdline: "key=val init 0 key2=other more stuff", | |
comment: "key value pairs and plain params", | |
expMap: map[string]string{ | |
"key": "val", | |
"key2": "other", | |
}, | |
expPlain: []string{ | |
"init", | |
"0", | |
"more", | |
"stuff", | |
}, | |
}, | |
{ | |
procCmdline: `"key=val" key2="val2"`, | |
comment: "quoted key values", | |
expMap: map[string]string{ | |
"key": "val", | |
"key2": "val2", | |
}, | |
}, | |
{ | |
procCmdline: `"key=val" key2="val2"`, | |
comment: "quoting just values", | |
expMap: map[string]string{ | |
"key": "val", | |
"key2": "val2", | |
}, | |
}, | |
{ | |
procCmdline: `"key=val" key2="this is a nested 'quote'"`, | |
comment: "nested quoted key values are ignored", | |
expMap: map[string]string{ | |
"key": "val", | |
}, | |
expPlain: []string{ | |
`key2="this is a nested 'quote'"`, | |
}, | |
}, | |
{ | |
procCmdline: `"key=val" init 0 key2=other "this is a long quote"`, | |
comment: "quoted plain values", | |
expMap: map[string]string{ | |
"key": "val", | |
"key2": "other", | |
}, | |
expPlain: []string{ | |
"init", | |
"0", | |
`this is a long quote`, | |
}, | |
}, | |
{ | |
procCmdline: `1"2"3`, | |
comment: "weird sandwiched quotes", | |
expPlain: []string{ | |
`123`, | |
}, | |
}, | |
{ | |
procCmdline: `bread" with peanut butter and "jelly`, | |
comment: "weird sandwiched quotes with spaces", | |
expPlain: []string{ | |
`bread with peanut butter and jelly`, | |
}, | |
}, | |
{ | |
procCmdline: `incomplete quoting"`, | |
comment: "incomplete quoting only fails last word", | |
expPlain: []string{ | |
"incomplete", | |
}, | |
expErr: "cannot scan words: incomplete quoting", | |
}, | |
{ | |
procCmdline: `complete quoting""`, | |
comment: "empty quote at end of word stripped off", | |
expPlain: []string{ | |
"complete", | |
"quoting", | |
}, | |
}, | |
{ | |
procCmdline: `'single quotes'`, | |
comment: "single quotes plain", | |
expPlain: []string{ | |
"single quotes", | |
}, | |
}, | |
{ | |
procCmdline: `k1='single quotes'`, | |
comment: "single quotes key value pairs", | |
expMap: map[string]string{ | |
"k1": "single quotes", | |
}, | |
}, | |
} | |
for _, t := range tt { | |
comment := Commentf(t.comment) | |
new := filepath.Join(c.MkDir(), "cmdline") | |
c.Assert(ioutil.WriteFile(new, []byte(t.procCmdline), 0644), IsNil, comment) | |
r := osutil.MockProcCmdline(new) | |
defer r() | |
m, plain, err := osutil.ParseKernelCommandLineParameters() | |
if t.expErr != "" { | |
c.Assert(err, ErrorMatches, t.expErr, comment) | |
} else { | |
c.Assert(err, IsNil, comment) | |
} | |
c.Assert(m, DeepEquals, t.expMap, comment) | |
c.Assert(plain, DeepEquals, t.expPlain, comment) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment