github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/work/shell_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build unix
     6  
     7  package work
     8  
     9  import (
    10  	"bytes"
    11  	"strings"
    12  	"testing"
    13  	"unicode"
    14  
    15  	"github.com/go-asm/go/testenv"
    16  )
    17  
    18  func FuzzSplitPkgConfigOutput(f *testing.F) {
    19  	testenv.MustHaveExecPath(f, "/bin/sh")
    20  
    21  	f.Add([]byte(`$FOO`))
    22  	f.Add([]byte(`\$FOO`))
    23  	f.Add([]byte(`${FOO}`))
    24  	f.Add([]byte(`\${FOO}`))
    25  	f.Add([]byte(`$(/bin/false)`))
    26  	f.Add([]byte(`\$(/bin/false)`))
    27  	f.Add([]byte(`$((0))`))
    28  	f.Add([]byte(`\$((0))`))
    29  	f.Add([]byte(`unescaped space`))
    30  	f.Add([]byte(`escaped\ space`))
    31  	f.Add([]byte(`"unterminated quote`))
    32  	f.Add([]byte(`'unterminated quote`))
    33  	f.Add([]byte(`unterminated escape\`))
    34  	f.Add([]byte(`"quote with unterminated escape\`))
    35  	f.Add([]byte(`'quoted "double quotes"'`))
    36  	f.Add([]byte(`"quoted 'single quotes'"`))
    37  	f.Add([]byte(`"\$0"`))
    38  	f.Add([]byte(`"\$\0"`))
    39  	f.Add([]byte(`"\$"`))
    40  	f.Add([]byte(`"\$ "`))
    41  
    42  	// Example positive inputs from TestSplitPkgConfigOutput.
    43  	// Some bare newlines have been removed so that the inputs
    44  	// are valid in the shell script we use for comparison.
    45  	f.Add([]byte(`-r:foo -L/usr/white\ space/lib -lfoo\ bar -lbar\ baz`))
    46  	f.Add([]byte(`-lextra\ fun\ arg\\`))
    47  	f.Add([]byte("\textra     whitespace\r"))
    48  	f.Add([]byte("     \r      "))
    49  	f.Add([]byte(`"-r:foo" "-L/usr/white space/lib" "-lfoo bar" "-lbar baz"`))
    50  	f.Add([]byte(`"-lextra fun arg\\"`))
    51  	f.Add([]byte(`"     \r\n\      "`))
    52  	f.Add([]byte(`""`))
    53  	f.Add([]byte(``))
    54  	f.Add([]byte(`"\\"`))
    55  	f.Add([]byte(`"\x"`))
    56  	f.Add([]byte(`"\\x"`))
    57  	f.Add([]byte(`'\\'`))
    58  	f.Add([]byte(`'\x'`))
    59  	f.Add([]byte(`"\\x"`))
    60  	f.Add([]byte("\\\n"))
    61  	f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED='"/test/share/doc"'`))
    62  	f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED="/test/share/doc"`))
    63  	f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED=\"/test/share/doc\"`))
    64  	f.Add([]byte(`-fPIC -I/test/include/foo -DQUOTED='/test/share/doc'`))
    65  	f.Add([]byte(`-DQUOTED='/te\st/share/d\oc'`))
    66  	f.Add([]byte(`-Dhello=10 -Dworld=+32 -DDEFINED_FROM_PKG_CONFIG=hello\ world`))
    67  	f.Add([]byte(`"broken\"" \\\a "a"`))
    68  
    69  	// Example negative inputs from TestSplitPkgConfigOutput.
    70  	f.Add([]byte(`"     \r\n      `))
    71  	f.Add([]byte(`"-r:foo" "-L/usr/white space/lib "-lfoo bar" "-lbar baz"`))
    72  	f.Add([]byte(`"-lextra fun arg\\`))
    73  	f.Add([]byte(`broken flag\`))
    74  	f.Add([]byte(`extra broken flag \`))
    75  	f.Add([]byte(`\`))
    76  	f.Add([]byte(`"broken\"" "extra" \`))
    77  
    78  	f.Fuzz(func(t *testing.T, b []byte) {
    79  		t.Parallel()
    80  
    81  		if bytes.ContainsAny(b, "*?[#~%\x00{}!") {
    82  			t.Skipf("skipping %#q: contains a sometimes-quoted character", b)
    83  		}
    84  		// splitPkgConfigOutput itself rejects inputs that contain unquoted
    85  		// shell operator characters. (Quoted shell characters are fine.)
    86  
    87  		for _, c := range b {
    88  			if c > unicode.MaxASCII {
    89  				t.Skipf("skipping %#q: contains a non-ASCII character %q", b, c)
    90  			}
    91  			if !unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c)) {
    92  				t.Skipf("skipping %#q: contains non-graphic character %q", b, c)
    93  			}
    94  		}
    95  
    96  		args, err := splitPkgConfigOutput(b)
    97  		if err != nil {
    98  			// We haven't checked that the shell would actually reject this input too,
    99  			// but if splitPkgConfigOutput rejected it it's probably too dangerous to
   100  			// run in the script.
   101  			t.Logf("%#q: %v", b, err)
   102  			return
   103  		}
   104  		t.Logf("splitPkgConfigOutput(%#q) = %#q", b, args)
   105  		if len(args) == 0 {
   106  			t.Skipf("skipping %#q: contains no arguments", b)
   107  		}
   108  
   109  		var buf strings.Builder
   110  		for _, arg := range args {
   111  			buf.WriteString(arg)
   112  			buf.WriteString("\n")
   113  		}
   114  		wantOut := buf.String()
   115  
   116  		if strings.Count(wantOut, "\n") != len(args)+bytes.Count(b, []byte("\n")) {
   117  			// One of the newlines in b was treated as a delimiter and not part of an
   118  			// argument. Our bash test script would interpret that as a syntax error.
   119  			t.Skipf("skipping %#q: contains a bare newline", b)
   120  		}
   121  
   122  		// We use the printf shell command to echo the arguments because, per
   123  		// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html#tag_20_37_16:
   124  		// “It is not possible to use echo portably across all POSIX systems unless
   125  		// both -n (as the first argument) and escape sequences are omitted.”
   126  		cmd := testenv.Command(t, "/bin/sh", "-c", "printf '%s\n' "+string(b))
   127  		cmd.Env = append(cmd.Environ(), "LC_ALL=POSIX", "POSIXLY_CORRECT=1")
   128  		cmd.Stderr = new(strings.Builder)
   129  		out, err := cmd.Output()
   130  		if err != nil {
   131  			t.Fatalf("%#q: %v\n%s", cmd.Args, err, cmd.Stderr)
   132  		}
   133  
   134  		if string(out) != wantOut {
   135  			t.Logf("%#q:\n%#q", cmd.Args, out)
   136  			t.Logf("want:\n%#q", wantOut)
   137  			t.Errorf("parsed args do not match")
   138  		}
   139  	})
   140  }