fix(ingestion): brain/raw/ readers go through an os.Root; symlinks are inert (#153) (#156)
CI / Lint / Test / Vet (push) Successful in 36s
CI / Mirror to GitHub (push) Successful in 4s

Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #156.
This commit is contained in:
2026-10-03 15:30:49 +00:00
committed by mathias
parent cad389f62f
commit 687afa0145
12 changed files with 811 additions and 51 deletions
+69
View File
@@ -3,8 +3,10 @@ package extract
import (
"fmt"
"io"
"os"
"strings"
"syscall"
)
// Text reads the file at path and returns its plain-text content.
@@ -41,3 +43,70 @@ func fileExt(path string) string {
}
return ""
}
// TextIn is Text for a file read through an os.Root (hyperguild#153): the
// read cannot leave the root, so a path checked earlier cannot be swapped
// for a symlink pointing elsewhere before it is read. Formats that need an
// external tool (pdf, docmark) are copied to a temp file from the handle
// opened through the root, then extracted from the copy.
func TextIn(r *os.Root, rel string) (string, error) {
fi, err := r.Lstat(rel)
if err != nil {
return "", fmt.Errorf("stat %s: %w", rel, err)
}
if !fi.Mode().IsRegular() {
return "", fmt.Errorf("%s: not a regular file", rel)
}
f, _, err := OpenRegular(r, rel)
if err != nil {
return "", fmt.Errorf("open %s: %w", rel, err)
}
defer func() { _ = f.Close() }()
ext := strings.ToLower(fileExt(rel))
switch ext {
case ".md", ".txt":
b, err := io.ReadAll(f)
if err != nil {
return "", fmt.Errorf("read %s: %w", rel, err)
}
return string(b), nil
case ".pdf", ".docx", ".xlsx", ".pptx", ".png", ".jpg", ".jpeg":
tmp, err := os.CreateTemp("", "ingest-*"+ext)
if err != nil {
return "", fmt.Errorf("temp copy: %w", err)
}
defer func() { _ = os.Remove(tmp.Name()) }()
if _, err := io.Copy(tmp, f); err != nil {
_ = tmp.Close()
return "", fmt.Errorf("copy %s: %w", rel, err)
}
if err := tmp.Close(); err != nil {
return "", fmt.Errorf("copy %s: %w", rel, err)
}
return Text(tmp.Name())
default:
return "", fmt.Errorf("unsupported file extension: %s", ext)
}
}
// OpenRegular opens rel inside r and returns it only if the OPENED handle is
// a regular file. The open is non-blocking, so a FIFO swapped in after an
// Lstat (PR #156 review: it hung open(2) forever) is refused promptly rather
// than stalling the reader. Callers keep their Lstat check, which refuses
// symlinks outright; this closes the race between that check and the open.
func OpenRegular(r *os.Root, rel string) (*os.File, os.FileInfo, error) {
f, err := r.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, nil, err
}
fi, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, nil, err
}
if !fi.Mode().IsRegular() {
_ = f.Close()
return nil, nil, fmt.Errorf("%s: not a regular file", rel)
}
return f, fi, nil
}