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 stays active until its target has resolved, not until the entire // original pathname has resolved. Remaining segments may reuse a finished link. type importLinkFrame struct { pending []string link string } func resolveHostImportPath(access *filesystemAccess, name string) (string, error) { frames := []importLinkFrame{{pending: strings.Split(filepath.ToSlash(name), "/")}} resolved := "" activeLinks := make(map[string]struct{}) for len(frames) > 0 { frame := &frames[len(frames)-1] if len(frame.pending) == 0 { delete(activeLinks, 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 := 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 } frames = append(frames, importLinkFrame{pending: strings.Split(targetPath, "/"), link: candidate}) 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 } frames := []importLinkFrame{{pending: strings.Split(name, "/")}} resolved := "" activeLinks := make(map[string]struct{}) for len(frames) > 0 { frame := &frames[len(frames)-1] if len(frame.pending) == 0 { delete(activeLinks, 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 = "." } // 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 } frames = append(frames, importLinkFrame{pending: strings.Split(targetPath, "/"), link: candidate}) resolved = "" } if resolved == "" { resolved = "." } return resolved, nil } func isImportLinkCycle(err error) bool { return errors.Is(err, errImportDirectoryLinkCycle) }