terminal: glyph request glyf decode

This commit is contained in:
Mitchell Hashimoto
2026-06-01 13:13:33 -07:00
parent 8fcead00e5
commit 2055bb6dd6
2 changed files with 95 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# Glyph Protocol
- The specification source of truth is:
<https://github.com/raphamorim/rio/blob/main/specs/glyph-protocol.md>
- A summary of the specification is available in
`src/terminal/apc/glyph.zig` at the top.
- Reference the specification whenever any changes are made to
this folder. Prefer the local specification over fetching the
latest, unless it is lacking information.

View File

@@ -1,6 +1,11 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Glyf = @import("../../../font/opentype/glyf.zig").Glyf;
/// Maximum decoded glyph payload size accepted by the protocol.
/// This is documented in the spec.
const max_payload_size = 64 * 1024; // 64 KiB
/// Stateful parser for a single glyph APC payload after the `25a1;` prefix.
pub const CommandParser = struct {
@@ -242,6 +247,65 @@ pub const Request = union(enum) {
self.raw[self.payload_idx + 1 ..];
}
/// Errors that can occur while decoding a register glyph payload.
pub const DecodeError = Allocator.Error || error{
/// The decoded payload exceeds the protocol limit.
PayloadTooLarge,
/// The payload could not be decoded or parsed as the declared format.
MalformedPayload,
/// The glyf payload is composite, which the protocol forbids.
CompositeUnsupported,
/// The glyf payload contains hinting instructions, which the
/// protocol forbids.
HintingUnsupported,
};
/// Decode this request's base64 glyf payload into an owned outline.
pub fn decodeGlyfPayload(self: Register, alloc: Allocator) DecodeError!Glyf.Outline {
// Prep base64 decoding, initial validation.
const Decoder = std.base64.standard.Decoder;
const payload_bytes = self.payload();
const size = Decoder.calcSizeForSlice(payload_bytes) catch
return error.MalformedPayload;
if (size > max_payload_size) return error.PayloadTooLarge;
// Max payload size is reasonable for stack and its likely
// we'll have stack space. We don't use much stack space in
// the future function calls either, so try a stack allocator
// here and fallback to heap as necessary.
var data_stack = std.heap.stackFallback(
max_payload_size,
alloc,
);
const data_alloc = data_stack.get();
const data = try data_alloc.alloc(u8, size);
defer data_alloc.free(data);
// Base64 decode
Decoder.decode(data, payload_bytes) catch
return error.MalformedPayload;
// Glyf.Entry borrows from `data`, but only for the duration of the
// decode call below. Glyf.Entry.decode returns an owned Outline, so
// it is safe to free `data` before returning that outline.
const glyf_entry = Glyf.Entry.init(data) catch return error.MalformedPayload;
return glyf_entry.decode(alloc) catch |err| switch (err) {
error.OutOfMemory => error.OutOfMemory,
// Unsupported fields
error.CompositeNotSupported => error.CompositeUnsupported,
error.InstructionsNotSupported => error.HintingUnsupported,
// Various semantic issues
error.EndOfStream,
error.EndPointsOutOfOrder,
error.TooManyPoints,
error.CoordinateOverflow,
=> error.MalformedPayload,
};
}
/// Return the raw option portion of a valid register command.
fn rawOptions(self: Register) []const u8 {
assert(self.raw.len >= 2);
@@ -690,6 +754,28 @@ test "register command with invalid payload" {
try testing.expectEqualStrings("%%%not-base64%%%", cmd.register.payload());
}
test "register decodes glyf payload" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "r;cp=e0a0;AAAAAAAAAAAAAA==");
defer cmd.deinit(testing.allocator);
var outline = try cmd.register.decodeGlyfPayload(testing.allocator);
defer outline.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), outline.points.len);
try testing.expectEqual(@as(usize, 0), outline.contours.len);
}
test "register rejects malformed glyf payload" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "r;cp=e0a0;%%%not-base64%%%");
defer cmd.deinit(testing.allocator);
try testing.expectError(error.MalformedPayload, cmd.register.decodeGlyfPayload(testing.allocator));
}
test "register response without payload" {
const testing = std.testing;