nntp_proxy/protocol/
response.rs1use nutype::nutype;
35
36#[nutype(derive(
41 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref
42))]
43pub struct StatusCode(u16);
44
45impl StatusCode {
46 #[inline]
48 #[must_use]
49 pub fn as_u16(&self) -> u16 {
50 self.into_inner()
51 }
52
53 #[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 #[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 #[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 #[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 #[inline]
94 #[must_use]
95 pub fn is_greeting(&self) -> bool {
96 matches!(self.into_inner(), 200 | 201)
97 }
98
99 #[inline]
101 #[must_use]
102 pub fn requires_auth_credentials(&self) -> bool {
103 matches!(self.into_inner(), 381 | 480)
104 }
105
106 #[inline]
108 #[must_use]
109 pub fn is_auth_accepted(&self) -> bool {
110 self.into_inner() == 281
111 }
112
113 #[inline]
115 #[must_use]
116 pub fn is_article_missing(&self) -> bool {
117 self.into_inner() == 430
118 }
119}
120
121impl StatusCode {
122 #[inline]
130 #[must_use]
131 pub fn parse(data: &[u8]) -> Option<Self> {
132 if data.len() < 3 {
133 return None;
134 }
135
136 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 if d0 > 9 || d1 > 9 || d2 > 9 {
144 return None;
145 }
146
147 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()); 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 let utf8_response = "200 Привет мир\r\n".as_bytes();
210 assert_eq!(StatusCode::parse(utf8_response), Some(StatusCode::new(200)));
211
212 let with_null = b"200 Test\x00Message\r\n";
214 assert_eq!(StatusCode::parse(with_null), Some(StatusCode::new(200)));
215
216 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}