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