Memoize completed native link dependencies
This commit is contained in:
@@ -126,6 +126,69 @@ type importLinkFrame struct {
|
||||
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) {
|
||||
frames := []importLinkFrame{{pending: strings.Split(filepath.ToSlash(name), "/")}}
|
||||
resolved := ""
|
||||
@@ -189,57 +252,27 @@ func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS,
|
||||
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
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user