find: -printf: don't panic on a multibyte char after an octal escape

`\NNN` octal escapes were parsed by slicing a fixed 3 bytes off the
format string, which panics when fewer than 3 octal digits are followed
by a multibyte character (e.g. `-printf '\0€'`): the 3-byte slice lands
inside the multibyte char and trips a char-boundary assertion.

Parse the octal escape from the leading octal digits only (1 to 3, all
ASCII) and advance by their byte length. This also fixes `\1`..`\7`,
which previously fell through to the single-character escape table and
errored instead of being treated as octal, matching GNU find.
This commit is contained in:
weili
2026-06-09 22:23:39 +02:00
committed by Sylvestre Ledru
parent e2d84e98d5
commit 53980035af
+40 -10
View File
@@ -167,16 +167,21 @@ impl FormatStringParser<'_> {
// Try parsing an octal sequence first.
let first = self.front()?;
if first.is_digit(OCTAL_RADIX) {
if let Ok(code) = self.peek(OCTAL_LEN).and_then(|octal| {
u32::from_str_radix(octal, OCTAL_RADIX).map_err(std::convert::Into::into)
}) {
// safe to unwrap: .peek() already succeeded above.
let octal = self.advance_by(OCTAL_LEN).unwrap();
return match char::from_u32(code) {
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
None => Err(format!("Invalid character value: \\{octal}").into()),
};
}
// A GNU octal escape is 1 to 3 octal digits. Consume only the leading
// octal digits (which are ASCII), rather than slicing a fixed 3 bytes
// that can land inside a following multibyte char.
let octal: String = self
.string
.chars()
.take(OCTAL_LEN)
.take_while(|c| c.is_digit(OCTAL_RADIX))
.collect();
let code = u32::from_str_radix(&octal, OCTAL_RADIX)?;
self.advance_by(octal.len())?;
return match char::from_u32(code) {
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
None => Err(format!("Invalid character value: \\{octal}").into()),
};
}
self.advance_one()?;
@@ -688,6 +693,31 @@ mod tests {
assert!(FormatString::parse("\\").is_err());
}
#[test]
fn test_parse_octal_escape_before_multibyte_char() {
assert_eq!(
FormatString::parse("\\0€").unwrap().components,
vec![
FormatComponent::Literal("\0".to_owned()),
FormatComponent::Literal("".to_owned()),
]
);
assert_eq!(
FormatString::parse("\\1😀").unwrap().components,
vec![
FormatComponent::Literal("\u{1}".to_owned()),
FormatComponent::Literal("😀".to_owned()),
]
);
assert_eq!(
FormatString::parse("\\00é").unwrap().components,
vec![
FormatComponent::Literal("\0".to_owned()),
FormatComponent::Literal("é".to_owned()),
]
);
}
#[test]
fn test_parse_formatting() {
fn unaligned_directive(directive: FormatDirective) -> FormatComponent {