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}