terminal/glyph: register request

This commit is contained in:
Mitchell Hashimoto
2026-06-05 06:32:41 -07:00
parent 0cd815f94a
commit cc91940993
2 changed files with 144 additions and 5 deletions

View File

@@ -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 = .{

View File

@@ -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));
}