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 | "io/ioutil" |
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 := ioutil.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 |
Members