lib/gif/src/decode/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const decode = @import("root.zig");
3
4 const Bounds = decode.Bounds;
5 const Plan = decode.Plan;
6 const Storage = decode.Storage;
7 const Animation = decode.Animation;
8 const animationFromBytes = decode.animationFromBytes;
9
10 const witness_transparent = [_]u8{
11 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x01, 0x00, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00,
12 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00, 0x21, 0xff, 0x0b, 0x49, 0x6d,
13 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63, 0x6b, 0x0e, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d,
14 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
15 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x0a, 0x00, 0x3b,
16 };
17
18 const witness_disposal = [_]u8{
19 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x02, 0x00, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00,
20 0x00, 0x00, 0x00, 0x21, 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e,
21 0x30, 0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x08, 0x0a, 0x00, 0x00, 0x00, 0x21, 0xff,
22 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63, 0x6b, 0x0e, 0x67, 0x61, 0x6d,
23 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35, 0x00, 0x2c, 0x00, 0x00, 0x00,
24 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x02, 0x84, 0x51, 0x00, 0x21, 0xf9, 0x04, 0x08, 0x0a,
25 0x00, 0x00, 0x00, 0x21, 0xff, 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63,
26 0x6b, 0x0e, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35,
27 0x00, 0x2c, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0x00, 0x00,
28 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
29 };
30
31 const test_bounds = Bounds{
32 .canvas_pixels = 16,
33 .frame_pixels = 16,
34 .frames = 4,
35 .compressed_bytes = 64,
36 .retained_rgba8_bytes = 256,
37 };
38
39 const TestDecode = struct {
40 storage: Storage,
41 animation: Animation,
42
43 fn init(allocator: std.mem.Allocator, bytes: []const u8) !TestDecode {
44 var storage = try Storage.init(allocator, .{ .bytes = bytes, .bounds = test_bounds });
45 errdefer storage.deinit(allocator);
46 storage.activate();
47 const animation = try animationFromBytes(&storage, bytes);
48 return .{ .animation = animation, .storage = storage };
49 }
50
51 fn deinit(self: *TestDecode, allocator: std.mem.Allocator) void {
52 self.storage.reset();
53 self.storage.deinit(allocator);
54 self.* = undefined;
55 }
56 };
57
58 test "transparent index leaves canvas pixels untouched" {
59 var decoded = try TestDecode.init(std.testing.allocator, &witness_transparent);
60 defer decoded.deinit(std.testing.allocator);
61 try std.testing.expectEqual(@as(usize, 1), decoded.animation.frames.len);
62 try std.testing.expectEqualSlices(u8, &.{ 255, 0, 0, 255, 0, 0, 0, 0 }, decoded.animation.frames[0].rgba8);
63 }
64
65 test "background disposal clears the frame region before the next frame" {
66 var decoded = try TestDecode.init(std.testing.allocator, &witness_disposal);
67 defer decoded.deinit(std.testing.allocator);
68 const animation = decoded.animation;
69 try std.testing.expectEqual(@as(usize, 2), animation.frames.len);
70 try std.testing.expectEqual(@as(?u16, 0), animation.loop_count);
71 try std.testing.expectEqual(@as(u16, 10), animation.frames[0].delay_cs);
72 try std.testing.expectEqualSlices(u8, &.{
73 255, 0, 0, 255, 255, 0, 0, 255,
74 255, 0, 0, 255, 255, 0, 0, 255,
75 }, animation.frames[0].rgba8);
76 try std.testing.expectEqualSlices(u8, &.{
77 0, 0, 0, 0, 0, 0, 0, 0,
78 0, 0, 0, 0, 0, 0, 255, 255,
79 }, animation.frames[1].rgba8);
80 }
81
82 test "interlaced rows land on their canvas lines" {
83 const allocator = std.testing.allocator;
84 var bytes = std.ArrayListUnmanaged(u8).empty;
85 defer bytes.deinit(allocator);
86 try bytes.appendSlice(allocator, "GIF89a");
87 try bytes.appendSlice(allocator, &.{ 1, 0, 4, 0, 0xf0, 0, 0 });
88 try bytes.appendSlice(allocator, &.{ 10, 20, 30, 40, 50, 60 });
89 try bytes.appendSlice(allocator, &.{ 0x2c, 0, 0, 0, 0, 1, 0, 4, 0, 0x40 });
90 try bytes.append(allocator, 2);
91 try bytes.appendSlice(allocator, &.{ 3, 0x44, 0x00, 0x05, 0x00 });
92 try bytes.append(allocator, 0x3b);
93
94 var decoded = try TestDecode.init(allocator, bytes.items);
95 defer decoded.deinit(allocator);
96 try std.testing.expectEqualSlices(u8, &.{
97 10, 20, 30, 255,
98 10, 20, 30, 255,
99 40, 50, 60, 255,
100 10, 20, 30, 255,
101 }, decoded.animation.frames[0].rgba8);
102 }
103
104 test "GIF decode planning rejects every caller bound at max plus one" {
105 comptime {
106 @stardustClaim(
107 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "gif_decode_boundaries"),
108 null,
109 null,
110 null,
111 null,
112 null,
113 null,
114 );
115 }
116
117 const plan = try Plan.inspect(&witness_disposal, test_bounds);
118 _ = try Plan.inspect(&witness_disposal, plan.exactBounds());
119
120 var bounds = plan.exactBounds();
121 bounds.canvas_pixels -= 1;
122 try std.testing.expectError(error.CanvasPixelCapacityExceeded, Plan.inspect(&witness_disposal, bounds));
123 bounds = plan.exactBounds();
124 bounds.frame_pixels -= 1;
125 try std.testing.expectError(error.FramePixelCapacityExceeded, Plan.inspect(&witness_disposal, bounds));
126 bounds = plan.exactBounds();
127 bounds.frames -= 1;
128 try std.testing.expectError(error.FrameCapacityExceeded, Plan.inspect(&witness_disposal, bounds));
129 bounds = plan.exactBounds();
130 bounds.compressed_bytes -= 1;
131 try std.testing.expectError(error.CompressedByteCapacityExceeded, Plan.inspect(&witness_disposal, bounds));
132 bounds = plan.exactBounds();
133 bounds.retained_rgba8_bytes -= 1;
134 try std.testing.expectError(error.RetainedRgba8CapacityExceeded, Plan.inspect(&witness_disposal, bounds));
135 }
136
137 test "GIF decode storage rejects concurrent use and resets after decode" {
138 comptime {
139 @stardustClaim(
140 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "gif_decode_reuse"),
141 null,
142 null,
143 null,
144 null,
145 null,
146 null,
147 );
148 }
149
150 const allocator = std.testing.allocator;
151 var storage = try Storage.init(allocator, .{ .bytes = &witness_transparent, .bounds = test_bounds });
152 defer storage.deinit(allocator);
153 storage.activate();
154
155 const first = try animationFromBytes(&storage, &witness_transparent);
156 try std.testing.expectEqual(@as(usize, 1), first.frames.len);
157 try std.testing.expectError(error.DecodeStorageInUse, animationFromBytes(&storage, &witness_transparent));
158 storage.reset();
159
160 var altered = witness_transparent;
161 altered[13] -%= 1;
162 try std.testing.expectError(error.DecodeInputMismatch, animationFromBytes(&storage, &altered));
163 try std.testing.expect(!storage.status().in_use);
164
165 const second = try animationFromBytes(&storage, &witness_transparent);
166 try std.testing.expectEqualSlices(u8, first.frames[0].rgba8, second.frames[0].rgba8);
167 storage.reset();
168 }
169
170 test "GIF decode errors release storage before retry" {
171 comptime {
172 @stardustClaim(
173 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "gif_decode_malformed"),
174 null,
175 null,
176 null,
177 null,
178 null,
179 null,
180 );
181 }
182
183 const allocator = std.testing.allocator;
184 var malformed = witness_transparent;
185 const descriptor_at = std.mem.indexOfScalar(u8, &malformed, 0x2c).?;
186 malformed[descriptor_at + 10] = 9;
187 var storage = try Storage.init(allocator, .{ .bytes = &malformed, .bounds = test_bounds });
188 defer storage.deinit(allocator);
189 storage.activate();
190
191 for (0..2) |_| {
192 try std.testing.expectError(error.InvalidCodeSize, animationFromBytes(&storage, &malformed));
193 try std.testing.expect(!storage.status().in_use);
194 }
195 }
196
197 test "Activated GIF decode storage performs no backing allocation" {
198 comptime {
199 @stardustClaim(
200 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "gif_decode_sealed"),
201 null,
202 null,
203 null,
204 null,
205 null,
206 null,
207 );
208 }
209
210 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
211 var storage = try Storage.init(failing.allocator(), .{ .bytes = &witness_disposal, .bounds = test_bounds });
212 defer storage.deinit(failing.allocator());
213 storage.activate();
214 failing.fail_index = failing.alloc_index;
215 failing.resize_fail_index = failing.resize_index;
216
217 for (0..8) |_| {
218 const animation = try animationFromBytes(&storage, &witness_disposal);
219 try std.testing.expectEqual(@as(usize, 2), animation.frames.len);
220 storage.reset();
221 }
222 try std.testing.expect(!failing.has_induced_failure);
223 }