// Copyright 2025 Don MacAskill. Licensed under MIT or Apache-2.0. //! This is a simple program to calculate a checksum from the command line use crc_fast::{checksum, checksum_file, CrcAlgorithm}; use std::env; use std::process::ExitCode; use std::str::FromStr; #[derive(Debug)] struct Config { algorithm: String, file: Option, string: Option, format: OutputFormat, benchmark: Option, } #[derive(Debug)] struct BenchmarkConfig { size: Option, duration: f64, } #[derive(Debug)] enum BenchmarkData { InMemory(Vec), File(String), } #[derive(Debug)] struct BenchmarkRunner { algorithm: CrcAlgorithm, data: BenchmarkData, duration: f64, } #[derive(Debug)] struct BenchmarkResult { iterations: u64, elapsed_seconds: f64, throughput_gibs: f64, time_per_iteration_nanos: f64, acceleration_target: String, data_size: u64, } #[derive(Debug, Clone)] enum OutputFormat { Hex, Decimal, } impl BenchmarkConfig { fn validate(&self) -> Result<(), String> { if self.duration <= 0.0 { return Err("Duration must be greater than 0".to_string()); } if let Some(size) = self.size { if size == 0 { return Err("Size must be greater than 0".to_string()); } } Ok(()) } } impl BenchmarkRunner { fn new(algorithm: CrcAlgorithm, data: BenchmarkData, duration: f64) -> Self { Self { algorithm, data, duration, } } fn run(&self) -> Result { use std::time::Instant; let start = Instant::now(); let mut iterations = 0u64; while start.elapsed().as_secs_f64() < self.duration { match &self.data { BenchmarkData::InMemory(data) => { std::hint::black_box(checksum(self.algorithm, data)); } BenchmarkData::File(filename) => { match checksum_file(self.algorithm, filename, None) { Ok(result) => { std::hint::black_box(result); } Err(e) => { return Err(format!("Failed to read file during benchmark: {}", e)); } } } } iterations += 1; } let elapsed = start.elapsed().as_secs_f64(); let data_size = match &self.data { BenchmarkData::InMemory(data) => data.len() as u64, BenchmarkData::File(filename) => std::fs::metadata(filename) .map(|m| m.len()) .map_err(|e| format!("Failed to get file size: {}", e))?, }; let acceleration_target = crc_fast::get_calculator_target(self.algorithm); Ok(BenchmarkResult::new( iterations, elapsed, acceleration_target, data_size, )) } } impl BenchmarkResult { fn new( iterations: u64, elapsed_seconds: f64, acceleration_target: String, data_size: u64, ) -> Self { let throughput_gibs = if elapsed_seconds > 0.0 { (data_size as f64 * iterations as f64) / elapsed_seconds / (1024.0 * 1024.0 * 1024.0) } else { 0.0 }; let time_per_iteration_nanos = if iterations > 0 { elapsed_seconds * 1_000_000_000.0 / iterations as f64 } else { 0.0 }; Self { iterations, elapsed_seconds, throughput_gibs, time_per_iteration_nanos, acceleration_target, data_size, } } } /// Fills a buffer with pseudo-random data using xorshift64* algorithm. /// This is a deterministic PRNG that's tiny, fast, and suitable for benchmark data generation. /// The seed is derived from the buffer size to provide some variability. /// This solves the problem of having to use a full-featured PRNG like rand for benchmark data generation. #[inline] fn fill_pseudo_random(buf: &mut [u8], seed: u64) { let mut x = seed; for b in buf { x ^= x >> 12; x ^= x << 25; x ^= x >> 27; let r = x.wrapping_mul(0x2545_F491_4F6C_DD1D); *b = r as u8; } } fn generate_random_data(size: usize) -> Result, String> { // Check for reasonable size limits to prevent memory issues if size > 1_073_741_824 { // 1 GiB limit return Err("Data size too large (maximum 1 GiB)".to_string()); } // Use vec! macro to avoid clippy warning about slow initialization let mut buf = vec![0u8; size]; // Derive seed from size for deterministic but varied data let seed = 0x9E37_79B9_7F4A_7C15_u64.wrapping_add(size as u64); fill_pseudo_random(&mut buf, seed); Ok(buf) } fn print_usage() { println!("Usage: checksum -a algorithm [-f file] [-s string] [--format hex|decimal]"); println!( " checksum -a algorithm -b [--size bytes] [--duration seconds] [-f file] [-s string]" ); println!(); println!("Example: checksum -a CRC-32/ISCSI -f myfile.txt"); println!("Example: checksum -a CRC-64/NVME -s 'Hello, world!' --format decimal"); println!("Example: checksum -a CRC-32/ISCSI -b --size 1048576 --duration 5.0"); println!(); println!("Options:"); println!(" -a algorithm Specify the checksum algorithm (required)"); println!(" -f file Calculate checksum for the specified file"); println!(" -h, --help Show this help message"); println!(" -s string Calculate checksum for the specified string"); println!(" --format hex|decimal Output format (default: hex)"); println!(); println!("Benchmarking:"); println!(" -b Enable benchmark mode"); println!(" --duration seconds Benchmark duration in seconds (default: 10.0)"); println!(" --size bytes Data size for random generation in benchmark mode (default: 1048576 [1MiB])"); println!(); println!(); println!("Note: In normal mode, either -f or -s must be provided, but not both."); println!(" In benchmark mode (-b), -f or -s are optional for using specific data."); } fn parse_args() -> Result { let args: Vec = env::args().collect(); if args.len() == 1 { return Err("No arguments provided".to_string()); } // Check for help flag if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) { return Err("help".to_string()); } let mut algorithm: Option = None; let mut file: Option = None; let mut string: Option = None; let mut format = OutputFormat::Hex; // Default to hex let mut benchmark_mode = false; let mut benchmark_size: Option = None; let mut benchmark_duration = 10.0; // Default duration let mut i = 1; // Skip program name while i < args.len() { match args[i].as_str() { "-a" => { if i + 1 >= args.len() { return Err("Missing algorithm after -a flag".to_string()); } algorithm = Some(args[i + 1].clone()); i += 2; } "-f" => { if i + 1 >= args.len() { return Err("Missing filename after -f flag".to_string()); } if string.is_some() { return Err("Cannot specify both -f and -s flags".to_string()); } file = Some(args[i + 1].clone()); i += 2; } "-s" => { if i + 1 >= args.len() { return Err("Missing string after -s flag".to_string()); } if file.is_some() { return Err("Cannot specify both -f and -s flags".to_string()); } string = Some(args[i + 1].clone()); i += 2; } "--format" => { if i + 1 >= args.len() { return Err("Missing format after --format flag".to_string()); } match args[i + 1].as_str() { "hex" => format = OutputFormat::Hex, "decimal" => format = OutputFormat::Decimal, invalid => { return Err(format!( "Invalid format '{}'. Use 'hex' or 'decimal'", invalid )) } } i += 2; } "-b" => { benchmark_mode = true; i += 1; } "--size" => { if i + 1 >= args.len() { return Err("Missing size value after --size flag".to_string()); } benchmark_size = Some( args[i + 1] .parse::() .map_err(|_| format!("Invalid size value: {}", args[i + 1]))?, ); i += 2; } "--duration" => { if i + 1 >= args.len() { return Err("Missing duration value after --duration flag".to_string()); } benchmark_duration = args[i + 1] .parse::() .map_err(|_| format!("Invalid duration value: {}", args[i + 1]))?; i += 2; } arg => { return Err(format!("Unknown argument: {}", arg)); } } } // Validate required arguments let algorithm = algorithm.ok_or("Algorithm (-a) is required")?; // Validate mutual exclusivity between benchmark and normal modes if !benchmark_mode && (benchmark_size.is_some() || benchmark_duration != 10.0) { return Err("--size and --duration can only be used with -b flag".to_string()); } // Create benchmark config if in benchmark mode let benchmark = if benchmark_mode { let config = BenchmarkConfig { size: benchmark_size, duration: benchmark_duration, }; config.validate()?; Some(config) } else { None }; // Validate input requirements based on mode if benchmark.is_none() { // Normal mode: require either file or string input if file.is_none() && string.is_none() { return Err( "Either -f (file) or -s (string) must be provided in normal mode".to_string(), ); } } // Benchmark mode: file and string are optional (will use generated data if neither provided) Ok(Config { algorithm, file, string, format, benchmark, }) } fn calculate_checksum(config: &Config) -> Result<(), String> { let algorithm = CrcAlgorithm::from_str(&config.algorithm) .map_err(|_| format!("Invalid algorithm: {}", config.algorithm))?; // Check if benchmark mode is enabled if let Some(benchmark_config) = &config.benchmark { return run_benchmark(config, benchmark_config, algorithm); } let checksum = if let Some(ref filename) = config.file { checksum_file(algorithm, filename, None).unwrap() } else if let Some(ref text) = config.string { checksum(algorithm, text.as_bytes()) } else { return Err("No input provided for checksum calculation".to_string()); }; match config.format { OutputFormat::Hex => println!("{:#x?}", checksum), OutputFormat::Decimal => println!("{}", checksum), } Ok(()) } fn run_benchmark( config: &Config, benchmark_config: &BenchmarkConfig, algorithm: CrcAlgorithm, ) -> Result<(), String> { // Determine data source and create BenchmarkData let data = if let Some(ref filename) = config.file { // Validate file exists before benchmarking if !std::path::Path::new(filename).exists() { return Err(format!("File not found: {}", filename)); } BenchmarkData::File(filename.clone()) } else if let Some(ref text) = config.string { BenchmarkData::InMemory(text.as_bytes().to_vec()) } else { // Generate random data with specified size or default (1 MiB) let size = benchmark_config.size.unwrap_or(1_048_576); let random_data = generate_random_data(size)?; BenchmarkData::InMemory(random_data) }; // Create and run benchmark let runner = BenchmarkRunner::new(algorithm, data, benchmark_config.duration); let result = runner.run()?; // Display results with algorithm name display_benchmark_results(&result, &config.algorithm); Ok(()) } // Format numbers with comma separators for better readability fn format_number_with_commas(n: u64) -> String { let s = n.to_string(); let mut result = String::new(); let chars: Vec = s.chars().collect(); for (i, ch) in chars.iter().enumerate() { if i > 0 && (chars.len() - i).is_multiple_of(3) { result.push(','); } result.push(*ch); } result } fn display_benchmark_results(result: &BenchmarkResult, algorithm_name: &str) { println!("Algorithm: {}", algorithm_name); println!("Acceleration Target: {}", result.acceleration_target); // Format data size with appropriate units let (size_value, size_unit) = if result.data_size >= 1_048_576 { (result.data_size as f64 / 1_048_576.0, "MiB") } else if result.data_size >= 1024 { (result.data_size as f64 / 1024.0, "KiB") } else { (result.data_size as f64, "bytes") }; println!( "Data Size: {} bytes ({:.1} {})", format_number_with_commas(result.data_size), size_value, size_unit ); println!("Duration: {:.2} seconds", result.elapsed_seconds); println!( "Iterations: {}", format_number_with_commas(result.iterations) ); println!("Throughput: {:.2} GiB/s", result.throughput_gibs); // Format time per iteration with appropriate units let (time_value, time_unit) = if result.time_per_iteration_nanos >= 1_000_000.0 { (result.time_per_iteration_nanos / 1_000_000.0, "ms") } else if result.time_per_iteration_nanos >= 1_000.0 { (result.time_per_iteration_nanos / 1_000.0, "μs") } else { (result.time_per_iteration_nanos, "ns") }; println!("Time per iteration: {:.1} {}", time_value, time_unit); } fn main() -> ExitCode { match parse_args() { Ok(config) => { if let Err(e) = calculate_checksum(&config) { eprintln!("Error: {}", e); return ExitCode::from(1); } } Err(msg) => { if msg == "help" { print_usage(); return ExitCode::SUCCESS; } else { eprintln!("Error: {}", msg); println!(); print_usage(); return ExitCode::from(1); } } } ExitCode::SUCCESS } #[cfg(test)] mod tests { use super::*; use std::str::FromStr; #[test] fn test_benchmark_config_validation_valid() { let config = BenchmarkConfig { size: Some(1024), duration: 5.0, }; assert!(config.validate().is_ok()); } #[test] fn test_benchmark_config_validation_zero_duration() { let config = BenchmarkConfig { size: Some(1024), duration: 0.0, }; assert!(config.validate().is_err()); assert_eq!( config.validate().unwrap_err(), "Duration must be greater than 0" ); } #[test] fn test_benchmark_config_validation_negative_duration() { let config = BenchmarkConfig { size: Some(1024), duration: -1.0, }; assert!(config.validate().is_err()); assert_eq!( config.validate().unwrap_err(), "Duration must be greater than 0" ); } #[test] fn test_benchmark_config_validation_zero_size() { let config = BenchmarkConfig { size: Some(0), duration: 5.0, }; assert!(config.validate().is_err()); assert_eq!( config.validate().unwrap_err(), "Size must be greater than 0" ); } #[test] fn test_benchmark_config_validation_none_size() { let config = BenchmarkConfig { size: None, duration: 5.0, }; assert!(config.validate().is_ok()); } #[test] fn test_generate_random_data_valid_size() { let data = generate_random_data(1024).unwrap(); assert_eq!(data.len(), 1024); } #[test] fn test_generate_random_data_large_size() { let result = generate_random_data(1_073_741_825); // 1 GiB + 1 assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Data size too large (maximum 1 GiB)"); } #[test] fn test_benchmark_result_calculation() { let result = BenchmarkResult::new( 1000, // iterations 2.0, // elapsed_seconds "test".to_string(), // acceleration_target 1024, // data_size ); assert_eq!(result.iterations, 1000); assert_eq!(result.elapsed_seconds, 2.0); assert_eq!(result.data_size, 1024); // Throughput should be (1024 * 1000) / 2.0 / (1024^3) GiB/s let expected_throughput = (1024.0 * 1000.0) / 2.0 / (1024.0 * 1024.0 * 1024.0); assert!((result.throughput_gibs - expected_throughput).abs() < 1e-10); // Time per iteration should be 2.0 * 1e9 / 1000 nanoseconds let expected_time_per_iter = 2.0 * 1_000_000_000.0 / 1000.0; assert!((result.time_per_iteration_nanos - expected_time_per_iter).abs() < 1e-6); } #[test] fn test_benchmark_runner_creation() { let algorithm = CrcAlgorithm::from_str("CRC-32/ISCSI").unwrap(); let data = BenchmarkData::InMemory(vec![1, 2, 3, 4]); let runner = BenchmarkRunner::new(algorithm, data, 1.0); assert_eq!(runner.duration, 1.0); assert_eq!(runner.algorithm, algorithm); } #[test] fn test_benchmark_runner_execution_in_memory() { let algorithm = CrcAlgorithm::from_str("CRC-32/ISCSI").unwrap(); let data = BenchmarkData::InMemory(vec![1, 2, 3, 4]); let runner = BenchmarkRunner::new(algorithm, data, 0.1); // Short duration for test let result = runner.run().unwrap(); assert!(result.iterations > 0); assert!(result.elapsed_seconds > 0.0); assert_eq!(result.data_size, 4); assert!(result.throughput_gibs >= 0.0); } #[test] fn test_parse_args_benchmark_mode() { // We can't easily mock std::env::args in unit tests, so we'll test the parsing logic // by creating a Config directly and validating its structure let config = Config { algorithm: "CRC-32/ISCSI".to_string(), file: None, string: None, format: OutputFormat::Hex, benchmark: Some(BenchmarkConfig { size: Some(1024), duration: 5.0, }), }; assert!(config.benchmark.is_some()); let benchmark_config = config.benchmark.unwrap(); assert_eq!(benchmark_config.size, Some(1024)); assert_eq!(benchmark_config.duration, 5.0); } #[test] fn test_parse_args_normal_mode() { let config = Config { algorithm: "CRC-32/ISCSI".to_string(), file: Some("test.txt".to_string()), string: None, format: OutputFormat::Hex, benchmark: None, }; assert!(config.benchmark.is_none()); assert_eq!(config.file, Some("test.txt".to_string())); } #[test] fn test_data_source_selection_file() { // Test that file input creates File variant let config = Config { algorithm: "CRC-32/ISCSI".to_string(), file: Some("test.txt".to_string()), string: None, format: OutputFormat::Hex, benchmark: Some(BenchmarkConfig { size: None, duration: 1.0, }), }; // This would be tested in the run_benchmark function // We can verify the config structure is correct for file input assert!(config.file.is_some()); assert!(config.string.is_none()); } #[test] fn test_data_source_selection_string() { let config = Config { algorithm: "CRC-32/ISCSI".to_string(), file: None, string: Some("test data".to_string()), format: OutputFormat::Hex, benchmark: Some(BenchmarkConfig { size: None, duration: 1.0, }), }; assert!(config.file.is_none()); assert!(config.string.is_some()); } #[test] fn test_data_source_selection_generated() { let config = Config { algorithm: "CRC-32/ISCSI".to_string(), file: None, string: None, format: OutputFormat::Hex, benchmark: Some(BenchmarkConfig { size: Some(1024), duration: 1.0, }), }; // When neither file nor string is provided, generated data should be used assert!(config.file.is_none()); assert!(config.string.is_none()); assert_eq!(config.benchmark.as_ref().unwrap().size, Some(1024)); } #[test] fn test_throughput_calculation_accuracy() { // Test with known values to verify calculation accuracy let data_size = 1_048_576u64; // 1 MiB let iterations = 1000u64; let elapsed_seconds = 1.0; let result = BenchmarkResult::new(iterations, elapsed_seconds, "test".to_string(), data_size); // Expected throughput: (1 MiB * 1000) / 1 second = 1000 MiB/s = ~0.9537 GiB/s let expected_gibs = (data_size as f64 * iterations as f64) / elapsed_seconds / (1024.0 * 1024.0 * 1024.0); assert!((result.throughput_gibs - expected_gibs).abs() < 1e-10); } #[test] fn test_output_format_variants() { let hex_format = OutputFormat::Hex; let decimal_format = OutputFormat::Decimal; // Test that both variants can be created and are different assert!(matches!(hex_format, OutputFormat::Hex)); assert!(matches!(decimal_format, OutputFormat::Decimal)); } #[test] fn test_format_number_with_commas() { assert_eq!(format_number_with_commas(0), "0"); assert_eq!(format_number_with_commas(123), "123"); assert_eq!(format_number_with_commas(1234), "1,234"); assert_eq!(format_number_with_commas(12345), "12,345"); assert_eq!(format_number_with_commas(123456), "123,456"); assert_eq!(format_number_with_commas(1234567), "1,234,567"); assert_eq!(format_number_with_commas(12345678), "12,345,678"); assert_eq!(format_number_with_commas(123456789), "123,456,789"); assert_eq!(format_number_with_commas(1000000000), "1,000,000,000"); } }