all repos

clerk @ master

missing tooling for ledger/hledger

clerk/internal/testutil/txtar/fs.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
tests: switch to txtar, 2 days ago
1
// Copyright 2024 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
	"errors"
9
	"fmt"
10
	"io"
11
	"io/fs"
12
	"path"
13
	"slices"
14
	"time"
15
)
16
17
// FS returns the file system form of an Archive.
18
// It returns an error if any of the file names in the archive
19
// are not valid file system names.
20
// The archive must not be modified while the FS is in use.
21
//
22
// If the file system detects that it has been modified, calls to the
23
// file system return an ErrModified error.
24
func FS(a *Archive) (fs.FS, error) {
25
	// Create a filesystem with a root directory.
26
	root := &node{fileinfo: fileinfo{path: ".", mode: readOnlyDir}}
27
	fsys := &filesystem{a, map[string]*node{root.path: root}}
28
29
	if err := initFiles(fsys); err != nil {
30
		return nil, fmt.Errorf("cannot create fs.FS from txtar.Archive: %s", err)
31
	}
32
	return fsys, nil
33
}
34
35
const (
36
	readOnly    fs.FileMode = 0o444 // read only mode
37
	readOnlyDir             = readOnly | fs.ModeDir
38
)
39
40
// ErrModified indicates that file system returned by FS
41
// noticed that the underlying archive has been modified
42
// since the call to FS. Detection of modification is best effort,
43
// to help diagnose misuse of the API, and is not guaranteed.
44
var ErrModified error = errors.New("txtar.Archive has been modified during txtar.FS")
45
46
// A filesystem is a simple in-memory file system for txtar archives,
47
// represented as a map from valid path names to information about the
48
// files or directories they represent.
49
//
50
// File system operations are read only. Modifications to the underlying
51
// *Archive may race. To help prevent this, the filesystem tries
52
// to detect modification during Open and return ErrModified if it
53
// is able to detect a modification.
54
type filesystem struct {
55
	ar    *Archive
56
	nodes map[string]*node
57
}
58
59
// node is a file or directory in the tree of a filesystem.
60
type node struct {
61
	fileinfo               // fs.FileInfo and fs.DirEntry implementation
62
	idx      int           // index into ar.Files (for files)
63
	entries  []fs.DirEntry // subdirectories and files (for directories)
64
}
65
66
var (
67
	_ fs.FS       = (*filesystem)(nil)
68
	_ fs.DirEntry = (*node)(nil)
69
)
70
71
// initFiles initializes fsys from fsys.ar.Files. Returns an error if there are any
72
// invalid file names or collisions between file or directories.
73
func initFiles(fsys *filesystem) error {
74
	for idx, file := range fsys.ar.Files {
75
		name := file.Name
76
		if !fs.ValidPath(name) {
77
			return fmt.Errorf("file %q is an invalid path", name)
78
		}
79
80
		n := &node{idx: idx, fileinfo: fileinfo{path: name, size: len(file.Data), mode: readOnly}}
81
		if err := insert(fsys, n); err != nil {
82
			return err
83
		}
84
	}
85
	return nil
86
}
87
88
// insert adds node n as an entry to its parent directory within the filesystem.
89
func insert(fsys *filesystem, n *node) error {
90
	if m := fsys.nodes[n.path]; m != nil {
91
		return fmt.Errorf("duplicate path %q", n.path)
92
	}
93
	fsys.nodes[n.path] = n
94
95
	// fsys.nodes contains "." to prevent infinite loops.
96
	parent, err := directory(fsys, path.Dir(n.path))
97
	if err != nil {
98
		return err
99
	}
100
	parent.entries = append(parent.entries, n)
101
	return nil
102
}
103
104
// directory returns the directory node with the path dir and lazily-creates it
105
// if it does not exist.
106
func directory(fsys *filesystem, dir string) (*node, error) {
107
	if m := fsys.nodes[dir]; m != nil && m.IsDir() {
108
		return m, nil // pre-existing directory
109
	}
110
111
	n := &node{fileinfo: fileinfo{path: dir, mode: readOnlyDir}}
112
	if err := insert(fsys, n); err != nil {
113
		return nil, err
114
	}
115
	return n, nil
116
}
117
118
// dataOf returns the data associated with the file t.
119
// May return ErrModified if fsys.ar has been modified.
120
func dataOf(fsys *filesystem, n *node) ([]byte, error) {
121
	if n.idx >= len(fsys.ar.Files) {
122
		return nil, ErrModified
123
	}
124
125
	f := fsys.ar.Files[n.idx]
126
	if f.Name != n.path || len(f.Data) != n.size {
127
		return nil, ErrModified
128
	}
129
	return f.Data, nil
130
}
131
132
func (fsys *filesystem) Open(name string) (fs.File, error) {
133
	if !fs.ValidPath(name) {
134
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
135
	}
136
137
	n := fsys.nodes[name]
138
	switch {
139
	case n == nil:
140
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
141
	case n.IsDir():
142
		return &openDir{fileinfo: n.fileinfo, entries: n.entries}, nil
143
	default:
144
		data, err := dataOf(fsys, n)
145
		if err != nil {
146
			return nil, err
147
		}
148
		return &openFile{fileinfo: n.fileinfo, data: data}, nil
149
	}
150
}
151
152
func (fsys *filesystem) ReadFile(name string) ([]byte, error) {
153
	file, err := fsys.Open(name)
154
	if err != nil {
155
		return nil, err
156
	}
157
	if file, ok := file.(*openFile); ok {
158
		return slices.Clone(file.data), nil
159
	}
160
	return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrInvalid}
161
}
162
163
// A fileinfo implements fs.FileInfo and fs.DirEntry for a given archive file.
164
type fileinfo struct {
165
	path string // unique path to the file or directory within a filesystem
166
	size int
167
	mode fs.FileMode
168
}
169
170
var (
171
	_ fs.FileInfo = (*fileinfo)(nil)
172
	_ fs.DirEntry = (*fileinfo)(nil)
173
)
174
175
func (i *fileinfo) Name() string               { return path.Base(i.path) }
176
func (i *fileinfo) Size() int64                { return int64(i.size) }
177
func (i *fileinfo) Mode() fs.FileMode          { return i.mode }
178
func (i *fileinfo) Type() fs.FileMode          { return i.mode.Type() }
179
func (i *fileinfo) ModTime() time.Time         { return time.Time{} }
180
func (i *fileinfo) IsDir() bool                { return i.mode&fs.ModeDir != 0 }
181
func (i *fileinfo) Sys() any                   { return nil }
182
func (i *fileinfo) Info() (fs.FileInfo, error) { return i, nil }
183
184
// An openFile is a regular (non-directory) fs.File open for reading.
185
type openFile struct {
186
	fileinfo
187
	data   []byte
188
	offset int64
189
}
190
191
var _ fs.File = (*openFile)(nil)
192
193
func (f *openFile) Stat() (fs.FileInfo, error) { return &f.fileinfo, nil }
194
func (f *openFile) Close() error               { return nil }
195
func (f *openFile) Read(b []byte) (int, error) {
196
	if f.offset >= int64(len(f.data)) {
197
		return 0, io.EOF
198
	}
199
	if f.offset < 0 {
200
		return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
201
	}
202
	n := copy(b, f.data[f.offset:])
203
	f.offset += int64(n)
204
	return n, nil
205
}
206
207
func (f *openFile) Seek(offset int64, whence int) (int64, error) {
208
	switch whence {
209
	case 0:
210
		// offset += 0
211
	case 1:
212
		offset += f.offset
213
	case 2:
214
		offset += int64(len(f.data))
215
	}
216
	if offset < 0 || offset > int64(len(f.data)) {
217
		return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
218
	}
219
	f.offset = offset
220
	return offset, nil
221
}
222
223
func (f *openFile) ReadAt(b []byte, offset int64) (int, error) {
224
	if offset < 0 || offset > int64(len(f.data)) {
225
		return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
226
	}
227
	n := copy(b, f.data[offset:])
228
	if n < len(b) {
229
		return n, io.EOF
230
	}
231
	return n, nil
232
}
233
234
// A openDir is a directory fs.File (so also an fs.ReadDirFile) open for reading.
235
type openDir struct {
236
	fileinfo
237
	entries []fs.DirEntry
238
	offset  int
239
}
240
241
var _ fs.ReadDirFile = (*openDir)(nil)
242
243
func (d *openDir) Stat() (fs.FileInfo, error) { return &d.fileinfo, nil }
244
func (d *openDir) Close() error               { return nil }
245
func (d *openDir) Read(b []byte) (int, error) {
246
	return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid}
247
}
248
249
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
250
	n := len(d.entries) - d.offset
251
	if n == 0 && count > 0 {
252
		return nil, io.EOF
253
	}
254
	if count > 0 && n > count {
255
		n = count
256
	}
257
	list := make([]fs.DirEntry, n)
258
	copy(list, d.entries[d.offset:d.offset+n])
259
	d.offset += n
260
	return list, nil
261
}