mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-03 06:09:04 +00:00
361 lines
11 KiB
Zig
361 lines
11 KiB
Zig
//! 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.
|
|
//!
|
|
//! The standalone decoder returns an allocator-owned hyperlink. PAGE decoding
|
|
//! reads strings directly into the destination page's allocator instead.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const io = @import("io.zig");
|
|
const terminal_hyperlink = @import("../hyperlink.zig");
|
|
const terminal_page = @import("../page.zig");
|
|
const terminal_size = @import("../size.zig");
|
|
|
|
const Kind = enum(u8) {
|
|
implicit = 1,
|
|
explicit = 2,
|
|
};
|
|
|
|
/// Errors possible while encoding one hyperlink entry.
|
|
pub const EncodeError = std.Io.Writer.Error;
|
|
|
|
/// Errors possible while decoding one allocator-owned hyperlink entry.
|
|
pub const DecodeError = std.Io.Reader.Error || Allocator.Error || error{
|
|
/// The hyperlink kind is not defined by snapshot version 1.
|
|
InvalidKind,
|
|
};
|
|
|
|
/// Errors possible while decoding directly into a native page.
|
|
pub const DecodePageError = std.Io.Reader.Error ||
|
|
terminal_page.Page.InsertHyperlinkError ||
|
|
error{
|
|
/// The hyperlink kind is not defined by snapshot version 1.
|
|
InvalidKind,
|
|
|
|
/// The hyperlink value already exists in the page.
|
|
DuplicateHyperlink,
|
|
};
|
|
|
|
/// Encode one hyperlink entry.
|
|
pub fn encode(
|
|
value: terminal_hyperlink.Hyperlink,
|
|
writer: *std.Io.Writer,
|
|
) EncodeError!void {
|
|
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 allocator-owned hyperlink entry.
|
|
///
|
|
/// The caller owns the returned value and must call `Hyperlink.deinit`.
|
|
pub fn decode(
|
|
reader: *std.Io.Reader,
|
|
alloc: Allocator,
|
|
) DecodeError!terminal_hyperlink.Hyperlink {
|
|
const kind_raw = try reader.takeByte();
|
|
const kind = std.enums.fromInt(Kind, kind_raw) orelse
|
|
return error.InvalidKind;
|
|
|
|
return switch (kind) {
|
|
.implicit => implicit: {
|
|
const id = try io.readInt(reader, u32);
|
|
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const uri = try alloc.alloc(u8, uri_len);
|
|
errdefer alloc.free(uri);
|
|
try reader.readSliceAll(uri);
|
|
|
|
break :implicit .{
|
|
.id = .{ .implicit = id },
|
|
.uri = uri,
|
|
};
|
|
},
|
|
|
|
.explicit => explicit: {
|
|
const id_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const id = try alloc.alloc(u8, id_len);
|
|
errdefer alloc.free(id);
|
|
try reader.readSliceAll(id);
|
|
|
|
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const uri = try alloc.alloc(u8, uri_len);
|
|
errdefer alloc.free(uri);
|
|
try reader.readSliceAll(uri);
|
|
|
|
break :explicit .{
|
|
.id = .{ .explicit = id },
|
|
.uri = uri,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Decode one hyperlink directly into page-owned storage.
|
|
///
|
|
/// 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 ID assigned by the destination page.
|
|
pub fn decodePage(
|
|
page: *terminal_page.Page,
|
|
reader: *std.Io.Reader,
|
|
) DecodePageError!terminal_hyperlink.Id {
|
|
const kind_raw = try reader.takeByte();
|
|
const kind = std.enums.fromInt(Kind, kind_raw) orelse
|
|
return error.InvalidKind;
|
|
|
|
const entry: terminal_hyperlink.PageEntry = switch (kind) {
|
|
.implicit => implicit: {
|
|
const id = try io.readInt(reader, u32);
|
|
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const uri = try decodePageString(
|
|
page,
|
|
reader,
|
|
uri_len,
|
|
);
|
|
|
|
break :implicit .{
|
|
.id = .{ .implicit = id },
|
|
.uri = uri,
|
|
};
|
|
},
|
|
|
|
.explicit => explicit: {
|
|
const id_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const id = try decodePageString(
|
|
page,
|
|
reader,
|
|
id_len,
|
|
);
|
|
errdefer if (id.len > 0) page.string_alloc.free(
|
|
page.memory,
|
|
id.slice(page.memory),
|
|
);
|
|
|
|
const uri_len: usize = @intCast(try io.readInt(reader, u32));
|
|
const uri = try decodePageString(
|
|
page,
|
|
reader,
|
|
uri_len,
|
|
);
|
|
|
|
break :explicit .{
|
|
.id = .{ .explicit = id },
|
|
.uri = uri,
|
|
};
|
|
},
|
|
};
|
|
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,
|
|
.{ .page = page },
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => error.SetOutOfMemory,
|
|
error.NeedsRehash => error.SetNeedsRehash,
|
|
};
|
|
}
|
|
|
|
fn decodePageString(
|
|
page: *terminal_page.Page,
|
|
reader: *std.Io.Reader,
|
|
len: usize,
|
|
) (std.Io.Reader.Error || error{StringsOutOfMemory})!terminal_size.Offset(u8).Slice {
|
|
if (len == 0) return .{};
|
|
|
|
// Allocate space for the string and read directly into it.
|
|
const value = page.string_alloc.alloc(
|
|
u8,
|
|
page.memory,
|
|
len,
|
|
) catch return error.StringsOutOfMemory;
|
|
errdefer page.string_alloc.free(page.memory, value);
|
|
try reader.readSliceAll(value);
|
|
|
|
return .{
|
|
.len = value.len,
|
|
.offset = terminal_size.getOffset(
|
|
u8,
|
|
page.memory,
|
|
&value[0],
|
|
),
|
|
};
|
|
}
|
|
|
|
test "golden implicit encoding" {
|
|
const value: terminal_hyperlink.Hyperlink = .{
|
|
.id = .{ .implicit = 0x01020304 },
|
|
.uri = "uri",
|
|
};
|
|
|
|
var buf: [128]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(),
|
|
);
|
|
}
|
|
|
|
test "golden explicit encoding" {
|
|
const value: terminal_hyperlink.Hyperlink = .{
|
|
.id = .{ .explicit = "id" },
|
|
.uri = "uri",
|
|
};
|
|
|
|
var buf: [128]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(),
|
|
);
|
|
}
|
|
|
|
test "empty strings round trip" {
|
|
const values: [2]terminal_hyperlink.Hyperlink = .{
|
|
.{
|
|
.id = .{ .implicit = 0 },
|
|
.uri = "",
|
|
},
|
|
.{
|
|
.id = .{ .explicit = "" },
|
|
.uri = "",
|
|
},
|
|
};
|
|
|
|
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());
|
|
const decoded = try decode(&reader, std.testing.allocator);
|
|
defer decoded.deinit(std.testing.allocator);
|
|
try std.testing.expectEqualStrings("", decoded.uri);
|
|
switch (value.id) {
|
|
.implicit => |id| try std.testing.expectEqual(
|
|
id,
|
|
decoded.id.implicit,
|
|
),
|
|
.explicit => |id| try std.testing.expectEqualStrings(
|
|
id,
|
|
decoded.id.explicit,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
try std.testing.expectError(
|
|
error.InvalidKind,
|
|
decode(&reader, std.testing.allocator),
|
|
);
|
|
}
|
|
}
|
|
|
|
test "decode allocation failure" {
|
|
{
|
|
var reader: std.Io.Reader = .fixed(
|
|
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
|
);
|
|
var failing = std.testing.FailingAllocator.init(
|
|
std.testing.allocator,
|
|
.{ .fail_index = 0 },
|
|
);
|
|
try std.testing.expectError(
|
|
error.OutOfMemory,
|
|
decode(&reader, failing.allocator()),
|
|
);
|
|
}
|
|
|
|
for (0..2) |fail_index| {
|
|
var failing = std.testing.FailingAllocator.init(
|
|
std.testing.allocator,
|
|
.{ .fail_index = fail_index },
|
|
);
|
|
var reader: std.Io.Reader = .fixed(
|
|
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
|
);
|
|
try std.testing.expectError(
|
|
error.OutOfMemory,
|
|
decode(&reader, failing.allocator()),
|
|
);
|
|
}
|
|
}
|
|
|
|
test "reject every truncation" {
|
|
const fixtures: [2][]const u8 = .{
|
|
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
|
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
|
};
|
|
|
|
for (fixtures) |fixture| {
|
|
for (0..fixture.len) |fixture_len| {
|
|
var reader: std.Io.Reader = .fixed(fixture[0..fixture_len]);
|
|
try std.testing.expectError(
|
|
error.EndOfStream,
|
|
decode(&reader, std.testing.allocator),
|
|
);
|
|
}
|
|
}
|
|
}
|