Wave 1 task core-hashing-keys: the crate's declared one-way door,
implemented byte-exact to ADR-002.
- src/hashing.rs: Algorithm (exactly GitBlobSha256 canonical /
GitBlobSha1 tolerated; BLAKE3 absent), Digest (known-length
derivation H("blob <len>\0" + content) as primary constructor;
from_git_oid_hex interop constructor, lowercase-only acceptance),
and the hashing::registry constants (0x01/0x02, 0x00 reserved; new
algorithm = new ADR)
- src/keys.rs: typed Key wrapper; as_bytes/from_bytes are the only
backend-boundary conversions; 1 algorithm byte + digest bytes
(33/21 total); rejects-never-guesses validation -> Error::KeyInvalid
- tests/git_interop.rs: the load-bearing test — byte-exact derivation
against the `git hash-object` CLI, both algorithms, small and
75-KiB content; external-oid round-trip into pool keys; both oid
formats coexisting in one flat pool
- named design point (oid hex policy): lowercase-only acceptance,
recorded in the task Notes and on from_git_oid_hex
Verified: cargo test (15 passed), cargo clippy --all-targets -- -D
warnings, cargo fmt --check, cargo test --all-features — all green.
145 lines
7.4 KiB
Markdown
145 lines
7.4 KiB
Markdown
---
|
||
id: core-hashing-keys
|
||
name: Hashing & keys — the canonical derivation and the one-way-door encoding
|
||
status: completed
|
||
depends_on: [core-error-model]
|
||
scope: narrow
|
||
risk: medium
|
||
impact: project
|
||
level: implementation
|
||
tags: [wave-1, keys]
|
||
---
|
||
|
||
## Description
|
||
|
||
Implement the content-address layer (`src/keys.rs` or `keys/` +
|
||
`hashing`, one module per file per convention) per ADR-002 and
|
||
hashing-and-keys.md. This is the crate's declared one-way door — key
|
||
bytes persist in backend rows and filenames with no migration story —
|
||
so the encoding is implemented exactly to the ADR text and reviewed
|
||
line-by-line at the wave gate.
|
||
|
||
- **`Digest`** — algorithm + digest pair. Algorithm enum with exactly
|
||
two members: `GitBlobSha256` (canonical), `GitBlobSha1` (tolerated
|
||
legacy). BLAKE3 is absent from the crate entirely (ADR-002 §Decision
|
||
class 3).
|
||
- **Canonical derivation**: `H("blob <len>\0" + content)` — SHA-256
|
||
canonical, SHA-1 same derivation. The preamble is the algorithm's
|
||
domain separation, not a special case (ADR-002 §The preamble as
|
||
abstraction). Known-length derivation is the primary constructor;
|
||
the unknown-length path's derivation-over-staged-bytes restart is
|
||
wave 2 facade logic, not here.
|
||
- **Interop constructor**: `from_git_oid_hex` shape (POC #1 finding 4)
|
||
— a pre-computed pair from an external git oid, both algorithms.
|
||
- **`Key`** — the typed wrapper; `as_bytes()` / `from_bytes()` are the
|
||
only conversions across the backend boundary. Physical encoding per
|
||
ADR-002: `1 algorithm byte + digest bytes` — `0x01` =
|
||
git-blob-sha-256 (32-byte digest; total 33), `0x02` = git-blob-sha-1
|
||
(20-byte digest; total 21). `0x00` is reserved and never assigned.
|
||
- **Validation**: truncated keys and unknown algorithm bytes are
|
||
rejects (`Error::KeyInvalid`), never guesses (the POC's
|
||
length-checking test is the standard).
|
||
- **Sort stability**: fixed-length enum-tagged keys sort in total
|
||
order (required for the sweep's live-set diffs) — asserted by test.
|
||
- **Registry constants**: the algorithm bytes are the ADR-002
|
||
registry; new algorithms require a new ADR (doc comment states
|
||
this).
|
||
- **Interop test (the load-bearing test)**: byte-exact derivation
|
||
against the `git hash-object` CLI for both algorithms (POC #1
|
||
findings 4/5's standard — validated against git, not just hardcoded
|
||
vectors), plus the external-oid round-trip: `from_git_oid_hex` →
|
||
`as_bytes` → `from_bytes` → equal, and tagged-key coexistence of
|
||
both algorithms in one flat collection.
|
||
- **Named design point for the review gate**: oid hex parsing policy —
|
||
git oids are lowercase hex; pin lowercase-only acceptance (reject
|
||
uppercase as malformed) or case-insensitive acceptance, with the
|
||
choice recorded. Lean lowercase-only (git's own output is always
|
||
lowercase; strictness at the boundary matches rejects-never-guesses)
|
||
unless implementation finds a concrete interop reason otherwise.
|
||
- Deps: `sha2`, `sha1` (RustCrypto), `hex` — added to the manifest by
|
||
this task.
|
||
|
||
## Acceptance Criteria
|
||
|
||
- [x] `Digest`/`Key`/algorithm enum per ADR-002; encoding byte-exact
|
||
(33/21 total lengths; 0x01/0x02 tags; 0x00 rejected)
|
||
- [x] Canonical derivation matches `git hash-object` byte-exact for
|
||
both algorithms (CLI-driven test, both a small and a multi-KiB
|
||
content case)
|
||
- [x] `from_git_oid_hex` round-trips external oids into pool keys
|
||
- [x] Rejects: truncated keys, unknown algorithm bytes (incl. 0x00,
|
||
0xFF), wrong digest lengths → `Error::KeyInvalid`, never guesses
|
||
- [x] Total-order sort stability asserted (mixed-algorithm collection
|
||
sorts deterministically)
|
||
- [x] Doc comments carry the one-way-door warning and the registry
|
||
note (new algorithm = new ADR)
|
||
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||
`cargo fmt --check` green
|
||
|
||
## References
|
||
|
||
- docs/architecture/hashing-and-keys.md
|
||
- docs/architecture/decisions/002-canonical-hash-and-key-encoding.md
|
||
- docs/research/poc-trait-dispatch-findings.md findings 4/5
|
||
- AGENTS.md convention 5 (the one-way door)
|
||
|
||
## Notes
|
||
|
||
- Module layout: two files, one module per file — `src/hashing.rs`
|
||
(the hash side: `Algorithm` + `Digest` + the derivation + the
|
||
interop constructor) and `src/keys.rs` (the key side: the typed
|
||
`Key` wrapper + the physical encoding). The scaffold's stub doc on
|
||
`hashing.rs` was replaced by the full module doc. Both re-exported
|
||
from `lib.rs` (`Algorithm`, `Digest`, `Key`, mirroring the
|
||
`Error` re-export).
|
||
- **Named design point resolved — oid hex parsing policy:
|
||
lowercase-only acceptance.** `git hash-object`'s own output is
|
||
always lowercase; strictness at the boundary is the
|
||
rejects-never-guesses posture. Uppercase oids are rejected as
|
||
malformed (`Error::KeyInvalid`) — the check is a byte scan for
|
||
`[0-9a-f]` before decode (the `hex` crate's `decode` alone would
|
||
accept uppercase). Policy stated verbatim on
|
||
`Digest::from_git_oid_hex` and covered by test. If an external
|
||
interop consumer ever needs uppercase acceptance, that is a change
|
||
at this boundary with its own test.
|
||
- `Algorithm` is declared `GitBlobSha256` before `GitBlobSha1` so
|
||
derived `Ord` matches registry-byte order; the sort-stability test
|
||
asserts derived order == `as_bytes()` byte order. `Digest` keeps
|
||
its fields private behind `from_parts` (pub(crate)), so every
|
||
digest's length stays pinned by its algorithm.
|
||
- Registry constants live in `hashing::registry` (`GIT_BLOB_SHA256`
|
||
0x01, `GIT_BLOB_SHA1` 0x02, `RESERVED` 0x00) with the new-ADR doc
|
||
note. `Algorithm::try_from_byte` is public; `Key::from_bytes` is
|
||
the boundary consumer.
|
||
- Deps pinned as the task scoped: `sha2 0.10`, `sha1 0.10`, `hex
|
||
0.4` (RustCrypto + hex). The generic derivation helper is bound
|
||
over the RustCrypto digest trait so the derivation body has zero
|
||
per-algorithm special-casing; the match arms pick only the hasher.
|
||
- No `unwrap`/`expect`/panic outside tests; library paths are
|
||
`Result`/`Option` only (error.rs's `Error::KeyInvalid` carries the
|
||
boundary rejections).
|
||
|
||
## Summary
|
||
|
||
`src/hashing.rs` + `src/keys.rs` implement the content-address layer
|
||
per ADR-002, the crate's declared one-way door. `Algorithm` (exactly
|
||
`GitBlobSha256` canonical / `GitBlobSha1` tolerated; BLAKE3 absent),
|
||
`Digest` (known-length derivation `H("blob <len>\0" + content)` as
|
||
primary constructor; `from_git_oid_hex` interop constructor with
|
||
lowercase-only acceptance, both oid formats selected by length), and
|
||
`Key` (typed wrapper; `as_bytes`/`from_bytes` the only backend-boundary
|
||
conversions; `1 algorithm byte + digest bytes` — 0x01+32, 0x02+20,
|
||
33/21 total) with rejects-never-guesses validation (`Error::KeyInvalid`
|
||
for truncated/unknown-tag/wrong-length/reserved-0x00; never a guess).
|
||
Registry constants in `hashing::registry` with the new-ADR doc note;
|
||
one-way-door warnings in both module docs. Tests: byte-exact
|
||
derivation against the live `git hash-object` CLI (the load-bearing
|
||
test — both algorithms × empty/short/binary-NUL/75-KiB, sha256 repos
|
||
via `git init --object-format=sha256` per POC finding 4's harness
|
||
gotcha), external-oid round-trip into pool keys, both-oid-formats
|
||
coexistence in one flat pool, hardcoded git-computed vectors, encoding
|
||
byte-exactness, full rejection matrix, and derived-order == byte-order
|
||
sort stability on mixed-algorithm collections. Verified: `cargo test`
|
||
(15 passed), `cargo clippy --all-targets -- -D warnings`,
|
||
`cargo fmt --check`, and `cargo test --all-features` all green. Deps
|
||
added: `sha1`, `sha2`, `hex`. |