lib/png/src/decode/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const flate = std.compress.flate;
4 const capacity_mod = @import("capacity.zig");
5 const model = @import("model.zig");
6 const plan_mod = @import("plan.zig");
7
8 pub const Regions = struct {
9 window: []u8,
10 prior: []u8,
11 current: []u8,
12 output: []u8,
13 };
14
15 pub const Status = struct {
16 phase: alloc_phase.capacity.Phase,
17 in_use: bool,
18 storage_bytes: usize,
19 input_bytes: usize,
20 image_pixels: usize,
21 source_row_bytes: usize,
22 idat_bytes: usize,
23 rgba8_bytes: usize,
24 };
25
26 pub const Storage = struct {
27 phase: alloc_phase.capacity.Phase,
28 capacity: capacity_mod.Capacity,
29 bytes: []align(capacity_mod.storage_alignment) u8,
30 window: []u8,
31 prior: []u8,
32 current: []u8,
33 output: []u8,
34 in_use: bool = false,
35
36 pub const Limits: type = capacity_mod.Limits;
37 pub const Capacity: type = capacity_mod.Capacity;
38 pub const Exhaustion: type = model.Exhaustion;
39 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
40 pub const AcquireError: type = plan_mod.Error;
41
42 pub const claim: alloc_phase.capacity.Declaration = .{
43 .source = .{
44 .id = "png.decode_storage",
45 .kind = .phase_static,
46 .limit_source = .caller,
47 .storage = .{
48 .covered = &.{
49 .{
50 .id = "deflate_history_window",
51 .lifetime = .steady,
52 .detail = "DEFLATE history window",
53 },
54 .{
55 .id = "prior_and_current_filtered_source_rows",
56 .lifetime = .steady,
57 .detail = "prior and current filtered source rows",
58 },
59 .{
60 .id = "decoded_rgba8_output",
61 .lifetime = .steady,
62 .detail = "decoded RGBA8 output",
63 },
64 },
65 .excluded = &.{
66 "caller-owned PNG input bytes",
67 "filesystem and presentation storage",
68 },
69 },
70 .capacity = .{
71 .inputs = &.{
72 alloc_phase.capacity.bindInput(Limits, "bounds_source_row_bytes", "bounds.source_row_bytes"),
73 alloc_phase.capacity.bindInput(Limits, "bounds_image_pixels", "bounds.image_pixels"),
74 },
75 .type_selectors = &.{},
76 .nodes = &.{
77 .{ .constant = 65536 },
78 .{ .input = 0 },
79 .{ .scale = .{ .node = 1, .coefficient = .{ .literal = 2 } } },
80 .{ .input = 1 },
81 .{ .scale = .{ .node = 3, .coefficient = .{ .literal = 4 } } },
82 .{ .add = .{ .left = 0, .right = 2 } },
83 .{ .add = .{ .left = 5, .right = 4 } },
84 },
85 .assertions = &.{.{
86 .scope = .closure_total,
87 .measure = .retained,
88 .relation = .exact,
89 .expression = 6,
90 }},
91 },
92 .overload = .{
93 .kind = .reject_before_mutation,
94 .detail = "Bounds, input mismatch, and concurrent use reject before output mutation.",
95 },
96 .risks = .{
97 .transitive = .{
98 .status = .witnessed,
99 .detail = "One region covers IDAT streaming, DEFLATE, row filters, and output.",
100 },
101 .foreign = .{
102 .status = .excluded,
103 .detail = "input ownership and output consumption remain caller effects",
104 },
105 },
106 .obligations = &.{
107 .{ .key = "png_decode_capacity", .role = .capacity_model },
108 .{ .key = "png_decode_acquisition", .role = .custom },
109 .{ .key = "png_decode_oom", .role = .custom },
110 .{ .key = "png_decode_boundaries", .role = .overload },
111 .{ .key = "png_decode_reuse", .role = .overload },
112 .{ .key = "png_decode_sealed", .role = .transitive_risk },
113 .{ .key = "png_decode_root", .role = .custom },
114 .{ .key = "png_decode_consumer", .role = .foreign_risk },
115 },
116 },
117 .bindings = .{
118 .owner = @This(),
119 .seal = .{
120 .family = alloc_phase.capacity.selector(@This().activate),
121 .premise = .{
122 .class = .checked_semantic_fact,
123 .authority = .checker,
124 },
125 },
126 .teardown = .{
127 .family = alloc_phase.capacity.selector(@This().deinit),
128 .premise = .{
129 .class = .checked_semantic_fact,
130 .authority = .checker,
131 },
132 },
133 },
134 };
135
136 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
137 const capacity = try Capacity.derive(limits);
138 const bytes = try allocator.alignedAlloc(
139 u8,
140 .fromByteUnits(capacity_mod.storage_alignment),
141 capacity.storage_bytes,
142 );
143 return .{
144 .phase = .initialization,
145 .capacity = capacity,
146 .bytes = bytes,
147 .window = bytes[0..flate.max_window_len],
148 .prior = bytes[capacity.prior_offset..][0..capacity.plan.source_row_bytes],
149 .current = bytes[capacity.current_offset..][0..capacity.plan.source_row_bytes],
150 .output = bytes[capacity.output_offset..][0..capacity.plan.rgba8_bytes],
151 };
152 }
153
154 pub fn activate(self: *Storage) void {
155 std.debug.assert(self.phase == .initialization);
156 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
157 self.phase = .steady;
158 }
159
160 pub fn acquire(self: *Storage, bytes: []const u8) AcquireError!Regions {
161 std.debug.assert(self.phase == .steady);
162 if (self.in_use) return error.DecodeStorageInUse;
163 const actual = try plan_mod.Plan.inspect(bytes, self.capacity.plan.bounds);
164 if (!std.meta.eql(actual, self.capacity.plan)) return error.DecodeInputMismatch;
165 self.in_use = true;
166 return .{
167 .window = self.window,
168 .prior = self.prior,
169 .current = self.current,
170 .output = self.output,
171 };
172 }
173
174 pub fn reset(self: *Storage) void {
175 std.debug.assert(self.phase == .steady);
176 std.debug.assert(self.in_use);
177 self.in_use = false;
178 }
179
180 pub fn plan(self: *const Storage) plan_mod.Plan {
181 return self.capacity.plan;
182 }
183
184 pub fn status(self: *const Storage) Status {
185 return .{
186 .phase = self.phase,
187 .in_use = self.in_use,
188 .storage_bytes = self.capacity.storage_bytes,
189 .input_bytes = self.capacity.plan.input_bytes,
190 .image_pixels = self.capacity.plan.image_pixels,
191 .source_row_bytes = self.capacity.plan.source_row_bytes,
192 .idat_bytes = self.capacity.plan.idat_bytes,
193 .rgba8_bytes = self.capacity.plan.rgba8_bytes,
194 };
195 }
196
197 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
198 std.debug.assert(self.phase != .teardown);
199 std.debug.assert(!self.in_use);
200 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
201 self.phase = .teardown;
202 allocator.free(self.bytes);
203 self.bytes = &.{};
204 self.window = &.{};
205 self.prior = &.{};
206 self.current = &.{};
207 self.output = &.{};
208 }
209 };
210
211 fn checkInitFailures(allocator: std.mem.Allocator) !void {
212 const witness = @import("fixture.zig").indexed;
213 var storage = try Storage.init(allocator, .{
214 .bytes = &witness,
215 .bounds = try plan_mod.Bounds.exact(&witness),
216 });
217 storage.deinit(allocator);
218 }
219
220 test "PNG decode storage acquires one exact aligned region" {
221 comptime {
222 @stardustClaim(
223 @import("alloc_phase").capacity.witness(Storage, "png_decode_acquisition"),
224 null,
225 null,
226 null,
227 null,
228 null,
229 null,
230 );
231 }
232
233 const witness = @import("fixture.zig").indexed;
234 const limits = capacity_mod.Limits{
235 .bytes = &witness,
236 .bounds = try plan_mod.Bounds.exact(&witness),
237 };
238 const capacity = try capacity_mod.Capacity.derive(limits);
239 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
240 var storage = try Storage.init(counting.allocator(), limits);
241 defer storage.deinit(counting.allocator());
242 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
243 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
244 storage.activate();
245 const regions = try storage.acquire(&witness);
246 defer storage.reset();
247 const base = @intFromPtr(storage.bytes.ptr);
248 try std.testing.expectEqual(base, @intFromPtr(regions.window.ptr));
249 try std.testing.expectEqual(base + capacity.prior_offset, @intFromPtr(regions.prior.ptr));
250 try std.testing.expectEqual(base + capacity.current_offset, @intFromPtr(regions.current.ptr));
251 try std.testing.expectEqual(base + capacity.output_offset, @intFromPtr(regions.output.ptr));
252 }
253
254 test "PNG decode storage retries after every allocation failure" {
255 comptime {
256 @stardustClaim(
257 @import("alloc_phase").capacity.witness(Storage, "png_decode_oom"),
258 null,
259 null,
260 null,
261 null,
262 null,
263 null,
264 );
265 }
266
267 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
268 }
269
270 comptime {
271 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
272 }