| // 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 |
| } |