chie_shared/types/
fixed_arrays.rs

1//! Fixed-size array types with const generics for type-safe cryptographic operations.
2//!
3//! This module provides zero-cost abstractions over fixed-size byte arrays commonly used
4//! in cryptographic operations. All types use const generics for compile-time size checking.
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::fmt;
8
9/// Generic fixed-size byte array with const generic size.
10///
11/// Provides a type-safe wrapper around fixed-size arrays with useful utility methods
12/// for encoding, comparison, and serialization.
13///
14/// # Type Safety
15///
16/// Different sizes create incompatible types at compile time:
17/// ```
18/// # use chie_shared::FixedBytes;
19/// let hash32 = FixedBytes::<32>::new([0u8; 32]);
20/// let hash64 = FixedBytes::<64>::new([0u8; 64]);
21/// // hash32 == hash64; // Compile error: different types
22/// ```
23#[derive(Clone, Copy, PartialEq, Eq, Hash)]
24pub struct FixedBytes<const N: usize> {
25    bytes: [u8; N],
26}
27
28impl<const N: usize> FixedBytes<N> {
29    /// Create a new fixed-size byte array.
30    #[must_use]
31    pub const fn new(bytes: [u8; N]) -> Self {
32        Self { bytes }
33    }
34
35    /// Create from a byte slice. Returns `None` if the slice length doesn't match.
36    ///
37    /// # Example
38    /// ```
39    /// # use chie_shared::FixedBytes;
40    /// let bytes = &[1, 2, 3, 4];
41    /// let fixed = FixedBytes::<4>::from_slice(bytes).unwrap();
42    /// assert_eq!(fixed.as_bytes(), bytes);
43    /// ```
44    #[must_use]
45    pub fn from_slice(slice: &[u8]) -> Option<Self> {
46        if slice.len() == N {
47            let mut bytes = [0u8; N];
48            bytes.copy_from_slice(slice);
49            Some(Self { bytes })
50        } else {
51            None
52        }
53    }
54
55    /// Create from a hexadecimal string.
56    ///
57    /// # Errors
58    /// Returns error if the hex string is invalid or has wrong length.
59    pub fn from_hex(hex: &str) -> Result<Self, String> {
60        if hex.len() != N * 2 {
61            return Err(format!(
62                "Invalid hex length: expected {} chars, got {}",
63                N * 2,
64                hex.len()
65            ));
66        }
67
68        let mut bytes = [0u8; N];
69        for i in 0..N {
70            let byte_str = &hex[i * 2..i * 2 + 2];
71            bytes[i] = u8::from_str_radix(byte_str, 16)
72                .map_err(|e| format!("Invalid hex character: {e}"))?;
73        }
74        Ok(Self { bytes })
75    }
76
77    /// Get the byte array as a slice.
78    #[must_use]
79    pub const fn as_bytes(&self) -> &[u8; N] {
80        &self.bytes
81    }
82
83    /// Get the byte array as a mutable slice.
84    #[must_use]
85    pub fn as_bytes_mut(&mut self) -> &mut [u8; N] {
86        &mut self.bytes
87    }
88
89    /// Convert to hexadecimal string.
90    #[must_use]
91    pub fn to_hex(&self) -> String {
92        self.bytes
93            .iter()
94            .map(|b| format!("{b:02x}"))
95            .collect::<String>()
96    }
97
98    /// Get the size of this array type at compile time.
99    #[must_use]
100    pub const fn size() -> usize {
101        N
102    }
103
104    /// Check if all bytes are zero.
105    #[must_use]
106    pub fn is_zero(&self) -> bool {
107        self.bytes.iter().all(|&b| b == 0)
108    }
109
110    /// Create a zeroed array.
111    #[must_use]
112    pub const fn zero() -> Self {
113        Self { bytes: [0u8; N] }
114    }
115}
116
117impl<const N: usize> Default for FixedBytes<N> {
118    fn default() -> Self {
119        Self::zero()
120    }
121}
122
123impl<const N: usize> AsRef<[u8]> for FixedBytes<N> {
124    fn as_ref(&self) -> &[u8] {
125        &self.bytes
126    }
127}
128
129impl<const N: usize> AsMut<[u8]> for FixedBytes<N> {
130    fn as_mut(&mut self) -> &mut [u8] {
131        &mut self.bytes
132    }
133}
134
135impl<const N: usize> From<[u8; N]> for FixedBytes<N> {
136    fn from(bytes: [u8; N]) -> Self {
137        Self::new(bytes)
138    }
139}
140
141impl<const N: usize> From<FixedBytes<N>> for [u8; N] {
142    fn from(fixed: FixedBytes<N>) -> Self {
143        fixed.bytes
144    }
145}
146
147impl<const N: usize> fmt::Debug for FixedBytes<N> {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "FixedBytes<{}>({})", N, self.to_hex())
150    }
151}
152
153impl<const N: usize> fmt::Display for FixedBytes<N> {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.to_hex())
156    }
157}
158
159// Serde implementations for const generic arrays
160impl<const N: usize> Serialize for FixedBytes<N> {
161    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162    where
163        S: Serializer,
164    {
165        // Serialize as a byte array (array of u8 values)
166        serializer.serialize_bytes(&self.bytes)
167    }
168}
169
170impl<'de, const N: usize> Deserialize<'de> for FixedBytes<N> {
171    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
172    where
173        D: Deserializer<'de>,
174    {
175        // Deserialize from bytes
176        let bytes: Vec<u8> = serde::de::Deserialize::deserialize(deserializer)?;
177        if bytes.len() != N {
178            return Err(serde::de::Error::custom(format!(
179                "expected {} bytes, got {}",
180                N,
181                bytes.len()
182            )));
183        }
184        let mut array = [0u8; N];
185        array.copy_from_slice(&bytes);
186        Ok(Self { bytes: array })
187    }
188}
189
190// Type aliases for common cryptographic sizes
191
192/// BLAKE3 hash (32 bytes / 256 bits)
193pub type Blake3Hash = FixedBytes<32>;
194
195/// Ed25519 signature (64 bytes / 512 bits)
196pub type Ed25519Signature = FixedBytes<64>;
197
198/// Ed25519 public key (32 bytes / 256 bits)
199pub type Ed25519PublicKey = FixedBytes<32>;
200
201/// Challenge nonce (32 bytes / 256 bits)
202pub type Nonce32 = FixedBytes<32>;
203
204/// SHA-256 hash (32 bytes / 256 bits)
205pub type Sha256Hash = FixedBytes<32>;
206
207/// SHA-512 hash (64 bytes / 512 bits)
208pub type Sha512Hash = FixedBytes<64>;
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_fixed_bytes_new() {
216        let bytes = [1u8; 32];
217        let fixed = FixedBytes::<32>::new(bytes);
218        assert_eq!(fixed.as_bytes(), &bytes);
219    }
220
221    #[test]
222    fn test_fixed_bytes_from_slice() {
223        let slice = &[1, 2, 3, 4];
224        let fixed = FixedBytes::<4>::from_slice(slice).unwrap();
225        assert_eq!(fixed.as_bytes(), &[1, 2, 3, 4]);
226
227        // Wrong size should fail
228        let wrong = FixedBytes::<8>::from_slice(slice);
229        assert!(wrong.is_none());
230    }
231
232    #[test]
233    fn test_fixed_bytes_hex_roundtrip() {
234        let bytes = [0x12, 0x34, 0xAB, 0xCD];
235        let fixed = FixedBytes::<4>::new(bytes);
236        let hex = fixed.to_hex();
237        assert_eq!(hex, "1234abcd");
238
239        let decoded = FixedBytes::<4>::from_hex(&hex).unwrap();
240        assert_eq!(decoded, fixed);
241    }
242
243    #[test]
244    fn test_fixed_bytes_from_hex_errors() {
245        // Wrong length
246        let result = FixedBytes::<4>::from_hex("12345");
247        assert!(result.is_err());
248
249        // Invalid hex character
250        let result = FixedBytes::<4>::from_hex("1234567g");
251        assert!(result.is_err());
252    }
253
254    #[test]
255    fn test_fixed_bytes_size() {
256        assert_eq!(FixedBytes::<32>::size(), 32);
257        assert_eq!(FixedBytes::<64>::size(), 64);
258        assert_eq!(Blake3Hash::size(), 32);
259        assert_eq!(Ed25519Signature::size(), 64);
260    }
261
262    #[test]
263    fn test_fixed_bytes_is_zero() {
264        let zero = FixedBytes::<8>::zero();
265        assert!(zero.is_zero());
266
267        let mut non_zero = FixedBytes::<8>::zero();
268        non_zero.as_bytes_mut()[0] = 1;
269        assert!(!non_zero.is_zero());
270    }
271
272    #[test]
273    fn test_fixed_bytes_default() {
274        let default = FixedBytes::<16>::default();
275        assert!(default.is_zero());
276    }
277
278    #[test]
279    fn test_fixed_bytes_display() {
280        let fixed = FixedBytes::<4>::new([0xAA, 0xBB, 0xCC, 0xDD]);
281        let display = format!("{fixed}");
282        assert_eq!(display, "aabbccdd");
283    }
284
285    #[test]
286    fn test_fixed_bytes_debug() {
287        let fixed = FixedBytes::<4>::new([0xAA, 0xBB, 0xCC, 0xDD]);
288        let debug = format!("{fixed:?}");
289        assert_eq!(debug, "FixedBytes<4>(aabbccdd)");
290    }
291
292    #[test]
293    fn test_type_aliases() {
294        let _hash: Blake3Hash = FixedBytes::zero();
295        let _sig: Ed25519Signature = FixedBytes::zero();
296        let _pubkey: Ed25519PublicKey = FixedBytes::zero();
297        let _nonce: Nonce32 = FixedBytes::zero();
298        let _sha256: Sha256Hash = FixedBytes::zero();
299        let _sha512: Sha512Hash = FixedBytes::zero();
300
301        // Verify sizes
302        assert_eq!(Blake3Hash::size(), 32);
303        assert_eq!(Ed25519Signature::size(), 64);
304        assert_eq!(Ed25519PublicKey::size(), 32);
305        assert_eq!(Nonce32::size(), 32);
306        assert_eq!(Sha256Hash::size(), 32);
307        assert_eq!(Sha512Hash::size(), 64);
308    }
309
310    #[test]
311    fn test_fixed_bytes_equality() {
312        let a = FixedBytes::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
313        let b = FixedBytes::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
314        let c = FixedBytes::<8>::new([1, 2, 3, 4, 5, 6, 7, 9]);
315
316        assert_eq!(a, b);
317        assert_ne!(a, c);
318    }
319
320    #[test]
321    fn test_fixed_bytes_conversions() {
322        let array = [1u8, 2, 3, 4];
323        let fixed: FixedBytes<4> = array.into();
324        let back: [u8; 4] = fixed.into();
325        assert_eq!(array, back);
326    }
327
328    #[test]
329    fn test_fixed_bytes_as_ref() {
330        let fixed = FixedBytes::<4>::new([1, 2, 3, 4]);
331        let slice: &[u8] = fixed.as_ref();
332        assert_eq!(slice, &[1, 2, 3, 4]);
333    }
334
335    #[test]
336    fn test_fixed_bytes_serde() {
337        let fixed = FixedBytes::<8>::new([1, 2, 3, 4, 5, 6, 7, 8]);
338        let json = serde_json::to_string(&fixed).unwrap();
339        let decoded: FixedBytes<8> = serde_json::from_str(&json).unwrap();
340        assert_eq!(fixed, decoded);
341    }
342}