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}