atlas_cli/hash/mod.rs
1//! # Hash Module
2//!
3//! This module provides cryptographic hash functions for the Atlas CLI, supporting
4//! SHA-256, SHA-384, and SHA-512 algorithms. It integrates with the `atlas-c2pa-lib`
5//! to use consistent hash algorithm types throughout the codebase.
6//!
7//! ## Features
8//!
9//! - Calculate hashes of byte data with configurable algorithms
10//! - Calculate file hashes efficiently using streaming
11//! - Combine multiple hashes into a single hash
12//! - Verify data integrity by comparing hashes
13//! - Automatic algorithm detection based on hash length
14//!
15//! ## Algorithm Support
16//!
17//! The module supports the following hash algorithms:
18//! - **SHA-256**: 256-bit hash (64 hex characters) - Default for backward compatibility
19//! - **SHA-384**: 384-bit hash (96 hex characters) - Default for new manifests
20//! - **SHA-512**: 512-bit hash (128 hex characters) - Maximum security
21//!
22//! ## Examples
23//!
24//! ### Basic hashing with default algorithm (SHA-384)
25//! ```
26//! use atlas_cli::hash::calculate_hash;
27//!
28//! let data = b"Hello, World!";
29//! let hash = calculate_hash(data);
30//! assert_eq!(hash.len(), 96); // SHA-384 produces 96 hex characters
31//! ```
32//!
33//! ### Hashing with specific algorithm
34//! ```
35//! use atlas_cli::hash::calculate_hash_with_algorithm;
36//! use atlas_c2pa_lib::cose::HashAlgorithm;
37//!
38//! let data = b"Hello, World!";
39//! let hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
40//! assert_eq!(hash.len(), 128); // SHA-512 produces 128 hex characters
41//! ```
42//!
43//! ### File hashing
44//! ```no_run
45//! use atlas_cli::hash::calculate_file_hash_with_algorithm;
46//! use atlas_c2pa_lib::cose::HashAlgorithm;
47//! use std::path::Path;
48//!
49//! let path = Path::new("large_file.bin");
50//! let hash = calculate_file_hash_with_algorithm(path, &HashAlgorithm::Sha384).unwrap();
51//! assert_eq!(hash.len(), 96); // SHA-384 produces 96 hex characters
52//! ```
53
54use crate::error::{Error, Result};
55use crate::utils::safe_open_file;
56use atlas_c2pa_lib::cose::HashAlgorithm;
57use sha2::{Digest, Sha256, Sha384, Sha512};
58use std::io::Read;
59use std::path::Path;
60use subtle::ConstantTimeEq;
61
62/// Calculate SHA-384 hash of the given data
63///
64/// This function uses SHA-384 by default. For other algorithms, use
65/// [`calculate_hash_with_algorithm`].
66///
67/// # Arguments
68///
69/// * `data` - The byte slice to hash
70///
71/// # Returns
72///
73/// A hexadecimal string representation of the hash (96 characters for SHA-384)
74///
75/// # Examples
76///
77/// ```
78/// use atlas_cli::hash::calculate_hash;
79///
80/// let data = b"Hello, World!";
81/// let hash = calculate_hash(data);
82///
83/// // SHA-384 produces 96 character hex string
84/// assert_eq!(hash.len(), 96);
85///
86/// // Same data produces same hash
87/// let hash2 = calculate_hash(data);
88/// assert_eq!(hash, hash2);
89///
90/// // Different data produces different hash
91/// let hash3 = calculate_hash(b"Different data");
92/// assert_ne!(hash, hash3);
93/// ```
94pub fn calculate_hash(data: &[u8]) -> String {
95 calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384)
96}
97
98/// Calculate hash of data using the specified algorithm
99///
100/// # Arguments
101///
102/// * `data` - The byte slice to hash
103/// * `algorithm` - The hash algorithm to use (SHA-256, SHA-384, or SHA-512)
104///
105/// # Returns
106///
107/// A hexadecimal string representation of the hash:
108/// - SHA-256: 64 characters
109/// - SHA-384: 96 characters
110/// - SHA-512: 128 characters
111///
112/// # Examples
113///
114/// ```
115/// use atlas_cli::hash::calculate_hash_with_algorithm;
116/// use atlas_c2pa_lib::cose::HashAlgorithm;
117///
118/// let data = b"Hello, World!";
119///
120/// // SHA-256
121/// let hash256 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
122/// assert_eq!(hash256.len(), 64);
123///
124/// // SHA-384
125/// let hash384 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
126/// assert_eq!(hash384.len(), 96);
127///
128/// // SHA-512
129/// let hash512 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
130/// assert_eq!(hash512.len(), 128);
131///
132/// // Different algorithms produce different hashes
133/// assert_ne!(hash256, hash384);
134/// assert_ne!(hash384, hash512);
135/// ```
136pub fn calculate_hash_with_algorithm(data: &[u8], algorithm: &HashAlgorithm) -> String {
137 match algorithm {
138 HashAlgorithm::Sha256 => hex::encode(Sha256::digest(data)),
139 HashAlgorithm::Sha384 => hex::encode(Sha384::digest(data)),
140 HashAlgorithm::Sha512 => hex::encode(Sha512::digest(data)),
141 }
142}
143
144/// Calculate SHA-256 hash of a file
145///
146/// This function uses SHA-256 by default. For other algorithms, use
147/// [`calculate_file_hash_with_algorithm`].
148///
149/// # Arguments
150///
151/// * `path` - Path to the file to hash
152///
153/// # Returns
154///
155/// * `Ok(String)` - The hexadecimal hash string (64 characters for SHA-384)
156/// * `Err(Error)` - If the file cannot be read
157///
158/// # Examples
159///
160/// ```no_run
161/// use atlas_cli::hash::calculate_file_hash;
162/// use std::path::Path;
163///
164/// let path = Path::new("example.txt");
165/// match calculate_file_hash(&path) {
166/// Ok(hash) => {
167/// assert_eq!(hash.len(), 96);
168/// println!("File hash: {}", hash);
169/// }
170/// Err(e) => eprintln!("Error: {}", e),
171/// }
172/// ```
173pub fn calculate_file_hash(path: impl AsRef<Path>) -> Result<String> {
174 calculate_file_hash_with_algorithm(path, &HashAlgorithm::Sha384)
175}
176
177/// Calculate hash of a file using the specified algorithm
178///
179/// This function efficiently hashes files of any size by reading them in chunks,
180/// avoiding loading the entire file into memory.
181///
182/// # Arguments
183///
184/// * `path` - Path to the file to hash
185/// * `algorithm` - The hash algorithm to use
186///
187/// # Returns
188///
189/// * `Ok(String)` - The hexadecimal hash string
190/// * `Err(Error)` - If the file cannot be read
191///
192/// # Examples
193///
194/// ```no_run
195/// use atlas_cli::hash::calculate_file_hash_with_algorithm;
196/// use atlas_c2pa_lib::cose::HashAlgorithm;
197/// use std::path::Path;
198///
199/// let path = Path::new("large_model.onnx");
200///
201/// // Use SHA-512 for maximum security
202/// let hash = calculate_file_hash_with_algorithm(&path, &HashAlgorithm::Sha512)?;
203/// assert_eq!(hash.len(), 128);
204///
205/// # Ok::<(), atlas_cli::error::Error>(())
206/// ```
207pub fn calculate_file_hash_with_algorithm(
208 path: impl AsRef<Path>,
209 algorithm: &HashAlgorithm,
210) -> Result<String> {
211 let file = safe_open_file(path.as_ref(), false)?;
212
213 match algorithm {
214 HashAlgorithm::Sha256 => hash_reader::<Sha256, _>(file),
215 HashAlgorithm::Sha512 => hash_reader::<Sha512, _>(file),
216 _ => hash_reader::<Sha384, _>(file),
217 }
218}
219
220///
221/// This function concatenates the decoded bytes of multiple hashes and produces
222/// a new SHA-384 hash. This is useful for creating a single hash that represents
223/// multiple components.
224///
225/// # Arguments
226///
227/// * `hashes` - Array of hexadecimal hash strings to combine
228///
229/// # Returns
230///
231/// * `Ok(String)` - The combined hash (96 characters, SHA-384)
232/// * `Err(Error)` - If any input hash is invalid hexadecimal
233///
234/// # Examples
235///
236/// ```
237/// use atlas_cli::hash::{calculate_hash, combine_hashes};
238///
239/// let hash1 = calculate_hash(b"data1");
240/// let hash2 = calculate_hash(b"data2");
241///
242/// let combined = combine_hashes(&[&hash1, &hash2]).unwrap();
243/// assert_eq!(combined.len(), 96);
244///
245/// // Order matters
246/// let combined_reversed = combine_hashes(&[&hash2, &hash1]).unwrap();
247/// assert_ne!(combined, combined_reversed);
248/// ```
249pub fn combine_hashes(hashes: &[&str]) -> Result<String> {
250 let mut hasher = Sha384::new();
251 for hash in hashes {
252 let bytes = hex::decode(hash).map_err(Error::HexDecode)?;
253 hasher.update(&bytes);
254 }
255 Ok(hex::encode(hasher.finalize()))
256}
257
258/// Verify that data matches the expected hash
259///
260/// This function automatically detects the hash algorithm based on the hash length
261/// and verifies that the provided data produces the same hash.
262///
263/// # Arguments
264///
265/// * `data` - The data to verify
266/// * `expected_hash` - The expected hash in hexadecimal format
267///
268/// # Returns
269///
270/// * `true` if the data matches the hash
271/// * `false` if the data doesn't match or the hash format is invalid
272///
273/// # Algorithm Detection
274///
275/// - 64 characters: SHA-256
276/// - 96 characters: SHA-384
277/// - 128 characters: SHA-512
278/// - Other lengths: Defaults to SHA-256
279///
280/// # Examples
281///
282/// ```
283/// use atlas_cli::hash::{calculate_hash, verify_hash};
284///
285/// let data = b"test data";
286/// let hash = calculate_hash(data);
287///
288/// // Correct data verifies successfully
289/// assert!(verify_hash(data, &hash));
290///
291/// // Wrong data fails verification
292/// assert!(!verify_hash(b"wrong data", &hash));
293///
294/// // Invalid hash format returns false
295/// assert!(!verify_hash(data, "invalid_hash"));
296/// ```
297pub fn verify_hash(data: &[u8], expected_hash: &str) -> bool {
298 let algorithm = detect_hash_algorithm(expected_hash);
299 let calculated_hash = calculate_hash_with_algorithm(data, &algorithm);
300
301 // Convert both to bytes for constant-time comparison
302 let calculated_bytes = calculated_hash.as_bytes();
303 let expected_bytes = expected_hash.as_bytes();
304
305 // Length must match first
306 if calculated_bytes.len() != expected_bytes.len() {
307 return false;
308 }
309
310 // Constant-time comparison
311 calculated_bytes.ct_eq(expected_bytes).into()
312}
313
314/// Verify hash with an explicitly specified algorithm
315///
316/// Use this when you know which algorithm was used to create the hash.
317///
318/// # Arguments
319///
320/// * `data` - The data to verify
321/// * `expected_hash` - The expected hash in hexadecimal format
322/// * `algorithm` - The hash algorithm that was used
323///
324/// # Returns
325///
326/// * `true` if the data matches the hash
327/// * `false` if the data doesn't match
328///
329/// # Examples
330///
331/// ```
332/// use atlas_cli::hash::{calculate_hash_with_algorithm, verify_hash_with_algorithm};
333/// use atlas_c2pa_lib::cose::HashAlgorithm;
334///
335/// let data = b"test data";
336/// let hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
337///
338/// // Verification with correct algorithm succeeds
339/// assert!(verify_hash_with_algorithm(data, &hash, &HashAlgorithm::Sha384));
340///
341/// // Verification with wrong algorithm fails
342/// assert!(!verify_hash_with_algorithm(data, &hash, &HashAlgorithm::Sha256));
343/// ```
344pub fn verify_hash_with_algorithm(
345 data: &[u8],
346 expected_hash: &str,
347 algorithm: &HashAlgorithm,
348) -> bool {
349 let calculated_hash = calculate_hash_with_algorithm(data, algorithm);
350
351 let calculated_bytes = match hex::decode(calculated_hash) {
352 Ok(b) => b,
353 Err(_) => return false,
354 };
355 let expected_bytes = match hex::decode(expected_hash) {
356 Ok(b) => b,
357 Err(_) => return false,
358 };
359
360 if calculated_bytes.len() != expected_bytes.len() {
361 return false;
362 }
363
364 calculated_bytes.ct_eq(&expected_bytes).into()
365}
366
367/// Detect hash algorithm based on hash length
368///
369/// This function infers the hash algorithm from the hexadecimal hash string length.
370///
371/// # Arguments
372///
373/// * `hash` - Hexadecimal hash string
374///
375/// # Returns
376///
377/// The detected `HashAlgorithm`:
378/// - 64 characters → SHA-256
379/// - 96 characters → SHA-384
380/// - 128 characters → SHA-512
381/// - Other lengths → SHA-384 (default)
382///
383/// # Examples
384///
385/// ```
386/// use atlas_cli::hash::detect_hash_algorithm;
387/// use atlas_c2pa_lib::cose::HashAlgorithm;
388///
389/// let sha256_hash = "a".repeat(64);
390/// let sha384_hash = "b".repeat(96);
391/// let sha512_hash = "c".repeat(128);
392///
393/// assert!(matches!(detect_hash_algorithm(&sha256_hash), HashAlgorithm::Sha256));
394/// assert!(matches!(detect_hash_algorithm(&sha384_hash), HashAlgorithm::Sha384));
395/// assert!(matches!(detect_hash_algorithm(&sha512_hash), HashAlgorithm::Sha512));
396/// ```
397pub fn detect_hash_algorithm(hash: &str) -> HashAlgorithm {
398 match hash.len() {
399 64 => HashAlgorithm::Sha256,
400 96 => HashAlgorithm::Sha384,
401 128 => HashAlgorithm::Sha512,
402 _ => HashAlgorithm::Sha384,
403 }
404}
405
406/// Get the expected hash length for an algorithm
407///
408/// # Arguments
409///
410/// * `algorithm` - Algorithm name as a string (case-insensitive)
411///
412/// # Returns
413///
414/// The expected hexadecimal string length:
415/// - "sha256" → 64
416/// - "sha384" → 96
417/// - "sha512" → 128
418/// - Other → 96 (default)
419///
420/// # Examples
421///
422/// ```
423/// use atlas_cli::hash::get_hash_length;
424///
425/// assert_eq!(get_hash_length("sha256"), 64);
426/// assert_eq!(get_hash_length("SHA384"), 96);
427/// assert_eq!(get_hash_length("sha512"), 128);
428/// assert_eq!(get_hash_length("unknown"), 96); // defaults to SHA-384
429/// ```
430pub fn get_hash_length(algorithm: &str) -> usize {
431 match algorithm.to_lowercase().as_str() {
432 "sha256" => 64,
433 "sha384" => 96,
434 "sha512" => 128,
435 _ => 96,
436 }
437}
438
439/// Get the algorithm name as used in manifests
440///
441/// Converts a `HashAlgorithm` to its string representation for storage in manifests.
442///
443/// # Arguments
444///
445/// * `algorithm` - The hash algorithm
446///
447/// # Returns
448///
449/// The algorithm name as a string:
450/// - `HashAlgorithm::Sha256` → "sha256"
451/// - `HashAlgorithm::Sha384` → "sha384"
452/// - `HashAlgorithm::Sha512` → "sha512"
453///
454/// # Examples
455///
456/// ```
457/// use atlas_cli::hash::algorithm_to_string;
458/// use atlas_c2pa_lib::cose::HashAlgorithm;
459///
460/// assert_eq!(algorithm_to_string(&HashAlgorithm::Sha256), "sha256");
461/// assert_eq!(algorithm_to_string(&HashAlgorithm::Sha384), "sha384");
462/// assert_eq!(algorithm_to_string(&HashAlgorithm::Sha512), "sha512");
463/// ```
464pub fn algorithm_to_string(algorithm: &HashAlgorithm) -> &'static str {
465 algorithm.as_str()
466}
467
468/// Parse algorithm from string
469///
470/// Converts a string algorithm name to a `HashAlgorithm` enum value.
471///
472/// # Arguments
473///
474/// * `s` - Algorithm name (case-sensitive: "sha256", "sha384", or "sha512")
475///
476/// # Returns
477///
478/// * `Ok(HashAlgorithm)` - The parsed algorithm
479/// * `Err(Error)` - If the algorithm name is not recognized
480///
481/// # Examples
482///
483/// ```
484/// use atlas_cli::hash::parse_algorithm;
485/// use atlas_c2pa_lib::cose::HashAlgorithm;
486///
487/// let algo = parse_algorithm("sha384").unwrap();
488/// assert!(matches!(algo, HashAlgorithm::Sha384));
489///
490/// // Invalid algorithm names return an error
491/// assert!(parse_algorithm("sha1").is_err());
492/// assert!(parse_algorithm("SHA256").is_err()); // case sensitive
493/// ```
494pub fn parse_algorithm(s: &str) -> Result<HashAlgorithm> {
495 use std::str::FromStr;
496 HashAlgorithm::from_str(s).map_err(Error::Validation)
497}
498
499/// Internal helper to hash data from a reader using streaming
500fn hash_reader<D: Digest, R: Read>(mut reader: R) -> Result<String> {
501 let mut hasher = D::new();
502 let mut buffer = [0; 8192];
503
504 loop {
505 let bytes_read = reader.read(&mut buffer)?;
506 if bytes_read == 0 {
507 break;
508 }
509 hasher.update(&buffer[..bytes_read]);
510 }
511
512 Ok(hex::encode(hasher.finalize()))
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::error::Result;
519 use crate::utils::safe_create_file;
520 use std::fs::OpenOptions;
521 use std::io::Write;
522 use tempfile::tempdir;
523
524 #[test]
525 fn test_calculate_hash() {
526 let data = b"test data";
527 let hash = calculate_hash(data);
528 assert_eq!(hash.len(), 96);
529 }
530 #[test]
531 fn test_calculate_hash_with_algorithms() -> Result<()> {
532 let data = b"test data";
533
534 // Test different algorithms produce different length hashes
535 let sha256 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
536 let sha384 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
537 let sha512 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
538
539 assert_eq!(sha256.len(), 64);
540 assert_eq!(sha384.len(), 96);
541 assert_eq!(sha512.len(), 128);
542
543 // Different algorithms produce different hashes
544 assert_ne!(sha256, sha384);
545 assert_ne!(sha384, sha512);
546 assert_ne!(sha256, sha512);
547
548 Ok(())
549 }
550
551 #[test]
552 fn test_calculate_file_hash() -> Result<()> {
553 let dir = tempdir()?;
554 let file_path = dir.path().join("test.txt");
555
556 // Create a test file
557 let mut file = safe_create_file(&file_path, false)?;
558 file.write_all(b"test data")?;
559
560 let hash = calculate_file_hash(&file_path)?;
561 assert_eq!(hash.len(), 96); // Changed from 64 to 96
562
563 // Verify hash changes with content
564 let mut file = safe_create_file(&file_path, false)?;
565 file.write_all(b"different data")?;
566
567 let new_hash = calculate_file_hash(&file_path)?;
568 assert_ne!(hash, new_hash);
569
570 Ok(())
571 }
572
573 #[test]
574 fn test_calculate_file_hash_with_algorithms() -> Result<()> {
575 let dir = tempdir()?;
576 let file_path = dir.path().join("test_algos.txt");
577
578 // Create a test file
579 let mut file = safe_create_file(&file_path, false)?;
580 file.write_all(b"test data for algorithms")?;
581
582 // Test different algorithms
583 let sha256 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha256)?;
584 let sha384 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha384)?;
585 let sha512 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha512)?;
586
587 assert_eq!(sha256.len(), 64);
588 assert_eq!(sha384.len(), 96);
589 assert_eq!(sha512.len(), 128);
590
591 // Different algorithms produce different hashes
592 assert_ne!(sha256, sha384);
593 assert_ne!(sha384, sha512);
594
595 Ok(())
596 }
597
598 #[test]
599 fn test_verify_hash() {
600 let data = b"test data";
601 let hash = calculate_hash(data);
602
603 assert!(verify_hash(data, &hash));
604 assert!(!verify_hash(b"different data", &hash));
605
606 // Additional verification tests
607 let test_data = b"test verification data";
608 let test_hash = calculate_hash(test_data);
609
610 // Verification should succeed with correct hash
611 assert!(verify_hash(test_data, &test_hash));
612
613 // Verification should fail with incorrect hash
614 assert!(!verify_hash(test_data, "incorrect_hash"));
615
616 // Verification should fail with empty hash
617 assert!(!verify_hash(test_data, ""));
618
619 // Verify empty data
620 let empty_hash = calculate_hash(b"");
621 assert!(verify_hash(b"", &empty_hash));
622
623 // Verification should fail with hash of wrong length
624 assert!(!verify_hash(test_data, "short"));
625
626 // Verification should fail with non-hex characters
627 assert!(!verify_hash(test_data, &("Z".repeat(64))));
628 }
629
630 #[test]
631 fn test_verify_hash_auto_detect() {
632 let data = b"test data";
633
634 // Create hashes with different algorithms
635 let sha256 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
636 let sha384 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
637 let sha512 = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
638
639 // Verify should auto-detect the algorithm
640 assert!(verify_hash(data, &sha256));
641 assert!(verify_hash(data, &sha384));
642 assert!(verify_hash(data, &sha512));
643 }
644
645 #[test]
646 fn test_detect_hash_algorithm() {
647 let sha256_hash = "a".repeat(64);
648 let sha384_hash = "b".repeat(96);
649 let sha512_hash = "c".repeat(128);
650
651 assert!(matches!(
652 detect_hash_algorithm(&sha256_hash),
653 HashAlgorithm::Sha256
654 ));
655 assert!(matches!(
656 detect_hash_algorithm(&sha384_hash),
657 HashAlgorithm::Sha384
658 ));
659 assert!(matches!(
660 detect_hash_algorithm(&sha512_hash),
661 HashAlgorithm::Sha512
662 ));
663
664 // Unknown length defaults to SHA-384
665 assert!(matches!(
666 detect_hash_algorithm("short"),
667 HashAlgorithm::Sha384
668 ));
669 }
670
671 #[test]
672 fn test_combine_hashes() -> Result<()> {
673 let hash1 = calculate_hash(b"data1");
674 let hash2 = calculate_hash(b"data2");
675
676 let combined = combine_hashes(&[&hash1, &hash2])?;
677 assert_eq!(combined.len(), 96);
678
679 // Test order matters
680 let combined2 = combine_hashes(&[&hash2, &hash1])?;
681 assert_ne!(combined, combined2);
682
683 Ok(())
684 }
685
686 #[test]
687 fn test_hash_idempotence() {
688 let data = b"hello world";
689 let hash1 = calculate_hash(data);
690 let hash2 = calculate_hash(data);
691
692 // The same data should produce the same hash
693 assert_eq!(hash1, hash2);
694 }
695
696 #[test]
697 fn test_hash_uniqueness() {
698 let data1 = b"hello world";
699 let data2 = b"Hello World"; // Capitalization should produce different hash
700
701 let hash1 = calculate_hash(data1);
702 let hash2 = calculate_hash(data2);
703
704 // Different data should produce different hashes
705 assert_ne!(hash1, hash2);
706 }
707
708 #[test]
709 fn test_empty_data_hash() {
710 let data = b"";
711 let hash = calculate_hash(data);
712
713 // Empty string should produce a valid hash with expected length
714 assert_eq!(hash.len(), 96);
715 // Known SHA-384 hash of empty string
716 assert_eq!(
717 hash,
718 "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
719 );
720 }
721
722 #[test]
723 fn test_hash_known_values() {
724 // Test vectors for SHA-384
725 let test_vectors: [(&[u8], &str); 2] = [
726 (
727 b"abc",
728 "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7",
729 ),
730 (
731 b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
732 "3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b",
733 ),
734 ];
735
736 for (input, expected) in &test_vectors {
737 let hash = calculate_hash(input);
738 assert_eq!(&hash, expected);
739 }
740 }
741
742 #[test]
743 fn test_algorithm_to_string() {
744 assert_eq!(algorithm_to_string(&HashAlgorithm::Sha256), "sha256");
745 assert_eq!(algorithm_to_string(&HashAlgorithm::Sha384), "sha384");
746 assert_eq!(algorithm_to_string(&HashAlgorithm::Sha512), "sha512");
747 }
748
749 #[test]
750 fn test_parse_algorithm() {
751 // Valid algorithms
752 assert!(matches!(
753 parse_algorithm("sha256").unwrap(),
754 HashAlgorithm::Sha256
755 ));
756 assert!(matches!(
757 parse_algorithm("sha384").unwrap(),
758 HashAlgorithm::Sha384
759 ));
760 assert!(matches!(
761 parse_algorithm("sha512").unwrap(),
762 HashAlgorithm::Sha512
763 ));
764
765 // Invalid algorithms
766 assert!(parse_algorithm("sha1").is_err());
767 assert!(parse_algorithm("SHA256").is_err()); // case sensitive
768 assert!(parse_algorithm("").is_err());
769 }
770
771 #[test]
772 fn test_get_hash_length() {
773 assert_eq!(get_hash_length("sha256"), 64);
774 assert_eq!(get_hash_length("SHA256"), 64); // case insensitive
775 assert_eq!(get_hash_length("sha384"), 96);
776 assert_eq!(get_hash_length("sha512"), 128);
777 assert_eq!(get_hash_length("unknown"), 96); // defaults to SHA-384
778 }
779
780 #[test]
781 fn test_combine_hashes_determinism() -> Result<()> {
782 let hash1 = calculate_hash(b"data1");
783 let hash2 = calculate_hash(b"data2");
784
785 let combined1 = combine_hashes(&[&hash1, &hash2])?;
786 let combined2 = combine_hashes(&[&hash1, &hash2])?;
787
788 // The same input hashes should produce the same combined hash
789 assert_eq!(combined1, combined2);
790
791 Ok(())
792 }
793
794 #[test]
795 fn test_combine_hashes_empty() -> Result<()> {
796 // Create a single hash
797 let hash1 = calculate_hash(b"data1");
798
799 // Test combining single hash
800 let result = combine_hashes(&[&hash1])?;
801 assert_eq!(result.len(), 96); // Changed from 64 to 96
802
803 // Test combining empty list of hashes
804 match combine_hashes(&[]) {
805 Ok(hash) => {
806 // If it succeeds, verify it's a valid hash
807 assert_eq!(hash.len(), 96); // Changed from 64 to 96
808 // The hash of empty input should be the SHA-384 of empty data
809 assert_eq!(
810 hash,
811 "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b"
812 );
813 }
814 Err(e) => {
815 // If it errors, the error should indicate empty input
816 assert!(
817 e.to_string().contains("empty")
818 || e.to_string().contains("no hashes")
819 || e.to_string().contains("invalid input"),
820 "Expected error about empty input, got: {e}"
821 );
822 }
823 }
824
825 Ok(())
826 }
827
828 #[test]
829 fn test_file_hash_changes() -> Result<()> {
830 let dir = tempdir()?;
831 let file_path = dir.path().join("test_changes.txt");
832
833 // Test with initial content
834 {
835 let mut file = safe_create_file(&file_path, false)?;
836 file.write_all(b"initial content")?;
837 }
838 let hash1 = calculate_file_hash(&file_path)?;
839
840 // Test after appending content
841 {
842 let mut file = OpenOptions::new().append(true).open(&file_path)?;
843 file.write_all(b" with more data")?;
844 }
845 let hash2 = calculate_file_hash(&file_path)?;
846
847 // Hashes should be different
848 assert_ne!(hash1, hash2);
849
850 // Test after overwriting with same content as initial
851 {
852 let mut file = safe_create_file(&file_path, false)?;
853 file.write_all(b"initial content")?;
854 }
855 let hash3 = calculate_file_hash(&file_path)?;
856
857 // Hash should be the same as the first hash
858 assert_eq!(hash1, hash3);
859
860 Ok(())
861 }
862
863 #[test]
864 fn test_large_file_hashing() -> Result<()> {
865 let dir = tempdir()?;
866 let file_path = dir.path().join("large_file.bin");
867
868 // Create a 10MB file
869 {
870 let mut file = safe_create_file(&file_path, false)?;
871 let chunk = vec![0x42u8; 1024 * 1024]; // 1MB chunk
872 for _ in 0..10 {
873 file.write_all(&chunk)?;
874 }
875 }
876
877 // Test that we can hash large files with different algorithms
878 let sha256 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha256)?;
879 let sha384 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha384)?;
880 let sha512 = calculate_file_hash_with_algorithm(&file_path, &HashAlgorithm::Sha512)?;
881
882 assert_eq!(sha256.len(), 64);
883 assert_eq!(sha384.len(), 96);
884 assert_eq!(sha512.len(), 128);
885
886 // All should be different
887 assert_ne!(sha256, sha384);
888 assert_ne!(sha384, sha512);
889 assert_ne!(sha256, sha512);
890
891 Ok(())
892 }
893
894 #[test]
895 fn test_cross_algorithm_verification() {
896 // Test that verification fails when using wrong algorithm
897 let data = b"cross algorithm test data";
898
899 // Create hashes with each algorithm
900 let sha256_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256);
901 let sha384_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384);
902 let sha512_hash = calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512);
903
904 // Verify with correct algorithms should succeed
905 assert!(verify_hash_with_algorithm(
906 data,
907 &sha256_hash,
908 &HashAlgorithm::Sha256
909 ));
910 assert!(verify_hash_with_algorithm(
911 data,
912 &sha384_hash,
913 &HashAlgorithm::Sha384
914 ));
915 assert!(verify_hash_with_algorithm(
916 data,
917 &sha512_hash,
918 &HashAlgorithm::Sha512
919 ));
920
921 // Verify with wrong algorithms should fail
922 assert!(!verify_hash_with_algorithm(
923 data,
924 &sha256_hash,
925 &HashAlgorithm::Sha384
926 ));
927 assert!(!verify_hash_with_algorithm(
928 data,
929 &sha256_hash,
930 &HashAlgorithm::Sha512
931 ));
932 assert!(!verify_hash_with_algorithm(
933 data,
934 &sha384_hash,
935 &HashAlgorithm::Sha256
936 ));
937 assert!(!verify_hash_with_algorithm(
938 data,
939 &sha384_hash,
940 &HashAlgorithm::Sha512
941 ));
942 assert!(!verify_hash_with_algorithm(
943 data,
944 &sha512_hash,
945 &HashAlgorithm::Sha256
946 ));
947 assert!(!verify_hash_with_algorithm(
948 data,
949 &sha512_hash,
950 &HashAlgorithm::Sha384
951 ));
952 }
953
954 #[test]
955 fn test_binary_data_hashing() {
956 // Test with various binary patterns
957 let test_cases = vec![
958 vec![0x00; 100], // All zeros
959 vec![0xFF; 100], // All ones
960 vec![0xAA; 100], // Alternating bits (10101010)
961 vec![0x55; 100], // Alternating bits (01010101)
962 (0..=255).collect::<Vec<u8>>(), // All byte values
963 ];
964
965 for (i, data) in test_cases.iter().enumerate() {
966 let hash = calculate_hash(data);
967 assert_eq!(hash.len(), 96, "Test case {} failed", i);
968
969 // Verify each produces unique hash
970 for (j, other_data) in test_cases.iter().enumerate() {
971 if i != j {
972 let other_hash = calculate_hash(other_data);
973 assert_ne!(
974 hash, other_hash,
975 "Test cases {} and {} produced same hash",
976 i, j
977 );
978 }
979 }
980 }
981 }
982
983 #[test]
984 fn test_utf8_string_hashing() {
985 // Test with various UTF-8 strings
986 let test_strings = vec![
987 "Hello, World!",
988 "Hello, World!", // Same string should produce same hash
989 "Hello, World! ", // Extra space should produce different hash
990 "Здравствуй, мир!", // Russian
991 "你好,世界!", // Chinese
992 "こんにちは、世界!", // Japanese
993 "🌍🌎🌏", // Emojis
994 "𝓗𝓮𝓵𝓵𝓸", // Mathematical alphanumeric symbols
995 "", // Empty string
996 " ", // Single space
997 "\n\r\t", // Whitespace characters
998 ];
999
1000 let mut hashes = Vec::new();
1001 for s in &test_strings {
1002 let hash = calculate_hash(s.as_bytes());
1003 hashes.push(hash);
1004 }
1005
1006 // First two should be equal (same string)
1007 assert_eq!(hashes[0], hashes[1]);
1008
1009 // All others should be unique
1010 for i in 0..hashes.len() {
1011 for j in 0..hashes.len() {
1012 if i != j && !(i == 0 && j == 1) && !(i == 1 && j == 0) {
1013 assert_ne!(
1014 hashes[i], hashes[j],
1015 "Strings '{}' and '{}' produced same hash",
1016 test_strings[i], test_strings[j]
1017 );
1018 }
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn test_incremental_data_hashing() -> Result<()> {
1025 // Test that hashing data incrementally produces consistent results
1026 let dir = tempdir()?;
1027 let file_path = dir.path().join("incremental.txt");
1028
1029 // Create a file with incremental content
1030 let mut content = String::new();
1031 let mut hashes = Vec::new();
1032
1033 for i in 0..10 {
1034 content.push_str(&format!("Line {}\n", i));
1035
1036 let mut file = safe_create_file(&file_path, false)?;
1037 file.write_all(content.as_bytes())?;
1038 drop(file); // Ensure file is closed
1039
1040 let hash = calculate_file_hash(&file_path)?;
1041 hashes.push(hash);
1042 }
1043
1044 // Each hash should be different
1045 for i in 0..hashes.len() {
1046 for j in i + 1..hashes.len() {
1047 assert_ne!(
1048 hashes[i], hashes[j],
1049 "Incremental content at positions {} and {} produced same hash",
1050 i, j
1051 );
1052 }
1053 }
1054
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn test_hash_consistency_across_algorithms() {
1060 // Test that the same data always produces the same hash for each algorithm
1061 let data = b"consistency test data";
1062 let iterations = 100;
1063
1064 let mut sha256_hashes = Vec::new();
1065 let mut sha384_hashes = Vec::new();
1066 let mut sha512_hashes = Vec::new();
1067
1068 for _ in 0..iterations {
1069 sha256_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha256));
1070 sha384_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha384));
1071 sha512_hashes.push(calculate_hash_with_algorithm(data, &HashAlgorithm::Sha512));
1072 }
1073
1074 // All hashes for the same algorithm should be identical
1075 for i in 1..iterations {
1076 assert_eq!(
1077 sha256_hashes[0], sha256_hashes[i],
1078 "SHA-256 inconsistent at iteration {}",
1079 i
1080 );
1081 assert_eq!(
1082 sha384_hashes[0], sha384_hashes[i],
1083 "SHA-384 inconsistent at iteration {}",
1084 i
1085 );
1086 assert_eq!(
1087 sha512_hashes[0], sha512_hashes[i],
1088 "SHA-512 inconsistent at iteration {}",
1089 i
1090 );
1091 }
1092 }
1093
1094 #[test]
1095 fn test_combine_hashes_edge_cases() -> Result<()> {
1096 // Test combining different numbers of hashes
1097 let hash1 = calculate_hash(b"data1");
1098 let hash2 = calculate_hash(b"data2");
1099 let hash3 = calculate_hash(b"data3");
1100
1101 // Single hash
1102 let single = combine_hashes(&[&hash1])?;
1103 assert_eq!(single.len(), 96);
1104
1105 // Two hashes
1106 let double = combine_hashes(&[&hash1, &hash2])?;
1107 assert_eq!(double.len(), 96);
1108 assert_ne!(single, double);
1109
1110 // Three hashes
1111 let triple = combine_hashes(&[&hash1, &hash2, &hash3])?;
1112 assert_eq!(triple.len(), 96);
1113 assert_ne!(double, triple);
1114
1115 // Test associativity - (A + B) + C should equal A + (B + C)
1116 let ab = combine_hashes(&[&hash1, &hash2])?;
1117 let ab_c = combine_hashes(&[&ab, &hash3])?;
1118
1119 let bc = combine_hashes(&[&hash2, &hash3])?;
1120 let a_bc = combine_hashes(&[&hash1, &bc])?;
1121
1122 // This is expected behavior.
1123 assert_ne!(ab_c, a_bc);
1124
1125 Ok(())
1126 }
1127
1128 #[test]
1129 fn test_file_not_found_error() {
1130 // Test proper error handling for non-existent files
1131 let result = calculate_file_hash("/this/path/should/not/exist/test.txt");
1132 assert!(result.is_err());
1133
1134 match result {
1135 Err(Error::Io(_)) => (), // Expected
1136 Err(e) => panic!("Expected Io error, got: {:?}", e),
1137 Ok(_) => panic!("Expected error for non-existent file"),
1138 }
1139 }
1140
1141 #[test]
1142 fn test_special_filenames() -> Result<()> {
1143 let dir = tempdir()?;
1144
1145 // Test with various special filenames
1146 let filenames = vec![
1147 "file with spaces.txt",
1148 "file-with-dashes.txt",
1149 "file_with_underscores.txt",
1150 "file.multiple.dots.txt",
1151 "UPPERCASE.TXT",
1152 "🦀rust🦀.txt", // Emoji in filename
1153 ".hidden_file",
1154 "very_long_filename_that_exceeds_typical_lengths_but_should_still_work_fine.txt",
1155 ];
1156
1157 for filename in filenames {
1158 let file_path = dir.path().join(filename);
1159 let mut file = safe_create_file(&file_path, false)?;
1160 file.write_all(b"test content")?;
1161 drop(file);
1162
1163 // Should be able to hash regardless of filename
1164 let hash = calculate_file_hash(&file_path)?;
1165 assert_eq!(hash.len(), 96, "Failed for filename: {}", filename);
1166 }
1167
1168 Ok(())
1169 }
1170
1171 #[test]
1172 fn test_concurrent_hashing_safety() {
1173 use std::sync::Arc;
1174 use std::thread;
1175
1176 // Test that hashing is thread-safe
1177 let data = Arc::new(b"concurrent test data".to_vec());
1178 let num_threads = 10;
1179 let iterations_per_thread = 100;
1180
1181 let mut handles = vec![];
1182
1183 for _ in 0..num_threads {
1184 let data_clone = Arc::clone(&data);
1185 let handle = thread::spawn(move || {
1186 let mut hashes = Vec::new();
1187 for _ in 0..iterations_per_thread {
1188 let hash = calculate_hash(&data_clone);
1189 hashes.push(hash);
1190 }
1191 hashes
1192 });
1193 handles.push(handle);
1194 }
1195
1196 // Collect all results
1197 let mut all_hashes = Vec::new();
1198 for handle in handles {
1199 let hashes = handle.join().expect("Thread panicked");
1200 all_hashes.extend(hashes);
1201 }
1202
1203 // All hashes should be identical
1204 let expected_hash = calculate_hash(&data);
1205 for (i, hash) in all_hashes.iter().enumerate() {
1206 assert_eq!(hash, &expected_hash, "Hash mismatch at index {}", i);
1207 }
1208 }
1209
1210 #[test]
1211 fn test_combine_hashes_with_invalid_hex() -> Result<()> {
1212 let valid_hash = calculate_hash(b"valid");
1213
1214 // Test with invalid hex string
1215 let result = combine_hashes(&[&valid_hash, "not_valid_hex"]);
1216 assert!(result.is_err());
1217
1218 // Test with odd-length hex string
1219 let result = combine_hashes(&[&valid_hash, "abc"]);
1220 assert!(result.is_err());
1221
1222 // Test with non-ASCII characters
1223 let result = combine_hashes(&[&valid_hash, "café"]);
1224 assert!(result.is_err());
1225
1226 Ok(())
1227 }
1228}