diff options
Diffstat (limited to 'libgitmail/src/patch')
-rw-r--r-- | libgitmail/src/patch/mod.rs | 103 | ||||
-rw-r--r-- | libgitmail/src/patch/parsers.rs | 163 |
2 files changed, 266 insertions, 0 deletions
diff --git a/libgitmail/src/patch/mod.rs b/libgitmail/src/patch/mod.rs new file mode 100644 index 0000000..b4b09a4 --- /dev/null +++ b/libgitmail/src/patch/mod.rs @@ -0,0 +1,103 @@ +use crate::{Error, Result}; +use std::collections::HashMap; + +mod parsers; +use parsers::get_header; +pub use parsers::{Segment, Subject}; + +#[doc(hidden)] +pub type HeaderMap = HashMap<String, String>; + +/// Message ID of an email in a thread +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Id(pub String); + +impl Id { + pub fn from(headers: &HeaderMap, key: &str) -> Result<Self> { + get_header(&headers, key).and_then(|h| match h { + Header::Single(h) => Ok(Self(h)), + _ => Err(Error::FailedParsing), + }) + } +} + +/// A semi typed header value for mail +pub(crate) enum Header { + /// A single header value + Single(String), + /// A set of values that was separated by `,` + Multi(Vec<String>), +} + +impl Header { + fn single(self) -> Result<String> { + match self { + Self::Single(s) => Ok(s), + Self::Multi(_) => Err(Error::FailedParsing), + } + } +} + +/// A single patch, metadata and email body +/// +/// This type is constructed from a single email in a thread, and can +/// then be combined into a [PatchSet](struct.PatchSet.html) via the +/// [PatchTree](struct.PatchTree.html) builder. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Patch<'mail> { + pub id: Id, + pub reply_to: Option<Id>, + pub subject: Subject, + pub headers: HeaderMap, + pub raw: &'mail str, +} + +impl<'mail> Patch<'mail> { + #[doc(hidden)] + pub fn preprocess(raw: &'mail str) -> HeaderMap { + let mail = mailparse::parse_mail(raw.as_bytes()) + .map_err(|_| Error::FailedParsing) + .unwrap(); + mail + .headers + .into_iter() + .fold(HashMap::new(), |mut acc, header| { + let key = header.get_key().unwrap(); + let val = header.get_value().unwrap(); + acc.insert(key, val); + acc + }) + } + + pub fn new(raw: &'mail str) -> Result<Self> { + let mail = mailparse::parse_mail(raw.as_bytes()) + .map_err(|_| Error::FailedParsing)?; + let headers = + mail + .headers + .into_iter() + .fold(HashMap::new(), |mut acc, header| { + let key = header.get_key().unwrap(); + let val = header.get_value().unwrap(); + acc.insert(key, val); + acc + }); + + get_header(&headers, "X-Mailer").and_then(|h| match h { + Header::Single(s) if s.contains("git-send-email") => Ok(()), + _ => Err(Error::NotAGitMail), + })?; + + Ok(Self { + id: Id::from(&headers, "Message-Id")?, + reply_to: Id::from(&headers, "In-Reply-To") + .map(|id| Some(id)) + .unwrap_or(None), + subject: get_header(&headers, "Subject") + .and_then(|h| h.single()) + .and_then(|s| Subject::from(s))?, + headers, + raw, + }) + } +} diff --git a/libgitmail/src/patch/parsers.rs b/libgitmail/src/patch/parsers.rs new file mode 100644 index 0000000..9779e2b --- /dev/null +++ b/libgitmail/src/patch/parsers.rs @@ -0,0 +1,163 @@ +//! Contains additional parsers that are required for git patch emails +//! +//! The parsers are written in nom, and generate +//! [Patch](../struct.Patch.html) objects, or an Error. A parser +//! should neven panic. +//! +//! There's also some unit and quick check tests that make sure that +//! valid git patch emails can be parsed. If you have emails you +//! don't mind becoming part of the test set, feel free to email in a +//! patch! + +use crate::{ + patch::{Header, HeaderMap}, + Error, Result, +}; +use nom::{ + bytes::complete::{is_not, tag, take_till}, + character::complete::char, + combinator::opt, + sequence::{delimited, preceded}, + IResult, +}; + +/// The segment data for a patch mail +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Segment { + /// The number of a patch in it's series + pub this: u8, + /// The total number of patches in a seriesThe first + pub max: u8, +} + +/// The typed subject line of a patch mail +/// +/// A message might be: "[PATCH v2 1/5], which is parsed into this +/// structure to be easier to work with +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Subject { + /// The patch revision (e.g. `v2`) + pub version: u8, + /// Patch segment information (e.g. `1/5`) + pub segment: Option<Segment>, + /// Subject prefix (e.g. `PATCH`) + pub prefix: String, + /// The actual patch commit message + pub message: String, +} + +impl Subject { + /// Create a typed subject from a subject string + pub fn from(s: String) -> Result<Self> { + Ok(Self::parse(s.as_str()).unwrap().1) + } + + /// This is a terrible function that does terrible things + fn parse(input: &str) -> IResult<&str, Self> { + let input = opt(tag("Re:"))(input).map(|(i, _)| i.trim())?; + let (msg, input) = Self::parens(input).map(|(a, b)| (a.trim(), b))?; + let (input, prefix) = + take_till(|c| c == ' ')(input).map(|(a, b)| (a.trim(), b))?; + + let (input, version) = + opt(preceded(tag("v"), take_till(|c| c == ' ')))(input) + .map(|(i, v)| (i.trim(), v.map_or(1, |v| v.parse::<u8>().unwrap())))?; + + let (max_str, this_opt) = opt(take_till(|c| c == '/'))(input)?; + + let segment = if max_str.len() > 1 { + let max = max_str[1..].parse::<u8>().unwrap_or(1); + let this = this_opt.unwrap().parse::<u8>().unwrap(); + Some(Segment { max, this }) + } else { + None + }; + + Ok(( + input, + Self { + message: msg.into(), + prefix: prefix.into(), + version, + segment, + }, + )) + } + + /// Grab text between the [ ... ] section of a subject + fn parens(input: &str) -> IResult<&str, &str> { + delimited(char('['), is_not("]"), char(']'))(input) + } +} + +/// A utility function that can get a header into a semi-typed field +pub(crate) fn get_header(headers: &HeaderMap, key: &str) -> Result<Header> { + headers.get(key).map_or(Err(Error::FailedParsing), |h| { + match h.split(",").map(|s| s.trim()).collect::<Vec<_>>() { + vec if vec.len() == 0 => Err(Error::FailedParsing), + vec if vec.len() == 1 => { + Ok(Header::Single(String::from(*vec.get(0).unwrap()))) + } + vec => Ok(Header::Multi( + vec.into_iter().map(|s| String::from(s)).collect(), + )), + } + }) +} + +#[test] +fn full_test() { + let subject = + "Re: [PATCH v2 2/3] docs: fix spelling of \"contributors\"".into(); + assert_eq!( + Subject::from(subject).unwrap(), + Subject { + message: "docs: fix spelling of \"contributors\"".into(), + prefix: "PATCH".into(), + version: 2, + segment: Some(Segment { max: 3, this: 2 }) + } + ); +} + +#[test] +fn minimal_test() { + let subject = "Re: [PATCH] docs: fix spelling of \"contributors\"".into(); + assert_eq!( + Subject::from(subject).unwrap(), + Subject { + message: "docs: fix spelling of \"contributors\"".into(), + prefix: "PATCH".into(), + version: 1, + segment: None + } + ); +} + +#[test] +fn version_test() { + let subject = "Re: [PATCH v4] docs: fix spelling of \"contributors\"".into(); + assert_eq!( + Subject::from(subject).unwrap(), + Subject { + message: "docs: fix spelling of \"contributors\"".into(), + prefix: "PATCH".into(), + version: 4, + segment: None + } + ); +} + +#[test] +fn double_digit_version_test() { + let subject = "Re: [PATCH v11] docs: fix spelling of \"contributors\"".into(); + assert_eq!( + Subject::from(subject).unwrap(), + Subject { + message: "docs: fix spelling of \"contributors\"".into(), + prefix: "PATCH".into(), + version: 11, + segment: None + } + ); +} |