From db280b6e9f530ba7691debb2a5b84d7772d699fe Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 2 Feb 2025 11:07:31 -0500 Subject: [PATCH] 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 --- src/uucore/src/lib/features/format/escape.rs | 47 +++++++++++--------- src/uucore/src/lib/features/format/mod.rs | 10 ++++- tests/by-util/test_printf.rs | 9 ++++ 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/uucore/src/lib/features/format/escape.rs b/src/uucore/src/lib/features/format/escape.rs index 9420507f3e3..cd4ea658c39 100644 --- a/src/uucore/src/lib/features/format/escape.rs +++ b/src/uucore/src/lib/features/format/escape.rs @@ -94,43 +94,50 @@ fn parse_unicode(input: &mut &[u8], digits: u8) -> Option { 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 { if let [c, new_rest @ ..] = rest { // This is for the \NNN syntax for octal sequences. // Note that '0' is intentionally omitted because that // would be the \0NNN syntax. if let b'1'..=b'7' = c { if let Some(parsed) = parse_code(rest, Base::Oct) { - return EscapedChar::Byte(parsed); + return Ok(EscapedChar::Byte(parsed)); } } *rest = new_rest; match c { - b'\\' => EscapedChar::Byte(b'\\'), - b'"' => EscapedChar::Byte(b'"'), - b'a' => EscapedChar::Byte(b'\x07'), - b'b' => EscapedChar::Byte(b'\x08'), - b'c' => EscapedChar::End, - b'e' => EscapedChar::Byte(b'\x1b'), - b'f' => EscapedChar::Byte(b'\x0c'), - b'n' => EscapedChar::Byte(b'\n'), - b'r' => EscapedChar::Byte(b'\r'), - b't' => EscapedChar::Byte(b'\t'), - b'v' => EscapedChar::Byte(b'\x0b'), + b'\\' => Ok(EscapedChar::Byte(b'\\')), + b'"' => Ok(EscapedChar::Byte(b'"')), + b'a' => Ok(EscapedChar::Byte(b'\x07')), + b'b' => Ok(EscapedChar::Byte(b'\x08')), + b'c' => Ok(EscapedChar::End), + b'e' => Ok(EscapedChar::Byte(b'\x1b')), + b'f' => Ok(EscapedChar::Byte(b'\x0c')), + b'n' => Ok(EscapedChar::Byte(b'\n')), + b'r' => Ok(EscapedChar::Byte(b'\r')), + b't' => Ok(EscapedChar::Byte(b'\t')), + b'v' => Ok(EscapedChar::Byte(b'\x0b')), b'x' => { if let Some(c) = parse_code(rest, Base::Hex) { - EscapedChar::Byte(c) + Ok(EscapedChar::Byte(c)) } else { - EscapedChar::Backslash(b'x') + Err(EscapeError {}) } } - b'0' => EscapedChar::Byte(parse_code(rest, Base::Oct).unwrap_or(b'\0')), - b'u' => EscapedChar::Char(parse_unicode(rest, 4).unwrap_or('\0')), - b'U' => EscapedChar::Char(parse_unicode(rest, 8).unwrap_or('\0')), - c => EscapedChar::Backslash(*c), + b'0' => Ok(EscapedChar::Byte( + parse_code(rest, Base::Oct).unwrap_or(b'\0'), + )), + 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 { - EscapedChar::Byte(b'\\') + Ok(EscapedChar::Byte(b'\\')) } } diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index 6a09b32e2a9..5707a2177d6 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -67,6 +67,8 @@ pub enum FormatError { InvalidPrecision(String), /// The format specifier ends with a %, as in `%f%`. EndsWithPercent(Vec), + /// The escape sequence `\x` appears without a literal hexadecimal value. + MissingHex, } impl Error for FormatError {} @@ -105,6 +107,7 @@ impl Display for FormatError { Self::IoError(_) => write!(f, "io error"), Self::NoMoreArguments => write!(f, "no more arguments"), 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 @ ..] => { 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 @ ..] => { current = rest; @@ -224,7 +230,7 @@ pub fn parse_escape_only(fmt: &[u8]) -> impl Iterator + '_ { [] => None, [b'\\', rest @ ..] => { current = rest; - Some(parse_escape_code(&mut current)) + Some(parse_escape_code(&mut current).unwrap_or(EscapedChar::Backslash(b'x'))) } [c, rest @ ..] => { current = rest; diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 044817214c0..86334d567ea 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -46,6 +46,15 @@ fn escaped_hex() { 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] fn escaped_octal() { new_ucmd!().args(&["\\101"]).succeeds().stdout_only("A");