cmd/go/internal/txtar: new text archive format

This format is for storing tests and test modules.

Change-Id: I3d2e30653bcedc82f80211631307ce64c2a43265
Reviewed-on: https://go-review.googlesource.com/123359
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/internal/txtar/archive.go b/vendor/cmd/go/internal/txtar/archive.go
new file mode 100644
index 0000000..923997c
--- /dev/null
+++ b/vendor/cmd/go/internal/txtar/archive.go
@@ -0,0 +1,142 @@
+// Copyright 2018 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 txtar implements a trivial text-based file archive format.
+//
+// The goals for the format are:
+//
+//	- be trivial enough to create and edit by hand.
+//	- be able to store trees of text files describing go command test cases.
+//	- diff nicely in git history and code reviews.
+//
+// Non-goals include being a completely general archive format,
+// storing binary data, storing file modes, storing special files like
+// symbolic links, and so on.
+//
+// Txtar format
+//
+// A txtar archive is zero or more comment lines and then a sequence of file entries.
+// Each file entry begins with a file marker line of the form "-- FILENAME --"
+// and is followed by zero or more file content lines making up the file data.
+// The comment or file content ends at the next file marker line.
+// The file marker line must begin with the three-byte sequence "-- "
+// and end with the three-byte sequence " --", but the enclosed
+// file name can be surrounded by additional white space,
+// all of which is stripped. The file name may itself contain spaces,
+// which are preserved.
+//
+// If the txtar file is missing a trailing newline on the final line,
+// parsers should consider a final newline to be present anyway.
+//
+// There are no possible syntax errors in a txtar archive.
+package txtar
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"strings"
+)
+
+// An Archive is a collection of files.
+type Archive struct {
+	Comment []byte
+	Files   []File
+}
+
+// A File is a single file in an archive.
+type File struct {
+	Name string // name of file ("foo/bar.txt")
+	Data []byte // text content of file
+}
+
+// Format returns the serialized form of an Archive.
+// It is assumed that the Archive data structure is well-formed:
+// a.Comment and all a.File[i].Data contain no file marker lines,
+// and all a.File[i].Name are non-empty and well-formed
+// (no leading or trailing spaces, no newlines, and so on).
+func Format(a *Archive) []byte {
+	var buf bytes.Buffer
+	buf.Write(fixNL(a.Comment))
+	for _, f := range a.Files {
+		fmt.Fprintf(&buf, "-- %s --\n", f.Name)
+		buf.Write(fixNL(f.Data))
+	}
+	return buf.Bytes()
+}
+
+// ParseFile parses the named file as an archive.
+func ParseFile(file string) (*Archive, error) {
+	data, err := ioutil.ReadFile(file)
+	if err != nil {
+		return nil, err
+	}
+	return Parse(data), nil
+}
+
+// Parse parses the serialized form of an Archive.
+// The returned Archive holds slices of data.
+func Parse(data []byte) *Archive {
+	a := new(Archive)
+	var name string
+	a.Comment, name, data = findFileMarker(data)
+	for name != "" {
+		f := File{name, nil}
+		f.Data, name, data = findFileMarker(data)
+		a.Files = append(a.Files, f)
+	}
+	return a
+}
+
+var (
+	newlineMarker = []byte("\n-- ")
+	marker        = []byte("-- ")
+	markerEnd     = []byte(" --")
+)
+
+// findFileMarker finds the next file marker in data,
+// extracts the file name, and returns the data before the marker,
+// the file name, and the data after the marker.
+// If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil.
+func findFileMarker(data []byte) (before []byte, name string, after []byte) {
+	var i int
+	for {
+		if name, after = isMarker(data[i:]); name != "" {
+			return data[:i], name, after
+		}
+		j := bytes.Index(data[i:], newlineMarker)
+		if j < 0 {
+			return fixNL(data), "", nil
+		}
+		i += j + 1 // positioned at start of new possible marker
+	}
+}
+
+// isMarker checks whether data begins with a file marker line.
+// If so, it returns the name from the line and the data after the line.
+// Otherwise it returns name == "" with an unspecified after.
+func isMarker(data []byte) (name string, after []byte) {
+	if !bytes.HasPrefix(data, marker) {
+		return "", nil
+	}
+	if i := bytes.IndexByte(data, '\n'); i >= 0 {
+		data, after = data[:i], data[i+1:]
+	}
+	if !bytes.HasSuffix(data, markerEnd) {
+		return "", nil
+	}
+	return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
+}
+
+// If data is empty or ends in \n, fixNL returns data.
+// Otherwise fixNL returns a new slice consisting of data with a final \n added.
+func fixNL(data []byte) []byte {
+	if len(data) == 0 || data[len(data)-1] == '\n' {
+		return data
+	}
+	d := make([]byte, len(data)+1)
+	copy(d, data)
+	d[len(data)] = '\n'
+	return d
+}
diff --git a/vendor/cmd/go/internal/txtar/archive_test.go b/vendor/cmd/go/internal/txtar/archive_test.go
new file mode 100644
index 0000000..3f734f6
--- /dev/null
+++ b/vendor/cmd/go/internal/txtar/archive_test.go
@@ -0,0 +1,67 @@
+// Copyright 2018 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 txtar
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+var tests = []struct {
+	name   string
+	text   string
+	parsed *Archive
+}{
+	{
+		name: "basic",
+		text: `comment1
+comment2
+-- file1 --
+File 1 text.
+-- foo ---
+More file 1 text.
+-- file 2 --
+File 2 text.
+-- empty --
+-- noNL --
+hello world`,
+		parsed: &Archive{
+			Comment: []byte("comment1\ncomment2\n"),
+			Files: []File{
+				{"file1", []byte("File 1 text.\n-- foo ---\nMore file 1 text.\n")},
+				{"file 2", []byte("File 2 text.\n")},
+				{"empty", []byte{}},
+				{"noNL", []byte("hello world\n")},
+			},
+		},
+	},
+}
+
+func Test(t *testing.T) {
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := Parse([]byte(tt.text))
+			if !reflect.DeepEqual(a, tt.parsed) {
+				t.Fatalf("Parse: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed))
+			}
+			text := Format(a)
+			a = Parse(text)
+			if !reflect.DeepEqual(a, tt.parsed) {
+				t.Fatalf("Parse after Format: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed))
+			}
+		})
+	}
+}
+
+func shortArchive(a *Archive) string {
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "comment: %q\n", a.Comment)
+	for _, f := range a.Files {
+		fmt.Fprintf(&buf, "file %q: %q\n", f.Name, f.Data)
+	}
+	return buf.String()
+}
diff --git a/vendor/cmd/go/testdata/addmod.go b/vendor/cmd/go/testdata/addmod.go
new file mode 100644
index 0000000..a3deba9
--- /dev/null
+++ b/vendor/cmd/go/testdata/addmod.go
@@ -0,0 +1,150 @@
+// Copyright 2018 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.
+
+// +build ignore
+
+// Addmod adds a module as a txtar archive to the testdata/mod directory.
+//
+// Usage:
+//
+//	go run addmod.go path@version...
+//
+// It should only be used for very small modules - we do not want to check
+// very large files into testdata/mod.
+//
+// It is acceptable to edit the archive afterward to remove or shorten files.
+// See mod/README for more information.
+//
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"../internal/txtar"
+)
+
+func usage() {
+	fmt.Fprintf(os.Stderr, "usage: go run addmod.go path@version...\n")
+	os.Exit(2)
+}
+
+var tmpdir string
+
+func fatalf(format string, args ...interface{}) {
+	os.RemoveAll(tmpdir)
+	log.Fatalf(format, args...)
+}
+
+const goCmd = "vgo"
+
+func main() {
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() == 0 {
+		usage()
+	}
+
+	log.SetPrefix("addmod: ")
+	log.SetFlags(0)
+
+	var err error
+	tmpdir, err = ioutil.TempDir("", "addmod-")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	run := func(command string, args ...string) string {
+		cmd := exec.Command(command, args...)
+		cmd.Dir = tmpdir
+		var stderr bytes.Buffer
+		cmd.Stderr = &stderr
+		out, err := cmd.Output()
+		if err != nil {
+			fatalf("%s %s: %v\n%s", command, strings.Join(args, " "), err, stderr.Bytes())
+		}
+		return string(out)
+	}
+
+	gopath := strings.TrimSpace(run("go", "env", "GOPATH"))
+	if gopath == "" {
+		fatalf("cannot find GOPATH")
+	}
+
+	exitCode := 0
+	for _, arg := range flag.Args() {
+		if err := ioutil.WriteFile(filepath.Join(tmpdir, "go.mod"), []byte("module m\n"), 0666); err != nil {
+			fatalf("%v", err)
+		}
+		run(goCmd, "get", "-d", arg)
+		path := arg
+		if i := strings.Index(path, "@"); i >= 0 {
+			path = path[:i]
+		}
+		out := run(goCmd, "list", "-m", "-f={{.Path}} {{.Version}} {{.Dir}}", path)
+		f := strings.Fields(out)
+		if len(f) != 3 {
+			log.Printf("go list -m %s: unexpected output %q", arg, out)
+			exitCode = 1
+			continue
+		}
+		path, vers, dir := f[0], f[1], f[2]
+		mod, err := ioutil.ReadFile(filepath.Join(gopath, "src/mod/cache/download", path, "@v", vers+".mod"))
+		if err != nil {
+			log.Printf("%s: %v", arg, err)
+			exitCode = 1
+			continue
+		}
+		info, err := ioutil.ReadFile(filepath.Join(gopath, "src/mod/cache/download", path, "@v", vers+".info"))
+		if err != nil {
+			log.Printf("%s: %v", arg, err)
+			exitCode = 1
+			continue
+		}
+
+		a := new(txtar.Archive)
+		a.Comment = []byte(fmt.Sprintf("module %s@%s\n\n", path, vers))
+		a.Files = []txtar.File{
+			{Name: ".mod", Data: mod},
+			{Name: ".info", Data: info},
+		}
+		dir = filepath.Clean(dir)
+		err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+			if !info.Mode().IsRegular() {
+				return nil
+			}
+			name := info.Name()
+			if name == "go.mod" || strings.HasSuffix(name, ".go") {
+				data, err := ioutil.ReadFile(path)
+				if err != nil {
+					return err
+				}
+				a.Files = append(a.Files, txtar.File{Name: strings.TrimPrefix(path, dir+string(filepath.Separator)), Data: data})
+			}
+			return nil
+		})
+		if err != nil {
+			log.Printf("%s: %v", arg, err)
+			exitCode = 1
+			continue
+		}
+
+		data := txtar.Format(a)
+		target := filepath.Join("mod", strings.Replace(path, "/", "_", -1)+"_"+vers+".txt")
+		if err := ioutil.WriteFile(target, data, 0666); err != nil {
+			log.Printf("%s: %v", arg, err)
+			exitCode = 1
+			continue
+		}
+	}
+	os.RemoveAll(tmpdir)
+	os.Exit(exitCode)
+}
diff --git a/vendor/cmd/go/testdata/savedir.go b/vendor/cmd/go/testdata/savedir.go
new file mode 100644
index 0000000..48a6318
--- /dev/null
+++ b/vendor/cmd/go/testdata/savedir.go
@@ -0,0 +1,79 @@
+// Copyright 2018 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.
+
+// +build ignore
+
+// Savedir archives a directory tree as a txtar archive printed to standard output.
+//
+// Usage:
+//
+//	go run savedir.go /path/to/dir >saved.txt
+//
+// Typically the tree is later extracted during a test with tg.extract("testdata/saved.txt").
+//
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"unicode/utf8"
+
+	"../internal/txtar"
+)
+
+func usage() {
+	fmt.Fprintf(os.Stderr, "usage: go run savedir.go dir >saved.txt\n")
+	os.Exit(2)
+}
+
+const goCmd = "vgo"
+
+func main() {
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() != 1 {
+		usage()
+	}
+
+	log.SetPrefix("savedir: ")
+	log.SetFlags(0)
+
+	dir := flag.Arg(0)
+
+	a := new(txtar.Archive)
+	dir = filepath.Clean(dir)
+	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if path == dir {
+			return nil
+		}
+		name := info.Name()
+		if strings.HasPrefix(name, ".") {
+			if info.IsDir() {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+		if !info.Mode().IsRegular() {
+			return nil
+		}
+		data, err := ioutil.ReadFile(path)
+		if err != nil {
+			log.Fatal(err)
+		}
+		if !utf8.Valid(data) {
+			log.Printf("%s: ignoring invalid UTF-8 data", path)
+			return nil
+		}
+		a.Files = append(a.Files, txtar.File{Name: strings.TrimPrefix(path, dir+string(filepath.Separator)), Data: data})
+		return nil
+	})
+
+	data := txtar.Format(a)
+	os.Stdout.Write(data)
+}