onod3000/src/bin/onod3000.rs
2026-10-25 17:30:41 +02:00

132 lines
No EOL
4.6 KiB
Rust

// 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 <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(())
}