Pulse/internal/pathutil/pathutil.go
2026-03-31 08:55:45 +01:00

67 lines
1.6 KiB
Go

package pathutil
import (
"fmt"
"path/filepath"
"strings"
"unicode"
)
// NormalizeDir returns a clean absolute directory path.
func NormalizeDir(dir string) (string, error) {
dir = strings.TrimSpace(dir)
if dir == "" {
return "", fmt.Errorf("directory path cannot be empty")
}
abs, err := filepath.Abs(filepath.Clean(dir))
if err != nil {
return "", fmt.Errorf("normalize directory path: %w", err)
}
return abs, nil
}
// JoinBaseFile safely joins a trusted root with a single basename child.
func JoinBaseFile(root, name string) (string, error) {
if strings.TrimSpace(name) == "" {
return "", fmt.Errorf("file name cannot be empty")
}
if filepath.Base(name) != name || name == "." || name == ".." || strings.ContainsRune(name, 0) {
return "", fmt.Errorf("unsafe file name %q", name)
}
joined := filepath.Join(root, name)
rel, err := filepath.Rel(root, joined)
if err != nil {
return "", fmt.Errorf("resolve child path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path escapes root")
}
return joined, nil
}
// LegacySafeFilename derives a filesystem-safe legacy filename.
func LegacySafeFilename(id, suffix string) (string, bool) {
id = strings.TrimSpace(id)
if id == "" || strings.ContainsRune(id, 0) {
return "", false
}
var b strings.Builder
for _, r := range id {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r), r == '-', r == '_', r == '.':
b.WriteRune(r)
default:
b.WriteByte('_')
}
}
name := strings.Trim(b.String(), "._")
if name == "" || name == "." || name == ".." {
return "", false
}
return name + suffix, true
}