Skip to content

Commit a4b73f6

Browse files
committed
txtar: add fs.FS support
Fixes #44158 From implementation copied with permission from https://github.com/josharian/txtarfs/blob/main/txtarfs_test.go golang/go#44158 (comment) Co-authored-by: josharian
1 parent 8e4f4c8 commit a4b73f6

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed

txtar/fs.go

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2021 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+
// +build go1.16
6+
7+
package txtar
8+
9+
import (
10+
"errors"
11+
"io"
12+
"io/fs"
13+
"path"
14+
"sort"
15+
"strings"
16+
"time"
17+
)
18+
19+
var _ fs.FS = (*Archive)(nil)
20+
21+
// Open implements fs.FS.
22+
func (a *Archive) Open(name string) (fs.File, error) {
23+
if !fs.ValidPath(name) {
24+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
25+
}
26+
27+
for _, f := range a.Files {
28+
if f.Name == name {
29+
return &openFile{f, 0}, nil
30+
}
31+
}
32+
var list []fileInfo
33+
var dirs = make(map[string]bool)
34+
if name == "." {
35+
for _, f := range a.Files {
36+
i := strings.Index(f.Name, "/")
37+
if i < 0 {
38+
list = append(list, fileInfo{f, 0666})
39+
} else {
40+
dirs[f.Name[:i]] = true
41+
}
42+
}
43+
} else {
44+
prefix := name + "/"
45+
for _, f := range a.Files {
46+
if strings.HasPrefix(f.Name, prefix) {
47+
felem := f.Name[len(prefix):]
48+
i := strings.Index(felem, "/")
49+
if i < 0 {
50+
list = append(list, fileInfo{f, 0666})
51+
} else {
52+
dirs[f.Name[len(prefix):len(prefix)+i]] = true
53+
}
54+
}
55+
}
56+
// If there are no children of the name,
57+
// then the directory is treated as not existing.
58+
if list == nil && len(dirs) == 0 {
59+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
60+
}
61+
}
62+
for name := range dirs {
63+
list = append(list, fileInfo{File{Name: name}, fs.ModeDir | 0666})
64+
}
65+
sort.Slice(list, func(i, j int) bool {
66+
return list[i].File.Name < list[j].File.Name
67+
})
68+
69+
return &openDir{name, fileInfo{File{Name: name}, fs.ModeDir | 0666}, list, 0}, nil
70+
}
71+
72+
var _ fs.ReadFileFS = (*Archive)(nil)
73+
74+
// ReadFile implements fs.ReadFileFS.
75+
func (a *Archive) ReadFile(name string) ([]byte, error) {
76+
if !fs.ValidPath(name) {
77+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
78+
}
79+
if name == "." {
80+
return nil, &fs.PathError{Op: "read", Path: name, Err: errors.New("is a directory")}
81+
}
82+
prefix := name + "/"
83+
for _, f := range a.Files {
84+
if f.Name == name {
85+
return f.Data, nil
86+
}
87+
// It's a directory
88+
if strings.HasPrefix(f.Name, prefix) {
89+
return nil, &fs.PathError{Op: "read", Path: name, Err: errors.New("is a directory")}
90+
}
91+
}
92+
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
93+
}
94+
95+
var _ fs.File = (*openFile)(nil)
96+
97+
type openFile struct {
98+
File
99+
offset int64
100+
}
101+
102+
func (o *openFile) Stat() (fs.FileInfo, error) { return fileInfo{o.File, 0666}, nil }
103+
104+
func (o *openFile) Close() error { return nil }
105+
106+
func (f *openFile) Read(b []byte) (int, error) {
107+
if f.offset >= int64(len(f.File.Data)) {
108+
return 0, io.EOF
109+
}
110+
if f.offset < 0 {
111+
return 0, &fs.PathError{Op: "read", Path: f.File.Name, Err: fs.ErrInvalid}
112+
}
113+
n := copy(b, f.File.Data[f.offset:])
114+
f.offset += int64(n)
115+
return n, nil
116+
}
117+
118+
func (f *openFile) Seek(offset int64, whence int) (int64, error) {
119+
switch whence {
120+
case 0:
121+
// offset += 0
122+
case 1:
123+
offset += f.offset
124+
case 2:
125+
offset += int64(len(f.File.Data))
126+
}
127+
if offset < 0 || offset > int64(len(f.File.Data)) {
128+
return 0, &fs.PathError{Op: "seek", Path: f.File.Name, Err: fs.ErrInvalid}
129+
}
130+
f.offset = offset
131+
return offset, nil
132+
}
133+
134+
func (f *openFile) ReadAt(b []byte, offset int64) (int, error) {
135+
if offset < 0 || offset > int64(len(f.File.Data)) {
136+
return 0, &fs.PathError{Op: "read", Path: f.File.Name, Err: fs.ErrInvalid}
137+
}
138+
n := copy(b, f.File.Data[offset:])
139+
if n < len(b) {
140+
return n, io.EOF
141+
}
142+
return n, nil
143+
}
144+
145+
var _ fs.FileInfo = fileInfo{}
146+
147+
type fileInfo struct {
148+
File
149+
m fs.FileMode
150+
}
151+
152+
func (f fileInfo) Name() string { return path.Base(f.File.Name) }
153+
func (f fileInfo) Size() int64 { return int64(len(f.File.Data)) }
154+
func (f fileInfo) Mode() fs.FileMode { return f.m }
155+
func (f fileInfo) Type() fs.FileMode { return f.m.Type() }
156+
func (f fileInfo) ModTime() time.Time { return time.Time{} }
157+
func (f fileInfo) IsDir() bool { return f.m.IsDir() }
158+
func (f fileInfo) Sys() interface{} { return f.File }
159+
func (f fileInfo) Info() (fs.FileInfo, error) { return f, nil }
160+
161+
type openDir struct {
162+
path string
163+
fileInfo
164+
entry []fileInfo
165+
offset int
166+
}
167+
168+
func (d *openDir) Stat() (fs.FileInfo, error) { return &d.fileInfo, nil }
169+
func (d *openDir) Close() error { return nil }
170+
func (d *openDir) Read(b []byte) (int, error) {
171+
return 0, &fs.PathError{Op: "read", Path: d.path, Err: errors.New("is a directory")}
172+
}
173+
174+
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
175+
n := len(d.entry) - d.offset
176+
if count > 0 && n > count {
177+
n = count
178+
}
179+
if n == 0 && count > 0 {
180+
return nil, io.EOF
181+
}
182+
list := make([]fs.DirEntry, n)
183+
for i := range list {
184+
list[i] = &d.entry[d.offset+i]
185+
}
186+
d.offset += n
187+
return list, nil
188+
}
189+
190+
// From constructs an Archive with the contents of fsys and an empty Comment.
191+
// Subsequent changes to fsys are not reflected in the returned archive.
192+
//
193+
// The transformation is lossy.
194+
// For example, because directories are implicit in txtar archives,
195+
// empty directories in fsys will be lost, and txtar does not represent file mode, mtime, or other file metadata.
196+
// From does not guarantee that a.File[i].Data contain no file marker lines.
197+
// See also warnings on Format.
198+
// In short, it is unwise to use txtar as a generic filesystem serialization mechanism.
199+
func From(fsys fs.FS) (*Archive, error) {
200+
ar := new(Archive)
201+
walkfn := func(path string, d fs.DirEntry, err error) error {
202+
if err != nil {
203+
return err
204+
}
205+
if d.IsDir() {
206+
// Directories in txtar are implicit.
207+
return nil
208+
}
209+
data, err := fs.ReadFile(fsys, path)
210+
if err != nil {
211+
return err
212+
}
213+
ar.Files = append(ar.Files, File{Name: path, Data: data})
214+
return nil
215+
}
216+
217+
if err := fs.WalkDir(fsys, ".", walkfn); err != nil {
218+
return nil, err
219+
}
220+
return ar, nil
221+
}

txtar/fs_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2021 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
6+
7+
import (
8+
"io/fs"
9+
"sort"
10+
"strings"
11+
"testing"
12+
"testing/fstest"
13+
)
14+
15+
func TestFS(t *testing.T) {
16+
for _, tc := range []struct{ name, input, files string }{
17+
{
18+
name: "empty",
19+
input: ``,
20+
files: "",
21+
},
22+
{
23+
name: "one",
24+
input: `
25+
-- one.txt --
26+
one
27+
`,
28+
files: "one.txt",
29+
},
30+
{
31+
name: "two",
32+
input: `
33+
-- one.txt --
34+
one
35+
-- two.txt --
36+
two
37+
`,
38+
files: "one.txt two.txt",
39+
},
40+
{
41+
name: "subdirectories",
42+
input: `
43+
-- one.txt --
44+
one
45+
-- 2/two.txt --
46+
two
47+
-- 2/3/three.txt --
48+
three
49+
-- 4/four.txt --
50+
three
51+
`,
52+
files: "one.txt 2/two.txt 2/3/three.txt 4/four.txt",
53+
},
54+
} {
55+
t.Run(tc.name, func(t *testing.T) {
56+
a := Parse([]byte(tc.input))
57+
files := strings.Fields(tc.files)
58+
if err := fstest.TestFS(a, files...); err != nil {
59+
t.Fatal(err)
60+
}
61+
for _, name := range files {
62+
for _, f := range a.Files {
63+
if f.Name == name {
64+
b, err := fs.ReadFile(a, name)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
if string(b) != string(f.Data) {
69+
t.Fatalf("mismatched contents for %q", name)
70+
}
71+
}
72+
}
73+
}
74+
a2, err := From(a)
75+
if err != nil {
76+
t.Fatalf("failed to write fsys for %v: %v", tc.name, err)
77+
}
78+
79+
if in, out := normalized(a), normalized(a2); in != out {
80+
t.Errorf("From round trip failed: %q != %q", in, out)
81+
}
82+
83+
})
84+
}
85+
}
86+
87+
func normalized(a *Archive) string {
88+
a.Comment = nil
89+
sort.Slice(a.Files, func(i, j int) bool {
90+
return a.Files[i].Name < a.Files[j].Name
91+
})
92+
return string(Format(a))
93+
}

0 commit comments

Comments
 (0)