mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-01 05:09:01 +00:00
terminal/snapshot: history robustness
Treat HISTORY row counts as canonical metadata rather than a reason to reject otherwise usable history. Restore topology from the declared PAGE sequence while keeping record framing, routing keys, and sequence boundaries strict.
This commit is contained in:
@@ -12,9 +12,10 @@
|
||||
//!
|
||||
//! The first SCREEN page may begin above the active area. Those incidental
|
||||
//! history rows are counted by `screen_overlap_rows`; they are not repeated
|
||||
//! after HISTORY. `total_rows` is the authoritative number of logical rows
|
||||
//! before the active area, including both the SCREEN overlap and the rows in
|
||||
//! the following PAGE records.
|
||||
//! after HISTORY. `total_rows` is the canonical number of logical rows before
|
||||
//! the active area, including both the SCREEN overlap and the rows in the
|
||||
//! following PAGE records. Decoders derive native row totals from the restored
|
||||
//! pages and may ignore inconsistent row metadata.
|
||||
//!
|
||||
//! All integers are unsigned and little-endian.
|
||||
//!
|
||||
@@ -59,9 +60,10 @@
|
||||
//! 16 +--------------------------------+
|
||||
//! ```
|
||||
//!
|
||||
//! The sum of `screen_overlap_rows` and the logical row counts of all
|
||||
//! following PAGE records must equal `total_rows`. A decoder uses `page_count`,
|
||||
//! rather than another record tag, to find the end of the page sequence.
|
||||
//! A canonical encoder sets `total_rows` to the sum of `screen_overlap_rows`
|
||||
//! and the logical row counts of all following PAGE records. A decoder uses
|
||||
//! `page_count`, rather than another record tag, to find the end of the page
|
||||
//! sequence.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -92,7 +94,7 @@ pub const Header = struct {
|
||||
/// Number of complete PAGE records immediately following this record.
|
||||
page_count: u32,
|
||||
|
||||
/// Authoritative number of logical rows before the active area.
|
||||
/// Canonical number of logical rows before the active area.
|
||||
total_rows: u64,
|
||||
|
||||
/// History rows already included at the start of the first SCREEN page.
|
||||
@@ -236,12 +238,6 @@ pub const DecodeError = Allocator.Error ||
|
||||
|
||||
/// The Screen already contains complete pages before its active page.
|
||||
ExistingHistory,
|
||||
|
||||
/// The claimed overlap does not match the restored SCREEN sequence.
|
||||
InvalidScreenOverlap,
|
||||
|
||||
/// The declared total does not match the restored historical rows.
|
||||
InvalidHistoryRows,
|
||||
};
|
||||
|
||||
/// Restore one HISTORY and its declared PAGE records into a native Screen.
|
||||
@@ -275,14 +271,11 @@ pub fn decode(
|
||||
if (terminal_screen.pages.getTopLeft(.screen).node != active_top.node) {
|
||||
return error.ExistingHistory;
|
||||
}
|
||||
if (header.screen_overlap_rows != active_top.y) {
|
||||
return error.InvalidScreenOverlap;
|
||||
}
|
||||
if (header.total_rows < header.screen_overlap_rows) {
|
||||
return error.InvalidHistoryRows;
|
||||
}
|
||||
|
||||
var restored_rows: u64 = header.screen_overlap_rows;
|
||||
// Row metadata is redundant with the restored SCREEN and PAGE topology.
|
||||
// Consume the declared number of PAGE records, but derive all native row
|
||||
// totals from their actual dimensions instead of rejecting inconsistent
|
||||
// metadata from another implementation.
|
||||
for (0..header.page_count) |_| {
|
||||
// PAGE exposes its exact capacity before decoding the payload, allowing
|
||||
// the destination PageList to allocate the final backing memory once.
|
||||
@@ -292,26 +285,11 @@ pub fn decode(
|
||||
defer allocation.deinit();
|
||||
try decoder.decode(allocation.page(), alloc);
|
||||
|
||||
// Validate the authoritative row total before publishing this page.
|
||||
const page_rows = allocation.page().size.rows;
|
||||
restored_rows = std.math.add(
|
||||
u64,
|
||||
restored_rows,
|
||||
page_rows,
|
||||
) catch return error.InvalidHistoryRows;
|
||||
if (restored_rows > header.total_rows) {
|
||||
return error.InvalidHistoryRows;
|
||||
}
|
||||
|
||||
const contains_prompt = hasSemanticPrompt(allocation.page());
|
||||
try allocation.finalize(.prepend);
|
||||
if (contains_prompt) terminal_screen.semantic_prompt.seen = true;
|
||||
}
|
||||
|
||||
if (restored_rows != header.total_rows) {
|
||||
return error.InvalidHistoryRows;
|
||||
}
|
||||
|
||||
terminal_screen.pages.assertIntegrity();
|
||||
terminal_screen.assertIntegrity();
|
||||
}
|
||||
@@ -363,6 +341,14 @@ test "HISTORY header golden encoding and decoding" {
|
||||
Header.decode(&truncated),
|
||||
);
|
||||
}
|
||||
|
||||
var invalid_key = test_header_fixture;
|
||||
invalid_key[0] = 2;
|
||||
var invalid_key_reader: std.Io.Reader = .fixed(&invalid_key);
|
||||
try std.testing.expectError(
|
||||
error.InvalidKey,
|
||||
Header.decode(&invalid_key_reader),
|
||||
);
|
||||
}
|
||||
|
||||
test "HISTORY encodes newest first and restores complete history" {
|
||||
@@ -657,7 +643,7 @@ test "HISTORY encodes and restores an empty sequence" {
|
||||
try std.testing.expectError(error.EndOfStream, decode_source.takeByte());
|
||||
}
|
||||
|
||||
test "HISTORY rejects manifest mismatches" {
|
||||
test "HISTORY accepts row metadata mismatches and rejects structural ones" {
|
||||
var terminal_screen = try TerminalScreen.init(
|
||||
std.testing.io,
|
||||
std.testing.allocator,
|
||||
@@ -669,8 +655,45 @@ test "HISTORY rejects manifest mismatches" {
|
||||
);
|
||||
defer terminal_screen.deinit();
|
||||
|
||||
// Encode a manifest without PAGE records and vary only the fixed fields so
|
||||
// each structural validation boundary is exercised in isolation.
|
||||
// Row counts are redundant metadata. Even internally inconsistent values
|
||||
// do not prevent restoring the topology declared by page_count.
|
||||
const accepted = [_]Header{
|
||||
.{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 0,
|
||||
.screen_overlap_rows = 1,
|
||||
},
|
||||
.{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = std.math.maxInt(u64),
|
||||
.screen_overlap_rows = std.math.maxInt(u16),
|
||||
},
|
||||
};
|
||||
for (accepted) |header| {
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer destination.deinit();
|
||||
var record_writer = try record.Writer.init(&destination, .history);
|
||||
try header.encode(record_writer.payloadWriter());
|
||||
try record_writer.finish();
|
||||
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
try decode(
|
||||
&source,
|
||||
std.testing.allocator,
|
||||
.primary,
|
||||
&terminal_screen,
|
||||
);
|
||||
try std.testing.expectError(error.EndOfStream, source.takeByte());
|
||||
terminal_screen.pages.assertIntegrity();
|
||||
terminal_screen.assertIntegrity();
|
||||
}
|
||||
|
||||
// Routing and sequence length still determine which native screen is
|
||||
// mutated and where the following record begins, so they remain strict.
|
||||
const Case = struct {
|
||||
header: Header,
|
||||
expected: anyerror,
|
||||
@@ -685,24 +708,6 @@ test "HISTORY rejects manifest mismatches" {
|
||||
},
|
||||
.expected = error.UnexpectedScreenKey,
|
||||
},
|
||||
.{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 1,
|
||||
},
|
||||
.expected = error.InvalidScreenOverlap,
|
||||
},
|
||||
.{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
.expected = error.InvalidHistoryRows,
|
||||
},
|
||||
.{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
|
||||
Reference in New Issue
Block a user