kernel/policy_limit.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Policy limits as values (`crates/kernel/spec/PolicyLimit.lean` §8-73): each type carries one
7//! ceiling's legal domain and the one refusal that says no, so a caller
8//! hands over what it observed and cannot spell a second refusal.
9//!
10//! The numbers live in `consts_policy` and the sentences live here. A
11//! limit has no public constructor, no getter, no `Display` and no
12//! serde: a caller outside this crate can neither mint a looser limit
13//! nor read the figure out to format a sentence of its own. The
14//! compile-failure counterexample is `tests/ui/forge_policy_limit.rs`.
15
16use crate::error::{AxCode, AxError};
17use crate::locator::Locator;
18
19/// Whether an observation is past a limit: the limit itself is
20/// admitted, one more is refused. The one boundary rule, and the one
21/// place an observation is narrowed to `u64`.
22///
23/// `usize` is at most 64 bits on every target Rust builds, so the
24/// narrowing cannot fail there; the failing arm still answers "past the
25/// limit", which keeps the rule off any target's pointer width.
26fn exceeds(limit: u64, observed: usize) -> bool {
27 match u64::try_from(observed) {
28 Ok(count) => count > limit,
29 Err(_) => true,
30 }
31}
32
33/// How many pictures one turn may put on a provider request. The legal
34/// domain is `0..=max`: a turn carrying exactly the limit is sent, and
35/// one more picture is refused before any byte leaves for the provider.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct ImagesPerTurn(u64);
38
39impl ImagesPerTurn {
40 /// The only way a value of this type exists: `consts_policy` mints
41 /// the policy instance with it, and no caller outside this crate
42 /// can mint a looser one.
43 pub(crate) const fn new(max: u64) -> ImagesPerTurn {
44 ImagesPerTurn(max)
45 }
46
47 /// Whether this turn's pictures fit, refusing past the limit with
48 /// the one sentence that names it.
49 ///
50 /// # Errors
51 /// `E_INVALID_ARGS` when `found` is past the limit; the recovery
52 /// names the limit and what to do with the pictures that do not fit.
53 pub fn admit(self, found: usize) -> Result<(), AxError> {
54 let max = self.0;
55 if exceeds(max, found) {
56 return Err(AxError::failure(
57 AxCode::InvalidArgs,
58 "put pictures on a provider request",
59 format!("this turn carries {found} pictures"),
60 )
61 .with_recovery(format!(
62 "one turn carries at most {max} pictures; send the rest in a later turn"
63 )));
64 }
65 Ok(())
66 }
67}
68
69/// How many bytes one picture may carry to a provider. Sized so a
70/// base64 body of it stays inside what every provider this city calls
71/// accepts (the number's derivation is on the constant in
72/// `consts_policy`).
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct ImageMaxBytes(u64);
75
76impl ImageMaxBytes {
77 /// The only way a value of this type exists; see [`ImagesPerTurn::new`].
78 pub(crate) const fn new(max: u64) -> ImageMaxBytes {
79 ImageMaxBytes(max)
80 }
81
82 /// Whether this picture's bytes fit, refusing past the limit with
83 /// the one sentence that names it.
84 ///
85 /// # Errors
86 /// `E_INVALID_ARGS` when `size` is past the limit; the recovery
87 /// names the limit and what to do with the picture.
88 pub fn admit(self, at: &Locator, size: usize) -> Result<(), AxError> {
89 let max = self.0;
90 if exceeds(max, size) {
91 return Err(AxError::failure(
92 AxCode::InvalidArgs,
93 "put a picture on a provider request",
94 format!("{at} is {size} bytes"),
95 )
96 .with_recovery(format!(
97 "one picture is at most {max} bytes; shrink it before attaching it"
98 )));
99 }
100 Ok(())
101 }
102}
103
104/// The quality one lossy picture may be encoded at. The legal domain is
105/// `0..=max`: the maximum is admitted and one more is refused, because a
106/// caller that asks for more has asked for something no encoder has, and
107/// a clamped answer tells it the picture is better than it is.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct ImageQuality(u64);
110
111impl ImageQuality {
112 /// The only way a value of this type exists; see [`ImagesPerTurn::new`].
113 ///
114 /// The type has no getter, so a caller outside this crate cannot read
115 /// the domain out to format a sentence of its own — the one refusal
116 /// below is the whole of what it says.
117 pub(crate) const fn new(max: u64) -> ImageQuality {
118 ImageQuality(max)
119 }
120
121 /// Whether an encoder has this quality.
122 ///
123 /// # Errors
124 /// `E_INVALID_ARGS` when the asked-for quality is outside the domain;
125 /// the recovery names the domain.
126 pub fn admit(self, asked: u32) -> Result<(), AxError> {
127 if u64::from(asked) <= self.0 {
128 return Ok(());
129 }
130 Err(AxError::failure(
131 AxCode::InvalidArgs,
132 "encode a picture",
133 format!("quality {asked} is outside 0..={}", self.0),
134 )
135 .with_recovery(format!(
136 "pass a quality between 0 and {}; an encoder has no others",
137 self.0
138 )))
139 }
140}
141
142/// How many concern zones a clock stamp may carry, the UTC row not
143/// among them. Refused rather than dropped: a configured zone that
144/// silently stops rendering is a wrong stamp, and a wrong stamp is
145/// worse than no stamp.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct ClockZonesMax(u64);
148
149impl ClockZonesMax {
150 /// The only way a value of this type exists; see [`ImagesPerTurn::new`].
151 pub(crate) const fn new(max: u64) -> ClockZonesMax {
152 ClockZonesMax(max)
153 }
154
155 /// Whether these configured zones fit, refusing past the limit with
156 /// the one sentence that names it.
157 ///
158 /// # Errors
159 /// `E_INVALID_ARGS` when `configured` is past the limit; the
160 /// recovery names the limit and where a person edits the rows.
161 pub fn admit(self, configured: usize) -> Result<(), AxError> {
162 let max = self.0;
163 if exceeds(max, configured) {
164 return Err(AxError::failure(
165 AxCode::InvalidArgs,
166 "format clock stamp",
167 format!("{configured} zones exceed CLOCK_ZONES_MAX={max}"),
168 )
169 .with_recovery(format!(
170 "keep at most {max} rows in `[clock] zones`; the UTC row is added on top \
171 of them and is never one of them"
172 )));
173 }
174 Ok(())
175 }
176}
177
178#[cfg(test)]
179#[allow(
180 clippy::unwrap_used,
181 clippy::expect_used,
182 clippy::panic,
183 clippy::indexing_slicing,
184 reason = "test code"
185)]
186mod tests {
187 use super::*;
188 use crate::consts_policy::{CLOCK_ZONES_MAX, IMAGE_MAX_BYTES, IMAGE_QUALITY, IMAGES_PER_TURN};
189
190 fn a_picture() -> Locator {
191 Locator::parse(&format!("cas:b3-{}", "0".repeat(64))).unwrap()
192 }
193
194 /// The boundary is the limit itself admitted and one past it
195 /// refused, and the refusal is compared whole: the four parts are
196 /// the contract, so a wording change has to be deliberate.
197 #[test]
198 fn a_turn_carries_the_limit_and_one_picture_past_it_is_refused() {
199 assert!(IMAGES_PER_TURN.admit(4).is_ok());
200 let expected = AxError::failure(
201 AxCode::InvalidArgs,
202 "put pictures on a provider request",
203 "this turn carries 5 pictures",
204 )
205 .with_recovery("one turn carries at most 4 pictures; send the rest in a later turn");
206 assert_eq!(IMAGES_PER_TURN.admit(5).unwrap_err(), expected);
207 }
208
209 /// The same boundary, and the same whole comparison, for the quality
210 /// an encoder is asked for.
211 #[test]
212 fn the_quality_domain_admits_its_limit_and_refuses_one_past_it() {
213 assert!(IMAGE_QUALITY.admit(100).is_ok());
214 assert!(IMAGE_QUALITY.admit(0).is_ok());
215 let expected = AxError::failure(
216 AxCode::InvalidArgs,
217 "encode a picture",
218 "quality 101 is outside 0..=100",
219 )
220 .with_recovery("pass a quality between 0 and 100; an encoder has no others");
221 assert_eq!(IMAGE_QUALITY.admit(101).unwrap_err(), expected);
222 }
223
224 #[test]
225 fn a_picture_fits_at_the_ceiling_and_one_past_it_is_refused() {
226 let at = a_picture();
227 assert!(IMAGE_MAX_BYTES.admit(&at, 2_097_152).is_ok());
228 let expected = AxError::failure(
229 AxCode::InvalidArgs,
230 "put a picture on a provider request",
231 format!("{at} is 2097153 bytes"),
232 )
233 .with_recovery("one picture is at most 2097152 bytes; shrink it before attaching it");
234 assert_eq!(IMAGE_MAX_BYTES.admit(&at, 2_097_153).unwrap_err(), expected);
235 }
236
237 #[test]
238 fn the_zone_count_admits_the_limit_and_one_past_it_is_refused() {
239 assert!(CLOCK_ZONES_MAX.admit(4).is_ok());
240 let expected = AxError::failure(
241 AxCode::InvalidArgs,
242 "format clock stamp",
243 "5 zones exceed CLOCK_ZONES_MAX=4",
244 )
245 .with_recovery(
246 "keep at most 4 rows in `[clock] zones`; the UTC row is added on top of them and is \
247 never one of them",
248 );
249 assert_eq!(CLOCK_ZONES_MAX.admit(5).unwrap_err(), expected);
250 }
251}