59 lines
1.7 KiB
Go
59 lines
1.7 KiB
Go
package pathsafe
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// JoinSlashRelativeUnderRoot validates a slash-style relative path and resolves
|
|
// it under root. The returned path uses the host filepath separator.
|
|
func JoinSlashRelativeUnderRoot(root, relative string) (string, error) {
|
|
rootClean := filepath.Clean(strings.TrimSpace(root))
|
|
if rootClean == "." || rootClean == "" {
|
|
return "", fmt.Errorf("root path is required")
|
|
}
|
|
|
|
normalized, err := NormalizeRelativeDestination(relative)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
joined := filepath.Clean(filepath.Join(rootClean, filepath.FromSlash(normalized)))
|
|
rel, err := filepath.Rel(rootClean, joined)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve relative path under root: %w", err)
|
|
}
|
|
if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", ErrRelativePathEscape
|
|
}
|
|
return joined, nil
|
|
}
|
|
|
|
// SlashRelativeFromRoot derives a slash-style relative path for target under
|
|
// root. Target may be absolute or relative to root.
|
|
func SlashRelativeFromRoot(root, target string) (string, error) {
|
|
rootClean := filepath.Clean(strings.TrimSpace(root))
|
|
if rootClean == "." || rootClean == "" {
|
|
return "", fmt.Errorf("root path is required")
|
|
}
|
|
|
|
targetClean := filepath.Clean(strings.TrimSpace(target))
|
|
if targetClean == "." || targetClean == "" {
|
|
return "", ErrRelativePathRequired
|
|
}
|
|
if !filepath.IsAbs(targetClean) {
|
|
targetClean = filepath.Clean(filepath.Join(rootClean, targetClean))
|
|
}
|
|
|
|
rel, err := filepath.Rel(rootClean, targetClean)
|
|
if err != nil {
|
|
return "", fmt.Errorf("derive path relative to root: %w", err)
|
|
}
|
|
normalized, err := NormalizeRelativeDestination(filepath.ToSlash(rel))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return normalized, nil
|
|
}
|