Skip to content

Commit 90ddb66

Browse files
committed
fix(resume): use char-aware string truncation for UTF-8 safety
1 parent 9b13828 commit 90ddb66

File tree

1 file changed

+42
-3
lines changed

1 file changed

+42
-3
lines changed

src/cortex-resume/src/resume_picker.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,15 @@ fn format_relative_time(time: &chrono::DateTime<chrono::Utc>) -> String {
153153
}
154154
}
155155

156-
/// Truncate string to fit width.
156+
/// Truncate string to fit width, handling multi-byte UTF-8 safely.
157157
fn truncate_string(s: &str, width: usize) -> String {
158-
if s.len() <= width {
158+
// Count actual character width, not byte length
159+
let char_count = s.chars().count();
160+
if char_count <= width {
159161
s.to_string()
160162
} else if width > 3 {
161-
format!("{}...", &s[..width - 3])
163+
let truncated: String = s.chars().take(width - 3).collect();
164+
format!("{}...", truncated)
162165
} else {
163166
s.chars().take(width).collect()
164167
}
@@ -176,4 +179,40 @@ mod tests {
176179
let hour_ago = now - chrono::Duration::hours(2);
177180
assert_eq!(format_relative_time(&hour_ago), "2h ago");
178181
}
182+
183+
#[test]
184+
fn test_truncate_string_ascii() {
185+
// Short string, no truncation needed
186+
assert_eq!(truncate_string("hello", 10), "hello");
187+
188+
// Exact fit
189+
assert_eq!(truncate_string("hello", 5), "hello");
190+
191+
// Needs truncation
192+
assert_eq!(truncate_string("hello world", 8), "hello...");
193+
194+
// Very short width
195+
assert_eq!(truncate_string("hello", 3), "hel");
196+
assert_eq!(truncate_string("hello", 2), "he");
197+
}
198+
199+
#[test]
200+
fn test_truncate_string_utf8() {
201+
// UTF-8 multi-byte characters (Japanese)
202+
let japanese = "こんにちは世界"; // 7 chars
203+
assert_eq!(truncate_string(japanese, 10), japanese); // No truncation
204+
assert_eq!(truncate_string(japanese, 7), japanese); // Exact fit
205+
assert_eq!(truncate_string(japanese, 6), "こんに..."); // Truncated (3 chars + ...)
206+
207+
// UTF-8 with emoji
208+
let emoji = "Hello 🌍🌎🌏"; // 9 chars: H,e,l,l,o, ,🌍,🌎,🌏
209+
assert_eq!(truncate_string(emoji, 20), emoji); // No truncation
210+
assert_eq!(truncate_string(emoji, 9), emoji); // Exact fit (9 chars)
211+
assert_eq!(truncate_string(emoji, 8), "Hello..."); // Truncated (5 chars + ...)
212+
213+
// Mixed UTF-8 and ASCII
214+
let mixed = "路径/path/文件"; // 11 chars
215+
assert_eq!(truncate_string(mixed, 20), mixed); // No truncation
216+
assert_eq!(truncate_string(mixed, 8), "路径/pa..."); // Truncated
217+
}
179218
}

0 commit comments

Comments
 (0)