From 38aa2ee34d28d6e946a7203bb5813f18443412ea Mon Sep 17 00:00:00 2001 From: Christopher Overbeck Date: Thu, 17 Sep 2026 09:57:36 -0500 Subject: [PATCH] Avoid slicing text inside multi-byte characters Right-to-left line reordering walked each atom's byte range and sliced the line at every byte, panicking when a neutral multi-byte character such as a guillemet preceded the first strong character. Walk characters instead. FIND/FINDALL located the REPLACE keyword in a Unicode-uppercased copy and sliced the original with that offset; uppercasing can change byte length (dotless i, ligatures). Uppercase ASCII only, which keeps offsets aligned. FILLET and CHAMFER inline R/D shorthands now test the original text before slicing past the first character. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/commands/inquiry.rs | 22 ++++++++++++++++++++-- src/entities/text_support.rs | 12 ++++++++++-- src/modules/draw/modify/fillet.rs | 4 ++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 161a6c28c..eb33b4ca0 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -1302,8 +1302,9 @@ impl OpenCADStudio { let all_mode = cmd.starts_with("FINDALL"); let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); - // Split at " REPLACE " keyword (case-insensitive) - let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ") + // Split at " REPLACE " keyword (case-insensitive). ASCII case + // mapping keeps byte offsets valid for slicing `rest`. + let (search, replacement) = if let Some(pos) = rest.to_ascii_uppercase().find(" REPLACE ") { (&rest[..pos], Some(rest[pos + 9..].trim())) } else { @@ -2386,3 +2387,20 @@ mod align_selected_bounds_tests { assert_eq!(line_start_x(&app, b), 2.0, "b must keep its offset from a, not collapse onto it"); } } + +#[cfg(test)] +mod find_replace_command_tests { + use super::*; + + /// Unicode case mapping can change a string's byte length (`ı` uppercases + /// to `I`), so the REPLACE keyword split must not slice with offsets + /// taken from a case-mapped copy. + #[test] + fn find_replace_accepts_search_text_whose_case_mapping_changes_length() { + for cmd in ["FIND ı REPLACE x", "FINDALL fi REPLACE fi", "FIND ʼn replace n"] { + let mut app = OpenCADStudio::new_for_test(); + let i = app.active_tab; + let _ = app.dispatch_inquiry(cmd, i); + } + } +} diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index f26f7d52a..c115c4e18 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -798,9 +798,11 @@ pub fn reorder_line_atoms(atoms: Vec, is_rtl: bool) -> Vec= 0.0 { @@ -2109,7 +2109,7 @@ impl CadCommand for ChamferCommand { return Some(CmdResult::NeedPoint); } // "D 5.0" or "D 5.0 3.0" inline shorthand - if upper.starts_with('D') { + if t.starts_with(['d', 'D']) { let body = t[1..].trim(); let parts: Vec = body .split_whitespace()