Files
notarius/internal/framework/readonlyfs/readonly_fs.go

194 lines
5.2 KiB
Go

// Package readonlyfs provides an immutable filesystem backed by owned bytes.
package readonlyfs
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"path"
"sort"
"time"
)
// FS is an immutable filesystem constructed from a map of file contents.
type FS struct {
files map[string][]byte
dirs map[string][]fs.DirEntry
}
var _ fs.FS = (*FS)(nil)
var _ fs.ReadFileFS = (*FS)(nil)
var _ fs.ReadDirFS = (*FS)(nil)
// New constructs an immutable filesystem. File names must be valid io/fs
// paths and the root path may not be used as a file.
func New(files map[string][]byte) (*FS, error) {
owned := make(map[string][]byte, len(files))
directories := map[string]struct{}{".": {}}
for name, data := range files {
if name == "." || !fs.ValidPath(name) {
return nil, fmt.Errorf("invalid file path %q", name)
}
owned[name] = append([]byte(nil), data...)
for directory := path.Dir(name); directory != "."; directory = path.Dir(directory) {
directories[directory] = struct{}{}
}
}
for directory := range directories {
if _, exists := owned[directory]; exists {
return nil, fmt.Errorf("file path %q conflicts with an inferred directory", directory)
}
}
dirs := make(map[string][]fs.DirEntry, len(directories))
for directory := range directories {
dirs[directory] = make([]fs.DirEntry, 0)
}
for directory := range directories {
if directory == "." {
continue
}
parent := path.Dir(directory)
dirs[parent] = append(dirs[parent], dirEntry{name: path.Base(directory), dir: true})
}
for name, data := range owned {
parent := path.Dir(name)
dirs[parent] = append(dirs[parent], dirEntry{name: path.Base(name), size: int64(len(data))})
}
for directory := range dirs {
sort.Slice(dirs[directory], func(left, right int) bool {
return dirs[directory][left].Name() < dirs[directory][right].Name()
})
}
return &FS{files: owned, dirs: dirs}, nil
}
func (f *FS) Open(name string) (fs.File, error) {
if err := validOpenPath(name); err != nil {
return nil, pathError("open", name, err)
}
if data, ok := f.files[name]; ok {
return &file{reader: bytes.NewReader(data), info: fileInfo{name: path.Base(name), size: int64(len(data))}}, nil
}
if entries, ok := f.dirs[name]; ok {
return &dir{name: path.Base(name), entries: entries}, nil
}
return nil, pathError("open", name, fs.ErrNotExist)
}
func (f *FS) ReadFile(name string) ([]byte, error) {
if err := validOpenPath(name); err != nil {
return nil, pathError("readfile", name, err)
}
data, ok := f.files[name]
if !ok {
if _, isDirectory := f.dirs[name]; isDirectory {
return nil, pathError("readfile", name, errIsDirectory)
}
return nil, pathError("readfile", name, fs.ErrNotExist)
}
return append([]byte(nil), data...), nil
}
func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
if err := validOpenPath(name); err != nil {
return nil, pathError("readdir", name, err)
}
entries, ok := f.dirs[name]
if !ok {
if _, isFile := f.files[name]; isFile {
return nil, pathError("readdir", name, errNotDirectory)
}
return nil, pathError("readdir", name, fs.ErrNotExist)
}
return append([]fs.DirEntry(nil), entries...), nil
}
func validOpenPath(name string) error {
if name == "." || fs.ValidPath(name) {
return nil
}
return fs.ErrInvalid
}
func pathError(operation, name string, err error) error {
return &fs.PathError{Op: operation, Path: name, Err: err}
}
var (
errIsDirectory = errors.New("is a directory")
errNotDirectory = errors.New("not a directory")
)
type file struct {
reader *bytes.Reader
info fileInfo
}
func (f *file) Stat() (fs.FileInfo, error) { return f.info, nil }
func (f *file) Read(data []byte) (int, error) {
return f.reader.Read(data)
}
func (f *file) Close() error { return nil }
type dir struct {
name string
offset int
entries []fs.DirEntry
}
func (d *dir) Stat() (fs.FileInfo, error) { return fileInfo{name: d.name, dir: true}, nil }
func (d *dir) Read([]byte) (int, error) { return 0, errIsDirectory }
func (d *dir) Close() error { return nil }
func (d *dir) ReadDir(count int) ([]fs.DirEntry, error) {
if count <= 0 {
entries := append([]fs.DirEntry(nil), d.entries[d.offset:]...)
d.offset = len(d.entries)
return entries, nil
}
if d.offset >= len(d.entries) {
return nil, io.EOF
}
end := len(d.entries)
if d.offset+count < end {
end = d.offset + count
}
entries := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
d.offset = end
return entries, nil
}
type dirEntry struct {
name string
dir bool
size int64
}
func (e dirEntry) Name() string { return e.name }
func (e dirEntry) IsDir() bool { return e.dir }
func (e dirEntry) Type() fs.FileMode { return e.mode().Type() }
func (e dirEntry) Info() (fs.FileInfo, error) {
return fileInfo{name: e.name, dir: e.dir, size: e.size}, nil
}
func (e dirEntry) mode() fs.FileMode {
if e.dir {
return fs.ModeDir | 0o555
}
return 0o444
}
type fileInfo struct {
name string
dir bool
size int64
}
func (i fileInfo) Name() string { return i.name }
func (i fileInfo) Size() int64 { return i.size }
func (i fileInfo) Mode() fs.FileMode { return dirEntry{dir: i.dir}.mode() }
func (i fileInfo) ModTime() time.Time { return time.Time{} }
func (i fileInfo) IsDir() bool { return i.dir }
func (i fileInfo) Sys() any { return nil }