Skip to main content

alibi_core/utils/
javascript.rs

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