Files
ConfigKit/internal/engine/filesystem_access.go

186 lines
4.3 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)
}
func (a *filesystemAccess) readDirectory(name string, visit func(fs.DirEntry) bool) error {
file, err := a.open(name)
if err != nil {
return err
}
if reader, ok := file.(fs.ReadDirFile); ok {
for {
entries, readErr := reader.ReadDir(globReadBatch)
for _, entry := range entries {
if !visit(entry) {
return file.Close()
}
}
if errors.Is(readErr, io.EOF) {
return file.Close()
}
if readErr != nil {
_ = file.Close()
return readErr
}
if len(entries) == 0 {
_ = file.Close()
return io.ErrNoProgress
}
}
}
if err := file.Close(); err != nil {
return 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 err
}
for _, entry := range entries {
if !visit(entry) {
break
}
}
return nil
}