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