mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-05 15:18:40 +00:00
terminal/snapshot: preserve style reader errors
Lenient style decoding previously caught every error, so PAGE and SCREEN could treat truncation or an I/O failure as an invalid semantic style and continue from a corrupted stream position. Add a nullable decoder that discards only invalid style contents while propagating reader failures. Update snapshot callers and cover both semantic fallback and structural failure behavior.
This commit is contained in:
@@ -370,7 +370,7 @@ fn decodePayloadBody(
|
||||
// Styles
|
||||
for (0..header.style_count) |_| {
|
||||
const native_id = try io.readInt(reader, TerminalStyleId);
|
||||
const value: ?TerminalStyle = style.decodeOrDiscard(reader) catch null;
|
||||
const value = try style.decodeOrNull(reader);
|
||||
|
||||
// Zero is reserved for the implicit default. For a duplicate encoded
|
||||
// ID, the first entry wins and this complete entry is simply ignored.
|
||||
@@ -1309,6 +1309,82 @@ test "decode validates dimensions" {
|
||||
}
|
||||
}
|
||||
|
||||
test "decode propagates a style table read failure" {
|
||||
const FailOnceReader = struct {
|
||||
source: std.Io.Reader,
|
||||
interface: std.Io.Reader,
|
||||
bytes_before_failure: usize,
|
||||
failed: bool = false,
|
||||
|
||||
fn init(bytes: []const u8, bytes_before_failure: usize) @This() {
|
||||
return .{
|
||||
.source = .fixed(bytes),
|
||||
.interface = .{
|
||||
.vtable = &.{ .stream = stream },
|
||||
.buffer = &.{},
|
||||
.seek = 0,
|
||||
.end = 0,
|
||||
},
|
||||
.bytes_before_failure = bytes_before_failure,
|
||||
};
|
||||
}
|
||||
|
||||
fn stream(
|
||||
reader: *std.Io.Reader,
|
||||
writer: *std.Io.Writer,
|
||||
limit: std.Io.Limit,
|
||||
) std.Io.Reader.StreamError!usize {
|
||||
const self: *@This() = @fieldParentPtr("interface", reader);
|
||||
if (self.bytes_before_failure == 0 and !self.failed) {
|
||||
self.failed = true;
|
||||
return error.ReadFailed;
|
||||
}
|
||||
|
||||
const read_limit = if (self.failed)
|
||||
limit
|
||||
else
|
||||
limit.min(.limited(self.bytes_before_failure));
|
||||
const n = try self.source.stream(writer, read_limit);
|
||||
if (!self.failed) self.bytes_before_failure -= n;
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
const header: Header = .{
|
||||
.columns = 1,
|
||||
.rows = 1,
|
||||
.style_count = 1,
|
||||
.hyperlink_count = 0,
|
||||
.style_capacity = 8,
|
||||
.hyperlink_capacity_bytes = 0,
|
||||
.grapheme_capacity_bytes = 0,
|
||||
.string_capacity_bytes = 0,
|
||||
};
|
||||
|
||||
// If the style read error is swallowed, its sixteen zero bytes are then
|
||||
// misread as a valid empty grid and the unframed payload appears to decode.
|
||||
var encoded: [Header.len + @sizeOf(TerminalStyleId) + style.len]u8 =
|
||||
@splat(0);
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try header.encode(&writer);
|
||||
try io.writeInt(&writer, TerminalStyleId, 1);
|
||||
try writer.splatByteAll(0, style.len);
|
||||
|
||||
var source = FailOnceReader.init(
|
||||
writer.buffered(),
|
||||
Header.len + @sizeOf(TerminalStyleId),
|
||||
);
|
||||
var decoded = decodePayload(
|
||||
&source.interface,
|
||||
std.testing.allocator,
|
||||
) catch |err| {
|
||||
try std.testing.expectEqual(error.ReadFailed, err);
|
||||
return;
|
||||
};
|
||||
defer decoded.deinit();
|
||||
try std.testing.expect(false);
|
||||
}
|
||||
|
||||
test "decode normalizes duplicate default and invalid style entries" {
|
||||
const header: Header = .{
|
||||
.columns = 1,
|
||||
|
||||
@@ -847,7 +847,7 @@ pub const SavedCursor = struct {
|
||||
return .{
|
||||
.x = try io.readInt(reader, u16),
|
||||
.y = try io.readInt(reader, u16),
|
||||
.pen = style.decodeOrDiscard(reader) catch .{},
|
||||
.pen = (try style.decodeOrNull(reader)) orelse .{},
|
||||
.flags = try Flags.decode(reader),
|
||||
.charset = decodeCharsetState(
|
||||
try io.readInt(reader, u16),
|
||||
@@ -1008,7 +1008,8 @@ pub const Header = struct {
|
||||
try reader.takeByte(),
|
||||
) orelse .block;
|
||||
const cursor_flags = try CursorFlags.decode(reader);
|
||||
const cursor_pen: TerminalStyle = style.decodeOrDiscard(reader) catch .{};
|
||||
const cursor_pen: TerminalStyle =
|
||||
(try style.decodeOrNull(reader)) orelse .{};
|
||||
const hyperlink_implicit_id = try io.readInt(reader, u32);
|
||||
|
||||
// Charset and selective-erase state.
|
||||
|
||||
@@ -172,6 +172,27 @@ pub fn decodeOrDiscard(
|
||||
return decode(&source);
|
||||
}
|
||||
|
||||
/// Decode one complete entry, returning null for invalid semantic contents.
|
||||
///
|
||||
/// Reader errors remain structural and are always propagated. This is the
|
||||
/// lenient entry point for enclosing codecs which can safely replace an
|
||||
/// invalid fixed-size style without treating truncation as invalid styling.
|
||||
pub fn decodeOrNull(
|
||||
reader: *std.Io.Reader,
|
||||
) std.Io.Reader.Error!?terminal_style.Style {
|
||||
return decodeOrDiscard(reader) catch |err| switch (err) {
|
||||
error.ReadFailed => return error.ReadFailed,
|
||||
error.EndOfStream => return error.EndOfStream,
|
||||
|
||||
error.InvalidColorKind,
|
||||
error.InvalidColor,
|
||||
error.InvalidUnderline,
|
||||
error.InvalidFlags,
|
||||
error.InvalidReserved,
|
||||
=> null,
|
||||
};
|
||||
}
|
||||
|
||||
fn encodeColor(
|
||||
value: terminal_style.Style.Color,
|
||||
writer: *std.Io.Writer,
|
||||
@@ -383,6 +404,16 @@ test "decodeOrDiscard preserves the next entry boundary" {
|
||||
try std.testing.expectEqual(@as(u8, 0xFF), try reader.takeByte());
|
||||
}
|
||||
|
||||
test "decodeOrNull distinguishes semantic errors from truncation" {
|
||||
var invalid: [len]u8 = @splat(0);
|
||||
invalid[0] = 3;
|
||||
var invalid_reader: std.Io.Reader = .fixed(&invalid);
|
||||
try std.testing.expectEqual(null, try decodeOrNull(&invalid_reader));
|
||||
|
||||
var truncated: std.Io.Reader = .fixed(invalid[0 .. len - 1]);
|
||||
try std.testing.expectError(error.EndOfStream, decodeOrNull(&truncated));
|
||||
}
|
||||
|
||||
test "reject every truncation" {
|
||||
const fixture = [_]u8{0} ** len;
|
||||
for (0..len) |fixture_len| {
|
||||
|
||||
Reference in New Issue
Block a user