Skip syntax highlighting for lines over 4 KiB (#34796)

## Why

Very long individual lines can make syntax highlighting consume excessive CPU or memory even when the overall input remains below the existing size and line-count limits.

## What changed

- Fall back to plain, unstyled text when any input line exceeds 4 KiB.
- Keep the existing 512 KiB total-size and 10,000-line guardrails.

## Testing

- Cover highlighting fallback and text preservation for a long single-line Bash command.
- Snapshot rendering of a command that exceeds the per-line limit.

GitOrigin-RevId: ba02997331a294d8a32ca89ee06acb12dd3bbd83
This commit is contained in:
Eric Traut authored and copyberry committed 2026-07-22 17:38:11 +00:00
1 parent 2c49493b5b
commit 5381edb133
3 files changed
+59 -4

No files matched your search

+25 -4
View File
@@ -18,9 +18,10 @@
//! swap/snapshot the theme for live preview. All highlighting functions read
//! the theme via `theme_lock()`.
//!
//! **Guardrails:** inputs exceeding 512 KB or 10 000 lines are rejected early
//! (returns `None`) to prevent pathological CPU/memory usage. Callers must
//! fall back to plain unstyled text.
//! **Guardrails:** inputs exceeding 512 KB or 10 000 lines, or containing an
//! individual line longer than 4 KiB, are rejected early (returns `None`) to
//! prevent pathological CPU/memory usage. Callers must fall back to plain
//! unstyled text.
use ratatui::style::Color as RtColor;
use ratatui::style::Modifier;
@@ -581,6 +582,9 @@ const MAX_HIGHLIGHT_BYTES: usize = 512 * 1024;
/// Skip highlighting for inputs with more than 10,000 lines.
const MAX_HIGHLIGHT_LINES: usize = 10_000;
/// Skip highlighting when an individual line is longer than 4 KiB.
pub(crate) const MAX_HIGHLIGHT_LINE_BYTES: usize = 4 * 1024;
/// Check whether an input exceeds the safe highlighting limits.
///
/// Callers that highlight content in a loop (e.g. per diff-line) should
@@ -611,7 +615,12 @@ fn highlight_to_line_spans_with_theme(
// Bail out early for oversized inputs to avoid excessive resource usage.
// Count actual lines (not newline bytes) to avoid an off-by-one when
// the input does not end with a newline.
if code.len() > MAX_HIGHLIGHT_BYTES || code.lines().count() > MAX_HIGHLIGHT_LINES {
if code.len() > MAX_HIGHLIGHT_BYTES
|| code.lines().count() > MAX_HIGHLIGHT_LINES
|| code
.lines()
.any(|line| line.len() > MAX_HIGHLIGHT_LINE_BYTES)
{
return None;
}
@@ -1139,6 +1148,18 @@ mod tests {
assert!(result.is_none(), "oversized input should fall back to None");
}
#[test]
fn long_single_line_bash_skips_highlighting_and_preserves_text() {
let token = "eHh4".repeat(MAX_HIGHLIGHT_LINE_BYTES / 4 + 1);
let code = format!("printf %s {token} | base64 -d >/dev/null");
assert!(highlight_code_to_styled_spans(&code, "bash").is_none());
assert_eq!(
highlight_code_to_lines(&code, "bash"),
vec![Line::from(code)]
);
}
#[test]
fn highlight_many_lines_falls_back() {
// Input exceeding MAX_HIGHLIGHT_LINES should return None.