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_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-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..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,6 +70,10 @@ impl SearchResult { path: None, // Path resolution is expensive, done on demand size: record.first_stream.size.length, allocated_size: record.first_stream.size.allocated, + // 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, @@ -125,6 +136,8 @@ impl SearchResult { path: None, size: stream_info.size.length, allocated_size: stream_info.size.allocated, + // Typed FRS flows through unchanged (same contract as + // `from_record`). frs: record.frs, parent_frs: name_info.parent_frs, is_directory, 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/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-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/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-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/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/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/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}; 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, 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,