1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 package txtar
33
34 import (
35 "bytes"
36 "errors"
37 "fmt"
38 "io/ioutil"
39 "os"
40 "path/filepath"
41 "strings"
42 "unicode/utf8"
43
44 "golang.org/x/tools/txtar"
45 )
46
47
48 type Archive = txtar.Archive
49
50
51 type File = txtar.File
52
53
54
55
56
57 func Format(a *Archive) []byte {
58 return txtar.Format(a)
59 }
60
61
62 func ParseFile(file string) (*Archive, error) {
63 data, err := ioutil.ReadFile(file)
64 if err != nil {
65 return nil, err
66 }
67 return Parse(data), nil
68 }
69
70
71
72
73
74
75 func Parse(data []byte) *Archive {
76 a := new(Archive)
77 var name string
78 a.Comment, name, data = findFileMarker(data)
79 for name != "" {
80 f := File{name, nil}
81 f.Data, name, data = findFileMarker(data)
82 a.Files = append(a.Files, f)
83 }
84 return a
85 }
86
87
88
89 func NeedsQuote(data []byte) bool {
90 _, _, after := findFileMarker(data)
91 return after != nil
92 }
93
94
95
96
97
98
99
100
101 func Quote(data []byte) ([]byte, error) {
102 if len(data) == 0 {
103 return nil, nil
104 }
105 if data[len(data)-1] != '\n' {
106 return nil, errors.New("data has no final newline")
107 }
108 if !utf8.Valid(data) {
109 return nil, fmt.Errorf("data contains non-UTF-8 characters")
110 }
111 var nd []byte
112 prev := byte('\n')
113 for _, b := range data {
114 if prev == '\n' {
115 nd = append(nd, '>')
116 }
117 nd = append(nd, b)
118 prev = b
119 }
120 return nd, nil
121 }
122
123
124 func Unquote(data []byte) ([]byte, error) {
125 if len(data) == 0 {
126 return nil, nil
127 }
128 if data[0] != '>' || data[len(data)-1] != '\n' {
129 return nil, errors.New("data does not appear to be quoted")
130 }
131 data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1)
132 data = bytes.TrimPrefix(data, []byte(">"))
133 return data, nil
134 }
135
136 var (
137 newlineMarker = []byte("\n-- ")
138 marker = []byte("-- ")
139 markerEnd = []byte(" --")
140 )
141
142
143
144
145
146 func findFileMarker(data []byte) (before []byte, name string, after []byte) {
147 var i int
148 for {
149 if name, after = isMarker(data[i:]); name != "" {
150 return data[:i], name, after
151 }
152 j := bytes.Index(data[i:], newlineMarker)
153 if j < 0 {
154 return fixNL(data), "", nil
155 }
156 i += j + 1
157 }
158 }
159
160
161
162
163 func isMarker(data []byte) (name string, after []byte) {
164 if !bytes.HasPrefix(data, marker) {
165 return "", nil
166 }
167 if i := bytes.IndexByte(data, '\n'); i >= 0 {
168 data, after = data[:i], data[i+1:]
169 if data[i-1] == '\r' {
170 data = data[:len(data)-1]
171 }
172 }
173 if !bytes.HasSuffix(data, markerEnd) {
174 return "", nil
175 }
176 return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
177 }
178
179
180
181 func fixNL(data []byte) []byte {
182 if len(data) == 0 || data[len(data)-1] == '\n' {
183 return data
184 }
185 d := make([]byte, len(data)+1)
186 copy(d, data)
187 d[len(data)] = '\n'
188 return d
189 }
190
191
192
193
194 func Write(a *Archive, dir string) error {
195 for _, f := range a.Files {
196 fp := filepath.Clean(filepath.FromSlash(f.Name))
197 if isAbs(fp) || strings.HasPrefix(fp, ".."+string(filepath.Separator)) {
198 return fmt.Errorf("%q: outside parent directory", f.Name)
199 }
200 fp = filepath.Join(dir, fp)
201
202 if err := os.MkdirAll(filepath.Dir(fp), 0o777); err != nil {
203 return err
204 }
205
206 out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
207 if err != nil {
208 return err
209 }
210
211 _, err = out.Write(f.Data)
212 cerr := out.Close()
213 if err != nil {
214 return err
215 }
216 if cerr != nil {
217 return cerr
218 }
219 }
220 return nil
221 }
222
223 func isAbs(p string) bool {
224
225
226 return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator))
227 }
228
View as plain text