commit 4d236d40358ba7b311dbb5353f7eb74e5b38221a
parent cd5822f712cf475b5365dbe76a885f40d036bec0
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 07:05:21 +0000
app-utils: add interval range helper
- add num_interval_range random helper
- use secure randomness across targets
- return invalid input for bad ranges
- add unit tests for range bounds
Diffstat:
4 files changed, 75 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1565,7 +1565,10 @@ dependencies = [
name = "radroots-app-utils"
version = "0.1.0"
dependencies = [
+ "getrandom 0.2.17",
+ "js-sys",
"radroots-types",
+ "web-sys",
]
[[package]]
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
@@ -11,3 +11,6 @@ crate-type = ["rlib"]
[dependencies]
radroots-types = { workspace = true }
+getrandom = { workspace = true }
+js-sys = { workspace = true }
+web-sys = { workspace = true }
diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
@@ -9,7 +9,7 @@ pub mod time;
pub mod types;
pub use errors::{err_msg, handle_err, throw_err, ERR_PREFIX_APP, ERR_PREFIX_UTILS};
-pub use numbers::{num_str, parse_float, parse_int};
+pub use numbers::{num_interval_range, num_str, parse_float, parse_int};
pub use path::{
parse_route_path,
resolve_route_path,
diff --git a/crates/utils/src/numbers/mod.rs b/crates/utils/src/numbers/mod.rs
@@ -1,5 +1,7 @@
#![forbid(unsafe_code)]
+use crate::error::RadrootsAppUtilsError;
+
pub fn parse_int(value: &str, fallback: i64) -> i64 {
value.trim().parse::<i64>().unwrap_or(fallback)
}
@@ -12,9 +14,56 @@ pub fn num_str<T: ToString>(value: T) -> String {
value.to_string()
}
+pub fn num_interval_range(min: i64, max: i64) -> Result<i64, RadrootsAppUtilsError> {
+ if min > max {
+ return Err(RadrootsAppUtilsError::InvalidInput);
+ }
+ if min == max {
+ return Ok(min);
+ }
+ let min_i128 = i128::from(min);
+ let max_i128 = i128::from(max);
+ let range = max_i128 - min_i128 + 1;
+ if range <= 0 || range > i128::from(u64::MAX) {
+ return Err(RadrootsAppUtilsError::InvalidInput);
+ }
+ let range_u64 = range as u64;
+ let max_acceptable = u64::MAX - (u64::MAX % range_u64);
+ loop {
+ let value = random_u64()?;
+ if value < max_acceptable {
+ let offset = (value % range_u64) as i128;
+ return Ok((min_i128 + offset) as i64);
+ }
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+fn random_u64() -> Result<u64, RadrootsAppUtilsError> {
+ let window = web_sys::window().ok_or(RadrootsAppUtilsError::Unavailable)?;
+ let crypto = window
+ .crypto()
+ .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
+ let array = js_sys::Uint8Array::new_with_length(8);
+ crypto
+ .get_random_values_with_u8_array(&array)
+ .map_err(|_| RadrootsAppUtilsError::Unavailable)?;
+ let mut bytes = [0u8; 8];
+ array.copy_to(&mut bytes);
+ Ok(u64::from_le_bytes(bytes))
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn random_u64() -> Result<u64, RadrootsAppUtilsError> {
+ let mut bytes = [0u8; 8];
+ getrandom::getrandom(&mut bytes).map_err(|_| RadrootsAppUtilsError::Unavailable)?;
+ Ok(u64::from_le_bytes(bytes))
+}
+
#[cfg(test)]
mod tests {
- use super::{num_str, parse_float, parse_int};
+ use super::{num_interval_range, num_str, parse_float, parse_int};
+ use crate::error::RadrootsAppUtilsError;
#[test]
fn parse_int_returns_fallback_on_invalid() {
@@ -40,4 +89,22 @@ mod tests {
fn num_str_formats_numbers() {
assert_eq!(num_str(42), "42");
}
+
+ #[test]
+ fn num_interval_range_rejects_invalid() {
+ let err = num_interval_range(2, 1).unwrap_err();
+ assert_eq!(err, RadrootsAppUtilsError::InvalidInput);
+ }
+
+ #[test]
+ fn num_interval_range_single_value() {
+ let value = num_interval_range(4, 4).expect("single value");
+ assert_eq!(value, 4);
+ }
+
+ #[test]
+ fn num_interval_range_within_bounds() {
+ let value = num_interval_range(1, 3).expect("range");
+ assert!((1..=3).contains(&value));
+ }
}