mod error;
mod headers;
pub mod yenc;
pub use error::ParseError;
pub use headers::{HeaderIter, Headers};
use crate::types::protocol::MessageId;
use yenc::validate_yenc_structure;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Article<'a> {
pub message_id: MessageId<'a>,
pub article_number: Option<u64>,
pub headers: Option<Headers<'a>>,
pub body: Option<&'a [u8]>,
}
impl<'a> TryFrom<&'a [u8]> for Article<'a> {
type Error = ParseError;
fn try_from(buf: &'a [u8]) -> Result<Self, Self::Error> {
Self::parse(buf, true)
}
}
impl<'a> Article<'a> {
pub fn parse(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
let status_code = parse_status_code(buf)?;
match status_code {
220 => Self::parse_article(buf, validate_yenc),
221 => Self::parse_head(buf),
222 => Self::parse_body(buf, validate_yenc),
223 => Self::parse_stat(buf),
_ => Err(ParseError::InvalidStatusCode(status_code)),
}
}
fn parse_article(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
let first_line_end = find_line_end(buf, 0)?;
let first_line = &buf[..first_line_end];
let (message_id, article_number) = parse_first_line(first_line)?;
let content_start = first_line_end + 2;
let separator_pos = find_blank_line(buf, content_start)?;
let headers_data = &buf[content_start..separator_pos];
let headers = Some(Headers::parse(headers_data)?);
let body_start = separator_pos + 4;
let body_data = &buf[body_start..];
if validate_yenc && body_data.starts_with(b"=ybegin") {
validate_yenc_structure(body_data)?;
}
let body = Some(body_data);
Ok(Article {
message_id,
article_number,
headers,
body,
})
}
fn parse_head(buf: &'a [u8]) -> Result<Self, ParseError> {
let first_line_end = find_line_end(buf, 0)?;
let first_line = &buf[..first_line_end];
let (message_id, article_number) = parse_first_line(first_line)?;
let content_start = first_line_end + 2;
if find_blank_line(buf, content_start).is_ok() {
return Err(ParseError::UnexpectedBody);
}
let headers_data = &buf[content_start..];
let headers = Some(Headers::parse(headers_data)?);
Ok(Article {
message_id,
article_number,
headers,
body: None,
})
}
fn parse_body(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
let first_line_end = find_line_end(buf, 0)?;
let first_line = &buf[..first_line_end];
let (message_id, article_number) = parse_first_line(first_line)?;
let body_start = first_line_end + 2;
let body_data = &buf[body_start..];
if validate_yenc && body_data.starts_with(b"=ybegin") {
validate_yenc_structure(body_data)?;
}
let body = Some(body_data);
Ok(Article {
message_id,
article_number,
headers: None,
body,
})
}
fn parse_stat(buf: &'a [u8]) -> Result<Self, ParseError> {
let first_line_end = find_line_end(buf, 0)?;
let first_line = &buf[..first_line_end];
let (message_id, article_number) = parse_first_line(first_line)?;
let content_start = first_line_end + 2;
if content_start < buf.len() {
return Err(ParseError::UnexpectedBody);
}
Ok(Article {
message_id,
article_number,
headers: None,
body: None,
})
}
#[must_use]
pub fn decode(&self) -> Option<Vec<u8>> {
let mut decoded = Vec::with_capacity(self.body?.len());
if !self.decode_into(&mut decoded) {
return None;
}
Some(decoded)
}
#[must_use]
pub fn decode_into(&self, output: &mut Vec<u8>) -> bool {
let body = match self.body {
Some(b) if b.starts_with(b"=ybegin") => b,
_ => return false,
};
output.clear();
for byte in body
.split(|&b| b == b'\n')
.skip(1) .map(|line| line.strip_suffix(b"\r").unwrap_or(line))
.take_while(|line| !line.starts_with(b"=yend") && !line.starts_with(b"=ypart"))
.flat_map(yenc::decode_yenc_line)
{
output.push(byte);
}
true
}
}
fn parse_status_code(buf: &[u8]) -> Result<u16, ParseError> {
crate::protocol::StatusCode::parse(buf)
.map(|sc| sc.as_u16())
.ok_or(ParseError::InvalidStatusCode(0))
}
fn parse_first_line(line: &[u8]) -> Result<(MessageId<'_>, Option<u64>), ParseError> {
let first_space = memchr::memchr(b' ', line)
.ok_or_else(|| ParseError::InvalidMessageId("No space after status code".to_string()))?;
let second_space = memchr::memchr(b' ', &line[first_space + 1..])
.map(|pos| first_space + 1 + pos)
.ok_or_else(|| ParseError::InvalidMessageId("No article number".to_string()))?;
let number_bytes = &line[first_space + 1..second_space];
let article_number = std::str::from_utf8(number_bytes)
.ok()
.and_then(|s| s.parse::<u64>().ok());
let msg_id_start = memchr::memchr(b'<', &line[second_space..])
.map(|pos| second_space + pos)
.ok_or_else(|| ParseError::InvalidMessageId("No '<' found".to_string()))?;
let msg_id_end = memchr::memchr(b'>', &line[msg_id_start..])
.map(|pos| msg_id_start + pos + 1)
.ok_or_else(|| ParseError::InvalidMessageId("No '>' found".to_string()))?;
let msg_id_bytes = &line[msg_id_start..msg_id_end];
let msg_id_str = std::str::from_utf8(msg_id_bytes)
.map_err(|_| ParseError::InvalidMessageId("Invalid UTF-8 in message-id".to_string()))?;
let message_id = MessageId::from_borrowed(msg_id_str)?;
Ok((message_id, article_number))
}
fn find_line_end(buf: &[u8], start: usize) -> Result<usize, ParseError> {
for i in start..buf.len() {
if buf[i] == b'\r' && i + 1 < buf.len() && buf[i + 1] == b'\n' {
return Ok(i);
}
}
Err(ParseError::BufferTooShort)
}
fn find_blank_line(buf: &[u8], start: usize) -> Result<usize, ParseError> {
for i in start..buf.len().saturating_sub(3) {
if &buf[i..i + 4] == b"\r\n\r\n" {
return Ok(i);
}
}
Err(ParseError::MissingSeparator)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_status_code() {
assert_eq!(parse_status_code(b"220 OK"), Ok(220));
assert_eq!(parse_status_code(b"221 OK"), Ok(221));
assert!(parse_status_code(b"X").is_err());
}
#[test]
fn test_find_blank_line() {
let buf = b"220 0 <msg>\r\nSubject: Test\r\n\r\nBody";
let pos = find_blank_line(buf, 12).unwrap();
assert_eq!(&buf[pos..pos + 4], b"\r\n\r\n");
}
#[test]
fn test_parse_article_220() {
let buf = b"220 100 <test@example.com> article\r\n\
Subject: Test\r\n\
\r\n\
Body content\r\n";
let article = Article::try_from(&buf[..]).unwrap();
assert_eq!(article.message_id.as_str(), "<test@example.com>");
assert_eq!(article.article_number, Some(100));
assert!(article.headers.is_some());
assert!(article.body.is_some());
}
#[test]
fn test_decode_yenc_body() {
let buf = b"222 100 <test@example.com> body\r\n\
=ybegin line=128 size=12 name=test.txt\r\n\
r\x8f\x96\x96\x99VJ\xa3o\x98\x8dK\r\n\
=yend size=12 crc32=0337ab3d\r\n";
let article = Article::parse(&buf[..], true).unwrap();
let decoded = article.decode();
assert!(decoded.is_some());
let data = decoded.unwrap();
assert!(!data.is_empty());
}
#[test]
fn test_decode_non_yenc_returns_none() {
let buf = b"222 100 <test@example.com> body\r\n\
This is plain text, not yenc\r\n";
let article = Article::parse(&buf[..], false).unwrap();
let decoded = article.decode();
assert!(decoded.is_none());
}
#[test]
fn test_decode_no_body_returns_none() {
let buf = b"223 100 <test@example.com>\r\n";
let article = Article::parse(&buf[..], false).unwrap();
let decoded = article.decode();
assert!(decoded.is_none());
}
}