Makes a real repository usable as a root, and closes two confinement bugs
present in 0.3.0. Upgrading is recommended for anyone serving a root that
contains symlinks or relies on --exclude.
Security
- Ancestor symlinks were followed.
O_NOFOLLOWguards only the name it
opens, so an intermediate link was resolved by the kernel before the flag
applied:read_text_file {"path":"link/creds.txt"}returned bytes from
outside the root. Every path component is now checked before the open, and
list_directoryvalidates its prefix the same way. - An exclude did not cover what was under it.
--exclude secretpruned the
walk at that directory but never matchedsecret/creds.txt, so naming the
path directly still served it. Excludes now cover the whole ancestor chain,
and the refusal names the exclude rule that refused. - Host paths could leak in errors. A path component long enough to fail
lstatproduced a Node message carrying the root's absolute location. All
such failures now read as "not served". - Built-in excludes are matched case-insensitively, so
NODE_MODULES/pkg.js
cannot alias past the excluded spelling on a case-insensitive filesystem. An
explicit--excludestays case-sensitive.
Added
- Built-in excludes. Dependency and tool output (
node_modules,_build,
.git,.venv,.next,__pycache__, and similar) plus credential
filenames (.env,.env.*,*.pem,*.p12,*.pfx, SSH private keys) are
excluded by default.--no-default-excludedrops the list.build,dist,
target,coverage, and*.keyare deliberately not excluded — each is
an ordinary word or the Keynote extension, and hiding real data silently is
the worse failure. - Multi-term search.
search_filesandsearch_textacceptany_of(up to
16 literal substrings) instead ofquery, andcase_insensitiveto fold
ASCII letters. A cursor is bound to the terms and folding it was issued for. start_lineonread_text_file. Page from a 1-based line, so reaching
row 4,000 of a CSV costs one page instead of pushing 3,999 rows through the
result budget first.byte_offsetstays absolute, so the read is still
citable. Only the bytes actually returned have to decode.- Traversal ceilings on the CLI:
--max-files,--max-directories,
--max-depth,--max-entries, and--max-scan-bytes(minimum 16384).
Changed
list_directoryno longer costs an inventory. It probes each child and
stops at the first served file, preserving the rule that a directory is
listed exactly when it holds something served.- Walk ceilings raised to admit a real repository (50000 files, 50000
directories, depth 64, 1000000 entries). search_textskips binary files. A file is skipped whole when one line in
its opening 8 KiB both contains a NUL and fails to decode as UTF-8 — both
signals, on the same line. On one 374 MB checkout this skipped 146 MB of
compiled artifacts whose every line would have been dropped anyway. Listings
stay content-blind, andread_text_filestill refuses the file by name.
Caveat: a file that begins with a binary header but holds real text further
in is skipped whole.- A bare
.names the root, as the empty string does../libandlib/.
still carry a dot segment and are still rejected. inventoryanddirectoryListingare no longer exported; a test pins the
export list.- Development moved to pnpm. This does not affect consumers.
Upgrading
The built-in exclude list can hide paths a working --include used to serve.
When a literal --include is covered by a built-in exclude, startup says so on
stderr and names the flag that turns the list off. Run with
--no-default-exclude to restore 0.3.x behavior exactly.