mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 03:09:42 +00:00
Misc runtime safety fixes (#13275)
Runtime safety violating scenarios found by GPT 5.6. Verified each one manually. See each commit. I'm going to keep searching so not going to merge this yet.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -2120,8 +2120,8 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
|
||||
// Calculate our new x/y
|
||||
const row = if (row_req == 0) 1 else row_req;
|
||||
const col = if (col_req == 0) 1 else col_req;
|
||||
const x = @min(params.x_max, col + params.x_offset) -| 1;
|
||||
const y = @min(params.y_max, row + params.y_offset) -| 1;
|
||||
const x = @min(params.x_max, col +| params.x_offset) -| 1;
|
||||
const y = @min(params.y_max, row +| params.y_offset) -| 1;
|
||||
|
||||
// If the y is unchanged then this is fast pointer math
|
||||
if (y == self.screens.active.cursor.y) {
|
||||
@@ -3816,6 +3816,24 @@ fn clearDirty(t: *Terminal) void {
|
||||
t.screens.active.pages.clearDirty();
|
||||
}
|
||||
|
||||
test "Terminal: setCursorPos saturates overflowing origin offsets" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
t.scrolling_region = .{
|
||||
.top = 2,
|
||||
.bottom = 7,
|
||||
.left = 3,
|
||||
.right = 8,
|
||||
};
|
||||
t.modes.set(.origin, true);
|
||||
|
||||
t.setCursorPos(std.math.maxInt(usize), std.math.maxInt(usize));
|
||||
try testing.expectEqual(@as(size.CellCountInt, 8), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(size.CellCountInt, 7), t.screens.active.cursor.y);
|
||||
}
|
||||
|
||||
test "Terminal: input with no control characters" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 40, .rows = 40 });
|
||||
|
||||
@@ -56,6 +56,10 @@ pub const ActiveSearch = struct {
|
||||
// Clear our previous sliding window
|
||||
self.window.clearAndRetainCapacity();
|
||||
|
||||
// An empty needle represents an inactive search and has no overlap
|
||||
// or history to load.
|
||||
if (self.window.needle.len == 0) return null;
|
||||
|
||||
// First up, add enough pages to cover the active area.
|
||||
var rem: usize = list.rows;
|
||||
var node_ = list.pages.last;
|
||||
|
||||
@@ -176,6 +176,11 @@ pub const SlidingWindow = struct {
|
||||
/// or `append()`. If the caller wants to retain the flattened highlight
|
||||
/// then they should clone it.
|
||||
pub fn next(self: *SlidingWindow) ?FlattenedHighlight {
|
||||
// An empty needle represents an inactive search. Searching for it
|
||||
// would otherwise produce a zero-length match, which highlight()
|
||||
// cannot represent because its end offset is inclusive.
|
||||
if (self.needle.len == 0) return null;
|
||||
|
||||
const slices = slices: {
|
||||
// If we have less data then the needle then we can't possibly match
|
||||
const data_len = self.data.len();
|
||||
@@ -704,6 +709,25 @@ test "SlidingWindow empty on init" {
|
||||
try testing.expectEqual(0, w.meta.len());
|
||||
}
|
||||
|
||||
test "SlidingWindow empty needle has no matches" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var w: SlidingWindow = try .init(alloc, .forward, "");
|
||||
defer w.deinit();
|
||||
|
||||
var s = try Screen.init(alloc, .{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
});
|
||||
defer s.deinit();
|
||||
try s.testWriteString("hello");
|
||||
|
||||
_ = try w.append(s.pages.pages.first.?);
|
||||
try testing.expectEqual(null, w.next());
|
||||
}
|
||||
|
||||
test "SlidingWindow single append" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -134,6 +134,7 @@ pub const ViewportSearch = struct {
|
||||
// Add enough overlap to cover needle.len - 1 bytes (if it
|
||||
// exists) so we can cover the overlap. We only do this for the
|
||||
// soft-wrapped prior pages.
|
||||
const overlap_len = self.window.needle.len -| 1;
|
||||
var node_ = fingerprint.nodes[0].prev;
|
||||
var added: usize = 0;
|
||||
while (node_) |node| : (node_ = node.prev) {
|
||||
@@ -142,7 +143,7 @@ pub const ViewportSearch = struct {
|
||||
// wrap so this should be fine.
|
||||
const appended = try self.window.appendIfWrapped(node) orelse break;
|
||||
added += appended.content_len;
|
||||
if (added >= self.window.needle.len - 1) break;
|
||||
if (added >= overlap_len) break;
|
||||
}
|
||||
|
||||
// We can use our fingerprint nodes to initialize our sliding
|
||||
@@ -162,7 +163,7 @@ pub const ViewportSearch = struct {
|
||||
while (node_) |node| : (node_ = node.next) {
|
||||
const appended = try self.window.append(node);
|
||||
added += appended.content_len;
|
||||
if (added >= self.window.needle.len - 1) break;
|
||||
if (added >= overlap_len) break;
|
||||
|
||||
// If this row doesn't wrap, then we can quit
|
||||
if (!appended.last_row_wrapped) break;
|
||||
|
||||
Reference in New Issue
Block a user