// 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 default 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 a 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"
	"log"
	"os"
	"path/filepath"
)

// encVersion1 is version 1 Go fuzzer corpus encoding.
var encVersion1 = "go test fuzz v1"

func encodeByteSlice(b []byte) []byte {
	return fmt.Appendf(nil, "%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 := os.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 os.WriteFile(outputArg, b, 0666)
				}
			}
		}
	}

	for _, f := range input {
		b, err := io.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)
	}
}
