Skip to content

Commit aa2c3d3

Browse files
authored
Fixes error handling when second decimal place at tail position (paupino#636)
* Fixes error handling when second decimal place at tail position * Added further bounds tests
1 parent 80e9f08 commit aa2c3d3

1 file changed

Lines changed: 108 additions & 11 deletions

File tree

src/str.rs

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,14 @@ fn dispatch_next<const POINT: bool, const NEG: bool, const HAS: bool, const BIG:
213213
}
214214
}
215215

216+
/// Dispatch the next non-digit byte:
217+
///
218+
/// * POINT - a decimal point has been seen
219+
/// * NEG - we've encountered a `-` and the number is negative
220+
/// * HAS - a digit has been encountered (when HAS is false it's invalid)
221+
/// * BIG - a number that uses 96 bits instead of only 64 bits
222+
/// * FIRST - true if it is the first byte in the string
223+
/// * ROUND - attempt to round underflow
216224
#[inline(never)]
217225
fn non_digit_dispatch_u64<
218226
const POINT: bool,
@@ -347,7 +355,17 @@ fn handle_full_128<const POINT: bool, const NEG: bool, const ROUND: bool>(
347355
let next = *next;
348356
if POINT && scale >= 28 {
349357
if ROUND {
350-
maybe_round(data, next, scale, POINT, NEG)
358+
// If it is an underscore at the rounding position we require slightly different handling to look ahead another digit
359+
if next == b'_' {
360+
if let Some((next, bytes)) = bytes.split_first() {
361+
handle_full_128::<POINT, NEG, ROUND>(data, bytes, scale, *next)
362+
} else {
363+
handle_data::<NEG, true>(data, scale)
364+
}
365+
} else {
366+
// Otherwise, we round as usual
367+
maybe_round(data, next, scale, POINT, NEG)
368+
}
351369
} else {
352370
Err(Error::Underflow)
353371
}
@@ -380,17 +398,11 @@ fn handle_full_128<const POINT: bool, const NEG: bool, const ROUND: bool>(
380398

381399
#[inline(never)]
382400
#[cold]
383-
fn maybe_round(
384-
mut data: u128,
385-
next_byte: u8,
386-
mut scale: u8,
387-
point: bool,
388-
negative: bool,
389-
) -> Result<Decimal, crate::Error> {
401+
fn maybe_round(mut data: u128, next_byte: u8, mut scale: u8, point: bool, negative: bool) -> Result<Decimal, Error> {
390402
let digit = match next_byte {
391403
b'0'..=b'9' => u32::from(next_byte - b'0'),
392-
b'_' => 0, // this should be an invalid string?
393-
b'.' if point => 0,
404+
b'_' => 0, // This is perhaps an error case, but keep this here for compatibility
405+
b'.' if !point => 0,
394406
b => return tail_invalid_digit(b),
395407
};
396408

@@ -933,7 +945,6 @@ mod test {
933945
);
934946
}
935947

936-
#[ignore]
937948
#[test]
938949
fn from_str_mantissa_overflow_4() {
939950
// Same test as above, however with underscores. This causes issues.
@@ -945,6 +956,92 @@ mod test {
945956
);
946957
}
947958

959+
#[test]
960+
fn invalid_input_1() {
961+
assert_eq!(
962+
parse_str_radix_10("1.0000000000000000000000000000.5"),
963+
Err(Error::from("Invalid decimal: two decimal points"))
964+
);
965+
}
966+
967+
#[test]
968+
fn invalid_input_2() {
969+
assert_eq!(
970+
parse_str_radix_10("1.0.5"),
971+
Err(Error::from("Invalid decimal: two decimal points"))
972+
);
973+
}
974+
975+
#[test]
976+
fn character_at_rounding_position() {
977+
let tests = [
978+
// digit is at the rounding position
979+
(
980+
"1.000_000_000_000_000_000_000_000_000_04",
981+
Ok(Decimal::from_i128_with_scale(
982+
1_000_000_000_000_000_000_000_000_000_0,
983+
28,
984+
)),
985+
),
986+
(
987+
"1.000_000_000_000_000_000_000_000_000_06",
988+
Ok(Decimal::from_i128_with_scale(
989+
1_000_000_000_000_000_000_000_000_000_1,
990+
28,
991+
)),
992+
),
993+
// Decimal point is at the rounding position
994+
(
995+
"1_000_000_000_000_000_000_000_000_000_0.4",
996+
Ok(Decimal::from_i128_with_scale(
997+
1_000_000_000_000_000_000_000_000_000_0,
998+
0,
999+
)),
1000+
),
1001+
(
1002+
"1_000_000_000_000_000_000_000_000_000_0.6",
1003+
Ok(Decimal::from_i128_with_scale(
1004+
1_000_000_000_000_000_000_000_000_000_1,
1005+
0,
1006+
)),
1007+
),
1008+
// Placeholder is at the rounding position
1009+
(
1010+
"1.000_000_000_000_000_000_000_000_000_0_4",
1011+
Ok(Decimal::from_i128_with_scale(
1012+
1_000_000_000_000_000_000_000_000_000_0,
1013+
28,
1014+
)),
1015+
),
1016+
(
1017+
"1.000_000_000_000_000_000_000_000_000_0_6",
1018+
Ok(Decimal::from_i128_with_scale(
1019+
1_000_000_000_000_000_000_000_000_000_1,
1020+
28,
1021+
)),
1022+
),
1023+
// Multiple placeholders at rounding position
1024+
(
1025+
"1.000_000_000_000_000_000_000_000_000_0__4",
1026+
Ok(Decimal::from_i128_with_scale(
1027+
1_000_000_000_000_000_000_000_000_000_0,
1028+
28,
1029+
)),
1030+
),
1031+
(
1032+
"1.000_000_000_000_000_000_000_000_000_0__6",
1033+
Ok(Decimal::from_i128_with_scale(
1034+
1_000_000_000_000_000_000_000_000_000_1,
1035+
28,
1036+
)),
1037+
),
1038+
];
1039+
1040+
for (input, expected) in tests.iter() {
1041+
assert_eq!(parse_str_radix_10(input), *expected, "Test input {}", input);
1042+
}
1043+
}
1044+
9481045
#[test]
9491046
fn from_str_edge_cases_1() {
9501047
assert_eq!(parse_str_radix_10(""), Err(Error::from("Invalid decimal: empty")));

0 commit comments

Comments
 (0)