Skip to main content

krabka_remote_storage/worm/manifest/
hex_bytes.rs

1//! The byte values a manifest serialises as a lowercase hex string.
2//!
3//! `Sha256Digest` fixes a 32-byte digest and `HexBytes` carries a key or a
4//! signature, whose length the type system cannot pin. Both share the hex
5//! codec in this file, and `ChainHead` uses it too.
6
7use {
8    hex::FromHex as _,
9    serde::{Deserialize, Deserializer, Serializer, de},
10    sha2::{Digest, Sha256},
11};
12
13/// `SHA-256` digest of one archived object's body.
14///
15/// Serialises as a lowercase hex string.
16#[krabka_macros::hex_bytes_identity]
17#[derive(Copy)]
18pub struct Sha256Digest(
19    #[serde(
20        serialize_with = "serialize_hex",
21        deserialize_with = "deserialize_hex32"
22    )]
23    pub [u8; 32],
24);
25
26impl Sha256Digest {
27    /// Digest of `bytes`.
28    #[must_use]
29    pub fn of(bytes: &[u8]) -> Self { Self(Sha256::digest(bytes).into()) }
30}
31
32/// Variable-length bytes that serialise as a lowercase hex string.
33///
34/// Used for the public key and the signature, neither of which has a fixed
35/// length the type system can pin.
36#[krabka_macros::hex_bytes_identity]
37pub struct HexBytes(
38    #[serde(serialize_with = "serialize_hex", deserialize_with = "deserialize_hex")] pub Vec<u8>,
39);
40
41pub(super) fn serialize_hex<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
42where
43    S: Serializer,
44{
45    serializer.serialize_str(&hex::encode(bytes))
46}
47
48pub(super) fn deserialize_hex32<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
49where
50    D: Deserializer<'de>,
51{
52    let text = String::deserialize(deserializer)?;
53    <[u8; 32]>::from_hex(&text)
54        .map_err(|_| de::Error::custom(format!("expected 64 hex characters, got `{text}`")))
55}
56
57fn deserialize_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
58where
59    D: Deserializer<'de>,
60{
61    let text = String::deserialize(deserializer)?;
62    hex::decode(&text).map_err(de::Error::custom)
63}
64
65#[cfg(test)]
66mod tests {
67    use {super::*, crate::worm::manifest::ChainHead, assert2::check};
68
69    #[test]
70    fn byte_identities_keep_their_hex_json_and_display() {
71        let digest = Sha256Digest([0x12; 32]);
72        let encoded = format!("\"{}\"", "12".repeat(32));
73        check!(serde_json::to_string(&digest).unwrap() == encoded);
74        check!(serde_json::from_str::<Sha256Digest>(&encoded).unwrap() == digest);
75        check!(digest.to_string() == "12".repeat(32));
76        check!(format!("{digest:?}") == format!("Sha256Digest({})", "12".repeat(32)));
77
78        let bytes = HexBytes(vec![0, 0xa5, 0xff]);
79        check!(serde_json::to_string(&bytes).unwrap() == "\"00a5ff\"");
80        check!(serde_json::from_str::<HexBytes>("\"00a5ff\"").unwrap() == bytes);
81        check!(bytes.to_string() == "00a5ff");
82        check!(format!("{bytes:?}") == "HexBytes(00a5ff)");
83    }
84
85    #[test]
86    fn hex_newtypes_round_trip_and_format() {
87        let head = ChainHead([0xa1; 32]);
88        let encoded = serde_json::to_string(&head).unwrap();
89        check!(encoded == format!("\"{}\"", "a1".repeat(32)));
90        check!(serde_json::from_str::<ChainHead>(&encoded).unwrap() == head);
91        check!(head.to_string() == "a1".repeat(32));
92        check!(format!("{head:?}") == format!("ChainHead({})", "a1".repeat(32)));
93
94        let digest = Sha256Digest::of(b"body");
95        check!(
96            serde_json::from_str::<Sha256Digest>(&serde_json::to_string(&digest).unwrap()).unwrap()
97                == digest
98        );
99        check!(format!("{digest:?}") == format!("Sha256Digest({digest})"));
100
101        check!(HexBytes(vec![0xde, 0xad]).to_string() == "dead");
102        check!(format!("{:?}", HexBytes(vec![0xde, 0xad])) == "HexBytes(dead)");
103    }
104
105    #[yare::parameterized(
106        empty = { "\"\"".to_owned() },
107        too_short = { format!("\"{}\"", "00".repeat(31)) },
108        too_long = { format!("\"{}\"", "00".repeat(33)) },
109        odd_length = { format!("\"{}0\"", "00".repeat(31)) },
110        non_hex = { format!("\"{}\"", "zz".repeat(32)) },
111        not_a_string = { "12345".to_owned() },
112        null = { "null".to_owned() },
113        array = { "[]".to_owned() },
114    )]
115    fn fixed_width_hex_newtypes_reject_bad_input_without_panicking(json: String) {
116        check!(serde_json::from_str::<ChainHead>(&json).is_err());
117        check!(serde_json::from_str::<Sha256Digest>(&json).is_err());
118    }
119
120    #[yare::parameterized(
121        empty = { "\"\"", Some(Vec::new()) },
122        even_length_hex = { "\"00ff10\"", Some(vec![0x00, 0xff, 0x10]) },
123        odd_length = { "\"abc\"", None },
124        non_hex = { "\"zz\"", None },
125        not_a_string = { "42", None },
126        null = { "null", None },
127    )]
128    fn hex_bytes_decode_cases_are_independent(json: &str, expected: Option<Vec<u8>>) {
129        check!(serde_json::from_str::<HexBytes>(json).ok().map(|h| h.0) == expected);
130    }
131}