diff --git a/src/terminal/snapshot/history.zig b/src/terminal/snapshot/history.zig index b49c05081..dc5fcb65d 100644 --- a/src/terminal/snapshot/history.zig +++ b/src/terminal/snapshot/history.zig @@ -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, }, diff --git a/src/terminal/snapshot/main.zig b/src/terminal/snapshot/main.zig index d1d092d59..6eb580f21 100644 --- a/src/terminal/snapshot/main.zig +++ b/src/terminal/snapshot/main.zig @@ -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 //! diff --git a/src/terminal/snapshot/screen.zig b/src/terminal/snapshot/screen.zig index 2fc3d0d8b..875d2bb0e 100644 --- a/src/terminal/snapshot/screen.zig +++ b/src/terminal/snapshot/screen.zig @@ -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 diff --git a/src/terminal/snapshot/snapshot.ksy b/src/terminal/snapshot/snapshot.ksy index b7f8eb1bf..ad8f6c3a0 100644 --- a/src/terminal/snapshot/snapshot.ksy +++ b/src/terminal/snapshot/snapshot.ksy @@ -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: diff --git a/src/terminal/snapshot/testdata/complete-v1.hex b/src/terminal/snapshot/testdata/complete-v1.hex index 01a72f1ea..7731a9c9d 100644 --- a/src/terminal/snapshot/testdata/complete-v1.hex +++ b/src/terminal/snapshot/testdata/complete-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/history-header-v1.hex b/src/terminal/snapshot/testdata/history-header-v1.hex index 6e44379dc..dc8333e5b 100644 --- a/src/terminal/snapshot/testdata/history-header-v1.hex +++ b/src/terminal/snapshot/testdata/history-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/screen-header-v1.hex b/src/terminal/snapshot/testdata/screen-header-v1.hex index 950d5736b..7a29df76c 100644 --- a/src/terminal/snapshot/testdata/screen-header-v1.hex +++ b/src/terminal/snapshot/testdata/screen-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/verify-kaitai.py b/src/terminal/snapshot/verify-kaitai.py index 1a88ab41c..fb3c750fe 100755 --- a/src/terminal/snapshot/verify-kaitai.py +++ b/src/terminal/snapshot/verify-kaitai.py @@ -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):