Skip to content

Commit 7b57e49

Browse files
committed
chore: development v0.3.44 - comprehensive testing complete [auto-commit]
1 parent fb52605 commit 7b57e49

File tree

21 files changed

+2089
-3153
lines changed

21 files changed

+2089
-3153
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ exclude = [
3636
# Workspace Package Metadata (inherited by all crates)
3737
# ─────────────────────────────────────────────────────────────────────────────
3838
[workspace.package]
39-
version = "0.3.43"
39+
version = "0.3.44"
4040
edition = "2024"
4141
rust-version = "1.85"
4242
license = "MPL-2.0 OR LicenseRef-UFFS-Commercial"

LOG/Output

Lines changed: 985 additions & 2999 deletions
Large diffs are not rendered by default.

crates/uffs-cli/src/commands/output.rs

Lines changed: 396 additions & 27 deletions
Large diffs are not rendered by default.

crates/uffs-cli/src/commands/raw_io.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,59 @@ pub(super) struct NativeOfflineQueryResults {
3636
pub(super) query_ms: u128,
3737
}
3838

39+
/// Index-only load result for the streaming output path.
40+
pub(super) struct NativeIndexOnly {
41+
/// Loaded offline index.
42+
pub(super) index: uffs_mft::MftIndex,
43+
/// Raw MFT load duration in milliseconds.
44+
pub(super) load_ms: u128,
45+
}
46+
47+
/// Load MFT index without running any query (for streaming output).
48+
pub(super) fn load_index_from_mft_file(
49+
mft_path: &Path,
50+
drive_letter: Option<char>,
51+
debug_tree: bool,
52+
chaos_seed: Option<u64>,
53+
reserved_allocated: Option<u64>,
54+
) -> Result<NativeIndexOnly> {
55+
use uffs_mft::LoadRawOptions;
56+
57+
let volume = drive_letter.unwrap_or('X');
58+
info!(volume = %volume, path = %mft_path.display(), "Loading raw MFT file (index only)");
59+
60+
let t_load = std::time::Instant::now();
61+
let is_iocp = uffs_mft::is_iocp_capture(mft_path).unwrap_or(false);
62+
63+
let index = if let Some(seed) = chaos_seed {
64+
use uffs_mft::io::readers::parallel::{ChaosMftReader, ChaosStrategy};
65+
let chaos_reader = ChaosMftReader::new(ChaosStrategy::Random { seed }, 2 * 1024 * 1024);
66+
chaos_reader
67+
.read_with_chaos(mft_path, volume)
68+
.with_context(|| format!("Failed to load MFT in chaos mode: {}", mft_path.display()))?
69+
} else if is_iocp && !debug_tree {
70+
let mut idx = uffs_mft::load_iocp_to_index(mft_path)
71+
.with_context(|| format!("Failed to load IOCP capture: {}", mft_path.display()))?;
72+
if let Some(ra) = reserved_allocated {
73+
if idx.reserved_allocated_bytes == 0 && ra > 0 {
74+
idx.reserved_allocated_bytes = ra;
75+
idx.compute_tree_metrics();
76+
}
77+
}
78+
idx
79+
} else {
80+
let options = LoadRawOptions {
81+
volume_letter: Some(volume),
82+
..Default::default()
83+
};
84+
MftReader::load_raw_to_index_with_options(mft_path, &options)
85+
.with_context(|| format!("Failed to load raw MFT: {}", mft_path.display()))?
86+
};
87+
let load_ms = t_load.elapsed().as_millis();
88+
89+
Ok(NativeIndexOnly { index, load_ms })
90+
}
91+
3992
/// Query filter options for the search command.
4093
pub struct QueryFilters<'a> {
4194
/// Parsed search pattern (glob, regex, or literal).

crates/uffs-cli/src/commands/search/mod.rs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,6 @@ fn should_use_index_path(
112112
clippy::print_stderr,
113113
reason = "intentional user-facing output to stderr"
114114
)]
115-
#[expect(
116-
clippy::too_many_lines,
117-
reason = "top-level search orchestrator remains the command surface entry point"
118-
)]
119115
#[expect(
120116
clippy::single_call_fn,
121117
reason = "public CLI entry point called from main dispatch"
@@ -209,6 +205,106 @@ pub async fn search(
209205
&& !benchmark
210206
&& can_write_native_results(format, &output_config)
211207
{
208+
// Detect full-scan (no filtering): use streaming direct-write-from-index
209+
// to skip SearchResult allocation entirely.
210+
let is_full_scan = !filters.files_only
211+
&& !filters.dirs_only
212+
&& !filters.hide_system
213+
&& filters.ext_filter.is_none()
214+
&& filters.min_size.is_none()
215+
&& filters.max_size.is_none()
216+
&& filters.limit == 0
217+
&& (filters.parsed.pattern() == "*"
218+
|| filters.parsed.pattern() == "**"
219+
|| filters.parsed.pattern() == "**/*"
220+
|| filters.parsed.pattern().is_empty());
221+
222+
if is_full_scan {
223+
info!(
224+
path = %mft_path.display(),
225+
format,
226+
"📂 Loading raw MFT file via STREAMING direct-from-index path (full scan)"
227+
);
228+
229+
// Load index only (skip IndexQuery::collect entirely)
230+
let native_index = super::raw_io::load_index_from_mft_file(
231+
mft_path,
232+
single_drive,
233+
debug_tree,
234+
chaos_seed,
235+
reserved_allocated,
236+
)?;
237+
238+
let t_output = std::time::Instant::now();
239+
240+
let is_console = matches!(
241+
out.to_lowercase().as_str(),
242+
"console" | "con" | "term" | "terminal"
243+
);
244+
// We need the row count for the footer, but we don't know it
245+
// until after streaming. Use a placeholder and write footer after.
246+
let cpp_pattern = format!(
247+
">{}:{}",
248+
native_index.index.volume,
249+
pattern.replace('*', ".*")
250+
);
251+
252+
let row_count = if is_console {
253+
let stdout_handle = std::io::stdout();
254+
let mut stdout = stdout_handle.lock();
255+
let footer_ctx = crate::commands::output::CppFooterContext {
256+
output_targets: &output_targets,
257+
pattern: &cpp_pattern,
258+
row_count: 0, // Updated by streaming writer
259+
};
260+
crate::commands::output::write_index_streaming(
261+
&native_index.index,
262+
&mut stdout,
263+
format,
264+
&output_config,
265+
&footer_ctx,
266+
)?
267+
} else {
268+
use std::io::Write as _;
269+
let file = std::fs::File::create(out)
270+
.with_context(|| format!("Failed to create output file: {out}"))?;
271+
let mut writer = std::io::BufWriter::with_capacity(256 * 1024, file);
272+
let footer_ctx = crate::commands::output::CppFooterContext {
273+
output_targets: &output_targets,
274+
pattern: &cpp_pattern,
275+
row_count: 0,
276+
};
277+
let count = crate::commands::output::write_index_streaming(
278+
&native_index.index,
279+
&mut writer,
280+
format,
281+
&output_config,
282+
&footer_ctx,
283+
)?;
284+
writer.flush()?;
285+
info!(file = out, "Results written to file");
286+
count
287+
};
288+
289+
let output_ms = t_output.elapsed().as_millis();
290+
291+
if profile {
292+
eprintln!("=== RAW MFT FILE TIMING (streaming) ===");
293+
eprintln!(
294+
" Load from file: {:>6} ms ({} records)",
295+
native_index.load_ms,
296+
native_index.index.len()
297+
);
298+
eprintln!(" Query/filter: skipped (streaming)");
299+
eprintln!(" Output/write: {output_ms:>6} ms ({row_count} rows)");
300+
eprintln!("=== TOTAL: {} ms ===", start_time.elapsed().as_millis());
301+
}
302+
303+
info!(count = row_count, "Search complete (streaming)");
304+
return Ok(());
305+
}
306+
307+
// Filtered query: use IndexQuery → SearchResult path
212308
info!(
213309
path = %mft_path.display(),
214310
format,

crates/uffs-core/src/index_search/query/expansion.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ impl<'a> RecordExpander<'a> {
6060
};
6161
let path_index = self.path_cache.index();
6262
let path_resolver = self.path_cache.resolver();
63+
let dir_cache = self.path_cache.dir_cache();
6364
let cached_path = self.resolve_paths.then(|| {
6465
debug_assert!(
6566
path_resolver.is_valid_idx(record_idx),
6667
"collect_results only resolves paths for valid record indices"
6768
);
68-
path_resolver.materialize_path(path_index, record_idx)
69+
path_resolver.materialize_path_cached(path_index, record_idx, dir_cache)
6970
});
7071

7172
let mut results = Vec::with_capacity(usize::from(name_count) * usize::from(stream_count));

crates/uffs-mft/src/index/base.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,68 @@ impl MftIndex {
247247
}
248248
}
249249

250+
/// Pre-size the `frs_to_idx` lookup table so that all FRS values up to
251+
/// `total_records` can be inserted without any resize checks.
252+
///
253+
/// Call this once before the parse loop to eliminate the per-call
254+
/// `if frs_usize >= self.frs_to_idx.len()` branch in
255+
/// `get_or_create_unified`.
256+
pub fn pre_size_frs_lookup(&mut self, total_records: usize) {
257+
// FRS numbers can be up to total_records (and sometimes slightly
258+
// beyond due to extension records referencing higher FRS values).
259+
// Over-allocate by 10% to cover most cases without any resize.
260+
let capacity = total_records + total_records / 10;
261+
if capacity > self.frs_to_idx.len() {
262+
self.frs_to_idx.resize(capacity, NO_ENTRY);
263+
}
264+
}
265+
266+
/// Ensure a record exists for `frs` and return its index in `records`.
267+
///
268+
/// This is the fast-path equivalent of `get_or_create_unified` that
269+
/// returns a `u32` index instead of a mutable reference. Callers can
270+
/// then use `self.records[idx]` directly, avoiding redundant lookups.
271+
#[inline]
272+
#[expect(
273+
clippy::cast_possible_truncation,
274+
reason = "FRS fits in usize on 64-bit"
275+
)]
276+
#[expect(
277+
clippy::indexing_slicing,
278+
reason = "bounds checked: resize ensures frs_usize < len"
279+
)]
280+
pub fn ensure_record(&mut self, frs: u64) -> u32 {
281+
let frs_usize = frs as usize;
282+
283+
if frs_usize >= self.frs_to_idx.len() {
284+
self.frs_to_idx.resize(frs_usize + 1, NO_ENTRY);
285+
}
286+
287+
let idx = self.frs_to_idx[frs_usize];
288+
if idx == NO_ENTRY {
289+
let new_idx = self.records.len() as u32;
290+
self.frs_to_idx[frs_usize] = new_idx;
291+
self.records.push(FileRecord::new_unified(frs));
292+
new_idx
293+
} else {
294+
idx
295+
}
296+
}
297+
250298
/// Find a record by FRS (returns None if not present)
299+
#[inline]
251300
#[must_use]
301+
#[expect(
302+
clippy::cast_possible_truncation,
303+
reason = "FRS and record index fit in usize on 64-bit"
304+
)]
252305
pub fn find(&self, frs: u64) -> Option<&FileRecord> {
253-
let frs_usize = usize::try_from(frs).ok()?;
306+
let frs_usize = frs as usize;
254307
let idx = *self.frs_to_idx.get(frs_usize)?;
255308
if idx == NO_ENTRY {
256309
None
257310
} else {
258-
self.records.get(usize::try_from(idx).ok()?)
311+
self.records.get(idx as usize)
259312
}
260313
}
261314

@@ -376,14 +429,19 @@ impl MftIndex {
376429
}
377430

378431
/// Convert FRS to record index (returns None if not present).
432+
#[inline]
379433
#[must_use]
434+
#[expect(
435+
clippy::cast_possible_truncation,
436+
reason = "FRS and record index fit in usize on 64-bit"
437+
)]
380438
pub fn frs_to_idx_opt(&self, frs: u64) -> Option<usize> {
381-
let frs_usize = usize::try_from(frs).ok()?;
439+
let frs_usize = frs as usize;
382440
let idx = *self.frs_to_idx.get(frs_usize)?;
383441
if idx == NO_ENTRY {
384442
None
385443
} else {
386-
Some(usize::try_from(idx).ok()?)
444+
Some(idx as usize)
387445
}
388446
}
389447

0 commit comments

Comments
 (0)