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 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. No `Result`, no `.unwrap()` at the boundary,
21//! and no `panic_fmt` in the linked binary: the inner byte-copy is a
22//! zip loop rather than `copy_from_slice`, so it needs no length proof
23//! on any toolchain (MSRV included).
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        // Advance one flat byte iterator per limb rather than
88        // `chunks_exact_mut(WORD_SIZE)`, whose `size()` divides by the
89        // (runtime-field) chunk size — a division rustc at MSRV can't prove
90        // non-zero, leaving a div-by-zero panic guard at `-Oz`. `by_ref().zip`
91        // over `iter_mut()` has no such division and stays panic-free.
92        let mut dst = out.iter_mut();
93        for word in self.array.iter() {
94            let word_bytes = word.to_le_bytes();
95            for (&src, slot) in word_bytes.as_ref().iter().zip(dst.by_ref()) {
96                *slot = src;
97            }
98        }
99        &out[..Self::BYTE_WIDTH]
100    }
101
102    /// Big-endian counterpart of [`to_le_bytes_fixed`](Self::to_le_bytes_fixed);
103    /// same const-asserted size guarantee and same panic-free intent.
104    ///
105    /// Returns the written window `&out[M - BYTE_WIDTH ..]`. If
106    /// `M > BYTE_WIDTH`, the leading bytes of `out` are left untouched
107    /// — mirror image of `to_le_bytes_fixed`, aligning the value with
108    /// the trailing window that `from_be_bytes_fixed` reads.
109    ///
110    /// ```
111    /// use fixed_bigint::FixedUInt;
112    /// type U16 = FixedUInt<u8, 2>;
113    /// let v = U16::from(0x1234u16);
114    /// let mut buf = [0u8; U16::BYTE_WIDTH];
115    /// let bytes = v.to_be_bytes_fixed(&mut buf);
116    /// assert_eq!(bytes, &[0x12, 0x34]);
117    /// ```
118    #[inline]
119    pub fn to_be_bytes_fixed<'a, const M: usize>(&self, out: &'a mut [u8; M]) -> &'a [u8] {
120        let _ = <Self as AssertBufferFits<M>>::CHECK;
121        let start = M - Self::BYTE_WIDTH;
122        // Walk words from MSB to LSB so the output is BE. Align to the
123        // trailing window so oversized buffers round-trip through
124        // `from_be_bytes_fixed`. See `to_le_bytes_fixed` for why this avoids
125        // `chunks_exact_mut`.
126        let mut dst = out[start..].iter_mut();
127        for word in self.array.iter().rev() {
128            let word_bytes = word.to_be_bytes();
129            for (&src, slot) in word_bytes.as_ref().iter().zip(dst.by_ref()) {
130                *slot = src;
131            }
132        }
133        &out[start..]
134    }
135
136    /// Deserialize from a fixed-size little-endian buffer. The const
137    /// `M >= BYTE_WIDTH` precondition fires at monomorphization. Reads
138    /// the first `BYTE_WIDTH` bytes (LE low-order bytes are at the
139    /// front); trailing bytes if `M > BYTE_WIDTH` are ignored.
140    ///
141    /// ```
142    /// use fixed_bigint::FixedUInt;
143    /// type U16 = FixedUInt<u8, 2>;
144    /// let buf = [0x34u8, 0x12];
145    /// let v = U16::from_le_bytes_fixed(&buf);
146    /// assert_eq!(v, U16::from(0x1234u16));
147    /// ```
148    #[inline]
149    pub fn from_le_bytes_fixed<const M: usize>(bytes: &[u8; M]) -> Self {
150        let _ = <Self as AssertBufferFits<M>>::CHECK;
151        // The helper takes `&[u8]` and bounds its loop by
152        // `min(bytes.len(), capacity)`; passing the full M-byte slice
153        // means `bytes.len() == M >= BYTE_WIDTH == capacity`, so the
154        // loop bound is BYTE_WIDTH and every indexed read is in range.
155        Self::from_array(impl_from_le_bytes_slice::<T, N>(bytes))
156    }
157
158    /// Deserialize from a fixed-size big-endian buffer. The const
159    /// `M >= BYTE_WIDTH` precondition fires at monomorphization. Reads
160    /// the last `BYTE_WIDTH` bytes (BE low-order bytes are at the end);
161    /// leading bytes if `M > BYTE_WIDTH` are ignored.
162    ///
163    /// ```
164    /// use fixed_bigint::FixedUInt;
165    /// type U16 = FixedUInt<u8, 2>;
166    /// let buf = [0x12u8, 0x34];
167    /// let v = U16::from_be_bytes_fixed(&buf);
168    /// assert_eq!(v, U16::from(0x1234u16));
169    /// ```
170    #[inline]
171    pub fn from_be_bytes_fixed<const M: usize>(bytes: &[u8; M]) -> Self {
172        let _ = <Self as AssertBufferFits<M>>::CHECK;
173        // The BE helper already handles the `bytes.len() > capacity`
174        // case by reading the trailing `capacity` bytes (BE low-order
175        // bytes are at the end). With M >= BYTE_WIDTH it picks the
176        // right window without our needing to compute `start` here.
177        Self::from_array(impl_from_be_bytes_slice::<T, N>(bytes))
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    type U16 = FixedUInt<u8, 2>;
186    type U32 = FixedUInt<u32, 1>; // single-limb u32 backing
187    type U64 = FixedUInt<u32, 2>; // two-limb u32 backing
188
189    // ─── to_le_bytes_fixed ────────────────────────────────────────────
190
191    #[test]
192    fn to_le_bytes_fixed_exact_size_round_trips() {
193        let v = U16::from(0x1234u16);
194        let mut buf = [0u8; U16::BYTE_WIDTH];
195        let written = v.to_le_bytes_fixed(&mut buf);
196        assert_eq!(written, &[0x34, 0x12]);
197        assert_eq!(buf, [0x34, 0x12]);
198    }
199
200    #[test]
201    fn to_le_bytes_fixed_oversized_leaves_trailing_untouched() {
202        let v = U16::from(0x1234u16);
203        let mut buf = [0xFFu8; 4];
204        let written = v.to_le_bytes_fixed(&mut buf);
205        assert_eq!(written, &[0x34, 0x12]);
206        assert_eq!(buf, [0x34, 0x12, 0xFF, 0xFF]);
207    }
208
209    #[test]
210    fn to_le_bytes_fixed_matches_slice_method() {
211        let v = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
212        let mut a = [0u8; U64::BYTE_WIDTH];
213        let mut b = [0u8; U64::BYTE_WIDTH];
214        let fixed = v.to_le_bytes_fixed(&mut a);
215        let slice = v.to_le_bytes(&mut b).unwrap();
216        assert_eq!(fixed, slice);
217    }
218
219    // ─── to_be_bytes_fixed ────────────────────────────────────────────
220
221    #[test]
222    fn to_be_bytes_fixed_exact_size_round_trips() {
223        let v = U16::from(0x1234u16);
224        let mut buf = [0u8; U16::BYTE_WIDTH];
225        let written = v.to_be_bytes_fixed(&mut buf);
226        assert_eq!(written, &[0x12, 0x34]);
227        assert_eq!(buf, [0x12, 0x34]);
228    }
229
230    #[test]
231    fn to_be_bytes_fixed_matches_slice_method() {
232        let v = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
233        let mut a = [0u8; U64::BYTE_WIDTH];
234        let mut b = [0u8; U64::BYTE_WIDTH];
235        let fixed = v.to_be_bytes_fixed(&mut a);
236        let slice = v.to_be_bytes(&mut b).unwrap();
237        assert_eq!(fixed, slice);
238    }
239
240    #[test]
241    fn to_be_bytes_fixed_oversized_writes_trailing_window() {
242        let v = U16::from(0x1234u16);
243        let mut buf = [0xFFu8; 4];
244        let written = v.to_be_bytes_fixed(&mut buf);
245        assert_eq!(written, &[0x12, 0x34]);
246        assert_eq!(buf, [0xFF, 0xFF, 0x12, 0x34]);
247    }
248
249    #[test]
250    fn to_be_fixed_from_be_fixed_round_trip_oversized() {
251        // The window `to_be_bytes_fixed` writes must match the window
252        // `from_be_bytes_fixed` reads, or oversized BE round-trips
253        // decode the untouched leading bytes.
254        let v = U16::from(0x1234u16);
255        let mut buf = [0u8; 4];
256        let _ = v.to_be_bytes_fixed(&mut buf);
257        let back: U16 = U16::from_be_bytes_fixed(&buf);
258        assert_eq!(back, v);
259    }
260
261    // ─── from_le_bytes_fixed ──────────────────────────────────────────
262
263    #[test]
264    fn from_le_bytes_fixed_exact_size() {
265        let buf = [0x34u8, 0x12];
266        let v: U16 = U16::from_le_bytes_fixed(&buf);
267        assert_eq!(v, U16::from(0x1234u16));
268    }
269
270    #[test]
271    fn from_le_bytes_fixed_oversized_takes_low_bytes() {
272        // U16 wants 2 bytes; provide 4. LE convention: take first 2.
273        let buf = [0x34u8, 0x12, 0xFF, 0xFF];
274        let v: U16 = U16::from_le_bytes_fixed(&buf);
275        assert_eq!(v, U16::from(0x1234u16));
276    }
277
278    #[test]
279    fn from_le_bytes_fixed_matches_slice_method() {
280        let buf = [0xEF, 0xBE, 0xAD, 0xDE, 0xBE, 0xBA, 0xFE, 0xCA];
281        let fixed: U64 = U64::from_le_bytes_fixed(&buf);
282        let slice: U64 = U64::from_le_bytes(&buf[..]);
283        assert_eq!(fixed, slice);
284    }
285
286    // ─── from_be_bytes_fixed ──────────────────────────────────────────
287
288    #[test]
289    fn from_be_bytes_fixed_exact_size() {
290        let buf = [0x12u8, 0x34];
291        let v: U16 = U16::from_be_bytes_fixed(&buf);
292        assert_eq!(v, U16::from(0x1234u16));
293    }
294
295    #[test]
296    fn from_be_bytes_fixed_oversized_takes_trailing_bytes() {
297        // U16 wants 2 bytes; provide 4. BE convention: take last 2.
298        let buf = [0xFFu8, 0xFF, 0x12, 0x34];
299        let v: U16 = U16::from_be_bytes_fixed(&buf);
300        assert_eq!(v, U16::from(0x1234u16));
301    }
302
303    #[test]
304    fn from_be_bytes_fixed_matches_slice_method() {
305        let buf = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
306        let fixed: U64 = U64::from_be_bytes_fixed(&buf);
307        let slice: U64 = U64::from_be_bytes(&buf[..]);
308        assert_eq!(fixed, slice);
309    }
310
311    // ─── round-trip across all four ───────────────────────────────────
312
313    #[test]
314    fn round_trip_le_fixed() {
315        let original = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
316        let mut buf = [0u8; U64::BYTE_WIDTH];
317        let _ = original.to_le_bytes_fixed(&mut buf);
318        let back: U64 = U64::from_le_bytes_fixed(&buf);
319        assert_eq!(back, original);
320    }
321
322    #[test]
323    fn round_trip_be_fixed() {
324        let original = U64::from_array([0xDEADBEEFu32, 0xCAFEBABEu32]);
325        let mut buf = [0u8; U64::BYTE_WIDTH];
326        let _ = original.to_be_bytes_fixed(&mut buf);
327        let back: U64 = U64::from_be_bytes_fixed(&buf);
328        assert_eq!(back, original);
329    }
330
331    // ─── wider carrier (sanity-check word stride math) ────────────────
332
333    #[test]
334    fn u32_single_limb_le() {
335        let v = U32::from(0x12345678u32);
336        let mut buf = [0u8; U32::BYTE_WIDTH];
337        let written = v.to_le_bytes_fixed(&mut buf);
338        assert_eq!(written, &[0x78, 0x56, 0x34, 0x12]);
339        let back: U32 = U32::from_le_bytes_fixed(&buf);
340        assert_eq!(back, v);
341    }
342
343    #[test]
344    fn u32_single_limb_be() {
345        let v = U32::from(0x12345678u32);
346        let mut buf = [0u8; U32::BYTE_WIDTH];
347        let written = v.to_be_bytes_fixed(&mut buf);
348        assert_eq!(written, &[0x12, 0x34, 0x56, 0x78]);
349        let back: U32 = U32::from_be_bytes_fixed(&buf);
350        assert_eq!(back, v);
351    }
352
353    #[test]
354    fn byte_width_is_usable_as_array_length() {
355        const BUF_LEN: usize = U64::BYTE_WIDTH;
356        let mut buf = [0u8; BUF_LEN];
357        let v = U64::from(42u32);
358        let _ = v.to_le_bytes_fixed(&mut buf);
359        assert_eq!(buf[0], 42);
360    }
361}