lib/gif/src/decode/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const capacity_mod = @import("capacity.zig");
  4 const model = @import("model.zig");
  5 const plan_mod = @import("plan.zig");
  6 
  7 pub const Regions = struct {
  8     frames: []model.Frame,
  9     canvas: []u8,
 10     previous_canvas: []u8,
 11     indices: []u8,
 12     compressed: []u8,
 13     retained_rgba8: []u8,
 14 };
 15 
 16 pub const Status = struct {
 17     phase: alloc_phase.capacity.Phase,
 18     in_use: bool,
 19     storage_bytes: usize,
 20     canvas_pixels: usize,
 21     frame_pixels: usize,
 22     frames: usize,
 23     compressed_bytes: usize,
 24     retained_rgba8_bytes: usize,
 25 };
 26 
 27 pub const Storage = struct {
 28     phase: alloc_phase.capacity.Phase,
 29     capacity: capacity_mod.Capacity,
 30     bytes: []align(capacity_mod.storage_alignment) u8,
 31     frames: []model.Frame,
 32     canvas: []u8,
 33     previous_canvas: []u8,
 34     indices: []u8,
 35     compressed: []u8,
 36     retained_rgba8: []u8,
 37     in_use: bool = false,
 38 
 39     pub const Limits: type = capacity_mod.Limits;
 40     pub const Capacity: type = capacity_mod.Capacity;
 41     pub const Exhaustion: type = model.Exhaustion;
 42     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 43     pub const AcquireError = plan_mod.Error;
 44 
 45     pub const claim: alloc_phase.capacity.Declaration = .{
 46         .source = .{
 47             .id = "gif.decode_storage",
 48             .kind = .phase_static,
 49             .limit_source = .caller,
 50             .storage = .{
 51                 .covered = &.{
 52                     .{
 53                         .id = "decoded_frame_descriptors",
 54                         .lifetime = .steady,
 55                         .detail = "decoded frame descriptors",
 56                     },
 57                     .{
 58                         .id = "current_and_previous_rgba8_canvases",
 59                         .lifetime = .steady,
 60                         .detail = "current and previous RGBA8 canvases",
 61                     },
 62                     .{
 63                         .id = "frame_index_and_compressed_byte_scratch",
 64                         .lifetime = .steady,
 65                         .detail = "frame index and compressed-byte scratch",
 66                     },
 67                     .{
 68                         .id = "retained_decoded_rgba8_frames",
 69                         .lifetime = .steady,
 70                         .detail = "retained decoded RGBA8 frames",
 71                     },
 72                 },
 73                 .excluded = &.{
 74                     "caller-owned GIF input bytes",
 75                     "GIF encoding palettes, indices, and output bytes",
 76                 },
 77             },
 78             .capacity = .{
 79                 .inputs = &.{
 80                     alloc_phase.capacity.bindInput(Limits, "bounds_frames", "bounds.frames"),
 81                     alloc_phase.capacity.bindInput(Limits, "bounds_canvas_pixels", "bounds.canvas_pixels"),
 82                     alloc_phase.capacity.bindInput(Limits, "bounds_frame_pixels", "bounds.frame_pixels"),
 83                     alloc_phase.capacity.bindInput(Limits, "bounds_compressed_bytes", "bounds.compressed_bytes"),
 84                     alloc_phase.capacity.bindInput(Limits, "bounds_retained_rgba8_bytes", "bounds.retained_rgba8_bytes"),
 85                 },
 86                 .type_selectors = &.{
 87                     alloc_phase.capacity.bindType(model.Frame, "frame"),
 88                 },
 89                 .nodes = &.{
 90                     .{ .input = 0 },
 91                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
 92                     .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 16 } } },
 93                     .{ .input = 1 },
 94                     .{ .scale = .{ .node = 3, .coefficient = .{ .literal = 8 } } },
 95                     .{ .input = 2 },
 96                     .{ .input = 3 },
 97                     .{ .input = 4 },
 98                     .{ .add = .{ .left = 2, .right = 4 } },
 99                     .{ .add = .{ .left = 8, .right = 5 } },
100                     .{ .add = .{ .left = 9, .right = 6 } },
101                     .{ .add = .{ .left = 10, .right = 7 } },
102                 },
103                 .assertions = &.{.{
104                     .scope = .closure_total,
105                     .measure = .retained,
106                     .relation = .exact,
107                     .expression = 11,
108                 }},
109             },
110             .overload = .{
111                 .kind = .reject_before_mutation,
112                 .detail = "structural planning rejects each caller bound before acquisition; input mismatch and concurrent acquisition leave the region reusable",
113             },
114             .risks = .{
115                 .transitive = .{
116                     .status = .witnessed,
117                     .detail = "container parsing, LZW decode, compositing, disposal, and retained frame construction use acquired regions only after activation",
118                 },
119                 .foreign = .{
120                     .status = .excluded,
121                     .detail = "input ownership and consumer presentation effects remain outside decode storage",
122                 },
123             },
124             .obligations = &.{
125                 .{ .key = "gif_decode_capacity", .role = .capacity_model },
126                 .{ .key = "gif_decode_acquisition", .role = .custom },
127                 .{ .key = "gif_decode_oom", .role = .custom },
128                 .{ .key = "gif_decode_boundaries", .role = .overload },
129                 .{ .key = "gif_decode_reuse", .role = .overload },
130                 .{ .key = "gif_decode_malformed", .role = .overload },
131                 .{ .key = "gif_decode_sealed", .role = .transitive_risk },
132                 .{ .key = "gif_decode_root", .role = .custom },
133                 .{ .key = "gif_decode_consumer", .role = .foreign_risk },
134             },
135         },
136         .bindings = .{
137             .owner = @This(),
138             .seal = .{
139                 .family = alloc_phase.capacity.selector(@This().activate),
140                 .premise = .{
141                     .class = .checked_semantic_fact,
142                     .authority = .checker,
143                 },
144             },
145             .teardown = .{
146                 .family = alloc_phase.capacity.selector(@This().deinit),
147                 .premise = .{
148                     .class = .checked_semantic_fact,
149                     .authority = .checker,
150                 },
151             },
152         },
153     };
154 
155     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
156         const capacity = try Capacity.derive(limits);
157         const bytes = try allocator.alignedAlloc(
158             u8,
159             .fromByteUnits(capacity_mod.storage_alignment),
160             capacity.storage_bytes,
161         );
162         return .{
163             .phase = .initialization,
164             .capacity = capacity,
165             .bytes = bytes,
166             .frames = typedSlice(model.Frame, bytes, capacity.frame_offset, capacity.plan.frames),
167             .canvas = bytes[capacity.canvas_offset..][0..capacity.plan.canvas_rgba8_bytes],
168             .previous_canvas = bytes[capacity.previous_canvas_offset..][0..capacity.plan.canvas_rgba8_bytes],
169             .indices = bytes[capacity.indices_offset..][0..capacity.plan.frame_pixels],
170             .compressed = bytes[capacity.compressed_offset..][0..capacity.plan.compressed_bytes],
171             .retained_rgba8 = bytes[capacity.retained_rgba8_offset..][0..capacity.plan.retained_rgba8_bytes],
172         };
173     }
174 
175     pub fn activate(self: *Storage) void {
176         std.debug.assert(self.phase == .initialization);
177         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
178         self.phase = .steady;
179     }
180 
181     pub fn acquire(self: *Storage, bytes: []const u8) AcquireError!Regions {
182         std.debug.assert(self.phase == .steady);
183         if (self.in_use) return error.DecodeStorageInUse;
184         const actual = try plan_mod.Plan.inspect(bytes, self.capacity.plan.exactBounds());
185         if (!std.meta.eql(actual, self.capacity.plan)) return error.DecodeInputMismatch;
186         @memset(self.canvas, 0);
187         self.in_use = true;
188         return .{
189             .frames = self.frames,
190             .canvas = self.canvas,
191             .previous_canvas = self.previous_canvas,
192             .indices = self.indices,
193             .compressed = self.compressed,
194             .retained_rgba8 = self.retained_rgba8,
195         };
196     }
197 
198     pub fn reset(self: *Storage) void {
199         std.debug.assert(self.phase == .steady);
200         std.debug.assert(self.in_use);
201         self.in_use = false;
202     }
203 
204     pub fn status(self: *const Storage) Status {
205         return .{
206             .phase = self.phase,
207             .in_use = self.in_use,
208             .storage_bytes = self.capacity.storage_bytes,
209             .canvas_pixels = self.capacity.plan.canvas_pixels,
210             .frame_pixels = self.capacity.plan.frame_pixels,
211             .frames = self.capacity.plan.frames,
212             .compressed_bytes = self.capacity.plan.compressed_bytes,
213             .retained_rgba8_bytes = self.capacity.plan.retained_rgba8_bytes,
214         };
215     }
216 
217     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
218         std.debug.assert(self.phase != .teardown);
219         std.debug.assert(!self.in_use);
220         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
221         self.phase = .teardown;
222         allocator.free(self.bytes);
223         self.bytes = &.{};
224         self.frames = &.{};
225         self.canvas = &.{};
226         self.previous_canvas = &.{};
227         self.indices = &.{};
228         self.compressed = &.{};
229         self.retained_rgba8 = &.{};
230     }
231 };
232 
233 fn typedSlice(
234     comptime T: type,
235     bytes: []align(capacity_mod.storage_alignment) u8,
236     offset: usize,
237     count: usize,
238 ) []T {
239     const byte_count = count * @sizeOf(T);
240     const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
241     return std.mem.bytesAsSlice(T, region);
242 }
243 
244 const witness = [_]u8{
245     0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x01, 0x00, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00,
246     0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44,
247     0x0a, 0x00, 0x3b,
248 };
249 
250 const witness_limits = capacity_mod.Limits{
251     .bytes = &witness,
252     .bounds = .{
253         .canvas_pixels = 2,
254         .frame_pixels = 2,
255         .frames = 1,
256         .compressed_bytes = 2,
257         .retained_rgba8_bytes = 8,
258     },
259 };
260 
261 fn checkInitFailures(allocator: std.mem.Allocator) !void {
262     var storage = try Storage.init(allocator, witness_limits);
263     storage.deinit(allocator);
264 }
265 
266 test "GIF decode storage acquires one exact aligned region" {
267     comptime {
268         @stardustClaim(
269             @import("alloc_phase").capacity.witness(Storage, "gif_decode_acquisition"),
270             null,
271             null,
272             null,
273             null,
274             null,
275             null,
276         );
277     }
278 
279     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
280     const capacity = try capacity_mod.Capacity.derive(witness_limits);
281     var storage = try Storage.init(counting.allocator(), witness_limits);
282     defer storage.deinit(counting.allocator());
283 
284     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
285     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
286     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
287     storage.activate();
288     const regions = try storage.acquire(&witness);
289     defer storage.reset();
290     const base = @intFromPtr(storage.bytes.ptr);
291     try std.testing.expectEqual(base + capacity.frame_offset, @intFromPtr(regions.frames.ptr));
292     try std.testing.expectEqual(base + capacity.canvas_offset, @intFromPtr(regions.canvas.ptr));
293     try std.testing.expectEqual(base + capacity.previous_canvas_offset, @intFromPtr(regions.previous_canvas.ptr));
294     try std.testing.expectEqual(base + capacity.indices_offset, @intFromPtr(regions.indices.ptr));
295     try std.testing.expectEqual(base + capacity.compressed_offset, @intFromPtr(regions.compressed.ptr));
296     try std.testing.expectEqual(base + capacity.retained_rgba8_offset, @intFromPtr(regions.retained_rgba8.ptr));
297 }
298 
299 test "GIF decode storage retries after every allocation failure" {
300     comptime {
301         @stardustClaim(
302             @import("alloc_phase").capacity.witness(Storage, "gif_decode_oom"),
303             null,
304             null,
305             null,
306             null,
307             null,
308             null,
309         );
310     }
311 
312     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
313 }
314 
315 comptime {
316     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
317 }