Skip to main content

rskit_util/
lib.rs

1//! Minimal domain-free utility crate for the rskit ecosystem.
2//!
3//! Provides fundamental helper modules for string casing, safe truncation,
4//! collection transformation, safe environment variable parsing, duration/byte
5//! size parsing, and stateless mathematical exponential backoff.
6//!
7//! # Modules
8//!
9//! - [`backoff`]: State-free mathematical backoff calculations with jitter.
10//! - [`bytes`]: Formatting and parsing human-readable data sizes.
11//! - [`collections`]: Vector grouping, chunking, indexing, and partition helpers.
12//! - [`mod@env`]: Safe environment variable parsing with defaults.
13//! - [`glob`]: Shell-style `*`/`?` matching over single string segments.
14//! - [`hash`]: Content/interop hashing — BLAKE3 and SHA-256.
15//! - [`secret`]: Prevent accidental credential leaks in logs/debug outputs.
16//! - [`sensitive`]: Matching helpers for names that commonly carry secrets.
17//! - [`strings`]: Casing, safe truncation, unique-shorthand resolution, and "did you mean?" suggestions.
18//! - [`template`]: Lightweight template engine (`{name}` interpolation).
19//! - [`time`]: Duration parsing, UTC date/time conversion, RFC 3339 helpers, and timing wrappers.
20
21#![warn(missing_docs)]
22
23pub mod backoff;
24pub mod bytes;
25pub mod collections;
26pub mod env;
27pub mod glob;
28pub mod hash;
29pub mod secret;
30pub mod sensitive;
31pub mod strings;
32pub mod template;
33pub mod time;
34
35pub use secret::SecretString;
36pub use sensitive::SecretKeyMatcher;
37pub use template::{Placeholder, Template, TemplatePart};
38
39pub(crate) fn parse_decimal_scaled(value: &str, multiplier: u128) -> Option<u128> {
40    if value.is_empty() || value.starts_with('-') || value.starts_with('+') {
41        return None;
42    }
43
44    let (whole, fraction) = value.split_once('.').unwrap_or((value, ""));
45    if whole.is_empty() && fraction.is_empty() {
46        return None;
47    }
48    if !whole.bytes().all(|byte| byte.is_ascii_digit())
49        || !fraction.bytes().all(|byte| byte.is_ascii_digit())
50    {
51        return None;
52    }
53
54    let whole = if whole.is_empty() {
55        0
56    } else {
57        whole.parse::<u128>().ok()?
58    };
59    let whole_scaled = whole.checked_mul(multiplier)?;
60
61    if fraction.is_empty() {
62        return Some(whole_scaled);
63    }
64
65    let scale = 10_u128.checked_pow(u32::try_from(fraction.len()).ok()?)?;
66    let fraction = fraction.parse::<u128>().ok()?;
67    let fraction_scaled = fraction.checked_mul(multiplier)?.checked_div(scale)?;
68    whole_scaled.checked_add(fraction_scaled)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::parse_decimal_scaled;
74
75    #[test]
76    fn parse_decimal_scaled_accepts_whole_and_fractional_values() {
77        assert_eq!(parse_decimal_scaled("0", 1000), Some(0));
78        assert_eq!(parse_decimal_scaled("12", 1000), Some(12_000));
79        assert_eq!(parse_decimal_scaled("12.345", 1000), Some(12_345));
80        assert_eq!(parse_decimal_scaled(".5", 1000), Some(500));
81        assert_eq!(parse_decimal_scaled("1.", 1000), Some(1000));
82        assert_eq!(parse_decimal_scaled("1.2345", 1000), Some(1234));
83    }
84
85    #[test]
86    fn parse_decimal_scaled_rejects_invalid_or_overflowing_values() {
87        assert_eq!(parse_decimal_scaled("", 1000), None);
88        assert_eq!(parse_decimal_scaled("+1", 1000), None);
89        assert_eq!(parse_decimal_scaled("-1", 1000), None);
90        assert_eq!(parse_decimal_scaled(".", 1000), None);
91        assert_eq!(parse_decimal_scaled("1.2.3", 1000), None);
92        assert_eq!(parse_decimal_scaled("1a", 1000), None);
93        assert_eq!(parse_decimal_scaled("2", u128::MAX), None);
94        assert_eq!(parse_decimal_scaled("1.1", u128::MAX), None);
95    }
96}