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}