// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build unix package work import ( "bytes" "internal/testenv" "strings" "testing" "unicode" ) func FuzzSplitPkgConfigOutput(f *testing.F) { testenv.MustHaveExecPath(f, "/bin/sh") f.Add([]byte(`$FOO`)) f.Add([]byte(`\$FOO`)) f.Add([]byte(`${FOO}`)) f.Add([]byte(`\${FOO}`)) f.Add([]byte(`$(/bin/false)`)) f.Add([]byte(`\$(/bin/false)`)) f.Add([]byte(`$((0))`)) f.Add([]byte(`\$((0))`)) f.Add([]byte(`unescaped space`)) f.Add([]byte(`escaped\ space`)) f.Add([]byte(`"unterminated quote`)) f.Add([]byte(`'unterminated quote`)) f.Add([]byte(`unterminated escape\`)) f.Add([]byte(`"quote with unterminated escape\`)) f.Add([]byte(`'quoted "double quotes"'`)) f.Add([]byte(`"quoted 'single quotes'"`)) f.Add([]byte(`"\$0"`)) f.Add([]byte(`"\$\0"`)) f.Add([]byte(`"\$"`)) f.Add([]byte(`"\$ "`)) // Example positive inputs from TestSplitPkgConfigOutput. // Some bare newlines have been removed so that the inputs // are valid in the shell script we use for comparison. f.Add([]byte(`-r:foo -L/usr/white\ space/lib -lfoo\ bar -lbar\ baz`)) f.Add([]byte(`-lextra\ fun\ arg\\`)) f.Add([]byte("\textra whitespace\r")) f.Add([]byte(" \r ")) f.Add([]byte(`"-r:foo" "-L/usr/white space/lib" "-lfoo bar" "-lbar baz"`)) f.Add([]byte(`"-lextra fun arg\\"`)) f.Add([]byte(`" \r\n\ "`)) f.Add([]byte(`""`)) f.Add([]byte(``)) f.Add([]byte(`"\\"`)) f.Add([]byte(`"\x"`)) f.Add([]byte(`"\\x"`)) f.Add([]byte(`'\\'`)) f.Add([]byte(`'\x'`)) f.Add([]byte(`"\\x"`)) f.Add([]byte("\\\n")) f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED='"/test/share/doc"'`)) f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED="/test/share/doc"`)) f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED=\"/test/share/doc\"`)) f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED='/test/share/doc'`)) f.Add([]byte(`-DQUOTED='/te\st/share/d\oc'`)) f.Add([]byte(`-Dhello=10 -Dworld=+32 -DDEFINED_FROM_PKG_CONFIG=hello\ world`)) f.Add([]byte(`"broken\"" \\\a "a"`)) // Example negative inputs from TestSplitPkgConfigOutput. f.Add([]byte(`" \r\n `)) f.Add([]byte(`"-r:foo" "-L/usr/white space/lib "-lfoo bar" "-lbar baz"`)) f.Add([]byte(`"-lextra fun arg\\`)) f.Add([]byte(`broken flag\`)) f.Add([]byte(`extra broken flag \`)) f.Add([]byte(`\`)) f.Add([]byte(`"broken\"" "extra" \`)) f.Fuzz(func(t *testing.T, b []byte) { t.Parallel() if bytes.ContainsAny(b, "*?[#~%\x00{}!") { t.Skipf("skipping %#q: contains a sometimes-quoted character", b) } // splitPkgConfigOutput itself rejects inputs that contain unquoted // shell operator characters. (Quoted shell characters are fine.) for _, c := range b { if c > unicode.MaxASCII { t.Skipf("skipping %#q: contains a non-ASCII character %q", b, c) } if !unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c)) { t.Skipf("skipping %#q: contains non-graphic character %q", b, c) } } args, err := splitPkgConfigOutput(b) if err != nil { // We haven't checked that the shell would actually reject this input too, // but if splitPkgConfigOutput rejected it it's probably too dangerous to // run in the script. t.Logf("%#q: %v", b, err) return } t.Logf("splitPkgConfigOutput(%#q) = %#q", b, args) if len(args) == 0 { t.Skipf("skipping %#q: contains no arguments", b) } var buf strings.Builder for _, arg := range args { buf.WriteString(arg) buf.WriteString("\n") } wantOut := buf.String() if strings.Count(wantOut, "\n") != len(args)+bytes.Count(b, []byte("\n")) { // One of the newlines in b was treated as a delimiter and not part of an // argument. Our bash test script would interpret that as a syntax error. t.Skipf("skipping %#q: contains a bare newline", b) } // We use the printf shell command to echo the arguments because, per // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html#tag_20_37_16: // “It is not possible to use echo portably across all POSIX systems unless // both -n (as the first argument) and escape sequences are omitted.” cmd := testenv.Command(t, "/bin/sh", "-c", "printf '%s\n' "+string(b)) cmd.Env = append(cmd.Environ(), "LC_ALL=POSIX", "POSIXLY_CORRECT=1") cmd.Stderr = new(strings.Builder) out, err := cmd.Output() if err != nil { t.Fatalf("%#q: %v\n%s", cmd.Args, err, cmd.Stderr) } if string(out) != wantOut { t.Logf("%#q:\n%#q", cmd.Args, out) t.Logf("want:\n%#q", wantOut) t.Errorf("parsed args do not match") } }) }