alibi_core/utils/
javascript.rs1#[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#[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#[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}