-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathfs.go
358 lines (324 loc) · 9.48 KB
/
fs.go
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
// Copyright 2021 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 gitfs
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
hashpkg "hash"
"io"
"io/fs"
"runtime/debug"
"time"
)
// A Hash is a SHA-1 Hash identifying a particular Git object.
type Hash [20]byte
func (h Hash) String() string { return fmt.Sprintf("%x", h[:]) }
// parseHash parses the (full-length) Git hash text.
func parseHash(text string) (Hash, error) {
x, err := hex.DecodeString(text)
if err != nil || len(x) != 20 {
return Hash{}, fmt.Errorf("invalid hash")
}
var h Hash
copy(h[:], x)
return h, nil
}
// An objType is an object type indicator.
// The values are the ones used in Git pack encoding
// (https://git-scm.com/docs/pack-format#_object_types).
type objType int
const (
objNone objType = 0
objCommit objType = 1
objTree objType = 2
objBlob objType = 3
objTag objType = 4
// 5 undefined
objOfsDelta objType = 6
objRefDelta objType = 7
)
var objTypes = [...]string{
objCommit: "commit",
objTree: "tree",
objBlob: "blob",
objTag: "tag",
}
func (t objType) String() string {
if t < 0 || int(t) >= len(objTypes) || objTypes[t] == "" {
return fmt.Sprintf("objType(%d)", int(t))
}
return objTypes[t]
}
// A dirEntry is a Git directory entry parsed from a tree object.
type dirEntry struct {
mode int
name []byte
hash Hash
}
// parseDirEntry parses the next directory entry from data,
// returning the entry and the number of bytes it occupied.
// If data is malformed, parseDirEntry returns dirEntry{}, 0.
func parseDirEntry(data []byte) (dirEntry, int) {
// Unclear where or if this format is documented by Git.
// Each directory entry is an octal mode, then a space,
// then a file name, then a NUL byte, then a 20-byte binary hash.
// Note that 'git cat-file -p <treehash>' shows a textual representation
// of this data, not the actual binary data. To see the binary data,
// use 'echo <treehash> | git cat-file --batch | hexdump -C'.
mode := 0
i := 0
for i < len(data) && data[i] != ' ' {
c := data[i]
if c < '0' || '7' < c {
return dirEntry{}, 0
}
mode = mode*8 + int(c) - '0'
i++
}
i++
j := i
for j < len(data) && data[j] != 0 {
j++
}
if len(data)-j < 1+20 {
return dirEntry{}, 0
}
name := data[i:j]
var h Hash
copy(h[:], data[j+1:])
return dirEntry{mode, name, h}, j + 1 + 20
}
// treeLookup looks in the tree object data for the directory entry with the given name,
// returning the mode and hash associated with the name.
func treeLookup(data []byte, name string) (mode int, h Hash, ok bool) {
// Note: The tree object directory entries are sorted by name,
// but the directory entry data is not self-synchronizing,
// so it's not possible to be clever and use a binary search here.
for len(data) > 0 {
e, size := parseDirEntry(data)
if size == 0 {
break
}
if string(e.name) == name {
return e.mode, e.hash, true
}
data = data[size:]
}
return 0, Hash{}, false
}
// commitKeyValue parses the commit object data
// looking for the first header line "key: value" matching the given key.
// It returns the associated value.
// (Try 'git cat-file -p <commithash>' to see the commit data format.)
func commitKeyValue(data []byte, key string) ([]byte, bool) {
for i := 0; i < len(data); i++ {
if i == 0 || data[i-1] == '\n' {
if data[i] == '\n' {
break
}
if len(data)-i >= len(key)+1 && data[len(key)] == ' ' && string(data[:len(key)]) == key {
val := data[len(key)+1:]
for j := 0; j < len(val); j++ {
if val[j] == '\n' {
val = val[:j]
break
}
}
return val, true
}
}
}
return nil, false
}
// A store is a collection of Git objects, indexed for lookup by hash.
type store struct {
sha1 hashpkg.Hash // reused hash state
index map[Hash]stored // lookup index
data []byte // concatenation of all object data
}
// A stored describes a single stored object.
type stored struct {
typ objType // object type
off int // object data is store.data[off:off+len]
len int
}
// add adds an object with the given type and content to s, returning its Hash.
// If the object is already stored in s, add succeeds but doesn't store a second copy.
func (s *store) add(typ objType, data []byte) (Hash, []byte) {
if s.sha1 == nil {
s.sha1 = sha1.New()
}
// Compute Git hash for object.
s.sha1.Reset()
fmt.Fprintf(s.sha1, "%s %d\x00", typ, len(data))
s.sha1.Write(data)
var h Hash
s.sha1.Sum(h[:0]) // appends into h
e, ok := s.index[h]
if !ok {
if s.index == nil {
s.index = make(map[Hash]stored)
}
e = stored{typ, len(s.data), len(data)}
s.index[h] = e
s.data = append(s.data, data...)
}
return h, s.data[e.off : e.off+e.len]
}
// object returns the type and data for the object with hash h.
// If there is no object with hash h, object returns 0, nil.
func (s *store) object(h Hash) (typ objType, data []byte) {
d, ok := s.index[h]
if !ok {
return 0, nil
}
return d.typ, s.data[d.off : d.off+d.len]
}
// commit returns a treeFS for the file system tree associated with the given commit hash.
func (s *store) commit(h Hash) (*treeFS, error) {
// The commit object data starts with key-value pairs
typ, data := s.object(h)
if typ == objNone {
return nil, fmt.Errorf("commit %s: no such hash", h)
}
if typ != objCommit {
return nil, fmt.Errorf("commit %s: unexpected type %s", h, typ)
}
treeHash, ok := commitKeyValue(data, "tree")
if !ok {
return nil, fmt.Errorf("commit %s: no tree", h)
}
h, err := parseHash(string(treeHash))
if err != nil {
return nil, fmt.Errorf("commit %s: invalid tree %q", h, treeHash)
}
return &treeFS{s, h}, nil
}
// A treeFS is an fs.FS serving a Git file system tree rooted at a given tree object hash.
type treeFS struct {
s *store
tree Hash // root tree
}
// Open opens the given file or directory, implementing the fs.FS Open method.
func (t *treeFS) Open(name string) (f fs.File, err error) {
defer func() {
if e := recover(); e != nil {
f = nil
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
}
}()
// Process each element in the slash-separated path, producing hash identified by name.
h := t.tree
start := 0 // index of start of final path element in name
if name != "." {
for i := 0; i <= len(name); i++ {
if i == len(name) || name[i] == '/' {
// Look up name in current tree object h.
typ, data := t.s.object(h)
if typ != objTree {
return nil, &fs.PathError{Path: name, Op: "open", Err: fs.ErrNotExist}
}
_, th, ok := treeLookup(data, name[start:i])
if !ok {
return nil, &fs.PathError{Path: name, Op: "open", Err: fs.ErrNotExist}
}
h = th
if i < len(name) {
start = i + 1
}
}
}
}
// The hash h is the hash for name. Load its object.
typ, data := t.s.object(h)
info := fileInfo{name, name[start:], 0, 0}
if typ == objBlob {
// Regular file.
info.mode = 0444
info.size = int64(len(data))
return &blobFile{info, bytes.NewReader(data)}, nil
}
if typ == objTree {
// Directory.
info.mode = fs.ModeDir | 0555
return &dirFile{t.s, info, data, 0}, nil
}
return nil, &fs.PathError{Path: name, Op: "open", Err: fmt.Errorf("unexpected git object type %s", typ)}
}
// fileInfo implements fs.FileInfo.
type fileInfo struct {
path string
name string
mode fs.FileMode
size int64
}
func (i *fileInfo) Name() string { return i.name }
func (i *fileInfo) Type() fs.FileMode { return i.mode & fs.ModeType }
func (i *fileInfo) Mode() fs.FileMode { return i.mode }
func (i *fileInfo) Sys() interface{} { return nil }
func (i *fileInfo) IsDir() bool { return i.mode&fs.ModeDir != 0 }
func (i *fileInfo) Size() int64 { return i.size }
func (i *fileInfo) Info() (fs.FileInfo, error) { return i, nil }
func (i *fileInfo) ModTime() time.Time { return time.Time{} }
func (i *fileInfo) err(op string, err error) error {
return &fs.PathError{Path: i.path, Op: op, Err: err}
}
// A blobFile implements fs.File for a regular file.
// The embedded bytes.Reader provides Read, Seek and other I/O methods.
type blobFile struct {
info fileInfo
*bytes.Reader
}
func (f *blobFile) Close() error { return nil }
func (f *blobFile) Stat() (fs.FileInfo, error) { return &f.info, nil }
// A dirFile implements fs.File for a directory.
type dirFile struct {
s *store
info fileInfo
data []byte
off int
}
func (f *dirFile) Close() error { return nil }
func (f *dirFile) Read([]byte) (int, error) { return 0, f.info.err("read", fs.ErrInvalid) }
func (f *dirFile) Stat() (fs.FileInfo, error) { return &f.info, nil }
func (f *dirFile) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == 0 {
// Allow rewind to start of directory.
f.off = 0
return 0, nil
}
return 0, f.info.err("seek", fs.ErrInvalid)
}
func (f *dirFile) ReadDir(n int) (list []fs.DirEntry, err error) {
defer func() {
if e := recover(); e != nil {
list = nil
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
}
}()
for (n <= 0 || len(list) < n) && f.off < len(f.data) {
e, size := parseDirEntry(f.data[f.off:])
if size == 0 {
break
}
f.off += size
typ, data := f.s.object(e.hash)
mode := fs.FileMode(0444)
if typ == objTree {
mode = fs.ModeDir | 0555
}
infoSize := int64(0)
if typ == objBlob {
infoSize = int64(len(data))
}
name := string(e.name)
list = append(list, &fileInfo{name, name, mode, infoSize})
}
if len(list) == 0 && n > 0 {
return list, io.EOF
}
return list, nil
}