From 502078a2766ea178aa22defb5020ac30a50f12f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jun 2026 20:00:54 -0700 Subject: [PATCH] terminal/apc/glyph: clone glossary entries Glossary entries own decoded glyph payloads, so callers could not copy an entry without sharing outline storage. Add clone methods for glossary entries and glyf outlines that allocate fresh contour and point slices while copying the scalar registration metadata directly. This keeps the ownership contract symmetric with deinit and lets cloned entries be released independently from their source. --- src/font/opentype/glyf.zig | 17 +++++++++++++ src/terminal/apc/glyph/Glossary.zig | 37 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/font/opentype/glyf.zig b/src/font/opentype/glyf.zig index 912b5fe84..1719b027e 100644 --- a/src/font/opentype/glyf.zig +++ b/src/font/opentype/glyf.zig @@ -50,6 +50,23 @@ pub const Glyf = struct { return self.points[start..end]; } + /// Deep copy this outline using `alloc`. + /// + /// The returned outline owns its contour and point storage and must be + /// released with `deinit`. + pub fn clone(self: *const Outline, alloc: Allocator) Allocator.Error!Outline { + const contours = try alloc.dupe(sfnt.uint16, self.contours); + errdefer alloc.free(contours); + + const points = try alloc.dupe(Point, self.points); + errdefer alloc.free(points); + + return .{ + .contours = contours, + .points = points, + }; + } + /// Free all memory owned by this outline. Pass in the same /// allocator used for decoding. pub fn deinit(self: *Outline, alloc: Allocator) void { diff --git a/src/terminal/apc/glyph/Glossary.zig b/src/terminal/apc/glyph/Glossary.zig index 28458dbb2..e82d9e8e8 100644 --- a/src/terminal/apc/glyph/Glossary.zig +++ b/src/terminal/apc/glyph/Glossary.zig @@ -193,6 +193,23 @@ pub const Entry = struct { self.* = undefined; } + /// Deep copy this glossary entry using `alloc`. + /// + /// The returned entry owns decoded glyph memory and must be released with + /// `deinit`. + pub fn clone(self: *const Entry, alloc: Allocator) Allocator.Error!Entry { + const glyph: Glyph = switch (self.glyph) { + .glyf => |*outline| .{ .glyf = try outline.clone(alloc) }, + }; + + return .{ + .glyph = glyph, + .design = self.design, + .width = self.width, + .constraint = self.constraint, + }; + } + /// Return the renderer constraint for a register request. /// /// Glyph Protocol ยง8.5 defines sizing, alignment, and padding in terms of @@ -320,6 +337,26 @@ test "Entry init decodes glyf payload and applies register fields" { try testing.expectEqual(@as(usize, 1), entry.glyph.glyf.contours.len); } +test "Entry clone deep copies decoded glyf payload" { + const testing = std.testing; + const alloc = testing.allocator; + + var entry = try testRegisterEntry(alloc, 0xE000); + defer entry.deinit(alloc); + + var cloned = try entry.clone(alloc); + defer cloned.deinit(alloc); + + try testing.expectEqual(entry.design, cloned.design); + try testing.expectEqual(entry.width, cloned.width); + try testing.expectEqual(entry.constraint, cloned.constraint); + + try testing.expectEqualSlices(u16, entry.glyph.glyf.contours, cloned.glyph.glyf.contours); + try testing.expectEqualSlices(Glyf.Outline.Point, entry.glyph.glyf.points, cloned.glyph.glyf.points); + try testing.expect(entry.glyph.glyf.contours.ptr != cloned.glyph.glyf.contours.ptr); + try testing.expect(entry.glyph.glyf.points.ptr != cloned.glyph.glyf.points.ptr); +} + test "Entry init rejects invalid register payload" { const testing = std.testing; const alloc = testing.allocator;