1use serde::Serialize;
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct LimitMeasurement {
9 measured: usize,
10 allowed: usize,
11}
12
13impl LimitMeasurement {
14 #[must_use]
16 pub const fn measured(self) -> usize {
17 self.measured
18 }
19
20 #[must_use]
22 pub const fn allowed(self) -> usize {
23 self.allowed
24 }
25}
26
27#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
29#[error("measured {measured} exceeds allowed {allowed}")]
30pub struct LimitExceeded {
31 measured: usize,
32 allowed: usize,
33}
34
35impl LimitExceeded {
36 #[must_use]
38 pub const fn measured(self) -> usize {
39 self.measured
40 }
41
42 #[must_use]
44 pub const fn allowed(self) -> usize {
45 self.allowed
46 }
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct LimitRange {
52 measured: u64,
53 minimum: u64,
54 maximum: u64,
55}
56
57impl LimitRange {
58 #[must_use]
60 pub const fn measured(self) -> u64 {
61 self.measured
62 }
63
64 #[must_use]
66 pub const fn minimum(self) -> u64 {
67 self.minimum
68 }
69
70 #[must_use]
72 pub const fn maximum(self) -> u64 {
73 self.maximum
74 }
75}
76
77#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
79#[error("measured {measured} is outside {minimum}..={maximum}")]
80pub struct LimitOutOfRange {
81 measured: u64,
82 minimum: u64,
83 maximum: u64,
84}
85
86impl LimitOutOfRange {
87 #[must_use]
89 pub const fn measured(self) -> u64 {
90 self.measured
91 }
92
93 #[must_use]
95 pub const fn minimum(self) -> u64 {
96 self.minimum
97 }
98
99 #[must_use]
101 pub const fn maximum(self) -> u64 {
102 self.maximum
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
108#[error("minimum {minimum} exceeds maximum {maximum}")]
109pub struct InvalidLimitRange {
110 minimum: u64,
111 maximum: u64,
112}
113
114impl InvalidLimitRange {
115 #[must_use]
117 pub const fn minimum(self) -> u64 {
118 self.minimum
119 }
120
121 #[must_use]
123 pub const fn maximum(self) -> u64 {
124 self.maximum
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
130pub enum LimitRangeError {
131 #[error(transparent)]
133 InvalidBounds(#[from] InvalidLimitRange),
134 #[error(transparent)]
136 OutOfRange(#[from] LimitOutOfRange),
137}
138
139#[derive(Debug, Error)]
141pub enum CompactJsonLimitError {
142 #[error("compact JSON encoding failed")]
144 Encoding(#[from] serde_json::Error),
145 #[error(transparent)]
147 Limit(#[from] LimitExceeded),
148}
149
150#[must_use]
152pub fn trimmed_unicode_scalar_count(value: &str) -> usize {
153 value.trim().chars().count()
154}
155
156pub fn compact_json_utf8_bytes(value: &impl Serialize) -> Result<usize, serde_json::Error> {
158 serde_json::to_vec(value).map(|encoded| encoded.len())
159}
160
161#[must_use]
163pub const fn byte_length(value: &[u8]) -> usize {
164 value.len()
165}
166
167#[must_use]
169pub const fn collection_length<Element>(value: &[Element]) -> usize {
170 value.len()
171}
172
173pub const fn check_measurement(
175 measured: usize,
176 allowed: usize,
177) -> Result<LimitMeasurement, LimitExceeded> {
178 if measured > allowed {
179 return Err(LimitExceeded { measured, allowed });
180 }
181 Ok(LimitMeasurement { measured, allowed })
182}
183
184pub const fn check_range(
189 measured: u64,
190 minimum: u64,
191 maximum: u64,
192) -> Result<LimitRange, LimitRangeError> {
193 if minimum > maximum {
194 return Err(LimitRangeError::InvalidBounds(InvalidLimitRange {
195 minimum,
196 maximum,
197 }));
198 }
199 if measured < minimum || measured > maximum {
200 return Err(LimitRangeError::OutOfRange(LimitOutOfRange {
201 measured,
202 minimum,
203 maximum,
204 }));
205 }
206 Ok(LimitRange {
207 measured,
208 minimum,
209 maximum,
210 })
211}
212
213pub fn check_trimmed_unicode_scalars(
215 value: &str,
216 allowed: usize,
217) -> Result<LimitMeasurement, LimitExceeded> {
218 check_measurement(trimmed_unicode_scalar_count(value), allowed)
219}
220
221pub fn check_compact_json_utf8_bytes(
223 value: &impl Serialize,
224 allowed: usize,
225) -> Result<LimitMeasurement, CompactJsonLimitError> {
226 let measured = compact_json_utf8_bytes(value)?;
227 check_measurement(measured, allowed).map_err(Into::into)
228}
229
230pub const fn check_bytes(value: &[u8], allowed: usize) -> Result<LimitMeasurement, LimitExceeded> {
232 check_measurement(byte_length(value), allowed)
233}
234
235pub const fn check_collection<Element>(
237 value: &[Element],
238 allowed: usize,
239) -> Result<LimitMeasurement, LimitExceeded> {
240 check_measurement(collection_length(value), allowed)
241}
242
243#[cfg(test)]
244mod tests {
245 use serde::Deserialize;
246 use serde_json::{Value, json};
247
248 use super::*;
249
250 #[derive(Debug, Deserialize)]
251 struct FixtureCorpus {
252 version: u8,
253 text: Vec<TextFixture>,
254 json: Vec<JsonFixture>,
255 bytes: Vec<BytesFixture>,
256 collections: Vec<CollectionFixture>,
257 ranges: Vec<RangeFixture>,
258 }
259
260 #[derive(Debug, Deserialize)]
261 struct TextFixture {
262 name: String,
263 value: String,
264 trimmed_unicode_scalars: usize,
265 }
266
267 #[derive(Debug, Deserialize)]
268 struct JsonFixture {
269 name: String,
270 value: Value,
271 compact: String,
272 utf8_bytes: usize,
273 }
274
275 #[derive(Debug, Deserialize)]
276 struct BytesFixture {
277 name: String,
278 value: Vec<u8>,
279 bytes: usize,
280 }
281
282 #[derive(Debug, Deserialize)]
283 struct CollectionFixture {
284 name: String,
285 value: Vec<Value>,
286 elements: usize,
287 }
288
289 #[derive(Debug, Deserialize)]
290 struct RangeFixture {
291 name: String,
292 measured: u64,
293 minimum: u64,
294 maximum: u64,
295 outcome: String,
296 }
297
298 fn fixtures() -> FixtureCorpus {
299 serde_json::from_str(include_str!(
300 "../../../../fixtures/limits/resource-budget-measurements-v1.json"
301 ))
302 .expect("resource-budget fixture should parse")
303 }
304
305 #[test]
306 fn shared_ranges_check_inclusive_bounds_and_distinguish_invalid_bounds() {
307 let ranges = fixtures().ranges;
308 assert_eq!(ranges.len(), 11);
309 for fixture in ranges {
310 let result = check_range(fixture.measured, fixture.minimum, fixture.maximum);
311 match fixture.outcome.as_str() {
312 "ok" => {
313 let measurement = result.expect(&fixture.name);
314 assert_eq!(measurement.measured(), fixture.measured, "{}", fixture.name);
315 assert_eq!(measurement.minimum(), fixture.minimum, "{}", fixture.name);
316 assert_eq!(measurement.maximum(), fixture.maximum, "{}", fixture.name);
317 }
318 "out_of_range" => {
319 let Err(LimitRangeError::OutOfRange(error)) = result else {
320 panic!(
321 "{}: expected out-of-range error, got {result:?}",
322 fixture.name
323 );
324 };
325 assert_eq!(error.measured(), fixture.measured, "{}", fixture.name);
326 assert_eq!(error.minimum(), fixture.minimum, "{}", fixture.name);
327 assert_eq!(error.maximum(), fixture.maximum, "{}", fixture.name);
328 }
329 "invalid_bounds" => {
330 let Err(LimitRangeError::InvalidBounds(error)) = result else {
331 panic!("{}: expected invalid bounds, got {result:?}", fixture.name);
332 };
333 assert_eq!(error.minimum(), fixture.minimum, "{}", fixture.name);
334 assert_eq!(error.maximum(), fixture.maximum, "{}", fixture.name);
335 }
336 outcome => panic!("unknown range fixture outcome {outcome}"),
337 }
338 }
339 }
340
341 #[test]
342 fn range_checks_accept_the_full_duration_seconds_width() {
343 assert_eq!(
344 check_range(u64::MAX, 0, u64::MAX)
345 .expect("full-width duration should fit")
346 .measured(),
347 u64::MAX
348 );
349 assert!(matches!(
350 check_range(0, u64::MAX, 0),
351 Err(LimitRangeError::InvalidBounds(_))
352 ));
353 }
354
355 #[test]
356 fn counts_ascii_composed_decomposed_and_emoji_scalars() {
357 assert_eq!(trimmed_unicode_scalar_count(" plain "), 5);
358 assert_eq!(trimmed_unicode_scalar_count("\u{e9}"), 1);
359 assert_eq!(trimmed_unicode_scalar_count("e\u{301}"), 2);
360 assert_eq!(
361 trimmed_unicode_scalar_count(
362 "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}"
363 ),
364 7
365 );
366 }
367
368 #[test]
369 fn measures_compact_objects_arrays_escapes_and_multibyte_strings() {
370 for fixture in fixtures().json {
371 let encoded = serde_json::to_string(&fixture.value).expect("fixture should encode");
372 assert_eq!(encoded, fixture.compact, "{}", fixture.name);
373 assert_eq!(
374 compact_json_utf8_bytes(&fixture.value).expect("fixture should encode"),
375 fixture.utf8_bytes,
376 "{}",
377 fixture.name
378 );
379 }
380 }
381
382 #[test]
383 fn byte_and_collection_checks_report_both_sides_of_the_boundary() {
384 let bytes = [1_u8, 2, 3];
385 let values = ["a", "b", "c"];
386
387 assert_eq!(
388 check_bytes(&bytes, 3)
389 .expect("boundary should pass")
390 .measured(),
391 3
392 );
393 assert_eq!(
394 check_collection(&values, 3)
395 .expect("boundary should pass")
396 .allowed(),
397 3
398 );
399 assert_eq!(
400 check_bytes(&bytes, 2),
401 Err(LimitExceeded {
402 measured: 3,
403 allowed: 2,
404 })
405 );
406 assert_eq!(
407 check_collection(&values, 2),
408 Err(LimitExceeded {
409 measured: 3,
410 allowed: 2,
411 })
412 );
413 }
414
415 #[test]
416 fn specialized_checks_return_measurement_and_allowed_values() {
417 assert_eq!(
418 check_trimmed_unicode_scalars(" e\u{301} ", 2),
419 Ok(LimitMeasurement {
420 measured: 2,
421 allowed: 2,
422 })
423 );
424 let error = check_compact_json_utf8_bytes(&json!({"value": "\u{e9}"}), 13)
425 .expect_err("fourteen-byte document should fail");
426 assert!(matches!(
427 error,
428 CompactJsonLimitError::Limit(LimitExceeded {
429 measured: 14,
430 allowed: 13,
431 })
432 ));
433 }
434
435 #[test]
436 fn shared_fixture_matches_all_rust_measurements() {
437 let fixtures = fixtures();
438 assert_eq!(fixtures.version, 1);
439 assert!(!fixtures.text.is_empty());
440 assert!(!fixtures.json.is_empty());
441 assert!(!fixtures.bytes.is_empty());
442 assert!(!fixtures.collections.is_empty());
443
444 for fixture in fixtures.text {
445 assert_eq!(
446 trimmed_unicode_scalar_count(&fixture.value),
447 fixture.trimmed_unicode_scalars,
448 "{}",
449 fixture.name
450 );
451 }
452 for fixture in fixtures.bytes {
453 assert_eq!(
454 byte_length(&fixture.value),
455 fixture.bytes,
456 "{}",
457 fixture.name
458 );
459 }
460 for fixture in fixtures.collections {
461 assert_eq!(
462 collection_length(&fixture.value),
463 fixture.elements,
464 "{}",
465 fixture.name
466 );
467 }
468 }
469}