diff --git a/crates/uffs-cli/src/main.rs b/crates/uffs-cli/src/main.rs index 9aa0c9c22..6d40a4691 100644 --- a/crates/uffs-cli/src/main.rs +++ b/crates/uffs-cli/src/main.rs @@ -237,7 +237,7 @@ struct Cli { limit: u32, /// Output format: table, json, csv, custom - #[arg(short, long, default_value = "custom")] + #[arg(short, long, default_value = "csv")] format: String, /// Case-sensitive matching (default: off) diff --git a/crates/uffs-mft/src/io/readers/parallel/to_index.rs b/crates/uffs-mft/src/io/readers/parallel/to_index.rs index b49f2273e..3145e845e 100644 --- a/crates/uffs-mft/src/io/readers/parallel/to_index.rs +++ b/crates/uffs-mft/src/io/readers/parallel/to_index.rs @@ -102,33 +102,22 @@ impl ParallelMftReader { ); // Generate read chunks with bitmap skip optimization - // For NVMe/SSD, use precise chunk generation to skip unused regions entirely + // CRITICAL: Use standard chunking for ALL drive types (bitmap is advisory, not + // authoritative) The bitmap should be used for I/O optimization (skip + // ranges, pre-allocation), NOT as an authoritative filter for which + // regions to read. If the bitmap is stale (common on live filesystems), + // treating it as authoritative causes record loss. Evidence: HDD path + // (using advisory bitmap) has only 6 missing records vs 10K+ on NVMe/SSD. let use_direct_chunk_io = matches!( self.drive_type, crate::platform::DriveType::Nvme | crate::platform::DriveType::Ssd ); - // For NVMe/SSD: use larger max to allow direct chunk-to-I/O mapping - // For HDD: use standard io_chunk_size for predictable sequential reads - const MAX_DIRECT_IO_SIZE: usize = 16 * 1024 * 1024; // 16MB max for direct I/O - - let sorted_chunks: Vec = match (&self.drive_type, &self.bitmap) { - (crate::platform::DriveType::Nvme | crate::platform::DriveType::Ssd, Some(bitmap)) => { - // NVMe/SSD: Use precise chunks that skip unused regions - // min_gap_records=64 means gaps smaller than 64KB are read through - // Use MAX_DIRECT_IO_SIZE as the max chunk size for direct I/O - let mut chunks = - generate_precise_read_chunks(&self.extent_map, bitmap, MAX_DIRECT_IO_SIZE, 64); - chunks.sort_by_key(|c| c.disk_offset); - chunks - } - _ => { - // HDD or no bitmap: Use standard chunk generation - let mut chunks = - generate_read_chunks(&self.extent_map, self.bitmap.as_ref(), self.chunk_size); - chunks.sort_by_key(|c| c.disk_offset); - chunks - } + let sorted_chunks: Vec = { + let mut chunks = + generate_read_chunks(&self.extent_map, self.bitmap.as_ref(), self.chunk_size); + chunks.sort_by_key(|c| c.disk_offset); + chunks }; // Build I/O operations with FRS tracking for inline parsing diff --git a/crates/uffs-mft/src/reader/index_read.rs b/crates/uffs-mft/src/reader/index_read.rs index cbf2e0de6..3428ec64d 100644 --- a/crates/uffs-mft/src/reader/index_read.rs +++ b/crates/uffs-mft/src/reader/index_read.rs @@ -620,6 +620,11 @@ impl MftReader { let mut index = result?; + // Sort directory children first so tree metrics traverse in correct order. + // Tree metrics computation depends on children being in sorted order to + // produce accurate directory sizes and descendant counts. + index.sort_directory_children(); + // Compute tree metrics (directory sizes, descendant counts). // The legacy path gets this from `from_parsed_records()`, but the // inline path bypasses that, so we must call it explicitly.