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
+26 -4
View File
@@ -289,11 +289,15 @@ func TestProcessDir_SkipsTunnelCandidateFiles(t *testing.T) {
// files, and extract.Text followed them, so a symlink to any pod-readable
// file would be extracted into the brain. The watcher now ingests only
// regular files, read through an os.Root on raw/.
func TestProcessDir_DoesNotIngestSymlinks(t *testing.T) {
func TestProcessDir_IngestsTheRealNoteAndSkipsTheSymlinkBesideIt(t *testing.T) {
brainDir := setupBrainDir(t)
outside := filepath.Join(t.TempDir(), "secret.md")
require.NoError(t, os.WriteFile(outside, []byte("SENTINEL-153-WATCHER"), 0o644))
require.NoError(t, os.Symlink(outside, filepath.Join(brainDir, "raw", "leak.md")))
// "a-" sorts before "real": a walk that stopped at the first symlink
// (filepath.SkipAll — one synced link halting all ingestion) would fail
// the call count below (PR #156 review).
require.NoError(t, os.Symlink(outside, filepath.Join(brainDir, "raw", "a-leak.md")))
require.NoError(t, os.WriteFile(filepath.Join(brainDir, "raw", "real.md"), []byte("real note"), 0o644))
var sawSentinel bool
calls := 0
@@ -308,7 +312,25 @@ func TestProcessDir_DoesNotIngestSymlinks(t *testing.T) {
},
Schema: "# Schema",
}}
_ = processDir(context.Background(), cfg, time.Now().UTC().Format("2006-01-02"))
errs := processDir(context.Background(), cfg, time.Now().UTC().Format("2006-01-02"))
assert.Empty(t, errs)
assert.False(t, sawSentinel, "the symlink target must never reach the pipeline")
assert.Zero(t, calls, "a symlink is not a note")
assert.Equal(t, 1, calls, "exactly the real note is ingested; the symlink is not a note")
}
// A symlinked raw/ is refused outright: os.OpenRoot would otherwise follow
// it and confine reads to wherever it points.
func TestProcessDir_RefusesASymlinkedRawDir(t *testing.T) {
brainDir := t.TempDir()
elsewhere := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(elsewhere, "x.md"), []byte("SENTINEL-153-RAWLINK"), 0o644))
require.NoError(t, os.Symlink(elsewhere, filepath.Join(brainDir, "raw")))
calls := 0
cfg := Config{BrainDir: brainDir, Interval: time.Hour, Pipeline: pipeline.Config{
Complete: func(context.Context, string, string) (string, error) { calls++; return "[]", nil },
Schema: "# Schema",
}}
errs := processDir(context.Background(), cfg, time.Now().UTC().Format("2006-01-02"))
assert.NotEmpty(t, errs, "a symlinked raw/ must be reported, not silently ingested")
assert.Zero(t, calls)
}