From cc91940993e70e611ba5d8609ffecfcfa53ade39 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jun 2026 06:32:41 -0700 Subject: [PATCH] terminal/glyph: register request --- src/terminal/apc/glyph/Glossary.zig | 7 +- src/terminal/apc/glyph/execute.zig | 142 +++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 5 deletions(-) diff --git a/src/terminal/apc/glyph/Glossary.zig b/src/terminal/apc/glyph/Glossary.zig index 46a276a66..f2b102d8b 100644 --- a/src/terminal/apc/glyph/Glossary.zig +++ b/src/terminal/apc/glyph/Glossary.zig @@ -21,6 +21,9 @@ pub const max_entries = 1024; /// An empty glossary with no registered glyphs. pub const empty: Glossary = .{ .entries = .empty }; +/// Errors that can occur while registering a glossary entry. +pub const RegisterError = Allocator.Error || error{OutOfNamespace}; + /// The set of entries in the glossary keyed by the codepoint. /// /// The array hash map preserves insertion order and has O(N) @@ -49,7 +52,7 @@ pub fn register( alloc: Allocator, cp: u21, entry: Entry, -) (Allocator.Error || error{OutOfNamespace})!void { +) RegisterError!void { // Validate codepoint according to spec. if (!isPrivateUse(cp)) return error.OutOfNamespace; @@ -143,7 +146,7 @@ pub const Entry = struct { /// decodes the base64 glyph payload, and stores the decoded outline. The /// returned entry owns decoded glyph memory and must be released with /// `deinit`. - pub fn init(alloc: Allocator, req: RegisterReq) InitError!Entry { + pub fn init(alloc: Allocator, req: RegisterReq) Entry.InitError!Entry { // Validate format const fmt = req.get(.fmt) orelse return error.InvalidOptions; const design: glyf_rasterize.DesignMetrics = .{ diff --git a/src/terminal/apc/glyph/execute.zig b/src/terminal/apc/glyph/execute.zig index 6ed4cd793..6a695383b 100644 --- a/src/terminal/apc/glyph/execute.zig +++ b/src/terminal/apc/glyph/execute.zig @@ -28,11 +28,147 @@ pub fn execute( glossary: *Glossary, req: *const Request, ) ?Response { - _ = alloc; - _ = glossary; log.debug("executing glyph protocol request: {t}", .{req.*}); return switch (req.*) { .support => .{ .support = .{ .fmt = supported_formats } }, - .query, .register, .clear => @panic("TODO"), + .register => |reg| register(alloc, glossary, reg), + .query, .clear => @panic("TODO"), }; } + +fn register( + alloc: Allocator, + glossary: *Glossary, + reg: Request.Register, +) ?Response { + const reply = reg.get(.reply) orelse .all; + const cp = registerFallible(alloc, glossary, reg) catch |err| return switch (reply) { + .none => null, + .all, .failures => .{ .register = .{ + .cp = reg.get(.cp) orelse 0, + .status = .err, + .reason = switch (err) { + error.OutOfMemory => .{ .other = "out_of_memory" }, + error.OutOfNamespace => .out_of_namespace, + error.PayloadTooLarge => .payload_too_large, + error.MalformedPayload => .malformed_payload, + error.CompositeUnsupported => .composite_unsupported, + error.HintingUnsupported => .hinting_unsupported, + error.InvalidOptions, + error.UnsupportedFormat, + => .malformed_payload, + }, + } }, + }; + + return switch (reply) { + .none, .failures => null, + .all => .{ .register = .{ .cp = cp } }, + }; +} + +fn registerFallible( + alloc: Allocator, + glossary: *Glossary, + reg: Request.Register, +) (Glossary.Entry.InitError || Glossary.RegisterError)!u21 { + const cp = reg.get(.cp) orelse + return error.MalformedPayload; + + var entry = try Glossary.Entry.init(alloc, reg); + errdefer entry.deinit(alloc); + + try glossary.register(alloc, cp, entry); + return cp; +} + +fn testParse(alloc: Allocator, data: []const u8) !Request { + var parser = request.CommandParser.init(alloc, 1024 * 1024); + defer parser.deinit(); + for (data) |byte| try parser.feed(byte); + return try parser.complete(alloc); +} + +test "execute register stores glyph and returns success" { + const testing = std.testing; + const alloc = testing.allocator; + + var glossary: Glossary = .empty; + defer glossary.deinit(alloc); + + var req = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA=="); + defer req.deinit(alloc); + + try testing.expectEqual(Response{ + .register = .{ .cp = 0xE0A0 }, + }, execute(alloc, &glossary, &req).?); + try testing.expect(glossary.contains(0xE0A0)); +} + +test "execute register reply failures suppresses success" { + const testing = std.testing; + const alloc = testing.allocator; + + var glossary: Glossary = .empty; + defer glossary.deinit(alloc); + + var req = try testParse(alloc, "r;cp=e0a0;reply=2;AAAAAAAAAAAAAA=="); + defer req.deinit(alloc); + + try testing.expect(execute(alloc, &glossary, &req) == null); + try testing.expect(glossary.contains(0xE0A0)); +} + +test "execute register reply none suppresses failure" { + const testing = std.testing; + const alloc = testing.allocator; + + var glossary: Glossary = .empty; + defer glossary.deinit(alloc); + + var req = try testParse(alloc, "r;cp=41;reply=0;%%%not-base64%%%"); + defer req.deinit(alloc); + + try testing.expect(execute(alloc, &glossary, &req) == null); + try testing.expect(!glossary.contains('A')); +} + +test "execute register rejects non-PUA" { + const testing = std.testing; + const alloc = testing.allocator; + + var glossary: Glossary = .empty; + defer glossary.deinit(alloc); + + var req = try testParse(alloc, "r;cp=41;AAAAAAAAAAAAAA=="); + defer req.deinit(alloc); + + try testing.expectEqual(Response{ + .register = .{ + .cp = 'A', + .status = .err, + .reason = .out_of_namespace, + }, + }, execute(alloc, &glossary, &req).?); + try testing.expect(!glossary.contains('A')); +} + +test "execute register reports malformed payload" { + const testing = std.testing; + const alloc = testing.allocator; + + var glossary: Glossary = .empty; + defer glossary.deinit(alloc); + + var req = try testParse(alloc, "r;cp=e0a0;%%%not-base64%%%"); + defer req.deinit(alloc); + + try testing.expectEqual(Response{ + .register = .{ + .cp = 0xE0A0, + .status = .err, + .reason = .malformed_payload, + }, + }, execute(alloc, &glossary, &req).?); + try testing.expect(!glossary.contains(0xE0A0)); +}