fix(ingestion): close the PR #156 review findings on raw/ reads
CI / Lint / Test / Vet (pull_request) Successful in 34s
CI / Mirror to GitHub (pull_request) Skipped

- Reverting the ingest call sites to extract.Text(path) stayed green;
  a test now fails if anything outside internal/extract calls
  extract.Text( (only TextIn, through an os.Root, may be used).
- The watcher symlink test passed for a watcher that ingested nothing,
  or stopped at the first symlink (SkipAll: one synced link halting all
  ingestion). It now plants a real note after the symlink and asserts
  exactly one ingest, and the symlinked-raw refusal has its own test.
- A FIFO swapped in between Lstat and Open hung open(2) forever.
  extract.OpenRegular opens non-blocking and checks the type on the
  handle; readRawNote, TextIn and the watcher's archive copy use it.
- The symlinked-root test covers pending, promote, reject, resolve and
  ReadIngestFile, and is named for exactly those.
- The hard-link residual is documented where it is accepted.

Out of scope, filed: #157 (symlinks elsewhere in the Syncthing-shared
brain dir reach search and other readers), #158 (writes under raw/
follow symlinks).

Refs #153

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:59:08 +02:00
co-authored by Claude Opus 5.5
parent 8a91feb02b
commit 87c80e4e82
7 changed files with 138 additions and 8 deletions
+24 -1
View File
@@ -6,6 +6,7 @@ import (
"io"
"os"
"strings"
"syscall"
)
// Text reads the file at path and returns its plain-text content.
@@ -56,7 +57,7 @@ func TextIn(r *os.Root, rel string) (string, error) {
if !fi.Mode().IsRegular() {
return "", fmt.Errorf("%s: not a regular file", rel)
}
f, err := r.Open(rel)
f, _, err := OpenRegular(r, rel)
if err != nil {
return "", fmt.Errorf("open %s: %w", rel, err)
}
@@ -87,3 +88,25 @@ func TextIn(r *os.Root, rel string) (string, error) {
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
}