const MAX_SNIPPET_BYTES: usize = 120;
pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn line_snippet(src: &[u8], byte_offset: usize) -> Option<String> {
if byte_offset >= src.len() {
return None;
}
let line_start = src[..byte_offset]
.iter()
.rposition(|&b| b == b'\n')
.map_or(0, |p| p + 1);
let line_end = src[byte_offset..]
.iter()
.position(|&b| b == b'\n')
.map_or(src.len(), |p| byte_offset + p);
let line = std::str::from_utf8(&src[line_start..line_end]).ok()?;
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.len() > MAX_SNIPPET_BYTES {
Some(format!(
"{}...",
truncate_at_char_boundary(trimmed, MAX_SNIPPET_BYTES)
))
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::{line_snippet, truncate_at_char_boundary};
#[test]
fn truncate_short_string_unchanged() {
assert_eq!(truncate_at_char_boundary("hello", 10), "hello");
assert_eq!(truncate_at_char_boundary("", 10), "");
}
#[test]
fn truncate_zero_max_returns_empty() {
assert_eq!(truncate_at_char_boundary("hello", 0), "");
assert_eq!(truncate_at_char_boundary("ਖਖਖ", 0), "");
}
#[test]
fn truncate_ascii_clean_at_byte_max() {
assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
}
#[test]
fn truncate_inside_multibyte_rounds_down() {
let s = "abcdਖef";
assert_eq!(truncate_at_char_boundary(s, 5), "abcd");
assert_eq!(truncate_at_char_boundary(s, 6), "abcd");
assert_eq!(truncate_at_char_boundary(s, 7), "abcdਖ");
}
#[test]
fn truncate_devanagari_gherkin_regex_literal() {
let regex_body = "stream.match(/(機能|功能|フィーチャ|기능|โครงหลัก|ความสามารถ|ความต้องการทางธุรกิจ|ಹೆಚ್ಚಳ|గుణము|ਮੁਹਾਂਦਰਾ|ਨਕਸ਼ ਨੁਹਾਰ|".to_string();
assert!(regex_body.len() > 256);
let truncated = truncate_at_char_boundary(®ex_body, 256);
assert!(regex_body.is_char_boundary(truncated.len()));
assert!(truncated.len() <= 256);
}
#[test]
fn ascii_short_line_returned_verbatim() {
let src = b"let x = 1;\nlet y = 2;\n";
assert_eq!(line_snippet(src, 0).as_deref(), Some("let x = 1;"));
assert_eq!(line_snippet(src, 11).as_deref(), Some("let y = 2;"));
}
#[test]
fn blank_line_returns_none() {
let src = b"x\n \n";
assert_eq!(line_snippet(src, 2), None);
}
#[test]
fn out_of_range_returns_none() {
let src = b"abc";
assert_eq!(line_snippet(src, 10), None);
}
#[test]
fn long_ascii_line_truncated_at_120_with_ellipsis() {
let long = "x".repeat(200);
let src = long.as_bytes();
let out = line_snippet(src, 0).unwrap();
assert!(out.ends_with("..."));
assert_eq!(out.len(), 123); }
#[test]
fn long_line_with_multibyte_char_at_boundary_does_not_panic() {
let prefix = "a".repeat(119);
let line = format!("expect(text).to eq('{}тест огромный текст ' * 50)", prefix);
let line = format!("{} {}", line, "тест ".repeat(50));
let src = line.as_bytes();
let out = line_snippet(src, 0).unwrap();
assert!(out.ends_with("..."));
assert!(std::str::from_utf8(out.as_bytes()).is_ok());
let stripped = out.strip_suffix("...").unwrap();
assert!(stripped.is_char_boundary(stripped.len()));
}
#[test]
fn truncation_at_emoji_boundary_safe() {
let mut line = "x".repeat(118);
line.push_str("🦀🦀🦀🦀🦀"); let src = line.as_bytes();
assert!(src.len() > 120);
let out = line_snippet(src, 0).unwrap();
assert!(std::str::from_utf8(out.as_bytes()).is_ok());
assert!(out.ends_with("..."));
}
#[test]
fn picks_correct_line_for_offset_in_middle() {
let src = b"first\nsecond line here\nthird\n";
assert_eq!(line_snippet(src, 6).as_deref(), Some("second line here"));
}
}