Proper parsing and validation of incoming mentions

This commit is contained in:
projectmoon 2024-03-30 11:52:26 +01:00
parent 51fbea4cb1
commit d30a272cbf
2 changed files with 88 additions and 20 deletions

View File

@ -12,12 +12,18 @@ pub(crate) enum GementionError {
#[error("Not a Titan resource")] #[error("Not a Titan resource")]
NotTitanResource, NotTitanResource,
#[error("Invalid mention type: {0}")]
InvalidMentionType(String),
#[error("Invalid body")] #[error("Invalid body")]
InvalidBody, InvalidBody,
#[error("Gemention Target not provided")] #[error("Gemention Target not provided")]
TargetNotProvided, TargetNotProvided,
#[error("Username not provided")]
UsernameNotProvided,
#[error("url parsing error: {0}")] #[error("url parsing error: {0}")]
UrlParsingError(#[from] url::ParseError), UrlParsingError(#[from] url::ParseError),

View File

@ -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 /// Wraps an incoming Titan request and provides handy methods for
/// extracting the expected information. /// extracting the expected information.
pub(crate) struct MentionUpload<'a> { 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() titan!(self).content.as_slice()
} }
@ -52,6 +67,10 @@ impl<'a> MentionUpload<'a> {
pub fn target(&self) -> Option<&str> { pub fn target(&self) -> Option<&str> {
self.client.parameter("target") 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> { impl<'a> TryFrom<&'a Client> for MentionUpload<'a> {
@ -76,29 +95,26 @@ pub(crate) struct Mention {
content: Option<String>, content: Option<String>,
} }
// This is for converting an incoming Titan request into a mention. It // This is for converting an incoming Titan request into a mention
// is very flexible: If the incoming value matches a specific format, // type. It will attempt to parse the first line to identify the type.
// it'll accurately convert the stringi nto a proper mention. // If the type cannot be identified, or if the string is not valid
// Otherwise, it just assumes comment. An error is returned only if // UTF-8, an error is returned.
// input is not utf8.
impl TryFrom<&[u8]> for MentionType { impl TryFrom<&[u8]> for MentionType {
type Error = GementionError; type Error = GementionError;
fn try_from(content: &[u8]) -> Result<Self, Self::Error> { fn try_from(content: &[u8]) -> Result<Self, Self::Error> {
let content = std::str::from_utf8(&content)?; 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 let content_type = content
.lines() .lines()
.next() .next()
.map(|line| line.trim().to_lowercase()); .map(|line| line.trim())
.unwrap_or("[none provided]");
Ok(match content_type { match content_type.to_lowercase() {
Some(value) if value == "reply" => Self::Reply, value if value == "reply" => Ok(Self::Reply),
Some(value) if value == "like" => Self::Like, value if value == "like" => Ok(Self::Like),
_ => Self::Reply, _ => Err(GementionError::InvalidMentionType(content_type.to_owned())),
}) }
} }
} }
@ -107,14 +123,35 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> { fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
if resource.mime() == "text/plain" { if resource.mime() == "text/plain" {
let mention_type = MentionType::try_from(resource.content())?; // Be flexible on mention type: if first line isn't a
let target = resource.target().ok_or(GementionError::TargetNotProvided)?; // 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 { Ok(Mention {
user,
mention_type, mention_type,
target: target.to_owned(), content,
user: "".to_string(), target,
content: None,
}) })
} else { } else {
Err(GementionError::InvalidBody) Err(GementionError::InvalidBody)
@ -175,3 +212,28 @@ impl GemBytes for MentionResponse {
format!("20 text/gemini\r\n{}", self.status).into_bytes() 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(())
}
}