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}