Skip to main content

icap_rs/
request.rs

1//! ICAP request types and helpers.
2//!
3//! This module defines:
4//! - [`Body`]: a generic HTTP body container used for embedded HTTP messages.
5//! - [`EmbeddedHttp`]: an enum for embedded HTTP messages (request/response) that
6//!   always carries `head` and `body` together.
7//! - [`Request<R, D>`]: a single ICAP request type parameterized by the body
8//!   carrier `R` and direction marker `D`.
9//! - [`OutboundRequest`]: the client-side request builder shape.
10//! - [`IncomingRequest`]: the server-side handler shape with read-only ICAP
11//!   metadata and mutable/consumable embedded HTTP access.
12//! ## Preview (server-side)
13//! By default, the server handles Preview (`Preview: N`) on the wire:
14//! - reads preview chunks first,
15//! - sends `ICAP/1.0 100 Continue` when preview is non-`ieof`,
16//! - then reads and de-chunks the remainder before invoking handlers.
17//!
18//! As a result, regular server handlers receive embedded bodies as `Body::Full`.
19//! Return `PreviewDecision` from a route handler when a service needs to make
20//! an early decision from preview bytes and return a final response before
21//! `100 Continue`.
22//! `Body::Preview` and `Body<BodyRead>::ensure_full()` remain available for custom
23//! integrations that build preview-aware pipelines explicitly.
24//!
25//! Request parsing requires `Encapsulated` on every ICAP request, including
26//! `OPTIONS`. Servers can opt into legacy compatibility parsing for old peers
27//! that omit `Encapsulated` on `OPTIONS`.
28//!
29//! ## Example (client: REQMOD with an embedded HTTP request)
30//! ```rust
31//! use http::Request as HttpRequest;
32//! use icap_rs::{Method, Request};
33//!
34//! let http_req = HttpRequest::builder()
35//!     .method("GET")
36//!     .uri("http://example.com/")
37//!     .header("Host", "example.com")
38//!     .body(Vec::new())
39//!     .unwrap();
40//!
41//! let icap_req: Request = Request::reqmod("icap/test")
42//!     .allow_204()
43//!     .preview(4)
44//!     .with_http_request(http_req)?;
45//!
46//! assert_eq!(icap_req.method(), Method::ReqMod);
47//! assert!(icap_req.allows_204());
48//! assert_eq!(icap_req.preview_size(), Some(4));
49//! # Ok::<(), icap_rs::Error>(())
50//! ```
51
52use crate::ICAP_VERSION;
53use crate::error::{Error, IcapResult};
54#[cfg(test)]
55use crate::error::{ProtocolError, ProtocolField};
56use crate::protocol::{
57    find_double_crlf, parse_header_lines, parse_http_request_start_line,
58    parse_http_response_start_line,
59};
60use http::{HeaderMap, HeaderName, HeaderValue, Request as HttpRequest, Response as HttpResponse};
61use memchr::memmem;
62use std::fmt;
63use std::future::Future;
64use std::str::FromStr;
65use tracing::trace;
66
67use std::io::Write as _;
68use std::pin::Pin;
69use tokio::io::AsyncRead;
70
71/// Single public ICAP request type used by both client and server.
72///
73/// The second type parameter is a direction marker:
74/// - [`Outbound`] is the default client-side builder shape.
75/// - [`Incoming`] is the server-side handler shape.
76///
77/// Server handlers receive [`IncomingRequest`], whose ICAP metadata is read-only
78/// through accessors. Services may inspect or modify the embedded HTTP message
79/// via [`Request::embedded_mut`] or consume it via [`Request::into_embedded`],
80/// but they cannot mutate the ICAP request line, ICAP headers, preview flags,
81/// or advertised `Allow` values through public API.
82///
83/// Server-only fields (`ISTag`, chunk trailers) are carried in
84/// [`DirectionMeta::Meta`] and are only accessible on [`IncomingRequest`].
85/// [`OutboundRequest`] pays zero overhead for those fields.
86#[derive(Debug)]
87#[must_use]
88pub struct Request<R = Vec<u8>, D: DirectionMeta = Outbound> {
89    /// ICAP method: `"OPTIONS" | "REQMOD" | "RESPMOD"`.
90    pub(crate) method: Method,
91    /// Full normalized service path (RFC 3507 §6.4), e.g. `"/v1/scan"`.
92    pub(crate) service: String,
93    /// ICAP headers (case-insensitive).
94    pub(crate) icap_headers: HeaderMap,
95    /// Optional embedded HTTP message (request/response).
96    pub(crate) embedded: Option<EmbeddedHttp<R>>,
97    /// `Preview: n` (if set).
98    pub(crate) preview_size: Option<usize>,
99    /// Whether `Allow: 204` should be advertised.
100    pub(crate) allow_204: bool,
101    /// Whether `Allow: 206` should be advertised.
102    pub(crate) allow_206: bool,
103    /// If `true` and `preview_size == Some(0)`, send `0; ieof` (fast 204 hint).
104    pub(crate) preview_ieof: bool,
105    /// Direction-specific metadata.
106    ///
107    /// [`OutboundMeta`] for client-side requests (zero-sized, zero overhead).
108    /// [`IncomingMeta`] for server-side requests (`ISTag` + chunk trailers).
109    pub(crate) meta: D::Meta,
110}
111
112/// ICAP request used by client send / build APIs.
113pub type OutboundRequest<R = Vec<u8>> = Request<R, Outbound>;
114
115/// ICAP request received by server route handlers.
116///
117/// Carries server-injected metadata (`ISTag`, chunk trailers) that is absent on
118/// the client-side [`OutboundRequest`]. Access it via [`Request::istag`] and
119/// [`Request::chunk_trailers`].
120pub type IncomingRequest<R = Vec<u8>> = Request<R, Incoming>;
121
122/// Client-side request marker.
123#[derive(Debug, Clone, Copy, Eq, PartialEq)]
124pub enum Outbound {}
125
126/// Server-side request marker.
127#[derive(Debug, Clone, Copy, Eq, PartialEq)]
128pub enum Incoming {}
129
130// ---------------------------------------------------------------------------
131// Direction metadata — sealed trait + per-direction metadata types
132// ---------------------------------------------------------------------------
133
134mod private {
135    pub trait Sealed {}
136    impl Sealed for super::Outbound {}
137    impl Sealed for super::Incoming {}
138}
139
140/// Associates each direction marker with the metadata type it carries inside
141/// [`Request<R, D>`].
142///
143/// This trait is **sealed**: only [`Outbound`] and [`Incoming`] implement it.
144/// Users cannot add new implementations.
145///
146/// The associated type [`DirectionMeta::Meta`] is an implementation detail and
147/// should not be used directly. Access server-injected metadata through the
148/// dedicated accessors on [`IncomingRequest`] instead:
149///
150/// - [`Request::istag`] — the `ISTag` resolved from `ServiceOptions` before
151///   the handler was called.
152/// - [`Request::chunk_trailers`] — HTTP chunk trailer headers (RFC 7230 §4.1.2).
153pub trait DirectionMeta: private::Sealed {
154    /// The metadata type stored inside `Request<R, D>` for this direction.
155    ///
156    /// `OutboundMeta` for [`Outbound`] (zero-sized, no overhead).
157    /// `IncomingMeta` for [`Incoming`] (server-injected fields).
158    #[doc(hidden)]
159    type Meta: Default + fmt::Debug + Clone;
160}
161
162/// Zero-sized metadata placeholder for [`Outbound`] requests.
163///
164/// Carries no data. The compiler optimises away any storage for this type.
165#[doc(hidden)]
166#[derive(Debug, Clone, Default)]
167pub struct OutboundMeta;
168
169/// Server-injected metadata available on [`IncomingRequest`].
170///
171/// Populated by the server connection loop before the route handler is called.
172/// Access it via [`Request::istag`] and [`Request::chunk_trailers`] rather than
173/// constructing or inspecting this type directly.
174#[doc(hidden)]
175#[derive(Debug, Clone, Default)]
176pub struct IncomingMeta {
177    pub(crate) istag: Option<String>,
178    pub(crate) chunk_trailers: HeaderMap,
179}
180
181impl DirectionMeta for Outbound {
182    type Meta = OutboundMeta;
183}
184
185impl DirectionMeta for Incoming {
186    type Meta = IncomingMeta;
187}
188
189/// ICAP protocol methods recognized by the server/router.
190///
191/// Defined by RFC 3507. In this crate:
192/// - `OPTIONS` is answered **automatically** by the server (capabilities discovery);
193///
194/// ### Methods
195/// - **REQMOD** — *Request modification*: the ICAP client (usually a proxy)
196///   sends an embedded HTTP **request** to be adapted.
197/// - **RESPMOD** — *Response modification*: the ICAP client sends an embedded
198///   HTTP **response** to be adapted.
199/// - **OPTIONS** — *Capability discovery*: clients learn which methods and
200///   features a service supports. **Handled automatically** by the server; do
201///   not register a handler for `OPTIONS`.
202///
203/// ### Conversions
204/// `Method` implements `From<&str>` / `From<String>` so you can pass
205/// strings in a builder-style API. Passing an unknown string will **panic**.
206#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
207pub enum Method {
208    /// Request modification (`REQMOD`).
209    ReqMod,
210    /// Response modification (`RESPMOD`).
211    RespMod,
212    /// Capability discovery (`OPTIONS`).
213    Options,
214}
215
216impl Method {
217    /// Returns the canonical ICAP token for this method.
218    ///
219    /// Always uppercase: `"REQMOD"`, `"RESPMOD"`, or `"OPTIONS"`.
220    #[inline]
221    pub const fn as_str(&self) -> &'static str {
222        match self {
223            Self::ReqMod => "REQMOD",
224            Self::RespMod => "RESPMOD",
225            Self::Options => "OPTIONS",
226        }
227    }
228
229    /// Parse an ICAP method token into a structured [`Method`].
230    ///
231    /// This is the non-panicking counterpart to `Method::from(&str)`.
232    pub fn parse_token(token: &str) -> IcapResult<Self> {
233        token.parse().map_err(|_| {
234            Error::invalid_method(format!("Unknown ICAP method string: {}", token.trim()))
235        })
236    }
237}
238
239impl fmt::Display for Method {
240    #[inline]
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        f.write_str(self.as_str())
243    }
244}
245
246impl FromStr for Method {
247    type Err = &'static str;
248
249    #[inline]
250    fn from_str(s: &str) -> Result<Self, Self::Err> {
251        let t = s.trim();
252        if t.eq_ignore_ascii_case("REQMOD") {
253            Ok(Self::ReqMod)
254        } else if t.eq_ignore_ascii_case("RESPMOD") {
255            Ok(Self::RespMod)
256        } else if t.eq_ignore_ascii_case("OPTIONS") {
257            Ok(Self::Options)
258        } else {
259            Err("Unknown ICAP method string")
260        }
261    }
262}
263
264impl From<&str> for Method {
265    #[inline]
266    fn from(s: &str) -> Self {
267        s.parse()
268            .unwrap_or_else(|_| panic!("Unknown ICAP method string: {s}"))
269    }
270}
271
272impl From<String> for Method {
273    #[inline]
274    fn from(s: String) -> Self {
275        s.as_str().into()
276    }
277}
278
279type BoxedUnitFut = Pin<Box<dyn Future<Output = IcapResult<()>> + Send>>;
280
281/// A one-shot handle to send `ICAP/1.0 100 Continue` when the handler decides
282/// to read past the preview boundary (server-side only).
283pub struct ContinueHandle {
284    send: Option<Box<dyn FnOnce() -> BoxedUnitFut + Send + Sync>>,
285}
286
287impl ContinueHandle {
288    pub fn new<F, Fut>(f: F) -> Self
289    where
290        F: FnOnce() -> Fut + Send + Sync + 'static,
291        Fut: Future<Output = IcapResult<()>> + Send + 'static,
292    {
293        Self {
294            send: Some(Box::new(move || Box::pin(f()))),
295        }
296    }
297
298    pub async fn send_100_continue(&mut self) -> IcapResult<()> {
299        if let Some(f) = self.send.take() {
300            f().await
301        } else {
302            Ok(())
303        }
304    }
305}
306
307/// The remainder of an HTTP body after the preview boundary.
308///
309/// `cont` is present only when `ieof=false` (i.e., more data exists and
310/// requires a `100 Continue` before the client sends it).
311pub struct Remainder<R> {
312    reader: R,
313    cont: Option<ContinueHandle>,
314}
315
316impl<R> Remainder<R> {
317    pub const fn new(reader: R, cont: Option<ContinueHandle>) -> Self {
318        Self { reader, cont }
319    }
320    pub async fn continue_if_needed(&mut self) -> IcapResult<()> {
321        if let Some(mut c) = self.cont.take() {
322            c.send_100_continue().await
323        } else {
324            Ok(())
325        }
326    }
327    pub fn take_reader(self) -> R {
328        self.reader
329    }
330}
331
332impl<R> fmt::Debug for Remainder<R> {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        f.debug_struct("Remainder")
335            .field("cont_present", &self.cont.is_some())
336            .finish_non_exhaustive()
337    }
338}
339
340/// Generic HTTP body used inside [`EmbeddedHttp`].
341///
342/// - `Empty` — no body (e.g., GET without payload, or OPTIONS).
343/// - `Preview` — the first `N` bytes are available in `bytes`, followed by the
344///   `remainder` stream. `ieof=true` indicates the whole body already fits into
345///   the preview and no `100 Continue` is needed.
346/// - `Full` — the complete body is available via `reader`.
347///
348/// Regular server routes normally receive `Full` bodies because the server owns
349/// the RFC Preview handshake. Preview-aware routes may see `Preview` before
350/// `100 Continue` is sent.
351pub enum Body<R> {
352    Empty,
353    Preview {
354        bytes: Vec<u8>,
355        ieof: bool,
356        remainder: Remainder<R>,
357    },
358    Full {
359        reader: R,
360    },
361}
362
363impl<R> fmt::Debug for Body<R> {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        match self {
366            Self::Empty => f.write_str("Body::Empty"),
367            Self::Full { .. } => f.write_str("Body::Full"),
368            Self::Preview {
369                bytes,
370                ieof,
371                remainder,
372            } => f
373                .debug_struct("Body::Preview")
374                .field("bytes_len", &bytes.len())
375                .field("ieof", ieof)
376                .field("remainder", remainder)
377                .finish(),
378        }
379    }
380}
381
382/// Trait-object body reader used by the server side for streaming.
383pub type BodyRead = Box<dyn AsyncRead + Unpin + Send>;
384
385/// An in-memory, non-blocking reader over bytes (used to feed preview bytes into `AsyncRead`).
386struct CursorReader<T>(std::io::Cursor<T>);
387
388impl<T: AsRef<[u8]> + Unpin> AsyncRead for CursorReader<T> {
389    fn poll_read(
390        mut self: Pin<&mut Self>,
391        _cx: &mut std::task::Context<'_>,
392        buf: &mut tokio::io::ReadBuf<'_>,
393    ) -> std::task::Poll<std::io::Result<()>> {
394        // Read directly into the ReadBuf's unfilled portion; invalid cursor positions
395        // are treated as EOF because this reader only advances within the backing slice.
396        let src = self.0.get_ref().as_ref();
397        let pos = usize::try_from(self.0.position()).map_or(src.len(), |pos| pos.min(src.len()));
398        let remaining = src.len().saturating_sub(pos);
399        if remaining > 0 {
400            let to_copy = remaining.min(buf.remaining());
401            buf.put_slice(&src[pos..pos + to_copy]);
402            self.0.set_position((pos + to_copy) as u64);
403        }
404        std::task::Poll::Ready(Ok(()))
405    }
406}
407impl Body<BodyRead> {
408    /// Ensure a full stream is available.
409    ///
410    /// If the body is currently `Preview { .. }` and `ieof=false`, this will
411    /// send `ICAP/1.0 100 Continue` **exactly once**, then convert the body into
412    /// `Full { reader }` where `reader` yields `preview-bytes` followed by the
413    /// remainder stream.
414    pub async fn ensure_full(&mut self) -> IcapResult<&mut (dyn AsyncRead + Unpin + Send)> {
415        struct Concat<A, B>(Option<A>, B);
416
417        impl<A: AsyncRead + Unpin, B: AsyncRead + Unpin> AsyncRead for Concat<A, B> {
418            fn poll_read(
419                mut self: Pin<&mut Self>,
420                cx: &mut std::task::Context<'_>,
421                buf: &mut tokio::io::ReadBuf<'_>,
422            ) -> std::task::Poll<std::io::Result<()>> {
423                if let Some(a) = self.0.as_mut() {
424                    let mut tmp = [0u8; 8192];
425                    let want = buf.remaining().min(tmp.len());
426                    let mut sub = tokio::io::ReadBuf::new(&mut tmp[..want]);
427
428                    match std::pin::Pin::new(a).poll_read(cx, &mut sub) {
429                        std::task::Poll::Pending => return std::task::Poll::Pending,
430                        std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
431                        std::task::Poll::Ready(Ok(())) => {
432                            let n = sub.filled().len();
433                            if n > 0 {
434                                buf.put_slice(sub.filled());
435                                return std::task::Poll::Ready(Ok(()));
436                            }
437                            self.0 = None;
438                        }
439                    }
440                }
441
442                std::pin::Pin::new(&mut self.1).poll_read(cx, buf)
443            }
444        }
445
446        match self {
447            Body::Empty => Err(Error::body("no body")),
448            Body::Full { reader } => Ok(reader.as_mut()),
449            Body::Preview {
450                bytes,
451                ieof,
452                remainder,
453            } => {
454                if !*ieof {
455                    remainder.continue_if_needed().await?;
456                }
457
458                let preview_reader: BodyRead =
459                    Box::new(CursorReader(std::io::Cursor::new(std::mem::take(bytes))));
460
461                let rem = std::mem::replace(
462                    remainder,
463                    Remainder::new(Box::new(tokio::io::empty()), None),
464                );
465                let tail = rem.take_reader();
466
467                let concat: BodyRead = Box::new(Concat(Some(preview_reader), tail));
468                *self = Body::Full { reader: concat };
469
470                match self {
471                    Body::Full { reader } => Ok(reader.as_mut()),
472                    _ => unreachable!(),
473                }
474            }
475        }
476    }
477}
478
479/// Embedded HTTP message inside an ICAP request.
480///
481/// Stores HTTP **head** and **body** together to avoid duplication.
482#[derive(Debug)]
483pub enum EmbeddedHttp<R> {
484    /// Embedded HTTP request (typical for `REQMOD`).
485    Req {
486        head: HttpRequest<()>,
487        body: Body<R>,
488    },
489    /// Embedded HTTP response (typical for `RESPMOD`).
490    ///
491    /// `req_head` carries the optional HTTP request context from the
492    /// `req-hdr` section of a RESPMOD request (RFC 3507 §4.4.1).
493    /// It is `Some` when the ICAP sender included the originating HTTP
494    /// request headers; `None` otherwise.
495    Resp {
496        /// Original HTTP request that triggered the response, when provided
497        /// by the ICAP client via `req-hdr` in the `Encapsulated` header.
498        req_head: Option<HttpRequest<()>>,
499        head: HttpResponse<()>,
500        body: Body<R>,
501    },
502}
503
504/// The type of embedded HTTP message carried by an ICAP request.
505#[derive(Debug, Clone, Copy, Eq, PartialEq)]
506#[must_use]
507pub enum EmbeddedHttpKind {
508    /// Embedded HTTP request (`req-hdr` / `req-body`).
509    Request,
510    /// Embedded HTTP response (`res-hdr` / `res-body`).
511    Response,
512}
513
514impl<R> EmbeddedHttp<R> {
515    /// Return whether this embedded message is an HTTP request or response.
516    pub const fn kind(&self) -> EmbeddedHttpKind {
517        match self {
518            Self::Req { .. } => EmbeddedHttpKind::Request,
519            Self::Resp { .. } => EmbeddedHttpKind::Response,
520        }
521    }
522
523    /// Return the optional HTTP request context from a RESPMOD embedded message.
524    ///
525    /// Present when the ICAP client included `req-hdr` in the `Encapsulated`
526    /// header of a RESPMOD request (RFC 3507 §4.4.1). Provides the HTTP request
527    /// that caused the response being modified.
528    ///
529    /// Always `None` for `Req` variants.
530    pub const fn respmod_request_head(&self) -> Option<&HttpRequest<()>> {
531        match self {
532            Self::Resp { req_head, .. } => req_head.as_ref(),
533            Self::Req { .. } => None,
534        }
535    }
536}
537
538/// Serialize embedded HTTP (client-side).
539///
540/// Returns `(http_head_bytes, body_bytes_up_to_limit, original_body_len)`.
541///
542/// `body_limit` caps how many body bytes are copied into the returned `Vec`.
543/// Pass `None` to copy the full body (required for [`Client::send`](crate::Client::send)
544/// so the remainder is available after `100 Continue`).
545/// Pass `Some(preview_size)` for dry-run helpers like [`Client::get_request`](crate::Client::get_request)
546/// where only the preview bytes need to appear in the wire buffer; the caller
547/// must use `original_body_len` to decide the correct chunk terminator
548/// (`ieof` vs `0\r\n\r\n`) regardless of how many bytes were actually copied.
549pub(crate) fn serialize_embedded_http(
550    e: &EmbeddedHttp<Vec<u8>>,
551    body_limit: Option<usize>,
552) -> (Vec<u8>, Option<Vec<u8>>, usize) {
553    #[inline]
554    fn copy_body(reader: &[u8], limit: Option<usize>) -> (Option<Vec<u8>>, usize) {
555        if reader.is_empty() {
556            return (None, 0);
557        }
558        let original_len = reader.len();
559        let end = limit.map_or(original_len, |lim| original_len.min(lim));
560        (Some(reader[..end].to_vec()), original_len)
561    }
562
563    match e {
564        EmbeddedHttp::Req { head, body } => {
565            let head_bytes = serialize_http_request_head(head);
566            let (body_bytes, original_len) = match body {
567                Body::Full { reader } => copy_body(reader, body_limit),
568                Body::Empty | Body::Preview { .. } => (None, 0),
569            };
570            (head_bytes, body_bytes, original_len)
571        }
572        EmbeddedHttp::Resp { head, body, .. } => {
573            let head_bytes = serialize_http_response_head(head);
574            let (body_bytes, original_len) = match body {
575                Body::Full { reader } => copy_body(reader, body_limit),
576                Body::Empty | Body::Preview { .. } => (None, 0),
577            };
578            (head_bytes, body_bytes, original_len)
579        }
580    }
581}
582
583fn serialize_http_request_head(head: &HttpRequest<()>) -> Vec<u8> {
584    let mut out = Vec::with_capacity(256 + head.headers().len() * 32);
585    write!(
586        &mut out,
587        "{} {} {}\r\n",
588        head.method(),
589        head.uri(),
590        crate::protocol::http_version_str(head.version())
591    )
592    .expect("write request line");
593    for (name, value) in head.headers() {
594        out.extend_from_slice(name.as_str().as_bytes());
595        out.extend_from_slice(b": ");
596        out.extend_from_slice(value.as_bytes());
597        out.extend_from_slice(b"\r\n");
598    }
599    out.extend_from_slice(b"\r\n");
600    out
601}
602
603fn serialize_http_response_head(head: &HttpResponse<()>) -> Vec<u8> {
604    let mut out = Vec::with_capacity(256 + head.headers().len() * 32);
605    write!(
606        &mut out,
607        "{} {} {}\r\n",
608        crate::protocol::http_version_str(head.version()),
609        head.status().as_u16(),
610        head.status().canonical_reason().unwrap_or("")
611    )
612    .expect("write status line");
613    for (name, value) in head.headers() {
614        out.extend_from_slice(name.as_str().as_bytes());
615        out.extend_from_slice(b": ");
616        out.extend_from_slice(value.as_bytes());
617        out.extend_from_slice(b"\r\n");
618    }
619    out.extend_from_slice(b"\r\n");
620    out
621}
622
623pub(crate) struct IncomingRequestParts<R> {
624    method: Method,
625    service: String,
626    icap_headers: HeaderMap,
627    embedded: Option<EmbeddedHttp<R>>,
628    preview_size: Option<usize>,
629    allow_204: bool,
630    allow_206: bool,
631    preview_ieof: bool,
632}
633
634impl<R, D: DirectionMeta> Request<R, D> {
635    /// Return the ICAP method.
636    #[inline]
637    pub const fn method(&self) -> Method {
638        self.method
639    }
640
641    /// Return the service path carried in the ICAP URI.
642    #[inline]
643    pub fn service(&self) -> &str {
644        &self.service
645    }
646
647    /// Return ICAP headers supplied on the request.
648    #[inline]
649    pub const fn icap_headers(&self) -> &HeaderMap {
650        &self.icap_headers
651    }
652
653    /// Return embedded HTTP, if this request carries one.
654    #[inline]
655    pub const fn embedded(&self) -> Option<&EmbeddedHttp<R>> {
656        self.embedded.as_ref()
657    }
658
659    /// Return mutable embedded HTTP, if this request carries one.
660    #[inline]
661    pub const fn embedded_mut(&mut self) -> Option<&mut EmbeddedHttp<R>> {
662        self.embedded.as_mut()
663    }
664
665    /// Consume the request and return the embedded HTTP message.
666    #[inline]
667    pub fn into_embedded(self) -> Option<EmbeddedHttp<R>> {
668        self.embedded
669    }
670
671    /// Return the requested preview size.
672    #[inline]
673    pub const fn preview_size(&self) -> Option<usize> {
674        self.preview_size
675    }
676
677    /// Return whether `Preview: 0` should be sent as `0; ieof`.
678    #[inline]
679    pub const fn is_preview_ieof(&self) -> bool {
680        self.preview_ieof
681    }
682
683    /// Return whether `Allow: 204` is advertised.
684    #[inline]
685    pub const fn allows_204(&self) -> bool {
686        self.allow_204
687    }
688
689    /// Return whether `Allow: 206` is advertised.
690    #[inline]
691    pub const fn allows_206(&self) -> bool {
692        self.allow_206
693    }
694}
695
696impl<R> Request<R, Outbound> {
697    /// Create a new outbound ICAP request.
698    pub fn new(method: Method, service: impl Into<String>) -> Self {
699        Self {
700            method,
701            service: service.into(),
702            icap_headers: HeaderMap::new(),
703            embedded: None,
704            preview_size: None,
705            allow_204: false,
706            allow_206: false,
707            preview_ieof: false,
708            meta: OutboundMeta,
709        }
710    }
711
712    /// Create a new outbound ICAP request from a method token without panicking.
713    pub fn try_new(method: &str, service: impl Into<String>) -> IcapResult<Self> {
714        Ok(Self::new(Method::parse_token(method)?, service))
715    }
716
717    /// Construct `OPTIONS` request.
718    pub fn options(service: impl Into<String>) -> Self {
719        Self::new(Method::Options, service)
720    }
721
722    /// Construct `REQMOD` request.
723    pub fn reqmod(service: impl Into<String>) -> Self {
724        Self::new(Method::ReqMod, service)
725    }
726
727    /// Construct `RESPMOD` request.
728    pub fn respmod(service: impl Into<String>) -> Self {
729        Self::new(Method::RespMod, service)
730    }
731
732    /// Try to set or override an ICAP header.
733    pub fn try_icap_header(mut self, name: &str, value: &str) -> IcapResult<Self> {
734        let n: HeaderName = name.parse()?;
735        let v: HeaderValue = HeaderValue::from_str(value)?;
736        self.icap_headers.insert(n, v);
737        Ok(self)
738    }
739
740    /// Set or override an ICAP header.
741    ///
742    /// # Panics
743    ///
744    /// Panics if `name` or `value` is not a valid HTTP header field. Use
745    /// [`Request::try_icap_header`] for untrusted input.
746    pub fn icap_header(self, name: &str, value: &str) -> Self {
747        self.try_icap_header(name, value)
748            .expect("invalid ICAP header name or value")
749    }
750
751    /// Advertise `Preview: n`.
752    ///
753    /// `Preview: 0` means the client sends an immediate zero-size preview chunk
754    /// and waits for either a final response or `100 Continue`.
755    pub const fn preview(mut self, n: usize) -> Self {
756        self.preview_size = Some(n);
757        self
758    }
759
760    /// Mark a `Preview: 0` request as complete using the `ieof` chunk extension.
761    pub const fn preview_ieof(mut self) -> Self {
762        self.preview_ieof = true;
763        self
764    }
765
766    /// Advertise `Allow: 204`.
767    pub const fn allow_204(mut self) -> Self {
768        self.allow_204 = true;
769        self
770    }
771
772    /// Advertise `Allow: 206`.
773    ///
774    /// The companion server can answer eligible no-modification flows with
775    /// `206 Partial Content` and `use-original-body`.
776    pub const fn allow_206(mut self) -> Self {
777        self.allow_206 = true;
778        self
779    }
780
781    /// True for `REQMOD`/`RESPMOD`.
782    #[inline]
783    pub const fn is_mod(&self) -> bool {
784        matches!(self.method, Method::ReqMod | Method::RespMod)
785    }
786
787    /// True for `OPTIONS`.
788    #[inline]
789    pub const fn is_options(&self) -> bool {
790        matches!(self.method, Method::Options)
791    }
792
793    /// Return the embedded HTTP message kind, if present.
794    #[inline]
795    pub const fn embedded_kind(&self) -> Option<EmbeddedHttpKind> {
796        match &self.embedded {
797            Some(embedded) => Some(embedded.kind()),
798            None => None,
799        }
800    }
801
802    /// Validate that this request can be serialized as a coherent ICAP request.
803    ///
804    /// This checks method-to-embedded-message compatibility but does not require
805    /// a buffered body, because streaming sends attach body bytes separately.
806    pub fn validate_for_send(&self) -> IcapResult<()> {
807        match (self.method, self.embedded_kind()) {
808            (Method::Options, Some(_)) => {
809                return Err(Error::serialization(
810                    "OPTIONS requests must not carry embedded HTTP",
811                ));
812            }
813            (Method::ReqMod, Some(EmbeddedHttpKind::Response)) => {
814                return Err(Error::serialization(
815                    "REQMOD requests must carry an embedded HTTP request",
816                ));
817            }
818            (Method::RespMod, Some(EmbeddedHttpKind::Request)) => {
819                return Err(Error::serialization(
820                    "RESPMOD requests must carry an embedded HTTP response",
821                ));
822            }
823            _ => {}
824        }
825
826        if self.preview_ieof && self.preview_size != Some(0) {
827            return Err(Error::serialization(
828                "preview_ieof is only valid together with Preview: 0",
829            ));
830        }
831
832        Ok(())
833    }
834}
835
836impl<R> Request<R, Incoming> {
837    #[allow(clippy::missing_const_for_fn)]
838    pub(crate) fn incoming(parts: IncomingRequestParts<R>) -> Self {
839        Self {
840            method: parts.method,
841            service: parts.service,
842            icap_headers: parts.icap_headers,
843            embedded: parts.embedded,
844            preview_size: parts.preview_size,
845            allow_204: parts.allow_204,
846            allow_206: parts.allow_206,
847            preview_ieof: parts.preview_ieof,
848            meta: IncomingMeta::default(),
849        }
850    }
851
852    /// Return chunk trailer headers that accompanied the embedded HTTP body.
853    ///
854    /// RFC 7230 §4.1.2 (applied by RFC 3507 §6.3) allows an ICAP sender to
855    /// append HTTP-style `Name: Value` trailer headers after the zero chunk.
856    /// This method returns those headers as parsed from the wire.
857    ///
858    /// The map is empty when no trailers were present.
859    ///
860    /// This accessor is only available on [`IncomingRequest`]; outbound client
861    /// requests never carry chunk trailers.
862    #[inline]
863    pub const fn chunk_trailers(&self) -> &HeaderMap {
864        &self.meta.chunk_trailers
865    }
866
867    /// Return the `ISTag` that the server resolved from
868    /// [`ServiceOptions`](crate::ServiceOptions) for this request, or `None` if
869    /// no `ServiceOptions` were configured.
870    ///
871    /// This is the same value that will appear in the ICAP response's `ISTag`
872    /// header when using
873    /// [`Response::no_content_with_istag`](crate::Response::no_content_with_istag)
874    /// or similar.
875    ///
876    /// This accessor is only available on [`IncomingRequest`]; outbound client
877    /// requests do not carry an `ISTag`.
878    #[inline]
879    pub fn istag(&self) -> Option<&str> {
880        self.meta.istag.as_deref()
881    }
882}
883
884/// Client-side convenience: attach embedded HTTP with **owned bytes**.
885impl Request<Vec<u8>, Outbound> {
886    /// Attach only HTTP request head (no buffered body bytes).
887    ///
888    /// Useful with streaming client APIs such as `Client::send_streaming_reader`.
889    pub fn with_http_request_head(mut self, head: HttpRequest<()>) -> IcapResult<Self> {
890        if self.method != Method::ReqMod {
891            return Err(Error::serialization(
892                "HTTP request heads can only be attached to REQMOD requests",
893            ));
894        }
895        self.embedded = Some(EmbeddedHttp::Req {
896            head,
897            body: Body::Empty,
898        });
899        Ok(self)
900    }
901
902    /// Attach only HTTP response head (no buffered body bytes).
903    ///
904    /// Useful with streaming client APIs such as `Client::send_streaming_reader`.
905    pub fn with_http_response_head(mut self, head: HttpResponse<()>) -> IcapResult<Self> {
906        if self.method != Method::RespMod {
907            return Err(Error::serialization(
908                "HTTP response heads can only be attached to RESPMOD requests",
909            ));
910        }
911        self.embedded = Some(EmbeddedHttp::Resp {
912            req_head: None,
913            head,
914            body: Body::Empty,
915        });
916        Ok(self)
917    }
918
919    /// Attach a complete embedded HTTP request.
920    ///
921    /// This is the usual client-side builder for `REQMOD` requests when the
922    /// HTTP entity body is already available in memory. For large bodies, use
923    /// [`Request::with_http_request_head`] together with a streaming client
924    pub fn with_http_request(mut self, req: HttpRequest<Vec<u8>>) -> IcapResult<Self> {
925        if self.method != Method::ReqMod {
926            return Err(Error::serialization(
927                "HTTP requests can only be attached to REQMOD requests",
928            ));
929        }
930        let (parts, body) = req.into_parts();
931        let head = HttpRequest::from_parts(parts, ());
932        self.embedded = Some(EmbeddedHttp::Req {
933            head,
934            body: Body::Full { reader: body },
935        });
936        Ok(self)
937    }
938
939    /// Attach a complete embedded HTTP response.
940    ///
941    /// This is the usual client-side builder for `RESPMOD` requests when the
942    /// HTTP entity body is already available in memory. For large bodies, use
943    /// [`Request::with_http_response_head`] together with a streaming client
944    pub fn with_http_response(mut self, resp: HttpResponse<Vec<u8>>) -> IcapResult<Self> {
945        if self.method != Method::RespMod {
946            return Err(Error::serialization(
947                "HTTP responses can only be attached to RESPMOD requests",
948            ));
949        }
950        let (parts, body) = resp.into_parts();
951        let head = HttpResponse::from_parts(parts, ());
952        self.embedded = Some(EmbeddedHttp::Resp {
953            req_head: None,
954            head,
955            body: Body::Full { reader: body },
956        });
957        Ok(self)
958    }
959
960    /// Attach a complete embedded HTTP response **with the original HTTP request context**.
961    ///
962    /// Equivalent to [`with_http_response`](Self::with_http_response) but also
963    /// includes the HTTP request that triggered the response (`req-hdr` in the
964    /// `Encapsulated` section, RFC 3507 §4.4.1). The ICAP server receives the
965    /// original request head via [`EmbeddedHttp::respmod_request_head`].
966    pub fn with_http_response_and_request_context(
967        mut self,
968        resp: HttpResponse<Vec<u8>>,
969        orig_req: HttpRequest<()>,
970    ) -> IcapResult<Self> {
971        if self.method != Method::RespMod {
972            return Err(Error::serialization(
973                "HTTP responses can only be attached to RESPMOD requests",
974            ));
975        }
976        let (parts, body) = resp.into_parts();
977        let head = HttpResponse::from_parts(parts, ());
978        self.embedded = Some(EmbeddedHttp::Resp {
979            req_head: Some(orig_req),
980            head,
981            body: Body::Full { reader: body },
982        });
983        Ok(self)
984    }
985}
986
987/// Normalize an ICAP service identifier to a canonical request path.
988///
989/// Accepts a full ICAP request-URI (`icap://host:port/v1/scan`), an absolute
990/// path (`/v1/scan`), or a bare service name (`scan`) and returns the canonical
991/// path used for routing: exactly one leading slash, no trailing slash (except
992/// for the root), and `*`/empty mapped to the root `/`.
993///
994/// This is the single source of truth for service-path form. It is applied when
995/// parsing an incoming request line, when a client builds a request-URI, and
996/// when the server registers and resolves routes, so all three agree on the
997/// same key.
998#[must_use]
999pub(crate) fn normalize_service_path(raw: &str) -> String {
1000    let s = raw.trim();
1001
1002    // `*` (server-wide OPTIONS) and the empty string map to the root.
1003    if s.is_empty() || s == "*" {
1004        return "/".to_string();
1005    }
1006
1007    // Reduce an absolute ICAP URI to its path component.
1008    let path = strip_icap_scheme(s).map_or(s, |authority_and_path| {
1009        authority_and_path
1010            .find('/')
1011            .map_or("/", |idx| &authority_and_path[idx..])
1012    });
1013
1014    let trimmed = path.trim_matches('/');
1015    if trimmed.is_empty() {
1016        "/".to_string()
1017    } else {
1018        format!("/{trimmed}")
1019    }
1020}
1021
1022/// Strip an `icap://` or `icaps://` scheme prefix (case-insensitive), returning
1023/// the remaining `authority[/path]` portion when a scheme is present.
1024fn strip_icap_scheme(s: &str) -> Option<&str> {
1025    for scheme in ["icaps://", "icap://"] {
1026        if let Some(prefix) = s.get(..scheme.len())
1027            && prefix.eq_ignore_ascii_case(scheme)
1028        {
1029            return Some(&s[scheme.len()..]);
1030        }
1031    }
1032    None
1033}
1034
1035/// Parse ICAP request from bytes
1036///
1037/// Note: this parser constructs `IncomingRequest<Vec<u8>>`, i.e. a fully buffered
1038/// embedded HTTP body when present.
1039pub(crate) fn parse_icap_request(data: &[u8]) -> IcapResult<IncomingRequest<Vec<u8>>> {
1040    parse_icap_request_with_mode(data, RequestParserMode::Strict)
1041}
1042
1043#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
1044pub(crate) enum RequestParserMode {
1045    Compatibility,
1046    #[default]
1047    Strict,
1048}
1049
1050pub(crate) fn parse_icap_request_with_mode(
1051    data: &[u8],
1052    mode: RequestParserMode,
1053) -> IcapResult<IncomingRequest<Vec<u8>>> {
1054    trace!("parse_icap_request: len={}", data.len());
1055
1056    let hdr_end =
1057        find_double_crlf(data).ok_or_else(|| Error::parse("ICAP request headers not complete"))?;
1058    let head = &data[..hdr_end];
1059    let head_str = std::str::from_utf8(head)?;
1060
1061    let mut lines = head_str.split("\r\n");
1062    let request_line = lines.next().ok_or_else(|| Error::parse("Empty request"))?;
1063    let mut parts = request_line.split_whitespace();
1064
1065    let method_str = parts
1066        .next()
1067        .ok_or_else(|| Error::parse("Invalid request line"))?;
1068    let method = match method_str.trim().to_ascii_uppercase().as_str() {
1069        "REQMOD" => Method::ReqMod,
1070        "RESPMOD" => Method::RespMod,
1071        "OPTIONS" => Method::Options,
1072        other => {
1073            return Err(Error::invalid_method(format!(
1074                "Unknown ICAP method: {other}"
1075            )));
1076        }
1077    };
1078
1079    let icap_uri = parts
1080        .next()
1081        .ok_or_else(|| Error::parse("Invalid request line"))?;
1082    let version = parts
1083        .next()
1084        .ok_or_else(|| Error::parse("Invalid request line"))?;
1085
1086    if !version.eq_ignore_ascii_case(ICAP_VERSION) {
1087        return Err(Error::invalid_version(version.to_string()));
1088    }
1089
1090    let icap_headers = parse_header_lines(lines)?;
1091
1092    if !icap_headers.contains_key("Host") {
1093        return Err(Error::missing_header("Host"));
1094    }
1095
1096    if (matches!(method, Method::ReqMod | Method::RespMod)
1097        || (mode == RequestParserMode::Strict && method == Method::Options))
1098        && !icap_headers.contains_key("Encapsulated")
1099    {
1100        return Err(Error::missing_header("Encapsulated"));
1101    }
1102
1103    let service = normalize_service_path(icap_uri);
1104
1105    let allow_204 = allow_contains_token(&icap_headers, "204");
1106    let allow_206 = allow_contains_token(&icap_headers, "206");
1107
1108    // RFC 3507 §4.5: `Preview` carries a non-negative integer count of body bytes
1109    // included in the preview. Malformed values are a protocol error.
1110    let preview_size = match icap_headers.get("Preview") {
1111        None => None,
1112        Some(v) => {
1113            let s = v
1114                .to_str()
1115                .map_err(|_| Error::parse("Preview header has non-ASCII value"))?;
1116            let trimmed = s.trim();
1117            let n = trimmed.parse::<usize>().map_err(|_| {
1118                Error::parse(format!("Preview header has invalid integer '{trimmed}'"))
1119            })?;
1120            Some(n)
1121        }
1122    };
1123
1124    // Encapsulated bytes start immediately after the ICAP header terminator.
1125    let enc_area = &data[hdr_end..];
1126
1127    let enc = match icap_headers.get("Encapsulated") {
1128        Some(v) => {
1129            let raw = v.to_str()?;
1130            crate::protocol::parse_encapsulated_value(raw)?
1131        }
1132        None => crate::protocol::Encapsulated::default(),
1133    };
1134    validate_encapsulated_for_method(method, &enc)?;
1135
1136    let http_hdr_off = match method {
1137        Method::ReqMod => enc.req_hdr,
1138        Method::RespMod => enc.res_hdr,
1139        Method::Options => None,
1140    };
1141
1142    let embedded = if let Some(hdr_off) = http_hdr_off {
1143        let hdr_end_off = next_offset_after(&enc, hdr_off);
1144        let http_region = slice_encapsulated(enc_area, hdr_off, hdr_end_off)?;
1145        if http_region.is_empty() {
1146            return Ok(IncomingRequest::incoming(IncomingRequestParts {
1147                method,
1148                service,
1149                icap_headers,
1150                embedded: None,
1151                preview_size,
1152                allow_204,
1153                allow_206,
1154                preview_ieof: false,
1155            }));
1156        }
1157
1158        let http_hdr_len = find_double_crlf(http_region)
1159            .ok_or_else(|| Error::http_parse("embedded HTTP headers not complete".to_string()))?;
1160        let http_head_bytes = &http_region[..http_hdr_len];
1161        let inline_body = &http_region[http_hdr_len..];
1162
1163        let first_line_end =
1164            memmem::find(http_head_bytes, b"\r\n").unwrap_or(http_head_bytes.len());
1165        let start_bytes = &http_head_bytes[..first_line_end];
1166        let start = std::str::from_utf8(start_bytes)?;
1167
1168        let http_head_str = std::str::from_utf8(http_head_bytes)?;
1169        let mut hlines = http_head_str.split("\r\n");
1170        let _ = hlines.next();
1171
1172        let http_headers = parse_header_lines(hlines)?;
1173
1174        let body_off = match method {
1175            Method::ReqMod => enc.req_body,
1176            Method::RespMod => enc.res_body,
1177            Method::Options => None,
1178        };
1179
1180        let no_body = body_off.is_none() && enc.null_body.is_some();
1181
1182        let body_bytes: Vec<u8> = if no_body {
1183            Vec::new()
1184        } else if let Some(boff) = body_off {
1185            // The body boundary is the next Encapsulated offset or the end of the area.
1186            let bend = next_offset_after(&enc, boff);
1187            let body_slice = slice_encapsulated(enc_area, boff, bend)?;
1188
1189            if boff < hdr_off {
1190                return Err(Error::header(
1191                    "Encapsulated offsets invalid (body before headers)",
1192                ));
1193            }
1194
1195            body_slice.to_vec()
1196        } else {
1197            inline_body.to_vec()
1198        };
1199
1200        if method == Method::RespMod {
1201            let (version, status) = parse_http_response_start_line(start)?;
1202
1203            let mut builder = HttpResponse::builder().status(status).version(version);
1204
1205            {
1206                let headers_mut = builder
1207                    .headers_mut()
1208                    .ok_or_else(|| Error::unexpected("response builder: headers_mut is None"))?;
1209                headers_mut.extend(http_headers);
1210            }
1211
1212            let head = builder
1213                .body(())
1214                .map_err(|e| Error::http_parse(format!("build http::Response head: {e}")))?;
1215
1216            // RFC 3507 §4.4.1: RESPMOD request may carry optional req-hdr
1217            // (the HTTP request that caused this response). Parse it when present.
1218            let req_head = if let Some(req_hdr_off) = enc.req_hdr {
1219                let req_end = next_offset_after(&enc, req_hdr_off);
1220                let req_region = slice_encapsulated(enc_area, req_hdr_off, req_end)?;
1221                parse_http_request_head_only(req_region)?
1222            } else {
1223                None
1224            };
1225
1226            Some(EmbeddedHttp::Resp {
1227                req_head,
1228                head,
1229                body: Body::Full { reader: body_bytes },
1230            })
1231        } else {
1232            let (http_method, uri, version) = parse_http_request_start_line(start)?;
1233
1234            let mut builder = HttpRequest::builder()
1235                .method(http_method)
1236                .uri(uri)
1237                .version(version);
1238
1239            {
1240                let headers_mut = builder
1241                    .headers_mut()
1242                    .ok_or_else(|| Error::unexpected("request builder: headers_mut is None"))?;
1243                headers_mut.extend(http_headers);
1244            }
1245
1246            let head = builder
1247                .body(())
1248                .map_err(|e| Error::http_parse(format!("build http::Request head: {e}")))?;
1249
1250            Some(EmbeddedHttp::Req {
1251                head,
1252                body: Body::Full { reader: body_bytes },
1253            })
1254        }
1255    } else {
1256        None
1257    };
1258
1259    Ok(IncomingRequest::incoming(IncomingRequestParts {
1260        method,
1261        service,
1262        icap_headers,
1263        embedded,
1264        preview_size,
1265        allow_204,
1266        allow_206,
1267        preview_ieof: false,
1268    }))
1269}
1270
1271/// Parse a region of bytes as an HTTP request head only (no body).
1272///
1273/// Used for the optional `req-hdr` section in RESPMOD requests (RFC 3507 §4.4.1).
1274/// Returns `None` when `region` is empty (the ICAP sender omitted the section).
1275fn parse_http_request_head_only(region: &[u8]) -> IcapResult<Option<HttpRequest<()>>> {
1276    if region.is_empty() {
1277        return Ok(None);
1278    }
1279
1280    let hdr_len = find_double_crlf(region)
1281        .ok_or_else(|| Error::http_parse("req-hdr section: HTTP request headers not complete"))?;
1282    let head_bytes = &region[..hdr_len];
1283
1284    let first_line_end = memmem::find(head_bytes, b"\r\n").unwrap_or(head_bytes.len());
1285    let start_str = std::str::from_utf8(&head_bytes[..first_line_end])?;
1286
1287    let head_str = std::str::from_utf8(head_bytes)?;
1288    let http_headers = parse_header_lines(head_str.split("\r\n").skip(1))?;
1289
1290    let (http_method, uri, version) = parse_http_request_start_line(start_str)?;
1291    let mut builder = HttpRequest::builder()
1292        .method(http_method)
1293        .uri(uri)
1294        .version(version);
1295    if let Some(h) = builder.headers_mut() {
1296        h.extend(http_headers);
1297    }
1298    let head = builder
1299        .body(())
1300        .map_err(|e| Error::http_parse(format!("build req-hdr HTTP request head: {e}")))?;
1301    Ok(Some(head))
1302}
1303
1304fn validate_encapsulated_for_method(
1305    method: Method,
1306    enc: &crate::protocol::Encapsulated,
1307) -> IcapResult<()> {
1308    if enc.null_body.is_some()
1309        && (enc.req_body.is_some() || enc.res_body.is_some() || enc.opt_body.is_some())
1310    {
1311        return Err(Error::header(
1312            "Encapsulated null-body must not be combined with body tokens",
1313        ));
1314    }
1315
1316    match method {
1317        Method::ReqMod => {
1318            if enc.res_hdr.is_some() || enc.res_body.is_some() || enc.opt_body.is_some() {
1319                return Err(Error::header(
1320                    "REQMOD Encapsulated must not contain response or opt-body parts",
1321                ));
1322            }
1323            if enc.req_body.is_some() && enc.req_hdr.is_none() {
1324                return Err(Error::header(
1325                    "REQMOD Encapsulated req-body requires req-hdr",
1326                ));
1327            }
1328        }
1329        Method::RespMod => {
1330            if enc.req_body.is_some() || enc.opt_body.is_some() {
1331                return Err(Error::header(
1332                    "RESPMOD Encapsulated must not contain req-body or opt-body parts",
1333                ));
1334            }
1335            if enc.res_body.is_some() && enc.res_hdr.is_none() {
1336                return Err(Error::header(
1337                    "RESPMOD Encapsulated res-body requires res-hdr",
1338                ));
1339            }
1340        }
1341        Method::Options => {
1342            if enc.req_hdr.is_some()
1343                || enc.res_hdr.is_some()
1344                || enc.req_body.is_some()
1345                || enc.res_body.is_some()
1346                || enc.opt_body.is_some()
1347            {
1348                return Err(Error::header(
1349                    "OPTIONS request Encapsulated must be absent or null-body",
1350                ));
1351            }
1352        }
1353    }
1354
1355    Ok(())
1356}
1357
1358fn next_offset_after(enc: &crate::protocol::Encapsulated, start: usize) -> Option<usize> {
1359    let mut min: Option<usize> = None;
1360
1361    let mut consider = |v: Option<usize>| {
1362        if let Some(o) = v
1363            && o > start
1364        {
1365            min = Some(min.map_or(o, |m| m.min(o)));
1366        }
1367    };
1368
1369    consider(enc.req_hdr);
1370    consider(enc.res_hdr);
1371    consider(enc.req_body);
1372    consider(enc.res_body);
1373    consider(enc.opt_body);
1374    consider(enc.null_body);
1375
1376    min
1377}
1378
1379fn slice_encapsulated(enc_area: &[u8], start: usize, end: Option<usize>) -> IcapResult<&[u8]> {
1380    if start > enc_area.len() {
1381        return Err(Error::header("Encapsulated offset out of bounds"));
1382    }
1383    let end = end.unwrap_or(enc_area.len());
1384    if end > enc_area.len() {
1385        return Err(Error::header("Encapsulated end offset out of bounds"));
1386    }
1387    if end < start {
1388        return Err(Error::header("Encapsulated offsets invalid (end < start)"));
1389    }
1390    Ok(&enc_area[start..end])
1391}
1392
1393#[inline]
1394fn allow_contains_token(headers: &HeaderMap, token: &str) -> bool {
1395    headers
1396        .get("Allow")
1397        .and_then(|v| v.to_str().ok())
1398        .is_some_and(|s| s.split(',').any(|p| p.trim().eq_ignore_ascii_case(token)))
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403    use super::*;
1404    use http::{
1405        HeaderValue, Method as HttpMethod, Request as HttpRequest, Response as HttpResponse,
1406        StatusCode as HttpStatus, Version,
1407    };
1408    use rstest::rstest;
1409
1410    #[inline]
1411    fn icap_bytes(s: &str) -> Vec<u8> {
1412        s.as_bytes().to_vec()
1413    }
1414
1415    #[rstest]
1416    #[case("reqmod", Method::ReqMod)]
1417    #[case("RESPMOD", Method::RespMod)]
1418    #[case("  Options  ", Method::Options)]
1419    fn method_from_str_is_case_insensitive(#[case] s: &str, #[case] expected: Method) {
1420        assert_eq!(Method::from(s), expected);
1421    }
1422
1423    #[test]
1424    #[should_panic(expected = "Unknown ICAP method string")]
1425    fn method_from_str_unknown_panics() {
1426        let _ = Method::from("PATCH");
1427    }
1428
1429    #[test]
1430    fn builder_creates_basic_requests() {
1431        let o: Request = Request::options("icap/test");
1432        assert_eq!(o.method, Method::Options);
1433        assert_eq!(o.service, "icap/test");
1434        assert!(!o.is_mod());
1435
1436        let r: Request = Request::reqmod("svc");
1437        assert_eq!(r.method, Method::ReqMod);
1438        assert!(r.is_mod());
1439
1440        let s: Request = Request::respmod("svc");
1441        assert_eq!(s.method, Method::RespMod);
1442        assert!(s.is_mod());
1443    }
1444
1445    #[test]
1446    fn builder_flags_preview_allow() {
1447        let req: Request = Request::reqmod("icap/test")
1448            .allow_204()
1449            .allow_206()
1450            .preview(16)
1451            .preview_ieof();
1452
1453        assert!(req.allow_204);
1454        assert!(req.allow_206);
1455        assert_eq!(req.preview_size, Some(16));
1456        assert!(req.preview_ieof);
1457    }
1458
1459    #[test]
1460    fn preview_zero_does_not_imply_ieof() {
1461        let req: Request = Request::reqmod("svc").preview(0);
1462        assert_eq!(req.preview_size, Some(0));
1463        assert!(!req.preview_ieof);
1464
1465        let req: Request = Request::reqmod("svc").preview(0).preview_ieof();
1466        assert_eq!(req.preview_size, Some(0));
1467        assert!(req.preview_ieof);
1468    }
1469
1470    #[test]
1471    fn builder_sets_and_overrides_headers() {
1472        let req: Request = Request::options("icap/test")
1473            .icap_header("Host", "icap.example.org")
1474            .icap_header("Host", "icap2.example.org");
1475
1476        assert_eq!(
1477            req.icap_headers.get("Host").unwrap(),
1478            &HeaderValue::from_static("icap2.example.org")
1479        );
1480    }
1481
1482    #[test]
1483    fn try_icap_header_rejects_invalid_header_input() {
1484        let err = Request::<Vec<u8>>::options("icap/test")
1485            .try_icap_header("Bad Header", "value")
1486            .expect_err("invalid header name should be rejected");
1487
1488        assert!(matches!(err, Error::Protocol(ProtocolError::HeaderName(_))));
1489    }
1490
1491    #[test]
1492    fn try_new_rejects_unknown_method_without_panic() {
1493        let err = Request::<Vec<u8>>::try_new("PATCH", "svc")
1494            .expect_err("unknown method should be rejected");
1495
1496        assert!(matches!(
1497            err,
1498            Error::Protocol(ProtocolError::InvalidField {
1499                field: ProtocolField::Method,
1500                ..
1501            })
1502        ));
1503    }
1504
1505    #[test]
1506    fn parse_reqmod_body_uses_req_body_offset() {
1507        let http = b"GET / HTTP/1.1\r\nHost: ex\r\n\r\n";
1508        let body = b"12345678";
1509
1510        let req_body_off = http.len(); // body starts immediately after HTTP headers
1511
1512        let raw = format!(
1513            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1514Host: icap.example.org\r\n\
1515Encapsulated: req-hdr=0, req-body={req_body_off}\r\n\
1516\r\n",
1517        )
1518        .into_bytes();
1519
1520        let mut bytes = raw;
1521        bytes.extend_from_slice(http);
1522        bytes.extend_from_slice(body);
1523
1524        let r = parse_icap_request(&bytes).expect("parse");
1525        match r.embedded {
1526            Some(EmbeddedHttp::Req {
1527                body: Body::Full { reader },
1528                ..
1529            }) => {
1530                assert_eq!(reader, body);
1531            }
1532            _ => panic!("expected embedded req with full body"),
1533        }
1534    }
1535
1536    #[test]
1537    fn builder_embeds_http_req_and_resp() {
1538        let http_req = HttpRequest::builder()
1539            .method("POST")
1540            .uri("/x")
1541            .version(Version::HTTP_11)
1542            .body(Vec::<u8>::new())
1543            .unwrap();
1544
1545        let req: Request = Request::reqmod("svc").with_http_request(http_req).unwrap();
1546        assert!(matches!(req.embedded, Some(EmbeddedHttp::Req { .. })));
1547
1548        let http_resp = HttpResponse::builder()
1549            .status(HttpStatus::OK)
1550            .version(Version::HTTP_11)
1551            .body(Vec::<u8>::new())
1552            .unwrap();
1553
1554        let req2: Request = Request::respmod("svc")
1555            .with_http_response(http_resp)
1556            .unwrap();
1557        assert!(matches!(req2.embedded, Some(EmbeddedHttp::Resp { .. })));
1558    }
1559
1560    #[test]
1561    fn builder_embeds_http_heads_without_buffered_body() {
1562        let req_head = HttpRequest::builder()
1563            .method("POST")
1564            .uri("/x")
1565            .version(Version::HTTP_11)
1566            .body(())
1567            .unwrap();
1568        let req: Request = Request::reqmod("svc")
1569            .with_http_request_head(req_head)
1570            .unwrap();
1571        assert!(matches!(
1572            req.embedded,
1573            Some(EmbeddedHttp::Req {
1574                body: Body::Empty,
1575                ..
1576            })
1577        ));
1578
1579        let resp_head = HttpResponse::builder()
1580            .status(HttpStatus::OK)
1581            .version(Version::HTTP_11)
1582            .body(())
1583            .unwrap();
1584        let req2: Request = Request::respmod("svc")
1585            .with_http_response_head(resp_head)
1586            .unwrap();
1587        assert!(matches!(
1588            req2.embedded,
1589            Some(EmbeddedHttp::Resp {
1590                body: Body::Empty,
1591                ..
1592            })
1593        ));
1594    }
1595
1596    #[test]
1597    fn validated_embedded_builders_reject_method_mismatch() {
1598        let http_req = HttpRequest::builder()
1599            .method("GET")
1600            .uri("/x")
1601            .version(Version::HTTP_11)
1602            .body(Vec::<u8>::new())
1603            .unwrap();
1604
1605        let err = Request::respmod("svc")
1606            .with_http_request(http_req)
1607            .expect_err("RESPMOD must reject embedded HTTP requests");
1608
1609        assert!(matches!(
1610            err,
1611            Error::Protocol(ProtocolError::Serialization(_))
1612        ));
1613
1614        let http_resp = HttpResponse::builder()
1615            .status(HttpStatus::OK)
1616            .version(Version::HTTP_11)
1617            .body(Vec::<u8>::new())
1618            .unwrap();
1619
1620        let err = Request::reqmod("svc")
1621            .with_http_response(http_resp)
1622            .expect_err("REQMOD must reject embedded HTTP responses");
1623
1624        assert!(matches!(
1625            err,
1626            Error::Protocol(ProtocolError::Serialization(_))
1627        ));
1628    }
1629
1630    #[test]
1631    fn validate_for_send_rejects_incoherent_request_shapes() {
1632        let req: Request = Request::reqmod("svc").preview(16).preview_ieof();
1633        let err = req
1634            .validate_for_send()
1635            .expect_err("ieof is only valid with Preview: 0");
1636        assert!(matches!(
1637            err,
1638            Error::Protocol(ProtocolError::Serialization(_))
1639        ));
1640    }
1641
1642    // ---------- Version & framing ----------
1643
1644    #[test]
1645    fn version_must_be_icap_1_0_in_request() {
1646        let raw = b"REQMOD icap://h/s ICAP/2.0\r\n\
1647                    Host: h\r\n\
1648                    Encapsulated: req-hdr=0\r\n\
1649                    \r\n";
1650        let err = parse_icap_request(raw).unwrap_err();
1651        assert!(
1652            matches!(err, Error::Protocol(ProtocolError::InvalidField { field: ProtocolField::Version, value: v, .. }) if v == "ICAP/2.0")
1653        );
1654    }
1655
1656    // RFC 3507 §6.4 — the service is identified by the full request-URI path,
1657    // not by the final path segment.
1658    #[rstest]
1659    #[case(
1660        "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: req-hdr=0\r\n\r\n",
1661        "/icap/test"
1662    )]
1663    #[case(
1664        "RESPMOD icap://icap.example.org/respmod ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: res-hdr=0\r\n\r\n",
1665        "/respmod"
1666    )]
1667    #[case(
1668        "REQMOD icap://icap.example.org/v1/scan/ ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: req-hdr=0\r\n\r\n",
1669        "/v1/scan"
1670    )]
1671    fn service_is_full_path_of_icap_uri(#[case] wire: &str, #[case] expected_service: &str) {
1672        let r = parse_icap_request(&icap_bytes(wire)).expect("parse");
1673        assert_eq!(r.service, expected_service);
1674    }
1675
1676    // RFC 3507 §6.4 — canonical service-path normalization.
1677    #[rstest]
1678    #[case("scan", "/scan")]
1679    #[case("/scan", "/scan")]
1680    #[case("/v1/scan", "/v1/scan")]
1681    #[case("v1/scan", "/v1/scan")]
1682    #[case("/v1/scan/", "/v1/scan")]
1683    #[case("icap://host:1344/v1/scan", "/v1/scan")]
1684    #[case("ICAP://host:1344/v1/scan", "/v1/scan")]
1685    #[case("icaps://host:1344/secure/scan", "/secure/scan")]
1686    #[case("icap://host:1344/", "/")]
1687    #[case("icap://host:1344", "/")]
1688    #[case("*", "/")]
1689    #[case("", "/")]
1690    #[case("/", "/")]
1691    fn normalize_service_path_cases(#[case] raw: &str, #[case] expected: &str) {
1692        assert_eq!(normalize_service_path(raw), expected);
1693    }
1694
1695    #[test]
1696    fn headers_are_case_insensitive_allow_parsed_with_whitespace() {
1697        let raw = icap_bytes(
1698            "REQMOD icap://h/s ICAP/1.0\r\n\
1699             host: icap.example.org\r\n\
1700             aLlOw: 206, 204 \r\n\
1701             Encapsulated: req-hdr=0\r\n\
1702             \r\n",
1703        );
1704        let r = parse_icap_request(&raw).expect("parse");
1705        assert!(r.allow_204);
1706        assert!(r.allow_206);
1707        assert_eq!(
1708            r.icap_headers.get("Host").unwrap(),
1709            &HeaderValue::from_static("icap.example.org")
1710        );
1711    }
1712
1713    #[test]
1714    fn parse_ignores_malformed_header_line_without_colon() {
1715        let raw = icap_bytes(
1716            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1717             Host: icap.example.org\r\n\
1718             ThisIsBadHeader\r\n\
1719             Encapsulated: null-body=0\r\n\
1720             \r\n",
1721        );
1722        let r = parse_icap_request(&raw).expect("parse");
1723        assert_eq!(
1724            r.icap_headers.get("Host").unwrap(),
1725            &HeaderValue::from_static("icap.example.org")
1726        );
1727    }
1728
1729    #[test]
1730    fn host_header_is_required() {
1731        let raw = b"REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1732                    Encapsulated: req-hdr=0\r\n\
1733                    \r\n";
1734        let err = parse_icap_request(raw).unwrap_err();
1735        let m = err.to_string().to_lowercase();
1736        assert!(m.contains("host"), "expected missing Host error; got: {m}");
1737    }
1738
1739    #[test]
1740    fn encapsulated_is_required_for_request() {
1741        let raw_req = b"REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1742                        Host: icap.example.org\r\n\
1743                        \r\n";
1744        let err1 = parse_icap_request(raw_req).unwrap_err();
1745        assert!(
1746            matches!(err1, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1747            "expected MissingHeader(Encapsulated); got: {err1}"
1748        );
1749
1750        let raw_resp = b"RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1751                         Host: icap.example.org\r\n\
1752                         \r\n";
1753        let err2 = parse_icap_request(raw_resp).unwrap_err();
1754        assert!(
1755            matches!(err2, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1756            "expected MissingHeader(Encapsulated); got: {err2}"
1757        );
1758    }
1759
1760    #[test]
1761    fn invalid_encapsulated_token_is_rejected() {
1762        let raw = icap_bytes(
1763            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1764             Host: icap.example.org\r\n\
1765             Encapsulated: req-hdr=0, bad-token=10\r\n\
1766             \r\n",
1767        );
1768        let err = parse_icap_request(&raw).unwrap_err();
1769        let m = err.to_string().to_lowercase();
1770        assert!(
1771            m.contains("encapsulated") || m.contains("invalid"),
1772            "expected Encapsulated parse error, got: {m}"
1773        );
1774    }
1775
1776    #[test]
1777    fn duplicate_encapsulated_part_is_rejected() {
1778        let raw = icap_bytes(
1779            "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1780             Host: icap.example.org\r\n\
1781             Encapsulated: res-hdr=0, res-hdr=10\r\n\
1782             \r\n",
1783        );
1784        let err = parse_icap_request(&raw).unwrap_err();
1785        let m = err.to_string().to_lowercase();
1786        assert!(
1787            m.contains("duplicate") && m.contains("encapsulated"),
1788            "expected duplicate Encapsulated part error, got: {m}"
1789        );
1790    }
1791
1792    #[test]
1793    fn reqmod_rejects_response_oriented_encapsulated_parts() {
1794        let raw = icap_bytes(
1795            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1796             Host: icap.example.org\r\n\
1797             Encapsulated: res-hdr=0\r\n\
1798             \r\n\
1799             HTTP/1.1 200 OK\r\n\
1800             Content-Length: 0\r\n\
1801             \r\n",
1802        );
1803        let err = parse_icap_request(&raw).unwrap_err();
1804        let m = err.to_string().to_lowercase();
1805        assert!(
1806            m.contains("reqmod") && m.contains("encapsulated"),
1807            "expected REQMOD Encapsulated validation error, got: {m}"
1808        );
1809    }
1810
1811    #[test]
1812    fn respmod_rejects_request_body_encapsulated_part() {
1813        let raw = icap_bytes(
1814            "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1815             Host: icap.example.org\r\n\
1816             Encapsulated: req-hdr=0, req-body=30\r\n\
1817             \r\n\
1818             GET / HTTP/1.1\r\n\
1819             Host: example.com\r\n\
1820             \r\n",
1821        );
1822        let err = parse_icap_request(&raw).unwrap_err();
1823        let m = err.to_string().to_lowercase();
1824        assert!(
1825            m.contains("respmod") && m.contains("encapsulated"),
1826            "expected RESPMOD Encapsulated validation error, got: {m}"
1827        );
1828    }
1829
1830    #[test]
1831    fn null_body_must_not_be_combined_with_body_tokens() {
1832        let raw = icap_bytes(
1833            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1834             Host: icap.example.org\r\n\
1835             Encapsulated: req-hdr=0, req-body=30, null-body=34\r\n\
1836             \r\n\
1837             GET / HTTP/1.1\r\n\
1838             Host: example.com\r\n\
1839             \r\n\
1840             body",
1841        );
1842        let err = parse_icap_request(&raw).unwrap_err();
1843        let m = err.to_string().to_lowercase();
1844        assert!(
1845            m.contains("null-body") && m.contains("body"),
1846            "expected null-body/body validation error, got: {m}"
1847        );
1848    }
1849
1850    #[test]
1851    fn options_request_rejects_embedded_http_parts() {
1852        let raw = icap_bytes(
1853            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1854             Host: icap.example.org\r\n\
1855             Encapsulated: req-hdr=0\r\n\
1856             \r\n",
1857        );
1858        let err = parse_icap_request(&raw).unwrap_err();
1859        let m = err.to_string().to_lowercase();
1860        assert!(
1861            m.contains("options") && m.contains("encapsulated"),
1862            "expected OPTIONS Encapsulated validation error, got: {m}"
1863        );
1864    }
1865
1866    #[test]
1867    fn compatibility_mode_accepts_options_without_encapsulated() {
1868        let raw = icap_bytes(
1869            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1870             Host: icap.example.org\r\n\
1871             \r\n",
1872        );
1873        let r = parse_icap_request_with_mode(&raw, RequestParserMode::Compatibility)
1874            .expect("compatibility mode keeps OPTIONS lenient");
1875        assert_eq!(r.method, Method::Options);
1876    }
1877
1878    #[test]
1879    fn default_parser_requires_encapsulated_for_options() {
1880        let raw = icap_bytes(
1881            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1882             Host: icap.example.org\r\n\
1883             \r\n",
1884        );
1885        let err = parse_icap_request(&raw).unwrap_err();
1886        assert!(
1887            matches!(err, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1888            "expected strict MissingHeader(Encapsulated), got: {err}"
1889        );
1890    }
1891
1892    #[test]
1893    fn default_parser_accepts_options_with_null_body() {
1894        let raw = icap_bytes(
1895            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1896             Host: icap.example.org\r\n\
1897             Encapsulated: null-body=0\r\n\
1898             \r\n",
1899        );
1900        let r = parse_icap_request(&raw).expect("strict parse");
1901        assert_eq!(r.method, Method::Options);
1902    }
1903
1904    #[test]
1905    fn parse_reqmod_with_allow_and_preview() {
1906        let raw = icap_bytes(
1907            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1908             Host: icap.example.org\r\n\
1909             Encapsulated: req-hdr=0\r\n\
1910             Allow: 204, 206\r\n\
1911             Preview: 128\r\n\
1912             \r\n",
1913        );
1914        let r = parse_icap_request(&raw).expect("parse");
1915        assert_eq!(r.method, Method::ReqMod);
1916        assert!(r.allow_204);
1917        assert!(r.allow_206);
1918        assert_eq!(r.preview_size, Some(128));
1919    }
1920
1921    #[test]
1922    fn parse_preview_not_a_number_is_rejected() {
1923        // RFC 3507 §4.5: `Preview` value must be a non-negative integer; a
1924        // malformed value is a protocol error rather than "no preview".
1925        let raw = icap_bytes(
1926            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1927             Host: icap.example.org\r\n\
1928             Preview: notanumber\r\n\
1929             Encapsulated: req-hdr=0\r\n\
1930             \r\n",
1931        );
1932        let err = parse_icap_request(&raw).expect_err("malformed Preview must error");
1933        let msg = err.to_string();
1934        assert!(
1935            msg.contains("Preview"),
1936            "error should mention Preview, got: {msg}"
1937        );
1938    }
1939
1940    // ---------- Embedded HTTP ----------
1941
1942    #[test]
1943    fn parse_reqmod_with_embedded_http_request_and_body() {
1944        let raw = icap_bytes(
1945            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1946             Host: icap.example.org\r\n\
1947             Encapsulated: req-hdr=0\r\n\
1948             \r\n\
1949             GET / HTTP/1.1\r\n\
1950             Host: example.com\r\n\
1951             \r\n\
1952             body...",
1953        );
1954        let r = parse_icap_request(&raw).expect("parse");
1955        match r.embedded {
1956            Some(EmbeddedHttp::Req { ref head, ref body }) => {
1957                assert_eq!(head.method(), &HttpMethod::GET);
1958                assert_eq!(head.uri(), "/");
1959                assert_eq!(
1960                    head.headers().get("Host").unwrap(),
1961                    &HeaderValue::from_static("example.com")
1962                );
1963                match body {
1964                    Body::Full { reader } => assert_eq!(reader, b"body..."),
1965                    _ => panic!("expected Full body"),
1966                }
1967            }
1968            _ => panic!("expected embedded HTTP request"),
1969        }
1970    }
1971
1972    #[test]
1973    fn rejects_incomplete_embedded_http_headers() {
1974        let raw = icap_bytes(
1975            "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1976             Host: icap.example.org\r\n\
1977             Encapsulated: req-hdr=0\r\n\
1978             \r\n\
1979             GET / HTTP/1.1\r\n\
1980             Host: example.com\r\n",
1981        );
1982
1983        let err = parse_icap_request(&raw).unwrap_err();
1984        assert!(
1985            matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
1986            "expected embedded HTTP parse error, got: {err}"
1987        );
1988    }
1989
1990    #[test]
1991    fn rejects_respmod_with_http_request_start_line() {
1992        let raw = icap_bytes(
1993            "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1994             Host: icap.example.org\r\n\
1995             Encapsulated: res-hdr=0\r\n\
1996             \r\n\
1997             GET / HTTP/1.1\r\n\
1998             Host: example.com\r\n\
1999             \r\n",
2000        );
2001
2002        let err = parse_icap_request(&raw).unwrap_err();
2003        assert!(
2004            matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
2005            "expected embedded HTTP status-line parse error, got: {err}"
2006        );
2007    }
2008
2009    #[test]
2010    fn rejects_respmod_with_invalid_status_code() {
2011        let raw = icap_bytes(
2012            "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
2013             Host: icap.example.org\r\n\
2014             Encapsulated: res-hdr=0\r\n\
2015             \r\n\
2016             HTTP/1.1 nope OK\r\n\
2017             Content-Length: 0\r\n\
2018             \r\n",
2019        );
2020
2021        let err = parse_icap_request(&raw).unwrap_err();
2022        assert!(
2023            matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
2024            "expected embedded HTTP status code parse error, got: {err}"
2025        );
2026    }
2027
2028    #[test]
2029    fn parse_minimal_options_with_null_body() {
2030        let raw = icap_bytes(
2031            "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
2032             Host: icap.example.org\r\n\
2033             Encapsulated: null-body=0\r\n\
2034             \r\n",
2035        );
2036        let r = parse_icap_request(&raw).expect("parse");
2037        assert_eq!(r.method, Method::Options);
2038        assert_eq!(r.service, "/icap/test");
2039        assert_eq!(
2040            r.icap_headers.get("Host").unwrap(),
2041            &HeaderValue::from_static("icap.example.org")
2042        );
2043        assert!(r.embedded.is_none());
2044        assert!(!r.allow_204);
2045        assert!(!r.allow_206);
2046        assert_eq!(r.preview_size, None);
2047    }
2048}