// 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 }