Files
ConfigKit/internal/engine/filesystem_access.go

205 lines
4.9 KiB
Go

package engine
import (
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
)
// namespaceToken identifies one caller-selected filesystem without comparing
// arbitrary fs.FS values. A token can be shared by copies of a source wrapper.
type namespaceToken struct{ marker byte }
type nativeNamespace struct {
filesystem fs.FS
label string
token *namespaceToken
}
func newNativeNamespace(label string, filesystem fs.FS) *nativeNamespace {
return &nativeNamespace{filesystem: filesystem, label: label, token: &namespaceToken{}}
}
type resourceIdentity struct {
namespace *namespaceToken
path string
occurrence *namespaceToken
}
// filesystemAccess is a load-scoped view of either a contained host root or a
// borrowed native filesystem. Names are canonical host paths or fs.FS paths.
type filesystemAccess struct {
rootPath string
root *os.Root
native fs.FS
token *namespaceToken
label string
}
func openHostAccess(rootPath string) (*filesystemAccess, error) {
root, err := os.OpenRoot(rootPath)
if err != nil {
return nil, err
}
return &filesystemAccess{rootPath: rootPath, root: root, token: &namespaceToken{}}, nil
}
func newNativeAccess(filesystem fs.FS, root string, token *namespaceToken) *filesystemAccess {
return &filesystemAccess{rootPath: root, native: filesystem, token: token}
}
func (a *filesystemAccess) close() error {
if a.root != nil {
return a.root.Close()
}
return nil
}
func (a *filesystemAccess) identity(name string) resourceIdentity {
return resourceIdentity{namespace: a.token, path: name}
}
func (a *filesystemAccess) syntheticIdentity() resourceIdentity {
return resourceIdentity{namespace: a.token, occurrence: &namespaceToken{}}
}
func (a *filesystemAccess) join(base, name string) string {
if a.native != nil {
return path.Join(base, name)
}
return filepath.Join(base, filepath.FromSlash(path.Clean(name)))
}
func (a *filesystemAccess) dir(name string) string {
if a.native != nil {
return path.Dir(name)
}
return filepath.Dir(name)
}
func (a *filesystemAccess) contains(name string) bool {
if a.native == nil {
return containedPath(a.rootPath, filepath.Clean(name))
}
if !fs.ValidPath(name) || !fs.ValidPath(a.rootPath) {
return false
}
return a.rootPath == "." || name == a.rootPath || strings.HasPrefix(name, a.rootPath+"/")
}
func (a *filesystemAccess) canonical(name string) (string, error) {
if a.native == nil {
return canonicalExistingPath(name)
}
if !fs.ValidPath(name) {
return "", fs.ErrInvalid
}
_, err := fs.Stat(a.native, name)
if err != nil {
return "", err
}
return name, nil
}
func (a *filesystemAccess) relative(name string) (string, error) {
if a.native != nil {
if !a.contains(name) {
return "", fs.ErrInvalid
}
return name, nil
}
relative, err := filepath.Rel(a.rootPath, name)
if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return "", fs.ErrInvalid
}
return relative, nil
}
func (a *filesystemAccess) stat(name string) (fs.FileInfo, error) {
if a.native != nil {
return fs.Stat(a.native, name)
}
relative, err := a.relative(name)
if err != nil {
return nil, err
}
return a.root.Stat(relative)
}
func (a *filesystemAccess) open(name string) (fs.File, error) {
if a.native != nil {
return a.native.Open(name)
}
relative, err := a.relative(name)
if err != nil {
return nil, err
}
return a.root.Open(relative)
}
type directoryReadError struct {
opened bool
err error
}
func (e *directoryReadError) Error() string { return "directory could not be read" }
func (e *directoryReadError) Unwrap() error { return e.err }
func missingDirectoryOpen(err error) bool {
var failure *directoryReadError
return errors.As(err, &failure) && !failure.opened && errors.Is(err, fs.ErrNotExist)
}
func (a *filesystemAccess) readDirectory(name string, visit func(fs.DirEntry) bool) error {
file, err := a.open(name)
if err != nil {
return &directoryReadError{err: err}
}
failure := func(err error) error {
if err == nil {
return nil
}
return &directoryReadError{opened: true, err: err}
}
if reader, ok := file.(fs.ReadDirFile); ok {
for {
entries, readErr := reader.ReadDir(globReadBatch)
for _, entry := range entries {
if !visit(entry) {
return failure(file.Close())
}
}
if errors.Is(readErr, io.EOF) {
return failure(file.Close())
}
if readErr != nil {
_ = file.Close()
return failure(readErr)
}
if len(entries) == 0 {
_ = file.Close()
return failure(io.ErrNoProgress)
}
}
}
if err := file.Close(); err != nil {
return failure(err)
}
// fs.ReadDir provides the standard fallback for implementations whose opened
// files do not implement fs.ReadDirFile. It may allocate all entries itself.
entries, err := fs.ReadDir(a.native, name)
if err != nil {
return failure(err)
}
for _, entry := range entries {
if !visit(entry) {
break
}
}
return nil
}