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 }