krabka_remote_storage/worm/manifest/
hex_bytes.rs1use {
8 hex::FromHex as _,
9 serde::{Deserialize, Deserializer, Serializer, de},
10 sha2::{Digest, Sha256},
11};
12
13#[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 #[must_use]
29 pub fn of(bytes: &[u8]) -> Self { Self(Sha256::digest(bytes).into()) }
30}
31
32#[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}