package api import ( "errors" "fmt" "path/filepath" "strings" ) // ErrOutsideIngestRoot means an ingest path does not resolve inside brain/raw/. var ErrOutsideIngestRoot = errors.New("ingest path must be inside brain/raw/") // ResolveIngestPath confines a caller-supplied ingest path to brain/raw/, the // inbox the watcher already owns (hyperguild#149). Relative paths resolve // against that root; symlinks are evaluated before the containment check, so // a link inside raw/ that points elsewhere is refused. The path must exist. func ResolveIngestPath(brainDir, p string) (string, error) { if strings.TrimSpace(p) == "" { return "", fmt.Errorf("%w: path is empty", ErrOutsideIngestRoot) } root, err := filepath.EvalSymlinks(filepath.Join(brainDir, "raw")) if err != nil { return "", fmt.Errorf("%w: inbox unavailable: %v", ErrOutsideIngestRoot, err) } if !filepath.IsAbs(p) { p = filepath.Join(root, p) } real, err := filepath.EvalSymlinks(filepath.Clean(p)) if err != nil { return "", fmt.Errorf("%w: %v", ErrOutsideIngestRoot, err) } rel, err := filepath.Rel(root, real) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) { return "", ErrOutsideIngestRoot } return real, nil }