• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaidokert / fixed-bigint-rs / 28763636231

06 Jul 2026 02:18AM UTC coverage: 97.47% (-0.1%) from 97.604%
28763636231

Pull #134

github

kaidokert
Address PR #134 review: zero-init BytesHolder, dedupe FromByteSlice length check

- `holder_be` / `holder_le` initialise via `BytesHolder::default()`
  (which zero-fills through the existing `ConstZero::ZERO` Default
  impl) instead of `BytesHolder::from_array(self.array)`. On the
  `&FixedUInt: ToBytes` path — the whole reason that impl exists —
  the old form read `self.array` through the reference and
  materialised a Copy of the secret limb array on the stack as the
  argument to `from_array`, then immediately overwrote it with the
  serialised bytes. That intermediate defeats the "no unwrapped
  secret on the stack" contract. Zero-init writes serialised bytes
  through the reference directly. Also drops a wasted Copy on the
  owned path.

  (gemini-code-assist inline finding, to_from_bytes.rs:197.)

- `FromByteSlice::from_be_slice` / `from_le_slice` route their
  empty + overflow length check through a shared crate-private
  `Self::check_slice_len` helper. Six lines to four, and future
  length-invariant changes only need to land in one place.

  (sourcery-ai general finding #2.)

sourcery-ai's general finding #1 — the stable and nightly
`ToBytes for &FixedUInt` impls take different code paths — is
declined for now. Stable uses `BytesHolder<T, N>` (unsafe
`from_raw_parts` reinterpretation over the limb storage) as the
associated type; nightly uses `ConstBytesHolder<{ byte_len::<T,N>() }>`
with a byte-by-byte const-eval-friendly loop. The two `Bytes`
associated types are structurally different — unifying would
require a shared abstraction that both variants can hit, more
churn than the divergence is worth on this hop. Revisit alongside
capability (c) in a follow-up.
Pull Request #134: Byte-trait alignment step 2: FromByteSlice + &FixedUInt: ToBytes

62 of 68 new or added lines in 2 files covered. (91.18%)

3197 of 3280 relevant lines covered (97.47%)

302.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.99
/src/fixeduint/to_from_bytes.rs
1
use core::borrow::{Borrow, BorrowMut};
2
use core::hash::Hash;
3

4
use super::{FixedUInt, MachineWord};
5
use crate::machineword::ConstMachineWord;
6
use const_num_traits::Personality;
7

8
// Helper, holds an owned copy of returned bytes.
9
//
10
// Not `Copy`: when the `zeroize` feature is enabled this type carries
11
// a `Drop` impl that wipes its contents, so copy semantics are not
12
// available.  Consumers that previously relied on implicit `BytesHolder:
13
// Copy` need an explicit `.clone()`.
14
#[derive(Eq, PartialEq, Clone, PartialOrd, Ord, Debug)]
15
pub struct BytesHolder<T: MachineWord, const N: usize> {
16
    array: [T; N],
17
}
18

19
c0nst::c0nst! {
20
    // `c0nst Default` so this is callable from a `const` context on
21
    // nightly (the `nightly` feature pulls in `feature(const_default)`).
22
    // On stable the c0nst macro renders the same impl as plain
23
    // `impl Default`, so downstream `BytesHolder::default()` callers see
24
    // no behavior change. Body uses the `[<T as ConstZero>::ZERO; N]`
25
    // initializer rather than `core::array::from_fn(...)` because the
26
    // closure-based helper isn't const-callable.
27
    c0nst impl<T: [c0nst] ConstMachineWord + MachineWord, const N: usize> Default for BytesHolder<T, N> {
28
        fn default() -> Self {
29
            Self::from_array([<T as const_num_traits::ConstZero>::ZERO; N])
30
        }
31
    }
32
}
11✔
33

34
impl<T: MachineWord, const N: usize> BytesHolder<T, N> {
35
    pub(crate) const fn from_array(array: [T; N]) -> Self {
11✔
36
        Self { array }
11✔
37
    }
11✔
38
    // Converts internal storage to a mutable byte slice
39
    fn as_byte_slice_mut(&mut self) -> &mut [u8] {
26✔
40
        // SAFETY: This is safe because the size of the array is the same as the size of the slice
41
        unsafe {
26✔
42
            core::slice::from_raw_parts_mut(
26✔
43
                &mut self.array as *mut T as *mut u8,
26✔
44
                N * core::mem::size_of::<T>(),
26✔
45
            )
26✔
46
        }
26✔
47
    }
26✔
48
    fn as_byte_slice(&self) -> &[u8] {
14✔
49
        // SAFETY: This is safe because the size of the array is the same as the size of the slice
50
        unsafe {
51
            core::slice::from_raw_parts(
14✔
52
                &self.array as *const T as *const u8,
14✔
53
                N * core::mem::size_of::<T>(),
14✔
54
            )
14✔
55
        }
56
    }
14✔
57
}
58

59
impl<T: MachineWord, const N: usize> Borrow<[u8]> for BytesHolder<T, N> {
60
    fn borrow(&self) -> &[u8] {
×
61
        self.as_byte_slice()
×
62
    }
×
63
}
64
impl<T: MachineWord, const N: usize> BorrowMut<[u8]> for BytesHolder<T, N> {
65
    fn borrow_mut(&mut self) -> &mut [u8] {
×
66
        self.as_byte_slice_mut()
×
67
    }
×
68
}
69
impl<T: MachineWord, const N: usize> AsRef<[u8]> for BytesHolder<T, N> {
70
    fn as_ref(&self) -> &[u8] {
14✔
71
        self.as_byte_slice()
14✔
72
    }
14✔
73
}
74
impl<T: MachineWord, const N: usize> AsMut<[u8]> for BytesHolder<T, N> {
75
    fn as_mut(&mut self) -> &mut [u8] {
7✔
76
        self.as_byte_slice_mut()
7✔
77
    }
7✔
78
}
79
impl<T: MachineWord, const N: usize> Hash for BytesHolder<T, N> {
80
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
×
81
        self.as_byte_slice().hash(state)
×
82
    }
×
83
}
84

85
#[cfg(feature = "zeroize")]
86
impl<T: MachineWord, const N: usize> zeroize::Zeroize for BytesHolder<T, N> {
87
    fn zeroize(&mut self) {
12✔
88
        // Wipe via the byte view so the guarantee covers the actual
89
        // memory representation, not just per-word `T::zeroize` (which
90
        // could be sub-word-granular for composite limb types).
91
        self.as_byte_slice_mut().zeroize();
12✔
92
    }
12✔
93
}
94

95
#[cfg(feature = "zeroize")]
96
impl<T: MachineWord, const N: usize> zeroize::ZeroizeOnDrop for BytesHolder<T, N> {}
97

98
#[cfg(feature = "zeroize")]
99
impl<T: MachineWord, const N: usize> Drop for BytesHolder<T, N> {
100
    fn drop(&mut self) {
11✔
101
        use zeroize::Zeroize;
102
        self.zeroize();
11✔
103
    }
11✔
104
}
105

106
// ── num_traits + const_num_traits ToBytes/FromBytes ──────────────────
107
//
108
// The two `ToBytes` traits differ only in receiver (`&self` vs `self`)
109
// and the two `FromBytes` traits do the same. Delegate through
110
// crate-private helpers on `FixedUInt` to keep them in lockstep. Both
111
// use `BytesHolder<T, N>` as the associated `Bytes` type; the
112
// const-num-traits variants are `#[cfg(not(feature = "nightly"))]`
113
// because `const_to_from_bytes.rs` provides better nightly impls via
114
// `ConstBytesHolder` + `generic_const_exprs`.
115

116
#[cfg(any(feature = "num-traits", not(feature = "nightly")))]
117
impl<T: MachineWord, const N: usize, P: Personality> FixedUInt<T, N, P>
118
where
119
    T: core::fmt::Debug,
120
{
121
    // Zero-init (`BytesHolder::default()`) rather than
122
    // `BytesHolder::from_array(self.array)`. On the `&FixedUInt` path
123
    // the latter would materialise the secret limb array on the stack
124
    // as a Copy of the referenced `[T; N]` before the serialisation
125
    // step overwrites it — defeating the CT/Zeroize goal of the
126
    // by-reference impl. Zero-init writes serialised bytes through
127
    // the reference directly. Also removes a wasted Copy on the
128
    // owned path.
129
    #[inline]
130
    fn holder_be(&self) -> BytesHolder<T, N> {
4✔
131
        let mut ret = BytesHolder::default();
4✔
132
        let _ = self.to_be_bytes(ret.as_byte_slice_mut());
4✔
133
        ret
4✔
134
    }
4✔
135

136
    #[inline]
137
    fn holder_le(&self) -> BytesHolder<T, N> {
3✔
138
        let mut ret = BytesHolder::default();
3✔
139
        let _ = self.to_le_bytes(ret.as_byte_slice_mut());
3✔
140
        ret
3✔
141
    }
3✔
142
}
143

144
#[cfg(feature = "num-traits")]
145
impl<T: MachineWord, const N: usize, P: Personality> num_traits::ToBytes for FixedUInt<T, N, P>
146
where
147
    T: core::fmt::Debug,
148
{
149
    type Bytes = BytesHolder<T, N>;
150
    fn to_be_bytes(&self) -> Self::Bytes {
4✔
151
        self.holder_be()
4✔
152
    }
4✔
153
    fn to_le_bytes(&self) -> Self::Bytes {
3✔
154
        self.holder_le()
3✔
155
    }
3✔
156
}
157

158
#[cfg(feature = "num-traits")]
159
impl<T: MachineWord, const N: usize, P: Personality> num_traits::FromBytes for FixedUInt<T, N, P>
160
where
161
    T: core::fmt::Debug,
162
{
163
    type Bytes = BytesHolder<T, N>;
164
    fn from_be_bytes(bytes: &Self::Bytes) -> Self {
3✔
165
        Self::from_be_bytes(bytes.as_ref())
3✔
166
    }
3✔
167
    fn from_le_bytes(bytes: &Self::Bytes) -> Self {
3✔
168
        Self::from_le_bytes(bytes.as_ref())
3✔
169
    }
3✔
170
}
171

172
#[cfg(not(feature = "nightly"))]
173
impl<T: MachineWord, const N: usize, P: Personality> const_num_traits::ToBytes
174
    for FixedUInt<T, N, P>
175
where
176
    T: core::fmt::Debug,
177
{
178
    type Bytes = BytesHolder<T, N>;
179
    fn to_be_bytes(self) -> Self::Bytes {
×
180
        self.holder_be()
×
181
    }
×
182
    fn to_le_bytes(self) -> Self::Bytes {
×
183
        self.holder_le()
×
184
    }
×
185
}
186

187
// `ToBytes for &FixedUInt` — needed by CT nonce paths where the value
188
// lives behind `Zeroizing<T>` and `(*r).to_le_bytes()` would deref-copy
189
// an unwrapped secret onto the stack. `<&T as ToBytes>::to_le_bytes(&*r)`
190
// reads through the reference; only the output `BytesHolder` is
191
// unwrapped material and the caller can wrap that (or rely on the
192
// crate-side `ZeroizeOnDrop` when the feature is on).
193
#[cfg(not(feature = "nightly"))]
194
impl<T: MachineWord, const N: usize, P: Personality> const_num_traits::ToBytes
195
    for &FixedUInt<T, N, P>
196
where
197
    T: core::fmt::Debug,
198
{
199
    type Bytes = BytesHolder<T, N>;
NEW
200
    fn to_be_bytes(self) -> Self::Bytes {
×
NEW
201
        self.holder_be()
×
NEW
202
    }
×
NEW
203
    fn to_le_bytes(self) -> Self::Bytes {
×
NEW
204
        self.holder_le()
×
NEW
205
    }
×
206
}
207

208
#[cfg(not(feature = "nightly"))]
209
impl<T: MachineWord, const N: usize, P: Personality> const_num_traits::FromBytes
210
    for FixedUInt<T, N, P>
211
where
212
    T: core::fmt::Debug,
213
{
214
    type Bytes = BytesHolder<T, N>;
215

216
    fn from_be_bytes(bytes: &Self::Bytes) -> Self {
×
217
        Self::from_be_bytes(bytes.as_ref())
×
218
    }
×
219

220
    fn from_le_bytes(bytes: &Self::Bytes) -> Self {
×
221
        Self::from_le_bytes(bytes.as_ref())
×
222
    }
×
223
}
224

225
#[cfg(test)]
226
#[cfg(feature = "num-traits")]
227
mod tests {
228
    use super::*;
229
    use num_traits::FromPrimitive;
230

231
    fn test_helper<T: num_traits::ToBytes>(input: &T, expected_be: &[u8]) {
5✔
232
        let mut buffer = [0u8; 256];
5✔
233
        buffer[..expected_be.len()].copy_from_slice(expected_be);
5✔
234
        let expected_le = &mut buffer[..expected_be.len()];
5✔
235
        expected_le.reverse();
5✔
236

237
        let out = input.to_be_bytes();
5✔
238
        assert_eq!(out.as_ref(), expected_be);
5✔
239
        let out = input.to_le_bytes();
5✔
240
        assert_eq!(out.as_ref(), expected_le);
5✔
241
    }
5✔
242

243
    #[test]
244
    fn test_to_bytes() {
1✔
245
        test_helper(&0xAB_u8, &[0xAB_u8]);
1✔
246
        test_helper(&0xABCD_u16, &[0xAB, 0xCD]);
1✔
247
        test_helper(
1✔
248
            &FixedUInt::<u8, 4>::from_u32(0x12345678).unwrap(),
1✔
249
            &[0x12, 0x34, 0x56, 0x78],
1✔
250
        );
251
        test_helper(
1✔
252
            &FixedUInt::<u16, 2>::from_u32(0x12345678).unwrap(),
1✔
253
            &[0x12, 0x34, 0x56, 0x78],
1✔
254
        );
255
        test_helper(
1✔
256
            &FixedUInt::<u32, 1>::from_u32(0x12345678).unwrap(),
1✔
257
            &[0x12, 0x34, 0x56, 0x78],
1✔
258
        );
259
    }
1✔
260

261
    fn from_helper<T>(input: &[u8], expected: T)
6✔
262
    where
6✔
263
        T: num_traits::FromBytes + core::fmt::Debug + core::cmp::PartialEq,
6✔
264
        T::Bytes: num_traits::ops::bytes::NumBytes + Default + core::fmt::Debug,
6✔
265
    {
266
        let mut bytes = T::Bytes::default();
6✔
267
        bytes.as_mut().copy_from_slice(input);
6✔
268
        let result = T::from_be_bytes(&bytes);
6✔
269
        assert_eq!(result, expected);
6✔
270
        bytes.as_mut().reverse();
6✔
271
        let result = T::from_le_bytes(&bytes);
6✔
272
        assert_eq!(result, expected);
6✔
273
    }
6✔
274

275
    #[cfg(feature = "zeroize")]
276
    #[test]
277
    fn test_zeroize_wipes_byte_view() {
1✔
278
        use zeroize::Zeroize;
279

280
        let value = FixedUInt::<u32, 4>::from_u32(0xDEAD_BEEF).unwrap();
1✔
281
        let mut bytes = <FixedUInt<u32, 4> as num_traits::ToBytes>::to_be_bytes(&value);
1✔
282
        // Seed every byte before the wipe — `from_u32` only fills the
283
        // low limb, leaving most of the 16-byte holder already zero.
284
        // A buggy `zeroize` that wiped only the populated bytes would
285
        // pass an `all-zero` check without the seed.
286
        for (index, byte) in bytes.as_mut().iter_mut().enumerate() {
16✔
287
            *byte = (index as u8).wrapping_add(1);
16✔
288
        }
16✔
289
        assert!(bytes.as_ref().iter().all(|b| *b != 0));
16✔
290
        bytes.zeroize();
1✔
291
        assert!(bytes.as_ref().iter().all(|b| *b == 0));
16✔
292
    }
1✔
293

294
    #[test]
295
    fn test_from_bytes() {
1✔
296
        from_helper(&[0xAB_u8], 0xAB_u8);
1✔
297
        from_helper(&[0xAB, 0xCD], 0xABCD_u16);
1✔
298
        from_helper(&[0x12, 0x34, 0x56, 0x78], 0x12345678_u32);
1✔
299
        from_helper(
1✔
300
            &[0x12, 0x34, 0x56, 0x78],
1✔
301
            FixedUInt::<u8, 4>::from_u32(0x12345678).unwrap(),
1✔
302
        );
303
        from_helper(
1✔
304
            &[0x12, 0x34, 0x56, 0x78],
1✔
305
            FixedUInt::<u16, 2>::from_u32(0x12345678).unwrap(),
1✔
306
        );
307
        from_helper(
1✔
308
            &[0x12, 0x34, 0x56, 0x78],
1✔
309
            FixedUInt::<u32, 1>::from_u32(0x12345678).unwrap(),
1✔
310
        );
311
    }
1✔
312

313
    #[test]
314
    fn nightly_const_eval_default() {
1✔
315
        let h: BytesHolder<u8, 4> = Default::default();
1✔
316
        assert_eq!(h.array, [0u8; 4]);
1✔
317

318
        #[cfg(feature = "nightly")]
319
        {
320
            const DEFAULT_U8X4: BytesHolder<u8, 4> = Default::default();
321
            const DEFAULT_U16X2: BytesHolder<u16, 2> = Default::default();
322
            const DEFAULT_U32X1: BytesHolder<u32, 1> = Default::default();
323
            assert_eq!(DEFAULT_U8X4.array, [0u8; 4]);
324
            assert_eq!(DEFAULT_U16X2.array, [0u16; 2]);
325
            assert_eq!(DEFAULT_U32X1.array, [0u32; 1]);
326
        }
327
    }
1✔
328
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc