hugin/cli/internal/paths/paths.go
2026-10-27 08:33:21 +01:00

70 lines
2.3 KiB
Go

// Package pfade prüft Pfade aus Bundles, bevor das CLI sie anfasst
// (→ KONZEPT.md 16.2: nicht aus dem Repo ausbrechen).
package paths
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
)
// windowsForbidden: Zeichen, die Windows in Namen nicht erlaubt oder umdeutet.
const windowsForbidden = `<>:"|?*`
// Validate meldet einen Fehler, wenn rel kein sauberer, relativer Pfad mit /
// ist: kein führender /, kein \, kein Laufwerk, keine Segmente "." oder "..".
// Dazu, was Windows umdeutet (Punkt oder Leerzeichen am Ende, Steuerzeichen),
// und nichts unter .git/ (Hooks dort führt git selbst aus).
func Validate(rel string) error {
switch {
case rel == "":
return errors.New("leerer Pfad")
case strings.HasPrefix(rel, "/"), strings.Contains(rel, `\`), strings.Contains(rel, ":"):
return fmt.Errorf("%q: nur relative Pfade mit / erlaubt", rel)
}
for _, part := range strings.Split(strings.TrimSuffix(rel, "/"), "/") {
if part == "" || part == "." || part == ".." {
return fmt.Errorf("%q: ungültiges Segment %q", rel, part)
}
if strings.HasSuffix(part, ".") || strings.HasSuffix(part, " ") || strings.ContainsAny(part, windowsForbidden) ||
strings.IndexFunc(part, func(r rune) bool { return r < 32 }) >= 0 {
return fmt.Errorf("%q: Segment %q geht nicht auf allen Systemen", rel, part)
}
}
if erstes, _, _ := strings.Cut(rel, "/"); strings.EqualFold(erstes, ".git") {
return fmt.Errorf("%q: Hugin schreibt nicht unter .git/", rel)
}
if path.Clean(rel) != strings.TrimSuffix(rel, "/") {
return fmt.Errorf("%q: Pfad ist nicht normalisiert", rel)
}
return nil
}
// InRepo prüft rel und liefert den absoluten Pfad unter repo. Symlinks auf dem
// Weg dorthin werden abgelehnt, damit kein Pfad über einen Link nach draußen führt.
func InRepo(repo, rel string) (string, error) {
if err := Validate(rel); err != nil {
return "", err
}
target := filepath.Join(repo, filepath.FromSlash(rel))
parts := strings.Split(strings.TrimSuffix(rel, "/"), "/")
current := repo
for _, part := range parts {
current = filepath.Join(current, part)
info, err := os.Lstat(current)
if errors.Is(err, fs.ErrNotExist) {
break
}
if err != nil {
return "", err
}
if info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("%q: Symlink im Pfad (%s)", rel, current)
}
}
return target, nil
}