From 682bd64ab16178e13a2d3150e6e5a881c5b13143 Mon Sep 17 00:00:00 2001 From: AZERDSQ Date: Wed, 8 Jul 2026 14:59:43 +0200 Subject: [PATCH] fix(find): stop panicking on multi-byte UTF-8 directory names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The directory-display truncation in find_cmd's output formatter sliced a String at a raw byte offset (dir.len() - 47) with no check that the offset lands on a UTF-8 character boundary. A directory path longer than 50 bytes whose 47-bytes-from-the-end cut point falls inside a multi-byte character (e.g. an accented letter like 'é') panics with "byte index is not a char boundary" instead of printing a truncated path. Extracted the truncation into a small `truncate_dir_display` helper and walk forward from the target byte offset to the next valid char boundary before slicing, instead of slicing blindly. The loop always terminates at dir.len(), which is always a boundary, so this cannot infinite-loop or panic. Adds three unit tests, including an exact reproduction of the directory name from the reported crash ("Les Plongées de Juliette/5-Pipeline/Versions-imprimables") verified to place the naive cut point mid-character before this fix. Note: the original report mentioned the panic only appeared alongside an unknown flag like -print. That didn't reproduce as a separate condition here -- any directory name long enough to hit this truncation path panics regardless of flags, which this fix covers. --- src/cmds/system/find_cmd.rs | 50 +++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/cmds/system/find_cmd.rs b/src/cmds/system/find_cmd.rs index 0e8d2eebb9..549574171e 100644 --- a/src/cmds/system/find_cmd.rs +++ b/src/cmds/system/find_cmd.rs @@ -26,6 +26,23 @@ fn glob_match_inner(pat: &[u8], name: &[u8]) -> bool { } } +/// Shorten a directory path for display, keeping the trailing ~47 bytes +/// prefixed with "...". Byte-index truncation can land inside a +/// multi-byte UTF-8 character (e.g. an accented letter), so this walks +/// forward from the target byte offset to the next valid char boundary +/// instead of slicing blindly -- the loop always terminates at +/// `dir.len()`, which is always a boundary. +fn truncate_dir_display(dir: &str) -> String { + if dir.len() <= 50 { + return dir.to_string(); + } + let target = dir.len() - 47; + let start = (target..=dir.len()) + .find(|&i| dir.is_char_boundary(i)) + .unwrap_or(dir.len()); + format!("...{}", &dir[start..]) +} + /// Parsed arguments from either native find or RTK find syntax. #[derive(Debug)] struct FindArgs { @@ -322,11 +339,7 @@ pub fn run( } let files_in_dir = &by_dir[dir]; - let dir_display = if dir.len() > 50 { - format!("...{}", &dir[dir.len() - 47..]) - } else { - dir.clone() - }; + let dir_display = truncate_dir_display(dir); let remaining_budget = max_results - displayed; if files_in_dir.len() <= remaining_budget { @@ -393,6 +406,33 @@ mod tests { values.iter().map(|s| s.to_string()).collect() } + // --- truncate_dir_display unit tests --- + + #[test] + fn truncate_dir_display_does_not_panic_on_multibyte_boundary() { + // Exact reproduction from the reported panic: a directory name + // whose 47-bytes-from-the-end cut point lands inside 'é'. + let dir = "Les Plongées de Juliette/5-Pipeline/Versions-imprimables"; + assert!(dir.len() > 50); + // Must not panic, and must not lose or corrupt UTF-8 data. + let result = truncate_dir_display(dir); + assert!(result.starts_with("...")); + assert!(result.is_char_boundary(0)); + } + + #[test] + fn truncate_dir_display_passes_through_short_paths() { + assert_eq!(truncate_dir_display("src/cmds"), "src/cmds"); + } + + #[test] + fn truncate_dir_display_truncates_long_ascii_paths() { + let dir = "a/".repeat(30); // 60 bytes, all ASCII + let result = truncate_dir_display(&dir); + assert!(result.starts_with("...")); + assert!(result.len() <= dir.len()); + } + // --- glob_match unit tests --- #[test]