Skip to main content

commonware_utils/sequence/
fixed_bytes.rs

1use crate::{Array, Span};
2use bytes::BufMut;
3use commonware_codec::{Buf, Error as CodecError, FixedArray, FixedSize, Read, ReadExt, Write};
4use commonware_formatting::Hex;
5use core::{
6    cmp::{Ord, Ordering, PartialOrd},
7    fmt::{Debug, Display},
8    hash::Hash,
9    ops::Deref,
10};
11use thiserror::Error;
12use zeroize::Zeroize;
13
14/// Errors returned by `Bytes` functions.
15#[derive(Error, Debug, PartialEq)]
16pub enum Error {
17    #[error("invalid length")]
18    InvalidLength,
19}
20
21/// An `Array` implementation for fixed-length byte arrays.
22#[derive(Clone, Eq, PartialEq, Hash, Debug, FixedArray)]
23#[fixed_array(infallible, bytes([u8; N]))]
24#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
25#[repr(transparent)]
26pub struct FixedBytes<const N: usize>([u8; N]);
27
28impl<const N: usize> FixedBytes<N> {
29    /// Creates a new `FixedBytes` instance from an array of length `N`.
30    pub const fn new(value: [u8; N]) -> Self {
31        Self(value)
32    }
33}
34
35impl<const N: usize> Ord for FixedBytes<N> {
36    #[inline]
37    fn cmp(&self, other: &Self) -> Ordering {
38        let (a, b) = (&self.0, &other.0);
39
40        // Up to 16 bytes the derived compare is faster. Past 64 bytes, unrolling bloats call sites.
41        if N <= 16 || N > 64 {
42            return a.cmp(b);
43        }
44        let (a_words, _) = a.as_chunks::<8>();
45        let (b_words, _) = b.as_chunks::<8>();
46        for (a, b) in a_words.iter().zip(b_words) {
47            let (a, b) = (u64::from_be_bytes(*a), u64::from_be_bytes(*b));
48            if a != b {
49                return a.cmp(&b);
50            }
51        }
52        if N.is_multiple_of(8) {
53            return Ordering::Equal;
54        }
55
56        // Every earlier byte is equal, so the last 8 bytes (overlapping the last whole word) decide.
57        let last = |x: &[u8; N]| u64::from_be_bytes(*x.last_chunk::<8>().expect("N > 16"));
58        last(a).cmp(&last(b))
59    }
60}
61
62impl<const N: usize> PartialOrd for FixedBytes<N> {
63    #[inline]
64    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
65        Some(self.cmp(other))
66    }
67}
68
69impl<const N: usize> Write for FixedBytes<N> {
70    fn write(&self, buf: &mut impl BufMut) {
71        self.0.write(buf);
72    }
73}
74
75impl<const N: usize> Read for FixedBytes<N> {
76    type Cfg = ();
77
78    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
79        Ok(Self(<[u8; N]>::read(buf)?))
80    }
81}
82
83impl<const N: usize> FixedSize for FixedBytes<N> {
84    const SIZE: usize = N;
85}
86
87impl<const N: usize> Span for FixedBytes<N> {}
88
89impl<const N: usize> Array for FixedBytes<N> {}
90
91impl<const N: usize> AsRef<[u8]> for FixedBytes<N> {
92    fn as_ref(&self) -> &[u8] {
93        &self.0
94    }
95}
96
97impl<const N: usize> Deref for FixedBytes<N> {
98    type Target = [u8];
99    fn deref(&self) -> &[u8] {
100        &self.0
101    }
102}
103
104impl<const N: usize> Display for FixedBytes<N> {
105    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106        write!(f, "{}", Hex(&self.0))
107    }
108}
109
110impl<const N: usize> Zeroize for FixedBytes<N> {
111    fn zeroize(&mut self) {
112        self.0.zeroize();
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::{fixed_bytes, test_rng};
120    use bytes::{Buf as _, BytesMut};
121    use commonware_codec::{Copying, DecodeExt, Encode};
122    use rand::RngExt as _;
123
124    #[test]
125    fn test_codec() {
126        let original = FixedBytes::new([1, 2, 3, 4]);
127        let encoded = original.encode();
128        assert_eq!(encoded.len(), original.len());
129        let decoded = FixedBytes::decode(encoded).unwrap();
130        assert_eq!(original, decoded);
131    }
132
133    #[test]
134    fn test_bytes_creation_and_conversion() {
135        let value = [1, 2, 3, 4];
136        let bytes = FixedBytes::new(value);
137        assert_eq!(bytes.as_ref(), &value);
138
139        let bytes_into = value.into();
140        assert_eq!(bytes, bytes_into);
141
142        let slice = [1, 2, 3, 4];
143        let bytes_from_slice = FixedBytes::decode(Copying(&slice)).unwrap();
144        assert_eq!(bytes_from_slice, bytes);
145
146        let vec = vec![1, 2, 3, 4];
147        let bytes_from_vec = FixedBytes::decode(vec).unwrap();
148        assert_eq!(bytes_from_vec, bytes);
149
150        // Test with incorrect length
151        let slice_too_short = [1, 2, 3];
152        assert!(matches!(
153            FixedBytes::<4>::decode(Copying(&slice_too_short)),
154            Err(CodecError::EndOfBuffer)
155        ));
156
157        let vec_too_long = vec![1, 2, 3, 4, 5];
158        assert!(matches!(
159            FixedBytes::<4>::decode(vec_too_long),
160            Err(CodecError::ExtraData(1))
161        ));
162    }
163
164    #[test]
165    fn test_read() {
166        let mut buf = BytesMut::from(&[1, 2, 3, 4][..]);
167        let bytes = FixedBytes::<4>::read(&mut buf).unwrap();
168        assert_eq!(bytes.as_ref(), &[1, 2, 3, 4]);
169        assert_eq!(buf.remaining(), 0);
170
171        let mut buf = BytesMut::from(&[1, 2, 3][..]);
172        let result = FixedBytes::<4>::read(&mut buf);
173        assert!(matches!(result, Err(CodecError::EndOfBuffer)));
174
175        let mut buf = BytesMut::from(&[1, 2, 3, 4, 5][..]);
176        let bytes = FixedBytes::<4>::read(&mut buf).unwrap();
177        assert_eq!(bytes.as_ref(), &[1, 2, 3, 4]);
178        assert_eq!(buf.remaining(), 1);
179        assert_eq!(buf[0], 5);
180    }
181
182    #[test]
183    fn test_display() {
184        let bytes = fixed_bytes!("0x01020304");
185        assert_eq!(format!("{bytes}"), "01020304");
186    }
187
188    #[test]
189    fn test_ord_and_eq() {
190        let a = FixedBytes::new([1, 2, 3, 4]);
191        let b = FixedBytes::new([1, 2, 3, 5]);
192        assert!(a < b);
193        assert_ne!(a, b);
194
195        let c = FixedBytes::new([1, 2, 3, 4]);
196        assert_eq!(a, c);
197    }
198
199    fn assert_matches<const N: usize>(a: &[u8; N], b: &[u8; N]) {
200        let (left, right) = (FixedBytes::new(*a), FixedBytes::new(*b));
201        assert_eq!(left.cmp(&right), a.cmp(b));
202        assert_eq!(right.cmp(&left), b.cmp(a));
203        assert_eq!(left.partial_cmp(&right), Some(a.cmp(b)));
204    }
205
206    fn check<const N: usize>() {
207        const BOUNDARIES: [u8; 4] = [0x00, 0x7f, 0x80, 0xff];
208        let mut rng = test_rng();
209        for _ in 0..8 {
210            let a: [u8; N] = rng.random();
211            assert_matches(&a, &a);
212
213            // Make every position the first difference.
214            for k in 0..N {
215                let mut b = a;
216                b[k] = a[k] ^ rng.random_range(1..=u8::MAX);
217                for byte in &mut b[k + 1..] {
218                    *byte = rng.random();
219                }
220                assert_matches(&a, &b);
221
222                // Boundary bytes at `k`, with suffixes that order the other way for some pairs.
223                for x in BOUNDARIES {
224                    for y in BOUNDARIES {
225                        let (mut a, mut b) = (a, a);
226                        a[k] = x;
227                        b[k] = y;
228                        a[k + 1..].fill(0xff);
229                        b[k + 1..].fill(0x00);
230                        assert_matches(&a, &b);
231                    }
232                }
233            }
234        }
235    }
236
237    /// Checks `FixedBytes` ordering against `[u8; N]` at every first-difference position, on both
238    /// sides of each size cutoff and at every remainder length.
239    #[test]
240    fn test_ord_matches_byte_order() {
241        check::<0>();
242        check::<5>();
243        check::<16>();
244        check::<17>();
245        check::<18>();
246        check::<19>();
247        check::<20>();
248        check::<21>();
249        check::<22>();
250        check::<23>();
251        check::<24>();
252        check::<31>();
253        check::<32>();
254        check::<33>();
255        check::<40>();
256        check::<63>();
257        check::<64>();
258        check::<65>();
259    }
260
261    #[cfg(feature = "arbitrary")]
262    mod conformance {
263        use super::*;
264        use commonware_codec::conformance::CodecConformance;
265
266        commonware_conformance::conformance_tests! {
267            CodecConformance<FixedBytes<16>>,
268        }
269    }
270}