11#!/usr/bin/env rust-script
22//! Drive-agnostic SHA256 parity verification for UFFS.
33//!
4- //! Runs uffs with cpp_port algorithms against an MFT file, then compares
5- //! the sorted output ( SHA256) against the C++ reference .
4+ //! Compares Rust output against C++ reference by sorting data rows and
5+ //! computing SHA256 over the reassembled content .
66//!
77//! # Usage
88//!
99//! ```bash
10- //! # D-drive (files directly in data_dir)
11- //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data D
10+ //! # Default mode: compare existing Rust output against C++ reference
11+ //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data D --rust /tmp/rust_final_audit.txt
12+ //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data/drive_s S --rust /tmp/rust_s.txt
1213//!
13- //! # S-drive (files in data_dir/drive_s/)
14- //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data/drive_s S
14+ //! # Regenerate mode: run uffs to generate fresh output, then compare
15+ //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data D --regenerate
16+ //! rust-script scripts/verify_parity.rs /Users/rnio/uffs_data/drive_s S --regenerate
1517//! ```
1618//!
19+ //! # Modes
20+ //!
21+ //! **Default (--rust <path>)**: Compares the provided Rust output file against
22+ //! the C++ reference. This is the safe mode since both files were generated
23+ //! in the same timezone/DST period.
24+ //!
25+ //! **--regenerate**: Runs uffs with cpp_port algorithms to produce fresh Rust
26+ //! output, then compares. WARNING: timestamps use the current local timezone
27+ //! offset. If DST has changed since the C++ reference was generated, timestamps
28+ //! will differ by 1 hour and SHA256 won't match. This is expected behavior
29+ //! (both C++ and Rust capture timezone at startup).
30+ //!
1731//! # Output structure
1832//!
1933//! Output files have 2 header lines (CSV header + blank), data rows in the
2943use sha2:: { Sha256 , Digest } ;
3044use std:: env;
3145use std:: fs;
32- use std:: io:: { BufRead , BufReader , Write } ;
46+ use std:: io:: { BufRead , BufReader } ;
3347use std:: path:: { Path , PathBuf } ;
3448use std:: process:: Command ;
3549
@@ -38,57 +52,127 @@ const FOOTER_LINES: usize = 4;
3852
3953fn main ( ) {
4054 let args: Vec < String > = env:: args ( ) . collect ( ) ;
41- if args. len ( ) != 3 {
42- eprintln ! ( "Usage: {} <data_dir> <drive_letter>" , args[ 0 ] ) ;
43- eprintln ! ( ) ;
44- eprintln ! ( "Examples:" ) ;
45- eprintln ! ( " {} /Users/rnio/uffs_data D" , args[ 0 ] ) ;
46- eprintln ! ( " {} /Users/rnio/uffs_data/drive_s S" , args[ 0 ] ) ;
55+
56+ // Parse arguments
57+ if args. len ( ) < 4 {
58+ print_usage ( & args[ 0 ] ) ;
4759 std:: process:: exit ( 1 ) ;
4860 }
4961
5062 let data_dir = PathBuf :: from ( & args[ 1 ] ) ;
5163 let drive_letter = args[ 2 ] . to_uppercase ( ) ;
5264 let drive_lower = drive_letter. to_lowercase ( ) ;
5365
54- println ! ( "=== UFFS SHA256 Parity Verification ===" ) ;
55- println ! ( "Data dir: {}" , data_dir. display( ) ) ;
56- println ! ( "Drive letter: {}" , drive_letter) ;
57- println ! ( ) ;
66+ // Determine mode
67+ let mode = & args[ 3 ] ;
68+ let rust_output = match mode. as_str ( ) {
69+ "--regenerate" => {
70+ // Regenerate mode: run uffs to produce fresh output
71+ regenerate_rust_output ( & data_dir, & drive_letter, & drive_lower)
72+ }
73+ "--rust" => {
74+ // Default mode: use provided Rust output file
75+ if args. len ( ) < 5 {
76+ eprintln ! ( "ERROR: --rust requires a path argument" ) ;
77+ print_usage ( & args[ 0 ] ) ;
78+ std:: process:: exit ( 1 ) ;
79+ }
80+ PathBuf :: from ( & args[ 4 ] )
81+ }
82+ _ => {
83+ eprintln ! ( "ERROR: Unknown mode: {}" , mode) ;
84+ print_usage ( & args[ 0 ] ) ;
85+ std:: process:: exit ( 1 ) ;
86+ }
87+ } ;
5888
59- // --- Locate files ---
60- let mft_file = data_dir. join ( format ! ( "{}_mft.bin" , drive_letter) ) ;
89+ // Validate files exist
6190 let cpp_file = data_dir. join ( format ! ( "cpp_{}.txt" , drive_lower) ) ;
6291
63- if !mft_file . exists ( ) {
64- eprintln ! ( "ERROR: MFT file not found: {}" , mft_file . display( ) ) ;
92+ if !rust_output . exists ( ) {
93+ eprintln ! ( "ERROR: Rust output file not found: {}" , rust_output . display( ) ) ;
6594 std:: process:: exit ( 1 ) ;
6695 }
6796 if !cpp_file. exists ( ) {
6897 eprintln ! ( "ERROR: C++ reference file not found: {}" , cpp_file. display( ) ) ;
6998 std:: process:: exit ( 1 ) ;
7099 }
71100
72- println ! ( "MFT file: {}" , mft_file. display( ) ) ;
101+ println ! ( "=== UFFS SHA256 Parity Verification ===" ) ;
102+ println ! ( "Data dir: {}" , data_dir. display( ) ) ;
103+ println ! ( "Drive letter: {}" , drive_letter) ;
73104 println ! ( "C++ reference: {}" , cpp_file. display( ) ) ;
105+ println ! ( "Rust output: {}" , rust_output. display( ) ) ;
106+ println ! ( ) ;
74107
75- // --- Locate uffs binary ---
108+ // Compute sorted SHA256 for both files
109+ println ! ( "Computing SHA256 of sorted output..." ) ;
110+ let ( cpp_hash, cpp_rows) = sorted_sha256 ( & cpp_file) ;
111+ let ( rust_hash, rust_rows) = sorted_sha256 ( & rust_output) ;
112+
113+ println ! ( ) ;
114+ println ! ( "C++ reference: {} ({} data rows)" , cpp_hash, cpp_rows) ;
115+ println ! ( "Rust output: {} ({} data rows)" , rust_hash, rust_rows) ;
116+ println ! ( ) ;
117+
118+ // Verdict
119+ if cpp_hash == rust_hash {
120+ println ! ( "RESULT: SHA256 MATCH" ) ;
121+ println ! ( " Parity verified for drive {}." , drive_letter) ;
122+ std:: process:: exit ( 0 ) ;
123+ } else {
124+ println ! ( "RESULT: SHA256 MISMATCH" ) ;
125+ println ! ( " C++: {}" , cpp_hash) ;
126+ println ! ( " Rust: {}" , rust_hash) ;
127+ println ! ( " Row count diff: {} (C++) vs {} (Rust)" , cpp_rows, rust_rows) ;
128+ println ! ( ) ;
129+
130+ // Show first few differing lines
131+ show_first_diffs ( & cpp_file, & rust_output) ;
132+
133+ std:: process:: exit ( 1 ) ;
134+ }
135+ }
136+
137+ fn print_usage ( prog : & str ) {
138+ eprintln ! ( "Usage: {} <data_dir> <drive_letter> [--rust <rust_output> | --regenerate]" , prog) ;
139+ eprintln ! ( ) ;
140+ eprintln ! ( "Examples:" ) ;
141+ eprintln ! ( " {} /Users/rnio/uffs_data D --rust /tmp/rust_final_audit.txt" , prog) ;
142+ eprintln ! ( " {} /Users/rnio/uffs_data/drive_s S --rust /tmp/rust_s.txt" , prog) ;
143+ eprintln ! ( " {} /Users/rnio/uffs_data D --regenerate" , prog) ;
144+ }
145+
146+ fn regenerate_rust_output ( data_dir : & Path , drive_letter : & str , drive_lower : & str ) -> PathBuf {
147+ println ! ( "Mode: --regenerate" ) ;
148+ println ! ( "WARNING: Timestamps will use current local timezone offset." ) ;
149+ println ! ( " If DST has changed since C++ reference was generated," ) ;
150+ println ! ( " timestamps will differ by 1 hour (expected behavior)." ) ;
151+ println ! ( ) ;
152+
153+ // Locate MFT file
154+ let mft_file = data_dir. join ( format ! ( "{}_mft.bin" , drive_letter) ) ;
155+ if !mft_file. exists ( ) {
156+ eprintln ! ( "ERROR: MFT file not found: {}" , mft_file. display( ) ) ;
157+ std:: process:: exit ( 1 ) ;
158+ }
159+ println ! ( "MFT file: {}" , mft_file. display( ) ) ;
160+
161+ // Locate uffs binary
76162 let uffs_bin = find_uffs_binary ( ) ;
77163 println ! ( "UFFS binary: {}" , uffs_bin. display( ) ) ;
78164 println ! ( ) ;
79165
80- // --- Run uffs to generate Rust output ---
166+ // Generate output
81167 let rust_output = data_dir. join ( format ! ( "verify_rust_{}.txt" , drive_lower) ) ;
82168 println ! ( "Running uffs scan (cpp_port algorithms)..." ) ;
83169
84- // Force PST timezone (UTC-8) to match C++ reference generated pre-DST
85170 let status = Command :: new ( & uffs_bin)
86171 . env ( "UFFS_EXPERIMENTAL" , "1" )
87- . env ( "TZ" , "PST8" )
88172 . args ( [
89173 "*" ,
90174 "--mft-file" , & mft_file. to_string_lossy ( ) ,
91- "--drive" , & drive_letter,
175+ "--drive" , drive_letter,
92176 "--parse-algo" , "cpp_port" ,
93177 "--tree-algo" , "cpp" ,
94178 "--io-algo" , "cpp" ,
@@ -98,7 +182,10 @@ fn main() {
98182 . status ( ) ;
99183
100184 match status {
101- Ok ( s) if s. success ( ) => println ! ( " uffs scan completed successfully." ) ,
185+ Ok ( s) if s. success ( ) => {
186+ println ! ( " uffs scan completed successfully." ) ;
187+ println ! ( ) ;
188+ }
102189 Ok ( s) => {
103190 eprintln ! ( "ERROR: uffs exited with status {}" , s) ;
104191 std:: process:: exit ( 1 ) ;
@@ -108,46 +195,12 @@ fn main() {
108195 std:: process:: exit ( 1 ) ;
109196 }
110197 }
111- println ! ( ) ;
112-
113- // --- Compute sorted SHA256 for both files ---
114- println ! ( "Computing SHA256 of sorted output..." ) ;
115- let ( cpp_hash, cpp_rows) = sorted_sha256 ( & cpp_file) ;
116- let ( rust_hash, rust_rows) = sorted_sha256 ( & rust_output) ;
117-
118- println ! ( ) ;
119- println ! ( "C++ reference: {} ({} data rows)" , cpp_hash, cpp_rows) ;
120- println ! ( "Rust output: {} ({} data rows)" , rust_hash, rust_rows) ;
121- println ! ( ) ;
122-
123- // --- Verdict ---
124- if cpp_hash == rust_hash {
125- println ! ( "RESULT: SHA256 MATCH" ) ;
126- println ! ( " Parity verified for drive {}." , drive_letter) ;
127198
128- // Clean up temporary file
129- let _ = fs:: remove_file ( & rust_output) ;
130-
131- std:: process:: exit ( 0 ) ;
132- } else {
133- println ! ( "RESULT: SHA256 MISMATCH" ) ;
134- println ! ( " C++: {}" , cpp_hash) ;
135- println ! ( " Rust: {}" , rust_hash) ;
136- println ! ( " Row count diff: {} (C++) vs {} (Rust)" , cpp_rows, rust_rows) ;
137- println ! ( ) ;
138- println ! ( " Rust output kept at: {}" , rust_output. display( ) ) ;
139-
140- // Show first few differing lines
141- show_first_diffs ( & cpp_file, & rust_output) ;
142-
143- std:: process:: exit ( 1 ) ;
144- }
199+ rust_output
145200}
146201
147202/// Find the uffs binary. Checks the literal `~` path from .cargo/config.toml.
148203fn find_uffs_binary ( ) -> PathBuf {
149- // The workspace root is the directory containing this script's parent
150- // We look for the binary relative to the workspace root with a literal ~ directory
151204 let workspace_root = find_workspace_root ( ) ;
152205
153206 // Check release first, then debug
@@ -194,7 +247,6 @@ fn find_uffs_binary() -> PathBuf {
194247
195248/// Find the workspace root by looking for Cargo.toml starting from the script location.
196249fn find_workspace_root ( ) -> PathBuf {
197- // Try current working directory first
198250 let cwd = env:: current_dir ( ) . unwrap_or_else ( |_| PathBuf :: from ( "." ) ) ;
199251 let mut dir = cwd. as_path ( ) ;
200252 loop {
@@ -206,8 +258,6 @@ fn find_workspace_root() -> PathBuf {
206258 None => break ,
207259 }
208260 }
209-
210- // Fallback to cwd
211261 cwd
212262}
213263
@@ -227,7 +277,6 @@ fn sorted_sha256(path: &Path) -> (String, usize) {
227277 if total <= HEADER_LINES + FOOTER_LINES {
228278 eprintln ! ( "WARNING: File {} has only {} lines (expected > {})" ,
229279 path. display( ) , total, HEADER_LINES + FOOTER_LINES ) ;
230- // Hash all content as-is
231280 let mut hasher = Sha256 :: new ( ) ;
232281 for line in & all_lines {
233282 hasher. update ( line. as_bytes ( ) ) ;
@@ -273,7 +322,6 @@ fn show_first_diffs(file_a: &Path, file_b: &Path) {
273322 println ! ( "First 5 differences in sorted data rows:" ) ;
274323 let mut diff_count = 0 ;
275324
276- let max_len = lines_a. len ( ) . max ( lines_b. len ( ) ) ;
277325 let mut ia = 0 ;
278326 let mut ib = 0 ;
279327
0 commit comments