mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 20:59:02 +00:00
terminal/snapshot: hyperlink and style encoding
This commit is contained in:
354
src/terminal/snapshot/hyperlink.zig
Normal file
354
src/terminal/snapshot/hyperlink.zig
Normal file
@@ -0,0 +1,354 @@
|
||||
//! Snapshot hyperlink entry encoding.
|
||||
//!
|
||||
//! Each entry contains a URI and either an implicit numeric ID or an explicit
|
||||
//! byte-string ID. Implicit IDs are generated by the terminal when the source
|
||||
//! hyperlink has no explicit ID. A record can use these entries to build a
|
||||
//! hyperlink table and assign indexes according to that record's format.
|
||||
//! Indexing and ordering are properties of the containing record rather than
|
||||
//! this codec.
|
||||
//!
|
||||
//! IDs and URIs are arbitrary byte strings. Their lengths are retained on the
|
||||
//! wire; they are not NUL-terminated and need not contain UTF-8.
|
||||
//!
|
||||
//! All integers are unsigned and little-endian.
|
||||
//!
|
||||
//! ## Implicit ID
|
||||
//!
|
||||
//! | Offset | Size | Field |
|
||||
//! | -----: | --------: | :----------------- |
|
||||
//! | 0 | 1 | Kind, `1` |
|
||||
//! | 1 | 4 | Implicit ID (`u32`) |
|
||||
//! | 5 | 4 | URI length (`u32`) |
|
||||
//! | 9 | `uri_len` | URI bytes |
|
||||
//!
|
||||
//! ## Explicit ID
|
||||
//!
|
||||
//! | Offset | Size | Field |
|
||||
//! | -----------: | --------: | :------------------------- |
|
||||
//! | 0 | 1 | Kind, `2` |
|
||||
//! | 1 | 4 | Explicit ID length (`u32`) |
|
||||
//! | 5 | `id_len` | Explicit ID bytes |
|
||||
//! | `5 + id_len` | 4 | URI length (`u32`) |
|
||||
//! | `9 + id_len` | `uri_len` | URI bytes |
|
||||
//!
|
||||
//! Both variants have nine bytes of fixed overhead. The remaining bytes are
|
||||
//! the URI plus the explicit ID, when present.
|
||||
//!
|
||||
//! Decoding never allocates. The caller provides storage for the explicit ID
|
||||
//! and URI bytes, and the returned hyperlink borrows slices of that storage.
|
||||
|
||||
const std = @import("std");
|
||||
const io = @import("io.zig");
|
||||
const terminal_hyperlink = @import("../hyperlink.zig");
|
||||
|
||||
const Kind = enum(u8) {
|
||||
implicit = 1,
|
||||
explicit = 2,
|
||||
};
|
||||
|
||||
/// Errors possible while determining encoded string lengths.
|
||||
pub const StringBytesError = error{
|
||||
/// A string cannot fit its `u32` length or local size arithmetic overflowed.
|
||||
StringTooLong,
|
||||
};
|
||||
|
||||
/// Errors possible while encoding one hyperlink entry.
|
||||
pub const EncodeError = std.Io.Writer.Error || StringBytesError;
|
||||
|
||||
/// Errors possible while decoding one hyperlink entry.
|
||||
pub const DecodeError = std.Io.Reader.Error || error{
|
||||
/// The hyperlink kind is not defined by snapshot version 0.
|
||||
InvalidKind,
|
||||
|
||||
/// Caller-provided string storage cannot hold the decoded entry.
|
||||
BufferTooSmall,
|
||||
};
|
||||
|
||||
/// One decoded hyperlink and the number of caller-owned string bytes it uses.
|
||||
pub const Decoded = struct {
|
||||
value: terminal_hyperlink.Hyperlink,
|
||||
string_bytes: usize,
|
||||
};
|
||||
|
||||
/// Return the raw explicit-ID and URI bytes encoded by `value`.
|
||||
///
|
||||
/// Length prefixes and the numeric implicit ID are not included.
|
||||
pub fn stringBytes(
|
||||
value: terminal_hyperlink.Hyperlink,
|
||||
) StringBytesError!usize {
|
||||
_ = std.math.cast(u32, value.uri.len) orelse {
|
||||
return error.StringTooLong;
|
||||
};
|
||||
var result = value.uri.len;
|
||||
|
||||
switch (value.id) {
|
||||
.implicit => {},
|
||||
.explicit => |id| {
|
||||
_ = std.math.cast(u32, id.len) orelse {
|
||||
return error.StringTooLong;
|
||||
};
|
||||
result = std.math.add(usize, result, id.len) catch {
|
||||
return error.StringTooLong;
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Return the complete encoded length of one hyperlink entry.
|
||||
pub fn encodedLen(
|
||||
value: terminal_hyperlink.Hyperlink,
|
||||
) StringBytesError!usize {
|
||||
const string_bytes = try stringBytes(value);
|
||||
return std.math.add(usize, 9, string_bytes) catch {
|
||||
return error.StringTooLong;
|
||||
};
|
||||
}
|
||||
|
||||
/// Encode one hyperlink entry.
|
||||
pub fn encode(
|
||||
value: terminal_hyperlink.Hyperlink,
|
||||
writer: *std.Io.Writer,
|
||||
) EncodeError!void {
|
||||
// Validate every length before writing any part of the entry.
|
||||
_ = try stringBytes(value);
|
||||
|
||||
switch (value.id) {
|
||||
.implicit => |id| {
|
||||
try writer.writeByte(@intFromEnum(Kind.implicit));
|
||||
try io.writeInt(writer, u32, id);
|
||||
try io.writeInt(writer, u32, @intCast(value.uri.len));
|
||||
try writer.writeAll(value.uri);
|
||||
},
|
||||
.explicit => |id| {
|
||||
try writer.writeByte(@intFromEnum(Kind.explicit));
|
||||
try io.writeInt(writer, u32, @intCast(id.len));
|
||||
try writer.writeAll(id);
|
||||
try io.writeInt(writer, u32, @intCast(value.uri.len));
|
||||
try writer.writeAll(value.uri);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one hyperlink entry into caller-owned string storage.
|
||||
///
|
||||
/// The returned hyperlink remains valid only as long as `strings` remains
|
||||
/// valid and unmodified. `string_bytes` identifies the prefix of `strings`
|
||||
/// used by this entry.
|
||||
pub fn decode(
|
||||
reader: *std.Io.Reader,
|
||||
strings: []u8,
|
||||
) DecodeError!Decoded {
|
||||
const kind_raw = try reader.takeByte();
|
||||
const kind = std.enums.fromInt(Kind, kind_raw) orelse {
|
||||
return error.InvalidKind;
|
||||
};
|
||||
|
||||
return switch (kind) {
|
||||
.implicit => decodeImplicit(reader, strings),
|
||||
.explicit => decodeExplicit(reader, strings),
|
||||
};
|
||||
}
|
||||
|
||||
fn decodeImplicit(
|
||||
reader: *std.Io.Reader,
|
||||
strings: []u8,
|
||||
) DecodeError!Decoded {
|
||||
const id = try io.readInt(reader, u32);
|
||||
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
||||
if (uri_len > strings.len) return error.BufferTooSmall;
|
||||
|
||||
const uri = strings[0..uri_len];
|
||||
try reader.readSliceAll(uri);
|
||||
return .{
|
||||
.value = .{
|
||||
.id = .{ .implicit = id },
|
||||
.uri = uri,
|
||||
},
|
||||
.string_bytes = uri.len,
|
||||
};
|
||||
}
|
||||
|
||||
fn decodeExplicit(
|
||||
reader: *std.Io.Reader,
|
||||
strings: []u8,
|
||||
) DecodeError!Decoded {
|
||||
const id_len: usize = @intCast(try io.readInt(reader, u32));
|
||||
if (id_len > strings.len) return error.BufferTooSmall;
|
||||
|
||||
const id = strings[0..id_len];
|
||||
try reader.readSliceAll(id);
|
||||
|
||||
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
||||
const remaining = strings[id.len..];
|
||||
if (uri_len > remaining.len) return error.BufferTooSmall;
|
||||
|
||||
const uri = remaining[0..uri_len];
|
||||
try reader.readSliceAll(uri);
|
||||
return .{
|
||||
.value = .{
|
||||
.id = .{ .explicit = id },
|
||||
.uri = uri,
|
||||
},
|
||||
.string_bytes = id.len + uri.len,
|
||||
};
|
||||
}
|
||||
|
||||
test "golden implicit encoding" {
|
||||
const value: terminal_hyperlink.Hyperlink = .{
|
||||
.id = .{ .implicit = 0x01020304 },
|
||||
.uri = "uri",
|
||||
};
|
||||
|
||||
var buf: [encodedLen(value) catch unreachable]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(value, &writer);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
||||
writer.buffered(),
|
||||
);
|
||||
try std.testing.expectEqual(@as(usize, 3), try stringBytes(value));
|
||||
}
|
||||
|
||||
test "golden explicit encoding" {
|
||||
const value: terminal_hyperlink.Hyperlink = .{
|
||||
.id = .{ .explicit = "id" },
|
||||
.uri = "uri",
|
||||
};
|
||||
|
||||
var buf: [encodedLen(value) catch unreachable]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(value, &writer);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
||||
writer.buffered(),
|
||||
);
|
||||
try std.testing.expectEqual(@as(usize, 5), try stringBytes(value));
|
||||
}
|
||||
|
||||
test "empty strings round trip" {
|
||||
const values = .{
|
||||
terminal_hyperlink.Hyperlink{
|
||||
.id = .{ .implicit = 0 },
|
||||
.uri = "",
|
||||
},
|
||||
terminal_hyperlink.Hyperlink{
|
||||
.id = .{ .explicit = "" },
|
||||
.uri = "",
|
||||
},
|
||||
};
|
||||
|
||||
inline for (values) |value| {
|
||||
var encoded: [9]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try encode(value, &writer);
|
||||
|
||||
var reader: std.Io.Reader = .fixed(writer.buffered());
|
||||
var strings: [0]u8 = .{};
|
||||
const decoded = try decode(&reader, &strings);
|
||||
try std.testing.expectEqual(@as(usize, 0), decoded.string_bytes);
|
||||
try std.testing.expectEqualStrings("", decoded.value.uri);
|
||||
switch (value.id) {
|
||||
.implicit => |id| try std.testing.expectEqual(
|
||||
id,
|
||||
decoded.value.id.implicit,
|
||||
),
|
||||
.explicit => |id| try std.testing.expectEqualStrings(
|
||||
id,
|
||||
decoded.value.id.explicit,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "decode with a one-byte reader buffer" {
|
||||
const fixture =
|
||||
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri" ++
|
||||
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri";
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var read_buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &read_buf);
|
||||
|
||||
var implicit_strings: [3]u8 = undefined;
|
||||
const implicit = try decode(&limited.interface, &implicit_strings);
|
||||
try std.testing.expectEqual(@as(usize, 3), implicit.string_bytes);
|
||||
try std.testing.expectEqualStrings("uri", implicit.value.uri);
|
||||
try std.testing.expectEqual(
|
||||
@as(u32, 0x01020304),
|
||||
implicit.value.id.implicit,
|
||||
);
|
||||
|
||||
var explicit_strings: [5]u8 = undefined;
|
||||
const explicit = try decode(&limited.interface, &explicit_strings);
|
||||
try std.testing.expectEqual(@as(usize, 5), explicit.string_bytes);
|
||||
try std.testing.expectEqualStrings("id", explicit.value.id.explicit);
|
||||
try std.testing.expectEqualStrings("uri", explicit.value.uri);
|
||||
}
|
||||
|
||||
test "reject invalid kinds" {
|
||||
inline for (.{ 0, 3, std.math.maxInt(u8) }) |kind| {
|
||||
var fixture: [1]u8 = .{kind};
|
||||
var reader: std.Io.Reader = .fixed(&fixture);
|
||||
var strings: [0]u8 = .{};
|
||||
try std.testing.expectError(
|
||||
error.InvalidKind,
|
||||
decode(&reader, &strings),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "reject insufficient string storage" {
|
||||
{
|
||||
var reader: std.Io.Reader = .fixed(
|
||||
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
||||
);
|
||||
var strings: [2]u8 = undefined;
|
||||
try std.testing.expectError(
|
||||
error.BufferTooSmall,
|
||||
decode(&reader, &strings),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
var reader: std.Io.Reader = .fixed(
|
||||
"\x02\x03\x00\x00\x00id!\x03\x00\x00\x00uri",
|
||||
);
|
||||
var strings: [2]u8 = undefined;
|
||||
try std.testing.expectError(
|
||||
error.BufferTooSmall,
|
||||
decode(&reader, &strings),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
var reader: std.Io.Reader = .fixed(
|
||||
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
||||
);
|
||||
var strings: [4]u8 = undefined;
|
||||
try std.testing.expectError(
|
||||
error.BufferTooSmall,
|
||||
decode(&reader, &strings),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "reject every truncation" {
|
||||
const fixtures = .{
|
||||
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
||||
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
||||
};
|
||||
|
||||
inline for (fixtures) |fixture| {
|
||||
for (0..fixture.len) |fixture_len| {
|
||||
var reader: std.Io.Reader = .fixed(fixture[0..fixture_len]);
|
||||
var strings: [5]u8 = undefined;
|
||||
try std.testing.expectError(
|
||||
error.EndOfStream,
|
||||
decode(&reader, &strings),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
358
src/terminal/snapshot/style.zig
Normal file
358
src/terminal/snapshot/style.zig
Normal file
@@ -0,0 +1,358 @@
|
||||
//! Snapshot style entry encoding.
|
||||
//!
|
||||
//! Each entry describes one terminal style. A record can use these entries to
|
||||
//! build a style table and assign indexes according to that record's format.
|
||||
//! Indexing, ordering, etc. are properties of the containing record.
|
||||
//!
|
||||
//! Styles are encoded field by field from the terminal's native `Style` type.
|
||||
//! The native packed representation is deliberately not part of the snapshot
|
||||
//! format. This gives flexibility for changing one side or the other.
|
||||
//!
|
||||
//! All integers are unsigned and little-endian.
|
||||
//!
|
||||
//! | Offset | Size | Field |
|
||||
//! | -----: | ---: | :------------------------ |
|
||||
//! | 0 | 4 | Foreground color |
|
||||
//! | 4 | 4 | Background color |
|
||||
//! | 8 | 4 | Underline color |
|
||||
//! | 12 | 2 | Style flags (`u16`) |
|
||||
//! | 14 | 2 | Reserved, must be zero |
|
||||
//!
|
||||
//! The trailing reserved field is explicit wire padding that rounds each
|
||||
//! style entry from 14 bytes to 16 bytes. This makes entry offsets and byte
|
||||
//! counts straightforward.
|
||||
//!
|
||||
//! Each color begins with a one-byte kind followed by three data bytes:
|
||||
//!
|
||||
//! | Kind | Meaning | Data bytes |
|
||||
//! | ---: | :------ | :--------------------------------- |
|
||||
//! | 0 | None | All zero |
|
||||
//! | 1 | Palette | Palette index, then two zero bytes |
|
||||
//! | 2 | RGB | Red, green, blue |
|
||||
//!
|
||||
//! Style flag bits 0 through 7 are bold, italic, faint, blink, inverse,
|
||||
//! invisible, strikethrough, and overline. Bits 8 through 10 contain the
|
||||
//! underline kind. Bits 11 through 15 must be zero.
|
||||
//!
|
||||
//! | Underline | Meaning |
|
||||
//! | --------: | :------ |
|
||||
//! | 0 | None |
|
||||
//! | 1 | Single |
|
||||
//! | 2 | Double |
|
||||
//! | 3 | Curly |
|
||||
//! | 4 | Dotted |
|
||||
//! | 5 | Dashed |
|
||||
//!
|
||||
//! Underline values 6 and 7 are invalid in snapshot version 0.
|
||||
|
||||
const std = @import("std");
|
||||
const io = @import("io.zig");
|
||||
const sgr = @import("../sgr.zig");
|
||||
const terminal_style = @import("../style.zig");
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
const Flags = packed struct(u16) {
|
||||
bold: bool = false,
|
||||
italic: bool = false,
|
||||
faint: bool = false,
|
||||
blink: bool = false,
|
||||
inverse: bool = false,
|
||||
invisible: bool = false,
|
||||
strikethrough: bool = false,
|
||||
overline: bool = false,
|
||||
underline: u3 = 0,
|
||||
reserved: u5 = 0,
|
||||
};
|
||||
|
||||
const ColorKind = enum(u8) {
|
||||
none = 0,
|
||||
palette = 1,
|
||||
rgb = 2,
|
||||
};
|
||||
|
||||
/// Errors possible while decoding one style entry.
|
||||
pub const DecodeError = std.Io.Reader.Error || error{
|
||||
/// A color kind is not defined by snapshot version 0.
|
||||
InvalidColorKind,
|
||||
|
||||
/// The encoded underline kind is not defined by snapshot version 0.
|
||||
InvalidUnderline,
|
||||
|
||||
/// One or more reserved style flag bits are set.
|
||||
InvalidFlags,
|
||||
|
||||
/// The trailing reserved field is not zero.
|
||||
InvalidReserved,
|
||||
};
|
||||
|
||||
/// Encode one terminal style as a fixed-size snapshot style entry.
|
||||
pub fn encode(
|
||||
value: terminal_style.Style,
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
try encodeColor(value.fg_color, writer);
|
||||
try encodeColor(value.bg_color, writer);
|
||||
try encodeColor(value.underline_color, writer);
|
||||
|
||||
const flags: Flags = .{
|
||||
.bold = value.flags.bold,
|
||||
.italic = value.flags.italic,
|
||||
.faint = value.flags.faint,
|
||||
.blink = value.flags.blink,
|
||||
.inverse = value.flags.inverse,
|
||||
.invisible = value.flags.invisible,
|
||||
.strikethrough = value.flags.strikethrough,
|
||||
.overline = value.flags.overline,
|
||||
.underline = @intFromEnum(value.flags.underline),
|
||||
};
|
||||
try io.writeInt(writer, u16, @bitCast(flags));
|
||||
try io.writeInt(writer, u16, 0);
|
||||
}
|
||||
|
||||
/// Decode and validate one fixed-size snapshot style entry.
|
||||
pub fn decode(reader: *std.Io.Reader) DecodeError!terminal_style.Style {
|
||||
const fg_color = try decodeColor(reader);
|
||||
const bg_color = try decodeColor(reader);
|
||||
const underline_color = try decodeColor(reader);
|
||||
|
||||
const flags: Flags = @bitCast(try io.readInt(reader, u16));
|
||||
if (flags.reserved != 0) return error.InvalidFlags;
|
||||
|
||||
const underline = std.enums.fromInt(
|
||||
sgr.Attribute.Underline,
|
||||
flags.underline,
|
||||
) orelse return error.InvalidUnderline;
|
||||
|
||||
const reserved = try io.readInt(reader, u16);
|
||||
if (reserved != 0) return error.InvalidReserved;
|
||||
|
||||
return .{
|
||||
.fg_color = fg_color,
|
||||
.bg_color = bg_color,
|
||||
.underline_color = underline_color,
|
||||
.flags = .{
|
||||
.bold = flags.bold,
|
||||
.italic = flags.italic,
|
||||
.faint = flags.faint,
|
||||
.blink = flags.blink,
|
||||
.inverse = flags.inverse,
|
||||
.invisible = flags.invisible,
|
||||
.strikethrough = flags.strikethrough,
|
||||
.overline = flags.overline,
|
||||
.underline = underline,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn encodeColor(
|
||||
value: terminal_style.Style.Color,
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
var encoded: [4]u8 = @splat(0);
|
||||
switch (value) {
|
||||
.none => encoded[0] = @intFromEnum(ColorKind.none),
|
||||
.palette => |index| {
|
||||
encoded[0] = @intFromEnum(ColorKind.palette);
|
||||
encoded[1] = index;
|
||||
},
|
||||
.rgb => |rgb| {
|
||||
encoded[0] = @intFromEnum(ColorKind.rgb);
|
||||
encoded[1] = rgb.r;
|
||||
encoded[2] = rgb.g;
|
||||
encoded[3] = rgb.b;
|
||||
},
|
||||
}
|
||||
try writer.writeAll(&encoded);
|
||||
}
|
||||
|
||||
fn decodeColor(
|
||||
reader: *std.Io.Reader,
|
||||
) DecodeError!terminal_style.Style.Color {
|
||||
// Colors are always 4 bytes
|
||||
var encoded: [4]u8 = undefined;
|
||||
try reader.readSliceAll(&encoded);
|
||||
|
||||
// Kind must be something we know about.
|
||||
const kind = std.enums.fromInt(ColorKind, encoded[0]) orelse {
|
||||
return error.InvalidColorKind;
|
||||
};
|
||||
|
||||
return switch (kind) {
|
||||
.none => .none,
|
||||
.palette => .{ .palette = encoded[1] },
|
||||
.rgb => .{ .rgb = .{
|
||||
.r = encoded[1],
|
||||
.g = encoded[2],
|
||||
.b = encoded[3],
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
fn computeLen() usize {
|
||||
comptime {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
encode(.{}, &writer) catch unreachable;
|
||||
return writer.end;
|
||||
}
|
||||
}
|
||||
|
||||
test "golden encoding" {
|
||||
const value: terminal_style.Style = .{
|
||||
.fg_color = .none,
|
||||
.bg_color = .{ .palette = 0x7f },
|
||||
.underline_color = .{ .rgb = .{
|
||||
.r = 0x12,
|
||||
.g = 0x34,
|
||||
.b = 0x56,
|
||||
} },
|
||||
.flags = .{
|
||||
.bold = true,
|
||||
.italic = true,
|
||||
.faint = true,
|
||||
.blink = true,
|
||||
.inverse = true,
|
||||
.invisible = true,
|
||||
.strikethrough = true,
|
||||
.overline = true,
|
||||
.underline = .curly,
|
||||
},
|
||||
};
|
||||
|
||||
var buf: [len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(value, &writer);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"\x00\x00\x00\x00" ++
|
||||
"\x01\x7f\x00\x00" ++
|
||||
"\x02\x12\x34\x56" ++
|
||||
"\xff\x03\x00\x00",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "flag bit layout" {
|
||||
const cases = .{
|
||||
.{ Flags{ .bold = true }, @as(u16, 1 << 0) },
|
||||
.{ Flags{ .italic = true }, @as(u16, 1 << 1) },
|
||||
.{ Flags{ .faint = true }, @as(u16, 1 << 2) },
|
||||
.{ Flags{ .blink = true }, @as(u16, 1 << 3) },
|
||||
.{ Flags{ .inverse = true }, @as(u16, 1 << 4) },
|
||||
.{ Flags{ .invisible = true }, @as(u16, 1 << 5) },
|
||||
.{ Flags{ .strikethrough = true }, @as(u16, 1 << 6) },
|
||||
.{ Flags{ .overline = true }, @as(u16, 1 << 7) },
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.single) },
|
||||
@as(u16, 1 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.double) },
|
||||
@as(u16, 2 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.curly) },
|
||||
@as(u16, 3 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.dotted) },
|
||||
@as(u16, 4 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.dashed) },
|
||||
@as(u16, 5 << 8),
|
||||
},
|
||||
.{ Flags{ .reserved = 1 }, @as(u16, 1 << 11) },
|
||||
};
|
||||
|
||||
inline for (cases) |case| {
|
||||
try std.testing.expectEqual(case[1], @as(u16, @bitCast(case[0])));
|
||||
}
|
||||
}
|
||||
|
||||
test "decode with a one-byte reader buffer" {
|
||||
const fixture =
|
||||
"\x00\x00\x00\x00" ++
|
||||
"\x01\x7f\x00\x00" ++
|
||||
"\x02\x12\x34\x56" ++
|
||||
"\xff\x03\x00\x00";
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &buf);
|
||||
|
||||
const expected: terminal_style.Style = .{
|
||||
.fg_color = .none,
|
||||
.bg_color = .{ .palette = 0x7f },
|
||||
.underline_color = .{ .rgb = .{
|
||||
.r = 0x12,
|
||||
.g = 0x34,
|
||||
.b = 0x56,
|
||||
} },
|
||||
.flags = .{
|
||||
.bold = true,
|
||||
.italic = true,
|
||||
.faint = true,
|
||||
.blink = true,
|
||||
.inverse = true,
|
||||
.invisible = true,
|
||||
.strikethrough = true,
|
||||
.overline = true,
|
||||
.underline = .curly,
|
||||
},
|
||||
};
|
||||
const actual = try decode(&limited.interface);
|
||||
try std.testing.expect(expected.eql(actual));
|
||||
}
|
||||
|
||||
test "reject invalid color kinds" {
|
||||
inline for (.{ 0, 4, 8 }) |offset| {
|
||||
var invalid_kind: [len]u8 = @splat(0);
|
||||
invalid_kind[offset] = 3;
|
||||
var reader: std.Io.Reader = .fixed(&invalid_kind);
|
||||
try std.testing.expectError(error.InvalidColorKind, decode(&reader));
|
||||
}
|
||||
}
|
||||
|
||||
test "reject invalid flags and reserved field" {
|
||||
inline for (.{ 6, 7 }) |underline| {
|
||||
var invalid_underline: [len]u8 = @splat(0);
|
||||
std.mem.writeInt(
|
||||
u16,
|
||||
invalid_underline[12..14],
|
||||
underline << 8,
|
||||
.little,
|
||||
);
|
||||
var reader: std.Io.Reader = .fixed(&invalid_underline);
|
||||
try std.testing.expectError(error.InvalidUnderline, decode(&reader));
|
||||
}
|
||||
|
||||
var invalid_flags: [len]u8 = @splat(0);
|
||||
std.mem.writeInt(u16, invalid_flags[12..14], 1 << 11, .little);
|
||||
var flags_reader: std.Io.Reader = .fixed(&invalid_flags);
|
||||
try std.testing.expectError(error.InvalidFlags, decode(&flags_reader));
|
||||
|
||||
var invalid_reserved: [len]u8 = @splat(0);
|
||||
std.mem.writeInt(u16, invalid_reserved[14..16], 1, .little);
|
||||
var reserved_reader: std.Io.Reader = .fixed(&invalid_reserved);
|
||||
try std.testing.expectError(
|
||||
error.InvalidReserved,
|
||||
decode(&reserved_reader),
|
||||
);
|
||||
}
|
||||
|
||||
test "reject every truncation" {
|
||||
const fixture = [_]u8{0} ** len;
|
||||
for (0..len) |fixture_len| {
|
||||
var reader: std.Io.Reader = .fixed(fixture[0..fixture_len]);
|
||||
try std.testing.expectError(error.EndOfStream, decode(&reader));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user