lib/zen/src/theme/plan.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const html = @import("../root.zig").html;
  3 const model = @import("model.zig");
  4 const scan = @import("scan.zig");
  5 const value = @import("value.zig");
  6 
  7 pub const Limits = struct {
  8     max_output_bytes: usize,
  9 };
 10 
 11 pub const default_limits: Limits = .{
 12     .max_output_bytes = 128 * 1024 * 1024,
 13 };
 14 
 15 pub const Plan = struct {
 16     layout_bytes: usize,
 17     placeholders: usize,
 18     output_bytes: usize,
 19 
 20     pub fn inspect(active: model.Theme, page: model.Page, limits: Limits) model.Error!Plan {
 21         var placeholders: usize = 0;
 22         var output_bytes: usize = 0;
 23         var iterator = scan.Iterator.init(active.layout);
 24         while (try iterator.next()) |event| switch (event) {
 25             .literal => |literal| output_bytes = try addOutput(
 26                 output_bytes,
 27                 literal.len,
 28                 limits.max_output_bytes,
 29             ),
 30             .placeholder => |key| {
 31                 placeholders = try added(placeholders, 1);
 32                 const resolved = try value.resolve(active, page, key);
 33                 const bytes = switch (resolved.encoding) {
 34                     .raw => resolved.text.len,
 35                     .escaped => try html.escapedLength(resolved.text),
 36                 };
 37                 output_bytes = try addOutput(
 38                     output_bytes,
 39                     bytes,
 40                     limits.max_output_bytes,
 41                 );
 42             },
 43         };
 44         return .{
 45             .layout_bytes = active.layout.len,
 46             .placeholders = placeholders,
 47             .output_bytes = output_bytes,
 48         };
 49     }
 50 
 51     pub fn exactLimits(self: Plan) Limits {
 52         return .{ .max_output_bytes = self.output_bytes };
 53     }
 54 
 55     pub fn require(self: Plan, limits: Limits) model.Exhaustion!void {
 56         if (self.output_bytes > limits.max_output_bytes) {
 57             return error.ThemeOutputByteCapacityExceeded;
 58         }
 59     }
 60 };
 61 
 62 fn addOutput(current: usize, amount: usize, limit: usize) model.Error!usize {
 63     const total = try added(current, amount);
 64     if (total > limit) return error.ThemeOutputByteCapacityExceeded;
 65     return total;
 66 }
 67 
 68 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
 69     return std.math.add(usize, left, right) catch error.CapacityOverflow;
 70 }
 71 
 72 test "theme render plan computes exact raw and escaped bytes" {
 73     const includes = [_]model.Include{
 74         .{ .name = "head", .html = "<b>raw</b>" },
 75     };
 76     const active = model.Theme{
 77         .layout = "A{{ title }}B{{content}}C{{include.head}}D",
 78         .includes = &includes,
 79     };
 80     const page = model.Page{ .title = "<&", .content = "<i>x</i>" };
 81     const plan = try Plan.inspect(active, page, default_limits);
 82     try std.testing.expectEqual(@as(usize, 3), plan.placeholders);
 83     try std.testing.expectEqual(
 84         "A".len + "&lt;&amp;".len + "B".len + "<i>x</i>".len +
 85             "C".len + "<b>raw</b>".len + "D".len,
 86         plan.output_bytes,
 87     );
 88     try std.testing.expectEqual(Limits{ .max_output_bytes = plan.output_bytes }, plan.exactLimits());
 89 }
 90 
 91 test "theme render plan validates templates and output capacity" {
 92     const page = model.Page{ .title = "Title", .content = "body" };
 93     try std.testing.expectError(
 94         error.UnclosedPlaceholder,
 95         Plan.inspect(.{ .layout = "{{title" }, page, default_limits),
 96     );
 97     try std.testing.expectError(
 98         error.UnknownPlaceholder,
 99         Plan.inspect(.{ .layout = "{{unknown}}" }, page, default_limits),
100     );
101     try std.testing.expectError(
102         error.ThemeOutputByteCapacityExceeded,
103         Plan.inspect(.{ .layout = "{{content}}" }, page, .{ .max_output_bytes = 3 }),
104     );
105 }