terminal/snapshot: more efficient binary form, optimize wire size + encode/decode speeds (#13566)

Reworks the terminal PAGE grid wire format and optimize both
encode/decode. Example improvements for 1MB of VT input w/ full
scrollback: ~30x smaller wire size, ~45x faster encoding and decoding.

> [!IMPORTANT]
>
> **Snapshot version 1 is still explicitly a work-in-progress format, so
this breaks wire compatibility**.

The original snapshot version I merged favored simplicity over
optimization. This was the format used a proof-of-concept in my own
projects, but I knew it wasn't what I wanted to ship. This PR looks at
the record formats and trades simplicity for performance, a fair trade
for a performance-sensitive binary format.

Overview of changes:

- **8-byte grid cells.** Cells are now one 64-bit word whose layout
deliberately coincides with the native cell. Previously, cells were 16
bytes each and in our 1MB corpus 97% of the data was `0`. Lol.
- **Blank trailing cells are not written.** Rows declare an encoded cell
count so trailing blank cells cost nothing.
- **Hardware CRC32C.** Added `src/crc32c.zig` that uses inline-asm on
aarch64/x86_64 to get hardware speeds for CRC32. Zig's stdlib is 0.56
GB/s, aarch64 hardware is 10 GB/s on my computer.
- **Variable-width cells.** Each row declares how many bytes transport
each cell word: 1, 2, 4, or 8 depending on the widest row cell.

## Format

Grid layout, per PAGE record:

```
   old                                new
   +--------------------------+      +--------------------------+
   | row 0                    |      | row 0                    |
   |   flags (1)              |      |   flags + width (1)      |
   |   cols * 16B cells with  |      |   encoded cell count (2) |
   |   inline suffixes        |      |   count * width cells    |
   +--------------------------+      +--------------------------+
   | ...                      |      | ...                      |
   +--------------------------+      +--------------------------+
   | row (rows - 1)           |      | row (rows - 1)           |
   +--------------------------+      +--------------------------+
                                     | grapheme suffix section  |
                                     +--------------------------+
```

Every row previously carried exactly `cols` cells; now it carries cells
only through its last non-default cell, and the cells past the count are
implicitly zero. The row flag byte gains the encoded cell width in its
previously reserved bits:

```
   bit 0 wrap                 bit 2-3 semantic prompt
   bit 1 wrap continuation    bit 4-5 encoded cell width (log2 bytes)
```

The cell itself, old fixed 16-byte header versus the new single word:

```
   old (16 bytes + inline suffixes)     new (one u64 word)
   +--------+---------+--------+        bit  0 +------------------+
   | kind 1 | width 1 | flags 1|               | content kind  2b |
   +--------+---------+--------+        bit  2 +------------------+
   | zero 1 | style id 2       |               | content      24b |
   +--------+------------------+        bit 26 +------------------+
   | hyperlink id 2            |               | style ID     16b |
   +---------------------------+        bit 42 +------------------+
   | value 4                   |               | width kind    2b |
   +---------------------------+        bit 44 +------------------+
   | grapheme count 4          |               | protected     1b |
   +---------------------------+        bit 45 +------------------+
   | grapheme cps 4 * count    |               | hyperlink     1b |
   +---------------------------+        bit 46 +------------------+
                                               | semantic      2b |
                                        bit 48 +------------------+
                                               | hyperlink ID 16b |
                                        bit 64 +------------------+
```

The word's bit layout intentionally matches the native cell (with the
wire hyperlink ID in the native padding), so full-width rows are a
straight copy of page memory. The row's encoded width then transports
each word truncated, and decode is the matching zero-extension:

```
   width | bytes | admitted cells
   ------+-------+------------------------------------------------
     0   |   1   | codepoint <= U+00FF, nothing else set
     1   |   2   | codepoint <= U+FFFF, nothing else set
     2   |   4   | any content kind/codepoint, style IDs 1-63,
         |       | narrow, no flags, no hyperlink
     3   |   8   | everything
```

Grapheme suffixes were inline after each cell, which forced per-cell
framing decisions; they are now one section after the rows, so a
grapheme-free page (the overwhelming case) pays 4 bytes total:

```
   old: ... | cell | cp cp | cell | ...      (inline, per cell)

   new: +----------------+----------------------------------+
        | entry count 4  | entries: row 2, col 2, count 2,  |
        |                |          count * codepoint 4     |
        +----------------+----------------------------------+
```

## Performance

Setup: `ghostty-bench +terminal-snapshot`, 80x24 terminal with unlimited
scrollback fed 1 MB of VT input.

Per-commit improvements:

| change                        | wire size | encode  | decode   |
|-------------------------------|-----------|---------|----------|
| baseline (v1 before this PR)  | 34.16 MB  | 92.8 ms | 119.8 ms |
| 8-byte cells + blank elision  | 7.66 MB   | 18.2 ms | 28.0 ms  |
| hardware CRC32C               | 7.66 MB   | 5.8 ms  | 15.5 ms  |
| gate page verification        | 7.66 MB   | 5.8 ms  | 12.2 ms  |
| staged PAGE payload decoding  | 7.66 MB   | 5.8 ms  | 8.1 ms   |
| variable-width cells          | 1.03 MB   | 2.0 ms  | 2.7 ms   |

Final result across various inputs:

| corpus | wire size | encode | decode |

|----------------------------|------------------------|----------------------|-----------------------|
| ascii lines 1-70 | 34.16 -> 1.03 MB (33x) | 92.8 -> 2.0 ms (46x) |
119.8 -> 2.7 ms (44x) |
| ascii full-width wrap | 16.01 -> 1.04 MB (15x) | 43.6 -> 1.3 ms (34x)
| 56.2 -> 1.8 ms (31x) |
| utf8 (wide/grapheme heavy) | 4.33 -> 1.89 MB (2.3x) | 12.4 -> 1.7 ms
(7x) | 19.0 -> 2.2 ms (9x) |

### Relationship with Compression

I expect that users of this will wrap everything in compression, so I
also benchmarked all my changes against a caller-owned zstd compressor
to ensure we're making the write tradeoffs. Less bytes means less time
in a compressor, even if a ton of 0s compresses really well.

My results: `zstd -1` over the `lines` snapshot drops from 12.7 ms to
0.8 ms, and the compressed artifact shrinks from 1.35 MB to 0.86 MB. So
the end state is a win-win.
This commit is contained in:
Mitchell Hashimoto
2026-08-02 15:01:00 -07:00
committed by GitHub
14 changed files with 2513 additions and 683 deletions

View File

@@ -0,0 +1,307 @@
//! Benchmarks the terminal binary snapshot codecs (`terminal/snapshot`).
//!
//! Encoding and decoding a snapshot are the hot paths for terminal
//! persistence and handoff, so both directions are measured against the
//! same terminal state.
//!
//! ## Input
//!
//! `--data` names a pre-generated VT byte stream (for example from
//! `ghostty-gen ascii`). The stream is fed to a terminal of the requested
//! dimensions during setup, outside the timed region. The resulting screen
//! and scrollback contents are what each step encodes or decodes.
//!
//! ## Modes
//!
//! * `noop` performs no codec work and establishes loop overhead.
//! * `encode` encodes one complete snapshot per loop into a reusable
//! buffer. Buffer growth happens on the first iteration only.
//! * `decode` restores one complete snapshot per loop from bytes prepared
//! during setup. The restored terminal is destroyed inside the step, so
//! this measures the complete restore lifecycle.
//! * `report` encodes once and prints total and per-record-tag sizes. It
//! is for inspecting the wire shape, not timing comparisons.
//!
//! ## Examples
//!
//! Build benchmarks in ReleaseFast mode:
//!
//! zig build -Demit-bench -Doptimize=ReleaseFast -Demit-macos-app=false
//!
//! Generate a deterministic corpus, then measure both directions:
//!
//! ghostty-gen ascii | head -c 1000000 > /tmp/ascii.vt
//! hyperfine --warmup 3 \
//! 'ghostty-bench +terminal-snapshot --mode=encode --loops=20 --data=/tmp/ascii.vt' \
//! 'ghostty-bench +terminal-snapshot --mode=decode --loops=20 --data=/tmp/ascii.vt'
const TerminalSnapshot = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const snapshot = terminalpkg.snapshot;
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-snapshot-bench");
alloc: Allocator,
opts: Options,
terminal: ?Terminal = null,
/// Reused across encode steps so buffer growth is a one-time setup cost.
encoded: std.Io.Writer.Allocating,
pub const Options = struct {
/// Set by the shared CLI parser for string option ownership.
_arena: ?std.heap.ArenaAllocator = null,
/// Select the operation performed inside the timed benchmark step.
mode: Mode = .encode,
/// Number of codec operations per benchmark step. Increase this when
/// the state is too small for stable `hyperfine` measurements.
loops: u32 = 1,
/// The size of the terminal. This affects wrapping, page sizes, and
/// the amount of state per encoded page.
@"terminal-rows": u16 = 24,
@"terminal-cols": u16 = 80,
/// Pre-generated VT stream fed to the terminal during setup. `-` reads
/// stdin, although a regular file is recommended so identical state can
/// be reused across runs. When unset, the terminal is empty.
data: ?[]const u8 = null,
pub fn deinit(self: *Options) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
};
pub const Mode = enum {
/// Establish the benchmark loop overhead.
noop,
/// Encode one complete snapshot per loop.
encode,
/// Restore one complete snapshot per loop, including its teardown.
decode,
/// Print encoded sizes by record tag. Not a timing benchmark.
report,
};
pub fn create(
alloc: Allocator,
opts: Options,
) !*TerminalSnapshot {
const ptr = try alloc.create(TerminalSnapshot);
errdefer alloc.destroy(ptr);
ptr.* = .{
.alloc = alloc,
.opts = opts,
.encoded = .init(alloc),
};
return ptr;
}
pub fn destroy(self: *TerminalSnapshot, alloc: Allocator) void {
if (self.terminal) |*t| t.deinit(self.alloc);
self.encoded.deinit();
alloc.destroy(self);
}
pub fn benchmark(self: *TerminalSnapshot) Benchmark {
return .init(self, .{
.stepFn = switch (self.opts.mode) {
.noop => stepNoop,
.encode => stepEncode,
.decode => stepDecode,
.report => stepReport,
},
.setupFn = setup,
.teardownFn = teardown,
});
}
/// Build the terminal state every mode shares and, for decode mode, the
/// encoded snapshot it restores. All of this is outside the timed region.
fn setup(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
self.setupImpl() catch |err| {
log.warn("failed to prepare snapshot benchmark err={}", .{err});
return error.BenchmarkFailed;
};
}
fn setupImpl(self: *TerminalSnapshot) !void {
if (self.terminal) |*t| t.deinit(self.alloc);
self.terminal = null;
self.terminal = try Terminal.init(global.io(), self.alloc, .{
.cols = self.opts.@"terminal-cols",
.rows = self.opts.@"terminal-rows",
.max_scrollback_bytes = null,
.max_scrollback_lines = null,
});
const terminal = &self.terminal.?;
// Feed the input corpus through the standard VT stream.
if (try options.dataFile(self.opts.data)) |data_f| {
defer data_f.close(global.io());
var stream = terminal.vtStream();
defer stream.deinit();
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = data_f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
var buf: [4096]u8 = undefined;
while (true) {
const n = try r.readSliceShort(&buf);
if (n == 0) break; // EOF reached
stream.nextSlice(buf[0..n]);
}
}
// Decode restores the same bytes every step. Encode mode also uses
// this buffer, in which case this is its one-time growth.
self.encoded.shrinkRetainingCapacity(0);
try snapshot.encode(
self.alloc,
&self.encoded.writer,
terminal,
.{ .continuation = .ground },
);
}
fn teardown(ptr: *anyopaque) void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
if (self.terminal) |*t| t.deinit(self.alloc);
self.terminal = null;
self.encoded.shrinkRetainingCapacity(0);
}
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
for (0..self.opts.loops) |_| {
std.mem.doNotOptimizeAway(self.encoded.written());
}
}
fn stepEncode(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
const terminal = &self.terminal.?;
for (0..self.opts.loops) |_| {
self.encoded.shrinkRetainingCapacity(0);
snapshot.encode(
self.alloc,
&self.encoded.writer,
terminal,
.{ .continuation = .ground },
) catch |err| {
log.warn("snapshot encoding failed err={}", .{err});
return error.BenchmarkFailed;
};
std.mem.doNotOptimizeAway(self.encoded.written());
}
}
fn stepDecode(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
const bytes = self.encoded.written();
for (0..self.opts.loops) |_| {
var reader: std.Io.Reader = .fixed(bytes);
var decoded = snapshot.decode(
self.alloc,
global.io(),
&reader,
.{ .max_continuation_bytes = 1024 * 1024 },
) catch |err| {
log.warn("snapshot decoding failed err={}", .{err});
return error.BenchmarkFailed;
};
std.mem.doNotOptimizeAway(&decoded);
decoded.deinit(self.alloc);
}
}
/// Print the encoded size grouped by record tag. This shares the encoder
/// with encode mode but deliberately makes no timing claims.
fn stepReport(ptr: *anyopaque) Benchmark.Error!void {
const self: *TerminalSnapshot = @ptrCast(@alignCast(ptr));
const bytes = self.encoded.written();
var payload_totals = std.enums.EnumArray(
snapshot.record.Tag,
u64,
).initFill(0);
var record_counts = std.enums.EnumArray(
snapshot.record.Tag,
u64,
).initFill(0);
var record_total: u64 = 0;
var offset: usize = snapshot.envelope.encoded_len;
while (offset + snapshot.record.Header.len <= bytes.len) {
const tag_raw = std.mem.readInt(u16, bytes[offset..][0..2], .little);
const payload_len = std.mem.readInt(
u32,
bytes[offset + 2 ..][0..4],
.little,
);
const tag = std.enums.fromInt(snapshot.record.Tag, tag_raw) orelse {
log.warn("unknown record tag {}", .{tag_raw});
return error.BenchmarkFailed;
};
payload_totals.getPtr(tag).* += payload_len;
record_counts.getPtr(tag).* += 1;
record_total += 1;
offset += snapshot.record.Header.len + payload_len;
}
var it = payload_totals.iterator();
while (it.next()) |entry| {
std.debug.print("terminal-snapshot tag={s} records={d} payload={d}\n", .{
@tagName(entry.key),
record_counts.get(entry.key),
entry.value.*,
});
}
std.debug.print(
"terminal-snapshot total records={d} framing={d} encoded={d}\n",
.{
record_total,
record_total * snapshot.record.Header.len +
snapshot.envelope.encoded_len,
bytes.len,
},
);
}
test TerminalSnapshot {
const testing = std.testing;
const impl: *TerminalSnapshot = try .create(testing.allocator, .{});
defer impl.destroy(testing.allocator);
const bench = impl.benchmark();
_ = try bench.run(.once);
}
test "TerminalSnapshot decode round trip" {
const testing = std.testing;
const impl: *TerminalSnapshot = try .create(testing.allocator, .{
.mode = .decode,
.@"terminal-rows" = 4,
.@"terminal-cols" = 8,
});
defer impl.destroy(testing.allocator);
const bench = impl.benchmark();
_ = try bench.run(.once);
}

View File

@@ -16,6 +16,7 @@ pub const Action = enum {
@"screen-clone",
@"terminal-parser",
@"terminal-resize",
@"terminal-snapshot",
@"terminal-stream",
@"is-symbol",
@"osc-parser",
@@ -40,6 +41,7 @@ pub const Action = enum {
.@"grapheme-break" => @import("GraphemeBreak.zig"),
.@"terminal-parser" => @import("TerminalParser.zig"),
.@"terminal-resize" => @import("TerminalResize.zig"),
.@"terminal-snapshot" => @import("TerminalSnapshot.zig"),
.@"is-symbol" => @import("IsSymbol.zig"),
.@"osc-parser" => @import("OscParser.zig"),
};

View File

@@ -8,6 +8,7 @@ pub const HyperlinkMap = @import("HyperlinkMap.zig");
pub const ScreenClone = @import("ScreenClone.zig");
pub const TerminalParser = @import("TerminalParser.zig");
pub const TerminalResize = @import("TerminalResize.zig");
pub const TerminalSnapshot = @import("TerminalSnapshot.zig");
pub const IsSymbol = @import("IsSymbol.zig");
pub const PageCompression = @import("PageCompression.zig");
pub const ScrollbackCompression = @import("ScrollbackCompression.zig");

193
src/crc32c.zig Normal file
View File

@@ -0,0 +1,193 @@
//! CRC32C with hardware acceleration.
//!
//! The Zig standard library implementation processes one byte per table
//! lookup (as of Zig 0.16), which is more than an order of magnitude slower
//! than the dedicated CRC32C instructions available on aarch64 (CRC
//! extension) and x86_64 (SSE4.2). This module selects the best backend at
//! compile time and falls back to the standard library elsewhere, including
//! WebAssembly.
//!
//! The resulting value is identical across all backends: this is the
//! iSCSI CRC32C parameter set (reflected, initial and final XOR
//! `0xFFFFFFFF`), matching `std.hash.crc.Crc32Iscsi`.
const std = @import("std");
const builtin = @import("builtin");
/// The standard-library implementation of the same parameter set. This is
/// both the portable fallback and the reference the tests compare against.
const Software = std.hash.crc.Crc32Iscsi;
const Backend = enum {
aarch64_crc,
x86_64_sse42,
software,
};
const backend: Backend = backend: {
switch (builtin.cpu.arch) {
.aarch64,
.aarch64_be,
=> if (std.Target.aarch64.featureSetHas(
builtin.cpu.features,
.crc,
)) break :backend .aarch64_crc,
// The self-hosted x86_64 backend cannot encode the CRC32
// instruction forms used below, so that combination falls back to
// the portable implementation.
.x86_64 => if (builtin.zig_backend == .stage2_llvm and
std.Target.x86.featureSetHas(
builtin.cpu.features,
.sse4_2,
)) break :backend .x86_64_sse42,
else => {},
}
break :backend .software;
};
/// Streaming CRC32C with the same interface shape as `std.hash.crc` types.
pub const Crc32c = struct {
crc: u32,
pub fn init() Crc32c {
return .{ .crc = 0xFFFF_FFFF };
}
pub fn update(self: *Crc32c, bytes: []const u8) void {
self.crc = switch (comptime backend) {
.aarch64_crc, .x86_64_sse42 => updateHardware(self.crc, bytes),
.software => software: {
var crc: Software = .{ .crc = self.crc };
crc.update(bytes);
break :software crc.crc;
},
};
}
pub fn final(self: Crc32c) u32 {
return self.crc ^ 0xFFFF_FFFF;
}
pub fn hash(bytes: []const u8) u32 {
var c: Crc32c = .init();
c.update(bytes);
return c.final();
}
};
/// One update pass using the dedicated CRC32C instructions. Both supported
/// architectures handle unaligned loads efficiently, so the loop reads
/// little-endian words directly from the input.
fn updateHardware(initial: u32, bytes: []const u8) u32 {
var crc = initial;
var remaining = bytes;
while (remaining.len >= 8) : (remaining = remaining[8..]) {
crc = step(u64, crc, std.mem.readInt(
u64,
remaining[0..8],
.little,
));
}
if (remaining.len >= 4) {
crc = step(u32, crc, std.mem.readInt(
u32,
remaining[0..4],
.little,
));
remaining = remaining[4..];
}
for (remaining) |byte| crc = step(u8, crc, byte);
return crc;
}
/// One CRC32C instruction folding `value` into the running CRC.
inline fn step(comptime T: type, crc: u32, value: T) u32 {
return switch (comptime backend) {
.aarch64_crc => switch (T) {
u8 => asm ("crc32cb %[out:w], %[crc:w], %[value:w]"
: [out] "=r" (-> u32),
: [crc] "r" (crc),
[value] "r" (value),
),
u32 => asm ("crc32cw %[out:w], %[crc:w], %[value:w]"
: [out] "=r" (-> u32),
: [crc] "r" (crc),
[value] "r" (value),
),
u64 => asm ("crc32cx %[out:w], %[crc:w], %[value:x]"
: [out] "=r" (-> u32),
: [crc] "r" (crc),
[value] "r" (value),
),
else => comptime unreachable,
},
.x86_64_sse42 => switch (T) {
u8 => asm ("crc32b %[value], %[out]"
: [out] "=r" (-> u32),
: [value] "r" (value),
[crc_in] "0" (crc),
),
u32 => asm ("crc32l %[value], %[out]"
: [out] "=r" (-> u32),
: [value] "r" (value),
[crc_in] "0" (crc),
),
u64 => @truncate(asm ("crc32q %[value], %[out]"
: [out] "=r" (-> u64),
: [value] "r" (value),
[crc_in] "0" (@as(u64, crc)),
)),
else => comptime unreachable,
},
.software => comptime unreachable,
};
}
test "matches the check value" {
// The catalog check value for CRC-32/ISCSI.
try std.testing.expectEqual(
@as(u32, 0xE3069283),
Crc32c.hash("123456789"),
);
}
test "matches the standard library at every length and split" {
var bytes: [259]u8 = undefined;
var prng = std.Random.DefaultPrng.init(0xC5C32C);
prng.random().bytes(&bytes);
for (0..bytes.len + 1) |len| {
const input = bytes[0..len];
try std.testing.expectEqual(
Software.hash(input),
Crc32c.hash(input),
);
// Streaming across arbitrary split points must not change the
// result: word batching may not leak state between updates.
var split: Crc32c = .init();
split.update(input[0 .. len / 3]);
split.update(input[len / 3 .. len - len / 3]);
split.update(input[len - len / 3 ..]);
try std.testing.expectEqual(Software.hash(input), split.final());
}
}
test "matches the standard library at every alignment" {
var bytes: [64 + 16]u8 = undefined;
var prng = std.Random.DefaultPrng.init(0xA11C);
prng.random().bytes(&bytes);
for (0..16) |offset| {
const input = bytes[offset..][0..64];
try std.testing.expectEqual(
Software.hash(input),
Crc32c.hash(input),
);
}
}

View File

@@ -7,7 +7,14 @@ const Bytes = @import("../Bytes.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
pub const Options = struct {};
pub const Options = struct {
/// When nonzero, emit lines whose printable length is uniformly
/// distributed in `[line-min, line-max]`, each terminated by CR LF.
/// When zero (the default), emit an unbroken stream of printable
/// bytes that relies on terminal wrapping.
@"line-min": usize = 0,
@"line-max": usize = 0,
};
fn checkAsciiAlphabet(c: u8) bool {
return switch (c) {
@@ -18,13 +25,16 @@ fn checkAsciiAlphabet(c: u8) bool {
pub const ascii = Bytes.generateAlphabet(checkAsciiAlphabet);
opts: Options,
/// Create a new terminal stream handler for the given arguments.
pub fn create(
alloc: Allocator,
_: Options,
opts: Options,
) !*Ascii {
const ptr = try alloc.create(Ascii);
errdefer alloc.destroy(ptr);
ptr.* = .{ .opts = opts };
return ptr;
}
@@ -32,16 +42,18 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void {
alloc.destroy(self);
}
pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
const line_max = @max(self.opts.@"line-min", self.opts.@"line-max");
const lines = line_max > 0;
var gen: Bytes = .{
.rand = rand,
.alphabet = ascii,
.min_len = 1024,
.max_len = 1024,
.min_len = if (lines) @max(self.opts.@"line-min", 1) else 1024,
.max_len = if (lines) line_max else 1024,
};
while (true) {
_ = gen.write(writer) catch |err| {
writeChunk(&gen, writer, lines) catch |err| {
const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err);
switch (@as(Error, err)) {
error.BrokenPipe => return, // stdout closed
@@ -51,6 +63,15 @@ pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
}
}
fn writeChunk(
gen: *const Bytes,
writer: *std.Io.Writer,
lines: bool,
) std.Io.Writer.Error!void {
_ = try gen.write(writer);
if (lines) try writer.writeAll("\r\n");
}
test Ascii {
const testing = std.testing;
const alloc = testing.allocator;
@@ -65,3 +86,30 @@ test Ascii {
var writer: std.Io.Writer = .fixed(&buf);
try impl.run(&writer, rand);
}
test "Ascii lines" {
const testing = std.testing;
const alloc = testing.allocator;
const impl: *Ascii = try .create(alloc, .{
.@"line-min" = 5,
.@"line-max" = 20,
});
defer impl.destroy(alloc);
var prng = std.Random.DefaultPrng.init(1);
const rand = prng.random();
var buf: [1024]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try impl.run(&writer, rand);
// Every emitted line respects the configured bounds.
var it = std.mem.splitSequence(u8, writer.buffered(), "\r\n");
while (it.next()) |line| {
// The fixed buffer may end mid-line.
if (it.rest().len == 0) break;
try testing.expect(line.len >= 5);
try testing.expect(line.len <= 20);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -63,19 +63,19 @@
//! | hyperlink_count entries |
//! +---------------------------+
//! | Grid |
//! | one record per row |
//! | rows and grapheme section |
//! +---------------------------+
//!
//! Style entry = encoded ID + style record
//! Hyperlink entry = encoded ID + hyperlink record
//! Grid = row 0 ... row (rows - 1)
//! Row = row header + cell 0 ... cell (columns - 1)
//! Cell = cell header + grapheme suffix codepoints
//! Grid = row 0 ... row (rows - 1) + grapheme suffix section
//! Row = row header + encoded cells
//! ```
//!
//! Following the header, the payload contains exactly `style_count` style
//! records and `hyperlink_count` hyperlink records, followed by exactly
//! `rows` row records. There is no padding between records.
//! `rows` row records and one grapheme suffix section. There is no padding
//! between records.
//!
//! Each style begins with an ID (`u16`) followed by the typical style
//! binary representation (in style.zig). The ID is what cells will use
@@ -93,6 +93,7 @@
//! Rows and cells use the grid encoding documented in `grid.zig`.
const std = @import("std");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const test_fixture = @import("fixture.zig");
const grid = @import("grid.zig");
@@ -119,6 +120,7 @@ const PayloadEncodeError = hyperlink.EncodeError || grid.EncodeError;
const PayloadDecodeError = std.Io.Reader.Error ||
Header.CapacityError ||
grid.DecodeError ||
error{
/// The hyperlink kind is not defined by snapshot version 1.
InvalidKind,
@@ -202,10 +204,17 @@ pub const Decoder = struct {
return self.header.pageCapacity() catch unreachable;
}
/// Largest remaining PAGE payload staged into one contiguous buffer.
/// This comfortably covers every payload a standard-capacity page can
/// produce while keeping the allocation an untrusted length can force
/// far below the record framing's four-byte length limit.
const max_staged_payload = 8 * 1024 * 1024;
/// Decode the remaining payload into caller-owned native page storage.
///
/// The destination must be freshly initialized with `capacity`. `alloc`
/// is used only for temporary ID remaps and integrity-check storage.
/// is used only for temporary staging, ID remaps, and integrity-check
/// storage.
pub fn decode(
self: *Decoder,
destination: *TerminalPage,
@@ -219,14 +228,52 @@ pub const Decoder = struct {
.cols = self.header.columns,
.rows = self.header.rows,
};
try decodePayloadBody(
self.record_reader.payloadReader(),
alloc,
destination,
self.header,
);
// `init` consumed exactly the fixed header from the declared
// payload, so this is the byte count of the tables and grid.
const remaining = self.record_reader.header.payload_len - Header.len;
if (remaining <= max_staged_payload) {
// Stage the payload with one bulk read. The whole payload
// passes through both hashes as one update and the payload
// decoders then parse a flat buffer, which keeps per-row work
// free of stream adapters. The CRC and exact-length checks in
// `finish` are unaffected.
const staged = try alloc.alloc(u8, remaining);
defer alloc.free(staged);
try self.record_reader.payloadReader().readSliceAll(staged);
var staged_reader: std.Io.Reader = .fixed(staged);
try decodePayloadBody(
&staged_reader,
alloc,
destination,
self.header,
);
if (staged_reader.bufferedLen() != 0) {
return error.PayloadNotExhausted;
}
} else {
// A payload this large is either hostile or a page far beyond
// native capacities. Decode it through the streaming payload
// reader so its declared length cannot force an allocation.
try decodePayloadBody(
self.record_reader.payloadReader(),
alloc,
destination,
self.header,
);
}
try self.record_reader.finish();
try destination.verifyIntegrity(alloc);
// The decoder normalizes every semantic value, so a complete decode
// upholds native page invariants by construction. Verifying them
// again is a defense against decoder bugs and follows the native
// page policy: full integrity verification only when slow runtime
// safety is enabled.
if (comptime build_options.slow_runtime_safety) {
try destination.verifyIntegrity(alloc);
}
}
};
@@ -284,15 +331,13 @@ fn decodePayloadBody(
page.pauseIntegrityChecks(true);
defer page.pauseIntegrityChecks(false);
var style_remap = grid.StyleRemap.init(alloc);
defer style_remap.deinit();
style_remap.ensureTotalCapacity(header.style_count) catch
var style_remap = grid.StyleRemap.init(alloc) catch
return error.OutOfMemory;
defer style_remap.deinit(alloc);
var hyperlink_remap = grid.HyperlinkRemap.init(alloc);
defer hyperlink_remap.deinit();
hyperlink_remap.ensureTotalCapacity(header.hyperlink_count) catch
var hyperlink_remap = grid.HyperlinkRemap.init(alloc) catch
return error.OutOfMemory;
defer hyperlink_remap.deinit(alloc);
// Styles
for (0..header.style_count) |_| {
@@ -316,10 +361,7 @@ fn decodePayloadBody(
valid,
) catch 0;
} else 0;
style_remap.putAssumeCapacityNoClobber(
native_id,
decoded_id,
);
style_remap.put(native_id, decoded_id);
}
// Hyperlinks
@@ -338,10 +380,7 @@ fn decodePayloadBody(
// Zero records an ignored table entry. Grid decoding treats every
// reference to it as no hyperlink.
hyperlink_remap.putAssumeCapacityNoClobber(
native_id,
decoded_id,
);
hyperlink_remap.put(native_id, decoded_id);
}
// Rows and cells
@@ -860,8 +899,7 @@ test "decode accepts unordered sparse style IDs and ignores zero" {
var descending: [
Header.len +
2 * (2 + style.len) +
1 +
16
7
]u8 = undefined;
var descending_writer: std.Io.Writer = .fixed(&descending);
try header.encode(&descending_writer);
@@ -869,8 +907,9 @@ test "decode accepts unordered sparse style IDs and ignores zero" {
try style.encode(.{ .flags = .{ .bold = true } }, &descending_writer);
try io.writeInt(&descending_writer, TerminalStyleId, 2);
try style.encode(.{ .flags = .{ .italic = true } }, &descending_writer);
try descending_writer.writeByte(0);
try descending_writer.splatByteAll(0, 16);
try descending_writer.writeByte(0); // row flags
try io.writeInt(&descending_writer, u16, 0); // cell count
try io.writeInt(&descending_writer, u32, 0); // grapheme section
var descending_reader: std.Io.Reader = .fixed(
descending_writer.buffered(),
@@ -893,7 +932,7 @@ test "decode accepts unordered sparse style IDs and ignores zero" {
.string_capacity_bytes = 0,
};
var zero: [Header.len + 2 + style.len + 17]u8 = undefined;
var zero: [Header.len + 2 + style.len + 7]u8 = undefined;
var zero_writer: std.Io.Writer = .fixed(&zero);
try one_header.encode(&zero_writer);
try io.writeInt(&zero_writer, TerminalStyleId, 0);
@@ -936,8 +975,7 @@ test "decode accepts unordered sparse hyperlink IDs" {
var encoded: [
Header.len +
2 * 14 +
1 +
16
7
]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
@@ -945,8 +983,9 @@ test "decode accepts unordered sparse hyperlink IDs" {
try hyperlink.encode(first, &writer);
try io.writeInt(&writer, TerminalHyperlinkId, 2);
try hyperlink.encode(second, &writer);
try writer.writeByte(0);
try writer.splatByteAll(0, 16);
try writer.writeByte(0); // row flags
try io.writeInt(&writer, u16, 0); // cell count
try io.writeInt(&writer, u32, 0); // grapheme section
var reader: std.Io.Reader = .fixed(writer.buffered());
var decoded = try decodePayload(
@@ -973,15 +1012,15 @@ test "decode defaults missing sparse cell references" {
.string_capacity_bytes = 0,
};
var style_encoded: [Header.len + 1 + 16]u8 = undefined;
var style_encoded: [Header.len + 3 + 8 + 4]u8 = undefined;
var style_writer: std.Io.Writer = .fixed(&style_encoded);
try style_header.encode(&style_writer);
try style_writer.writeByte(0);
try style_writer.writeAll(&.{ 0, 0, 0, 0 });
try io.writeInt(&style_writer, TerminalStyleId, 1);
try io.writeInt(&style_writer, TerminalHyperlinkId, 0);
try io.writeInt(&style_writer, u32, 0);
try io.writeInt(&style_writer, u32, 0);
try style_writer.writeByte(0x30); // row flags, full cell width
try io.writeInt(&style_writer, u16, 1); // cell count
try io.writeInt(&style_writer, u64, @bitCast(grid.Cell{
.style_id = 1,
}));
try io.writeInt(&style_writer, u32, 0); // grapheme section
var style_reader: std.Io.Reader = .fixed(style_writer.buffered());
var style_page = try decodePayload(
@@ -1006,15 +1045,16 @@ test "decode defaults missing sparse cell references" {
.string_capacity_bytes = 0,
};
var hyperlink_encoded: [Header.len + 1 + 16]u8 = undefined;
var hyperlink_encoded: [Header.len + 3 + 8 + 4]u8 = undefined;
var hyperlink_writer: std.Io.Writer = .fixed(&hyperlink_encoded);
try hyperlink_header.encode(&hyperlink_writer);
try hyperlink_writer.writeByte(0);
try hyperlink_writer.writeAll(&.{ 0, 0, 0, 0 });
try io.writeInt(&hyperlink_writer, TerminalStyleId, 0);
try io.writeInt(&hyperlink_writer, TerminalHyperlinkId, 1);
try io.writeInt(&hyperlink_writer, u32, 0);
try io.writeInt(&hyperlink_writer, u32, 0);
try hyperlink_writer.writeByte(0x30); // row flags, full cell width
try io.writeInt(&hyperlink_writer, u16, 1); // cell count
try io.writeInt(&hyperlink_writer, u64, @bitCast(grid.Cell{
.hyperlink = true,
.hyperlink_id = 1,
}));
try io.writeInt(&hyperlink_writer, u32, 0); // grapheme section
var hyperlink_reader: std.Io.Reader = .fixed(
hyperlink_writer.buffered(),
@@ -1044,41 +1084,42 @@ test "decode normalizes invalid grid semantics" {
.string_capacity_bytes = 0,
};
var encoded: [Header.len + 1 + 3 * 16 + 12]u8 = undefined;
var encoded: [Header.len + 3 + 3 * 8 + 4 + 10]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
// Preserve wrap while degrading the unknown semantic-prompt value to none
// and ignoring all reserved row bits.
try writer.writeByte(0xFD);
try io.writeInt(&writer, u16, 3);
// An unknown content and width kind, invalid semantic content, reserved
// bytes, and suffix data all degrade to a blank narrow output cell.
try writer.writeAll(&.{ 3, 4, 0xFF, 0xFF });
try io.writeInt(&writer, TerminalStyleId, 0);
try io.writeInt(&writer, TerminalHyperlinkId, 0);
try io.writeInt(&writer, u32, 0xD800);
try io.writeInt(&writer, u32, 1);
try io.writeInt(&writer, u32, 0x110000);
// A text cell replaces an invalid Unicode scalar with U+FFFD while its
// reserved semantic content degrades to output.
try io.writeInt(&writer, u64, @bitCast(grid.Cell{
.content = 0xD800,
.semantic_content = 3,
}));
// A recognized text cell replaces an invalid Unicode scalar with U+FFFD.
// Its valid suffix cannot fit the advertised zero capacity, so decoding
// preserves the base character and drops the optional suffix.
try writer.writeAll(&.{ 0, 0, 0, 0 });
try io.writeInt(&writer, TerminalStyleId, 0);
try io.writeInt(&writer, TerminalHyperlinkId, 0);
try io.writeInt(&writer, u32, 0xD800);
try io.writeInt(&writer, u32, 1);
try io.writeInt(&writer, u32, 'x');
// A declared grapheme suffix cannot fit the advertised zero capacity,
// so decoding preserves the base character and drops the suffix.
try io.writeInt(&writer, u64, @bitCast(grid.Cell{
.kind = 1,
.content = 'x',
}));
// Reserved palette bits and a nonsensical suffix do not obscure the valid
// low palette byte; the suffix is consumed and ignored.
try writer.writeAll(&.{ 1, 0, 0, 0xFF });
try io.writeInt(&writer, TerminalStyleId, 0);
try io.writeInt(&writer, TerminalHyperlinkId, 0);
try io.writeInt(&writer, u32, 0xFFFFFF07);
// Reserved palette content bits do not obscure the valid low palette
// byte.
try io.writeInt(&writer, u64, @bitCast(grid.Cell{
.kind = 2,
.content = 0xFFFF07,
}));
// The grapheme section carries the suffix for the second cell.
try io.writeInt(&writer, u32, 1);
try io.writeInt(&writer, u32, 'x');
try io.writeInt(&writer, u16, 0);
try io.writeInt(&writer, u16, 1);
try io.writeInt(&writer, u16, 1);
try io.writeInt(&writer, u32, 0x0301);
var reader: std.Io.Reader = .fixed(writer.buffered());
var page = try decodePayload(&reader, std.testing.allocator);
@@ -1091,7 +1132,7 @@ test "decode normalizes invalid grid semantics" {
terminal_page.Row.SemanticPrompt.none,
first.row.semantic_prompt,
);
try std.testing.expectEqual(@as(u21, 0), first.cell.codepoint());
try std.testing.expectEqual(@as(u21, 0xFFFD), first.cell.codepoint());
try std.testing.expectEqual(
terminal_page.Cell.Wide.narrow,
first.cell.wide,
@@ -1104,7 +1145,7 @@ test "decode normalizes invalid grid semantics" {
try std.testing.expect(!first.cell.hasGrapheme());
const second = page.getRowAndCell(1, 0).cell;
try std.testing.expectEqual(@as(u21, 0xFFFD), second.codepoint());
try std.testing.expectEqual(@as(u21, 'x'), second.codepoint());
try std.testing.expect(!second.hasGrapheme());
const third = page.getRowAndCell(2, 0).cell;

View File

@@ -29,9 +29,10 @@ const Blake3 = std.crypto.hash.Blake3;
/// The running digest shared by snapshot stream codecs and checkpoints.
pub const PrefixDigest = [Blake3.digest_length]u8;
/// CRC32C as specified by the snapshot format. Zig names this standard
/// parameter set after its iSCSI use.
pub const Crc32c = std.hash.crc.Crc32Iscsi;
/// CRC32C as specified by the snapshot format. This is the parameter set
/// Zig's standard library names after its iSCSI use, backed by dedicated
/// CRC32C instructions where the target has them.
pub const Crc32c = @import("../../crc32c.zig").Crc32c;
/// Identifies the layout and meaning of a record payload.
/// The current snapshot version rejects every value not listed here.
@@ -301,8 +302,9 @@ pub const Reader = struct {
// such as peek and discard.
limited_buffer: [1]u8,
// PAGE decoding performs many small reads. 256 bytes batches several cells
// while CRC32C is calculated without making Reader large on the stack.
// Fixed-header decoding performs several small reads. 256 bytes batches
// them while CRC32C is calculated without making Reader large on the
// stack. Bulk payload reads bypass this buffer entirely.
hashing_buffer: [256]u8,
limited: std.Io.Reader.Limited,

View File

@@ -205,6 +205,7 @@ const Allocator = std.mem.Allocator;
const hyperlink = @import("hyperlink.zig");
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const grid = @import("grid.zig");
const page = @import("page.zig");
const record = @import("record.zig");
const style = @import("style.zig");
@@ -2261,20 +2262,14 @@ test "SCREEN decode ignores a PAGE with an empty hyperlink URI" {
// One narrow codepoint cell refers to the hyperlink table entry above.
// Since that entry is ignored, the cell must restore without a hyperlink.
try page_payload.writeByte(0);
try page_payload.writeAll(&.{ 0, 0, 0, 0 });
try io.writeInt(
page_payload,
terminal_style.Id,
0,
);
try io.writeInt(
page_payload,
terminal_hyperlink.Id,
1,
);
try io.writeInt(page_payload, u32, 'A');
try io.writeInt(page_payload, u32, 0);
try page_payload.writeByte(0x30); // row flags, full cell width
try io.writeInt(page_payload, u16, 1); // cell count
try io.writeInt(page_payload, u64, @bitCast(grid.Cell{
.content = 'A',
.hyperlink = true,
.hyperlink_id = 1,
}));
try io.writeInt(page_payload, u32, 0); // grapheme section
try stream.finish();
var source: std.Io.Reader = .fixed(destination.written());

View File

@@ -914,25 +914,41 @@ types:
type: grid_row(columns)
repeat: expr
repeat-expr: num_rows
- id: num_grapheme_entries
type: u4
- id: grapheme_entries
type: grapheme_entry(num_rows, columns)
repeat: expr
repeat-expr: num_grapheme_entries
grid_row:
params:
- id: num_cells
- id: columns
type: u2
seq:
- id: flags
type: grid_row_flags
- id: cell_count
type: u2
valid:
expr: _ <= columns
- id: cells
type: grid_cell(_index, num_cells, flags.wrap)
type:
switch-on: flags.width_log2
cases:
0: grid_cell_1
1: grid_cell_2
2: grid_cell_4
3: grid_cell(_index, columns, flags.wrap)
repeat: expr
repeat-expr: num_cells
repeat-expr: cell_count
grid_row_flags:
seq:
- id: raw
type: u1
valid:
expr: (_ & 0xf0) == 0 and ((_ >> 2) & 0x3) <= 2
expr: (_ & 0xc0) == 0 and ((_ >> 2) & 0x3) <= 2
instances:
wrap:
value: (raw & 1) != 0
@@ -940,8 +956,59 @@ types:
value: (raw & 2) != 0
semantic_prompt:
value: (raw >> 2) & 0x3
width_log2:
value: (raw >> 4) & 0x3
grid_cell_1:
doc: One-byte encoded cell; the value is a codepoint at or below U+00FF.
seq:
- id: codepoint
type: u1
grid_cell_2:
doc: |
Two-byte encoded cell; the value is a codepoint at or below U+FFFF.
Canonical encoders never emit surrogates.
seq:
- id: codepoint
type: u2
valid:
expr: not (_ >= 0xd800 and _ <= 0xdfff)
grid_cell_4:
doc: |
Four-byte encoded cell holding the low half of the cell word: any
content kind and codepoint, style IDs one through sixty-three, and no
width, flag, or hyperlink bits.
seq:
- id: raw
type: u4
valid:
expr: |
(content_kind >= 2 or
(content <= 0x10ffff and
not (content >= 0xd800 and content <= 0xdfff))) and
(content_kind != 2 or content <= 0xff)
instances:
content_kind:
value: raw % 4
content:
value: (raw / 4) % 16777216
style_id:
value: raw / 67108864
grid_cell:
doc: |
One 64-bit little-endian cell word. The word is parsed as two 32-bit
halves so every derived field stays within JavaScript's safe integer
range, following the same approach as mode_set.
Canonical rules validated here: reserved semantic content is not
emitted, the hyperlink flag matches a nonzero hyperlink ID, codepoint
content is a Unicode scalar value, palette content uses only its low
eight bits, and spacer relationships match the preceding cell. Wide
markers cannot be validated forward because their spacer tail may be
the next cell or the implicit narrow cell after a short row.
params:
- id: index
type: u2
@@ -950,59 +1017,62 @@ types:
- 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
- id: lo
type: u4
- id: hi
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
semantic_content <= 2 and
hyperlink == (hyperlink_id != 0) and
(content_kind >= 2 or
(content <= 0x10ffff and
not (content >= 0xd800 and content <= 0xdfff))) and
(content_kind != 2 or content <= 0xff) and
(width != 2 or
(index > 0 and
_parent.cells[index - 1].as<grid_cell>.width == 1)) and
(width != 3 or (index + 1 == columns and row_wrap))
instances:
content_kind:
value: lo % 4
content:
value: (lo / 4) % 16777216
style_id:
value: (lo / 67108864) + (hi % 1024) * 64
width:
value: (hi / 1024) % 4
protected:
value: (hi / 4096) % 2 != 0
hyperlink:
value: (hi / 8192) % 2 != 0
semantic_content:
value: (hi / 16384) % 4
hyperlink_id:
value: hi / 65536
grapheme_entry:
seq:
- id: row
type: u2
valid:
expr: |
content_kind == 0 ?
(_ == 0 or value != 0) :
_ == 0
- id: graphemes
expr: _ < num_rows
- id: col
type: u2
valid:
expr: _ < columns
- id: num_codepoints
type: u2
valid:
min: 1
- id: codepoints
type: u4
repeat: expr
repeat-expr: num_graphemes
repeat-expr: num_codepoints
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
expr: _ <= 0x10ffff and not (_ >= 0xd800 and _ <= 0xdfff)
params:
- id: num_rows
type: u2
- id: columns
type: u2

View File

@@ -78,65 +78,47 @@ ee ee 80 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000037a
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 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 0x0000040c: page record, payload 36 bytes
03 00 24 00 00 00 a2 4f 26 d1 02 00 03 00 00 00 # 0x0000040c
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 01 # 0x0000041c
00 43 00 01 00 44 00 01 00 45 00 00 00 00 # 0x0000042c
# 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 0x0000043a: screen record, payload 54 bytes
02 00 36 00 00 00 ce 1f 9f 08 01 00 01 00 00 00 # 0x0000043a
00 00 00 00 00 00 01 00 02 00 01 00 00 00 00 00 # 0x0000044a
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000045a
00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000046a
# 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 0x0000047a: page record, payload 38 bytes
03 00 26 00 00 00 45 61 97 32 02 00 03 00 00 00 # 0x0000047a
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 02 # 0x0000048a
00 72 6e 03 02 00 61 74 03 01 00 65 00 00 00 00 # 0x0000049a
# offset 0x0000054e: continuation record, payload 0 bytes
07 00 00 00 00 00 27 80 63 d1 # 0x0000054e
# offset 0x000004aa: continuation record, payload 0 bytes
07 00 00 00 00 00 27 80 63 d1 # 0x000004aa
# offset 0x00000558: ready record, payload 32 bytes
05 00 20 00 00 00 d4 d1 fc d0 8d 69 76 c1 6e c6 # 0x00000558
88 21 bd 10 97 41 97 35 63 2d ce 9e 3c b8 fa cf # 0x00000568
57 04 f5 4b e0 70 66 5e 7e b6 # 0x00000578
# offset 0x000004b4: ready record, payload 32 bytes
05 00 20 00 00 00 4d 17 72 ed dd 87 26 75 cf 8e # 0x000004b4
e5 1f 37 e3 05 92 0a e2 f8 ef c1 16 54 be 49 e7 # 0x000004c4
b2 6c df 40 d2 77 fe 05 82 ea # 0x000004d4
# offset 0x00000582: history record, payload 6 bytes
04 00 06 00 00 00 20 32 ed e1 00 00 02 00 00 00 # 0x00000582
# offset 0x000004de: history record, payload 6 bytes
04 00 06 00 00 00 20 32 ed e1 00 00 02 00 00 00 # 0x000004de
# offset 0x00000592: page record, payload 86 bytes
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000592
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005a2
00 00 00 00 00 00 00 42 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
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005e2
# offset 0x000004ee: page record, payload 31 bytes
03 00 1f 00 00 00 4a ed 2f c2 02 00 02 00 00 00 # 0x000004ee
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 01 # 0x000004fe
00 42 00 00 00 00 00 00 00 # 0x0000050e
# offset 0x000005f2: page record, payload 86 bytes
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005f2
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000602
00 00 00 00 00 00 00 41 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
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000642
# offset 0x00000517: page record, payload 31 bytes
03 00 1f 00 00 00 23 6a 6b 19 02 00 02 00 00 00 # 0x00000517
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 01 # 0x00000527
00 41 00 00 00 00 00 00 00 # 0x00000537
# offset 0x00000652: history record, payload 6 bytes
04 00 06 00 00 00 b8 7a ba b1 01 00 00 00 00 00 # 0x00000652
# offset 0x00000540: history record, payload 6 bytes
04 00 06 00 00 00 b8 7a ba b1 01 00 00 00 00 00 # 0x00000540
# offset 0x00000662: finish record, payload 32 bytes
06 00 20 00 00 00 f3 a4 cf b6 f2 2b 17 88 1a 28 # 0x00000662
a7 27 6e f7 de bc 7a 52 f9 a4 f9 c3 32 5b e0 09 # 0x00000672
8a 1a 7d a0 9f 8b 32 31 67 41 # 0x00000682
# offset 0x00000550: finish record, payload 32 bytes
06 00 20 00 00 00 5a 2d a8 f4 25 b2 49 b4 3e 64 # 0x00000550
5a 2e d6 7d 7f 3d 60 5b db 9b 4c 3a 29 00 97 ec # 0x00000560
d6 c7 1d ea da d3 a9 fd 3d 68 # 0x00000570

View File

@@ -1,32 +1,15 @@
# Ghostty snapshot fixture
# Kaitai type: grid
# Kaitai params: 2 3
# Kaitai params: 4 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.
# row 0: prompt
04
# row 0, cell 0: protected prompt, wide "A"
00 01 05 00 00 00 00 00 41 00 00 00 00 00 00 00
# row 0, cell 1: input spacer tail
00 02 02 00 00 00 00 00 00 00 00 00 00 00 00 00
# row 0, cell 2: "x" with two grapheme suffixes
00 00 00 00 00 00 00 00 78 00 00 00 02 00 00 00
01 03 00 00 02 03 00 00
# row 1: wrapped prompt continuation
0b
# row 1, cell 0: palette background 7
01 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00
# row 1, cell 1: protected RGB background
02 00 01 00 00 00 00 00 aa bb cc 00 00 00 00 00
# row 1, cell 2: spacer head
00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00
# offset 0x00000000: encoded bytes
34 03 00 04 01 00 00 00 94 00 00 00 00 00 00 00 # 0x00000000
48 00 00 e1 01 00 00 00 00 00 00 3b 03 00 1e 00 # 0x00000010
00 00 00 00 00 00 ab ee 32 03 00 10 00 00 00 00 # 0x00000020
00 00 00 0c 00 00 00 03 00 68 00 69 10 02 00 16 # 0x00000030
04 21 00 01 00 00 00 00 00 02 00 02 00 01 03 00 # 0x00000040
00 02 03 00 00 # 0x00000050

View File

@@ -7,6 +7,6 @@
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
03 00 25 00 00 00 8c 05 d6 d3 01 00 01 00 00 00 # 0x00000000
03 00 1b 00 00 00 5e 0d 0a d4 01 00 01 00 00 00 # 0x00000000
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000010
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000020
00 00 00 00 00 # 0x00000020

View File

@@ -15,10 +15,8 @@
00 00 03 00 00 00 00 00 01 2a 00 00 00 00 00 00 # 0x00000024
00 00 00 00 01 00 02 01 00 00 00 61 05 00 00 00 # 0x00000034
61 6c 70 68 61 03 00 01 04 03 02 01 04 00 00 00 # 0x00000044
62 65 74 61 04 00 01 05 00 01 00 01 00 41 00 00 # 0x00000054
00 00 00 00 00 00 02 02 00 03 00 03 00 00 00 00 # 0x00000064
00 00 00 00 00 01 00 00 00 01 00 01 00 07 00 00 # 0x00000074
00 00 00 00 00 0b 00 00 00 00 00 00 00 00 78 00 # 0x00000084
00 00 02 00 00 00 01 03 00 00 02 03 00 00 02 00 # 0x00000094
01 00 00 00 00 00 aa bb cc 00 00 00 00 00 00 03 # 0x000000a4
00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000000b4
62 65 74 61 34 03 00 04 01 00 04 00 b4 01 00 00 # 0x00000054
00 00 0c 00 68 03 00 1e 00 00 04 00 20 01 00 3b # 0x00000064
03 00 e1 01 00 00 00 00 00 00 ab ee 32 03 00 10 # 0x00000074
00 00 00 00 00 00 00 0c 00 00 01 00 00 00 01 00 # 0x00000084
00 00 02 00 01 03 00 00 02 03 00 00 # 0x00000094