Skip to main content

oxicrypto_hash/
xof.rs

1// ── XOF / cSHAKE / TupleHash / BLAKE2b-keyed / hash_file ────────────────────
2//
3//! Extendable-output functions (XOFs) and related constructs per NIST SP 800-185.
4//!
5//! Provides:
6//! - SHAKE128 / SHAKE256 XOFs (FIPS 202)
7//! - cSHAKE128 / cSHAKE256 with function-name and customization strings
8//! - TupleHash128 / TupleHash256 (unambiguous encoding of byte-string tuples)
9//! - BLAKE2b keyed-hash (variable-length MAC mode, RFC 7693)
10//! - [`hash_file`] convenience functions (behind `std` feature)
11
12use alloc::vec::Vec;
13use cshake::digest::{ExtendableOutput, Update, XofReader};
14use oxicrypto_core::CryptoError;
15use shake::digest::{ExtendableOutput as ShakeExtend, Update as ShakeUpdate};
16
17// ── SHAKE128 / SHAKE256 ───────────────────────────────────────────────────────
18
19/// SHAKE128 XOF reader: stream an arbitrary number of output bytes from a
20/// finalised SHAKE128 state.
21pub struct Shake128Reader(shake::Shake128Reader);
22
23impl Shake128Reader {
24    /// Fill `out` with the next output bytes.
25    pub fn read(&mut self, out: &mut [u8]) {
26        self.0.read(out);
27    }
28}
29
30/// SHAKE256 XOF reader: stream an arbitrary number of output bytes from a
31/// finalised SHAKE256 state.
32pub struct Shake256Reader(shake::Shake256Reader);
33
34impl Shake256Reader {
35    /// Fill `out` with the next output bytes.
36    pub fn read(&mut self, out: &mut [u8]) {
37        self.0.read(out);
38    }
39}
40
41/// Hash `msg` with SHAKE128 and fill `out` with extendable output.
42pub fn shake128(msg: &[u8], out: &mut [u8]) {
43    let mut h = shake::Shake128::default();
44    ShakeUpdate::update(&mut h, msg);
45    let mut reader = ShakeExtend::finalize_xof(h);
46    reader.read(out);
47}
48
49/// Hash `msg` with SHAKE256 and fill `out` with extendable output.
50pub fn shake256(msg: &[u8], out: &mut [u8]) {
51    let mut h = shake::Shake256::default();
52    ShakeUpdate::update(&mut h, msg);
53    let mut reader = ShakeExtend::finalize_xof(h);
54    reader.read(out);
55}
56
57/// Begin a SHAKE128 computation, returning a finalisable hasher.
58///
59/// The `msg` argument absorbs message data; the returned [`Shake128Reader`]
60/// provides streaming output.
61pub fn shake128_start(msg: &[u8]) -> Shake128Reader {
62    let mut h = shake::Shake128::default();
63    ShakeUpdate::update(&mut h, msg);
64    Shake128Reader(ShakeExtend::finalize_xof(h))
65}
66
67/// Begin a SHAKE256 computation, returning a finalisable hasher.
68pub fn shake256_start(msg: &[u8]) -> Shake256Reader {
69    let mut h = shake::Shake256::default();
70    ShakeUpdate::update(&mut h, msg);
71    Shake256Reader(ShakeExtend::finalize_xof(h))
72}
73
74// ── cSHAKE128 / cSHAKE256 ────────────────────────────────────────────────────
75
76/// Hash `msg` with cSHAKE128 (NIST SP 800-185 §3) and fill `out`.
77///
78/// When both `function_name` and `customization` are empty this degrades to
79/// SHAKE128 (per spec).
80pub fn cshake128(msg: &[u8], function_name: &[u8], customization: &[u8], out: &mut [u8]) {
81    let mut h = cshake::CShake128::new_with_function_name(function_name, customization);
82    Update::update(&mut h, msg);
83    let mut reader = ExtendableOutput::finalize_xof(h);
84    reader.read(out);
85}
86
87/// Hash `msg` with cSHAKE256 (NIST SP 800-185 §3) and fill `out`.
88///
89/// When both `function_name` and `customization` are empty this degrades to
90/// SHAKE256 (per spec).
91pub fn cshake256(msg: &[u8], function_name: &[u8], customization: &[u8], out: &mut [u8]) {
92    let mut h = cshake::CShake256::new_with_function_name(function_name, customization);
93    Update::update(&mut h, msg);
94    let mut reader = ExtendableOutput::finalize_xof(h);
95    reader.read(out);
96}
97
98// ── TupleHash encoding helpers (SP 800-185 §2.3) ────────────────────────────
99
100/// `left_encode(x)`: big-endian byte encoding of `x`, prepended by a 1-byte
101/// length count indicating the number of non-zero bytes.
102pub(crate) fn left_encode(x: u64) -> Vec<u8> {
103    if x == 0 {
104        return alloc::vec![1u8, 0u8];
105    }
106    let be = x.to_be_bytes();
107    let leading_zeros = be.iter().take_while(|&&b| b == 0).count();
108    let n = 8 - leading_zeros; // number of significant bytes
109    let mut out = Vec::with_capacity(1 + n);
110    out.push(n as u8);
111    out.extend_from_slice(&be[leading_zeros..]);
112    out
113}
114
115/// `right_encode(x)`: big-endian byte encoding of `x`, followed by a 1-byte
116/// length count indicating the number of non-zero bytes.
117pub(crate) fn right_encode(x: u64) -> Vec<u8> {
118    if x == 0 {
119        return alloc::vec![0u8, 1u8];
120    }
121    let be = x.to_be_bytes();
122    let leading_zeros = be.iter().take_while(|&&b| b == 0).count();
123    let n = 8 - leading_zeros;
124    let mut out = Vec::with_capacity(n + 1);
125    out.extend_from_slice(&be[leading_zeros..]);
126    out.push(n as u8);
127    out
128}
129
130/// `encode_string(s)` = `left_encode(s.len() * 8)` || `s`.
131///
132/// # Errors
133///
134/// Returns [`CryptoError::BadInput`] if `s.len() * 8` overflows a `u64`. This is
135/// unreachable in practice (a slice cannot exceed `isize::MAX` bytes), but the
136/// check avoids any panic per the no-unwrap policy.
137pub(crate) fn encode_string(s: &[u8]) -> Result<Vec<u8>, CryptoError> {
138    let bit_len = (s.len() as u64)
139        .checked_mul(8)
140        .ok_or(CryptoError::BadInput)?;
141    let mut out = left_encode(bit_len);
142    out.extend_from_slice(s);
143    Ok(out)
144}
145
146// ── TupleHash128 / TupleHash256 ───────────────────────────────────────────────
147
148/// TupleHash128 (NIST SP 800-185 §5).
149///
150/// Hashes the *tuple* of byte strings in `inputs` with optional `customization`
151/// string; result length equals `out.len()`.
152///
153/// Crucially, `tuple_hash128(&[b"ab", b"c"], ...)` differs from
154/// `tuple_hash128(&[b"a", b"bc"], ...)` — the encoding is unambiguous.
155///
156/// # Errors
157///
158/// Returns [`CryptoError::BadInput`] if any input length or `out.len()`,
159/// multiplied by 8, overflows a `u64` (unreachable in practice).
160pub fn tuple_hash128(
161    inputs: &[&[u8]],
162    customization: &[u8],
163    out: &mut [u8],
164) -> Result<(), CryptoError> {
165    let mut h = cshake::CShake128::new_with_function_name(b"TupleHash", customization);
166
167    // encode_tuple(X) = encode_string(X[0]) || ... || encode_string(X[n-1])
168    for &input in inputs {
169        let encoded = encode_string(input)?;
170        Update::update(&mut h, &encoded);
171    }
172
173    // right_encode(L) where L = output length in bits
174    let out_bits = (out.len() as u64)
175        .checked_mul(8)
176        .ok_or(CryptoError::BadInput)?;
177    let renc = right_encode(out_bits);
178    Update::update(&mut h, &renc);
179
180    let mut reader = ExtendableOutput::finalize_xof(h);
181    reader.read(out);
182    Ok(())
183}
184
185/// TupleHash256 (NIST SP 800-185 §5).
186///
187/// Hashes the *tuple* of byte strings in `inputs` with optional `customization`
188/// string; result length equals `out.len()`.
189///
190/// # Errors
191///
192/// Returns [`CryptoError::BadInput`] if any input length or `out.len()`,
193/// multiplied by 8, overflows a `u64` (unreachable in practice).
194pub fn tuple_hash256(
195    inputs: &[&[u8]],
196    customization: &[u8],
197    out: &mut [u8],
198) -> Result<(), CryptoError> {
199    let mut h = cshake::CShake256::new_with_function_name(b"TupleHash", customization);
200
201    for &input in inputs {
202        let encoded = encode_string(input)?;
203        Update::update(&mut h, &encoded);
204    }
205
206    let out_bits = (out.len() as u64)
207        .checked_mul(8)
208        .ok_or(CryptoError::BadInput)?;
209    let renc = right_encode(out_bits);
210    Update::update(&mut h, &renc);
211
212    let mut reader = ExtendableOutput::finalize_xof(h);
213    reader.read(out);
214    Ok(())
215}
216
217// ── BLAKE2b keyed-hash mode ───────────────────────────────────────────────────
218
219/// BLAKE2b keyed-hash (MAC mode), with variable output size 1–64 bytes.
220///
221/// BLAKE2b in keyed mode is a one-pass MAC: the key is absorbed as the first
222/// block. Keys must be between 1 and 64 bytes inclusive.
223pub struct Blake2bKeyed {
224    key: Vec<u8>,
225}
226
227impl core::fmt::Debug for Blake2bKeyed {
228    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
229        write!(f, "Blake2bKeyed(***)")
230    }
231}
232
233impl Blake2bKeyed {
234    /// Create a keyed BLAKE2b hasher.
235    ///
236    /// # Errors
237    ///
238    /// Returns [`CryptoError::InvalidKey`] if `key` is empty or longer than 64 bytes.
239    pub fn new(key: &[u8]) -> Result<Self, CryptoError> {
240        if key.is_empty() || key.len() > 64 {
241            return Err(CryptoError::InvalidKey);
242        }
243        Ok(Self { key: key.to_vec() })
244    }
245
246    /// Hash `msg` under this key; output is written to `out`.
247    ///
248    /// `out.len()` must be between 1 and 64 bytes inclusive.
249    ///
250    /// # Errors
251    ///
252    /// Returns [`CryptoError::BadInput`] if `out` is empty or longer than 64 bytes.
253    /// Returns [`CryptoError::InvalidKey`] if the key is invalid (should not
254    /// happen after successful [`new`](Self::new)).
255    pub fn hash(&self, msg: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
256        blake2b_keyed(&self.key, msg, out)
257    }
258}
259
260/// Hash `msg` under `key` with BLAKE2b in keyed mode; output is written to `out`.
261///
262/// Both `key.len()` (1–64 bytes) and `out.len()` (1–64 bytes) are validated.
263///
264/// # Errors
265///
266/// Returns [`CryptoError::InvalidKey`] if `key` is empty or longer than 64 bytes.
267/// Returns [`CryptoError::BadInput`] if `out` is empty or longer than 64 bytes.
268pub fn blake2b_keyed(key: &[u8], msg: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
269    use blake2::digest::{FixedOutput, KeyInit, Update as MacUpdate};
270
271    if key.is_empty() || key.len() > 64 {
272        return Err(CryptoError::InvalidKey);
273    }
274    if out.is_empty() || out.len() > 64 {
275        return Err(CryptoError::BadInput);
276    }
277
278    // Blake2bMac512 gives a full 64-byte MAC; we then truncate to out.len()
279    let mut mac =
280        blake2::Blake2bMac512::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
281    MacUpdate::update(&mut mac, msg);
282    let full = mac.finalize_fixed();
283    out.copy_from_slice(&full[..out.len()]);
284    Ok(())
285}
286
287// ── hash_file (std feature) ───────────────────────────────────────────────────
288
289/// Hash a file at `path` with SHA-256, returning a 32-byte digest.
290///
291/// Reads the file in 64 KB chunks. Maps I/O errors to [`CryptoError::Internal`].
292///
293/// # Errors
294///
295/// Returns [`CryptoError::Internal`] if the file cannot be read.
296#[cfg(feature = "std")]
297pub fn hash_file_sha256(path: &std::path::Path) -> Result<[u8; 32], CryptoError> {
298    use sha2::Digest;
299    use std::io::Read;
300
301    let file = std::fs::File::open(path).map_err(|_| CryptoError::Internal("cannot open file"))?;
302    let mut reader = std::io::BufReader::new(file);
303    let mut hasher = sha2::Sha256::new();
304    let mut buf = alloc::vec![0u8; 65536];
305
306    loop {
307        let n = reader
308            .read(&mut buf)
309            .map_err(|_| CryptoError::Internal("file read error"))?;
310        if n == 0 {
311            break;
312        }
313        Digest::update(&mut hasher, &buf[..n]);
314    }
315
316    let result = Digest::finalize(hasher);
317    let mut out = [0u8; 32];
318    out.copy_from_slice(&result);
319    Ok(out)
320}
321
322/// Hash a file at `path` with SHA-512, returning a 64-byte digest.
323///
324/// Reads the file in 64 KB chunks. Maps I/O errors to [`CryptoError::Internal`].
325///
326/// # Errors
327///
328/// Returns [`CryptoError::Internal`] if the file cannot be read.
329#[cfg(feature = "std")]
330pub fn hash_file_sha512(path: &std::path::Path) -> Result<[u8; 64], CryptoError> {
331    use sha2::Digest;
332    use std::io::Read;
333
334    let file = std::fs::File::open(path).map_err(|_| CryptoError::Internal("cannot open file"))?;
335    let mut reader = std::io::BufReader::new(file);
336    let mut hasher = sha2::Sha512::new();
337    let mut buf = alloc::vec![0u8; 65536];
338
339    loop {
340        let n = reader
341            .read(&mut buf)
342            .map_err(|_| CryptoError::Internal("file read error"))?;
343        if n == 0 {
344            break;
345        }
346        Digest::update(&mut hasher, &buf[..n]);
347    }
348
349    let result = Digest::finalize(hasher);
350    let mut out = [0u8; 64];
351    out.copy_from_slice(&result);
352    Ok(out)
353}
354
355/// Hash a file at `path` with BLAKE3, returning a 32-byte digest.
356///
357/// Reads the file in 64 KB chunks. Maps I/O errors to [`CryptoError::Internal`].
358///
359/// # Errors
360///
361/// Returns [`CryptoError::Internal`] if the file cannot be read.
362#[cfg(feature = "std")]
363pub fn hash_file_blake3(path: &std::path::Path) -> Result<[u8; 32], CryptoError> {
364    use std::io::Read;
365
366    let file = std::fs::File::open(path).map_err(|_| CryptoError::Internal("cannot open file"))?;
367    let mut reader = std::io::BufReader::new(file);
368    let mut hasher = blake3::Hasher::new();
369    let mut buf = alloc::vec![0u8; 65536];
370
371    loop {
372        let n = reader
373            .read(&mut buf)
374            .map_err(|_| CryptoError::Internal("file read error"))?;
375        if n == 0 {
376            break;
377        }
378        hasher.update(&buf[..n]);
379    }
380
381    Ok(*hasher.finalize().as_bytes())
382}
383
384// ── Tests ─────────────────────────────────────────────────────────────────────
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    // ── SHAKE128 ──────────────────────────────────────────────────────────────
391
392    #[test]
393    fn shake128_nonzero_output() {
394        let mut out = [0u8; 32];
395        shake128(b"abc", &mut out);
396        assert!(
397            out.iter().any(|&b| b != 0),
398            "SHAKE128 output must be non-zero"
399        );
400    }
401
402    #[test]
403    fn shake128_64_prefix_matches_32() {
404        let mut out32 = [0u8; 32];
405        let mut out64 = [0u8; 64];
406        shake128(b"abc", &mut out32);
407        shake128(b"abc", &mut out64);
408        assert_eq!(
409            out32,
410            out64[..32],
411            "64-byte SHAKE128 must be prefixed by 32-byte output"
412        );
413    }
414
415    #[test]
416    fn shake256_nonzero_output() {
417        let mut out = [0u8; 32];
418        shake256(b"abc", &mut out);
419        assert!(out.iter().any(|&b| b != 0));
420    }
421
422    #[test]
423    fn shake128_reader_matches_one_shot() {
424        let mut expected = [0u8; 48];
425        shake128(b"hello", &mut expected);
426
427        let mut reader = shake128_start(b"hello");
428        let mut actual = [0u8; 48];
429        reader.read(&mut actual);
430        assert_eq!(expected, actual);
431    }
432
433    // ── cSHAKE128 ─────────────────────────────────────────────────────────────
434
435    #[test]
436    fn cshake128_empty_equals_shake128() {
437        let mut shake_out = [0u8; 32];
438        let mut cshake_out = [0u8; 32];
439        shake128(b"abc", &mut shake_out);
440        cshake128(b"abc", b"", b"", &mut cshake_out);
441        assert_eq!(
442            shake_out, cshake_out,
443            "cSHAKE128 with empty N and S must equal SHAKE128"
444        );
445    }
446
447    #[test]
448    fn cshake128_custom_differs_from_shake128() {
449        let mut shake_out = [0u8; 32];
450        let mut cshake_out = [0u8; 32];
451        shake128(b"abc", &mut shake_out);
452        cshake128(b"abc", b"Email Signature", b"", &mut cshake_out);
453        assert_ne!(
454            shake_out, cshake_out,
455            "cSHAKE128 with non-empty N must differ from SHAKE128"
456        );
457    }
458
459    #[test]
460    fn cshake256_empty_equals_shake256() {
461        let mut shake_out = [0u8; 64];
462        let mut cshake_out = [0u8; 64];
463        shake256(b"abc", &mut shake_out);
464        cshake256(b"abc", b"", b"", &mut cshake_out);
465        assert_eq!(
466            shake_out, cshake_out,
467            "cSHAKE256 with empty N and S must equal SHAKE256"
468        );
469    }
470
471    #[test]
472    fn cshake128_customization_matters() {
473        let mut out1 = [0u8; 32];
474        let mut out2 = [0u8; 32];
475        cshake128(b"abc", b"", b"customA", &mut out1);
476        cshake128(b"abc", b"", b"customB", &mut out2);
477        assert_ne!(
478            out1, out2,
479            "Different customization strings must produce different outputs"
480        );
481    }
482
483    // ── TupleHash ──────────────────────────────────────────────────────────────
484
485    #[test]
486    fn tuple_hash128_unambiguous() {
487        let mut out1 = [0u8; 32];
488        let mut out2 = [0u8; 32];
489        tuple_hash128(&[b"ab", b"c"], b"", &mut out1).unwrap();
490        tuple_hash128(&[b"a", b"bc"], b"", &mut out2).unwrap();
491        assert_ne!(
492            out1, out2,
493            "TupleHash128 must disambiguate ('ab','c') from ('a','bc')"
494        );
495    }
496
497    #[test]
498    fn tuple_hash256_unambiguous() {
499        let mut out1 = [0u8; 32];
500        let mut out2 = [0u8; 32];
501        tuple_hash256(&[b"ab", b"c"], b"", &mut out1).unwrap();
502        tuple_hash256(&[b"a", b"bc"], b"", &mut out2).unwrap();
503        assert_ne!(
504            out1, out2,
505            "TupleHash256 must disambiguate ('ab','c') from ('a','bc')"
506        );
507    }
508
509    #[test]
510    fn tuple_hash128_deterministic() {
511        let mut out1 = [0u8; 32];
512        let mut out2 = [0u8; 32];
513        tuple_hash128(&[b"hello", b"world"], b"custom", &mut out1).unwrap();
514        tuple_hash128(&[b"hello", b"world"], b"custom", &mut out2).unwrap();
515        assert_eq!(out1, out2, "TupleHash128 must be deterministic");
516    }
517
518    #[test]
519    fn tuple_hash128_customization_matters() {
520        let mut out1 = [0u8; 32];
521        let mut out2 = [0u8; 32];
522        tuple_hash128(&[b"data"], b"custA", &mut out1).unwrap();
523        tuple_hash128(&[b"data"], b"custB", &mut out2).unwrap();
524        assert_ne!(
525            out1, out2,
526            "Different customizations must produce different TupleHash128 outputs"
527        );
528    }
529
530    // ── Encoding helpers ────────────────────────────────────────────────────────
531
532    #[test]
533    fn left_encode_zero() {
534        assert_eq!(left_encode(0), alloc::vec![1u8, 0u8]);
535    }
536
537    #[test]
538    fn left_encode_one() {
539        // 1 → 1 byte significant; value = 0x01
540        assert_eq!(left_encode(1), alloc::vec![1u8, 1u8]);
541    }
542
543    #[test]
544    fn right_encode_zero() {
545        assert_eq!(right_encode(0), alloc::vec![0u8, 1u8]);
546    }
547
548    #[test]
549    fn right_encode_one() {
550        assert_eq!(right_encode(1), alloc::vec![1u8, 1u8]);
551    }
552
553    // ── BLAKE2b keyed ────────────────────────────────────────────────────────────
554
555    #[test]
556    fn blake2b_keyed_different_keys() {
557        let mut out1 = [0u8; 32];
558        let mut out2 = [0u8; 32];
559        blake2b_keyed(b"key1", b"message", &mut out1).unwrap();
560        blake2b_keyed(b"key2", b"message", &mut out2).unwrap();
561        assert_ne!(
562            out1, out2,
563            "Different keys must produce different BLAKE2b-keyed outputs"
564        );
565    }
566
567    #[test]
568    fn blake2b_keyed_different_messages() {
569        let mut out1 = [0u8; 32];
570        let mut out2 = [0u8; 32];
571        blake2b_keyed(b"key", b"message1", &mut out1).unwrap();
572        blake2b_keyed(b"key", b"message2", &mut out2).unwrap();
573        assert_ne!(out1, out2);
574    }
575
576    #[test]
577    fn blake2b_keyed_empty_key_rejected() {
578        let mut out = [0u8; 32];
579        assert_eq!(
580            blake2b_keyed(b"", b"msg", &mut out).unwrap_err(),
581            CryptoError::InvalidKey
582        );
583    }
584
585    #[test]
586    fn blake2b_keyed_too_long_key_rejected() {
587        let mut out = [0u8; 32];
588        assert_eq!(
589            blake2b_keyed(&[0u8; 65], b"msg", &mut out).unwrap_err(),
590            CryptoError::InvalidKey
591        );
592    }
593
594    #[test]
595    fn blake2b_keyed_empty_output_rejected() {
596        assert_eq!(
597            blake2b_keyed(b"key", b"msg", &mut []).unwrap_err(),
598            CryptoError::BadInput
599        );
600    }
601
602    #[test]
603    fn blake2b_keyed_64byte_key_ok() {
604        let mut out = [0u8; 64];
605        blake2b_keyed(&[0x42u8; 64], b"hello", &mut out).unwrap();
606        assert!(out.iter().any(|&b| b != 0));
607    }
608
609    #[test]
610    fn blake2b_keyed_struct_api() {
611        let key = b"my secret key";
612        let msg = b"hello world";
613        let mut out_fn = [0u8; 32];
614        let mut out_struct = [0u8; 32];
615
616        blake2b_keyed(key, msg, &mut out_fn).unwrap();
617        Blake2bKeyed::new(key)
618            .unwrap()
619            .hash(msg, &mut out_struct)
620            .unwrap();
621
622        assert_eq!(
623            out_fn, out_struct,
624            "Free function and struct API must agree"
625        );
626    }
627
628    #[test]
629    fn blake2b_keyed_struct_empty_key_rejected() {
630        assert_eq!(Blake2bKeyed::new(b"").unwrap_err(), CryptoError::InvalidKey);
631    }
632
633    // ── hash_file ─────────────────────────────────────────────────────────────
634
635    #[cfg(feature = "std")]
636    #[test]
637    fn hash_file_sha256_matches_in_memory() {
638        use sha2::Digest;
639        use std::io::Write;
640
641        let content = b"Hello, hash_file test!";
642
643        let mut path = std::env::temp_dir();
644        path.push("oxicrypto_hash_file_test.bin");
645
646        {
647            let mut f = std::fs::File::create(&path).unwrap();
648            f.write_all(content).unwrap();
649        }
650
651        let expected = sha2::Sha256::digest(content);
652        let actual = hash_file_sha256(&path).unwrap();
653
654        assert_eq!(actual.as_slice(), expected.as_slice());
655
656        let _ = std::fs::remove_file(&path);
657    }
658
659    #[cfg(feature = "std")]
660    #[test]
661    fn hash_file_sha512_matches_in_memory() {
662        use sha2::Digest;
663        use std::io::Write;
664
665        let content = b"SHA-512 file hash test";
666
667        let mut path = std::env::temp_dir();
668        path.push("oxicrypto_hash_file_sha512_test.bin");
669
670        {
671            let mut f = std::fs::File::create(&path).unwrap();
672            f.write_all(content).unwrap();
673        }
674
675        let expected = sha2::Sha512::digest(content);
676        let actual = hash_file_sha512(&path).unwrap();
677
678        assert_eq!(actual.as_slice(), expected.as_slice());
679
680        let _ = std::fs::remove_file(&path);
681    }
682
683    #[cfg(feature = "std")]
684    #[test]
685    fn hash_file_blake3_matches_in_memory() {
686        use std::io::Write;
687
688        let content = b"BLAKE3 file hash test";
689
690        let mut path = std::env::temp_dir();
691        path.push("oxicrypto_hash_file_blake3_test.bin");
692
693        {
694            let mut f = std::fs::File::create(&path).unwrap();
695            f.write_all(content).unwrap();
696        }
697
698        let expected = *blake3::hash(content).as_bytes();
699        let actual = hash_file_blake3(&path).unwrap();
700
701        assert_eq!(actual, expected);
702
703        let _ = std::fs::remove_file(&path);
704    }
705
706    #[cfg(feature = "std")]
707    #[test]
708    fn hash_file_sha256_not_found() {
709        let path = std::env::temp_dir().join("oxicrypto_nonexistent_file_12345678.bin");
710        let err = hash_file_sha256(&path).unwrap_err();
711        assert!(matches!(err, CryptoError::Internal(_)));
712    }
713}