Skip to main content

alibi_core/utils/
javascript.rs

1//! Shared JavaScript primitive coercion used by actual identity and storage boundaries.
2
3#[must_use]
4pub fn trim(value: &str) -> &str {
5    value.trim_matches(is_whitespace)
6}
7
8#[must_use]
9pub const fn is_whitespace(character: char) -> bool {
10    matches!(
11        character,
12        '\u{0009}'..='\u{000D}'
13            | '\u{0020}'
14            | '\u{00A0}'
15            | '\u{1680}'
16            | '\u{2000}'..='\u{200A}'
17            | '\u{2028}'
18            | '\u{2029}'
19            | '\u{202F}'
20            | '\u{205F}'
21            | '\u{3000}'
22            | '\u{FEFF}'
23    )
24}
25
26#[must_use]
27pub fn string_to_number(value: &str) -> Option<f64> {
28    let value = trim(value);
29    if value.is_empty() {
30        return Some(0.0);
31    }
32    for (prefixes, radix, bits) in [
33        (["0x", "0X"], 16, 4),
34        (["0o", "0O"], 8, 3),
35        (["0b", "0B"], 2, 1),
36    ] {
37        if let Some(digits) = prefixes
38            .iter()
39            .find_map(|prefix| value.strip_prefix(prefix))
40        {
41            return radix_number(digits, radix, bits);
42        }
43    }
44    match value {
45        "Infinity" | "+Infinity" => Some(f64::INFINITY),
46        "-Infinity" => Some(f64::NEG_INFINITY),
47        _ if value.bytes().all(|byte| {
48            byte.is_ascii_digit() || matches!(byte, b'+' | b'-' | b'.' | b'e' | b'E')
49        }) =>
50        {
51            value.parse().ok()
52        }
53        _ => None,
54    }
55}
56
57/// Parse power-of-two radix integers with one IEEE-754 rounding, including
58/// values wider than u64. Incremental floating addition would round each digit
59/// and can change the chain presented to the verifier.
60#[expect(
61    clippy::as_conversions,
62    clippy::cast_possible_truncation,
63    clippy::cast_possible_wrap,
64    clippy::cast_precision_loss,
65    reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
66)]
67fn radix_number(digits: &str, radix: u32, bits_per_digit: usize) -> Option<f64> {
68    if digits.is_empty() {
69        return None;
70    }
71    let digits = digits
72        .chars()
73        .map(|character| character.to_digit(radix))
74        .collect::<Option<Vec<_>>>()?;
75    let Some(first_nonzero) = digits.iter().position(|digit| *digit != 0) else {
76        return Some(0.0);
77    };
78    let significant = digits.get(first_nonzero..)?;
79    let first = *significant.first()?;
80    let first_bits = first.bit_width() as usize;
81    let bit_length = first_bits + (significant.len() - 1) * bits_per_digit;
82    if bit_length > 1024 {
83        return Some(f64::INFINITY);
84    }
85    let mut mantissa = 0u64;
86    let mut position = 0;
87    let mut guard = false;
88    let mut sticky = false;
89    for (index, digit) in significant.iter().enumerate() {
90        let width = if index == 0 {
91            first_bits
92        } else {
93            bits_per_digit
94        };
95        for bit in (0..width).rev() {
96            let set = (*digit >> bit) & 1 != 0;
97            match position.cmp(&53) {
98                std::cmp::Ordering::Less => mantissa = (mantissa << 1) | u64::from(set),
99                std::cmp::Ordering::Equal => guard = set,
100                std::cmp::Ordering::Greater => sticky |= set,
101            }
102            position += 1;
103        }
104    }
105    if bit_length <= 53 {
106        return Some(mantissa as f64);
107    }
108    if guard && (sticky || mantissa & 1 != 0) {
109        mantissa += 1;
110    }
111    Some(mantissa as f64 * 2.0f64.powi((bit_length - 53) as i32))
112}