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 classify(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::classify_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 classify_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::classify("AUTHINFO USER testuser"),
619 NntpCommand::AuthUser
620 );
621 assert_eq!(
622 NntpCommand::classify("AUTHINFO PASS testpass"),
623 NntpCommand::AuthPass
624 );
625 assert_eq!(
626 NntpCommand::classify(" AUTHINFO USER whitespace "),
627 NntpCommand::AuthUser
628 );
629
630 assert_eq!(
632 NntpCommand::classify("GROUP alt.test"),
633 NntpCommand::Stateful
634 );
635 assert_eq!(NntpCommand::classify("NEXT"), NntpCommand::Stateful);
636 assert_eq!(NntpCommand::classify("LAST"), NntpCommand::Stateful);
637 assert_eq!(
638 NntpCommand::classify("LISTGROUP alt.test"),
639 NntpCommand::Stateful
640 );
641 assert_eq!(
642 NntpCommand::classify("ARTICLE 12345"),
643 NntpCommand::Stateful
644 );
645 assert_eq!(NntpCommand::classify("ARTICLE"), NntpCommand::Stateful);
646 assert_eq!(NntpCommand::classify("HEAD 67890"), NntpCommand::Stateful);
647 assert_eq!(NntpCommand::classify("STAT"), NntpCommand::Stateful);
648 assert_eq!(NntpCommand::classify("XOVER 1-100"), NntpCommand::Stateful);
649
650 assert_eq!(
652 NntpCommand::classify("ARTICLE <message@example.com>"),
653 NntpCommand::ArticleByMessageId
654 );
655 assert_eq!(
656 NntpCommand::classify("BODY <test@server.org>"),
657 NntpCommand::ArticleByMessageId
658 );
659 assert_eq!(
660 NntpCommand::classify("HEAD <another@example.net>"),
661 NntpCommand::ArticleByMessageId
662 );
663 assert_eq!(
664 NntpCommand::classify("STAT <id@host.com>"),
665 NntpCommand::ArticleByMessageId
666 );
667
668 assert_eq!(NntpCommand::classify("HELP"), NntpCommand::Stateless);
670 assert_eq!(NntpCommand::classify("LIST"), NntpCommand::Stateless);
671 assert_eq!(NntpCommand::classify("DATE"), NntpCommand::Stateless);
672 assert_eq!(
673 NntpCommand::classify("CAPABILITIES"),
674 NntpCommand::Stateless
675 );
676 assert_eq!(NntpCommand::classify("QUIT"), NntpCommand::Stateless);
677 assert_eq!(NntpCommand::classify("LIST ACTIVE"), NntpCommand::Stateless);
678 assert_eq!(
679 NntpCommand::classify("UNKNOWN COMMAND"),
680 NntpCommand::Stateless
681 );
682 }
683
684 #[test]
685 fn test_case_insensitivity() {
686 assert_eq!(NntpCommand::classify("list"), NntpCommand::Stateless);
688 assert_eq!(NntpCommand::classify("LiSt"), NntpCommand::Stateless);
689 assert_eq!(NntpCommand::classify("QUIT"), NntpCommand::Stateless);
690 assert_eq!(NntpCommand::classify("quit"), NntpCommand::Stateless);
691 assert_eq!(
692 NntpCommand::classify("group alt.test"),
693 NntpCommand::Stateful
694 );
695 assert_eq!(
696 NntpCommand::classify("GROUP alt.test"),
697 NntpCommand::Stateful
698 );
699 }
700
701 #[test]
702 fn test_empty_and_whitespace_commands() {
703 assert_eq!(NntpCommand::classify(""), NntpCommand::Stateless);
705
706 assert_eq!(NntpCommand::classify(" "), NntpCommand::Stateless);
708
709 assert_eq!(NntpCommand::classify("\t\t "), NntpCommand::Stateless);
711 }
712
713 #[test]
714 fn test_malformed_authinfo_commands() {
715 assert_eq!(NntpCommand::classify("AUTHINFO"), NntpCommand::Stateless);
717
718 assert_eq!(
720 NntpCommand::classify("AUTHINFO INVALID"),
721 NntpCommand::Stateless
722 );
723
724 assert_eq!(
726 NntpCommand::classify("AUTHINFO USER"),
727 NntpCommand::AuthUser
728 );
729
730 assert_eq!(
732 NntpCommand::classify("AUTHINFO PASS"),
733 NntpCommand::AuthPass
734 );
735 }
736
737 #[test]
738 fn test_article_commands_with_various_message_ids() {
739 assert_eq!(
741 NntpCommand::classify("ARTICLE <test@example.com>"),
742 NntpCommand::ArticleByMessageId
743 );
744
745 assert_eq!(
747 NntpCommand::classify("ARTICLE <msg.123@news.example.co.uk>"),
748 NntpCommand::ArticleByMessageId
749 );
750
751 assert_eq!(
753 NntpCommand::classify("ARTICLE <user+tag@domain.com>"),
754 NntpCommand::ArticleByMessageId
755 );
756
757 assert_eq!(
759 NntpCommand::classify("BODY <test@test.com>"),
760 NntpCommand::ArticleByMessageId
761 );
762
763 assert_eq!(
765 NntpCommand::classify("HEAD <id@host>"),
766 NntpCommand::ArticleByMessageId
767 );
768
769 assert_eq!(
771 NntpCommand::classify("STAT <msg@server>"),
772 NntpCommand::ArticleByMessageId
773 );
774 }
775
776 #[test]
777 fn test_article_commands_without_message_id() {
778 assert_eq!(
780 NntpCommand::classify("ARTICLE 12345"),
781 NntpCommand::Stateful
782 );
783
784 assert_eq!(NntpCommand::classify("ARTICLE"), NntpCommand::Stateful);
786
787 assert_eq!(NntpCommand::classify("BODY 999"), NntpCommand::Stateful);
789
790 assert_eq!(NntpCommand::classify("HEAD 123"), NntpCommand::Stateful);
792 }
793
794 #[test]
795 fn test_special_characters_in_commands() {
796 assert_eq!(NntpCommand::classify("LIST\r\n"), NntpCommand::Stateless);
798
799 assert_eq!(
801 NntpCommand::classify(" LIST ACTIVE "),
802 NntpCommand::Stateless
803 );
804
805 assert_eq!(
807 NntpCommand::classify("LIST\tACTIVE"),
808 NntpCommand::Stateless
809 );
810 }
811
812 #[test]
813 fn test_very_long_commands() {
814 let long_command = format!("LIST {}", "A".repeat(1000));
816 assert_eq!(NntpCommand::classify(&long_command), NntpCommand::Stateless);
817
818 let long_group = format!("GROUP {}", "alt.".repeat(100));
820 assert_eq!(NntpCommand::classify(&long_group), NntpCommand::Stateful);
821
822 let long_msgid = format!("ARTICLE <{}@example.com>", "x".repeat(500));
824 assert_eq!(
825 NntpCommand::classify(&long_msgid),
826 NntpCommand::ArticleByMessageId
827 );
828 }
829
830 #[test]
831 fn test_list_command_variations() {
832 assert_eq!(NntpCommand::classify("LIST"), NntpCommand::Stateless);
834
835 assert_eq!(NntpCommand::classify("LIST ACTIVE"), NntpCommand::Stateless);
837
838 assert_eq!(
840 NntpCommand::classify("LIST NEWSGROUPS"),
841 NntpCommand::Stateless
842 );
843
844 assert_eq!(
846 NntpCommand::classify("LIST OVERVIEW.FMT"),
847 NntpCommand::Stateless
848 );
849 }
850
851 #[test]
852 fn test_boundary_conditions() {
853 assert_eq!(NntpCommand::classify("X"), NntpCommand::Stateless);
855
856 assert_eq!(
858 NntpCommand::classify("NOTARTICLE <test@example.com>"),
859 NntpCommand::Stateless
860 );
861
862 assert_eq!(
864 NntpCommand::classify("ARTICLE test@example.com"),
865 NntpCommand::Stateful
866 );
867 }
868
869 #[test]
870 fn test_non_routable_commands() {
871 assert_eq!(NntpCommand::classify("POST"), NntpCommand::NonRoutable);
873
874 assert_eq!(
876 NntpCommand::classify("IHAVE <test@example.com>"),
877 NntpCommand::NonRoutable
878 );
879
880 assert_eq!(
882 NntpCommand::classify("NEWGROUPS 20240101 000000 GMT"),
883 NntpCommand::NonRoutable
884 );
885
886 assert_eq!(
888 NntpCommand::classify("NEWNEWS * 20240101 000000 GMT"),
889 NntpCommand::NonRoutable
890 );
891 }
892
893 #[test]
894 fn test_non_routable_case_insensitive() {
895 assert_eq!(NntpCommand::classify("post"), NntpCommand::NonRoutable);
896
897 assert_eq!(NntpCommand::classify("Post"), NntpCommand::NonRoutable);
898
899 assert_eq!(
900 NntpCommand::classify("IHAVE <msg>"),
901 NntpCommand::NonRoutable
902 );
903
904 assert_eq!(
905 NntpCommand::classify("ihave <msg>"),
906 NntpCommand::NonRoutable
907 );
908 }
909
910 #[test]
911 fn test_is_stateful() {
912 assert!(NntpCommand::Stateful.is_stateful());
914
915 assert!(!NntpCommand::ArticleByMessageId.is_stateful());
917 assert!(!NntpCommand::Stateless.is_stateful());
918 assert!(!NntpCommand::AuthUser.is_stateful());
919 assert!(!NntpCommand::AuthPass.is_stateful());
920 assert!(!NntpCommand::NonRoutable.is_stateful());
921
922 assert!(NntpCommand::classify("GROUP alt.test").is_stateful());
924 assert!(NntpCommand::classify("XOVER 1-100").is_stateful());
925 assert!(NntpCommand::classify("ARTICLE 123").is_stateful());
926 assert!(!NntpCommand::classify("ARTICLE <msg@example.com>").is_stateful());
927 assert!(!NntpCommand::classify("LIST").is_stateful());
928 assert!(!NntpCommand::classify("AUTHINFO USER test").is_stateful());
929 }
930
931 #[test]
932 fn test_comprehensive_stateful_commands() {
933 assert!(NntpCommand::classify("GROUP alt.test").is_stateful());
935 assert!(NntpCommand::classify("group comp.lang.rust").is_stateful());
936 assert!(NntpCommand::classify("Group misc.test").is_stateful());
937
938 assert!(NntpCommand::classify("XOVER 1-100").is_stateful());
940 assert!(NntpCommand::classify("xover 50-75").is_stateful());
941 assert!(NntpCommand::classify("Xover 200").is_stateful());
942 assert!(NntpCommand::classify("XOVER").is_stateful()); assert!(NntpCommand::classify("OVER 1-100").is_stateful());
946 assert!(NntpCommand::classify("over 50-75").is_stateful());
947 assert!(NntpCommand::classify("Over 200").is_stateful());
948
949 assert!(NntpCommand::classify("XHDR subject 1-100").is_stateful());
951 assert!(NntpCommand::classify("xhdr from 50-75").is_stateful());
952 assert!(NntpCommand::classify("HDR message-id 1-10").is_stateful());
953 assert!(NntpCommand::classify("hdr references 100").is_stateful());
954
955 assert!(NntpCommand::classify("NEXT").is_stateful());
957 assert!(NntpCommand::classify("next").is_stateful());
958 assert!(NntpCommand::classify("Next").is_stateful());
959 assert!(NntpCommand::classify("LAST").is_stateful());
960 assert!(NntpCommand::classify("last").is_stateful());
961 assert!(NntpCommand::classify("Last").is_stateful());
962
963 assert!(NntpCommand::classify("LISTGROUP alt.test").is_stateful());
965 assert!(NntpCommand::classify("listgroup comp.lang.rust").is_stateful());
966 assert!(NntpCommand::classify("Listgroup misc.test 1-100").is_stateful());
967
968 assert!(NntpCommand::classify("ARTICLE 123").is_stateful());
970 assert!(NntpCommand::classify("article 456").is_stateful());
971 assert!(NntpCommand::classify("Article 789").is_stateful());
972 assert!(NntpCommand::classify("HEAD 123").is_stateful());
973 assert!(NntpCommand::classify("head 456").is_stateful());
974 assert!(NntpCommand::classify("Head 789").is_stateful());
975 assert!(NntpCommand::classify("BODY 123").is_stateful());
976 assert!(NntpCommand::classify("body 456").is_stateful());
977 assert!(NntpCommand::classify("Body 789").is_stateful());
978 assert!(NntpCommand::classify("STAT 123").is_stateful());
979 assert!(NntpCommand::classify("stat 456").is_stateful());
980 assert!(NntpCommand::classify("Stat 789").is_stateful());
981 }
982
983 #[test]
984 fn test_comprehensive_stateless_commands() {
985 assert!(!NntpCommand::classify("ARTICLE <msg@example.com>").is_stateful());
987 assert!(!NntpCommand::classify("article <test@test.com>").is_stateful());
988 assert!(!NntpCommand::classify("Article <foo@bar.net>").is_stateful());
989 assert!(!NntpCommand::classify("HEAD <msg@example.com>").is_stateful());
990 assert!(!NntpCommand::classify("head <test@test.com>").is_stateful());
991 assert!(!NntpCommand::classify("BODY <msg@example.com>").is_stateful());
992 assert!(!NntpCommand::classify("body <test@test.com>").is_stateful());
993 assert!(!NntpCommand::classify("STAT <msg@example.com>").is_stateful());
994 assert!(!NntpCommand::classify("stat <test@test.com>").is_stateful());
995
996 assert!(!NntpCommand::classify("LIST").is_stateful());
998 assert!(!NntpCommand::classify("list").is_stateful());
999 assert!(!NntpCommand::classify("List").is_stateful());
1000 assert!(!NntpCommand::classify("LIST ACTIVE").is_stateful());
1001 assert!(!NntpCommand::classify("LIST NEWSGROUPS").is_stateful());
1002 assert!(!NntpCommand::classify("list active alt.*").is_stateful());
1003
1004 assert!(!NntpCommand::classify("DATE").is_stateful());
1006 assert!(!NntpCommand::classify("date").is_stateful());
1007 assert!(!NntpCommand::classify("CAPABILITIES").is_stateful());
1008 assert!(!NntpCommand::classify("capabilities").is_stateful());
1009 assert!(!NntpCommand::classify("HELP").is_stateful());
1010 assert!(!NntpCommand::classify("help").is_stateful());
1011 assert!(!NntpCommand::classify("QUIT").is_stateful());
1012 assert!(!NntpCommand::classify("quit").is_stateful());
1013
1014 assert!(!NntpCommand::classify("AUTHINFO USER testuser").is_stateful());
1016 assert!(!NntpCommand::classify("authinfo user test").is_stateful());
1017 assert!(!NntpCommand::classify("AUTHINFO PASS testpass").is_stateful());
1018 assert!(!NntpCommand::classify("authinfo pass secret").is_stateful());
1019
1020 assert!(!NntpCommand::classify("POST").is_stateful());
1022 assert!(!NntpCommand::classify("post").is_stateful());
1023 assert!(!NntpCommand::classify("IHAVE <msg@example.com>").is_stateful());
1024 assert!(!NntpCommand::classify("ihave <test@test.com>").is_stateful());
1025 }
1026
1027 #[test]
1028 fn test_edge_cases_for_stateful_detection() {
1029 assert!(NntpCommand::classify("ARTICLE").is_stateful());
1031 assert!(NntpCommand::classify("HEAD").is_stateful());
1032 assert!(NntpCommand::classify("BODY").is_stateful());
1033 assert!(NntpCommand::classify("STAT").is_stateful());
1034
1035 assert!(NntpCommand::classify("GROUP alt.test").is_stateful());
1037 assert!(NntpCommand::classify("XOVER 1-100").is_stateful());
1038 assert!(!NntpCommand::classify("LIST ACTIVE").is_stateful());
1039
1040 assert!(NntpCommand::classify("Group alt.test").is_stateful());
1043 assert!(NntpCommand::classify("Xover 1-100").is_stateful());
1044 assert!(!NntpCommand::classify("List").is_stateful());
1045
1046 assert!(NntpCommand::classify("ARTICLE 12345").is_stateful()); assert!(!NntpCommand::classify("ARTICLE <12345@example.com>").is_stateful()); assert!(!NntpCommand::classify("ARTICLE <a.b.c@example.com>").is_stateful());
1052 assert!(!NntpCommand::classify("ARTICLE <123.456.789@server.net>").is_stateful());
1053 assert!(
1054 !NntpCommand::classify("HEAD <very-long-message-id@domain.example.org>").is_stateful()
1055 );
1056 }
1057}