graphrefly_core/hash.rs
1//! Synchronous SHA-256 hashing.
2//!
3//! GraphReFly's public substrate exposes `sha256Hex` (cache-key /
4//! content-addressing helper used by versioning, snapshot paths, and the
5//! presentation-layer adapters). The TS reference impl is async only
6//! because `crypto.subtle.digest` returns a `Promise` — there is nothing
7//! intrinsically async about SHA-256.
8//!
9//! Per the Rust-port invariant "No async runtime in Core" (CLAUDE.md
10//! invariant 4 / D070 / D077), the hashing itself stays **fully
11//! synchronous** here. The async-everywhere wrapping demanded by the
12//! `Impl` parity contract is applied only at the napi boundary
13//! (`graphrefly-bindings-js`), never in this crate.
14//!
15//! @module
16
17use sha2::{Digest, Sha256};
18
19/// Hex-encode the SHA-256 digest of `bytes`. Pure, synchronous, no
20/// allocation beyond the 64-char output `String`. Mirrors the TS
21/// `sha256Hex` byte semantics: the caller is responsible for encoding a
22/// `string` input to UTF-8 bytes before calling (the napi boundary does
23/// this).
24#[must_use]
25pub fn sha256_hex(bytes: &[u8]) -> String {
26 let mut hasher = Sha256::new();
27 hasher.update(bytes);
28 let digest = hasher.finalize();
29 hex::encode(digest)
30}
31
32#[cfg(test)]
33mod tests {
34 use super::sha256_hex;
35
36 #[test]
37 fn sha256_hex_known_vectors() {
38 // RFC-known: SHA-256("") and SHA-256("abc").
39 assert_eq!(
40 sha256_hex(b""),
41 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
42 );
43 assert_eq!(
44 sha256_hex(b"abc"),
45 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
46 );
47 }
48
49 #[test]
50 fn sha256_hex_matches_ts_string_byte_semantics() {
51 // The TS `sha256Hex("hello")` path UTF-8-encodes the string;
52 // the Rust napi boundary does the same, so the digest must match
53 // the well-known SHA-256("hello").
54 assert_eq!(
55 sha256_hex("hello".as_bytes()),
56 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
57 );
58 }
59}