70 lines
2.3 KiB
Go
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
|
|
}
|