186 lines
4.3 KiB
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
|
|
}
|