205 lines
4.9 KiB
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
|
|
}
|