diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index fe32239ba..29108a17a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -723,9 +723,14 @@ fn printCell( self.screens.active.cursor.page_row, spacer_cell[0..1], ); + + // If we're near the left edge, a wide char may have + // wrapped from the previous row, leaving a spacer_head + // at the end of that row. Clear it so the previous row + // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); - head_cell.wide = .narrow; + if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } }, @@ -744,9 +749,13 @@ fn printCell( self.screens.active.cursor.page_row, wide_cell[0..1], ); + // If we're near the left edge, a wide char may have + // wrapped from the previous row, leaving a spacer_head + // at the end of that row. Clear it so the previous row + // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); - head_cell.wide = .narrow; + if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } }, @@ -3341,6 +3350,44 @@ test "Terminal: print over wide char at 0,0" { try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); } +test "Terminal: print over wide char at col 0 corrupts previous row" { + // Crash found by AFL++ fuzzer (afl-out/stream/default/crashes/id:000002). + // + // printCell, when overwriting a wide cell with a narrow cell at x<=1 + // and y>0, sets the last cell of the previous row to .narrow — even + // when that cell is a .spacer_tail rather than a .spacer_head. This + // orphans the .wide cell at cols-2. + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + // Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal). + for (0..10) |_| try t.print(0x4E2D); + + // Move cursor to row 1, col 0 (on top of a wide char) and print a + // narrow character. This triggers printCell's .wide branch which + // corrupts row 0's last cell: col 9 changes from .spacer_tail to + // .narrow, orphaning the .wide at col 8. + t.setCursorPos(2, 1); + try t.print('A'); + + // Row 1, col 0 should be narrow (we just overwrote the wide char). + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + try testing.expectEqual(Cell.Wide.narrow, list_cell.cell.wide); + } + // Row 0, col 8 should still be .wide (the last wide char on the row). + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 8, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); + } + // Row 0, col 9 must remain .spacer_tail to pair with the .wide at col 8. + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); + } +} + test "Terminal: print over wide spacer tail" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);