1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package txtar implements a trivial text-based file archive format. 6 // 7 // The goals for the format are: 8 // 9 // - be trivial enough to create and edit by hand. 10 // - be able to store trees of text files describing go command test cases. 11 // - diff nicely in git history and code reviews. 12 // 13 // Non-goals include being a completely general archive format, 14 // storing binary data, storing file modes, storing special files like 15 // symbolic links, and so on. 16 // 17 // # Txtar format 18 // 19 // A txtar archive is zero or more comment lines and then a sequence of file entries. 20 // Each file entry begins with a file marker line of the form "-- FILENAME --" 21 // and is followed by zero or more file content lines making up the file data. 22 // The comment or file content ends at the next file marker line. 23 // The file marker line must begin with the three-byte sequence "-- " 24 // and end with the three-byte sequence " --", but the enclosed 25 // file name can be surrounding by additional white space, 26 // all of which is stripped. 27 // 28 // If the txtar file is missing a trailing newline on the final line, 29 // parsers should consider a final newline to be present anyway. 30 // 31 // There are no possible syntax errors in a txtar archive. 32 package txtar 33 34 import ( 35 "bytes" 36 "fmt" 37 "os" 38 "strings" 39 ) 40 41 // An Archive is a collection of files. 42 type Archive struct { 43 Comment []byte 44 Files []File 45 } 46 47 // A File is a single file in an archive. 48 type File struct { 49 Name string // name of file ("foo/bar.txt") 50 Data []byte // text content of file 51 } 52 53 // Format returns the serialized form of an Archive. 54 // It is assumed that the Archive data structure is well-formed: 55 // a.Comment and all a.File[i].Data contain no file marker lines, 56 // and all a.File[i].Name is non-empty. 57 func Format(a *Archive) []byte { 58 var buf bytes.Buffer 59 buf.Write(fixNL(a.Comment)) 60 for _, f := range a.Files { 61 fmt.Fprintf(&buf, "-- %s --\n", f.Name) 62 buf.Write(fixNL(f.Data)) 63 } 64 return buf.Bytes() 65 } 66 67 // ParseFile parses the named file as an archive. 68 func ParseFile(file string) (*Archive, error) { 69 data, err := os.ReadFile(file) 70 if err != nil { 71 return nil, err 72 } 73 return Parse(data), nil 74 } 75 76 // Parse parses the serialized form of an Archive. 77 // The returned Archive holds slices of data. 78 func Parse(data []byte) *Archive { 79 a := new(Archive) 80 var name string 81 a.Comment, name, data = findFileMarker(data) 82 for name != "" { 83 f := File{name, nil} 84 f.Data, name, data = findFileMarker(data) 85 a.Files = append(a.Files, f) 86 } 87 return a 88 } 89 90 var ( 91 newlineMarker = []byte("\n-- ") 92 marker = []byte("-- ") 93 markerEnd = []byte(" --") 94 ) 95 96 // findFileMarker finds the next file marker in data, 97 // extracts the file name, and returns the data before the marker, 98 // the file name, and the data after the marker. 99 // If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil. 100 func findFileMarker(data []byte) (before []byte, name string, after []byte) { 101 var i int 102 for { 103 if name, after = isMarker(data[i:]); name != "" { 104 return data[:i], name, after 105 } 106 j := bytes.Index(data[i:], newlineMarker) 107 if j < 0 { 108 return fixNL(data), "", nil 109 } 110 i += j + 1 // positioned at start of new possible marker 111 } 112 } 113 114 // isMarker checks whether data begins with a file marker line. 115 // If so, it returns the name from the line and the data after the line. 116 // Otherwise it returns name == "" with an unspecified after. 117 func isMarker(data []byte) (name string, after []byte) { 118 if !bytes.HasPrefix(data, marker) { 119 return "", nil 120 } 121 if i := bytes.IndexByte(data, '\n'); i >= 0 { 122 data, after = data[:i], data[i+1:] 123 } 124 if !(bytes.HasSuffix(data, markerEnd) && len(data) >= len(marker)+len(markerEnd)) { 125 return "", nil 126 } 127 return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after 128 } 129 130 // If data is empty or ends in \n, fixNL returns data. 131 // Otherwise fixNL returns a new slice consisting of data with a final \n added. 132 func fixNL(data []byte) []byte { 133 if len(data) == 0 || data[len(data)-1] == '\n' { 134 return data 135 } 136 d := make([]byte, len(data)+1) 137 copy(d, data) 138 d[len(data)] = '\n' 139 return d 140 } 141