Share immutable in-memory filesystems

This commit is contained in:
2026-08-09 01:47:44 +00:00
parent 3e66127b94
commit d653bf1b90
5 changed files with 434 additions and 311 deletions

View File

@@ -0,0 +1,193 @@
// 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 }

View File

@@ -0,0 +1,153 @@
package readonlyfs
import (
"errors"
"io"
"io/fs"
"testing"
"testing/fstest"
)
func TestNewPassesFSTest(t *testing.T) {
for _, test := range []struct {
name string
files map[string][]byte
paths []string
}{
{name: "empty", files: map[string][]byte{}, paths: nil},
{name: "single file", files: map[string][]byte{"message.txt": []byte("hello")}, paths: []string{"message.txt"}},
{name: "nested tree", files: map[string][]byte{"a/one.txt": []byte("one"), "a/b/two.txt": []byte("two")}, paths: []string{"a/one.txt", "a/b/two.txt"}},
} {
t.Run(test.name, func(t *testing.T) {
fsys, err := New(test.files)
if err != nil {
t.Fatal(err)
}
if err := fstest.TestFS(fsys, test.paths...); err != nil {
t.Fatal(err)
}
})
}
}
func TestNewOwnsConstructionAndReadBytes(t *testing.T) {
input := []byte("value")
fsys, err := New(map[string][]byte{"value.txt": input})
if err != nil {
t.Fatal(err)
}
input[0] = 'x'
first, err := fsys.ReadFile("value.txt")
if err != nil {
t.Fatal(err)
}
first[0] = 'y'
second, err := fsys.ReadFile("value.txt")
if err != nil {
t.Fatal(err)
}
if string(second) != "value" {
t.Fatalf("ReadFile() = %q, want owned bytes", second)
}
}
func TestFilesystemStandardBehavior(t *testing.T) {
fsys, err := New(map[string][]byte{
"z.txt": []byte("z"),
"nested/a.md": []byte("abc"),
"nested/b.md": []byte("b"),
})
if err != nil {
t.Fatal(err)
}
entries, err := fsys.ReadDir("nested")
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 || entries[0].Name() != "a.md" || entries[1].Name() != "b.md" {
t.Fatalf("ReadDir(nested) = %#v, want sorted entries", entries)
}
info, err := entries[0].Info()
if err != nil || info.Mode().Perm() != 0o444 || info.Size() != 3 {
t.Fatalf("file info = %#v, %v", info, err)
}
root, err := fsys.Open(".")
if err != nil {
t.Fatal(err)
}
rootInfo, err := root.Stat()
if err != nil || !rootInfo.IsDir() || rootInfo.Mode() != fs.ModeDir|0o555 {
t.Fatalf("root info = %#v, %v", rootInfo, err)
}
directory := root.(fs.ReadDirFile)
first, err := directory.ReadDir(1)
if err != nil || len(first) != 1 || first[0].Name() != "nested" {
t.Fatalf("first directory page = %#v, %v", first, err)
}
second, err := directory.ReadDir(1)
if err != nil || len(second) != 1 || second[0].Name() != "z.txt" {
t.Fatalf("second directory page = %#v, %v", second, err)
}
if _, err := directory.ReadDir(1); !errors.Is(err, io.EOF) {
t.Fatalf("final directory page error = %v, want EOF", err)
}
secondRoot, err := fsys.Open(".")
if err != nil {
t.Fatal(err)
}
secondDirectory := secondRoot.(fs.ReadDirFile)
firstAgain, err := secondDirectory.ReadDir(1)
if err != nil || len(firstAgain) != 1 || firstAgain[0].Name() != "nested" {
t.Fatalf("independent directory handle page = %#v, %v", firstAgain, err)
}
left, err := fsys.Open("nested/a.md")
if err != nil {
t.Fatal(err)
}
right, err := fsys.Open("nested/a.md")
if err != nil {
t.Fatal(err)
}
leftByte := make([]byte, 1)
rightByte := make([]byte, 1)
if _, err := left.Read(leftByte); err != nil {
t.Fatal(err)
}
if _, err := right.Read(rightByte); err != nil {
t.Fatal(err)
}
if string(leftByte) != "a" || string(rightByte) != "a" {
t.Fatalf("independent file handles read %q and %q", leftByte, rightByte)
}
for _, test := range []struct {
name string
op func() error
want error
}{
{name: "invalid open path", op: func() error { _, err := fsys.Open("../value"); return err }, want: fs.ErrInvalid},
{name: "missing file", op: func() error { _, err := fsys.Open("missing"); return err }, want: fs.ErrNotExist},
{name: "read directory", op: func() error { _, err := fsys.ReadFile("nested"); return err }, want: errIsDirectory},
{name: "read directory from file", op: func() error { _, err := fsys.ReadDir("z.txt"); return err }, want: errNotDirectory},
} {
t.Run(test.name, func(t *testing.T) {
if err := test.op(); !errors.Is(err, test.want) {
t.Fatalf("operation error = %v, want %v", err, test.want)
}
})
}
}
func TestNewRejectsInvalidAndConflictingPaths(t *testing.T) {
for _, files := range []map[string][]byte{
{".": []byte("root")},
{"": []byte("empty")},
{"../outside": []byte("outside")},
{"nested//file": []byte("invalid")},
{"file": []byte("file"), "file/nested": []byte("nested")},
} {
if _, err := New(files); err == nil {
t.Fatalf("New(%#v) error = nil, want rejection", files)
}
}
}