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}