fix-deviant(terminal): no panics on input-reachable terminal states
Intentional deviation from Ghostty 1.3.1 (DEVIANT_FIXES.md entry 10). Two terminal inputs reach states that Zig handles only by assertion (Debug/ReleaseSafe abort; ReleaseFast continues): - Toggling mode 2027 around a BOM before a wide character hits `assert(!b)` in print_grapheme. This repository's release profile keeps debug assertions, so terminal output could crash a release build. The assertion is removed and the path behaves like Zig ReleaseFast. - A grapheme (e.g. a Devanagari conjunct) at the right margin on the bottom row with scrolling margins returned InvalidCoordinate and left a grapheme flag without data, panicking later in the formatter. The grapheme now moves from the cell that actually holds it and no cell is flagged without data. tools/difftest gains a Rust-only panic hunt (`difftest_fuzz panics`, lenient driver): ~60k debug-build cases now run without any panic, and 3000 Zig differential cases show 0 divergences. Test Plan: - cargo test -p ghostty-terminal difftest_mode_2027_toggle_with_bom_does_not_panic difftest_grapheme_wrap_scrolling_margins_keeps_grapheme - cargo run -p difftest --bin difftest_fuzz -- panics
This commit is contained in:
1 parent
7b59457cb8
commit
325288b7ee
5 files changed
+107
-10
No files matched your search
@@ -1078,6 +1078,31 @@ mod tests {
|
||||
assert!(!screen_row_wrap(&t, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn difftest_grapheme_wrap_scrolling_margins_keeps_grapheme() {
|
||||
// Same Devanagari conjunct at the right margin, but on the bottom row
|
||||
// so the wrap scrolls the margin region and the grapheme data travels
|
||||
// with its row. Zig asserts/faults (`moveGrapheme` on a blank cell);
|
||||
// Rust must neither error nor leave a grapheme flag without data.
|
||||
let mut t = term(255, 5);
|
||||
t.vt_stream()
|
||||
.next_slice("\x1b[?2027h\x1b[?69h\x1b[;9s\x1b[24H\t\u{915}\u{94d}\u{937}".as_bytes())
|
||||
.unwrap();
|
||||
let text = t.plain_string().unwrap();
|
||||
assert!(text.contains('\u{915}'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn difftest_mode_2027_toggle_with_bom_does_not_panic() {
|
||||
// Zig asserts `!graphemeBreak` while replaying an existing grapheme
|
||||
// (aborts in safe builds, ignored in ReleaseFast). It must never panic
|
||||
// here: this repository's release profile enables debug assertions.
|
||||
let mut t = term(20, 8);
|
||||
t.vt_stream()
|
||||
.next_slice(b"\x1b[?2027la\xef\xbb\xbf\x1b[?2027h\xe4\xb8\xad")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn difftest_grapheme_wrap_with_margins_moves_prev_cell() {
|
||||
// Devanagari conjunct widening the cell at the last column while a
|
||||
|
||||
@@ -820,8 +820,12 @@ impl Terminal {
|
||||
.expect("prev cell");
|
||||
let cps = page.lookup_grapheme(idx).expect("grapheme data");
|
||||
for &cp2 in cps {
|
||||
let b = ghostty_unicode::grapheme_break(cp1, cp2, &mut state);
|
||||
debug_assert!(!b);
|
||||
// Zig asserts `!graphemeBreak(cp1, cp2)` here, but that
|
||||
// is reachable from input (mode 2027 toggled after the
|
||||
// grapheme was built, e.g. with a BOM), so it must not
|
||||
// panic. ReleaseFast ignores the result; we do too. This
|
||||
// only advances `state`.
|
||||
let _ = ghostty_unicode::grapheme_break(cp1, cp2, &mut state);
|
||||
cp1 = cp2;
|
||||
}
|
||||
}
|
||||
@@ -943,18 +947,31 @@ impl Terminal {
|
||||
let new_idx = page.cell_index(new_x, new_y).expect("new cell");
|
||||
// Zig moves from `prev.cell`, NOT from `old_pin`
|
||||
// (they differ when margins keep the row from
|
||||
// scrolling the cell out of `old_pin`).
|
||||
let old_idx = prev_idx;
|
||||
page.move_grapheme(old_idx, new_idx)?;
|
||||
page.cells[old_idx].set_content_tag(ContentTag::Codepoint);
|
||||
page.cells[new_idx].set_content_tag(ContentTag::CodepointGrapheme);
|
||||
page.rows[new_y].set_grapheme(true);
|
||||
// scrolling the cell out of `old_pin`). When
|
||||
// the wrap DID scroll the margin region, the
|
||||
// data travelled with its row to `old_pin` and
|
||||
// `prev.cell` is blank: Zig then asserts
|
||||
// (page.zig moveGrapheme) or faults in
|
||||
// ReleaseFast. Move from where the data really
|
||||
// is, and never flag a cell without data.
|
||||
let old_idx = if page.cells[prev_idx].has_grapheme() {
|
||||
prev_idx
|
||||
} else {
|
||||
page.cell_index(old_x, old_y).expect("old cell")
|
||||
};
|
||||
if page.cells[old_idx].has_grapheme() {
|
||||
page.move_grapheme(old_idx, new_idx)?;
|
||||
page.cells[old_idx].set_content_tag(ContentTag::Codepoint);
|
||||
page.cells[new_idx]
|
||||
.set_content_tag(ContentTag::CodepointGrapheme);
|
||||
page.rows[new_y].set_grapheme(true);
|
||||
}
|
||||
page.update_row_grapheme_flag(old_y);
|
||||
} else {
|
||||
let cps: Vec<u32> = {
|
||||
let page = self.screen().page(old_pin.node);
|
||||
let old_idx = page.cell_index(old_x, old_y).expect("old cell");
|
||||
page.lookup_grapheme(old_idx).expect("grapheme").to_vec()
|
||||
page.lookup_grapheme(old_idx).unwrap_or_default().to_vec()
|
||||
};
|
||||
for cp in cps {
|
||||
let x = self.screen().cursor.x;
|
||||
|
||||
Reference in new issue
Block a user