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 }