Compare commits

...

10 commits

Author SHA1 Message Date
Nathan Fisher
67b7a2d0ea Client cert verifier: return error if name does not match 2023-08-18 19:06:38 -04:00
Nathan Fisher
8cc3c13389 Begin implementing client certificate validator 2023-08-18 19:01:14 -04:00
Nathan Fisher
8dcd8455d4 Integrate unique message id creation with message receipt; Add
appropriate error cases for new infra;
2023-06-26 18:56:14 -04:00
Nathan Fisher
49b6728c78 Remove debug print statement from previous commit 2023-06-26 12:49:55 -04:00
Nathan Fisher
68cccc840b Fix error introduced in previous commit, where days in month was not
being updated while creating a DateTime struct from a u64 timestamp
2023-06-26 12:47:55 -04:00
Nathan Fisher
af49f327ec Add Id type for creating unique message identifiers; Change DateTime
to only deal with instants after the Unix epoch and simplify the math.
2023-06-26 00:16:05 -04:00
Nathan Fisher
64b5051341 Fix permissions setting for new account folder creation in the
`Filesystem` storage backend
2023-06-23 18:47:44 -04:00
Nathan Fisher
c6675cb024 Create error type for Filesystem instead of using io::Error; Fix
creation of user account folder so that the entire directory has
the correct permissions, bot just the Inbox folder
2023-06-23 12:21:59 -04:00
f17bda9349 Added long doc comment for Filesystem storage type 2023-06-23 10:04:40 -04:00
Nathan Fisher
3a25914ec5 Added create_folder method to MailStore trait; Add code to set
restrictive permissions on mail account folders;
2023-06-23 00:13:56 -04:00
15 changed files with 545 additions and 89 deletions

View file

@ -5,13 +5,20 @@ edition = "2021"
[dependencies]
digest = "0.10"
libc = "0.2.146"
rustls-pemfile = "1.0"
sha2 = "0.10"
time = "0.3"
tinyrand = "0.5"
x509-parser = "0.15"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies.pw]
git = "https://codeberg.org/jeang3nie/pw_rs.git"
default-features = false
features = [ "libc" ]
[dependencies.rustls]
version = "0.21"
features = [ "dangerous_configuration" ]

View file

@ -1,2 +1,78 @@
use {
crate::prelude::{IdError, ParseMessageError, ParseRequestError},
std::{fmt, io, string::FromUtf8Error},
};
#[derive(Debug)]
pub struct Error;
pub enum Error {
Message(ParseMessageError),
Id(IdError),
Io(io::Error),
Request(ParseRequestError),
Storage(String),
Rustls(rustls::Error),
Utf8,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Message(e) => write!(f, "{e}"),
Self::Id(e) => write!(f, "{e}"),
Self::Io(e) => write!(f, "{e}"),
Self::Storage(e) => write!(f, "{e}"),
Self::Request(e) => write!(f, "{e}"),
Self::Rustls(e) => write!(f, "{e}"),
Self::Utf8 => write!(f, "Utf8 error"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Message(e) => Some(e),
Self::Id(e) => Some(e),
Self::Io(e) => Some(e),
Self::Request(e) => Some(e),
Self::Rustls(e) => Some(e),
_ => None,
}
}
}
impl From<IdError> for Error {
fn from(value: IdError) -> Self {
Self::Id(value)
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl From<ParseMessageError> for Error {
fn from(value: ParseMessageError) -> Self {
Self::Message(value)
}
}
impl From<ParseRequestError> for Error {
fn from(value: ParseRequestError) -> Self {
Self::Request(value)
}
}
impl From<rustls::Error> for Error {
fn from(value: rustls::Error) -> Self {
Self::Rustls(value)
}
}
impl From<FromUtf8Error> for Error {
fn from(_value: FromUtf8Error) -> Self {
Self::Utf8
}
}

View file

@ -3,6 +3,18 @@ pub mod builder;
pub mod error;
pub mod verifier;
use {
crate::{
prelude::{ClientCertificateStore, Id, MailStore, MessageParser},
request::Request,
response::Response,
},
std::{
io::Read,
sync::{Arc, Mutex},
},
};
pub use self::{
builder::Builder,
error::Error,
@ -14,4 +26,30 @@ pub struct Connection {
pub inner: rustls::ServerConnection,
}
impl Connection {}
impl Connection {
pub fn handle_request<C: ClientCertificateStore + FingerPrintStore, S: MailStore>(
&mut self,
verifier: Verifier<C>,
mailstore: Arc<Mutex<S>>,
) -> Result<Response, Error> {
let mut buf: Vec<u8> = Vec::with_capacity(2048);
self.inner.reader().read_to_end(&mut buf)?;
let raw = String::from_utf8(buf)?;
let request: Request = raw.parse()?;
let res = if let Ok(mut store) = mailstore.lock() {
if store.has_mailuser(&request.recipient.to_string()) {
let id = Id::new()?;
let msg = MessageParser::new(&id.to_string()).parse(&request.message)?;
store
.add_message(&request.recipient.to_string(), "Inbox", msg)
.map_err(|e| Error::Storage(e.to_string()))?;
true
} else {
false
}
} else {
false
};
todo!()
}
}

View file

@ -1,6 +1,10 @@
use crate::mailuser::Mailuser;
use rustls::server::ClientCertVerifier;
use std::sync::Mutex;
use crate::{fingerprint::GetFingerprint, mailuser::Mailuser};
use rustls::server::{ClientCertVerified, ClientCertVerifier};
use std::{
io::Read,
sync::{Arc, Mutex},
};
use x509_parser::prelude::*;
#[derive(Debug)]
pub struct Verifier<S: FingerPrintStore> {
@ -23,7 +27,29 @@ impl<S: FingerPrintStore> ClientCertVerifier for Verifier<S> {
end_entity: &rustls::Certificate,
intermediates: &[rustls::Certificate],
now: std::time::SystemTime,
) -> Result<rustls::server::ClientCertVerified, rustls::Error> {
) -> Result<ClientCertVerified, rustls::Error> {
let fingerprint = end_entity.fingerprint()?;
if let Ok(store) = self.store.lock() {
if let Some(user) = store.get_mailuser(&fingerprint.fingerprint) {
let (_, pk) = X509Certificate::from_der(end_entity.as_ref()).map_err(|e| {
rustls::Error::InvalidCertificate(rustls::CertificateError::Other(Arc::new(e)))
})?;
let subject = pk.subject();
let mut name_match = false;
subject.iter_common_name().for_each(|n| {
let mut val = n.attr_value().data;
let mut name = String::new();
if val.read_to_string(&mut name).is_ok() {
name_match = name == user.to_string();
}
});
if !name_match {
return Err(rustls::Error::InvalidCertificate(
rustls::CertificateError::NotValidForName,
));
}
}
}
todo!()
}
}

View file

@ -1,4 +1,7 @@
use {std::fmt, x509_parser::prelude::X509Error};
use {
std::{fmt, sync::Arc},
x509_parser::prelude::X509Error,
};
#[derive(Debug)]
/// Errors which can occur when fingerprinting a certificate
@ -38,3 +41,15 @@ impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for Error {
Self::X509(value.into())
}
}
impl From<Error> for rustls::Error {
fn from(value: Error) -> Self {
match value {
Error::Fmt => Self::General(String::from("Format Error")),
Error::InvalidForDate => Self::InvalidCertificate(rustls::CertificateError::Expired),
Error::X509(e) => {
Self::InvalidCertificate(rustls::CertificateError::Other(Arc::new(e)))
}
}
}
}

View file

@ -1,4 +1,4 @@
use crate::prelude::{DateTime, Link, Mailbox, Recipients};
use crate::prelude::{DateTime, Id, Link, Mailbox, Recipients};
use std::fmt;
#[derive(Debug)]
@ -30,6 +30,12 @@ pub enum GemtextNode {
Link(Link),
}
impl From<Id> for GemtextNode {
fn from(value: Id) -> Self {
Self::Timestamp(value.into())
}
}
impl fmt::Display for GemtextNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View file

@ -1,23 +1,23 @@
use std::io::BufReader;
use crate::prelude::Certificate;
use {
super::*,
crate::{
message::Parser as MessageParser,
prelude::{ClientCertificateStore, ParseMailboxError},
prelude::{Certificate, ClientCertificateStore},
},
rustls_pemfile::{read_one, Item},
std::{
ffi::OsString,
fs::{self, File},
io::{self, BufWriter, Write},
io::{self, BufReader, BufWriter, Write},
iter,
os::unix::fs::DirBuilderExt,
path::{Path, PathBuf},
},
};
mod error;
use std::ffi::CString;
pub use error::Error;
pub trait MultiDomain: MailStore {
type Error;
@ -29,6 +29,43 @@ pub trait MultiDomain: MailStore {
) -> Result<(), <Self as filesystem::MultiDomain>::Error>;
}
/// The Filestore storage backend uses the filesystem to map folders to domains,
/// mail users and folders of messages. It can also be used as a `ClientCertificateStore`.
/// The layout of files inside a filesystem mailstore is as below.
/// ```Sh
/// test/mailstore/
/// └── misfin.example.org
/// ├── dick
/// │   ├── Inbox
/// │   │   ├── 619310.gmi
/// │   │   └── 868379.gmi
/// │   ├── blurb
/// │   └── certificate.pem
/// └── jane
/// ├── Inbox
/// │   ├── 252366.gmi
/// │   └── 654064.gmi
/// ├── blurb
/// └── certificate.pem
/// ```
/// In this made up example the Misfin server in question serves the domain
/// "misfin.example.org", which has the users "dick" and "jane", whose mailbox
/// addresses would be "dick@misfin.example.org" and "jane@misfin.example.org".
/// Within each account folder any subfolders are to be considered as mail folders,
/// with the default folder for delivering new mail being "Inbox". The "blurb"
/// file (optional) contains the display name of the user, and their client
/// certificate is stored in pem-encoded ascii armor format in "certificate.pem",
/// if the `Filesystem` is also to be used as a `ClientCertificateStore`.
///
/// The permissions on each individual account folder are set to 0o2700 at the
/// time of creation, such that the folder is read/write for the user and group
/// and any entries created within will have the same group membership. If the
/// username of the account corresponds to a group name on the system, and the
/// user which the server runs as is a member of this group, then said group will
/// own the directory. In this way the server will be able to write incoming
/// mail into the folder and the user will be able to read and manage their
/// folders and messages, without being able to see any other user's mail and
/// without the server requiring elevated privileges.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct Filesystem {
@ -66,7 +103,7 @@ impl From<OsString> for Filesystem {
}
impl MailStore for Filesystem {
type Error = io::Error;
type Error = Error;
fn users(&self) -> Vec<Mailuser> {
let mut users = vec![];
@ -149,6 +186,19 @@ impl MailStore for Filesystem {
Some(folder)
}
fn create_folder(&self, user: &str, folder: &str) -> Result<(), Error> {
if self.has_mailuser(user) {
let user: Mailbox = user.parse()?;
let user: Mailuser = user.into();
let mut path = self.path.clone();
path.push(&user.host.to_string());
path.push(&user.username);
path.push(folder);
fs::create_dir_all(&path)?;
}
Ok(())
}
fn get_message(&self, user: &str, folder: &str, id: &str) -> Option<Message> {
self.get_folder(user, folder)
.and_then(|f| f.messages.get(id).cloned())
@ -160,26 +210,23 @@ impl MailStore for Filesystem {
folder: &str,
message: Message,
) -> Result<(), Self::Error> {
let Some((user, host)) = user.rsplit_once('@') else {
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
};
let mb: Mailbox = user.parse()?;
let mut path = self.path.clone();
path.push(host);
path.push(user);
path.push(&mb.host.to_string());
path.push(&mb.username);
path.push(folder);
path.push(&message.id);
path.set_extension("gmi");
let mut fd = File::create(path)?;
write!(fd, "{message}")
write!(fd, "{message}")?;
Ok(())
}
fn delete_message(&mut self, user: &str, folder: &str, id: &str) -> Result<(), Self::Error> {
let Some((user, host)) = user.rsplit_once('@') else {
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
};
let mb: Mailbox = user.parse()?;
let mut path = self.path.clone();
path.push(host);
path.push(user);
path.push(&mb.host.to_string());
path.push(&mb.username);
path.push(folder);
path.push(id);
path.set_extension("gmi");
@ -195,13 +242,11 @@ impl MailStore for Filesystem {
id: &str,
folder1: &str,
folder2: &str,
) -> Result<(), io::Error> {
let Some((user, host)) = user.rsplit_once('@') else {
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
};
) -> Result<(), Self::Error> {
let mb: Mailbox = user.parse()?;
let mut infile = self.path.clone();
infile.push(host);
infile.push(user);
infile.push(&mb.host.to_string());
infile.push(&mb.username);
let mut outfile = infile.clone();
infile.push(folder1);
infile.push(id);
@ -215,35 +260,45 @@ impl MailStore for Filesystem {
}
fn add_user(&mut self, user: &str) -> Result<(), Self::Error> {
let mb: Mailbox = user
.parse()
.map_err(|e: ParseMailboxError| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let mb: Mailbox = user.parse()?;
let mut path = self.path.clone();
path.push(&mb.host.to_string());
path.push(&mb.username);
path.push("Inbox");
fs::create_dir_all(&path)?;
fs::DirBuilder::new().recursive(true).create(&path)?;
let p = CString::new(path.to_str().ok_or(Error::Utf8)?.to_string())?;
if let Some(pw) = pw::Passwd::getpw()? {
let groups = pw.groups()?;
if let Some(gr) = groups.iter().find(|g| g.name == mb.username) {
chown(p.clone(), pw.uid, gr.gid)?;
}
}
// We have to explicitly call `chown` after creating the directory,
// rather than setting permissions during creation, as the umask
// might squash some of the bits we're specifically trying to set.
chmod(p, 0o2770)?;
if let Some(ref blurb) = mb.blurb {
let _last = path.pop();
path.push("blurb");
let fd = File::options()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
.open(&path)?;
let mut writer = BufWriter::new(fd);
writer.write_all(blurb.as_bytes())?;
}
let _last = path.pop();
path.push("Inbox");
fs::create_dir_all(&path)?;
Ok(())
}
fn remove_user(&mut self, user: &str) -> bool {
let Ok(mu) = user.parse::<Mailbox>() else {
let Ok(mb) = user.parse::<Mailbox>() else {
return false;
};
let mut path = self.path.clone();
path.push(&mu.host.to_string());
path.push(&mu.username);
path.push(&mb.host.to_string());
path.push(&mb.username);
if path.exists() {
if fs::remove_dir_all(path).is_ok() {
return true;
@ -367,6 +422,8 @@ impl ClientCertificateStore for Filesystem {
#[cfg(test)]
mod tests {
use std::os::unix::prelude::PermissionsExt;
use super::*;
fn store() -> Filesystem {
@ -460,7 +517,29 @@ mod tests {
.add_user("rob@misfin.example.org Rob Zombie")
.unwrap();
assert!(store().has_mailuser("rob@misfin.example.org"));
let permissions = fs::metadata("test/mailstore/misfin.example.org/rob")
.unwrap()
.permissions();
assert_eq!(permissions.mode(), 0o42770);
assert!(store().remove_user("rob@misfin.example.org"));
assert!(!store().has_mailuser("rob@misfin.example.org"));
}
}
fn chown(path: CString, uid: u32, gid: u32) -> Result<(), io::Error> {
unsafe {
if libc::chown(path.as_ptr(), uid, gid) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn chmod(path: CString, mode: u32) -> Result<(), io::Error> {
unsafe {
if libc::chmod(path.as_ptr(), mode) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}

View file

@ -0,0 +1,67 @@
use {
crate::prelude::ParseMailboxError,
std::{ffi::NulError, fmt, io, str::Utf8Error},
};
#[derive(Debug)]
pub enum Error {
Io(io::Error),
MailBox(ParseMailboxError),
FFi(NulError),
Permissions(pw::Error),
Utf8,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "Filesystem error: {e}"),
Self::FFi(e) => write!(f, "Filesystem error: {e}"),
Self::MailBox(e) => write!(f, "Filesystem error: {e}"),
Self::Permissions(e) => write!(f, "Filesystem error: {e}"),
Self::Utf8 => write!(f, "Filesystem error: Utf8 failure"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::FFi(e) => Some(e),
Self::MailBox(e) => Some(e),
Self::Permissions(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl From<NulError> for Error {
fn from(value: NulError) -> Self {
Self::FFi(value)
}
}
impl From<ParseMailboxError> for Error {
fn from(value: ParseMailboxError) -> Self {
Self::MailBox(value)
}
}
impl From<pw::Error> for Error {
fn from(value: pw::Error) -> Self {
Self::Permissions(value)
}
}
impl From<Utf8Error> for Error {
fn from(_value: Utf8Error) -> Self {
Self::Utf8
}
}

View file

@ -6,10 +6,11 @@ use std::{collections::HashMap, io, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod filesystem;
mod filesystem;
pub use filesystem::{Error as FilesystemError, Filesystem, MultiDomain};
pub trait MailStore {
type Error;
type Error: std::error::Error;
/// Retreives a list of all valid mailusers on this server
fn users(&self) -> Vec<Mailuser>;
@ -19,6 +20,8 @@ pub trait MailStore {
fn has_mailuser(&self, mailuser: &str) -> bool;
/// Retreives all of the messages for this user
fn get_folder(&self, user: &str, folder: &str) -> Option<Folder>;
/// Creates a folder for the specified user, if it doesn't already exist
fn create_folder(&self, user: &str, folder: &str) -> Result<(), Self::Error>;
/// Checks whether this server has a message with the given title for this user and if so
/// returns the message
fn get_message(&self, user: &str, folder: &str, id: &str) -> Option<Message>;
@ -66,12 +69,6 @@ pub struct Folder {
pub messages: HashMap<String, Message>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MultiDomain {
pub domains: HashMap<String, Domain>,
}
impl MailStore for Domain {
type Error = io::Error;
@ -97,6 +94,19 @@ impl MailStore for Domain {
.and_then(|u| u.folders.get(folder).cloned())
}
fn create_folder(&self, user: &str, folder: &str) -> Result<(), io::Error> {
self.users.get(user).cloned().and_then(|mut u| {
u.folders.insert(
folder.to_string(),
Folder {
name: folder.to_string(),
messages: HashMap::new(),
},
)
});
Ok(())
}
fn get_message(&self, user: &str, folder: &str, title: &str) -> Option<Message> {
self.users
.get(user)

110
src/message/id.rs Normal file
View file

@ -0,0 +1,110 @@
use {
std::{
error, fmt,
fs::File,
io::{self, Read},
time::{SystemTime, SystemTimeError, UNIX_EPOCH},
},
tinyrand::{RandRange, Seeded, StdRand},
};
const ALPHABET: [char; 67] = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9', '@', '%', '&', '=', '+',
];
#[derive(Clone, Debug)]
pub struct Id {
pub time: u64,
pub id: [char; 8],
}
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.", self.time)?;
self.id.iter().try_for_each(|c| write!(f, "{c}"))
}
}
impl Id {
pub fn new() -> Result<Self, Error> {
let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let mut rand = StdRand::seed(random_seed()?);
let mut id = ['0', '0', '0', '0', '0', '0', '0', '0'];
for item in &mut id {
if let Some(c) = ALPHABET.get(rand.next_range(0..68)) {
*item = *c;
}
}
Ok(Self { time, id })
}
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Time(SystemTimeError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::Time(e) => write!(f, "{e}"),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Time(e) => Some(e),
}
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl From<SystemTimeError> for Error {
fn from(value: SystemTimeError) -> Self {
Self::Time(value)
}
}
fn u64_from_bytes(b: [u8; 8]) -> u64 {
u64::from(b[0])
| (u64::from(b[1]) << 8)
| (u64::from(b[2]) << 16)
| (u64::from(b[3]) << 24)
| (u64::from(b[4]) << 32)
| (u64::from(b[5]) << 40)
| (u64::from(b[6]) << 48)
| (u64::from(b[7]) << 56)
}
fn random_seed() -> io::Result<u64> {
let mut buf = [0, 0, 0, 0, 0, 0, 0, 0];
let mut fd = File::open("/dev/urandom")?;
fd.read_exact(&mut buf)?;
Ok(u64_from_bytes(buf))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn new_id() {
let a = Id::new().unwrap();
let b = Id::new().unwrap();
assert_eq!(a.time / 10, b.time / 10);
assert_ne!(a.id, b.id);
}
}

View file

@ -6,9 +6,15 @@ use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
mod error;
mod id;
mod link;
mod parser;
pub use {error::Error, link::Link, parser::Parser};
pub use {
error::Error,
id::{Error as IdError, Id},
link::Link,
parser::Parser,
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Recipients {

View file

@ -22,6 +22,7 @@ impl Parser {
}
}
#[allow(clippy::missing_panics_doc)]
pub fn parse(mut self, content: &str) -> Result<Message, super::Error> {
let lines = content.lines();
for l in lines {

View file

@ -4,8 +4,11 @@ pub use super::{
gemtext::{GemtextNode, Parser as GemtextParser},
host::{Error as ParseHostError, Host},
mailbox::{Error as ParseMailboxError, Mailbox},
mailstore::{Account, Domain, Filesystem, FilesystemError, Folder, MailStore},
mailuser::Mailuser,
message::{Error as ParseMessageError, Link, Message, Recipients},
message::{
Error as ParseMessageError, Id, IdError, Link, Message, Parser as MessageParser, Recipients,
},
//receiver,
request::{Error as ParseRequestError, Request},
response::{Error as ParseResponseError, Response},

View file

@ -1,4 +1,6 @@
//! A container representing a moment in ISO-8601 format Time
#![allow(clippy::module_name_repetitions)]
use crate::prelude::Id;
use std::{fmt, str::FromStr};
mod error;
@ -8,13 +10,13 @@ pub use {error::Error, parser::Parser};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const SECS_PER_DAY: i64 = 86400;
const SECS_PER_DAY: u64 = 86400;
fn days_since_epoch(ts: i64) -> i64 {
fn days_since_epoch(ts: u64) -> u64 {
ts / SECS_PER_DAY
}
fn is_leap(year: i64) -> bool {
fn is_leap(year: u32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 16 == 0)
}
@ -22,7 +24,7 @@ fn is_leap(year: i64) -> bool {
/// given month is February, then we also need to know
/// whether it is a leap year or not, hence this function
/// takes the year as an argument as well.
pub fn days_in_month(month: u8, year: i64) -> i64 {
pub fn days_in_month(month: u8, year: u32) -> u8 {
assert!(month < 13);
match month {
1 | 3 | 5 | 7 | 10 | 12 => 31,
@ -142,8 +144,8 @@ impl fmt::Display for DateTime {
impl PartialOrd for DateTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let a: i64 = (*self).into();
let b: i64 = (*other).into();
let a: u64 = (*self).into();
let b: u64 = (*other).into();
a.partial_cmp(&b)
}
}
@ -156,12 +158,8 @@ impl FromStr for DateTime {
}
}
impl From<i64> for DateTime {
fn from(ts: i64) -> Self {
// Only allow using this function for positive timestamp values,
// ie no dates before 1970-01-01
assert!(ts.is_positive());
impl From<u64> for DateTime {
fn from(ts: u64) -> Self {
let mut year = 1970;
let mut days = days_since_epoch(ts);
loop {
@ -174,9 +172,11 @@ impl From<i64> for DateTime {
}
}
let mut month = 1;
while days_in_month(month, year) <= days {
days -= days_in_month(month, year);
let mut m_days = u64::from(days_in_month(month, year));
while m_days <= days {
days -= m_days;
month += 1;
m_days = u64::from(days_in_month(month, year));
}
let seconds = ts % SECS_PER_DAY;
let minutes = seconds / 60;
@ -184,8 +184,8 @@ impl From<i64> for DateTime {
let hour = minutes / 60;
let minute = minutes % 60;
DateTime {
year: year as u32,
month: month.try_into().unwrap(),
year,
month,
day: (days + 1).try_into().unwrap(),
hour: Some((hour).try_into().unwrap()),
minute: Some((minute).try_into().unwrap()),
@ -195,20 +195,28 @@ impl From<i64> for DateTime {
}
}
impl From<DateTime> for i64 {
#[allow(clippy::cast_possible_truncation)]
impl From<Id> for DateTime {
fn from(value: Id) -> Self {
let time = value.time;
time.into()
}
}
impl From<DateTime> for u64 {
fn from(dt: DateTime) -> Self {
let mut seconds = dt.second.unwrap_or(0).into();
seconds += dt.minute.unwrap_or(0) as i64 * 60;
seconds += (dt.hour.unwrap_or(1)) as i64 * 3600;
seconds += SECS_PER_DAY * (dt.day as i64 - 1);
seconds += u64::from(dt.minute.unwrap_or(0)) * 60;
seconds += u64::from(dt.hour.unwrap_or(1)) * 3600;
seconds += SECS_PER_DAY * (u64::from(dt.day) - 1);
let mut m = 1;
while m < dt.month {
seconds += SECS_PER_DAY * days_in_month(m, dt.year.into());
seconds += SECS_PER_DAY * u64::from(days_in_month(m, dt.year));
m += 1;
}
let mut y = 1970;
while y < dt.year {
if is_leap(y.into()) {
if is_leap(y) {
seconds += SECS_PER_DAY * 366;
} else {
seconds += SECS_PER_DAY * 365;
@ -216,9 +224,9 @@ impl From<DateTime> for i64 {
y += 1;
}
if let Some(TimeZone::Offset(offset)) = dt.tz {
let mut offset_seconds = offset.hours as i64 * 3600;
let mut offset_seconds = u64::from(offset.hours) * 3600;
if let Some(offset_minutes) = offset.minutes {
offset_seconds += offset_minutes as i64 * 60;
offset_seconds += u64::from(offset_minutes) * 60;
}
match offset.sign {
Sign::Positive => seconds += offset_seconds,
@ -237,7 +245,7 @@ impl DateTime {
/// offset is applied and the result returned.
pub fn normalize(&mut self) -> &Self {
if let Some(TimeZone::Offset(_)) = self.tz {
*self = i64::from(*self).into();
*self = u64::from(*self).into();
}
self
}

View file

@ -55,7 +55,7 @@ impl<'a> Parser<'a> {
#[allow(clippy::missing_panics_doc)]
fn validate_day(&self, day: u8) -> Result<(), Error> {
let max = days_in_month(self.month.unwrap(), self.year.unwrap() as i64) as u8;
let max = days_in_month(self.month.unwrap(), self.year.unwrap());
if day > max {
Err(Error::InvalidDay)
} else {
@ -316,18 +316,22 @@ impl<'a> Parser<'a> {
}
_ => {}
}
if self.year.is_none() || self.month.is_none() || self.day.is_none() {
return Err(Error::Truncated);
if let Some(year) = self.year {
if let Some(month) = self.month {
if let Some(day) = self.day {
return Ok(DateTime {
year,
month,
day,
hour: self.hour,
minute: self.minute,
second: self.second,
tz: self.tz,
});
}
}
}
Ok(DateTime {
year: self.year.unwrap(),
month: self.month.unwrap(),
day: self.day.unwrap(),
hour: self.hour,
minute: self.minute,
second: self.second,
tz: self.tz,
})
Err(Error::Truncated)
}
}