nntp_proxy/protocol/
response.rs

1//! NNTP response parsing and handling
2
3/// Represents a parsed NNTP response
4#[derive(Debug, Clone, PartialEq)]
5pub struct NntpResponse {
6    /// Status code (e.g., 200, 381, 500)
7    pub status_code: u16,
8    /// Whether this is a multiline response
9    pub is_multiline: bool,
10    /// Complete response data including status line
11    pub data: Vec<u8>,
12}
13
14impl NntpResponse {
15    /// Parse a status code from response data
16    #[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    /// Check if a response indicates a multiline response
27    #[inline]
28    #[allow(dead_code)]
29    pub fn is_multiline_response(status_code: u16) -> bool {
30        // Multiline responses in NNTP typically have codes like:
31        // 1xx informational (multiline)
32        // 2xx success with data (some multiline: 215, 220, 221, 222, 224, 225, 230, 231, 282)
33        // 4xx/5xx errors are single line
34        match status_code {
35            100..=199 => true, // Informational multiline
36            215 | 220 | 221 | 222 | 224 | 225 | 230 | 231 | 282 => true, // Article/list data
37            _ => false,
38        }
39    }
40
41    /// Check if data contains the end-of-multiline marker
42    #[inline]
43    #[allow(dead_code)]
44    pub fn has_multiline_terminator(data: &[u8]) -> bool {
45        // NNTP multiline responses end with "\r\n.\r\n"
46        if data.len() < 5 {
47            return false;
48        }
49
50        // Look for the terminator at the end
51        data.ends_with(b"\r\n.\r\n") || data.ends_with(b"\n.\r\n")
52    }
53}
54
55/// Response parser for NNTP protocol
56pub struct ResponseParser;
57
58impl ResponseParser {
59    /// Check if a response starts with a success code
60    #[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    /// Check if response is a greeting (200 or 201)
66    #[allow(dead_code)]
67    pub fn is_greeting(data: &[u8]) -> bool {
68        // Check directly on bytes to avoid allocation
69        data.len() >= 3 && (data.starts_with(b"200") || data.starts_with(b"201"))
70    }
71
72    /// Check if response indicates authentication is required
73    #[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    /// Check if response indicates successful authentication
79    #[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)); // LIST
105        assert!(NntpResponse::is_multiline_response(220)); // ARTICLE
106        assert!(NntpResponse::is_multiline_response(221)); // HEAD
107        assert!(NntpResponse::is_multiline_response(222)); // BODY
108        assert!(!NntpResponse::is_multiline_response(200)); // Greeting
109        assert!(!NntpResponse::is_multiline_response(381)); // Auth required
110        assert!(!NntpResponse::is_multiline_response(500)); // Error
111    }
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")); // Too short
123    }
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        // Non-numeric status
159        assert_eq!(NntpResponse::parse_status_code(b"ABC Invalid\r\n"), None);
160
161        // Missing status code
162        assert_eq!(NntpResponse::parse_status_code(b"Missing code\r\n"), None);
163
164        // Incomplete status code
165        assert_eq!(NntpResponse::parse_status_code(b"20"), None);
166        assert_eq!(NntpResponse::parse_status_code(b"2"), None);
167
168        // Status code with invalid characters
169        assert_eq!(NntpResponse::parse_status_code(b"2X0 Error\r\n"), None);
170
171        // Negative status (shouldn't parse)
172        assert_eq!(NntpResponse::parse_status_code(b"-200 Invalid\r\n"), None);
173    }
174
175    #[test]
176    fn test_incomplete_responses() {
177        // Empty response
178        assert_eq!(NntpResponse::parse_status_code(b""), None);
179
180        // Only newline
181        assert_eq!(NntpResponse::parse_status_code(b"\r\n"), None);
182
183        // Status without message
184        assert_eq!(NntpResponse::parse_status_code(b"200"), Some(200));
185
186        // Status with just space
187        assert_eq!(NntpResponse::parse_status_code(b"200 "), Some(200));
188    }
189
190    #[test]
191    fn test_boundary_status_codes() {
192        // Minimum valid code
193        assert_eq!(NntpResponse::parse_status_code(b"100 Info\r\n"), Some(100));
194
195        // Maximum valid code
196        assert_eq!(NntpResponse::parse_status_code(b"599 Error\r\n"), Some(599));
197
198        // Out of range codes (but still parse)
199        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        // Four digit code (only first 3 parsed)
203        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        // Standard greeting
212        assert!(ResponseParser::is_greeting(
213            b"200 news.example.com ready\r\n"
214        ));
215
216        // Read-only greeting
217        assert!(ResponseParser::is_greeting(b"201 read-only access\r\n"));
218
219        // Minimal greeting
220        assert!(ResponseParser::is_greeting(b"200\r\n"));
221
222        // Greeting without CRLF
223        assert!(ResponseParser::is_greeting(b"200 Ready"));
224
225        // Not a greeting (4xx/5xx codes)
226        assert!(!ResponseParser::is_greeting(b"400 Service unavailable\r\n"));
227        assert!(!ResponseParser::is_greeting(b"502 Service unavailable\r\n"));
228
229        // Auth-related but not greeting
230        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        // Standard auth required
237        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        // Without message
245        assert!(ResponseParser::is_auth_required(b"381\r\n"));
246        assert!(ResponseParser::is_auth_required(b"480\r\n"));
247
248        // Without CRLF
249        assert!(ResponseParser::is_auth_required(b"381 Password required"));
250
251        // Not auth required
252        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        // Standard success
260        assert!(ResponseParser::is_auth_success(
261            b"281 Authentication accepted\r\n"
262        ));
263
264        // Minimal success
265        assert!(ResponseParser::is_auth_success(b"281\r\n"));
266
267        // Without CRLF
268        assert!(ResponseParser::is_auth_success(b"281 OK"));
269
270        // Not success (other 2xx codes)
271        assert!(!ResponseParser::is_auth_success(b"200 Ready\r\n"));
272        assert!(!ResponseParser::is_auth_success(b"211 Group selected\r\n"));
273
274        // Failures
275        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        // Standard terminator
289        assert!(NntpResponse::has_multiline_terminator(
290            b"line1\r\nline2\r\n.\r\n"
291        ));
292
293        // Terminator with just LF
294        assert!(NntpResponse::has_multiline_terminator(b"data\n.\r\n"));
295
296        // No terminator
297        assert!(!NntpResponse::has_multiline_terminator(
298            b"line1\r\nline2\r\n"
299        ));
300
301        // Terminator in middle (should not match - ends_with check)
302        assert!(!NntpResponse::has_multiline_terminator(b".\r\nmore data"));
303
304        // Just the terminator (exactly 5 bytes)
305        assert!(NntpResponse::has_multiline_terminator(b"\r\n.\r\n"));
306        // This is only 4 bytes, so should be false
307        assert!(!NntpResponse::has_multiline_terminator(b"\n.\r\n"));
308
309        // Too short
310        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        // 2xx codes (success)
318        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        // 1xx codes (informational - not success)
328        assert!(!ResponseParser::is_success_response(b"100 Info\r\n"));
329
330        // 3xx codes (further action - still considered success in NNTP context)
331        assert!(ResponseParser::is_success_response(
332            b"381 Password required\r\n"
333        ));
334
335        // 4xx codes (client error)
336        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        // 5xx codes (server error)
342        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        // 1xx informational (all multiline)
353        assert!(NntpResponse::is_multiline_response(100));
354        assert!(NntpResponse::is_multiline_response(150));
355        assert!(NntpResponse::is_multiline_response(199));
356
357        // 2xx specific multiline codes
358        assert!(NntpResponse::is_multiline_response(215)); // LIST
359        assert!(NntpResponse::is_multiline_response(220)); // ARTICLE
360        assert!(NntpResponse::is_multiline_response(221)); // HEAD
361        assert!(NntpResponse::is_multiline_response(222)); // BODY
362        assert!(NntpResponse::is_multiline_response(224)); // OVER
363        assert!(NntpResponse::is_multiline_response(225)); // HDR
364        assert!(NntpResponse::is_multiline_response(230)); // NEWNEWS
365        assert!(NntpResponse::is_multiline_response(231)); // NEWGROUPS
366
367        // 2xx non-multiline codes
368        assert!(!NntpResponse::is_multiline_response(200)); // Greeting
369        assert!(!NntpResponse::is_multiline_response(205)); // Goodbye
370        assert!(!NntpResponse::is_multiline_response(211)); // Group selected
371        assert!(!NntpResponse::is_multiline_response(281)); // Auth accepted
372
373        // 3xx, 4xx, 5xx are not multiline
374        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        // Response with UTF-8 characters in message
382        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        // Auth response with UTF-8
387        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        // Response with null bytes in message part (after status code)
395        let with_null = b"200 Test\x00Message\r\n";
396        assert_eq!(NntpResponse::parse_status_code(with_null), Some(200));
397
398        // Response with high bytes
399        let with_high_bytes = b"200 \xFF\xFE\r\n";
400        assert_eq!(NntpResponse::parse_status_code(with_high_bytes), Some(200));
401    }
402}