Skip to main content

tuwunel_core/utils/
math.rs

1//! Arithmetic checking and numeric conversion helpers.
2//!
3//! Exported macros separate recoverable, expected, and prevalidated arithmetic.
4//! Conversion helpers centralize errors, panics, and deliberate truncation.
5
6use std::{
7	num::NonZeroUsize,
8	sync::atomic::{AtomicU64, Ordering},
9};
10
11use ruma::UInt;
12
13mod expect_into;
14mod expected;
15#[cfg(test)]
16mod tests;
17mod tried;
18
19/// Transforms arithmetic expressions into checked operations.
20///
21/// Successful evaluation yields [`Some`], while a failed operation yields
22/// [`None`]. The [`crate::checked!`] macro converts that optional result into
23/// crate error handling.
24pub use checked_ops::checked_ops;
25
26/// Converts values with [`TryFrom`] and panics on failure.
27///
28/// The conversion delegates to [`expect_into`] and panics on failure. Its
29/// destination type can be inferred from the call context.
30pub use self::expect_into::ExpectInto;
31/// Adds checked arithmetic methods that panic on failure.
32///
33/// Each operation panics when its underlying checked operation fails. The trait
34/// covers addition, subtraction, multiplication, division, and remainder.
35pub use self::expected::Expected;
36/// Adds checked arithmetic methods that return a [`Result`].
37///
38/// Each operation returns [`Error::Arithmetic`] when its checked operation
39/// fails. The trait covers addition, subtraction, multiplication, division, and
40/// remainder.
41pub use self::tried::Tried;
42use crate::{Err, Error, Result, debug::type_name, err};
43
44#[expect(
45	clippy::lossy_float_literal,
46	reason = "2^64 is exactly representable"
47)]
48const USIZE_MAX_EXCLUSIVE: f64 = match usize::BITS {
49	| 16 => 65_536.0,
50	| 32 => 4_294_967_296.0,
51	| 64 => 18_446_744_073_709_551_616.0,
52	| _ => panic!("unsupported usize width"),
53};
54
55/// Combines per-call and configuration concurrency caps.
56///
57/// An absent per-call cap and a zero configuration cap each mean unbounded.
58/// When both sides are bounded, the lower cap wins.
59#[inline]
60#[must_use]
61pub fn effective_cap(requested: Option<NonZeroUsize>, config: usize) -> usize {
62	requested
63		.map_or(usize::MAX, NonZeroUsize::get)
64		.min(NonZeroUsize::new(config).map_or(usize::MAX, NonZeroUsize::get))
65}
66
67/// Evaluates a checked arithmetic expression as a [`Result`].
68///
69/// A successful expression returns its value. Overflow or another invalid
70/// operation returns [`Error::Arithmetic`] through a cold error path.
71#[macro_export]
72#[collapse_debuginfo(yes)]
73macro_rules! checked {
74	($($input:tt)+) => {
75		$crate::utils::math::checked_ops!($($input)+)
76			.ok_or_else(
77				// The compiler will now attempt to inline the math predicate
78				// while moving the error handling out to .text.unlikely.
79				#[cold]
80				|| $crate::err!(Arithmetic("operation overflowed or result invalid"))
81			)
82	};
83}
84
85/// Evaluates a checked arithmetic expression and panics on failure.
86///
87/// Use this when failure is not realistically expected but the expression does
88/// not meet the safety bar for `validated!`. The first form accepts a custom
89/// panic message; the second uses a default.
90#[macro_export]
91#[collapse_debuginfo(yes)]
92macro_rules! expected {
93	($msg:literal, $($input:tt)+) => {
94		$crate::checked!($($input)+).expect($msg)
95	};
96
97	($($input:tt)+) => {
98		$crate::expected!("arithmetic expression expectation failure", $($input)+)
99	};
100}
101
102/// Evaluates arithmetic with checks enabled only in debug builds.
103///
104/// Debug builds use checked operations and panic when the expression overflows
105/// or is otherwise invalid. Release builds evaluate the expression directly,
106/// so callers must ensure every operation is valid.
107#[cfg(not(debug_assertions))]
108#[macro_export]
109#[collapse_debuginfo(yes)]
110macro_rules! validated {
111	($($input:tt)+) => {
112		{
113			// TODO rewrite when stmt_expr_attributes is stable
114			#[expect(clippy::arithmetic_side_effects)]
115			let __res = ($($input)+);
116			__res
117		}
118	};
119}
120
121/// Evaluates arithmetic with checks enabled only in debug builds.
122///
123/// Debug builds use checked operations and panic when the expression overflows
124/// or is otherwise invalid. Release builds evaluate the expression directly,
125/// so callers must ensure every operation is valid.
126#[cfg(debug_assertions)]
127#[macro_export]
128#[collapse_debuginfo(yes)]
129macro_rules! validated {
130	($($input:tt)+) => {
131		$crate::expected!("validated arithmetic expression failed", $($input)+)
132	}
133}
134
135/// Converts a representable nonnegative `f64` to `usize` by truncating toward
136/// zero.
137///
138/// Negative, non-finite, and out-of-range values return [`Error::Arithmetic`].
139/// Negative zero is accepted; valid fractional values are truncated toward
140/// zero.
141#[inline]
142pub fn usize_from_f64(val: f64) -> Result<usize, Error> {
143	if !(0.0..USIZE_MAX_EXCLUSIVE).contains(&val) {
144		return Err!(Arithmetic("Float is not representable as usize"));
145	}
146
147	// SAFETY: The range check proves `val` is finite, nonnegative, and
148	// representable after truncation.
149	Ok(unsafe { val.to_int_unchecked::<usize>() })
150}
151
152/// Converts a Matrix unsigned integer to `usize`.
153///
154/// The conversion is exact. It panics if the value exceeds the platform's
155/// `usize` range.
156#[inline]
157#[must_use]
158pub fn usize_from_ruma(val: UInt) -> usize {
159	usize::try_from(val).expect("failed conversion from ruma::UInt to usize")
160}
161
162/// Converts a Matrix unsigned integer to a bounded `usize`.
163///
164/// Conversion failure uses `fallback`. The result is limited to `max` after
165/// either conversion path.
166#[inline]
167#[must_use]
168pub fn usize_from_ruma_bounded(val: UInt, fallback: usize, max: usize) -> usize {
169	usize::try_from(val).unwrap_or(fallback).min(max)
170}
171
172/// Converts a `u64` to a Matrix unsigned integer.
173///
174/// The conversion is exact. It panics if the value exceeds the range supported
175/// by [`UInt`].
176#[inline]
177#[must_use]
178pub fn ruma_from_u64(val: u64) -> UInt {
179	UInt::try_from(val).expect("failed conversion from u64 to ruma::UInt")
180}
181
182/// Converts a `usize` to a Matrix unsigned integer.
183///
184/// The conversion is exact. It panics if the value exceeds the range supported
185/// by [`UInt`].
186#[inline]
187#[must_use]
188pub fn ruma_from_usize(val: usize) -> UInt {
189	UInt::try_from(val).expect("failed conversion from usize to ruma::UInt")
190}
191
192/// Converts a `usize` to a Matrix unsigned integer, saturating at the largest
193/// value [`UInt`] supports.
194///
195/// Suits a wire limit taken from a local count, where an oversized value means
196/// as many as the protocol allows rather than an error.
197#[inline]
198#[must_use]
199pub fn ruma_from_usize_saturating(val: usize) -> UInt {
200	UInt::new_saturating(u64_from_usize_saturating(val))
201}
202
203/// Converts a `u64` to `usize` with deliberate truncation when necessary.
204///
205/// Targets with a narrower `usize` discard the high bits. The conversion is
206/// exact when `usize` is at least 64 bits wide.
207#[inline]
208#[must_use]
209#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
210pub fn usize_from_u64_truncated(val: u64) -> usize { val as usize }
211
212/// Converts a `usize` to `u64`, saturating at `u64::MAX`.
213///
214/// The conversion is exact wherever `usize` is at most 64 bits wide.
215#[inline]
216#[must_use]
217pub fn u64_from_usize_saturating(val: usize) -> u64 { val.try_into().unwrap_or(u64::MAX) }
218
219/// Converts a `u128` to `u64`, saturating at `u64::MAX`.
220///
221/// Suits whole-unit `Duration` reads such as `as_millis()`, where clamping an
222/// out-of-range value beats failing on it.
223#[inline]
224#[must_use]
225pub fn u64_from_u128_saturating(val: u128) -> u64 { val.try_into().unwrap_or(u64::MAX) }
226
227/// Adds a `usize` count to an atomic `u64` counter, saturating the count at
228/// `u64::MAX`.
229///
230/// Returns the previous value; the counter itself wraps as `fetch_add` does.
231#[inline]
232pub fn fetch_add_usize(counter: &AtomicU64, count: usize, order: Ordering) -> u64 {
233	counter.fetch_add(u64_from_usize_saturating(count), order)
234}
235
236/// Converts a value with [`TryFrom`] and panics if conversion fails.
237///
238/// Successful conversions return the destination value. A failed conversion
239/// terminates with a fixed expectation message.
240#[inline]
241pub fn expect_into<Dst: TryFrom<Src>, Src>(src: Src) -> Dst {
242	try_into(src).expect("failed conversion from Src to Dst")
243}
244
245/// Converts a value with [`TryFrom`] and maps failure to an arithmetic error.
246///
247/// Successful conversions return the destination value unchanged. A failure
248/// records the source and destination type names and discards the original
249/// error.
250#[inline]
251pub fn try_into<Dst: TryFrom<Src>, Src>(src: Src) -> Result<Dst> {
252	Dst::try_from(src).map_err(|_| {
253		err!(Arithmetic(
254			"failed to convert from {} to {}",
255			type_name::<Src>(),
256			type_name::<Dst>()
257		))
258	})
259}