From 0ff4e41b22b4da6e5603dd69570eabf4083c8ede Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Jul 2026 19:54:57 -0700 Subject: [PATCH] terminal: fix pin wrapping at row boundaries Pin.leftWrap and rightWrap calculated the destination using the remainder after consuming the current row. When that remainder was an exact multiple of the column count, rightWrap subtracted one from zero and leftWrap produced a column equal to the width. Dereferencing either pin could panic. A maximum usize offset on a one-column page also overflowed the row count. Base the row and column calculations on the remainder minus one. This maps exact multiples to the final cell of the correct row and keeps the maximum offset calculation in range so traversal reports overflow normally. --- src/terminal/PageList.zig | 64 +++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 167654d61..04b58b99b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -6251,13 +6251,14 @@ pub const Pin = struct { if (n <= remaining_in_row) return self.left(n); const extra_after_remaining = n - remaining_in_row; - - const rows_off = 1 + extra_after_remaining / cols; + const rows_off = 1 + (extra_after_remaining - 1) / cols; switch (self.upOverflow(rows_off)) { .offset => |v| { var result = v; - result.x = @intCast(cols - extra_after_remaining % cols); + result.x = @intCast( + cols - 1 - (extra_after_remaining - 1) % cols, + ); return result; }, .overflow => return null, @@ -6278,13 +6279,12 @@ pub const Pin = struct { if (n <= remaining_in_row) return self.right(n); const extra_after_remaining = n - remaining_in_row; - - const rows_off = 1 + extra_after_remaining / cols; + const rows_off = 1 + (extra_after_remaining - 1) / cols; switch (self.downOverflow(rows_off)) { .offset => |v| { var result = v; - result.x = @intCast(extra_after_remaining % cols - 1); + result.x = @intCast((extra_after_remaining - 1) % cols); return result; }, .overflow => return null, @@ -6451,6 +6451,58 @@ fn growColdPagesForTest(self: *PageList, count: usize) !void { } } +test "PageList Pin rightWrap exact row multiple" { + const testing = std.testing; + + var s = try init(testing.allocator, 10, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .x = 5, .y = 0 } }).?; + const wrapped = start.rightWrap(14).?; + _ = wrapped.rowAndCell(); + + try testing.expectEqual( + point.Point{ .active = .{ .x = 9, .y = 1 } }, + s.pointFromPin(.active, wrapped), + ); +} + +test "PageList Pin leftWrap exact row multiple" { + const testing = std.testing; + + var s = try init(testing.allocator, 10, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .x = 5, .y = 2 } }).?; + const wrapped = start.leftWrap(15).?; + _ = wrapped.rowAndCell(); + + try testing.expectEqual( + point.Point{ .active = .{ .x = 0, .y = 1 } }, + s.pointFromPin(.active, wrapped), + ); +} + +test "PageList Pin rightWrap maximum distance" { + const testing = std.testing; + + var s = try init(testing.allocator, 1, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .y = 0 } }).?; + try testing.expectEqual(null, start.rightWrap(std.math.maxInt(usize))); +} + +test "PageList Pin leftWrap maximum distance" { + const testing = std.testing; + + var s = try init(testing.allocator, 1, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .y = 2 } }).?; + try testing.expectEqual(null, start.leftWrap(std.math.maxInt(usize))); +} + test "PageList incremental compression skips visible history" { const testing = std.testing;