Skip to main content

nntp_proxy/protocol/
response.rs

1//! NNTP Response Parsing and Handling
2//!
3//! This module implements efficient parsing of NNTP server responses according to
4//! [RFC 3977](https://datatracker.ietf.org/doc/html/rfc3977) with optimizations
5//! for high-throughput proxy use.
6//!
7//! # NNTP Protocol References
8//!
9//! - **[RFC 3977 §3.2]** - Response format and status codes
10//! - **[RFC 3977 §3.4.1]** - Multiline data blocks
11//! - **[RFC 5536 §3.1.3]** - Message-ID format specification
12//!
13//! [RFC 3977 §3.2]: https://datatracker.ietf.org/doc/html/rfc3977#section-3.2
14//! [RFC 3977 §3.4.1]: https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1
15//! [RFC 5536 §3.1.3]: https://datatracker.ietf.org/doc/html/rfc5536#section-3.1.3
16//!
17//! # Response Format
18//!
19//! Per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2):
20//! ```text
21//! response     = status-line [CRLF multiline-data]
22//! status-line  = status-code SP status-text CRLF
23//! status-code  = 3DIGIT
24//! ```
25//!
26//! # Multiline Responses
27//!
28//! Per [RFC 3977 §3.4.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1):
29//! ```text
30//! Multiline responses end with a line containing a single period:
31//! CRLF "." CRLF
32//! ```
33
34use nutype::nutype;
35
36/// Raw NNTP status code (3-digit number)
37///
38/// Per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2),
39/// all NNTP responses start with a 3-digit status code (100-599).
40#[nutype(derive(
41    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref
42))]
43pub struct StatusCode(u16);
44
45impl StatusCode {
46    /// Get the raw numeric value
47    #[inline]
48    #[must_use]
49    pub fn as_u16(&self) -> u16 {
50        self.into_inner()
51    }
52
53    /// Check if this is a success code (2xx or 3xx)
54    ///
55    /// Per [RFC 3977 §3.2.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2.1):
56    /// - 2xx: Success
57    /// - 3xx: Success so far, send more input
58    #[inline]
59    #[must_use]
60    pub fn is_success(&self) -> bool {
61        let code = self.into_inner();
62        (200..400).contains(&code)
63    }
64
65    /// Check if this is an error code (4xx or 5xx)
66    #[inline]
67    #[must_use]
68    pub fn is_error(&self) -> bool {
69        let code = self.into_inner();
70        (400..600).contains(&code)
71    }
72
73    /// Check if this is an informational code (1xx)
74    #[inline]
75    #[must_use]
76    pub fn is_informational(&self) -> bool {
77        let code = self.into_inner();
78        (100..200).contains(&code)
79    }
80}
81
82/// Categorized NNTP response code for type-safe handling
83///
84/// This enum categorizes NNTP response codes based on their semantics and
85/// handling requirements per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2).
86///
87/// # Response Code Ranges
88///
89/// Per [RFC 3977 §3.2.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2.1):
90/// - **1xx**: Informational (multiline data follows)
91/// - **2xx**: Success (may be multiline)
92/// - **3xx**: Success so far, further input expected
93/// - **4xx**: Temporary failure
94/// - **5xx**: Permanent failure
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum NntpResponse {
97    /// Server greeting - [RFC 3977 §5.1](https://datatracker.ietf.org/doc/html/rfc3977#section-5.1)
98    /// - 200: Posting allowed
99    /// - 201: No posting allowed
100    Greeting(StatusCode),
101
102    /// Disconnect/goodbye - [RFC 3977 §5.4](https://datatracker.ietf.org/doc/html/rfc3977#section-5.4)
103    /// - 205: Connection closing
104    Disconnect,
105
106    /// Authentication required - [RFC 4643 §2.3](https://datatracker.ietf.org/doc/html/rfc4643#section-2.3)
107    /// - 381: Password required
108    /// - 480: Authentication required
109    AuthRequired(StatusCode),
110
111    /// Authentication successful - [RFC 4643 §2.5.1](https://datatracker.ietf.org/doc/html/rfc4643#section-2.5.1)
112    /// - 281: Authentication accepted
113    AuthSuccess,
114
115    /// Multiline data response
116    /// Per [RFC 3977 §3.4.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1):
117    /// - All 1xx codes (100-199)
118    /// - Specific 2xx codes: 215, 220, 221, 222, 224, 225, 230, 231, 282
119    MultilineData(StatusCode),
120
121    /// Single-line response (everything else)
122    SingleLine(StatusCode),
123
124    /// Invalid or unparseable response
125    Invalid,
126}
127
128impl NntpResponse {
129    /// Parse response data into a categorized response code
130    ///
131    /// Per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2),
132    /// responses start with a 3-digit status code.
133    ///
134    /// **Optimization**: Direct byte-to-digit conversion avoids UTF-8 overhead.
135    #[inline]
136    pub fn parse(data: &[u8]) -> Self {
137        let code = match StatusCode::parse(data) {
138            Some(c) => c,
139            None => return Self::Invalid,
140        };
141
142        match code.as_u16() {
143            // [RFC 3977 §5.1](https://datatracker.ietf.org/doc/html/rfc3977#section-5.1)
144            200 | 201 => Self::Greeting(code),
145
146            // [RFC 3977 §5.4](https://datatracker.ietf.org/doc/html/rfc3977#section-5.4)
147            205 => Self::Disconnect,
148
149            // [RFC 4643 §2.5.1](https://datatracker.ietf.org/doc/html/rfc4643#section-2.5.1)
150            281 => Self::AuthSuccess,
151
152            // [RFC 4643 §2.3](https://datatracker.ietf.org/doc/html/rfc4643#section-2.3)
153            381 | 480 => Self::AuthRequired(code),
154
155            // Multiline responses per [RFC 3977 §3.4.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1)
156            // All 1xx are informational multiline
157            100..=199 => Self::MultilineData(code),
158            // Specific 2xx multiline responses
159            215 | 220 | 221 | 222 | 224 | 225 | 230 | 231 | 282 => Self::MultilineData(code),
160
161            // Everything else is a single-line response
162            _ => Self::SingleLine(code),
163        }
164    }
165
166    /// Check if this response type is multiline
167    ///
168    /// Per [RFC 3977 §3.4.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1),
169    /// multiline responses require special handling with terminator detection.
170    #[inline]
171    #[must_use]
172    pub const fn is_multiline(&self) -> bool {
173        matches!(self, Self::MultilineData(_))
174    }
175
176    /// Get the numeric status code if available
177    #[inline]
178    #[must_use]
179    pub fn status_code(&self) -> Option<StatusCode> {
180        match self {
181            Self::Greeting(c)
182            | Self::AuthRequired(c)
183            | Self::MultilineData(c)
184            | Self::SingleLine(c) => Some(*c),
185            Self::Disconnect => Some(StatusCode::new(205)),
186            Self::AuthSuccess => Some(StatusCode::new(281)),
187            Self::Invalid => None,
188        }
189    }
190
191    /// Check if this is a success response (2xx or 3xx)
192    ///
193    /// Per [RFC 3977 §3.2.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2.1):
194    /// - 2xx: Success
195    /// - 3xx: Success so far, send more input
196    #[inline]
197    #[must_use]
198    pub fn is_success(&self) -> bool {
199        self.status_code().is_some_and(|code| code.is_success())
200    }
201}
202
203impl StatusCode {
204    /// Parse a status code from response data
205    ///
206    /// Per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2),
207    /// responses begin with a 3-digit status code (ASCII digits '0'-'9').
208    ///
209    /// **Optimization**: Direct byte-to-digit conversion without UTF-8 validation.
210    /// Status codes are guaranteed to be ASCII digits per the RFC.
211    #[inline]
212    pub fn parse(data: &[u8]) -> Option<Self> {
213        if data.len() < 3 {
214            return None;
215        }
216
217        // Fast path: Direct ASCII digit conversion without UTF-8 overhead
218        // Per RFC 3977, status codes are exactly 3 ASCII digits
219        let d0 = data[0].wrapping_sub(b'0');
220        let d1 = data[1].wrapping_sub(b'0');
221        let d2 = data[2].wrapping_sub(b'0');
222
223        // Validate all three are digits (0-9)
224        if d0 > 9 || d1 > 9 || d2 > 9 {
225            return None;
226        }
227
228        // Combine into u16: d0*100 + d1*10 + d2
229        let code = (d0 as u16) * 100 + (d1 as u16) * 10 + (d2 as u16);
230        Some(Self::new(code))
231    }
232
233    /// Check if this code indicates a multiline response
234    ///
235    /// Per [RFC 3977 §3.4.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.4.1),
236    /// certain status codes indicate multiline data follows.
237    ///
238    /// # Multiline Response Codes
239    /// - **1xx**: All informational responses (100-199)
240    /// - **2xx**: Specific codes - 215, 220, 221, 222, 224, 225, 230, 231, 282
241    #[inline]
242    #[must_use]
243    pub fn is_multiline(&self) -> bool {
244        match **self {
245            100..=199 => true, // All 1xx are multiline
246            215 | 220 | 221 | 222 | 224 | 225 | 230 | 231 | 282 => true, // Specific 2xx codes
247            _ => false,
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_status_code_categories() {
258        assert!(StatusCode::new(100).is_informational());
259        assert!(StatusCode::new(200).is_success());
260        assert!(StatusCode::new(381).is_success()); // 3xx counts as success
261        assert!(StatusCode::new(400).is_error());
262        assert!(StatusCode::new(500).is_error());
263        assert!(!StatusCode::new(200).is_error());
264    }
265
266    #[test]
267    fn test_status_code_multiline() {
268        // All 1xx are multiline
269        assert!(StatusCode::new(111).is_multiline());
270        // Specific 2xx multiline codes
271        assert!(StatusCode::new(220).is_multiline());
272        assert!(!StatusCode::new(200).is_multiline());
273        assert!(!StatusCode::new(400).is_multiline());
274    }
275
276    #[test]
277    fn test_status_code_parsing() {
278        assert_eq!(StatusCode::parse(b"200"), Some(StatusCode::new(200)));
279        assert_eq!(
280            StatusCode::parse(b"200 Ready\r\n"),
281            Some(StatusCode::new(200))
282        );
283        assert_eq!(
284            StatusCode::parse(b"381 Password required\r\n"),
285            Some(StatusCode::new(381))
286        );
287        assert_eq!(
288            StatusCode::parse(b"500 Error\r\n"),
289            Some(StatusCode::new(500))
290        );
291        assert_eq!(StatusCode::parse(b""), None);
292        assert_eq!(StatusCode::parse(b"XX"), None);
293        assert_eq!(StatusCode::parse(b"ABC Invalid\r\n"), None);
294        assert_eq!(StatusCode::parse(b"20"), None);
295        assert_eq!(StatusCode::parse(b"2X0 Error\r\n"), None);
296    }
297
298    #[test]
299    fn test_nntp_response_categorization() {
300        // Greetings
301        assert!(matches!(
302            NntpResponse::parse(b"200 OK\r\n"),
303            NntpResponse::Greeting(_)
304        ));
305        assert!(matches!(
306            NntpResponse::parse(b"201 No posting\r\n"),
307            NntpResponse::Greeting(_)
308        ));
309
310        // Disconnect
311        assert_eq!(
312            NntpResponse::parse(b"205 Bye\r\n"),
313            NntpResponse::Disconnect
314        );
315
316        // Auth
317        assert!(matches!(
318            NntpResponse::parse(b"381 Pass\r\n"),
319            NntpResponse::AuthRequired(_)
320        ));
321        assert!(matches!(
322            NntpResponse::parse(b"480 Auth required\r\n"),
323            NntpResponse::AuthRequired(_)
324        ));
325        assert_eq!(
326            NntpResponse::parse(b"281 OK\r\n"),
327            NntpResponse::AuthSuccess
328        );
329
330        // Multiline
331        assert!(matches!(
332            NntpResponse::parse(b"220 Art\r\n"),
333            NntpResponse::MultilineData(_)
334        ));
335        assert!(matches!(
336            NntpResponse::parse(b"215 LIST\r\n"),
337            NntpResponse::MultilineData(_)
338        ));
339        assert!(matches!(
340            NntpResponse::parse(b"100 Help\r\n"),
341            NntpResponse::MultilineData(_)
342        ));
343
344        // Single-line
345        assert!(matches!(
346            NntpResponse::parse(b"211 Group\r\n"),
347            NntpResponse::SingleLine(_)
348        ));
349        assert!(matches!(
350            NntpResponse::parse(b"400 Error\r\n"),
351            NntpResponse::SingleLine(_)
352        ));
353
354        // Invalid
355        assert_eq!(NntpResponse::parse(b""), NntpResponse::Invalid);
356        assert_eq!(NntpResponse::parse(b"XXX\r\n"), NntpResponse::Invalid);
357    }
358
359    #[test]
360    fn test_response_is_success() {
361        assert!(NntpResponse::parse(b"200 OK\r\n").is_success());
362        assert!(NntpResponse::parse(b"281 Auth\r\n").is_success());
363        assert!(NntpResponse::parse(b"381 Password required\r\n").is_success()); // 3xx is success
364        assert!(!NntpResponse::parse(b"400 Error\r\n").is_success());
365        assert!(!NntpResponse::parse(b"500 Error\r\n").is_success());
366        assert!(!NntpResponse::Invalid.is_success());
367    }
368
369    #[test]
370    fn test_response_is_multiline() {
371        assert!(NntpResponse::parse(b"220 Article\r\n").is_multiline());
372        assert!(NntpResponse::parse(b"215 LIST\r\n").is_multiline());
373        assert!(!NntpResponse::parse(b"200 OK\r\n").is_multiline());
374        assert!(!NntpResponse::parse(b"211 Group\r\n").is_multiline());
375    }
376
377    #[test]
378    fn test_response_status_code() {
379        assert_eq!(
380            NntpResponse::parse(b"200 OK\r\n").status_code(),
381            Some(StatusCode::new(200))
382        );
383        assert_eq!(
384            NntpResponse::Disconnect.status_code(),
385            Some(StatusCode::new(205))
386        );
387        assert_eq!(
388            NntpResponse::AuthSuccess.status_code(),
389            Some(StatusCode::new(281))
390        );
391        assert_eq!(NntpResponse::Invalid.status_code(), None);
392    }
393
394    #[test]
395    fn test_edge_cases() {
396        // UTF-8 in responses
397        let utf8_response = "200 Привет мир\r\n".as_bytes();
398        assert_eq!(StatusCode::parse(utf8_response), Some(StatusCode::new(200)));
399
400        // Response with binary data
401        let with_null = b"200 Test\x00Message\r\n";
402        assert_eq!(StatusCode::parse(with_null), Some(StatusCode::new(200)));
403
404        // Boundary status codes
405        assert_eq!(
406            StatusCode::parse(b"100 Info\r\n"),
407            Some(StatusCode::new(100))
408        );
409        assert_eq!(
410            StatusCode::parse(b"599 Error\r\n"),
411            Some(StatusCode::new(599))
412        );
413    }
414}