Skip to content

Commit fd6e9bd

Browse files
committed
chore: development v0.2.16 - comprehensive testing complete [auto-commit]
1 parent 34f68a1 commit fd6e9bd

File tree

16 files changed

+181
-56
lines changed

16 files changed

+181
-56
lines changed

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
@@ -32,7 +32,7 @@ members = [
3232
# Workspace Package Metadata (inherited by all crates)
3333
# ─────────────────────────────────────────────────────────────────────────────
3434
[workspace.package]
35-
version = "0.2.15"
35+
version = "0.2.16"
3636
edition = "2024"
3737
rust-version = "1.85"
3838
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.15)
24+
### Benchmark Results (v0.2.16)
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.15** | **18.7 Million** | **~142 seconds** | All disks, fast mode |
36+
| **UFFS v0.2.16** | **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: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,19 @@ use uffs_mft::{MftProgress, MftReader};
304304
/// - Extension filtering: `--ext pictures,mp4,pdf`
305305
/// - Output customization: `--out`, `--columns`, `--sep`, `--quotes`,
306306
/// `--header`, `--pos`, `--neg`
307-
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
307+
#[allow(
308+
clippy::too_many_arguments,
309+
clippy::fn_params_excessive_bools,
310+
clippy::print_stderr
311+
)]
308312
pub async fn search(
309313
pattern: &str,
310314
single_drive: Option<char>,
311315
multi_drives: Option<Vec<char>>,
312316
index: Option<PathBuf>,
313317
files_only: bool,
314318
dirs_only: bool,
319+
hide_system: bool,
315320
min_size: Option<u64>,
316321
max_size: Option<u64>,
317322
limit: u32,
@@ -326,6 +331,9 @@ pub async fn search(
326331
pos: &str,
327332
neg: &str,
328333
) -> Result<()> {
334+
// Start timing for "Finished in X s" output (C++ compatibility)
335+
let start_time = std::time::Instant::now();
336+
329337
// Parse the pattern to extract drive prefix and pattern type
330338
let parsed = ParsedPattern::parse(pattern)
331339
.with_context(|| format!("Invalid pattern: {pattern}"))?
@@ -337,6 +345,7 @@ pub async fn search(
337345
ext_filter,
338346
files_only,
339347
dirs_only,
348+
hide_system,
340349
min_size,
341350
max_size,
342351
limit,
@@ -360,7 +369,7 @@ pub async fn search(
360369

361370
if needs_streaming {
362371
// Streaming mode: output results as each drive completes
363-
return search_streaming(
372+
let result = search_streaming(
364373
multi_drives,
365374
single_drive,
366375
&filters,
@@ -369,6 +378,10 @@ pub async fn search(
369378
&output_config,
370379
)
371380
.await;
381+
// Print timing after streaming completes
382+
let elapsed = start_time.elapsed();
383+
eprintln!("Finished in {} s", elapsed.as_secs());
384+
return result;
372385
}
373386
}
374387

@@ -390,6 +403,10 @@ pub async fn search(
390403
// Output results
391404
write_results(&results, format, out, &output_config)?;
392405

406+
// Print timing (C++ compatibility: "Finished in X s")
407+
let elapsed = start_time.elapsed();
408+
eprintln!("Finished in {} s", elapsed.as_secs());
409+
393410
info!(count = results.height(), "Search complete");
394411
Ok(())
395412
}
@@ -511,6 +528,9 @@ async fn load_and_filter_data(
511528
filtered = resolver
512529
.add_path_column_auto(&filtered)
513530
.context("Failed to add path column")?;
531+
// Add path_only column (directory portion of path)
532+
filtered = uffs_core::add_path_only_column(&filtered)
533+
.context("Failed to add path_only column")?;
514534
}
515535

516536
return Ok(filtered);
@@ -553,6 +573,8 @@ struct QueryFilters<'a> {
553573
files_only: bool,
554574
/// Only return directories (not files).
555575
dirs_only: bool,
576+
/// Hide system files (files starting with $).
577+
hide_system: bool,
556578
/// Minimum file size filter.
557579
min_size: Option<u64>,
558580
/// Maximum file size filter.
@@ -586,6 +608,11 @@ fn execute_query(
586608
query = query.directories_only();
587609
}
588610

611+
// Apply hide_system filter (exclude files starting with $)
612+
if filters.hide_system {
613+
query = query.hide_system_files();
614+
}
615+
589616
// Apply size filters
590617
if let Some(min) = filters.min_size {
591618
query = query.min_size(min);
@@ -893,7 +920,25 @@ async fn search_multi_drive_filtered(
893920
// DataFrames)
894921
let with_paths = if let Some(ref mut resolver) = path_resolver {
895922
match resolver.add_path_column_auto(&filtered) {
896-
Ok(df) => df,
923+
Ok(df) => {
924+
// Add path_only column (directory portion of path)
925+
match uffs_core::add_path_only_column(&df) {
926+
Ok(df_with_path_only) => df_with_path_only,
927+
Err(e) => {
928+
let _ = tx
929+
.send(DriveResult {
930+
drive: drive_char,
931+
df: None,
932+
records_read,
933+
matches,
934+
error: Some(format!("Failed to add path_only: {e}")),
935+
paths_resolved: false,
936+
})
937+
.await;
938+
return;
939+
}
940+
}
941+
}
897942
Err(e) => {
898943
let _ = tx
899944
.send(DriveResult {

crates/uffs-cli/src/main.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ struct Cli {
121121
#[arg(long)]
122122
dirs_only: bool,
123123

124+
/// Hide system files (files starting with $)
125+
#[arg(long)]
126+
hide_system: bool,
127+
124128
/// Minimum file size in bytes
125129
#[arg(long)]
126130
min_size: Option<u64>,
@@ -150,20 +154,21 @@ struct Cli {
150154
out: String,
151155

152156
/// Columns to output (comma-separated or "all")
153-
/// Default: path only (CPP compatible)
154-
#[arg(long, default_value = "path")]
157+
/// Default: all columns (CPP compatible)
158+
#[arg(long, default_value = "all")]
155159
columns: String,
156160

157161
/// Column separator (default: comma)
158162
#[arg(long, default_value = ",")]
159163
sep: String,
160164

161-
/// Quote character for string values (empty = no quotes)
162-
#[arg(long, default_value = "")]
165+
/// Quote character for string values (default: double-quote for CPP
166+
/// compatibility)
167+
#[arg(long, default_value = "\"")]
163168
quotes: String,
164169

165-
/// Include header row in output
166-
#[arg(long, default_value = "false")]
170+
/// Include header row in output (default: true for CPP compatibility)
171+
#[arg(long, default_value = "true")]
167172
header: bool,
168173

169174
/// Representation for active/true boolean attributes
@@ -217,6 +222,10 @@ enum Commands {
217222
#[arg(long)]
218223
dirs_only: bool,
219224

225+
/// Hide system files (files starting with $)
226+
#[arg(long)]
227+
hide_system: bool,
228+
220229
/// Minimum file size in bytes
221230
#[arg(long)]
222231
min_size: Option<u64>,
@@ -261,8 +270,8 @@ enum Commands {
261270
/// hidden, system, archive, readonly, compressed, encrypted,
262271
/// sparse, reparse, offline, notindexed, temporary, virtual,
263272
/// pinned, unpinned, descendants
264-
/// Default: path only (CPP compatible)
265-
#[arg(long, default_value = "path")]
273+
/// Default: all columns (CPP compatible)
274+
#[arg(long, default_value = "all")]
266275
columns: String,
267276

268277
/// Column separator (default: comma)
@@ -271,12 +280,13 @@ enum Commands {
271280
#[arg(long, default_value = ",")]
272281
sep: String,
273282

274-
/// Quote character for string values (empty = no quotes)
275-
#[arg(long, default_value = "")]
283+
/// Quote character for string values (default: double-quote for CPP
284+
/// compatibility)
285+
#[arg(long, default_value = "\"")]
276286
quotes: String,
277287

278-
/// Include header row in output
279-
#[arg(long, default_value = "false")]
288+
/// Include header row in output (default: true for CPP compatibility)
289+
#[arg(long, default_value = "true")]
280290
header: bool,
281291

282292
/// Representation for active/true boolean attributes
@@ -444,6 +454,7 @@ async fn run() -> Result<()> {
444454
index,
445455
files_only,
446456
dirs_only,
457+
hide_system,
447458
min_size,
448459
max_size,
449460
limit,
@@ -465,6 +476,7 @@ async fn run() -> Result<()> {
465476
index,
466477
files_only,
467478
dirs_only,
479+
hide_system,
468480
min_size,
469481
max_size,
470482
limit,
@@ -504,6 +516,7 @@ async fn run() -> Result<()> {
504516
cli.index,
505517
cli.files_only,
506518
cli.dirs_only,
519+
cli.hide_system,
507520
cli.min_size,
508521
cli.max_size,
509522
cli.limit,

crates/uffs-core/src/compiled_pattern.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ impl CompiledPattern {
158158
col_expr.str().starts_with(lit(prefix.clone()))
159159
} else {
160160
let escaped = regex_escape(prefix);
161-
col_expr.str().contains(lit(format!("(?i)^{escaped}")), true)
161+
col_expr
162+
.str()
163+
.contains(lit(format!("(?i)^{escaped}")), true)
162164
}
163165
}
164166

@@ -168,7 +170,9 @@ impl CompiledPattern {
168170
col_expr.str().ends_with(lit(suffix.clone()))
169171
} else {
170172
let escaped = regex_escape(suffix);
171-
col_expr.str().contains(lit(format!("(?i){escaped}$")), true)
173+
col_expr
174+
.str()
175+
.contains(lit(format!("(?i){escaped}$")), true)
172176
}
173177
}
174178

@@ -873,8 +877,10 @@ mod tests {
873877
"$I07QSZ8.TXT",
874878
"test.TXT",
875879
];
876-
let df =
877-
DataFrame::new(input_names.len(), vec![Column::new("name".into(), &input_names)])?;
880+
let df = DataFrame::new(
881+
input_names.len(),
882+
vec![Column::new("name".into(), &input_names)],
883+
)?;
878884

879885
// Test case-sensitive suffix matching
880886
let pattern = CompiledPattern::Suffix(".txt".to_owned());
@@ -903,8 +909,10 @@ mod tests {
903909
"normal.txt",
904910
"$BITMAP",
905911
];
906-
let df =
907-
DataFrame::new(input_names.len(), vec![Column::new("name".into(), &input_names)])?;
912+
let df = DataFrame::new(
913+
input_names.len(),
914+
vec![Column::new("name".into(), &input_names)],
915+
)?;
908916

909917
// Test that files starting with $ are matched by *.txt pattern
910918
let pattern = CompiledPattern::Suffix(".txt".to_owned());
@@ -920,7 +928,12 @@ mod tests {
920928
);
921929

922930
// Verify $I07QSZ8.txt is in the results
923-
let matched_names: Vec<&str> = result.column("name")?.str()?.into_iter().flatten().collect();
931+
let matched_names: Vec<&str> = result
932+
.column("name")?
933+
.str()?
934+
.into_iter()
935+
.flatten()
936+
.collect();
924937
assert!(
925938
matched_names.contains(&"$I07QSZ8.txt") || matched_names.contains(&"$i07qsz8.txt"),
926939
"Should include $I07QSZ8.txt: {matched_names:?}"

crates/uffs-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub use extensions::{
7777
pub use path_resolver::add_path_column_multi_drive;
7878
pub use path_resolver::{
7979
FastPathResolver, FastPathResolverMultiDrive, FastPathResolverStats, NameArena, PathResolver,
80-
add_paths_from_full_data,
80+
add_path_only_column, add_paths_from_full_data,
8181
};
8282
pub use query::MftQuery;
8383
// Re-export commonly used types

0 commit comments

Comments
 (0)