package api import ( "errors" "os" "github.com/mathiasbq/hyperguild/ingestion/internal/extract" "path/filepath" "strings" ) // ErrOutsideIngestRoot is the one refusal for every bad ingest path. It never // carries the underlying error: "no such file" versus "permission denied" // would let a caller probe which server files exist (PR #152 review). var ErrOutsideIngestRoot = errors.New("ingest path must be an existing file or directory 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 "", ErrOutsideIngestRoot } // raw/ itself must be a real directory: if it were a symlink, containment // inside it would mean nothing (hyperguild#153 item 4). if fi, lerr := os.Lstat(filepath.Join(brainDir, "raw")); lerr != nil || fi.Mode()&os.ModeSymlink != 0 || !fi.IsDir() { return "", ErrOutsideIngestRoot } root, err := filepath.EvalSymlinks(filepath.Join(brainDir, "raw")) if err != nil { return "", ErrOutsideIngestRoot } if !filepath.IsAbs(p) { p = filepath.Join(root, p) } real, err := filepath.EvalSymlinks(filepath.Clean(p)) if err != nil { return "", ErrOutsideIngestRoot } rel, err := filepath.Rel(root, real) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) { return "", ErrOutsideIngestRoot } return real, nil } // ReadIngestFile extracts text from an ingest path already confined by // ResolveIngestPath, reading it through the raw/ root rather than reopening // the path by name (hyperguild#153 item 3): if the file is swapped for a // symlink after the check, the read still cannot leave raw/. func ReadIngestFile(brainDir, resolved string) (string, error) { root, rawPath, err := OpenRawRoot(brainDir) if err != nil { return "", ErrOutsideIngestRoot } defer func() { _ = root.Close() }() realRoot, err := filepath.EvalSymlinks(rawPath) if err != nil { return "", ErrOutsideIngestRoot } rel, err := filepath.Rel(realRoot, resolved) if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) { return "", ErrOutsideIngestRoot } return extract.TextIn(root, rel) }