dig_network_block/
serde_hex.rs

1//! Serde helpers to serialize/deserialize byte arrays/vectors as 0x-prefixed hex strings.
2//!
3//! - `hex_vec`: for `Vec<u8>` of any length.
4//! - `hex32`: for `[u8; 32]` with exact length enforcement.
5//! - `hex48`: for `[u8; 48]` with exact length enforcement.
6//!
7//! These helpers ensure strict `0x` prefix and lowercase hex encoding.
8
9use serde::{Deserialize, Deserializer, Serializer};
10use thiserror::Error;
11
12/// Errors that can occur during hex (de)serialization.
13#[derive(Debug, Error)]
14pub enum HexSerdeError {
15    /// Input string must begin with `0x` prefix.
16    #[error("missing 0x prefix")]
17    MissingPrefix,
18
19    /// Input contained non-hex characters or odd-length digits.
20    #[error("invalid hex encoding: {0}")]
21    InvalidHex(String),
22
23    /// For fixed-size arrays: decoded byte length did not match the expected size.
24    #[error("length mismatch: expected {expected} bytes, got {actual} bytes")]
25    LengthMismatch { expected: usize, actual: usize },
26}
27
28fn strip_0x(s: &str) -> Result<&str, HexSerdeError> {
29    if let Some(rest) = s.strip_prefix("0x") {
30        Ok(rest)
31    } else {
32        Err(HexSerdeError::MissingPrefix)
33    }
34}
35
36fn encode_lower_hex_prefixed(bytes: &[u8]) -> String {
37    let mut out = String::with_capacity(2 + bytes.len() * 2);
38    out.push_str("0x");
39    out.push_str(&hex::encode(bytes));
40    out
41}
42
43/// Serde helpers for `Vec<u8>` as 0x-hex.
44pub mod hex_vec {
45    use super::*;
46
47    /// Serialize a `Vec<u8>` as an `"0x..."` lowercase hex string.
48    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: Serializer,
51    {
52        let s = encode_lower_hex_prefixed(bytes);
53        serializer.serialize_str(&s)
54    }
55
56    /// Deserialize a `Vec<u8>` from an `"0x..."` lowercase hex string.
57    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
58    where
59        D: Deserializer<'de>,
60    {
61        let s: String = String::deserialize(deserializer)?;
62        let hex_part = strip_0x(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
63        let bytes = hex::decode(hex_part).map_err(|e| {
64            serde::de::Error::custom(HexSerdeError::InvalidHex(e.to_string()).to_string())
65        })?;
66        Ok(bytes)
67    }
68}
69
70/// Serde helpers for `[u8; 32]` as 0x-hex.
71pub mod hex32 {
72    use super::*;
73
74    /// Serialize a `[u8; 32]` as an `"0x..."` lowercase hex string.
75    pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
76    where
77        S: Serializer,
78    {
79        let s = encode_lower_hex_prefixed(bytes);
80        serializer.serialize_str(&s)
81    }
82
83    /// Deserialize a `[u8; 32]` from an `"0x..."` lowercase hex string.
84    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
85    where
86        D: Deserializer<'de>,
87    {
88        let s: String = String::deserialize(deserializer)?;
89        let hex_part = strip_0x(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
90        let bytes = hex::decode(hex_part).map_err(|e| {
91            serde::de::Error::custom(HexSerdeError::InvalidHex(e.to_string()).to_string())
92        })?;
93        if bytes.len() != 32 {
94            return Err(serde::de::Error::custom(
95                HexSerdeError::LengthMismatch {
96                    expected: 32,
97                    actual: bytes.len(),
98                }
99                .to_string(),
100            ));
101        }
102        let mut arr = [0u8; 32];
103        arr.copy_from_slice(&bytes);
104        Ok(arr)
105    }
106}
107
108/// Serde helpers for `[u8; 48]` as 0x-hex.
109pub mod hex48 {
110    use super::*;
111
112    /// Serialize a `[u8; 48]` as an `"0x..."` lowercase hex string.
113    pub fn serialize<S>(bytes: &[u8; 48], serializer: S) -> Result<S::Ok, S::Error>
114    where
115        S: Serializer,
116    {
117        let s = encode_lower_hex_prefixed(bytes);
118        serializer.serialize_str(&s)
119    }
120
121    /// Deserialize a `[u8; 48]` from an `"0x..."` lowercase hex string.
122    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 48], D::Error>
123    where
124        D: Deserializer<'de>,
125    {
126        let s: String = String::deserialize(deserializer)?;
127        let hex_part = strip_0x(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
128        let bytes = hex::decode(hex_part).map_err(|e| {
129            serde::de::Error::custom(HexSerdeError::InvalidHex(e.to_string()).to_string())
130        })?;
131        if bytes.len() != 48 {
132            return Err(serde::de::Error::custom(
133                HexSerdeError::LengthMismatch {
134                    expected: 48,
135                    actual: bytes.len(),
136                }
137                .to_string(),
138            ));
139        }
140        let mut arr = [0u8; 48];
141        arr.copy_from_slice(&bytes);
142        Ok(arr)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148
149    use serde::{Deserialize, Serialize};
150
151    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
152    struct VecWrap(#[serde(with = "crate::serde_hex::hex_vec")] Vec<u8>);
153
154    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
155    struct Arr32Wrap(#[serde(with = "crate::serde_hex::hex32")] [u8; 32]);
156
157    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
158    struct Arr48Wrap(#[serde(with = "crate::serde_hex::hex48")] [u8; 48]);
159
160    #[test]
161    fn vec_round_trip() {
162        let v = VecWrap(vec![0x00, 0x01, 0xaa, 0xff]);
163        let s = serde_json::to_string(&v).unwrap();
164        // Newtype struct serializes as inner value (a JSON string)
165        assert_eq!(s, "\"0x0001aaff\"");
166        let back: VecWrap = serde_json::from_str(&s).unwrap();
167        assert_eq!(back, v);
168    }
169
170    #[test]
171    fn arr32_round_trip() {
172        let mut a = [0u8; 32];
173        a[0] = 0xde;
174        a[31] = 0xad;
175        let w = Arr32Wrap(a);
176        let s = serde_json::to_string(&w).unwrap();
177        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
178        let s_hex = v.as_str().unwrap();
179        assert!(s_hex.starts_with("0x"));
180        assert_eq!(s_hex.len(), 2 + 64);
181        let back: Arr32Wrap = serde_json::from_str(&s).unwrap();
182        assert_eq!(back, w);
183    }
184
185    #[test]
186    fn arr48_round_trip() {
187        let mut a = [0u8; 48];
188        a[0] = 0x12;
189        a[47] = 0x34;
190        let w = Arr48Wrap(a);
191        let s = serde_json::to_string(&w).unwrap();
192        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
193        let s_hex = v.as_str().unwrap();
194        assert!(s_hex.starts_with("0x"));
195        assert_eq!(s_hex.len(), 2 + 96);
196        let back: Arr48Wrap = serde_json::from_str(&s).unwrap();
197        assert_eq!(back, w);
198    }
199
200    #[test]
201    fn vec_rejects_missing_prefix() {
202        let s = "\"deadbeef\""; // no 0x
203        let err = serde_json::from_str::<VecWrap>(s).unwrap_err();
204        let msg = err.to_string();
205        assert!(msg.contains("missing 0x prefix"));
206    }
207
208    #[test]
209    fn arr32_wrong_length_rejected() {
210        // 31 bytes (62 hex chars) with 0x
211        let s = format!("\"0x{}\"", "00".repeat(31));
212        let err = serde_json::from_str::<Arr32Wrap>(&s).unwrap_err();
213        let msg = err.to_string();
214        assert!(msg.contains("length mismatch"));
215    }
216
217    #[test]
218    fn arr48_wrong_length_rejected() {
219        // 49 bytes
220        let s = format!("\"0x{}\"", "ff".repeat(49));
221        let err = serde_json::from_str::<Arr48Wrap>(&s).unwrap_err();
222        let msg = err.to_string();
223        assert!(msg.contains("length mismatch"));
224    }
225
226    #[test]
227    fn invalid_hex_char_rejected() {
228        let s = "\"0xzz\""; // invalid hex
229        let err = serde_json::from_str::<VecWrap>(s).unwrap_err();
230        let msg = err.to_string();
231        assert!(msg.contains("invalid hex encoding"));
232    }
233}