From 64be6ce5b77fbf018c644561feff0c97893b34a8 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sat, 16 May 2026 11:07:21 -0700 Subject: [PATCH 1/3] =?UTF-8?q?refactor(uffs-mft):=20migrate=20index=20lay?= =?UTF-8?q?er=20to=20Frs=20/=20ParentFrs=20newtypes=20=E2=80=94=20Phase=20?= =?UTF-8?q?4=20sub-phase=205d.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- crates/uffs-core/src/aggregate/duplicates.rs | 32 ++-- .../src/aggregate/integration_tests.rs | 12 +- crates/uffs-core/src/compact.rs | 57 ++++--- crates/uffs-core/src/compact_tests.rs | 46 +++--- crates/uffs-core/src/index_search/result.rs | 12 +- crates/uffs-core/src/index_search/tests.rs | 30 ++-- crates/uffs-core/src/search/backend_tests.rs | 64 ++++---- crates/uffs-core/src/search/filters/tests.rs | 8 +- crates/uffs-core/src/search/query_tests.rs | 122 +++++++-------- crates/uffs-daemon/src/index/tests/mod.rs | 28 ++-- crates/uffs-diag/src/bin/dump_mft_records.rs | 5 +- crates/uffs-mft/src/commands/load.rs | 2 +- crates/uffs-mft/src/frs.rs | 94 +++++++++++- crates/uffs-mft/src/index/base.rs | 43 +++--- crates/uffs-mft/src/index/builder.rs | 89 +++++------ crates/uffs-mft/src/index/child_order.rs | 34 +++-- crates/uffs-mft/src/index/dataframe.rs | 9 +- crates/uffs-mft/src/index/fragment.rs | 5 +- crates/uffs-mft/src/index/merge.rs | 5 +- crates/uffs-mft/src/index/model.rs | 9 +- crates/uffs-mft/src/index/path_resolver.rs | 57 ++++--- crates/uffs-mft/src/index/paths.rs | 33 ++-- .../uffs-mft/src/index/storage/deserialize.rs | 14 +- crates/uffs-mft/src/index/tests_ads.rs | 12 +- crates/uffs-mft/src/index/tests_children.rs | 40 ++--- crates/uffs-mft/src/index/tests_core.rs | 6 +- crates/uffs-mft/src/index/tests_extensions.rs | 28 ++-- crates/uffs-mft/src/index/tests_helpers.rs | 2 +- crates/uffs-mft/src/index/tests_merge.rs | 142 ++++++++++-------- crates/uffs-mft/src/index/tests_perf.rs | 14 +- crates/uffs-mft/src/index/tests_tree.rs | 96 ++++++------ crates/uffs-mft/src/index/tree.rs | 8 +- crates/uffs-mft/src/index/types.rs | 31 ++-- crates/uffs-mft/src/io/parser/fragment.rs | 32 ++-- .../src/io/parser/fragment_extension.rs | 41 +++-- crates/uffs-mft/src/io/parser/index.rs | 26 ++-- .../uffs-mft/src/io/parser/index_extension.rs | 19 ++- crates/uffs-mft/src/io/parser/unified.rs | 17 ++- crates/uffs-mft/src/parse/direct_index.rs | 25 +-- .../src/parse/direct_index_extension.rs | 15 +- .../src/parse/direct_index_extension_tests.rs | 4 +- crates/uffs-mft/src/parse/index_helpers.rs | 31 ++-- crates/uffs-mft/src/tree_metrics.rs | 18 ++- 43 files changed, 833 insertions(+), 584 deletions(-) diff --git a/crates/uffs-core/src/aggregate/duplicates.rs b/crates/uffs-core/src/aggregate/duplicates.rs index f2b203d29..e157298c9 100644 --- a/crates/uffs-core/src/aggregate/duplicates.rs +++ b/crates/uffs-core/src/aggregate/duplicates.rs @@ -347,18 +347,18 @@ mod tests { // Root directory. let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let add_file = |index: &mut MftIndex, frs: u64, name: &str, size: u64| { let off = index.add_name(name); let ext = index.intern_extension(name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: size, allocated: size, @@ -591,20 +591,20 @@ mod tests { // Root. let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // 10 unique files — all different names and sizes. for i in 0..10_u64 { let name = format!("file_{i}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(100 + i); + let rec = idx.get_or_create((100 + i).into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: (i + 1) * 100, allocated: (i + 1) * 512, @@ -638,20 +638,20 @@ mod tests { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::T); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Two zero-byte files with same name — should NOT be duplicates. for i in 0..2_u64 { let name = "empty.txt"; let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(100 + i); + let rec = idx.get_or_create((100 + i).into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 0, allocated: 0, @@ -680,22 +680,22 @@ mod tests { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::T); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Two directories with same name — should NOT be duplicates. for i in 0..2_u64 { let name = "subdir"; let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(100 + i); + let rec = idx.get_or_create((100 + i).into()); rec.stdinfo.set_directory(true); rec.stdinfo.flags = 0x10; // directory rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 4096, allocated: 4096, diff --git a/crates/uffs-core/src/aggregate/integration_tests.rs b/crates/uffs-core/src/aggregate/integration_tests.rs index 7af8a9b6e..5893fd3bc 100644 --- a/crates/uffs-core/src/aggregate/integration_tests.rs +++ b/crates/uffs-core/src/aggregate/integration_tests.rs @@ -41,21 +41,21 @@ fn build_agg_test_drive() -> DriveCompactIndex { // Root directory. let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Projects directory. let dir_name = "Projects"; let dir_off = idx.add_name(dir_name); let dir_ext = idx.intern_extension(dir_name); - let dir = idx.get_or_create(100); + let dir = idx.get_or_create(100.into()); dir.stdinfo.set_directory(true); dir.stdinfo.flags = 0x10; dir.first_name.name = IndexNameRef::new(dir_off, uffs_mft::len_to_u16(dir_name.len()), true, dir_ext); - dir.first_name.parent_frs = ROOT_FRS; + dir.first_name.parent_frs = Into::into(ROOT_FRS); // Files: (name, frs, size, allocated, modified_timestamp) let files: &[(&str, u64, u64, u64, i64)] = &[ @@ -71,9 +71,9 @@ fn build_agg_test_drive() -> DriveCompactIndex { for &(name, frs, size, allocated, modified) in files { let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = 100; + rec.first_name.parent_frs = Into::into(100); rec.first_stream.size = SizeInfo { length: size, allocated, diff --git a/crates/uffs-core/src/compact.rs b/crates/uffs-core/src/compact.rs index 58b04496f..2e02e84ad 100644 --- a/crates/uffs-core/src/compact.rs +++ b/crates/uffs-core/src/compact.rs @@ -533,7 +533,7 @@ impl DriveCompactIndex { fn expand_ads_streams( index: &MftIndex, record: &uffs_mft::index::FileRecord, - resolve_parent: &dyn Fn(u64, u64) -> u32, + resolve_parent: &dyn Fn(uffs_mft::ParentFrs, uffs_mft::Frs) -> u32, names: &mut Vec, extra: &mut Vec, ) { @@ -599,6 +599,36 @@ fn expand_ads_streams( } } +/// Resolve a typed `ParentFrs` (vs an own typed `Frs`) into a compact-record +/// index, returning `u32::MAX` for the "no real parent" cases (self-reference, +/// `NO_ENTRY` sentinel, or root). +/// +/// Extracted as a free helper so the typed `ParentFrs`/`Frs` signature is +/// enforced at every call site AND so `build_compact_index` stays under +/// the clippy `too_many_lines` budget. +#[expect( + clippy::single_call_fn, + reason = "Wrapped by a closure in build_compact_index; kept free-standing \ + for clippy::too_many_lines budget headroom" +)] +fn resolve_parent_compact_idx( + index: &MftIndex, + parent_frs: uffs_mft::ParentFrs, + own_frs: uffs_mft::Frs, +) -> u32 { + let parent = parent_frs.as_frs(); + if parent == own_frs || parent_frs.raw() == u64::from(uffs_mft::NO_ENTRY) || parent.is_root() { + return u32::MAX; + } + let parent_usize = uffs_mft::frs_to_usize(parent.raw()); + index + .frs_to_idx + .get(parent_usize) + .copied() + .filter(|&idx| idx != uffs_mft::NO_ENTRY) + .unwrap_or(u32::MAX) +} + /// Expand hardlinks and ADS into additional `CompactRecord` entries. /// /// Phase 2 (hardlinks): for each valid record with `name_count > 1`, walks the @@ -613,7 +643,7 @@ fn expand_ads_streams( fn expand_links_and_ads( index: &MftIndex, resolver: &uffs_mft::index::PathResolver, - resolve_parent: &dyn Fn(u64, u64) -> u32, + resolve_parent: &dyn Fn(uffs_mft::ParentFrs, uffs_mft::Frs) -> u32, names: &mut Vec, ) -> Vec { let mut extra: Vec = Vec::new(); @@ -773,22 +803,13 @@ pub fn build_compact_index( // propagates invalidity to descendants (e.g., $Extend children). let resolver = PathResolver::build(index, false); - // Helper: resolve parent_frs → compact index. - let resolve_parent = |parent_frs: u64, own_frs: u64| -> u32 { - if parent_frs == own_frs - || parent_frs == u64::from(uffs_mft::NO_ENTRY) - || parent_frs == uffs_mft::ROOT_FRS - { - u32::MAX - } else { - let parent_usize = uffs_mft::frs_to_usize(parent_frs); - index - .frs_to_idx - .get(parent_usize) - .copied() - .filter(|&idx| idx != uffs_mft::NO_ENTRY) - .unwrap_or(u32::MAX) - } + // Closure wraps the free helper `resolve_parent_compact_idx` so the + // typed `ParentFrs`/`Frs` signature is enforced at every call site + // (own↔parent swap becomes a compile error). Keeping the helper + // free-standing also keeps `build_compact_index` under the + // clippy::too_many_lines budget. + let resolve_parent = |parent_frs: uffs_mft::ParentFrs, own_frs: uffs_mft::Frs| -> u32 { + resolve_parent_compact_idx(index, parent_frs, own_frs) }; // Phase 1: build primary compact records (parallel). diff --git a/crates/uffs-core/src/compact_tests.rs b/crates/uffs-core/src/compact_tests.rs index adac9084c..7042370ff 100644 --- a/crates/uffs-core/src/compact_tests.rs +++ b/crates/uffs-core/src/compact_tests.rs @@ -27,25 +27,25 @@ fn fixture_index() -> MftIndex { // Root (FRS 5) let root_name = push_name(&mut idx, "."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = root_name; - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Docs directory (FRS 100) let docs_name = push_name(&mut idx, "Docs"); - let docs = idx.get_or_create(100); + let docs = idx.get_or_create(100.into()); docs.stdinfo.set_directory(true); docs.first_name.name = docs_name; - docs.first_name.parent_frs = ROOT_FRS; + docs.first_name.parent_frs = Into::into(ROOT_FRS); docs.descendants = 3; docs.treesize = 500; // file.txt (FRS 200) — simple single-name, single-stream let file_name = push_name(&mut idx, "file.txt"); - let file = idx.get_or_create(200); + let file = idx.get_or_create(200.into()); file.first_name.name = file_name; - file.first_name.parent_frs = 100; + file.first_name.parent_frs = Into::into(100); file.first_stream.size = SizeInfo { length: 120, allocated: 128, @@ -64,12 +64,12 @@ fn fixture_index() -> MftIndex { next_entry: NO_ENTRY, name: hl_alt_name, _pad0: [0; 4], - parent_frs: ROOT_FRS, + parent_frs: Into::into(ROOT_FRS), }); - let hl = idx.get_or_create(201); + let hl = idx.get_or_create(201.into()); hl.first_name.name = hl_primary; - hl.first_name.parent_frs = 100; + hl.first_name.parent_frs = Into::into(100); hl.first_name.next_entry = link_idx; hl.name_count = 2; hl.first_stream.size = SizeInfo { @@ -93,9 +93,9 @@ fn fixture_index() -> MftIndex { _pad0: [0; 3], }); - let ads = idx.get_or_create(202); + let ads = idx.get_or_create(202.into()); ads.first_name.name = ads_name; - ads.first_name.parent_frs = 100; + ads.first_name.parent_frs = Into::into(100); ads.first_stream.size = SizeInfo { length: 300, allocated: 512, @@ -105,9 +105,9 @@ fn fixture_index() -> MftIndex { // $MFT system metafile (FRS 0) — should be filterable let mft_name = push_name(&mut idx, "$MFT"); - let mft_rec = idx.get_or_create(0); + let mft_rec = idx.get_or_create(0.into()); mft_rec.first_name.name = mft_name; - mft_rec.first_name.parent_frs = ROOT_FRS; + mft_rec.first_name.parent_frs = Into::into(ROOT_FRS); mft_rec.first_stream.size = SizeInfo { length: 500_000, allocated: 512_000, @@ -373,16 +373,16 @@ fn fixture_index_with_ads() -> MftIndex { // Root (FRS 5) let root_name = push_name(&mut idx, "."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = root_name; - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // file.pdf (FRS 100) — has a Zone.Identifier ADS let file_name = push_name(&mut idx, "file.pdf"); - let file = idx.get_or_create(100); + let file = idx.get_or_create(100.into()); file.first_name.name = file_name; - file.first_name.parent_frs = ROOT_FRS; + file.first_name.parent_frs = Into::into(ROOT_FRS); file.first_stream.size = SizeInfo { length: 50_000, allocated: 51_200, @@ -409,7 +409,7 @@ fn fixture_index_with_ads() -> MftIndex { }); // Chain ADS to the record's stream list - let file_idx = idx.frs_to_idx_opt(100).unwrap(); + let file_idx = idx.frs_to_idx_opt(100.into()).unwrap(); let file_mut = idx.records.get_mut(file_idx).expect("record must exist"); file_mut.first_stream.next_entry = ads_si; file_mut.stream_count = 2; @@ -501,16 +501,16 @@ fn ads_on_directory_strips_directory_flag() { // Create root (FRS 5) let root_name = push_name(&mut idx, "."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = root_name; - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Create a directory (FRS 200) with DIRECTORY | ARCHIVE flags let dir_name = push_name(&mut idx, "Airlink 430W"); - let dir_rec = idx.get_or_create(200); + let dir_rec = idx.get_or_create(200.into()); dir_rec.first_name.name = dir_name; - dir_rec.first_name.parent_frs = ROOT_FRS; + dir_rec.first_name.parent_frs = Into::into(ROOT_FRS); dir_rec.stdinfo.set_directory(true); dir_rec.stdinfo.flags |= StandardInfo::IS_ARCHIVE; @@ -529,7 +529,7 @@ fn ads_on_directory_strips_directory_flag() { _pad0: [0; 3], }); - let dir_idx = idx.frs_to_idx_opt(200).unwrap(); + let dir_idx = idx.frs_to_idx_opt(200.into()).unwrap(); let dir_mut = idx.records.get_mut(dir_idx).expect("record must exist"); dir_mut.first_stream.next_entry = ads_si; dir_mut.stream_count = 2; diff --git a/crates/uffs-core/src/index_search/result.rs b/crates/uffs-core/src/index_search/result.rs index 54e850aa9..60c30a259 100644 --- a/crates/uffs-core/src/index_search/result.rs +++ b/crates/uffs-core/src/index_search/result.rs @@ -63,8 +63,11 @@ impl SearchResult { path: None, // Path resolution is expensive, done on demand size: record.first_stream.size.length, allocated_size: record.first_stream.size.allocated, - frs: record.frs, - parent_frs: record.first_name.parent_frs, + // `SearchResult` is the consumer-facing DTO and keeps raw + // `u64` for wire-format / DataFrame parity; demote the typed + // index fields at this construction boundary only. + frs: record.frs.raw(), + parent_frs: record.first_name.parent_frs.raw(), is_directory, stream_name: String::new(), name_index: 0, @@ -125,8 +128,9 @@ impl SearchResult { path: None, size: stream_info.size.length, allocated_size: stream_info.size.allocated, - frs: record.frs, - parent_frs: name_info.parent_frs, + // Same DTO-boundary demotion as `from_record_default`. + frs: record.frs.raw(), + parent_frs: name_info.parent_frs.raw(), is_directory, stream_name: stream_name.to_owned(), name_index: name_idx, diff --git a/crates/uffs-core/src/index_search/tests.rs b/crates/uffs-core/src/index_search/tests.rs index 124a54cf9..bf63073b0 100644 --- a/crates/uffs-core/src/index_search/tests.rs +++ b/crates/uffs-core/src/index_search/tests.rs @@ -44,24 +44,24 @@ fn build_index_query_fixture() -> Result { let mut index = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_name = push_name_ref(&mut index, ".")?; - let root = index.get_or_create(ROOT_FRS); + let root = index.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = root_name; - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let docs_frs = 100_u64; let docs_name = push_name_ref(&mut index, "Docs")?; - let docs = index.get_or_create(docs_frs); + let docs = index.get_or_create(docs_frs.into()); docs.stdinfo.set_directory(true); docs.first_name.name = docs_name; - docs.first_name.parent_frs = ROOT_FRS; + docs.first_name.parent_frs = Into::into(ROOT_FRS); let links_frs = 101_u64; let links_name = push_name_ref(&mut index, "Links")?; - let links = index.get_or_create(links_frs); + let links = index.get_or_create(links_frs.into()); links.stdinfo.set_directory(true); links.first_name.name = links_name; - links.first_name.parent_frs = ROOT_FRS; + links.first_name.parent_frs = Into::into(ROOT_FRS); let primary_name = push_file_name_ref(&mut index, "alpha.txt")?; let hard_link_name = push_file_name_ref(&mut index, "alpha_link.txt")?; @@ -72,7 +72,7 @@ fn build_index_query_fixture() -> Result { next_entry: NO_ENTRY, name: hard_link_name, _pad0: [0; 4], - parent_frs: links_frs, + parent_frs: Into::into(links_frs), }); let ads_idx = u32::try_from(index.streams.len())?; @@ -87,9 +87,9 @@ fn build_index_query_fixture() -> Result { _pad0: [0; 3], }); - let alpha = index.get_or_create(200); + let alpha = index.get_or_create(200.into()); alpha.first_name.name = primary_name; - alpha.first_name.parent_frs = docs_frs; + alpha.first_name.parent_frs = Into::into(docs_frs); alpha.first_name.next_entry = hard_link_idx; alpha.name_count = 2; alpha.first_stream.size = SizeInfo { @@ -102,9 +102,9 @@ fn build_index_query_fixture() -> Result { alpha.total_stream_count = 2; let beta_name = push_file_name_ref(&mut index, "beta.bin")?; - let beta = index.get_or_create(201); + let beta = index.get_or_create(201.into()); beta.first_name.name = beta_name; - beta.first_name.parent_frs = docs_frs; + beta.first_name.parent_frs = Into::into(docs_frs); beta.first_stream.size = SizeInfo { length: 20, allocated: 64, @@ -179,10 +179,10 @@ fn extensions() { fn extension_index_integration() { let mut index = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_name_offset = index.add_name("."); - let root = index.get_or_create(ROOT_FRS); + let root = index.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_name_offset, 1, true, 0); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let files = [ ("readme.txt", 1000), @@ -198,10 +198,10 @@ fn extension_index_integration() { let offset = index.add_name(name); let ext_id = index.intern_extension(name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: *size, allocated: *size, diff --git a/crates/uffs-core/src/search/backend_tests.rs b/crates/uffs-core/src/search/backend_tests.rs index a7441df5b..fd2bde79f 100644 --- a/crates/uffs-core/src/search/backend_tests.rs +++ b/crates/uffs-core/src/search/backend_tests.rs @@ -382,21 +382,21 @@ fn build_two_drive_backend() -> MultiDriveBackend { ] { let mut idx = MftIndex::new(letter); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let f_off = idx.add_name(file_name); let f_ext = idx.intern_extension(file_name); - let file_rec = idx.get_or_create(200); + let file_rec = idx.get_or_create(200.into()); file_rec.first_name.name = IndexNameRef::new( f_off, u16::try_from(file_name.len()).expect("name too long"), true, f_ext, ); - file_rec.first_name.parent_frs = ROOT_FRS; + file_rec.first_name.parent_frs = Into::into(ROOT_FRS); file_rec.first_stream.size = SizeInfo { length: file_size, allocated: file_size.next_multiple_of(512), @@ -631,10 +631,10 @@ fn build_siblings_fixture() -> DriveIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Insertion order (FRS 200, 201, 202) is deliberately non-alphabetical // so tree-walk `sort_indices_by_name` on the root's children has to @@ -644,14 +644,14 @@ fn build_siblings_fixture() -> DriveIndex { for (frs, name) in [(200, "gamma.txt"), (201, "alpha.txt"), (202, "beta.txt")] { let n_off = idx.add_name(name); let n_ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( n_off, u16::try_from(name.len()).expect("name too long"), true, n_ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -680,21 +680,21 @@ fn build_two_drive_index() -> DriveIndex { ] { let mut idx = MftIndex::new(letter); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let f_off = idx.add_name(file_name); let f_ext = idx.intern_extension(file_name); - let file_rec = idx.get_or_create(200); + let file_rec = idx.get_or_create(200.into()); file_rec.first_name.name = IndexNameRef::new( f_off, u16::try_from(file_name.len()).expect("name too long"), true, f_ext, ); - file_rec.first_name.parent_frs = ROOT_FRS; + file_rec.first_name.parent_frs = Into::into(ROOT_FRS); file_rec.first_stream.size = SizeInfo { length: file_size, allocated: file_size.next_multiple_of(512), @@ -937,10 +937,10 @@ fn build_dbt_triple_fixture() -> DriveIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Three candidates for a `*.dbt` query. `frs` values are arbitrary // but must be distinct. @@ -961,14 +961,14 @@ fn build_dbt_triple_fixture() -> DriveIndex { ] { let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.stdinfo.flags = 0x20; } @@ -1155,10 +1155,10 @@ fn build_no_dbt_fixture() -> DriveIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Dotless directory literally named `dbt`. `intern_extension` // assigns this `extension_id = 0` (no dot), so the resolved-ID @@ -1168,14 +1168,14 @@ fn build_no_dbt_fixture() -> DriveIndex { let dbt_name = "dbt"; let dbt_off = idx.add_name(dbt_name); let dbt_ext = idx.intern_extension(dbt_name); - let dbt_rec = idx.get_or_create(300); + let dbt_rec = idx.get_or_create(300.into()); dbt_rec.first_name.name = IndexNameRef::new( dbt_off, u16::try_from(dbt_name.len()).expect("name too long"), true, dbt_ext, ); - dbt_rec.first_name.parent_frs = ROOT_FRS; + dbt_rec.first_name.parent_frs = Into::into(ROOT_FRS); dbt_rec.stdinfo.flags = 0x10; // DIRECTORY dbt_rec.stdinfo.set_directory(true); @@ -1184,14 +1184,14 @@ fn build_no_dbt_fixture() -> DriveIndex { let txt_name = "notes.txt"; let txt_off = idx.add_name(txt_name); let txt_ext = idx.intern_extension(txt_name); - let txt_rec = idx.get_or_create(301); + let txt_rec = idx.get_or_create(301.into()); txt_rec.first_name.name = IndexNameRef::new( txt_off, u16::try_from(txt_name.len()).expect("name too long"), true, txt_ext, ); - txt_rec.first_name.parent_frs = ROOT_FRS; + txt_rec.first_name.parent_frs = Into::into(ROOT_FRS); txt_rec.stdinfo.flags = 0x20; let (drive, _, _) = build_compact_index(uffs_mft::platform::DriveLetter::C, &idx); @@ -1668,14 +1668,14 @@ fn build_nested_fixture() -> DriveIndex { fn make_file(idx: &mut MftIndex, frs: u64, name: &str, parent: u64) { let n_off = idx.add_name(name); let n_ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( n_off, u16::try_from(name.len()).expect("name too long"), true, n_ext, ); - rec.first_name.parent_frs = parent; + rec.first_name.parent_frs = Into::into(parent); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -1686,23 +1686,23 @@ fn build_nested_fixture() -> DriveIndex { fn make_dir(idx: &mut MftIndex, frs: u64, name: &str, parent: u64) { let n_off = idx.add_name(name); let n_ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( n_off, u16::try_from(name.len()).expect("name too long"), true, n_ext, ); - rec.first_name.parent_frs = parent; + rec.first_name.parent_frs = Into::into(parent); rec.stdinfo.set_directory(true); } let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Root-level entries (non-alphabetical FRS order). make_file(&mut idx, 202, "zeta.txt", ROOT_FRS); @@ -2386,24 +2386,24 @@ fn build_bloom_skip_fixture() -> DriveIndex { ] { let mut idx = MftIndex::new(letter); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); for i in 0_u32..FILES_PER_DRIVE { let file_name = format!("file_{i:03}.{ext}"); let n_off = idx.add_name(&file_name); let n_ext = idx.intern_extension(&file_name); let frs = u64::from(200_u32.saturating_add(i)); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( n_off, u16::try_from(file_name.len()).expect("name too long"), true, n_ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, diff --git a/crates/uffs-core/src/search/filters/tests.rs b/crates/uffs-core/src/search/filters/tests.rs index aae08cddd..0d014bcd5 100644 --- a/crates/uffs-core/src/search/filters/tests.rs +++ b/crates/uffs-core/src/search/filters/tests.rs @@ -41,22 +41,22 @@ fn test_drive_with_rs_file() -> DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let name = "readme.rs"; let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(100); + let rec = idx.get_or_create(100.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.stdinfo.flags = 0x20; let (drive, _, _) = build_compact_index(uffs_mft::platform::DriveLetter::C, &idx); diff --git a/crates/uffs-core/src/search/query_tests.rs b/crates/uffs-core/src/search/query_tests.rs index c433ca459..4ceeb03a6 100644 --- a/crates/uffs-core/src/search/query_tests.rs +++ b/crates/uffs-core/src/search/query_tests.rs @@ -17,15 +17,15 @@ fn build_test_drive() -> DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let dir_name = "Projects"; let dir_off = idx.add_name(dir_name); let dir_ext = idx.intern_extension(dir_name); - let dir = idx.get_or_create(100); + let dir = idx.get_or_create(100.into()); dir.stdinfo.set_directory(true); dir.stdinfo.flags = 0x10; dir.first_name.name = IndexNameRef::new( @@ -34,21 +34,21 @@ fn build_test_drive() -> DriveCompactIndex { true, dir_ext, ); - dir.first_name.parent_frs = ROOT_FRS; + dir.first_name.parent_frs = Into::into(ROOT_FRS); dir.descendants = 2; dir.treesize = 700; let f1_name = "readme.txt"; let f1_off = idx.add_name(f1_name); let f1_ext = idx.intern_extension(f1_name); - let f1 = idx.get_or_create(200); + let f1 = idx.get_or_create(200.into()); f1.first_name.name = IndexNameRef::new( f1_off, u16::try_from(f1_name.len()).expect("name too long"), true, f1_ext, ); - f1.first_name.parent_frs = 100; + f1.first_name.parent_frs = Into::into(100); f1.first_stream.size = SizeInfo { length: 400, allocated: 512, @@ -60,14 +60,14 @@ fn build_test_drive() -> DriveCompactIndex { let f2_name = "data.csv"; let f2_off = idx.add_name(f2_name); let f2_ext = idx.intern_extension(f2_name); - let f2 = idx.get_or_create(201); + let f2 = idx.get_or_create(201.into()); f2.first_name.name = IndexNameRef::new( f2_off, u16::try_from(f2_name.len()).expect("name too long"), true, f2_ext, ); - f2.first_name.parent_frs = 100; + f2.first_name.parent_frs = Into::into(100); f2.first_stream.size = SizeInfo { length: 300, allocated: 512, @@ -78,14 +78,14 @@ fn build_test_drive() -> DriveCompactIndex { let sys_name = "$MFT"; let sys_off = idx.add_name(sys_name); let sys_ext = idx.intern_extension(sys_name); - let sys = idx.get_or_create(0); + let sys = idx.get_or_create(0.into()); sys.first_name.name = IndexNameRef::new( sys_off, u16::try_from(sys_name.len()).expect("name too long"), true, sys_ext, ); - sys.first_name.parent_frs = ROOT_FRS; + sys.first_name.parent_frs = Into::into(ROOT_FRS); sys.first_stream.size = SizeInfo { length: 1_000_000, allocated: 1_048_576, @@ -100,23 +100,23 @@ fn build_test_drive() -> DriveCompactIndex { fn build_large_drive(count: usize) -> DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); for i in 0..count { let frs = (i as u64) + 100; let name = format!("f{i:05}.txt"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -366,16 +366,16 @@ fn build_ads_on_dir_drive() -> DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // A directory with an ADS let dir_name = "MyFolder"; let dir_off = idx.add_name(dir_name); let dir_ext = idx.intern_extension(dir_name); - let dir_rec = idx.get_or_create(100); + let dir_rec = idx.get_or_create(100.into()); dir_rec.stdinfo.set_directory(true); dir_rec.stdinfo.flags |= StandardInfo::IS_ARCHIVE; dir_rec.first_name.name = IndexNameRef::new( @@ -384,7 +384,7 @@ fn build_ads_on_dir_drive() -> DriveCompactIndex { true, dir_ext, ); - dir_rec.first_name.parent_frs = ROOT_FRS; + dir_rec.first_name.parent_frs = Into::into(ROOT_FRS); // Add ADS stream let stream_name = "metadata"; @@ -407,7 +407,7 @@ fn build_ads_on_dir_drive() -> DriveCompactIndex { _pad0: [0; 3], }); - let dir_idx = idx.frs_to_idx_opt(100).expect("dir idx"); + let dir_idx = idx.frs_to_idx_opt(100.into()).expect("dir idx"); let dir_mut = idx.records.get_mut(dir_idx).expect("dir record"); dir_mut.first_stream.next_entry = si; dir_mut.stream_count = 2; @@ -505,22 +505,22 @@ fn build_flag_test_drive( // Root directory. let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // File WITH the flag. let f1_off = idx.add_name(flagged_name); let f1_ext = idx.intern_extension(flagged_name); - let f1 = idx.get_or_create(100); + let f1 = idx.get_or_create(100.into()); f1.first_name.name = IndexNameRef::new( f1_off, u16::try_from(flagged_name.len()).expect("name"), true, f1_ext, ); - f1.first_name.parent_frs = ROOT_FRS; + f1.first_name.parent_frs = Into::into(ROOT_FRS); f1.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -532,14 +532,14 @@ fn build_flag_test_drive( // File WITHOUT the flag — same timestamps. let f2_off = idx.add_name(unflagged_name); let f2_ext = idx.intern_extension(unflagged_name); - let f2 = idx.get_or_create(101); + let f2 = idx.get_or_create(101.into()); f2.first_name.name = IndexNameRef::new( f2_off, u16::try_from(unflagged_name.len()).expect("name"), true, f2_ext, ); - f2.first_name.parent_frs = ROOT_FRS; + f2.first_name.parent_frs = Into::into(ROOT_FRS); f2.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -599,28 +599,28 @@ fn top_n_sort_by_directory_flag() { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // A directory. let dir_off = idx.add_name("mydir"); let dir_ext = idx.intern_extension("mydir"); - let dir_rec = idx.get_or_create(100); + let dir_rec = idx.get_or_create(100.into()); dir_rec.stdinfo.set_directory(true); dir_rec.stdinfo.flags = 0x0010; // directory dir_rec.first_name.name = IndexNameRef::new(dir_off, 5, true, dir_ext); - dir_rec.first_name.parent_frs = ROOT_FRS; + dir_rec.first_name.parent_frs = Into::into(ROOT_FRS); dir_rec.stdinfo.modified = 5_000_000; // A file with the same timestamp. let file_off = idx.add_name("myfile.txt"); let file_ext = idx.intern_extension("myfile.txt"); - let file_rec = idx.get_or_create(101); + let file_rec = idx.get_or_create(101.into()); file_rec.stdinfo.flags = 0x0020; // archive, NOT directory file_rec.first_name.name = IndexNameRef::new(file_off, 10, true, file_ext); - file_rec.first_name.parent_frs = ROOT_FRS; + file_rec.first_name.parent_frs = Into::into(ROOT_FRS); file_rec.first_stream.size = SizeInfo { length: 50, allocated: 512, @@ -731,10 +731,10 @@ fn build_mixed_drive(n_files: usize, n_dirs: usize) -> DriveCompactIndex { // Root directory (FRS 5). let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Files first (FRS 100..). for i in 0..n_files { @@ -742,10 +742,10 @@ fn build_mixed_drive(n_files: usize, n_dirs: usize) -> DriveCompactIndex { let name = format!("file_{i:04}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -761,12 +761,12 @@ fn build_mixed_drive(n_files: usize, n_dirs: usize) -> DriveCompactIndex { let name = format!("dir_{i:04}"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.stdinfo.set_directory(true); rec.stdinfo.flags = 0x10; // directory flag rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.stdinfo.modified = 5_000_000; // same timestamp rec.stdinfo.created = 5_000_000; } @@ -834,10 +834,10 @@ fn heap_eviction_directory_asc_files_come_last() { fn heap_eviction_hidden_desc() { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // 20 normal files (no hidden flag). for i in 0_u64..20 { @@ -845,10 +845,10 @@ fn heap_eviction_hidden_desc() { let name = format!("normal_{i:04}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -862,10 +862,10 @@ fn heap_eviction_hidden_desc() { let name = format!("hidden_{i:04}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -994,19 +994,19 @@ fn search_index_star_sort_hidden_desc() { // Reuse the hidden drive from heap_eviction_hidden_desc. let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); for i in 0_u64..20 { let frs = i + 100; let name = format!("normal_{i:04}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -1019,10 +1019,10 @@ fn search_index_star_sort_hidden_desc() { let name = format!("hidden_{i:04}.dat"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, u16::try_from(name.len()).expect("name"), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -1082,10 +1082,10 @@ fn build_bulkiness_test_drive() -> (DriveCompactIndex, [&'static str; 3]) { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::C); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // (name, frs, size, allocated) — all three files share the same // `modified` so a regression that falls back to `rec.modified` @@ -1102,14 +1102,14 @@ fn build_bulkiness_test_drive() -> (DriveCompactIndex, [&'static str; 3]) { for &(name, frs, size, allocated) in files { let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: size, allocated, @@ -1221,23 +1221,23 @@ fn build_modified_gradient_drive( ) -> DriveCompactIndex { let mut idx = MftIndex::new(letter); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); for i in 0..count { let frs = base_frs + (i as u64); let name = format!("{letter}_f{i:05}.txt"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, @@ -1356,24 +1356,24 @@ fn build_two_extension_drive( ) -> DriveCompactIndex { let mut idx = MftIndex::new(letter); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); for i in 0..count { let frs = 100 + (i as u64); let ext_part = if i % 2 == 0 { "dll" } else { "txt" }; let name = format!("file_{i:05}.{ext_part}"); let off = idx.add_name(&name); let ext = idx.intern_extension(&name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new( off, u16::try_from(name.len()).expect("name too long"), true, ext, ); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 100, allocated: 512, diff --git a/crates/uffs-daemon/src/index/tests/mod.rs b/crates/uffs-daemon/src/index/tests/mod.rs index 2ee94d6c3..96355576f 100644 --- a/crates/uffs-daemon/src/index/tests/mod.rs +++ b/crates/uffs-daemon/src/index/tests/mod.rs @@ -72,21 +72,21 @@ fn build_test_drive() -> uffs_core::compact::DriveCompactIndex { // Root directory let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); // Subdirectory "Projects" let dir_name = "Projects"; let dir_off = idx.add_name(dir_name); let dir_ext = idx.intern_extension(dir_name); - let dir = idx.get_or_create(100); + let dir = idx.get_or_create(100.into()); dir.stdinfo.set_directory(true); dir.stdinfo.flags = 0x10; // directory flag dir.first_name.name = IndexNameRef::new(dir_off, uffs_mft::len_to_u16(dir_name.len()), true, dir_ext); - dir.first_name.parent_frs = ROOT_FRS; + dir.first_name.parent_frs = Into::into(ROOT_FRS); // Files with different extensions and sizes let files: &[(&str, u64, u64, u64)] = &[ @@ -100,9 +100,9 @@ fn build_test_drive() -> uffs_core::compact::DriveCompactIndex { for &(name, frs, size, allocated) in files { let off = idx.add_name(name); let ext = idx.intern_extension(name); - let rec = idx.get_or_create(frs); + let rec = idx.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(name.len()), true, ext); - rec.first_name.parent_frs = 100; // under Projects + rec.first_name.parent_frs = Into::into(100); // under Projects rec.first_stream.size = SizeInfo { length: size, allocated, @@ -135,17 +135,17 @@ fn spec(kind: &str) -> AggregateSpecWire { fn build_test_drive_d() -> uffs_core::compact::DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::D); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let file = "alpha.txt"; let off = idx.add_name(file); let ext = idx.intern_extension(file); - let rec = idx.get_or_create(200); + let rec = idx.get_or_create(200.into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(file.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 42, allocated: 512, @@ -164,17 +164,17 @@ fn build_test_drive_d() -> uffs_core::compact::DriveCompactIndex { fn build_test_drive_e() -> uffs_core::compact::DriveCompactIndex { let mut idx = MftIndex::new(uffs_mft::platform::DriveLetter::E); let root_off = idx.add_name("."); - let root = idx.get_or_create(ROOT_FRS); + let root = idx.get_or_create(ROOT_FRS.into()); root.stdinfo.set_directory(true); root.first_name.name = IndexNameRef::new(root_off, 1, true, IndexNameRef::NO_EXTENSION); - root.first_name.parent_frs = ROOT_FRS; + root.first_name.parent_frs = Into::into(ROOT_FRS); let file = "beta.bin"; let off = idx.add_name(file); let ext = idx.intern_extension(file); - let rec = idx.get_or_create(300); + let rec = idx.get_or_create(300.into()); rec.first_name.name = IndexNameRef::new(off, uffs_mft::len_to_u16(file.len()), true, ext); - rec.first_name.parent_frs = ROOT_FRS; + rec.first_name.parent_frs = Into::into(ROOT_FRS); rec.first_stream.size = SizeInfo { length: 84, allocated: 1024, diff --git a/crates/uffs-diag/src/bin/dump_mft_records.rs b/crates/uffs-diag/src/bin/dump_mft_records.rs index 30a421bef..8b4bfcb87 100644 --- a/crates/uffs-diag/src/bin/dump_mft_records.rs +++ b/crates/uffs-diag/src/bin/dump_mft_records.rs @@ -358,8 +358,9 @@ fn test_merge(raw_path: &str, base_frs: u64, ext_frs: u64) -> Result<()> { println!(" records.len(): {}", index.records.len()); println!(" children.len(): {}", index.children_count()); - // Find the record in the index - if let Some(record) = index.find(base_frs) { + // Find the record in the index. Lift the CLI-parsed raw `u64` to + // a typed `Frs` at the typed-API boundary. + if let Some(record) = index.find(uffs_mft::Frs::new(base_frs)) { println!("\n=== FRS {base_frs} in MftIndex ==="); println!(" frs: {}", record.frs); println!(" name: {:?}", index.get_name(record.first_name.name)); diff --git a/crates/uffs-mft/src/commands/load.rs b/crates/uffs-mft/src/commands/load.rs index ec75ab2f9..7b678b777 100644 --- a/crates/uffs-mft/src/commands/load.rs +++ b/crates/uffs-mft/src/commands/load.rs @@ -290,7 +290,7 @@ pub(crate) fn cmd_load( } // Show root directory specifically - if let Some(root) = index.records.iter().find(|r| r.frs == 5) { + if let Some(root) = index.records.iter().find(|r| r.frs.is_root()) { println!(); println!("📁 ROOT DIRECTORY (FRS 5):"); println!( diff --git a/crates/uffs-mft/src/frs.rs b/crates/uffs-mft/src/frs.rs index 9a6c4c543..9990fd91d 100644 --- a/crates/uffs-mft/src/frs.rs +++ b/crates/uffs-mft/src/frs.rs @@ -52,7 +52,25 @@ use core::fmt; /// the on-disk `$FILE_NAME.parent_directory` field, and every parse / /// index / query API. We preserve the bit pattern byte-for-byte so /// on-disk + on-wire formats are unchanged. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +/// +/// `#[repr(transparent)]` over `u64` plus `bytemuck::Pod + Zeroable` derives +/// allow this newtype to slot into `bytemuck::Pod` index structures +/// (`FileRecord`, `LinkInfo`, `ChildInfo`) without changing their on-disk +/// representation — the field is bit-identical to a bare `u64` for +/// memory-mapped serialization. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Default, + bytemuck::Pod, + bytemuck::Zeroable, +)] #[repr(transparent)] pub struct Frs(u64); @@ -135,7 +153,24 @@ impl fmt::Display for Frs { /// /// To look the parent record up in the index, convert explicitly via /// [`ParentFrs::as_frs`]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +/// +/// `#[repr(transparent)]` over [`Frs`] (itself transparent over `u64`) +/// plus `bytemuck::Pod + Zeroable` derives allow this newtype to slot +/// into `bytemuck::Pod` index structures (`LinkInfo.parent_frs`) +/// without changing their on-disk representation. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Default, + bytemuck::Pod, + bytemuck::Zeroable, +)] #[repr(transparent)] pub struct ParentFrs(Frs); @@ -329,6 +364,61 @@ mod tests { } } + #[test] + fn frs_is_pod_zeroable_with_u64_layout() { + // bytemuck::Pod regression pin: the on-disk index format + // (`FileRecord`, `LinkInfo`, `ChildInfo` are all `bytemuck::Pod`) + // requires `Frs` to be `Pod + Zeroable` AND byte-identical to + // `u64`. Any change to the newtype that breaks repr(transparent) + // or adds non-Pod fields will fail this gate. + use core::mem::{align_of, size_of}; + + assert_eq!( + size_of::(), + size_of::(), + "Frs size drifted from u64" + ); + assert_eq!( + align_of::(), + align_of::(), + "Frs alignment drifted from u64" + ); + // Compare bytes-of-Frs against bytes-of-u64 directly so the + // assertion is endianness-agnostic by construction (clippy's + // host_endian_bytes lint forbids `to_ne_bytes` here, and + // picking `to_le_bytes` / `to_be_bytes` would falsely imply + // we care which endianness the in-memory layout has — we + // only care that Frs's layout matches u64's). + let raw: u64 = 0xDEAD_BEEF_CAFE_F00D; + assert_eq!(bytemuck::bytes_of(&Frs::new(raw)), bytemuck::bytes_of(&raw)); + let zero: Frs = bytemuck::Zeroable::zeroed(); + assert_eq!(zero, Frs::ZERO); + } + + #[test] + fn parent_frs_is_pod_zeroable_with_u64_layout() { + use core::mem::{align_of, size_of}; + + assert_eq!( + size_of::(), + size_of::(), + "ParentFrs size drifted from u64" + ); + assert_eq!( + align_of::(), + align_of::(), + "ParentFrs alignment drifted from u64" + ); + // Same endianness-agnostic comparison as `frs_is_pod_zeroable_…`. + let raw: u64 = 0xDEAD_BEEF_CAFE_F00D; + assert_eq!( + bytemuck::bytes_of(&ParentFrs::new(raw)), + bytemuck::bytes_of(&raw) + ); + let zero: ParentFrs = bytemuck::Zeroable::zeroed(); + assert_eq!(zero, ParentFrs::ZERO); + } + #[test] fn parent_frs_does_not_coerce_to_frs_silently() { // Compile-time contract: this test exists primarily as diff --git a/crates/uffs-mft/src/index/base.rs b/crates/uffs-mft/src/index/base.rs index c00ba85af..42b009bb8 100644 --- a/crates/uffs-mft/src/index/base.rs +++ b/crates/uffs-mft/src/index/base.rs @@ -7,6 +7,7 @@ use super::{ ChildInfo, ExtensionIndex, ExtensionTable, FileRecord, IndexNameRef, IndexStreamInfo, LinkInfo, MftIndex, MftStats, NO_ENTRY, frs_to_usize, len_to_u32, }; +use crate::frs::Frs; use crate::platform::DriveLetter; /// Returns the current Unix-microsecond timestamp for `build_epoch`. @@ -131,9 +132,12 @@ impl MftIndex { for record in &self.records { stats.record_count += 1; - // Track max FRS - if record.frs > stats.max_frs { - stats.max_frs = record.frs; + // Track max FRS — `MftStats::max_frs` is still raw `u64` (DTO + // surface scheduled for 5d.3); cross the typed -> raw boundary + // explicitly via `Frs::raw()`. + let frs_raw = record.frs.raw(); + if frs_raw > stats.max_frs { + stats.max_frs = frs_raw; } // Get file size from first stream @@ -189,14 +193,15 @@ impl MftIndex { stats.ads_count += 1; } - // System metafile detection - if record.frs <= SYSTEM_METAFILE_MAX_FRS && record.frs != ROOT_FRS_LOCAL { + // System metafile detection (typed -> raw at the numeric + // comparison boundary). + if frs_raw <= SYSTEM_METAFILE_MAX_FRS && frs_raw != ROOT_FRS_LOCAL { stats.system_metafile_count += 1; } - // Child of system metafile detection - let parent_frs = record.first_name.parent_frs; - if parent_frs <= SYSTEM_METAFILE_MAX_FRS && parent_frs != ROOT_FRS_LOCAL { + // Child of system metafile detection. + let parent_frs_raw = record.first_name.parent_frs.raw(); + if parent_frs_raw <= SYSTEM_METAFILE_MAX_FRS && parent_frs_raw != ROOT_FRS_LOCAL { stats.system_child_count += 1; } @@ -215,8 +220,8 @@ impl MftIndex { clippy::indexing_slicing, reason = "bounds checked: resize ensures frs_usize < len" )] - pub fn get_or_create(&mut self, frs: u64) -> &mut FileRecord { - let frs_usize = frs_to_usize(frs); + pub fn get_or_create(&mut self, frs: Frs) -> &mut FileRecord { + let frs_usize = frs_to_usize(frs.raw()); // Expand lookup table if needed if frs_usize >= self.frs_to_idx.len() { @@ -246,8 +251,8 @@ impl MftIndex { clippy::indexing_slicing, reason = "bounds checked: resize ensures frs_usize < len" )] - pub fn get_or_create_unified(&mut self, frs: u64) -> &mut FileRecord { - let frs_usize = frs_to_usize(frs); + pub fn get_or_create_unified(&mut self, frs: Frs) -> &mut FileRecord { + let frs_usize = frs_to_usize(frs.raw()); // Expand lookup table if needed if frs_usize >= self.frs_to_idx.len() { @@ -293,8 +298,8 @@ impl MftIndex { clippy::indexing_slicing, reason = "bounds checked: resize ensures frs_usize < len" )] - pub(crate) fn ensure_record(&mut self, frs: u64) -> u32 { - let frs_usize = frs_to_usize(frs); + pub(crate) fn ensure_record(&mut self, frs: Frs) -> u32 { + let frs_usize = frs_to_usize(frs.raw()); if frs_usize >= self.frs_to_idx.len() { self.frs_to_idx.resize(frs_usize + 1, NO_ENTRY); @@ -311,11 +316,11 @@ impl MftIndex { } } - /// Find a record by FRS (returns None if not present) + /// Find a record by FRS (returns None if not present). #[inline] #[must_use] - pub fn find(&self, frs: u64) -> Option<&FileRecord> { - let frs_usize = frs_to_usize(frs); + pub fn find(&self, frs: Frs) -> Option<&FileRecord> { + let frs_usize = frs_to_usize(frs.raw()); let idx = *self.frs_to_idx.get(frs_usize)?; if idx == NO_ENTRY { None @@ -444,8 +449,8 @@ impl MftIndex { /// Convert FRS to record index (returns None if not present). #[inline] #[must_use] - pub fn frs_to_idx_opt(&self, frs: u64) -> Option { - let frs_usize = frs_to_usize(frs); + pub fn frs_to_idx_opt(&self, frs: Frs) -> Option { + let frs_usize = frs_to_usize(frs.raw()); let idx = *self.frs_to_idx.get(frs_usize)?; if idx == NO_ENTRY { None diff --git a/crates/uffs-mft/src/index/builder.rs b/crates/uffs-mft/src/index/builder.rs index 3a070a4e5..0c142bddd 100644 --- a/crates/uffs-mft/src/index/builder.rs +++ b/crates/uffs-mft/src/index/builder.rs @@ -8,6 +8,7 @@ use super::{ InternalStreamInfo, LinkInfo, MftIndex, NO_ENTRY, SizeInfo, StandardInfo, frs_to_usize, len_to_u16, len_to_u32, }; +use crate::frs::ParentFrs; // ============================================================================ // Building MftIndex from ParsedRecords (Cross-Platform) @@ -77,23 +78,26 @@ impl MftIndex { continue; } - // Parse → index typed-to-raw boundary. The parse layer - // (sub-phase 5d.1) hands us typed `Frs` / `ParentFrs` - // values; the index storage (`FileRecord.frs`, - // `ChildInfo.child_frs`, `LinkInfo.parent_frs`, …) is - // still `u64`. Sub-phase 5d.2 will migrate the index - // side; until then we demote once at loop entry so the - // body keeps a single typed→raw boundary point with a - // clear citation. - let parsed_frs = parsed.frs.raw(); - let parsed_parent_frs = parsed.parent_frs.raw(); - let parsed_base_frs = parsed.base_frs.raw(); + // Sub-phase 5d.2: typed `Frs` / `ParentFrs` from the parse + // layer flow directly into the typed `FileRecord.frs`, + // `FileRecord.base_frs`, `LinkInfo.parent_frs`, and + // `ChildInfo.child_frs` fields below. The historic + // typed→raw boundary that lived here in sub-phase 5d.1 is + // gone — only numeric-comparison and `Vec` resize sites + // call `.raw()` now. + let parsed_frs = parsed.frs; + let parsed_parent_frs = parsed.parent_frs; + let parsed_base_frs = parsed.base_frs; // === Collect stats (cheap - just incrementing counters) === + // `MftStats::max_frs` is a raw `u64` DTO field (5d.3 surface); + // `.raw()` once here at the numeric-comparison boundary. + let parsed_frs_raw = parsed_frs.raw(); + let parsed_parent_frs_raw = parsed_parent_frs.raw(); index.stats.record_count += 1; index.stats.total_name_bytes += parsed.name.len() as u64; - if parsed_frs > index.stats.max_frs { - index.stats.max_frs = parsed_frs; + if parsed_frs_raw > index.stats.max_frs { + index.stats.max_frs = parsed_frs_raw; } if parsed.is_directory { index.stats.dir_count += 1; @@ -107,11 +111,13 @@ impl MftIndex { index.stats.ads_count += 1; } // System metafile detection - if parsed_frs <= SYSTEM_METAFILE_MAX_FRS && parsed_frs != ROOT_FRS_LOCAL { + if parsed_frs_raw <= SYSTEM_METAFILE_MAX_FRS && parsed_frs_raw != ROOT_FRS_LOCAL { index.stats.system_metafile_count += 1; } // Child of system metafile detection - if parsed_parent_frs <= SYSTEM_METAFILE_MAX_FRS && parsed_parent_frs != ROOT_FRS_LOCAL { + if parsed_parent_frs_raw <= SYSTEM_METAFILE_MAX_FRS + && parsed_parent_frs_raw != ROOT_FRS_LOCAL + { index.stats.system_child_count += 1; } @@ -192,18 +198,16 @@ impl MftIndex { } } // End record borrow here - // Store additional names (hardlinks) in the links vector - // Skip the name that matches first_name (the primary/best name) - // Note: parsed.name is the BEST name (selected by PrimaryNameTracker), - // which may not be parsed.names[0]. We must filter by matching name+parent. + // Store additional names (hardlinks) in the links vector. + // Skip the name that matches first_name (the primary/best name). + // Note: `parsed.name` is the BEST name (selected by `PrimaryNameTracker`), + // which may not be `parsed.names[0]`. Filter by matching name+parent; + // the parent comparison is now structural typed-`ParentFrs` equality. let additional_names: Vec<_> = parsed .names .iter() .filter(|n| !(n.name == parsed.name && n.parent_frs == parsed.parent_frs)) .collect(); - // (`n.parent_frs == parsed.parent_frs` compares two `ParentFrs` - // values — the typed equality is a structural compile-time win - // over the prior raw `u64` comparison.) // Update name_count to reflect actual stored names (1 primary + additional) // This must be done AFTER filtering to avoid counting duplicates @@ -224,8 +228,8 @@ impl MftIndex { next_entry: prev_link_idx, name: IndexNameRef::new(extra_offset, extra_len, extra_ascii, extra_ext_id), _pad0: [0; 4], - // `LinkInfo.parent_frs` is still `u64` (migrated in 5d.2). - parent_frs: extra_name.parent_frs.raw(), + // `LinkInfo.parent_frs` is typed `ParentFrs` (5d.2). + parent_frs: extra_name.parent_frs, }); prev_link_idx = link_idx; } @@ -347,24 +351,21 @@ impl MftIndex { } // Build parent-child relationships for all hard links. - // Each $FILE_NAME attribute gets its own child edge so tree metrics - // can attribute proportional shares correctly. - // Each child entry stores its name_index so we can calculate proportional - // shares. + // Each `$FILE_NAME` attribute gets its own child edge so tree + // metrics can attribute proportional shares correctly. + // Each child entry stores its name_index so we can calculate + // proportional shares. + let no_entry_parent = ParentFrs::new(u64::from(NO_ENTRY)); for (name_idx, name_info) in parsed.names.iter().enumerate() { - // Per-name boundary: `NameInfo.parent_frs` is typed - // (`ParentFrs`); the parent walker below indexes the - // `frs_to_idx` Vec by `usize` and stores `u64` in - // `ChildInfo` / `FileRecord` — all 5d.2-scope u64 - // surfaces. - let parent_frs = name_info.parent_frs.raw(); - if parent_frs == parsed_frs || parent_frs == u64::from(NO_ENTRY) { + let parent_frs = name_info.parent_frs; + if parent_frs.as_frs() == parsed_frs || parent_frs == no_entry_parent { continue; } - // Ensure parent exists + // Ensure parent exists — `frs_to_idx` is `Vec` indexed + // by `usize`, so `.raw()` only at the indexing boundary. let parent_idx = { - let parent_frs_usize = frs_to_usize(parent_frs); + let parent_frs_usize = frs_to_usize(parent_frs.raw()); if parent_frs_usize >= index.frs_to_idx.len() { index.frs_to_idx.resize(parent_frs_usize + 1, NO_ENTRY); } @@ -372,7 +373,7 @@ impl MftIndex { // Create placeholder parent let new_idx = len_to_u32(index.records.len()); index.frs_to_idx[parent_frs_usize] = new_idx; - index.records.push(FileRecord::new(parent_frs)); + index.records.push(FileRecord::new(parent_frs.as_frs())); } index.frs_to_idx[parent_frs_usize] }; @@ -396,20 +397,22 @@ impl MftIndex { }); } - // Handle case where names is empty (shouldn't happen, but be safe) + // Handle case where names is empty (shouldn't happen, but be safe). if parsed.names.is_empty() - && parsed_parent_frs != parsed_frs - && parsed_parent_frs != u64::from(NO_ENTRY) + && parsed_parent_frs.as_frs() != parsed_frs + && parsed_parent_frs != no_entry_parent { let parent_idx = { - let parent_frs_usize = frs_to_usize(parsed_parent_frs); + let parent_frs_usize = frs_to_usize(parsed_parent_frs.raw()); if parent_frs_usize >= index.frs_to_idx.len() { index.frs_to_idx.resize(parent_frs_usize + 1, NO_ENTRY); } if index.frs_to_idx[parent_frs_usize] == NO_ENTRY { let new_idx = len_to_u32(index.records.len()); index.frs_to_idx[parent_frs_usize] = new_idx; - index.records.push(FileRecord::new(parsed_parent_frs)); + index + .records + .push(FileRecord::new(parsed_parent_frs.as_frs())); } index.frs_to_idx[parent_frs_usize] }; diff --git a/crates/uffs-mft/src/index/child_order.rs b/crates/uffs-mft/src/index/child_order.rs index 619db5099..938f466a4 100644 --- a/crates/uffs-mft/src/index/child_order.rs +++ b/crates/uffs-mft/src/index/child_order.rs @@ -4,6 +4,7 @@ //! Directory child-link maintenance and stable child ordering helpers. use super::{ChildInfo, FileRecord, MftIndex, NO_ENTRY, frs_to_usize, len_to_u16, len_to_u32}; +use crate::frs::{Frs, ParentFrs}; impl MftIndex { /// Add a child entry to a parent directory. @@ -12,18 +13,25 @@ impl MftIndex { /// Used for building parent-child relationships for tree metrics. /// /// # Arguments - /// * `parent_frs` - FRS of the parent directory - /// * `child_frs` - FRS of the child file/directory + /// * `parent_frs` - typed parent-directory FRS (closes the historic + /// `(parent_frs, own_frs)` swap hazard at the type system). + /// * `child_frs` - typed FRS of the child file/directory. /// * `name_index` - Which hardlink this is (0 for primary, 1+ for /// additional) /// /// If the parent record does not exist yet, this creates a placeholder - /// record so child entries are preserved even when chunks are processed + /// record so child edges are preserved even when chunks are processed /// out of order. - pub(crate) fn add_child_entry(&mut self, parent_frs: u64, child_frs: u64, name_index: u16) { + pub(crate) fn add_child_entry( + &mut self, + parent_frs: ParentFrs, + child_frs: Frs, + name_index: u16, + ) { // Create a parent placeholder if it does not exist yet so // child edges are not dropped during out-of-order processing. - let parent_frs_usize = frs_to_usize(parent_frs); + let parent_frs_as_frs = parent_frs.as_frs(); + let parent_frs_usize = frs_to_usize(parent_frs_as_frs.raw()); // Expand lookup table if needed if parent_frs_usize >= self.frs_to_idx.len() { @@ -37,7 +45,7 @@ impl MftIndex { let parent_idx = if *frs_slot == NO_ENTRY { let new_idx = len_to_u32(self.records.len()); *frs_slot = new_idx; - self.records.push(FileRecord::new(parent_frs)); + self.records.push(FileRecord::new(parent_frs_as_frs)); new_idx as usize } else { *frs_slot as usize @@ -99,6 +107,8 @@ impl MftIndex { for (parent_frs, child_frs, name_index) in edges { self.add_child_entry(parent_frs, child_frs, name_index); } + // typed `(ParentFrs, Frs, u16)` edges flow through without further + // conversion — see `collect_parent_child_edges` below. tracing::debug!( children = self.children.len(), "[TRIP] MftIndex::rebuild_children_from_names EXIT" @@ -107,9 +117,9 @@ impl MftIndex { /// Walk every record's link chain and collect `(parent_frs, child_frs, /// name_index)` edges for child-list reconstruction. - fn collect_parent_child_edges(&self) -> Vec<(u64, u64, u16)> { - let no_entry_frs: u64 = u64::from(NO_ENTRY); - let mut edges: Vec<(u64, u64, u16)> = + fn collect_parent_child_edges(&self) -> Vec<(ParentFrs, Frs, u16)> { + let no_entry_parent = ParentFrs::new(u64::from(NO_ENTRY)); + let mut edges: Vec<(ParentFrs, Frs, u16)> = Vec::with_capacity(self.records.len().saturating_mul(2)); for rec in &self.records { @@ -121,8 +131,10 @@ impl MftIndex { let parent_frs = current_link.parent_frs; // Skip missing/placeholder parents and self-references - // (root has parent == self). - if parent_frs != no_entry_frs && parent_frs != child_frs { + // (root has parent == self). The typed parent-vs-own + // boundary is enforced via `ParentFrs::as_frs()` for + // the self-reference comparison. + if parent_frs != no_entry_parent && parent_frs.as_frs() != child_frs { // Remap list index → parse index (link chain is stored // in reverse encounter order). let parse_index = len_to_u16(name_count - 1 - name_index); diff --git a/crates/uffs-mft/src/index/dataframe.rs b/crates/uffs-mft/src/index/dataframe.rs index d35528990..5022596ab 100644 --- a/crates/uffs-mft/src/index/dataframe.rs +++ b/crates/uffs-mft/src/index/dataframe.rs @@ -130,10 +130,12 @@ impl MftIndex { }; // Extract data from records for rec in &self.records { - frs.push(rec.frs); + // Polars-bound `Vec` columns: demote at the push site. + // `frs` column is still `u64` on the DataFrame surface (5d.4). + frs.push(rec.frs.raw()); seq.push(rec.sequence_number); lsn.push(rec.lsn); - parent.push(rec.first_name.parent_frs); + parent.push(rec.first_name.parent_frs.raw()); name.push(self.record_name(rec).to_owned()); ns.push(rec.namespace); size.push(rec.first_stream.size.length); @@ -176,7 +178,7 @@ impl MftIndex { is_deleted.push(rec.is_deleted()); is_corrupt.push(rec.is_corrupt()); is_extension.push(rec.is_extension()); - base_frs_col.push(rec.base_frs); + base_frs_col.push(rec.base_frs.raw()); } // Tree metrics (pre-computed via compute_tree_metrics()) // Use the tree_metrics() method as the single source of truth (Fix #3) @@ -185,6 +187,7 @@ impl MftIndex { treesize.push(ts); tree_allocated.push(ta); path.push(self.build_path(rec.frs)); + // typed `Frs` flows directly into `build_path` (5d.2). } // Build DataFrame let dt = DataType::Datetime(TimeUnit::Microseconds, None); diff --git a/crates/uffs-mft/src/index/fragment.rs b/crates/uffs-mft/src/index/fragment.rs index b8a299744..66b74e535 100644 --- a/crates/uffs-mft/src/index/fragment.rs +++ b/crates/uffs-mft/src/index/fragment.rs @@ -7,6 +7,7 @@ use super::{ ChildInfo, ExtensionTable, FileRecord, IndexStreamInfo, InternalStreamInfo, LinkInfo, NO_ENTRY, frs_to_usize, len_to_u32, }; +use crate::frs::Frs; // ============================================================================ // MftIndexFragment - Partial index for parallel parsing @@ -84,8 +85,8 @@ impl MftIndexFragment { clippy::indexing_slicing, reason = "bounds checked: resize ensures frs_usize < len" )] - pub fn get_or_create(&mut self, frs: u64) -> &mut FileRecord { - let frs_usize = frs_to_usize(frs); + pub fn get_or_create(&mut self, frs: Frs) -> &mut FileRecord { + let frs_usize = frs_to_usize(frs.raw()); // Expand lookup table if needed if frs_usize >= self.frs_to_idx.len() { diff --git a/crates/uffs-mft/src/index/merge.rs b/crates/uffs-mft/src/index/merge.rs index 766261f23..6e33e7eea 100644 --- a/crates/uffs-mft/src/index/merge.rs +++ b/crates/uffs-mft/src/index/merge.rs @@ -146,8 +146,11 @@ impl MftIndex { let mut deferred_merges: Vec<(u32, FileRecord)> = Vec::new(); for mut record in records { + // `record.frs` is typed `Frs`; the `frs_to_idx` lookup table is + // `Vec` indexed by `usize`, so demote via `.raw()` at the + // indexing boundary only. let frs = record.frs; - let frs_usize = usize::try_from(frs).unwrap_or(usize::MAX); + let frs_usize = usize::try_from(frs.raw()).unwrap_or(usize::MAX); Self::adjust_name_ref( &mut record.first_name.name, diff --git a/crates/uffs-mft/src/index/model.rs b/crates/uffs-mft/src/index/model.rs index 886fd2ecc..05c48a2fc 100644 --- a/crates/uffs-mft/src/index/model.rs +++ b/crates/uffs-mft/src/index/model.rs @@ -7,6 +7,7 @@ use super::{ ExtensionIndex, ExtensionTable, FileRecord, IndexStreamInfo, InternalStreamInfo, LinkInfo, MftStats, }; +use crate::frs::Frs; use crate::platform::DriveLetter; /// Directory child entry. @@ -14,6 +15,9 @@ use crate::platform::DriveLetter; /// 24 bytes per entry (with explicit padding). Derives `bytemuck::Pod` /// so the entire children array can be serialized/deserialized as a single /// `memcpy` (v11+). +/// +/// The [`Frs`] newtype is `#[repr(transparent)]` over `u64`, so the on-disk +/// layout is byte-identical to the historic `u64` `child_frs` field. #[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct ChildInfo { @@ -25,8 +29,9 @@ pub struct ChildInfo { reason = "bytemuck Pod requires all fields same visibility" )] pub _pad0: [u8; 4], - /// FRS of the child file or directory. - pub child_frs: u64, + /// FRS of the child file or directory (typed [`Frs`]; bit-identical + /// to `u64` on disk). + pub child_frs: Frs, /// Which name index to use for hard links. pub name_index: u16, /// Explicit tail padding for struct alignment (8-byte boundary). diff --git a/crates/uffs-mft/src/index/path_resolver.rs b/crates/uffs-mft/src/index/path_resolver.rs index d60369124..b9ea5a1bd 100644 --- a/crates/uffs-mft/src/index/path_resolver.rs +++ b/crates/uffs-mft/src/index/path_resolver.rs @@ -6,7 +6,8 @@ //! //! Extracted from `paths.rs` to keep it under the 800 LOC threshold. -use super::{FileRecord, MftIndex, NO_ENTRY, ROOT_FRS}; +use super::{FileRecord, MftIndex, NO_ENTRY}; +use crate::frs::{Frs, ParentFrs}; // ============================================================================ // PathResolver - Ultra-fast path validity and on-demand materialization @@ -77,7 +78,7 @@ impl PathResolver { /// Check if a record with the given FRS is valid. #[must_use] - pub fn is_valid(&self, index: &MftIndex, frs: u64) -> bool { + pub fn is_valid(&self, index: &MftIndex, frs: Frs) -> bool { index .frs_to_idx_opt(frs) .is_some_and(|idx| self.is_valid_idx(idx)) @@ -158,13 +159,13 @@ impl PathResolver { chain.push(current_idx); let parent_frs = record.first_name.parent_frs; - if parent_frs == ROOT_FRS - || parent_frs == record.frs - || parent_frs == u64::from(NO_ENTRY) + if parent_frs.is_root() + || parent_frs.as_frs() == record.frs + || parent_frs == ParentFrs::new(u64::from(NO_ENTRY)) { break; } - let Some(parent_idx) = index.frs_to_idx_opt(parent_frs) else { + let Some(parent_idx) = index.frs_to_idx_opt(parent_frs.as_frs()) else { break; }; current_idx = parent_idx; @@ -243,13 +244,13 @@ impl PathResolver { chain.push(current_idx); let parent_frs = record.first_name.parent_frs; - if parent_frs == ROOT_FRS - || parent_frs == record.frs - || parent_frs == u64::from(NO_ENTRY) + if parent_frs.is_root() + || parent_frs.as_frs() == record.frs + || parent_frs == ParentFrs::new(u64::from(NO_ENTRY)) { break; } - let Some(parent_idx) = index.frs_to_idx_opt(parent_frs) else { + let Some(parent_idx) = index.frs_to_idx_opt(parent_frs.as_frs()) else { break; }; current_idx = parent_idx; @@ -303,13 +304,13 @@ impl PathResolver { chain.push(current_idx); let parent_frs = record.first_name.parent_frs; - if parent_frs == ROOT_FRS - || parent_frs == record.frs - || parent_frs == u64::from(NO_ENTRY) + if parent_frs.is_root() + || parent_frs.as_frs() == record.frs + || parent_frs == ParentFrs::new(u64::from(NO_ENTRY)) { break; } - let Some(parent_idx) = index.frs_to_idx_opt(parent_frs) else { + let Some(parent_idx) = index.frs_to_idx_opt(parent_frs.as_frs()) else { break; }; current_idx = parent_idx; @@ -365,9 +366,9 @@ impl PathResolver { }; let parent_frs = link.parent_frs; - let parent_path = if let Some(pidx) = index.frs_to_idx_opt(parent_frs) { + let parent_path = if let Some(pidx) = index.frs_to_idx_opt(parent_frs.as_frs()) { self.materialize_path(index, pidx) - } else if parent_frs == ROOT_FRS { + } else if parent_frs.is_root() { // Normalize root to "X:\\" (not "X:") so hardlink paths keep // standard absolute-drive semantics. let mut root_path = String::with_capacity(3); @@ -398,8 +399,11 @@ impl PathResolver { /// Mark system metafiles (FRS 0-15 except root) as invalid. fn mark_system_metafiles_invalid(&mut self, index: &MftIndex) { for (idx, record) in index.records.iter().enumerate() { - if record.frs <= SYSTEM_METAFILE_MAX_FRS - && record.frs != ROOT_FRS + // System metafiles are FRS 0-15 except root — typed comparison + // uses `.raw()` only at the numeric range check. + let frs_raw = record.frs.raw(); + if frs_raw <= SYSTEM_METAFILE_MAX_FRS + && !record.frs.is_root() && let Some(state) = self.state.get_mut(idx) { *state = path_state::INVALID; @@ -433,6 +437,7 @@ impl PathResolver { && let Some(state) = self.state.get_mut(child_idx) && *state == path_state::UNSEEN { + // typed `Frs` flows through `frs_to_idx_opt` directly. *state = path_state::INVALID; self.invalid_count += 1; queue.push_back(child_idx); @@ -471,17 +476,19 @@ impl PathResolver { let parent_frs = record.first_name.parent_frs; - if parent_frs == ROOT_FRS { + if parent_frs.is_root() { break path_state::VALID; } - if parent_frs == record.frs || parent_frs == u64::from(NO_ENTRY) { - if record.frs == ROOT_FRS { + if parent_frs.as_frs() == record.frs + || parent_frs == ParentFrs::new(u64::from(NO_ENTRY)) + { + if record.frs.is_root() { break path_state::VALID; } break path_state::INVALID; } - let Some(parent_idx) = index.frs_to_idx_opt(parent_frs) else { + let Some(parent_idx) = index.frs_to_idx_opt(parent_frs.as_frs()) else { break path_state::INVALID; }; current_idx = parent_idx; @@ -557,7 +564,7 @@ impl<'a> PathCache<'a> { /// Get the path for a record (materializes on demand). #[must_use] - pub fn get(&self, frs: u64) -> Option { + pub fn get(&self, frs: Frs) -> Option { let idx = self.index.frs_to_idx_opt(frs)?; self.resolver .is_valid_idx(idx) @@ -566,13 +573,13 @@ impl<'a> PathCache<'a> { /// Check if a record is valid (has a path, not illegal). #[must_use] - pub fn is_valid(&self, frs: u64) -> bool { + pub fn is_valid(&self, frs: Frs) -> bool { self.resolver.is_valid(self.index, frs) } /// Check if a record is illegal (filtered out). #[must_use] - pub fn is_illegal(&self, frs: u64) -> bool { + pub fn is_illegal(&self, frs: Frs) -> bool { self.index .frs_to_idx_opt(frs) .is_some_and(|idx| !self.resolver.is_valid_idx(idx)) diff --git a/crates/uffs-mft/src/index/paths.rs b/crates/uffs-mft/src/index/paths.rs index b0fbea6ed..7cd1bdea6 100644 --- a/crates/uffs-mft/src/index/paths.rs +++ b/crates/uffs-mft/src/index/paths.rs @@ -7,7 +7,8 @@ //! extraction to `index/path_resolver.rs` in Wave 5 — see //! `docs/architecture/FILE_SIZE_REFACTOR_WAVES.md`. -use super::{FileRecord, IndexStreamInfo, LinkInfo, MftIndex, NO_ENTRY, ROOT_FRS}; +use super::{FileRecord, IndexStreamInfo, LinkInfo, MftIndex, NO_ENTRY}; +use crate::frs::{Frs, ParentFrs}; // ============================================================================ // Name/Stream Iteration (for hard link and ADS expansion) @@ -215,21 +216,24 @@ impl MftIndex { components.push(name.to_owned()); } - // Walk up the parent chain from this name's parent - let mut current_frs = name_info.parent_frs; + // Walk up the parent chain from this name's parent. + // `name_info.parent_frs` is typed `ParentFrs`; demote with + // `.as_frs()` only at the `find()` call site. + let no_entry_parent = ParentFrs::new(u64::from(NO_ENTRY)); + let mut current_parent = name_info.parent_frs; - while current_frs != u64::from(NO_ENTRY) && current_frs != ROOT_FRS { - if let Some(parent_record) = self.find(current_frs) { + while current_parent != no_entry_parent && !current_parent.is_root() { + if let Some(parent_record) = self.find(current_parent.as_frs()) { let parent_name = self.record_name(parent_record); if !parent_name.is_empty() && parent_name != "." { components.push(parent_name.to_owned()); } - let parent_frs = parent_record.first_name.parent_frs; - if parent_frs == u64::from(NO_ENTRY) || parent_frs == current_frs { + let next_parent = parent_record.first_name.parent_frs; + if next_parent == no_entry_parent || next_parent == current_parent { break; } - current_frs = parent_frs; + current_parent = next_parent; } else { break; } @@ -271,26 +275,27 @@ impl MftIndex { /// /// This is done on-demand (not stored) to save memory. #[must_use] - pub fn build_path(&self, frs: u64) -> String { + pub fn build_path(&self, frs: Frs) -> String { let mut components = Vec::new(); let mut current_frs = frs; + let no_entry_parent = ParentFrs::new(u64::from(NO_ENTRY)); - // Walk up the parent chain + // Walk up the parent chain. while let Some(record) = self.find(current_frs) { let name = self.record_name(record); if !name.is_empty() && name != "." { components.push(name.to_owned()); } - // Move to parent + // Move to parent. let parent_frs = record.first_name.parent_frs; - if parent_frs == u64::from(NO_ENTRY) || parent_frs == current_frs { + if parent_frs == no_entry_parent || parent_frs.as_frs() == current_frs { break; // Root or self-reference } - if parent_frs == ROOT_FRS { + if parent_frs.is_root() { break; // Reached root } - current_frs = parent_frs; + current_frs = parent_frs.as_frs(); } // Reverse and join with a standard drive-qualified backslash path. diff --git a/crates/uffs-mft/src/index/storage/deserialize.rs b/crates/uffs-mft/src/index/storage/deserialize.rs index 19d971e49..3204c779e 100644 --- a/crates/uffs-mft/src/index/storage/deserialize.rs +++ b/crates/uffs-mft/src/index/storage/deserialize.rs @@ -297,7 +297,11 @@ impl MftIndex { let fn_mft_changed = if version >= 4 { read_i64!() } else { 0 }; records.push(FileRecord { - frs, + // Legacy v3-v9 deserialization boundary: raw `u64` from + // disk is lifted into typed `Frs` at the struct-literal + // construction site. v10+ Pod paths above are + // bit-identical via `repr(transparent)`. + frs: crate::frs::Frs::new(frs), sequence_number, namespace, forensic_flags, @@ -305,7 +309,7 @@ impl MftIndex { lsn, reparse_tag, _pad1: [0; 4], - base_frs, + base_frs: crate::frs::Frs::new(base_frs), stdinfo: StandardInfo { created, modified, @@ -330,7 +334,7 @@ impl MftIndex { meta: link_name_meta, }, _pad0: [0; 4], - parent_frs: link_parent_frs, + parent_frs: crate::frs::ParentFrs::new(link_parent_frs), }, first_stream: IndexStreamInfo { size: SizeInfo { @@ -420,7 +424,7 @@ impl MftIndex { meta: name_meta, }, _pad0: [0; 4], - parent_frs, + parent_frs: crate::frs::ParentFrs::new(parent_frs), }); } links @@ -526,7 +530,7 @@ impl MftIndex { children.push(ChildInfo { next_entry, _pad0: [0; 4], - child_frs, + child_frs: crate::frs::Frs::new(child_frs), name_index, _pad1: [0; 6], }); diff --git a/crates/uffs-mft/src/index/tests_ads.rs b/crates/uffs-mft/src/index/tests_ads.rs index 374045f93..1124527d8 100644 --- a/crates/uffs-mft/src/index/tests_ads.rs +++ b/crates/uffs-mft/src/index/tests_ads.rs @@ -15,9 +15,9 @@ fn create_index_with_ads() -> MftIndex { // Create a file record (FRS 100) let name_ref = push_index_name(&mut index, "test.pdf"); - let ri = index.get_or_create(100); + let ri = index.get_or_create(100.into()); ri.first_name.name = name_ref; - ri.first_name.parent_frs = 5; + ri.first_name.parent_frs = Into::into(5); ri.set_has_default_data(); ri.first_stream.size.length = 1024; ri.first_stream.size.allocated = 4096; @@ -46,7 +46,7 @@ fn create_index_with_ads() -> MftIndex { }); // Chain ADS to the record's stream list - let rec_idx = index.frs_to_idx_opt(100).unwrap(); + let rec_idx = index.frs_to_idx_opt(100.into()).unwrap(); let rec = &mut index.records[rec_idx]; rec.first_stream.next_entry = ads_si; rec.stream_count += 1; @@ -58,7 +58,7 @@ fn create_index_with_ads() -> MftIndex { #[test] fn ads_stream_count_includes_ads() { let index = create_index_with_ads(); - let ri = index.frs_to_idx_opt(100).unwrap(); + let ri = index.frs_to_idx_opt(100.into()).unwrap(); let rec = &index.records[ri]; // File should have 2 streams: default $DATA + Zone.Identifier ADS @@ -72,7 +72,7 @@ fn ads_stream_count_includes_ads() { #[test] fn ads_stream_iterable_via_iter_streams() { let index = create_index_with_ads(); - let ri = index.frs_to_idx_opt(100).unwrap(); + let ri = index.frs_to_idx_opt(100.into()).unwrap(); let rec = &index.records[ri]; let streams: Vec<(u16, &IndexStreamInfo)> = index.iter_streams(rec).collect(); @@ -99,7 +99,7 @@ fn ads_stream_iterable_via_iter_streams() { #[test] fn ads_stream_accessible_via_get_stream_at() { let index = create_index_with_ads(); - let ri = index.frs_to_idx_opt(100).unwrap(); + let ri = index.frs_to_idx_opt(100.into()).unwrap(); let rec = &index.records[ri]; // Index 0: default $DATA diff --git a/crates/uffs-mft/src/index/tests_children.rs b/crates/uffs-mft/src/index/tests_children.rs index 30b41b061..d48101dd7 100644 --- a/crates/uffs-mft/src/index/tests_children.rs +++ b/crates/uffs-mft/src/index/tests_children.rs @@ -11,7 +11,7 @@ fn sort_directory_children_basic() { // Create a directory (FRS 100) let dir_frs = 100_u64; - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.stdinfo.set_directory(true); // Create child files with unsorted names @@ -24,16 +24,16 @@ fn sort_directory_children_basic() { let offset = index.add_name(name); let ext_id = index.intern_extension(name); - let rec = index.get_or_create(child_frs); + let rec = index.get_or_create(child_frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); - rec.first_name.parent_frs = dir_frs; + rec.first_name.parent_frs = Into::into(dir_frs); // Add child to directory's children list let child_info = ChildInfo { next_entry: NO_ENTRY, _pad0: [0; 4], - child_frs, + child_frs: child_frs.into(), name_index: 0, _pad1: [0; 6], }; @@ -42,7 +42,7 @@ fn sort_directory_children_basic() { // Link to previous child or set as first child if i == 0 { - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.first_child = child_idx; } else { let prev_child_idx = (child_idx - 1) as usize; @@ -55,7 +55,7 @@ fn sort_directory_children_basic() { // Verify children are sorted (case-insensitive) // Expected order: apple.txt, Banana.txt, cherry.txt, zebra.txt - let dir_idx = index.frs_to_idx_opt(dir_frs).unwrap(); + let dir_idx = index.frs_to_idx_opt(dir_frs.into()).unwrap(); let mut current_idx = index.records[dir_idx].first_child; let mut sorted_names = Vec::new(); @@ -81,14 +81,14 @@ fn sort_directory_children_empty() { // Create a directory with no children let dir_frs = 100_u64; - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.stdinfo.set_directory(true); // Sort should not crash index.sort_directory_children(); // Verify first_child is still NO_ENTRY - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); assert_eq!(dir_rec.first_child, NO_ENTRY); } @@ -98,34 +98,34 @@ fn sort_directory_children_single_child() { // Create a directory with one child let dir_frs = 100_u64; - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.stdinfo.set_directory(true); let child_frs = 200_u64; let offset = index.add_name("only_child.txt"); let ext_id = index.intern_extension("only_child.txt"); - let rec = index.get_or_create(child_frs); + let rec = index.get_or_create(child_frs.into()); rec.first_name.name = IndexNameRef::new(offset, 14, true, ext_id); - rec.first_name.parent_frs = dir_frs; + rec.first_name.parent_frs = Into::into(dir_frs); let child_info = ChildInfo { next_entry: NO_ENTRY, _pad0: [0; 4], - child_frs, + child_frs: child_frs.into(), name_index: 0, _pad1: [0; 6], }; let child_idx = u32::try_from(index.children.len()).unwrap(); index.children.push(child_info); - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.first_child = child_idx; // Sort should not crash index.sort_directory_children(); // Verify child is still there - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); assert_eq!(dir_rec.first_child, child_idx); assert_eq!( index.children[usize::try_from(child_idx).unwrap()].next_entry, @@ -141,7 +141,7 @@ fn sort_directory_children_performance() { // Create a directory with 1000 children let dir_frs = 100_u64; - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.stdinfo.set_directory(true); // Add 1000 children with random names @@ -150,15 +150,15 @@ fn sort_directory_children_performance() { let name = format!("file_{:04}.txt", 1000 - i); // Reverse order let offset = index.add_name(&name); let ext_id = index.intern_extension(&name); - let rec = index.get_or_create(child_frs); + let rec = index.get_or_create(child_frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); - rec.first_name.parent_frs = dir_frs; + rec.first_name.parent_frs = Into::into(dir_frs); let child_info = ChildInfo { next_entry: NO_ENTRY, _pad0: [0; 4], - child_frs, + child_frs: child_frs.into(), name_index: 0, _pad1: [0; 6], }; @@ -166,7 +166,7 @@ fn sort_directory_children_performance() { index.children.push(child_info); if i == 0 { - let dir_rec = index.get_or_create(dir_frs); + let dir_rec = index.get_or_create(dir_frs.into()); dir_rec.first_child = child_idx; } else { let prev_child_idx = (child_idx - 1) as usize; @@ -182,7 +182,7 @@ fn sort_directory_children_performance() { println!("Sorted 1000 children in {:?}", elapsed); // Verify first few children are sorted - let dir_idx = index.frs_to_idx_opt(dir_frs).unwrap(); + let dir_idx = index.frs_to_idx_opt(dir_frs.into()).unwrap(); let mut current_idx = index.records[dir_idx].first_child; let mut sorted_names = Vec::new(); diff --git a/crates/uffs-mft/src/index/tests_core.rs b/crates/uffs-mft/src/index/tests_core.rs index 318180268..95e4e9b2f 100644 --- a/crates/uffs-mft/src/index/tests_core.rs +++ b/crates/uffs-mft/src/index/tests_core.rs @@ -40,16 +40,16 @@ fn index_basic_operations() { let mut index = MftIndex::new(crate::platform::DriveLetter::C); // Add a record - let record = index.get_or_create(100); + let record = index.get_or_create(100.into()); record.stdinfo.set_directory(true); // Find it - let found = index.find(100); + let found = index.find(100_u64.into()); assert!(found.is_some()); assert!(found.unwrap().is_directory()); // Not found - assert!(index.find(999).is_none()); + assert!(index.find(999_u64.into()).is_none()); } #[test] diff --git a/crates/uffs-mft/src/index/tests_extensions.rs b/crates/uffs-mft/src/index/tests_extensions.rs index eb5d1dc63..99980389d 100644 --- a/crates/uffs-mft/src/index/tests_extensions.rs +++ b/crates/uffs-mft/src/index/tests_extensions.rs @@ -96,15 +96,15 @@ fn extension_table_serialization() { index.extensions.record_file(ext_id3, 512); // Now create records and set their fields - let record1 = index.get_or_create(100); + let record1 = index.get_or_create(100.into()); record1.stdinfo.set_directory(false); record1.first_name.name = IndexNameRef::new(name1_offset, 8, true, ext_id1); - let record2 = index.get_or_create(101); + let record2 = index.get_or_create(101.into()); record2.stdinfo.set_directory(false); record2.first_name.name = IndexNameRef::new(name2_offset, 7, true, ext_id2); - let record3 = index.get_or_create(102); + let record3 = index.get_or_create(102.into()); record3.stdinfo.set_directory(false); record3.first_name.name = IndexNameRef::new(name3_offset, 11, true, ext_id3); @@ -174,23 +174,23 @@ fn extension_index_build() { let ext_none = index.intern_extension(name5); // Create records - let rec1 = index.get_or_create(100); + let rec1 = index.get_or_create(100.into()); rec1.first_name.name = IndexNameRef::new(offset1, u16::try_from(name1.len()).unwrap(), true, ext_txt); - let rec2 = index.get_or_create(101); + let rec2 = index.get_or_create(101.into()); rec2.first_name.name = IndexNameRef::new(offset2, u16::try_from(name2.len()).unwrap(), true, ext_txt); - let rec3 = index.get_or_create(102); + let rec3 = index.get_or_create(102.into()); rec3.first_name.name = IndexNameRef::new(offset3, u16::try_from(name3.len()).unwrap(), true, ext_rs); - let rec4 = index.get_or_create(103); + let rec4 = index.get_or_create(103.into()); rec4.first_name.name = IndexNameRef::new(offset4, u16::try_from(name4.len()).unwrap(), true, ext_rs); - let rec5 = index.get_or_create(104); + let rec5 = index.get_or_create(104.into()); rec5.first_name.name = IndexNameRef::new(offset5, u16::try_from(name5.len()).unwrap(), true, ext_none); @@ -250,7 +250,7 @@ fn extension_index_with_hard_links() { let link_idx = u32::try_from(index.links.len()).unwrap(); // Create record with primary name - let rec = index.get_or_create(100); + let rec = index.get_or_create(100.into()); rec.first_name.name = IndexNameRef::new(offset1, u16::try_from(name1.len()).unwrap(), true, ext_txt); rec.name_count = 2; @@ -261,7 +261,7 @@ fn extension_index_with_hard_links() { next_entry: NO_ENTRY, name: IndexNameRef::new(offset2, u16::try_from(name2.len()).unwrap(), true, ext_rs), _pad0: [0; 4], - parent_frs: 5, // same parent + parent_frs: Into::into(5_u64), // same parent }); // Build extension index @@ -361,7 +361,7 @@ fn extension_table_top_by_bytes() { let offset = index.add_name(name); let ext_id = index.intern_extension(name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); rec.first_stream.size = SizeInfo { @@ -416,7 +416,7 @@ fn extension_table_top_by_count() { let offset = index.add_name(name); let ext_id = index.intern_extension(name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); rec.first_stream.size = SizeInfo { @@ -469,7 +469,7 @@ fn byte_tracking_accuracy() { let offset = index.add_name(name); let ext_id = index.intern_extension(name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); rec.first_stream.size = SizeInfo { @@ -555,7 +555,7 @@ fn extension_index_performance() { }; let offset = index.add_name(&name); - let rec = index.get_or_create(i); + let rec = index.get_or_create(i.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); } diff --git a/crates/uffs-mft/src/index/tests_helpers.rs b/crates/uffs-mft/src/index/tests_helpers.rs index 36a9c2e2e..f474b5a9c 100644 --- a/crates/uffs-mft/src/index/tests_helpers.rs +++ b/crates/uffs-mft/src/index/tests_helpers.rs @@ -31,7 +31,7 @@ pub(super) fn push_index_name(index: &mut MftIndex, name: &str) -> IndexNameRef /// Resolve a record index from an FRS for test assertions. pub(super) fn record_idx(index: &MftIndex, frs: u64) -> usize { - index.frs_to_idx_opt(frs).unwrap() + index.frs_to_idx_opt(frs.into()).unwrap() } /// Count the number of child edges currently attached to a directory. diff --git a/crates/uffs-mft/src/index/tests_merge.rs b/crates/uffs-mft/src/index/tests_merge.rs index 685f46af0..4edbd0ff8 100644 --- a/crates/uffs-mft/src/index/tests_merge.rs +++ b/crates/uffs-mft/src/index/tests_merge.rs @@ -20,27 +20,33 @@ fn extension_before_base_in_same_fragment() { next_entry: link0_idx + 1, name: name2_ref, _pad0: [0; 4], - parent_frs: 5, + parent_frs: Into::into(5), }); let link1_idx = u32::try_from(fragment.links.len()).unwrap(); fragment.links.push(LinkInfo { next_entry: NO_ENTRY, name: name3_ref, _pad0: [0; 4], - parent_frs: 6, + parent_frs: Into::into(6), }); - let record = fragment.get_or_create(100); + let record = fragment.get_or_create(100.into()); record.first_name.name = name2_ref; - record.first_name.parent_frs = 5; + record.first_name.parent_frs = Into::into(5); record.first_name.next_entry = link1_idx; record.name_count = 2; - assert!(fragment.get_or_create(100).first_name.name.is_valid()); - assert_eq!(fragment.get_or_create(100).name_count, 2); + assert!( + fragment + .get_or_create(100.into()) + .first_name + .name + .is_valid() + ); + assert_eq!(fragment.get_or_create(100.into()).name_count, 2); let name1_ref = push_fragment_name(&mut fragment, "name1.txt"); - let record = fragment.get_or_create(100); + let record = fragment.get_or_create(100.into()); let existing_first_name = record.first_name; let existing_name_valid = existing_first_name.name.is_valid(); let existing_name_count = if existing_name_valid { @@ -53,7 +59,7 @@ fn extension_before_base_in_same_fragment() { next_entry: NO_ENTRY, name: name1_ref, _pad0: [0; 4], - parent_frs: 5, + parent_frs: Into::into(5), }; let first_name_next_entry = if existing_name_valid { @@ -64,11 +70,11 @@ fn extension_before_base_in_same_fragment() { NO_ENTRY }; - let record = fragment.get_or_create(100); + let record = fragment.get_or_create(100.into()); record.first_name.next_entry = first_name_next_entry; record.name_count = 1 + existing_name_count; - let record = fragment.get_or_create(100); + let record = fragment.get_or_create(100.into()); assert_eq!(record.name_count, 3); assert!(record.first_name.name.is_valid()); @@ -91,30 +97,42 @@ fn cross_fragment_merge_extension_placeholder() { let mut fragment_a = MftIndexFragment::with_capacity(10); let ext_name_ref = push_fragment_name(&mut fragment_a, "hardlink.txt"); - let record_a = fragment_a.get_or_create(100); + let record_a = fragment_a.get_or_create(100.into()); record_a.first_name.name = ext_name_ref; - record_a.first_name.parent_frs = 5; + record_a.first_name.parent_frs = Into::into(5); record_a.first_name.next_entry = NO_ENTRY; record_a.name_count = 1; - assert!(fragment_a.get_or_create(100).first_name.name.is_valid()); - assert_eq!(fragment_a.get_or_create(100).stdinfo.created, 0); - assert!(!fragment_a.get_or_create(100).has_base_data()); + assert!( + fragment_a + .get_or_create(100.into()) + .first_name + .name + .is_valid() + ); + assert_eq!(fragment_a.get_or_create(100.into()).stdinfo.created, 0); + assert!(!fragment_a.get_or_create(100.into()).has_base_data()); let mut fragment_b = MftIndexFragment::with_capacity(10); let base_name_ref = push_fragment_name(&mut fragment_b, "original.txt"); - let record_b = fragment_b.get_or_create(100); + let record_b = fragment_b.get_or_create(100.into()); record_b.first_name.name = base_name_ref; - record_b.first_name.parent_frs = 5; + record_b.first_name.parent_frs = Into::into(5); record_b.first_name.next_entry = NO_ENTRY; record_b.name_count = 1; record_b.stdinfo.created = 132_456_789_012_345_678; record_b.stdinfo.modified = 132_456_789_012_345_678; - assert!(fragment_b.get_or_create(100).first_name.name.is_valid()); - assert_ne!(fragment_b.get_or_create(100).stdinfo.created, 0); - assert!(fragment_b.get_or_create(100).has_base_data()); + assert!( + fragment_b + .get_or_create(100.into()) + .first_name + .name + .is_valid() + ); + assert_ne!(fragment_b.get_or_create(100.into()).stdinfo.created, 0); + assert!(fragment_b.get_or_create(100.into()).has_base_data()); let mut index = MftIndex::new(crate::platform::DriveLetter::D); index.merge_fragments(vec![fragment_a, fragment_b]); @@ -138,21 +156,21 @@ fn cross_fragment_merge_multiple_extension_names() { next_entry: NO_ENTRY, name: ext_hardlink_c, _pad0: [0; 4], - parent_frs: 10, + parent_frs: Into::into(10), }); - let record_a = fragment_a.get_or_create(100); + let record_a = fragment_a.get_or_create(100.into()); record_a.first_name.name = ext_hardlink_b; - record_a.first_name.parent_frs = 5; + record_a.first_name.parent_frs = Into::into(5); record_a.first_name.next_entry = link_c_idx; record_a.name_count = 2; let mut fragment_b = MftIndexFragment::with_capacity(10); let base_original = push_fragment_name(&mut fragment_b, "original_a.txt"); - let record_b = fragment_b.get_or_create(100); + let record_b = fragment_b.get_or_create(100.into()); record_b.first_name.name = base_original; - record_b.first_name.parent_frs = 5; + record_b.first_name.parent_frs = Into::into(5); record_b.first_name.next_entry = NO_ENTRY; record_b.name_count = 1; record_b.stdinfo.created = 132_456_789_012_345_678; @@ -180,9 +198,9 @@ fn cross_fragment_merge_base_first() { let mut fragment_a = MftIndexFragment::with_capacity(10); let base_original = push_fragment_name(&mut fragment_a, "original_a.txt"); - let record_a = fragment_a.get_or_create(100); + let record_a = fragment_a.get_or_create(100.into()); record_a.first_name.name = base_original; - record_a.first_name.parent_frs = 5; + record_a.first_name.parent_frs = Into::into(5); record_a.first_name.next_entry = NO_ENTRY; record_a.name_count = 1; record_a.stdinfo.created = 132_456_789_012_345_678; @@ -197,12 +215,12 @@ fn cross_fragment_merge_base_first() { next_entry: NO_ENTRY, name: ext_hardlink_c, _pad0: [0; 4], - parent_frs: 10, + parent_frs: Into::into(10), }); - let record_b = fragment_b.get_or_create(100); + let record_b = fragment_b.get_or_create(100.into()); record_b.first_name.name = ext_hardlink_b; - record_b.first_name.parent_frs = 5; + record_b.first_name.parent_frs = Into::into(5); record_b.first_name.next_entry = link_c_idx; record_b.name_count = 2; @@ -229,30 +247,30 @@ fn rebuild_children_from_names_basic() { let mut index = MftIndex::new(crate::platform::DriveLetter::C); let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; + root_rec.first_name.parent_frs = Into::into(root_frs); root_rec.first_child = NO_ENTRY; let dir1_frs = 100_u64; let dir1_name = push_index_name(&mut index, "dir1"); - let rec = index.get_or_create(dir1_frs); + let rec = index.get_or_create(dir1_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = dir1_name; - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); rec.first_child = NO_ENTRY; let file1_frs = 200_u64; let file1_name = push_index_name(&mut index, "file1.txt"); - let rec = index.get_or_create(file1_frs); + let rec = index.get_or_create(file1_frs.into()); rec.first_name.name = file1_name; - rec.first_name.parent_frs = dir1_frs; + rec.first_name.parent_frs = Into::into(dir1_frs); let file2_frs = 201_u64; let file2_name = push_index_name(&mut index, "file2.txt"); - let rec = index.get_or_create(file2_frs); + let rec = index.get_or_create(file2_frs.into()); rec.first_name.name = file2_name; - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); assert_eq!( index.records[record_idx(&index, root_frs)].first_child, @@ -288,17 +306,17 @@ fn rebuild_children_from_names_hardlinks() { let file_frs = 200_u64; let dir1_name = push_index_name(&mut index, "dir1"); - let dir1_rec = index.get_or_create(dir1_frs); + let dir1_rec = index.get_or_create(dir1_frs.into()); dir1_rec.stdinfo.set_directory(true); dir1_rec.first_name.name = dir1_name; - dir1_rec.first_name.parent_frs = dir1_frs; + dir1_rec.first_name.parent_frs = Into::into(dir1_frs); dir1_rec.first_child = NO_ENTRY; let dir2_name = push_index_name(&mut index, "dir2"); - let dir2_rec = index.get_or_create(dir2_frs); + let dir2_rec = index.get_or_create(dir2_frs.into()); dir2_rec.stdinfo.set_directory(true); dir2_rec.first_name.name = dir2_name; - dir2_rec.first_name.parent_frs = dir2_frs; + dir2_rec.first_name.parent_frs = Into::into(dir2_frs); dir2_rec.first_child = NO_ENTRY; let file_name = push_index_name(&mut index, "file.txt"); @@ -307,13 +325,13 @@ fn rebuild_children_from_names_hardlinks() { next_entry: NO_ENTRY, name: link_name, _pad0: [0; 4], - parent_frs: dir1_frs, + parent_frs: Into::into(dir1_frs), }); let link_idx = len_to_u32(index.links.len() - 1); - let file_rec = index.get_or_create(file_frs); + let file_rec = index.get_or_create(file_frs.into()); file_rec.first_name.name = file_name; - file_rec.first_name.parent_frs = dir2_frs; + file_rec.first_name.parent_frs = Into::into(dir2_frs); file_rec.first_name.next_entry = link_idx; file_rec.name_count = 2; @@ -329,11 +347,11 @@ fn rebuild_children_from_names_hardlinks() { index.rebuild_children_from_names(); let child1 = &index.children[index.records[record_idx(&index, dir1_frs)].first_child as usize]; - assert_eq!(child1.child_frs, file_frs); + assert_eq!(child1.child_frs, file_frs.into()); assert_eq!(child1.name_index, 0); let child2 = &index.children[index.records[record_idx(&index, dir2_frs)].first_child as usize]; - assert_eq!(child2.child_frs, file_frs); + assert_eq!(child2.child_frs, file_frs.into()); assert_eq!(child2.name_index, 1); } @@ -343,9 +361,9 @@ fn rebuild_children_from_names_skips_root_self_reference() { let mut index = MftIndex::new(crate::platform::DriveLetter::C); let root_frs = 5_u64; - let rec = index.get_or_create(root_frs); + let rec = index.get_or_create(root_frs.into()); rec.stdinfo.set_directory(true); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); rec.first_child = NO_ENTRY; index.rebuild_children_from_names(); @@ -363,18 +381,18 @@ fn tree_metrics_empty_directory_descendants() { let mut index = MftIndex::new(crate::platform::DriveLetter::C); let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; + root_rec.first_name.parent_frs = Into::into(root_frs); let empty_dir_frs = 100_u64; let empty_dir_name = push_index_name(&mut index, "EmptyDir"); - let rec = index.get_or_create(empty_dir_frs); + let rec = index.get_or_create(empty_dir_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = empty_dir_name; - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); - index.add_child_entry(root_frs, empty_dir_frs, 0); + index.add_child_entry(Into::into(root_frs), Into::into(empty_dir_frs), 0); index.compute_tree_metrics(); assert_eq!( @@ -390,17 +408,17 @@ fn tree_metrics_internal_streams_two_channel() { let mut index = MftIndex::new(crate::platform::DriveLetter::C); let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; + root_rec.first_name.parent_frs = Into::into(root_frs); let dir_frs = 100_u64; let dir_name = push_index_name(&mut index, "DirWithInternal"); let dir_idx_for_internal = { - let rec = index.get_or_create(dir_frs); + let rec = index.get_or_create(dir_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = dir_name; - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); rec.total_stream_count = 2; usize::try_from(index.frs_to_idx[usize::try_from(dir_frs).unwrap()]).unwrap() }; @@ -418,16 +436,16 @@ fn tree_metrics_internal_streams_two_channel() { let file_frs = 200_u64; let file_name = push_index_name(&mut index, "file.txt"); - let rec = index.get_or_create(file_frs); + let rec = index.get_or_create(file_frs.into()); rec.first_name.name = file_name; - rec.first_name.parent_frs = dir_frs; + rec.first_name.parent_frs = Into::into(dir_frs); rec.first_stream.size = SizeInfo { length: 1000, allocated: 4096, }; - index.add_child_entry(root_frs, dir_frs, 0); - index.add_child_entry(dir_frs, file_frs, 0); + index.add_child_entry(Into::into(root_frs), Into::into(dir_frs), 0); + index.add_child_entry(Into::into(dir_frs), Into::into(file_frs), 0); crate::tree_metrics::compute_tree_metrics(&mut index, false, false); let dir_idx = record_idx(&index, dir_frs); diff --git a/crates/uffs-mft/src/index/tests_perf.rs b/crates/uffs-mft/src/index/tests_perf.rs index 967eb8f33..77921f734 100644 --- a/crates/uffs-mft/src/index/tests_perf.rs +++ b/crates/uffs-mft/src/index/tests_perf.rs @@ -71,7 +71,7 @@ fn extension_index_query_performance() { // Create record with extension let name = format!("file{i}.ext{}", i % 10); let offset = index.add_name(&name); - let rec = index.get_or_create(frs); + let rec = index.get_or_create(frs.into()); rec.first_name.name = IndexNameRef::new(offset, u16::try_from(name.len()).unwrap(), true, ext_id); rec.first_stream.size.length = 1024; @@ -118,16 +118,16 @@ fn full_postprocessing_performance() { // Add root directory let root_frs = 5; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; // Self-parent + root_rec.first_name.parent_frs = Into::into(root_frs); // Self-parent // Add 100 directories for dir_i in 0..100 { let dir_frs = 100 + dir_i; - let rec = index.get_or_create(dir_frs); + let rec = index.get_or_create(dir_frs.into()); rec.stdinfo.set_directory(true); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); } // Add 1000 files per directory (100K total) @@ -135,8 +135,8 @@ fn full_postprocessing_performance() { let dir_frs = 100 + dir_i; for file_i in 0..1000 { let file_frs = 10_000 + dir_i * 1000 + file_i; - let rec = index.get_or_create(file_frs); - rec.first_name.parent_frs = dir_frs; + let rec = index.get_or_create(file_frs.into()); + rec.first_name.parent_frs = Into::into(dir_frs); rec.first_stream.size.length = 1024; } } diff --git a/crates/uffs-mft/src/index/tests_tree.rs b/crates/uffs-mft/src/index/tests_tree.rs index a6d11a822..f977c2baa 100644 --- a/crates/uffs-mft/src/index/tests_tree.rs +++ b/crates/uffs-mft/src/index/tests_tree.rs @@ -18,24 +18,24 @@ fn compute_tree_metrics_simple() { // Root directory let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; // Self-parent + root_rec.first_name.parent_frs = Into::into(root_frs); // Self-parent // dir1 let dir1_frs = 100_u64; let offset = index.add_name("dir1"); - let rec = index.get_or_create(dir1_frs); + let rec = index.get_or_create(dir1_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = IndexNameRef::new(offset, 4, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); // file1.txt (child of dir1) let file1_frs = 200_u64; let offset = index.add_name("file1.txt"); - let rec = index.get_or_create(file1_frs); + let rec = index.get_or_create(file1_frs.into()); rec.first_name.name = IndexNameRef::new(offset, 9, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = dir1_frs; + rec.first_name.parent_frs = Into::into(dir1_frs); rec.first_stream.size = SizeInfo { length: 1000, allocated: 4096, @@ -44,9 +44,9 @@ fn compute_tree_metrics_simple() { // file2.txt (child of dir1) let file2_frs = 201_u64; let offset = index.add_name("file2.txt"); - let rec = index.get_or_create(file2_frs); + let rec = index.get_or_create(file2_frs.into()); rec.first_name.name = IndexNameRef::new(offset, 9, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = dir1_frs; + rec.first_name.parent_frs = Into::into(dir1_frs); rec.first_stream.size = SizeInfo { length: 2000, allocated: 4096, @@ -55,38 +55,38 @@ fn compute_tree_metrics_simple() { // file3.txt (child of root) let file3_frs = 202_u64; let offset = index.add_name("file3.txt"); - let rec = index.get_or_create(file3_frs); + let rec = index.get_or_create(file3_frs.into()); rec.first_name.name = IndexNameRef::new(offset, 9, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); rec.first_stream.size = SizeInfo { length: 500, allocated: 4096, }; // Add child entries (required for tree metrics algorithm) - index.add_child_entry(root_frs, dir1_frs, 0); - index.add_child_entry(root_frs, file3_frs, 0); - index.add_child_entry(dir1_frs, file1_frs, 0); - index.add_child_entry(dir1_frs, file2_frs, 0); + index.add_child_entry(Into::into(root_frs), Into::into(dir1_frs), 0); + index.add_child_entry(Into::into(root_frs), Into::into(file3_frs), 0); + index.add_child_entry(Into::into(dir1_frs), Into::into(file1_frs), 0); + index.add_child_entry(Into::into(dir1_frs), Into::into(file2_frs), 0); // Compute tree metrics index.compute_tree_metrics(); // Verify file1.txt (leaf) // Files have descendants = 0, but contribute 1 to their parent. - let file1_idx = index.frs_to_idx_opt(file1_frs).unwrap(); + let file1_idx = index.frs_to_idx_opt(file1_frs.into()).unwrap(); assert_eq!(index.records[file1_idx].descendants, 0); assert_eq!(index.records[file1_idx].treesize, 1000); assert_eq!(index.records[file1_idx].tree_allocated, 4096); // Verify file2.txt (leaf) - let file2_idx = index.frs_to_idx_opt(file2_frs).unwrap(); + let file2_idx = index.frs_to_idx_opt(file2_frs.into()).unwrap(); assert_eq!(index.records[file2_idx].descendants, 0); assert_eq!(index.records[file2_idx].treesize, 2000); assert_eq!(index.records[file2_idx].tree_allocated, 4096); // Verify file3.txt (leaf) - let file3_idx = index.frs_to_idx_opt(file3_frs).unwrap(); + let file3_idx = index.frs_to_idx_opt(file3_frs.into()).unwrap(); assert_eq!(index.records[file3_idx].descendants, 0); assert_eq!(index.records[file3_idx].treesize, 500); assert_eq!(index.records[file3_idx].tree_allocated, 4096); @@ -94,7 +94,7 @@ fn compute_tree_metrics_simple() { // Verify dir1 (has 2 children: file1 and file2) // descendants = 1 (self) + sum(max(1, child.descendants)) // dir1 = 1 + 1 + 1 = 3 - let dir1_idx = index.frs_to_idx_opt(dir1_frs).unwrap(); + let dir1_idx = index.frs_to_idx_opt(dir1_frs.into()).unwrap(); assert_eq!(index.records[dir1_idx].descendants, 3); // 1 + file1(1) + file2(1) assert_eq!(index.records[dir1_idx].treesize, 3000); // 0 + 1000 + 2000 assert_eq!(index.records[dir1_idx].tree_allocated, 8192); // 0 + 4096 + 4096 @@ -102,7 +102,7 @@ fn compute_tree_metrics_simple() { // Verify root (has dir1 + file3) // descendants = 1 (self) + sum(child.descendants) // root = 1 + 3 + 1 = 5 - let root_idx = index.frs_to_idx_opt(root_frs).unwrap(); + let root_idx = index.frs_to_idx_opt(root_frs.into()).unwrap(); assert_eq!(index.records[root_idx].descendants, 5); // 1 + dir1(3) + file3(1) assert_eq!(index.records[root_idx].treesize, 3500); // 0 + 3000 + 500 assert_eq!(index.records[root_idx].tree_allocated, 12288); // 0 + 8192 + @@ -122,50 +122,50 @@ fn compute_tree_metrics_deep_tree() { // Root let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; + root_rec.first_name.parent_frs = Into::into(root_frs); // dir1 let dir1_frs = 100_u64; let offset = index.add_name("dir1"); - let rec = index.get_or_create(dir1_frs); + let rec = index.get_or_create(dir1_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = IndexNameRef::new(offset, 4, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); // dir2 let dir2_frs = 101_u64; let offset = index.add_name("dir2"); - let rec = index.get_or_create(dir2_frs); + let rec = index.get_or_create(dir2_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = IndexNameRef::new(offset, 4, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = dir1_frs; + rec.first_name.parent_frs = Into::into(dir1_frs); // dir3 let dir3_frs = 102_u64; let offset = index.add_name("dir3"); - let rec = index.get_or_create(dir3_frs); + let rec = index.get_or_create(dir3_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = IndexNameRef::new(offset, 4, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = dir2_frs; + rec.first_name.parent_frs = Into::into(dir2_frs); // file.txt let file_frs = 200_u64; let offset = index.add_name("file.txt"); - let rec = index.get_or_create(file_frs); + let rec = index.get_or_create(file_frs.into()); rec.first_name.name = IndexNameRef::new(offset, 8, true, IndexNameRef::NO_EXTENSION); - rec.first_name.parent_frs = dir3_frs; + rec.first_name.parent_frs = Into::into(dir3_frs); rec.first_stream.size = SizeInfo { length: 1000, allocated: 4096, }; // Add child entries (required for tree metrics algorithm) - index.add_child_entry(root_frs, dir1_frs, 0); - index.add_child_entry(dir1_frs, dir2_frs, 0); - index.add_child_entry(dir2_frs, dir3_frs, 0); - index.add_child_entry(dir3_frs, file_frs, 0); + index.add_child_entry(Into::into(root_frs), Into::into(dir1_frs), 0); + index.add_child_entry(Into::into(dir1_frs), Into::into(dir2_frs), 0); + index.add_child_entry(Into::into(dir2_frs), Into::into(dir3_frs), 0); + index.add_child_entry(Into::into(dir3_frs), Into::into(file_frs), 0); // Compute tree metrics index.compute_tree_metrics(); @@ -176,27 +176,27 @@ fn compute_tree_metrics_deep_tree() { // dir2 = 1+2=3, dir1 = 1+3=4, root = 1+4=5 // Verify file.txt (leaf) - let file_idx = index.frs_to_idx_opt(file_frs).unwrap(); + let file_idx = index.frs_to_idx_opt(file_frs.into()).unwrap(); assert_eq!(index.records[file_idx].descendants, 0); // Files have 0 assert_eq!(index.records[file_idx].treesize, 1000); // Verify dir3 (has 1 child: file.txt) - let dir3_idx = index.frs_to_idx_opt(dir3_frs).unwrap(); + let dir3_idx = index.frs_to_idx_opt(dir3_frs.into()).unwrap(); assert_eq!(index.records[dir3_idx].descendants, 2); // 1 + max(1, file.txt(0)) = 1 + 1 = 2 assert_eq!(index.records[dir3_idx].treesize, 1000); // Verify dir2 (has 1 child: dir3) - let dir2_idx = index.frs_to_idx_opt(dir2_frs).unwrap(); + let dir2_idx = index.frs_to_idx_opt(dir2_frs.into()).unwrap(); assert_eq!(index.records[dir2_idx].descendants, 3); // 1 + dir3(2) assert_eq!(index.records[dir2_idx].treesize, 1000); // Verify dir1 (has 1 child: dir2) - let dir1_idx = index.frs_to_idx_opt(dir1_frs).unwrap(); + let dir1_idx = index.frs_to_idx_opt(dir1_frs.into()).unwrap(); assert_eq!(index.records[dir1_idx].descendants, 4); // 1 + dir2(3) assert_eq!(index.records[dir1_idx].treesize, 1000); // Verify root (has 1 child: dir1) - let root_idx = index.frs_to_idx_opt(root_frs).unwrap(); + let root_idx = index.frs_to_idx_opt(root_frs.into()).unwrap(); assert_eq!(index.records[root_idx].descendants, 5); // 1 + dir1(4) assert_eq!(index.records[root_idx].treesize, 1000); } @@ -221,9 +221,9 @@ fn compute_tree_metrics_performance() { // Structure: root -> 100 directories -> 100 files each let root_frs = 5_u64; - let root_rec = index.get_or_create(root_frs); + let root_rec = index.get_or_create(root_frs.into()); root_rec.stdinfo.set_directory(true); - root_rec.first_name.parent_frs = root_frs; + root_rec.first_name.parent_frs = Into::into(root_frs); let mut frs_counter = 1000_u64; @@ -232,7 +232,7 @@ fn compute_tree_metrics_performance() { let dir_frs = 100 + dir_idx; let dir_name = format!("dir{:03}", dir_idx); let offset = index.add_name(&dir_name); - let rec = index.get_or_create(dir_frs); + let rec = index.get_or_create(dir_frs.into()); rec.stdinfo.set_directory(true); rec.first_name.name = IndexNameRef::new( offset, @@ -240,10 +240,10 @@ fn compute_tree_metrics_performance() { true, IndexNameRef::NO_EXTENSION, ); - rec.first_name.parent_frs = root_frs; + rec.first_name.parent_frs = Into::into(root_frs); // Add child entry for directory - index.add_child_entry(root_frs, dir_frs, 0); + index.add_child_entry(Into::into(root_frs), Into::into(dir_frs), 0); // Create 100 files in each directory for file_idx in 0..100 { @@ -252,21 +252,21 @@ fn compute_tree_metrics_performance() { let file_name = format!("file{:03}.txt", file_idx); let offset = index.add_name(&file_name); - let rec = index.get_or_create(file_frs); + let rec = index.get_or_create(file_frs.into()); rec.first_name.name = IndexNameRef::new( offset, u16::try_from(file_name.len()).unwrap(), true, IndexNameRef::NO_EXTENSION, ); - rec.first_name.parent_frs = dir_frs; + rec.first_name.parent_frs = Into::into(dir_frs); rec.first_stream.size = SizeInfo { length: 1000, allocated: 4096, }; // Add child entry for file - index.add_child_entry(dir_frs, file_frs, 0); + index.add_child_entry(Into::into(dir_frs), Into::into(file_frs), 0); } } @@ -286,7 +286,7 @@ fn compute_tree_metrics_performance() { // sum(max(1, child.descendants)) Each file = 0 (but contributes 1 to // parent) Each dir_i = 1 (self) + 100 files * max(1,0) = 1 + 100 = 101 // root = 1 (self) + 100 dirs * 101 = 10,101 - let root_idx = index.frs_to_idx_opt(root_frs).unwrap(); + let root_idx = index.frs_to_idx_opt(root_frs.into()).unwrap(); assert_eq!(index.records[root_idx].descendants, 10_101); // 1 + 100 * 101 // Verify root has correct total size @@ -294,7 +294,7 @@ fn compute_tree_metrics_performance() { // Verify a directory has correct descendants // Each dir = 1 (self) + 100 files * max(1,0) = 1 + 100 = 101 - let dir0_idx = index.frs_to_idx_opt(100).unwrap(); + let dir0_idx = index.frs_to_idx_opt(100.into()).unwrap(); assert_eq!(index.records[dir0_idx].descendants, 101); // 1 + 100 * max(1,0) // Computation should be fast (< 50ms for 10,000 files) diff --git a/crates/uffs-mft/src/index/tree.rs b/crates/uffs-mft/src/index/tree.rs index 2f4fd103c..bed1b46dc 100644 --- a/crates/uffs-mft/src/index/tree.rs +++ b/crates/uffs-mft/src/index/tree.rs @@ -140,7 +140,7 @@ impl MftIndex { // Also check root specifically for treesize=0 (belt-and-suspenders). // Root should always have treesize > 0 if there are any files on the volume. let root_looks_bad = self - .frs_to_idx_opt(5) + .frs_to_idx_opt(crate::frs::Frs::ROOT) .and_then(|root_idx| self.records.get(root_idx)) .is_some_and(|root| { root.stdinfo.is_directory() && (root.descendants == 0 || root.treesize == 0) @@ -187,9 +187,13 @@ impl MftIndex { .enumerate() .filter(|(_, rec)| rec.stdinfo.is_directory() && rec.descendants == 0) .map(|(idx, rec)| { + // Tuple is logged via `tracing::warn!`; the macro + // requires `tracing::Value`, which the typed + // newtype does not impl. Demote with `.raw()` at the + // tuple boundary so the warning carries the bare FRS. ( idx, - rec.frs, + rec.frs.raw(), rec.first_child, rec.name_count, rec.total_stream_count, diff --git a/crates/uffs-mft/src/index/types.rs b/crates/uffs-mft/src/index/types.rs index 62b1b778a..99944d10c 100644 --- a/crates/uffs-mft/src/index/types.rs +++ b/crates/uffs-mft/src/index/types.rs @@ -6,6 +6,7 @@ //! `StandardInfo` lives in sibling module `standard_info.rs`. use super::standard_info::StandardInfo; +use crate::frs::{Frs, ParentFrs}; // ============================================================================ // Constants @@ -14,7 +15,8 @@ use super::standard_info::StandardInfo; /// Sentinel value indicating "no entry" in linked-list fields. pub const NO_ENTRY: u32 = u32::MAX; -/// Root directory FRS in NTFS +/// Root directory FRS in NTFS — raw `u64` for external consumers (DTOs, wire +/// format, fixtures). Internal code should prefer the typed [`Frs::ROOT`]. pub const ROOT_FRS: u64 = 5; // ============================================================================ @@ -340,9 +342,8 @@ impl IndexNameRef { /// /// Most files have only one name, stored inline in `FileRecord::first_name`. /// Files with multiple hard links form a linked list via `next_entry`. -/// -/// Uses `u64` for `parent_frs` so the index can represent the full NTFS FRS -/// range. +/// [`ParentFrs`] is `#[repr(transparent)]` over `u64`, so on-disk layout +/// is byte-identical to the historic `u64` field. #[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct LinkInfo { @@ -356,8 +357,8 @@ pub struct LinkInfo { reason = "bytemuck Pod requires all fields same visibility" )] pub _pad0: [u8; 4], - /// Parent directory FRS (u64 to support all valid NTFS volumes) - pub parent_frs: u64, + /// Parent directory FRS (typed [`ParentFrs`]; `u64`-equivalent on disk). + pub parent_frs: ParentFrs, } // ============================================================================ @@ -476,8 +477,8 @@ pub struct InternalStreamInfo { #[derive(Debug, Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct FileRecord { - /// FRS (File Record Segment) number - primary key - pub frs: u64, + /// FRS number — primary key (typed [`Frs`]; `u64`-equivalent on disk). + pub frs: Frs, /// Sequence number (incremented when FRS is reused, forensic value) pub sequence_number: u16, /// Primary filename namespace (0=POSIX, 1=Win32, 2=DOS, 3=Win32+DOS) @@ -503,8 +504,8 @@ pub struct FileRecord { reason = "bytemuck Pod requires all fields same visibility" )] pub _pad1: [u8; 4], - /// Base FRS for extension records (0 for base records). - pub base_frs: u64, + /// Base FRS for extension records ([`Frs::ZERO`] for base records). + pub base_frs: Frs, /// Timestamps and bit-packed attributes from `$STANDARD_INFORMATION` pub stdinfo: StandardInfo, /// Number of hard links (usually 1) @@ -560,9 +561,9 @@ pub struct FileRecord { } impl FileRecord { - /// Create a new record for the given FRS + /// Create a new record for the given FRS. #[must_use] - pub fn new(frs: u64) -> Self { + pub fn new(frs: Frs) -> Self { Self { frs, name_count: 1, // Every file has at least one name @@ -577,7 +578,7 @@ impl FileRecord { meta: 0, }, _pad0: [0; 4], - parent_frs: u64::from(NO_ENTRY), + parent_frs: ParentFrs::new(u64::from(NO_ENTRY)), }, first_stream: IndexStreamInfo { next_entry: NO_ENTRY, @@ -600,7 +601,7 @@ impl FileRecord { /// attribute, including the first, producing correct-by-construction /// values that match the unified parser's expectations. #[must_use] - pub(crate) fn new_unified(frs: u64) -> Self { + pub(crate) fn new_unified(frs: Frs) -> Self { Self { frs, forensic_flags: 0b10_0000, // bit 5: is_unified @@ -616,7 +617,7 @@ impl FileRecord { meta: 0, }, _pad0: [0; 4], - parent_frs: u64::from(NO_ENTRY), + parent_frs: ParentFrs::new(u64::from(NO_ENTRY)), }, first_stream: IndexStreamInfo { size: SizeInfo::default(), diff --git a/crates/uffs-mft/src/io/parser/fragment.rs b/crates/uffs-mft/src/io/parser/fragment.rs index f020cf6b7..cc6a67101 100644 --- a/crates/uffs-mft/src/io/parser/fragment.rs +++ b/crates/uffs-mft/src/io/parser/fragment.rs @@ -312,7 +312,8 @@ pub fn parse_record_to_fragment( next_entry: NO_ENTRY, name: link_name_ref, _pad0: [0; 4], - parent_frs: link_parent, + // Typed `ParentFrs` slot — lift parser-local raw `u64`. + parent_frs: crate::frs::ParentFrs::new(link_parent), }); link_indices.push(link_idx); } @@ -346,9 +347,11 @@ pub fn parse_record_to_fragment( stream_indices.push(stream_idx); } - // Create parent placeholder if needed (within this fragment) + // Create parent placeholder if needed (within this fragment). + // Boundary: lift parser-local raw `u64` to typed `Frs` once. + let frs_typed = crate::frs::Frs::new(frs); if parent_frs != frs && parent_frs != 0 { - fragment.get_or_create(parent_frs); + fragment.get_or_create(crate::frs::Frs::new(parent_frs)); // ^ side effect only: ensures parent placeholder exists } @@ -357,7 +360,7 @@ pub fn parse_record_to_fragment( // that were processed BEFORE this base record in the same fragment. // We must preserve those extension names/streams and chain them to base record // data. - let record = fragment.get_or_create(frs); + let record = fragment.get_or_create(frs_typed); // Save any existing extension data BEFORE overwriting // Copy the entire first_name LinkInfo so we can add it as a link later @@ -389,7 +392,8 @@ pub fn parse_record_to_fragment( next_entry: NO_ENTRY, name: name_ref, _pad0: [0; 4], - parent_frs, + // Typed `ParentFrs` slot — lift raw `u64` parser local. + parent_frs: crate::frs::ParentFrs::new(parent_frs), }; // Chain the base record's additional links together @@ -449,7 +453,7 @@ pub fn parse_record_to_fragment( // Now set first_name.next_entry on the record. // (Separate borrow scope because we mutate fragment.links/streams above // and need a fresh &mut record.) - let rec_for_name = fragment.get_or_create(frs); + let rec_for_name = fragment.get_or_create(frs_typed); rec_for_name.first_name.next_entry = first_name_next_entry; // Chain streams: base ADS -> extension ADS (must be done before borrowing @@ -460,7 +464,7 @@ pub fn parse_record_to_fragment( } // Now get record and update counts and first_stream chain - let rec_for_counts = fragment.get_or_create(frs); + let rec_for_counts = fragment.get_or_create(frs_typed); // Calculate total name count // Base: 1 (first_name) + additional_count @@ -500,7 +504,8 @@ pub fn parse_record_to_fragment( // Create placeholder parent let new_idx = len_to_u32(frag.records.len()); frag.frs_to_idx[p_frs_usize] = new_idx; - frag.records.push(crate::index::FileRecord::new(p_frs)); + frag.records + .push(crate::index::FileRecord::new(crate::frs::Frs::new(p_frs))); } frag.frs_to_idx[p_frs_usize] }; @@ -514,7 +519,8 @@ pub fn parse_record_to_fragment( frag.children.push(ChildInfo { next_entry: old_first_child, _pad0: [0; 4], - child_frs: frs, + // Typed `Frs` slot — reuse cached typed FRS. + child_frs: frs_typed, name_index: name_idx, _pad1: [0; 6], }); @@ -574,8 +580,10 @@ fn store_nameless_record( stream_indices.push(stream_idx); } - // Now create the record and set up streams - let record = fragment.get_or_create(frs); + // Now create the record and set up streams. Boundary: lift + // parser-local raw `u64` to typed `Frs`. + let frs_typed = crate::frs::Frs::new(frs); + let record = fragment.get_or_create(frs_typed); record.stdinfo = std_info; record.first_stream.size = SizeInfo { length: default_size, @@ -589,7 +597,7 @@ fn store_nameless_record( let next_idx = stream_indices[i + 1]; fragment.streams[current_idx].next_entry = next_idx; } - let rec_for_stream = fragment.get_or_create(frs); + let rec_for_stream = fragment.get_or_create(frs_typed); rec_for_stream.first_stream.next_entry = stream_indices[0]; rec_for_stream.stream_count = 1 + len_to_u16(additional_stream_count); } diff --git a/crates/uffs-mft/src/io/parser/fragment_extension.rs b/crates/uffs-mft/src/io/parser/fragment_extension.rs index 03dc99f00..6f42c78e7 100644 --- a/crates/uffs-mft/src/io/parser/fragment_extension.rs +++ b/crates/uffs-mft/src/io/parser/fragment_extension.rs @@ -222,7 +222,8 @@ fn merge_extension_into_fragment( next_entry: NO_ENTRY, name: name_ref, _pad0: [0; 4], - parent_frs: *parent_frs, + // Typed `ParentFrs` slot — lift parser-local raw `u64`. + parent_frs: crate::frs::ParentFrs::new(*parent_frs), }); link_indices.push(link_idx); } @@ -252,10 +253,11 @@ fn merge_extension_into_fragment( stream_indices.push(stream_idx); } - // Ensure parent directories exist + // Ensure parent directories exist. Boundary: lift parser-local raw + // `u64` to typed `Frs` at the typed-API call site. for (_, parent_frs) in names { if *parent_frs != base_frs && *parent_frs != 0 { - fragment.get_or_create(*parent_frs); + fragment.get_or_create(crate::frs::Frs::new(*parent_frs)); // ^ side effect: ensures parent placeholder exists } } @@ -269,8 +271,10 @@ fn merge_extension_into_fragment( } // Get the first_name.next_entry, first_stream.next_entry, and first_name - // validity before we start modifying things - let record = fragment.get_or_create(base_frs); + // validity before we start modifying things. Lift parser-local raw `u64` + // to typed `Frs` once for all the typed-API call sites in this function. + let base_frs_typed = crate::frs::Frs::new(base_frs); + let record = fragment.get_or_create(base_frs_typed); let first_name_valid = record.first_name.name.is_valid(); let first_name_next = record.first_name.next_entry; let first_stream_next = record.first_stream.next_entry; @@ -293,10 +297,10 @@ fn merge_extension_into_fragment( if let Some(end_idx) = stream_chain_end { fragment.streams[u32_as_usize(end_idx)].next_entry = stream_indices[0]; } else { - let rec_for_stream = fragment.get_or_create(base_frs); + let rec_for_stream = fragment.get_or_create(base_frs_typed); rec_for_stream.first_stream.next_entry = stream_indices[0]; } - let rec_for_count = fragment.get_or_create(base_frs); + let rec_for_count = fragment.get_or_create(base_frs_typed); rec_for_count.stream_count += len_to_u16(stream_indices.len()); rec_for_count.total_stream_count += len_to_u16(stream_indices.len()); } @@ -337,29 +341,32 @@ fn attach_links( return; } + // Boundary: lift parser-local raw `u64` to typed `Frs` once for all + // the typed-API call sites in this function. + let base_frs_typed = crate::frs::Frs::new(base_frs); if first_name_valid { // Base record already has a name — chain extension names as additional hard // links if let Some(end_idx) = link_chain_end { fragment.links[u32_as_usize(end_idx)].next_entry = link_indices[0]; } else { - let rec_for_link_chain = fragment.get_or_create(base_frs); + let rec_for_link_chain = fragment.get_or_create(base_frs_typed); rec_for_link_chain.first_name.next_entry = link_indices[0]; } - let rec_for_link_count = fragment.get_or_create(base_frs); + let rec_for_link_count = fragment.get_or_create(base_frs_typed); rec_for_link_count.name_count += len_to_u16(link_indices.len()); } else { // Copy the first extension name directly into first_name // This matches established behavior (ntfs_index.hpp lines 559-567) let first_link_name = fragment.links[u32_as_usize(link_indices[0])].name; let first_link_parent = fragment.links[u32_as_usize(link_indices[0])].parent_frs; - let rec_for_first_name = fragment.get_or_create(base_frs); + let rec_for_first_name = fragment.get_or_create(base_frs_typed); rec_for_first_name.first_name.name = first_link_name; rec_for_first_name.first_name.parent_frs = first_link_parent; // Chain remaining links (if any) to first_name.next_entry if link_indices.len() > 1 { - let rec_for_extra_links = fragment.get_or_create(base_frs); + let rec_for_extra_links = fragment.get_or_create(base_frs_typed); rec_for_extra_links.first_name.next_entry = link_indices[1]; rec_for_extra_links.name_count += len_to_u16(link_indices.len().saturating_sub(1)); } @@ -376,7 +383,10 @@ fn build_parent_child_entries( base_frs: u64, names: &[(String, u64)], ) { - let record = fragment.get_or_create(base_frs); + // Boundary: lift parser-local raw `u64` to typed `Frs` once for the + // typed-API and `ChildInfo` writes below. + let base_frs_typed = crate::frs::Frs::new(base_frs); + let record = fragment.get_or_create(base_frs_typed); let existing_name_count = record.name_count; for (name_idx, (_, parent_frs)) in names.iter().enumerate() { @@ -394,7 +404,9 @@ fn build_parent_child_entries( if fragment.frs_to_idx[p_frs_usize] == NO_ENTRY { let new_idx = len_to_u32(fragment.records.len()); fragment.frs_to_idx[p_frs_usize] = new_idx; - fragment.records.push(crate::index::FileRecord::new(p_frs)); + fragment + .records + .push(crate::index::FileRecord::new(crate::frs::Frs::new(p_frs))); } fragment.frs_to_idx[p_frs_usize] }; @@ -413,7 +425,8 @@ fn build_parent_child_entries( fragment.children.push(ChildInfo { next_entry: old_first_child, _pad0: [0; 4], - child_frs: base_frs, + // Typed `Frs` slot — reuse cached typed FRS. + child_frs: base_frs_typed, name_index: effective_name_idx, _pad1: [0; 6], }); diff --git a/crates/uffs-mft/src/io/parser/index.rs b/crates/uffs-mft/src/io/parser/index.rs index 8c44e18b8..a513dc45b 100644 --- a/crates/uffs-mft/src/io/parser/index.rs +++ b/crates/uffs-mft/src/io/parser/index.rs @@ -335,8 +335,9 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf if name_len == 0 { // Default stream — mark that unnamed $DATA exists - // (distinguishes "empty $DATA" from "no $DATA") - let rec = index.get_or_create(frs); + // (distinguishes "empty $DATA" from "no $DATA"). + // Boundary: lift parser-local raw `u64` to typed `Frs`. + let rec = index.get_or_create(crate::frs::Frs::new(frs)); rec.set_has_default_data(); default_size = size; default_allocated = allocated; @@ -662,8 +663,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf alloc_total: internal_alloc_total, } = build_internal_stream_chain(index, internal_streams); - // Snapshot and setup record using helper - let record = index.get_or_create(frs); + // Snapshot and setup record using helper. Lift parser-local raw + // `u64` to typed `Frs` once for all the typed-API call sites. + let frs_typed = crate::frs::Frs::new(frs); + let record = index.get_or_create(frs_typed); let ext = ExtensionSnapshot { stream_head: record.first_stream.next_entry, stream_count: record.stream_count.saturating_sub(1), @@ -694,10 +697,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf // Chain ADS streams and set counts if !stream_indices.is_empty() { chain_streams(index, &stream_indices); - let rec_chain = index.get_or_create(frs); + let rec_chain = index.get_or_create(frs_typed); rec_chain.first_stream.next_entry = stream_indices[0]; } - let rec_counts = index.get_or_create(frs); + let rec_counts = index.get_or_create(frs_typed); rec_counts.stream_count = 1 + len_to_u16(additional_stream_count); rec_counts.total_stream_count = 1 + len_to_u16(additional_stream_count) + len_to_u16(internal_stream_count); @@ -753,15 +756,15 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf alloc_total: internal_alloc_total, } = build_internal_stream_chain(index, internal_streams); - // Ensure parent exists (create placeholder if needed) - do this before getting - // our record + // Ensure parent exists (create placeholder if needed) - do this before + // getting our record. Lift parser-local raw `u64` to typed `Frs`. if parent_frs != frs && parent_frs != 0 { - index.get_or_create(parent_frs); + index.get_or_create(crate::frs::Frs::new(parent_frs)); // ^ side effect: ensures parent placeholder exists } // Snapshot and setup record - let record = index.get_or_create(frs); + let record = index.get_or_create(crate::frs::Frs::new(frs)); let ext = ExtensionSnapshot { stream_head: record.first_stream.next_entry, stream_count: record.stream_count.saturating_sub(1), @@ -793,7 +796,8 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf next_entry: NO_ENTRY, name: name_ref, _pad0: [0; 4], - parent_frs, + // Typed `ParentFrs` slot — lift raw `u64` parser local. + parent_frs: crate::frs::ParentFrs::new(parent_frs), }; record.name_count = 1 + len_to_u16(additional_count); record.stream_count = 1 + len_to_u16(additional_stream_count); diff --git a/crates/uffs-mft/src/io/parser/index_extension.rs b/crates/uffs-mft/src/io/parser/index_extension.rs index 652ea7d4c..1822567b3 100644 --- a/crates/uffs-mft/src/io/parser/index_extension.rs +++ b/crates/uffs-mft/src/io/parser/index_extension.rs @@ -513,7 +513,8 @@ pub(super) fn parse_extension_to_index( next_entry: NO_ENTRY, name: name_ref, _pad0: [0; 4], - parent_frs: *parent_frs, + // Typed `ParentFrs` slot — lift parser-local raw `u64`. + parent_frs: crate::frs::ParentFrs::new(*parent_frs), }); link_indices.push(link_idx); } @@ -541,10 +542,11 @@ pub(super) fn parse_extension_to_index( stream_indices.push(stream_idx); } - // Ensure parent directories exist for the new names + // Ensure parent directories exist for the new names. Boundary: lift + // parser-local raw `u64` to typed `Frs` at the typed-API call site. for (_, parent_frs) in &names { if *parent_frs != base_frs && *parent_frs != 0 { - index.get_or_create(*parent_frs); + index.get_or_create(crate::frs::Frs::new(*parent_frs)); // ^ side effect: ensures parent placeholder exists } } @@ -553,13 +555,13 @@ pub(super) fn parse_extension_to_index( let base_frs_usize = frs_to_usize(base_frs); if base_frs_usize >= index.frs_to_idx.len() { // Base record doesn't exist yet — create a placeholder - index.get_or_create(base_frs); + index.get_or_create(crate::frs::Frs::new(base_frs)); } let record_idx = index.frs_to_idx[base_frs_usize]; if record_idx == NO_ENTRY { // Base record doesn't exist — create it - index.get_or_create(base_frs); + index.get_or_create(crate::frs::Frs::new(base_frs)); } // Now get the record and chain the new links/streams @@ -792,7 +794,9 @@ pub(super) fn parse_extension_to_index( // Create placeholder parent let new_idx = len_to_u32(index.records.len()); index.frs_to_idx[p_frs_usize] = new_idx; - index.records.push(crate::index::FileRecord::new(p_frs)); + index + .records + .push(crate::index::FileRecord::new(crate::frs::Frs::new(p_frs))); } index.frs_to_idx[p_frs_usize] }; @@ -826,7 +830,8 @@ pub(super) fn parse_extension_to_index( index.children.push(ChildInfo { next_entry: old_first_child, _pad0: [0; 4], - child_frs: base_frs, + // Typed `Frs` slot — lift parser-local raw `u64`. + child_frs: crate::frs::Frs::new(base_frs), name_index: effective_name_idx, _pad1: [0; 6], }); diff --git a/crates/uffs-mft/src/io/parser/unified.rs b/crates/uffs-mft/src/io/parser/unified.rs index 8cc6dffa2..7e686697d 100644 --- a/crates/uffs-mft/src/io/parser/unified.rs +++ b/crates/uffs-mft/src/io/parser/unified.rs @@ -124,8 +124,10 @@ pub fn process_record(data: &[u8], frs: u64, index: &mut MftIndex, name_buf: &mu // Get or create the base record (zero-based counts). // Cache the record index so we don't repeat the frs→idx lookup for - // every attribute in this record. - let base_ri = u32_as_usize(index.ensure_record(frs_base)); + // every attribute in this record. Boundary: lift parser-local raw + // `u64` to typed `Frs` for the typed index API. + let frs_base_typed = crate::frs::Frs::new(frs_base); + let base_ri = u32_as_usize(index.ensure_record(frs_base_typed)); // ── Attribute loop ───────────────────────────────────────────────── let mut offset = usize::from(header.first_attribute_offset); @@ -211,14 +213,18 @@ pub fn process_record(data: &[u8], frs: u64, index: &mut MftIndex, name_buf: &mu ); index.records[base_ri].first_name.name = name_ref; - index.records[base_ri].first_name.parent_frs = parent_frs; + // Typed `ParentFrs` slot — lift parser-local raw `u64`. + index.records[base_ri].first_name.parent_frs = + crate::frs::ParentFrs::new(parent_frs); // Build parent-child relationship. // name_index = name_count BEFORE increment let name_index = index.records[base_ri].name_count; if parent_frs != frs_base && parent_frs != u64::from(NO_ENTRY) { - let parent_ri = u32_as_usize(index.ensure_record(parent_frs)); + let parent_ri = u32_as_usize( + index.ensure_record(crate::frs::Frs::new(parent_frs)), + ); let child_idx = len_to_u32(index.children.len()); let old_fc = index.records[parent_ri].first_child; index.records[parent_ri].first_child = child_idx; @@ -226,7 +232,8 @@ pub fn process_record(data: &[u8], frs: u64, index: &mut MftIndex, name_buf: &mu index.children.push(ChildInfo { next_entry: old_fc, _pad0: [0; 4], - child_frs: frs_base, + // Typed `Frs` slot — reuse cached typed FRS. + child_frs: frs_base_typed, name_index, _pad1: [0; 6], }); diff --git a/crates/uffs-mft/src/parse/direct_index.rs b/crates/uffs-mft/src/parse/direct_index.rs index 51580a3e5..2857e840f 100644 --- a/crates/uffs-mft/src/parse/direct_index.rs +++ b/crates/uffs-mft/src/parse/direct_index.rs @@ -657,8 +657,10 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf .map(|(name, size, alloc)| add_stream_to_index(index, &name, size, alloc)) .collect(); - // Setup record and chain streams - let record = index.get_or_create(frs); + // Setup record and chain streams. + // Boundary: lift the raw `u64` FRS argument (kernel/USN buffer) + // into a typed `Frs` once for the typed index API. + let record = index.get_or_create(crate::frs::Frs::new(frs)); record.stdinfo = std_info; record.first_stream.size = SizeInfo { length: default_size, @@ -667,7 +669,7 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf if !stream_indices.is_empty() { chain_streams(index, &stream_indices); - let record = index.get_or_create(frs); + let record = index.get_or_create(crate::frs::Frs::new(frs)); record.first_stream.next_entry = stream_indices[0]; record.stream_count = 1 + len_to_u16(additional_stream_count); } @@ -702,15 +704,16 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf .map(|(name, size, alloc)| add_stream_to_index(index, &name, size, alloc)) .collect(); - // Ensure parent exists (create placeholder if needed) - do this before getting - // our record + // Ensure parent exists (create placeholder if needed) - do this before + // getting our record. Boundary: typed wrap of the raw `u64` from this + // file's parse-layer locals into the typed index API. if parent_frs != frs && parent_frs != 0 { - let _ = index.get_or_create(parent_frs); + let _ = index.get_or_create(crate::frs::Frs::new(parent_frs)); } - // Now get or create the record in the index - no more index mutations after - // this - let record = index.get_or_create(frs); + // Now get or create the record in the index - no more index mutations + // after this. + let record = index.get_or_create(crate::frs::Frs::new(frs)); record.stdinfo = std_info; record.first_stream.size = SizeInfo { length: default_size, @@ -720,7 +723,9 @@ pub fn parse_record_to_index(data: &[u8], frs: u64, index: &mut crate::index::Mf next_entry: NO_ENTRY, name: name_ref, _pad0: [0; 4], - parent_frs, + // Typed `ParentFrs` written into the typed `LinkInfo.parent_frs` + // slot; raw `u64` only lives as a parse-layer local. + parent_frs: crate::frs::ParentFrs::new(parent_frs), }; record.name_count = 1 + len_to_u16(additional_count); // stream_count = 1 (default) + additional ADS diff --git a/crates/uffs-mft/src/parse/direct_index_extension.rs b/crates/uffs-mft/src/parse/direct_index_extension.rs index 7ff31d025..6e6676a5b 100644 --- a/crates/uffs-mft/src/parse/direct_index_extension.rs +++ b/crates/uffs-mft/src/parse/direct_index_extension.rs @@ -554,10 +554,11 @@ pub(super) fn parse_extension_to_index( .map(|(name, size, alloc)| add_stream_to_index(index, name, *size, *alloc)) .collect(); - // Ensure parent directories exist for the new names + // Ensure parent directories exist for the new names. Parser-local + // raw `u64` lifts to typed `Frs` at the typed-API boundary. for (_, parent_frs) in &names { if *parent_frs != base_frs && *parent_frs != 0 { - let _ = index.get_or_create(*parent_frs); + let _ = index.get_or_create(crate::frs::Frs::new(*parent_frs)); } } @@ -565,13 +566,13 @@ pub(super) fn parse_extension_to_index( let base_frs_usize = frs_to_usize(base_frs); if base_frs_usize >= index.frs_to_idx.len() { // Base record doesn't exist yet - create a placeholder - let _ = index.get_or_create(base_frs); + let _ = index.get_or_create(crate::frs::Frs::new(base_frs)); } let record_idx = index.frs_to_idx[base_frs_usize]; if record_idx == NO_ENTRY { // Base record doesn't exist - create it - let _ = index.get_or_create(base_frs); + let _ = index.get_or_create(crate::frs::Frs::new(base_frs)); } // Now get the record and chain the new links/streams @@ -771,7 +772,9 @@ pub(super) fn parse_extension_to_index( // Create placeholder parent let new_idx = len_to_u32(index.records.len()); index.frs_to_idx[p_frs_usize] = new_idx; - index.records.push(crate::index::FileRecord::new(p_frs)); + index + .records + .push(crate::index::FileRecord::new(crate::frs::Frs::new(p_frs))); } index.frs_to_idx[p_frs_usize] }; @@ -796,7 +799,7 @@ pub(super) fn parse_extension_to_index( index.children.push(ChildInfo { next_entry: old_first_child, _pad0: [0; 4], - child_frs: base_frs, + child_frs: crate::frs::Frs::new(base_frs), name_index: effective_name_idx, _pad1: [0; 6], }); diff --git a/crates/uffs-mft/src/parse/direct_index_extension_tests.rs b/crates/uffs-mft/src/parse/direct_index_extension_tests.rs index 6013623cb..8e522f817 100644 --- a/crates/uffs-mft/src/parse/direct_index_extension_tests.rs +++ b/crates/uffs-mft/src/parse/direct_index_extension_tests.rs @@ -14,7 +14,9 @@ use crate::index::{IndexNameRef, IndexStreamInfo, MftIndex, NO_ENTRY, SizeInfo}; /// Test helper to create a `FileRecord` with specified `first_stream` size fn create_test_record(frs: u64, length: u64, allocated: u64) -> crate::index::FileRecord { crate::index::FileRecord { - frs, + // Test helper takes raw `u64` for ergonomic callers; lift to typed + // `Frs` at the index-field boundary. + frs: frs.into(), first_stream: IndexStreamInfo { size: SizeInfo { length, allocated }, next_entry: NO_ENTRY, diff --git a/crates/uffs-mft/src/parse/index_helpers.rs b/crates/uffs-mft/src/parse/index_helpers.rs index 99b5966fd..049aef4a9 100644 --- a/crates/uffs-mft/src/parse/index_helpers.rs +++ b/crates/uffs-mft/src/parse/index_helpers.rs @@ -140,7 +140,9 @@ pub(crate) fn add_link_to_index(index: &mut MftIndex, link_name: &str, link_pare next_entry: NO_ENTRY, name: link_name_ref, _pad0: [0; 4], - parent_frs: link_parent, + // Parser locals are still raw `u64`; lift to typed `ParentFrs` + // at the typed index-struct construction boundary. + parent_frs: crate::frs::ParentFrs::new(link_parent), }); link_idx } @@ -157,7 +159,9 @@ pub(crate) fn add_child_entry( return; } - // Ensure parent exists + // Ensure parent exists. `frs_to_idx` is `Vec` indexed by `usize`, + // so `parent_frs` stays raw here; typed `Frs` is built only when + // writing to a typed index field. let parent_idx = { let p_frs_usize = frs_to_usize(parent_frs); if p_frs_usize >= index.frs_to_idx.len() { @@ -168,7 +172,9 @@ pub(crate) fn add_child_entry( index.frs_to_idx[p_frs_usize] = new_idx; index .records - .push(crate::index::FileRecord::new(parent_frs)); + .push(crate::index::FileRecord::new(crate::frs::Frs::new( + parent_frs, + ))); } index.frs_to_idx[p_frs_usize] }; @@ -182,7 +188,8 @@ pub(crate) fn add_child_entry( index.children.push(ChildInfo { next_entry: old_first_child, _pad0: [0; 4], - child_frs, + // Lift parser-local raw `u64` to typed `Frs` at the boundary. + child_frs: crate::frs::Frs::new(child_frs), name_index: name_idx, _pad1: [0; 6], }); @@ -222,16 +229,19 @@ pub(crate) fn merge_extension_streams( first_internal: u32, ext: &ExtensionSnapshot, ) { + // Lift parser-local raw `u64` to typed `Frs` once for all the typed + // `get_or_create` calls below. + let frs_typed = crate::frs::Frs::new(frs); // Merge user-visible streams if ext.stream_count > 0 { let tail = base_stream_tail.unwrap_or(NO_ENTRY); if tail != NO_ENTRY { index.streams[u32_as_usize(tail)].next_entry = ext.stream_head; } else { - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.first_stream.next_entry = ext.stream_head; } - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.stream_count += ext.stream_count; record.total_stream_count += ext.stream_count; } @@ -245,10 +255,10 @@ pub(crate) fn merge_extension_streams( } index.internal_streams[u32_as_usize(tail)].next_entry = ext.internal_head; } else { - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.first_internal_stream = ext.internal_head; } - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.internal_streams_size += ext.internal_size; record.internal_streams_allocated += ext.internal_alloc; record.total_stream_count += ext.total_extra.saturating_sub(ext.stream_count); @@ -264,14 +274,15 @@ pub(crate) fn merge_extension_names( ext: &ExtensionSnapshot, ) { if ext.name_count > 0 { + let frs_typed = crate::frs::Frs::new(frs); let tail = base_name_tail.unwrap_or(NO_ENTRY); if tail != NO_ENTRY { index.links[u32_as_usize(tail)].next_entry = ext.name_next; } else { - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.first_name.next_entry = ext.name_next; } - let record = index.get_or_create(frs); + let record = index.get_or_create(frs_typed); record.name_count += ext.name_count; } } diff --git a/crates/uffs-mft/src/tree_metrics.rs b/crates/uffs-mft/src/tree_metrics.rs index 9a809e926..45a5a7c8f 100644 --- a/crates/uffs-mft/src/tree_metrics.rs +++ b/crates/uffs-mft/src/tree_metrics.rs @@ -131,7 +131,7 @@ const fn compute_name_info(name_index: u32, total_names: u32) -> u32 { fn compute_name_info_checked( name_index: u32, total_names: u32, - child_frs: u64, + child_frs: crate::frs::Frs, debug: bool, ) -> u32 { if total_names <= 1 { @@ -139,9 +139,12 @@ fn compute_name_info_checked( } // Check for out-of-range name_index (parity risk) if name_index >= total_names { + // `tracing::Value` is not impl'd on the typed `Frs` newtype, so + // demote with `.raw()` only at the logging-field boundary. + let child_frs_raw = child_frs.raw(); // Always log in release mode too - this is a parity diagnostic tracing::warn!( - child_frs, + child_frs = child_frs_raw, name_index, total_names, "[TRIP] name_index out of range; clamping (parity risk)" @@ -149,7 +152,7 @@ fn compute_name_info_checked( if debug { // Extra verbose output in debug mode tracing::debug!( - child_frs, + child_frs = child_frs_raw, name_index, total_names, "[TRIP] compute_name_info_checked: clamping name_index to total_names-1" @@ -242,9 +245,7 @@ impl TreeTraversal<'_> { /// Start DFS from the NTFS root directory (FRS 5). fn traverse_from_root(&mut self) { - const ROOT_FRS: u64 = 5; - - if let Some(root_idx) = self.index.frs_to_idx_opt(ROOT_FRS) { + if let Some(root_idx) = self.index.frs_to_idx_opt(crate::frs::Frs::ROOT) { tracing::debug!(root_idx, "[TRIP] starting from ROOT (FRS=5)"); let _: Agg = self.preprocess(root_idx, 0, 1); tracing::debug!("[TRIP] ROOT traversal done"); @@ -565,8 +566,11 @@ pub(crate) fn compute_tree_metrics(index: &mut MftIndex, debug: bool, skip_orpha fn warn_unstamped_directories(index: &MftIndex) { for (idx, rec) in index.records.iter().enumerate() { if rec.stdinfo.is_directory() && rec.descendants == 0 { + // `tracing::Value` is not implemented for the typed `Frs` + // newtype; demote with `.raw()` only at this logging + // boundary so the warning preserves its u64 field shape. tracing::warn!( - frs = rec.frs, + frs = rec.frs.raw(), idx = idx, first_child = rec.first_child, name_count = rec.name_count, From fb822a90f0b624e120f919e15456c6642c7d5f99 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sat, 16 May 2026 12:15:00 -0700 Subject: [PATCH 2/3] =?UTF-8?q?refactor(uffs-mft):=20migrate=20journal/USN?= =?UTF-8?q?=20layer=20to=20Frs=20/=20ParentFrs=20newtypes=20=E2=80=94=20Ph?= =?UTF-8?q?ase=204=20sub-phase=205d.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` → `HashMap`. `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` keyed by `usize`, and (b) the returned `frs_to_read: Vec` 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` 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). --- crates/uffs-core/src/compact_loader.rs | 15 +++++-- crates/uffs-core/src/compact_loader_tests.rs | 40 +++++++++---------- .../src/cache/journal_loop/tests.rs | 5 ++- crates/uffs-daemon/src/cache/journal_sink.rs | 21 +++++++--- crates/uffs-daemon/src/cache/shard/tests.rs | 4 +- crates/uffs-mft/src/index/usn.rs | 11 +++-- crates/uffs-mft/src/usn/mod.rs | 28 ++++++++----- crates/uffs-mft/src/usn/windows.rs | 11 ++++- 8 files changed, 89 insertions(+), 46 deletions(-) diff --git a/crates/uffs-core/src/compact_loader.rs b/crates/uffs-core/src/compact_loader.rs index b907572d4..cdd78989c 100644 --- a/crates/uffs-core/src/compact_loader.rs +++ b/crates/uffs-core/src/compact_loader.rs @@ -507,7 +507,12 @@ pub fn apply_usn_patch( let mut stats = PatchStats::default(); for change in changes { - let frs_usize = uffs_mft::frs_to_usize(change.frs); + // Typed `Frs` → raw `u64` lift at the frs_to_compact CSR lookup + // boundary. The mapping table is `Vec` indexed by + // `usize`, so demoting once per change keeps the inner index + // arithmetic on raw values without leaking raw FRS into the + // outer `FileChange` API. + let frs_usize = uffs_mft::frs_to_usize(change.frs.raw()); let compact_idx = drive .frs_to_compact .get(frs_usize) @@ -552,7 +557,10 @@ pub fn apply_usn_patch( .as_mut_vec() .extend_from_slice(change.filename.as_bytes()); - let parent_frs_usize = uffs_mft::frs_to_usize(change.parent_frs); + // Typed `ParentFrs` → raw `u64` lift at the + // frs_to_compact CSR lookup boundary (same rationale as + // the `change.frs.raw()` lift above). + let parent_frs_usize = uffs_mft::frs_to_usize(change.parent_frs.raw()); let parent_compact = drive .frs_to_compact .get(parent_frs_usize) @@ -618,7 +626,8 @@ pub fn apply_usn_patch( rec.name_len = uffs_mft::len_to_u16(change.filename.len()); } - let new_parent_frs = uffs_mft::frs_to_usize(change.parent_frs); + // Typed `ParentFrs` → raw lift on the rename path. + let new_parent_frs = uffs_mft::frs_to_usize(change.parent_frs.raw()); let new_parent_compact = drive .frs_to_compact .get(new_parent_frs) diff --git a/crates/uffs-core/src/compact_loader_tests.rs b/crates/uffs-core/src/compact_loader_tests.rs index 32ff5bc01..41cb5bd86 100644 --- a/crates/uffs-core/src/compact_loader_tests.rs +++ b/crates/uffs-core/src/compact_loader_tests.rs @@ -125,29 +125,29 @@ fn apply_usn_patch_handles_create_delete_rename_skip() { let changes = vec![ // Delete FRS 10 ("foo.txt"). FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }, // Rename FRS 11 ("bar.rs" → "bar2.rs"), parent unchanged. FileChange { - frs: 11, - parent_frs: 5, + frs: 11_u64.into(), + parent_frs: 5_u64.into(), filename: "bar2.rs".to_owned(), renamed: true, ..FileChange::default() }, // Create FRS 13 ("new.txt", parent=root). FileChange { - frs: 13, - parent_frs: 5, + frs: 13_u64.into(), + parent_frs: 5_u64.into(), filename: "new.txt".to_owned(), created: true, ..FileChange::default() }, // Skip: FRS 99 doesn't map to any compact record. FileChange { - frs: 99, + frs: 99_u64.into(), deleted: true, ..FileChange::default() }, @@ -180,7 +180,7 @@ fn apply_usn_patch_handles_create_delete_rename_skip() { fn apply_usn_patch_marks_deleted_record_with_zero_name_len() { let mut drive = make_synthetic_drive(); let changes = vec![FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }]; @@ -207,8 +207,8 @@ fn apply_usn_patch_marks_deleted_record_with_zero_name_len() { fn apply_usn_patch_renamed_record_has_new_name_in_blob() { let mut drive = make_synthetic_drive(); let changes = vec![FileChange { - frs: 11, - parent_frs: 5, + frs: 11_u64.into(), + parent_frs: 5_u64.into(), filename: "bar2.rs".to_owned(), renamed: true, ..FileChange::default() @@ -246,8 +246,8 @@ fn apply_usn_patch_created_record_appended_with_correct_parent() { let initial_record_count = drive.records.len(); let changes = vec![FileChange { - frs: 13, - parent_frs: 5, + frs: 13_u64.into(), + parent_frs: 5_u64.into(), filename: "new.txt".to_owned(), created: true, ..FileChange::default() @@ -315,7 +315,7 @@ fn apply_usn_patch_rebuilds_children_csr_excluding_deletes() { ); let changes = vec![FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }]; @@ -375,8 +375,8 @@ fn apply_usn_patch_keeps_frs_to_compact_in_lockstep() { ); apply_usn_patch(&mut drive, &[FileChange { - frs: 13, - parent_frs: 5, + frs: 13_u64.into(), + parent_frs: 5_u64.into(), filename: "n1.txt".to_owned(), created: true, ..FileChange::default() @@ -399,7 +399,7 @@ fn apply_usn_patch_keeps_frs_to_compact_in_lockstep() { // ── 2. Delete existing FRS 10 → expect slot reset to u32::MAX. apply_usn_patch(&mut drive, &[FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }]); @@ -415,7 +415,7 @@ fn apply_usn_patch_keeps_frs_to_compact_in_lockstep() { // ── 3. Reuse round-trip: delete FRS 13, then create FRS 13 again. apply_usn_patch(&mut drive, &[FileChange { - frs: 13, + frs: 13_u64.into(), deleted: true, ..FileChange::default() }]); @@ -431,8 +431,8 @@ fn apply_usn_patch_keeps_frs_to_compact_in_lockstep() { let pre_recreate_records = drive.records.len(); apply_usn_patch(&mut drive, &[FileChange { - frs: 13, - parent_frs: 5, + frs: 13_u64.into(), + parent_frs: 5_u64.into(), filename: "n2.txt".to_owned(), created: true, ..FileChange::default() @@ -453,8 +453,8 @@ fn apply_usn_patch_keeps_frs_to_compact_in_lockstep() { // ── 4. Out-of-range create grows the mapping. apply_usn_patch(&mut drive, &[FileChange { - frs: 200, - parent_frs: 5, + frs: 200_u64.into(), + parent_frs: 5_u64.into(), filename: "far.txt".to_owned(), created: true, ..FileChange::default() diff --git a/crates/uffs-daemon/src/cache/journal_loop/tests.rs b/crates/uffs-daemon/src/cache/journal_loop/tests.rs index a51874c43..81d14668b 100644 --- a/crates/uffs-daemon/src/cache/journal_loop/tests.rs +++ b/crates/uffs-daemon/src/cache/journal_loop/tests.rs @@ -270,9 +270,12 @@ fn null_cursor_store() -> Arc { /// Helper: small fake change so the queue is exercising a /// non-trivial batch shape (not just `vec![]`). +/// +/// Lifts the raw `u64` test literal into the typed [`uffs_mft::Frs`] +/// at this single construction boundary. fn one_change(frs: u64) -> FileChange { FileChange { - frs, + frs: frs.into(), deleted: true, ..FileChange::default() } diff --git a/crates/uffs-daemon/src/cache/journal_sink.rs b/crates/uffs-daemon/src/cache/journal_sink.rs index 210b48ad0..37922af19 100644 --- a/crates/uffs-daemon/src/cache/journal_sink.rs +++ b/crates/uffs-daemon/src/cache/journal_sink.rs @@ -344,9 +344,14 @@ mod tests { /// `FileChange::default()` because the sink doesn't inspect them /// — only `IndexManager::handle_journal_save` does (covered in /// `cache::shard::tests` and the patch end-to-end suite). + /// + /// Takes a raw `u64` because FRS values are most naturally + /// written as integer literals in test fixtures; lifts to the + /// typed `Frs` at this single construction boundary so the rest + /// of the test surface keeps the typed contract. fn make_change(frs: u64) -> FileChange { FileChange { - frs, + frs: frs.into(), ..FileChange::default() } } @@ -355,6 +360,9 @@ mod tests { /// dropping the `lock_pending()` guard before returning so the /// caller's assertions don't hold the mutex (satisfies /// `clippy::significant_drop_tightening` in tests). + /// + /// Demotes typed `Frs` → raw `u64` at the snapshot boundary so + /// assertion literals stay as integer arrays. fn pending_frs_for_letter( sink: &RegistryPatchSink, letter: uffs_mft::platform::DriveLetter, @@ -362,7 +370,7 @@ mod tests { let guard = sink.lock_pending(); guard .get(&letter) - .map(|buf| buf.iter().map(|change| change.frs).collect()) + .map(|buf| buf.iter().map(|change| change.frs.raw()).collect()) } /// Pin: `accept` appends to the per-letter pending buffer and @@ -443,7 +451,10 @@ mod tests { assert_eq!(letter, uffs_mft::platform::DriveLetter::C); assert!(matches!(reason, SaveReason::EventsExceeded)); assert_eq!( - changes.iter().map(|change| change.frs).collect::>(), + changes + .iter() + .map(|change| change.frs.raw()) + .collect::>(), [10, 11], "Save must carry the buffered changes in send order", ); @@ -549,7 +560,7 @@ mod tests { assert_eq!( c_changes .iter() - .map(|change| change.frs) + .map(|change| change.frs.raw()) .collect::>(), [1], ); @@ -571,7 +582,7 @@ mod tests { assert_eq!( d_changes .iter() - .map(|change| change.frs) + .map(|change| change.frs.raw()) .collect::>(), [2, 3], "'D's buffer must be preserved across 'C's drain", diff --git a/crates/uffs-daemon/src/cache/shard/tests.rs b/crates/uffs-daemon/src/cache/shard/tests.rs index 342dd6c0e..93296d275 100644 --- a/crates/uffs-daemon/src/cache/shard/tests.rs +++ b/crates/uffs-daemon/src/cache/shard/tests.rs @@ -548,7 +548,7 @@ fn apply_usn_patch_to_body_returns_none_on_parked() { let shard = ShardEntry::new_parked(uffs_mft::platform::DriveLetter::C, stats, parked_body); let changes = vec![FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }]; @@ -592,7 +592,7 @@ fn apply_usn_patch_to_body_lands_delete_on_new_arc() { // delete-on-warm path through the full `apply_usn_patch_to_body` // surface without re-specifying the mapping. let changes = vec![FileChange { - frs: 10, + frs: 10_u64.into(), deleted: true, ..FileChange::default() }]; diff --git a/crates/uffs-mft/src/index/usn.rs b/crates/uffs-mft/src/index/usn.rs index c517335eb..b3e21052f 100644 --- a/crates/uffs-mft/src/index/usn.rs +++ b/crates/uffs-mft/src/index/usn.rs @@ -44,8 +44,13 @@ impl MftIndex { let mut frs_to_read: Vec = Vec::new(); for change in changes { - let frs = change.frs; - let frs_usize = frs_to_usize(frs); + // `change.frs` is typed `Frs`; lift to raw `u64` once at the + // `frs_to_idx` / `frs_to_read` boundary because both the + // index lookup table is `Vec` keyed by `usize` and the + // returned `frs_to_read` feeds the kernel-loop arithmetic + // input of `read_targeted_frs_records(&[u64])`. + let frs_raw = change.frs.raw(); + let frs_usize = frs_to_usize(frs_raw); let idx = self.frs_to_idx.get(frs_usize).copied().unwrap_or(NO_ENTRY); if change.deleted { @@ -62,7 +67,7 @@ impl MftIndex { { // Collect FRS for targeted MFT read — the read will // overwrite or create the full record with correct data. - frs_to_read.push(frs); + frs_to_read.push(frs_raw); } else { stats.skipped += 1; } diff --git a/crates/uffs-mft/src/usn/mod.rs b/crates/uffs-mft/src/usn/mod.rs index a2846b4e4..54d791a54 100644 --- a/crates/uffs-mft/src/usn/mod.rs +++ b/crates/uffs-mft/src/usn/mod.rs @@ -34,6 +34,8 @@ use core::fmt; use std::collections::HashMap; +use crate::frs::{Frs, ParentFrs}; + /// Monotonically-increasing per-volume Update Sequence Number from the /// NTFS USN journal. /// @@ -147,10 +149,11 @@ pub struct UsnJournalInfo { /// A single USN Journal change record. #[derive(Debug, Clone)] pub struct UsnRecord { - /// File Reference Number (FRS) - pub frs: u64, - /// Parent directory FRS - pub parent_frs: u64, + /// File Reference Number (typed [`Frs`] — fixed at the on-disk + /// parser boundary in `usn::windows::read_usn_journal`). + pub frs: Frs, + /// Parent directory FRS (typed [`ParentFrs`]). + pub parent_frs: ParentFrs, /// USN of this record pub usn: Usn, /// Reason flags (bitmask of `USN_REASON_*`) @@ -263,10 +266,11 @@ impl UsnRecord { )] #[derive(Debug, Clone, Default)] pub struct FileChange { - /// File Reference Number - pub frs: u64, - /// Parent directory FRS (latest) - pub parent_frs: u64, + /// File Reference Number (typed [`Frs`]). + pub frs: Frs, + /// Parent directory FRS — latest seen across the aggregated + /// `UsnRecord` stream (typed [`ParentFrs`]). + pub parent_frs: ParentFrs, /// Filename (latest) pub filename: String, /// Was the file created? @@ -282,9 +286,13 @@ pub struct FileChange { } /// Aggregates multiple USN records into per-file changes. +/// +/// Keyed by the typed [`Frs`] — `Frs` derives `Hash + Eq + Copy` so +/// dropping it into a `HashMap` key is bit-identical to keying on the +/// raw `u64` it wraps. #[must_use] -pub fn aggregate_changes(records: &[UsnRecord]) -> HashMap { - let mut changes: HashMap = HashMap::new(); +pub fn aggregate_changes(records: &[UsnRecord]) -> HashMap { + let mut changes: HashMap = HashMap::new(); for record in records { let entry = changes.entry(record.frs).or_insert_with(|| FileChange { frs: record.frs, diff --git a/crates/uffs-mft/src/usn/windows.rs b/crates/uffs-mft/src/usn/windows.rs index f73c46bb1..6ca352d32 100644 --- a/crates/uffs-mft/src/usn/windows.rs +++ b/crates/uffs-mft/src/usn/windows.rs @@ -317,9 +317,16 @@ pub fn read_usn_journal( .collect(); String::from_utf16_lossy(&name_u16) }); + // 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. Mask, then lift into the typed `Frs` / `ParentFrs` + // domain at this single parser-boundary site. + let frs_raw = header.file_reference_number & 0x0000_FFFF_FFFF_FFFF; + let parent_frs_raw = header.parent_file_reference_number & 0x0000_FFFF_FFFF_FFFF; all_records.push(UsnRecord { - frs: header.file_reference_number & 0x0000_FFFF_FFFF_FFFF, - parent_frs: header.parent_file_reference_number & 0x0000_FFFF_FFFF_FFFF, + frs: crate::frs::Frs::new(frs_raw), + parent_frs: crate::frs::ParentFrs::new(parent_frs_raw), usn: super::Usn::new(header.usn), reason: header.reason, file_attributes: header.file_attributes, From 3b96806c2ea7a88af5b3ab6f80aac0bbbb770b26 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sat, 16 May 2026 14:19:11 -0700 Subject: [PATCH 3/3] =?UTF-8?q?refactor(uffs-core):=20migrate=20SearchResu?= =?UTF-8?q?lt=20to=20Frs=20/=20ParentFrs=20newtypes=20=E2=80=94=20Phase=20?= =?UTF-8?q?4=20sub-phase=205d.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` / `parent_frs: Vec` 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 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 (64be6ce5b) 5d.3 journal — UsnRecord, FileChange, USN apply pipeline (fb822a90f) 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). --- crates/uffs-core/src/index_search/result.rs | 33 +++++++++++++-------- crates/uffs-core/src/path_resolver/fast.rs | 20 +++++++++++++ crates/uffs-mft/src/parse/columns.rs | 16 ++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/crates/uffs-core/src/index_search/result.rs b/crates/uffs-core/src/index_search/result.rs index 60c30a259..5a9faa53a 100644 --- a/crates/uffs-core/src/index_search/result.rs +++ b/crates/uffs-core/src/index_search/result.rs @@ -4,6 +4,7 @@ //! Search result modeling for direct `MftIndex` search. use uffs_mft::index::{FileRecord, MftIndex}; +use uffs_mft::{Frs, ParentFrs}; /// Result of a search on `MftIndex`. /// @@ -22,10 +23,16 @@ pub struct SearchResult { /// Allocated size on disk (0 for resident files, cluster-aligned for /// non-resident). pub allocated_size: u64, - /// File Reference Segment number. - pub frs: u64, - /// Parent FRS (for this specific hard link). - pub parent_frs: u64, + /// File Reference Segment number (typed [`Frs`]). + /// + /// Consumers that need a raw `u64` (e.g. CSV / JSON / polars + /// columnar output) demote via [`Frs::raw`] or `u64::from(frs)` + /// at their own final-wire boundary. + pub frs: Frs, + /// Parent FRS for this specific hard link (typed [`ParentFrs`]). + /// + /// Same `.raw()` / `u64::from(...)` demotion contract as `frs`. + pub parent_frs: ParentFrs, /// Whether this is a directory. pub is_directory: bool, /// Stream name (empty for default `$DATA` stream). @@ -63,11 +70,12 @@ impl SearchResult { path: None, // Path resolution is expensive, done on demand size: record.first_stream.size.length, allocated_size: record.first_stream.size.allocated, - // `SearchResult` is the consumer-facing DTO and keeps raw - // `u64` for wire-format / DataFrame parity; demote the typed - // index fields at this construction boundary only. - frs: record.frs.raw(), - parent_frs: record.first_name.parent_frs.raw(), + // Typed FRS values flow through unchanged — the DTO + // carries the typed contract so consumers can't transpose + // own ↔ parent at compile time; demotion to raw `u64` + // happens at any final-wire (CSV / JSON / polars) site. + frs: record.frs, + parent_frs: record.first_name.parent_frs, is_directory, stream_name: String::new(), name_index: 0, @@ -128,9 +136,10 @@ impl SearchResult { path: None, size: stream_info.size.length, allocated_size: stream_info.size.allocated, - // Same DTO-boundary demotion as `from_record_default`. - frs: record.frs.raw(), - parent_frs: name_info.parent_frs.raw(), + // Typed FRS flows through unchanged (same contract as + // `from_record`). + frs: record.frs, + parent_frs: name_info.parent_frs, is_directory, stream_name: stream_name.to_owned(), name_index: name_idx, diff --git a/crates/uffs-core/src/path_resolver/fast.rs b/crates/uffs-core/src/path_resolver/fast.rs index c91f8f73e..c6a7ea56e 100644 --- a/crates/uffs-core/src/path_resolver/fast.rs +++ b/crates/uffs-core/src/path_resolver/fast.rs @@ -2,6 +2,26 @@ // Copyright (c) 2025-2026 SKY, LLC. //! Fast Vec-backed path resolution. +//! +//! # FRS wire-boundary policy (Phase 4 sub-phase 5d.4) +//! +//! Every public method on [`FastPathResolver`] takes either a +//! [`uffs_polars::DataFrame`] with `frs: u64` / `parent_frs: u64` +//! columns or a single `frs: u64` parameter that is read out of a +//! polars column. The polars column **is** the FRS wire boundary by +//! deliberate Phase-4 doctrine — every typed `Frs` / `ParentFrs` +//! value in the workspace ultimately demotes to a raw `u64` at the +//! polars / CSV / JSON edge. +//! +//! As a result the internal [`FastEntry.parent_frs`], the +//! [`FastPathResolver::path_cache`] key, and the +//! [`FastPathResolver::max_frs`] field stay raw `u64`: they live one +//! `for row_idx in 0..df.height()` 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. Callers that have a typed [`uffs_mft::Frs`] +//! demote via `frs.raw()` at the [`FastPathResolver::resolve`] / +//! [`FastPathResolver::resolve_cached`] boundary. use core::mem::size_of; diff --git a/crates/uffs-mft/src/parse/columns.rs b/crates/uffs-mft/src/parse/columns.rs index 01812a96d..92f5c1da5 100644 --- a/crates/uffs-mft/src/parse/columns.rs +++ b/crates/uffs-mft/src/parse/columns.rs @@ -2,6 +2,22 @@ // Copyright (c) 2025-2026 SKY, LLC. //! Column-oriented accumulation helpers for parsed MFT records. +//! +//! # FRS wire-boundary policy (Phase 4 sub-phase 5d.4) +//! +//! The `frs: Vec` / `parent_frs: Vec` fields are the +//! columnar staging buffers that feed +//! [`crate::reader::dataframe_build`] and ultimately become +//! `polars::Series::new("frs", _)` columns. They are deliberately +//! raw `u64` because the polars column type is the FRS wire boundary +//! by Phase-4 doctrine — every typed [`crate::Frs`] / [`crate::ParentFrs`] +//! value in the workspace demotes to raw `u64` at the polars / CSV / +//! JSON edge. +//! +//! Callers with typed FRS values demote via `frs.raw()` / +//! `u64::from(frs)` once per record before pushing into the column +//! vectors; see [`ParsedColumns::push_record`] for the canonical +//! lift site (`self.frs.push(record.frs.raw())`). use tracing::{debug, info, warn};