Skip to content

Commit e597d5c

Browse files
committed
chore: development v0.2.131 - comprehensive testing complete [auto-commit]
1 parent 905aa76 commit e597d5c

File tree

13 files changed

+290
-126
lines changed

13 files changed

+290
-126
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Cleaned up all TTAPI references from justfile and build scripts
2424
- Updated justfile header and recipes for UFFS
2525

26-
## [0.2.130] - 2026-01-27
26+
## [0.2.131] - 2026-01-27
2727

2828
### Added
2929
- Baseline CI validation for modernization effort
@@ -46,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4646
### Fixed
4747
- Various MFT parsing edge cases
4848

49-
[Unreleased]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.130...HEAD
50-
[0.2.130]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.114...v0.2.130
49+
[Unreleased]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.131...HEAD
50+
[0.2.131]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.114...v0.2.131
5151
[0.2.114]: https://github.com/githubrobbi/UltraFastFileSearch/releases/tag/v0.2.114
5252

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ exclude = [
3939
# Workspace Package Metadata (inherited by all crates)
4040
# ─────────────────────────────────────────────────────────────────────────────
4141
[workspace.package]
42-
version = "0.2.130"
42+
version = "0.2.131"
4343
edition = "2024"
4444
rust-version = "1.85"
4545
license = "MPL-2.0 OR LicenseRef-UFFS-Commercial"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Traditional file search tools (including `os.walk`, `FindFirstFile`, etc.) work
2121

2222
**UFFS reads the MFT directly** - once - and queries it in memory using Polars DataFrames. This is like reading the entire phonebook once instead of looking up each name individually.
2323

24-
### Benchmark Results (v0.2.130)
24+
### Benchmark Results (v0.2.131)
2525

2626
| Drive Type | Records | Time | Throughput |
2727
|------------|---------|------|------------|
@@ -33,7 +33,7 @@ Traditional file search tools (including `os.walk`, `FindFirstFile`, etc.) work
3333

3434
| Comparison | Records | Time | Notes |
3535
|------------|---------|------|-------|
36-
| **UFFS v0.2.130** | **18.7 Million** | **~142 seconds** | All disks, fast mode |
36+
| **UFFS v0.2.131** | **18.7 Million** | **~142 seconds** | All disks, fast mode |
3737
| UFFS v0.1.30 | 18.7 Million | ~315 seconds | Baseline |
3838
| Everything | 19 Million | 178 seconds | All disks |
3939
| WizFile | 6.5 Million | 299 seconds | Single HDD |

crates/uffs-cli/src/commands.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ pub async fn search(
337337
single_drive: Option<char>,
338338
multi_drives: Option<Vec<char>>,
339339
index: Option<PathBuf>,
340+
mft_file: Option<PathBuf>,
340341
files_only: bool,
341342
dirs_only: bool,
342343
hide_system: bool,
@@ -403,7 +404,11 @@ pub async fn search(
403404
// Index path is the default (fast, cached) - DataFrame path is fallback
404405
let use_index_path = should_use_index_path(mode, index.as_ref(), multi_drives.as_ref());
405406

406-
let mut results = if use_index_path {
407+
// Handle raw MFT file input (cross-platform debugging)
408+
let mut results = if let Some(mft_path) = mft_file.as_ref() {
409+
info!(path = %mft_path.display(), "📂 Loading from raw MFT file");
410+
load_and_filter_from_mft_file(mft_path, single_drive, &filters, needs_paths, profile)?
411+
} else if use_index_path {
407412
info!("🚀 Using fast cached MftIndex query path");
408413
#[cfg(windows)]
409414
{
@@ -607,6 +612,56 @@ async fn search_streaming(
607612
}
608613
}
609614

615+
/// Load and filter search data from a raw MFT file (cross-platform debugging).
616+
///
617+
/// This function loads a previously saved raw MFT file and processes it
618+
/// exactly like a live MFT read, enabling debugging on any platform.
619+
/// Same pipeline as Windows live read - only the load source differs.
620+
#[allow(clippy::single_call_fn, clippy::print_stderr)]
621+
fn load_and_filter_from_mft_file(
622+
mft_path: &Path,
623+
drive_letter: Option<char>,
624+
filters: &QueryFilters<'_>,
625+
needs_paths: bool,
626+
profile: bool,
627+
) -> Result<uffs_mft::DataFrame> {
628+
use uffs_mft::{LoadRawOptions, MftReader};
629+
630+
let volume = drive_letter.unwrap_or('X');
631+
info!(volume = %volume, path = %mft_path.display(), "Loading raw MFT file");
632+
633+
// Load raw MFT into MftIndex (same as live read, just from file)
634+
let t_load = std::time::Instant::now();
635+
let options = LoadRawOptions {
636+
volume_letter: Some(volume),
637+
..Default::default()
638+
};
639+
let index = MftReader::load_raw_to_index_with_options(mft_path, &options)
640+
.with_context(|| format!("Failed to load raw MFT: {}", mft_path.display()))?;
641+
let load_ms = t_load.elapsed().as_millis();
642+
643+
// Execute query on index (same as Windows live path)
644+
let t_query = std::time::Instant::now();
645+
let results = execute_index_query(&index, filters, needs_paths)?;
646+
let query_ms = t_query.elapsed().as_millis();
647+
648+
if profile {
649+
let total_ms = load_ms + query_ms;
650+
eprintln!("=== RAW MFT FILE TIMING ===");
651+
eprintln!(
652+
" Load from file: {load_ms:>6} ms ({} records)",
653+
index.len()
654+
);
655+
eprintln!(
656+
" Query/filter: {query_ms:>6} ms ({} matches)",
657+
results.height()
658+
);
659+
eprintln!(" TOTAL: {total_ms:>6} ms");
660+
}
661+
662+
Ok(results)
663+
}
664+
610665
/// Load and filter search data from index file, multiple drives, single drive,
611666
/// or all NTFS drives.
612667
///
@@ -1023,7 +1078,6 @@ fn execute_query(
10231078
///
10241079
/// This is the fast path for simple queries. Returns results as a `DataFrame`
10251080
/// for compatibility with the output pipeline.
1026-
#[cfg(windows)]
10271081
#[allow(clippy::single_call_fn)]
10281082
fn execute_index_query(
10291083
index: &uffs_mft::MftIndex,
@@ -1084,12 +1138,17 @@ fn execute_index_query(
10841138
/// Convert `IndexQuery` results to a `DataFrame` for output compatibility.
10851139
///
10861140
/// **TEMPORARY**: This function exists only for compatibility with the current
1087-
/// output pipeline which expects a DataFrame. The proper solution is to output
1088-
/// directly from SearchResults without DataFrame conversion.
1141+
/// output pipeline which expects a `DataFrame`. The proper solution is to
1142+
/// output directly from `SearchResults` without `DataFrame` conversion.
10891143
///
1090-
/// TODO: Remove this function and output directly from SearchResults +
1091-
/// MftIndex.
1092-
#[cfg(windows)]
1144+
/// TODO: Remove this function and output directly from `SearchResults` +
1145+
/// `MftIndex`.
1146+
#[allow(
1147+
clippy::single_call_fn,
1148+
clippy::too_many_lines,
1149+
clippy::min_ident_chars,
1150+
clippy::option_if_let_else
1151+
)]
10931152
fn results_to_dataframe(
10941153
index: &uffs_mft::MftIndex,
10951154
results: &[uffs_core::SearchResult],
@@ -1166,7 +1225,7 @@ fn results_to_dataframe(
11661225
None
11671226
}
11681227
})
1169-
.map(|s| s.to_lowercase())
1228+
.map(str::to_lowercase)
11701229
.unwrap_or_default()
11711230
};
11721231
file_types.push(file_type);

crates/uffs-cli/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,18 @@ struct Cli {
189189
drives: Option<Vec<char>>,
190190

191191
/// Use pre-built index file instead of live MFT
192-
#[arg(short, long, conflicts_with_all = ["drive", "drives"])]
192+
#[arg(short, long, conflicts_with_all = ["drive", "drives", "mft_file"])]
193193
index: Option<PathBuf>,
194194

195+
/// Use raw MFT file instead of live MFT (cross-platform debugging)
196+
///
197+
/// Load a previously saved raw MFT file (from `uffs save-raw` or `uffs_mft
198+
/// save`). Use `--drive` to specify the volume letter for path
199+
/// resolution (default: X). Example: `uffs "*" --mft-file G_mft.bin
200+
/// --drive G`
201+
#[arg(long, conflicts_with_all = ["index", "drives"])]
202+
mft_file: Option<PathBuf>,
203+
195204
/// Show only files (exclude directories)
196205
#[arg(long)]
197206
files_only: bool,
@@ -466,6 +475,7 @@ async fn run() -> Result<()> {
466475
cli.drive,
467476
cli.drives,
468477
cli.index,
478+
cli.mft_file,
469479
cli.files_only,
470480
cli.dirs_only,
471481
cli.hide_system,

0 commit comments

Comments
 (0)