@@ -42,6 +42,36 @@ static GLOBAL: MiMalloc = MiMalloc;
4242
4343mod commands;
4444
45+ /// Parse a drive letter from various formats for CPP compatibility.
46+ ///
47+ /// Accepts:
48+ /// - Single letter: `C`, `c`
49+ /// - With colon: `C:`, `c:`
50+ ///
51+ /// Returns uppercase drive letter.
52+ fn parse_drive_letter ( input : & str ) -> Result < char , String > {
53+ let trimmed = input. trim ( ) ;
54+ // Strip trailing colon if present (CPP compatibility: "C:" -> "C")
55+ let letter_str = trimmed. strip_suffix ( ':' ) . unwrap_or ( trimmed) ;
56+
57+ if letter_str. len ( ) != 1 {
58+ return Err ( format ! (
59+ "Invalid drive letter '{input}': expected single letter like 'C' or 'C:'"
60+ ) ) ;
61+ }
62+
63+ let ch = letter_str
64+ . chars ( )
65+ . next ( )
66+ . ok_or_else ( || format ! ( "Invalid drive letter '{input}'" ) ) ?;
67+
68+ if !ch. is_ascii_alphabetic ( ) {
69+ return Err ( format ! ( "Invalid drive letter '{input}': must be A-Z" ) ) ;
70+ }
71+
72+ Ok ( ch. to_ascii_uppercase ( ) )
73+ }
74+
4575/// UFFS - Ultra Fast File Search using direct MFT reading
4676#[ derive( Parser ) ]
4777#[ command( name = "uffs" ) ]
@@ -68,12 +98,12 @@ struct Cli {
6898 #[ arg( value_name = "PATTERN" ) ]
6999 pattern : Option < String > ,
70100
71- /// Drive letter to search (e.g., C). Overrides drive in pattern.
72- #[ arg( short, long, conflicts_with = "drives" ) ]
101+ /// Drive letter to search (e.g., C or C: ). Overrides drive in pattern.
102+ #[ arg( short, long, conflicts_with = "drives" , value_parser = parse_drive_letter ) ]
73103 drive : Option < char > ,
74104
75- /// Multiple drive letters to search concurrently (e.g., C,D,E)
76- #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" ) ]
105+ /// Multiple drive letters to search concurrently (e.g., C,D,E or C:,D:,E: )
106+ #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" , value_parser = parse_drive_letter ) ]
77107 drives : Option < Vec < char > > ,
78108
79109 /// Use pre-built index file instead of live MFT
@@ -162,12 +192,13 @@ enum Commands {
162192 /// `">.*\.log$"` - REGEX for .log files
163193 pattern : String ,
164194
165- /// Drive letter to search (e.g., C). Overrides drive in pattern.
166- #[ arg( short, long, conflicts_with = "drives" ) ]
195+ /// Drive letter to search (e.g., C or C: ). Overrides drive in pattern.
196+ #[ arg( short, long, conflicts_with = "drives" , value_parser = parse_drive_letter ) ]
167197 drive : Option < char > ,
168198
169- /// Multiple drive letters to search concurrently (e.g., C,D,E)
170- #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" ) ]
199+ /// Multiple drive letters to search concurrently (e.g., C,D,E or
200+ /// C:,D:,E:)
201+ #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" , value_parser = parse_drive_letter) ]
171202 drives : Option < Vec < char > > ,
172203
173204 /// Use pre-built index file instead of live MFT
@@ -254,12 +285,14 @@ enum Commands {
254285
255286 /// Build an index from a drive's MFT
256287 Index {
257- /// Drive letter to index (e.g., C). Use --drives for multiple drives.
258- #[ arg( short, long, conflicts_with = "drives" ) ]
288+ /// Drive letter to index (e.g., C or C:). Use --drives for multiple
289+ /// drives.
290+ #[ arg( short, long, conflicts_with = "drives" , value_parser = parse_drive_letter) ]
259291 drive : Option < char > ,
260292
261- /// Multiple drive letters to index concurrently (e.g., C,D,E)
262- #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" ) ]
293+ /// Multiple drive letters to index concurrently (e.g., C,D,E or
294+ /// C:,D:,E:)
295+ #[ arg( long, value_delimiter = ',' , conflicts_with = "drive" , value_parser = parse_drive_letter) ]
263296 drives : Option < Vec < char > > ,
264297
265298 /// Output file path
@@ -286,8 +319,8 @@ enum Commands {
286319
287320 /// Save raw MFT bytes to a file for offline analysis
288321 SaveRaw {
289- /// Drive letter to read MFT from (e.g., C)
290- #[ arg( short, long) ]
322+ /// Drive letter to read MFT from (e.g., C or C: )
323+ #[ arg( short, long, value_parser = parse_drive_letter ) ]
291324 drive : char ,
292325
293326 /// Output file path for raw MFT data
@@ -320,9 +353,9 @@ enum Commands {
320353
321354/// Initialize logging with terminal + file support.
322355///
323- /// If `verbose` is true and `RUST_LOG` is not set, uses `info` level for terminal.
324- /// Otherwise, terminal logging is controlled by `RUST_LOG` (default: `error`).
325- /// File logging is controlled by `RUST_LOG_FILE` (default: `info`).
356+ /// If `verbose` is true and `RUST_LOG` is not set, uses `info` level for
357+ /// terminal. Otherwise, terminal logging is controlled by `RUST_LOG` (default:
358+ /// `error`). File logging is controlled by `RUST_LOG_FILE` (default: `info`).
326359/// Log directory is controlled by `UFFS_LOG_DIR` (default: `~/bin/rust`).
327360///
328361/// Returns a guard that must be kept alive for the duration of the program.
@@ -373,18 +406,26 @@ fn init_logging(verbose: bool) -> tracing_appender::non_blocking::WorkerGuard {
373406 // Timer format
374407 let timer = UtcTime :: rfc_3339 ( ) ;
375408
376- // Terminal layer (with ANSI colors)
409+ // Terminal layer (with ANSI colors, file/line info, thread IDs )
377410 let terminal_layer = tracing_subscriber:: fmt:: layer ( )
378411 . with_writer ( stdout)
379412 . with_timer ( timer. clone ( ) )
380413 . with_ansi ( true )
414+ . with_file ( true )
415+ . with_line_number ( true )
416+ . with_thread_ids ( true )
417+ . with_target ( true )
381418 . with_filter ( terminal_filter) ;
382419
383- // File layer (no ANSI colors)
420+ // File layer (no ANSI colors, but with full context )
384421 let file_layer = tracing_subscriber:: fmt:: layer ( )
385422 . with_writer ( non_blocking)
386423 . with_timer ( timer)
387424 . with_ansi ( false )
425+ . with_file ( true )
426+ . with_line_number ( true )
427+ . with_thread_ids ( true )
428+ . with_target ( true )
388429 . with_filter ( file_filter) ;
389430
390431 // Combine layers
@@ -401,8 +442,9 @@ fn init_logging(verbose: bool) -> tracing_appender::non_blocking::WorkerGuard {
401442#[ tokio:: main]
402443#[ allow( clippy:: too_many_lines) ]
403444async fn main ( ) -> Result < ( ) > {
404- // Check for -v/--verbose flag early to set log level before initializing logging
405- // This allows `uffs -v search ...` to show info-level logs without RUST_LOG=info
445+ // Check for -v/--verbose flag early to set log level before initializing
446+ // logging This allows `uffs -v search ...` to show info-level logs without
447+ // RUST_LOG=info
406448 let verbose = std:: env:: args ( ) . any ( |arg| arg == "-v" || arg == "--verbose" ) ;
407449
408450 // Initialize logging with terminal + file support
0 commit comments