From 805e5cdd14c3fac52bb59b24fbafb7cc6ea46829 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Thu, 1 Jun 2023 23:34:36 -0400 Subject: [PATCH 01/15] Some work towards the gemtext parser --- src/connection/builder.rs | 5 +- src/connection/mod.rs | 3 +- src/mailstore/mod.rs | 4 +- src/message/link.rs | 10 ++- src/message/mod.rs | 31 --------- src/message/parser.rs | 128 +++++++++++++++++++++++++++++++++++++- 6 files changed, 141 insertions(+), 40 deletions(-) diff --git a/src/connection/builder.rs b/src/connection/builder.rs index 84170f6..c51430f 100644 --- a/src/connection/builder.rs +++ b/src/connection/builder.rs @@ -10,7 +10,10 @@ pub struct Builder { impl Default for Builder { fn default() -> Self { - Self { stream: None, verifier: None } + Self { + stream: None, + verifier: None, + } } } diff --git a/src/connection/mod.rs b/src/connection/mod.rs index 7b6fecc..ef4b905 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -7,5 +7,4 @@ pub struct Connection { pub inner: rustls::ServerConnection, } -impl Connection { -} +impl Connection {} diff --git a/src/mailstore/mod.rs b/src/mailstore/mod.rs index eafe519..976217f 100644 --- a/src/mailstore/mod.rs +++ b/src/mailstore/mod.rs @@ -86,9 +86,7 @@ impl MailStore for Domain { } fn has_mailuser(&self, mailuser: &str) -> bool { - self.users() - .iter() - .any(|x| x.username == mailuser) + self.users().iter().any(|x| x.username == mailuser) } fn get_folder(&self, user: &str, folder: &str) -> Option { diff --git a/src/message/link.rs b/src/message/link.rs index 2ee0e92..10df54f 100644 --- a/src/message/link.rs +++ b/src/message/link.rs @@ -27,9 +27,15 @@ impl FromStr for Link { return Err(super::Error::MalformedLink); }; if let Some((url, display)) = s.split_once(char::is_whitespace) { - Ok(Self { url: url.to_string(), display: Some(display.to_string()) }) + Ok(Self { + url: url.to_string(), + display: Some(display.to_string()), + }) } else { - Ok(Self { url: s.to_string(), display: None }) + Ok(Self { + url: s.to_string(), + display: None, + }) } } } diff --git a/src/message/mod.rs b/src/message/mod.rs index 35554ec..327b5f8 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -21,37 +21,6 @@ impl fmt::Display for Recipients { } } -#[derive(Clone, Debug, PartialEq)] -pub enum Lines { - Sender(Mailbox), - Recipients(Recipients), - Timestamp(String), - Text(String), - Heading1(String), - Heading2(String), - Heading3(String), - Quote(String), - Preformatted(String), - Link(Link), -} - -impl fmt::Display for Lines { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Sender(m) => writeln!(f, "< {m}"), - Self::Recipients(r) => writeln!(f, "{r}"), - Self::Timestamp(t) => writeln!(f, "@ {t}"), - Self::Text(t) => writeln!(f, "{t}"), - Self::Heading1(h) => writeln!(f, "# {h}"), - Self::Heading2(h) => writeln!(f, "## {h}"), - Self::Heading3(h) => writeln!(f, "### {h}"), - Self::Quote(q) => writeln!(f, "> {q}"), - Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), - Self::Link(l) => writeln!(f, "=> {l}"), - } - } -} - #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct Message { diff --git a/src/message/parser.rs b/src/message/parser.rs index 50868b4..b848b6e 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -1,2 +1,128 @@ +use std::fmt; +use crate::prelude::Mailbox; +use super::{Link, Recipients}; + #[derive(Debug)] -pub struct Parser; +pub struct PreBlk<'a> { + alt: Option<&'a str>, + lines: Vec<&'a str>, +} + +#[derive(Debug)] +enum State<'a> { + Normal, + Preformatted(PreBlk<'a>), + Quote(Vec<&'a str>), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GemtextNode { + Sender(Mailbox), + Recipients(Recipients), + Timestamp(String), + Text(String), + Heading1(String), + Heading2(String), + Heading3(String), + Quote(String), + Preformatted(String), + Link(Link), +} + +impl fmt::Display for GemtextNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sender(m) => writeln!(f, "< {m}"), + Self::Recipients(r) => writeln!(f, "{r}"), + Self::Timestamp(t) => writeln!(f, "@ {t}"), + Self::Text(t) => writeln!(f, "{t}"), + Self::Heading1(h) => writeln!(f, "# {h}"), + Self::Heading2(h) => writeln!(f, "## {h}"), + Self::Heading3(h) => writeln!(f, "### {h}"), + Self::Quote(q) => writeln!(f, "> {q}"), + Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), + Self::Link(l) => writeln!(f, "=> {l}"), + } + } +} + +impl<'a> GemtextNode { + fn parse_link(text: &'a str) -> Self { + todo!() + } + + fn parse_prompt(text: &'a str) -> Self { + todo!() + } + + fn parse_heading(text: &'a str) -> Self { + todo!() + } + + fn parse_list_item(text: &'a str) -> Self { + todo!() + } + + fn parse_blockquote(text: &'a str) -> Self { + todo!() + } + + fn parse_senders(text: &'a str) -> Self { + todo!() + } + + fn parse_recipients(text: &'a str) -> Self { + todo!() + } + + fn parse_timestamp(text: &'a str) -> Self { + todo!() + } +} + +#[derive(Debug)] +pub struct Parser<'a> { + state: State<'a>, + lines: Vec, +} + +impl<'a> Parser<'a> { + pub fn new() -> Self { + Self { + state: State::Normal, + lines: vec![] + } + } + + fn parse(mut self, raw: &'a str) -> Vec { + for line in raw.lines() { + match self.state { + State::Normal => self.parse_normal(line), + State::Preformatted(_) => self.parse_preformatted(line), + State::Quote(_) => self.parse_quote(line), + } + } + self.lines + } + + fn parse_normal(&mut self, line: &'a str) { + match line { + s if s.starts_with("=>") => {}, + s if s.starts_with('#') => {}, + s if s.starts_with('*') => {}, + s if s.starts_with('>') => {}, + s if s.starts_with("```") => {}, + s if s.starts_with('<') => {}, + s if s.starts_with(':') => {}, + s if s.starts_with('@') => {}, + s => {}, + } + } + + fn parse_preformatted(&mut self, line: &'a str) { + } + + fn parse_quote(&mut self, line: &'a str) { + } +} + From 200dd8b451764ba38e3628502cb8d46be0cd72ab Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 00:26:19 -0400 Subject: [PATCH 02/15] Progress on the Gemtext parser --- src/message/mod.rs | 2 +- src/message/parser.rs | 77 ++++++++++++++++++++++++++++++++++--------- src/prelude.rs | 2 +- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/message/mod.rs b/src/message/mod.rs index 327b5f8..1c64aba 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; mod error; mod link; mod parser; -pub use {error::Error, link::Link, parser::Parser}; +pub use {error::Error, link::Link, parser::{GemtextNode, Parser}}; #[derive(Clone, Debug, PartialEq)] pub struct Recipients { diff --git a/src/message/parser.rs b/src/message/parser.rs index b848b6e..2d2234a 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -17,13 +17,14 @@ enum State<'a> { #[derive(Clone, Debug, PartialEq)] pub enum GemtextNode { - Sender(Mailbox), + Sender(Vec), Recipients(Recipients), Timestamp(String), Text(String), Heading1(String), Heading2(String), Heading3(String), + ListItem(String), Quote(String), Preformatted(String), Link(Link), @@ -32,13 +33,18 @@ pub enum GemtextNode { impl fmt::Display for GemtextNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Sender(m) => writeln!(f, "< {m}"), + Self::Sender(m) => { + write!(f, "<")?; + m.iter().try_for_each(|m| write!(f, " {m}"))?; + writeln!(f) + }, Self::Recipients(r) => writeln!(f, "{r}"), Self::Timestamp(t) => writeln!(f, "@ {t}"), Self::Text(t) => writeln!(f, "{t}"), Self::Heading1(h) => writeln!(f, "# {h}"), Self::Heading2(h) => writeln!(f, "## {h}"), Self::Heading3(h) => writeln!(f, "### {h}"), + Self::ListItem(l) => writeln!(f, "* {l}"), Self::Quote(q) => writeln!(f, "> {q}"), Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), Self::Link(l) => writeln!(f, "=> {l}"), @@ -48,27 +54,56 @@ impl fmt::Display for GemtextNode { impl<'a> GemtextNode { fn parse_link(text: &'a str) -> Self { - todo!() - } - - fn parse_prompt(text: &'a str) -> Self { - todo!() + if let Ok(link) = text.parse() { + Self::Link(link) + } else { + Self::Text(text.to_string()) + } } fn parse_heading(text: &'a str) -> Self { - todo!() + if let Some((h, s)) = text.split_once(char::is_whitespace) { + match h { + "#" => Self::Heading1(s.to_string()), + "##" => Self::Heading2(s.to_string()), + "###" => Self::Heading3(s.to_string()), + _ => Self::Text(text.to_string()), + } + } else { + Self::Text(text.to_string()) + } } fn parse_list_item(text: &'a str) -> Self { - todo!() + match text.split_once(char::is_whitespace) { + Some((pre, s)) if pre == "*" => GemtextNode::ListItem(s.to_string()), + _ => GemtextNode::Text(text.to_string()), + } } fn parse_blockquote(text: &'a str) -> Self { - todo!() + match text.split_once(char::is_whitespace) { + Some((prefix, suffix)) if prefix == ">" => GemtextNode::Quote(suffix.to_string()), + _ => GemtextNode::Text(text.to_string()), + } } fn parse_senders(text: &'a str) -> Self { - todo!() + let mut split = text.split_whitespace(); + match split.next() { + Some(s) if s == "<" => { + let mut senders: Vec = vec![]; + for s in split { + if let Ok(m) = s.parse() { + senders.push(m); + } else { + return Self::Text(text.to_string()); + } + } + Self::Sender(senders) + } + _ => Self::Text(text.to_string()) + } } fn parse_recipients(text: &'a str) -> Self { @@ -94,7 +129,7 @@ impl<'a> Parser<'a> { } } - fn parse(mut self, raw: &'a str) -> Vec { + pub fn parse(mut self, raw: &'a str) -> Vec { for line in raw.lines() { match self.state { State::Normal => self.parse_normal(line), @@ -105,11 +140,23 @@ impl<'a> Parser<'a> { self.lines } + fn link(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_link(line)); + } + + fn heading(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_heading(line)); + } + + fn list_item(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_list_item(line)); + } + fn parse_normal(&mut self, line: &'a str) { match line { - s if s.starts_with("=>") => {}, - s if s.starts_with('#') => {}, - s if s.starts_with('*') => {}, + s if s.starts_with("=>") => self.link(s), + s if s.starts_with('#') => self.heading(s), + s if s.starts_with('*') => self.list_item(s), s if s.starts_with('>') => {}, s if s.starts_with("```") => {}, s if s.starts_with('<') => {}, diff --git a/src/prelude.rs b/src/prelude.rs index e0ec573..f89579c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use super::{ host::{Error as ParseHostError, Host}, mailbox::{Error as ParseMailboxError, Mailbox}, mailuser::Mailuser, - message::{Error as ParseMessageError, Lines, Message, Recipients}, + message::{Error as ParseMessageError, GemtextNode, Message, Recipients}, //receiver, request::{Error as ParseRequestError, Request}, response::{Error as ParseResponseError, Response}, From a9fcab700606612a303474e2edee3492aed41ed1 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 09:55:10 -0400 Subject: [PATCH 03/15] Implement a little more of the Gemtext parser --- src/message/parser.rs | 85 +++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/src/message/parser.rs b/src/message/parser.rs index 2d2234a..e573c0a 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -17,7 +17,7 @@ enum State<'a> { #[derive(Clone, Debug, PartialEq)] pub enum GemtextNode { - Sender(Vec), + Sender(Mailbox), Recipients(Recipients), Timestamp(String), Text(String), @@ -33,12 +33,8 @@ pub enum GemtextNode { impl fmt::Display for GemtextNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Sender(m) => { - write!(f, "<")?; - m.iter().try_for_each(|m| write!(f, " {m}"))?; - writeln!(f) - }, Self::Recipients(r) => writeln!(f, "{r}"), + Self::Sender(m) => writeln!(f, "{m}"), Self::Timestamp(t) => writeln!(f, "@ {t}"), Self::Text(t) => writeln!(f, "{t}"), Self::Heading1(h) => writeln!(f, "# {h}"), @@ -81,33 +77,31 @@ impl<'a> GemtextNode { } } - fn parse_blockquote(text: &'a str) -> Self { - match text.split_once(char::is_whitespace) { - Some((prefix, suffix)) if prefix == ">" => GemtextNode::Quote(suffix.to_string()), - _ => GemtextNode::Text(text.to_string()), - } - } - fn parse_senders(text: &'a str) -> Self { - let mut split = text.split_whitespace(); - match split.next() { - Some(s) if s == "<" => { - let mut senders: Vec = vec![]; - for s in split { - if let Ok(m) = s.parse() { - senders.push(m); - } else { - return Self::Text(text.to_string()); - } - } - Self::Sender(senders) - } - _ => Self::Text(text.to_string()) + let Some(line) = text.strip_prefix('<') else { + return Self::Text(text.to_string()); + }; + if let Ok(user) = line.parse() { + Self::Sender(user) + } else { + Self::Text(text.to_string()) } } fn parse_recipients(text: &'a str) -> Self { - todo!() + let Some(line) = text.strip_prefix(':') else { + return Self::Text(text.to_string()); + }; + let split = line.split_whitespace(); + let mut recipients: Recipients = Recipients { boxes: vec![] }; + for s in split { + if let Ok(m) = s.parse() { + recipients.boxes.push(m); + } else { + return Self::Text(text.to_string()); + } + } + Self::Recipients(recipients) } fn parse_timestamp(text: &'a str) -> Self { @@ -152,24 +146,51 @@ impl<'a> Parser<'a> { self.lines.push(GemtextNode::parse_list_item(line)); } + fn quote(&mut self, line: &'a str) { + match line.split_once(char::is_whitespace) { + Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), + _ => self.lines.push(GemtextNode::Text(line.to_string())), + } + } + + fn enter_preformatted(&mut self, line: &'a str) { + let alt = if line.len() > 3 { + Some(line[3..].trim()) + } else { + None + }; + let preblk = PreBlk { alt, lines: vec![] }; + self.state = State::Preformatted(preblk); + } + + fn senders(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_senders(line)); + } + + fn recipients(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_recipients(line)); + } + fn parse_normal(&mut self, line: &'a str) { match line { s if s.starts_with("=>") => self.link(s), s if s.starts_with('#') => self.heading(s), s if s.starts_with('*') => self.list_item(s), - s if s.starts_with('>') => {}, - s if s.starts_with("```") => {}, - s if s.starts_with('<') => {}, - s if s.starts_with(':') => {}, + s if s.starts_with('>') => self.quote(s), + s if s.starts_with("```") => self.enter_preformatted(s), + s if s.starts_with('<') => self.senders(s), + s if s.starts_with(':') => self.recipients(s), s if s.starts_with('@') => {}, s => {}, } } fn parse_preformatted(&mut self, line: &'a str) { + todo!() } fn parse_quote(&mut self, line: &'a str) { + todo!() } } From 1dd66684bfd2d15593f61cc1454288ef85ecc65f Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 10:03:02 -0400 Subject: [PATCH 04/15] Parser: add "title" field and set it with the first heading found --- src/message/parser.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/message/parser.rs b/src/message/parser.rs index e573c0a..dd0544a 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -112,6 +112,7 @@ impl<'a> GemtextNode { #[derive(Debug)] pub struct Parser<'a> { state: State<'a>, + title: Option, lines: Vec, } @@ -119,6 +120,7 @@ impl<'a> Parser<'a> { pub fn new() -> Self { Self { state: State::Normal, + title: None, lines: vec![] } } @@ -139,7 +141,14 @@ impl<'a> Parser<'a> { } fn heading(&mut self, line: &'a str) { - self.lines.push(GemtextNode::parse_heading(line)); + let line = GemtextNode::parse_heading(line); + if self.title.is_none() { + match &line { + GemtextNode::Heading1(t) => self.title = Some(t.clone()), + _ => {}, + } + } + self.lines.push(line); } fn list_item(&mut self, line: &'a str) { From 5113775933fe8e5ac5904809e47bb7f746c61bff Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 10:27:45 -0400 Subject: [PATCH 05/15] Parser - implement preformatted mode --- src/connection/builder.rs | 2 +- src/connection/mod.rs | 6 +++++- src/connection/verifier.rs | 12 ++++++------ src/message/mod.rs | 6 +++++- src/message/parser.rs | 31 ++++++++++++++++++++++++------- 5 files changed, 41 insertions(+), 16 deletions(-) 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/message/mod.rs b/src/message/mod.rs index 1c64aba..4b16693 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -7,7 +7,11 @@ use serde::{Deserialize, Serialize}; mod error; mod link; mod parser; -pub use {error::Error, link::Link, parser::{GemtextNode, Parser}}; +pub use { + error::Error, + link::Link, + parser::{GemtextNode, Parser}, +}; #[derive(Clone, Debug, PartialEq)] pub struct Recipients { diff --git a/src/message/parser.rs b/src/message/parser.rs index dd0544a..1a3ba0f 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -1,6 +1,6 @@ -use std::fmt; -use crate::prelude::Mailbox; use super::{Link, Recipients}; +use crate::prelude::Mailbox; +use std::fmt; #[derive(Debug)] pub struct PreBlk<'a> { @@ -121,7 +121,7 @@ impl<'a> Parser<'a> { Self { state: State::Normal, title: None, - lines: vec![] + lines: vec![], } } @@ -145,7 +145,7 @@ impl<'a> Parser<'a> { if self.title.is_none() { match &line { GemtextNode::Heading1(t) => self.title = Some(t.clone()), - _ => {}, + _ => {} } } self.lines.push(line); @@ -172,6 +172,17 @@ impl<'a> Parser<'a> { self.state = State::Preformatted(preblk); } + fn leave_preformatted(&mut self) { + match &self.state { + State::Preformatted(v) => { + let s = v.lines.join("\n").to_string(); + self.lines.push(GemtextNode::Text(s)); + self.state = State::Normal; + } + _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), + } + } + fn senders(&mut self, line: &'a str) { self.lines.push(GemtextNode::parse_senders(line)); } @@ -190,16 +201,22 @@ impl<'a> Parser<'a> { s if s.starts_with('<') => self.senders(s), s if s.starts_with(':') => self.recipients(s), s if s.starts_with('@') => {}, - s => {}, + s => self.lines.push(GemtextNode::Text(s.to_string())), } } fn parse_preformatted(&mut self, line: &'a str) { - todo!() + if line.starts_with("```") { + self.leave_preformatted(); + } else { + match &mut self.state { + State::Preformatted(p) => p.lines.push(line), + _ => panic!("Attempt to parse as preformatted when not in preformatted mode"), + } + } } fn parse_quote(&mut self, line: &'a str) { todo!() } } - From c36009db461aba73253ae7fb027eb50a14fc94b8 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 11:09:39 -0400 Subject: [PATCH 06/15] Handle leaving quote mode properly; --- src/message/parser.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/message/parser.rs b/src/message/parser.rs index 1a3ba0f..98e531e 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -26,7 +26,7 @@ pub enum GemtextNode { Heading3(String), ListItem(String), Quote(String), - Preformatted(String), + Preformatted(Option, String), Link(Link), } @@ -42,7 +42,10 @@ impl fmt::Display for GemtextNode { Self::Heading3(h) => writeln!(f, "### {h}"), Self::ListItem(l) => writeln!(f, "* {l}"), Self::Quote(q) => writeln!(f, "> {q}"), - Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), + Self::Preformatted(a, p) => match a { + None => writeln!(f, "```\n{}\n```", p), + Some(alt) => writeln!(f, "```{alt}\n{}\n```", p), + }, Self::Link(l) => writeln!(f, "=> {l}"), } } @@ -155,13 +158,25 @@ impl<'a> Parser<'a> { self.lines.push(GemtextNode::parse_list_item(line)); } - fn quote(&mut self, line: &'a str) { + fn enter_quote(&mut self, line: &'a str) { match line.split_once(char::is_whitespace) { Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), _ => self.lines.push(GemtextNode::Text(line.to_string())), } } + fn leave_quote(&mut self, line: &'a str) { + match &mut self.state { + State::Quote(q) => { + let quote = q.join("\n").to_string(); + self.lines.push(GemtextNode::Quote(quote)); + } + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + self.state = State::Normal; + self.lines.push(GemtextNode::Text(line.to_string())); + } + fn enter_preformatted(&mut self, line: &'a str) { let alt = if line.len() > 3 { Some(line[3..].trim()) @@ -196,7 +211,7 @@ impl<'a> Parser<'a> { s if s.starts_with("=>") => self.link(s), s if s.starts_with('#') => self.heading(s), s if s.starts_with('*') => self.list_item(s), - s if s.starts_with('>') => self.quote(s), + s if s.starts_with('>') => self.enter_quote(s), s if s.starts_with("```") => self.enter_preformatted(s), s if s.starts_with('<') => self.senders(s), s if s.starts_with(':') => self.recipients(s), @@ -217,6 +232,13 @@ impl<'a> Parser<'a> { } fn parse_quote(&mut self, line: &'a str) { - todo!() + if let Some(suffix) = line.strip_prefix('>') { + match &mut self.state { + State::Quote(q) => q.push(suffix.trim()), + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + } else { + self.leave_quote(line); + } } } From 4b43b283dd606d989a71b5dcd7cbc3e4067c28c3 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 2 Jun 2023 11:13:22 -0400 Subject: [PATCH 07/15] Parser - fix for pushing wrong line type when leaving preformatted block --- src/message/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/message/parser.rs b/src/message/parser.rs index 98e531e..2913c42 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -191,7 +191,7 @@ impl<'a> Parser<'a> { match &self.state { State::Preformatted(v) => { let s = v.lines.join("\n").to_string(); - self.lines.push(GemtextNode::Text(s)); + self.lines.push(GemtextNode::Preformatted(v.alt.map(str::to_string), s)); self.state = State::Normal; } _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), From 98c8d2ac6e83a18bf33f7cb803d266599ba58ab0 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Mon, 5 Jun 2023 11:10:53 -0400 Subject: [PATCH 08/15] Add `FromStr` for `Recipients` --- src/message/error.rs | 9 ++++++++- src/message/mod.rs | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/message/error.rs b/src/message/error.rs index 20d4b7c..0097d6a 100644 --- a/src/message/error.rs +++ b/src/message/error.rs @@ -1,4 +1,4 @@ -use {crate::prelude::ParseHostError, std::fmt}; +use {crate::prelude::{ParseHostError, ParseMailboxError}, std::fmt}; #[derive(Debug, PartialEq)] /// Errors which can occur when parsing a request @@ -8,6 +8,7 @@ pub enum Error { EmptyHost, EmptyMessage, ParseHostError(ParseHostError), + ParseMailboxError(ParseMailboxError), MalformedLink, } @@ -34,3 +35,9 @@ impl From for Error { Self::ParseHostError(value) } } + +impl From for Error { + fn from(value: ParseMailboxError) -> Self { + Self::ParseMailboxError(value) + } +} diff --git a/src/message/mod.rs b/src/message/mod.rs index 4b16693..6acd3e3 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,4 +1,4 @@ -use crate::prelude::{Host, Mailbox}; +use crate::prelude::Mailbox; use std::{fmt, str::FromStr}; #[cfg(feature = "serde")] @@ -25,6 +25,22 @@ impl fmt::Display for Recipients { } } +impl FromStr for Recipients { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut rec = Recipients { boxes: vec![] }; + if let Some(s) = s.strip_prefix(':') { + s.split_whitespace().try_for_each(|r| { + let r = r.parse()?; + rec.boxes.push(r); + Ok::<(), Error>(()) + })?; + } + Ok(rec) + } +} + #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct Message { From bad5a230cc069390d0670326c30cd36af4505765 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Mon, 5 Jun 2023 11:23:15 -0400 Subject: [PATCH 09/15] crate::message - add tests (two failing due to not being implemented) --- src/message/mod.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/message/mod.rs b/src/message/mod.rs index 6acd3e3..f9f1ece 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -75,3 +75,32 @@ impl FromStr for Message { todo!() } } + +#[cfg(test)] +mod tests { + use super::*; + + static REC: &'static str = ": joe@example.org, jane@pizza.hut, mark@gemi.dev"; + + #[test] + fn parse_recipients() { + let rec: Recipients = REC.parse().unwrap(); + assert!(rec.boxes.len() == 3); + } + + #[test] + fn print_recipients() { + let rec: Recipients = REC.parse().unwrap(); + assert_eq!(rec.to_string(), REC); + } + + #[test] + fn parse_message() { + todo!() + } + + #[test] + fn print_message() { + todo!() + } +} From ff96b5b56be8cbc08c037d0e1597ff3039c180df Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Tue, 6 Jun 2023 14:00:31 -0400 Subject: [PATCH 10/15] Fix stack overflow when converting status enum back to u8 TODO: Add more test coverage! --- src/sender/mod.rs | 7 ++++--- src/status/mod.rs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/sender/mod.rs b/src/sender/mod.rs index fa3a762..6aab624 100644 --- a/src/sender/mod.rs +++ b/src/sender/mod.rs @@ -70,7 +70,8 @@ where let cfg = match client_cert { None => cfg.with_no_client_auth(), Some(c) => { - let rustls_cert = rustls::Certificate::read_bytes(&c.der)?; + //let rustls_cert = rustls::Certificate::read_bytes(&c.der)?; + let rustls_cert = rustls::Certificate(c.der); let cert_chain = vec![rustls_cert]; let key_der = rustls::PrivateKey(c.key); cfg.with_single_cert(cert_chain, key_der)? @@ -79,8 +80,8 @@ where let client = ClientConnection::new(Arc::new(cfg), dnsname)?; let mut stream = StreamOwned::new(client, tcp_stream); stream.write_all(self.request.to_string().as_bytes())?; - let mut buf = vec![]; - stream.read_to_end(&mut buf)?; + let mut buf = Vec::with_capacity(1024); + let _res = stream.read_to_end(&mut buf); stream.conn.send_close_notify(); drop(stream); let res = buf.try_into()?; diff --git a/src/status/mod.rs b/src/status/mod.rs index ca88b1d..1ddb6f7 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -1,4 +1,6 @@ mod error; +use std::fmt; + pub use error::Error; #[cfg(feature = "serde")] @@ -32,7 +34,8 @@ pub enum Status { impl From for u8 { fn from(value: Status) -> Self { match value { - Status::Input | Status::Success => value.into(), + Status::Input => 10, + Status::Success => 20, Status::Redirect(n) => 30 + n as u8, Status::TemporaryFailure(n) => 40 + n as u8, Status::PermanentFailure(n) => 50 + n as u8, @@ -200,6 +203,18 @@ impl TryFrom for AuthenticationFailure { mod tests { use super::*; + #[test] + fn to_number_success() { + let num: u8 = Status::Success.into(); + assert_eq!(num, 20); + } + + #[test] + fn to_number_redirect() { + let num: u8 = Status::Redirect(Redirect::Temporary).into(); + assert_eq!(num, 30); + } + #[test] fn parse_status_success() { let status = Status::try_from(21).unwrap(); From 4642443d37e8a451edf63926c29372e9631a9f35 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Wed, 7 Jun 2023 13:49:19 -0400 Subject: [PATCH 11/15] Wrote message parser --- src/connection/verifier.rs | 3 +- src/gemtext/mod.rs | 2 + src/gemtext/parser.rs | 244 +++++++++++++++++++++++++++++ src/lib.rs | 1 + src/mailstore/filesystem.rs | 9 +- src/message/error.rs | 7 +- src/message/mod.rs | 20 +-- src/message/parser.rs | 305 +++++++++--------------------------- src/prelude.rs | 3 +- src/sender/mod.rs | 2 +- src/status/mod.rs | 2 - 11 files changed, 343 insertions(+), 255 deletions(-) create mode 100644 src/gemtext/mod.rs create mode 100644 src/gemtext/parser.rs diff --git a/src/connection/verifier.rs b/src/connection/verifier.rs index 745916b..3e40f3e 100644 --- a/src/connection/verifier.rs +++ b/src/connection/verifier.rs @@ -1,6 +1,5 @@ +use crate::mailuser::Mailuser; use rustls::server::ClientCertVerifier; - -use crate::{mailuser::Mailuser, prelude::CertificateStore}; use std::sync::Mutex; #[derive(Debug)] diff --git a/src/gemtext/mod.rs b/src/gemtext/mod.rs new file mode 100644 index 0000000..4bcc143 --- /dev/null +++ b/src/gemtext/mod.rs @@ -0,0 +1,2 @@ +pub mod parser; +pub use parser::{GemtextNode, Parser}; diff --git a/src/gemtext/parser.rs b/src/gemtext/parser.rs new file mode 100644 index 0000000..86e2d03 --- /dev/null +++ b/src/gemtext/parser.rs @@ -0,0 +1,244 @@ +use crate::prelude::{Link, Mailbox, Recipients}; +use std::fmt; + +#[derive(Debug)] +pub struct PreBlk<'a> { + alt: Option<&'a str>, + lines: Vec<&'a str>, +} + +#[derive(Debug)] +enum State<'a> { + Normal, + Preformatted(PreBlk<'a>), + Quote(Vec<&'a str>), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GemtextNode { + Sender(Mailbox), + Recipients(Recipients), + Timestamp(String), + Text(String), + Heading1(String), + Heading2(String), + Heading3(String), + ListItem(String), + Quote(String), + Preformatted(Option, String), + Link(Link), +} + +impl fmt::Display for GemtextNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Recipients(r) => writeln!(f, "{r}"), + Self::Sender(m) => writeln!(f, "{m}"), + Self::Timestamp(t) => writeln!(f, "@ {t}"), + Self::Text(t) => writeln!(f, "{t}"), + Self::Heading1(h) => writeln!(f, "# {h}"), + Self::Heading2(h) => writeln!(f, "## {h}"), + Self::Heading3(h) => writeln!(f, "### {h}"), + Self::ListItem(l) => writeln!(f, "* {l}"), + Self::Quote(q) => writeln!(f, "> {q}"), + Self::Preformatted(a, p) => match a { + None => writeln!(f, "```\n{}\n```", p), + Some(alt) => writeln!(f, "```{alt}\n{}\n```", p), + }, + Self::Link(l) => writeln!(f, "=> {l}"), + } + } +} + +impl<'a> GemtextNode { + fn parse_link(text: &'a str) -> Self { + if let Ok(link) = text.parse() { + Self::Link(link) + } else { + Self::Text(text.to_string()) + } + } + + fn parse_heading(text: &'a str) -> Self { + if let Some((h, s)) = text.split_once(char::is_whitespace) { + match h { + "#" => Self::Heading1(s.to_string()), + "##" => Self::Heading2(s.to_string()), + "###" => Self::Heading3(s.to_string()), + _ => Self::Text(text.to_string()), + } + } else { + Self::Text(text.to_string()) + } + } + + fn parse_list_item(text: &'a str) -> Self { + match text.split_once(char::is_whitespace) { + Some((pre, s)) if pre == "*" => GemtextNode::ListItem(s.to_string()), + _ => GemtextNode::Text(text.to_string()), + } + } + + fn parse_senders(text: &'a str) -> Self { + let Some(line) = text.strip_prefix('<') else { + return Self::Text(text.to_string()); + }; + if let Ok(user) = line.parse() { + Self::Sender(user) + } else { + Self::Text(text.to_string()) + } + } + + fn parse_recipients(text: &'a str) -> Self { + let Some(line) = text.strip_prefix(':') else { + return Self::Text(text.to_string()); + }; + let split = line.split_whitespace(); + let mut recipients: Recipients = Recipients { boxes: vec![] }; + for s in split { + if let Ok(m) = s.parse() { + recipients.boxes.push(m); + } else { + return Self::Text(text.to_string()); + } + } + Self::Recipients(recipients) + } + + fn parse_timestamp(text: &'a str) -> Self { + todo!() + } +} + +#[derive(Debug)] +pub struct Parser<'a> { + state: State<'a>, + title: Option, + lines: Vec, +} + +impl<'a> Parser<'a> { + pub fn new() -> Self { + Self { + state: State::Normal, + title: None, + lines: vec![], + } + } + + pub fn parse(mut self, raw: &'a str) -> Vec { + for line in raw.lines() { + match self.state { + State::Normal => self.parse_normal(line), + State::Preformatted(_) => self.parse_preformatted(line), + State::Quote(_) => self.parse_quote(line), + } + } + self.lines + } + + fn link(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_link(line)); + } + + fn heading(&mut self, line: &'a str) { + let line = GemtextNode::parse_heading(line); + if self.title.is_none() { + match &line { + GemtextNode::Heading1(t) => self.title = Some(t.clone()), + _ => {} + } + } + self.lines.push(line); + } + + fn list_item(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_list_item(line)); + } + + fn enter_quote(&mut self, line: &'a str) { + match line.split_once(char::is_whitespace) { + Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), + _ => self.lines.push(GemtextNode::Text(line.to_string())), + } + } + + fn leave_quote(&mut self, line: &'a str) { + match &mut self.state { + State::Quote(q) => { + let quote = q.join("\n").to_string(); + self.lines.push(GemtextNode::Quote(quote)); + } + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + self.state = State::Normal; + self.lines.push(GemtextNode::Text(line.to_string())); + } + + fn enter_preformatted(&mut self, line: &'a str) { + let alt = if line.len() > 3 { + Some(line[3..].trim()) + } else { + None + }; + let preblk = PreBlk { alt, lines: vec![] }; + self.state = State::Preformatted(preblk); + } + + fn leave_preformatted(&mut self) { + match &self.state { + State::Preformatted(v) => { + let s = v.lines.join("\n").to_string(); + self.lines + .push(GemtextNode::Preformatted(v.alt.map(str::to_string), s)); + self.state = State::Normal; + } + _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), + } + } + + fn senders(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_senders(line)); + } + + fn recipients(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_recipients(line)); + } + + fn parse_normal(&mut self, line: &'a str) { + match line { + s if s.starts_with("=>") => self.link(s), + s if s.starts_with('#') => self.heading(s), + s if s.starts_with('*') => self.list_item(s), + s if s.starts_with('>') => self.enter_quote(s), + s if s.starts_with("```") => self.enter_preformatted(s), + s if s.starts_with('<') => self.senders(s), + s if s.starts_with(':') => self.recipients(s), + s if s.starts_with('@') => {} + s => self.lines.push(GemtextNode::Text(s.to_string())), + } + } + + fn parse_preformatted(&mut self, line: &'a str) { + if line.starts_with("```") { + self.leave_preformatted(); + } else { + match &mut self.state { + State::Preformatted(p) => p.lines.push(line), + _ => panic!("Attempt to parse as preformatted when not in preformatted mode"), + } + } + } + + fn parse_quote(&mut self, line: &'a str) { + if let Some(suffix) = line.strip_prefix('>') { + match &mut self.state { + State::Quote(q) => q.push(suffix.trim()), + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + } else { + self.leave_quote(line); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index bea027e..21dc3e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod certificate; pub mod connection; pub mod fingerprint; +pub mod gemtext; pub mod host; pub mod mailbox; pub mod mailstore; diff --git a/src/mailstore/filesystem.rs b/src/mailstore/filesystem.rs index 8cb1ea7..5308d1a 100644 --- a/src/mailstore/filesystem.rs +++ b/src/mailstore/filesystem.rs @@ -1,3 +1,5 @@ +use crate::message::Parser as MessageParser; + use super::*; use std::{ fs::{self, File}, @@ -64,8 +66,11 @@ impl MailStore for Filesystem { }; dir.filter_map(Result::ok).for_each(|e| { if let Ok(contents) = fs::read_to_string(e.path()) { - if let Ok(message) = contents.parse::() { - folder.messages.insert(message.id.clone(), message); + if let Some(p) = e.path().to_str() { + let parser = MessageParser::new(p); + if let Ok(message) = parser.parse(&contents) { + folder.messages.insert(message.id.clone(), message); + } } } }); diff --git a/src/message/error.rs b/src/message/error.rs index 0097d6a..8b4bb28 100644 --- a/src/message/error.rs +++ b/src/message/error.rs @@ -1,4 +1,7 @@ -use {crate::prelude::{ParseHostError, ParseMailboxError}, std::fmt}; +use { + crate::prelude::{ParseHostError, ParseMailboxError}, + std::fmt, +}; #[derive(Debug, PartialEq)] /// Errors which can occur when parsing a request @@ -7,6 +10,7 @@ pub enum Error { EmptyUser, EmptyHost, EmptyMessage, + EmptySender, ParseHostError(ParseHostError), ParseMailboxError(ParseMailboxError), MalformedLink, @@ -25,6 +29,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::ParseHostError(e) => Some(e), + Self::ParseMailboxError(e) => Some(e), _ => None, } } diff --git a/src/message/mod.rs b/src/message/mod.rs index f9f1ece..abc383e 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -7,13 +7,9 @@ use serde::{Deserialize, Serialize}; mod error; mod link; mod parser; -pub use { - error::Error, - link::Link, - parser::{GemtextNode, Parser}, -}; +pub use {error::Error, link::Link, parser::Parser}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Recipients { pub boxes: Vec, } @@ -55,6 +51,7 @@ pub struct Message { impl fmt::Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "< {}", self.from)?; if !self.senders.is_empty() { write!(f, "< ")?; self.senders.iter().try_for_each(|s| writeln!(f, "{s}"))?; @@ -64,18 +61,13 @@ impl fmt::Display for Message { self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?; writeln!(f)?; } + if let Some(ref t) = self.timstamp { + writeln!(f, "@ {t}")?; + } write!(f, "{}\r\n", self.body) } } -impl FromStr for Message { - type Err = String; - - fn from_str(s: &str) -> Result { - todo!() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/message/parser.rs b/src/message/parser.rs index 2913c42..b016989 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -1,244 +1,85 @@ -use super::{Link, Recipients}; -use crate::prelude::Mailbox; -use std::fmt; +use { + super::{Message, Recipients}, + crate::prelude::Mailbox, +}; -#[derive(Debug)] -pub struct PreBlk<'a> { - alt: Option<&'a str>, - lines: Vec<&'a str>, -} - -#[derive(Debug)] -enum State<'a> { - Normal, - Preformatted(PreBlk<'a>), - Quote(Vec<&'a str>), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum GemtextNode { - Sender(Mailbox), - Recipients(Recipients), - Timestamp(String), - Text(String), - Heading1(String), - Heading2(String), - Heading3(String), - ListItem(String), - Quote(String), - Preformatted(Option, String), - Link(Link), -} - -impl fmt::Display for GemtextNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Recipients(r) => writeln!(f, "{r}"), - Self::Sender(m) => writeln!(f, "{m}"), - Self::Timestamp(t) => writeln!(f, "@ {t}"), - Self::Text(t) => writeln!(f, "{t}"), - Self::Heading1(h) => writeln!(f, "# {h}"), - Self::Heading2(h) => writeln!(f, "## {h}"), - Self::Heading3(h) => writeln!(f, "### {h}"), - Self::ListItem(l) => writeln!(f, "* {l}"), - Self::Quote(q) => writeln!(f, "> {q}"), - Self::Preformatted(a, p) => match a { - None => writeln!(f, "```\n{}\n```", p), - Some(alt) => writeln!(f, "```{alt}\n{}\n```", p), - }, - Self::Link(l) => writeln!(f, "=> {l}"), - } - } -} - -impl<'a> GemtextNode { - fn parse_link(text: &'a str) -> Self { - if let Ok(link) = text.parse() { - Self::Link(link) - } else { - Self::Text(text.to_string()) - } - } - - fn parse_heading(text: &'a str) -> Self { - if let Some((h, s)) = text.split_once(char::is_whitespace) { - match h { - "#" => Self::Heading1(s.to_string()), - "##" => Self::Heading2(s.to_string()), - "###" => Self::Heading3(s.to_string()), - _ => Self::Text(text.to_string()), - } - } else { - Self::Text(text.to_string()) - } - } - - fn parse_list_item(text: &'a str) -> Self { - match text.split_once(char::is_whitespace) { - Some((pre, s)) if pre == "*" => GemtextNode::ListItem(s.to_string()), - _ => GemtextNode::Text(text.to_string()), - } - } - - fn parse_senders(text: &'a str) -> Self { - let Some(line) = text.strip_prefix('<') else { - return Self::Text(text.to_string()); - }; - if let Ok(user) = line.parse() { - Self::Sender(user) - } else { - Self::Text(text.to_string()) - } - } - - fn parse_recipients(text: &'a str) -> Self { - let Some(line) = text.strip_prefix(':') else { - return Self::Text(text.to_string()); - }; - let split = line.split_whitespace(); - let mut recipients: Recipients = Recipients { boxes: vec![] }; - for s in split { - if let Ok(m) = s.parse() { - recipients.boxes.push(m); - } else { - return Self::Text(text.to_string()); - } - } - Self::Recipients(recipients) - } - - fn parse_timestamp(text: &'a str) -> Self { - todo!() - } -} - -#[derive(Debug)] -pub struct Parser<'a> { - state: State<'a>, +#[derive(Debug, Default)] +pub struct Parser { + id: String, + from: Option, + senders: Vec, + recipients: Recipients, + timestamp: Option, title: Option, - lines: Vec, + body: String, } -impl<'a> Parser<'a> { - pub fn new() -> Self { - Self { - state: State::Normal, - title: None, - lines: vec![], - } +impl Parser { + pub fn new(id: &str) -> Self { + let mut p = Self::default(); + p.id = id.to_string(); + p } - pub fn parse(mut self, raw: &'a str) -> Vec { - for line in raw.lines() { - match self.state { - State::Normal => self.parse_normal(line), - State::Preformatted(_) => self.parse_preformatted(line), - State::Quote(_) => self.parse_quote(line), + pub fn parse(mut self, content: &str) -> Result { + let lines = content.lines(); + for l in lines { + match l { + s if s.starts_with("<") && self.from.is_none() && self.body.is_empty() => { + let s = s.strip_prefix('<').unwrap().trim(); + let from: Mailbox = s.parse()?; + self.from = Some(from); + } + s if s.starts_with("<") && self.body.is_empty() => { + let sndr = s.strip_prefix('<').unwrap().trim(); + let from: Mailbox = sndr.parse()?; + self.senders.push(from); + } + s if s.starts_with(":") && self.body.is_empty() => { + self.recipients = s.parse()?; + } + s if s.starts_with("@") && self.timestamp.is_none() && self.body.is_empty() => { + self.timestamp = Some(s.strip_prefix("@").unwrap().trim().to_string()); + } + s if s.starts_with("###") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("###").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + self.body.push_str(s); + self.body.push('\n'); + } + s if s.starts_with("##") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("##").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + self.body.push_str(s); + self.body.push('\n'); + } + s if s.starts_with("#") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("#").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + self.body.push_str(s); + self.body.push('\n'); + } + s => { + self.body.push_str(s); + self.body.push('\n'); + } } } - self.lines - } - - fn link(&mut self, line: &'a str) { - self.lines.push(GemtextNode::parse_link(line)); - } - - fn heading(&mut self, line: &'a str) { - let line = GemtextNode::parse_heading(line); - if self.title.is_none() { - match &line { - GemtextNode::Heading1(t) => self.title = Some(t.clone()), - _ => {} - } - } - self.lines.push(line); - } - - fn list_item(&mut self, line: &'a str) { - self.lines.push(GemtextNode::parse_list_item(line)); - } - - fn enter_quote(&mut self, line: &'a str) { - match line.split_once(char::is_whitespace) { - Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), - _ => self.lines.push(GemtextNode::Text(line.to_string())), - } - } - - fn leave_quote(&mut self, line: &'a str) { - match &mut self.state { - State::Quote(q) => { - let quote = q.join("\n").to_string(); - self.lines.push(GemtextNode::Quote(quote)); - } - _ => panic!("Attempt to parse as quote when not in quote mode"), - } - self.state = State::Normal; - self.lines.push(GemtextNode::Text(line.to_string())); - } - - fn enter_preformatted(&mut self, line: &'a str) { - let alt = if line.len() > 3 { - Some(line[3..].trim()) + if self.from.is_none() { + Err(super::Error::EmptySender) } else { - None - }; - let preblk = PreBlk { alt, lines: vec![] }; - self.state = State::Preformatted(preblk); - } - - fn leave_preformatted(&mut self) { - match &self.state { - State::Preformatted(v) => { - let s = v.lines.join("\n").to_string(); - self.lines.push(GemtextNode::Preformatted(v.alt.map(str::to_string), s)); - self.state = State::Normal; - } - _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), - } - } - - fn senders(&mut self, line: &'a str) { - self.lines.push(GemtextNode::parse_senders(line)); - } - - fn recipients(&mut self, line: &'a str) { - self.lines.push(GemtextNode::parse_recipients(line)); - } - - fn parse_normal(&mut self, line: &'a str) { - match line { - s if s.starts_with("=>") => self.link(s), - s if s.starts_with('#') => self.heading(s), - s if s.starts_with('*') => self.list_item(s), - s if s.starts_with('>') => self.enter_quote(s), - s if s.starts_with("```") => self.enter_preformatted(s), - s if s.starts_with('<') => self.senders(s), - s if s.starts_with(':') => self.recipients(s), - s if s.starts_with('@') => {}, - s => self.lines.push(GemtextNode::Text(s.to_string())), - } - } - - fn parse_preformatted(&mut self, line: &'a str) { - if line.starts_with("```") { - self.leave_preformatted(); - } else { - match &mut self.state { - State::Preformatted(p) => p.lines.push(line), - _ => panic!("Attempt to parse as preformatted when not in preformatted mode"), - } - } - } - - fn parse_quote(&mut self, line: &'a str) { - if let Some(suffix) = line.strip_prefix('>') { - match &mut self.state { - State::Quote(q) => q.push(suffix.trim()), - _ => panic!("Attempt to parse as quote when not in quote mode"), - } - } else { - self.leave_quote(line); + Ok(Message { + id: self.id, + from: self.from.unwrap(), + senders: self.senders, + recipients: self.recipients.boxes, + timstamp: self.timestamp, + title: self.title, + body: self.body, + }) } } } diff --git a/src/prelude.rs b/src/prelude.rs index f89579c..55b36cd 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,10 +1,11 @@ pub use super::{ certificate::{Certificate, CertificateStore, ClientCertificateStore}, fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, + gemtext::{GemtextNode, Parser}, host::{Error as ParseHostError, Host}, mailbox::{Error as ParseMailboxError, Mailbox}, mailuser::Mailuser, - message::{Error as ParseMessageError, GemtextNode, Message, Recipients}, + message::{Error as ParseMessageError, Link, Message, Recipients}, //receiver, request::{Error as ParseRequestError, Request}, response::{Error as ParseResponseError, Response}, diff --git a/src/sender/mod.rs b/src/sender/mod.rs index 6aab624..b9daef1 100644 --- a/src/sender/mod.rs +++ b/src/sender/mod.rs @@ -1,6 +1,6 @@ use { crate::prelude::{CertificateStore, ClientCertificateStore, Mailuser, Request, Response}, - rustls::{internal::msgs::codec::Codec, ClientConfig, ClientConnection, StreamOwned}, + rustls::{ClientConfig, ClientConnection, StreamOwned}, std::{ io::{self, Read, Write}, net::{TcpStream, ToSocketAddrs}, diff --git a/src/status/mod.rs b/src/status/mod.rs index 1ddb6f7..36a3ed3 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -1,6 +1,4 @@ mod error; -use std::fmt; - pub use error::Error; #[cfg(feature = "serde")] From 676c7b34dc12c3f16b01efc8d07a79f31fcf83bc Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Wed, 7 Jun 2023 16:00:03 -0400 Subject: [PATCH 12/15] Status - complete test coverage --- src/status/mod.rs | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/status/mod.rs b/src/status/mod.rs index 36a3ed3..c1a1777 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -68,7 +68,7 @@ pub enum Redirect { /// The mailbox has moved to a different address, and all future /// messages should be sent to that address. Permanent = 1, - Other, + Other = 2, } impl TryFrom for Redirect { @@ -103,7 +103,7 @@ pub enum TemporaryFailure { RateLimit = 4, /// The mailbox isn't accepting mail right now, but it might in the future. MailboxFull = 5, - Other, + Other = 6, } impl TryFrom for TemporaryFailure { @@ -140,7 +140,7 @@ pub enum PermanentFailure { DomainNotServiced = 3, /// Your request is malformed, and won't be accepted by the mailserver. BadRequest = 9, - Other, + Other = 4, } impl TryFrom for PermanentFailure { @@ -178,7 +178,7 @@ pub enum AuthenticationFailure { /// The mailserver needs you to complete a task to confirm that you are a /// legitimate sender. (This is reserved for a Hashcash style anti-spam measure). ProofRequired = 4, - Other, + Other = 5, } impl TryFrom for AuthenticationFailure { @@ -209,8 +209,40 @@ mod tests { #[test] fn to_number_redirect() { - let num: u8 = Status::Redirect(Redirect::Temporary).into(); - assert_eq!(num, 30); + assert_eq!(u8::from(Status::Redirect(Redirect::Temporary)), 30); + assert_eq!(u8::from(Status::Redirect(Redirect::Permanent)), 31); + assert_eq!(u8::from(Status::Redirect(Redirect::Other)), 32); + } + + #[test] + fn to_number_temporary() { + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::TemporaryError)), 40); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::ServerUnavailable)), 41); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::CgiError)), 42); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::ProxyError)), 43); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::RateLimit)), 44); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::MailboxFull)), 45); + assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::Other)), 46); + } + + #[test] + fn to_number_permanent() { + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::PermanentError)), 50); + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::MailboxNonexistent)), 51); + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::MailboxGone)), 52); + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::DomainNotServiced)), 53); + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::BadRequest)), 59); + assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::Other)), 54); + } + + #[test] + fn to_number_auth() { + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::CertificateRequired)), 60); + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::UnauthorizedSender)), 61); + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::CertificateInvalid)), 62); + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::IdentityMismatch)), 63); + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::ProofRequired)), 64); + assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::Other)), 65); } #[test] From 22972ed43d091e73e708a6ade4318710e4fa560f Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Wed, 7 Jun 2023 16:32:51 -0400 Subject: [PATCH 13/15] Added testing for message::Parser --- src/message/mod.rs | 14 ++----------- src/message/parser.rs | 46 ++++++++++++++++++++++++++++++++++++++----- test/ox42sc69.gmi | 5 +++++ 3 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 test/ox42sc69.gmi diff --git a/src/message/mod.rs b/src/message/mod.rs index abc383e..a9b3a41 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -44,7 +44,7 @@ pub struct Message { pub from: Mailbox, pub senders: Vec, pub recipients: Vec, - pub timstamp: Option, + pub timestamp: Option, pub title: Option, pub body: String, } @@ -61,7 +61,7 @@ impl fmt::Display for Message { self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?; writeln!(f)?; } - if let Some(ref t) = self.timstamp { + if let Some(ref t) = self.timestamp { writeln!(f, "@ {t}")?; } write!(f, "{}\r\n", self.body) @@ -85,14 +85,4 @@ mod tests { let rec: Recipients = REC.parse().unwrap(); assert_eq!(rec.to_string(), REC); } - - #[test] - fn parse_message() { - todo!() - } - - #[test] - fn print_message() { - todo!() - } } diff --git a/src/message/parser.rs b/src/message/parser.rs index b016989..5d2c44c 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -45,26 +45,34 @@ impl Parser { if let Some(t) = s.strip_prefix("###").map(|x| x.trim().to_string()) { self.title = Some(t); } + if !self.body.is_empty() { + self.body.push('\n'); + } self.body.push_str(s); - self.body.push('\n'); } s if s.starts_with("##") && self.title.is_none() => { if let Some(t) = s.strip_prefix("##").map(|x| x.trim().to_string()) { self.title = Some(t); } + if !self.body.is_empty() { + self.body.push('\n'); + } self.body.push_str(s); - self.body.push('\n'); } s if s.starts_with("#") && self.title.is_none() => { if let Some(t) = s.strip_prefix("#").map(|x| x.trim().to_string()) { self.title = Some(t); } + if !self.body.is_empty() { + self.body.push('\n'); + } self.body.push_str(s); - self.body.push('\n'); } s => { + if !self.body.is_empty() { + self.body.push('\n'); + } self.body.push_str(s); - self.body.push('\n'); } } } @@ -76,10 +84,38 @@ impl Parser { from: self.from.unwrap(), senders: self.senders, recipients: self.recipients.boxes, - timstamp: self.timestamp, + timestamp: self.timestamp, title: self.title, body: self.body, }) } } } + +#[cfg(test)] +mod tests { + use super::*; + + const RAW: &'static str = include_str!("../../test/ox42sc69.gmi"); + + #[test] + fn parse_message() { + let msg: Message = Parser::new("ox42sc69").parse(RAW).unwrap(); + assert_eq!(msg.id, "ox42sc69"); + assert_eq!(msg.from.to_string(), "joe@gemini.example.org"); + assert_eq!(msg.timestamp.unwrap(), "2023-06-07T16:09:42Z"); + assert!(msg.senders.is_empty()); + assert!(msg.recipients.is_empty()); + assert_eq!(msg.title.unwrap(), "How 'bout dose Bears?"); + assert_eq!(msg.body, "# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n"); + } + + #[test] + fn print_message() { + let msg: Message = Parser::new("ox42sc69").parse(RAW).unwrap(); + assert_eq!( + msg.to_string(), + "< joe@gemini.example.org\n@ 2023-06-07T16:09:42Z\n# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n\r\n" + ); + } +} diff --git a/test/ox42sc69.gmi b/test/ox42sc69.gmi new file mode 100644 index 0000000..900b67e --- /dev/null +++ b/test/ox42sc69.gmi @@ -0,0 +1,5 @@ +< joe@gemini.example.org +@ 2023-06-07T16:09:42Z +# How 'bout dose Bears? +When are they coming for dinner anyway? + From 608d63def9a26a8a841c5c5b0b45c98728cf9ad8 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Wed, 7 Jun 2023 22:11:07 -0400 Subject: [PATCH 14/15] Add a bunch of files to the test/mailstore directory for runnings tests against; Partially write tests for Gemtext parser --- src/gemtext/parser.rs | 86 +++++++++++++- src/message/parser.rs | 12 +- src/status/mod.rs | 111 +++++++++++++++--- .../mail.gmi.org/dick/Inbox/623273.gmi | 0 .../mail.gmi.org/dick/Inbox/738127.gmi | 0 .../mail.gmi.org/dick/Lists/158468.gmi | 0 .../mail.gmi.org/dick/Lists/431652.gmi | 0 test/mailstore/mail.gmi.org/dick/blurb | 1 + .../mail.gmi.org/jane/Inbox/488571.gmi | 0 .../mail.gmi.org/jane/Inbox/867017.gmi | 0 .../mail.gmi.org/jane/Lists/476324.gmi | 0 .../mail.gmi.org/jane/Lists/891249.gmi | 0 test/mailstore/mail.gmi.org/jane/blurb | 1 + .../misfin.example.org/dick/Inbox/619310.gmi} | 0 .../misfin.example.org/dick/Inbox/868379.gmi | 0 .../misfin.example.org/dick/Lists/448785.gmi | 0 .../misfin.example.org/dick/Lists/645815.gmi | 0 test/mailstore/misfin.example.org/dick/blurb | 1 + .../misfin.example.org/jane/Inbox/252366.gmi | 0 .../misfin.example.org/jane/Inbox/654064.gmi | 0 .../misfin.example.org/jane/Lists/620426.gmi | 0 .../misfin.example.org/jane/Lists/700070.gmi | 12 ++ test/mailstore/misfin.example.org/jane/blurb | 1 + 23 files changed, 199 insertions(+), 26 deletions(-) create mode 100644 test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi create mode 100644 test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi create mode 100644 test/mailstore/mail.gmi.org/dick/Lists/158468.gmi create mode 100644 test/mailstore/mail.gmi.org/dick/Lists/431652.gmi create mode 100644 test/mailstore/mail.gmi.org/dick/blurb create mode 100644 test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi create mode 100644 test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi create mode 100644 test/mailstore/mail.gmi.org/jane/Lists/476324.gmi create mode 100644 test/mailstore/mail.gmi.org/jane/Lists/891249.gmi create mode 100644 test/mailstore/mail.gmi.org/jane/blurb rename test/{ox42sc69.gmi => mailstore/misfin.example.org/dick/Inbox/619310.gmi} (100%) create mode 100644 test/mailstore/misfin.example.org/dick/Inbox/868379.gmi create mode 100644 test/mailstore/misfin.example.org/dick/Lists/448785.gmi create mode 100644 test/mailstore/misfin.example.org/dick/Lists/645815.gmi create mode 100644 test/mailstore/misfin.example.org/dick/blurb create mode 100644 test/mailstore/misfin.example.org/jane/Inbox/252366.gmi create mode 100644 test/mailstore/misfin.example.org/jane/Inbox/654064.gmi create mode 100644 test/mailstore/misfin.example.org/jane/Lists/620426.gmi create mode 100644 test/mailstore/misfin.example.org/jane/Lists/700070.gmi create mode 100644 test/mailstore/misfin.example.org/jane/blurb diff --git a/src/gemtext/parser.rs b/src/gemtext/parser.rs index 86e2d03..42a9955 100644 --- a/src/gemtext/parser.rs +++ b/src/gemtext/parser.rs @@ -80,7 +80,7 @@ impl<'a> GemtextNode { } fn parse_senders(text: &'a str) -> Self { - let Some(line) = text.strip_prefix('<') else { + let Some(line) = text.strip_prefix('<').map(|x| x.trim()) else { return Self::Text(text.to_string()); }; if let Ok(user) = line.parse() { @@ -107,7 +107,10 @@ impl<'a> GemtextNode { } fn parse_timestamp(text: &'a str) -> Self { - todo!() + let Some(line) = text.strip_prefix('@').map(|x| x.trim()) else { + return Self::Text(text.to_string()); + }; + Self::Timestamp(line.to_string()) } } @@ -206,6 +209,10 @@ impl<'a> Parser<'a> { self.lines.push(GemtextNode::parse_recipients(line)); } + fn timestamp(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_timestamp(line)); + } + fn parse_normal(&mut self, line: &'a str) { match line { s if s.starts_with("=>") => self.link(s), @@ -215,7 +222,7 @@ impl<'a> Parser<'a> { s if s.starts_with("```") => self.enter_preformatted(s), s if s.starts_with('<') => self.senders(s), s if s.starts_with(':') => self.recipients(s), - s if s.starts_with('@') => {} + s if s.starts_with('@') => self.timestamp(s), s => self.lines.push(GemtextNode::Text(s.to_string())), } } @@ -242,3 +249,76 @@ impl<'a> Parser<'a> { } } } + +#[cfg(test)] +mod tests { + use crate::prelude::Host; + + use super::*; + + static RAW: &'static str = + include_str!("../../test/mailstore/misfin.example.org/jane/Lists/700070.gmi"); + + fn parse_raw() -> Vec { + Parser::new().parse(RAW) + } + + #[test] + fn nodes() { + let nodes = parse_raw(); + let mut nodes = nodes.iter(); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Sender(Mailbox { + username: "ed".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string() + }, + blurb: Some("Eddy the Head".to_string()) + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Sender(Mailbox { + username: "bruce".to_string(), + host: Host { + subdomain: None, + domain: "dickinson".to_string(), + tld: "jam".to_string(), + }, + blurb: Some("Bruce Dickinson".to_string()), + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Recipients(Recipients { + boxes: vec![ + Mailbox { + username: "nicko".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string(), + }, + blurb: None, + }, + Mailbox { + username: "steve".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string() + }, + blurb: None, + } + ] + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Timestamp("2023-06-07T21:27:15Z".to_string()), + ) + } +} diff --git a/src/message/parser.rs b/src/message/parser.rs index 5d2c44c..dd5ef49 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -96,18 +96,22 @@ impl Parser { mod tests { use super::*; - const RAW: &'static str = include_str!("../../test/ox42sc69.gmi"); + const RAW: &'static str = + include_str!("../../test/mailstore/misfin.example.org/dick/Inbox/619310.gmi"); #[test] fn parse_message() { - let msg: Message = Parser::new("ox42sc69").parse(RAW).unwrap(); - assert_eq!(msg.id, "ox42sc69"); + let msg: Message = Parser::new("619310").parse(RAW).unwrap(); + assert_eq!(msg.id, "619310"); assert_eq!(msg.from.to_string(), "joe@gemini.example.org"); assert_eq!(msg.timestamp.unwrap(), "2023-06-07T16:09:42Z"); assert!(msg.senders.is_empty()); assert!(msg.recipients.is_empty()); assert_eq!(msg.title.unwrap(), "How 'bout dose Bears?"); - assert_eq!(msg.body, "# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n"); + assert_eq!( + msg.body, + "# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n" + ); } #[test] diff --git a/src/status/mod.rs b/src/status/mod.rs index c1a1777..ffe642a 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -216,33 +216,106 @@ mod tests { #[test] fn to_number_temporary() { - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::TemporaryError)), 40); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::ServerUnavailable)), 41); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::CgiError)), 42); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::ProxyError)), 43); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::RateLimit)), 44); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::MailboxFull)), 45); - assert_eq!(u8::from(Status::TemporaryFailure(TemporaryFailure::Other)), 46); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::TemporaryError)), + 40 + ); + assert_eq!( + u8::from(Status::TemporaryFailure( + TemporaryFailure::ServerUnavailable + )), + 41 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::CgiError)), + 42 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::ProxyError)), + 43 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::RateLimit)), + 44 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::MailboxFull)), + 45 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::Other)), + 46 + ); } #[test] fn to_number_permanent() { - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::PermanentError)), 50); - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::MailboxNonexistent)), 51); - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::MailboxGone)), 52); - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::DomainNotServiced)), 53); - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::BadRequest)), 59); - assert_eq!(u8::from(Status::PermanentFailure(PermanentFailure::Other)), 54); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::PermanentError)), + 50 + ); + assert_eq!( + u8::from(Status::PermanentFailure( + PermanentFailure::MailboxNonexistent + )), + 51 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::MailboxGone)), + 52 + ); + assert_eq!( + u8::from(Status::PermanentFailure( + PermanentFailure::DomainNotServiced + )), + 53 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::BadRequest)), + 59 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::Other)), + 54 + ); } #[test] fn to_number_auth() { - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::CertificateRequired)), 60); - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::UnauthorizedSender)), 61); - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::CertificateInvalid)), 62); - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::IdentityMismatch)), 63); - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::ProofRequired)), 64); - assert_eq!(u8::from(Status::AuthenticationFailure(AuthenticationFailure::Other)), 65); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::CertificateRequired + )), + 60 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::UnauthorizedSender + )), + 61 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::CertificateInvalid + )), + 62 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::IdentityMismatch + )), + 63 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::ProofRequired + )), + 64 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure(AuthenticationFailure::Other)), + 65 + ); } #[test] diff --git a/test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi b/test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi b/test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Lists/158468.gmi b/test/mailstore/mail.gmi.org/dick/Lists/158468.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Lists/431652.gmi b/test/mailstore/mail.gmi.org/dick/Lists/431652.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/blurb b/test/mailstore/mail.gmi.org/dick/blurb new file mode 100644 index 0000000..418b2fa --- /dev/null +++ b/test/mailstore/mail.gmi.org/dick/blurb @@ -0,0 +1 @@ +Richard Stallman diff --git a/test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi b/test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi b/test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Lists/476324.gmi b/test/mailstore/mail.gmi.org/jane/Lists/476324.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Lists/891249.gmi b/test/mailstore/mail.gmi.org/jane/Lists/891249.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/blurb b/test/mailstore/mail.gmi.org/jane/blurb new file mode 100644 index 0000000..b549676 --- /dev/null +++ b/test/mailstore/mail.gmi.org/jane/blurb @@ -0,0 +1 @@ +Janet Weiss diff --git a/test/ox42sc69.gmi b/test/mailstore/misfin.example.org/dick/Inbox/619310.gmi similarity index 100% rename from test/ox42sc69.gmi rename to test/mailstore/misfin.example.org/dick/Inbox/619310.gmi diff --git a/test/mailstore/misfin.example.org/dick/Inbox/868379.gmi b/test/mailstore/misfin.example.org/dick/Inbox/868379.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/Lists/448785.gmi b/test/mailstore/misfin.example.org/dick/Lists/448785.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/Lists/645815.gmi b/test/mailstore/misfin.example.org/dick/Lists/645815.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/blurb b/test/mailstore/misfin.example.org/dick/blurb new file mode 100644 index 0000000..a956b32 --- /dev/null +++ b/test/mailstore/misfin.example.org/dick/blurb @@ -0,0 +1 @@ +Rick Baker diff --git a/test/mailstore/misfin.example.org/jane/Inbox/252366.gmi b/test/mailstore/misfin.example.org/jane/Inbox/252366.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Inbox/654064.gmi b/test/mailstore/misfin.example.org/jane/Inbox/654064.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Lists/620426.gmi b/test/mailstore/misfin.example.org/jane/Lists/620426.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Lists/700070.gmi b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi new file mode 100644 index 0000000..27977f0 --- /dev/null +++ b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi @@ -0,0 +1,12 @@ +< ed@iron.maiden Eddy the Head +< bruce@dickinson.jam Bruce Dickinson +: nicko@iron.maiden steve@iron.maiden +@ 2023-06-07T21:27:15Z + +# Checking in +The new album's in the can. A few things: +* Needs more cowbell +* Adrian's guitar is too loud, drowning out the cowbell +* Steve has a fever + +> You're gonna want that cowbell! diff --git a/test/mailstore/misfin.example.org/jane/blurb b/test/mailstore/misfin.example.org/jane/blurb new file mode 100644 index 0000000..176be3c --- /dev/null +++ b/test/mailstore/misfin.example.org/jane/blurb @@ -0,0 +1 @@ +Jane Doh From ef67fe7aa114205cedc83e3b5848d29428e864ea Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Thu, 8 Jun 2023 10:20:08 -0400 Subject: [PATCH 15/15] Finished tests on the gemtext parser, revealing yet another bug which was also fixed. --- src/gemtext/parser.rs | 52 ++++++++++++++++++- .../misfin.example.org/jane/Lists/700070.gmi | 1 + 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/gemtext/parser.rs b/src/gemtext/parser.rs index 42a9955..ee08f0c 100644 --- a/src/gemtext/parser.rs +++ b/src/gemtext/parser.rs @@ -138,6 +138,14 @@ impl<'a> Parser<'a> { State::Quote(_) => self.parse_quote(line), } } + match self.state { + State::Normal => {} + State::Preformatted(_) => self.leave_preformatted(), + State::Quote(q) => { + let quote = q.join("\n").to_string(); + self.lines.push(GemtextNode::Quote(quote)); + } + } self.lines } @@ -319,6 +327,48 @@ mod tests { assert_eq!( nodes.next().cloned().unwrap(), GemtextNode::Timestamp("2023-06-07T21:27:15Z".to_string()), - ) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Heading1("Checking in".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("The new album's in the can. A few things:".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem("Needs more cowbell".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem( + "Adrian's guitar is too loud, drowning out the cowbell".to_string() + ), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem("Steve has a fever".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Link(Link { + url: "gemini://private.iron.maiden/clip.mp3".to_string(), + display: Some("clip".to_string()) + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Quote("You're gonna want that cowbell!".to_string()), + ); + assert!(nodes.next().is_none()); } } diff --git a/test/mailstore/misfin.example.org/jane/Lists/700070.gmi b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi index 27977f0..78dc6c7 100644 --- a/test/mailstore/misfin.example.org/jane/Lists/700070.gmi +++ b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi @@ -8,5 +8,6 @@ The new album's in the can. A few things: * Needs more cowbell * Adrian's guitar is too loud, drowning out the cowbell * Steve has a fever +=> gemini://private.iron.maiden/clip.mp3 clip > You're gonna want that cowbell!