mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-05 15:18:40 +00:00
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:
307
src/benchmark/TerminalSnapshot.zig
Normal file
307
src/benchmark/TerminalSnapshot.zig
Normal 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);
|
||||
}
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
@@ -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
193
src/crc32c.zig
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
88
src/terminal/snapshot/testdata/complete-v1.hex
vendored
88
src/terminal/snapshot/testdata/complete-v1.hex
vendored
@@ -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
|
||||
|
||||
33
src/terminal/snapshot/testdata/grid-v1.hex
vendored
33
src/terminal/snapshot/testdata/grid-v1.hex
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
12
src/terminal/snapshot/testdata/page-v1.hex
vendored
12
src/terminal/snapshot/testdata/page-v1.hex
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user