Skip to main content

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}