refactor(uffs-mft): Frs / ParentFrs newtypes — parse layer (Phase 4 sub-phase 5d.1, refs #191)#259
Merged
Conversation
…(Phase 4 sub-phase 5d.1)
First slice of the FRS migration along the playbook's data-flow split
(5d.1 parse → 5d.2 index → 5d.3 query → 5d.4 wire). Introduces a
typed parent-vs-child distinction so the historic `(parent_frs: u64,
own_frs: u64)` swap hazard becomes a compile error, and the documented
sentinels (`Frs::ZERO`, `Frs::ROOT`, `ParentFrs::ZERO`,
`ParentFrs::ROOT`) replace the open-coded `0` / `5` magic numbers at
construction sites.
## Newtypes
* `uffs_mft::frs::Frs(u64)` — typed FRS value with `ZERO` (the `$MFT`
self-record / "null reference") and `ROOT` (FRS 5 — `.` root) plus
`is_zero` / `is_root` predicates and the standard `new` / `raw` /
`Display` surface. Derived `Copy + Eq + Ord + Hash + Default`.
* `uffs_mft::frs::ParentFrs(Frs)` — composed-over-`Frs` newtype for
parent-directory references. Provides `ParentFrs::of(Frs)` and
`ParentFrs::as_frs()` so the parent-vs-child boundary becomes
explicit at every promotion / demotion site.
Both newtypes are `#[repr(transparent)]` so they preserve the raw
`u64` bit pattern byte-identically for wire-format round-trip.
## Migrated parse-layer fields
* `ntfs::NameInfo.parent_frs: u64` → `ParentFrs`
* `ntfs::NameInfo.source_frs: u64` → `Frs`
* `parse::ParsedRecord.frs / parent_frs / base_frs` → `Frs` /
`ParentFrs` / `Frs`
* `parse::ExtensionAttributes.base_frs / extension_frs` → `Frs` / `Frs`
* `parse::name_tracker::PrimaryNameTracker.parent_frs: u64` →
`ParentFrs` (mirrors the migrated `NameInfo`).
## Construction sites (parse internals)
`parse/attribute_helpers.rs::parse_file_name_full`,
`parse/full.rs::parse_record_full`,
`parse/placeholders.rs::create_placeholder_record`,
`parse/forensic.rs` (corrupt-record path) and
`parse/forensic/{base,extension}.rs` now lift kernel-decoded `u64`
values into `Frs::new(...)` / `ParentFrs::new(...)` at the on-disk →
typed boundary, with citation comments at each lift point. The
`parse_record* (data, frs: u64)` public input signatures stay `u64`
because FRS values are derived arithmetically from chunk offsets in
the read loop; the typed surface is the *output* contract.
`parse/merger.rs` calls `.raw()` at the four `frs_to_usize` /
`TryFrom<u64>` index-arithmetic boundaries.
## Boundary down-casts at consumers (5d.2 will erase these)
* `parse/columns.rs::push_record{,_expanded}` — `ParsedColumns`
Polars-bound `Vec<u64>` columns downcast at the push site.
* `reader/dataframe_build.rs` (4 sites) — same Polars-bound `Vec<u64>`
pattern.
* `index/builder.rs::from_parsed_records` — single typed→raw boundary
at loop entry (`parsed_frs`, `parsed_parent_frs`, `parsed_base_frs`
locals) with a doc-comment citing the 5d.2 follow-up; per-name
`.raw()` at the inner parent-walker site.
* `uffs-diag/src/bin/dump_mft_records.rs` — wraps the CLI-parsed `u64`
argument with `Frs::new(...)` at the merged-record lookup.
## Wire format unchanged
Pinned by:
* `frs::tests::frs_raw_roundtrip_preserves_u64_exactly` /
`parent_frs_raw_roundtrip_preserves_u64_exactly` — any `u64`
survives `Frs::new` → `.raw()` byte-identically (covers `0`, `1`,
`5`, `42`, `1 << 47`, `u64::MAX`).
* `frs::tests::frs_sentinels_match_literal_values` /
`parent_frs_sentinels_match_literal_values` — `ZERO` / `ROOT` match
the documented `0` / `5` literals exactly.
* `frs::tests::parent_frs_of_and_as_frs_roundtrip` — promotion /
demotion across the `Frs` ↔ `ParentFrs` boundary is bit-identical.
* Updated `parse::tests::*` assertions — every existing parse
regression test now compares against `Frs::new(...)` /
`Frs::ROOT` / `ParentFrs::ROOT` etc., preserving the prior
numeric expectations.
## Verification
* `cargo check --workspace --all-targets` ✅
* `cargo xwin check --target x86_64-pc-windows-msvc --workspace
--all-targets` ✅
* `cargo test --workspace --lib --bins` — 180 uffs-mft lib tests pass
(was 169; +11 new `Frs` / `ParentFrs` regression tests).
* `just lint-pre-push` — all 23 gates green (fmt, file-size, drift,
vet, machete, typos, reuse, cargo-check, lint-ci, lint-prod,
lint-tests, rustdoc, doc-tests, tests, smoke, deny,
lint-ci-windows).
## Out of scope (follow-up sub-phases)
* `index::types::FileRecord.{frs, base_frs}`,
`index::types::FILE_NAME.parent_frs`,
`index::model::ChildEntry.child_frs`, `index::*` lookup APIs
(`find`, `get_or_create`, `frs_to_idx_opt`, ...) — **5d.2**.
* Query / search surfaces (`uffs-core::index_search::result.frs`,
...) — **5d.3**.
* USN / wire DTOs (`uffs_mft::usn::UsnRecord.{frs, parent_frs}`,
`uffs_mft::usn::FileChange.{frs, parent_frs}`, broker-protocol
result rows) — **5d.4**.
Refs #191
githubrobbi
added a commit
that referenced
this pull request
May 16, 2026
….4) (#261) * refactor(uffs-mft): migrate index layer to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.2 Second slice of the FRS migration along the playbook's data-flow split (5d.1 parse → 5d.2 index → 5d.3 query → 5d.4 wire). Pushes the typed `Frs` / `ParentFrs` newtypes introduced in #259 across the index layer's storage types, public methods, and every cross-crate consumer so the parent-vs-child distinction is now type-checked end-to-end from the on-disk parse boundary through every record-construction, record-lookup, and record-mutation site. ## Migrated index storage fields * `index::types::FileRecord.frs: u64` → `Frs` * `index::types::FileRecord.base_frs: u64` → `Frs` * `index::types::LinkInfo.parent_frs: u64` → `ParentFrs` All three remain `#[repr(transparent)]` over `u64`, so on-disk layout stays byte-identical (Pod / Zeroable derives unchanged; `bytemuck` contract preserved for memcpy serialise / deserialise). ## Migrated public method signatures `index/base.rs` (the primary lookup surface): * `FileRecord::new(frs: u64)` → `FileRecord::new(frs: Frs)` * `FileRecord::new_unified(frs: u64)` → `(frs: Frs)` * `MftIndex::get_or_create(&mut self, frs: u64)` → `(frs: Frs)` * `MftIndex::get_or_create_unified(&mut self, frs: u64)` → `(frs: Frs)` * `MftIndex::ensure_record(&mut self, frs: u64)` → `(frs: Frs)` * `MftIndex::find(&self, frs: u64)` → `(frs: Frs)` * `MftIndex::frs_to_idx_opt(&self, frs: u64)` → `(frs: Frs)` Other index methods: * `MftIndex::build_path(&self, frs: u64)` → `(frs: Frs)` (paths.rs) * `MftIndex::add_child_entry(parent_frs: u64, child_frs: u64, name_index)` → `(parent_frs: ParentFrs, child_frs: Frs, name_index)` (child_order.rs) * `PathResolver::is_valid(&self, frs: u64)` → `(frs: Frs)` * `PathResolver::is_illegal(&self, frs: u64)` → `(frs: Frs)` * `IllegalRefs::is_valid(&self, index: &MftIndex, frs: u64)` → `(frs: Frs)` * `path_resolver::PathCache::get(&self, frs: u64)` → `(frs: Frs)` ## Construction-site lifts (raw → typed) Every place inside `parse/`, `io/parser/`, `index/{builder,merge,model, fragment,base,child_order,paths,tree,storage}.rs` and the index helpers in `parse/index_helpers.rs` that previously stored a raw `u64` FRS now lifts at the on-disk → typed boundary, with a citation comment naming the upstream source (parser-emitted `Frs` value, kernel-decoded `u64` chunk offset, or sentinel literal). The `ROOT_FRS: u64 = 5` constant stays a raw `u64` (DTO surface for external consumers — wire format, fixtures); internal call sites lift via `ROOT_FRS.into()` / `Into::into(ROOT_FRS)` at the typed boundary. `Frs::ROOT` and `ParentFrs::ROOT` remain the preferred construction path for new code per the 5d.1 contract. ## Cross-crate caller updates * `uffs-core::compact` — `build_compact_index` walks records via the typed `record.frs` and lifts at every `frs_to_idx`-indexing site; the closure that resolves a parent FRS to a compact-record index is now factored out into `resolve_parent_compact_idx(index, parent_frs: ParentFrs, own_frs: Frs)` so the typed signature is enforced at every call site (own↔parent swap = compile error) AND so `build_compact_index` stays under `clippy::too_many_lines`. * `uffs-core::aggregate::duplicates` — Pod-fixture builders use `(100 + i).into()` / `ROOT_FRS.into()` at construction. * `uffs-core::index_search::result` + cross-crate test fixtures (`uffs-core::compact_tests`, `uffs-core::index_search::tests`, `uffs-core::search::*::tests`, `uffs-core::aggregate::integration_tests`) now construct typed `Frs` / `ParentFrs` values. * `uffs-daemon::index::tests::build_test_drive*` — three synthetic drive builders updated to the typed surface (rebuilt from scratch with the new newtypes). * `uffs-diag::bin::dump_mft_records` — diagnostic binary lifts at the CLI-input boundary. ## Pod-layout test endianness fix `crates/uffs-mft/src/frs.rs` — the `frs_is_pod_zeroable_with_u64_layout` and `parent_frs_is_pod_zeroable_with_u64_layout` regression tests previously compared `bytemuck::bytes_of(&Frs::new(…))` against `u64::to_ne_bytes(…)`. Clippy's `host_endian_bytes` lint forbids `to_ne_bytes` because it leaks host-endian semantics; switched to `bytemuck::bytes_of(&raw)` which is endianness-agnostic by construction (we only assert layout parity, not a specific endianness). ## Test-fixture migration All `index/tests_*.rs` (`tests_core`, `tests_children`, `tests_extensions`, `tests_helpers`, `tests_merge`, `tests_perf`, `tests_tree`, `tests_ads`), `parse/direct_index_extension_tests`, and the cross-crate fixtures listed above lift at construction. Where a test owns a typed `child_frs: Frs` and constructs a `ChildInfo { child_frs, … }`, the shorthand expands to `child_frs: child_frs.into()` to bridge raw u64 test-locals into the typed field; conversely, where the field is already typed and the helper takes `Frs` (e.g. `frs_to_idx_opt`), the redundant `.into()` is dropped to satisfy `clippy::useless_conversion`. Three `: ParentFrs` annotations on `let no_entry_parent = ParentFrs::new(…)` in `index/{builder,child_order,paths}.rs` are dropped per `clippy::redundant_type_annotations` (type is clearly inferable from the constructor). ## Verification * `cargo fmt --all --check` — clean * `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean * `just lint-prod` (pedantic + nursery + cargo, libs/bins) — clean * `just lint-tests` (pedantic + nursery + cargo, tests, unwrap/expect allowed) — clean * `cargo nextest run --workspace --all-features` — 1793 / 1793 pass (14 skipped, all pre-existing) * `cargo nextest run --workspace --profile pre-push-smoke` — 1608 / 1608 pass * `cargo test --doc --workspace --all-features` (with `RUSTDOCFLAGS=-Dwarnings`) — clean * `RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --all-features --no-deps` — clean * `cargo deny check` — clean * `just lint-ci-windows` (cross-platform Windows clippy via cargo-xwin) — clean * `just lint-pre-push` — all 26 gates green in 101 s ## Behavior preservation No DTO / wire-format / on-disk layout changes. `Frs` and `ParentFrs` are `#[repr(transparent)]` over `u64`; `bytemuck::bytes_of(&Frs::new(x))` == `bytemuck::bytes_of(&x)` (regression test enforces this in `frs.rs`). The `MftStats::max_frs: u64` DTO field stays raw; the internal max-tracking loop in `index/base.rs::compute_stats` now lifts to `record.frs.raw()` once per record at the comparison boundary. Refs #191 (FRS newtype migration umbrella). * refactor(uffs-mft): migrate journal/USN layer to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.3 Third slice of the FRS migration along the playbook's data-flow split (5d.1 parse → 5d.2 index → **5d.3 query/journal** → 5d.4 wire). Pushes the typed `Frs` / `ParentFrs` newtypes introduced in #259 through the USN-journal DTO surface and the surgical-patch pipeline that consumes it, so the kernel-decoded → typed lift sits at exactly one point (`usn::windows::read_usn_journal`) and every downstream consumer (`apply_usn_deletes`, `apply_usn_patch`, the daemon's `PatchSink` / `IndexManager` glue, every test fixture in the journal cluster) operates on typed values. ## Migrated DTO fields * `uffs_mft::usn::UsnRecord.frs: u64` → `Frs` * `uffs_mft::usn::UsnRecord.parent_frs: u64` → `ParentFrs` * `uffs_mft::usn::FileChange.frs: u64` → `Frs` * `uffs_mft::usn::FileChange.parent_frs: u64` → `ParentFrs` Both DTOs are `#[derive(Debug, Clone[, Default])]` only — no serde, no Pod, no on-disk / on-wire layout (the wire-side path goes through `compact_storage` / `aggregate_wire`, not these in-process DTOs). `Frs` and `ParentFrs` are `#[repr(transparent)]` over `u64` so any future memcpy serialiser stays bit-identical. ## Migrated function signature * `uffs_mft::usn::aggregate_changes(records: &[UsnRecord])` — return type `HashMap<u64, FileChange>` → `HashMap<Frs, FileChange>`. `Frs` already derives `Hash + Eq + Copy + Default` (per 5d.1) so the hash-map key swap is bit-identical to the raw `u64` key it replaces. Every existing caller (`uffs_daemon::cache::journal_loop::sources` chains `.into_values().collect()` so the key type is dropped at the boundary) compiles unchanged. ## Construction-site lifts (raw → typed) * `uffs_mft::usn::windows::read_usn_journal` — the single on-disk → typed boundary. NTFS file references are 64-bit values whose low 48 bits encode the FRS (the high 16 bits are the sequence number, which downstream consumers don't need). The mask is unchanged; the lift `Frs::new(masked) / ParentFrs::new(masked)` now happens inline at the `all_records.push(UsnRecord { … })` site, with a citation comment naming the parser-boundary contract. ## Typed → raw demotions (single-point lift) * `uffs_mft::index::MftIndex::apply_usn_deletes` — `change.frs.raw()` lifted once per change because (a) the `frs_to_idx` lookup table is `Vec<u32>` keyed by `usize`, and (b) the returned `frs_to_read: Vec<u64>` feeds the kernel-loop arithmetic input of `read_targeted_frs_records(&[u64])` (which stays raw per the 5d.1 parser-boundary policy). Single lift, citation-commented. * `uffs_core::compact_loader::apply_usn_patch` — three lift sites at the `frs_to_compact` CSR-lookup boundary (one for `change.frs`, two for `change.parent_frs` on the create and rename branches). Each documents that the CSR table is `Vec<u32>` indexed by `usize` so the demotion is a single `.raw()` call per change. ## Test-fixture migration * `uffs_core::compact_loader_tests` — 20 `FileChange { frs: N, … }` and `parent_frs: N` literals lifted via `N_u64.into()`. No test semantics changed; the integer literals stay in the source for readability and the lift happens at the field-construction boundary. * `uffs_daemon::cache::journal_sink` (test module) — `make_change(frs: u64)` now lifts via `frs.into()` at the single construction site, and the three snapshot helpers (`pending_frs_for_letter`, `c_changes.iter().map(…)`, `d_changes.iter().map(…)`) demote via `change.frs.raw()` so assertion literals stay as integer arrays (`[100, 101]`, `[1, 2, 3, 4]`, etc.). Two new doc-comment paragraphs name the construction / snapshot boundaries. * `uffs_daemon::cache::journal_loop::tests` — `one_change(frs: u64)` helper lifts via `frs.into()` at construction. * `uffs_daemon::cache::shard::tests` — two `FileChange { frs: 10, … }` fixtures lifted via `10_u64.into()`. ## Boundary policy preserved These typed fields stay **raw `u64`** by deliberate design — they either represent kernel-loop arithmetic inputs or DTO surface contracts that are documented as wire/columnar parity points: * `MftError::RecordRead { frs: u64 }` and `MftError::InvalidRecord(u64)` — constructed inside `io/readers/basic.rs` from the raw `frs: u64` parameter that the kernel-loop arithmetic derives from chunk offsets (per 5d.1 boundary policy). * `index::MftStats::max_frs: u64` — DTO surface (per 5d.2 commit message; the internal max-tracking loop lifts to `record.frs.raw()` at the comparison boundary). * `index_search::result::SearchResult.frs / .parent_frs: u64` — consumer-facing DTO with documented "wire-format / DataFrame parity" semantics; deferred to 5d.4 (wire) or a deliberate non-migration. * `path_resolver::fast::FastPathResolver` — every public method takes a `polars::DataFrame` with `frs: u64` columns or a single `frs: u64` parameter that comes from a polars column; the polars column IS the wire boundary by definition. Migration deferred. * `io/parser/*.rs::parse_record_to_index(_, frs: u64, _)` and friends — kernel-loop arithmetic input (per 5d.1 contract). * `read_targeted_frs_records(_, _, frs_list: &[u64])` — same. ## Verification * `cargo fmt --all -- --check` — clean * `cargo check --workspace --all-targets --all-features` — clean * `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean * `cargo nextest run --workspace --all-features --no-fail-fast` — 1793 / 1793 pass (14 skipped, pre-existing) * `just lint-pre-push` — all 23 active gates green in 77 s (`fmt`, `file-size`, `gates/hooks/workflow/fast/manifest-drift`, `commit-subjects`, `vet`, `vet-audit-discipline`, `machete`, `typos`, `reuse`, `cargo-check`, `lint-ci`, `lint-prod`, `lint-tests`, `rustdoc`, `doc-tests`, `tests`, `smoke`, `deny`, `lint-ci-windows`) ## Behavior preservation No on-disk, on-wire, or columnar layout changes. `UsnRecord` and `FileChange` are in-process DTOs without serde / Pod derives; swapping their FRS fields from `u64` to `Frs` / `ParentFrs` is a zero-cost source-level rename because both newtypes are `#[repr(transparent)]` over `u64`. `aggregate_changes`'s hash-map key type swap is bit-identical because `Frs` forwards `Hash` and `Eq` to the underlying `u64`. Refs #191 (FRS newtype migration umbrella). * refactor(uffs-core): migrate SearchResult to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.4 Fourth and final slice of the FRS migration along the playbook's data-flow split (5d.1 parse → 5d.2 index → 5d.3 query/journal → **5d.4 wire**). Promotes the consumer-facing search-result DTO to typed FRS values and formalises the deliberate raw-`u64` wire-boundary policy at the two remaining polars / DataFrame edges with module-level doc comments. ## Migrated DTO fields * `uffs_core::index_search::result::SearchResult.frs: u64` → `Frs` * `uffs_core::index_search::result::SearchResult.parent_frs: u64` → `ParentFrs` Both newtypes are `#[repr(transparent)]` over `u64` and derive `Copy + Clone + PartialEq + Eq + PartialOrd + Ord + Hash + Default` plus `bytemuck::Pod + Zeroable`, so the field swap is bit-identical in memory and transparent to every existing usage: `Display` forwards to the inner `u64`, tuple `sort()` / `assert_eq!` keep working, and any caller that ultimately demands a raw `u64` (CSV, JSON, polars columnar output) does a `.raw()` or `u64::from(...)` at its own final-wire site. ## Construction-site simplifications * `SearchResult::from_record` and `SearchResult::from_expanded` no longer perform the `.raw()` demotion at the construction boundary. The typed [`FileRecord.frs`] / [`NameInfo.parent_frs`] values (typed since 5d.1 / 5d.2) flow through unchanged into the typed `SearchResult` fields — one fewer lift in the hot expansion path, and the boundary citation comment now reads "typed FRS values flow through unchanged" instead of the previous "demote at this construction boundary only" wording. ## Formalised wire-boundary policy (deliberate raw `u64`) Two module-level doc comments document the surfaces that remain deliberately raw because the polars column / columnar staging is **itself** the FRS wire boundary by Phase-4 doctrine. These were already raw-by-design — this commit elevates the inline policy comments to module docs so the contract is discoverable from the crate docs and not just from individual lift sites. * `uffs_core::path_resolver::fast` — every public method on `FastPathResolver` takes either a `uffs_polars::DataFrame` with `frs: u64` / `parent_frs: u64` columns or a single `frs: u64` parameter read out of one of those columns. The internal `FastEntry.parent_frs`, `FastPathResolver::path_cache` key, and `FastPathResolver::max_frs` field all stay raw `u64`: they live one row-loop downstream of the polars lift, and lifting them into the typed domain only to immediately demote on the lookup-by-`u64` path would add ceremony without type-safety win. Typed callers demote via `frs.raw()` at the `resolve` / `resolve_cached` boundary. * `uffs_mft::parse::columns` — the `ParsedColumns.frs: Vec<u64>` / `parent_frs: Vec<u64>` staging buffers feed `reader::dataframe_build` and ultimately become `polars::Series::new("frs", _)`. The canonical lift site is `ParsedColumns::push_record`, which does `self.frs.push(record.frs.raw())` — single demotion, one citation comment. ## Non-migrated surfaces (audit closure) Phase 5d.4 also closes the audit on the wire layer by confirming the following surfaces have no FRS sites that need touching: * `uffs_client::protocol::aggregate_wire` — the JSON-serialisable aggregation-wire family (`AggregateSpecWire`, `BucketWire`, `StatsWire`, `DrilldownWire`, `SampleRowWire`, `AggregateResultWire`) carries no FRS fields. The playbook's step-5 note about "add `impl From<Frs> for u64` only at the `aggregate_wire` boundary" is moot — there is no boundary there because the wire schema doesn't transmit FRS. * `uffs_broker_protocol` — broker control protocol, no FRS. * `uffs_core::compact_storage` — already memcpy-serialised via `bytemuck::Pod` over `Frs` / `ParentFrs`; on-disk layout is bit-identical to the pre-newtype representation. ## Verification * `cargo fmt --all -- --check` — clean * `cargo check --workspace --all-targets --all-features` — clean * `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean * `RUSTDOCFLAGS='-Dwarnings -Drustdoc::broken_intra_doc_links' \ cargo doc -p uffs-core -p uffs-mft --all-features --no-deps` — clean * `cargo nextest run --workspace --all-features --no-fail-fast` — 1793 / 1793 pass (14 skipped, pre-existing) * `just lint-pre-push` — all 23 active gates green in 103 s ## Behavior preservation Zero on-disk, on-wire, or columnar layout changes: * `SearchResult` was never serde-derived; it's a process-local DTO consumed by the search dispatch, the daemon's response builders, and the index_search test suite. Field-type rename is a source-level refactor only. * `Frs` and `ParentFrs` are `#[repr(transparent)]` over `u64` and forward `Display`, `Hash`, `Eq`, `PartialOrd`, `Ord`. The only test in this crate that reads `result.frs` collects it into a tuple, sorts the tuple vec, and `assert_eq!`s against another tuple vec built the same way — both sides switch to `Frs` in lockstep, comparison stays bit-identical to the prior `u64` version. * The two doc-comment-only changes (`path_resolver::fast`, `parse::columns`) add no code; they elevate the inline boundary-citation comments already present at individual lift sites to module-level discoverability. ## Phase 5d closure This commit closes the FRS-newtype migration arc: 5d.1 parse — Frs, ParentFrs newtypes + parse-layer plumbing (#259) 5d.2 index — MftIndex, FileRecord, LinkInfo, ChildInfo (64be6ce) 5d.3 journal — UsnRecord, FileChange, USN apply pipeline (fb822a9) 5d.4 wire — SearchResult + formalised raw-u64 boundary docs (this commit) Every typed-FRS site now has a single documented lift point at the parser / polars boundary, and every remaining raw-`u64` site is either kernel-loop arithmetic input or a polars / DataFrame column that is the wire boundary itself. Refs #191 (FRS newtype migration umbrella).
6 tasks
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.
Summary
Phase 4 sub-phase 5d.1 — first slice of the FRS migration along
the playbook's data-flow split (5d.1 parse → 5d.2 index → 5d.3 query
→ 5d.4 wire).
Introduces two typed newtypes that close the historic own-FRS vs
parent-FRS swap hazard and the sentinel-meaning ambiguity
(
frs == 0meaning both "null reference" and "$MFT self-record")without touching the on-disk or wire format.
Newtypes (
crates/uffs-mft/src/frs.rs)Frs(u64)— typed FRS value with documented sentinelsFrs::ZERO/Frs::ROOTand predicatesis_zero/is_root.ParentFrs(Frs)— composed-over-Frsnewtype for parent-directoryreferences; the type system makes parent-vs-child swaps a compile
error. Convert via
ParentFrs::of(Frs)/ParentFrs::as_frs()at the explicit promotion / demotion sites.
Both are
#[repr(transparent)](byte-identical tou64) and deriveCopy + Eq + Ord + Hash + Defaultso they slot intoHashMap/BTreeMapkeys and tracing fields.Migrated parse-layer surface
NameInfo.parent_frsu64ParentFrsNameInfo.source_frsu64FrsParsedRecord.frsu64FrsParsedRecord.parent_frsu64ParentFrsParsedRecord.base_frsu64FrsExtensionAttributes.base_frsu64FrsExtensionAttributes.extension_frsu64FrsPrimaryNameTracker.parent_frsu64ParentFrsThe
parse_record* (data, frs: u64)public input signatures stayu64because the FRS is derived arithmetically from chunk offsets inthe read loop; the typed surface is the output contract.
Boundary down-casts (5d.2 will erase these)
The index storage, Polars
Vec<u64>columns, and theuffs-diagCLIare still
u64. Each crossing point uses an explicit.raw()/Frs::new()lift with a citation comment:index/builder.rs::from_parsed_records— single typed→raw demoteat loop entry (
parsed_frs,parsed_parent_frs,parsed_base_frslocals) with a doc-comment naming sub-phase 5d.2 as the eraser.
parse/columns.rs::push_record{,_expanded}— Polars-bound columns.reader/dataframe_build.rs(4 sites) — same pattern.parse/merger.rs(3 sites) —frs_to_usize/TryFrom<u64>index-arithmetic boundaries.
uffs-diag/src/bin/dump_mft_records.rs— wraps the CLI-parsedu64for the merged-record lookup.Wire format unchanged
Pinned by:
frs::tests::frs_raw_roundtrip_preserves_u64_exactly/parent_frs_raw_roundtrip_preserves_u64_exactly— anyu64survives
Frs::new→.raw()byte-identically (covers0,1,5,42,1 << 47,u64::MAX).frs::tests::frs_sentinels_match_literal_values/parent_frs_sentinels_match_literal_values—ZERO/ROOTarethe documented
0/5literals exactly.frs::tests::parent_frs_of_and_as_frs_roundtrip— promotion /demotion across the
Frs↔ParentFrsboundary is bit-identical.parse::tests::*— every existing parse regression testcompares against the typed surface (
Frs::new(...),Frs::ROOT,ParentFrs::ROOT, etc.) preserving the prior numeric expectations.Verification
cargo check --workspace --all-targetscargo xwin check --target x86_64-pc-windows-msvc --workspace --all-targetscargo test --workspace --lib --bins— 180uffs-mftlib testspass (was 169; +11 new
Frs/ParentFrsregression tests).just lint-pre-push— all 23 gates green (fmt, file-size,drift, vet, machete, typos, reuse, cargo-check, lint-ci,
lint-prod, lint-tests, rustdoc, doc-tests, tests, smoke, deny,
lint-ci-windows).
Out of scope (follow-up sub-phases)
index::types::FileRecord.{frs, base_frs},index::types::FILE_NAME.parent_frs,index::model::ChildEntry.child_frs,index::*lookup APIs — 5d.2.UsnRecord,FileChange, broker-protocol rows) — 5d.4.Refs #191