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

printf: error on missing hexadecial escape value

Change `printf` to correctly terminate with an error message when an
escape sequence starts with `\x` but doesn't include a literal
hexadecimal value after. For example, before this commit,

    printf '\x'

would output `\x`, but after this commit, it terminates with an error
message,

    printf: missing hexadecimal number in escape

Fixes #7097
This commit is contained in:
Jeffrey Finkelstein 2025-02-02 11:07:31 -05:00
parent c2505841e0
commit db280b6e9f
3 changed files with 44 additions and 22 deletions

View file

@ -94,43 +94,50 @@ fn parse_unicode(input: &mut &[u8], digits: u8) -> Option<char> {
char::from_u32(ret) char::from_u32(ret)
} }
pub fn parse_escape_code(rest: &mut &[u8]) -> EscapedChar { /// Represents an invalid escape sequence.
#[derive(Debug)]
pub struct EscapeError {}
/// Parse an escape sequence, like `\n` or `\xff`, etc.
pub fn parse_escape_code(rest: &mut &[u8]) -> Result<EscapedChar, EscapeError> {
if let [c, new_rest @ ..] = rest { if let [c, new_rest @ ..] = rest {
// This is for the \NNN syntax for octal sequences. // This is for the \NNN syntax for octal sequences.
// Note that '0' is intentionally omitted because that // Note that '0' is intentionally omitted because that
// would be the \0NNN syntax. // would be the \0NNN syntax.
if let b'1'..=b'7' = c { if let b'1'..=b'7' = c {
if let Some(parsed) = parse_code(rest, Base::Oct) { if let Some(parsed) = parse_code(rest, Base::Oct) {
return EscapedChar::Byte(parsed); return Ok(EscapedChar::Byte(parsed));
} }
} }
*rest = new_rest; *rest = new_rest;
match c { match c {
b'\\' => EscapedChar::Byte(b'\\'), b'\\' => Ok(EscapedChar::Byte(b'\\')),
b'"' => EscapedChar::Byte(b'"'), b'"' => Ok(EscapedChar::Byte(b'"')),
b'a' => EscapedChar::Byte(b'\x07'), b'a' => Ok(EscapedChar::Byte(b'\x07')),
b'b' => EscapedChar::Byte(b'\x08'), b'b' => Ok(EscapedChar::Byte(b'\x08')),
b'c' => EscapedChar::End, b'c' => Ok(EscapedChar::End),
b'e' => EscapedChar::Byte(b'\x1b'), b'e' => Ok(EscapedChar::Byte(b'\x1b')),
b'f' => EscapedChar::Byte(b'\x0c'), b'f' => Ok(EscapedChar::Byte(b'\x0c')),
b'n' => EscapedChar::Byte(b'\n'), b'n' => Ok(EscapedChar::Byte(b'\n')),
b'r' => EscapedChar::Byte(b'\r'), b'r' => Ok(EscapedChar::Byte(b'\r')),
b't' => EscapedChar::Byte(b'\t'), b't' => Ok(EscapedChar::Byte(b'\t')),
b'v' => EscapedChar::Byte(b'\x0b'), b'v' => Ok(EscapedChar::Byte(b'\x0b')),
b'x' => { b'x' => {
if let Some(c) = parse_code(rest, Base::Hex) { if let Some(c) = parse_code(rest, Base::Hex) {
EscapedChar::Byte(c) Ok(EscapedChar::Byte(c))
} else { } else {
EscapedChar::Backslash(b'x') Err(EscapeError {})
} }
} }
b'0' => EscapedChar::Byte(parse_code(rest, Base::Oct).unwrap_or(b'\0')), b'0' => Ok(EscapedChar::Byte(
b'u' => EscapedChar::Char(parse_unicode(rest, 4).unwrap_or('\0')), parse_code(rest, Base::Oct).unwrap_or(b'\0'),
b'U' => EscapedChar::Char(parse_unicode(rest, 8).unwrap_or('\0')), )),
c => EscapedChar::Backslash(*c), b'u' => Ok(EscapedChar::Char(parse_unicode(rest, 4).unwrap_or('\0'))),
b'U' => Ok(EscapedChar::Char(parse_unicode(rest, 8).unwrap_or('\0'))),
c => Ok(EscapedChar::Backslash(*c)),
} }
} else { } else {
EscapedChar::Byte(b'\\') Ok(EscapedChar::Byte(b'\\'))
} }
} }

View file

@ -67,6 +67,8 @@ pub enum FormatError {
InvalidPrecision(String), InvalidPrecision(String),
/// The format specifier ends with a %, as in `%f%`. /// The format specifier ends with a %, as in `%f%`.
EndsWithPercent(Vec<u8>), EndsWithPercent(Vec<u8>),
/// The escape sequence `\x` appears without a literal hexadecimal value.
MissingHex,
} }
impl Error for FormatError {} impl Error for FormatError {}
@ -105,6 +107,7 @@ impl Display for FormatError {
Self::IoError(_) => write!(f, "io error"), Self::IoError(_) => write!(f, "io error"),
Self::NoMoreArguments => write!(f, "no more arguments"), Self::NoMoreArguments => write!(f, "no more arguments"),
Self::InvalidArgument(_) => write!(f, "invalid argument"), Self::InvalidArgument(_) => write!(f, "invalid argument"),
Self::MissingHex => write!(f, "missing hexadecimal number in escape"),
} }
} }
} }
@ -181,7 +184,10 @@ pub fn parse_spec_and_escape(
} }
[b'\\', rest @ ..] => { [b'\\', rest @ ..] => {
current = rest; current = rest;
Some(Ok(FormatItem::Char(parse_escape_code(&mut current)))) Some(match parse_escape_code(&mut current) {
Ok(c) => Ok(FormatItem::Char(c)),
Err(_) => Err(FormatError::MissingHex),
})
} }
[c, rest @ ..] => { [c, rest @ ..] => {
current = rest; current = rest;
@ -224,7 +230,7 @@ pub fn parse_escape_only(fmt: &[u8]) -> impl Iterator<Item = EscapedChar> + '_ {
[] => None, [] => None,
[b'\\', rest @ ..] => { [b'\\', rest @ ..] => {
current = rest; current = rest;
Some(parse_escape_code(&mut current)) Some(parse_escape_code(&mut current).unwrap_or(EscapedChar::Backslash(b'x')))
} }
[c, rest @ ..] => { [c, rest @ ..] => {
current = rest; current = rest;

View file

@ -46,6 +46,15 @@ fn escaped_hex() {
new_ucmd!().args(&["\\x41"]).succeeds().stdout_only("A"); new_ucmd!().args(&["\\x41"]).succeeds().stdout_only("A");
} }
#[test]
fn test_missing_escaped_hex_value() {
new_ucmd!()
.arg(r"\x")
.fails()
.code_is(1)
.stderr_only("printf: missing hexadecimal number in escape\n");
}
#[test] #[test]
fn escaped_octal() { fn escaped_octal() {
new_ucmd!().args(&["\\101"]).succeeds().stdout_only("A"); new_ucmd!().args(&["\\101"]).succeeds().stdout_only("A");