package app import ( "fmt" "os" "path/filepath" "strings" ) func validateScopedTarget(root, target, policy string, requireDir bool) (scopedDir, error) { cleanRoot := strings.TrimSpace(root) cleanTarget := strings.TrimSpace(target) if cleanRoot == "" { return scopedDir{}, fmt.Errorf("cleanup policy %s: root path is required", policy) } if cleanTarget == "" { return scopedDir{}, fmt.Errorf("cleanup policy %s: target path is required", policy) } rootAbs, err := filepath.Abs(cleanRoot) if err != nil { return scopedDir{}, fmt.Errorf("cleanup policy %s: resolve root %q: %w", policy, cleanRoot, err) } targetAbs, err := filepath.Abs(cleanTarget) if err != nil { return scopedDir{}, fmt.Errorf("cleanup policy %s: resolve target %q: %w", policy, cleanTarget, err) } rel, err := filepath.Rel(rootAbs, targetAbs) if err != nil { return scopedDir{}, fmt.Errorf("cleanup policy %s: relative path from %q to %q: %w", policy, rootAbs, targetAbs, err) } if rel == "." { return scopedDir{}, fmt.Errorf("cleanup policy %s: refusing to delete root directory %q", policy, rootAbs) } if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { return scopedDir{}, fmt.Errorf("cleanup policy %s: refusing to delete path outside root: root=%q target=%q", policy, rootAbs, targetAbs) } info, err := os.Lstat(targetAbs) if err != nil { if os.IsNotExist(err) { return scopedDir{RootAbs: rootAbs, TargetAbs: targetAbs, Exists: false}, nil } return scopedDir{}, fmt.Errorf("cleanup policy %s: stat target %q: %w", policy, targetAbs, err) } if info.Mode()&os.ModeSymlink != 0 { return scopedDir{}, fmt.Errorf("cleanup policy %s: refusing to delete symlink path %q", policy, targetAbs) } if requireDir && !info.IsDir() { return scopedDir{}, fmt.Errorf("cleanup policy %s: target %q is not a directory", policy, targetAbs) } if !requireDir && info.IsDir() { return scopedDir{}, fmt.Errorf("cleanup policy %s: target %q is a directory", policy, targetAbs) } return scopedDir{RootAbs: rootAbs, TargetAbs: targetAbs, Exists: true}, nil } func validateCleanRoot(root, policy string) (string, bool, error) { cleanRoot := strings.TrimSpace(root) if cleanRoot == "" { return "", false, fmt.Errorf("cleanup policy %s: root path is required", policy) } rootAbs, err := filepath.Abs(cleanRoot) if err != nil { return "", false, fmt.Errorf("cleanup policy %s: resolve root %q: %w", policy, cleanRoot, err) } info, err := os.Lstat(rootAbs) if err != nil { if os.IsNotExist(err) { return rootAbs, false, nil } return "", false, fmt.Errorf("cleanup policy %s: stat root %q: %w", policy, rootAbs, err) } if info.Mode()&os.ModeSymlink != 0 { return "", false, fmt.Errorf("cleanup policy %s: refusing to clean symlink root %q", policy, rootAbs) } if !info.IsDir() { return "", false, fmt.Errorf("cleanup policy %s: root %q is not a directory", policy, rootAbs) } return rootAbs, true, nil }