Skip to main content

xdrs/cxs/
max_size.rs

1//! Worst-case CXS size bounds.
2//!
3//! [`ConstMaxSize`] reports, per type, the worst-case CXS encoded size over
4//! all values of the type as an associated const — usable in const contexts
5//! (e.g. sizing a static buffer) without a value in hand.
6//!
7//! Bounds fold through by-value layout only; boundedness does not cross
8//! pointers. The blanket `&T` impl is a literal `None` that never names the
9//! pointee's bound, and the `Box<T>` and element-generic `VecM<T, MAX>`
10//! impls are likewise literal `None` (heap-backed indirection). This is what
11//! makes const-evaluation cycles impossible for recursive types: recursion
12//! must pass through indirection, and every indirection bounds to `None`
13//! without evaluating anything further.
14//!
15//! `u8` deliberately does not implement [`ConstMaxSize`]: a lone `u8` is not
16//! XDR-encodable, and its absence is what keeps the `[u8; N]`/`[T; N]` and
17//! `VecM<u8, MAX>`/`VecM<T, MAX>` impl pairs disjoint.
18
19use super::const_writer::{
20    max_size_add, max_size_array, max_size_bool, max_size_f32, max_size_f64, max_size_i16,
21    max_size_i32, max_size_i64, max_size_max, max_size_opaque_fixed, max_size_opaque_variable,
22    max_size_u16, max_size_u32, max_size_u64, size_i32,
23};
24#[cfg(feature = "int128")]
25use super::const_writer::{max_size_i128, max_size_u128};
26use core::convert::Infallible;
27use core::marker::PhantomData;
28use xdr_common::{BytesM, StaticIfNoAlloc, StringM, VecM};
29
30/// Worst-case CXS encoded size over all values of a type.
31///
32/// `MAX_SIZE` is `Some(bound)` when a finite bound exists, or `None` when
33/// the type is unbounded, reached through indirection (`&T`, `Box<T>`,
34/// `VecM<T, MAX>`), or its bound overflows `usize` on this target.
35///
36/// Implemented for user types via `#[derive(ConstXDRSize)]`; implement
37/// manually for foreign types.
38pub trait ConstMaxSize {
39    /// Worst-case CXS encoded size in bytes, or `None` if unbounded.
40    const MAX_SIZE: Option<usize>;
41}
42
43macro_rules! impl_primitive {
44    ($($ty:ty => $max_fn:ident),* $(,)?) => {$(
45        impl ConstMaxSize for $ty {
46            const MAX_SIZE: Option<usize> = Some($max_fn());
47        }
48    )*};
49}
50
51impl_primitive! {
52    i16 => max_size_i16,
53    u16 => max_size_u16,
54    i32 => max_size_i32,
55    u32 => max_size_u32,
56    i64 => max_size_i64,
57    u64 => max_size_u64,
58    bool => max_size_bool,
59    f32 => max_size_f32,
60    f64 => max_size_f64,
61}
62
63#[cfg(feature = "int128")]
64impl_primitive! {
65    i128 => max_size_i128,
66    u128 => max_size_u128,
67}
68
69impl<const N: usize> ConstMaxSize for [u8; N] {
70    const MAX_SIZE: Option<usize> = max_size_opaque_fixed(N);
71}
72
73impl<T: ConstMaxSize, const N: usize> ConstMaxSize for [T; N] {
74    const MAX_SIZE: Option<usize> = max_size_array(N, T::MAX_SIZE);
75}
76
77// Pointer cut: boundedness does not cross a reference. Literal `None`, no
78// bound on `T`, so the pointee's bound is never named (or evaluated).
79impl<T: ?Sized> ConstMaxSize for &T {
80    const MAX_SIZE: Option<usize> = None;
81}
82
83// Pointer cut: an owned heap `Box` is indirection — a cycle edge for
84// recursive types, even though it XDR-encodes transparently as its pointee.
85// Literal `None`, no bound on `T`, so the pointee's bound is never named (or
86// evaluated). In no-`alloc` builds `Box<T>` is an inline newtype around `T`
87// (see below) and takes `T`'s bound; this covers the `alloc` heap `Box`.
88#[cfg(feature = "alloc")]
89impl<T: ?Sized> ConstMaxSize for alloc::boxed::Box<T> {
90    const MAX_SIZE: Option<usize> = None;
91}
92
93// No-`alloc` `Box<T>` holds `T` inline: not a pointer, so it takes `T`'s
94// bound rather than cutting the fold.
95#[cfg(not(feature = "alloc"))]
96impl<T: ConstMaxSize> ConstMaxSize for xdr_common::Box<T> {
97    const MAX_SIZE: Option<usize> = T::MAX_SIZE;
98}
99
100impl<T: ConstMaxSize> ConstMaxSize for Option<T> {
101    const MAX_SIZE: Option<usize> = max_size_add(Some(size_i32(1)), T::MAX_SIZE);
102}
103
104impl<T: ConstMaxSize, E: ConstMaxSize> ConstMaxSize for Result<T, E> {
105    const MAX_SIZE: Option<usize> =
106        max_size_add(Some(size_i32(1)), max_size_max(T::MAX_SIZE, E::MAX_SIZE));
107}
108
109impl<T> ConstMaxSize for PhantomData<T> {
110    const MAX_SIZE: Option<usize> = Some(0);
111}
112
113impl ConstMaxSize for Infallible {
114    const MAX_SIZE: Option<usize> = Some(0);
115}
116
117impl ConstMaxSize for () {
118    const MAX_SIZE: Option<usize> = Some(0);
119}
120
121macro_rules! impl_tuple {
122    ($($t:ident),+) => {
123        impl<$($t: ConstMaxSize),+> ConstMaxSize for ($($t,)+) {
124            const MAX_SIZE: Option<usize> = {
125                let mut total: Option<usize> = Some(0);
126                $( total = max_size_add(total, $t::MAX_SIZE); )+
127                total
128            };
129        }
130    };
131}
132
133impl_tuple!(A);
134impl_tuple!(A, B);
135impl_tuple!(A, B, C);
136impl_tuple!(A, B, C, D);
137impl_tuple!(A, B, C, D, E);
138impl_tuple!(A, B, C, D, E, F);
139impl_tuple!(A, B, C, D, E, F, G);
140impl_tuple!(A, B, C, D, E, F, G, H);
141impl_tuple!(A, B, C, D, E, F, G, H, I);
142impl_tuple!(A, B, C, D, E, F, G, H, I, J);
143impl_tuple!(A, B, C, D, E, F, G, H, I, J, K);
144impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
145
146impl<const MAX: u32> ConstMaxSize for BytesM<MAX> {
147    const MAX_SIZE: Option<usize> = max_size_opaque_variable(MAX);
148}
149
150impl<const MAX: u32> ConstMaxSize for StringM<MAX> {
151    const MAX_SIZE: Option<usize> = max_size_opaque_variable(MAX);
152}
153
154impl<const MAX: u32> ConstMaxSize for VecM<u8, MAX> {
155    const MAX_SIZE: Option<usize> = max_size_opaque_variable(MAX);
156}
157
158// Pointer cut: an element-generic `VecM` is heap-backed indirection — a
159// cycle edge for recursive types. Literal `None`; the `T: ConstMaxSize`
160// bound exists only to keep this impl disjoint from `VecM<u8, MAX>`, and
161// `StaticIfNoAlloc` mirrors the bound on `VecM` itself.
162impl<T: ConstMaxSize + StaticIfNoAlloc, const MAX: u32> ConstMaxSize for VecM<T, MAX> {
163    const MAX_SIZE: Option<usize> = None;
164}
165
166#[cfg(test)]
167mod tests {
168    use super::ConstMaxSize;
169    use xdr_common::{BytesM, StringM, VecM};
170
171    const fn bound<T: ConstMaxSize>() -> Option<usize> {
172        T::MAX_SIZE
173    }
174
175    // Primitives: worst-case CXS widths.
176    const _: () = assert!(matches!(bound::<u32>(), Some(5)));
177    const _: () = assert!(matches!(bound::<i64>(), Some(9)));
178    const _: () = assert!(matches!(bound::<bool>(), Some(1)));
179    const _: () = assert!(matches!(bound::<f32>(), Some(4)));
180    const _: () = assert!(matches!(bound::<u16>(), Some(3)));
181
182    // Fixed opaque bytes: no padding in CXS.
183    const _: () = assert!(matches!(bound::<[u8; 5]>(), Some(5)));
184    // Element arrays multiply the element bound.
185    const _: () = assert!(matches!(bound::<[u32; 3]>(), Some(15)));
186
187    // Pointer cuts: all references bound to None.
188    const _: () = assert!(bound::<&'static u32>().is_none());
189    const _: () = assert!(bound::<&'static [u8]>().is_none());
190    const _: () = assert!(bound::<&'static str>().is_none());
191    const _: () = assert!(bound::<&'static [u32]>().is_none());
192
193    // Owned heap `Box` is also a pointer cut (alloc only; no-alloc `Box`
194    // holds `T` inline and is not a pointer). None propagates through an
195    // enclosing fold.
196    #[cfg(feature = "alloc")]
197    const _: () = assert!(bound::<alloc::boxed::Box<u32>>().is_none());
198    #[cfg(feature = "alloc")]
199    const _: () = assert!(bound::<alloc::boxed::Box<[u8; 4]>>().is_none());
200    #[cfg(feature = "alloc")]
201    const _: () = assert!(bound::<Option<alloc::boxed::Box<u32>>>().is_none());
202
203    // A type whose recursion passes through `Box` folds to `None` with no
204    // const-eval cycle — the property the literal `None` protects. Mirrors
205    // what `#[derive(ConstXDRSize)]` emits for `struct Rec(Option<Box<Rec>>)`.
206    #[cfg(feature = "alloc")]
207    struct Rec;
208    #[cfg(feature = "alloc")]
209    impl ConstMaxSize for Rec {
210        const MAX_SIZE: Option<usize> = <Option<alloc::boxed::Box<Rec>> as ConstMaxSize>::MAX_SIZE;
211    }
212    #[cfg(feature = "alloc")]
213    const _: () = assert!(bound::<Rec>().is_none());
214
215    // Option adds the worst-case discriminant; None propagates.
216    const _: () = assert!(matches!(bound::<Option<u32>>(), Some(6)));
217    const _: () = assert!(bound::<Option<&'static u32>>().is_none());
218
219    // Result takes the max arm plus discriminant.
220    const _: () = assert!(matches!(bound::<Result<u32, i64>>(), Some(10)));
221
222    // Tuples fold element bounds. Unit is zero.
223    const _: () = assert!(matches!(bound::<(u16, i16)>(), Some(6)));
224    const _: () = assert!(matches!(bound::<()>(), Some(0)));
225
226    // Zero-sized markers.
227    const _: () = assert!(matches!(bound::<core::marker::PhantomData<u64>>(), Some(0)));
228    const _: () = assert!(matches!(bound::<core::convert::Infallible>(), Some(0)));
229
230    // A zero-length array of unbounded elements still encodes zero bytes.
231    const _: () = assert!(matches!(bound::<[&'static [u8]; 0]>(), Some(0)));
232
233    // Bounded byte containers: exact. size_u32(16) == 1 in CXS.
234    const _: () = assert!(matches!(bound::<BytesM<16>>(), Some(17)));
235    const _: () = assert!(matches!(bound::<StringM<8>>(), Some(9)));
236    const _: () = assert!(matches!(bound::<VecM<u8, 4>>(), Some(5)));
237
238    // Element-generic VecM is heap indirection: literal None even with MAX.
239    const _: () = assert!(bound::<VecM<u32, 5>>().is_none());
240
241    // Unspelled MAX defaults to u32::MAX at the type level.
242    #[cfg(target_pointer_width = "64")]
243    const _: () = assert!(matches!(
244        bound::<BytesM>(),
245        Some(n) if n == 5 + u32::MAX as usize
246    ));
247
248    #[cfg(feature = "int128")]
249    const _: () = assert!(matches!(bound::<i128>(), Some(18)));
250    #[cfg(feature = "int128")]
251    const _: () = assert!(matches!(bound::<u128>(), Some(18)));
252
253    #[test]
254    fn bounds_are_stable() {
255        // Const asserts above are the real test; exercise at runtime too.
256        assert_eq!(bound::<u32>(), Some(5));
257        assert_eq!(bound::<Option<u32>>(), Some(6));
258        assert_eq!(bound::<VecM<u32, 5>>(), None);
259    }
260}