Unify host and native filesystem acquisition
This commit is contained in:
184
internal/engine/filesystem_access.go
Normal file
184
internal/engine/filesystem_access.go
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user