const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const LimitedAllocator = @import("../../../datastruct/main.zig").LimitedAllocator; const Glyf = @import("../../../font/opentype/glyf.zig").Glyf; const fraction = @import("../../fraction.zig"); /// 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 { alloc: Allocator, data: std.ArrayList(u8) = .empty, /// Maximum bytes the data payload can buffer. This is to prevent /// malicious input from causing us to allocate too much memory. max_bytes: usize, pub const Error = Allocator.Error || error{InvalidFormat}; /// Create a glyph APC parser that buffers the raw command bytes. pub fn init(alloc: Allocator, max_bytes: usize) CommandParser { return .{ .alloc = alloc, .max_bytes = max_bytes }; } /// Release any buffered command bytes owned by the parser. pub fn deinit(self: *CommandParser) void { self.data.deinit(self.alloc); } /// Append one more byte of APC payload to the buffered command. pub fn feed(self: *CommandParser, byte: u8) Allocator.Error!void { if (self.data.items.len >= self.max_bytes) return error.OutOfMemory; try self.data.append(self.alloc, byte); } /// Append a slice of APC payload bytes to the buffered command. /// Equivalent to calling feed for each byte, but appends in bulk. pub fn feedSlice(self: *CommandParser, bytes: []const u8) Allocator.Error!void { if (self.data.items.len + bytes.len > self.max_bytes) return error.OutOfMemory; try self.data.appendSlice(self.alloc, bytes); } /// Finish parsing and return an owned request that can outlive the parser. pub fn complete(self: *CommandParser, alloc: Allocator) Error!Request { // Normalize bare single-byte verbs like `s` into `s;` so the parsed // command always has the standard `verb;...` layout. if (self.data.items.len == 1) try self.data.append(self.alloc, ';'); const raw = try self.data.toOwnedSlice(alloc); // Ownership of the buffered bytes has moved to `raw`, so clear the // array list before we build the final command value. self.data = .empty; errdefer alloc.free(raw); return try Request.parse(alloc, raw); } }; /// Parsed glyph APC request with the verb classified eagerly. pub const Request = union(enum) { /// Support query (bare `s` verb, no options). support, /// Codepoint coverage query. query: Query, /// Glyph registration request. register: Register, /// Registration clear request. clear: Clear, /// Query verb payload with lazily-decoded options. pub const Query = struct { raw: []const u8, /// Initialize a query command from owned raw command bytes. pub fn init(raw: []const u8) Query { return .{ .raw = raw }; } /// Options recognized for the glyph query request. pub const Option = enum { /// Target Unicode codepoint encoded in hexadecimal. cp, /// Return the decoded Zig type for a query option. pub fn Type(comptime self: Option) type { return switch (self) { .cp => u21, }; } /// Return the wire-format option key for this query option. fn key(comptime self: Option) []const u8 { return @tagName(self); } /// Read and decode a query option from the raw option string. pub fn read(comptime self: Option, raw: []const u8) ?self.Type() { const value = optionValue(raw, self.key()) orelse return null; return switch (self) { .cp => std.fmt.parseInt(u21, value, 16) catch null, }; } }; /// Lazily decode a query option on demand. pub fn get(self: Query, comptime option: Option) ?option.Type() { return option.read(self.rawOptions()); } /// Return the raw option portion of a valid query command. fn rawOptions(self: Query) []const u8 { assert(self.raw.len >= 2); assert(self.raw[0] == 'q'); assert(self.raw[1] == ';'); return self.raw[2..]; } }; /// Register verb payload with lazily-decoded options and optional base64 data. pub const Register = struct { raw: []const u8, payload_idx: usize, /// Initialize a register command from owned raw command bytes. pub fn init(raw: []const u8) ?Register { if (raw.len < 2) return null; if (raw[0] != 'r') return null; if (raw[1] != ';') return null; const payload_idx = std.mem.lastIndexOfScalar(u8, raw, ';') orelse return null; if (payload_idx <= 1) return null; return .{ .raw = raw, .payload_idx = payload_idx, }; } /// Options recognized for the glyph register verb. pub const Option = enum { /// Target Unicode codepoint encoded in hexadecimal. cp, /// Glyph payload format. fmt, /// Requested reply verbosity for registration. reply, /// Units-per-em for the glyph coordinate system. upm, /// Authored advance width in units-per-em units. aw, /// Authored line height in units-per-em units. lh, /// Unicode cell width for terminal layout. width, /// Glyph scale policy. size, /// Glyph placement within the render span. @"align", /// Fractional insets from the render span edges. pad, /// Return the decoded Zig type for a register option. pub fn Type(comptime self: Option) type { return switch (self) { .cp => u21, .fmt => Format, .reply => Reply, .upm => u32, .aw => u32, .lh => u32, .width => Width, .size => Size, .@"align" => Align, .pad => Pad, }; } /// Return the protocol default value for this option, if any. pub fn default(comptime self: Option) ?self.Type() { return switch (self) { .cp => null, .fmt => .glyf, .reply => .all, .upm => 1000, .aw => null, .lh => null, .width => .narrow, .size => .height, .@"align" => .{}, .pad => .{}, }; } /// Return the wire-format option key for this register option. fn key(comptime self: Option) []const u8 { return @tagName(self); } /// Read and decode a register option from the raw option string. pub fn read(comptime self: Option, raw: []const u8) ?self.Type() { const value = optionValue(raw, self.key()) orelse return null; return switch (self) { .cp => std.fmt.parseInt(u21, value, 16) catch null, .fmt => Format.init(value), .reply => Reply.init(value) orelse .all, .upm => std.fmt.parseInt(u32, value, 10) catch null, .aw => std.fmt.parseInt(u32, value, 10) catch null, .lh => std.fmt.parseInt(u32, value, 10) catch null, .width => Width.init(value), .size => Size.init(value), .@"align" => Align.init(value), .pad => Pad.init(value), }; } }; /// Lazily decode a register option on demand, applying protocol /// defaults when the option is omitted. pub fn get(self: Register, comptime option: Option) ?option.Type() { const raw = self.rawOptions(); if (optionValue(raw, option.key()) == null) { return switch (option) { .aw, .lh => self.get(.upm), else => option.default(), }; } return option.read(raw); } /// Return the base64 payload carried by a register request. /// /// If no payload is present, this returns an empty slice. The returned /// bytes may still be invalid base64; this function only exposes the raw /// payload segment and does not validate or decode it. pub fn payload(self: Register) []const u8 { assert(self.raw.len >= 2); assert(self.raw[0] == 'r'); assert(self.raw[1] == ';'); return if (self.payload_idx == self.raw.len) "" else 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, max_allocation_bytes: usize, ) 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; // The points slice is the dominant decoded allocation. Limiting // every allocation made by the decoder prevents a compact record // from expanding into an unbounded persistent outline. var limited: LimitedAllocator = .init(alloc, max_allocation_bytes); return glyf_entry.decode(limited.allocator()) catch |err| switch (err) { error.OutOfMemory => if (limited.limit_exceeded) error.PayloadTooLarge else error.OutOfMemory, // Unsupported fields error.CompositeNotSupported => error.CompositeUnsupported, error.InstructionsNotSupported => error.HintingUnsupported, // Various semantic issues error.ReadFailed, 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); assert(self.raw[0] == 'r'); assert(self.raw[1] == ';'); assert(self.payload_idx >= 2); assert(self.payload_idx <= self.raw.len); return self.raw[2..self.payload_idx]; } }; /// Clear verb payload with lazily-decoded options. pub const Clear = struct { raw: []const u8, /// Initialize a clear command from owned raw command bytes. pub fn init(raw: []const u8) Clear { return .{ .raw = raw }; } /// Options recognized for the glyph clear request. pub const Option = enum { /// Target Unicode codepoint encoded in hexadecimal. cp, /// Return the decoded Zig type for a clear option. pub fn Type(comptime self: Option) type { return switch (self) { .cp => u21, }; } /// Return the wire-format option key for this clear option. fn key(comptime self: Option) []const u8 { return @tagName(self); } /// Read and decode a clear option from the raw option string. pub fn read(comptime self: Option, raw: []const u8) ?self.Type() { const value = optionValue(raw, self.key()) orelse return null; return switch (self) { .cp => std.fmt.parseInt(u21, value, 16) catch null, }; } /// Return whether the option is present in the raw option string, /// independent of whether its value can be decoded. pub fn present(comptime self: Option, raw: []const u8) bool { return optionValue(raw, self.key()) != null; } }; /// Lazily decode a clear option on demand. pub fn get(self: Clear, comptime option: Option) ?option.Type() { return option.read(self.rawOptions()); } /// Return whether a clear option was provided, even if malformed. pub fn has(self: Clear, comptime option: Option) bool { return option.present(self.rawOptions()); } /// Return the raw option portion of a valid clear command. fn rawOptions(self: Clear) []const u8 { assert(self.raw.len >= 2); assert(self.raw[0] == 'c'); assert(self.raw[1] == ';'); return self.raw[2..]; } }; /// Parse an owned glyph APC payload into its eagerly-classified request /// form. /// /// The raw format here is strict on its requirements to avoid /// edge cases: it must contain the request AND the request must /// end in a semicolon (even if there are no options). The spec itself /// does not require this but we artificially insert it in our parser /// to simplify parsing later. pub fn parse(alloc: Allocator, raw: []const u8) error{InvalidFormat}!Request { if (raw.len < 2) return error.InvalidFormat; if (raw[1] != ';') return error.InvalidFormat; return switch (raw[0]) { 's' => { alloc.free(raw); return .support; }, 'q' => .{ .query = .init(raw) }, 'r' => .{ .register = Register.init(raw) orelse return error.InvalidFormat }, 'c' => .{ .clear = .init(raw) }, else => error.InvalidFormat, }; } /// Free the raw bytes retained by any request variant. pub fn deinit(self: *Request, alloc: Allocator) void { switch (self.*) { .support => {}, inline else => |*cmd| if (cmd.raw.len > 0) alloc.free(cmd.raw), } } }; /// Glyph payload formats named by the protocol. pub const Format = enum { /// TrueType simple glyph outline data. glyf, /// OpenType COLR version 0 layered color glyph data. colrv0, /// OpenType COLR version 1 paint graph glyph data. colrv1, /// Parse a glyph payload format name. pub fn init(value: []const u8) ?Format { return std.meta.stringToEnum(Format, value); } }; /// Register command reply verbosity. pub const Reply = enum(u2) { /// Suppress both success and failure replies. none = 0, /// Emit replies for both success and failure cases. all = 1, /// Emit replies only for failure cases. failures = 2, /// Parse the register command reply mode from its single-digit encoding. pub fn init(value: []const u8) ?Reply { if (value.len != 1) return null; return switch (value[0]) { '0' => .none, '1' => .all, '2' => .failures, else => null, }; } }; /// Register command width override for terminal layout. pub const Width = enum(u2) { /// One terminal cell. narrow = 1, /// Two terminal cells. wide = 2, /// Parse the register command width from its single-digit encoding. pub fn init(value: []const u8) ?Width { if (value.len != 1) return null; return switch (value[0]) { '1' => .narrow, '2' => .wide, else => null, }; } }; /// Register command glyph scale policy. pub const Size = enum { height, advance, contain, cover, stretch, /// Parse a glyph scale policy name. pub fn init(value: []const u8) ?Size { return std.meta.stringToEnum(Size, value); } }; /// Register command glyph placement within the render span. pub const Align = struct { horizontal: Horizontal = .center, vertical: Vertical = .center, pub const Horizontal = enum { start, center, end, fn init(value: []const u8) ?Horizontal { return std.meta.stringToEnum(Horizontal, value); } }; pub const Vertical = enum { start, center, end, baseline, fn init(value: []const u8) ?Vertical { return std.meta.stringToEnum(Vertical, value); } }; /// Parse an align value in `,` form. pub fn init(value: []const u8) ?Align { var it = std.mem.splitScalar(u8, value, ','); const horizontal = Horizontal.init(it.next() orelse return null) orelse return null; const vertical = Vertical.init(it.next() orelse return null) orelse return null; if (it.next() != null) return null; return .{ .horizontal = horizontal, .vertical = vertical, }; } }; /// Register command fractional insets from the render span edges. pub const Pad = struct { top: f64 = 0, right: f64 = 0, bottom: f64 = 0, left: f64 = 0, /// Parse a pad value in `,,,` form. pub fn init(value: []const u8) ?Pad { var it = std.mem.splitScalar(u8, value, ','); const top = parseFraction(it.next() orelse return null) orelse return null; const right = parseFraction(it.next() orelse return null) orelse return null; const bottom = parseFraction(it.next() orelse return null) orelse return null; const left = parseFraction(it.next() orelse return null) orelse return null; if (it.next() != null) return null; // Glyph Protocol §8.5.2: "If `l + r ≥ 1` or `t + b ≥ 1` // the terminal MUST treat the request as if `pad=0,0,0,0`." if (left + right >= 1 or top + bottom >= 1) return .{}; return .{ .top = top, .right = right, .bottom = bottom, .left = left, }; } /// Parse one pad component from the spec's `0.0`–`1.0` fractional range. /// Top/bottom fractions are relative to cell height; left/right fractions /// are relative to render span width. fn parseFraction(value: []const u8) ?f64 { return fraction.parse(value); } }; /// Find the last occurrence of `key=value` for a lazily-parsed option list. fn optionValue(raw: []const u8, comptime key: []const u8) ?[]const u8 { var remaining = raw; var result: ?[]const u8 = null; while (remaining.len > 0) { // Options are semicolon-delimited, so each loop peels off one segment // and checks whether it matches the requested key. const len = std.mem.indexOfScalar(u8, remaining, ';') orelse remaining.len; const full = remaining[0..len]; if (std.mem.indexOfScalar(u8, full, '=')) |eql_idx| { if (std.mem.eql(u8, full[0..eql_idx], key)) { result = full[eql_idx + 1 ..]; } } if (len == remaining.len) break; remaining = remaining[len + 1 ..]; } return result; } fn testParse(alloc: Allocator, data: []const u8) CommandParser.Error!Request { var parser = CommandParser.init(alloc, 1024 * 1024); defer parser.deinit(); for (data) |byte| try parser.feed(byte); return try parser.complete(alloc); } test "support command" { const testing = std.testing; var cmd = try testParse(testing.allocator, "s"); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .support); } test "query command" { const testing = std.testing; var cmd = try testParse(testing.allocator, "q;cp=E0A0"); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .query); try testing.expectEqual(@as(u21, 0xE0A0), cmd.query.get(.cp).?); } test "register command with payload" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=e0a0;fmt=glyf;upm=1000;reply=2;QQ==", ); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?); try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?); try testing.expectEqual(@as(u32, 1000), cmd.register.get(.upm).?); try testing.expectEqual(Reply.failures, cmd.register.get(.reply).?); try testing.expectEqualStrings("QQ==", cmd.register.payload()); } test "register command with sizing and placement options" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=e0a0;upm=2048;aw=1024;lh=1536;width=2;size=contain;align=end,baseline;pad=0.1,0.2,0.3,0.4;QQ==", ); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(@as(u32, 2048), cmd.register.get(.upm).?); try testing.expectEqual(@as(u32, 1024), cmd.register.get(.aw).?); try testing.expectEqual(@as(u32, 1536), cmd.register.get(.lh).?); try testing.expectEqual(Width.wide, cmd.register.get(.width).?); try testing.expectEqual(Size.contain, cmd.register.get(.size).?); try testing.expectEqual(Align{ .horizontal = .end, .vertical = .baseline, }, cmd.register.get(.@"align").?); try testing.expectEqual(Pad{ .top = 0.1, .right = 0.2, .bottom = 0.3, .left = 0.4, }, cmd.register.get(.pad).?); try testing.expectEqualStrings("QQ==", cmd.register.payload()); } test "register option defaults" { const testing = std.testing; const Option = Request.Register.Option; try testing.expect(Option.cp.default() == null); try testing.expectEqual(Format.glyf, Option.fmt.default().?); try testing.expectEqual(@as(u32, 1000), Option.upm.default().?); try testing.expect(Option.aw.default() == null); try testing.expect(Option.lh.default() == null); try testing.expectEqual(Width.narrow, Option.width.default().?); try testing.expectEqual(Size.height, Option.size.default().?); try testing.expectEqual(Align{}, Option.@"align".default().?); try testing.expectEqual(Pad{}, Option.pad.default().?); try testing.expectEqual(Reply.all, Option.reply.default().?); } test "register command defaults" { const testing = std.testing; var cmd = try testParse(testing.allocator, "r;cp=e0a0;QQ=="); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?); try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?); try testing.expectEqual(@as(u32, 1000), cmd.register.get(.upm).?); try testing.expectEqual(@as(u32, 1000), cmd.register.get(.aw).?); try testing.expectEqual(@as(u32, 1000), cmd.register.get(.lh).?); try testing.expectEqual(Width.narrow, cmd.register.get(.width).?); try testing.expectEqual(Size.height, cmd.register.get(.size).?); try testing.expectEqual(Align{}, cmd.register.get(.@"align").?); try testing.expectEqual(Pad{}, cmd.register.get(.pad).?); try testing.expectEqual(Reply.all, cmd.register.get(.reply).?); } test "register command aw and lh default to upm" { const testing = std.testing; var cmd = try testParse(testing.allocator, "r;cp=e0a0;upm=2048;QQ=="); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(@as(u32, 2048), cmd.register.get(.aw).?); try testing.expectEqual(@as(u32, 2048), cmd.register.get(.lh).?); } test "register command invalid sizing and placement options" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=e0a0;width=3;size=invalid;align=center,middle;pad=0,1.2,0,0;QQ==", ); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expect(cmd.register.get(.width) == null); try testing.expect(cmd.register.get(.size) == null); try testing.expect(cmd.register.get(.@"align") == null); try testing.expect(cmd.register.get(.pad) == null); } test "register command degenerate padding defaults to no padding" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=e0a0;pad=0.4,0.2,0.6,0.1;QQ==", ); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(Pad{}, cmd.register.get(.pad).?); } test "register command invalid reply falls back to reply=1" { const testing = std.testing; var cmd = try testParse(testing.allocator, "r;cp=e0a0;reply=9;QQ=="); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(Reply.all, cmd.register.get(.reply).?); } test "register command duplicate options use the last value" { const testing = std.testing; var cmd = try testParse(testing.allocator, "r;cp=e0a0;reply=1;reply=2;QQ=="); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(Reply.failures, cmd.register.get(.reply).?); } test "register command with invalid payload" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=e0a0;fmt=glyf;%%%not-base64%%%", ); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .register); try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?); try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?); try testing.expectEqualStrings("%%%not-base64%%%", cmd.register.payload()); } test "register command rejects missing payload separator" { const testing = std.testing; for ([_][]const u8{ "r", "r;cp=e0a0", "r;foo" }) |data| { try testing.expectError( error.InvalidFormat, testParse(testing.allocator, data), ); } } 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, std.math.maxInt(usize), ); 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, std.math.maxInt(usize)), ); } test "register response without payload" { const testing = std.testing; var cmd = try testParse( testing.allocator, "r;cp=E0A0;status=4;reason=out_of_namespace", ); defer cmd.deinit(testing.allocator); // Register parsing is request-only, so the final segment is always treated // as payload rather than as a response field. try testing.expect(cmd == .register); try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?); try testing.expectEqualStrings("reason=out_of_namespace", cmd.register.payload()); } test "clear command" { const testing = std.testing; var cmd = try testParse(testing.allocator, "c;cp=e0a0"); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .clear); try testing.expect(cmd.clear.has(.cp)); try testing.expectEqual(@as(u21, 0xE0A0), cmd.clear.get(.cp).?); } test "clear command tracks malformed cp presence" { const testing = std.testing; for ([_][]const u8{ "c;cp=zz", "c;cp=", "c;cp=200000" }) |data| { var cmd = try testParse(testing.allocator, data); defer cmd.deinit(testing.allocator); try testing.expect(cmd == .clear); try testing.expect(cmd.clear.has(.cp)); try testing.expect(cmd.clear.get(.cp) == null); } } test "invalid command" { const testing = std.testing; try testing.expectError( error.InvalidFormat, testParse(testing.allocator, "x"), ); }