From d30a272cbf0ea287b49a6692d728e348972119ef Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sat, 30 Mar 2024 11:52:26 +0100 Subject: [PATCH] Proper parsing and validation of incoming mentions --- src/error.rs | 6 +++ src/models/mentions.rs | 102 +++++++++++++++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/error.rs b/src/error.rs index 3dbe3d3..56f241c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,12 +12,18 @@ pub(crate) enum GementionError { #[error("Not a Titan resource")] NotTitanResource, + #[error("Invalid mention type: {0}")] + InvalidMentionType(String), + #[error("Invalid body")] InvalidBody, #[error("Gemention Target not provided")] TargetNotProvided, + #[error("Username not provided")] + UsernameNotProvided, + #[error("url parsing error: {0}")] UrlParsingError(#[from] url::ParseError), diff --git a/src/models/mentions.rs b/src/models/mentions.rs index 13a94ff..16bebf4 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -10,6 +10,21 @@ macro_rules! titan { }; } +fn parse_content_body(raw_body: &[u8]) -> Result<&str, GementionError> { + let content = std::str::from_utf8(raw_body)?; + + // Drop 1st line if it is a mention type. Otherwise return whole thing. + let mention_type = MentionType::try_from(raw_body); + let content: &str = if let Ok(_) = mention_type { + let amt_to_chop = content.lines().next().map(|line| line.len()).unwrap(); + &content[amt_to_chop..] + } else { + content + }; + + Ok(content.trim()) +} + /// Wraps an incoming Titan request and provides handy methods for /// extracting the expected information. pub(crate) struct MentionUpload<'a> { @@ -25,7 +40,7 @@ impl<'a> MentionUpload<'a> { } } - pub fn content(&self) -> &[u8] { + pub fn raw_body(&self) -> &[u8] { titan!(self).content.as_slice() } @@ -52,6 +67,10 @@ impl<'a> MentionUpload<'a> { pub fn target(&self) -> Option<&str> { self.client.parameter("target") } + + pub fn content(&self) -> Result<&str, GementionError> { + parse_content_body(self.raw_body()) + } } impl<'a> TryFrom<&'a Client> for MentionUpload<'a> { @@ -76,29 +95,26 @@ pub(crate) struct Mention { content: Option, } -// This is for converting an incoming Titan request into a mention. It -// is very flexible: If the incoming value matches a specific format, -// it'll accurately convert the stringi nto a proper mention. -// Otherwise, it just assumes comment. An error is returned only if -// input is not utf8. +// This is for converting an incoming Titan request into a mention +// type. It will attempt to parse the first line to identify the type. +// If the type cannot be identified, or if the string is not valid +// UTF-8, an error is returned. impl TryFrom<&[u8]> for MentionType { type Error = GementionError; fn try_from(content: &[u8]) -> Result { let content = std::str::from_utf8(&content)?; - //TODO 1st line = type (reply, like, etc) rest = comment - // content. user should be yoinked from the incoming - // connection. so we cannot do from &[u8] let content_type = content .lines() .next() - .map(|line| line.trim().to_lowercase()); + .map(|line| line.trim()) + .unwrap_or("[none provided]"); - Ok(match content_type { - Some(value) if value == "reply" => Self::Reply, - Some(value) if value == "like" => Self::Like, - _ => Self::Reply, - }) + match content_type.to_lowercase() { + value if value == "reply" => Ok(Self::Reply), + value if value == "like" => Ok(Self::Like), + _ => Err(GementionError::InvalidMentionType(content_type.to_owned())), + } } } @@ -107,14 +123,35 @@ impl<'a> TryFrom> for Mention { fn try_from(resource: MentionUpload<'a>) -> Result { if resource.mime() == "text/plain" { - let mention_type = MentionType::try_from(resource.content())?; - let target = resource.target().ok_or(GementionError::TargetNotProvided)?; + // Be flexible on mention type: if first line isn't a + // mention type, just assume reply. + let mention_type = match MentionType::try_from(resource.raw_body()) { + Ok(mention_type) => mention_type, + Err(GementionError::InvalidMentionType(_)) => MentionType::Reply, + Err(e) => return Err(e), + }; + + let target = resource + .target() + .ok_or(GementionError::TargetNotProvided)? + .to_owned(); + + let user = resource + .username() + .ok_or(GementionError::UsernameNotProvided)?; + + let content = resource.content()?.to_owned(); + let content = if !content.is_empty() { + Some(content) + } else { + None + }; Ok(Mention { + user, mention_type, - target: target.to_owned(), - user: "".to_string(), - content: None, + content, + target, }) } else { Err(GementionError::InvalidBody) @@ -175,3 +212,28 @@ impl GemBytes for MentionResponse { format!("20 text/gemini\r\n{}", self.status).into_bytes() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_comment_body_with_mention_type() -> Result<(), GementionError> { + let body = "reply\nthis is my comment body, which is a reply.".as_bytes(); + let expected = "this is my comment body, which is a reply."; + let parsed = parse_content_body(body)?; + + assert_eq!(expected, parsed); + Ok(()) + } + + #[test] + fn parse_comment_body_without_mention_type() -> Result<(), GementionError> { + let expected = "this is my comment body, which has no mention type."; + let body = expected.as_bytes(); + let parsed = parse_content_body(body)?; + + assert_eq!(expected, parsed); + Ok(()) + } +}