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 }