use justerm_core::{
Color, CursorShape, DecodeError, Engine, Frame, FrameKind, ScrollOp, decode, encode,
};
use std::num::NonZeroU32;
const COLS_AT: usize = 5;
const ROWS_AT: usize = 7;
fn patch_unique(buf: &mut [u8], needle: &[u8], patch: &[u8]) {
assert_eq!(
needle.len(),
patch.len(),
"patch must not resize the buffer"
);
let hits: Vec<usize> = buf
.windows(needle.len())
.enumerate()
.filter(|(_, w)| *w == needle)
.map(|(i, _)| i)
.collect();
assert_eq!(
hits.len(),
1,
"expected exactly one occurrence of {needle:?} in the wire buffer, found {}",
hits.len()
);
buf[hits[0]..hits[0] + patch.len()].copy_from_slice(patch);
}
#[test]
fn a_span_reaching_past_the_declared_width_is_rejected() {
let mut e = Engine::new(9, 2);
e.feed(b"abcdefghi");
let frame = e.frame();
assert_eq!(
(frame.spans[0].left, frame.spans[0].right),
(0, 8),
"the fixture only means something if the span really spans all nine columns"
);
let mut bytes = encode(&frame);
assert_eq!(decode(&bytes), Ok(frame), "unpatched, it round-trips");
bytes[COLS_AT..COLS_AT + 2].copy_from_slice(&4u16.to_le_bytes());
assert_eq!(
decode(&bytes),
Err(DecodeError::BadSpan),
"a span whose last column is past the frame's own width is malformed input"
);
}
#[test]
fn widening_the_span_within_the_frame_is_still_rejected_on_length_not_on_bounds() {
let mut e = Engine::new(9, 2);
e.feed(b"ab"); e.reset_damage(); e.feed(b"\x1b[2;1Hxyz"); let frame = e.frame();
let narrow = frame
.spans
.iter()
.min_by_key(|s| s.right - s.left)
.expect("the frame has spans");
let (line, left, right) = (narrow.line, narrow.left, narrow.right);
assert!(
right + 1 < frame.cols,
"fixture: the span must be narrower than the frame ({right} vs {} cols) for the \
widening to stay inside it",
frame.cols
);
let triple = |r: u16| {
let mut v = line.to_le_bytes().to_vec();
v.extend_from_slice(&left.to_le_bytes());
v.extend_from_slice(&r.to_le_bytes());
v
};
let mut bytes = encode(&frame);
patch_unique(&mut bytes, &triple(right), &triple(frame.cols - 1));
assert_eq!(
decode(&bytes),
Err(DecodeError::Truncated),
"a widening that stays inside the frame is a length failure, not a bounds failure"
);
let mut bytes = encode(&frame);
patch_unique(&mut bytes, &triple(right), &triple(255));
assert_eq!(decode(&bytes), Err(DecodeError::BadSpan));
}
#[test]
fn a_span_on_a_line_past_the_declared_height_is_rejected() {
let mut e = Engine::new(4, 3);
e.feed(b"aaaa\r\nbbbb\r\ncccc");
let frame = e.frame();
assert!(
frame.spans.iter().any(|s| s.line == 2),
"the fixture needs a span on the row the shrunk header will exclude"
);
let mut bytes = encode(&frame);
bytes[ROWS_AT..ROWS_AT + 2].copy_from_slice(&1u16.to_le_bytes());
assert_eq!(decode(&bytes), Err(DecodeError::BadSpan));
}
fn frame_with_scroll(top: usize, bottom: usize) -> Frame {
Frame {
cols: 8,
rows: 3,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: Some(ScrollOp {
top,
bottom,
count: 1,
}),
spans: vec![],
link_table: vec![],
overlay: Default::default(),
}
}
#[test]
fn a_scroll_region_reaching_past_the_declared_height_is_rejected() {
let frame = frame_with_scroll(0, 5);
assert_eq!(decode(&encode(&frame)), Err(DecodeError::BadSpan));
}
#[test]
fn an_empty_scroll_region_is_left_alone_even_when_it_points_past_the_end() {
let frame = frame_with_scroll(5, 0);
assert_eq!(decode(&encode(&frame)), Ok(frame));
}
fn frame_with_one_ucolor() -> (justerm_core::Frame, u32) {
let mut e = Engine::new(9, 2);
e.feed(b"\x1b[4m\x1b[58:2::9:9:9mABCDEFGHI");
let frame = e.frame();
let packed = justerm_core::encode_color(Color::Rgb(9, 9, 9));
(frame, packed)
}
#[test]
fn an_underline_colour_keyed_past_the_end_of_its_span_is_rejected() {
let (frame, packed) = frame_with_one_ucolor();
assert!(
frame.spans[0].ucolors.contains_key(&0),
"fixture: column 0 carries the colour"
);
let mut bytes = encode(&frame);
let mut needle = 0u16.to_le_bytes().to_vec();
needle.extend_from_slice(&packed.to_le_bytes());
let mut patch = 9999u16.to_le_bytes().to_vec();
patch.extend_from_slice(&packed.to_le_bytes());
patch_unique(&mut bytes, &needle, &patch);
assert_eq!(
decode(&bytes),
Err(DecodeError::BadSpan),
"a group entry keyed outside the span it rides on is malformed input"
);
}
#[test]
fn a_combining_cluster_keyed_past_the_end_of_its_span_is_rejected() {
let mut e = Engine::new(4, 2);
e.feed("ae\u{0301}f".as_bytes());
let frame = e.frame();
assert!(
!frame.spans[0].combining.is_empty(),
"fixture: the span carries a combining cluster"
);
let mut bytes = encode(&frame);
let mut needle = 1u16.to_le_bytes().to_vec();
needle.extend_from_slice(&1u32.to_le_bytes());
needle.extend_from_slice(&0x0301u32.to_le_bytes());
let mut patch = 9999u16.to_le_bytes().to_vec();
patch.extend_from_slice(&1u32.to_le_bytes());
patch.extend_from_slice(&0x0301u32.to_le_bytes());
patch_unique(&mut bytes, &needle, &patch);
assert_eq!(decode(&bytes), Err(DecodeError::BadSpan));
}
#[test]
fn a_hyperlink_reference_keyed_past_the_end_of_its_span_is_rejected() {
let mut e = Engine::new(6, 2);
e.feed(b"\x1b]8;;http://x\x07ab\x1b]8;;\x07");
let frame = e.frame();
assert_eq!(
frame.spans[0].links.keys().copied().collect::<Vec<_>>(),
vec![0, 1],
"fixture: two adjacent linked cells, which is what makes the group findable below"
);
let mut bytes = encode(&frame);
let entries = |first_col: u16| {
let mut v = 2u32.to_le_bytes().to_vec();
v.extend_from_slice(&first_col.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
v
};
patch_unique(&mut bytes, &entries(0), &entries(9999));
assert_eq!(decode(&bytes), Err(DecodeError::BadSpan));
}
fn frame_with_keys_outside_their_span() -> Frame {
let mut e = Engine::new(9, 2);
e.feed(b"\x1b]8;;http://x\x07\x1b[4m\x1b[58:5:1mABCDEFGHI\x1b]8;;\x07");
let mut frame = e.frame();
let idx = NonZeroU32::new(1).expect("1 is non-zero");
let span = &mut frame.spans[0];
span.ucolors.clear();
span.ucolors.insert(2, Color::Indexed(1)); span.ucolors.insert(65539, Color::Indexed(1)); span.combining.clear();
span.combining.insert(65540, vec!['\u{0301}']);
span.links.clear();
span.links.insert(1, idx); span.links.insert(65541, idx); frame
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "past the end of its")]
fn encode_names_the_producer_of_a_key_outside_its_span() {
let _ = encode(&frame_with_keys_outside_their_span());
}
#[cfg(not(debug_assertions))]
#[test]
fn encode_drops_a_key_outside_its_span_rather_than_narrowing_it_onto_a_live_cell() {
let frame = frame_with_keys_outside_their_span();
let decoded = decode(&encode(&frame)).expect("the encoded frame is still decodable");
let dspan = &decoded.spans[0];
assert_eq!(
dspan.ucolors.keys().copied().collect::<Vec<_>>(),
vec![2],
"the in-range entry survives and the impossible one is not written at all"
);
assert!(
dspan.combining.is_empty(),
"the same rule holds for every group, not just the one the issue named"
);
let armed: Vec<(usize, char)> = dspan
.cells
.iter()
.enumerate()
.filter(|(_, c)| c.is_ucolored())
.map(|(i, c)| (i, c.c()))
.collect();
assert_eq!(
armed,
vec![(2, 'C')],
"and no unrelated live glyph is coloured — 65539 used to land on 'D' at column 3"
);
assert!(
!dspan.cells.iter().any(|c| c.is_combined()),
"nor marked as carrying a cluster it does not have"
);
assert_eq!(
dspan.links.keys().copied().collect::<Vec<_>>(),
vec![1],
"the link group answers the same way — every group, or the rule is not a rule"
);
let linked: Vec<usize> = dspan
.cells
.iter()
.enumerate()
.filter(|(_, c)| c.is_linked())
.map(|(i, _)| i)
.collect();
assert_eq!(
linked,
vec![1],
"and 65541 does not narrow onto column 5 and link a glyph that carries no URI"
);
}
#[test]
fn no_frame_this_engine_produces_is_rejected_by_its_own_decoder() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
let mut raws: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.expect("fixtures dir")
.filter_map(|e| {
let p = e.expect("dir entry").path();
(p.extension().is_some_and(|x| x == "raw")).then_some(p)
})
.collect();
raws.sort();
assert!(
raws.len() >= 6,
"the corpus is the evidence: {} captures found",
raws.len()
);
let mut frames = 0usize;
for path in &raws {
let bytes = std::fs::read(path).expect("capture");
for (cols, rows) in [(80usize, 24usize), (40, 10), (2, 2)] {
let mut e = Engine::new(cols, rows);
for chunk in bytes.chunks(512) {
e.feed(chunk);
let frame = e.frame();
assert!(
decode(&encode(&frame)).is_ok(),
"{} at {cols}x{rows} produced a frame its own decoder rejects",
path.display()
);
frames += 1;
}
}
}
assert!(frames >= 250, "only {frames} frames checked");
}
#[test]
fn an_engine_frame_with_every_group_populated_is_still_a_wire_fixed_point() {
let mut e = Engine::new(12, 3);
e.feed(b"\x1b]8;;http://x\x07li\x1b]8;;\x07");
e.feed("\x1b[4m\x1b[58:5:2mne\u{0301}s\r\n".as_bytes());
e.feed(b"second row\r\n");
let frame = e.frame();
let span = &frame.spans[0];
assert!(
!span.links.is_empty() && !span.ucolors.is_empty(),
"fixture: the frame must actually exercise the groups it claims to"
);
assert!(
frame.spans.iter().any(|s| !s.combining.is_empty()),
"fixture: and a combining cluster somewhere in the frame"
);
assert_eq!(decode(&encode(&frame)), Ok(frame));
}