Skip to content

Commit 349e568

Browse files
authored
Merge pull request #7694 from drinkcat/printf-fix-empty
uucore: parser: num_parser: Return error if no digit has been parsed uucore: parser: num_parser: Ignore empty exponents uucore: parser: num_parser: Parse "0x"/"0b" as PartialMatch
2 parents 219e87d + fce9f79 commit 349e568

File tree

2 files changed

+246
-13
lines changed

2 files changed

+246
-13
lines changed

src/uucore/src/lib/features/parser/num_parser.rs

Lines changed: 166 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,18 +450,15 @@ pub(crate) fn parse<'a>(
450450
} else {
451451
(Base::Decimal, unsigned)
452452
};
453-
if rest.is_empty() {
454-
return Err(ExtendedParserError::NotNumeric);
455-
}
456453

457454
// Parse the integral part of the number
458455
let mut chars = rest.chars().enumerate().fuse().peekable();
459-
let mut digits = BigUint::zero();
456+
let mut digits: Option<BigUint> = None;
460457
let mut scale = 0u64;
461-
let mut exponent = BigInt::zero();
458+
let mut exponent: Option<BigInt> = None;
462459
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
463460
chars.next();
464-
digits = digits * base as u8 + d;
461+
digits = Some(digits.unwrap_or_default() * base as u8 + d);
465462
}
466463

467464
// Parse fractional/exponent part of the number for supported bases.
@@ -472,7 +469,7 @@ pub(crate) fn parse<'a>(
472469
chars.next();
473470
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
474471
chars.next();
475-
(digits, scale) = (digits * base as u8 + d, scale + 1);
472+
(digits, scale) = (Some(digits.unwrap_or_default() * base as u8 + d), scale + 1);
476473
}
477474
}
478475

@@ -487,6 +484,8 @@ pub(crate) fn parse<'a>(
487484
.peek()
488485
.is_some_and(|&(_, c)| c.to_ascii_lowercase() == exp_char)
489486
{
487+
// Save the iterator position in case we do not parse any exponent.
488+
let save_chars = chars.clone();
490489
chars.next();
491490
let exp_negative = match chars.peek() {
492491
Some((_, '-')) => {
@@ -501,23 +500,40 @@ pub(crate) fn parse<'a>(
501500
};
502501
while let Some(d) = chars.peek().and_then(|&(_, c)| Base::Decimal.digit(c)) {
503502
chars.next();
504-
exponent = exponent * 10 + d as i64;
503+
exponent = Some(exponent.unwrap_or_default() * 10 + d as i64);
505504
}
506-
if exp_negative {
507-
exponent = -exponent;
505+
if let Some(exp) = &exponent {
506+
if exp_negative {
507+
exponent = Some(-exp);
508+
}
509+
} else {
510+
// No exponent actually parsed, reset iterator to return partial match.
511+
chars = save_chars;
508512
}
509513
}
510514
}
511515

512-
// If nothing has been parsed, check if this is a special value, or declare the parsing unsuccessful
513-
if let Some((0, _)) = chars.peek() {
516+
// If no digit has been parsed, check if this is a special value, or declare the parsing unsuccessful
517+
if digits.is_none() {
518+
// If we trimmed an initial `0x`/`0b`, return a partial match.
519+
if rest != unsigned {
520+
let ebd = if negative {
521+
ExtendedBigDecimal::MinusZero
522+
} else {
523+
ExtendedBigDecimal::zero()
524+
};
525+
return Err(ExtendedParserError::PartialMatch(ebd, &unsigned[1..]));
526+
}
527+
514528
return if target == ParseTarget::Integral {
515529
Err(ExtendedParserError::NotNumeric)
516530
} else {
517531
parse_special_value(unsigned, negative, allowed_suffixes)
518532
};
519533
}
520534

535+
let mut digits = digits.unwrap();
536+
521537
if let Some((_, ch)) = chars.peek() {
522538
if let Some(times) = allowed_suffixes
523539
.iter()
@@ -529,7 +545,8 @@ pub(crate) fn parse<'a>(
529545
}
530546
}
531547

532-
let ebd_result = construct_extended_big_decimal(digits, negative, base, scale, exponent);
548+
let ebd_result =
549+
construct_extended_big_decimal(digits, negative, base, scale, exponent.unwrap_or_default());
533550

534551
// Return what has been parsed so far. If there are extra characters, mark the
535552
// parsing as a partial match.
@@ -625,6 +642,15 @@ mod tests {
625642
i64::extended_parse(&format!("{}", i64::MIN as i128 - 1)),
626643
Err(ExtendedParserError::Overflow(i64::MIN))
627644
));
645+
646+
assert!(matches!(
647+
i64::extended_parse(""),
648+
Err(ExtendedParserError::NotNumeric)
649+
));
650+
assert!(matches!(
651+
i64::extended_parse("."),
652+
Err(ExtendedParserError::NotNumeric)
653+
));
628654
}
629655

630656
#[test]
@@ -659,6 +685,16 @@ mod tests {
659685
Ok(0.15),
660686
f64::extended_parse(".150000000000000000000000000231313")
661687
);
688+
assert!(matches!(f64::extended_parse("123.15e"),
689+
Err(ExtendedParserError::PartialMatch(f, "e")) if f == 123.15));
690+
assert!(matches!(f64::extended_parse("123.15E"),
691+
Err(ExtendedParserError::PartialMatch(f, "E")) if f == 123.15));
692+
assert!(matches!(f64::extended_parse("123.15e-"),
693+
Err(ExtendedParserError::PartialMatch(f, "e-")) if f == 123.15));
694+
assert!(matches!(f64::extended_parse("123.15e+"),
695+
Err(ExtendedParserError::PartialMatch(f, "e+")) if f == 123.15));
696+
assert!(matches!(f64::extended_parse("123.15e."),
697+
Err(ExtendedParserError::PartialMatch(f, "e.")) if f == 123.15));
662698
assert!(matches!(f64::extended_parse("1.2.3"),
663699
Err(ExtendedParserError::PartialMatch(f, ".3")) if f == 1.2));
664700
assert!(matches!(f64::extended_parse("123.15p5"),
@@ -811,6 +847,48 @@ mod tests {
811847
ExtendedBigDecimal::extended_parse(&format!("-0e{}", i64::MIN + 2)),
812848
Ok(ExtendedBigDecimal::MinusZero)
813849
);
850+
851+
/* Invalid numbers */
852+
assert_eq!(
853+
Err(ExtendedParserError::NotNumeric),
854+
ExtendedBigDecimal::extended_parse("")
855+
);
856+
assert_eq!(
857+
Err(ExtendedParserError::NotNumeric),
858+
ExtendedBigDecimal::extended_parse(".")
859+
);
860+
assert_eq!(
861+
Err(ExtendedParserError::NotNumeric),
862+
ExtendedBigDecimal::extended_parse("e")
863+
);
864+
assert_eq!(
865+
Err(ExtendedParserError::NotNumeric),
866+
ExtendedBigDecimal::extended_parse(".e")
867+
);
868+
assert_eq!(
869+
Err(ExtendedParserError::NotNumeric),
870+
ExtendedBigDecimal::extended_parse("-e")
871+
);
872+
assert_eq!(
873+
Err(ExtendedParserError::NotNumeric),
874+
ExtendedBigDecimal::extended_parse("+.e")
875+
);
876+
assert_eq!(
877+
Err(ExtendedParserError::NotNumeric),
878+
ExtendedBigDecimal::extended_parse("e10")
879+
);
880+
assert_eq!(
881+
Err(ExtendedParserError::NotNumeric),
882+
ExtendedBigDecimal::extended_parse("e-10")
883+
);
884+
assert_eq!(
885+
Err(ExtendedParserError::NotNumeric),
886+
ExtendedBigDecimal::extended_parse("-e10")
887+
);
888+
assert_eq!(
889+
Err(ExtendedParserError::NotNumeric),
890+
ExtendedBigDecimal::extended_parse("+e10")
891+
);
814892
}
815893

816894
#[test]
@@ -831,6 +909,15 @@ mod tests {
831909
// but we can check that the number still gets parsed properly: 0x0.8e5 is 0x8e5 / 16**3
832910
assert_eq!(Ok(0.555908203125), f64::extended_parse("0x0.8e5"));
833911

912+
assert!(matches!(f64::extended_parse("0x0.1p"),
913+
Err(ExtendedParserError::PartialMatch(f, "p")) if f == 0.0625));
914+
assert!(matches!(f64::extended_parse("0x0.1p-"),
915+
Err(ExtendedParserError::PartialMatch(f, "p-")) if f == 0.0625));
916+
assert!(matches!(f64::extended_parse("0x.1p+"),
917+
Err(ExtendedParserError::PartialMatch(f, "p+")) if f == 0.0625));
918+
assert!(matches!(f64::extended_parse("0x.1p."),
919+
Err(ExtendedParserError::PartialMatch(f, "p.")) if f == 0.0625));
920+
834921
assert_eq!(
835922
Ok(ExtendedBigDecimal::BigDecimal(
836923
BigDecimal::from_str("0.0625").unwrap()
@@ -887,6 +974,42 @@ mod tests {
887974
ExtendedBigDecimal::MinusZero
888975
))
889976
));
977+
978+
// Not actually hex numbers, but the prefixes look like it.
979+
assert!(matches!(f64::extended_parse("0x"),
980+
Err(ExtendedParserError::PartialMatch(f, "x")) if f == 0.0));
981+
assert!(matches!(f64::extended_parse("0x."),
982+
Err(ExtendedParserError::PartialMatch(f, "x.")) if f == 0.0));
983+
assert!(matches!(f64::extended_parse("0xp"),
984+
Err(ExtendedParserError::PartialMatch(f, "xp")) if f == 0.0));
985+
assert!(matches!(f64::extended_parse("0xp-2"),
986+
Err(ExtendedParserError::PartialMatch(f, "xp-2")) if f == 0.0));
987+
assert!(matches!(f64::extended_parse("0x.p-2"),
988+
Err(ExtendedParserError::PartialMatch(f, "x.p-2")) if f == 0.0));
989+
assert!(matches!(f64::extended_parse("0X"),
990+
Err(ExtendedParserError::PartialMatch(f, "X")) if f == 0.0));
991+
assert!(matches!(f64::extended_parse("-0x"),
992+
Err(ExtendedParserError::PartialMatch(f, "x")) if f == -0.0));
993+
assert!(matches!(f64::extended_parse("+0x"),
994+
Err(ExtendedParserError::PartialMatch(f, "x")) if f == 0.0));
995+
assert!(matches!(f64::extended_parse("-0x."),
996+
Err(ExtendedParserError::PartialMatch(f, "x.")) if f == -0.0));
997+
assert!(matches!(
998+
u64::extended_parse("0x"),
999+
Err(ExtendedParserError::PartialMatch(0, "x"))
1000+
));
1001+
assert!(matches!(
1002+
u64::extended_parse("-0x"),
1003+
Err(ExtendedParserError::PartialMatch(0, "x"))
1004+
));
1005+
assert!(matches!(
1006+
i64::extended_parse("0x"),
1007+
Err(ExtendedParserError::PartialMatch(0, "x"))
1008+
));
1009+
assert!(matches!(
1010+
i64::extended_parse("-0x"),
1011+
Err(ExtendedParserError::PartialMatch(0, "x"))
1012+
));
8901013
}
8911014

8921015
#[test]
@@ -920,6 +1043,27 @@ mod tests {
9201043
assert_eq!(Ok(0b1011), u64::extended_parse("+0b1011"));
9211044
assert_eq!(Ok(-0b1011), i64::extended_parse("-0b1011"));
9221045

1046+
assert!(matches!(
1047+
u64::extended_parse("0b"),
1048+
Err(ExtendedParserError::PartialMatch(0, "b"))
1049+
));
1050+
assert!(matches!(
1051+
u64::extended_parse("0b."),
1052+
Err(ExtendedParserError::PartialMatch(0, "b."))
1053+
));
1054+
assert!(matches!(
1055+
u64::extended_parse("-0b"),
1056+
Err(ExtendedParserError::PartialMatch(0, "b"))
1057+
));
1058+
assert!(matches!(
1059+
i64::extended_parse("0b"),
1060+
Err(ExtendedParserError::PartialMatch(0, "b"))
1061+
));
1062+
assert!(matches!(
1063+
i64::extended_parse("-0b"),
1064+
Err(ExtendedParserError::PartialMatch(0, "b"))
1065+
));
1066+
9231067
// Binary not allowed for floats
9241068
assert!(matches!(
9251069
f64::extended_parse("0b100"),
@@ -935,6 +1079,15 @@ mod tests {
9351079
ebd == ExtendedBigDecimal::zero(),
9361080
_ => false,
9371081
});
1082+
1083+
assert!(match ExtendedBigDecimal::extended_parse("0b") {
1084+
Err(ExtendedParserError::PartialMatch(ebd, "b")) => ebd == ExtendedBigDecimal::zero(),
1085+
_ => false,
1086+
});
1087+
assert!(match ExtendedBigDecimal::extended_parse("0b.") {
1088+
Err(ExtendedParserError::PartialMatch(ebd, "b.")) => ebd == ExtendedBigDecimal::zero(),
1089+
_ => false,
1090+
});
9381091
}
9391092

9401093
#[test]

tests/by-util/test_printf.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,12 @@ fn partial_integer() {
826826
.fails_with_code(1)
827827
.stdout_is("42 is a lot")
828828
.stderr_is("printf: '42x23': value not completely converted\n");
829+
830+
new_ucmd!()
831+
.args(&["%d is not %s", "0xwa", "a lot"])
832+
.fails_with_code(1)
833+
.stdout_is("0 is not a lot")
834+
.stderr_is("printf: '0xwa': value not completely converted\n");
829835
}
830836

831837
#[test]
@@ -1280,6 +1286,80 @@ fn float_switch_switch_decimal_scientific() {
12801286
.stdout_only("1e-05");
12811287
}
12821288

1289+
#[test]
1290+
fn float_arg_zero() {
1291+
new_ucmd!()
1292+
.args(&["%f", "0."])
1293+
.succeeds()
1294+
.stdout_only("0.000000");
1295+
1296+
new_ucmd!()
1297+
.args(&["%f", ".0"])
1298+
.succeeds()
1299+
.stdout_only("0.000000");
1300+
1301+
new_ucmd!()
1302+
.args(&["%f", ".0e100000"])
1303+
.succeeds()
1304+
.stdout_only("0.000000");
1305+
}
1306+
1307+
#[test]
1308+
fn float_arg_invalid() {
1309+
// Just a dot fails.
1310+
new_ucmd!()
1311+
.args(&["%f", "."])
1312+
.fails()
1313+
.stdout_is("0.000000")
1314+
.stderr_contains("expected a numeric value");
1315+
1316+
new_ucmd!()
1317+
.args(&["%f", "-."])
1318+
.fails()
1319+
.stdout_is("0.000000")
1320+
.stderr_contains("expected a numeric value");
1321+
1322+
// Just an exponent indicator fails.
1323+
new_ucmd!()
1324+
.args(&["%f", "e"])
1325+
.fails()
1326+
.stdout_is("0.000000")
1327+
.stderr_contains("expected a numeric value");
1328+
1329+
// No digit but only exponent fails
1330+
new_ucmd!()
1331+
.args(&["%f", ".e12"])
1332+
.fails()
1333+
.stdout_is("0.000000")
1334+
.stderr_contains("expected a numeric value");
1335+
1336+
// No exponent partially fails
1337+
new_ucmd!()
1338+
.args(&["%f", "123e"])
1339+
.fails()
1340+
.stdout_is("123.000000")
1341+
.stderr_contains("value not completely converted");
1342+
1343+
// Nothing past `0x` parses as zero
1344+
new_ucmd!()
1345+
.args(&["%f", "0x"])
1346+
.fails()
1347+
.stdout_is("0.000000")
1348+
.stderr_contains("value not completely converted");
1349+
1350+
new_ucmd!()
1351+
.args(&["%f", "0x."])
1352+
.fails()
1353+
.stdout_is("0.000000")
1354+
.stderr_contains("value not completely converted");
1355+
1356+
new_ucmd!()
1357+
.args(&["%f", "0xp12"])
1358+
.fails()
1359+
.stdout_is("0.000000")
1360+
.stderr_contains("value not completely converted");
1361+
}
1362+
12831363
#[test]
12841364
fn float_arg_with_whitespace() {
12851365
new_ucmd!()

0 commit comments

Comments
 (0)