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>,
string: Option<String>,
format: OutputFormat,
benchmark: Option<BenchmarkConfig>,
}
#[derive(Debug)]
struct BenchmarkConfig {
size: Option<usize>,
duration: f64,
}
#[derive(Debug)]
enum BenchmarkData {
InMemory(Vec<u8>),
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<BenchmarkResult, String> {
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,
}
}
}
#[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<Vec<u8>, String> {
if size > 1_073_741_824 {
return Err("Data size too large (maximum 1 GiB)".to_string());
}
let mut buf = vec![0u8; size];
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<Config, String> {
let args: Vec<String> = env::args().collect();
if args.len() == 1 {
return Err("No arguments provided".to_string());
}
if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) {
return Err("help".to_string());
}
let mut algorithm: Option<String> = None;
let mut file: Option<String> = None;
let mut string: Option<String> = None;
let mut format = OutputFormat::Hex; let mut benchmark_mode = false;
let mut benchmark_size: Option<usize> = None;
let mut benchmark_duration = 10.0;
let mut i = 1; 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::<usize>()
.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::<f64>()
.map_err(|_| format!("Invalid duration value: {}", args[i + 1]))?;
i += 2;
}
arg => {
return Err(format!("Unknown argument: {}", arg));
}
}
}
let algorithm = algorithm.ok_or("Algorithm (-a) is required")?;
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());
}
let benchmark = if benchmark_mode {
let config = BenchmarkConfig {
size: benchmark_size,
duration: benchmark_duration,
};
config.validate()?;
Some(config)
} else {
None
};
if benchmark.is_none() {
if file.is_none() && string.is_none() {
return Err(
"Either -f (file) or -s (string) must be provided in normal mode".to_string(),
);
}
}
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))?;
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> {
let data = if let Some(ref filename) = config.file {
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 {
let size = benchmark_config.size.unwrap_or(1_048_576);
let random_data = generate_random_data(size)?;
BenchmarkData::InMemory(random_data)
};
let runner = BenchmarkRunner::new(algorithm, data, benchmark_config.duration);
let result = runner.run()?;
display_benchmark_results(&result, &config.algorithm);
Ok(())
}
fn format_number_with_commas(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
let chars: Vec<char> = 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);
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);
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); 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, 2.0, "test".to_string(), 1024, );
assert_eq!(result.iterations, 1000);
assert_eq!(result.elapsed_seconds, 2.0);
assert_eq!(result.data_size, 1024);
let expected_throughput = (1024.0 * 1000.0) / 2.0 / (1024.0 * 1024.0 * 1024.0);
assert!((result.throughput_gibs - expected_throughput).abs() < 1e-10);
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);
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() {
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() {
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,
}),
};
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,
}),
};
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() {
let data_size = 1_048_576u64; let iterations = 1000u64;
let elapsed_seconds = 1.0;
let result =
BenchmarkResult::new(iterations, elapsed_seconds, "test".to_string(), data_size);
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;
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");
}
}