nntp_proxy/protocol/article/
mod.rs1mod error;
7mod headers;
8pub mod yenc;
9
10pub use error::ParseError;
11pub use headers::{HeaderIter, Headers};
12
13use crate::types::protocol::MessageId;
14use yenc::validate_yenc_structure;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Article<'a> {
25 pub message_id: MessageId<'a>,
26 pub article_number: Option<u64>,
27 pub headers: Option<Headers<'a>>,
28 pub body: Option<&'a [u8]>,
29}
30
31impl<'a> TryFrom<&'a [u8]> for Article<'a> {
32 type Error = ParseError;
33
34 fn try_from(buf: &'a [u8]) -> Result<Self, Self::Error> {
37 Self::parse(buf, true)
38 }
39}
40
41impl<'a> Article<'a> {
42 pub fn parse(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
52 let status_code = parse_status_code(buf)?;
54
55 match status_code {
57 220 => Self::parse_article(buf, validate_yenc),
58 221 => Self::parse_head(buf),
59 222 => Self::parse_body(buf, validate_yenc),
60 223 => Self::parse_stat(buf),
61 _ => Err(ParseError::InvalidStatusCode(status_code)),
62 }
63 }
64
65 fn parse_article(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
67 let first_line_end = find_line_end(buf, 0)?;
69 let first_line = &buf[..first_line_end];
70
71 let (message_id, article_number) = parse_first_line(first_line)?;
72
73 let content_start = first_line_end + 2;
75 let separator_pos = find_blank_line(buf, content_start)?;
76
77 let headers_data = &buf[content_start..separator_pos];
79 let headers = Some(Headers::parse(headers_data)?);
80
81 let body_start = separator_pos + 4;
83
84 let body_data = &buf[body_start..];
85
86 if validate_yenc && body_data.starts_with(b"=ybegin") {
88 validate_yenc_structure(body_data)?;
89 }
90
91 let body = Some(body_data);
92
93 Ok(Article {
94 message_id,
95 article_number,
96 headers,
97 body,
98 })
99 }
100
101 fn parse_head(buf: &'a [u8]) -> Result<Self, ParseError> {
103 let first_line_end = find_line_end(buf, 0)?;
105 let first_line = &buf[..first_line_end];
106
107 let (message_id, article_number) = parse_first_line(first_line)?;
108
109 let content_start = first_line_end + 2;
110 if find_blank_line(buf, content_start).is_ok() {
111 return Err(ParseError::UnexpectedBody);
112 }
113
114 let headers_data = &buf[content_start..];
115 let headers = Some(Headers::parse(headers_data)?);
116
117 Ok(Article {
118 message_id,
119 article_number,
120 headers,
121 body: None,
122 })
123 }
124
125 fn parse_body(buf: &'a [u8], validate_yenc: bool) -> Result<Self, ParseError> {
127 let first_line_end = find_line_end(buf, 0)?;
129 let first_line = &buf[..first_line_end];
130
131 let (message_id, article_number) = parse_first_line(first_line)?;
132
133 let body_start = first_line_end + 2;
134 let body_data = &buf[body_start..];
135
136 if validate_yenc && body_data.starts_with(b"=ybegin") {
138 validate_yenc_structure(body_data)?;
139 }
140
141 let body = Some(body_data);
142
143 Ok(Article {
144 message_id,
145 article_number,
146 headers: None,
147 body,
148 })
149 }
150
151 fn parse_stat(buf: &'a [u8]) -> Result<Self, ParseError> {
153 let first_line_end = find_line_end(buf, 0)?;
155 let first_line = &buf[..first_line_end];
156
157 let (message_id, article_number) = parse_first_line(first_line)?;
158
159 let content_start = first_line_end + 2;
160 if content_start < buf.len() {
161 return Err(ParseError::UnexpectedBody);
162 }
163
164 Ok(Article {
165 message_id,
166 article_number,
167 headers: None,
168 body: None,
169 })
170 }
171
172 #[must_use]
181 pub fn decode(&self) -> Option<Vec<u8>> {
182 let mut decoded = Vec::with_capacity(self.body?.len());
184 if !self.decode_into(&mut decoded) {
185 return None;
186 }
187 Some(decoded)
188 }
189
190 #[must_use]
203 pub fn decode_into(&self, output: &mut Vec<u8>) -> bool {
204 let body = match self.body {
206 Some(b) if b.starts_with(b"=ybegin") => b,
207 _ => return false,
208 };
209
210 output.clear();
211
212 for byte in body
215 .split(|&b| b == b'\n')
216 .skip(1) .map(|line| line.strip_suffix(b"\r").unwrap_or(line))
218 .take_while(|line| !line.starts_with(b"=yend") && !line.starts_with(b"=ypart"))
219 .flat_map(yenc::decode_yenc_line)
220 {
221 output.push(byte);
222 }
223
224 true
225 }
226}
227
228fn parse_status_code(buf: &[u8]) -> Result<u16, ParseError> {
230 crate::protocol::StatusCode::parse(buf)
231 .map(|sc| sc.as_u16())
232 .ok_or(ParseError::InvalidStatusCode(0))
233}
234
235fn parse_first_line(line: &[u8]) -> Result<(MessageId<'_>, Option<u64>), ParseError> {
237 let first_space = memchr::memchr(b' ', line)
241 .ok_or_else(|| ParseError::InvalidMessageId("No space after status code".to_string()))?;
242
243 let second_space = memchr::memchr(b' ', &line[first_space + 1..])
245 .map(|pos| first_space + 1 + pos)
246 .ok_or_else(|| ParseError::InvalidMessageId("No article number".to_string()))?;
247
248 let number_bytes = &line[first_space + 1..second_space];
250 let article_number = std::str::from_utf8(number_bytes)
251 .ok()
252 .and_then(|s| s.parse::<u64>().ok());
253
254 let msg_id_start = memchr::memchr(b'<', &line[second_space..])
256 .map(|pos| second_space + pos)
257 .ok_or_else(|| ParseError::InvalidMessageId("No '<' found".to_string()))?;
258
259 let msg_id_end = memchr::memchr(b'>', &line[msg_id_start..])
261 .map(|pos| msg_id_start + pos + 1)
262 .ok_or_else(|| ParseError::InvalidMessageId("No '>' found".to_string()))?;
263
264 let msg_id_bytes = &line[msg_id_start..msg_id_end];
266 let msg_id_str = std::str::from_utf8(msg_id_bytes)
267 .map_err(|_| ParseError::InvalidMessageId("Invalid UTF-8 in message-id".to_string()))?;
268 let message_id = MessageId::from_borrowed(msg_id_str)?;
269
270 Ok((message_id, article_number))
271}
272
273fn find_line_end(buf: &[u8], start: usize) -> Result<usize, ParseError> {
275 for i in start..buf.len() {
276 if buf[i] == b'\r' && i + 1 < buf.len() && buf[i + 1] == b'\n' {
277 return Ok(i);
278 }
279 }
280 Err(ParseError::BufferTooShort)
281}
282
283fn find_blank_line(buf: &[u8], start: usize) -> Result<usize, ParseError> {
285 for i in start..buf.len().saturating_sub(3) {
287 if &buf[i..i + 4] == b"\r\n\r\n" {
288 return Ok(i);
289 }
290 }
291 Err(ParseError::MissingSeparator)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_parse_status_code() {
300 assert_eq!(parse_status_code(b"220 OK"), Ok(220));
301 assert_eq!(parse_status_code(b"221 OK"), Ok(221));
302 assert!(parse_status_code(b"X").is_err());
303 }
304
305 #[test]
306 fn test_find_blank_line() {
307 let buf = b"220 0 <msg>\r\nSubject: Test\r\n\r\nBody";
308 let pos = find_blank_line(buf, 12).unwrap();
309 assert_eq!(&buf[pos..pos + 4], b"\r\n\r\n");
310 }
311
312 #[test]
313 fn test_parse_article_220() {
314 let buf = b"220 100 <test@example.com> article\r\n\
315 Subject: Test\r\n\
316 \r\n\
317 Body content\r\n";
318
319 let article = Article::try_from(&buf[..]).unwrap();
320 assert_eq!(article.message_id.as_str(), "<test@example.com>");
321 assert_eq!(article.article_number, Some(100));
322 assert!(article.headers.is_some());
323 assert!(article.body.is_some());
324 }
325
326 #[test]
327 fn test_decode_yenc_body() {
328 let buf = b"222 100 <test@example.com> body\r\n\
330 =ybegin line=128 size=12 name=test.txt\r\n\
331 r\x8f\x96\x96\x99VJ\xa3o\x98\x8dK\r\n\
332 =yend size=12 crc32=0337ab3d\r\n";
333
334 let article = Article::parse(&buf[..], true).unwrap();
335 let decoded = article.decode();
336
337 assert!(decoded.is_some());
338 let data = decoded.unwrap();
339 assert!(!data.is_empty());
341 }
342
343 #[test]
344 fn test_decode_non_yenc_returns_none() {
345 let buf = b"222 100 <test@example.com> body\r\n\
346 This is plain text, not yenc\r\n";
347
348 let article = Article::parse(&buf[..], false).unwrap();
349 let decoded = article.decode();
350
351 assert!(decoded.is_none());
352 }
353
354 #[test]
355 fn test_decode_no_body_returns_none() {
356 let buf = b"223 100 <test@example.com>\r\n";
357
358 let article = Article::parse(&buf[..], false).unwrap();
359 let decoded = article.decode();
360
361 assert!(decoded.is_none());
362 }
363}