onod3000/src/bin/onod3000.rs

140 lines
No EOL
5.1 KiB
Rust

// MIT License
// Copyright (c) 2025 Tobias Raayoni Last (@randogoth)
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// ---
// 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 <file_path>] [--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<TestResult> = 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(())
}