app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 3+++
Mcrates/utils/Cargo.toml | 3+++
Mcrates/utils/src/lib.rs | 2+-
Mcrates/utils/src/numbers/mod.rs | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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)); + } }