package literals_test import ( "fmt" "go/ast" "go/parser" "go/printer" "go/token" "go/types" mathrand "math/rand" "os" "os/exec" "path/filepath" "runtime" "strings" "sync/atomic" "testing" "mvdan.cc/garble/internal/literals" ) // The fuzzing string is passed in as a string and []byte literal. var fuzzTemplate = ` package main var str string = %#[1]v var strFold string = "x" + %#[1]v + "y" var byt []byte = %#[2]v var bytPtr *[]byte = &%#[2]v func main() { println(str) println(strFold) println("--") println(string(byt)) println(string(*bytPtr)) } `[1:] func FuzzObfuscate(f *testing.F) { initialRandSeed := int64(123) f.Add("", initialRandSeed) f.Add("short", initialRandSeed) f.Add("long_enough_string", initialRandSeed) f.Add("binary_\x00\x01\x02", initialRandSeed) f.Add("whitespace \n\t\t", initialRandSeed) f.Add(strings.Repeat("x", (2<<10)+1), initialRandSeed) // past maxSize tdir := f.TempDir() var tdirCounter atomic.Int64 f.Fuzz(func(t *testing.T, in string, randSeed int64) { // The code below is an extreme simplification of what "garble build" does, // but it does significantly less, allowing the fuzz function to be faster. // For example, we only obfuscate the literals, not any identifiers. // Note that the fuzzer is still quite slow, as it still builds a binary. // Create the source, parse it, and typecheck it. srcText := fmt.Sprintf(fuzzTemplate, in, []byte(in)) t.Log(srcText) // shown on failures fset := token.NewFileSet() srcSyntax, err := parser.ParseFile(fset, "", srcText, parser.SkipObjectResolution) if err != nil { t.Fatal(err) } info := types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), Uses: make(map[*ast.Ident]types.Object), } var conf types.Config if _, err := conf.Check("p", fset, []*ast.File{srcSyntax}, &info); err != nil { t.Fatal(err) } // Obfuscate the literals and print the source back. rand := mathrand.New(mathrand.NewSource(randSeed)) srcSyntax = literals.Obfuscate(rand, srcSyntax, &info, nil) count := tdirCounter.Add(1) f, err := os.Create(filepath.Join(tdir, fmt.Sprintf("src_%d.go", count))) if err != nil { t.Fatal(err) } srcPath := f.Name() t.Cleanup(func() { f.Close() os.Remove(srcPath) }) if err := printer.Fprint(f, fset, srcSyntax); err != nil { t.Fatal(err) } // Build the main package. Use some flags to avoid work. binPath := strings.TrimSuffix(srcPath, ".go") if runtime.GOOS == "windows" { binPath += ".exe" } if out, err := exec.Command( "go", "build", "-trimpath", "-ldflags=-w -s", "-p", "1", "-o", binPath, srcPath, ).CombinedOutput(); err != nil { t.Fatalf("%v: %s", err, out) } t.Cleanup(func() { os.Remove(binPath) }) // Run the binary, expecting the output to match. out, err := exec.Command(binPath).CombinedOutput() if err != nil { t.Fatalf("%v: %s", err, out) } want := fmt.Sprintf("%[1]s\nx%[1]sy\n--\n%[1]s\n%[1]s\n", in) if got := string(out); got != want { t.Fatalf("got: %q\nwant: %q", got, want) } }) }