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")]
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),

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
/// 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<String>,
}
// 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<Self, Self::Error> {
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<MentionUpload<'a>> for Mention {
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
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(())
}
}