1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

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
This commit is contained in:
Dorian Péron 2025-04-16 23:32:39 +02:00 committed by GitHub
commit 349e56897c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 246 additions and 13 deletions

View file

@ -450,18 +450,15 @@ pub(crate) fn parse<'a>(
} else { } else {
(Base::Decimal, unsigned) (Base::Decimal, unsigned)
}; };
if rest.is_empty() {
return Err(ExtendedParserError::NotNumeric);
}
// Parse the integral part of the number // Parse the integral part of the number
let mut chars = rest.chars().enumerate().fuse().peekable(); let mut chars = rest.chars().enumerate().fuse().peekable();
let mut digits = BigUint::zero(); let mut digits: Option<BigUint> = None;
let mut scale = 0u64; let mut scale = 0u64;
let mut exponent = BigInt::zero(); let mut exponent: Option<BigInt> = None;
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) { while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
chars.next(); chars.next();
digits = digits * base as u8 + d; digits = Some(digits.unwrap_or_default() * base as u8 + d);
} }
// Parse fractional/exponent part of the number for supported bases. // Parse fractional/exponent part of the number for supported bases.
@ -472,7 +469,7 @@ pub(crate) fn parse<'a>(
chars.next(); chars.next();
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) { while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
chars.next(); chars.next();
(digits, scale) = (digits * base as u8 + d, scale + 1); (digits, scale) = (Some(digits.unwrap_or_default() * base as u8 + d), scale + 1);
} }
} }
@ -487,6 +484,8 @@ pub(crate) fn parse<'a>(
.peek() .peek()
.is_some_and(|&(_, c)| c.to_ascii_lowercase() == exp_char) .is_some_and(|&(_, c)| c.to_ascii_lowercase() == exp_char)
{ {
// Save the iterator position in case we do not parse any exponent.
let save_chars = chars.clone();
chars.next(); chars.next();
let exp_negative = match chars.peek() { let exp_negative = match chars.peek() {
Some((_, '-')) => { Some((_, '-')) => {
@ -501,16 +500,31 @@ pub(crate) fn parse<'a>(
}; };
while let Some(d) = chars.peek().and_then(|&(_, c)| Base::Decimal.digit(c)) { while let Some(d) = chars.peek().and_then(|&(_, c)| Base::Decimal.digit(c)) {
chars.next(); chars.next();
exponent = exponent * 10 + d as i64; exponent = Some(exponent.unwrap_or_default() * 10 + d as i64);
} }
if exp_negative { if let Some(exp) = &exponent {
exponent = -exponent; if exp_negative {
exponent = Some(-exp);
}
} else {
// No exponent actually parsed, reset iterator to return partial match.
chars = save_chars;
} }
} }
} }
// If nothing has been parsed, check if this is a special value, or declare the parsing unsuccessful // If no digit has been parsed, check if this is a special value, or declare the parsing unsuccessful
if let Some((0, _)) = chars.peek() { if digits.is_none() {
// If we trimmed an initial `0x`/`0b`, return a partial match.
if rest != unsigned {
let ebd = if negative {
ExtendedBigDecimal::MinusZero
} else {
ExtendedBigDecimal::zero()
};
return Err(ExtendedParserError::PartialMatch(ebd, &unsigned[1..]));
}
return if target == ParseTarget::Integral { return if target == ParseTarget::Integral {
Err(ExtendedParserError::NotNumeric) Err(ExtendedParserError::NotNumeric)
} else { } else {
@ -518,6 +532,8 @@ pub(crate) fn parse<'a>(
}; };
} }
let mut digits = digits.unwrap();
if let Some((_, ch)) = chars.peek() { if let Some((_, ch)) = chars.peek() {
if let Some(times) = allowed_suffixes if let Some(times) = allowed_suffixes
.iter() .iter()
@ -529,7 +545,8 @@ pub(crate) fn parse<'a>(
} }
} }
let ebd_result = construct_extended_big_decimal(digits, negative, base, scale, exponent); let ebd_result =
construct_extended_big_decimal(digits, negative, base, scale, exponent.unwrap_or_default());
// Return what has been parsed so far. If there are extra characters, mark the // Return what has been parsed so far. If there are extra characters, mark the
// parsing as a partial match. // parsing as a partial match.
@ -625,6 +642,15 @@ mod tests {
i64::extended_parse(&format!("{}", i64::MIN as i128 - 1)), i64::extended_parse(&format!("{}", i64::MIN as i128 - 1)),
Err(ExtendedParserError::Overflow(i64::MIN)) Err(ExtendedParserError::Overflow(i64::MIN))
)); ));
assert!(matches!(
i64::extended_parse(""),
Err(ExtendedParserError::NotNumeric)
));
assert!(matches!(
i64::extended_parse("."),
Err(ExtendedParserError::NotNumeric)
));
} }
#[test] #[test]
@ -659,6 +685,16 @@ mod tests {
Ok(0.15), Ok(0.15),
f64::extended_parse(".150000000000000000000000000231313") f64::extended_parse(".150000000000000000000000000231313")
); );
assert!(matches!(f64::extended_parse("123.15e"),
Err(ExtendedParserError::PartialMatch(f, "e")) if f == 123.15));
assert!(matches!(f64::extended_parse("123.15E"),
Err(ExtendedParserError::PartialMatch(f, "E")) if f == 123.15));
assert!(matches!(f64::extended_parse("123.15e-"),
Err(ExtendedParserError::PartialMatch(f, "e-")) if f == 123.15));
assert!(matches!(f64::extended_parse("123.15e+"),
Err(ExtendedParserError::PartialMatch(f, "e+")) if f == 123.15));
assert!(matches!(f64::extended_parse("123.15e."),
Err(ExtendedParserError::PartialMatch(f, "e.")) if f == 123.15));
assert!(matches!(f64::extended_parse("1.2.3"), assert!(matches!(f64::extended_parse("1.2.3"),
Err(ExtendedParserError::PartialMatch(f, ".3")) if f == 1.2)); Err(ExtendedParserError::PartialMatch(f, ".3")) if f == 1.2));
assert!(matches!(f64::extended_parse("123.15p5"), assert!(matches!(f64::extended_parse("123.15p5"),
@ -811,6 +847,48 @@ mod tests {
ExtendedBigDecimal::extended_parse(&format!("-0e{}", i64::MIN + 2)), ExtendedBigDecimal::extended_parse(&format!("-0e{}", i64::MIN + 2)),
Ok(ExtendedBigDecimal::MinusZero) Ok(ExtendedBigDecimal::MinusZero)
); );
/* Invalid numbers */
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse(".")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("e")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse(".e")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("-e")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("+.e")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("e10")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("e-10")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("-e10")
);
assert_eq!(
Err(ExtendedParserError::NotNumeric),
ExtendedBigDecimal::extended_parse("+e10")
);
} }
#[test] #[test]
@ -831,6 +909,15 @@ mod tests {
// but we can check that the number still gets parsed properly: 0x0.8e5 is 0x8e5 / 16**3 // but we can check that the number still gets parsed properly: 0x0.8e5 is 0x8e5 / 16**3
assert_eq!(Ok(0.555908203125), f64::extended_parse("0x0.8e5")); assert_eq!(Ok(0.555908203125), f64::extended_parse("0x0.8e5"));
assert!(matches!(f64::extended_parse("0x0.1p"),
Err(ExtendedParserError::PartialMatch(f, "p")) if f == 0.0625));
assert!(matches!(f64::extended_parse("0x0.1p-"),
Err(ExtendedParserError::PartialMatch(f, "p-")) if f == 0.0625));
assert!(matches!(f64::extended_parse("0x.1p+"),
Err(ExtendedParserError::PartialMatch(f, "p+")) if f == 0.0625));
assert!(matches!(f64::extended_parse("0x.1p."),
Err(ExtendedParserError::PartialMatch(f, "p.")) if f == 0.0625));
assert_eq!( assert_eq!(
Ok(ExtendedBigDecimal::BigDecimal( Ok(ExtendedBigDecimal::BigDecimal(
BigDecimal::from_str("0.0625").unwrap() BigDecimal::from_str("0.0625").unwrap()
@ -887,6 +974,42 @@ mod tests {
ExtendedBigDecimal::MinusZero ExtendedBigDecimal::MinusZero
)) ))
)); ));
// Not actually hex numbers, but the prefixes look like it.
assert!(matches!(f64::extended_parse("0x"),
Err(ExtendedParserError::PartialMatch(f, "x")) if f == 0.0));
assert!(matches!(f64::extended_parse("0x."),
Err(ExtendedParserError::PartialMatch(f, "x.")) if f == 0.0));
assert!(matches!(f64::extended_parse("0xp"),
Err(ExtendedParserError::PartialMatch(f, "xp")) if f == 0.0));
assert!(matches!(f64::extended_parse("0xp-2"),
Err(ExtendedParserError::PartialMatch(f, "xp-2")) if f == 0.0));
assert!(matches!(f64::extended_parse("0x.p-2"),
Err(ExtendedParserError::PartialMatch(f, "x.p-2")) if f == 0.0));
assert!(matches!(f64::extended_parse("0X"),
Err(ExtendedParserError::PartialMatch(f, "X")) if f == 0.0));
assert!(matches!(f64::extended_parse("-0x"),
Err(ExtendedParserError::PartialMatch(f, "x")) if f == -0.0));
assert!(matches!(f64::extended_parse("+0x"),
Err(ExtendedParserError::PartialMatch(f, "x")) if f == 0.0));
assert!(matches!(f64::extended_parse("-0x."),
Err(ExtendedParserError::PartialMatch(f, "x.")) if f == -0.0));
assert!(matches!(
u64::extended_parse("0x"),
Err(ExtendedParserError::PartialMatch(0, "x"))
));
assert!(matches!(
u64::extended_parse("-0x"),
Err(ExtendedParserError::PartialMatch(0, "x"))
));
assert!(matches!(
i64::extended_parse("0x"),
Err(ExtendedParserError::PartialMatch(0, "x"))
));
assert!(matches!(
i64::extended_parse("-0x"),
Err(ExtendedParserError::PartialMatch(0, "x"))
));
} }
#[test] #[test]
@ -920,6 +1043,27 @@ mod tests {
assert_eq!(Ok(0b1011), u64::extended_parse("+0b1011")); assert_eq!(Ok(0b1011), u64::extended_parse("+0b1011"));
assert_eq!(Ok(-0b1011), i64::extended_parse("-0b1011")); assert_eq!(Ok(-0b1011), i64::extended_parse("-0b1011"));
assert!(matches!(
u64::extended_parse("0b"),
Err(ExtendedParserError::PartialMatch(0, "b"))
));
assert!(matches!(
u64::extended_parse("0b."),
Err(ExtendedParserError::PartialMatch(0, "b."))
));
assert!(matches!(
u64::extended_parse("-0b"),
Err(ExtendedParserError::PartialMatch(0, "b"))
));
assert!(matches!(
i64::extended_parse("0b"),
Err(ExtendedParserError::PartialMatch(0, "b"))
));
assert!(matches!(
i64::extended_parse("-0b"),
Err(ExtendedParserError::PartialMatch(0, "b"))
));
// Binary not allowed for floats // Binary not allowed for floats
assert!(matches!( assert!(matches!(
f64::extended_parse("0b100"), f64::extended_parse("0b100"),
@ -935,6 +1079,15 @@ mod tests {
ebd == ExtendedBigDecimal::zero(), ebd == ExtendedBigDecimal::zero(),
_ => false, _ => false,
}); });
assert!(match ExtendedBigDecimal::extended_parse("0b") {
Err(ExtendedParserError::PartialMatch(ebd, "b")) => ebd == ExtendedBigDecimal::zero(),
_ => false,
});
assert!(match ExtendedBigDecimal::extended_parse("0b.") {
Err(ExtendedParserError::PartialMatch(ebd, "b.")) => ebd == ExtendedBigDecimal::zero(),
_ => false,
});
} }
#[test] #[test]

View file

@ -826,6 +826,12 @@ fn partial_integer() {
.fails_with_code(1) .fails_with_code(1)
.stdout_is("42 is a lot") .stdout_is("42 is a lot")
.stderr_is("printf: '42x23': value not completely converted\n"); .stderr_is("printf: '42x23': value not completely converted\n");
new_ucmd!()
.args(&["%d is not %s", "0xwa", "a lot"])
.fails_with_code(1)
.stdout_is("0 is not a lot")
.stderr_is("printf: '0xwa': value not completely converted\n");
} }
#[test] #[test]
@ -1280,6 +1286,80 @@ fn float_switch_switch_decimal_scientific() {
.stdout_only("1e-05"); .stdout_only("1e-05");
} }
#[test]
fn float_arg_zero() {
new_ucmd!()
.args(&["%f", "0."])
.succeeds()
.stdout_only("0.000000");
new_ucmd!()
.args(&["%f", ".0"])
.succeeds()
.stdout_only("0.000000");
new_ucmd!()
.args(&["%f", ".0e100000"])
.succeeds()
.stdout_only("0.000000");
}
#[test]
fn float_arg_invalid() {
// Just a dot fails.
new_ucmd!()
.args(&["%f", "."])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
new_ucmd!()
.args(&["%f", "-."])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// Just an exponent indicator fails.
new_ucmd!()
.args(&["%f", "e"])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// No digit but only exponent fails
new_ucmd!()
.args(&["%f", ".e12"])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// No exponent partially fails
new_ucmd!()
.args(&["%f", "123e"])
.fails()
.stdout_is("123.000000")
.stderr_contains("value not completely converted");
// Nothing past `0x` parses as zero
new_ucmd!()
.args(&["%f", "0x"])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
new_ucmd!()
.args(&["%f", "0x."])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
new_ucmd!()
.args(&["%f", "0xp12"])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
}
#[test] #[test]
fn float_arg_with_whitespace() { fn float_arg_with_whitespace() {
new_ucmd!() new_ucmd!()