terminal/snapshot: move history size hints to screen record

Publish each screen's logical history extent before READY so clients can size scrollbars while older pages are still arriving. Keep the value advisory and continue deriving native PageList totals from decoded pages.

Reduce HISTORY to its structural screen key and page count, and update the format documentation, Kaitai schema, verifier, and versioned fixtures.
This commit is contained in:
Mitchell Hashimoto
2026-07-31 11:57:58 -07:00
parent d37e1fe184
commit e2e74fecbe
8 changed files with 172 additions and 235 deletions

View File

@@ -7,15 +7,13 @@
//! to oldest.
//!
//! Every encoded SCREEN has one corresponding HISTORY record. A screen with no
//! history uses zero for all three count fields and has no following PAGE
//! records.
//! older pages uses a zero page count and has no following PAGE records.
//!
//! 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 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.
//! history rows are already present at READY and are not repeated after
//! HISTORY. The corresponding SCREEN header declares the complete logical
//! history extent, including both that resident overlap and the following PAGE
//! records.
//!
//! All integers are unsigned and little-endian.
//!
@@ -53,17 +51,10 @@
//! 2 +--------------------------------+
//! | Following page count (u32) |
//! 6 +--------------------------------+
//! | Total logical history rows |
//! | u64 |
//! 14 +--------------------------------+
//! | SCREEN overlap rows (u16) |
//! 16 +--------------------------------+
//! ```
//!
//! 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.
//! 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;
@@ -85,7 +76,7 @@ pub const Header = struct {
comptime {
// This size is part of the wire format. If it changes, the snapshot
// version must also change.
std.debug.assert(len == 16);
std.debug.assert(len == 6);
}
/// Identifies the previously encoded screen that owns this history.
@@ -94,12 +85,6 @@ pub const Header = struct {
/// Number of complete PAGE records immediately following this record.
page_count: u32,
/// Canonical number of logical rows before the active area.
total_rows: u64,
/// History rows already included at the start of the first SCREEN page.
screen_overlap_rows: u16,
/// Encode the fixed HISTORY payload.
pub fn encode(
self: Header,
@@ -107,8 +92,6 @@ pub const Header = struct {
) std.Io.Writer.Error!void {
try io.writeInt(writer, u16, @intCast(@intFromEnum(self.key)));
try io.writeInt(writer, u32, self.page_count);
try io.writeInt(writer, u64, self.total_rows);
try io.writeInt(writer, u16, self.screen_overlap_rows);
}
pub const DecodeError = std.Io.Reader.Error || error{InvalidKey};
@@ -126,8 +109,6 @@ pub const Header = struct {
return .{
.key = key,
.page_count = try io.readInt(reader, u32),
.total_rows = try io.readInt(reader, u64),
.screen_overlap_rows = try io.readInt(reader, u16),
};
}
@@ -138,8 +119,6 @@ pub const Header = struct {
const value: Header = .{
.key = .primary,
.page_count = 0,
.total_rows = 0,
.screen_overlap_rows = 0,
};
value.encode(&writer) catch unreachable;
return writer.end;
@@ -154,9 +133,6 @@ pub const EncodeError = Allocator.Error ||
error{
/// The complete historical prefix has more pages than the header fits.
PageCountOverflow,
/// The complete historical prefix has more rows than the header fits.
RowCountOverflow,
};
/// Encode one screen's HISTORY and its complete historical pages.
@@ -172,21 +148,14 @@ pub fn encode(
// SCREEN begins at the page containing the active area's first row. Its
// leading rows are already resident; every previous complete page belongs
// to this HISTORY sequence.
const active_top = terminal_screen.pages.getTopLeft(.active);
const page_count: usize, const total_rows: u64 = count: {
const first = terminal_screen.pages.getTopLeft(.active).node.prev;
const page_count: usize = count: {
var page_count: usize = 0;
var total_rows: u64 = active_top.y;
var node = active_top.node.prev;
var node = first;
while (node) |current| : (node = current.prev) {
page_count += 1;
total_rows = std.math.add(
u64,
total_rows,
current.rows(),
) catch return error.RowCountOverflow;
}
break :count .{ page_count, total_rows };
break :count page_count;
};
const header: Header = .{
@@ -195,12 +164,9 @@ pub fn encode(
u32,
page_count,
) orelse return error.PageCountOverflow,
.total_rows = total_rows,
.screen_overlap_rows = active_top.y,
};
// HISTORY declares exactly how many PAGE records follow and how their rows
// combine with the overlap already carried by SCREEN.
// HISTORY declares exactly how many PAGE records follow.
{
const payload = destination.begin(.history);
errdefer destination.cancel();
@@ -211,7 +177,7 @@ pub fn encode(
// Walk backward so each page can be prepended by the decoder immediately.
// PreservedPage borrows resident pages and clones compressed pages without
// changing the source node's representation.
var node = active_top.node.prev;
var node = first;
while (node) |current| : (node = current.prev) {
var preserved = try current.pagePreservingState(terminal_screen.alloc);
defer preserved.deinit();
@@ -277,9 +243,7 @@ pub const Decoder = struct {
return error.ExistingHistory;
}
// Row metadata is redundant with the restored SCREEN and PAGE
// topology. Consume the declared PAGE records, but derive all native
// row totals from their actual dimensions.
// Native row totals remain derived from the actual PAGE dimensions.
for (0..self.header.page_count) |_| {
// PAGE exposes its exact capacity before decoding the payload,
// allowing the destination PageList to allocate the final backing
@@ -341,8 +305,6 @@ test "HISTORY header golden encoding and decoding" {
const expected: Header = .{
.key = .alternate,
.page_count = 0x01020304,
.total_rows = 0x0102030405060708,
.screen_overlap_rows = 0x090a,
};
var encoded: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
@@ -456,16 +418,6 @@ test "HISTORY encodes newest first and restores complete history" {
try history_record.finish();
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u32, 2), header.page_count);
try std.testing.expectEqual(
@as(u16, active_top.y),
header.screen_overlap_rows,
);
try std.testing.expectEqual(
@as(u64, active_top.y) +
oldest_history.rows() +
newest_history.rows(),
header.total_rows,
);
var decoded_newest = try page.decode(
&history_source,
@@ -504,6 +456,10 @@ test "HISTORY encodes newest first and restores complete history" {
TerminalScreenKey.primary,
decoded_screen.key,
);
try std.testing.expectEqual(
@as(u64, @intCast(source_screen.pages.total_rows - screen_rows)),
decoded_screen.history_rows,
);
const restored = &decoded_screen.screen;
try std.testing.expect(!restored.semantic_prompt.seen);
try decode(
@@ -660,8 +616,6 @@ test "HISTORY encodes and restores an empty sequence" {
const header = try Header.decode(history_record.payloadReader());
try history_record.finish();
try std.testing.expectEqual(@as(u32, 0), header.page_count);
try std.testing.expectEqual(@as(u64, 0), header.total_rows);
try std.testing.expectEqual(@as(u16, 0), header.screen_overlap_rows);
try std.testing.expectError(error.EndOfStream, inspect_source.takeByte());
var decode_source: std.Io.Reader = .fixed(destination.written());
@@ -679,7 +633,7 @@ test "HISTORY encodes and restores an empty sequence" {
try std.testing.expectError(error.EndOfStream, decode_source.takeByte());
}
test "HISTORY accepts row metadata mismatches and rejects structural ones" {
test "HISTORY rejects invalid routing and incomplete sequences" {
var terminal_screen = try TerminalScreen.init(
std.testing.io,
std.testing.allocator,
@@ -691,49 +645,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
);
defer terminal_screen.deinit();
// 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 stream: record.Writer = .init(
std.testing.allocator,
&destination.writer,
);
defer stream.deinit();
const payload = stream.begin(.history);
errdefer stream.cancel();
try header.encode(payload);
try stream.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 {
@@ -745,8 +656,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
.header = .{
.key = .alternate,
.page_count = 0,
.total_rows = 0,
.screen_overlap_rows = 0,
},
.expected = error.UnexpectedScreenKey,
},
@@ -754,8 +663,6 @@ test "HISTORY accepts row metadata mismatches and rejects structural ones" {
.header = .{
.key = .primary,
.page_count = 1,
.total_rows = 1,
.screen_overlap_rows = 0,
},
.expected = error.EndOfStream,
},

View File

@@ -74,7 +74,9 @@
//! READY and FINISH contain BLAKE3-256 digests of all preceding snapshot bytes.
//! READY therefore validates the renderable active-state prefix. FINISH covers
//! READY and all history as well, validating the complete snapshot and its
//! record ordering.
//! record ordering. Each SCREEN declares its complete logical history extent,
//! allowing a client to size its scrollbar at READY even though older PAGE
//! records arrive afterward.
//!
//! ## Encoding
//!

View File

@@ -9,13 +9,14 @@
//!
//! The final screen-height rows are the active area. When its boundary falls
//! inside the first encoded page, that page also contains an incidental history
//! prefix. A client may expose or ignore those resident rows, but must not
//! require them or infer the complete history extent from them.
//! prefix. `history_rows` declares the complete logical history extent so a
//! client can size its scrollbar at READY without waiting for HISTORY. A client
//! may expose or ignore the resident overlap carried by SCREEN. The extent is
//! advisory; native row totals remain derived from decoded PAGE records.
//!
//! Screen dimensions are terminal-wide state and are not repeated here. Page
//! capacities, native page IDs and pointers, authoritative history extent and
//! availability, scrollbar state, selection, viewport, dirty state, and derived
//! semantic-prompt state are also omitted.
//! capacities, native page IDs and pointers, history availability, selection,
//! viewport, dirty state, and derived semantic-prompt state are also omitted.
//!
//! All integers are unsigned and little-endian.
//!
@@ -37,10 +38,9 @@
//! n = page_count
//! ```
//!
//! The HISTORY record for this screen provides the authoritative history
//! manifest. It counts any incidental prefix above as the SCREEN overlap, then
//! sends the older complete pages after the terminal becomes ready. The prefix
//! is not sent again.
//! The HISTORY record for this screen declares and sends the older complete
//! pages after the terminal becomes ready. The incidental prefix is already
//! present in SCREEN and is not sent again.
//!
//! The SCREEN payload begins with a fixed header. When the header says there is
//! no saved cursor, the payload is:
@@ -48,7 +48,7 @@
//! ```text
//! 0 +----------------------+
//! | Header |
//! 45 +----------------------+
//! 53 +----------------------+
//! | Cursor hyperlink |
//! | variable |
//! end +----------------------+
@@ -59,9 +59,9 @@
//! ```text
//! 0 +----------------------+
//! | Header |
//! 45 +----------------------+
//! 53 +----------------------+
//! | Saved cursor |
//! 68 +----------------------+
//! 76 +----------------------+
//! | Cursor hyperlink |
//! | variable |
//! end +----------------------+
@@ -79,33 +79,35 @@
//! 2 +----------------------------------+
//! | Page count (u16) |
//! 4 +----------------------------------+
//! | Logical history rows (u64) |
//! 12 +----------------------------------+
//! | Cursor x (u16) |
//! 6 +----------------------------------+
//! 14 +----------------------------------+
//! | Cursor y (u16) |
//! 8 +----------------------------------+
//! 16 +----------------------------------+
//! | Cursor visual style (u8) |
//! 9 +----------------------------------+
//! 17 +----------------------------------+
//! | Cursor flags (u8) |
//! 10 +----------------------------------+
//! 18 +----------------------------------+
//! | Concrete cursor pen (Style) |
//! 26 +----------------------------------+
//! | Hyperlink implicit counter (u32) |
//! 30 +----------------------------------+
//! | Current charset (CharsetState) |
//! 32 +----------------------------------+
//! | Protected mode (u8) |
//! 33 +----------------------------------+
//! | Kitty keyboard stack index (u8) |
//! 34 +----------------------------------+
//! | Hyperlink implicit counter (u32) |
//! 38 +----------------------------------+
//! | Current charset (CharsetState) |
//! 40 +----------------------------------+
//! | Protected mode (u8) |
//! 41 +----------------------------------+
//! | Kitty keyboard stack index (u8) |
//! 42 +----------------------------------+
//! | Kitty keyboard stack flags |
//! | 8 * u8 |
//! 42 +----------------------------------+
//! 50 +----------------------------------+
//! | Semantic-click kind (u8) |
//! 43 +----------------------------------+
//! 51 +----------------------------------+
//! | Semantic-click value (u8) |
//! 44 +----------------------------------+
//! 52 +----------------------------------+
//! | Saved-cursor-present (u8) |
//! 45 +----------------------------------+
//! 53 +----------------------------------+
//! ```
//!
//! The concrete cursor pen uses the fixed style encoding from `style.zig`.
@@ -298,6 +300,8 @@ pub const DecodeError = PayloadDecodeError ||
/// One decoded SCREEN sequence and the native screen identified by its header.
pub const Decoded = struct {
key: TerminalScreenKey,
/// Expected history extent at READY, including resident SCREEN overlap.
history_rows: u64,
screen: TerminalScreen,
/// Release the decoded native screen before ownership is transferred.
@@ -507,7 +511,11 @@ pub fn decode(
// before transferring ownership to the caller.
result.pages.assertIntegrity();
result.assertIntegrity();
return .{ .key = key, .screen = result };
return .{
.key = key,
.history_rows = header.history_rows,
.screen = result,
};
}
/// Errors possible while decoding fixed SCREEN payload fields.
@@ -868,12 +876,15 @@ pub const Header = struct {
pub const len = computeLen();
comptime {
std.debug.assert(len == 45);
std.debug.assert(len == 53);
}
key: TerminalScreenKey,
/// Complete pages in the minimal suffix covering the active area.
page_count: u16,
/// Advisory logical rows before the active area, including resident
/// SCREEN overlap.
history_rows: u64,
cursor_x: u16,
cursor_y: u16,
cursor_style: TerminalScreen.CursorStyle,
@@ -895,6 +906,9 @@ pub const Header = struct {
return .{
.key = key,
.page_count = page_count,
.history_rows = @intCast(
screen.pages.total_rows - screen.pages.rows,
),
.cursor_x = screen.cursor.x,
.cursor_y = screen.cursor.y,
.cursor_style = screen.cursor.cursor_style,
@@ -932,9 +946,10 @@ pub const Header = struct {
}
}
// Screen identity and cursor position.
// Screen identity, expected history extent, and cursor position.
try io.writeInt(writer, u16, @intCast(@intFromEnum(self.key)));
try io.writeInt(writer, u16, self.page_count);
try io.writeInt(writer, u64, self.history_rows);
try io.writeInt(writer, u16, self.cursor_x);
try io.writeInt(writer, u16, self.cursor_y);
@@ -975,12 +990,13 @@ pub const Header = struct {
/// The screen key remains structural because it routes the following PAGE
/// sequence. Unknown semantic fields use native defaults instead.
pub fn decode(reader: *std.Io.Reader) PayloadDecodeError!Header {
// Screen identity and cursor position.
// Screen identity, expected history extent, and cursor position.
const key = enumFromInt(
TerminalScreenKey,
try io.readInt(reader, u16),
) orelse return error.InvalidKey;
const page_count = try io.readInt(reader, u16);
const history_rows = try io.readInt(reader, u64);
const cursor_x = try io.readInt(reader, u16);
const cursor_y = try io.readInt(reader, u16);
@@ -1012,6 +1028,7 @@ pub const Header = struct {
return .{
.key = key,
.page_count = page_count,
.history_rows = history_rows,
.cursor_x = cursor_x,
.cursor_y = cursor_y,
@@ -1037,6 +1054,7 @@ pub const Header = struct {
const value: Header = .{
.key = .primary,
.page_count = 0,
.history_rows = 0,
.cursor_x = 0,
.cursor_y = 0,
.cursor_style = .bar,
@@ -1135,6 +1153,7 @@ fn testHeader() Header {
return .{
.key = .alternate,
.page_count = 0x0203,
.history_rows = 0x0102030405060708,
.cursor_x = 0x0405,
.cursor_y = 0x0607,
.cursor_style = .block_hollow,
@@ -1444,16 +1463,16 @@ test "header decoding rejects structural values" {
test "header decoding normalizes unknown semantic values" {
var fixture = [_]u8{0} ** Header.len;
fixture[8] = 4; // Unknown cursor style.
fixture[9] = 0xFF; // Known flags, unknown semantic value, reserved bits.
fixture[10] = 3; // Unknown cursor foreground color kind.
fixture[31] = 0xD0; // Invalid single shift plus a reserved bit.
fixture[32] = 3; // Unknown protected mode.
fixture[33] = 8; // Out-of-range Kitty keyboard index.
@memset(fixture[34..42], 0xFF); // Known flags plus reserved bits.
fixture[42] = 3; // Unknown semantic-click kind.
fixture[43] = 0xFF;
fixture[44] = 2; // Noncanonical true.
fixture[16] = 4; // Unknown cursor style.
fixture[17] = 0xFF; // Known flags, unknown semantic value, reserved bits.
fixture[18] = 3; // Unknown cursor foreground color kind.
fixture[39] = 0xD0; // Invalid single shift plus a reserved bit.
fixture[40] = 3; // Unknown protected mode.
fixture[41] = 8; // Out-of-range Kitty keyboard index.
@memset(fixture[42..50], 0xFF); // Known flags plus reserved bits.
fixture[50] = 3; // Unknown semantic-click kind.
fixture[51] = 0xFF;
fixture[52] = 2; // Noncanonical true.
var reader: std.Io.Reader = .fixed(&fixture);
const decoded = try Header.decode(&reader);
@@ -1504,8 +1523,8 @@ test "header decoding normalizes unknown semantic values" {
};
for (invalid_semantic_clicks) |invalid| {
var semantic_fixture = [_]u8{0} ** Header.len;
semantic_fixture[42] = invalid[0];
semantic_fixture[43] = invalid[1];
semantic_fixture[50] = invalid[0];
semantic_fixture[51] = invalid[1];
var semantic_reader: std.Io.Reader = .fixed(&semantic_fixture);
const semantic_decoded = try Header.decode(&semantic_reader);
try std.testing.expectEqual(
@@ -1541,6 +1560,7 @@ test "native SCREEN payload omits absent optional state" {
const header = try Header.decode(&reader);
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u16, 1), header.page_count);
try std.testing.expectEqual(@as(u64, 0), header.history_rows);
try std.testing.expect(!header.saved_cursor_present);
try std.testing.expectEqual(
null,
@@ -1867,6 +1887,10 @@ test "SCREEN encodes the minimal complete-page active suffix" {
const header = try Header.decode(payload_reader);
try std.testing.expectEqual(TerminalScreenKey.primary, header.key);
try std.testing.expectEqual(@as(u16, 2), header.page_count);
try std.testing.expectEqual(
@as(u64, @intCast(screen.pages.total_rows - screen.pages.rows)),
header.history_rows,
);
try std.testing.expect(!header.saved_cursor_present);
try std.testing.expectEqual(
null,
@@ -1904,11 +1928,22 @@ test "SCREEN encodes the minimal complete-page active suffix" {
);
defer decoded.deinit();
try std.testing.expectEqual(TerminalScreenKey.primary, decoded.key);
try std.testing.expectEqual(header.history_rows, decoded.history_rows);
const restored = &decoded.screen;
try std.testing.expectEqual(@as(usize, 2), restored.pages.totalPages());
const restored_top = restored.pages.getTopLeft(.active);
try std.testing.expectEqual(@as(u16, 1), restored_top.y);
try std.testing.expectEqual(
@as(usize, restored_top.y),
restored.pages.total_rows - restored.pages.rows,
);
try std.testing.expect(
decoded.history_rows > @as(
u64,
@intCast(restored.pages.total_rows - restored.pages.rows),
),
);
try std.testing.expectEqual(
@as(u21, 'B'),
restored.pages.getTopLeft(.screen).node

View File

@@ -11,7 +11,8 @@ doc: |
renderable screen sequences, a READY checkpoint, matching history sequences,
and a FINISH checkpoint. SCREEN pages are oldest-to-newest. HISTORY pages are
newest-to-oldest. FINISH terminates the snapshot; bytes that follow belong to
the containing transport and are outside this schema.
the containing transport and are outside this schema. Each SCREEN declares
its complete logical history extent before READY.
Record CRC32C values and checkpoint BLAKE3-256 digests are represented here
but cannot be calculated by portable Kaitai Struct expressions. The adjacent
@@ -568,6 +569,8 @@ types:
type: u2
valid:
min: 1
- id: history_rows
type: u8
- id: cursor_x
type: u2
- id: cursor_y
@@ -710,10 +713,6 @@ types:
expr: _.to_i <= 1
- id: page_count
type: u4
- id: total_rows
type: u8
- id: screen_overlap_rows
type: u2
- id: trailing_data
size-eos: true
valid:

View File

@@ -72,70 +72,68 @@ ee ee 80 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000037a
00 63 6f 6d 70 6c 65 74 65 20 73 6e 61 70 73 68 # 0x000003ba
6f 74 # 0x000003ca
# offset 0x000003cc: screen record, payload 46 bytes
02 00 2e 00 00 00 dc 02 f1 37 00 00 01 00 00 00 # 0x000003cc
00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003dc
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000003ec
00 00 00 00 00 00 00 00 # 0x000003fc
# offset 0x000003cc: screen record, payload 54 bytes
02 00 36 00 00 00 67 31 0e c6 00 00 01 00 04 00 # 0x000003cc
00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 # 0x000003dc
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003ec
00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003fc
# offset 0x00000404: page record, payload 119 bytes
03 00 77 00 00 00 48 b7 91 cb 02 00 03 00 00 00 # 0x00000404
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000414
00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 00 # 0x00000424
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000434
00 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 # 0x00000444
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000454
00 00 00 00 00 00 00 00 00 45 00 00 00 00 00 00 # 0x00000464
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000474
00 # 0x00000484
# offset 0x0000040c: page record, payload 119 bytes
03 00 77 00 00 00 48 b7 91 cb 02 00 03 00 00 00 # 0x0000040c
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x0000041c
00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 00 # 0x0000042c
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000043c
00 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 # 0x0000044c
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000045c
00 00 00 00 00 00 00 00 00 45 00 00 00 00 00 00 # 0x0000046c
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000047c
00 # 0x0000048c
# offset 0x00000485: screen record, payload 46 bytes
02 00 2e 00 00 00 05 b9 5a 3a 01 00 01 00 01 00 # 0x00000485
02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000495
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000004a5
00 00 00 00 00 00 00 00 # 0x000004b5
# offset 0x0000048d: screen record, payload 54 bytes
02 00 36 00 00 00 ce 1f 9f 08 01 00 01 00 00 00 # 0x0000048d
00 00 00 00 00 00 01 00 02 00 01 00 00 00 00 00 # 0x0000049d
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000004ad
00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000004bd
# offset 0x000004bd: page record, payload 119 bytes
03 00 77 00 00 00 84 a6 e9 08 02 00 03 00 00 00 # 0x000004bd
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 00 # 0x000004cd
00 00 00 00 00 00 00 72 00 00 00 00 00 00 00 00 # 0x000004dd
00 00 00 00 00 00 00 6e 00 00 00 00 00 00 00 03 # 0x000004ed
00 00 00 00 00 00 00 00 61 00 00 00 00 00 00 00 # 0x000004fd
00 00 00 00 00 00 00 00 74 00 00 00 00 00 00 00 # 0x0000050d
03 00 00 00 00 00 00 00 00 65 00 00 00 00 00 00 # 0x0000051d
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000052d
00 # 0x0000053d
# offset 0x000004cd: page record, payload 119 bytes
03 00 77 00 00 00 84 a6 e9 08 02 00 03 00 00 00 # 0x000004cd
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 00 # 0x000004dd
00 00 00 00 00 00 00 72 00 00 00 00 00 00 00 00 # 0x000004ed
00 00 00 00 00 00 00 6e 00 00 00 00 00 00 00 03 # 0x000004fd
00 00 00 00 00 00 00 00 61 00 00 00 00 00 00 00 # 0x0000050d
00 00 00 00 00 00 00 00 74 00 00 00 00 00 00 00 # 0x0000051d
03 00 00 00 00 00 00 00 00 65 00 00 00 00 00 00 # 0x0000052d
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000053d
00 # 0x0000054d
# offset 0x0000053e: ready record, payload 32 bytes
05 00 20 00 00 00 29 ce 51 e1 28 ab 80 18 19 00 # 0x0000053e
ef b3 15 fa 9d 03 c3 42 7f 77 2d f3 0c 00 87 cc # 0x0000054e
a1 dc ac ce 6c fc fb 27 66 e1 # 0x0000055e
# offset 0x0000054e: ready record, payload 32 bytes
05 00 20 00 00 00 43 0d 7b 3a 84 06 56 ba 02 59 # 0x0000054e
04 24 20 59 96 92 14 ba 76 53 80 40 09 12 85 a7 # 0x0000055e
f2 b5 b8 f9 63 98 e8 dc 9a 39 # 0x0000056e
# offset 0x00000568: history record, payload 16 bytes
04 00 10 00 00 00 cc 85 a3 b6 00 00 02 00 00 00 # 0x00000568
04 00 00 00 00 00 00 00 00 00 # 0x00000578
# offset 0x00000578: history record, payload 6 bytes
04 00 06 00 00 00 20 32 ed e1 00 00 02 00 00 00 # 0x00000578
# offset 0x00000582: page record, payload 86 bytes
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000582
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000592
00 00 00 00 00 00 00 42 00 00 00 00 00 00 00 00 # 0x000005a2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005b2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005c2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005d2
# offset 0x00000588: page record, payload 86 bytes
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000588
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000598
00 00 00 00 00 00 00 42 00 00 00 00 00 00 00 00 # 0x000005a8
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005b8
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005c8
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005d8
# offset 0x000005e2: page record, payload 86 bytes
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005e2
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005f2
00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 00 # 0x00000602
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000612
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000622
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000632
# offset 0x000005e8: page record, payload 86 bytes
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005e8
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005f8
00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 00 # 0x00000608
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000618
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000628
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000638
# offset 0x00000642: history record, payload 16 bytes
04 00 10 00 00 00 39 57 cc cf 01 00 00 00 00 00 # 0x00000642
00 00 00 00 00 00 00 00 00 00 # 0x00000652
# offset 0x00000648: history record, payload 6 bytes
04 00 06 00 00 00 b8 7a ba b1 01 00 00 00 00 00 # 0x00000648
# offset 0x0000065c: finish record, payload 32 bytes
06 00 20 00 00 00 05 71 15 22 3e 4d e9 7a c1 02 # 0x0000065c
0b a3 6a 10 a0 88 1c 07 03 2c 68 52 15 be 76 d0 # 0x0000066c
4c a9 a6 2b fd f4 ac 49 62 2b # 0x0000067c
# offset 0x00000658: finish record, payload 32 bytes
06 00 20 00 00 00 8d d6 46 7a ac 02 c8 09 9d 1a # 0x00000658
5a 00 ce 47 5e d7 b0 ae 54 10 bb 0d f1 35 7c ab # 0x00000668
92 fd ae d3 5e 1a 77 1e 40 7f # 0x00000678

View File

@@ -7,4 +7,4 @@
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
01 00 04 03 02 01 08 07 06 05 04 03 02 01 0a 09 # 0x00000000
01 00 04 03 02 01 # 0x00000000

View File

@@ -7,6 +7,7 @@
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
01 00 03 02 05 04 07 06 03 19 00 00 00 00 01 7f # 0x00000000
00 00 02 12 34 56 ff 03 00 00 0d 0c 0b 0a e4 3d # 0x00000010
02 07 01 02 04 08 10 1f 00 11 02 03 01 # 0x00000020
01 00 03 02 08 07 06 05 04 03 02 01 05 04 07 06 # 0x00000000
03 19 00 00 00 00 01 7f 00 00 02 12 34 56 ff 03 # 0x00000010
00 00 0d 0c 0b 0a e4 3d 02 07 01 02 04 08 10 1f # 0x00000020
00 11 02 03 01 # 0x00000030

View File

@@ -256,19 +256,14 @@ def validate_complete_snapshot(snapshot: Any, data: bytes) -> None:
"the active area"
)
overlap_rows = screen_rows - terminal_header.rows
if payload.screen_overlap_rows != overlap_rows:
raise ValueError(
f"HISTORY key {payload.key}: screen_overlap_rows "
f"{payload.screen_overlap_rows} does not match {overlap_rows}"
)
total_rows = payload.screen_overlap_rows + sum(
history_rows = overlap_rows + sum(
page.payload.header.rows for page in sequence.pages
)
if total_rows != payload.total_rows:
if history_rows != screen.screen.payload.header.history_rows:
raise ValueError(
f"HISTORY key {payload.key}: total_rows "
f"{payload.total_rows} does not match {total_rows}"
f"SCREEN key {payload.key}: history_rows "
f"{screen.screen.payload.header.history_rows} "
f"does not match {history_rows}"
)
for record in all_snapshot_records(snapshot):