fix(migrate): truncate on a char boundary in validate::truncate

truncate() sliced source.chunk (arbitrary UTF-8 from the source SQLite
database) at a raw byte offset. A multi-byte character straddling byte
40 panics with "byte index 40 is not a char boundary" instead of
producing the mismatch diagnostic the code exists to report — and this
is the default validate_hdf5 path, not test-only. Cut on the nearest
char boundary at or before 40 instead.

INT-10
This commit is contained in:
ClawHDF5 Coding Agent
2026-08-17 00:27:48 +00:00
parent 7314971fe7
commit 55c3737130
+30 -1
View File
@@ -144,7 +144,8 @@ fn truncate(s: &str) -> String {
if s.len() <= 40 { if s.len() <= 40 {
s.to_string() s.to_string()
} else { } else {
format!("{}…", &s[..40]) let cut = s.char_indices().nth(40).map(|(i, _)| i).unwrap_or(s.len());
format!("{}…", &s[..cut])
} }
} }
@@ -161,3 +162,31 @@ fn sample_indices(n: usize, full: bool) -> Vec<usize> {
idx.dedup(); idx.dedup();
idx idx
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_short_string_unchanged() {
assert_eq!(truncate("hello"), "hello");
}
/// A multi-byte character straddling byte offset 40 must not panic a
/// byte-index slice — this is arbitrary UTF-8 chunk text from an
/// untrusted source database, not test-only input.
#[test]
fn truncate_multibyte_char_at_boundary_does_not_panic() {
// 39 ASCII bytes then a 4-byte emoji straddling the byte-40 cut point.
let s = format!("{}{}", "a".repeat(39), "😀".repeat(5));
let result = truncate(&s);
assert!(result.ends_with('…'));
assert!(result.chars().count() < s.chars().count());
}
#[test]
fn truncate_exactly_at_limit_unchanged() {
let s = "a".repeat(40);
assert_eq!(truncate(&s), s);
}
}