lib/zen/src/site/path/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 const capacity_mod = @import("capacity.zig");
  5 const model = @import("model.zig");
  6 const plan_mod = @import("plan.zig");
  7 const transform = @import("transform.zig");
  8 
  9 pub const Paths = struct {
 10     target: []const u8,
 11     public: []const u8,
 12 };
 13 
 14 pub const Status = struct {
 15     phase: alloc_phase.capacity.Phase,
 16     in_use: bool,
 17     storage_bytes: usize,
 18     max_target_path_bytes: usize,
 19     max_public_path_bytes: usize,
 20     target_path_bytes: usize,
 21     public_path_bytes: usize,
 22     high_water_target_path_bytes: usize,
 23     high_water_public_path_bytes: usize,
 24     rejected_path_count: u64,
 25 };
 26 
 27 pub const Storage = struct {
 28     phase: alloc_phase.capacity.Phase,
 29     capacity: capacity_mod.Capacity,
 30     bytes: []u8,
 31     target_path: []u8,
 32     public_path: []u8,
 33     in_use: bool = false,
 34     target_path_bytes: usize = 0,
 35     public_path_bytes: usize = 0,
 36     high_water_target_path_bytes: usize = 0,
 37     high_water_public_path_bytes: usize = 0,
 38     rejected_path_count: u64 = 0,
 39 
 40     pub const Limits: type = capacity_mod.Limits;
 41     pub const Capacity: type = capacity_mod.Capacity;
 42     pub const Exhaustion: type = model.Exhaustion;
 43     pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
 44     pub const AcquireError: type = model.Error;
 45 
 46     pub const claim: alloc_phase.capacity.Declaration = .{
 47         .source = .{
 48             .id = "zen.site_path_storage",
 49             .kind = .phase_static,
 50             .limit_source = .caller,
 51             .storage = .{
 52                 .covered = &.{
 53                     .{
 54                         .id = "markdown_target_path_bytes",
 55                         .lifetime = .steady,
 56                         .detail = "Markdown target path bytes",
 57                     },
 58                     .{
 59                         .id = "public_page_or_clean_url_path_bytes",
 60                         .lifetime = .steady,
 61                         .detail = "public page or clean URL path bytes",
 62                     },
 63                 },
 64                 .excluded = &.{
 65                     "caller-owned relative paths and build roots",
 66                     "retained site catalog strings, file contents, rendered pages, and filesystem internals",
 67                 },
 68             },
 69             .capacity = .{
 70                 .inputs = &.{
 71                     alloc_phase.capacity.bindInput(Limits, "max_target_path_bytes", "max_target_path_bytes"),
 72                     alloc_phase.capacity.bindInput(Limits, "max_public_path_bytes", "max_public_path_bytes"),
 73                 },
 74                 .type_selectors = &.{},
 75                 .nodes = &.{
 76                     .{ .input = 0 },
 77                     .{ .input = 1 },
 78                     .{ .add = .{ .left = 0, .right = 1 } },
 79                 },
 80                 .assertions = &.{.{
 81                     .scope = .closure_total,
 82                     .measure = .retained,
 83                     .relation = .exact,
 84                     .expression = 2,
 85                 }},
 86             },
 87             .overload = .{
 88                 .kind = .reject_before_mutation,
 89                 .detail = "max plus one and in-use requests reject before either reusable path region changes; only rejection telemetry advances",
 90             },
 91             .risks = .{
 92                 .transitive = .{
 93                     .status = .witnessed,
 94                     .detail = "planning and path filling use borrowed relative paths plus the two acquired byte regions only",
 95                 },
 96                 .foreign = .{
 97                     .status = .excluded,
 98                     .detail = "path transformation crosses no operating-system boundary; directory and file effects remain site-build consumers",
 99                 },
100             },
101             .obligations = &.{
102                 .{ .key = "zen_site_path_capacity", .role = .capacity_model },
103                 .{ .key = "zen_site_path_acquisition", .role = .custom },
104                 .{ .key = "zen_site_path_oom", .role = .custom },
105                 .{ .key = "zen_site_path_boundaries", .role = .overload },
106                 .{ .key = "zen_site_path_reuse", .role = .overload },
107                 .{ .key = "zen_site_path_sealed", .role = .transitive_risk },
108                 .{ .key = "zen_site_path_root", .role = .custom },
109                 .{ .key = "zen_site_path_consumer", .role = .foreign_risk },
110             },
111         },
112         .bindings = .{
113             .owner = @This(),
114             .seal = .{
115                 .family = alloc_phase.capacity.selector(@This().activate),
116                 .premise = .{
117                     .class = .checked_semantic_fact,
118                     .authority = .checker,
119                 },
120             },
121             .teardown = .{
122                 .family = alloc_phase.capacity.selector(@This().deinit),
123                 .premise = .{
124                     .class = .checked_semantic_fact,
125                     .authority = .checker,
126                 },
127             },
128         },
129     };
130 
131     pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
132         const capacity = try Capacity.derive(limits);
133         const bytes = if (capacity.storage_bytes == 0)
134             @as([]u8, &.{})
135         else
136             try allocator.alloc(u8, capacity.storage_bytes);
137         return .{
138             .phase = .initialization,
139             .capacity = capacity,
140             .bytes = bytes,
141             .target_path = bytes[capacity.target_path_offset..][0..limits.max_target_path_bytes],
142             .public_path = bytes[capacity.public_path_offset..][0..limits.max_public_path_bytes],
143         };
144     }
145 
146     pub fn activate(self: *Storage) void {
147         std.debug.assert(self.phase == .initialization);
148         self.assertStorage();
149         self.phase = .steady;
150     }
151 
152     pub fn acquire(
153         self: *Storage,
154         relative_path: []const u8,
155         clean_urls: bool,
156         kind: model.PublicKind,
157     ) AcquireError!Paths {
158         std.debug.assert(self.phase == .steady);
159         if (self.in_use) return self.reject(error.SitePathStorageInUse);
160         const plan = plan_mod.Plan.inspect(
161             relative_path,
162             clean_urls,
163             kind,
164             self.capacity.limits,
165         ) catch |err| {
166             self.rejected_path_count +|= 1;
167             return err;
168         };
169         self.in_use = true;
170         self.target_path_bytes = plan.target_path_bytes;
171         self.public_path_bytes = plan.public_path_bytes;
172         self.high_water_target_path_bytes = @max(
173             self.high_water_target_path_bytes,
174             plan.target_path_bytes,
175         );
176         self.high_water_public_path_bytes = @max(
177             self.high_water_public_path_bytes,
178             plan.public_path_bytes,
179         );
180         const target = transform.writeTarget(
181             self.target_path[0..plan.target_path_bytes],
182             relative_path,
183             clean_urls,
184         );
185         const public = transform.writePublic(
186             self.public_path[0..plan.public_path_bytes],
187             target,
188             clean_urls,
189             kind,
190         );
191         self.assertStorage();
192         return .{ .target = target, .public = public };
193     }
194 
195     pub fn reset(self: *Storage) void {
196         std.debug.assert(self.phase == .steady);
197         std.debug.assert(self.in_use);
198         self.in_use = false;
199         self.target_path_bytes = 0;
200         self.public_path_bytes = 0;
201         self.assertStorage();
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             .max_target_path_bytes = self.capacity.limits.max_target_path_bytes,
210             .max_public_path_bytes = self.capacity.limits.max_public_path_bytes,
211             .target_path_bytes = self.target_path_bytes,
212             .public_path_bytes = self.public_path_bytes,
213             .high_water_target_path_bytes = self.high_water_target_path_bytes,
214             .high_water_public_path_bytes = self.high_water_public_path_bytes,
215             .rejected_path_count = self.rejected_path_count,
216         };
217     }
218 
219     pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
220         std.debug.assert(self.phase != .teardown);
221         std.debug.assert(!self.in_use);
222         self.assertStorage();
223         self.phase = .teardown;
224         allocator.free(self.bytes);
225         self.bytes = &.{};
226         self.target_path = &.{};
227         self.public_path = &.{};
228     }
229 
230     fn reject(self: *Storage, err: model.Exhaustion) model.Exhaustion {
231         self.rejected_path_count +|= 1;
232         return err;
233     }
234 
235     fn assertStorage(self: *const Storage) void {
236         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
237         std.debug.assert(self.target_path.len == self.capacity.limits.max_target_path_bytes);
238         std.debug.assert(self.public_path.len == self.capacity.limits.max_public_path_bytes);
239         std.debug.assert(self.target_path_bytes <= self.target_path.len);
240         std.debug.assert(self.public_path_bytes <= self.public_path.len);
241         std.debug.assert(self.high_water_target_path_bytes <= self.target_path.len);
242         std.debug.assert(self.high_water_public_path_bytes <= self.public_path.len);
243         if (!self.in_use) {
244             std.debug.assert(self.target_path_bytes == 0);
245             std.debug.assert(self.public_path_bytes == 0);
246         }
247     }
248 };
249 
250 fn checkInitFailures(allocator: std.mem.Allocator) !void {
251     var storage = try Storage.init(allocator, .{
252         .max_target_path_bytes = 83,
253         .max_public_path_bytes = 84,
254     });
255     storage.deinit(allocator);
256 }
257 
258 test "site path storage acquires one exact region" {
259     comptime {
260         @stardustClaim(
261             @import("alloc_phase").capacity.witness(Storage, "zen_site_path_acquisition"),
262             null,
263             null,
264             null,
265             null,
266             null,
267             null,
268         );
269     }
270 
271     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
272     const limits = capacity_mod.Limits{
273         .max_target_path_bytes = 83,
274         .max_public_path_bytes = 84,
275     };
276     const capacity = try capacity_mod.Capacity.derive(limits);
277     var storage = try Storage.init(counting.allocator(), limits);
278     defer storage.deinit(counting.allocator());
279     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
280     try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
281     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
282 }
283 
284 test "site path storage retries after every allocation failure" {
285     comptime {
286         @stardustClaim(
287             @import("alloc_phase").capacity.witness(Storage, "zen_site_path_oom"),
288             null,
289             null,
290             null,
291             null,
292             null,
293             null,
294         );
295     }
296 
297     try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
298 }
299 
300 comptime {
301     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
302 }