@@ -52,31 +52,11 @@ fn add_drive_progress(multi_progress: &MultiProgress, drive: char) -> ProgressBa
5252 progress_bar
5353}
5454
55- /// Create a progress bar for saving raw MFT bytes.
56- /// Returns `None` if progress is disabled via `UFFS_NO_PROGRESS=1`.
57- #[ cfg( windows) ]
58- fn create_save_raw_progress ( ) -> Option < ProgressBar > {
59- if is_progress_disabled ( ) {
60- return None ;
61- }
62-
63- let progress_bar = ProgressBar :: new ( 0 ) ;
64- progress_bar. set_style (
65- ProgressStyle :: default_bar ( )
66- . template ( "{spinner:.cyan} [{elapsed_precise}] {bar:40.cyan/blue} {bytes}/{total_bytes} 💾 saving..." )
67- . unwrap_or_else ( |_| ProgressStyle :: default_bar ( ) )
68- . progress_chars ( "━━╸" ) ,
69- ) ;
70- Some ( progress_bar)
71- }
72-
7355use uffs_core:: output:: OutputConfig ;
7456use uffs_core:: pattern:: ParsedPattern ;
7557use uffs_core:: tree:: add_tree_columns;
7658use uffs_core:: { MftQuery , export_csv, export_json, export_table} ;
77- #[ cfg( windows) ]
78- use uffs_mft:: SaveRawOptions ;
79- use uffs_mft:: { MftProgress , MftReader , load_raw_mft_header} ;
59+ use uffs_mft:: { MftProgress , MftReader } ;
8060
8161/// Search for files matching a pattern.
8262///
@@ -633,6 +613,27 @@ pub fn info(path: &Path) -> Result<()> {
633613 Ok ( ( ) )
634614}
635615
616+ /// Format file size in human-readable format.
617+ #[ allow( clippy:: cast_precision_loss, clippy:: float_arithmetic) ]
618+ fn format_size ( bytes : u64 ) -> String {
619+ const KB : u64 = 1024 ;
620+ const MB : u64 = KB * 1024 ;
621+ const GB : u64 = MB * 1024 ;
622+ const TB : u64 = GB * 1024 ;
623+
624+ if bytes >= TB {
625+ format ! ( "{:.2} TB" , bytes as f64 / TB as f64 )
626+ } else if bytes >= GB {
627+ format ! ( "{:.2} GB" , bytes as f64 / GB as f64 )
628+ } else if bytes >= MB {
629+ format ! ( "{:.2} MB" , bytes as f64 / MB as f64 )
630+ } else if bytes >= KB {
631+ format ! ( "{:.2} KB" , bytes as f64 / KB as f64 )
632+ } else {
633+ format ! ( "{bytes} B" )
634+ }
635+ }
636+
636637/// Show statistics about files in an index.
637638///
638639/// # Errors
@@ -690,182 +691,3 @@ pub fn stats(path: &Path, top: u32) -> Result<()> {
690691
691692 Ok ( ( ) )
692693}
693-
694- /// Format file size in human-readable format.
695- #[ allow( clippy:: cast_precision_loss, clippy:: float_arithmetic) ]
696- fn format_size ( bytes : u64 ) -> String {
697- const KB : u64 = 1024 ;
698- const MB : u64 = KB * 1024 ;
699- const GB : u64 = MB * 1024 ;
700- const TB : u64 = GB * 1024 ;
701-
702- if bytes >= TB {
703- format ! ( "{:.2} TB" , bytes as f64 / TB as f64 )
704- } else if bytes >= GB {
705- format ! ( "{:.2} GB" , bytes as f64 / GB as f64 )
706- } else if bytes >= MB {
707- format ! ( "{:.2} MB" , bytes as f64 / MB as f64 )
708- } else if bytes >= KB {
709- format ! ( "{:.2} KB" , bytes as f64 / KB as f64 )
710- } else {
711- format ! ( "{bytes} B" )
712- }
713- }
714-
715- /// Save raw MFT bytes to a file for offline analysis.
716- #[ cfg( windows) ]
717- pub async fn save_raw (
718- drive : char ,
719- output : & Path ,
720- compress : bool ,
721- compression_level : i32 ,
722- ) -> Result < ( ) > {
723- info ! ( drive = %drive, "Reading raw MFT from drive" ) ;
724-
725- let reader = MftReader :: open ( drive)
726- . await
727- . with_context ( || format ! ( "Failed to open drive {drive}:" ) ) ?;
728-
729- // Create progress bar (None if disabled)
730- let pb = create_save_raw_progress ( ) ;
731-
732- let options = SaveRawOptions {
733- compress,
734- compression_level,
735- } ;
736-
737- let header = reader
738- . save_raw_to_file ( output, & options)
739- . await
740- . with_context ( || format ! ( "Failed to save raw MFT to {}" , output. display( ) ) ) ?;
741-
742- if let Some ( ref p) = pb {
743- p. finish_and_clear ( ) ;
744- }
745-
746- let mut stdout = std:: io:: stdout ( ) . lock ( ) ;
747- writeln ! ( stdout) ?;
748- writeln ! ( stdout, "=== Raw MFT Saved ===" ) ?;
749- writeln ! ( stdout, "Output: {}" , output. display( ) ) ?;
750- writeln ! ( stdout, "Records: {}" , header. record_count) ?;
751- writeln ! ( stdout, "Record size: {} bytes" , header. record_size) ?;
752- writeln ! (
753- stdout,
754- "Original size: {}" ,
755- format_size( header. original_size)
756- ) ?;
757- if header. is_compressed ( ) {
758- writeln ! (
759- stdout,
760- "Compressed size: {}" ,
761- format_size( header. compressed_size)
762- ) ?;
763- #[ allow( clippy:: cast_precision_loss, clippy:: float_arithmetic) ]
764- let ratio = header. compressed_size as f64 / header. original_size as f64 * 100.0_f64 ;
765- writeln ! ( stdout, "Compression: {ratio:.1}%" ) ?;
766- } else {
767- writeln ! ( stdout, "Compression: none" ) ?;
768- }
769-
770- Ok ( ( ) )
771- }
772-
773- /// Save raw MFT bytes - non-Windows stub.
774- #[ cfg( not( windows) ) ]
775- // Platform-specific stub must match Windows signature; called once per platform is expected.
776- #[ allow( clippy:: unused_async, clippy:: single_call_fn) ]
777- pub async fn save_raw (
778- _drive : char ,
779- _output : & Path ,
780- _compress : bool ,
781- _compression_level : i32 ,
782- ) -> Result < ( ) > {
783- anyhow:: bail!( "Raw MFT saving is only supported on Windows" ) ;
784- }
785-
786- /// Load raw MFT from a saved file.
787- ///
788- /// # Errors
789- ///
790- /// Returns an error if:
791- /// - The raw MFT file cannot be loaded
792- /// - Writing to stdout fails
793- /// - On non-Windows: always fails (NTFS parsing not supported)
794- // CLI command handler - separate function for testability and maintainability.
795- #[ allow( clippy:: single_call_fn) ]
796- pub fn load_raw ( input : & Path , output : Option < & Path > , info_only : bool ) -> Result < ( ) > {
797- // Load header first
798- let header = load_raw_mft_header ( input)
799- . with_context ( || format ! ( "Failed to load raw MFT header from {}" , input. display( ) ) ) ?;
800-
801- let mut stdout = std:: io:: stdout ( ) . lock ( ) ;
802- writeln ! ( stdout, "=== Raw MFT File Info ===" ) ?;
803- writeln ! ( stdout, "File: {}" , input. display( ) ) ?;
804- writeln ! ( stdout, "Version: {}" , header. version) ?;
805- writeln ! ( stdout, "Records: {}" , header. record_count) ?;
806- writeln ! ( stdout, "Record size: {} bytes" , header. record_size) ?;
807- writeln ! (
808- stdout,
809- "Original size: {}" ,
810- format_size( header. original_size)
811- ) ?;
812- if header. is_compressed ( ) {
813- writeln ! (
814- stdout,
815- "Compressed size: {}" ,
816- format_size( header. compressed_size)
817- ) ?;
818- #[ allow( clippy:: cast_precision_loss, clippy:: float_arithmetic) ]
819- let ratio = header. compressed_size as f64 / header. original_size as f64 * 100.0_f64 ;
820- writeln ! ( stdout, "Compression: {ratio:.1}%" ) ?;
821- } else {
822- writeln ! ( stdout, "Compression: none" ) ?;
823- }
824- // Drop stdout lock before potentially long operations
825- drop ( stdout) ;
826-
827- if info_only {
828- return Ok ( ( ) ) ;
829- }
830-
831- // Parse and export
832- #[ cfg( windows) ]
833- {
834- let output = output. context ( "--output is required when not using --info-only" ) ?;
835-
836- info ! ( "Parsing MFT records" ) ;
837-
838- let df = MftReader :: load_raw_to_dataframe ( input)
839- . with_context ( || format ! ( "Failed to parse raw MFT from {}" , input. display( ) ) ) ?;
840-
841- info ! ( records = df. height( ) , "Parsed records" ) ;
842-
843- // Determine output format from extension
844- let ext = output
845- . extension ( )
846- . and_then ( |e| e. to_str ( ) )
847- . unwrap_or ( "parquet" ) ;
848-
849- match ext {
850- "csv" => {
851- let mut file = File :: create ( output) ?;
852- export_csv ( & df, & mut file) ?;
853- info ! ( path = %output. display( ) , "Exported to CSV" ) ;
854- }
855- _ => {
856- let mut df = df;
857- MftReader :: save_parquet ( & mut df, output) ?;
858- info ! ( path = %output. display( ) , "Exported to Parquet" ) ;
859- }
860- }
861-
862- Ok ( ( ) )
863- }
864-
865- #[ cfg( not( windows) ) ]
866- {
867- // Silence unused variable warning on non-Windows
868- let _: Option < & Path > = output;
869- anyhow:: bail!( "Raw MFT parsing is only supported on Windows (requires NTFS parsing)" ) ;
870- }
871- }
0 commit comments