diff --git a/cmd/file2fuzz/main.go b/cmd/file2fuzz/main.go new file mode 100644 index 000000000..f0c8939ad --- /dev/null +++ b/cmd/file2fuzz/main.go @@ -0,0 +1,132 @@ +// Copyright 2021 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. + +// file2fuzz converts binary files, such as those used by go-fuzz, to the Go +// fuzzing corpus format. +// +// Usage: +// +// file2fuzz [-o output] [input...] +// +// The defualt behavior is to read input from stdin and write the converted +// output to stdout. If any position arguments are provided stdin is ignored +// and the arguments are assumed to be input files to convert. +// +// The -o flag provides an path to write output files to. If only one positional +// argument is specified it may be a file path or an existing directory, if there are +// multiple inputs specified it must be a directory. If a directory is provided +// the name of the file will be the SHA-256 hash of its contents. +// +package main + +import ( + "crypto/sha256" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +// encVersion1 is version 1 Go fuzzer corpus encoding. +var encVersion1 = "go test fuzz v1" + +func encodeByteSlice(b []byte) []byte { + return []byte(fmt.Sprintf("%s\n[]byte(%q)", encVersion1, b)) +} + +func usage() { + fmt.Fprintf(os.Stderr, "usage: file2fuzz [-o output] [input...]\nconverts files to Go fuzzer corpus format\n") + fmt.Fprintf(os.Stderr, "\tinput: files to convert\n") + fmt.Fprintf(os.Stderr, "\t-o: where to write converted file(s)\n") + os.Exit(2) +} +func dirWriter(dir string) func([]byte) error { + return func(b []byte) error { + sum := fmt.Sprintf("%x", sha256.Sum256(b)) + name := filepath.Join(dir, sum) + if err := os.MkdirAll(dir, 0777); err != nil { + return err + } + if err := ioutil.WriteFile(name, b, 0666); err != nil { + os.Remove(name) + return err + } + return nil + } +} + +func convert(inputArgs []string, outputArg string) error { + var input []io.Reader + if args := inputArgs; len(args) == 0 { + input = []io.Reader{os.Stdin} + } else { + for _, a := range args { + f, err := os.Open(a) + if err != nil { + return fmt.Errorf("unable to open %q: %s", a, err) + } + defer f.Close() + if fi, err := f.Stat(); err != nil { + return fmt.Errorf("unable to open %q: %s", a, err) + } else if fi.IsDir() { + return fmt.Errorf("%q is a directory, not a file", a) + } + input = append(input, f) + } + } + + var output func([]byte) error + if outputArg == "" { + if len(inputArgs) > 1 { + return errors.New("-o required with multiple input files") + } + output = func(b []byte) error { + _, err := os.Stdout.Write(b) + return err + } + } else { + if len(inputArgs) > 1 { + output = dirWriter(outputArg) + } else { + if fi, err := os.Stat(outputArg); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to open %q for writing: %s", outputArg, err) + } else if err == nil && fi.IsDir() { + output = dirWriter(outputArg) + } else { + output = func(b []byte) error { + return ioutil.WriteFile(outputArg, b, 0666) + } + } + } + } + + for _, f := range input { + b, err := ioutil.ReadAll(f) + if err != nil { + return fmt.Errorf("unable to read input: %s", err) + } + if err := output(encodeByteSlice(b)); err != nil { + return fmt.Errorf("unable to write output: %s", err) + } + } + + return nil +} + +func main() { + log.SetFlags(0) + log.SetPrefix("file2fuzz: ") + + output := flag.String("o", "", "where to write converted file(s)") + flag.Usage = usage + flag.Parse() + + if err := convert(flag.Args(), *output); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/file2fuzz/main_test.go b/cmd/file2fuzz/main_test.go new file mode 100644 index 000000000..55d824cf9 --- /dev/null +++ b/cmd/file2fuzz/main_test.go @@ -0,0 +1,181 @@ +// Copyright 2021 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. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" +) + +// The setup for this test is mostly cribbed from x/exp/txtar. + +var buildBin struct { + once sync.Once + name string + err error +} + +func binPath(t *testing.T) string { + t.Helper() + if _, err := exec.LookPath("go"); err != nil { + t.Skipf("cannot build file2fuzz binary: %v", err) + } + + buildBin.once.Do(func() { + exe, err := ioutil.TempFile("", "file2fuzz-*.exe") + if err != nil { + buildBin.err = err + return + } + exe.Close() + buildBin.name = exe.Name() + + cmd := exec.Command("go", "build", "-o", buildBin.name, ".") + out, err := cmd.CombinedOutput() + if err != nil { + buildBin.err = fmt.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, out) + } + }) + + if buildBin.err != nil { + if runtime.GOOS == "android" { + t.Skipf("skipping test after failing to build file2fuzz binary: go_android_exec may have failed to copy needed dependencies (see https://golang.org/issue/37088)") + } + t.Fatal(buildBin.err) + } + return buildBin.name +} + +func TestMain(m *testing.M) { + os.Exit(m.Run()) + if buildBin.name != "" { + os.Remove(buildBin.name) + } +} + +func file2fuzz(t *testing.T, dir string, args []string, stdin string) (string, bool) { + t.Helper() + cmd := exec.Command(binPath(t), args...) + cmd.Dir = dir + if stdin != "" { + cmd.Stdin = strings.NewReader(stdin) + } + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), true + } + return string(out), false +} + +func TestFile2Fuzz(t *testing.T) { + type file struct { + name string + dir bool + content string + } + tests := []struct { + name string + args []string + stdin string + inputFiles []file + expectedStdout string + expectedFiles []file + expectedError string + }{ + { + name: "stdin, stdout", + stdin: "hello", + expectedStdout: "go test fuzz v1\n[]byte(\"hello\")", + }, + { + name: "stdin, output file", + stdin: "hello", + args: []string{"-o", "output"}, + expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}}, + }, + { + name: "stdin, output directory", + stdin: "hello", + args: []string{"-o", "output"}, + inputFiles: []file{{name: "output", dir: true}}, + expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}}, + }, + { + name: "input file, output file", + args: []string{"-o", "output", "input"}, + inputFiles: []file{{name: "input", content: "hello"}}, + expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}}, + }, + { + name: "input file, output directory", + args: []string{"-o", "output", "input"}, + inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}}, + expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}}, + }, + { + name: "input files, output directory", + args: []string{"-o", "output", "input", "input-2"}, + inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}}, + expectedFiles: []file{ + {name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}, + {name: "output/28059db30ce420ff65b2c29b749804c69c601aeca21b3cbf0644244ff080d7a5", content: "go test fuzz v1\n[]byte(\"hello :)\")"}, + }, + }, + { + name: "input files, no output", + args: []string{"input", "input-2"}, + inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}}, + expectedError: "file2fuzz: -o required with multiple input files\n", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tmp, err := ioutil.TempDir(os.TempDir(), "file2fuzz") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %s", err) + } + defer os.RemoveAll(tmp) + for _, f := range tc.inputFiles { + if f.dir { + if err := os.Mkdir(filepath.Join(tmp, f.name), 0777); err != nil { + t.Fatalf("failed to create test directory: %s", err) + } + } else { + if err := ioutil.WriteFile(filepath.Join(tmp, f.name), []byte(f.content), 0666); err != nil { + t.Fatalf("failed to create test input file: %s", err) + } + } + } + + out, failed := file2fuzz(t, tmp, tc.args, tc.stdin) + if failed && tc.expectedError == "" { + t.Fatalf("file2fuzz failed unexpectedly: %s", out) + } else if failed && out != tc.expectedError { + t.Fatalf("file2fuzz returned unexpected error: got %q, want %q", out, tc.expectedError) + } + if !failed && out != tc.expectedStdout { + t.Fatalf("file2fuzz unexpected stdout: got %q, want %q", out, tc.expectedStdout) + } + + for _, f := range tc.expectedFiles { + c, err := ioutil.ReadFile(filepath.Join(tmp, f.name)) + if err != nil { + t.Fatalf("failed to read expected output file %q: %s", f.name, err) + } + if string(c) != f.content { + t.Fatalf("expected output file %q contains unexpected content: got %s, want %s", f.name, string(c), f.content) + } + } + }) + } +}