1use std::num::{NonZeroU64, NonZeroUsize};
7
8use serde::{Deserialize, Serialize};
9
10use super::bounded_number::impl_bounded_number;
11use super::text_newtype::impl_text_newtype;
12
13pub const DEFAULT_HEURISTICS_MAX_DEPTH: usize = 10;
15
16pub const MAX_TIMEOUT_SECONDS: u64 = 900;
34
35pub const MAX_HEURISTICS_DEPTH: usize = 64;
44
45#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
47#[error("max_concurrent_server_starts must be at least 1")]
48pub struct InvalidServerStartConcurrency;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(try_from = "usize", into = "usize")]
65pub struct ServerStartConcurrency(NonZeroUsize);
66
67impl ServerStartConcurrency {
68 pub const fn new(limit: usize) -> Result<Self, InvalidServerStartConcurrency> {
74 match NonZeroUsize::new(limit) {
75 Some(limit) => Ok(Self(limit)),
76 None => Err(InvalidServerStartConcurrency),
77 }
78 }
79
80 #[must_use]
82 pub const fn get(self) -> usize {
83 self.0.get()
84 }
85}
86
87impl_bounded_number!(
88 ServerStartConcurrency,
89 usize,
90 InvalidServerStartConcurrency,
91 default = 8,
92 "Eight servers at a time.",
93 into = ServerStartConcurrency::get
94);
95
96#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
98#[error("heuristics_max_depth ({value}) exceeds the maximum of {max}")]
99pub struct InvalidSearchDepth {
100 pub value: usize,
102 pub max: usize,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(try_from = "usize", into = "usize")]
123pub struct SearchDepth(u8);
124
125impl SearchDepth {
126 pub const fn new(depth: usize) -> Result<Self, InvalidSearchDepth> {
132 const { assert!(MAX_HEURISTICS_DEPTH <= u8::MAX as usize) };
133 if depth > MAX_HEURISTICS_DEPTH {
134 return Err(InvalidSearchDepth {
135 value: depth,
136 max: MAX_HEURISTICS_DEPTH,
137 });
138 }
139 #[expect(
140 clippy::cast_possible_truncation,
141 reason = "bounded by MAX_HEURISTICS_DEPTH, asserted to fit u8 above"
142 )]
143 Ok(Self(depth as u8))
144 }
145
146 #[must_use]
148 pub const fn get(self) -> usize {
149 self.0 as usize
150 }
151}
152
153impl_bounded_number!(
154 SearchDepth,
155 usize,
156 InvalidSearchDepth,
157 default = DEFAULT_HEURISTICS_MAX_DEPTH,
158 "The built-in default, [`DEFAULT_HEURISTICS_MAX_DEPTH`].",
159 into = SearchDepth::get
160);
161
162pub const MAX_FILE_SIZE_LIMIT: u64 = 1 << 30;
168
169#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
171#[error(
172 "max_file_size ({value}) exceeds the hard cap of {max} bytes; use a lower value, or 0 to \
173 disable the per-file limit"
174)]
175pub struct InvalidSizeLimit {
176 pub value: u64,
178 pub max: u64,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
202#[serde(try_from = "u64", into = "u64")]
203pub struct SizeLimit(Option<NonZeroU64>);
204
205impl SizeLimit {
206 pub const UNLIMITED: Self = Self(None);
208
209 pub const fn new(bytes: u64) -> Result<Self, InvalidSizeLimit> {
215 if bytes > MAX_FILE_SIZE_LIMIT {
216 return Err(InvalidSizeLimit {
217 value: bytes,
218 max: MAX_FILE_SIZE_LIMIT,
219 });
220 }
221 Ok(Self(NonZeroU64::new(bytes)))
222 }
223
224 #[must_use]
231 pub const fn from_static(bytes: u64) -> Self {
232 match Self::new(bytes) {
233 Ok(limit) => limit,
234 Err(_) => panic!("size limit above MAX_FILE_SIZE_LIMIT"),
235 }
236 }
237
238 #[must_use]
240 pub const fn get(self) -> Option<NonZeroU64> {
241 self.0
242 }
243
244 #[must_use]
246 pub const fn admits(self, bytes: u64) -> bool {
247 match self.0 {
248 Some(max) => bytes <= max.get(),
249 None => true,
250 }
251 }
252
253 #[must_use]
257 pub const fn read_cap(self) -> u64 {
258 match self.0 {
259 Some(max) => Self::read_cap_for(max),
260 None => u64::MAX,
261 }
262 }
263
264 #[must_use]
266 pub(crate) const fn read_cap_for(max: NonZeroU64) -> u64 {
267 max.get().saturating_add(1)
268 }
269}
270
271impl_bounded_number!(
272 SizeLimit,
273 u64,
274 InvalidSizeLimit,
275 default = 10 * 1024 * 1024,
276 "The built-in default, 10 MiB.",
277 into = |limit: SizeLimit| limit.0.map_or(0, NonZeroU64::get)
278);
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
294#[serde(from = "usize", into = "usize")]
295pub struct DocumentLimit(Option<NonZeroUsize>);
296
297impl DocumentLimit {
298 pub const UNLIMITED: Self = Self(None);
300
301 pub const DEFAULT: Self = Self::new(100);
303
304 #[must_use]
306 pub const fn new(count: usize) -> Self {
307 Self(NonZeroUsize::new(count))
308 }
309
310 #[must_use]
312 pub const fn get(self) -> Option<NonZeroUsize> {
313 self.0
314 }
315}
316
317impl Default for DocumentLimit {
318 fn default() -> Self {
319 Self::DEFAULT
320 }
321}
322
323impl From<usize> for DocumentLimit {
324 fn from(count: usize) -> Self {
325 Self::new(count)
326 }
327}
328
329impl From<DocumentLimit> for usize {
330 fn from(limit: DocumentLimit) -> Self {
331 limit.0.map_or(0, NonZeroUsize::get)
332 }
333}
334
335#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
337pub enum InvalidBoundedText {
338 #[error("cannot be empty")]
340 Blank,
341 #[error("exceeds the maximum of {max} bytes ({len} given)")]
343 TooLong {
344 len: usize,
346 max: usize,
348 },
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
367#[serde(try_from = "String", into = "String")]
368pub struct BoundedText<const MAX: usize>(String);
369
370impl<const MAX: usize> BoundedText<MAX> {
371 pub fn new(text: impl Into<String>) -> Result<Self, InvalidBoundedText> {
378 let text = text.into();
379 if text.trim().is_empty() {
380 return Err(InvalidBoundedText::Blank);
381 }
382 let len = text.len();
383 if len > MAX {
384 return Err(InvalidBoundedText::TooLong { len, max: MAX });
385 }
386 Ok(Self(text))
387 }
388}
389
390impl_text_newtype!(@impl [const MAX: usize] BoundedText<MAX>, InvalidBoundedText);
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_bounded_text_blank_is_reported_before_length() {
398 assert_eq!(BoundedText::<4>::new(""), Err(InvalidBoundedText::Blank));
399 assert_eq!(
400 BoundedText::<4>::new(" "),
401 Err(InvalidBoundedText::Blank)
402 );
403 }
404
405 #[test]
406 fn test_bounded_text_counts_bytes_not_chars() {
407 assert!(BoundedText::<4>::new("éé").is_ok());
408 assert_eq!(
409 BoundedText::<4>::new("ééé"),
410 Err(InvalidBoundedText::TooLong { len: 6, max: 4 })
411 );
412 assert_eq!(
413 BoundedText::<4>::new("ééé").unwrap_err().to_string(),
414 "exceeds the maximum of 4 bytes (6 given)"
415 );
416 }
417
418 #[test]
419 fn test_bounded_text_has_the_shared_text_conversions() {
420 let text: BoundedText<8> = "hello".parse().unwrap();
421 assert_eq!(text.as_ref() as &str, "hello");
422 assert_eq!(std::borrow::Borrow::<str>::borrow(&text), "hello");
423 assert_eq!(text.to_string(), "hello");
424 assert_eq!(String::from(text), "hello");
425 assert!("123456789".parse::<BoundedText<8>>().is_err());
426 }
427
428 #[test]
429 fn test_bounded_text_serde() {
430 #[derive(Debug, Serialize, Deserialize, PartialEq)]
431 struct Holder {
432 text: BoundedText<5>,
433 }
434
435 let holder: Holder = toml::from_str(r#"text = "abc""#).unwrap();
436 assert_eq!(holder.text, "abc");
437 assert_eq!(toml::to_string(&holder).unwrap().trim(), r#"text = "abc""#);
438 assert!(toml::from_str::<Holder>(r#"text = "abcdef""#).is_err());
439 assert!(toml::from_str::<Holder>(r#"text = " ""#).is_err());
440 }
441
442 #[test]
443 fn test_size_limit_zero_is_unlimited_and_cap_is_enforced() {
444 assert_eq!(SizeLimit::new(0), Ok(SizeLimit::UNLIMITED));
445 assert!(SizeLimit::new(MAX_FILE_SIZE_LIMIT).is_ok());
446 assert_eq!(
447 SizeLimit::new(MAX_FILE_SIZE_LIMIT + 1),
448 Err(InvalidSizeLimit {
449 value: MAX_FILE_SIZE_LIMIT + 1,
450 max: MAX_FILE_SIZE_LIMIT
451 })
452 );
453 }
454
455 #[test]
456 fn test_size_limit_admits_up_to_and_including_the_limit() {
457 let limit = SizeLimit::from_static(4);
458 assert!(limit.admits(0));
459 assert!(limit.admits(4));
460 assert!(!limit.admits(5));
461 assert!(SizeLimit::UNLIMITED.admits(u64::MAX));
462 }
463
464 #[test]
465 fn test_read_cap_for_is_the_limit_plus_one_and_saturates() {
466 assert_eq!(SizeLimit::read_cap_for(NonZeroU64::new(100).unwrap()), 101);
467 assert_eq!(SizeLimit::read_cap_for(NonZeroU64::MAX), u64::MAX);
468 }
469
470 #[test]
471 fn test_server_start_concurrency_rejects_zero_with_a_typed_error() {
472 assert_eq!(
473 ServerStartConcurrency::new(0),
474 Err(InvalidServerStartConcurrency)
475 );
476 assert_eq!(ServerStartConcurrency::new(3).unwrap().get(), 3);
477 }
478
479 #[test]
480 fn test_size_limit_read_cap_is_limit_plus_one_or_unbounded() {
481 assert_eq!(SizeLimit::from_static(100).read_cap(), 101);
482 assert_eq!(
483 SizeLimit::from_static(MAX_FILE_SIZE_LIMIT).read_cap(),
484 MAX_FILE_SIZE_LIMIT + 1
485 );
486 assert_eq!(SizeLimit::UNLIMITED.read_cap(), u64::MAX);
487 }
488
489 #[test]
490 fn test_size_limit_and_document_limit_serde_use_zero_for_unlimited() {
491 #[derive(Debug, Serialize, Deserialize, PartialEq)]
492 struct Holder {
493 size: SizeLimit,
494 docs: DocumentLimit,
495 }
496
497 let holder: Holder = toml::from_str("size = 0\ndocs = 0").unwrap();
498 assert_eq!(holder.size, SizeLimit::UNLIMITED);
499 assert_eq!(holder.docs, DocumentLimit::UNLIMITED);
500 assert_eq!(
501 toml::to_string(&holder).unwrap().trim(),
502 "size = 0\ndocs = 0"
503 );
504 let holder: Holder = toml::from_str("size = 7\ndocs = 3").unwrap();
505 assert_eq!(holder.size.get().unwrap().get(), 7);
506 assert_eq!(holder.docs.get().unwrap().get(), 3);
507 assert!(toml::from_str::<Holder>("size = 1073741825\ndocs = 1").is_err());
508 assert!(toml::from_str::<Holder>("size = 1\ndocs = -1").is_err());
509 }
510
511 #[test]
512 fn test_defaults_match_documented_values() {
513 assert_eq!(SizeLimit::default().get().unwrap().get(), 10 * 1024 * 1024);
514 assert_eq!(DocumentLimit::default().get().unwrap().get(), 100);
515 }
516
517 #[test]
518 fn test_search_depth_bounds() {
519 assert_eq!(SearchDepth::new(0).unwrap().get(), 0);
520 assert_eq!(
521 SearchDepth::new(MAX_HEURISTICS_DEPTH).unwrap().get(),
522 MAX_HEURISTICS_DEPTH
523 );
524 assert!(SearchDepth::new(MAX_HEURISTICS_DEPTH + 1).is_err());
525 assert_eq!(SearchDepth::default().get(), DEFAULT_HEURISTICS_MAX_DEPTH);
526 }
527
528 #[test]
529 fn test_search_depth_error_reports_range() {
530 let err = SearchDepth::try_from(999_999).unwrap_err();
531 assert_eq!(
532 err.to_string(),
533 "heuristics_max_depth (999999) exceeds the maximum of 64"
534 );
535 }
536
537 #[test]
538 fn test_search_depth_serde() {
539 #[derive(Debug, Serialize, Deserialize, PartialEq)]
540 struct Holder {
541 depth: SearchDepth,
542 }
543
544 let holder: Holder = toml::from_str("depth = 5").unwrap();
545 assert_eq!(holder.depth.get(), 5);
546 assert_eq!(toml::to_string(&holder).unwrap().trim(), "depth = 5");
547 assert!(toml::from_str::<Holder>("depth = 65").is_err());
548 assert!(toml::from_str::<Holder>("depth = -1").is_err());
549 }
550}