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.
This commit is contained in:
Mitchell Hashimoto
2026-07-09 19:54:57 -07:00
parent 5bc6588e43
commit 0ff4e41b22

View File

@@ -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;