Proper parsing and validation of incoming mentions
This commit is contained in:
parent
51fbea4cb1
commit
d30a272cbf
|
@ -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),
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue