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>
99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
// 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)
|
|
})
|
|
}
|