86 lines
2.0 KiB
Go
86 lines
2.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Resolve joins rel to root and rejects paths that escape the workspace,
|
|
// including symlink targets outside root. Missing files are allowed when their
|
|
// existing ancestors stay inside root.
|
|
func Resolve(root, rel string) (string, error) {
|
|
rootAbs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rootAbs = filepath.Clean(rootAbs)
|
|
resolvedRoot, err := filepath.EvalSymlinks(rootAbs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel = strings.TrimSpace(rel)
|
|
if rel == "" {
|
|
rel = "."
|
|
}
|
|
if filepath.IsAbs(rel) {
|
|
inside, err := filepath.Rel(rootAbs, filepath.Clean(rel))
|
|
if err != nil || escapes(inside) {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
rel = inside
|
|
}
|
|
rel = filepath.Clean(rel)
|
|
if escapes(rel) {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
if rel == "." {
|
|
return resolvedRoot, nil
|
|
}
|
|
current := resolvedRoot
|
|
parts := strings.Split(rel, string(os.PathSeparator))
|
|
for i, part := range parts {
|
|
if part == "" || part == "." {
|
|
continue
|
|
}
|
|
if part == ".." {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
next := filepath.Join(current, part)
|
|
info, err := os.Lstat(next)
|
|
if os.IsNotExist(err) {
|
|
tail := filepath.Join(parts[i:]...)
|
|
return filepath.Join(current, tail), nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
target, err := filepath.EvalSymlinks(next)
|
|
if err != nil {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
relTarget, err := filepath.Rel(resolvedRoot, target)
|
|
if err != nil || escapes(relTarget) {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
current = target
|
|
continue
|
|
}
|
|
current = next
|
|
}
|
|
relFinal, err := filepath.Rel(resolvedRoot, current)
|
|
if err != nil || escapes(relFinal) {
|
|
return "", fmt.Errorf("path escapes workspace")
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func escapes(rel string) bool {
|
|
if rel == ".." {
|
|
return true
|
|
}
|
|
return strings.HasPrefix(rel, ".."+string(os.PathSeparator))
|
|
}
|