froe is an independent Rust implementation of Apache Jackrabbit Oak's
segment-tar ("TarMK") storage format — the repository format of Apache
Jackrabbit Oak and Adobe Experience Manager. Its contract is severe:
anything froe writes must leave the repository in a state a real AEM
instance starts and runs against without any problem. Every rule in this
document exists to protect that contract.
The on-disk format is defined by what Oak's Java implementation writes and tolerates — not by documentation, not by intuition, and not by the Java source comments (which are wrong in places the code is not).
- Ground truth lives in
docs/analysis/: byte-exact, adversarially verified specifications extracted from the Java sources. Consult them before touching any parser or serializer. If a spec is silent on something you need, extend the spec first (citing the Java file and method), then implement. - Load-bearing quirks are reproduced deliberately — signed UUID index
ordering, validation constants copied across format versions, wrapping
32-bit arithmetic, masked shift distances. Never "fix" one without
proof from the Java sources that it is not observable behavior. The
distilled list is in
docs/storage-format.md. - Deliberate deviations are allowed only when they are strictly safer
than Java (for example: in-memory recovery instead of writing
.ro.bakfiles; returning errors on cyclic corrupt records where Java hangs). Every deviation must be documented at the deviation site and must never lose data Java would return, nor write bytes Java would not accept.
- Read-only means read-only. Read paths never take the repository
lock, never create files, never modify files — pointing
froeat a live repository must always be safe. - Write paths take the lock first. Every mutating operation acquires
repo.lockexclusively before touching anything, exactly like Oak, so a running AEM instance andfroecan never write concurrently. - Durability ordering is part of the format. Segment data is forced to disk before the journal line referencing it is appended. Never reorder writes for convenience or speed.
- Corrupt input returns errors. No input bytes — however hostile — may cause a panic, an unbounded allocation, unbounded recursion, or an infinite loop. Walks over record graphs carry cycle bounds; sizes from files are validated before allocation; arithmetic on file-supplied values is checked or deliberately wrapping (matching Java), never implicitly overflowing.
These paths are cumulative, and the strictest downstream effect controls the classification. For example, a read-only planner or preview that can authorize later deletion is high-risk even though the analysis itself does not write.
- Documentation, local refactors, and isolated read-only behavior follow the code standards, applicable test layers, and stable host gate below.
- Parsers and read-only format interpretation additionally cite the Oak evidence, use independent fixtures, and run the portability checks relevant to the changed representation.
- High-risk changes are storage serializers or other changes that
introduce, broaden, or reorder bytes published on disk, locking,
recovery, durability, destructive behavior, or reachability of
repository bytes. They require the safety case, fault matrix,
load-bearing guard tests, cross-target checks, and frozen independent
review in
docs/high-risk-changes.md.
A narrow fix that preserves an established high-risk design may link to its existing safety case and update only the affected invariant. Decide the path before coding; do not discover the proof obligations after the implementation is complete.
- Minimal dependencies. Everything in the tree must be strictly necessary and pull its weight. The crate manifests are the authoritative inventory; large format backends stay feature-gated. CRC32, Java string hashing, JSON encoding, and timestamp formatting are hand-implemented — a new dependency needs a reason those did not.
- User-facing documentation moves with the code. A change that adds,
removes, or alters a capability updates the capability inventory in
docs/oak-segment-tar-feature-map.md, the affected command's guide, and any remediation text, in the same commit. A guide that claims a capability is unimplemented after it ships, or omits a side effect the code now has — a one-way format upgrade, a file an interrupted run leaves behind — reads as correct and costs a review cycle to rediscover. - Idiomatic Rust, not transliterated Java. The format semantics are
exact; the implementation is not a line-by-line translation. Errors
are
Results, not exceptions; ownership replaces defensive copying; iterators replace index loops where clearer. - Rust edition 2024, workspace lints as configured in the root
Cargo.toml:missing_docs,unreachable_pub, and clippy'spedanticgroup must all be clean.
The rest of this section is craft guidance. It is a set of heuristics with a stated purpose, not a checklist to satisfy mechanically: a reviewer may ask why a rule was not followed, and "following it here made the code harder to read" is a complete answer. Where a heuristic collides with Rust idiom or with format fidelity, the idiom and the format win.
- No abbreviations in file, method, or variable names:
most_significant_bits, notmsb;identifier, notid;record_number, notrec_no. Universally standard acronyms (UUID, CRC32, JCR, TAR, JSON, CLI) are acceptable; ad-hoc shortenings are not. The cargo-mandatedsrc/directory is the tolerated exception. - Names reveal intent, not mechanism or type.
reclaimed_bytesbeatscount;retained_generationsbeatsfiltered. A name that needs the line after it to be understood is the wrong name. Loop and closure bindings are held to the same standard as fields — a three-line closure does not earnx. - Types are nouns, functions are verbs. Predicates read as
questions:
is_reachable,has_binary_references,can_reclaim. Conversions follow the Rust API guidelines —as_for a borrowed view,to_for a copy,into_for a consuming conversion — because those prefixes carry cost information a reader relies on. - The same concept keeps the same word everywhere. The format already has enough near-synonyms (segment, record, entry, blob); a port that renames a concept per module makes every cross-module read a translation exercise. When Oak's name is the clearest one, use Oak's name so the analysis documents and the code share a vocabulary.
- One thing, at one level of abstraction. A function that both decides what to do and performs the byte-level how forces a reader to hold two altitudes at once. Split at that seam. The reliable smells are a comment introducing a block, a block whose locals are used nowhere else, and a name containing "and".
- A hundred lines is the limit, and there is no escape hatch.
clippy's
too_many_linesis the enforcement, and#[allow(clippy::too_many_lines)]is forbidden anywhere in the tree — production, fixtures, and tests alike. The lint counts physical lines between a function's braces, ignoring blank and comment-only lines, so the budget is real code. A function that needs more than that is telling you it holds more than one idea. - Twenty decision points, and seven levels of nesting.
cognitive_complexityandexcessive_nestinginclippy.tomlhold the two, and the comments there say what each one does and does not measure — the first counts branches without weighting them by depth, which is why the second exists. Production sits at 16 and 7 today, so both are ceilings rather than a cleanup project. - Split at the seams the function already has. A long body almost always has phases separated by blank lines and a comment introducing each; those comments are the names of the functions hiding inside it. Extract them so the caller reads as the sequence it always was, with each step's what in its name and its why in its own doc comment. Where a linear order is load-bearing — a crash-safe mutation sequence, a durability protocol — the extracted steps stay in the same order in the same caller, so the ordering is still read top to bottom in one place. What must not survive is one screen's worth of code that a reader has to re-derive that structure from every time.
- Few parameters, and none of them bare
bool. Past roughly three, group them into a struct — a caller passing five positional arguments cannot be reviewed for argument order. A boolean at a call site is unreadable (compact(store, true, false)); use a two-variant enum whose names say what each choice means. This matters most on the write path, where a transposed flag is a data-loss bug. - No side effects the name does not advertise. A function named for
a question does not mutate; a function named for a query does not
write files, take the lock, or advance a generation. Where an
operation must both compute and persist, the name says so
(
write_and_flush, notprepare). Read-only means read-only is the same rule enforced at function granularity. - Typed errors, never sentinel values. Failure is a
Resultwith a variant a caller can match on.Optionmeans absence, never failure;-1,0, and empty collections never stand in for an error. A variant carries the values needed to report the fault — the offending offset, the expected and actual checksum — because a message the caller must reconstruct gets reconstructed wrongly.
- Documented, concisely. Every module starts with a doc comment explaining what it models and the non-obvious facts of the format it implements. Public items carry doc comments. Safety comments describe the exact mechanism and scope proved by the code, not a stronger idealized state.
- Comments explain why; the code explains what. A comment restating the next line is noise that goes stale. In this crate the why is usually unavailable from the code at any quality — that Oak writes a signed comparison here, that a constant was copied unchanged into a later format version, that an arithmetic step wraps deliberately. Those comments are mandatory and cite the Java.
- A comment compensating for a name is a rename. If a line needs a gloss to say what a variable holds, the variable is misnamed. Fix the name and delete the comment.
- No commented-out code. Version control keeps it. A disabled test
is either deleted or marked
#[ignore]with a reason and a linked issue.
- Related things stay close. A helper sits directly below its only
caller; a type's inherent
implsits next to the type. Vertical distance implies unrelatedness, so distance between two things that must change together is a defect. A module that has grown past the point where related things can stay close is a module to split. - A thousand lines is the limit for a file.
scripts/oversized-files.shenforces it and CI runs it, because clippy has no lint for this —too_many_linesmeasures a function's body, not a module. Split at the seams the file already has, and move each test to the module it now belongs with. A directory module (foo/mod.rsplus submodules) is the usual shape; an inherentimplmay be divided across them, so a large type does not force a large file. - One reason to change per module and per type. A type that models a format structure does not also own I/O scheduling or progress reporting.
- Ask which way the dependency points before moving a module. A name
like
maintenance_fault_injectionlooks like it wants to live undermaintenance/, but its cutpoints are called from the tar writer and the store writer too — modulesmaintenanceis built on. Moving it in would have made them reach into a private module above them.journal_maintenancewas the opposite: every caller was already insidemaintenance/, so moving it in only removed reach nobody was using. - DRY, with two deliberate exceptions. Duplication is the default
maintenance hazard, and shared logic belongs in one place. But the
independent encoder in
crates/froe/tests/support/duplicates production encoding on purpose — sharing code with production would destroy the property that makes it evidence — and validation constants repeated across format versions are duplicated because Oak duplicates them. Both are documented at the site; neither is ever "cleaned up". - Tell, don't ask. Behavior lives with the data it operates on. A caller that reaches through two accessors to compute something the owner could answer wants a method on the owner instead.
- Abstract on the second implementation, not the first. Traits and generics earn their place when there is a real second implementer or a test needs a seam. A trait with one implementation, a builder for a struct with two fields, or a configuration knob no caller sets is speculative generality — the same instinct minimal dependencies exists to resist. YAGNI applies to internal structure, not to format coverage: the format's own complexity is not optional.
Tests are the port's proof of fidelity. Every change keeps all applicable layers green. New functionality explains why any layer does not apply:
- Unit tests with hand-crafted bytes. Every parser and serializer is exercised against fixtures written out by hand from the specifications — including corrupt, truncated, hostile, and boundary-value inputs.
- Independent-encoder round-trips. Integration tests build
repositories through
crates/froe/tests/support/— a separate encoder that shares no production encoding or parsing code with the reader or writer, its single production import beingchecksum::crc32, which known-answer vectors pin independently — so a self-consistent bug in one cannot hide in the other. Writer changes additionally round-trip through the reader: everything written must be read back identically. - End-to-end store tests. Mutating operations run against complete synthetic repositories on disk and assert the full post-state: archives reopen with valid indexes, the journal resolves, content is intact, and read-only invariants (no lock, no writes) hold where promised.
- Oak/AEM interoperability for substantial writer changes. Before a new format-writing or maintenance feature is called production-ready, open its output with a real compatible Oak/AEM version and exercise froe against Oak-produced input. If that environment is unavailable, record the gap explicitly and keep the feature labelled beta; a froe-to-froe round trip is not a substitute. Record the exact Oak/AEM build, producer-to-consumer direction, operation exercised, and verified post-state.
The standards above apply to test code without discount — a test suite this large is read far more often than it is written, and a test nobody can read is a test nobody dares change when the format demands it.
- One concept per test, named for the concept. The name states the
property being pinned, so a failure line alone tells a reader what
broke:
rejects_index_entry_past_archive_end, nottest_index_3. Several assertions that together pin one property are fine; two unrelated properties in one test are two tests, because the first failure hides the second. - Independent and repeatable. Tests share no mutable state, run in any order, and depend on no wall-clock time, no ambient environment, and no leftover directory from a previous run. Anything written goes to a temporary directory the test owns and removes.
- Fixture-building belongs in helpers; the property belongs in the test. A reader should see the arrangement summarized and the assertion in full, not thirty lines of byte-array setup obscuring one comparison. Helpers are named for what they produce.
- Assert the observable post-state, not the implementation path. A test that pins internal call order breaks on every refactor and proves nothing about the format.
High-risk tests exercise every materially distinct production phase or
caller; a helper test alone is not evidence that the protection is wired
into production. The authoritative guard-neutralization, fault-harness,
environment-wiring, scale, and mutation-test requirements are in
docs/high-risk-changes.md.
The same proof principle applies at every tier to newly introduced or semantically changed refusal and resource-limit guards: add a named test that reaches the production-facing entry, fails when only that guard is neutralized, and pins the exact boundary or typed error. Helper tests may supplement that wiring proof. A declared work or memory budget documents its accounting unit, charges every operation or allocation in that unit, and has an exact limit/limit-plus-one regression.
Public configuration and diagnostic types must be usable as documented from a downstream crate. If a type is deliberately non-exhaustive, provide constructors or builders for every supported configuration; if an error is internal, keep it crate-private or document the public path that can produce it. Add an external integration test whenever Rust visibility or type construction is part of the contract.
The executable CI matrix in .github/workflows/ci.yml
is authoritative for automated platform coverage. Before requesting
review, every code change runs the stable host gate:
$ cargo +stable fmt --all -- --check
$ cargo +stable test --workspace --all-features --no-fail-fast
$ cargo +stable test --workspace --all-features --release --no-fail-fast
$ cargo +stable clippy --workspace --all-targets --all-features -- -D warnings
$ RUSTDOCFLAGS="-D warnings" cargo +stable doc --workspace --all-features --no-depsDocumentation-only changes with no generated or code-facing effect may
run only the relevant documentation, formatting, and diff checks; record
the omitted gates. Before final review, the relevant CI matrix must also
be green. If no CI run is available, local MSRV host tests may support
preliminary review, but they do not replace native macOS or Windows jobs;
record those platform axes as unexecuted. High-risk and
portability-sensitive changes also run the MSRV, cross-target, and
integer-width checks in
docs/high-risk-changes.md. Code behind
cfg(test) is part of the target's compilation surface. Record whether
each platform was executed or only cross-compiled, and list relevant
environments not exercised.
Diff checks cover only tracked paths. Before checking an uncommitted
change, ensure git status --short contains no intended ?? path (stage
new files or add them with intent-to-add), then run git diff --check HEAD. For committed work, check the complete review range with git diff --check BASE..HEAD.
- Development happens on
develop, the default branch. Releases are cut and tagged there, followingdocs/releasing.md.maintracks the latest released commit: the release workflow fast-forwards it after the crates reach the registry, so it is a pipeline-maintained pointer rather than a branch anyone commits to. Open changes againstdevelop. - Commits follow the conventional commit prefixes
cliff.tomlgroups into release notes —feat:,fix:,harden:,perf:,refactor:,docs:,test:,chore:— with bodies that explain why, in full sentences. - A change to format-facing code cites the specification section (or the Java file and method) that justifies it, in the commit body or the code.
- Leave code cleaner than you found it — in its own commit. Renaming
a confusing local, deleting a stale comment, or splitting a function
that has outgrown its name is welcome as you pass through. Keep those
edits in a
refactor:commit separate from the behavior change, because a format-facing diff that also carries unrelated cleanups costs a reviewer the ability to see what actually changed on disk — and on a high-risk path, that reviewer is the last line of defense. A cleanup commit asserts that no byte written to disk changed.
High-risk final review uses a committed, frozen BASE..HEAD range and a
reviewer that did not author the patch — normally an adversarial automated
pass, since this project is single-maintainer. Later edits invalidate the
affected review and gates. The authoritative review procedure, evidence
requirements, what an automated pass can and cannot stand in for, and the
follow-up-commit policy are in
docs/high-risk-changes.md.
What a reviewer owes in return — that a finding asserting Oak behavior
cites the Java rather than an analysis document, that a claim about the
language or a public API quotes the definition, that validating a
committed range isolates it, and how severity and adversarial
verification are calibrated — is in
docs/reviewing.md. A false finding costs the
author a cycle just as a missed defect costs a release.
Apache-2.0. By contributing you agree to license your work under the
same terms. Apache Jackrabbit Oak is a trademark of the Apache Software
Foundation; froe is an independent implementation, not an Apache
project.