mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: prevent UTF-8 panics in byte-index string truncation
Replace unsafe `&s[..n]` patterns with `floor_char_boundary(s, n)` at 3 production code sites where the truncation index could land mid-multibyte character, panicking on non-ASCII input: - src/llm/nearai_chat.rs: API response truncation in error message - src/cli/memory.rs: memory content display truncation - src/cli/config.rs: config value display truncation All 3 sites operate on external or user-supplied strings that may contain non-ASCII characters. The existing `crate::util::floor_char_boundary` utility (used at 18 other call sites) walks back to the nearest char boundary, preventing the panic. Adds regression test with multi-byte characters (combining accents and 4-byte emoji) for truncate_content. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
+2
-1
@@ -127,7 +127,8 @@ async fn list_settings(
|
||||
}
|
||||
|
||||
let display_value = if value.len() > 60 {
|
||||
format!("{}...", &value[..57])
|
||||
let end = crate::util::floor_char_boundary(&value, 57);
|
||||
format!("{}...", &value[..end])
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
+17
-1
@@ -256,7 +256,8 @@ fn truncate_content(s: &str, max_len: usize) -> String {
|
||||
if s.len() <= max_len {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}...", &s[..max_len])
|
||||
let end = crate::util::floor_char_boundary(s, max_len);
|
||||
format!("{}...", &s[..end])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,4 +293,19 @@ mod tests {
|
||||
assert_eq!(truncate_content("hello", 10), "hello");
|
||||
assert_eq!(truncate_content("hello world", 5), "hello...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_content_multibyte_does_not_panic() {
|
||||
// "cafe\u{0301}" = "café" where é is e + combining accent (2 bytes for accent)
|
||||
// Slicing at byte 5 would land inside the combining character
|
||||
let s = "caf\u{00e9} au lait"; // café = 5 bytes (é is 2 bytes)
|
||||
let result = truncate_content(s, 4); // byte 4 is inside é
|
||||
assert!(result.ends_with("..."));
|
||||
assert!(!result.is_empty());
|
||||
|
||||
// 4-byte emoji: slicing mid-emoji must not panic
|
||||
let emoji = "Hi \u{1F600} there"; // 😀 is 4 bytes
|
||||
let result = truncate_content(emoji, 4); // byte 4 is inside 😀
|
||||
assert!(result.ends_with("..."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ impl NearAiChatProvider {
|
||||
provider: "nearai_chat".to_string(),
|
||||
reason: format!(
|
||||
"No model names found in response: {}",
|
||||
&response_text[..response_text.len().min(300)]
|
||||
&response_text[..crate::util::floor_char_boundary(&response_text, 300)]
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user