terminal/snapshot: encode sparse page grids

This commit is contained in:
Mitchell Hashimoto
2026-07-27 13:37:09 -07:00
parent 2fc238ed01
commit 406f5e7d82
4 changed files with 1135 additions and 115 deletions

View File

@@ -0,0 +1,562 @@
//! Grid (rows and cells) encoding.
//!
//! A grid contains the rows and cells of a terminal page. Its dimensions are
//! supplied by the containing record rather than repeated here. The encoder
//! writes exactly `rows` row records, and every row contains exactly `columns`
//! cell records.
//!
//! All records are tightly packed with no padding between them. All integers
//! are unsigned and little-endian.
//!
//! ## Row
//!
//! Each row has the following format:
//!
//! | Offset | Size | Field |
//! | -----: | -------: | :-------------------------- |
//! | 0 | 1 | Row flags |
//! | 1 | variable | Exactly `columns` cells |
//!
//! Cells are encoded consecutively. Since cells may contain a variable number
//! of grapheme codepoints, the next row begins immediately after the final
//! cell and its grapheme codepoints.
//!
//! The row flag byte has the following format:
//!
//! | Bits | Field |
//! | ---: | :------------------------ |
//! | 0 | Wrap |
//! | 1 | Wrap continuation |
//! | 2-3 | Semantic prompt |
//! | 4-7 | Reserved, zero |
//!
//! Semantic prompt values are:
//!
//! | Value | Meaning |
//! | ----: | :------------------ |
//! | 0 | None |
//! | 1 | Prompt |
//! | 2 | Prompt continuation |
//!
//! Value 3 is invalid in snapshot version 0.
//!
//! ## Cell
//!
//! Each cell has the following format:
//!
//! | Offset | Size | Field |
//! | -----: | --------: | :---------------------------- |
//! | 0 | 1 | Content kind |
//! | 1 | 1 | Width kind |
//! | 2 | 1 | Protected and semantic flags |
//! | 3 | 1 | Reserved, zero |
//! | 4 | 2 | Style ID |
//! | 6 | 2 | Hyperlink ID |
//! | 8 | 4 | Codepoint or packed color |
//! | 12 | 4 | Grapheme suffix count |
//! | 16 | 4 * count | Grapheme suffix codepoints |
//!
//! Content kinds are:
//!
//! | Value | Meaning |
//! | ----: | :----------------- |
//! | 0 | Codepoint |
//! | 1 | Palette background |
//! | 2 | RGB background |
//!
//! For codepoint content, the value is a Unicode scalar encoded as a `u32`.
//! For a palette background, the low byte is the palette index and the other
//! three bytes are zero. For an RGB background, the low three bytes are red,
//! green, and blue, and the high byte is zero.
//!
//! Width kinds are:
//!
//! | Value | Meaning |
//! | ----: | :---------- |
//! | 0 | Narrow |
//! | 1 | Wide |
//! | 2 | Spacer tail |
//! | 3 | Spacer head |
//!
//! The cell flag byte has the following format:
//!
//! | Bits | Field |
//! | ---: | :---------------- |
//! | 0 | Protected |
//! | 1-2 | Semantic content |
//! | 3-7 | Reserved, zero |
//!
//! Semantic content values are:
//!
//! | Value | Meaning |
//! | ----: | :------ |
//! | 0 | Output |
//! | 1 | Input |
//! | 2 | Prompt |
//!
//! Value 3 is invalid in snapshot version 0.
//!
//! Style and hyperlink ID zero mean no style and no hyperlink. Other IDs
//! refer to entries in the containing record's separate style and hyperlink
//! tables.
//!
//! Grapheme suffixes are valid only for codepoint content. Each suffix is a
//! Unicode scalar encoded as a `u32`. A nonzero suffix count requires a
//! nonzero base codepoint.
const std = @import("std");
const io = @import("io.zig");
const kitty = @import("../kitty.zig");
const terminal_hyperlink = @import("../hyperlink.zig");
const terminal_page = @import("../page.zig");
const terminal_style = @import("../style.zig");
const TerminalCell = terminal_page.Cell;
const TerminalHyperlinkId = terminal_hyperlink.Id;
const TerminalPage = terminal_page.Page;
const TerminalRow = terminal_page.Row;
const TerminalStyleId = terminal_style.Id;
/// Maps encoded style table IDs to IDs assigned by the destination page.
///
/// Build this by inserting each decoded style into the page, then recording
/// the encoded ID and the ID returned by the page's style set. Style ID zero is
/// implicit and does not need an entry.
pub const StyleRemap = std.AutoHashMap(
TerminalStyleId,
TerminalStyleId,
);
/// Maps encoded hyperlink table IDs to IDs assigned by the destination page.
///
/// Build this by inserting each decoded hyperlink into the page, then
/// recording the encoded ID and the ID returned by the page's hyperlink set.
/// Hyperlink ID zero is implicit and does not need an entry.
pub const HyperlinkRemap = std.AutoHashMap(
TerminalHyperlinkId,
TerminalHyperlinkId,
);
pub const EncodeError = std.Io.Writer.Error;
/// Encode every row and cell directly from a page.
pub fn encode(
page: *const TerminalPage,
writer: *std.Io.Writer,
) EncodeError!void {
// Encoding assumes a structurally valid page. This assertion performs the
// comprehensive check in slow-safety builds without adding an allocator to
// the encoder API.
page.assertIntegrity();
for (0..page.size.rows) |y| {
// Row header
const row = page.getRow(y);
const row_header: RowHeader = .{
.wrap = row.wrap,
.wrap_continuation = row.wrap_continuation,
.semantic_prompt = switch (row.semantic_prompt) {
.none => .none,
.prompt => .prompt,
.prompt_continuation => .prompt_continuation,
},
};
try writer.writeByte(@bitCast(row_header));
// Cells
const cells = page.getCells(row);
for (cells) |*cell| {
const graphemes: []const u21 = if (cell.hasGrapheme())
page.lookupGrapheme(cell) orelse unreachable
else
&.{};
// The page has two codepoint tags depending on whether suffixes
// exist, but the wire represents both with one content kind.
const kind: CellHeader.Kind = switch (cell.content_tag) {
.codepoint, .codepoint_grapheme => .codepoint,
.bg_color_palette => .bg_color_palette,
.bg_color_rgb => .bg_color_rgb,
};
const value: CellHeader.Value = switch (kind) {
.codepoint => .{
.codepoint = cell.content.codepoint.data,
},
.bg_color_palette => .{ .bg_color_palette = .{
.index = cell.content.color_palette.data,
} },
.bg_color_rgb => .{ .bg_color_rgb = .{
.r = cell.content.color_rgb.r,
.g = cell.content.color_rgb.g,
.b = cell.content.color_rgb.b,
} },
};
const style_id = cell.style_id;
const hyperlink_id: TerminalHyperlinkId = if (cell.hyperlink)
page.lookupHyperlink(cell) orelse unreachable
else
0;
const header: CellHeader = .{
.content_kind = kind,
.width = cell.wide,
.protected = cell.protected,
.semantic_content = cell.semantic_content,
.style_id = style_id,
.hyperlink_id = hyperlink_id,
.value = value,
.grapheme_count = @intCast(graphemes.len),
};
try header.encode(writer);
for (graphemes) |suffix| try io.writeInt(
writer,
u32,
suffix,
);
}
}
}
pub const DecodeError = CellHeader.DecodeError || error{
/// A row contains undefined flag values.
InvalidRow,
/// A cell contains an undefined kind, width, flag, or reserved value.
InvalidCell,
/// A grapheme suffix is invalid or attached to non-codepoint content.
InvalidGrapheme,
/// The advertised grapheme capacity cannot hold the encoded suffixes.
InvalidGraphemeCapacity,
/// A codepoint is not a Unicode scalar value.
InvalidCodepoint,
/// A packed background color has nonzero reserved bytes.
InvalidColor,
/// Wide and spacer cells do not form a valid row.
InvalidWideCell,
/// The hyperlink map cannot hold the encoded cell references.
InvalidHyperlinkCapacity,
/// PAGE records do not support Kitty graphics placeholders.
UnsupportedKittyGraphics,
};
/// Decode every row and cell directly into an initialized, empty page.
///
/// The grid does not encode dimensions, so `page` must already have the exact
/// row and column count expected by the containing record. This function reads
/// exactly `page.size.rows` rows with `page.size.cols` cells each. The page
/// must also have enough grapheme and hyperlink-map capacity for the decoded
/// contents.
///
/// Style and hyperlink table entries must be inserted into `page` before
/// calling this function. As each table entry is inserted, the caller records
/// its encoded ID and page-assigned ID in `style_remap` or `hyperlink_remap`.
/// ID zero always means the default style or no hyperlink. A nonzero ID missing
/// from its remap is also treated as zero so unknown table references do not
/// prevent the rest of the grid from decoding.
pub fn decode(
page: *TerminalPage,
reader: *std.Io.Reader,
style_remap: *const StyleRemap,
hyperlink_remap: *const HyperlinkRemap,
) DecodeError!void {
for (0..page.size.rows) |y| {
const row_header: RowHeader = @bitCast(try reader.takeByte());
if (row_header._padding != 0) return error.InvalidRow;
const semantic_prompt: TerminalRow.SemanticPrompt =
switch (row_header.semantic_prompt) {
.none => .none,
.prompt => .prompt,
.prompt_continuation => .prompt_continuation,
.invalid => return error.InvalidRow,
};
const row = page.getRow(y);
row.wrap = row_header.wrap;
row.wrap_continuation = row_header.wrap_continuation;
row.semantic_prompt = semantic_prompt;
const cells = page.getCells(row);
for (cells, 0..) |*cell, x| {
const header = try CellHeader.decode(reader);
// IDs belong to the encoded page. Translate them to IDs assigned
// by the destination page before storing them on cells.
const encoded_style_id = header.style_id;
const style_id = if (encoded_style_id == 0)
0
else
style_remap.get(encoded_style_id) orelse 0;
const encoded_hyperlink_id = header.hyperlink_id;
const hyperlink_id = if (encoded_hyperlink_id == 0)
0
else
hyperlink_remap.get(encoded_hyperlink_id) orelse 0;
cell.* = .init(0);
switch (header.content_kind) {
.codepoint => {
const cp = std.math.cast(u21, header.value.codepoint) orelse
return error.InvalidCodepoint;
if (!validCodepoint(cp)) return error.InvalidCodepoint;
if (cp == kitty.graphics.unicode.placeholder) {
return error.UnsupportedKittyGraphics;
}
if (header.grapheme_count > 0 and cp == 0) {
return error.InvalidGrapheme;
}
cell.content = .{ .codepoint = .{ .data = cp } };
},
.bg_color_palette => {
const palette = header.value.bg_color_palette;
if (palette._padding != 0) return error.InvalidColor;
if (header.grapheme_count != 0) {
return error.InvalidGrapheme;
}
cell.content_tag = .bg_color_palette;
cell.content = .{
.color_palette = .{ .data = palette.index },
};
},
.bg_color_rgb => {
const rgb = header.value.bg_color_rgb;
if (rgb._padding != 0) return error.InvalidColor;
if (header.grapheme_count != 0) {
return error.InvalidGrapheme;
}
cell.content_tag = .bg_color_rgb;
cell.content = .{ .color_rgb = .{
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
} };
},
}
cell.wide = header.width;
cell.protected = header.protected;
cell.semantic_content = header.semantic_content;
if (style_id != 0) {
// The table owns one reference and each decoded cell owns one
// additional reference.
page.styles.use(page.memory, style_id);
cell.style_id = style_id;
row.styled = true;
}
if (hyperlink_id != 0) {
// setHyperlink records the cell mapping but intentionally does
// not increment the set's reference count.
page.hyperlink_set.use(page.memory, hyperlink_id);
page.setHyperlink(row, cell, hyperlink_id) catch
return error.InvalidHyperlinkCapacity;
}
// Append directly into page-owned grapheme storage so no
// intermediate suffix buffer is needed.
for (0..header.grapheme_count) |_| {
const suffix_raw = try io.readInt(reader, u32);
const suffix = std.math.cast(u21, suffix_raw) orelse
return error.InvalidCodepoint;
if (!validCodepoint(suffix)) return error.InvalidCodepoint;
if (suffix == kitty.graphics.unicode.placeholder) {
return error.UnsupportedKittyGraphics;
}
page.appendGrapheme(row, cell, suffix) catch
return error.InvalidGraphemeCapacity;
}
switch (header.width) {
.narrow, .wide => {},
.spacer_tail => if (x == 0 or
cells[x - 1].wide != .wide)
{
return error.InvalidWideCell;
},
.spacer_head => if (x + 1 != cells.len or !row.wrap) {
return error.InvalidWideCell;
},
}
}
}
}
/// The header before every row.
const RowHeader = packed struct(u8) {
wrap: bool = false,
wrap_continuation: bool = false,
semantic_prompt: SemanticPrompt = .none,
_padding: u4 = 0,
const SemanticPrompt = enum(u2) {
none = 0,
prompt = 1,
prompt_continuation = 2,
invalid = 3,
};
};
/// The fixed fields that precede a cell's grapheme suffix codepoints.
pub const CellHeader = struct {
/// Number of bytes written by `encode`, calculated using the encoder itself
/// so this remains synchronized with the field-by-field wire format.
pub const len = computeLen();
comptime {
// This size is part of the wire format. If it changes, the snapshot
// version and golden fixtures must also change.
std.debug.assert(len == 16);
}
/// Interpretation of `value`.
content_kind: Kind = .codepoint,
/// Display width and spacer role of the cell.
width: TerminalCell.Wide = .narrow,
/// Whether selective erase operations protect the cell.
protected: bool = false,
/// Semantic role assigned by shell integration.
semantic_content: TerminalCell.SemanticContent = .output,
/// ID in the encoded page's style table, or zero for the default style.
style_id: TerminalStyleId = 0,
/// ID in the encoded page's hyperlink table, or zero for no hyperlink.
hyperlink_id: TerminalHyperlinkId = 0,
/// Codepoint or packed background color selected by `content_kind`.
value: Value = .{ .codepoint = 0 },
/// Number of grapheme suffix codepoints immediately following the header.
grapheme_count: u32 = 0,
/// Errors possible while decoding a fixed cell header.
pub const DecodeError = std.Io.Reader.Error || error{InvalidCell};
/// Encode the fixed cell header.
pub fn encode(
self: CellHeader,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
const semantic_content: Flags.SemanticContent = switch (self.semantic_content) {
.output => .output,
.input => .input,
.prompt => .prompt,
};
const flags: Flags = .{
.protected = self.protected,
.semantic_content = semantic_content,
};
// 0 1 2 3 4 6 8 12 16
// +-------+-------+-------+-------+--------+--------+--------+--------+
// | kind | width | flags | zero | style | link | value | count |
// | u8 | u8 | u8 | u8 | u16 LE | u16 LE | u32 LE | u32 LE |
// +-------+-------+-------+-------+--------+--------+--------+--------+
try writer.writeByte(@intFromEnum(self.content_kind));
try writer.writeByte(@intFromEnum(self.width));
try writer.writeByte(@bitCast(flags));
try writer.writeByte(0);
try io.writeInt(writer, TerminalStyleId, self.style_id);
try io.writeInt(writer, TerminalHyperlinkId, self.hyperlink_id);
try io.writeInt(writer, u32, @bitCast(self.value));
try io.writeInt(writer, u32, self.grapheme_count);
}
/// Decode and validate the fixed cell header.
pub fn decode(reader: *std.Io.Reader) CellHeader.DecodeError!CellHeader {
const content_kind = std.enums.fromInt(
Kind,
try reader.takeByte(),
) orelse return error.InvalidCell;
const width = std.enums.fromInt(
TerminalCell.Wide,
try reader.takeByte(),
) orelse return error.InvalidCell;
const flags: Flags = @bitCast(try reader.takeByte());
if (flags._padding != 0) return error.InvalidCell;
const semantic_content: TerminalCell.SemanticContent = switch (flags.semantic_content) {
.output => .output,
.input => .input,
.prompt => .prompt,
.invalid => return error.InvalidCell,
};
// This byte is reserved so the IDs and content value remain naturally
// aligned within the fixed cell header.
if (try reader.takeByte() != 0) return error.InvalidCell;
return .{
.content_kind = content_kind,
.width = width,
.protected = flags.protected,
.semantic_content = semantic_content,
.style_id = try io.readInt(reader, TerminalStyleId),
.hyperlink_id = try io.readInt(reader, TerminalHyperlinkId),
.value = @bitCast(try io.readInt(reader, u32)),
.grapheme_count = try io.readInt(reader, u32),
};
}
/// Computes the fixed header size using the encoder itself.
fn computeLen() usize {
comptime {
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
CellHeader.encode(.{}, &writer) catch unreachable;
return writer.end;
}
}
/// Determines how `value` is interpreted.
pub const Kind = enum(u8) {
codepoint = 0,
bg_color_palette = 1,
bg_color_rgb = 2,
};
/// The alternate interpretations of the four-byte content field.
pub const Value = packed union(u32) {
codepoint: u32,
bg_color_palette: packed struct(u32) {
index: u8,
_padding: u24 = 0,
},
bg_color_rgb: packed struct(u32) {
r: u8,
g: u8,
b: u8,
_padding: u8 = 0,
},
};
const Flags = packed struct(u8) {
protected: bool = false,
semantic_content: SemanticContent = .output,
_padding: u5 = 0,
const SemanticContent = enum(u2) {
output = 0,
input = 1,
prompt = 2,
invalid = 3,
};
};
};
fn validCodepoint(cp: u21) bool {
return cp <= 0x10FFFF and (cp < 0xD800 or cp > 0xDFFF);
}

View File

@@ -64,6 +64,9 @@ pub const DecodePageError = std.Io.Reader.Error ||
error{
/// The hyperlink kind is not defined by snapshot version 0.
InvalidKind,
/// The hyperlink value already exists in the page.
DuplicateHyperlink,
};
/// Encode one hyperlink entry.
@@ -136,7 +139,7 @@ pub fn decode(
///
/// Explicit ID and URI bytes are read into the page string allocator and the
/// completed entry is inserted into the page hyperlink set. The returned ID is
/// the native page ID assigned to the entry.
/// the native ID assigned by the destination page.
pub fn decodePage(
page: *terminal_page.Page,
reader: *std.Io.Reader,
@@ -188,6 +191,14 @@ pub fn decodePage(
};
errdefer entry.free(page);
if (page.hyperlink_set.lookupContext(
page.memory,
entry,
.{ .page = page },
) != null) {
return error.DuplicateHyperlink;
}
return page.hyperlink_set.addContext(
page.memory,
entry,
@@ -219,7 +230,7 @@ fn decodePageString(
.offset = terminal_size.getOffset(
u8,
page.memory,
value.ptr,
&value[0],
),
};
}

View File

@@ -44,7 +44,12 @@
//! CONTINUATION, READY, and FINISH.
pub const envelope = @import("envelope.zig");
pub const grid = @import("grid.zig");
pub const hyperlink = @import("hyperlink.zig");
pub const page = @import("page.zig");
pub const record = @import("record.zig");
pub const style = @import("style.zig");
test {
@import("std").testing.refAllDecls(@This());
}

View File

@@ -40,9 +40,9 @@
//!
//! Next, the style count and hyperlink count denote the number of
//! styles and hyperlinks respectively that are sent with the page.
//! The default style and absence of a hyperlink are implicit at wire
//! index zero and are not included in these counts. Encoded table entries
//! receive one-based wire indexes in their encoded order.
//! The default style and absence of a hyperlink are implicit at native
//! ID zero and are not included in these counts. Each encoded table entry
//! carries its nonzero native page ID.
//!
//! The final four fields are allocation hints copied from the source page.
//! These represent upper limits on what this page might contain. A decoder
@@ -51,14 +51,50 @@
//!
//! ### Payload
//!
//! This is still a work-in-progress. The current implementation encodes and
//! decodes the header, style table, and hyperlink table only. It does not yet
//! produce or consume a complete PAGE payload.
//! ```text
//! +---------------------------+
//! | Header |
//! | 20 bytes |
//! +---------------------------+
//! | Style table |
//! | style_count entries |
//! +---------------------------+
//! | Hyperlink table |
//! | hyperlink_count entries |
//! +---------------------------+
//! | Grid |
//! | one record per row |
//! +---------------------------+
//!
//! Following the header, data is tightly packed in the following order:
//! styles, hyperlinks, cells. TODO!
//! 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
//! ```
//!
//! 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.
//!
//! Each style begins with an ID (`u16`) followed by the typical style
//! binary representation (in style.zig). The ID is what cells will use
//! to reference the style. Decoders must maintain some kind of mapping
//! in order to associate these properly.
//!
//! Each hyperlink also begins with an ID (`u16`) with the same semantics
//! as the style ID. The hyperlink and style IDs are separate namespaces,
//! so they may collide. Following the ID, the hyperlink binary format
//! is encoded directly.
//!
//! Both style and hyperlink IDs are not guaranteed to be in any specific
//! order and may contain gaps (e.g. ID 1 and 3 but not 2).
//!
//! Rows and cells use the grid encoding documented in `grid.zig`.
const std = @import("std");
const Allocator = std.mem.Allocator;
const grid = @import("grid.zig");
const hyperlink = @import("hyperlink.zig");
const io = @import("io.zig");
const style = @import("style.zig");
@@ -68,19 +104,23 @@ const terminal_style = @import("../style.zig");
// Frequent constants we use
const TerminalHyperlink = terminal_hyperlink.Hyperlink;
const TerminalHyperlinkId = terminal_hyperlink.Id;
const TerminalHyperlinkPageEntry = terminal_hyperlink.PageEntry;
const TerminalHyperlinkSet = terminal_hyperlink.Set;
const TerminalCell = terminal_page.Cell;
const TerminalPage = terminal_page.Page;
const TerminalPageCapacity = terminal_page.Capacity;
const TerminalRow = terminal_page.Row;
const TerminalStyle = terminal_style.Style;
const TerminalStyleId = terminal_style.Id;
const TerminalStyleSet = terminal_style.Set;
/// Errors possible while encoding the native PAGE prefix.
pub const EncodeError = hyperlink.EncodeError;
/// Errors possible while encoding a native PAGE.
pub const EncodeError = hyperlink.EncodeError || grid.EncodeError;
/// Errors possible while decoding the native PAGE prefix.
/// Errors possible while decoding a native PAGE.
pub const DecodeError = style.DecodeError ||
grid.DecodeError ||
Header.CapacityError ||
error{
/// The hyperlink kind is not defined by snapshot version 0.
@@ -98,15 +138,15 @@ pub const DecodeError = style.DecodeError ||
/// The default style cannot appear in the non-default style table.
DefaultStyle,
/// An encoded table ID is zero or appears more than once.
InvalidStyleId,
InvalidHyperlinkId,
/// Native page backing memory could not be allocated.
OutOfMemory,
};
/// Encode the PAGE header and lookup tables directly from a native page.
///
/// Only the currently implemented PAGE prefix is written. Rows and cells will
/// be appended by a later increment. The page is iterated in place and no
/// temporary storage is allocated or retained.
/// Encode a complete PAGE directly from a native page.
pub fn encode(
page: *const TerminalPage,
writer: *std.Io.Writer,
@@ -114,30 +154,31 @@ pub fn encode(
// Write header
try Header.init(page).encode(writer);
// Packed styles
// Sparse styles
var style_it = page.styles.iterator(page.memory);
while (style_it.next()) |entry| {
try io.writeInt(writer, TerminalStyleId, entry.id);
try style.encode(entry.value_ptr.*, writer);
}
// Packed hyperlinks
// Sparse hyperlinks
var hyperlink_it = page.hyperlink_set.iterator(page.memory);
while (hyperlink_it.next()) |entry| {
try io.writeInt(writer, TerminalHyperlinkId, entry.id);
try hyperlink.encode(pageHyperlink(page, entry.value_ptr), writer);
}
// Rows and cells
try grid.encode(page, writer);
}
/// Decode the PAGE header and lookup tables directly into a native page.
/// Decode a complete PAGE directly into a fresh native page.
///
/// The fixed header is validated before allocating the page. Style entries
/// are inserted directly into the page style set, while hyperlink strings are
/// read into the page string allocator before their native entries are
/// inserted. No caller-owned table or string buffers are required.
///
/// Only the currently implemented PAGE prefix is consumed. Rows and cells
/// will be decoded by a later increment.
/// `alloc` is used only for the temporary native-ID remap tables. Styles,
/// hyperlinks, strings, graphemes, rows, and cells are stored in the page.
pub fn decode(
reader: *std.Io.Reader,
alloc: Allocator,
) DecodeError!TerminalPage {
// Decode the header, validate capacities, init page
const header = try Header.decode(reader);
@@ -145,22 +186,65 @@ pub fn decode(
var page = TerminalPage.init(capacity) catch
return error.OutOfMemory;
errdefer page.deinit();
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
return error.OutOfMemory;
var hyperlink_remap = grid.HyperlinkRemap.init(alloc);
defer hyperlink_remap.deinit();
hyperlink_remap.ensureTotalCapacity(header.hyperlink_count) catch
return error.OutOfMemory;
// Styles
for (0..header.style_count) |wire_index| {
for (0..header.style_count) |_| {
// Zero denotes the implicit default style. Reusing an encoded ID would
// make cell references ambiguous because it would name multiple table
// entries.
const native_id = try io.readInt(reader, TerminalStyleId);
if (native_id == 0 or style_remap.contains(native_id)) {
return error.InvalidStyleId;
}
// Decode the style itself. It must never be the default style.
const value = try style.decode(reader);
if (value.default()) return error.DefaultStyle;
const native_id = page.styles.add(
// If we already have the style, its invalid.
if (page.styles.lookup(
page.memory,
value,
) catch return error.InvalidStyleCapacity;
if (native_id != wire_index + 1) return error.DuplicateStyle;
) != null) return error.DuplicateStyle;
// Add our style, get our real ID on this side, and store it in the
// remap table.
const decoded_id = page.styles.add(
page.memory,
value,
) catch |err| switch (err) {
error.OutOfMemory,
error.NeedsRehash,
=> return error.InvalidStyleCapacity,
};
style_remap.putAssumeCapacityNoClobber(
native_id,
decoded_id,
);
}
// Hyperlinks
for (0..header.hyperlink_count) |wire_index| {
const native_id = hyperlink.decodePage(
for (0..header.hyperlink_count) |_| {
// Zero denotes no hyperlink. As with styles, a repeated encoded ID
// would make cell references ambiguous.
const native_id = try io.readInt(reader, TerminalHyperlinkId);
if (native_id == 0 or hyperlink_remap.contains(native_id)) {
return error.InvalidHyperlinkId;
}
const decoded_id = hyperlink.decodePage(
&page,
reader,
) catch |err| switch (err) {
@@ -168,11 +252,26 @@ pub fn decode(
error.SetOutOfMemory,
error.SetNeedsRehash,
=> return error.InvalidHyperlinkCapacity,
else => return err,
error.DuplicateHyperlink => return error.DuplicateHyperlink,
error.InvalidKind => return error.InvalidKind,
error.EndOfStream => return error.EndOfStream,
error.ReadFailed => return error.ReadFailed,
};
if (native_id != wire_index + 1) return error.DuplicateHyperlink;
hyperlink_remap.putAssumeCapacityNoClobber(
native_id,
decoded_id,
);
}
// Rows and cells
try grid.decode(
&page,
reader,
&style_remap,
&hyperlink_remap,
);
return page;
}
@@ -331,6 +430,21 @@ fn pageHyperlink(
};
}
const test_page_fixture =
"\x03\x00\x02\x00\x02\x00\x02\x00\x08\x00\x00\x02\x80\x00\x00\x00" ++
"\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x01\x2a\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x01\x00\x00\x00\x61" ++
"\x05\x00\x00\x00\x61\x6c\x70\x68\x61\x03\x00\x01\x04\x03\x02\x01" ++
"\x04\x00\x00\x00\x62\x65\x74\x61\x04\x00\x01\x05\x00\x01\x00\x01" ++
"\x00\x41\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00\x03\x00\x03" ++
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x01" ++
"\x00\x07\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x78\x00\x00\x00\x02\x00\x00\x00\x01\x03\x00\x00\x02\x03" ++
"\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\xaa\xbb\xcc\x00\x00\x00" ++
"\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00";
test "golden encoding" {
const header: Header = .{
.columns = 0x0102,
@@ -390,7 +504,7 @@ test "reject every truncation" {
}
}
test "encode native page lookup tables" {
test "encode and decode a sparse native page" {
const capacity: TerminalPageCapacity = .{
.cols = 3,
.rows = 2,
@@ -453,6 +567,33 @@ test "encode native page lookup tables" {
&.{ 0x0301, 0x0302 },
);
first.cell.content = .{ .codepoint = .{ .data = 'A' } };
first.cell.wide = .wide;
first.cell.protected = true;
first.cell.semantic_content = .prompt;
first.row.semantic_prompt = .prompt;
second.cell.wide = .spacer_tail;
second.cell.semantic_content = .input;
third.cell.content_tag = .bg_color_palette;
third.cell.content = .{ .color_palette = .{ .data = 7 } };
const rgb = page.getRowAndCell(1, 1);
rgb.cell.content_tag = .bg_color_rgb;
rgb.cell.content = .{ .color_rgb = .{
.r = 0xaa,
.g = 0xbb,
.b = 0xcc,
} };
rgb.cell.protected = true;
const spacer_head = page.getRowAndCell(2, 1);
spacer_head.cell.wide = .spacer_head;
spacer_head.row.wrap = true;
spacer_head.row.wrap_continuation = true;
spacer_head.row.semantic_prompt = .prompt_continuation;
const header: Header = .{
.columns = 3,
.rows = 2,
@@ -468,97 +609,380 @@ test "encode native page lookup tables" {
var counter: std.Io.Writer.Discarding = .init(&.{});
try encode(&page, &counter.writer);
var encoded: [128]u8 = undefined;
var encoded: [512]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try encode(&page, &writer);
const fixture =
"\x03\x00\x02\x00\x02\x00\x02\x00" ++
"\x08\x00\x00\x02\x80\x00\x00\x00" ++
"\x00\x01\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x01\x00\x00\x00" ++
"\x00\x00\x00\x00\x01\x2a\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x02\x01\x00\x00\x00a\x05\x00\x00\x00alpha" ++
"\x01\x04\x03\x02\x01\x04\x00\x00\x00beta";
const fixture = test_page_fixture;
try std.testing.expectEqualStrings(fixture, writer.buffered());
try std.testing.expectEqual(@as(u64, fixture.len), counter.count);
}
test "decode lookup tables directly into a native page" {
const header: Header = .{
.columns = 80,
.rows = 24,
.style_count = 2,
.hyperlink_count = 2,
.style_capacity = 16,
.hyperlink_capacity_bytes = 512,
.grapheme_capacity_bytes = 128,
.string_capacity_bytes = 256,
};
const fixture =
"\x50\x00\x18\x00\x02\x00\x02\x00" ++
"\x10\x00\x00\x02\x80\x00\x00\x00" ++
"\x00\x01\x00\x00" ++
"\x01\x2a\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x01\x00\x00\x00" ++
"\x00\x00\x00\x00\x02\xaa\xbb\xcc" ++
"\x00\x00\x00\x00\x00\x02\x00\x00" ++
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri" ++
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00url";
var source: std.Io.Reader = .fixed(fixture);
var source: std.Io.Reader = .fixed(writer.buffered());
var read_buf: [1]u8 = undefined;
var limited = source.limited(.unlimited, &read_buf);
var decoded = try decode(&limited.interface);
var decoded = try decode(&limited.interface, std.testing.allocator);
defer decoded.deinit();
try std.testing.expectEqual(header, Header.init(&decoded));
try decoded.verifyIntegrity(std.testing.allocator);
var style_it = decoded.styles.iterator(decoded.memory);
const style_a = style_it.next().?;
try std.testing.expectEqual(@as(TerminalStyleId, 1), style_a.id);
const decoded_style_a = style_it.next().?;
try std.testing.expectEqual(@as(TerminalStyleId, 1), decoded_style_a.id);
try std.testing.expect((TerminalStyle{
.fg_color = .{ .palette = 42 },
.flags = .{ .bold = true },
}).eql(style_a.value_ptr.*));
const style_b = style_it.next().?;
try std.testing.expectEqual(@as(TerminalStyleId, 2), style_b.id);
}).eql(decoded_style_a.value_ptr.*));
const decoded_style_b = style_it.next().?;
try std.testing.expectEqual(@as(TerminalStyleId, 2), decoded_style_b.id);
try std.testing.expect((TerminalStyle{
.bg_color = .{ .rgb = .{
.r = 0xaa,
.g = 0xbb,
.b = 0xcc,
} },
.flags = .{ .underline = .double },
}).eql(style_b.value_ptr.*));
.bg_color = .{ .palette = 42 },
}).eql(decoded_style_b.value_ptr.*));
try std.testing.expectEqual(null, style_it.next());
var hyperlink_it = decoded.hyperlink_set.iterator(decoded.memory);
const hyperlink_a = pageHyperlink(
&decoded,
hyperlink_it.next().?.value_ptr,
);
try std.testing.expectEqual(
@as(u32, 0x01020304),
hyperlink_a.id.implicit,
);
try std.testing.expectEqualStrings("uri", hyperlink_a.uri);
const hyperlink_b = pageHyperlink(
&decoded,
hyperlink_it.next().?.value_ptr,
);
try std.testing.expectEqualStrings("id", hyperlink_b.id.explicit);
try std.testing.expectEqualStrings("url", hyperlink_b.uri);
const decoded_link_a = hyperlink_it.next().?;
try std.testing.expectEqual(@as(TerminalHyperlinkId, 1), decoded_link_a.id);
const decoded_hyperlink_a = pageHyperlink(&decoded, decoded_link_a.value_ptr);
try std.testing.expectEqualStrings("a", decoded_hyperlink_a.id.explicit);
try std.testing.expectEqualStrings("alpha", decoded_hyperlink_a.uri);
const decoded_link_b = hyperlink_it.next().?;
try std.testing.expectEqual(@as(TerminalHyperlinkId, 2), decoded_link_b.id);
const decoded_hyperlink_b = pageHyperlink(&decoded, decoded_link_b.value_ptr);
try std.testing.expectEqual(@as(u32, 0x01020304), decoded_hyperlink_b.id.implicit);
try std.testing.expectEqualStrings("beta", decoded_hyperlink_b.uri);
try std.testing.expectEqual(null, hyperlink_it.next());
var reencoded: [fixture.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&reencoded);
try encode(&decoded, &writer);
try std.testing.expectEqualStrings(fixture, writer.buffered());
const decoded_first = decoded.getRowAndCell(0, 0);
try std.testing.expectEqual(@as(u21, 'A'), decoded_first.cell.codepoint());
try std.testing.expectEqual(TerminalCell.Wide.wide, decoded_first.cell.wide);
try std.testing.expect(decoded_first.cell.protected);
try std.testing.expectEqual(
TerminalCell.SemanticContent.prompt,
decoded_first.cell.semantic_content,
);
try std.testing.expectEqual(@as(TerminalStyleId, 1), decoded_first.cell.style_id);
try std.testing.expectEqual(
@as(TerminalHyperlinkId, 1),
decoded.lookupHyperlink(decoded_first.cell).?,
);
const decoded_second = decoded.getRowAndCell(1, 0);
try std.testing.expectEqual(TerminalCell.Wide.spacer_tail, decoded_second.cell.wide);
try std.testing.expectEqual(@as(TerminalStyleId, 2), decoded_second.cell.style_id);
try std.testing.expectEqual(
@as(TerminalHyperlinkId, 2),
decoded.lookupHyperlink(decoded_second.cell).?,
);
const decoded_grapheme = decoded.getRowAndCell(0, 1);
try std.testing.expectEqualSlices(
u21,
&.{ 0x0301, 0x0302 },
decoded.lookupGrapheme(decoded_grapheme.cell).?,
);
const decoded_rgb = decoded.getRowAndCell(1, 1);
try std.testing.expectEqual(TerminalCell.ContentTag.bg_color_rgb, decoded_rgb.cell.content_tag);
try std.testing.expectEqual(
TerminalCell.RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc },
decoded_rgb.cell.content.color_rgb,
);
const decoded_spacer_head = decoded.getRowAndCell(2, 1);
try std.testing.expectEqual(
TerminalCell.Wide.spacer_head,
decoded_spacer_head.cell.wide,
);
try std.testing.expect(decoded_spacer_head.row.wrap);
try std.testing.expect(decoded_spacer_head.row.wrap_continuation);
try std.testing.expectEqual(
TerminalRow.SemanticPrompt.prompt_continuation,
decoded_spacer_head.row.semantic_prompt,
);
var reencoded: [512]u8 = undefined;
var rewriter: std.Io.Writer = .fixed(&reencoded);
try encode(&decoded, &rewriter);
try std.testing.expect(!std.mem.eql(
u8,
writer.buffered(),
rewriter.buffered(),
));
var reencoded_reader: std.Io.Reader = .fixed(rewriter.buffered());
var decoded_again = try decode(
&reencoded_reader,
std.testing.allocator,
);
defer decoded_again.deinit();
try decoded_again.verifyIntegrity(std.testing.allocator);
var reencoded_again: [512]u8 = undefined;
var rewriter_again: std.Io.Writer = .fixed(&reencoded_again);
try encode(&decoded_again, &rewriter_again);
try std.testing.expectEqualStrings(
rewriter.buffered(),
rewriter_again.buffered(),
);
}
test "decode sparse page rejects every truncation" {
for (0..test_page_fixture.len) |len| {
var reader: std.Io.Reader = .fixed(test_page_fixture[0..len]);
try std.testing.expectError(
error.EndOfStream,
decode(&reader, std.testing.allocator),
);
}
}
test "decode accepts unordered sparse style IDs and rejects zero" {
const header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 2,
.hyperlink_count = 0,
.style_capacity = 8,
.hyperlink_capacity_bytes = 0,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var descending: [
Header.len +
2 * (2 + style.len) +
1 +
grid.CellHeader.len
]u8 = undefined;
var descending_writer: std.Io.Writer = .fixed(&descending);
try header.encode(&descending_writer);
try io.writeInt(&descending_writer, TerminalStyleId, 3);
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 grid.CellHeader.encode(.{}, &descending_writer);
var descending_reader: std.Io.Reader = .fixed(
descending_writer.buffered(),
);
var decoded_descending = try decode(
&descending_reader,
std.testing.allocator,
);
defer decoded_descending.deinit();
try std.testing.expectEqual(@as(usize, 2), decoded_descending.styles.count());
const one_header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 1,
.hyperlink_count = 0,
.style_capacity = 8,
.hyperlink_capacity_bytes = 0,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var zero: [Header.len + 2 + style.len]u8 = undefined;
var zero_writer: std.Io.Writer = .fixed(&zero);
try one_header.encode(&zero_writer);
try io.writeInt(&zero_writer, TerminalStyleId, 0);
try style.encode(.{ .flags = .{ .bold = true } }, &zero_writer);
var zero_reader: std.Io.Reader = .fixed(zero_writer.buffered());
try std.testing.expectError(
error.InvalidStyleId,
decode(&zero_reader, std.testing.allocator),
);
}
test "decode accepts unordered sparse hyperlink IDs" {
const header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 0,
.hyperlink_count = 2,
.style_capacity = 0,
.hyperlink_capacity_bytes = 512,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
const first: TerminalHyperlink = .{
.id = .{ .implicit = 1 },
.uri = "",
};
const second: TerminalHyperlink = .{
.id = .{ .implicit = 2 },
.uri = "",
};
var encoded: [
Header.len +
2 * 11 +
1 +
grid.CellHeader.len
]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
try io.writeInt(&writer, TerminalHyperlinkId, 3);
try hyperlink.encode(first, &writer);
try io.writeInt(&writer, TerminalHyperlinkId, 2);
try hyperlink.encode(second, &writer);
try writer.writeByte(0);
try grid.CellHeader.encode(.{}, &writer);
var reader: std.Io.Reader = .fixed(writer.buffered());
var decoded = try decode(
&reader,
std.testing.allocator,
);
defer decoded.deinit();
try std.testing.expectEqual(
@as(usize, 2),
decoded.hyperlink_set.count(),
);
}
test "decode defaults missing sparse cell references" {
const style_header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 0,
.hyperlink_count = 0,
.style_capacity = 8,
.hyperlink_capacity_bytes = 0,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var style_encoded: [Header.len + 1 + 16]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);
var style_reader: std.Io.Reader = .fixed(style_writer.buffered());
var style_page = try decode(
&style_reader,
std.testing.allocator,
);
defer style_page.deinit();
try std.testing.expectEqual(
@as(TerminalStyleId, 0),
style_page.getRowAndCell(0, 0).cell.style_id,
);
const hyperlink_header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 0,
.hyperlink_count = 0,
.style_capacity = 0,
.hyperlink_capacity_bytes = 512,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var hyperlink_encoded: [Header.len + 1 + 16]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);
var hyperlink_reader: std.Io.Reader = .fixed(
hyperlink_writer.buffered(),
);
var hyperlink_page = try decode(
&hyperlink_reader,
std.testing.allocator,
);
defer hyperlink_page.deinit();
const hyperlink_cell = hyperlink_page.getRowAndCell(0, 0).cell;
try std.testing.expect(!hyperlink_cell.hyperlink);
try std.testing.expectEqual(
null,
hyperlink_page.lookupHyperlink(hyperlink_cell),
);
}
test "decode rejects undefined row and cell values" {
const header: Header = .{
.columns = 1,
.rows = 1,
.style_count = 0,
.hyperlink_count = 0,
.style_capacity = 0,
.hyperlink_capacity_bytes = 0,
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var invalid_row: [Header.len + 1]u8 = undefined;
var row_writer: std.Io.Writer = .fixed(&invalid_row);
try header.encode(&row_writer);
try row_writer.writeByte(0x10);
var row_reader: std.Io.Reader = .fixed(row_writer.buffered());
try std.testing.expectError(
error.InvalidRow,
decode(&row_reader, std.testing.allocator),
);
var invalid_cell: [Header.len + 2]u8 = undefined;
var cell_writer: std.Io.Writer = .fixed(&invalid_cell);
try header.encode(&cell_writer);
try cell_writer.writeByte(0);
try cell_writer.writeByte(3);
var cell_reader: std.Io.Reader = .fixed(cell_writer.buffered());
try std.testing.expectError(
error.InvalidCell,
decode(&cell_reader, std.testing.allocator),
);
var invalid_codepoint: [Header.len + 1 + 16]u8 = undefined;
var codepoint_writer: std.Io.Writer = .fixed(&invalid_codepoint);
try header.encode(&codepoint_writer);
try codepoint_writer.writeByte(0);
try codepoint_writer.writeAll(&.{ 0, 0, 0, 0 });
try io.writeInt(&codepoint_writer, TerminalStyleId, 0);
try io.writeInt(&codepoint_writer, TerminalHyperlinkId, 0);
try io.writeInt(&codepoint_writer, u32, 0xD800);
try io.writeInt(&codepoint_writer, u32, 0);
var codepoint_reader: std.Io.Reader = .fixed(
codepoint_writer.buffered(),
);
try std.testing.expectError(
error.InvalidCodepoint,
decode(&codepoint_reader, std.testing.allocator),
);
var invalid_color: [Header.len + 1 + 16]u8 = undefined;
var color_writer: std.Io.Writer = .fixed(&invalid_color);
try header.encode(&color_writer);
try color_writer.writeByte(0);
try color_writer.writeAll(&.{ 1, 0, 0, 0 });
try io.writeInt(&color_writer, TerminalStyleId, 0);
try io.writeInt(&color_writer, TerminalHyperlinkId, 0);
try io.writeInt(&color_writer, u32, 0x100);
try io.writeInt(&color_writer, u32, 0);
var color_reader: std.Io.Reader = .fixed(color_writer.buffered());
try std.testing.expectError(
error.InvalidColor,
decode(&color_reader, std.testing.allocator),
);
}
test "decode validates dimensions and native table capacities" {
@@ -623,7 +1047,10 @@ test "decode validates dimensions and native table capacities" {
try case.header.encode(&writer);
var reader: std.Io.Reader = .fixed(writer.buffered());
try std.testing.expectError(case.expected, decode(&reader));
try std.testing.expectError(
case.expected,
decode(&reader, std.testing.allocator),
);
}
}
@@ -642,14 +1069,19 @@ test "decode rejects duplicate and default style entries" {
.flags = .{ .bold = true },
};
var encoded: [Header.len + 2 * style.len]u8 = undefined;
var encoded: [Header.len + 2 * (2 + style.len)]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
try io.writeInt(&writer, TerminalStyleId, 1);
try style.encode(duplicate_style, &writer);
try io.writeInt(&writer, TerminalStyleId, 3);
try style.encode(duplicate_style, &writer);
var reader: std.Io.Reader = .fixed(writer.buffered());
try std.testing.expectError(error.DuplicateStyle, decode(&reader));
try std.testing.expectError(
error.DuplicateStyle,
decode(&reader, std.testing.allocator),
);
const default_header: Header = .{
.columns = 80,
@@ -661,13 +1093,17 @@ test "decode rejects duplicate and default style entries" {
.grapheme_capacity_bytes = 0,
.string_capacity_bytes = 0,
};
var default_encoded: [Header.len + style.len]u8 = undefined;
var default_encoded: [Header.len + 2 + style.len]u8 = undefined;
var default_writer: std.Io.Writer = .fixed(&default_encoded);
try default_header.encode(&default_writer);
try io.writeInt(&default_writer, TerminalStyleId, 1);
try style.encode(.{}, &default_writer);
var default_reader: std.Io.Reader = .fixed(default_writer.buffered());
try std.testing.expectError(error.DefaultStyle, decode(&default_reader));
try std.testing.expectError(
error.DefaultStyle,
decode(&default_reader, std.testing.allocator),
);
}
test "decode rejects duplicate hyperlinks with empty strings" {
@@ -686,14 +1122,19 @@ test "decode rejects duplicate hyperlinks with empty strings" {
.uri = "",
};
var encoded: [Header.len + 18]u8 = undefined;
var encoded: [Header.len + 22]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
try io.writeInt(&writer, TerminalHyperlinkId, 1);
try hyperlink.encode(duplicate, &writer);
try io.writeInt(&writer, TerminalHyperlinkId, 3);
try hyperlink.encode(duplicate, &writer);
var reader: std.Io.Reader = .fixed(writer.buffered());
try std.testing.expectError(error.DuplicateHyperlink, decode(&reader));
try std.testing.expectError(
error.DuplicateHyperlink,
decode(&reader, std.testing.allocator),
);
}
test "decode reads hyperlink strings into page storage" {
@@ -716,14 +1157,15 @@ test "decode reads hyperlink strings into page storage" {
.string_capacity_bytes = 0,
};
var encoded: [Header.len + 14]u8 = undefined;
var encoded: [Header.len + 16]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try header.encode(&writer);
try io.writeInt(&writer, TerminalHyperlinkId, 1);
try hyperlink.encode(link, &writer);
var reader: std.Io.Reader = .fixed(writer.buffered());
try std.testing.expectError(
error.InvalidStringCapacity,
decode(&reader),
decode(&reader, std.testing.allocator),
);
}