mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
terminal/snapshot: kaitai verification
Describe the complete version 1 snapshot format with a Kaitai schema and make every golden fixture self-describing for automatic discovery. Add a verifier that compiles the schema, parses all fixtures, and checks record checksums, checkpoint digests, and cross-record invariants. Preserve Kaitai metadata when generating fixture candidates and provide the compiler and Python runtime dependencies through the development shell. Keep the mode registry portable to Kaitai JavaScript targets so the complete fixture also works in the web IDE.
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
zlib,
|
||||
alejandra,
|
||||
jq,
|
||||
kaitai-struct-compiler,
|
||||
minisign,
|
||||
pandoc,
|
||||
pinact,
|
||||
@@ -85,6 +86,11 @@
|
||||
gi_typelib_path = import ./build-support/gi-typelib-path.nix {
|
||||
inherit pkgs lib stdenv;
|
||||
};
|
||||
python = python3.withPackages (python-pkgs: [
|
||||
python-pkgs.blake3
|
||||
python-pkgs.kaitaistruct
|
||||
python-pkgs.ucs-detect
|
||||
]);
|
||||
in
|
||||
mkShell {
|
||||
name = "ghostty";
|
||||
@@ -116,9 +122,10 @@ in
|
||||
|
||||
# Testing
|
||||
parallel
|
||||
python3
|
||||
python
|
||||
vttest
|
||||
hyperfine
|
||||
kaitai-struct-compiler
|
||||
|
||||
# wasm
|
||||
wabt
|
||||
@@ -138,11 +145,6 @@ in
|
||||
blueprint-compiler
|
||||
libadwaita
|
||||
gtk4
|
||||
|
||||
# Python packages
|
||||
(python3.withPackages (python-pkgs: [
|
||||
python-pkgs.ucs-detect
|
||||
]))
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||
# My nix shell environment installs the non-interactive version
|
||||
@@ -242,6 +244,6 @@ in
|
||||
# We need to remove "xcrun" from the PATH. It is injected by
|
||||
# some dependency but we need to rely on system Xcode tools
|
||||
export PATH=$(echo "$PATH" | awk -v RS=: -v ORS=: '$0 !~ /xcrun/ || $0 == "/usr/bin" {print}' | sed 's/:$//')
|
||||
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/bin:/usr/local/opt/llvm/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||||
export PATH="${python}/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/bin:/usr/local/opt/llvm/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||||
'');
|
||||
}
|
||||
|
||||
@@ -66,9 +66,7 @@ fn computeLen() usize {
|
||||
}
|
||||
}
|
||||
|
||||
const test_golden_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/envelope-v1.hex"),
|
||||
);
|
||||
const test_golden_fixture = test_fixture.parse(@embedFile("testdata/envelope-v1.hex"));
|
||||
|
||||
test "golden encoding" {
|
||||
var buf: [encoded_len]u8 = undefined;
|
||||
|
||||
@@ -49,6 +49,23 @@
|
||||
//! );
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Kaitai
|
||||
//!
|
||||
//! Every fixture has three machine-readable comment fields:
|
||||
//!
|
||||
//! ```text
|
||||
//! # Kaitai type: page_payload
|
||||
//! # Kaitai params:
|
||||
//! # Kaitai offset: 0
|
||||
//! ```
|
||||
//!
|
||||
//! These select a type from `snapshot.ksy`, its whitespace-separated
|
||||
//! parameters, and the byte offset where parsing begins. The Kaitai verifier
|
||||
//! discovers fixtures from the filesystem and uses this metadata instead of
|
||||
//! maintaining a parallel filename table. Generated candidates copy the
|
||||
//! metadata from their checked-in reference.
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const envelope = @import("envelope.zig");
|
||||
@@ -128,6 +145,17 @@ pub fn expectEqual(
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
// Kaitai parser selection lives in the reference itself. Read the textual
|
||||
// source so generated candidates preserve that metadata even though the
|
||||
// byte comparison below uses the comptime-decoded reference.
|
||||
const reference_source = try std.Io.Dir.cwd().readFileAlloc(
|
||||
testing.io,
|
||||
reference_path,
|
||||
alloc,
|
||||
.unlimited,
|
||||
);
|
||||
defer alloc.free(reference_source);
|
||||
|
||||
// Always materialize a candidate, even when it will match. This keeps the
|
||||
// generation path exercised on every test run while avoiding repository
|
||||
// artifacts for the normal successful case.
|
||||
@@ -143,7 +171,12 @@ pub fn expectEqual(
|
||||
testing.io,
|
||||
&write_buffer,
|
||||
);
|
||||
try format(kind, actual, &candidate_writer.interface);
|
||||
try format(
|
||||
kind,
|
||||
reference_source,
|
||||
actual,
|
||||
&candidate_writer.interface,
|
||||
);
|
||||
try candidate_writer.interface.flush();
|
||||
}
|
||||
|
||||
@@ -267,14 +300,16 @@ fn hexNibble(comptime value: u8) u8 {
|
||||
|
||||
fn format(
|
||||
kind: Kind,
|
||||
reference_source: []const u8,
|
||||
bytes: []const u8,
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
) (std.Io.Writer.Error || error{InvalidKaitaiMetadata})!void {
|
||||
// Every generated candidate starts with enough maintenance context to be
|
||||
// understandable when copied out of a failing test's temp directory.
|
||||
try writer.writeAll("# Ghostty snapshot fixture\n");
|
||||
try writeKaitaiMetadata(reference_source, writer);
|
||||
try writer.print(
|
||||
"# Ghostty snapshot fixture\n" ++
|
||||
"# Wire version: {}\n" ++
|
||||
"# Wire version: {}\n" ++
|
||||
"# Generated by its snapshot test; review before replacing.\n" ++
|
||||
"# On mismatch, the candidate is copied to the repository root.\n",
|
||||
.{envelope.version},
|
||||
@@ -295,6 +330,34 @@ fn format(
|
||||
}
|
||||
}
|
||||
|
||||
fn writeKaitaiMetadata(
|
||||
reference_source: []const u8,
|
||||
writer: *std.Io.Writer,
|
||||
) (std.Io.Writer.Error || error{InvalidKaitaiMetadata})!void {
|
||||
const prefixes = [_][]const u8{
|
||||
"# Kaitai type:",
|
||||
"# Kaitai params:",
|
||||
"# Kaitai offset:",
|
||||
};
|
||||
var found = [_]bool{false} ** prefixes.len;
|
||||
|
||||
var lines = std.mem.splitScalar(u8, reference_source, '\n');
|
||||
while (lines.next()) |untrimmed| {
|
||||
const line = std.mem.trim(u8, untrimmed, " \r");
|
||||
for (prefixes, &found) |prefix, *seen| {
|
||||
if (!std.mem.startsWith(u8, line, prefix)) continue;
|
||||
if (seen.*) return error.InvalidKaitaiMetadata;
|
||||
seen.* = true;
|
||||
try writer.print("{s}\n", .{line});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (found) |seen| {
|
||||
if (!seen) return error.InvalidKaitaiMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
fn formatPage(
|
||||
bytes: []const u8,
|
||||
writer: *std.Io.Writer,
|
||||
|
||||
@@ -288,9 +288,7 @@ pub fn decode(
|
||||
// the destination PageList to allocate the final backing memory once.
|
||||
var decoder: page.Decoder = undefined;
|
||||
try decoder.init(source);
|
||||
var allocation = try terminal_screen.pages.allocatePage(
|
||||
decoder.capacity(),
|
||||
);
|
||||
var allocation = try terminal_screen.pages.allocatePage(decoder.capacity());
|
||||
defer allocation.deinit();
|
||||
try decoder.decode(allocation.page(), alloc);
|
||||
|
||||
@@ -319,9 +317,7 @@ pub fn decode(
|
||||
}
|
||||
|
||||
fn hasSemanticPrompt(terminal_page: *const TerminalPage) bool {
|
||||
const rows = terminal_page.rows.ptr(
|
||||
terminal_page.memory,
|
||||
)[0..terminal_page.size.rows];
|
||||
const rows = terminal_page.rows.ptr(terminal_page.memory)[0..terminal_page.size.rows];
|
||||
for (rows) |row| {
|
||||
if (row.semantic_prompt != .none) return true;
|
||||
|
||||
@@ -335,9 +331,7 @@ fn hasSemanticPrompt(terminal_page: *const TerminalPage) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
const test_header_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/history-header-v1.hex"),
|
||||
);
|
||||
const test_header_fixture = test_fixture.parse(@embedFile("testdata/history-header-v1.hex"));
|
||||
|
||||
test "HISTORY header golden encoding and decoding" {
|
||||
const expected: Header = .{
|
||||
|
||||
@@ -271,13 +271,8 @@ fn decodePageString(
|
||||
};
|
||||
}
|
||||
|
||||
const test_implicit_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/hyperlink-implicit-v1.hex"),
|
||||
);
|
||||
|
||||
const test_explicit_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/hyperlink-explicit-v1.hex"),
|
||||
);
|
||||
const test_implicit_fixture = test_fixture.parse(@embedFile("testdata/hyperlink-implicit-v1.hex"));
|
||||
const test_explicit_fixture = test_fixture.parse(@embedFile("testdata/hyperlink-explicit-v1.hex"));
|
||||
|
||||
test "golden implicit encoding" {
|
||||
const value: terminal_hyperlink.Hyperlink = .{
|
||||
|
||||
@@ -547,17 +547,9 @@ fn pageHyperlink(
|
||||
};
|
||||
}
|
||||
|
||||
const test_page_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/page-v1.hex"),
|
||||
);
|
||||
|
||||
const test_header_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/page-header-v1.hex"),
|
||||
);
|
||||
|
||||
const test_empty_framed_page_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/page-empty-record-v1.hex"),
|
||||
);
|
||||
const test_page_fixture = test_fixture.parse(@embedFile("testdata/page-v1.hex"));
|
||||
const test_header_fixture = test_fixture.parse(@embedFile("testdata/page-header-v1.hex"));
|
||||
const test_empty_framed_page_fixture = test_fixture.parse(@embedFile("testdata/page-empty-record-v1.hex"));
|
||||
|
||||
test "PAGE header golden encoding and decoding" {
|
||||
const header: Header = .{
|
||||
|
||||
@@ -1117,13 +1117,8 @@ pub fn decodeCursorHyperlink(
|
||||
return try hyperlink.decode(reader, alloc);
|
||||
}
|
||||
|
||||
const test_header_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/screen-header-v1.hex"),
|
||||
);
|
||||
|
||||
const test_saved_cursor_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/screen-saved-cursor-v1.hex"),
|
||||
);
|
||||
const test_header_fixture = test_fixture.parse(@embedFile("testdata/screen-header-v1.hex"));
|
||||
const test_saved_cursor_fixture = test_fixture.parse(@embedFile("testdata/screen-saved-cursor-v1.hex"));
|
||||
|
||||
fn testCharsetState() TerminalScreen.CharsetState {
|
||||
var result: TerminalScreen.CharsetState = .{
|
||||
|
||||
998
src/terminal/snapshot/snapshot.ksy
Normal file
998
src/terminal/snapshot/snapshot.ksy
Normal file
@@ -0,0 +1,998 @@
|
||||
meta:
|
||||
id: ghostty_snapshot
|
||||
title: Ghostty terminal snapshot
|
||||
application: Ghostty
|
||||
license: MIT
|
||||
endian: le
|
||||
doc: |
|
||||
Ghostty terminal snapshot format version 1.
|
||||
|
||||
A complete snapshot contains an envelope, terminal-wide state, one or two
|
||||
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.
|
||||
|
||||
Record CRC32C values and checkpoint BLAKE3-256 digests are represented here
|
||||
but cannot be calculated by portable Kaitai Struct expressions. The adjacent
|
||||
verify-kaitai.py script validates those values after parsing.
|
||||
seq:
|
||||
- id: envelope
|
||||
type: envelope
|
||||
- id: terminal
|
||||
type: terminal_record
|
||||
- id: screens
|
||||
type: screen_sequence
|
||||
repeat: expr
|
||||
repeat-expr: terminal.payload.header.screen_count
|
||||
- id: ready
|
||||
type: checkpoint_record(5)
|
||||
- id: histories
|
||||
type: history_sequence
|
||||
repeat: expr
|
||||
repeat-expr: terminal.payload.header.screen_count
|
||||
- id: finish
|
||||
type: checkpoint_record(6)
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
expr: _.size == 0
|
||||
enums:
|
||||
record_tag:
|
||||
1: terminal
|
||||
2: screen
|
||||
3: page
|
||||
4: history
|
||||
5: ready
|
||||
6: finish
|
||||
screen_key:
|
||||
0: primary
|
||||
1: alternate
|
||||
cursor_style:
|
||||
0: bar
|
||||
1: block
|
||||
2: underline
|
||||
3: block_hollow
|
||||
status_display:
|
||||
0: main
|
||||
1: status_line
|
||||
shell_redraw:
|
||||
0: full
|
||||
1: none
|
||||
2: last
|
||||
mouse_event:
|
||||
0: none
|
||||
1: x10
|
||||
2: normal
|
||||
3: button
|
||||
4: any
|
||||
mouse_format:
|
||||
0: x10
|
||||
1: utf8
|
||||
2: sgr
|
||||
3: urxvt
|
||||
4: sgr_pixels
|
||||
mouse_shape:
|
||||
0: default
|
||||
1: context_menu
|
||||
2: help
|
||||
3: pointer
|
||||
4: progress
|
||||
5: wait
|
||||
6: cell
|
||||
7: crosshair
|
||||
8: text
|
||||
9: vertical_text
|
||||
10: alias
|
||||
11: copy
|
||||
12: move
|
||||
13: no_drop
|
||||
14: not_allowed
|
||||
15: grab
|
||||
16: grabbing
|
||||
17: all_scroll
|
||||
18: col_resize
|
||||
19: row_resize
|
||||
20: n_resize
|
||||
21: e_resize
|
||||
22: s_resize
|
||||
23: w_resize
|
||||
24: ne_resize
|
||||
25: nw_resize
|
||||
26: se_resize
|
||||
27: sw_resize
|
||||
28: ew_resize
|
||||
29: ns_resize
|
||||
30: nesw_resize
|
||||
31: nwse_resize
|
||||
32: zoom_in
|
||||
33: zoom_out
|
||||
protected_mode:
|
||||
0: "off"
|
||||
1: iso
|
||||
2: dec
|
||||
semantic_click_kind:
|
||||
0: none
|
||||
1: click_events
|
||||
2: cl
|
||||
color_kind:
|
||||
0: none
|
||||
1: palette
|
||||
2: rgb
|
||||
hyperlink_kind:
|
||||
0: none
|
||||
1: implicit
|
||||
2: explicit
|
||||
cell_content_kind:
|
||||
0: codepoint
|
||||
1: background_palette
|
||||
2: background_rgb
|
||||
cell_width:
|
||||
0: narrow
|
||||
1: wide
|
||||
2: spacer_tail
|
||||
3: spacer_head
|
||||
underline:
|
||||
0: none
|
||||
1: single
|
||||
2: double
|
||||
3: curly
|
||||
4: dotted
|
||||
5: dashed
|
||||
semantic_content:
|
||||
0: output
|
||||
1: input
|
||||
2: prompt
|
||||
semantic_prompt:
|
||||
0: none
|
||||
1: prompt
|
||||
2: prompt_continuation
|
||||
types:
|
||||
envelope:
|
||||
doc: Fixed ten-byte snapshot identification and version header.
|
||||
seq:
|
||||
- id: magic
|
||||
contents: [0x47, 0x48, 0x4f, 0x53, 0x54, 0x53, 0x4e, 0x50]
|
||||
- id: version
|
||||
type: u2
|
||||
valid: 1
|
||||
|
||||
record_header:
|
||||
doc: |
|
||||
Common record framing. crc32c covers the encoded tag, payload length,
|
||||
and payload bytes, excluding the crc32c field itself.
|
||||
params:
|
||||
- id: expected_tag
|
||||
type: u2
|
||||
seq:
|
||||
- id: tag
|
||||
type: u2
|
||||
valid: expected_tag
|
||||
- id: payload_length
|
||||
type: u4
|
||||
- id: crc32c
|
||||
type: u4
|
||||
|
||||
terminal_record:
|
||||
seq:
|
||||
- id: header
|
||||
type: record_header(1)
|
||||
- id: payload
|
||||
type: terminal_payload
|
||||
size: header.payload_length
|
||||
|
||||
screen_record:
|
||||
seq:
|
||||
- id: header
|
||||
type: record_header(2)
|
||||
- id: payload
|
||||
type: screen_payload
|
||||
size: header.payload_length
|
||||
|
||||
page_record:
|
||||
seq:
|
||||
- id: header
|
||||
type: record_header(3)
|
||||
- id: payload
|
||||
type: page_payload
|
||||
size: header.payload_length
|
||||
|
||||
history_record:
|
||||
seq:
|
||||
- id: header
|
||||
type: record_header(4)
|
||||
- id: payload
|
||||
type: history_payload
|
||||
size: header.payload_length
|
||||
|
||||
checkpoint_record:
|
||||
params:
|
||||
- id: expected_tag
|
||||
type: u2
|
||||
seq:
|
||||
- id: header
|
||||
type: record_header(expected_tag)
|
||||
- id: payload
|
||||
type: checkpoint_payload
|
||||
size: header.payload_length
|
||||
|
||||
screen_sequence:
|
||||
doc: SCREEN followed by its declared PAGE records, oldest-to-newest.
|
||||
seq:
|
||||
- id: screen
|
||||
type: screen_record
|
||||
- id: pages
|
||||
type: page_record
|
||||
repeat: expr
|
||||
repeat-expr: screen.payload.header.page_count
|
||||
|
||||
history_sequence:
|
||||
doc: HISTORY followed by its declared PAGE records, newest-to-oldest.
|
||||
seq:
|
||||
- id: history
|
||||
type: history_record
|
||||
- id: pages
|
||||
type: page_record
|
||||
repeat: expr
|
||||
repeat-expr: history.payload.page_count
|
||||
|
||||
checkpoint_payload:
|
||||
seq:
|
||||
- id: prefix_digest
|
||||
size: 32
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
expr: _.size == 0
|
||||
|
||||
terminal_payload:
|
||||
seq:
|
||||
- id: header
|
||||
type: terminal_header
|
||||
- id: tab_stops
|
||||
type: tab_stops(header.columns)
|
||||
- id: original_palette
|
||||
type: rgb
|
||||
repeat: expr
|
||||
repeat-expr: 256
|
||||
- id: palette_override_mask
|
||||
size: 32
|
||||
- id: palette_overrides
|
||||
type: palette_override(palette_override_mask, _index)
|
||||
repeat: expr
|
||||
repeat-expr: 256
|
||||
- id: len_pwd
|
||||
type: u4
|
||||
- id: pwd
|
||||
size: len_pwd
|
||||
- id: len_title
|
||||
type: u4
|
||||
- id: title
|
||||
size: len_title
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
expr: _.size == 0
|
||||
|
||||
terminal_header:
|
||||
seq:
|
||||
- id: columns
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: rows
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: width_px
|
||||
type: u4
|
||||
- id: height_px
|
||||
type: u4
|
||||
- id: scrolling_region_top
|
||||
type: u2
|
||||
- id: scrolling_region_bottom
|
||||
type: u2
|
||||
valid:
|
||||
expr: _ >= scrolling_region_top and _ < rows
|
||||
- id: scrolling_region_left
|
||||
type: u2
|
||||
- id: scrolling_region_right
|
||||
type: u2
|
||||
valid:
|
||||
expr: _ >= scrolling_region_left and _ < columns
|
||||
- id: status_display
|
||||
type: u1
|
||||
enum: status_display
|
||||
valid:
|
||||
expr: _.to_i <= 1
|
||||
- id: active_screen_key
|
||||
type: u2
|
||||
enum: screen_key
|
||||
valid:
|
||||
expr: _.to_i <= 1
|
||||
- id: screen_count
|
||||
type: u2
|
||||
valid:
|
||||
expr: (_ == 1 or _ == 2) and (active_screen_key.to_i == 0 or _ == 2)
|
||||
- id: previous_codepoint
|
||||
type: u4
|
||||
valid:
|
||||
expr: _ == 0xffffffff or (_ <= 0x10ffff and not (_ >= 0xd800 and _ <= 0xdfff))
|
||||
- id: cursor_is_default
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
- id: cursor_default_style
|
||||
type: u1
|
||||
enum: cursor_style
|
||||
valid:
|
||||
expr: _.to_i <= 3
|
||||
- id: cursor_default_blink
|
||||
type: u1
|
||||
valid:
|
||||
max: 2
|
||||
- id: shell_redraw
|
||||
type: u1
|
||||
enum: shell_redraw
|
||||
valid:
|
||||
expr: _.to_i <= 2
|
||||
- id: modify_other_keys_2
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
- id: mouse_event
|
||||
type: u1
|
||||
enum: mouse_event
|
||||
valid:
|
||||
expr: _.to_i <= 4
|
||||
- id: mouse_format
|
||||
type: u1
|
||||
enum: mouse_format
|
||||
valid:
|
||||
expr: _.to_i <= 4
|
||||
- id: mouse_shift_capture
|
||||
type: u1
|
||||
valid:
|
||||
max: 2
|
||||
- id: mouse_shape
|
||||
type: u1
|
||||
enum: mouse_shape
|
||||
valid:
|
||||
expr: _.to_i <= 33
|
||||
- id: password_input
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
- id: current_modes
|
||||
type: mode_set
|
||||
- id: saved_modes
|
||||
type: mode_set
|
||||
- id: default_modes
|
||||
type: mode_set
|
||||
- id: background
|
||||
type: dynamic_rgb
|
||||
- id: foreground
|
||||
type: dynamic_rgb
|
||||
- id: cursor_color
|
||||
type: dynamic_rgb
|
||||
- id: max_scrollback_bytes
|
||||
type: u8
|
||||
- id: max_scrollback_rows
|
||||
type: u8
|
||||
|
||||
mode_set:
|
||||
doc: |
|
||||
The stable packed registry shared by current, saved, and default modes.
|
||||
Each named instance exposes one bit from the little-endian integer.
|
||||
Arithmetic division is used instead of bitwise operations because the
|
||||
JavaScript target implements those operations with signed 32-bit values.
|
||||
All values remain exact because the registry occupies only 42 bits,
|
||||
within JavaScript's 53-bit safe integer range.
|
||||
seq:
|
||||
- id: raw
|
||||
type: u8
|
||||
valid:
|
||||
max: 4398046511103
|
||||
instances:
|
||||
disable_keyboard:
|
||||
value: (raw / 1) % 2 != 0
|
||||
insert:
|
||||
value: (raw / 2) % 2 != 0
|
||||
send_receive_mode:
|
||||
value: (raw / 4) % 2 != 0
|
||||
linefeed:
|
||||
value: (raw / 8) % 2 != 0
|
||||
cursor_keys:
|
||||
value: (raw / 16) % 2 != 0
|
||||
column_132:
|
||||
value: (raw / 32) % 2 != 0
|
||||
slow_scroll:
|
||||
value: (raw / 64) % 2 != 0
|
||||
reverse_colors:
|
||||
value: (raw / 128) % 2 != 0
|
||||
origin:
|
||||
value: (raw / 256) % 2 != 0
|
||||
wraparound:
|
||||
value: (raw / 512) % 2 != 0
|
||||
autorepeat:
|
||||
value: (raw / 1024) % 2 != 0
|
||||
mouse_event_x10:
|
||||
value: (raw / 2048) % 2 != 0
|
||||
cursor_blinking:
|
||||
value: (raw / 4096) % 2 != 0
|
||||
cursor_visible:
|
||||
value: (raw / 8192) % 2 != 0
|
||||
enable_mode_3:
|
||||
value: (raw / 16384) % 2 != 0
|
||||
reverse_wrap:
|
||||
value: (raw / 32768) % 2 != 0
|
||||
alt_screen_legacy:
|
||||
value: (raw / 65536) % 2 != 0
|
||||
keypad_keys:
|
||||
value: (raw / 131072) % 2 != 0
|
||||
backarrow_key_mode:
|
||||
value: (raw / 262144) % 2 != 0
|
||||
enable_left_and_right_margin:
|
||||
value: (raw / 524288) % 2 != 0
|
||||
mouse_event_normal:
|
||||
value: (raw / 1048576) % 2 != 0
|
||||
mouse_event_button:
|
||||
value: (raw / 2097152) % 2 != 0
|
||||
mouse_event_any:
|
||||
value: (raw / 4194304) % 2 != 0
|
||||
focus_event:
|
||||
value: (raw / 8388608) % 2 != 0
|
||||
mouse_format_utf8:
|
||||
value: (raw / 16777216) % 2 != 0
|
||||
mouse_format_sgr:
|
||||
value: (raw / 33554432) % 2 != 0
|
||||
mouse_alternate_scroll:
|
||||
value: (raw / 67108864) % 2 != 0
|
||||
mouse_format_urxvt:
|
||||
value: (raw / 134217728) % 2 != 0
|
||||
mouse_format_sgr_pixels:
|
||||
value: (raw / 268435456) % 2 != 0
|
||||
ignore_keypad_with_numlock:
|
||||
value: (raw / 536870912) % 2 != 0
|
||||
alt_esc_prefix:
|
||||
value: (raw / 1073741824) % 2 != 0
|
||||
alt_sends_escape:
|
||||
value: (raw / 2147483648) % 2 != 0
|
||||
reverse_wrap_extended:
|
||||
value: (raw / 4294967296) % 2 != 0
|
||||
alt_screen:
|
||||
value: (raw / 8589934592) % 2 != 0
|
||||
save_cursor:
|
||||
value: (raw / 17179869184) % 2 != 0
|
||||
alt_screen_save_cursor_clear_enter:
|
||||
value: (raw / 34359738368) % 2 != 0
|
||||
bracketed_paste:
|
||||
value: (raw / 68719476736) % 2 != 0
|
||||
synchronized_output:
|
||||
value: (raw / 137438953472) % 2 != 0
|
||||
grapheme_cluster:
|
||||
value: (raw / 274877906944) % 2 != 0
|
||||
report_color_scheme:
|
||||
value: (raw / 549755813888) % 2 != 0
|
||||
report_visibility:
|
||||
value: (raw / 1099511627776) % 2 != 0
|
||||
in_band_size_reports:
|
||||
value: (raw / 2199023255552) % 2 != 0
|
||||
|
||||
tab_stops:
|
||||
params:
|
||||
- id: columns
|
||||
type: u2
|
||||
seq:
|
||||
- id: complete_bytes
|
||||
size: columns / 8
|
||||
- id: partial_byte
|
||||
type: u1
|
||||
if: columns % 8 != 0
|
||||
valid:
|
||||
expr: _ < (1 << (columns % 8))
|
||||
|
||||
rgb:
|
||||
seq:
|
||||
- id: red
|
||||
type: u1
|
||||
- id: green
|
||||
type: u1
|
||||
- id: blue
|
||||
type: u1
|
||||
|
||||
dynamic_rgb:
|
||||
seq:
|
||||
- id: default_present
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
- id: default_red
|
||||
type: u1
|
||||
valid:
|
||||
expr: default_present == 1 or _ == 0
|
||||
- id: default_green
|
||||
type: u1
|
||||
valid:
|
||||
expr: default_present == 1 or _ == 0
|
||||
- id: default_blue
|
||||
type: u1
|
||||
valid:
|
||||
expr: default_present == 1 or _ == 0
|
||||
- id: override_present
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
- id: override_red
|
||||
type: u1
|
||||
valid:
|
||||
expr: override_present == 1 or _ == 0
|
||||
- id: override_green
|
||||
type: u1
|
||||
valid:
|
||||
expr: override_present == 1 or _ == 0
|
||||
- id: override_blue
|
||||
type: u1
|
||||
valid:
|
||||
expr: override_present == 1 or _ == 0
|
||||
|
||||
palette_override:
|
||||
params:
|
||||
- id: mask
|
||||
type: bytes
|
||||
- id: index
|
||||
type: u2
|
||||
seq:
|
||||
- id: color
|
||||
type: rgb
|
||||
if: (mask[index / 8] & (1 << (index % 8))) != 0
|
||||
|
||||
screen_payload:
|
||||
seq:
|
||||
- id: header
|
||||
type: screen_header
|
||||
- id: saved_cursor
|
||||
type: saved_cursor
|
||||
if: header.saved_cursor_present == 1
|
||||
- id: cursor_hyperlink
|
||||
type: hyperlink(true, false)
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
expr: _.size == 0
|
||||
|
||||
screen_header:
|
||||
seq:
|
||||
- id: key
|
||||
type: u2
|
||||
enum: screen_key
|
||||
valid:
|
||||
expr: _.to_i <= 1
|
||||
- id: page_count
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: cursor_x
|
||||
type: u2
|
||||
- id: cursor_y
|
||||
type: u2
|
||||
- id: cursor_style
|
||||
type: u1
|
||||
enum: cursor_style
|
||||
valid:
|
||||
expr: _.to_i <= 3
|
||||
- id: cursor_flags
|
||||
type: cursor_flags
|
||||
- id: cursor_pen
|
||||
type: style
|
||||
- id: hyperlink_implicit_id
|
||||
type: u4
|
||||
- id: charset
|
||||
type: charset_state
|
||||
- id: protected_mode
|
||||
type: u1
|
||||
enum: protected_mode
|
||||
valid:
|
||||
expr: _.to_i <= 2
|
||||
- id: kitty_keyboard_index
|
||||
type: u1
|
||||
valid:
|
||||
max: 7
|
||||
- id: kitty_keyboard_flags
|
||||
type: kitty_keyboard_flags
|
||||
repeat: expr
|
||||
repeat-expr: 8
|
||||
- id: semantic_click_kind
|
||||
type: u1
|
||||
enum: semantic_click_kind
|
||||
valid:
|
||||
expr: _.to_i <= 2
|
||||
- id: semantic_click_value
|
||||
type: u1
|
||||
valid:
|
||||
expr: |
|
||||
semantic_click_kind.to_i == 0 ? _ == 0 :
|
||||
semantic_click_kind.to_i == 1 ? _ <= 1 :
|
||||
_ <= 3
|
||||
- id: saved_cursor_present
|
||||
type: u1
|
||||
valid:
|
||||
max: 1
|
||||
|
||||
cursor_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u1
|
||||
valid:
|
||||
expr: (_ & 0xe0) == 0 and ((_ >> 2) & 0x3) <= 2
|
||||
instances:
|
||||
pending_wrap:
|
||||
value: (raw & (1 << 0)) != 0
|
||||
protected:
|
||||
value: (raw & (1 << 1)) != 0
|
||||
semantic_content:
|
||||
value: (raw >> 2) & 0x3
|
||||
semantic_content_clear_eol:
|
||||
value: (raw & (1 << 4)) != 0
|
||||
|
||||
charset_state:
|
||||
doc: |
|
||||
Four selected character sets, the GL and GR slots, and an optional
|
||||
single-shift slot. single_shift is zero for none or one plus a slot.
|
||||
seq:
|
||||
- id: raw
|
||||
type: u2
|
||||
valid:
|
||||
expr: (_ & 0x8000) == 0 and ((_ >> 12) & 0x7) <= 4
|
||||
instances:
|
||||
g0:
|
||||
value: (raw >> 0) & 0x3
|
||||
g1:
|
||||
value: (raw >> 2) & 0x3
|
||||
g2:
|
||||
value: (raw >> 4) & 0x3
|
||||
g3:
|
||||
value: (raw >> 6) & 0x3
|
||||
gl:
|
||||
value: (raw >> 8) & 0x3
|
||||
gr:
|
||||
value: (raw >> 10) & 0x3
|
||||
single_shift:
|
||||
value: (raw >> 12) & 0x7
|
||||
|
||||
kitty_keyboard_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u1
|
||||
valid:
|
||||
expr: (_ & 0xe0) == 0
|
||||
instances:
|
||||
disambiguate:
|
||||
value: (raw & (1 << 0)) != 0
|
||||
report_events:
|
||||
value: (raw & (1 << 1)) != 0
|
||||
report_alternates:
|
||||
value: (raw & (1 << 2)) != 0
|
||||
report_all:
|
||||
value: (raw & (1 << 3)) != 0
|
||||
report_associated:
|
||||
value: (raw & (1 << 4)) != 0
|
||||
|
||||
saved_cursor:
|
||||
seq:
|
||||
- id: x
|
||||
type: u2
|
||||
- id: y
|
||||
type: u2
|
||||
- id: pen
|
||||
type: style
|
||||
- id: flags
|
||||
type: saved_cursor_flags
|
||||
- id: charset
|
||||
type: charset_state
|
||||
|
||||
saved_cursor_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u1
|
||||
valid:
|
||||
expr: (_ & 0xf8) == 0
|
||||
instances:
|
||||
protected:
|
||||
value: (raw & (1 << 0)) != 0
|
||||
pending_wrap:
|
||||
value: (raw & (1 << 1)) != 0
|
||||
origin:
|
||||
value: (raw & (1 << 2)) != 0
|
||||
|
||||
history_payload:
|
||||
seq:
|
||||
- id: key
|
||||
type: u2
|
||||
enum: screen_key
|
||||
valid:
|
||||
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:
|
||||
expr: _.size == 0
|
||||
|
||||
page_payload:
|
||||
seq:
|
||||
- id: header
|
||||
type: page_header
|
||||
- id: styles
|
||||
type: style_table_entry
|
||||
repeat: expr
|
||||
repeat-expr: header.style_count
|
||||
- id: hyperlinks
|
||||
type: hyperlink_table_entry
|
||||
repeat: expr
|
||||
repeat-expr: header.hyperlink_count
|
||||
- id: grid
|
||||
type: grid(header.rows, header.columns)
|
||||
- id: trailing_data
|
||||
size-eos: true
|
||||
valid:
|
||||
expr: _.size == 0
|
||||
|
||||
page_header:
|
||||
seq:
|
||||
- id: columns
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: rows
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: style_count
|
||||
type: u2
|
||||
- id: hyperlink_count
|
||||
type: u2
|
||||
- id: style_capacity
|
||||
type: u2
|
||||
- id: hyperlink_capacity_bytes
|
||||
type: u2
|
||||
- id: grapheme_capacity_bytes
|
||||
type: u4
|
||||
- id: string_capacity_bytes
|
||||
type: u4
|
||||
|
||||
style_table_entry:
|
||||
seq:
|
||||
- id: encoded_id
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: value
|
||||
type: style
|
||||
|
||||
hyperlink_table_entry:
|
||||
seq:
|
||||
- id: encoded_id
|
||||
type: u2
|
||||
valid:
|
||||
min: 1
|
||||
- id: value
|
||||
type: hyperlink(false, true)
|
||||
|
||||
style:
|
||||
seq:
|
||||
- id: foreground
|
||||
type: style_color
|
||||
- id: background
|
||||
type: style_color
|
||||
- id: underline_color
|
||||
type: style_color
|
||||
- id: flags
|
||||
type: style_flags
|
||||
- id: reserved
|
||||
type: u2
|
||||
valid: 0
|
||||
|
||||
style_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u2
|
||||
valid:
|
||||
expr: (_ & 0xf800) == 0 and ((_ >> 8) & 0x7) <= 5
|
||||
instances:
|
||||
bold:
|
||||
value: (raw & (1 << 0)) != 0
|
||||
italic:
|
||||
value: (raw & (1 << 1)) != 0
|
||||
faint:
|
||||
value: (raw & (1 << 2)) != 0
|
||||
blink:
|
||||
value: (raw & (1 << 3)) != 0
|
||||
inverse:
|
||||
value: (raw & (1 << 4)) != 0
|
||||
invisible:
|
||||
value: (raw & (1 << 5)) != 0
|
||||
strikethrough:
|
||||
value: (raw & (1 << 6)) != 0
|
||||
overline:
|
||||
value: (raw & (1 << 7)) != 0
|
||||
underline:
|
||||
value: (raw >> 8) & 0x7
|
||||
|
||||
style_color:
|
||||
seq:
|
||||
- id: kind
|
||||
type: u1
|
||||
enum: color_kind
|
||||
valid:
|
||||
expr: _.to_i <= 2
|
||||
- id: first
|
||||
type: u1
|
||||
valid:
|
||||
expr: kind.to_i != 0 or _ == 0
|
||||
- id: second
|
||||
type: u1
|
||||
valid:
|
||||
expr: kind.to_i == 2 or _ == 0
|
||||
- id: third
|
||||
type: u1
|
||||
valid:
|
||||
expr: kind.to_i == 2 or _ == 0
|
||||
|
||||
hyperlink:
|
||||
params:
|
||||
- id: allow_none
|
||||
type: bool
|
||||
- id: allow_empty
|
||||
type: bool
|
||||
seq:
|
||||
- id: kind
|
||||
type: u1
|
||||
enum: hyperlink_kind
|
||||
valid:
|
||||
expr: _.to_i <= 2 and (allow_none or _.to_i != 0)
|
||||
- id: implicit
|
||||
type: implicit_hyperlink(allow_empty)
|
||||
if: kind.to_i == 1
|
||||
- id: explicit
|
||||
type: explicit_hyperlink(allow_empty)
|
||||
if: kind.to_i == 2
|
||||
|
||||
implicit_hyperlink:
|
||||
params:
|
||||
- id: allow_empty
|
||||
type: bool
|
||||
seq:
|
||||
- id: id
|
||||
type: u4
|
||||
- id: len_uri
|
||||
type: u4
|
||||
valid:
|
||||
expr: allow_empty or _ >= 1
|
||||
- id: uri
|
||||
size: len_uri
|
||||
|
||||
explicit_hyperlink:
|
||||
params:
|
||||
- id: allow_empty
|
||||
type: bool
|
||||
seq:
|
||||
- id: len_id
|
||||
type: u4
|
||||
valid:
|
||||
expr: allow_empty or _ >= 1
|
||||
- id: id
|
||||
size: len_id
|
||||
- id: len_uri
|
||||
type: u4
|
||||
valid:
|
||||
expr: allow_empty or _ >= 1
|
||||
- id: uri
|
||||
size: len_uri
|
||||
|
||||
grid:
|
||||
params:
|
||||
- id: num_rows
|
||||
type: u2
|
||||
- id: columns
|
||||
type: u2
|
||||
seq:
|
||||
- id: rows
|
||||
type: grid_row(columns)
|
||||
repeat: expr
|
||||
repeat-expr: num_rows
|
||||
|
||||
grid_row:
|
||||
params:
|
||||
- id: num_cells
|
||||
type: u2
|
||||
seq:
|
||||
- id: flags
|
||||
type: grid_row_flags
|
||||
- id: cells
|
||||
type: grid_cell(_index, num_cells, flags.wrap)
|
||||
repeat: expr
|
||||
repeat-expr: num_cells
|
||||
|
||||
grid_row_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u1
|
||||
valid:
|
||||
expr: (_ & 0xf0) == 0 and ((_ >> 2) & 0x3) <= 2
|
||||
instances:
|
||||
wrap:
|
||||
value: (raw & 1) != 0
|
||||
wrap_continuation:
|
||||
value: (raw & 2) != 0
|
||||
semantic_prompt:
|
||||
value: (raw >> 2) & 0x3
|
||||
|
||||
grid_cell:
|
||||
params:
|
||||
- id: index
|
||||
type: u2
|
||||
- id: columns
|
||||
type: u2
|
||||
- id: row_wrap
|
||||
type: bool
|
||||
seq:
|
||||
- id: content_kind
|
||||
type: u1
|
||||
valid:
|
||||
max: 2
|
||||
- id: width
|
||||
type: u1
|
||||
valid:
|
||||
expr: |
|
||||
_ <= 3 and
|
||||
(_ != 2 or
|
||||
(index > 0 and _parent.cells[index - 1].width == 1)) and
|
||||
(_ != 3 or
|
||||
(index + 1 == columns and row_wrap))
|
||||
- id: flags
|
||||
type: grid_cell_flags
|
||||
- id: reserved
|
||||
type: u1
|
||||
valid: 0
|
||||
- id: style_id
|
||||
type: u2
|
||||
- id: hyperlink_id
|
||||
type: u2
|
||||
- id: value
|
||||
type: u4
|
||||
valid:
|
||||
expr: |
|
||||
content_kind == 0 ?
|
||||
(_ <= 0x10ffff and not (_ >= 0xd800 and _ <= 0xdfff) and _ != 0x10eeee) :
|
||||
content_kind == 1 ?
|
||||
_ <= 0xff :
|
||||
_ <= 0xffffff
|
||||
- id: num_graphemes
|
||||
type: u4
|
||||
valid:
|
||||
expr: |
|
||||
content_kind == 0 ?
|
||||
(_ == 0 or value != 0) :
|
||||
_ == 0
|
||||
- id: graphemes
|
||||
type: u4
|
||||
repeat: expr
|
||||
repeat-expr: num_graphemes
|
||||
valid:
|
||||
expr: _ <= 0x10ffff and not (_ >= 0xd800 and _ <= 0xdfff) and _ != 0x10eeee
|
||||
|
||||
grid_cell_flags:
|
||||
seq:
|
||||
- id: raw
|
||||
type: u1
|
||||
valid:
|
||||
expr: (_ & 0xf8) == 0 and ((_ >> 1) & 0x3) <= 2
|
||||
instances:
|
||||
protected:
|
||||
value: (raw & (1 << 0)) != 0
|
||||
semantic_content:
|
||||
value: (raw >> 1) & 0x3
|
||||
@@ -216,9 +216,7 @@ fn computeLen() usize {
|
||||
}
|
||||
}
|
||||
|
||||
const test_golden_fixture = test_fixture.parse(
|
||||
@embedFile("testdata/style-v1.hex"),
|
||||
);
|
||||
const test_golden_fixture = test_fixture.parse(@embedFile("testdata/style-v1.hex"));
|
||||
|
||||
test "golden encoding and decoding" {
|
||||
const value: terminal_style.Style = .{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: checkpoint_record
|
||||
# Kaitai params: 5
|
||||
# Kaitai offset: 3
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: ghostty_snapshot
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: envelope
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
3
src/terminal/snapshot/testdata/grid-v1.hex
vendored
3
src/terminal/snapshot/testdata/grid-v1.hex
vendored
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: grid
|
||||
# Kaitai params: 2 3
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: history_payload
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: hyperlink
|
||||
# Kaitai params: false false
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: hyperlink
|
||||
# Kaitai params: false false
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: page_record
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: page_header
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
3
src/terminal/snapshot/testdata/page-v1.hex
vendored
3
src/terminal/snapshot/testdata/page-v1.hex
vendored
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: page_payload
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: record_header
|
||||
# Kaitai params: 3
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: screen_header
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: saved_cursor
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
3
src/terminal/snapshot/testdata/style-v1.hex
vendored
3
src/terminal/snapshot/testdata/style-v1.hex
vendored
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: style
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Ghostty snapshot fixture
|
||||
# Kaitai type: terminal_header
|
||||
# Kaitai params:
|
||||
# Kaitai offset: 0
|
||||
# Wire version: 1
|
||||
# Generated by its snapshot test; review before replacing.
|
||||
# On mismatch, the candidate is copied to the repository root.
|
||||
|
||||
422
src/terminal/snapshot/verify-kaitai.py
Executable file
422
src/terminal/snapshot/verify-kaitai.py
Executable file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the snapshot Kaitai schema and parse every checked-in fixture.
|
||||
|
||||
Run this from a Ghostty development shell:
|
||||
|
||||
src/terminal/snapshot/verify-kaitai.py
|
||||
|
||||
To turn an annotated fixture into a binary suitable for the Kaitai Web IDE:
|
||||
|
||||
src/terminal/snapshot/verify-kaitai.py \
|
||||
--write-binary \
|
||||
src/terminal/snapshot/testdata/complete-v1.hex \
|
||||
/tmp/complete-v1.bin
|
||||
|
||||
Then load `snapshot.ksy` and the generated binary into
|
||||
https://ide.kaitai.io/.
|
||||
|
||||
The generated Python parser exists only in a temporary directory. This keeps
|
||||
snapshot.ksy as the source of truth and ensures this check cannot accidentally
|
||||
pass against stale generated code. After structural parsing, the script checks
|
||||
record CRC32C values, checkpoint BLAKE3 digests, and cross-record invariants
|
||||
that portable Kaitai expressions cannot represent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import io
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from blake3 import blake3
|
||||
from kaitaistruct import KaitaiStream
|
||||
except ImportError as error:
|
||||
raise SystemExit(
|
||||
"missing Kaitai verifier dependencies; run this inside "
|
||||
"`nix develop`"
|
||||
) from error
|
||||
|
||||
|
||||
SNAPSHOT_DIR = Path(__file__).resolve().parent
|
||||
SCHEMA_PATH = SNAPSHOT_DIR / "snapshot.ksy"
|
||||
TESTDATA_DIR = SNAPSHOT_DIR / "testdata"
|
||||
SNAPSHOT_ENVELOPE_SIZE = 10
|
||||
RECORD_HEADER_SIZE = struct.calcsize("<HII")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fixture:
|
||||
path: Path
|
||||
type_name: str
|
||||
params: tuple[object, ...]
|
||||
offset: int
|
||||
data: bytes
|
||||
|
||||
|
||||
def load_fixture(path: Path) -> Fixture:
|
||||
"""Load one self-describing annotated hexadecimal fixture."""
|
||||
metadata: dict[str, str] = {}
|
||||
chunks: list[str] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
content, separator, comment = line.partition("#")
|
||||
chunks.extend(content.split())
|
||||
|
||||
if not separator:
|
||||
continue
|
||||
comment = comment.strip()
|
||||
if not comment.startswith("Kaitai "):
|
||||
continue
|
||||
key, colon, value = comment.removeprefix("Kaitai ").partition(":")
|
||||
if not colon or key not in {"type", "params", "offset"}:
|
||||
raise ValueError(f"{path}: invalid Kaitai metadata: {comment}")
|
||||
if key in metadata:
|
||||
raise ValueError(f"{path}: duplicate Kaitai {key} metadata")
|
||||
metadata[key] = value.strip()
|
||||
|
||||
try:
|
||||
data = bytes.fromhex(" ".join(chunks))
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{path}: invalid annotated hexadecimal") from error
|
||||
|
||||
required = {"type", "params", "offset"}
|
||||
if metadata.keys() != required:
|
||||
missing = sorted(required - metadata.keys())
|
||||
extra = sorted(metadata.keys() - required)
|
||||
raise ValueError(
|
||||
f"{path}: Kaitai metadata drift: missing={missing}, extra={extra}"
|
||||
)
|
||||
if not metadata["type"]:
|
||||
raise ValueError(f"{path}: empty Kaitai type")
|
||||
|
||||
params: list[object] = []
|
||||
for value in metadata["params"].split():
|
||||
if value == "true":
|
||||
params.append(True)
|
||||
elif value == "false":
|
||||
params.append(False)
|
||||
else:
|
||||
try:
|
||||
params.append(int(value, 0))
|
||||
except ValueError as error:
|
||||
raise ValueError(
|
||||
f"{path}: invalid Kaitai parameter {value!r}"
|
||||
) from error
|
||||
|
||||
try:
|
||||
offset = int(metadata["offset"], 0)
|
||||
except ValueError as error:
|
||||
raise ValueError(f"{path}: invalid Kaitai offset") from error
|
||||
if offset < 0 or offset > len(data):
|
||||
raise ValueError(f"{path}: Kaitai offset is outside the fixture")
|
||||
|
||||
return Fixture(
|
||||
path=path,
|
||||
type_name=metadata["type"],
|
||||
params=tuple(params),
|
||||
offset=offset,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
def parse_type(
|
||||
parser_type: type[Any],
|
||||
data: bytes,
|
||||
*params: object,
|
||||
) -> Any:
|
||||
"""Construct one generated Kaitai type and require exact exhaustion."""
|
||||
stream = KaitaiStream(io.BytesIO(data))
|
||||
result = parser_type(*params, stream)
|
||||
if not stream.is_eof():
|
||||
raise ValueError(
|
||||
f"{parser_type.__name__} left "
|
||||
f"{stream.size() - stream.pos()} trailing bytes"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def crc32c(data: bytes) -> int:
|
||||
"""Return CRC32C using the reflected Castagnoli polynomial."""
|
||||
result = 0xFFFFFFFF
|
||||
for byte in data:
|
||||
result ^= byte
|
||||
for _ in range(8):
|
||||
result = (
|
||||
(result >> 1) ^ 0x82F63B78
|
||||
if result & 1
|
||||
else result >> 1
|
||||
)
|
||||
return result ^ 0xFFFFFFFF
|
||||
|
||||
|
||||
def validate_record(record: Any, data: bytes, offset: int) -> int:
|
||||
"""Validate one parsed record's source bytes and CRC32C."""
|
||||
payload = record._raw_payload
|
||||
header = record.header
|
||||
if header.payload_length != len(payload):
|
||||
raise ValueError(
|
||||
f"record at 0x{offset:x}: declared {header.payload_length} "
|
||||
f"payload bytes but parsed {len(payload)}"
|
||||
)
|
||||
|
||||
encoded_header = data[offset : offset + RECORD_HEADER_SIZE]
|
||||
expected_header = struct.pack(
|
||||
"<HII",
|
||||
header.tag,
|
||||
header.payload_length,
|
||||
header.crc32c,
|
||||
)
|
||||
if encoded_header != expected_header:
|
||||
raise ValueError(f"record at 0x{offset:x}: parsed header drift")
|
||||
|
||||
covered = struct.pack("<HI", header.tag, header.payload_length) + payload
|
||||
actual_crc = crc32c(covered)
|
||||
if header.crc32c != actual_crc:
|
||||
raise ValueError(
|
||||
f"record at 0x{offset:x}: CRC32C 0x{header.crc32c:08x} "
|
||||
f"does not match 0x{actual_crc:08x}"
|
||||
)
|
||||
|
||||
return offset + RECORD_HEADER_SIZE + len(payload)
|
||||
|
||||
|
||||
def all_snapshot_records(snapshot: Any) -> list[Any]:
|
||||
"""Return complete-snapshot records in their authenticated wire order."""
|
||||
records = [snapshot.terminal]
|
||||
for sequence in snapshot.screens:
|
||||
records.append(sequence.screen)
|
||||
records.extend(sequence.pages)
|
||||
records.append(snapshot.ready)
|
||||
for sequence in snapshot.histories:
|
||||
records.append(sequence.history)
|
||||
records.extend(sequence.pages)
|
||||
records.append(snapshot.finish)
|
||||
return records
|
||||
|
||||
|
||||
def validate_complete_snapshot(snapshot: Any, data: bytes) -> None:
|
||||
"""Validate ordering relationships, record CRCs, and checkpoints."""
|
||||
screen_keys = [
|
||||
sequence.screen.payload.header.key for sequence in snapshot.screens
|
||||
]
|
||||
history_keys = [
|
||||
sequence.history.payload.key for sequence in snapshot.histories
|
||||
]
|
||||
if len(set(screen_keys)) != len(screen_keys):
|
||||
raise ValueError("complete snapshot contains a duplicate SCREEN key")
|
||||
if len(set(history_keys)) != len(history_keys):
|
||||
raise ValueError("complete snapshot contains a duplicate HISTORY key")
|
||||
if set(screen_keys) != set(history_keys):
|
||||
raise ValueError("SCREEN and HISTORY keys do not match")
|
||||
|
||||
terminal_header = snapshot.terminal.payload.header
|
||||
expected_key_values = (
|
||||
{0} if terminal_header.screen_count == 1 else {0, 1}
|
||||
)
|
||||
screen_key_values = {key.value for key in screen_keys}
|
||||
if screen_key_values != expected_key_values:
|
||||
raise ValueError(
|
||||
f"TERMINAL declares screen keys {expected_key_values}, "
|
||||
f"but SCREEN records contain {screen_key_values}"
|
||||
)
|
||||
|
||||
screens_by_key = {
|
||||
sequence.screen.payload.header.key: sequence
|
||||
for sequence in snapshot.screens
|
||||
}
|
||||
for sequence in snapshot.screens:
|
||||
header = sequence.screen.payload.header
|
||||
if (
|
||||
header.cursor_x >= terminal_header.columns
|
||||
or header.cursor_y >= terminal_header.rows
|
||||
or (
|
||||
header.cursor_flags.pending_wrap
|
||||
and header.cursor_x != terminal_header.columns - 1
|
||||
)
|
||||
):
|
||||
raise ValueError(f"SCREEN key {header.key}: invalid cursor position")
|
||||
|
||||
for sequence in snapshot.histories:
|
||||
payload = sequence.history.payload
|
||||
screen = screens_by_key[payload.key]
|
||||
screen_rows = sum(
|
||||
page.payload.header.rows for page in screen.pages
|
||||
)
|
||||
if screen_rows < terminal_header.rows:
|
||||
raise ValueError(
|
||||
f"SCREEN key {payload.key}: PAGE rows do not cover "
|
||||
"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(
|
||||
page.payload.header.rows for page in sequence.pages
|
||||
)
|
||||
if total_rows != payload.total_rows:
|
||||
raise ValueError(
|
||||
f"HISTORY key {payload.key}: total_rows "
|
||||
f"{payload.total_rows} does not match {total_rows}"
|
||||
)
|
||||
|
||||
for record in all_snapshot_records(snapshot):
|
||||
if not hasattr(record, "payload"):
|
||||
continue
|
||||
payload = record.payload
|
||||
if not hasattr(payload, "styles"):
|
||||
continue
|
||||
style_ids = [entry.encoded_id for entry in payload.styles]
|
||||
hyperlink_ids = [entry.encoded_id for entry in payload.hyperlinks]
|
||||
if len(set(style_ids)) != len(style_ids):
|
||||
raise ValueError("PAGE contains a duplicate style ID")
|
||||
if len(set(hyperlink_ids)) != len(hyperlink_ids):
|
||||
raise ValueError("PAGE contains a duplicate hyperlink ID")
|
||||
|
||||
offset = SNAPSHOT_ENVELOPE_SIZE
|
||||
for record in all_snapshot_records(snapshot):
|
||||
if record is snapshot.ready:
|
||||
expected = blake3(data[:offset]).digest()
|
||||
if record.payload.prefix_digest != expected:
|
||||
raise ValueError("READY BLAKE3-256 digest does not match")
|
||||
elif record is snapshot.finish:
|
||||
expected = blake3(data[:offset]).digest()
|
||||
if record.payload.prefix_digest != expected:
|
||||
raise ValueError("FINISH BLAKE3-256 digest does not match")
|
||||
offset = validate_record(record, data, offset)
|
||||
|
||||
if offset != len(data):
|
||||
raise ValueError(
|
||||
f"complete snapshot accounted for {offset} of {len(data)} bytes"
|
||||
)
|
||||
|
||||
|
||||
def compile_schema(output_dir: Path) -> type[Any]:
|
||||
"""Compile snapshot.ksy and import its temporary Python parser."""
|
||||
compiler = shutil.which("kaitai-struct-compiler") or shutil.which("ksc")
|
||||
if compiler is None:
|
||||
raise SystemExit(
|
||||
"kaitai-struct-compiler is not available; run this inside "
|
||||
"`nix develop`"
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"--target=python",
|
||||
f"--outdir={output_dir}",
|
||||
str(SCHEMA_PATH),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(output_dir))
|
||||
try:
|
||||
module = importlib.import_module("ghostty_snapshot")
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
return module.GhosttySnapshot
|
||||
|
||||
|
||||
def resolve_type(parser: type[Any], type_name: str) -> type[Any]:
|
||||
"""Resolve one KSY snake-case type name on the generated parser."""
|
||||
root_type_name = "".join(
|
||||
f"_{character.lower()}" if character.isupper() else character
|
||||
for character in parser.__name__
|
||||
).lstrip("_")
|
||||
if type_name == root_type_name:
|
||||
return parser
|
||||
|
||||
class_name = "".join(
|
||||
part[:1].upper() + part[1:] for part in type_name.split("_")
|
||||
)
|
||||
try:
|
||||
return getattr(parser, class_name)
|
||||
except AttributeError as error:
|
||||
raise ValueError(f"snapshot.ksy has no type {type_name!r}") from error
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cli = argparse.ArgumentParser(description=__doc__)
|
||||
cli.add_argument(
|
||||
"--write-binary",
|
||||
nargs=2,
|
||||
metavar=("FIXTURE", "OUTPUT"),
|
||||
type=Path,
|
||||
help="decode one annotated fixture into a raw binary",
|
||||
)
|
||||
args = cli.parse_args()
|
||||
|
||||
if args.write_binary:
|
||||
fixture_path, output_path = args.write_binary
|
||||
fixture = load_fixture(fixture_path)
|
||||
output_path.write_bytes(fixture.data)
|
||||
print(f"wrote {len(fixture.data)} bytes to {output_path}")
|
||||
return 0
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="ghostty-kaitai-") as temp:
|
||||
parser = compile_schema(Path(temp))
|
||||
|
||||
fixture_paths = sorted(TESTDATA_DIR.rglob("*.hex"))
|
||||
if not fixture_paths:
|
||||
raise ValueError(f"{TESTDATA_DIR}: no snapshot fixtures")
|
||||
|
||||
for path in fixture_paths:
|
||||
fixture = load_fixture(path)
|
||||
parser_type = resolve_type(parser, fixture.type_name)
|
||||
fixture_label = path.relative_to(TESTDATA_DIR)
|
||||
try:
|
||||
parsed = parse_type(
|
||||
parser_type,
|
||||
fixture.data[fixture.offset:],
|
||||
*fixture.params,
|
||||
)
|
||||
except Exception as error:
|
||||
raise ValueError(
|
||||
f"{fixture_label}: Kaitai parse failed"
|
||||
) from error
|
||||
|
||||
if parser_type is parser:
|
||||
if fixture.offset != 0:
|
||||
raise ValueError(
|
||||
f"{fixture_label}: root parser must begin at offset zero"
|
||||
)
|
||||
validate_complete_snapshot(parsed, fixture.data)
|
||||
elif hasattr(parsed, "_raw_payload"):
|
||||
record_end = validate_record(
|
||||
parsed,
|
||||
fixture.data,
|
||||
fixture.offset,
|
||||
)
|
||||
if record_end != len(fixture.data):
|
||||
raise ValueError(
|
||||
f"{fixture_label}: record has trailing data"
|
||||
)
|
||||
if hasattr(parsed.payload, "prefix_digest"):
|
||||
expected = blake3(
|
||||
fixture.data[:fixture.offset]
|
||||
).digest()
|
||||
if parsed.payload.prefix_digest != expected:
|
||||
raise ValueError(
|
||||
f"{fixture_label}: checkpoint digest does not match"
|
||||
)
|
||||
|
||||
print(f"ok {fixture_label}")
|
||||
|
||||
print(f"validated {len(fixture_paths)} snapshot fixtures")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user