Skip to main content

fixed_bigint/fixeduint/
byte_conversion_panic_free.rs

1// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Fixed-size byte conversion: typed buffers, compile-time size check.
16//!
17//! Panic-free-signature counterparts to the slice-based
18//! `FixedUInt::{to,from}_{le,be}_bytes`. Take `&[u8; M]` / `&mut [u8; M]`
19//! and verify `M >= BYTE_WIDTH` at monomorphization; wrong-size callers
20//! fail at compile time. The signature guarantee is "no `Result`, no
21//! `.unwrap()` at the boundary" — not "no `panic_fmt` in the linked
22//! binary": `copy_from_slice` still carries a length check LLVM can't
23//! elide through the trait boundary.
24//!
25//! Oversized-buffer convention when `M > BYTE_WIDTH`: LE uses the
26//! leading `BYTE_WIDTH` bytes and BE uses the trailing `BYTE_WIDTH`
27//! bytes. `to_*_bytes_fixed` writes into that window and returns it;
28//! `from_*_bytes_fixed` reads from it. The pair is designed to round-
29//! trip against itself — matches the slice-based `from_*_bytes` window
30//! choice, but *not* the slice-based `to_be_bytes`, which writes to
31//! the leading window on oversized input.
32
33// `let _ = <T as AssertBufferFits<M>>::CHECK;` forces the const to
34// evaluate at monomorphization; a bare path-statement isn't a reliable
35// substitute across rustc versions.
36#![allow(clippy::let_unit_value)]
37
38use super::{FixedUInt, MachineWord, impl_from_be_bytes_slice, impl_from_le_bytes_slice};
39use const_num_traits::Personality;
40
41/// Type-level compile-time assertion that buffer-of-length-`M` fits a
42/// `FixedUInt<T,N,P>`'s byte width. The associated const `CHECK` evaluates
43/// to a `()`-or-compile-error: on a monomorphization where `M >= BYTE_WIDTH`
44/// the body of `assert!` is a no-op; otherwise it is a const-eval error
45/// that aborts compilation with the diagnostic message.
46///
47/// Why a trait + associated const instead of a `const { assert!(...) }`
48/// block: on nightly with `generic_const_exprs` enabled, in-fn
49/// `const { … M … }` blocks become "generic constants" that the compiler
50/// rejects with "overly complex generic constant". Moving the assertion
51/// to an associated const on a trait impl sidesteps that — the impl
52/// header carries the generics, and the const item body is a plain
53/// expression referencing them.
54trait AssertBufferFits<const M: usize> {
55    const CHECK: ();
56}
57
58impl<T: MachineWord, const N: usize, P: Personality, const M: usize> AssertBufferFits<M>
59    for FixedUInt<T, N, P>
60{
61    const CHECK: () = assert!(
62        M >= Self::BYTE_WIDTH,
63        "*_bytes_fixed: buffer size M must be >= FixedUInt::BYTE_WIDTH (= N * size_of::<T>())",
64    );
65}
66
67impl<T: MachineWord, const N: usize, P: Personality> FixedUInt<T, N, P> {
68    /// Serialize little-endian into a fixed-size buffer. The const
69    /// `M >= BYTE_WIDTH` precondition fires at monomorphization, so
70    /// wrong-size callers fail at compile time and the produced binary
71    /// contains no runtime panic path from this method.
72    ///
73    /// Returns the written prefix (`&out[..BYTE_WIDTH]`). If
74    /// `M > BYTE_WIDTH`, the trailing bytes of `out` are left untouched.
75    ///
76    /// ```
77    /// use fixed_bigint::FixedUInt;
78    /// type U16 = FixedUInt<u8, 2>;
79    /// let v = U16::from(0x1234u16);
80    /// let mut buf = [0u8; U16::BYTE_WIDTH];
81    /// let bytes = v.to_le_bytes_fixed(&mut buf);
82    /// assert_eq!(bytes, &[0x34, 0x12]);
83    /// ```
84    #[inline]
85    pub fn to_le_bytes_fixed<'a, const M: usize>(&self, out: &'a mut [u8; M]) -> &'a [u8] {
86        let _ = <Self as AssertBufferFits<M>>::CHECK;
87        let word_size = Self::WORD_SIZE;
88        for (chunk, word) in out.chunks_exact_mut(word_size).zip(self.array.iter()) {
89            chunk.copy_from_slice(word.to_le_bytes().as_ref());
90        }
91        &out[..Self::BYTE_WIDTH]
92    }
93
94    /// Big-endian counterpart of [`to_le_bytes_fixed`](Self::to_le_bytes_fixed);
95    /// same const-asserted size guarantee and same panic-free intent.
96    ///
97    /// Returns the written window `&out[M - BYTE_WIDTH ..]`. If
98    /// `M > BYTE_WIDTH`, the leading bytes of `out` are left untouched
99    /// — mirror image of `to_le_bytes_fixed`, aligning the value with
100    /// the trailing window that `from_be_bytes_fixed` reads.
101    ///
102    /// ```
103    /// use fixed_bigint::FixedUInt;
104    /// type U16 = FixedUInt<u8, 2>;
105    /// let v = U16::from(0x1234u16);
106    /// let mut buf = [0u8; U16::BYTE_WIDTH];
107    /// let bytes = v.to_be_bytes_fixed(&mut buf);
108    /// assert_eq!(bytes, &[0x12, 0x34]);
109    /// ```
110    #[inline]
111    pub fn to_be_bytes_fixed<'a, const M: usize>(&self, out: &'a mut [u8; M]) -> &'a [u8] {
112        let _ = <Self as AssertBufferFits<M>>::CHECK;
113        let word_size = Self::WORD_SIZE;
114        let start = M - Self::BYTE_WIDTH;
115        // Walk words from MSB to LSB so the output is BE. Align to the
116        // trailing window so oversized buffers round-trip through
117        // `from_be_bytes_fixed`.
118        for (chunk, word) in out[start..]
119            .chunks_exact_mut(word_size)
120            .zip(self.array.iter().rev())
121        {
122            chunk.copy_from_slice(word.to_be_bytes().as_ref());
123        }
124        &out[start..]
125    }
126
127    /// Deserialize from a fixed-size little-endian buffer. The const
128    /// `M >= BYTE_WIDTH` precondition fires at monomorphization. Reads
129    /// the first `BYTE_WIDTH` bytes (LE low-order bytes are at the
130    /// front); trailing bytes if `M > BYTE_WIDTH` are ignored.
131    ///
132    /// ```
133    /// use fixed_bigint::FixedUInt;
134    /// type U16 = FixedUInt<u8, 2>;
135    /// let buf = [0x34u8, 0x12];
136    /// let v = U16::from_le_bytes_fixed(&buf);
137    /// assert_eq!(v, U16::from(0x1234u16));
138    /// ```
139    #[inline]
140    pub fn from_le_bytes_fixed<const M: usize>(bytes: &[u8; M]) -> Self {
141        let _ = <Self as AssertBufferFits<M>>::CHECK;
142        // The helper takes `&[u8]` and bounds its loop by
143        // `min(bytes.len(), capacity)`; passing the full M-byte slice
144        // means `bytes.len() == M >= BYTE_WIDTH == capacity`, so the
145        // loop bound is BYTE_WIDTH and every indexed read is in range.
146        Self::from_array(impl_from_le_bytes_slice::<T, N>(bytes))
147    }
148
149    /// Deserialize from a fixed-size big-endian buffer. The const
150    /// `M >= BYTE_WIDTH` precondition fires at monomorphization. Reads
151    /// the last `BYTE_WIDTH` bytes (BE low-order bytes are at the end);
152    /// leading bytes if `M > BYTE_WIDTH` are ignored.
153    ///
154    /// ```
155    /// use fixed_bigint::FixedUInt;
156    /// type U16 = FixedUInt<u8, 2>;
157    /// let buf = [0x12u8, 0x34];
158    /// let v = U16::from_be_bytes_fixed(&buf);
159    /// assert_eq!(v, U16::from(0x1234u16));
160    /// ```
161    #[inline]
162    pub fn from_be_bytes_fixed<const M: usize>(bytes: &[u8; M]) -> Self {
163        let _ = <Self as AssertBufferFits<M>>::CHECK;
164        // The BE helper already handles the `bytes.len() > capacity`
165        // case by reading the trailing `capacity` bytes (BE low-order
166        // bytes are at the end). With M >= BYTE_WIDTH it picks the
167        // right window without our needing to compute `start` here.
168        Self::from_array(impl_from_be_bytes_slice::<T, N>(bytes))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    type U16 = FixedUInt<u8, 2>;
177    type U32 = FixedUInt<u32, 1>; // single-limb u32 backing
178    type U64 = FixedUInt<u32, 2>; // two-limb u32 backing
179
180    // ─── to_le_bytes_fixed ────────────────────────────────────────────
181
182    #[test]
183    fn to_le_bytes_fixed_exact_size_round_trips() {
184        let v = U16::from(0x1234u16);
185        let mut buf = [0u8; U16::BYTE_WIDTH];
186        let written = v.to_le_bytes_fixed(&mut buf);
187        assert_eq!(written, &[0x34, 0x12]);
188        assert_eq!(buf, [0x34, 0x12]);
189    }
190
191    #[test]
192    fn to_le_bytes_fixed_oversized_leaves_trailing_untouched() {
193        let v = U16::from(0x1234u16);
194        let mut buf = [0xFFu8; 4];
195        let written = v.to_le_bytes_fixed(&mut buf);
196        assert_eq!(written, &[0x34, 0x12]);
197        assert_eq!(buf, [0x34, 0x12, 0xFF, 0xFF]);
198    }
199
200    #[test]
201    fn to_le_bytes_fixed_matches_slice_method() {
202        let v = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
203        let mut a = [0u8; U64::BYTE_WIDTH];
204        let mut b = [0u8; U64::BYTE_WIDTH];
205        let fixed = v.to_le_bytes_fixed(&mut a);
206        let slice = v.to_le_bytes(&mut b).unwrap();
207        assert_eq!(fixed, slice);
208    }
209
210    // ─── to_be_bytes_fixed ────────────────────────────────────────────
211
212    #[test]
213    fn to_be_bytes_fixed_exact_size_round_trips() {
214        let v = U16::from(0x1234u16);
215        let mut buf = [0u8; U16::BYTE_WIDTH];
216        let written = v.to_be_bytes_fixed(&mut buf);
217        assert_eq!(written, &[0x12, 0x34]);
218        assert_eq!(buf, [0x12, 0x34]);
219    }
220
221    #[test]
222    fn to_be_bytes_fixed_matches_slice_method() {
223        let v = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
224        let mut a = [0u8; U64::BYTE_WIDTH];
225        let mut b = [0u8; U64::BYTE_WIDTH];
226        let fixed = v.to_be_bytes_fixed(&mut a);
227        let slice = v.to_be_bytes(&mut b).unwrap();
228        assert_eq!(fixed, slice);
229    }
230
231    #[test]
232    fn to_be_bytes_fixed_oversized_writes_trailing_window() {
233        let v = U16::from(0x1234u16);
234        let mut buf = [0xFFu8; 4];
235        let written = v.to_be_bytes_fixed(&mut buf);
236        assert_eq!(written, &[0x12, 0x34]);
237        assert_eq!(buf, [0xFF, 0xFF, 0x12, 0x34]);
238    }
239
240    #[test]
241    fn to_be_fixed_from_be_fixed_round_trip_oversized() {
242        // The window `to_be_bytes_fixed` writes must match the window
243        // `from_be_bytes_fixed` reads, or oversized BE round-trips
244        // decode the untouched leading bytes.
245        let v = U16::from(0x1234u16);
246        let mut buf = [0u8; 4];
247        let _ = v.to_be_bytes_fixed(&mut buf);
248        let back: U16 = U16::from_be_bytes_fixed(&buf);
249        assert_eq!(back, v);
250    }
251
252    // ─── from_le_bytes_fixed ──────────────────────────────────────────
253
254    #[test]
255    fn from_le_bytes_fixed_exact_size() {
256        let buf = [0x34u8, 0x12];
257        let v: U16 = U16::from_le_bytes_fixed(&buf);
258        assert_eq!(v, U16::from(0x1234u16));
259    }
260
261    #[test]
262    fn from_le_bytes_fixed_oversized_takes_low_bytes() {
263        // U16 wants 2 bytes; provide 4. LE convention: take first 2.
264        let buf = [0x34u8, 0x12, 0xFF, 0xFF];
265        let v: U16 = U16::from_le_bytes_fixed(&buf);
266        assert_eq!(v, U16::from(0x1234u16));
267    }
268
269    #[test]
270    fn from_le_bytes_fixed_matches_slice_method() {
271        let buf = [0xEF, 0xBE, 0xAD, 0xDE, 0xBE, 0xBA, 0xFE, 0xCA];
272        let fixed: U64 = U64::from_le_bytes_fixed(&buf);
273        let slice: U64 = U64::from_le_bytes(&buf[..]);
274        assert_eq!(fixed, slice);
275    }
276
277    // ─── from_be_bytes_fixed ──────────────────────────────────────────
278
279    #[test]
280    fn from_be_bytes_fixed_exact_size() {
281        let buf = [0x12u8, 0x34];
282        let v: U16 = U16::from_be_bytes_fixed(&buf);
283        assert_eq!(v, U16::from(0x1234u16));
284    }
285
286    #[test]
287    fn from_be_bytes_fixed_oversized_takes_trailing_bytes() {
288        // U16 wants 2 bytes; provide 4. BE convention: take last 2.
289        let buf = [0xFFu8, 0xFF, 0x12, 0x34];
290        let v: U16 = U16::from_be_bytes_fixed(&buf);
291        assert_eq!(v, U16::from(0x1234u16));
292    }
293
294    #[test]
295    fn from_be_bytes_fixed_matches_slice_method() {
296        let buf = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
297        let fixed: U64 = U64::from_be_bytes_fixed(&buf);
298        let slice: U64 = U64::from_be_bytes(&buf[..]);
299        assert_eq!(fixed, slice);
300    }
301
302    // ─── round-trip across all four ───────────────────────────────────
303
304    #[test]
305    fn round_trip_le_fixed() {
306        let original = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
307        let mut buf = [0u8; U64::BYTE_WIDTH];
308        let _ = original.to_le_bytes_fixed(&mut buf);
309        let back: U64 = U64::from_le_bytes_fixed(&buf);
310        assert_eq!(back, original);
311    }
312
313    #[test]
314    fn round_trip_be_fixed() {
315        let original = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
316        let mut buf = [0u8; U64::BYTE_WIDTH];
317        let _ = original.to_be_bytes_fixed(&mut buf);
318        let back: U64 = U64::from_be_bytes_fixed(&buf);
319        assert_eq!(back, original);
320    }
321
322    // ─── wider carrier (sanity-check word stride math) ────────────────
323
324    #[test]
325    fn u32_single_limb_le() {
326        let v = U32::from(0x12345678u32);
327        let mut buf = [0u8; U32::BYTE_WIDTH];
328        let written = v.to_le_bytes_fixed(&mut buf);
329        assert_eq!(written, &[0x78, 0x56, 0x34, 0x12]);
330        let back: U32 = U32::from_le_bytes_fixed(&buf);
331        assert_eq!(back, v);
332    }
333
334    #[test]
335    fn u32_single_limb_be() {
336        let v = U32::from(0x12345678u32);
337        let mut buf = [0u8; U32::BYTE_WIDTH];
338        let written = v.to_be_bytes_fixed(&mut buf);
339        assert_eq!(written, &[0x12, 0x34, 0x56, 0x78]);
340        let back: U32 = U32::from_be_bytes_fixed(&buf);
341        assert_eq!(back, v);
342    }
343
344    #[test]
345    fn byte_width_is_usable_as_array_length() {
346        const BUF_LEN: usize = U64::BYTE_WIDTH;
347        let mut buf = [0u8; BUF_LEN];
348        let v = U64::from(42u32);
349        let _ = v.to_le_bytes_fixed(&mut buf);
350        assert_eq!(buf[0], 42);
351    }
352}