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}