1use smallvec::SmallVec;
8
9use super::{StatusCode, codes};
10use crate::types::{BackendId, MessageId};
11
12pub const MAX_COMMAND_LINE_OCTETS: usize = 512;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum RequestKind {
16 Article,
17 Body,
18 Head,
19 Stat,
20 Group,
21 ListGroup,
22 Last,
23 Next,
24 List,
25 Date,
26 Help,
27 Capabilities,
28 Mode,
29 Quit,
30 Over,
31 Xover,
32 Hdr,
33 Xhdr,
34 NewGroups,
35 NewNews,
36 Post,
37 Ihave,
38 Check,
39 TakeThis,
40 AuthInfo,
41 StartTls,
42 Unknown,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum RequestRouteClass {
47 ArticleByMessageId,
48 Stateless,
49 Stateful,
50 Local,
51 Reject,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum RequestCacheStatus {
56 Hit,
57 PartialHit,
58 Miss,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
62pub struct RequestWireLen(usize);
63
64impl RequestWireLen {
65 #[must_use]
66 pub const fn new(value: usize) -> Self {
67 Self(value)
68 }
69
70 #[must_use]
71 pub const fn get(self) -> usize {
72 self.0
73 }
74
75 #[must_use]
76 pub const fn as_u64(self) -> u64 {
77 self.0 as u64
78 }
79}
80
81impl From<usize> for RequestWireLen {
82 fn from(value: usize) -> Self {
83 Self::new(value)
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
88pub struct ResponseWireLen(usize);
89
90impl ResponseWireLen {
91 #[must_use]
92 pub const fn new(value: usize) -> Self {
93 Self(value)
94 }
95
96 #[must_use]
97 pub const fn get(self) -> usize {
98 self.0
99 }
100}
101
102impl From<usize> for ResponseWireLen {
103 fn from(value: usize) -> Self {
104 Self::new(value)
105 }
106}
107
108#[cfg(test)]
109#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
110pub struct ResponsePayloadLen(usize);
111
112#[cfg(test)]
113impl ResponsePayloadLen {
114 #[must_use]
115 pub(crate) const fn new(value: usize) -> Self {
116 Self(value)
117 }
118}
119
120#[cfg(test)]
121impl From<usize> for ResponsePayloadLen {
122 fn from(value: usize) -> Self {
123 Self::new(value)
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128pub struct RequestResponseMetadata {
129 status: StatusCode,
130 wire_len: ResponseWireLen,
131}
132
133impl RequestResponseMetadata {
134 #[must_use]
135 pub(crate) const fn new(status: StatusCode, wire_len: ResponseWireLen) -> Self {
136 Self { status, wire_len }
137 }
138
139 #[must_use]
140 #[allow(dead_code)]
141 pub(crate) const fn status(self) -> StatusCode {
142 self.status
143 }
144
145 #[must_use]
146 pub(crate) const fn wire_len(self) -> ResponseWireLen {
147 self.wire_len
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
152pub struct RequestCacheAvailability {
153 checked: usize,
154 missing: usize,
155}
156
157impl RequestCacheAvailability {
158 #[must_use]
159 pub(crate) const fn from_bits(checked: usize, missing: usize) -> Self {
160 Self { checked, missing }
161 }
162
163 #[must_use]
164 pub(crate) const fn missing_bits(self) -> usize {
165 self.missing
166 }
167
168 #[must_use]
169 pub(crate) fn backend_has_article(self, backend_id: BackendId) -> bool {
170 let mask = backend_id.availability_bit();
171 self.checked & mask != 0 && self.missing & mask == 0
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
176pub struct RequestCacheTier(u8);
177
178impl RequestCacheTier {
179 #[must_use]
180 pub(crate) const fn new(value: u8) -> Self {
181 Self(value)
182 }
183}
184
185impl From<u8> for RequestCacheTier {
186 fn from(value: u8) -> Self {
187 Self::new(value)
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
192pub struct RequestCacheTimestampMillis(u64);
193
194impl RequestCacheTimestampMillis {
195 #[must_use]
196 pub(crate) const fn new(value: u64) -> Self {
197 Self(value)
198 }
199}
200
201impl From<u64> for RequestCacheTimestampMillis {
202 fn from(value: u64) -> Self {
203 Self::new(value)
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum RequestCachePayloadKind {
209 Missing,
210 AvailabilityOnly,
211 Article,
212 Head,
213 Body,
214 Stat,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
218pub struct RequestCacheArticleNumber(u64);
219
220impl RequestCacheArticleNumber {
221 #[must_use]
222 pub(crate) const fn new(value: u64) -> Self {
223 Self(value)
224 }
225}
226
227impl From<u64> for RequestCacheArticleNumber {
228 fn from(value: u64) -> Self {
229 Self::new(value)
230 }
231}
232
233#[derive(Debug)]
234pub struct RequestContext {
235 kind: RequestKind,
236 verb: SmallVec<[u8; 16]>,
237 args: SmallVec<[u8; 512]>,
238 message_id: Option<(usize, usize)>,
239 cache_status: Option<RequestCacheStatus>,
240 cache_entry: Option<RequestCacheEntryMetadata>,
241 backend_id: Option<BackendId>,
242 response: Option<RequestResponseMetadata>,
243 response_payload: Option<crate::pool::ChunkedResponse>,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247struct RequestLine<'a> {
248 kind: RequestKind,
249 verb: &'a [u8],
250 args: &'a [u8],
251 message_id: Option<(usize, usize)>,
252}
253
254impl<'a> RequestLine<'a> {
255 #[must_use]
256 pub fn parse(line: &'a [u8]) -> Self {
257 let bytes = trim_line_end(line);
258 let split = memchr::memchr(b' ', bytes).unwrap_or(bytes.len());
259 let verb = &bytes[..split];
260 let args = if split < bytes.len() {
261 &bytes[split + 1..]
262 } else {
263 &[]
264 };
265
266 Self {
267 kind: classify_verb(verb),
268 verb,
269 args,
270 message_id: find_message_id(args),
271 }
272 }
273
274 #[must_use]
275 pub const fn kind(&self) -> RequestKind {
276 self.kind
277 }
278
279 #[must_use]
280 pub const fn verb(&self) -> &'a [u8] {
281 self.verb
282 }
283
284 #[must_use]
285 pub const fn args(&self) -> &'a [u8] {
286 self.args
287 }
288
289 #[must_use]
290 #[cfg(test)]
291 pub fn message_id(&self) -> Option<&str> {
292 let (start, end) = self.message_id?;
293 std::str::from_utf8(&self.args[start..end]).ok()
294 }
295
296 #[must_use]
297 #[cfg(test)]
298 pub fn message_id_value(&self) -> Option<MessageId<'a>> {
299 let (start, end) = self.message_id?;
300 MessageId::from_borrowed(std::str::from_utf8(&self.args[start..end]).ok()?).ok()
301 }
302
303 #[must_use]
304 const fn message_id_span(&self) -> Option<(usize, usize)> {
305 self.message_id
306 }
307
308 #[must_use]
309 #[cfg(test)]
310 pub const fn route_class(&self) -> RequestRouteClass {
311 route_class(self.kind, self.message_id.is_some())
312 }
313}
314
315impl Clone for RequestContext {
316 fn clone(&self) -> Self {
317 debug_assert!(
318 self.response_payload.is_none(),
319 "completed response payloads are not cloned"
320 );
321 Self {
322 kind: self.kind,
323 verb: self.verb.clone(),
324 args: self.args.clone(),
325 message_id: self.message_id,
326 cache_status: self.cache_status,
327 cache_entry: self.cache_entry,
328 backend_id: self.backend_id,
329 response: self.response,
330 response_payload: None,
331 }
332 }
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub struct RequestCacheEntryMetadata {
337 status: StatusCode,
338 availability: RequestCacheAvailability,
339 tier: RequestCacheTier,
340 timestamp: RequestCacheTimestampMillis,
341 payload_kind: RequestCachePayloadKind,
342 article_number: Option<RequestCacheArticleNumber>,
343}
344
345impl RequestCacheEntryMetadata {
346 #[must_use]
347 pub(crate) const fn new(
348 status: StatusCode,
349 availability: RequestCacheAvailability,
350 tier: RequestCacheTier,
351 timestamp: RequestCacheTimestampMillis,
352 payload_kind: RequestCachePayloadKind,
353 article_number: Option<RequestCacheArticleNumber>,
354 ) -> Self {
355 Self {
356 status,
357 availability,
358 tier,
359 timestamp,
360 payload_kind,
361 article_number,
362 }
363 }
364
365 #[must_use]
366 pub(crate) const fn status(self) -> StatusCode {
367 self.status
368 }
369
370 #[must_use]
371 pub(crate) const fn availability(self) -> RequestCacheAvailability {
372 self.availability
373 }
374
375 #[cfg(test)]
376 #[must_use]
377 pub(crate) const fn tier(self) -> RequestCacheTier {
378 self.tier
379 }
380
381 #[cfg(test)]
382 #[must_use]
383 pub(crate) const fn timestamp(self) -> RequestCacheTimestampMillis {
384 self.timestamp
385 }
386
387 #[cfg(test)]
388 #[must_use]
389 pub(crate) const fn payload_kind(self) -> RequestCachePayloadKind {
390 self.payload_kind
391 }
392
393 #[cfg(test)]
394 #[must_use]
395 pub(crate) const fn article_number(self) -> Option<RequestCacheArticleNumber> {
396 self.article_number
397 }
398}
399
400impl RequestContext {
401 #[must_use]
402 pub fn parse(line: &[u8]) -> Option<Self> {
403 if line.len() > MAX_COMMAND_LINE_OCTETS {
404 return None;
405 }
406
407 let line = RequestLine::parse(line);
408 (!line.verb().is_empty()).then(|| Self::from_request_line(line))
409 }
410
411 #[must_use]
412 fn from_request_line(line: RequestLine<'_>) -> Self {
413 Self::from_parts(
414 line.kind(),
415 SmallVec::from_slice(line.verb()),
416 SmallVec::from_slice(line.args()),
417 line.message_id_span(),
418 )
419 }
420
421 #[must_use]
422 pub(crate) fn from_verb_args(verb: &[u8], args: &[u8]) -> Self {
423 let verb = SmallVec::from_slice(verb);
424 let args = SmallVec::from_slice(args);
425 let kind = classify_verb(&verb);
426 let message_id = find_message_id(&args);
427
428 Self::from_parts(kind, verb, args, message_id)
429 }
430
431 #[must_use]
432 pub(crate) fn from_verb_arg_slices(verb: &[u8], args: &[&[u8]]) -> Self {
433 let verb = SmallVec::from_slice(verb);
434 let arg_len = args.iter().map(|part| part.len()).sum();
435 let mut joined_args = SmallVec::<[u8; 512]>::with_capacity(arg_len);
436 for part in args {
437 joined_args.extend_from_slice(part);
438 }
439 let kind = classify_verb(&verb);
440 let message_id = find_message_id(&joined_args);
441
442 Self::from_parts(kind, verb, joined_args, message_id)
443 }
444
445 const fn from_parts(
446 kind: RequestKind,
447 verb: SmallVec<[u8; 16]>,
448 args: SmallVec<[u8; 512]>,
449 message_id: Option<(usize, usize)>,
450 ) -> Self {
451 Self {
452 kind,
453 verb,
454 args,
455 message_id,
456 cache_status: None,
457 cache_entry: None,
458 backend_id: None,
459 response: None,
460 response_payload: None,
461 }
462 }
463
464 #[inline]
465 #[must_use]
466 pub const fn kind(&self) -> RequestKind {
467 self.kind
468 }
469
470 #[inline]
471 #[must_use]
472 pub const fn backend_id(&self) -> Option<BackendId> {
473 self.backend_id
474 }
475
476 #[cfg(test)]
477 #[inline]
478 #[must_use]
479 pub(crate) const fn cache_status(&self) -> Option<RequestCacheStatus> {
480 self.cache_status
481 }
482
483 #[inline]
484 #[must_use]
485 pub(crate) const fn cache_availability(&self) -> Option<RequestCacheAvailability> {
486 match self.cache_entry {
487 Some(entry) => Some(entry.availability()),
488 None => None,
489 }
490 }
491
492 #[inline]
493 #[must_use]
494 pub(crate) fn cache_records_backend_has_article(&self, backend_id: BackendId) -> bool {
495 self.cache_availability()
496 .is_some_and(|availability| availability.backend_has_article(backend_id))
497 }
498
499 #[inline]
500 #[must_use]
501 pub const fn cache_entry_status(&self) -> Option<StatusCode> {
502 match self.cache_entry {
503 Some(entry) => Some(entry.status()),
504 None => None,
505 }
506 }
507
508 #[cfg(test)]
509 #[inline]
510 #[must_use]
511 pub(crate) const fn cache_entry_tier(&self) -> Option<RequestCacheTier> {
512 match self.cache_entry {
513 Some(entry) => Some(entry.tier()),
514 None => None,
515 }
516 }
517
518 #[cfg(test)]
519 #[inline]
520 #[must_use]
521 pub(crate) const fn cache_entry_timestamp(&self) -> Option<RequestCacheTimestampMillis> {
522 match self.cache_entry {
523 Some(entry) => Some(entry.timestamp()),
524 None => None,
525 }
526 }
527
528 #[cfg(test)]
529 #[inline]
530 #[must_use]
531 pub(crate) const fn cache_payload_kind(&self) -> Option<RequestCachePayloadKind> {
532 match self.cache_entry {
533 Some(entry) => Some(entry.payload_kind()),
534 None => None,
535 }
536 }
537
538 #[cfg(test)]
539 #[inline]
540 #[must_use]
541 pub(crate) const fn cache_article_number(&self) -> Option<RequestCacheArticleNumber> {
542 match self.cache_entry {
543 Some(entry) => entry.article_number(),
544 None => None,
545 }
546 }
547
548 #[cfg(test)]
549 #[inline]
550 #[must_use]
551 pub(crate) const fn cache_entry_metadata(&self) -> Option<RequestCacheEntryMetadata> {
552 self.cache_entry
553 }
554
555 #[inline]
556 pub(crate) const fn record_cache_status(&mut self, status: RequestCacheStatus) {
557 self.cache_status = Some(status);
558 }
559
560 #[inline]
561 pub(crate) const fn record_cache_entry_metadata(
562 &mut self,
563 metadata: RequestCacheEntryMetadata,
564 ) {
565 self.cache_entry = Some(metadata);
566 }
567
568 #[inline]
569 #[must_use]
570 pub const fn response_status(&self) -> Option<StatusCode> {
571 match self.response {
572 Some(response) => Some(response.status),
573 None => None,
574 }
575 }
576
577 #[inline]
578 #[must_use]
579 pub const fn response_wire_len(&self) -> Option<ResponseWireLen> {
580 match self.response {
581 Some(response) => Some(response.wire_len),
582 None => None,
583 }
584 }
585
586 #[inline]
587 #[must_use]
588 pub(crate) const fn response_metadata(&self) -> Option<RequestResponseMetadata> {
589 self.response
590 }
591
592 #[inline]
593 #[must_use]
594 #[cfg(test)]
595 pub(crate) const fn response_payload_len(&self) -> Option<ResponsePayloadLen> {
596 match &self.response_payload {
597 Some(response) => Some(ResponsePayloadLen::new(response.len())),
598 None => None,
599 }
600 }
601
602 #[inline]
603 #[allow(dead_code)]
604 pub(crate) fn complete_backend_response(
605 &mut self,
606 backend_id: BackendId,
607 status: StatusCode,
608 response: crate::pool::ChunkedResponse,
609 ) {
610 self.backend_id = Some(backend_id);
611 self.response = Some(RequestResponseMetadata::new(status, response.len().into()));
612 self.response_payload = Some(response);
613 }
614
615 #[inline]
616 #[allow(dead_code)]
617 pub(crate) fn take_response_payload(&mut self) -> Option<crate::pool::ChunkedResponse> {
618 self.response_payload.take()
619 }
620
621 #[inline]
622 #[cfg(test)]
623 #[must_use]
624 pub(crate) fn response_payload(&self) -> Option<&crate::pool::ChunkedResponse> {
625 self.response_payload.as_ref()
626 }
627
628 #[inline]
629 #[cfg(test)]
630 #[must_use]
631 pub(crate) fn response_payload_eq(&self, expected: &[u8]) -> Option<bool> {
632 let response = self.response_payload()?;
633 if response.len() != expected.len() {
634 return Some(false);
635 }
636
637 let mut offset = 0;
638 Some(response.iter_chunks().all(|chunk| {
639 let end = offset + chunk.len();
640 let matches = expected
641 .get(offset..end)
642 .is_some_and(|expected_chunk| expected_chunk == chunk);
643 offset = end;
644 matches
645 }))
646 }
647
648 #[inline]
649 pub(crate) const fn record_backend_response(
650 &mut self,
651 backend_id: BackendId,
652 response: RequestResponseMetadata,
653 ) {
654 self.backend_id = Some(backend_id);
655 self.response = Some(response);
656 }
657
658 #[inline]
659 pub(crate) const fn record_cache_response(&mut self, response: RequestResponseMetadata) {
660 self.cache_status = Some(RequestCacheStatus::Hit);
661 self.response = Some(response);
662 }
663
664 #[inline]
665 pub(crate) const fn record_local_response(&mut self, response: RequestResponseMetadata) {
666 self.response = Some(response);
667 }
668
669 #[inline]
670 #[must_use]
671 pub fn verb(&self) -> &[u8] {
672 &self.verb
673 }
674
675 #[inline]
676 #[must_use]
677 pub fn args(&self) -> &[u8] {
678 &self.args
679 }
680
681 #[must_use]
682 pub fn message_id(&self) -> Option<&str> {
683 let (start, end) = self.message_id?;
684 std::str::from_utf8(&self.args[start..end]).ok()
685 }
686
687 #[must_use]
688 pub fn message_id_value(&self) -> Option<MessageId<'_>> {
689 let (start, end) = self.message_id?;
690 MessageId::from_borrowed(std::str::from_utf8(&self.args[start..end]).ok()?).ok()
691 }
692
693 #[must_use]
694 pub const fn has_message_id(&self) -> bool {
695 self.message_id.is_some()
696 }
697
698 #[must_use]
699 pub const fn is_stat(&self) -> bool {
700 matches!(self.kind, RequestKind::Stat)
701 }
702
703 #[must_use]
704 pub const fn is_head(&self) -> bool {
705 matches!(self.kind, RequestKind::Head)
706 }
707
708 #[must_use]
709 pub const fn is_unknown_extension(&self) -> bool {
710 matches!(self.kind, RequestKind::Unknown)
711 }
712
713 #[must_use]
714 pub const fn route_class(&self) -> RequestRouteClass {
715 route_class(self.kind, self.message_id.is_some())
716 }
717
718 #[must_use]
719 pub const fn is_pipelineable(&self) -> bool {
720 matches!(self.route_class(), RequestRouteClass::ArticleByMessageId)
721 }
722
723 #[must_use]
724 pub const fn is_large_transfer(&self) -> bool {
725 matches!(self.kind, RequestKind::Article | RequestKind::Body) && self.message_id.is_some()
726 }
727
728 #[must_use]
729 pub fn request_wire_len(&self) -> RequestWireLen {
730 (self.verb.len() + usize::from(!self.args.is_empty()) + self.args.len() + 2).into()
731 }
732
733 pub async fn write_wire_to<W>(&self, writer: &mut W) -> std::io::Result<()>
738 where
739 W: tokio::io::AsyncWrite + Unpin,
740 {
741 use std::io::IoSlice;
742
743 if self.args().is_empty() {
744 let mut slices = [IoSlice::new(self.verb()), IoSlice::new(b"\r\n")];
745 crate::io_util::write_all_vectored(writer, &mut slices).await
746 } else {
747 let mut slices = [
748 IoSlice::new(self.verb()),
749 IoSlice::new(b" "),
750 IoSlice::new(self.args()),
751 IoSlice::new(b"\r\n"),
752 ];
753 crate::io_util::write_all_vectored(writer, &mut slices).await
754 }
755 }
756
757 #[must_use]
758 pub fn has_response_body(&self, status: StatusCode) -> bool {
759 request_kind_has_response_body(self.kind, status)
760 }
761}
762
763#[must_use]
764pub(crate) fn request_kind_has_response_body(kind: RequestKind, status: StatusCode) -> bool {
765 let code = status.as_u16();
766 if status.is_error() {
767 return false;
768 }
769
770 matches!(
771 (kind, code),
772 (RequestKind::Article, 220)
773 | (RequestKind::Head, 221)
774 | (RequestKind::Body, 222)
775 | (RequestKind::ListGroup, 211)
776 | (RequestKind::Help, 100)
777 | (RequestKind::Capabilities, 101)
778 | (RequestKind::List, 215)
779 | (RequestKind::Over | RequestKind::Xover, 224)
780 | (RequestKind::Hdr | RequestKind::Xhdr, 225)
781 | (RequestKind::NewNews, 230)
782 | (RequestKind::NewGroups, 231)
783 ) || matches!(kind, RequestKind::Unknown) && status_implies_response_body(code)
784}
785
786fn trim_line_end(mut line: &[u8]) -> &[u8] {
787 while matches!(line.last(), Some(b'\r' | b'\n')) {
788 line = &line[..line.len() - 1];
789 }
790 line
791}
792
793const fn route_class(kind: RequestKind, has_message_id: bool) -> RequestRouteClass {
794 match kind {
795 RequestKind::Capabilities | RequestKind::Quit | RequestKind::AuthInfo => {
796 RequestRouteClass::Local
797 }
798 RequestKind::Post
799 | RequestKind::Ihave
800 | RequestKind::Check
801 | RequestKind::TakeThis
802 | RequestKind::StartTls => RequestRouteClass::Reject,
803 RequestKind::Article | RequestKind::Body | RequestKind::Head | RequestKind::Stat
804 if has_message_id =>
805 {
806 RequestRouteClass::ArticleByMessageId
807 }
808 RequestKind::Article
809 | RequestKind::Body
810 | RequestKind::Head
811 | RequestKind::Stat
812 | RequestKind::Group
813 | RequestKind::ListGroup
814 | RequestKind::Last
815 | RequestKind::Next
816 | RequestKind::Over
817 | RequestKind::Xover
818 | RequestKind::Hdr
819 | RequestKind::Xhdr
820 | RequestKind::Unknown => RequestRouteClass::Stateful,
821 RequestKind::List
822 | RequestKind::Date
823 | RequestKind::Help
824 | RequestKind::Mode
825 | RequestKind::NewGroups
826 | RequestKind::NewNews => RequestRouteClass::Stateless,
827 }
828}
829
830const fn classify_verb(verb: &[u8]) -> RequestKind {
831 macro_rules! classify_verbs {
832 ($verb:expr; $($len:literal => { $($lit:literal => $kind:expr),+ $(,)? }),+ $(,)?) => {{
833 match $verb.len() {
834 $(
835 $len => {
836 $(
837 const _: [(); $len] = [(); $lit.len()];
838 )+
839 $(
840 if eq_ignore_ascii_case_const($verb, $lit) {
841 $kind
842 } else
843 )+
844 {
845 RequestKind::Unknown
846 }
847 }
848 )+
849 _ => RequestKind::Unknown,
850 }
851 }};
852 }
853
854 classify_verbs!(verb;
855 3 => {
856 b"HDR" => RequestKind::Hdr,
857 },
858 4 => {
859 b"BODY" => RequestKind::Body,
860 b"DATE" => RequestKind::Date,
861 b"HEAD" => RequestKind::Head,
862 b"HELP" => RequestKind::Help,
863 b"LAST" => RequestKind::Last,
864 b"LIST" => RequestKind::List,
865 b"MODE" => RequestKind::Mode,
866 b"NEXT" => RequestKind::Next,
867 b"OVER" => RequestKind::Over,
868 b"POST" => RequestKind::Post,
869 b"QUIT" => RequestKind::Quit,
870 b"STAT" => RequestKind::Stat,
871 b"XHDR" => RequestKind::Xhdr,
872 },
873 5 => {
874 b"CHECK" => RequestKind::Check,
875 b"GROUP" => RequestKind::Group,
876 b"IHAVE" => RequestKind::Ihave,
877 b"XOVER" => RequestKind::Xover,
878 },
879 7 => {
880 b"ARTICLE" => RequestKind::Article,
881 b"NEWNEWS" => RequestKind::NewNews,
882 },
883 8 => {
884 b"AUTHINFO" => RequestKind::AuthInfo,
885 b"STARTTLS" => RequestKind::StartTls,
886 b"TAKETHIS" => RequestKind::TakeThis,
887 },
888 9 => {
889 b"LISTGROUP" => RequestKind::ListGroup,
890 b"NEWGROUPS" => RequestKind::NewGroups,
891 },
892 12 => {
893 b"CAPABILITIES" => RequestKind::Capabilities,
894 },
895 )
896}
897
898const fn eq_ignore_ascii_case_const(left: &[u8], right: &[u8]) -> bool {
899 if left.len() != right.len() {
900 return false;
901 }
902
903 let mut index = 0;
904 while index < left.len() {
905 if ascii_upper(left[index]) != ascii_upper(right[index]) {
906 return false;
907 }
908 index += 1;
909 }
910
911 true
912}
913
914const fn ascii_upper(byte: u8) -> u8 {
915 match byte {
916 b'a'..=b'z' => byte - 32,
917 _ => byte,
918 }
919}
920
921fn find_message_id(args: &[u8]) -> Option<(usize, usize)> {
922 let start = args.iter().position(|byte| !byte.is_ascii_whitespace())?;
923 let end = args.iter().rposition(|byte| !byte.is_ascii_whitespace())? + 1;
924 let trimmed = &args[start..end];
925
926 if !trimmed.starts_with(b"<")
927 || !trimmed.ends_with(b">")
928 || trimmed[1..trimmed.len() - 1]
929 .iter()
930 .any(u8::is_ascii_whitespace)
931 {
932 return None;
933 }
934
935 MessageId::from_borrowed(std::str::from_utf8(trimmed).ok()?).ok()?;
936 Some((start, end))
937}
938
939const fn status_implies_response_body(code: u16) -> bool {
940 matches!(
941 code,
942 codes::HELP_TEXT
943 | codes::CAPABILITY_LIST
944 | codes::INFORMATION_FOLLOWS
945 | codes::ARTICLE_FOLLOWS
946 | codes::HEAD_FOLLOWS
947 | codes::BODY_FOLLOWS
948 | codes::OVERVIEW_FOLLOWS
949 | codes::HEADERS_FOLLOW
950 | codes::NEW_ARTICLES_FOLLOW
951 | codes::NEW_GROUPS_FOLLOW
952 | 282
953 | 288
954 )
955}
956
957#[cfg(test)]
958mod tests {
959 use super::*;
960 use futures::executor::block_on;
961 use std::io::IoSlice;
962 use std::pin::Pin;
963 use std::task::{Context, Poll};
964 use tokio::io::AsyncWrite;
965
966 #[derive(Default)]
967 struct CountingWriter {
968 bytes: Vec<u8>,
969 writes: usize,
970 vectored_writes: usize,
971 }
972
973 impl AsyncWrite for CountingWriter {
974 fn poll_write(
975 mut self: Pin<&mut Self>,
976 _cx: &mut Context<'_>,
977 buf: &[u8],
978 ) -> Poll<std::io::Result<usize>> {
979 self.writes += 1;
980 self.bytes.extend_from_slice(buf);
981 Poll::Ready(Ok(buf.len()))
982 }
983
984 fn poll_write_vectored(
985 mut self: Pin<&mut Self>,
986 _cx: &mut Context<'_>,
987 bufs: &[IoSlice<'_>],
988 ) -> Poll<std::io::Result<usize>> {
989 self.vectored_writes += 1;
990 let len = bufs.iter().map(|buf| buf.len()).sum();
991 for buf in bufs {
992 self.bytes.extend_from_slice(buf);
993 }
994 Poll::Ready(Ok(len))
995 }
996
997 fn is_write_vectored(&self) -> bool {
998 true
999 }
1000
1001 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
1002 Poll::Ready(Ok(()))
1003 }
1004
1005 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
1006 Poll::Ready(Ok(()))
1007 }
1008 }
1009
1010 fn wire(context: &RequestContext) -> Vec<u8> {
1011 let mut out = Vec::with_capacity(context.request_wire_len().get());
1012 block_on(context.write_wire_to(&mut out)).unwrap();
1013 out
1014 }
1015
1016 fn request_context(line: &[u8]) -> RequestContext {
1017 RequestContext::parse(line).expect("valid request line")
1018 }
1019
1020 #[test]
1021 fn typed_request_context_parses_message_id() {
1022 let ctx = request_context(b"ARTICLE <a@b>\r\n");
1023 assert_eq!(ctx.kind(), RequestKind::Article);
1024 assert_eq!(ctx.request_wire_len(), RequestWireLen::new(15));
1025 assert_eq!(ctx.message_id(), Some("<a@b>"));
1026 assert_eq!(ctx.cache_status(), None);
1027 assert_eq!(ctx.cache_availability(), None);
1028 assert_eq!(ctx.cache_entry_status(), None);
1029 assert_eq!(ctx.cache_entry_tier(), None);
1030 assert_eq!(ctx.cache_entry_timestamp(), None);
1031 assert_eq!(ctx.cache_payload_kind(), None);
1032 assert_eq!(ctx.cache_article_number(), None);
1033 assert_eq!(ctx.backend_id(), None);
1034 assert_eq!(ctx.response_status(), None);
1035 assert_eq!(ctx.response_wire_len(), None);
1036 assert!(ctx.is_pipelineable());
1037 assert_eq!(wire(&ctx), b"ARTICLE <a@b>\r\n");
1038 }
1039
1040 #[test]
1041 fn typed_request_context_uses_vectored_write() {
1042 let ctx = request_context(b"ARTICLE <a@b>\r\n");
1043 let mut out = CountingWriter::default();
1044
1045 block_on(ctx.write_wire_to(&mut out)).unwrap();
1046
1047 assert_eq!(out.bytes, b"ARTICLE <a@b>\r\n");
1048 assert_eq!(out.writes, 0);
1049 assert_eq!(out.vectored_writes, 1);
1050 }
1051
1052 #[test]
1053 fn borrowed_request_line_parses_without_owning_bytes() {
1054 let bytes = b"ARTICLE <a@b>\r\n";
1055 let parsed = RequestLine::parse(bytes);
1056
1057 assert_eq!(parsed.kind(), RequestKind::Article);
1058 assert_eq!(parsed.verb(), b"ARTICLE");
1059 assert_eq!(parsed.args(), b"<a@b>");
1060 assert_eq!(parsed.message_id(), Some("<a@b>"));
1061 assert_eq!(
1062 parsed.message_id_value(),
1063 Some(MessageId::from_borrowed("<a@b>").unwrap())
1064 );
1065 assert_eq!(parsed.route_class(), RequestRouteClass::ArticleByMessageId);
1066 }
1067
1068 #[test]
1069 fn borrowed_request_line_requires_exact_message_id_argument() {
1070 let extra_token = RequestLine::parse(b"ARTICLE 123 <a@b>\r\n");
1071 let spaced = RequestLine::parse(b"ARTICLE <a@b> \r\n");
1072
1073 assert_eq!(extra_token.message_id(), None);
1074 assert_eq!(extra_token.route_class(), RequestRouteClass::Stateful);
1075
1076 assert_eq!(spaced.message_id(), Some("<a@b>"));
1077 assert_eq!(spaced.route_class(), RequestRouteClass::ArticleByMessageId);
1078 }
1079
1080 #[test]
1081 fn request_context_owns_borrowed_request_line() {
1082 let parsed = RequestLine::parse(b"BODY <a@b>\r\n");
1083 let ctx = RequestContext::from_request_line(parsed);
1084
1085 assert_eq!(ctx.kind(), RequestKind::Body);
1086 assert_eq!(ctx.verb(), b"BODY");
1087 assert_eq!(ctx.args(), b"<a@b>");
1088 assert_eq!(ctx.message_id(), Some("<a@b>"));
1089 assert_eq!(ctx.request_wire_len(), RequestWireLen::new(12));
1090 }
1091
1092 #[test]
1093 fn request_context_parses_wire_line_at_boundary() {
1094 let ctx = RequestContext::parse(b"BODY <a@b>\r\n").expect("valid request line");
1095
1096 assert_eq!(ctx.kind(), RequestKind::Body);
1097 assert_eq!(ctx.verb(), b"BODY");
1098 assert_eq!(ctx.args(), b"<a@b>");
1099 assert_eq!(ctx.message_id(), Some("<a@b>"));
1100 assert_eq!(ctx.request_wire_len(), RequestWireLen::new(12));
1101 }
1102
1103 #[test]
1104 fn request_context_parse_rejects_empty_request_lines() {
1105 assert!(RequestContext::parse(b"").is_none());
1106 assert!(RequestContext::parse(b"\r\n").is_none());
1107 assert!(RequestContext::parse(b" \r\n").is_none());
1108 }
1109
1110 #[test]
1111 fn request_context_parse_rejects_oversized_request_lines() {
1112 let mut line = b"ARTICLE <".to_vec();
1113 line.extend(std::iter::repeat_n(b'a', 496));
1114 line.extend_from_slice(b"@example.com>\r\n");
1115
1116 assert_eq!(line.len(), 520);
1117 assert!(RequestContext::parse(&line).is_none());
1118 }
1119
1120 #[tokio::test]
1121 async fn request_context_writes_wire_bytes_without_command_buffer() {
1122 let ctx = request_context(b"BODY <a@b>\r\n");
1123 let mut out = Vec::new();
1124
1125 ctx.write_wire_to(&mut out).await.unwrap();
1126
1127 assert_eq!(out, b"BODY <a@b>\r\n");
1128 }
1129
1130 #[test]
1131 fn typed_request_context_parses_request_bytes() {
1132 let ctx = request_context(b"XFOO \xff\r\n");
1133
1134 assert_eq!(ctx.kind(), RequestKind::Unknown);
1135 assert_eq!(ctx.verb(), b"XFOO");
1136 assert_eq!(ctx.args(), b"\xff");
1137 assert_eq!(ctx.route_class(), RequestRouteClass::Stateful);
1138 assert_eq!(wire(&ctx), b"XFOO \xff\r\n");
1139 }
1140
1141 #[test]
1142 fn request_context_records_backend_response_when_known() {
1143 let mut ctx = request_context(b"STAT <a@b>\r\n");
1144 let backend_id = BackendId::from_index(2);
1145 let status = StatusCode::new(223);
1146
1147 ctx.record_backend_response(
1148 backend_id,
1149 RequestResponseMetadata::new(status, ResponseWireLen::new(19)),
1150 );
1151
1152 assert_eq!(ctx.backend_id(), Some(backend_id));
1153 assert_eq!(
1154 ctx.response_metadata(),
1155 Some(RequestResponseMetadata::new(
1156 status,
1157 ResponseWireLen::new(19)
1158 ))
1159 );
1160 assert_eq!(ctx.response_status(), Some(status));
1161 assert_eq!(ctx.response_wire_len(), Some(ResponseWireLen::new(19)));
1162 assert_eq!(wire(&ctx), b"STAT <a@b>\r\n");
1163 }
1164
1165 #[test]
1166 fn request_context_compares_chunked_response_payload_without_flattening() {
1167 let mut ctx = request_context(b"STAT <a@b>\r\n");
1168 let backend_id = BackendId::from_index(2);
1169 let pool = crate::pool::BufferPool::for_tests();
1170 let mut response = crate::pool::ChunkedResponse::default();
1171 response.extend_from_slice(&pool, b"223 0 ");
1172 response.extend_from_slice(&pool, b"<a@b>\r\n");
1173
1174 ctx.complete_backend_response(backend_id, StatusCode::new(223), response);
1175
1176 assert_eq!(
1177 ctx.response_payload_len(),
1178 Some(ResponsePayloadLen::new(13))
1179 );
1180 assert_eq!(ctx.response_payload_eq(b"223 0 <a@b>\r\n"), Some(true));
1181 assert_eq!(ctx.response_payload_eq(b"223 0 <other>\r\n"), Some(false));
1182 assert_eq!(
1183 ctx.response_payload_eq(b"223 0 <a@b>\r\nextra"),
1184 Some(false)
1185 );
1186 }
1187
1188 #[test]
1189 fn request_context_records_cache_status_when_known() {
1190 let mut ctx = request_context(b"BODY <a@b>\r\n");
1191
1192 ctx.record_cache_status(RequestCacheStatus::PartialHit);
1193
1194 assert_eq!(ctx.cache_status(), Some(RequestCacheStatus::PartialHit));
1195 assert_eq!(wire(&ctx), b"BODY <a@b>\r\n");
1196 }
1197
1198 #[test]
1199 fn request_context_records_cache_entry_metadata_together() {
1200 let mut ctx = request_context(b"ARTICLE <a@b>\r\n");
1201 let status = StatusCode::new(430);
1202 let availability = RequestCacheAvailability::from_bits(0b0000_0010, 0b0000_0010);
1203 let tier = RequestCacheTier::new(2);
1204 let timestamp = RequestCacheTimestampMillis::new(123_456);
1205 let payload_kind = RequestCachePayloadKind::Missing;
1206 let article_number = Some(RequestCacheArticleNumber::new(42));
1207
1208 ctx.record_cache_entry_metadata(RequestCacheEntryMetadata::new(
1209 status,
1210 availability,
1211 tier,
1212 timestamp,
1213 payload_kind,
1214 article_number,
1215 ));
1216
1217 assert_eq!(
1218 ctx.cache_entry_metadata(),
1219 Some(RequestCacheEntryMetadata::new(
1220 status,
1221 availability,
1222 tier,
1223 timestamp,
1224 payload_kind,
1225 article_number
1226 ))
1227 );
1228 assert_eq!(ctx.cache_entry_status(), Some(status));
1229 assert_eq!(ctx.cache_availability(), Some(availability));
1230 assert_eq!(ctx.cache_entry_tier(), Some(tier));
1231 assert_eq!(ctx.cache_entry_timestamp(), Some(timestamp));
1232 assert_eq!(ctx.cache_payload_kind(), Some(payload_kind));
1233 assert_eq!(ctx.cache_article_number(), article_number);
1234 assert_eq!(ctx.response_status(), None);
1235 assert_eq!(wire(&ctx), b"ARTICLE <a@b>\r\n");
1236 }
1237
1238 #[test]
1239 fn request_context_detects_cached_backend_has_article() {
1240 let mut ctx = request_context(b"ARTICLE <a@b>\r\n");
1241
1242 ctx.record_cache_entry_metadata(RequestCacheEntryMetadata::new(
1243 StatusCode::new(220),
1244 RequestCacheAvailability::from_bits(0b0000_0110, 0b0000_0010),
1245 RequestCacheTier::new(0),
1246 RequestCacheTimestampMillis::new(1),
1247 RequestCachePayloadKind::AvailabilityOnly,
1248 None,
1249 ));
1250
1251 assert!(!ctx.cache_records_backend_has_article(BackendId::from_index(0)));
1252 assert!(!ctx.cache_records_backend_has_article(BackendId::from_index(1)));
1253 assert!(ctx.cache_records_backend_has_article(BackendId::from_index(2)));
1254 }
1255
1256 #[test]
1257 fn request_cache_availability_detects_backend_eight_bit() {
1258 let availability = RequestCacheAvailability::from_bits(0b1_0000_0000, 0);
1259
1260 assert!(availability.backend_has_article(BackendId::from_index(8)));
1261 assert!(!availability.backend_has_article(BackendId::from_index(7)));
1262 }
1263
1264 #[test]
1265 #[cfg(debug_assertions)]
1266 fn request_cache_availability_cannot_receive_out_of_range_backend() {
1267 assert!(BackendId::try_from_index(usize::BITS as usize).is_none());
1268 }
1269
1270 #[test]
1271 fn request_context_records_cache_response_when_served() {
1272 let mut ctx = request_context(b"HEAD <a@b>\r\n");
1273 let status = StatusCode::new(221);
1274
1275 ctx.record_cache_response(RequestResponseMetadata::new(
1276 status,
1277 ResponseWireLen::new(24),
1278 ));
1279
1280 assert_eq!(ctx.cache_status(), Some(RequestCacheStatus::Hit));
1281 assert_eq!(ctx.backend_id(), None);
1282 assert_eq!(ctx.response_status(), Some(status));
1283 assert_eq!(ctx.response_wire_len(), Some(ResponseWireLen::new(24)));
1284 assert_eq!(wire(&ctx), b"HEAD <a@b>\r\n");
1285 }
1286
1287 #[test]
1288 fn request_context_records_local_response_without_backend() {
1289 let mut ctx = request_context(b"QUIT\r\n");
1290 let status = StatusCode::new(205);
1291
1292 ctx.record_local_response(RequestResponseMetadata::new(
1293 status,
1294 ResponseWireLen::new(24),
1295 ));
1296
1297 assert_eq!(ctx.backend_id(), None);
1298 assert_eq!(ctx.response_status(), Some(status));
1299 assert_eq!(ctx.response_wire_len(), Some(ResponseWireLen::new(24)));
1300 assert_eq!(wire(&ctx), b"QUIT\r\n");
1301 }
1302
1303 #[test]
1304 fn unknown_extensions_are_stateful() {
1305 let ctx = request_context(b"XFOO arg\r\n");
1306 assert_eq!(ctx.kind(), RequestKind::Unknown);
1307 assert_eq!(ctx.route_class(), RequestRouteClass::Stateful);
1308 }
1309
1310 #[test]
1311 fn all_rfc_command_verbs_classify_to_request_kinds() {
1312 let cases = [
1313 ("ARTICLE <a@b>\r\n", RequestKind::Article),
1314 ("BODY <a@b>\r\n", RequestKind::Body),
1315 ("HEAD <a@b>\r\n", RequestKind::Head),
1316 ("STAT <a@b>\r\n", RequestKind::Stat),
1317 ("GROUP alt.test\r\n", RequestKind::Group),
1318 ("LISTGROUP alt.test\r\n", RequestKind::ListGroup),
1319 ("LAST\r\n", RequestKind::Last),
1320 ("NEXT\r\n", RequestKind::Next),
1321 ("LIST\r\n", RequestKind::List),
1322 ("DATE\r\n", RequestKind::Date),
1323 ("HELP\r\n", RequestKind::Help),
1324 ("CAPABILITIES\r\n", RequestKind::Capabilities),
1325 ("MODE READER\r\n", RequestKind::Mode),
1326 ("QUIT\r\n", RequestKind::Quit),
1327 ("OVER 1-10\r\n", RequestKind::Over),
1328 ("XOVER 1-10\r\n", RequestKind::Xover),
1329 ("HDR Subject 1-10\r\n", RequestKind::Hdr),
1330 ("XHDR Subject 1-10\r\n", RequestKind::Xhdr),
1331 ("NEWGROUPS 20260101 000000 GMT\r\n", RequestKind::NewGroups),
1332 ("NEWNEWS * 20260101 000000 GMT\r\n", RequestKind::NewNews),
1333 ("POST\r\n", RequestKind::Post),
1334 ("IHAVE <a@b>\r\n", RequestKind::Ihave),
1335 ("CHECK <a@b>\r\n", RequestKind::Check),
1336 ("TAKETHIS <a@b>\r\n", RequestKind::TakeThis),
1337 ("AUTHINFO USER test\r\n", RequestKind::AuthInfo),
1338 ("STARTTLS\r\n", RequestKind::StartTls),
1339 ];
1340
1341 for (line, expected) in cases {
1342 assert_eq!(
1343 RequestLine::parse(line.as_bytes()).kind(),
1344 expected,
1345 "{line}"
1346 );
1347 }
1348 }
1349
1350 #[test]
1351 fn request_route_classes_match_typed_command_behavior() {
1352 let cases = [
1353 ("ARTICLE <a@b>\r\n", RequestRouteClass::ArticleByMessageId),
1354 ("BODY 123\r\n", RequestRouteClass::Stateful),
1355 ("GROUP alt.test\r\n", RequestRouteClass::Stateful),
1356 ("LIST\r\n", RequestRouteClass::Stateless),
1357 ("MODE READER\r\n", RequestRouteClass::Stateless),
1358 ("QUIT\r\n", RequestRouteClass::Local),
1359 ("AUTHINFO PASS secret\r\n", RequestRouteClass::Local),
1360 ("POST\r\n", RequestRouteClass::Reject),
1361 ("IHAVE <a@b>\r\n", RequestRouteClass::Reject),
1362 ("CHECK <a@b>\r\n", RequestRouteClass::Reject),
1363 ("TAKETHIS <a@b>\r\n", RequestRouteClass::Reject),
1364 ("STARTTLS\r\n", RequestRouteClass::Reject),
1365 ("XFOO arg\r\n", RequestRouteClass::Stateful),
1366 ];
1367
1368 for (line, expected) in cases {
1369 assert_eq!(
1370 RequestLine::parse(line.as_bytes()).route_class(),
1371 expected,
1372 "{line}"
1373 );
1374 }
1375 }
1376
1377 #[test]
1378 fn request_context_derives_response_body_expectation() {
1379 let group = request_context(b"GROUP alt.test\r\n");
1380 let listgroup = request_context(b"LISTGROUP alt.test\r\n");
1381 let unknown = request_context(b"XFEATURE TEST\r\n");
1382 assert!(!group.has_response_body(StatusCode::new(211)));
1383 assert!(listgroup.has_response_body(StatusCode::new(211)));
1384 assert!(!request_context(b"ARTICLE <x@y>\r\n").has_response_body(StatusCode::new(430)));
1385 assert!(unknown.has_response_body(StatusCode::new(282)));
1386 assert!(unknown.has_response_body(StatusCode::new(288)));
1387 assert!(!unknown.has_response_body(StatusCode::new(281)));
1388 }
1389}