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