Skip to main content

steel_utils/nbt/snbt/
number.rs

1use std::num::{IntErrorKind, ParseIntError};
2
3use simdnbt::owned::NbtTag;
4
5use super::error::{SnbtErrorKind, SnbtNumberType};
6
7pub(super) fn parse_number_token(
8    token: &str,
9    default_kind: DefaultIntegerKind,
10) -> Result<NbtTag, SnbtErrorKind> {
11    if should_parse_as_float(token) {
12        return parse_float_token(token);
13    }
14
15    parse_integer_token(token, default_kind)
16}
17
18fn should_parse_as_float(token: &str) -> bool {
19    if has_radix_prefix(token) {
20        return false;
21    }
22
23    token.contains('.')
24        || token.contains('e')
25        || token.contains('E')
26        || token.ends_with(['f', 'F', 'd', 'D'])
27}
28
29fn has_radix_prefix(token: &str) -> bool {
30    let stripped = token
31        .strip_prefix(['+', '-'])
32        .map_or(token, |stripped| stripped);
33    stripped.starts_with("0x")
34        || stripped.starts_with("0X")
35        || stripped.starts_with("0b")
36        || stripped.starts_with("0B")
37}
38
39fn parse_float_token(token: &str) -> Result<NbtTag, SnbtErrorKind> {
40    let (body, kind) = if token.ends_with(['f', 'F']) {
41        (&token[..token.len() - 1], FloatKind::Float)
42    } else if token.ends_with(['d', 'D']) {
43        (&token[..token.len() - 1], FloatKind::Double)
44    } else {
45        (token, FloatKind::Double)
46    };
47    validate_float_underscore_placement(body)?;
48    let body = normalize_number_digits(body)?;
49    let value = body
50        .parse::<f64>()
51        .map_err(|_| SnbtErrorKind::InvalidFloatingPoint)?;
52    if !value.is_finite() {
53        return Err(SnbtErrorKind::NonFiniteNumber);
54    }
55
56    match kind {
57        FloatKind::Float => {
58            let value = value as f32;
59            if !value.is_finite() {
60                return Err(SnbtErrorKind::NonFiniteNumber);
61            }
62            Ok(NbtTag::Float(value))
63        }
64        FloatKind::Double => Ok(NbtTag::Double(value)),
65    }
66}
67
68const fn validate_float_underscore_placement(input: &str) -> Result<(), SnbtErrorKind> {
69    let bytes = input.as_bytes();
70    let mut index = 0;
71    while index < bytes.len() {
72        if bytes[index] != b'_' {
73            index += 1;
74            continue;
75        }
76
77        let run_start = index;
78        while index < bytes.len() && bytes[index] == b'_' {
79            index += 1;
80        }
81        let surrounded_by_digits = run_start > 0
82            && index < bytes.len()
83            && bytes[run_start - 1].is_ascii_digit()
84            && bytes[index].is_ascii_digit();
85        if !surrounded_by_digits {
86            return Err(SnbtErrorKind::InvalidUnderscore);
87        }
88    }
89    Ok(())
90}
91
92fn parse_integer_token(
93    token: &str,
94    default_kind: DefaultIntegerKind,
95) -> Result<NbtTag, SnbtErrorKind> {
96    const SUFFIXES: &[(&str, IntegerKind, IntegerSignedness)] = &[
97        ("ub", IntegerKind::Byte, IntegerSignedness::Unsigned),
98        ("us", IntegerKind::Short, IntegerSignedness::Unsigned),
99        ("ui", IntegerKind::Int, IntegerSignedness::Unsigned),
100        ("ul", IntegerKind::Long, IntegerSignedness::Unsigned),
101        ("sb", IntegerKind::Byte, IntegerSignedness::Signed),
102        ("ss", IntegerKind::Short, IntegerSignedness::Signed),
103        ("si", IntegerKind::Int, IntegerSignedness::Signed),
104        ("sl", IntegerKind::Long, IntegerSignedness::Signed),
105        ("b", IntegerKind::Byte, IntegerSignedness::Default),
106        ("s", IntegerKind::Short, IntegerSignedness::Default),
107        ("i", IntegerKind::Int, IntegerSignedness::Default),
108        ("l", IntegerKind::Long, IntegerSignedness::Default),
109    ];
110
111    let lower = token.to_ascii_lowercase();
112    for &(suffix, kind, signedness) in SUFFIXES {
113        // Vanilla's hex numeral rule consumes `b` as a digit before suffix parsing.
114        if suffix == "b" && has_hex_radix_prefix(token) {
115            continue;
116        }
117        let Some(body) = lower.strip_suffix(suffix) else {
118            continue;
119        };
120        let original_body = &token[..body.len()];
121        if original_body.is_empty() {
122            continue;
123        }
124        return parse_integer_body(original_body, kind, signedness);
125    }
126
127    parse_integer_body(token, default_kind.into(), IntegerSignedness::Default)
128}
129
130fn parse_integer_body(
131    body: &str,
132    kind: IntegerKind,
133    signedness: IntegerSignedness,
134) -> Result<NbtTag, SnbtErrorKind> {
135    let (negative, body) = match body.as_bytes().first().copied() {
136        Some(b'-') => (true, &body[1..]),
137        Some(b'+') => (false, &body[1..]),
138        _ => (false, body),
139    };
140    if body.is_empty() {
141        return Err(SnbtErrorKind::InvalidInteger);
142    }
143
144    let (radix, digits) = if body.starts_with("0x") || body.starts_with("0X") {
145        (16, &body[2..])
146    } else if body.starts_with("0b") || body.starts_with("0B") {
147        (2, &body[2..])
148    } else {
149        (10, body)
150    };
151    if digits.is_empty() {
152        return Err(SnbtErrorKind::InvalidInteger);
153    }
154    if radix == 10 && digits.len() > 1 && digits.starts_with('0') {
155        return Err(SnbtErrorKind::LeadingZero);
156    }
157
158    let digits = normalize_number_digits(digits)?;
159    let signed = signedness == IntegerSignedness::Signed
160        || (radix == 10 && signedness != IntegerSignedness::Unsigned);
161    if negative && !signed {
162        return Err(SnbtErrorKind::ExpectedNonNegativeNumber);
163    }
164
165    if signed {
166        let magnitude = i128::from_str_radix(&digits, radix).map_err(integer_parse_error_kind)?;
167        let value = if negative { -magnitude } else { magnitude };
168        return kind.to_signed_tag(value);
169    }
170
171    let value = u128::from_str_radix(&digits, radix).map_err(integer_parse_error_kind)?;
172    kind.to_unsigned_tag(value)
173}
174
175const fn integer_parse_error_kind(error: ParseIntError) -> SnbtErrorKind {
176    match error.kind() {
177        IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => SnbtErrorKind::IntegerTooLarge,
178        _ => SnbtErrorKind::InvalidInteger,
179    }
180}
181
182fn normalize_number_digits(input: &str) -> Result<String, SnbtErrorKind> {
183    if input.is_empty() {
184        return Err(SnbtErrorKind::InvalidNumber);
185    }
186    if input.starts_with('_') || input.ends_with('_') {
187        return Err(SnbtErrorKind::InvalidUnderscore);
188    }
189
190    Ok(input.chars().filter(|ch| *ch != '_').collect())
191}
192
193fn has_hex_radix_prefix(token: &str) -> bool {
194    let stripped = token
195        .strip_prefix(['+', '-'])
196        .map_or(token, |stripped| stripped);
197    stripped.starts_with("0x") || stripped.starts_with("0X")
198}
199
200pub(super) fn integer_tag_value(tag: &NbtTag) -> Option<(IntegerKind, i64)> {
201    match tag {
202        NbtTag::Byte(value) => Some((IntegerKind::Byte, i64::from(*value))),
203        NbtTag::Short(value) => Some((IntegerKind::Short, i64::from(*value))),
204        NbtTag::Int(value) => Some((IntegerKind::Int, i64::from(*value))),
205        NbtTag::Long(value) => Some((IntegerKind::Long, *value)),
206        _ => None,
207    }
208}
209
210pub(super) fn bool_tag_value(tag: &NbtTag) -> Option<bool> {
211    match tag {
212        NbtTag::Byte(value) => Some(*value != 0),
213        NbtTag::Short(value) => Some(*value != 0),
214        NbtTag::Int(value) => Some(*value != 0),
215        NbtTag::Long(value) => Some(*value != 0),
216        NbtTag::Float(value) => Some(*value != 0.0),
217        NbtTag::Double(value) => Some(*value != 0.0),
218        _ => None,
219    }
220}
221
222pub(super) const fn can_start_number(ch: char) -> bool {
223    matches!(ch, '+' | '-' | '.' | '0'..='9')
224}
225
226pub(super) struct NumberScanError {
227    pub(super) cursor: usize,
228    pub(super) kind: SnbtErrorKind,
229}
230
231impl NumberScanError {
232    const fn new(cursor: usize, kind: SnbtErrorKind) -> Self {
233        Self { cursor, kind }
234    }
235}
236
237pub(super) fn scan_number_token(input: &str, allow_float: bool) -> Result<usize, NumberScanError> {
238    let bytes = input.as_bytes();
239    let has_sign = matches!(bytes.first(), Some(b'+' | b'-'));
240    let mut cursor = usize::from(has_sign);
241    let Some(&first) = bytes.get(cursor) else {
242        return Err(NumberScanError::new(
243            cursor,
244            SnbtErrorKind::ExpectedDecimalNumeral,
245        ));
246    };
247
248    if first == b'.' {
249        if !allow_float {
250            return Err(NumberScanError::new(0, SnbtErrorKind::ExpectedNumber));
251        }
252
253        cursor += 1;
254        cursor = scan_required_numeral(
255            bytes,
256            cursor,
257            |byte| byte.is_ascii_digit(),
258            SnbtErrorKind::ExpectedDecimalNumeral,
259        )?;
260        cursor = scan_optional_exponent(bytes, cursor);
261        return Ok(cursor + float_suffix_len(&bytes[cursor..]));
262    }
263
264    if !first.is_ascii_digit() {
265        return Err(NumberScanError::new(
266            cursor,
267            if has_sign {
268                SnbtErrorKind::ExpectedDecimalNumeral
269            } else {
270                SnbtErrorKind::ExpectedNumber
271            },
272        ));
273    }
274
275    if first == b'0' {
276        if matches!(bytes.get(cursor + 1), Some(b'x' | b'X')) {
277            cursor += 2;
278            cursor = scan_required_numeral(
279                bytes,
280                cursor,
281                |byte| byte.is_ascii_hexdigit(),
282                SnbtErrorKind::ExpectedHexNumeral,
283            )?;
284            return Ok(cursor + integer_suffix_len(&bytes[cursor..]));
285        }
286        if matches!(bytes.get(cursor + 1), Some(b'b' | b'B'))
287            && matches!(bytes.get(cursor + 2), Some(b'0' | b'1' | b'_'))
288        {
289            cursor += 2;
290            cursor = scan_required_numeral(
291                bytes,
292                cursor,
293                |byte| matches!(byte, b'0' | b'1'),
294                SnbtErrorKind::ExpectedBinaryNumeral,
295            )?;
296            return Ok(cursor + integer_suffix_len(&bytes[cursor..]));
297        }
298    }
299
300    let numeral_start = cursor;
301    cursor = scan_required_numeral(
302        bytes,
303        cursor,
304        |byte| byte.is_ascii_digit(),
305        SnbtErrorKind::ExpectedDecimalNumeral,
306    )?;
307
308    if allow_float {
309        match bytes.get(cursor) {
310            Some(b'.') => {
311                cursor += 1;
312                cursor =
313                    try_scan_numeral(bytes, cursor, |byte| byte.is_ascii_digit()).unwrap_or(cursor);
314                cursor = scan_optional_exponent(bytes, cursor);
315                return Ok(cursor + float_suffix_len(&bytes[cursor..]));
316            }
317            Some(b'e' | b'E') => {
318                if let Some(exponent_end) = try_scan_exponent(bytes, cursor) {
319                    cursor = exponent_end;
320                    return Ok(cursor + float_suffix_len(&bytes[cursor..]));
321                }
322            }
323            Some(b'f' | b'F' | b'd' | b'D') => return Ok(cursor + 1),
324            _ => {}
325        }
326    }
327
328    let digit_count = bytes[numeral_start..cursor]
329        .iter()
330        .filter(|byte| **byte != b'_')
331        .count();
332    if first == b'0' && digit_count > 1 {
333        return Err(NumberScanError::new(cursor, SnbtErrorKind::LeadingZero));
334    }
335
336    Ok(cursor + integer_suffix_len(&bytes[cursor..]))
337}
338
339fn scan_required_numeral(
340    bytes: &[u8],
341    start: usize,
342    accepts_digit: impl Fn(u8) -> bool,
343    expected: SnbtErrorKind,
344) -> Result<usize, NumberScanError> {
345    let mut cursor = start;
346    while bytes
347        .get(cursor)
348        .is_some_and(|byte| accepts_digit(*byte) || *byte == b'_')
349    {
350        cursor += 1;
351    }
352
353    if cursor == start {
354        return Err(NumberScanError::new(start, expected));
355    }
356    if bytes[start] == b'_' || bytes[cursor - 1] == b'_' {
357        return Err(NumberScanError::new(
358            start,
359            SnbtErrorKind::InvalidUnderscore,
360        ));
361    }
362
363    Ok(cursor)
364}
365
366fn try_scan_numeral(
367    bytes: &[u8],
368    start: usize,
369    accepts_digit: impl Fn(u8) -> bool,
370) -> Option<usize> {
371    scan_required_numeral(
372        bytes,
373        start,
374        accepts_digit,
375        SnbtErrorKind::ExpectedDecimalNumeral,
376    )
377    .ok()
378}
379
380fn scan_optional_exponent(bytes: &[u8], cursor: usize) -> usize {
381    try_scan_exponent(bytes, cursor).unwrap_or(cursor)
382}
383
384fn try_scan_exponent(bytes: &[u8], cursor: usize) -> Option<usize> {
385    if !matches!(bytes.get(cursor), Some(b'e' | b'E')) {
386        return None;
387    }
388
389    let numeral_start =
390        cursor + 1 + usize::from(matches!(bytes.get(cursor + 1), Some(b'+' | b'-')));
391    try_scan_numeral(bytes, numeral_start, |byte| byte.is_ascii_digit())
392}
393
394const fn float_suffix_len(bytes: &[u8]) -> usize {
395    matches!(bytes.first(), Some(b'f' | b'F' | b'd' | b'D')) as usize
396}
397
398fn integer_suffix_len(bytes: &[u8]) -> usize {
399    if matches!(bytes.first(), Some(b'u' | b'U' | b's' | b'S'))
400        && matches!(
401            bytes.get(1),
402            Some(b'b' | b'B' | b's' | b'S' | b'i' | b'I' | b'l' | b'L')
403        )
404    {
405        return 2;
406    }
407    usize::from(matches!(
408        bytes.first(),
409        Some(b'b' | b'B' | b's' | b'S' | b'i' | b'I' | b'l' | b'L')
410    ))
411}
412
413pub(super) fn is_unsuffixed_decimal_integer_token(token: &str) -> bool {
414    let digits = token
415        .strip_prefix(['+', '-'])
416        .map_or(token, |stripped| stripped);
417    !digits.is_empty()
418        && digits
419            .bytes()
420            .all(|byte| byte.is_ascii_digit() || byte == b'_')
421}
422
423pub(super) const fn is_allowed_in_unquoted_string(ch: char) -> bool {
424    matches!(ch, '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' | '-' | '.' | '+')
425}
426
427pub(super) const fn is_allowed_in_unicode_name(ch: char) -> bool {
428    ch.is_ascii_alphanumeric() || matches!(ch, '-' | ' ')
429}
430
431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
432pub(super) enum DefaultIntegerKind {
433    Byte,
434    Int,
435    Long,
436}
437
438impl From<DefaultIntegerKind> for IntegerKind {
439    fn from(value: DefaultIntegerKind) -> Self {
440        match value {
441            DefaultIntegerKind::Byte => Self::Byte,
442            DefaultIntegerKind::Int => Self::Int,
443            DefaultIntegerKind::Long => Self::Long,
444        }
445    }
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449pub(super) enum IntegerKind {
450    Byte,
451    Short,
452    Int,
453    Long,
454}
455
456impl IntegerKind {
457    fn to_signed_tag(self, value: i128) -> Result<NbtTag, SnbtErrorKind> {
458        match self {
459            Self::Byte => {
460                let value = i8::try_from(value).map_err(|_| SnbtErrorKind::NumberOutOfRange {
461                    number_type: self.into(),
462                    unsigned: false,
463                })?;
464                Ok(NbtTag::Byte(value))
465            }
466            Self::Short => {
467                let value = i16::try_from(value).map_err(|_| SnbtErrorKind::NumberOutOfRange {
468                    number_type: self.into(),
469                    unsigned: false,
470                })?;
471                Ok(NbtTag::Short(value))
472            }
473            Self::Int => {
474                let value = i32::try_from(value).map_err(|_| SnbtErrorKind::NumberOutOfRange {
475                    number_type: self.into(),
476                    unsigned: false,
477                })?;
478                Ok(NbtTag::Int(value))
479            }
480            Self::Long => {
481                let value = i64::try_from(value).map_err(|_| SnbtErrorKind::NumberOutOfRange {
482                    number_type: self.into(),
483                    unsigned: false,
484                })?;
485                Ok(NbtTag::Long(value))
486            }
487        }
488    }
489
490    fn to_unsigned_tag(self, value: u128) -> Result<NbtTag, SnbtErrorKind> {
491        match self {
492            Self::Byte => {
493                if value > u128::from(u8::MAX) {
494                    return Err(SnbtErrorKind::NumberOutOfRange {
495                        number_type: self.into(),
496                        unsigned: true,
497                    });
498                }
499                Ok(NbtTag::Byte(value as u8 as i8))
500            }
501            Self::Short => {
502                if value > u128::from(u16::MAX) {
503                    return Err(SnbtErrorKind::NumberOutOfRange {
504                        number_type: self.into(),
505                        unsigned: true,
506                    });
507                }
508                Ok(NbtTag::Short(value as u16 as i16))
509            }
510            Self::Int => {
511                if value > u128::from(u32::MAX) {
512                    return Err(SnbtErrorKind::NumberOutOfRange {
513                        number_type: self.into(),
514                        unsigned: true,
515                    });
516                }
517                Ok(NbtTag::Int(value as u32 as i32))
518            }
519            Self::Long => {
520                if value > u128::from(u64::MAX) {
521                    return Err(SnbtErrorKind::NumberOutOfRange {
522                        number_type: self.into(),
523                        unsigned: true,
524                    });
525                }
526                Ok(NbtTag::Long(value as u64 as i64))
527            }
528        }
529    }
530}
531
532impl From<IntegerKind> for SnbtNumberType {
533    fn from(value: IntegerKind) -> Self {
534        match value {
535            IntegerKind::Byte => Self::Byte,
536            IntegerKind::Short => Self::Short,
537            IntegerKind::Int => Self::Int,
538            IntegerKind::Long => Self::Long,
539        }
540    }
541}
542
543#[derive(Clone, Copy, Debug, PartialEq, Eq)]
544enum IntegerSignedness {
545    Default,
546    Signed,
547    Unsigned,
548}
549
550#[derive(Clone, Copy, Debug, PartialEq, Eq)]
551enum FloatKind {
552    Float,
553    Double,
554}