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 }