nntp_proxy/protocol/
response.rs1#[derive(Debug, Clone, PartialEq)]
5pub struct NntpResponse {
6 pub status_code: u16,
8 pub is_multiline: bool,
10 pub data: Vec<u8>,
12}
13
14impl NntpResponse {
15 #[inline]
17 pub fn parse_status_code(data: &[u8]) -> Option<u16> {
18 if data.len() < 3 {
19 return None;
20 }
21
22 let code_str = std::str::from_utf8(&data[0..3]).ok()?;
23 code_str.parse().ok()
24 }
25
26 #[inline]
28 #[allow(dead_code)]
29 pub fn is_multiline_response(status_code: u16) -> bool {
30 match status_code {
35 100..=199 => true, 215 | 220 | 221 | 222 | 224 | 225 | 230 | 231 | 282 => true, _ => false,
38 }
39 }
40
41 #[inline]
43 #[allow(dead_code)]
44 pub fn has_multiline_terminator(data: &[u8]) -> bool {
45 if data.len() < 5 {
47 return false;
48 }
49
50 data.ends_with(b"\r\n.\r\n") || data.ends_with(b"\n.\r\n")
52 }
53}
54
55pub struct ResponseParser;
57
58impl ResponseParser {
59 #[allow(dead_code)]
61 pub fn is_success_response(data: &[u8]) -> bool {
62 NntpResponse::parse_status_code(data).is_some_and(|code| (200..400).contains(&code))
63 }
64
65 #[allow(dead_code)]
67 pub fn is_greeting(data: &[u8]) -> bool {
68 data.len() >= 3 && (data.starts_with(b"200") || data.starts_with(b"201"))
70 }
71
72 #[allow(dead_code)]
74 pub fn is_auth_required(data: &[u8]) -> bool {
75 NntpResponse::parse_status_code(data).is_some_and(|code| code == 381 || code == 480)
76 }
77
78 #[allow(dead_code)]
80 pub fn is_auth_success(data: &[u8]) -> bool {
81 NntpResponse::parse_status_code(data).is_some_and(|code| code == 281)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_parse_status_code() {
91 assert_eq!(NntpResponse::parse_status_code(b"200 Ready\r\n"), Some(200));
92 assert_eq!(
93 NntpResponse::parse_status_code(b"381 Password required\r\n"),
94 Some(381)
95 );
96 assert_eq!(NntpResponse::parse_status_code(b"500 Error\r\n"), Some(500));
97 assert_eq!(NntpResponse::parse_status_code(b"XX"), None);
98 assert_eq!(NntpResponse::parse_status_code(b""), None);
99 }
100
101 #[test]
102 fn test_is_multiline_response() {
103 assert!(NntpResponse::is_multiline_response(100));
104 assert!(NntpResponse::is_multiline_response(215)); assert!(NntpResponse::is_multiline_response(220)); assert!(NntpResponse::is_multiline_response(221)); assert!(NntpResponse::is_multiline_response(222)); assert!(!NntpResponse::is_multiline_response(200)); assert!(!NntpResponse::is_multiline_response(381)); assert!(!NntpResponse::is_multiline_response(500)); }
112
113 #[test]
114 fn test_has_multiline_terminator() {
115 assert!(NntpResponse::has_multiline_terminator(
116 b"data\r\nmore data\r\n.\r\n"
117 ));
118 assert!(NntpResponse::has_multiline_terminator(
119 b"single line\n.\r\n"
120 ));
121 assert!(!NntpResponse::has_multiline_terminator(b"incomplete\r\n"));
122 assert!(!NntpResponse::has_multiline_terminator(b".\r\n")); }
124
125 #[test]
126 fn test_is_success_response() {
127 assert!(ResponseParser::is_success_response(b"200 Ready\r\n"));
128 assert!(ResponseParser::is_success_response(b"281 Auth OK\r\n"));
129 assert!(!ResponseParser::is_success_response(b"400 Error\r\n"));
130 assert!(!ResponseParser::is_success_response(b"500 Error\r\n"));
131 }
132
133 #[test]
134 fn test_is_greeting() {
135 assert!(ResponseParser::is_greeting(
136 b"200 news.example.com ready\r\n"
137 ));
138 assert!(ResponseParser::is_greeting(b"201 read-only\r\n"));
139 assert!(!ResponseParser::is_greeting(b"381 Password required\r\n"));
140 }
141
142 #[test]
143 fn test_auth_responses() {
144 assert!(ResponseParser::is_auth_required(
145 b"381 Password required\r\n"
146 ));
147 assert!(ResponseParser::is_auth_required(b"480 Auth required\r\n"));
148 assert!(!ResponseParser::is_auth_required(b"200 Ready\r\n"));
149
150 assert!(ResponseParser::is_auth_success(b"281 Auth accepted\r\n"));
151 assert!(!ResponseParser::is_auth_success(
152 b"381 Password required\r\n"
153 ));
154 }
155
156 #[test]
157 fn test_malformed_status_codes() {
158 assert_eq!(NntpResponse::parse_status_code(b"ABC Invalid\r\n"), None);
160
161 assert_eq!(NntpResponse::parse_status_code(b"Missing code\r\n"), None);
163
164 assert_eq!(NntpResponse::parse_status_code(b"20"), None);
166 assert_eq!(NntpResponse::parse_status_code(b"2"), None);
167
168 assert_eq!(NntpResponse::parse_status_code(b"2X0 Error\r\n"), None);
170
171 assert_eq!(NntpResponse::parse_status_code(b"-200 Invalid\r\n"), None);
173 }
174
175 #[test]
176 fn test_incomplete_responses() {
177 assert_eq!(NntpResponse::parse_status_code(b""), None);
179
180 assert_eq!(NntpResponse::parse_status_code(b"\r\n"), None);
182
183 assert_eq!(NntpResponse::parse_status_code(b"200"), Some(200));
185
186 assert_eq!(NntpResponse::parse_status_code(b"200 "), Some(200));
188 }
189
190 #[test]
191 fn test_boundary_status_codes() {
192 assert_eq!(NntpResponse::parse_status_code(b"100 Info\r\n"), Some(100));
194
195 assert_eq!(NntpResponse::parse_status_code(b"599 Error\r\n"), Some(599));
197
198 assert_eq!(NntpResponse::parse_status_code(b"000 Zero\r\n"), Some(0));
200 assert_eq!(NntpResponse::parse_status_code(b"999 Max\r\n"), Some(999));
201
202 assert_eq!(
204 NntpResponse::parse_status_code(b"1234 Invalid\r\n"),
205 Some(123)
206 );
207 }
208
209 #[test]
210 fn test_greeting_variations() {
211 assert!(ResponseParser::is_greeting(
213 b"200 news.example.com ready\r\n"
214 ));
215
216 assert!(ResponseParser::is_greeting(b"201 read-only access\r\n"));
218
219 assert!(ResponseParser::is_greeting(b"200\r\n"));
221
222 assert!(ResponseParser::is_greeting(b"200 Ready"));
224
225 assert!(!ResponseParser::is_greeting(b"400 Service unavailable\r\n"));
227 assert!(!ResponseParser::is_greeting(b"502 Service unavailable\r\n"));
228
229 assert!(!ResponseParser::is_greeting(b"281 Auth accepted\r\n"));
231 assert!(!ResponseParser::is_greeting(b"381 Password required\r\n"));
232 }
233
234 #[test]
235 fn test_auth_required_edge_cases() {
236 assert!(ResponseParser::is_auth_required(
238 b"381 Password required\r\n"
239 ));
240 assert!(ResponseParser::is_auth_required(
241 b"480 Authentication required\r\n"
242 ));
243
244 assert!(ResponseParser::is_auth_required(b"381\r\n"));
246 assert!(ResponseParser::is_auth_required(b"480\r\n"));
247
248 assert!(ResponseParser::is_auth_required(b"381 Password required"));
250
251 assert!(!ResponseParser::is_auth_required(b"200 Ready\r\n"));
253 assert!(!ResponseParser::is_auth_required(b"281 Auth accepted\r\n"));
254 assert!(!ResponseParser::is_auth_required(b"482 Auth rejected\r\n"));
255 }
256
257 #[test]
258 fn test_auth_success_edge_cases() {
259 assert!(ResponseParser::is_auth_success(
261 b"281 Authentication accepted\r\n"
262 ));
263
264 assert!(ResponseParser::is_auth_success(b"281\r\n"));
266
267 assert!(ResponseParser::is_auth_success(b"281 OK"));
269
270 assert!(!ResponseParser::is_auth_success(b"200 Ready\r\n"));
272 assert!(!ResponseParser::is_auth_success(b"211 Group selected\r\n"));
273
274 assert!(!ResponseParser::is_auth_success(
276 b"381 Password required\r\n"
277 ));
278 assert!(!ResponseParser::is_auth_success(
279 b"481 Authentication failed\r\n"
280 ));
281 assert!(!ResponseParser::is_auth_success(
282 b"482 Authentication rejected\r\n"
283 ));
284 }
285
286 #[test]
287 fn test_multiline_terminator_variations() {
288 assert!(NntpResponse::has_multiline_terminator(
290 b"line1\r\nline2\r\n.\r\n"
291 ));
292
293 assert!(NntpResponse::has_multiline_terminator(b"data\n.\r\n"));
295
296 assert!(!NntpResponse::has_multiline_terminator(
298 b"line1\r\nline2\r\n"
299 ));
300
301 assert!(!NntpResponse::has_multiline_terminator(b".\r\nmore data"));
303
304 assert!(NntpResponse::has_multiline_terminator(b"\r\n.\r\n"));
306 assert!(!NntpResponse::has_multiline_terminator(b"\n.\r\n"));
308
309 assert!(!NntpResponse::has_multiline_terminator(b".\r\n"));
311 assert!(!NntpResponse::has_multiline_terminator(b"abc"));
312 assert!(!NntpResponse::has_multiline_terminator(b""));
313 }
314
315 #[test]
316 fn test_success_response_edge_cases() {
317 assert!(ResponseParser::is_success_response(b"200 OK\r\n"));
319 assert!(ResponseParser::is_success_response(
320 b"211 Group selected\r\n"
321 ));
322 assert!(ResponseParser::is_success_response(
323 b"220 Article follows\r\n"
324 ));
325 assert!(ResponseParser::is_success_response(b"281 Auth OK\r\n"));
326
327 assert!(!ResponseParser::is_success_response(b"100 Info\r\n"));
329
330 assert!(ResponseParser::is_success_response(
332 b"381 Password required\r\n"
333 ));
334
335 assert!(!ResponseParser::is_success_response(b"400 Bad request\r\n"));
337 assert!(!ResponseParser::is_success_response(
338 b"430 No such article\r\n"
339 ));
340
341 assert!(!ResponseParser::is_success_response(
343 b"500 Internal error\r\n"
344 ));
345 assert!(!ResponseParser::is_success_response(
346 b"502 Service unavailable\r\n"
347 ));
348 }
349
350 #[test]
351 fn test_multiline_response_categories() {
352 assert!(NntpResponse::is_multiline_response(100));
354 assert!(NntpResponse::is_multiline_response(150));
355 assert!(NntpResponse::is_multiline_response(199));
356
357 assert!(NntpResponse::is_multiline_response(215)); assert!(NntpResponse::is_multiline_response(220)); assert!(NntpResponse::is_multiline_response(221)); assert!(NntpResponse::is_multiline_response(222)); assert!(NntpResponse::is_multiline_response(224)); assert!(NntpResponse::is_multiline_response(225)); assert!(NntpResponse::is_multiline_response(230)); assert!(NntpResponse::is_multiline_response(231)); assert!(!NntpResponse::is_multiline_response(200)); assert!(!NntpResponse::is_multiline_response(205)); assert!(!NntpResponse::is_multiline_response(211)); assert!(!NntpResponse::is_multiline_response(281)); assert!(!NntpResponse::is_multiline_response(381));
375 assert!(!NntpResponse::is_multiline_response(430));
376 assert!(!NntpResponse::is_multiline_response(500));
377 }
378
379 #[test]
380 fn test_utf8_in_responses() {
381 let utf8_response = "200 Привет мир\r\n".as_bytes();
383 assert_eq!(NntpResponse::parse_status_code(utf8_response), Some(200));
384 assert!(ResponseParser::is_greeting(utf8_response));
385
386 let utf8_auth = "281 认证成功\r\n".as_bytes();
388 assert_eq!(NntpResponse::parse_status_code(utf8_auth), Some(281));
389 assert!(ResponseParser::is_auth_success(utf8_auth));
390 }
391
392 #[test]
393 fn test_response_with_binary_data() {
394 let with_null = b"200 Test\x00Message\r\n";
396 assert_eq!(NntpResponse::parse_status_code(with_null), Some(200));
397
398 let with_high_bytes = b"200 \xFF\xFE\r\n";
400 assert_eq!(NntpResponse::parse_status_code(with_high_bytes), Some(200));
401 }
402}