lib/zen/src/site/catalog/plan.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const model = @import("model.zig");
  4 
  5 pub const Limits = struct {
  6     max_pages: usize,
  7     max_text_bytes: usize,
  8 };
  9 
 10 pub const default_limits: Limits = .{
 11     .max_pages = 4096,
 12     .max_text_bytes = 64 * 1024 * 1024,
 13 };
 14 
 15 pub const Demand = struct {
 16     relative_path_bytes: usize,
 17     url_bytes: usize,
 18     title_bytes: usize,
 19     date_bytes: usize,
 20     type_label_bytes: usize,
 21 
 22     pub fn fromInput(input: model.EntryInput) Demand {
 23         const demand = Demand{
 24             .relative_path_bytes = input.relative_path.len,
 25             .url_bytes = input.url.len,
 26             .title_bytes = input.title.len,
 27             .date_bytes = input.date.len,
 28             .type_label_bytes = input.type_label.len,
 29         };
 30         std.debug.assert(demand.relative_path_bytes == input.relative_path.len);
 31         std.debug.assert(demand.url_bytes == input.url.len);
 32         std.debug.assert(demand.title_bytes == input.title.len);
 33         std.debug.assert(demand.date_bytes == input.date.len);
 34         std.debug.assert(demand.type_label_bytes == input.type_label.len);
 35         return demand;
 36     }
 37 
 38     pub fn textBytes(self: Demand) error{CapacityOverflow}!usize {
 39         var total = try added(self.relative_path_bytes, self.url_bytes);
 40         std.debug.assert(total >= self.relative_path_bytes);
 41         std.debug.assert(total >= self.url_bytes);
 42         total = try added(total, self.title_bytes);
 43         std.debug.assert(total >= self.title_bytes);
 44         total = try added(total, self.date_bytes);
 45         std.debug.assert(total >= self.date_bytes);
 46         const result = try added(total, self.type_label_bytes);
 47         std.debug.assert(result >= self.type_label_bytes);
 48         return result;
 49     }
 50 };
 51 
 52 pub const Plan = struct {
 53     pages: usize = 0,
 54     text_bytes: usize = 0,
 55 
 56     pub fn inspect(demand: Demand, limits: Limits) model.Error!Plan {
 57         var plan: Plan = .{};
 58         try plan.observe(demand, limits);
 59         std.debug.assert(plan.pages == 1);
 60         std.debug.assert(plan.text_bytes == try demand.textBytes());
 61         return plan;
 62     }
 63 
 64     pub fn observe(self: *Plan, demand: Demand, limits: Limits) model.Error!void {
 65         const next_pages = try added(self.pages, 1);
 66         if (next_pages > limits.max_pages) return error.SiteCatalogPageCapacityExceeded;
 67         const next_text = try added(self.text_bytes, try demand.textBytes());
 68         if (next_text > limits.max_text_bytes) return error.SiteCatalogTextCapacityExceeded;
 69         self.pages = next_pages;
 70         self.text_bytes = next_text;
 71         std.debug.assert(self.pages <= limits.max_pages);
 72         std.debug.assert(self.text_bytes <= limits.max_text_bytes);
 73     }
 74 
 75     pub fn exactLimits(self: Plan) Limits {
 76         const limits = Limits{ .max_pages = self.pages, .max_text_bytes = self.text_bytes };
 77         std.debug.assert(limits.max_pages == self.pages);
 78         std.debug.assert(limits.max_text_bytes == self.text_bytes);
 79         return limits;
 80     }
 81 };
 82 
 83 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
 84     return std.math.add(usize, left, right) catch error.CapacityOverflow;
 85 }
 86 
 87 test "site catalog plan rejects every limit before mutation" {
 88     const demand = Demand{
 89         .relative_path_bytes = 8,
 90         .url_bytes = 9,
 91         .title_bytes = 5,
 92         .date_bytes = 10,
 93         .type_label_bytes = 0,
 94     };
 95     var plan = try Plan.inspect(demand, .{ .max_pages = 2, .max_text_bytes = 64 });
 96     try std.testing.expectError(error.SiteCatalogPageCapacityExceeded, plan.observe(
 97         demand,
 98         .{ .max_pages = 1, .max_text_bytes = 64 },
 99     ));
100     try std.testing.expectEqual(
101         Limits{ .max_pages = 1, .max_text_bytes = 32 },
102         plan.exactLimits(),
103     );
104     try std.testing.expectError(error.SiteCatalogTextCapacityExceeded, plan.observe(
105         demand,
106         .{ .max_pages = 2, .max_text_bytes = 63 },
107     ));
108     try std.testing.expectEqual(
109         Limits{ .max_pages = 1, .max_text_bytes = 32 },
110         plan.exactLimits(),
111     );
112 }
113 
114 test "site catalog demand rejects checked arithmetic overflow" {
115     try std.testing.expectError(error.CapacityOverflow, (Demand{
116         .relative_path_bytes = std.math.maxInt(usize),
117         .url_bytes = 1,
118         .title_bytes = 0,
119         .date_bytes = 0,
120         .type_label_bytes = 0,
121     }).textBytes());
122 }