257 lines
7.5 KiB
Go
257 lines
7.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
errImportDirectoryLinkCycle = errors.New("directory link cycle")
|
|
errImportPathOutsideRoot = errors.New("resolved import path is outside the configuration root")
|
|
)
|
|
|
|
// importEntryTarget describes a discovered entry without changing its lexical
|
|
// candidate spelling. identity is resolved only to detect active traversal loops.
|
|
type importEntryTarget struct {
|
|
symlink bool
|
|
dir bool
|
|
identity string
|
|
}
|
|
|
|
func (a *filesystemAccess) importEntryTarget(name string, entry fs.DirEntry, follow bool) (importEntryTarget, error) {
|
|
if a.native == nil {
|
|
relative, err := a.relative(name)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
// Root.Lstat cannot follow absolute links in intermediate components,
|
|
// even when contained. Resolve parents first, leaving the final entry
|
|
// unexpanded so its discovered-link follow policy still applies.
|
|
if parent := filepath.Dir(relative); parent != "." {
|
|
resolvedParent, err := resolveHostImportPath(a, parent)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
relative = filepath.Join(filepath.FromSlash(resolvedParent), filepath.Base(relative))
|
|
}
|
|
info, err := a.root.Lstat(relative)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
symlink := info.Mode()&fs.ModeSymlink != 0
|
|
if symlink && !follow {
|
|
return importEntryTarget{symlink: true}, nil
|
|
}
|
|
if !symlink && !info.IsDir() {
|
|
return importEntryTarget{}, nil
|
|
}
|
|
resolved, err := resolveHostImportPath(a, relative)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
resolvedPath := filepath.Join(a.rootPath, filepath.FromSlash(resolved))
|
|
info, err = a.root.Stat(filepath.FromSlash(resolved))
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
resolvedPath = filepath.Clean(resolvedPath)
|
|
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: resolvedPath}, nil
|
|
}
|
|
|
|
linkFS, linksKnown := a.native.(fs.ReadLinkFS)
|
|
if linksKnown {
|
|
info, err := linkFS.Lstat(name)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
symlink := info.Mode()&fs.ModeSymlink != 0
|
|
if symlink && !follow {
|
|
return importEntryTarget{symlink: true}, nil
|
|
}
|
|
if !symlink && !info.IsDir() {
|
|
return importEntryTarget{}, nil
|
|
}
|
|
resolved, err := resolveNativeImportPath(a, linkFS, name)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
if !a.contains(resolved) {
|
|
return importEntryTarget{}, errImportPathOutsideRoot
|
|
}
|
|
info, err = fs.Stat(a.native, resolved)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: resolved}, nil
|
|
}
|
|
|
|
symlink := entry != nil && entry.Type()&fs.ModeSymlink != 0
|
|
if symlink && !follow {
|
|
return importEntryTarget{symlink: true}, nil
|
|
}
|
|
if entry != nil && !symlink {
|
|
mode := entry.Type()
|
|
if mode == 0 {
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
mode = info.Mode()
|
|
symlink = mode&fs.ModeSymlink != 0
|
|
if symlink && !follow {
|
|
return importEntryTarget{symlink: true}, nil
|
|
}
|
|
}
|
|
if !symlink && !mode.IsDir() {
|
|
return importEntryTarget{}, nil
|
|
}
|
|
if !symlink && mode.IsDir() {
|
|
return importEntryTarget{dir: true, identity: name}, nil
|
|
}
|
|
}
|
|
info, err := fs.Stat(a.native, name)
|
|
if err != nil {
|
|
return importEntryTarget{}, err
|
|
}
|
|
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: name}, nil
|
|
}
|
|
|
|
// A link remains active until its own target resolves. Completed targets can be
|
|
// reused while the caller continues resolving its remaining components.
|
|
type importLinkFrame struct {
|
|
pending []string
|
|
link string
|
|
}
|
|
|
|
type importLinkOperations struct {
|
|
lstat func(string) (fs.FileInfo, error)
|
|
readlink func(string) (string, error)
|
|
normalizeTarget func(string, string) (string, error)
|
|
}
|
|
|
|
func resolveImportLinkPath(name string, operations importLinkOperations) (string, error) {
|
|
frames := []importLinkFrame{{pending: strings.Split(name, "/")}}
|
|
resolved := ""
|
|
active := make(map[string]struct{})
|
|
completed := make(map[string]string)
|
|
for len(frames) > 0 {
|
|
frame := &frames[len(frames)-1]
|
|
if len(frame.pending) == 0 {
|
|
if frame.link != "" {
|
|
if resolved == "" {
|
|
resolved = "."
|
|
}
|
|
completed[frame.link] = resolved
|
|
delete(active, frame.link)
|
|
}
|
|
frames = frames[:len(frames)-1]
|
|
continue
|
|
}
|
|
segment := frame.pending[0]
|
|
frame.pending = frame.pending[1:]
|
|
candidate := path.Join(resolved, segment)
|
|
if candidate == "" {
|
|
candidate = "."
|
|
}
|
|
info, err := operations.lstat(candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if info.Mode()&fs.ModeSymlink == 0 {
|
|
resolved = candidate
|
|
continue
|
|
}
|
|
if target, ok := completed[candidate]; ok {
|
|
resolved = target
|
|
continue
|
|
}
|
|
if _, exists := active[candidate]; exists {
|
|
return "", errImportDirectoryLinkCycle
|
|
}
|
|
target, err := operations.readlink(candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
targetPath, err := operations.normalizeTarget(candidate, target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
active[candidate] = struct{}{}
|
|
frames = append(frames, importLinkFrame{pending: strings.Split(targetPath, "/"), link: candidate})
|
|
resolved = ""
|
|
}
|
|
if resolved == "" {
|
|
resolved = "."
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func resolveHostImportPath(access *filesystemAccess, name string) (string, error) {
|
|
return resolveImportLinkPath(filepath.ToSlash(name), hostImportLinkOperations(access))
|
|
}
|
|
|
|
func hostImportLinkOperations(access *filesystemAccess) importLinkOperations {
|
|
return importLinkOperations{
|
|
lstat: func(candidate string) (fs.FileInfo, error) {
|
|
return access.root.Lstat(filepath.FromSlash(candidate))
|
|
},
|
|
readlink: func(candidate string) (string, error) {
|
|
return access.root.Readlink(filepath.FromSlash(candidate))
|
|
},
|
|
normalizeTarget: func(candidate, target string) (string, error) {
|
|
var targetPath string
|
|
if filepath.IsAbs(target) {
|
|
relative, err := filepath.Rel(access.rootPath, filepath.Clean(target))
|
|
if err != nil {
|
|
return "", errImportPathOutsideRoot
|
|
}
|
|
targetPath = path.Clean(filepath.ToSlash(relative))
|
|
} else {
|
|
targetPath = path.Clean(path.Join(path.Dir(candidate), filepath.ToSlash(target)))
|
|
}
|
|
if !withinPortableRoot(targetPath) {
|
|
return "", errImportPathOutsideRoot
|
|
}
|
|
return targetPath, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func withinPortableRoot(name string) bool {
|
|
return name == "." || name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/")
|
|
}
|
|
|
|
func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS, name string) (string, error) {
|
|
if !fs.ValidPath(name) || !access.contains(name) {
|
|
return "", fs.ErrInvalid
|
|
}
|
|
return resolveImportLinkPath(name, importLinkOperations{
|
|
lstat: func(candidate string) (fs.FileInfo, error) {
|
|
// Ancestors of a multi-component root are resolution prefixes, not
|
|
// selected targets. Complete paths and link targets remain contained.
|
|
if !access.contains(candidate) && !strings.HasPrefix(access.rootPath, candidate+"/") {
|
|
return nil, errImportPathOutsideRoot
|
|
}
|
|
return filesystem.Lstat(candidate)
|
|
},
|
|
readlink: filesystem.ReadLink,
|
|
normalizeTarget: func(candidate, target string) (string, error) {
|
|
if target == "" || strings.HasPrefix(target, "/") {
|
|
return "", errImportPathOutsideRoot
|
|
}
|
|
targetPath := path.Clean(path.Join(path.Dir(candidate), target))
|
|
if !fs.ValidPath(targetPath) || !access.contains(targetPath) {
|
|
return "", errImportPathOutsideRoot
|
|
}
|
|
return targetPath, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
func isImportLinkCycle(err error) bool {
|
|
return errors.Is(err, errImportDirectoryLinkCycle)
|
|
}
|