1use std::{
2 convert::Infallible,
3 fmt::Display,
4 num::ParseIntError,
5 str::{from_utf8, FromStr},
6};
7
8use itertools::Itertools;
9use serde::{Deserialize, Serialize};
10
11use crate::connection::{Status, StatusCode};
12
13macro_rules! regex {
14 ($re:literal $(,)?) => {{
15 static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
16 RE.get_or_init(|| regex::Regex::new($re).unwrap())
17 }};
18}
19
20#[derive(Clone, Debug)]
21pub struct Article {
22 pub headers: Headers,
23 pub body: Body,
24}
25
26impl Article {
27 pub fn from_u8(bytes: &[u8]) -> Result<Self, ArticleParseError> {
28 if let Some((headers, body)) = split_text(bytes) {
29 Ok(Article {
30 headers: Headers::from_u8(headers)?,
31 body: Body::new(body.to_vec()),
32 })
33 } else {
34 Err(ArticleParseError)
35 }
36 }
37}
38
39fn split_text(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
40 for n in 0..bytes.len() - 4 {
41 if bytes[n..n + 4] == [0xd, 0xa, 0xd, 0xa] {
42 let headers = &bytes[0..n];
43 let body = &bytes[n + 4..];
44 return Some((headers, body));
45 }
46 }
47
48 None
49}
50
51pub struct ArticleParseError;
52
53#[derive(Clone, Debug)]
54pub struct Headers(Vec<Header>);
55
56impl Headers {
57 pub fn from_u8(bytes: &[u8]) -> Result<Headers, ArticleParseError> {
58 from_utf8(bytes)
59 .map_err(|_| ArticleParseError)
60 .and_then(|text| text.parse::<Headers>().map_err(|_| ArticleParseError))
61 }
62}
63
64impl FromStr for Headers {
65 type Err = Vec<HeaderParseError>;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 let (successes, failures): (Vec<Header>, Vec<HeaderParseError>) =
69 s.lines().map(Header::from_str).partition_result();
70
71 if failures.is_empty() {
72 Ok(Headers(successes))
73 } else {
74 Err(failures)
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
80pub struct Header {
81 pub key: String,
82 pub value: String,
83}
84
85impl FromStr for Header {
86 type Err = HeaderParseError;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 let mut header = s.splitn(2, ':');
90 header
91 .next()
92 .and_then(|key| {
93 header.next().map(|val| Header {
94 key: key.trim().to_string(),
95 value: val.trim().to_string(),
96 })
97 })
98 .ok_or_else(|| HeaderParseError(s.to_string()))
99 }
100}
101
102pub struct HeaderParseError(String);
103
104#[derive(Clone, Debug)]
105pub struct Body(Vec<u8>);
106
107impl Body {
108 pub fn new(data: Vec<u8>) -> Self {
109 Body(data)
110 }
111
112 pub fn bytes(&self) -> Vec<u8> {
113 self.0.to_vec()
114 }
115}
116
117#[derive(Debug, PartialEq, Eq)]
118pub struct ArticleStatus {
119 pub status_code: StatusCode,
120 pub article_number: ArticleNumber,
121 pub message_id: MessageId,
122 pub message: String,
123}
124
125impl TryFrom<Status> for ArticleStatus {
126 type Error = ArticleStatusParseError;
127
128 fn try_from(status: Status) -> Result<Self, Self::Error> {
129 regex!(r"^(?P<article_number>\d+) <(?P<message_id>.+)> (?P<message>.+)$")
130 .captures(&status.message)
131 .and_then(|caps| {
132 let article_number = caps
133 .name("article_number")?
134 .as_str()
135 .parse::<ArticleNumber>()
136 .ok()?;
137 let message_id = caps
138 .name("message_id")?
139 .as_str()
140 .parse::<MessageId>()
141 .ok()?;
142 let message = caps.name("message")?.as_str().to_string();
143
144 Some(ArticleStatus {
145 status_code: status.status_code,
146 article_number,
147 message_id,
148 message,
149 })
150 })
151 .ok_or(ArticleStatusParseError)
152 }
153}
154
155#[derive(Debug, PartialEq, Eq)]
156pub struct ArticleStatusParseError;
157
158#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, Debug)]
159pub struct ArticleNumber(usize);
160
161impl From<usize> for ArticleNumber {
162 fn from(val: usize) -> Self {
163 Self(val)
164 }
165}
166
167impl FromStr for ArticleNumber {
168 type Err = ParseIntError;
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
171 s.parse().map(Self)
172 }
173}
174
175#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Deserialize, Serialize)]
176pub struct MessageId(String);
177
178impl FromStr for MessageId {
179 type Err = Infallible;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 String::from_str(s).map(Self)
183 }
184}
185
186impl Display for MessageId {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 self.0.fmt(f)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::str::FromStr;
195
196 use crate::{
197 article::{split_text, ArticleNumber, ArticleStatus, ArticleStatusParseError, MessageId},
198 connection::{Status, StatusCode},
199 };
200
201 #[test]
202 fn split_text_with_delimiter() {
203 let result: Option<(&[u8], &[u8])> = split_text(b"a\r\n\r\nb");
204 let expected: Option<(&[u8], &[u8])> = Some((b"a", b"b"));
205 assert_eq!(result, expected);
206 }
207
208 #[test]
209 fn split_text_without_delimiter() {
210 let result: Option<(&[u8], &[u8])> = split_text(b"a\r\r\nb");
211 assert_eq!(result, None);
212 }
213
214 #[test]
215 fn parse_article_status() {
216 let result = ArticleStatus::try_from(Status {
217 status_code: StatusCode::from(220),
218 message: "2 <message.002> article exists".to_string(),
219 });
220 assert_eq!(
221 result,
222 Ok(ArticleStatus {
223 status_code: StatusCode::from(220),
224 article_number: ArticleNumber::from(2),
225 message_id: MessageId::from_str("message.002").unwrap(),
226 message: "article exists".to_string()
227 })
228 );
229 }
230
231 #[test]
232 fn parse_article_status_with_angle_brackets_message() {
233 let result = ArticleStatus::try_from(Status {
234 status_code: StatusCode::from(220),
235 message: "2 <message.002> <article exists>".to_string(),
236 });
237 assert_eq!(
238 result,
239 Ok(ArticleStatus {
240 status_code: StatusCode::from(220),
241 article_number: ArticleNumber::from(2),
242 message_id: MessageId::from_str("message.002").unwrap(),
243 message: "<article exists>".to_string()
244 })
245 );
246 }
247
248 #[test]
249 fn parse_article_status_without_angle_brackets() {
250 let result = ArticleStatus::try_from(Status {
251 status_code: StatusCode::from(220),
252 message: "2 message.002 article exists".to_string(),
253 });
254 assert_eq!(result, Err(ArticleStatusParseError));
255 }
256
257 #[test]
258 fn parse_article_status_without_message() {
259 let result = ArticleStatus::try_from(Status {
260 status_code: StatusCode::from(220),
261 message: "2 <message.002>".to_string(),
262 });
263 assert_eq!(result, Err(ArticleStatusParseError));
264 }
265
266 #[test]
267 fn parse_article_status_without_article_number() {
268 let result = ArticleStatus::try_from(Status {
269 status_code: StatusCode::from(220),
270 message: "<message.002> article exists".to_string(),
271 });
272 assert_eq!(result, Err(ArticleStatusParseError));
273 }
274
275 #[test]
276 fn parse_article_status_without_non_integer_article_number() {
277 let result = ArticleStatus::try_from(Status {
278 status_code: StatusCode::from(220),
279 message: "2.3 <message.002> article exists".to_string(),
280 });
281 assert_eq!(result, Err(ArticleStatusParseError));
282 }
283}