Files
ConfigKit/internal/engine/filesystem_links.go

221 lines
6.1 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
}
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
}
func resolveHostImportPath(access *filesystemAccess, name string) (string, error) {
pending := strings.Split(filepath.ToSlash(name), "/")
resolved := ""
activeLinks := make(map[string]struct{})
for len(pending) > 0 {
segment := pending[0]
pending = pending[1:]
candidate := path.Join(resolved, segment)
if candidate == "" {
candidate = "."
}
info, err := access.root.Lstat(filepath.FromSlash(candidate))
if err != nil {
return "", err
}
if info.Mode()&fs.ModeSymlink == 0 {
resolved = candidate
continue
}
if _, exists := activeLinks[candidate]; exists {
return "", errImportDirectoryLinkCycle
}
activeLinks[candidate] = struct{}{}
target, err := access.root.Readlink(filepath.FromSlash(candidate))
if err != nil {
return "", err
}
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
}
remaining := append([]string(nil), pending...)
pending = append(strings.Split(targetPath, "/"), remaining...)
resolved = ""
}
if resolved == "" {
resolved = "."
}
return resolved, 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
}
pending := strings.Split(name, "/")
resolved := ""
activeLinks := make(map[string]struct{})
for len(pending) > 0 {
segment := pending[0]
pending = pending[1:]
candidate := path.Join(resolved, segment)
if candidate == "" {
candidate = "."
}
// 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 "", errImportPathOutsideRoot
}
info, err := filesystem.Lstat(candidate)
if err != nil {
return "", err
}
if info.Mode()&fs.ModeSymlink == 0 {
resolved = candidate
continue
}
if _, exists := activeLinks[candidate]; exists {
return "", errImportDirectoryLinkCycle
}
activeLinks[candidate] = struct{}{}
target, err := filesystem.ReadLink(candidate)
if err != nil {
return "", err
}
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
}
remaining := append([]string(nil), pending...)
pending = append(strings.Split(targetPath, "/"), remaining...)
resolved = ""
}
if resolved == "" {
resolved = "."
}
return resolved, nil
}
func isImportLinkCycle(err error) bool {
return errors.Is(err, errImportDirectoryLinkCycle)
}