Skip to main content

ssh_cli/domain/
limits.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Timeout and character-budget newtypes.
3#![forbid(unsafe_code)]
4
5use super::error::DomainError;
6use crate::validation::{MAX_CHAR_LIMIT, MAX_TIMEOUT_MS};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::fmt;
9use std::num::NonZeroUsize;
10
11/// Timeout in milliseconds (`1..=MAX_TIMEOUT_MS`).
12#[repr(transparent)]
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct TimeoutMs(u64);
15
16impl TimeoutMs {
17    /// Parses a timeout in the allowed range.
18    pub fn try_new(ms: u64) -> Result<Self, DomainError> {
19        if ms == 0 || ms > MAX_TIMEOUT_MS {
20            return Err(DomainError::new(
21                "timeout_ms",
22                format!("timeout_ms must be 1..={MAX_TIMEOUT_MS}"),
23            ));
24        }
25        Ok(Self(ms))
26    }
27
28    /// Milliseconds value.
29    #[must_use]
30    pub const fn get(self) -> u64 {
31        self.0
32    }
33
34    /// Alias for [`Self::get`].
35    #[must_use]
36    pub const fn as_u64(self) -> u64 {
37        self.0
38    }
39}
40
41impl fmt::Display for TimeoutMs {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47impl Serialize for TimeoutMs {
48    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
49        s.serialize_u64(self.0)
50    }
51}
52
53impl<'de> Deserialize<'de> for TimeoutMs {
54    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
55        let v = u64::deserialize(d)?;
56        Self::try_new(v).map_err(serde::de::Error::custom)
57    }
58}
59
60/// Character budget: `0` on the wire means unlimited.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
62pub enum CharLimit {
63    /// No limit (wire `0`).
64    Unlimited,
65    /// Finite limit `1..=MAX_CHAR_LIMIT`.
66    Limited(NonZeroUsize),
67}
68
69impl CharLimit {
70    /// Parses wire/config `usize` (`0` → unlimited).
71    pub fn try_new(n: usize) -> Result<Self, DomainError> {
72        if n == 0 {
73            return Ok(Self::Unlimited);
74        }
75        if n > MAX_CHAR_LIMIT {
76            return Err(DomainError::new(
77                "char_limit",
78                format!("char limit must be 0..={MAX_CHAR_LIMIT}"),
79            ));
80        }
81        Ok(Self::Limited(NonZeroUsize::new(n).expect("n > 0")))
82    }
83
84    /// Effective comparison limit (`usize::MAX` when unlimited).
85    #[must_use]
86    pub fn effective(self) -> usize {
87        match self {
88            Self::Unlimited => usize::MAX,
89            Self::Limited(n) => n.get(),
90        }
91    }
92
93    /// Wire / config representation (`0` = unlimited).
94    #[must_use]
95    pub fn wire(self) -> usize {
96        match self {
97            Self::Unlimited => 0,
98            Self::Limited(n) => n.get(),
99        }
100    }
101}
102
103impl Serialize for CharLimit {
104    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
105        s.serialize_u64(self.wire() as u64)
106    }
107}
108
109impl<'de> Deserialize<'de> for CharLimit {
110    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
111        let v = u64::deserialize(d)?;
112        let n = usize::try_from(v).map_err(serde::de::Error::custom)?;
113        Self::try_new(n).map_err(serde::de::Error::custom)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::validation::{MAX_CHAR_LIMIT, MAX_TIMEOUT_MS};
121
122    #[test]
123    fn timeout_range() {
124        assert!(TimeoutMs::try_new(0).is_err());
125        assert!(TimeoutMs::try_new(MAX_TIMEOUT_MS + 1).is_err());
126        assert_eq!(TimeoutMs::try_new(1000).unwrap().get(), 1000);
127    }
128
129    #[test]
130    fn char_limit() {
131        assert_eq!(CharLimit::try_new(0).unwrap().effective(), usize::MAX);
132        assert_eq!(CharLimit::try_new(10).unwrap().wire(), 10);
133        assert!(CharLimit::try_new(MAX_CHAR_LIMIT + 1).is_err());
134    }
135}