Repository: uutils/coreutils (language: rust)

Issue:
printf: renders \x as a literal but should error due to missing hexadecimal number
Steps to reproduce:
```
printf '\x'
```

What happens now: uutils `printf` renders this as a literal, as-is:
```
\x
```

What I expected to happen: GNU `printf` terminates with an error message:
```
printf: missing hexadecimal number in escape
```

Notes: this is causing a failure in the GNU test file `tests/printf/printf.sh`.


Discussion / hints:

Fix patch:
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> {
     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 {
         // 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<u8>),
+    /// 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<Item = EscapedChar> + '_ {
         [] => 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;


Test patch:
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");