1macro_rules! command_cases {
63 ($name:ident, $upper:literal, $lower:literal, $title:literal, $doc:expr) => {
64 #[doc = $doc]
65 const $name: &[&[u8]; 3] = &[$upper.as_bytes(), $lower.as_bytes(), $title.as_bytes()];
66
67 const _: () = {
70 assert!($doc.len() > 0, "Command documentation cannot be empty");
73 };
74 };
75}
76
77command_cases!(
81 ARTICLE_CASES,
82 "ARTICLE",
83 "article",
84 "Article",
85 "[RFC 3977 §6.2.1](https://datatracker.ietf.org/doc/html/rfc3977#section-6.2.1) - ARTICLE command\n\
86 Retrieve article by message-ID or number"
87);
88
89command_cases!(
90 BODY_CASES,
91 "BODY",
92 "body",
93 "Body",
94 "[RFC 3977 §6.2.3](https://datatracker.ietf.org/doc/html/rfc3977#section-6.2.3) - BODY command\n\
95 Retrieve article body by message-ID or number"
96);
97
98command_cases!(
99 HEAD_CASES,
100 "HEAD",
101 "head",
102 "Head",
103 "[RFC 3977 §6.2.2](https://datatracker.ietf.org/doc/html/rfc3977#section-6.2.2) - HEAD command\n\
104 Retrieve article headers by message-ID or number"
105);
106
107command_cases!(
108 STAT_CASES,
109 "STAT",
110 "stat",
111 "Stat",
112 "[RFC 3977 §6.2.4](https://datatracker.ietf.org/doc/html/rfc3977#section-6.2.4) - STAT command\n\
113 Check article existence by message-ID or number (no body transfer)"
114);
115
116command_cases!(
117 GROUP_CASES,
118 "GROUP",
119 "group",
120 "Group",
121 "[RFC 3977 §6.1.1](https://datatracker.ietf.org/doc/html/rfc3977#section-6.1.1) - GROUP command\n\
122 Select a newsgroup and set current article pointer"
123);
124
125command_cases!(
126 AUTHINFO_CASES,
127 "AUTHINFO",
128 "authinfo",
129 "Authinfo",
130 "[RFC 4643 §2.3](https://datatracker.ietf.org/doc/html/rfc4643#section-2.3) - AUTHINFO command\n\
131 Authentication mechanism (AUTHINFO USER/PASS, AUTHINFO SASL, etc.)"
132);
133
134command_cases!(
135 LIST_CASES,
136 "LIST",
137 "list",
138 "List",
139 "[RFC 3977 §7.6.1](https://datatracker.ietf.org/doc/html/rfc3977#section-7.6.1) - LIST command\n\
140 List newsgroups, active groups, overview format, etc."
141);
142
143command_cases!(
144 DATE_CASES,
145 "DATE",
146 "date",
147 "Date",
148 "[RFC 3977 §7.1](https://datatracker.ietf.org/doc/html/rfc3977#section-7.1) - DATE command\n\
149 Get server's current UTC date/time"
150);
151
152command_cases!(
153 CAPABILITIES_CASES,
154 "CAPABILITIES",
155 "capabilities",
156 "Capabilities",
157 "[RFC 3977 §5.2](https://datatracker.ietf.org/doc/html/rfc3977#section-5.2) - CAPABILITIES command\n\
158 Report server capabilities and extensions"
159);
160
161command_cases!(
162 MODE_CASES,
163 "MODE",
164 "mode",
165 "Mode",
166 "[RFC 3977 §5.3](https://datatracker.ietf.org/doc/html/rfc3977#section-5.3) - MODE READER command\n\
167 Indicate client is a news reader (vs transit agent)"
168);
169
170command_cases!(
171 HELP_CASES,
172 "HELP",
173 "help",
174 "Help",
175 "[RFC 3977 §7.2](https://datatracker.ietf.org/doc/html/rfc3977#section-7.2) - HELP command\n\
176 Get server help text"
177);
178
179command_cases!(
180 QUIT_CASES,
181 "QUIT",
182 "quit",
183 "Quit",
184 "[RFC 3977 §5.4](https://datatracker.ietf.org/doc/html/rfc3977#section-5.4) - QUIT command\n\
185 Close connection gracefully"
186);
187
188command_cases!(
189 XOVER_CASES,
190 "XOVER",
191 "xover",
192 "Xover",
193 "[RFC 2980 §2.8](https://datatracker.ietf.org/doc/html/rfc2980#section-2.8) - XOVER command (legacy)\n\
194 Retrieve overview information (superseded by OVER in RFC 3977)"
195);
196
197command_cases!(
198 OVER_CASES,
199 "OVER",
200 "over",
201 "Over",
202 "[RFC 3977 §8.3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-8.3.2) - OVER command\n\
203 Retrieve overview information for article range"
204);
205
206command_cases!(
207 XHDR_CASES,
208 "XHDR",
209 "xhdr",
210 "Xhdr",
211 "[RFC 2980 §2.6](https://datatracker.ietf.org/doc/html/rfc2980#section-2.6) - XHDR command (legacy)\n\
212 Retrieve specific header fields (superseded by HDR in RFC 3977)"
213);
214
215command_cases!(
216 HDR_CASES,
217 "HDR",
218 "hdr",
219 "Hdr",
220 "[RFC 3977 §8.5](https://datatracker.ietf.org/doc/html/rfc3977#section-8.5) - HDR command\n\
221 Retrieve header field for article range"
222);
223
224command_cases!(
225 NEXT_CASES,
226 "NEXT",
227 "next",
228 "Next",
229 "[RFC 3977 §6.1.3](https://datatracker.ietf.org/doc/html/rfc3977#section-6.1.3) - NEXT command\n\
230 Advance to next article in current group"
231);
232
233command_cases!(
234 LAST_CASES,
235 "LAST",
236 "last",
237 "Last",
238 "[RFC 3977 §6.1.2](https://datatracker.ietf.org/doc/html/rfc3977#section-6.1.2) - LAST command\n\
239 Move to previous article in current group"
240);
241
242command_cases!(
243 LISTGROUP_CASES,
244 "LISTGROUP",
245 "listgroup",
246 "Listgroup",
247 "[RFC 3977 §6.1.2](https://datatracker.ietf.org/doc/html/rfc3977#section-6.1.2) - LISTGROUP command\n\
248 List article numbers in a newsgroup"
249);
250
251command_cases!(
252 POST_CASES,
253 "POST",
254 "post",
255 "Post",
256 "[RFC 3977 §6.3.1](https://datatracker.ietf.org/doc/html/rfc3977#section-6.3.1) - POST command\n\
257 Post a new article (requires multiline input)"
258);
259
260command_cases!(
261 IHAVE_CASES,
262 "IHAVE",
263 "ihave",
264 "Ihave",
265 "[RFC 3977 §6.3.2](https://datatracker.ietf.org/doc/html/rfc3977#section-6.3.2) - IHAVE command\n\
266 Offer article for transfer (transit/peering)"
267);
268
269command_cases!(
270 NEWGROUPS_CASES,
271 "NEWGROUPS",
272 "newgroups",
273 "Newgroups",
274 "[RFC 3977 §7.3](https://datatracker.ietf.org/doc/html/rfc3977#section-7.3) - NEWGROUPS command\n\
275 List new newsgroups since date/time"
276);
277
278command_cases!(
279 NEWNEWS_CASES,
280 "NEWNEWS",
281 "newnews",
282 "Newnews",
283 "[RFC 3977 §7.4](https://datatracker.ietf.org/doc/html/rfc3977#section-7.4) - NEWNEWS command\n\
284 List new article message-IDs since date/time"
285);
286
287#[inline(always)]
302fn matches_any(cmd: &[u8], cases: &[&[u8]; 3]) -> bool {
303 cmd == cases[0] || cmd == cases[1] || cmd == cases[2]
305}
306
307#[inline(always)]
334fn is_article_cmd_with_msgid(bytes: &[u8]) -> bool {
335 let len = bytes.len();
336
337 if len < 7 {
339 return false;
340 }
341
342 if len >= 6 {
345 if bytes[0..5] == *b"BODY " && bytes[5] == b'<' {
348 return true;
349 }
350 if bytes[0..5] == *b"HEAD " && bytes[5] == b'<' {
351 return true;
352 }
353 if bytes[0..5] == *b"STAT " && bytes[5] == b'<' {
354 return true;
355 }
356
357 if (bytes[0..5] == *b"body " || bytes[0..5] == *b"Body ") && bytes[5] == b'<' {
359 return true;
360 }
361 if (bytes[0..5] == *b"head " || bytes[0..5] == *b"Head ") && bytes[5] == b'<' {
362 return true;
363 }
364 if (bytes[0..5] == *b"stat " || bytes[0..5] == *b"Stat ") && bytes[5] == b'<' {
365 return true;
366 }
367 }
368
369 if len >= 9 {
372 if bytes[0..8] == *b"ARTICLE " && bytes[8] == b'<' {
374 return true;
375 }
376
377 if (bytes[0..8] == *b"article " || bytes[0..8] == *b"Article ") && bytes[8] == b'<' {
379 return true;
380 }
381 }
382
383 false
384}
385
386#[derive(Debug, PartialEq)]
418pub enum NntpCommand {
419 AuthUser,
422
423 AuthPass,
426
427 Stateful,
430
431 NonRoutable,
435
436 Stateless,
439
440 ArticleByMessageId,
443}
444
445impl NntpCommand {
446 #[inline]
451 #[must_use]
452 pub const fn is_stateful(&self) -> bool {
453 matches!(self, Self::Stateful)
454 }
455
456 #[inline]
485 pub fn parse(command: &str) -> Self {
486 let trimmed = command.trim();
487 let bytes = trimmed.as_bytes();
488
489 if is_article_cmd_with_msgid(bytes) {
495 return Self::ArticleByMessageId;
496 }
497
498 let cmd_end = memchr::memchr(b' ', bytes).unwrap_or(bytes.len());
506 let cmd = &bytes[..cmd_end];
507
508 if matches_any(cmd, ARTICLE_CASES)
513 || matches_any(cmd, BODY_CASES)
514 || matches_any(cmd, HEAD_CASES)
515 || matches_any(cmd, STAT_CASES)
516 {
517 return Self::Stateful;
518 }
519
520 if matches_any(cmd, GROUP_CASES) {
524 return Self::Stateful;
525 }
526
527 if matches_any(cmd, AUTHINFO_CASES) {
530 return Self::parse_authinfo(bytes, cmd_end);
531 }
532
533 if matches_any(cmd, LIST_CASES)
536 || matches_any(cmd, DATE_CASES)
537 || matches_any(cmd, CAPABILITIES_CASES)
538 || matches_any(cmd, MODE_CASES)
539 || matches_any(cmd, HELP_CASES)
540 || matches_any(cmd, QUIT_CASES)
541 {
542 return Self::Stateless;
543 }
544
545 if matches_any(cmd, XOVER_CASES)
549 || matches_any(cmd, OVER_CASES)
550 || matches_any(cmd, XHDR_CASES)
551 || matches_any(cmd, HDR_CASES)
552 {
553 return Self::Stateful;
554 }
555
556 if matches_any(cmd, NEXT_CASES)
560 || matches_any(cmd, LAST_CASES)
561 || matches_any(cmd, LISTGROUP_CASES)
562 {
563 return Self::Stateful;
564 }
565
566 if matches_any(cmd, POST_CASES)
570 || matches_any(cmd, IHAVE_CASES)
571 || matches_any(cmd, NEWGROUPS_CASES)
572 || matches_any(cmd, NEWNEWS_CASES)
573 {
574 return Self::NonRoutable;
575 }
576
577 Self::Stateless
579 }
580
581 #[inline]
591 fn parse_authinfo(bytes: &[u8], cmd_end: usize) -> Self {
592 if cmd_end + 1 >= bytes.len() {
593 return Self::Stateless; }
595
596 let args = &bytes[cmd_end + 1..];
597 if args.len() < 4 {
598 return Self::Stateless; }
600
601 match &args[..4] {
603 b"USER" | b"user" | b"User" => Self::AuthUser,
604 b"PASS" | b"pass" | b"Pass" => Self::AuthPass,
605 _ => Self::Stateless, }
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn test_nntp_command_classification() {
616 assert_eq!(
618 NntpCommand::parse("AUTHINFO USER testuser"),
619 NntpCommand::AuthUser
620 );
621 assert_eq!(
622 NntpCommand::parse("AUTHINFO PASS testpass"),
623 NntpCommand::AuthPass
624 );
625 assert_eq!(
626 NntpCommand::parse(" AUTHINFO USER whitespace "),
627 NntpCommand::AuthUser
628 );
629
630 assert_eq!(NntpCommand::parse("GROUP alt.test"), NntpCommand::Stateful);
632 assert_eq!(NntpCommand::parse("NEXT"), NntpCommand::Stateful);
633 assert_eq!(NntpCommand::parse("LAST"), NntpCommand::Stateful);
634 assert_eq!(
635 NntpCommand::parse("LISTGROUP alt.test"),
636 NntpCommand::Stateful
637 );
638 assert_eq!(NntpCommand::parse("ARTICLE 12345"), NntpCommand::Stateful);
639 assert_eq!(NntpCommand::parse("ARTICLE"), NntpCommand::Stateful);
640 assert_eq!(NntpCommand::parse("HEAD 67890"), NntpCommand::Stateful);
641 assert_eq!(NntpCommand::parse("STAT"), NntpCommand::Stateful);
642 assert_eq!(NntpCommand::parse("XOVER 1-100"), NntpCommand::Stateful);
643
644 assert_eq!(
646 NntpCommand::parse("ARTICLE <message@example.com>"),
647 NntpCommand::ArticleByMessageId
648 );
649 assert_eq!(
650 NntpCommand::parse("BODY <test@server.org>"),
651 NntpCommand::ArticleByMessageId
652 );
653 assert_eq!(
654 NntpCommand::parse("HEAD <another@example.net>"),
655 NntpCommand::ArticleByMessageId
656 );
657 assert_eq!(
658 NntpCommand::parse("STAT <id@host.com>"),
659 NntpCommand::ArticleByMessageId
660 );
661
662 assert_eq!(NntpCommand::parse("HELP"), NntpCommand::Stateless);
664 assert_eq!(NntpCommand::parse("LIST"), NntpCommand::Stateless);
665 assert_eq!(NntpCommand::parse("DATE"), NntpCommand::Stateless);
666 assert_eq!(NntpCommand::parse("CAPABILITIES"), NntpCommand::Stateless);
667 assert_eq!(NntpCommand::parse("QUIT"), NntpCommand::Stateless);
668 assert_eq!(NntpCommand::parse("LIST ACTIVE"), NntpCommand::Stateless);
669 assert_eq!(
670 NntpCommand::parse("UNKNOWN COMMAND"),
671 NntpCommand::Stateless
672 );
673 }
674
675 #[test]
676 fn test_case_insensitivity() {
677 assert_eq!(NntpCommand::parse("list"), NntpCommand::Stateless);
679 assert_eq!(NntpCommand::parse("LiSt"), NntpCommand::Stateless);
680 assert_eq!(NntpCommand::parse("QUIT"), NntpCommand::Stateless);
681 assert_eq!(NntpCommand::parse("quit"), NntpCommand::Stateless);
682 assert_eq!(NntpCommand::parse("group alt.test"), NntpCommand::Stateful);
683 assert_eq!(NntpCommand::parse("GROUP alt.test"), NntpCommand::Stateful);
684 }
685
686 #[test]
687 fn test_empty_and_whitespace_commands() {
688 assert_eq!(NntpCommand::parse(""), NntpCommand::Stateless);
690
691 assert_eq!(NntpCommand::parse(" "), NntpCommand::Stateless);
693
694 assert_eq!(NntpCommand::parse("\t\t "), NntpCommand::Stateless);
696 }
697
698 #[test]
699 fn test_malformed_authinfo_commands() {
700 assert_eq!(NntpCommand::parse("AUTHINFO"), NntpCommand::Stateless);
702
703 assert_eq!(
705 NntpCommand::parse("AUTHINFO INVALID"),
706 NntpCommand::Stateless
707 );
708
709 assert_eq!(NntpCommand::parse("AUTHINFO USER"), NntpCommand::AuthUser);
711
712 assert_eq!(NntpCommand::parse("AUTHINFO PASS"), NntpCommand::AuthPass);
714 }
715
716 #[test]
717 fn test_article_commands_with_various_message_ids() {
718 assert_eq!(
720 NntpCommand::parse("ARTICLE <test@example.com>"),
721 NntpCommand::ArticleByMessageId
722 );
723
724 assert_eq!(
726 NntpCommand::parse("ARTICLE <msg.123@news.example.co.uk>"),
727 NntpCommand::ArticleByMessageId
728 );
729
730 assert_eq!(
732 NntpCommand::parse("ARTICLE <user+tag@domain.com>"),
733 NntpCommand::ArticleByMessageId
734 );
735
736 assert_eq!(
738 NntpCommand::parse("BODY <test@test.com>"),
739 NntpCommand::ArticleByMessageId
740 );
741
742 assert_eq!(
744 NntpCommand::parse("HEAD <id@host>"),
745 NntpCommand::ArticleByMessageId
746 );
747
748 assert_eq!(
750 NntpCommand::parse("STAT <msg@server>"),
751 NntpCommand::ArticleByMessageId
752 );
753 }
754
755 #[test]
756 fn test_article_commands_without_message_id() {
757 assert_eq!(NntpCommand::parse("ARTICLE 12345"), NntpCommand::Stateful);
759
760 assert_eq!(NntpCommand::parse("ARTICLE"), NntpCommand::Stateful);
762
763 assert_eq!(NntpCommand::parse("BODY 999"), NntpCommand::Stateful);
765
766 assert_eq!(NntpCommand::parse("HEAD 123"), NntpCommand::Stateful);
768 }
769
770 #[test]
771 fn test_special_characters_in_commands() {
772 assert_eq!(NntpCommand::parse("LIST\r\n"), NntpCommand::Stateless);
774
775 assert_eq!(
777 NntpCommand::parse(" LIST ACTIVE "),
778 NntpCommand::Stateless
779 );
780
781 assert_eq!(NntpCommand::parse("LIST\tACTIVE"), NntpCommand::Stateless);
783 }
784
785 #[test]
786 fn test_very_long_commands() {
787 let long_command = format!("LIST {}", "A".repeat(1000));
789 assert_eq!(NntpCommand::parse(&long_command), NntpCommand::Stateless);
790
791 let long_group = format!("GROUP {}", "alt.".repeat(100));
793 assert_eq!(NntpCommand::parse(&long_group), NntpCommand::Stateful);
794
795 let long_msgid = format!("ARTICLE <{}@example.com>", "x".repeat(500));
797 assert_eq!(
798 NntpCommand::parse(&long_msgid),
799 NntpCommand::ArticleByMessageId
800 );
801 }
802
803 #[test]
804 fn test_list_command_variations() {
805 assert_eq!(NntpCommand::parse("LIST"), NntpCommand::Stateless);
807
808 assert_eq!(NntpCommand::parse("LIST ACTIVE"), NntpCommand::Stateless);
810
811 assert_eq!(
813 NntpCommand::parse("LIST NEWSGROUPS"),
814 NntpCommand::Stateless
815 );
816
817 assert_eq!(
819 NntpCommand::parse("LIST OVERVIEW.FMT"),
820 NntpCommand::Stateless
821 );
822 }
823
824 #[test]
825 fn test_boundary_conditions() {
826 assert_eq!(NntpCommand::parse("X"), NntpCommand::Stateless);
828
829 assert_eq!(
831 NntpCommand::parse("NOTARTICLE <test@example.com>"),
832 NntpCommand::Stateless
833 );
834
835 assert_eq!(
837 NntpCommand::parse("ARTICLE test@example.com"),
838 NntpCommand::Stateful
839 );
840 }
841
842 #[test]
843 fn test_non_routable_commands() {
844 assert_eq!(NntpCommand::parse("POST"), NntpCommand::NonRoutable);
846
847 assert_eq!(
849 NntpCommand::parse("IHAVE <test@example.com>"),
850 NntpCommand::NonRoutable
851 );
852
853 assert_eq!(
855 NntpCommand::parse("NEWGROUPS 20240101 000000 GMT"),
856 NntpCommand::NonRoutable
857 );
858
859 assert_eq!(
861 NntpCommand::parse("NEWNEWS * 20240101 000000 GMT"),
862 NntpCommand::NonRoutable
863 );
864 }
865
866 #[test]
867 fn test_non_routable_case_insensitive() {
868 assert_eq!(NntpCommand::parse("post"), NntpCommand::NonRoutable);
869
870 assert_eq!(NntpCommand::parse("Post"), NntpCommand::NonRoutable);
871
872 assert_eq!(NntpCommand::parse("IHAVE <msg>"), NntpCommand::NonRoutable);
873
874 assert_eq!(NntpCommand::parse("ihave <msg>"), NntpCommand::NonRoutable);
875 }
876
877 #[test]
878 fn test_is_stateful() {
879 assert!(NntpCommand::Stateful.is_stateful());
881
882 assert!(!NntpCommand::ArticleByMessageId.is_stateful());
884 assert!(!NntpCommand::Stateless.is_stateful());
885 assert!(!NntpCommand::AuthUser.is_stateful());
886 assert!(!NntpCommand::AuthPass.is_stateful());
887 assert!(!NntpCommand::NonRoutable.is_stateful());
888
889 assert!(NntpCommand::parse("GROUP alt.test").is_stateful());
891 assert!(NntpCommand::parse("XOVER 1-100").is_stateful());
892 assert!(NntpCommand::parse("ARTICLE 123").is_stateful());
893 assert!(!NntpCommand::parse("ARTICLE <msg@example.com>").is_stateful());
894 assert!(!NntpCommand::parse("LIST").is_stateful());
895 assert!(!NntpCommand::parse("AUTHINFO USER test").is_stateful());
896 }
897
898 #[test]
899 fn test_comprehensive_stateful_commands() {
900 assert!(NntpCommand::parse("GROUP alt.test").is_stateful());
902 assert!(NntpCommand::parse("group comp.lang.rust").is_stateful());
903 assert!(NntpCommand::parse("Group misc.test").is_stateful());
904
905 assert!(NntpCommand::parse("XOVER 1-100").is_stateful());
907 assert!(NntpCommand::parse("xover 50-75").is_stateful());
908 assert!(NntpCommand::parse("Xover 200").is_stateful());
909 assert!(NntpCommand::parse("XOVER").is_stateful()); assert!(NntpCommand::parse("OVER 1-100").is_stateful());
913 assert!(NntpCommand::parse("over 50-75").is_stateful());
914 assert!(NntpCommand::parse("Over 200").is_stateful());
915
916 assert!(NntpCommand::parse("XHDR subject 1-100").is_stateful());
918 assert!(NntpCommand::parse("xhdr from 50-75").is_stateful());
919 assert!(NntpCommand::parse("HDR message-id 1-10").is_stateful());
920 assert!(NntpCommand::parse("hdr references 100").is_stateful());
921
922 assert!(NntpCommand::parse("NEXT").is_stateful());
924 assert!(NntpCommand::parse("next").is_stateful());
925 assert!(NntpCommand::parse("Next").is_stateful());
926 assert!(NntpCommand::parse("LAST").is_stateful());
927 assert!(NntpCommand::parse("last").is_stateful());
928 assert!(NntpCommand::parse("Last").is_stateful());
929
930 assert!(NntpCommand::parse("LISTGROUP alt.test").is_stateful());
932 assert!(NntpCommand::parse("listgroup comp.lang.rust").is_stateful());
933 assert!(NntpCommand::parse("Listgroup misc.test 1-100").is_stateful());
934
935 assert!(NntpCommand::parse("ARTICLE 123").is_stateful());
937 assert!(NntpCommand::parse("article 456").is_stateful());
938 assert!(NntpCommand::parse("Article 789").is_stateful());
939 assert!(NntpCommand::parse("HEAD 123").is_stateful());
940 assert!(NntpCommand::parse("head 456").is_stateful());
941 assert!(NntpCommand::parse("Head 789").is_stateful());
942 assert!(NntpCommand::parse("BODY 123").is_stateful());
943 assert!(NntpCommand::parse("body 456").is_stateful());
944 assert!(NntpCommand::parse("Body 789").is_stateful());
945 assert!(NntpCommand::parse("STAT 123").is_stateful());
946 assert!(NntpCommand::parse("stat 456").is_stateful());
947 assert!(NntpCommand::parse("Stat 789").is_stateful());
948 }
949
950 #[test]
951 fn test_comprehensive_stateless_commands() {
952 assert!(!NntpCommand::parse("ARTICLE <msg@example.com>").is_stateful());
954 assert!(!NntpCommand::parse("article <test@test.com>").is_stateful());
955 assert!(!NntpCommand::parse("Article <foo@bar.net>").is_stateful());
956 assert!(!NntpCommand::parse("HEAD <msg@example.com>").is_stateful());
957 assert!(!NntpCommand::parse("head <test@test.com>").is_stateful());
958 assert!(!NntpCommand::parse("BODY <msg@example.com>").is_stateful());
959 assert!(!NntpCommand::parse("body <test@test.com>").is_stateful());
960 assert!(!NntpCommand::parse("STAT <msg@example.com>").is_stateful());
961 assert!(!NntpCommand::parse("stat <test@test.com>").is_stateful());
962
963 assert!(!NntpCommand::parse("LIST").is_stateful());
965 assert!(!NntpCommand::parse("list").is_stateful());
966 assert!(!NntpCommand::parse("List").is_stateful());
967 assert!(!NntpCommand::parse("LIST ACTIVE").is_stateful());
968 assert!(!NntpCommand::parse("LIST NEWSGROUPS").is_stateful());
969 assert!(!NntpCommand::parse("list active alt.*").is_stateful());
970
971 assert!(!NntpCommand::parse("DATE").is_stateful());
973 assert!(!NntpCommand::parse("date").is_stateful());
974 assert!(!NntpCommand::parse("CAPABILITIES").is_stateful());
975 assert!(!NntpCommand::parse("capabilities").is_stateful());
976 assert!(!NntpCommand::parse("HELP").is_stateful());
977 assert!(!NntpCommand::parse("help").is_stateful());
978 assert!(!NntpCommand::parse("QUIT").is_stateful());
979 assert!(!NntpCommand::parse("quit").is_stateful());
980
981 assert!(!NntpCommand::parse("AUTHINFO USER testuser").is_stateful());
983 assert!(!NntpCommand::parse("authinfo user test").is_stateful());
984 assert!(!NntpCommand::parse("AUTHINFO PASS testpass").is_stateful());
985 assert!(!NntpCommand::parse("authinfo pass secret").is_stateful());
986
987 assert!(!NntpCommand::parse("POST").is_stateful());
989 assert!(!NntpCommand::parse("post").is_stateful());
990 assert!(!NntpCommand::parse("IHAVE <msg@example.com>").is_stateful());
991 assert!(!NntpCommand::parse("ihave <test@test.com>").is_stateful());
992 }
993
994 #[test]
995 fn test_edge_cases_for_stateful_detection() {
996 assert!(NntpCommand::parse("ARTICLE").is_stateful());
998 assert!(NntpCommand::parse("HEAD").is_stateful());
999 assert!(NntpCommand::parse("BODY").is_stateful());
1000 assert!(NntpCommand::parse("STAT").is_stateful());
1001
1002 assert!(NntpCommand::parse("GROUP alt.test").is_stateful());
1004 assert!(NntpCommand::parse("XOVER 1-100").is_stateful());
1005 assert!(!NntpCommand::parse("LIST ACTIVE").is_stateful());
1006
1007 assert!(NntpCommand::parse("Group alt.test").is_stateful());
1010 assert!(NntpCommand::parse("Xover 1-100").is_stateful());
1011 assert!(!NntpCommand::parse("List").is_stateful());
1012
1013 assert!(NntpCommand::parse("ARTICLE 12345").is_stateful()); assert!(!NntpCommand::parse("ARTICLE <12345@example.com>").is_stateful()); assert!(!NntpCommand::parse("ARTICLE <a.b.c@example.com>").is_stateful());
1019 assert!(!NntpCommand::parse("ARTICLE <123.456.789@server.net>").is_stateful());
1020 assert!(
1021 !NntpCommand::parse("HEAD <very-long-message-id@domain.example.org>").is_stateful()
1022 );
1023 }
1024}