fix(fsck): reject ambiguous arguments and separate failure classes - #13
Merged
farhan-syah merged 3 commits intoJul 26, 2026
Conversation
Split pagedb-fsck.rs into a directory module (cli/run/main) so grammar parsing, execution, and exit-code policy are separately testable. Adds --page-size for stores created at a non-default page size and a --help/-h flag, and gives usage errors, an unreadable store, and an actual integrity failure distinct exit codes (2, 3, 1) instead of one generic non-zero.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(fsck): reject ambiguous arguments and separate failure classes
Summary
pagedb-fsckscannedstd::env::args()permissively: the last--realmwon,a
--realmwith no value fell back to the default realm, unknown--optionsbecame the KEK, and extra positional tokens were ignored. An operator who
mistyped a command got a filesystem diagnosis for a problem that did not exist,
or — worse — a clean report for the wrong realm.
This PR resolves the whole command line into a validated value before any key
decoding, VFS construction, or database open, and makes each failure class the
tool can encounter distinguishable from the others.
fsckis a diagnostic trust boundary. Its output is only useful if "I typedthe command wrong", "the store will not open", and "the store is damaged" are
three different answers.
The grammar
Every previously valid invocation still parses, in any argument order. What is
now rejected:
through to the positional slot and resurfaced as
invalid hex KEKforsomething the operator never meant as a key;
--help/-hprints the grammar on stdout and exits 0. It is answered fromany position and before the path check, because a tool that has just tightened
its grammar has to be able to state that grammar — and a missing path is
exactly when you would ask.
Failures name their specific condition through a typed
CliError, not astring, so the parser's tests assert on the condition rather than on wording.
Exit codes
Previously everything that was not a usage error exited 1: an absent store, a
wrong key, and an actually damaged database were indistinguishable to any
caller that was not a human reading stdout.
--deep, the report was cleanThis also fixes
let _ = report.write_text(...): a verdict that could not bedelivered was exiting 0, reporting "clean" to a caller that never saw why.
This is a compatibility break for scripts that treat every non-zero exit as
one class. Those scripts could not have acted on the old code differently
anyway, which is the point.
--page-sizeWhile reworking the argument path:
Db::open_read_onlywas being called with ahardcoded
4096. Header B lives at byte offsetpage_size, so fsck couldnot open any store not created at 4096 bytes — it read the wrong bytes and
blamed the header. pagedb supports 4096 through 65536 (
tests/page_size_range.rs).--page-sizemakes those stores inspectable. A failed open now also names thepage-size and realm assumptions it used, rather than leaving an operator
hunting for corruption that is not there.
This is wider than argument validation, and deliberately so: it is a store the
checker simply could not read.
Read-only contract
The byte-preservation test runs the real binary as a separate process rather
than inferring read-only behaviour from which constructor appears in source:
build a store with one committed key and one sealed, catalog-linked segment;
snapshot every authoritative
main.dbandseg/**byte; runpagedb-fsck --deep --realm <zero> <kek>; require success andresult: CLEAN;compare every path and byte.
It now also asserts the snapshot actually contains a segment entry. Without
that, a future layout change would silently reduce the comparison to
main.dbalone and the segment half of the claim would pass while covering nothing.
Layout
src/bin/pagedb-fsck.rsbecomes a directory module, since the grammar now hasreal types:
main.rs— entry point and the wasm32 stub, nothing elsecli.rs— grammar,CliError,parse, and its unit testsrun.rs— exit-code policy, key and realm decoding, open, deep walkThe binary also gains
#![warn(clippy::all, clippy::pedantic)]. It had no lintattributes before, because a binary does not inherit them from
lib.rs.ExitCodeis now behind the nativecfg, removing an unused-import warningfrom wasm32 builds.
Tests
14 parser unit tests beside the code, and 7 integration tests driving the built
binary. The unit tests cover the direction a stricter parser actually
endangers — that valid commands still parse.
--deep --realm X KEK,KEK --realm X --deep, and two further orderings must all yield the identicalCliArgs.Integration coverage: byte preservation,
PAGEDB_KEKfallback, a shallow open,a non-default page size with and without the override, the rejection table,
--helpon both spellings, and the exit-code taxonomy (usage vs. absent storevs. wrong key vs. a store whose live segment has been damaged).
Each fix was confirmed to fail without itself, not merely observed green:
is_optionrestricted back to----helphandling removedEXIT_OPERATIONALcollapsed back to 1Verification
Run against this branch with
mainmerged in:Scope
No change to page layout, AAD, encryption, allocation, free-list format,
recovery, the public library API, or any dependency.
Db::open_read_only, theretention policy, the VFS, and the deep-walk implementation are untouched — the
only call-site change is passing the requested page size instead of a constant.
The README documents the grammar, the defaults,
--page-size, and the exitcodes.