pub const DEFAULT_MAX_LINES: usize = 2000;
pub const DEFAULT_MAX_BYTES: usize = 50 * 1024;
pub const GREP_MAX_LINE_LENGTH: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TruncatedBy {
Lines,
Bytes,
None,
}
#[derive(Debug, Clone)]
pub struct TruncationResult {
pub content: String,
pub truncated: bool,
pub truncated_by: TruncatedBy,
pub total_lines: usize,
pub total_bytes: usize,
pub output_lines: usize,
pub output_bytes: usize,
pub last_line_partial: bool,
pub first_line_exceeds_limit: bool,
pub max_lines: usize,
pub max_bytes: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineRange {
pub content: String,
pub selected_lines: usize,
pub total_lines: usize,
pub offset_beyond_end: bool,
}
pub fn line_range(bytes: &[u8], offset: Option<i64>, limit: Option<i64>) -> LineRange {
let text = String::from_utf8_lossy(bytes).to_string();
let all: Vec<&str> = text.split('\n').collect();
let total_lines = all.len();
let start = match offset {
Some(o) => (o - 1).max(0) as usize,
None => 0,
};
if start >= total_lines {
return LineRange {
offset_beyond_end: true,
total_lines,
..Default::default()
};
}
let end = match limit {
Some(l) => (start + l.max(0) as usize).min(total_lines),
None => total_lines,
};
LineRange {
content: all[start..end].join("\n"),
selected_lines: end - start,
total_lines,
offset_beyond_end: false,
}
}
pub fn append_bounded(sink: &mut Vec<u8>, data: &[u8], max_bytes: usize, truncated: &mut bool) {
if *truncated {
return;
}
let room = max_bytes.saturating_sub(sink.len());
if data.len() > room {
sink.extend_from_slice(&data[..room]);
*truncated = true;
} else {
sink.extend_from_slice(data);
}
}
pub fn format_size(bytes: usize) -> String {
if bytes < 1024 {
format!("{}B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", bytes as f64 / 1024.0)
} else {
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
}
}
fn split_lines_for_counting(content: &str) -> Vec<&str> {
if content.is_empty() {
return vec![];
}
let mut lines: Vec<&str> = content.split('\n').collect();
if content.ends_with('\n') {
lines.pop();
}
lines
}
pub fn truncate_head(content: &str, options: (Option<usize>, Option<usize>)) -> TruncationResult {
let max_lines = options.0.unwrap_or(DEFAULT_MAX_LINES);
let max_bytes = options.1.unwrap_or(DEFAULT_MAX_BYTES);
let total_bytes = content.len();
let lines = split_lines_for_counting(content);
let total_lines = lines.len();
if total_lines <= max_lines && total_bytes <= max_bytes {
return TruncationResult {
content: content.to_string(),
truncated: false,
truncated_by: TruncatedBy::None,
total_lines,
total_bytes,
output_lines: total_lines,
output_bytes: total_bytes,
last_line_partial: false,
first_line_exceeds_limit: false,
max_lines,
max_bytes,
};
}
let first_line_bytes = lines.first().map(|l| l.len()).unwrap_or(0);
if first_line_bytes > max_bytes {
return TruncationResult {
content: String::new(),
truncated: true,
truncated_by: TruncatedBy::Bytes,
total_lines,
total_bytes,
output_lines: 0,
output_bytes: 0,
last_line_partial: false,
first_line_exceeds_limit: true,
max_lines,
max_bytes,
};
}
let mut output_lines_arr: Vec<&str> = Vec::new();
let mut output_bytes_count = 0usize;
let mut truncated_by = TruncatedBy::Lines;
for (i, line) in lines.iter().enumerate() {
if i >= max_lines {
break;
}
let line_bytes = line.len() + if i > 0 { 1 } else { 0 };
if output_bytes_count + line_bytes > max_bytes {
truncated_by = TruncatedBy::Bytes;
break;
}
output_lines_arr.push(line);
output_bytes_count += line_bytes;
}
if output_lines_arr.len() >= max_lines && output_bytes_count <= max_bytes {
truncated_by = TruncatedBy::Lines;
}
let output_content = output_lines_arr.join("\n");
let final_output_bytes = output_content.len();
TruncationResult {
content: output_content,
truncated: true,
truncated_by,
total_lines,
total_bytes,
output_lines: output_lines_arr.len(),
output_bytes: final_output_bytes,
last_line_partial: false,
first_line_exceeds_limit: false,
max_lines,
max_bytes,
}
}
pub fn truncate_tail(content: &str, options: (Option<usize>, Option<usize>)) -> TruncationResult {
let max_lines = options.0.unwrap_or(DEFAULT_MAX_LINES);
let max_bytes = options.1.unwrap_or(DEFAULT_MAX_BYTES);
let total_bytes = content.len();
let lines = split_lines_for_counting(content);
let total_lines = lines.len();
if total_lines <= max_lines && total_bytes <= max_bytes {
return TruncationResult {
content: content.to_string(),
truncated: false,
truncated_by: TruncatedBy::None,
total_lines,
total_bytes,
output_lines: total_lines,
output_bytes: total_bytes,
last_line_partial: false,
first_line_exceeds_limit: false,
max_lines,
max_bytes,
};
}
let mut output_lines_arr: Vec<String> = Vec::new();
let mut output_bytes_count = 0usize;
let mut truncated_by = TruncatedBy::Lines;
let mut last_line_partial = false;
let mut i = lines.len();
while i > 0 && output_lines_arr.len() < max_lines {
i -= 1;
let line = lines[i];
let line_bytes = line.len() + if !output_lines_arr.is_empty() { 1 } else { 0 };
if output_bytes_count + line_bytes > max_bytes {
truncated_by = TruncatedBy::Bytes;
if output_lines_arr.is_empty() {
let truncated_line = truncate_string_to_bytes_from_end(line, max_bytes);
output_lines_arr.insert(0, truncated_line);
output_bytes_count = output_lines_arr[0].len();
last_line_partial = true;
}
break;
}
output_lines_arr.insert(0, line.to_string());
output_bytes_count += line_bytes;
}
if output_lines_arr.len() >= max_lines && output_bytes_count <= max_bytes {
truncated_by = TruncatedBy::Lines;
}
let output_content = output_lines_arr.join("\n");
let final_output_bytes = output_content.len();
TruncationResult {
content: output_content,
truncated: true,
truncated_by,
total_lines,
total_bytes,
output_lines: output_lines_arr.len(),
output_bytes: final_output_bytes,
last_line_partial,
first_line_exceeds_limit: false,
max_lines,
max_bytes,
}
}
fn truncate_string_to_bytes_from_end(s: &str, max_bytes: usize) -> String {
if max_bytes == 0 {
return String::new();
}
let bytes = s.as_bytes();
let mut used = 0usize;
let mut out_start = bytes.len();
let mut pos = bytes.len();
while pos > 0 {
let mut char_start = pos - 1;
while char_start > 0 && (bytes[char_start] & 0xC0) == 0x80 {
char_start -= 1;
}
let char_bytes = pos - char_start;
if used + char_bytes > max_bytes {
break;
}
used += char_bytes;
out_start = char_start;
pos = char_start;
}
s[out_start..].to_string()
}
pub fn truncate_line(line: &str, max_chars: usize) -> (String, bool) {
if line.chars().count() <= max_chars {
return (line.to_string(), false);
}
let truncated: String = line.chars().take(max_chars).collect();
(format!("{}... [truncated]", truncated), true)
}
pub fn head_bytes(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 tail_bytes(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut start = s.len() - max_bytes;
while start < s.len() && !s.is_char_boundary(start) {
start += 1;
}
&s[start..]
}
pub fn tail_chars(text: &str, max_chars: usize) -> String {
let count = text.chars().count();
if count <= max_chars {
return text.to_string();
}
text.chars().skip(count - max_chars).collect()
}
pub fn truncate_chars(s: &str, max_chars: usize, postfix: Option<&str>, is_trim: bool) -> String {
let text = if is_trim { s.trim() } else { s };
if text.chars().count() <= max_chars {
return text.to_string();
}
let mut out: String = text.chars().take(max_chars).collect();
if let Some(postfix) = postfix {
out.push_str(postfix);
}
out
}
pub fn truncate_chars_within(s: &str, max_chars: usize, postfix: &str) -> String {
if s.chars().count() <= max_chars {
return s.to_string();
}
let quota = max_chars.saturating_sub(postfix.chars().count());
let mut out: String = s.chars().take(quota).collect();
out.push_str(postfix);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_bounded_stops_at_the_cap() {
let mut sink: Vec<u8> = Vec::new();
let mut truncated = false;
append_bounded(&mut sink, b"abc", 5, &mut truncated);
assert!(!truncated);
append_bounded(&mut sink, b"def", 5, &mut truncated);
assert!(truncated);
append_bounded(&mut sink, b"ghi", 5, &mut truncated);
assert_eq!(sink, b"abcde");
}
#[test]
fn line_range_slices_like_local_read() {
let r = line_range(b"l1\nl2\nl3\nl4", Some(2), Some(2));
assert_eq!(r.content, "l2\nl3");
assert_eq!(r.selected_lines, 2);
assert_eq!(r.total_lines, 4);
assert!(!r.offset_beyond_end);
let beyond = line_range(b"l1\nl2", Some(9), None);
assert!(beyond.offset_beyond_end);
assert!(beyond.content.is_empty());
}
#[test]
fn tail_chars_keeps_the_last_characters() {
assert_eq!(tail_chars("abcdef", 3), "def");
assert_eq!(tail_chars("abc", 10), "abc");
assert_eq!(tail_chars("中文内容", 2), "内容");
}
#[test]
fn truncate_chars_respects_utf8_boundaries() {
assert_eq!(truncate_chars("abcdef", 3, None, false), "abc");
assert_eq!(truncate_chars("中文内容", 2, None, false), "中文");
assert_eq!(truncate_chars("short", 99, None, false), "short");
assert_eq!(truncate_chars("", 3, None, false), "");
}
#[test]
fn truncate_chars_appends_postfix_only_when_truncated() {
assert_eq!(truncate_chars("abcdef", 3, Some("…"), false), "abc…");
assert_eq!(truncate_chars("abc", 3, Some("…"), false), "abc");
assert_eq!(truncate_chars("中文内容", 2, Some("..."), false), "中文...");
}
#[test]
fn truncate_chars_trims_only_when_asked() {
assert_eq!(truncate_chars(" abc ", 99, None, true), "abc");
assert_eq!(truncate_chars(" abcdef ", 3, Some("…"), true), "abc…");
assert_eq!(truncate_chars(" abc ", 99, None, false), " abc ");
}
#[test]
fn truncate_chars_within_keeps_total_length() {
assert_eq!(truncate_chars_within("abcdefghij", 5, "…"), "abcd…");
assert_eq!(truncate_chars_within("abcde", 5, "…"), "abcde");
assert_eq!(truncate_chars_within("abcdefghij", 5, "..."), "ab...");
assert_eq!(truncate_chars_within("中文内容", 2, "…"), "中…");
assert_eq!(truncate_chars_within("", 0, "…"), "");
}
}