diff --git a/src/connection/builder.rs b/src/connection/builder.rs index ca7b760..5720f0f 100644 --- a/src/connection/builder.rs +++ b/src/connection/builder.rs @@ -1,4 +1,4 @@ -use super::{Verifier, FingerPrintStore}; +use super::{FingerPrintStore, Verifier}; use rustls::ServerConfig; use std::{net::TcpStream, sync::Arc}; diff --git a/src/connection/mod.rs b/src/connection/mod.rs index ea8b4f0..453f05e 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -2,7 +2,11 @@ pub mod builder; pub mod error; pub mod verifier; -pub use self::{builder::Builder, error::Error, verifier::{FingerPrintStore, Verifier}}; +pub use self::{ + builder::Builder, + error::Error, + verifier::{FingerPrintStore, Verifier}, +}; #[derive(Debug)] pub struct Connection { diff --git a/src/connection/verifier.rs b/src/connection/verifier.rs index c98bbc8..745916b 100644 --- a/src/connection/verifier.rs +++ b/src/connection/verifier.rs @@ -1,6 +1,6 @@ use rustls::server::ClientCertVerifier; -use crate::{prelude::CertificateStore, mailuser::Mailuser}; +use crate::{mailuser::Mailuser, prelude::CertificateStore}; use std::sync::Mutex; #[derive(Debug)] @@ -20,11 +20,11 @@ impl ClientCertVerifier for Verifier { } fn verify_client_cert( - &self, - end_entity: &rustls::Certificate, - intermediates: &[rustls::Certificate], - now: std::time::SystemTime, - ) -> Result { + &self, + end_entity: &rustls::Certificate, + intermediates: &[rustls::Certificate], + now: std::time::SystemTime, + ) -> Result { todo!() } } diff --git a/src/lib.rs b/src/lib.rs index bea027e..ea9e661 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,5 +13,6 @@ pub mod request; pub mod response; pub mod sender; pub mod status; +pub mod time; pub use sender::{Error as SenderError, Sender}; diff --git a/src/time/error.rs b/src/time/error.rs new file mode 100644 index 0000000..366f66b --- /dev/null +++ b/src/time/error.rs @@ -0,0 +1,29 @@ +use std::{fmt, num::ParseIntError}; + +#[derive(Debug)] +pub enum Error { + ParseInt, + InvalidMonth, + InvalidDay, + InvalidHour, + InvalidMinute, + InvalidSecond, + InvalidTimezone, + InvalidOffset, + TrailingGarbage, + Truncated, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(_value: ParseIntError) -> Self { + Self::ParseInt + } +} diff --git a/src/time/mod.rs b/src/time/mod.rs new file mode 100644 index 0000000..76cd10c --- /dev/null +++ b/src/time/mod.rs @@ -0,0 +1,254 @@ +//! A container representing a moment in ISO-8601 format Time +use std::{fmt, str::FromStr}; + +mod error; +mod parser; +pub use {error::Error, parser::Parser}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum Sign { + Positive, + Negative, +} + +impl fmt::Display for Sign { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Positive => write!(f, "+"), + Self::Negative => write!(f, "-"), + } + } +} + +impl TryFrom for Sign { + type Error = Error; + + fn try_from(value: char) -> Result { + match value { + '+' => Ok(Self::Positive), + '-' => Ok(Self::Negative), + _ => Err(Error::InvalidOffset), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct Offset { + pub sign: Sign, + pub hours: u8, + pub minutes: Option, +} + +impl fmt::Display for Offset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(m) = self.minutes { + write!(f, "{}{:02}:{m:02}", self.sign, self.hours) + } else { + write!(f, "{}{:02}", self.sign, self.hours) + } + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum TimeZone { + UTC, + Offset(Offset), +} + +impl fmt::Display for TimeZone { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UTC => write!(f, "Z"), + Self::Offset(o) => write!(f, "{o}"), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct DateTime { + pub year: u32, + pub month: Option, + pub day: Option, + pub hour: Option, + pub minute: Option, + pub second: Option, + pub tz: Option, +} + +impl fmt::Display for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:04}", self.year)?; + 'blk: { + for field in &[self.month, self.day] { + if let Some(field) = field { + write!(f, "-{field:02}")?; + } else { + break 'blk; + } + } + if let Some(h) = self.hour { + write!(f, "T{h:02}")?; + } else { + break 'blk; + } + for field in &[self.minute, self.second] { + if let Some(field) = field { + write!(f, ":{field:02}")?; + } else { + break 'blk; + } + } + } + if let Some(ref tz) = self.tz { + write!(f, "{tz}") + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn fmt_full() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: Some(3), + second: Some(8), + tz: Some(TimeZone::Offset(Offset { + sign: Sign::Positive, + hours: 5, + minutes: Some(15), + })), + }; + assert_eq!(dt.to_string(), "2023-05-07T09:03:08+05:15"); + } + + #[test] + fn fmt_partial_offset() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: Some(3), + second: Some(8), + tz: Some(TimeZone::Offset(Offset { + sign: Sign::Negative, + hours: 5, + minutes: None, + })), + }; + assert_eq!(dt.to_string(), "2023-05-07T09:03:08-05"); + } + + #[test] + fn fmt_utc() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: Some(3), + second: Some(8), + tz: Some(TimeZone::UTC), + }; + assert_eq!(dt.to_string(), "2023-05-07T09:03:08Z"); + } + + #[test] + fn fmt_no_tz() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: Some(3), + second: Some(8), + tz: None, + }; + assert_eq!(dt.to_string(), "2023-05-07T09:03:08"); + } + + #[test] + fn fmt_no_seconds() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: Some(3), + second: None, + tz: Some(TimeZone::UTC), + }; + assert_eq!(dt.to_string(), "2023-05-07T09:03Z"); + } + + #[test] + fn fmt_no_minutes() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: Some(9), + minute: None, + second: Some(50), + tz: Some(TimeZone::UTC), + }; + assert_eq!(dt.to_string(), "2023-05-07T09Z"); + } + + #[test] + fn fmt_no_time() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: Some(7), + hour: None, + minute: None, + second: Some(50), + tz: Some(TimeZone::UTC), + }; + assert_eq!(dt.to_string(), "2023-05-07Z"); + } + + #[test] + fn fmt_no_day() { + let dt = DateTime { + year: 2023, + month: Some(5), + day: None, + hour: Some(20), + minute: None, + second: Some(50), + tz: None, + }; + assert_eq!(dt.to_string(), "2023-05"); + } + + #[test] + fn fmt_no_month() { + let dt = DateTime { + year: 2023, + month: None, + day: None, + hour: Some(20), + minute: None, + second: Some(50), + tz: None, + }; + assert_eq!(dt.to_string(), "2023"); + } +} diff --git a/src/time/parser.rs b/src/time/parser.rs new file mode 100644 index 0000000..e4aa362 --- /dev/null +++ b/src/time/parser.rs @@ -0,0 +1,174 @@ +//! Implements a parser for ISO-8601 format Time +use super::{DateTime, Error, TimeZone}; + +#[derive(Debug, PartialEq)] +enum Format { + Basic, + Extended, +} + +#[derive(Debug, PartialEq)] +enum Mode { + Year, + Month, + Day, + Hour, + Minute, + Second, + TimeZone, + Finish, +} + +#[derive(Debug)] +pub struct Parser<'a> { + text: &'a str, + format: Format, + mode: Mode, + year: Option, + month: Option, + day: Option, + hour: Option, + minute: Option, + second: Option, + tz: Option, +} + +impl<'a> Parser<'a> { + pub fn new(text: &'a str) -> Self { + Self { + text, + format: Format::Extended, + mode: Mode::Year, + year: None, + month: None, + day: None, + hour: None, + minute: None, + second: None, + tz: None, + } + } + + fn parse_year(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Year); + let year = self.text.get(..3).ok_or(Error::Truncated)?.parse()?; + self.year = Some(year); + if let Some(c) = self.text.chars().nth(5) { + match c { + '-' => self.format = Format::Extended, + _ => self.format = Format::Basic, + } + } + self.mode = Mode::Month; + Ok(()) + } + + fn parse_month(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Month); + let month = match self.format { + Format::Basic => self.text.get(4..6), + Format::Extended => self.text.get(5..7), + }; + if let Some(m) = month { + let month = m.parse()?; + if month > 12 { + return Err(Error::InvalidMonth); + } + self.month = Some(month); + self.mode = Mode::Day; + } else { + self.mode = Mode::TimeZone; + } + Ok(()) + } + + fn parse_day(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Day); + let day = match self.format { + Format::Basic => self.text.get(6..8), + Format::Extended => self.text.get(8..10), + }; + if let Some(d) = day { + let day = d.parse()?; + let max = match self.month { + Some(1|3|5|7|10|12) => 31, + Some(2) => { + if self.year.unwrap() % 4 == 0 { + 29 + } else { + 28 + } + }, + Some(4|6|9|11) => 30, + _ => return Err(Error::InvalidMonth), + }; + if day > max { + return Err(Error::InvalidDay); + } + self.day = Some(day); + let tidx = match self.format { + Format::Basic => 8, + Format::Extended => 10, + }; + if let Some(c) = self.text.chars().nth(tidx) { + match c { + 'T' => self.mode = Mode::Hour, + 'Z' | '-' | '+' => self.mode = Mode::TimeZone, + _ => return Err(Error::InvalidHour), + } + } else { + self.mode = Mode::Finish; + } + } else { + self.mode = Mode::TimeZone; + } + Ok(()) + } + + fn parse_hour(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Hour); + todo!() + } + + fn parse_minute(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Minute); + todo!() + } + + fn parse_second(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::Second); + todo!() + } + + fn parse_timezone(&mut self) -> Result<(), Error> { + assert_eq!(self.mode, Mode::TimeZone); + todo!() + } + + pub fn parse(mut self) -> Result { + loop { + match self.mode { + Mode::Year => self.parse_year()?, + Mode::Month => self.parse_month()?, + Mode::Day => self.parse_day()?, + Mode::Hour => self.parse_hour()?, + Mode::Minute => self.parse_minute()?, + Mode::Second => self.parse_second()?, + Mode::TimeZone => { + self.parse_timezone()?; + break; + } + Mode::Finish => break, + } + } + Ok(DateTime { + year: self.year.unwrap(), + month: self.month, + day: self.day, + hour: self.hour, + minute: self.minute, + second: self.second, + tz: self.tz, + }) + } +}