Skip to main content

canic_cdk/utils/
hash.rs

1//!
2//! Shared SHA-256 helpers for wasm/module identity and hex rendering.
3//!
4
5use sha2::{Digest, Sha256};
6use std::{error::Error, fmt};
7
8///
9/// HashBytes
10///
11
12pub type HashBytes = Vec<u8>;
13
14/// Compute SHA-256 bytes from an in-memory byte slice.
15#[must_use]
16pub fn sha256_bytes(bytes: &[u8]) -> HashBytes {
17    let mut hasher = Sha256::new();
18    hasher.update(bytes);
19    hasher.finalize().to_vec()
20}
21
22/// Compute lowercase hexadecimal SHA-256 from an in-memory byte slice.
23#[must_use]
24pub fn sha256_hex(bytes: &[u8]) -> String {
25    hex_bytes(sha256_bytes(bytes))
26}
27
28/// Compute raw wasm module hash bytes.
29#[must_use]
30pub fn wasm_hash(bytes: &[u8]) -> HashBytes {
31    sha256_bytes(bytes)
32}
33
34/// Compute lowercase hexadecimal wasm module hash.
35#[must_use]
36pub fn wasm_hash_hex(bytes: &[u8]) -> String {
37    sha256_hex(bytes)
38}
39
40/// Render one byte slice as lowercase hexadecimal.
41#[must_use]
42pub fn hex_bytes(bytes: impl AsRef<[u8]>) -> String {
43    let bytes = bytes.as_ref();
44    let mut encoded = String::with_capacity(bytes.len() * 2);
45
46    for byte in bytes {
47        encoded.push(hex_char(byte >> 4));
48        encoded.push(hex_char(byte & 0x0f));
49    }
50
51    encoded
52}
53
54/// Decode one even-length hexadecimal string into bytes.
55pub fn decode_hex(hex: &str) -> Result<HashBytes, DecodeHexError> {
56    if !hex.len().is_multiple_of(2) {
57        return Err(DecodeHexError::OddLength(hex.len()));
58    }
59
60    let mut bytes = Vec::with_capacity(hex.len() / 2);
61    for index in (0..hex.len()).step_by(2) {
62        let high = decode_nibble(hex.as_bytes()[index], index)?;
63        let low = decode_nibble(hex.as_bytes()[index + 1], index + 1)?;
64        bytes.push((high << 4) | low);
65    }
66
67    Ok(bytes)
68}
69
70// Convert one four-bit nibble to lowercase hexadecimal.
71fn hex_char(nibble: u8) -> char {
72    char::from(b"0123456789abcdef"[usize::from(nibble & 0x0f)])
73}
74
75// Decode one ASCII hex digit.
76fn decode_nibble(byte: u8, index: usize) -> Result<u8, DecodeHexError> {
77    match byte {
78        b'0'..=b'9' => Ok(byte - b'0'),
79        b'a'..=b'f' => Ok(byte - b'a' + 10),
80        b'A'..=b'F' => Ok(byte - b'A' + 10),
81        _ => Err(DecodeHexError::InvalidDigit {
82            index,
83            byte: char::from(byte),
84        }),
85    }
86}
87
88///
89/// DecodeHexError
90///
91
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum DecodeHexError {
94    OddLength(usize),
95
96    InvalidDigit { index: usize, byte: char },
97}
98
99impl fmt::Display for DecodeHexError {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::OddLength(length) => {
103                write!(formatter, "hex string must have even length, got {length}")
104            }
105            Self::InvalidDigit { index, byte } => {
106                write!(formatter, "invalid hex digit {byte:?} at index {index}")
107            }
108        }
109    }
110}
111
112impl Error for DecodeHexError {}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb924\
119                                27ae41e4649b934ca495991b7852b855";
120
121    #[test]
122    fn wasm_hash_hex_matches_sha256_vector() {
123        assert_eq!(wasm_hash_hex(&[]), EMPTY_SHA256);
124    }
125
126    #[test]
127    fn hex_round_trip_accepts_upper_and_lowercase() {
128        assert_eq!(decode_hex("01aBff").expect("decode hex"), vec![1, 171, 255]);
129        assert_eq!(hex_bytes([1, 171, 255]), "01abff");
130    }
131
132    #[test]
133    fn decode_hex_rejects_invalid_input() {
134        assert!(matches!(decode_hex("f"), Err(DecodeHexError::OddLength(1))));
135        assert!(matches!(
136            decode_hex("0g"),
137            Err(DecodeHexError::InvalidDigit { index: 1, .. })
138        ));
139    }
140}