Skip to main content

cortexkit_bus_trait/
message.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::str::FromStr;
4
5use sha2::{Digest, Sha256};
6
7pub type Headers = BTreeMap<String, String>;
8pub type MessageId = String;
9
10/// A SHA-256 content digest in its canonical lowercase hexadecimal wire form.
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct ContentDigest([u8; 32]);
13
14impl ContentDigest {
15    pub fn of_bytes(bytes: &[u8]) -> Self {
16        Self(Sha256::digest(bytes).into())
17    }
18
19    pub fn as_bytes(&self) -> &[u8; 32] {
20        &self.0
21    }
22}
23
24impl fmt::Debug for ContentDigest {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        fmt::Display::fmt(self, formatter)
27    }
28}
29
30impl fmt::Display for ContentDigest {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        for byte in self.0 {
33            write!(formatter, "{byte:02x}")?;
34        }
35        Ok(())
36    }
37}
38
39impl FromStr for ContentDigest {
40    type Err = &'static str;
41
42    fn from_str(value: &str) -> Result<Self, Self::Err> {
43        if value.len() != 64 {
44            return Err("SHA-256 digest must contain exactly 64 hexadecimal characters");
45        }
46        if value
47            .bytes()
48            .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
49        {
50            return Err("SHA-256 digest must use lowercase hexadecimal");
51        }
52
53        let mut digest = [0_u8; 32];
54        for (index, byte) in digest.iter_mut().enumerate() {
55            let offset = index * 2;
56            let pair = &value[offset..offset + 2];
57            *byte = u8::from_str_radix(pair, 16).expect("hexadecimal was checked above");
58        }
59        Ok(Self(digest))
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Message {
65    pub subject: String,
66    pub id: MessageId,
67    pub digest: ContentDigest,
68    pub headers: Headers,
69}
70
71impl Message {
72    pub fn wire_size(&self) -> usize {
73        self.subject.len()
74            + self.id.len()
75            + 64
76            + self
77                .headers
78                .iter()
79                .map(|(name, value)| name.len() + value.len())
80                .sum::<usize>()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn digest_wire_form_is_lowercase_sha256() {
90        let digest = ContentDigest::of_bytes(b"record");
91        let encoded = digest.to_string();
92
93        assert_eq!(encoded.len(), 64);
94        assert_eq!(encoded.parse::<ContentDigest>(), Ok(digest));
95        assert!(encoded.to_uppercase().parse::<ContentDigest>().is_err());
96    }
97}