- brain_reject on a parked pdf/txt moves the bytes; it no longer prepends YAML - raw/ notes are read through a cap (readCapped); an endless reader test fails instead of exhausting memory - a byte-order mark no longer hides a note's wing: key - INGEST_WING_ROUTING is parsed strictly; a typo stops the server rather than leaving routing silently off - tests for the reserved-wing check in every disguise (Sources, sources., ...) Mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
85 lines
2.7 KiB
Go
85 lines
2.7 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/extract"
|
|
)
|
|
|
|
// errNotANote is returned for a raw/ entry that is not a regular file — in
|
|
// particular a symlink, whose target may be anywhere (hyperguild#153).
|
|
var errNotANote = errors.New("not a regular note file in raw/ (symlinks are refused)")
|
|
|
|
// errRawNotDir means brain/raw is not a real directory (e.g. a symlink), so
|
|
// containment inside it would mean nothing (hyperguild#153 item 4).
|
|
var errRawNotDir = errors.New("brain/raw is not a real directory")
|
|
|
|
// OpenRawRoot opens brain/raw as an os.Root, refusing a raw/ that is itself a
|
|
// symlink. Reads through the root cannot leave it, even if an entry is
|
|
// swapped for a symlink between a check and the read. It returns the root
|
|
// and raw/'s path. A missing raw/ returns an error wrapping os.ErrNotExist.
|
|
func OpenRawRoot(brainDir string) (*os.Root, string, error) {
|
|
p := filepath.Join(brainDir, "raw")
|
|
fi, err := os.Lstat(p)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if fi.Mode()&os.ModeSymlink != 0 || !fi.IsDir() {
|
|
return nil, "", errRawNotDir
|
|
}
|
|
r, err := os.OpenRoot(p)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("open raw root: %w", err)
|
|
}
|
|
return r, p, nil
|
|
}
|
|
|
|
// maxRawNoteBytes bounds what promote, reject and the pending listing read from
|
|
// one raw/ entry. A variable so a test can lower it.
|
|
var maxRawNoteBytes int64 = 64 << 20
|
|
|
|
// readRawNote reads one raw/ entry by basename through the root, refusing
|
|
// anything that is not a regular file.
|
|
//
|
|
// Accepted residual: a HARD link to an outside file is a regular file and is
|
|
// read. Syncthing syncs contents, not links, so a peer cannot create one;
|
|
// fs.protected_hardlinks=1 and the pod's secrets live on a different
|
|
// filesystem from the brain volume, so a local caller cannot link them in.
|
|
// Refusing Nlink > 1 is the next step if either assumption changes.
|
|
func readRawNote(r *os.Root, base string) ([]byte, os.FileInfo, error) {
|
|
fi, err := r.Lstat(base)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return nil, nil, errNotANote
|
|
}
|
|
f, _, err := extract.OpenRegular(r, base)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
b, err := readCapped(f, maxRawNoteBytes)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", base, err)
|
|
}
|
|
return b, fi, nil
|
|
}
|
|
|
|
// readCapped reads r but never more than max+1 bytes, so an oversize or growing
|
|
// file costs at most that much memory.
|
|
func readCapped(r io.Reader, max int64) ([]byte, error) {
|
|
b, err := io.ReadAll(io.LimitReader(r, max+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(b)) > max {
|
|
return nil, fmt.Errorf("note exceeds %d bytes", max)
|
|
}
|
|
return b, nil
|
|
}
|