// Copyright 2025 Tobias Raayoni Last (@randogoth) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // --- // This project is a Rust port of the original Java implementation by Paul Uszak. // Original work is licensed under the terms specified in the respective file header. use std::fs::File; use std::io::{self, Read}; use std::path::Path; use onod3000::calibration::MIN_BYTES; use onod3000::{Onod, TestResult, Verdict, TESTS}; fn main() -> io::Result<()> { let mut args = std::env::args(); args.next(); // Skip program name let mut input_data = Vec::new(); let mut source = String::from("stdin"); let mut file_path = None; let mut depth = true; while let Some(arg) = args.next() { if arg == "-f" { if let Some(path) = args.next() { file_path = Some(path); } else { eprintln!("Error: No file path provided after -f."); std::process::exit(1); } } else if arg == "--depth-off" { depth = false; } else { eprintln!("Usage: {} [-f ] [--depth-off]", std::env::args().next().unwrap()); std::process::exit(1); } } if let Some(file_path) = file_path { source = file_path.clone(); let path = Path::new(&file_path); let mut file = File::open(path)?; file.read_to_end(&mut input_data)?; } else { io::stdin().read_to_end(&mut input_data)?; } if input_data.is_empty() { eprintln!("Error: No input data provided."); std::process::exit(1); } println!("\nTesting {} bytes from {}.", input_data.len(), source); println!("----------------------------------------------------------------"); println!("Randomness Test Value Z-Score P-Value Cal-P Pass"); println!("----------------------------------------------------------------"); let mut passed_tests = 0; let alpha = 0.01; let score = Onod::score(&input_data, depth); // Below the calibrated size the score has no test results, so run them raw. let tests: Vec = if score.tests.is_empty() { TESTS .iter() .map(|&(name, key)| { let (stat, z, p_raw) = Onod::run(key, &input_data); TestResult { name, stat, z, p_raw, p_cal: None } }) .collect() } else { score.tests.clone() }; for test in &tests { // Pass on the calibrated p-value where there is one. let p_value = test.p_cal.unwrap_or(test.p_raw); let result = if p_value >= alpha && test.stat != -1.0 { passed_tests += 1; "✅" } else { if test.stat == -1.0 { "SKIP" } else { "❌" } }; let p_cal = test.p_cal.map_or(String::from("-"), |p| format!("{:.4}", p)); println!( "{:<15} {:>15.3} {:>8.4} {:>7.4} {:>7} {:>2}", test.name, test.stat, test.z, test.p_raw, p_cal, result ); } println!("----------------------------------------------------------------"); println!("{}/{} tests passed.", passed_tests, tests.len()); println!("----------------------------------------------------------------"); if score.verdict == Verdict::Insufficient && score.tests.is_empty() { println!("Score needs at least {} bytes.", MIN_BYTES); } else if score.verdict == Verdict::Insufficient { println!("Score unavailable: no calibration table compiled in."); } else { let weakest = score.weakest.map_or(String::new(), |i| { format!(" · weakest: {} p={:.3}", tests[i].name, tests[i].p_cal.unwrap_or(f64::NAN)) }); let depth = if depth { format!(" · Depth {:.1}{} bits", score.depth_bits, if score.depth_complete { "+" } else { "" }) } else { String::new() }; println!("Score {:.2} ({}){}{}", score.score, score.verdict.as_str(), weakest, depth); } println!("----------------------------------------------------------------"); Ok(()) }