use crate::error::{Error, Result};
use crate::utils::safe_open_file;
use atlas_c2pa_lib::cose::HashAlgorithm;
use sha2::{Digest, Sha256, Sha384, Sha512};
use std::io::Read;
use std::path::Path;
use subtle::ConstantTimeEq;
pub fn calculate_hash(data: &[u8]) -> String {
calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384)
}
pub fn calculate_hash_with_algorithm(data: &[u8], algorithm: &HashAlgorithm) -> String {
match algorithm {
HashAlgorithm::Sha256 => hex::encode(Sha256::digest(data)),
HashAlgorithm::Sha384 => hex::encode(Sha384::digest(data)),
HashAlgorithm::Sha512 => hex::encode(Sha512::digest(data)),
}
}
pub fn calculate_file_hash(path: impl AsRef<Path>) -> Result<String> {
calculate_file_hash_with_algorithm(path, &HashAlgorithm::Sha384)
}
pub fn calculate_file_hash_with_algorithm(
path: impl AsRef<Path>,
algorithm: &HashAlgorithm,
) -> Result<String> {
let file = safe_open_file(path.as_ref(), false)?;
match algorithm {
HashAlgorithm::Sha256 => hash_reader::<Sha256, _>(file),
HashAlgorithm::Sha512 => hash_reader::<Sha512, _>(file),
_ => hash_reader::<Sha384, _>(file),
}
}
pub fn combine_hashes(hashes: &[&str]) -> Result<String> {
let mut hasher = Sha384::new();
for hash in hashes {
let bytes = hex::decode(hash).map_err(Error::HexDecode)?;
hasher.update(&bytes);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn verify_hash(data: &[u8], expected_hash: &str) -> bool {
let algorithm = detect_hash_algorithm(expected_hash);
let calculated_hash = calculate_hash_with_algorithm(data, &algorithm);
let calculated_bytes = calculated_hash.as_bytes();
let expected_bytes = expected_hash.as_bytes();
if calculated_bytes.len() != expected_bytes.len() {
return false;
}
calculated_bytes.ct_eq(expected_bytes).into()
}
pub fn verify_hash_with_algorithm(
data: &[u8],
expected_hash: &str,
algorithm: &HashAlgorithm,
) -> bool {
let calculated_hash = calculate_hash_with_algorithm(data, algorithm);
let calculated_bytes = match hex::decode(calculated_hash) {
Ok(b) => b,
Err(_) => return false,
};
let expected_bytes = match hex::decode(expected_hash) {
Ok(b) => b,
Err(_) => return false,
};
if calculated_bytes.len() != expected_bytes.len() {
return false;
}
calculated_bytes.ct_eq(&expected_bytes).into()
}
pub fn detect_hash_algorithm(hash: &str) -> HashAlgorithm {
match hash.len() {
64 => HashAlgorithm::Sha256,
96 => HashAlgorithm::Sha384,
128 => HashAlgorithm::Sha512,
_ => HashAlgorithm::Sha384,
}
}
pub fn get_hash_length(algorithm: &str) -> usize {
match algorithm.to_lowercase().as_str() {
"sha256" => 64,
"sha384" => 96,
"sha512" => 128,
_ => 96,
}
}
pub fn algorithm_to_string(algorithm: &HashAlgorithm) -> &'static str {
algorithm.as_str()
}
pub fn parse_algorithm(s: &str) -> Result<HashAlgorithm> {
use std::str::FromStr;
HashAlgorithm::from_str(s).map_err(Error::Validation)
}
fn hash_reader<D: Digest, R: Read>(mut reader: R) -> Result<String> {
let mut hasher = D::new();
let mut buffer = [0; 8192];
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hex::encode(hasher.finalize()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
use crate::utils::safe_create_file;
use std::fs::OpenOptions;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_calculate_hash() {
let data = b"test data";
let hash = calculate_hash(data);
assert_eq!(hash.len(), 96);
}
#[test]
fn test_calculate_hash_with_algorithms() -> Result<()> {
let data = b"test data";
let sha256 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
let sha384 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
let sha512 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
assert_eq!(sha256.len(), 64);
assert_eq!(sha384.len(), 96);
assert_eq!(sha512.len(), 128);
assert_ne!(sha256, sha384);
assert_ne!(sha384, sha512);
assert_ne!(sha256, sha512);
Ok(())
}
#[test]
fn test_calculate_file_hash() -> Result<()> {
let dir = tempdir()?;
let file_path = dir.path().join("test.txt");
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"test data")?;
let hash = calculate_file_hash(&file_path)?;
assert_eq!(hash.len(), 96);
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"different data")?;
let new_hash = calculate_file_hash(&file_path)?;
assert_ne!(hash, new_hash);
Ok(())
}
#[test]
fn test_calculate_file_hash_with_algorithms() -> Result<()> {
let dir = tempdir()?;
let file_path = dir.path().join("test_algos.txt");
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"test data for algorithms")?;
let sha256 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha256)?;
let sha384 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha384)?;
let sha512 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha512)?;
assert_eq!(sha256.len(), 64);
assert_eq!(sha384.len(), 96);
assert_eq!(sha512.len(), 128);
assert_ne!(sha256, sha384);
assert_ne!(sha384, sha512);
Ok(())
}
#[test]
fn test_verify_hash() {
let data = b"test data";
let hash = calculate_hash(data);
assert!(verify_hash(data, &hash));
assert!(!verify_hash(b"different data", &hash));
let test_data = b"test verification data";
let test_hash = calculate_hash(test_data);
assert!(verify_hash(test_data, &test_hash));
assert!(!verify_hash(test_data, "incorrect_hash"));
assert!(!verify_hash(test_data, ""));
let empty_hash = calculate_hash(b"");
assert!(verify_hash(b"", &empty_hash));
assert!(!verify_hash(test_data, "short"));
assert!(!verify_hash(test_data, &("Z".repeat(64))));
}
#[test]
fn test_verify_hash_auto_detect() {
let data = b"test data";
let sha256 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
let sha384 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
let sha512 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
assert!(verify_hash(data, &sha256));
assert!(verify_hash(data, &sha384));
assert!(verify_hash(data, &sha512));
}
#[test]
fn test_detect_hash_algorithm() {
let sha256_hash = "a".repeat(64);
let sha384_hash = "b".repeat(96);
let sha512_hash = "c".repeat(128);
assert!(matches!(
detect_hash_algorithm(&sha256_hash),
HashAlgorithm::Sha256
));
assert!(matches!(
detect_hash_algorithm(&sha384_hash),
HashAlgorithm::Sha384
));
assert!(matches!(
detect_hash_algorithm(&sha512_hash),
HashAlgorithm::Sha512
));
assert!(matches!(
detect_hash_algorithm("short"),
HashAlgorithm::Sha384
));
}
#[test]
fn test_combine_hashes() -> Result<()> {
let hash1 = calculate_hash(b"data1");
let hash2 = calculate_hash(b"data2");
let combined = combine_hashes(&[&hash1, &hash2])?;
assert_eq!(combined.len(), 96);
let combined2 = combine_hashes(&[&hash2, &hash1])?;
assert_ne!(combined, combined2);
Ok(())
}
#[test]
fn test_hash_idempotence() {
let data = b"hello world";
let hash1 = calculate_hash(data);
let hash2 = calculate_hash(data);
assert_eq!(hash1, hash2);
}
#[test]
fn test_hash_uniqueness() {
let data1 = b"hello world";
let data2 = b"Hello World";
let hash1 = calculate_hash(data1);
let hash2 = calculate_hash(data2);
assert_ne!(hash1, hash2);
}
#[test]
fn test_empty_data_hash() {
let data = b"";
let hash = calculate_hash(data);
assert_eq!(hash.len(), 96);
assert_eq!(
hash,
"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
);
}
#[test]
fn test_hash_known_values() {
let test_vectors: [(&[u8], &str); 2] = [
(
b"abc",
"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7",
),
(
b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
"3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b",
),
];
for (input, expected) in &test_vectors {
let hash = calculate_hash(input);
assert_eq!(&hash, expected);
}
}
#[test]
fn test_algorithm_to_string() {
assert_eq!(algorithm_to_string(&HashAlgorithm::Sha256), "sha256");
assert_eq!(algorithm_to_string(&HashAlgorithm::Sha384), "sha384");
assert_eq!(algorithm_to_string(&HashAlgorithm::Sha512), "sha512");
}
#[test]
fn test_parse_algorithm() {
assert!(matches!(
parse_algorithm("sha256").unwrap(),
HashAlgorithm::Sha256
));
assert!(matches!(
parse_algorithm("sha384").unwrap(),
HashAlgorithm::Sha384
));
assert!(matches!(
parse_algorithm("sha512").unwrap(),
HashAlgorithm::Sha512
));
assert!(parse_algorithm("sha1").is_err());
assert!(parse_algorithm("SHA256").is_err()); assert!(parse_algorithm("").is_err());
}
#[test]
fn test_get_hash_length() {
assert_eq!(get_hash_length("sha256"), 64);
assert_eq!(get_hash_length("SHA256"), 64); assert_eq!(get_hash_length("sha384"), 96);
assert_eq!(get_hash_length("sha512"), 128);
assert_eq!(get_hash_length("unknown"), 96); }
#[test]
fn test_combine_hashes_determinism() -> Result<()> {
let hash1 = calculate_hash(b"data1");
let hash2 = calculate_hash(b"data2");
let combined1 = combine_hashes(&[&hash1, &hash2])?;
let combined2 = combine_hashes(&[&hash1, &hash2])?;
assert_eq!(combined1, combined2);
Ok(())
}
#[test]
fn test_combine_hashes_empty() -> Result<()> {
let hash1 = calculate_hash(b"data1");
let result = combine_hashes(&[&hash1])?;
assert_eq!(result.len(), 96);
match combine_hashes(&[]) {
Ok(hash) => {
assert_eq!(hash.len(), 96); assert_eq!(
hash,
"38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
);
}
Err(e) => {
assert!(
e.to_string().contains("empty")
|| e.to_string().contains("no hashes")
|| e.to_string().contains("invalid input"),
"Expected error about empty input, got: {e}"
);
}
}
Ok(())
}
#[test]
fn test_file_hash_changes() -> Result<()> {
let dir = tempdir()?;
let file_path = dir.path().join("test_changes.txt");
{
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"initial content")?;
}
let hash1 = calculate_file_hash(&file_path)?;
{
let mut file = OpenOptions::new().append(true).open(&file_path)?;
file.write_all(b" with more data")?;
}
let hash2 = calculate_file_hash(&file_path)?;
assert_ne!(hash1, hash2);
{
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"initial content")?;
}
let hash3 = calculate_file_hash(&file_path)?;
assert_eq!(hash1, hash3);
Ok(())
}
#[test]
fn test_large_file_hashing() -> Result<()> {
let dir = tempdir()?;
let file_path = dir.path().join("large_file.bin");
{
let mut file = safe_create_file(&file_path, false)?;
let chunk = vec![0x42u8; 1024 * 1024]; for _ in 0..10 {
file.write_all(&chunk)?;
}
}
let sha256 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha256)?;
let sha384 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha384)?;
let sha512 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha512)?;
assert_eq!(sha256.len(), 64);
assert_eq!(sha384.len(), 96);
assert_eq!(sha512.len(), 128);
assert_ne!(sha256, sha384);
assert_ne!(sha384, sha512);
assert_ne!(sha256, sha512);
Ok(())
}
#[test]
fn test_cross_algorithm_verification() {
let data = b"cross algorithm test data";
let sha256_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
let sha384_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
let sha512_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
assert!(verify_hash_with_algorithm(
data,
&sha256_hash,
&HashAlgorithm::Sha256
));
assert!(verify_hash_with_algorithm(
data,
&sha384_hash,
&HashAlgorithm::Sha384
));
assert!(verify_hash_with_algorithm(
data,
&sha512_hash,
&HashAlgorithm::Sha512
));
assert!(!verify_hash_with_algorithm(
data,
&sha256_hash,
&HashAlgorithm::Sha384
));
assert!(!verify_hash_with_algorithm(
data,
&sha256_hash,
&HashAlgorithm::Sha512
));
assert!(!verify_hash_with_algorithm(
data,
&sha384_hash,
&HashAlgorithm::Sha256
));
assert!(!verify_hash_with_algorithm(
data,
&sha384_hash,
&HashAlgorithm::Sha512
));
assert!(!verify_hash_with_algorithm(
data,
&sha512_hash,
&HashAlgorithm::Sha256
));
assert!(!verify_hash_with_algorithm(
data,
&sha512_hash,
&HashAlgorithm::Sha384
));
}
#[test]
fn test_binary_data_hashing() {
let test_cases = vec![
vec![0x00; 100], vec![0xFF; 100], vec![0xAA; 100], vec![0x55; 100], (0..=255).collect::<Vec<u8>>(), ];
for (i, data) in test_cases.iter().enumerate() {
let hash = calculate_hash(data);
assert_eq!(hash.len(), 96, "Test case {} failed", i);
for (j, other_data) in test_cases.iter().enumerate() {
if i != j {
let other_hash = calculate_hash(other_data);
assert_ne!(
hash, other_hash,
"Test cases {} and {} produced same hash",
i, j
);
}
}
}
}
#[test]
fn test_utf8_string_hashing() {
let test_strings = vec![
"Hello, World!",
"Hello, World!", "Hello, World! ", "Здравствуй, мир!", "你好,世界!", "こんにちは、世界!", "🌍🌎🌏", "𝓗𝓮𝓵𝓵𝓸", "", " ", "\n\r\t", ];
let mut hashes = Vec::new();
for s in &test_strings {
let hash = calculate_hash(s.as_bytes());
hashes.push(hash);
}
assert_eq!(hashes[0], hashes[1]);
for i in 0..hashes.len() {
for j in 0..hashes.len() {
if i != j && !(i == 0 && j == 1) && !(i == 1 && j == 0) {
assert_ne!(
hashes[i], hashes[j],
"Strings '{}' and '{}' produced same hash",
test_strings[i], test_strings[j]
);
}
}
}
}
#[test]
fn test_incremental_data_hashing() -> Result<()> {
let dir = tempdir()?;
let file_path = dir.path().join("incremental.txt");
let mut content = String::new();
let mut hashes = Vec::new();
for i in 0..10 {
content.push_str(&format!("Line {}\n", i));
let mut file = safe_create_file(&file_path, false)?;
file.write_all(content.as_bytes())?;
drop(file);
let hash = calculate_file_hash(&file_path)?;
hashes.push(hash);
}
for i in 0..hashes.len() {
for j in i + 1..hashes.len() {
assert_ne!(
hashes[i], hashes[j],
"Incremental content at positions {} and {} produced same hash",
i, j
);
}
}
Ok(())
}
#[test]
fn test_hash_consistency_across_algorithms() {
let data = b"consistency test data";
let iterations = 100;
let mut sha256_hashes = Vec::new();
let mut sha384_hashes = Vec::new();
let mut sha512_hashes = Vec::new();
for _ in 0..iterations {
sha256_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256));
sha384_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384));
sha512_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512));
}
for i in 1..iterations {
assert_eq!(
sha256_hashes[0], sha256_hashes[i],
"SHA-256 inconsistent at iteration {}",
i
);
assert_eq!(
sha384_hashes[0], sha384_hashes[i],
"SHA-384 inconsistent at iteration {}",
i
);
assert_eq!(
sha512_hashes[0], sha512_hashes[i],
"SHA-512 inconsistent at iteration {}",
i
);
}
}
#[test]
fn test_combine_hashes_edge_cases() -> Result<()> {
let hash1 = calculate_hash(b"data1");
let hash2 = calculate_hash(b"data2");
let hash3 = calculate_hash(b"data3");
let single = combine_hashes(&[&hash1])?;
assert_eq!(single.len(), 96);
let double = combine_hashes(&[&hash1, &hash2])?;
assert_eq!(double.len(), 96);
assert_ne!(single, double);
let triple = combine_hashes(&[&hash1, &hash2, &hash3])?;
assert_eq!(triple.len(), 96);
assert_ne!(double, triple);
let ab = combine_hashes(&[&hash1, &hash2])?;
let ab_c = combine_hashes(&[&ab, &hash3])?;
let bc = combine_hashes(&[&hash2, &hash3])?;
let a_bc = combine_hashes(&[&hash1, &bc])?;
assert_ne!(ab_c, a_bc);
Ok(())
}
#[test]
fn test_file_not_found_error() {
let result = calculate_file_hash("/this/path/should/not/exist/test.txt");
assert!(result.is_err());
match result {
Err(Error::Io(_)) => (), Err(e) => panic!("Expected Io error, got: {:?}", e),
Ok(_) => panic!("Expected error for non-existent file"),
}
}
#[test]
fn test_special_filenames() -> Result<()> {
let dir = tempdir()?;
let filenames = vec![
"file with spaces.txt",
"file-with-dashes.txt",
"file_with_underscores.txt",
"file.multiple.dots.txt",
"UPPERCASE.TXT",
"🦀rust🦀.txt", ".hidden_file",
"very_long_filename_that_exceeds_typical_lengths_but_should_still_work_fine.txt",
];
for filename in filenames {
let file_path = dir.path().join(filename);
let mut file = safe_create_file(&file_path, false)?;
file.write_all(b"test content")?;
drop(file);
let hash = calculate_file_hash(&file_path)?;
assert_eq!(hash.len(), 96, "Failed for filename: {}", filename);
}
Ok(())
}
#[test]
fn test_concurrent_hashing_safety() {
use std::sync::Arc;
use std::thread;
let data = Arc::new(b"concurrent test data".to_vec());
let num_threads = 10;
let iterations_per_thread = 100;
let mut handles = vec![];
for _ in 0..num_threads {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut hashes = Vec::new();
for _ in 0..iterations_per_thread {
let hash = calculate_hash(&data_clone);
hashes.push(hash);
}
hashes
});
handles.push(handle);
}
let mut all_hashes = Vec::new();
for handle in handles {
let hashes = handle.join().expect("Thread panicked");
all_hashes.extend(hashes);
}
let expected_hash = calculate_hash(&data);
for (i, hash) in all_hashes.iter().enumerate() {
assert_eq!(hash, &expected_hash, "Hash mismatch at index {}", i);
}
}
#[test]
fn test_combine_hashes_with_invalid_hex() -> Result<()> {
let valid_hash = calculate_hash(b"valid");
let result = combine_hashes(&[&valid_hash, "not_valid_hex"]);
assert!(result.is_err());
let result = combine_hashes(&[&valid_hash, "abc"]);
assert!(result.is_err());
let result = combine_hashes(&[&valid_hash, "café"]);
assert!(result.is_err());
Ok(())
}
}