otp_std/
period.rs

1//! Time-based One-Time Password (TOTP) periods.
2
3use std::{fmt, str::FromStr, time::Duration};
4
5use const_macros::{const_early, const_ok, const_try};
6
7use miette::Diagnostic;
8
9#[cfg(feature = "serde")]
10use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
11
12use thiserror::Error;
13
14use crate::{int, macros::errors};
15
16/// The minimum period value.
17pub const MIN: u64 = 1;
18
19/// The default period value.
20pub const DEFAULT: u64 = 30;
21
22/// Represents errors that can occur during period creation.
23///
24/// This error is returned when the given value is less than [`MIN`].
25#[derive(Debug, Error, Diagnostic)]
26#[error("expected period to be at least `{MIN}`, got `{value}`")]
27#[diagnostic(
28    code(otp_std::period),
29    help("make sure the period is at least `{MIN}`")
30)]
31pub struct Error {
32    /// The invalid value.
33    pub value: u64,
34}
35
36impl Error {
37    /// Constructs [`Self`].
38    pub const fn new(value: u64) -> Self {
39        Self { value }
40    }
41}
42
43/// Represents sources of errors that can occur when parsing [`Period`] values.
44#[derive(Debug, Error, Diagnostic)]
45#[error(transparent)]
46#[diagnostic(transparent)]
47pub enum ParseErrorSource {
48    /// Invalid period value.
49    Period(#[from] Error),
50    /// Integer parse error.
51    Int(#[from] int::ParseError),
52}
53
54/// Represents errors that occur when parsing [`Period`] values.
55#[derive(Debug, Error, Diagnostic)]
56#[error("failed to parse `{string}` to digits")]
57#[diagnostic(
58    code(otp_std::period::parse),
59    help("see the report for more information")
60)]
61pub struct ParseError {
62    /// The source of this error.
63    #[source]
64    #[diagnostic_source]
65    pub source: ParseErrorSource,
66    /// The string that could not be parsed.
67    pub string: String,
68}
69
70impl ParseError {
71    /// Constructs [`Self`].
72    pub const fn new(source: ParseErrorSource, string: String) -> Self {
73        Self { source, string }
74    }
75
76    /// Constructs [`Self`] from [`struct@Error`].
77    pub fn period(error: Error, string: String) -> Self {
78        Self::new(error.into(), string)
79    }
80
81    /// Constructs [`Self`] from [`int::ParseError`].
82    pub fn int(error: int::ParseError, string: String) -> Self {
83        Self::new(error.into(), string)
84    }
85}
86
87/// Represents time periods.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct Period {
90    value: u64,
91}
92
93#[cfg(feature = "serde")]
94impl Serialize for Period {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        self.get().serialize(serializer)
97    }
98}
99
100#[cfg(feature = "serde")]
101impl<'de> Deserialize<'de> for Period {
102    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103        let value = u64::deserialize(deserializer)?;
104
105        Self::new(value).map_err(de::Error::custom)
106    }
107}
108
109errors! {
110    Type = ParseError,
111    Hack = $,
112    int_error => int(error, string => to_owned),
113    period_error => period(error, string => to_owned),
114}
115
116impl FromStr for Period {
117    type Err = ParseError;
118
119    fn from_str(string: &str) -> Result<Self, Self::Err> {
120        let value = string
121            .parse()
122            .map_err(|error| int_error!(int::wrap(error), string))?;
123
124        Self::new(value).map_err(|error| period_error!(error, string))
125    }
126}
127
128impl fmt::Display for Period {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        self.get().fmt(formatter)
131    }
132}
133
134impl TryFrom<u64> for Period {
135    type Error = Error;
136
137    fn try_from(value: u64) -> Result<Self, Self::Error> {
138        Self::new(value)
139    }
140}
141
142impl From<Period> for u64 {
143    fn from(period: Period) -> Self {
144        period.get()
145    }
146}
147
148impl Default for Period {
149    fn default() -> Self {
150        Self::DEFAULT
151    }
152}
153
154errors! {
155    Type = Error,
156    Hack = $,
157    error => new(value),
158}
159
160impl Period {
161    /// Constructs [`Self`], if possible.
162    ///
163    /// # Errors
164    ///
165    /// See [`check`] for more information.
166    ///
167    /// [`check`]: Self::check
168    pub const fn new(value: u64) -> Result<Self, Error> {
169        const_try!(Self::check(value));
170
171        // SAFETY: the value is in the valid range for `Self`
172        Ok(unsafe { Self::new_unchecked(value) })
173    }
174
175    /// Similar to [`new`], but the error is discarded.
176    ///
177    /// [`new`]: Self::new
178    pub const fn new_ok(value: u64) -> Option<Self> {
179        const_ok!(Self::new(value))
180    }
181
182    /// Checks if the given value is valid for [`Self`].
183    ///
184    /// # Errors
185    ///
186    /// Returns [`struct@Error`] if the given value is less than [`MIN`].
187    pub const fn check(value: u64) -> Result<(), Error> {
188        const_early!(value < MIN => error!(value));
189
190        Ok(())
191    }
192
193    /// Constructs [`Self`] without checking the given value.
194    ///
195    /// # Safety
196    ///
197    /// The given value must be at least [`MIN`].
198    ///
199    /// This invariant can be checked using [`check`].
200    ///
201    /// [`check`]: Self::check
202    pub const unsafe fn new_unchecked(value: u64) -> Self {
203        Self { value }
204    }
205
206    /// Returns the value wrapped in [`Self`].
207    pub const fn get(self) -> u64 {
208        self.value
209    }
210
211    /// Returns the period as [`Duration`].
212    pub const fn as_duration(self) -> Duration {
213        Duration::from_secs(self.get())
214    }
215
216    /// The minimum [`Self`] value.
217    pub const MIN: Self = Self::new_ok(MIN).unwrap();
218
219    /// The default [`Self`] value.
220    pub const DEFAULT: Self = Self::new_ok(DEFAULT).unwrap();
221}