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 }