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 a continuation code (3xx)
74    ///
75    /// Per [RFC 3977 §3.2.1](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2.1):
76    /// - 3xx: Success so far, send more input
77    #[inline]
78    #[must_use]
79    pub fn is_continuation(&self) -> bool {
80        let code = self.into_inner();
81        (300..400).contains(&code)
82    }
83
84    /// Check if this is an informational code (1xx)
85    #[inline]
86    #[must_use]
87    pub fn is_informational(&self) -> bool {
88        let code = self.into_inner();
89        (100..200).contains(&code)
90    }
91
92    /// Backend greeting accepted by RFC 3977 connection setup.
93    #[inline]
94    #[must_use]
95    pub fn is_greeting(&self) -> bool {
96        matches!(self.into_inner(), 200 | 201)
97    }
98
99    /// AUTHINFO response indicating a password or authentication is required.
100    #[inline]
101    #[must_use]
102    pub fn requires_auth_credentials(&self) -> bool {
103        matches!(self.into_inner(), 381 | 480)
104    }
105
106    /// AUTHINFO response indicating authentication succeeded.
107    #[inline]
108    #[must_use]
109    pub fn is_auth_accepted(&self) -> bool {
110        self.into_inner() == 281
111    }
112
113    /// Article-not-found response.
114    #[inline]
115    #[must_use]
116    pub fn is_article_missing(&self) -> bool {
117        self.into_inner() == 430
118    }
119}
120
121impl StatusCode {
122    /// Parse a status code from response data
123    ///
124    /// Per [RFC 3977 §3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-3.2),
125    /// responses begin with a 3-digit status code (ASCII digits '0'-'9').
126    ///
127    /// **Optimization**: Direct byte-to-digit conversion without UTF-8 validation.
128    /// Status codes are guaranteed to be ASCII digits per the RFC.
129    #[inline]
130    #[must_use]
131    pub fn parse(data: &[u8]) -> Option<Self> {
132        if data.len() < 3 {
133            return None;
134        }
135
136        // Fast path: Direct ASCII digit conversion without UTF-8 overhead
137        // Per RFC 3977, status codes are exactly 3 ASCII digits
138        let d0 = data[0].wrapping_sub(b'0');
139        let d1 = data[1].wrapping_sub(b'0');
140        let d2 = data[2].wrapping_sub(b'0');
141
142        // Validate all three are digits (0-9)
143        if d0 > 9 || d1 > 9 || d2 > 9 {
144            return None;
145        }
146
147        // Combine into u16: d0*100 + d1*10 + d2
148        let code = u16::from(d0) * 100 + u16::from(d1) * 10 + u16::from(d2);
149        Some(Self::new(code))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_status_code_categories() {
159        assert!(StatusCode::new(100).is_informational());
160        assert!(StatusCode::new(200).is_success());
161        assert!(StatusCode::new(381).is_success()); // 3xx counts as success
162        assert!(StatusCode::new(400).is_error());
163        assert!(StatusCode::new(500).is_error());
164        assert!(!StatusCode::new(200).is_error());
165    }
166
167    #[test]
168    fn test_status_code_parsing() {
169        assert_eq!(StatusCode::parse(b"200"), Some(StatusCode::new(200)));
170        assert_eq!(
171            StatusCode::parse(b"200 Ready\r\n"),
172            Some(StatusCode::new(200))
173        );
174        assert_eq!(
175            StatusCode::parse(b"381 Password required\r\n"),
176            Some(StatusCode::new(381))
177        );
178        assert_eq!(
179            StatusCode::parse(b"500 Error\r\n"),
180            Some(StatusCode::new(500))
181        );
182        assert_eq!(StatusCode::parse(b""), None);
183        assert_eq!(StatusCode::parse(b"XX"), None);
184        assert_eq!(StatusCode::parse(b"ABC Invalid\r\n"), None);
185        assert_eq!(StatusCode::parse(b"20"), None);
186        assert_eq!(StatusCode::parse(b"2X0 Error\r\n"), None);
187    }
188
189    #[test]
190    fn test_status_code_setup_helpers() {
191        assert!(StatusCode::new(200).is_greeting());
192        assert!(StatusCode::new(201).is_greeting());
193        assert!(!StatusCode::new(205).is_greeting());
194
195        assert!(StatusCode::new(381).requires_auth_credentials());
196        assert!(StatusCode::new(480).requires_auth_credentials());
197        assert!(!StatusCode::new(281).requires_auth_credentials());
198
199        assert!(StatusCode::new(281).is_auth_accepted());
200        assert!(!StatusCode::new(381).is_auth_accepted());
201
202        assert!(StatusCode::new(430).is_article_missing());
203        assert!(!StatusCode::new(400).is_article_missing());
204    }
205
206    #[test]
207    fn test_edge_cases() {
208        // UTF-8 in responses
209        let utf8_response = "200 Привет мир\r\n".as_bytes();
210        assert_eq!(StatusCode::parse(utf8_response), Some(StatusCode::new(200)));
211
212        // Response with binary data
213        let with_null = b"200 Test\x00Message\r\n";
214        assert_eq!(StatusCode::parse(with_null), Some(StatusCode::new(200)));
215
216        // Boundary status codes
217        assert_eq!(
218            StatusCode::parse(b"100 Info\r\n"),
219            Some(StatusCode::new(100))
220        );
221        assert_eq!(
222            StatusCode::parse(b"599 Error\r\n"),
223            Some(StatusCode::new(599))
224        );
225    }
226}