fix(search): walk and read the brain through an os.Root (#157, slice 1)
Option 1 of #157, first slice. Search walked and read the brain by path, so a symlink planted in the Syncthing-shared brain dir put an outside file's contents in brain_query results, and a FIFO swapped into the wiki hung the query forever (reproduced: the FIFO race test blocked for the full 6s timeout before this change). New package internal/brainfs: Open (os.Root on the brain dir), ReadRegular (Lstat + non-blocking open + type check on the handle) and WalkRegular (walks the root's FS, yields regular files only, never follows a link). search.Query and hydrate use it. Tests (red first): a wiki symlink is not followed, a symlinked dir is not descended, hydrate refuses a symlink, and race tests swap a symlink or a FIFO into a wiki note while Query loops; any leaked byte or a hang fails. Refs #157 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// Package brainfs reads the brain directory through an os.Root (hyperguild#157).
|
||||
// The brain dir is a Syncthing sendreceive folder and Syncthing syncs
|
||||
// symlinks; a reader that walks and reads by path follows a planted symlink
|
||||
// out of the brain, and blocks forever on a planted FIFO. Readers that go
|
||||
// through a Brain only ever read regular files inside the brain: os.Root
|
||||
// refuses to leave the root even if an entry is swapped mid-read, and every
|
||||
// open is non-blocking with the type checked on the opened handle.
|
||||
package brainfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ErrNotRegular is returned for an entry that is not a regular file.
|
||||
var ErrNotRegular = errors.New("not a regular file")
|
||||
|
||||
// Brain is an open brain directory.
|
||||
type Brain struct {
|
||||
root *os.Root
|
||||
dir string
|
||||
}
|
||||
|
||||
// Open opens brainDir (which may itself be a mount symlink) as a root.
|
||||
func Open(brainDir string) (*Brain, error) {
|
||||
r, err := os.OpenRoot(brainDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open brain root: %w", err)
|
||||
}
|
||||
return &Brain{root: r, dir: brainDir}, nil
|
||||
}
|
||||
|
||||
// Close releases the root.
|
||||
func (b *Brain) Close() error { return b.root.Close() }
|
||||
|
||||
// Rel converts a path under the brain dir to a root-relative path.
|
||||
func (b *Brain) Rel(path string) (string, error) {
|
||||
rel, err := filepath.Rel(b.dir, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rel == ".." || len(rel) > 2 && rel[:3] == ".."+string(filepath.Separator) {
|
||||
return "", fmt.Errorf("%s is outside the brain dir", path)
|
||||
}
|
||||
return filepath.ToSlash(rel), nil
|
||||
}
|
||||
|
||||
// ReadRegular reads a brain-relative path, refusing anything that is not a
|
||||
// regular file, both before the open and on the opened handle.
|
||||
func (b *Brain) ReadRegular(rel string) ([]byte, error) {
|
||||
fi, err := b.root.Lstat(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
||||
}
|
||||
f, err := b.root.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
hfi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hfi.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular)
|
||||
}
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
// WalkRegular walks a brain-relative directory through the root and calls fn
|
||||
// for each regular file (directories are descended; symlinks, FIFOs and
|
||||
// devices are skipped, never followed). An entry error is passed to onErr and
|
||||
// the walk continues; a nil onErr ignores it.
|
||||
func (b *Brain) WalkRegular(relDir string, fn func(rel string) error, onErr func(rel string, err error)) error {
|
||||
return fs.WalkDir(b.root.FS(), filepath.ToSlash(relDir), func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if onErr != nil {
|
||||
onErr(p, err)
|
||||
}
|
||||
if d != nil && d.IsDir() {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() || !d.Type().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
return fn(p)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user