Skip to main content

nntp_proxy/protocol/
commands.rs

1//! NNTP command construction helpers
2//!
3//! This module provides functions for constructing well-formed NNTP commands
4//! according to RFC 3977 and RFC 4643.
5
6use super::RequestContext;
7use crate::types::MessageId;
8
9/// QUIT command (RFC 3977 Section 5.4)
10pub const QUIT: &[u8] = b"QUIT\r\n";
11
12/// COMPRESS DEFLATE command (RFC 8054 ยง2.2)
13pub const COMPRESS_DEFLATE: &[u8] = b"COMPRESS DEFLATE\r\n";
14
15/// Construct AUTHINFO USER request (RFC 4643 Section 2.3)
16///
17/// Returns a typed request context with verb `AUTHINFO` and args `USER <username>`.
18#[inline]
19#[must_use]
20pub fn authinfo_user(username: &str) -> RequestContext {
21    RequestContext::from_verb_arg_slices(b"AUTHINFO", &[b"USER ", username.as_bytes()])
22}
23
24/// Construct AUTHINFO PASS request (RFC 4643 Section 2.4)
25///
26/// Returns a typed request context with verb `AUTHINFO` and args `PASS <password>`.
27#[inline]
28#[must_use]
29pub fn authinfo_pass(password: &str) -> RequestContext {
30    RequestContext::from_verb_arg_slices(b"AUTHINFO", &[b"PASS ", password.as_bytes()])
31}
32
33#[inline]
34#[must_use]
35pub fn article_request(msgid: &MessageId<'_>) -> RequestContext {
36    RequestContext::from_verb_args(b"ARTICLE", msgid.as_str().as_bytes())
37}
38
39#[inline]
40#[must_use]
41pub fn body_request(msgid: &MessageId<'_>) -> RequestContext {
42    RequestContext::from_verb_args(b"BODY", msgid.as_str().as_bytes())
43}
44
45#[inline]
46#[must_use]
47pub fn head_request(msgid: &MessageId<'_>) -> RequestContext {
48    RequestContext::from_verb_args(b"HEAD", msgid.as_str().as_bytes())
49}
50
51#[inline]
52#[must_use]
53pub fn stat_request(msgid: &MessageId<'_>) -> RequestContext {
54    RequestContext::from_verb_args(b"STAT", msgid.as_str().as_bytes())
55}
56
57#[inline]
58#[must_use]
59pub fn date_request() -> RequestContext {
60    RequestContext::from_verb_args(b"DATE", b"")
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use futures::executor::block_on;
67
68    fn msgid(value: &str) -> MessageId<'_> {
69        MessageId::from_borrowed(value).unwrap()
70    }
71
72    fn wire(context: &RequestContext) -> Vec<u8> {
73        let mut out = Vec::with_capacity(context.request_wire_len().get());
74        block_on(context.write_wire_to(&mut out)).unwrap();
75        out
76    }
77
78    #[test]
79    fn test_quit_command() {
80        assert_eq!(QUIT, b"QUIT\r\n");
81    }
82
83    #[test]
84    fn test_date_request() {
85        assert_eq!(wire(&date_request()), b"DATE\r\n");
86    }
87
88    #[test]
89    fn test_authinfo_user() {
90        assert_eq!(
91            wire(&authinfo_user("testuser")),
92            b"AUTHINFO USER testuser\r\n"
93        );
94        assert_eq!(wire(&authinfo_user("")), b"AUTHINFO USER \r\n");
95        assert_eq!(
96            authinfo_user("testuser").kind(),
97            crate::protocol::RequestKind::AuthInfo
98        );
99    }
100
101    #[test]
102    fn test_authinfo_user_special_chars() {
103        assert_eq!(
104            wire(&authinfo_user("user@example.com")),
105            b"AUTHINFO USER user@example.com\r\n"
106        );
107    }
108
109    #[test]
110    fn test_authinfo_user_spaces() {
111        assert_eq!(
112            wire(&authinfo_user("user with spaces")),
113            b"AUTHINFO USER user with spaces\r\n"
114        );
115    }
116
117    #[test]
118    fn test_authinfo_pass() {
119        assert_eq!(wire(&authinfo_pass("secret")), b"AUTHINFO PASS secret\r\n");
120        assert_eq!(wire(&authinfo_pass("")), b"AUTHINFO PASS \r\n");
121        assert_eq!(
122            authinfo_pass("secret").kind(),
123            crate::protocol::RequestKind::AuthInfo
124        );
125    }
126
127    #[test]
128    fn test_authinfo_pass_special_chars() {
129        assert_eq!(
130            wire(&authinfo_pass("p@ssw0rd!#$")),
131            b"AUTHINFO PASS p@ssw0rd!#$\r\n"
132        );
133    }
134
135    #[test]
136    fn test_authinfo_pass_spaces() {
137        assert_eq!(
138            wire(&authinfo_pass("pass word")),
139            b"AUTHINFO PASS pass word\r\n"
140        );
141    }
142
143    #[test]
144    fn test_authinfo_crlf_termination() {
145        assert!(wire(&authinfo_user("user")).ends_with(b"\r\n"));
146        assert!(wire(&authinfo_pass("pass")).ends_with(b"\r\n"));
147    }
148
149    #[test]
150    fn test_article_request() {
151        assert_eq!(
152            wire(&article_request(&msgid("<test@example.com>"))),
153            b"ARTICLE <test@example.com>\r\n"
154        );
155        assert_eq!(
156            wire(&article_request(&msgid("<msg123@news.server.com>"))),
157            b"ARTICLE <msg123@news.server.com>\r\n"
158        );
159    }
160
161    #[test]
162    fn test_body_request() {
163        assert_eq!(
164            wire(&body_request(&msgid("<test@example.com>"))),
165            b"BODY <test@example.com>\r\n"
166        );
167    }
168
169    #[test]
170    fn test_head_request() {
171        assert_eq!(
172            wire(&head_request(&msgid("<test@example.com>"))),
173            b"HEAD <test@example.com>\r\n"
174        );
175    }
176
177    #[test]
178    fn test_stat_request() {
179        assert_eq!(
180            wire(&stat_request(&msgid("<test@example.com>"))),
181            b"STAT <test@example.com>\r\n"
182        );
183    }
184
185    #[test]
186    fn test_commands_end_with_crlf() {
187        assert!(wire(&authinfo_user("test")).ends_with(b"\r\n"));
188        assert!(wire(&authinfo_pass("test")).ends_with(b"\r\n"));
189        assert!(wire(&article_request(&msgid("<test@example.com>"))).ends_with(b"\r\n"));
190        assert!(wire(&body_request(&msgid("<test@example.com>"))).ends_with(b"\r\n"));
191        assert!(wire(&head_request(&msgid("<test@example.com>"))).ends_with(b"\r\n"));
192        assert!(wire(&stat_request(&msgid("<test@example.com>"))).ends_with(b"\r\n"));
193    }
194}