mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 00:21:46 +00:00
terminal/snapshot: harden grapheme suffix decode
Grapheme suffix decoding accepted U+0000 even though zero is the native empty-cell sentinel. It also appended codepoints one at a time and, when page capacity failed after a prefix had been stored, left that truncated prefix attached to the cell. Hostile snapshots could therefore introduce invalid cluster data or render a partial cluster depending on allocator capacity. Ignore NUL alongside invalid scalar values. If any append runs out of native capacity, remove the prefix already attached and consume the rest of the declared suffix without applying it, making delivery atomic at the cluster level. Cover NUL input and a failure after 128 accepted suffix codepoints.
This commit is contained in:
@@ -1067,20 +1067,25 @@ fn decodeGraphemes(
|
||||
break :target .{ .row = row, .cell = cell };
|
||||
};
|
||||
|
||||
// Always consume every declared codepoint. Invalid scalars and
|
||||
// suffixes that exceed the native capacity are dropped
|
||||
// independently without affecting the rest of the grid.
|
||||
// Always consume every declared codepoint. Invalid scalars and NUL are
|
||||
// not meaningful grapheme suffix components and are ignored. If native
|
||||
// capacity is exhausted, remove any prefix already attached so the
|
||||
// cell never exposes a truncated cluster.
|
||||
var accept = target != null;
|
||||
for (0..cp_count) |_| {
|
||||
const cp = try io.readInt(reader, u32);
|
||||
if (!accept) continue;
|
||||
if (!validScalar(cp)) continue;
|
||||
if (cp == 0 or !validScalar(cp)) continue;
|
||||
|
||||
page.appendGrapheme(
|
||||
target.?.row,
|
||||
target.?.cell,
|
||||
@intCast(cp),
|
||||
) catch {
|
||||
if (target.?.cell.hasGrapheme()) {
|
||||
page.clearGrapheme(target.?.cell);
|
||||
page.updateRowGraphemeFlag(target.?.row);
|
||||
}
|
||||
accept = false;
|
||||
};
|
||||
}
|
||||
@@ -1752,8 +1757,9 @@ test "grid drops undeliverable grapheme entries" {
|
||||
// Valid entry with one invalid scalar dropped from within it.
|
||||
try io.writeInt(&writer, u16, 0);
|
||||
try io.writeInt(&writer, u16, 0);
|
||||
try io.writeInt(&writer, u16, 3);
|
||||
try io.writeInt(&writer, u16, 4);
|
||||
try io.writeInt(&writer, u32, 0x0301);
|
||||
try io.writeInt(&writer, u32, 0);
|
||||
try io.writeInt(&writer, u32, 0xD800);
|
||||
try io.writeInt(&writer, u32, 0x0302);
|
||||
// Duplicate entry for the same cell is consumed and dropped.
|
||||
@@ -1794,6 +1800,48 @@ test "grid drops undeliverable grapheme entries" {
|
||||
try testing.expect(!page.getRowAndCell(3, 0).cell.hasGrapheme());
|
||||
}
|
||||
|
||||
test "grid drops a complete grapheme when capacity fails mid-cluster" {
|
||||
const testing = std.testing;
|
||||
var page = try TerminalPage.init(.{
|
||||
.cols = 1,
|
||||
.rows = 1,
|
||||
.grapheme_bytes = 16,
|
||||
});
|
||||
defer page.deinit();
|
||||
var style_remap = try StyleRemap.init(testing.allocator);
|
||||
defer style_remap.deinit(testing.allocator);
|
||||
var hyperlink_remap = try HyperlinkRemap.init(testing.allocator);
|
||||
defer hyperlink_remap.deinit(testing.allocator);
|
||||
|
||||
var payload: [1024]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&payload);
|
||||
try writer.writeByte(@bitCast(Row{ .cell_width = .eight }));
|
||||
try io.writeInt(&writer, u16, 1);
|
||||
try io.writeInt(&writer, u64, @bitCast(Cell{ .kind = 1, .content = 'x' }));
|
||||
try io.writeInt(&writer, u32, 1);
|
||||
try io.writeInt(&writer, u16, 0);
|
||||
try io.writeInt(&writer, u16, 0);
|
||||
// BitmapAllocator rounds this capacity to 64 four-codepoint chunks. The
|
||||
// 129th suffix needs 33 new chunks while the old 32-chunk slice is still
|
||||
// live, forcing the append's atomic replacement allocation to fail.
|
||||
try io.writeInt(&writer, u16, 129);
|
||||
for (0..129) |i| try io.writeInt(
|
||||
&writer,
|
||||
u32,
|
||||
@intCast(0x0300 + i),
|
||||
);
|
||||
|
||||
var reader: std.Io.Reader = .fixed(writer.buffered());
|
||||
try decode(&page, &reader, &style_remap, &hyperlink_remap);
|
||||
try page.verifyIntegrity(testing.allocator);
|
||||
|
||||
const cell = page.getRowAndCell(0, 0);
|
||||
try testing.expectEqual(@as(u21, 'x'), cell.cell.codepoint());
|
||||
try testing.expect(!cell.cell.hasGrapheme());
|
||||
try testing.expect(!cell.row.grapheme);
|
||||
try testing.expectEqual(@as(usize, 0), page.graphemeCount());
|
||||
}
|
||||
|
||||
test "grid encodes rows at their narrowest width" {
|
||||
const testing = std.testing;
|
||||
var page = try TerminalPage.init(.{
|
||||
|
||||
Reference in New Issue
Block a user