|  | // 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 surrounding by additional white space, | 
|  | // all of which is stripped. | 
|  | // | 
|  | // 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 is non-empty. | 
|  | 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 | 
|  | } |