194 lines
5.2 KiB
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 }
|