// 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 }