1use crate::ICAP_VERSION;
30use crate::error::{Error, IcapResult};
31#[cfg(test)]
32use crate::error::{ProtocolError, ProtocolField};
33use crate::protocol::{
34 Encapsulated, dechunk_icap_entity_with_use_original_body, find_double_crlf, istag_header_value,
35 parse_encapsulated_value, parse_icap_response_head, parse_one_chunk, serialize_http_request,
36 serialize_http_response, validate_istag,
37};
38use http::{HeaderMap, HeaderName, HeaderValue};
39use std::fmt;
40use std::marker::PhantomData;
41use tracing::trace;
42
43pub type StatusCode = http::StatusCode;
63
64#[derive(Debug, Clone)]
74#[must_use]
75pub struct Response<D = Outgoing> {
76 pub(crate) version: String,
78 pub(crate) status_code: StatusCode,
80 pub(crate) status_text: String,
82 pub(crate) headers: HeaderMap,
84 pub(crate) use_original_body: Option<usize>,
86 pub(crate) body: Vec<u8>,
88 pub(crate) chunk_trailers: HeaderMap,
91 pub(crate) direction: PhantomData<D>,
92}
93
94#[derive(Debug, Clone, Copy, Eq, PartialEq)]
96pub enum Outgoing {}
97
98#[derive(Debug, Clone, Copy, Eq, PartialEq)]
100pub enum Parsed {}
101
102pub type OutgoingResponse = Response<Outgoing>;
104
105pub type ParsedResponse = Response<Parsed>;
107
108#[inline]
109fn ensure_owned_response<'a>(
110 owned: &'a mut Option<OutgoingResponse>,
111 original: &OutgoingResponse,
112) -> &'a mut OutgoingResponse {
113 if owned.is_none() {
114 *owned = Some(original.clone());
115 }
116 owned.as_mut().expect("owned response")
117}
118
119impl<D> Response<D> {
120 #[inline]
122 pub fn version(&self) -> &str {
123 &self.version
124 }
125
126 #[inline]
128 pub const fn status_code(&self) -> StatusCode {
129 self.status_code
130 }
131
132 #[inline]
134 pub fn status_text(&self) -> &str {
135 &self.status_text
136 }
137
138 #[inline]
140 pub fn body(&self) -> &[u8] {
141 &self.body
142 }
143
144 pub fn get_header(&self, name: &str) -> Option<&HeaderValue> {
146 self.headers.get(name)
147 }
148
149 pub const fn headers(&self) -> &HeaderMap {
151 &self.headers
152 }
153
154 pub const fn use_original_body_offset(&self) -> Option<usize> {
160 self.use_original_body
161 }
162
163 pub fn has_header(&self, name: &str) -> bool {
165 self.headers.contains_key(name)
166 }
167
168 pub fn is_success(&self) -> bool {
170 self.status_code.is_success()
171 }
172
173 pub fn is_client_error(&self) -> bool {
175 self.status_code.is_client_error()
176 }
177
178 pub fn is_server_error(&self) -> bool {
180 self.status_code.is_server_error()
181 }
182}
183
184impl ParsedResponse {
185 pub fn from_raw(raw: &[u8]) -> IcapResult<Self> {
191 parse_icap_response(raw)
192 }
193
194 #[inline]
202 pub const fn chunk_trailers(&self) -> &HeaderMap {
203 &self.chunk_trailers
204 }
205}
206
207impl Response<Outgoing> {
208 pub fn new(status_code: StatusCode, status_text: &str) -> Self {
210 Self {
211 version: ICAP_VERSION.to_string(),
212 status_code,
213 status_text: status_text.to_string(),
214 headers: HeaderMap::new(),
215 use_original_body: None,
216 body: Vec::new(),
217 chunk_trailers: HeaderMap::new(),
218 direction: PhantomData,
219 }
220 }
221
222 pub fn ok() -> Self {
227 Self::new(StatusCode::OK, "OK")
228 }
229
230 pub fn ok_with_istag(istag: &str) -> IcapResult<Self> {
232 Self::ok().try_set_istag(istag)
233 }
234
235 pub fn no_content() -> Self {
237 Self::new(StatusCode::NO_CONTENT, "No Content")
238 }
239
240 pub fn no_content_with_istag(istag: &str) -> IcapResult<Self> {
245 Self::no_content().try_set_istag(istag)
246 }
247
248 pub fn partial_content() -> Self {
255 Self::new(StatusCode::PARTIAL_CONTENT, "Partial Content")
256 }
257
258 pub fn partial_content_with_istag(istag: &str) -> IcapResult<Self> {
260 Self::partial_content().try_set_istag(istag)
261 }
262
263 pub fn no_content_with_headers(headers: HeaderMap) -> IcapResult<Self> {
265 let istag = headers
266 .get("ISTag")
267 .ok_or_else(|| Error::missing_header("ISTag"))?
268 .to_str()?;
269 validate_istag(istag)?;
270
271 Ok(Self {
272 version: ICAP_VERSION.to_string(),
273 status_code: StatusCode::NO_CONTENT,
274 status_text: "No Content".to_string(),
275 headers,
276 use_original_body: None,
277 body: Vec::new(),
278 chunk_trailers: HeaderMap::new(),
279 direction: PhantomData,
280 })
281 }
282
283 pub fn try_add_header(mut self, name: &str, value: &str) -> IcapResult<Self> {
287 if name.eq_ignore_ascii_case("ISTag") {
288 let val = istag_header_value(value)?;
289 self.headers.insert(HeaderName::from_static("istag"), val);
290 return Ok(self);
291 }
292
293 let n: HeaderName = name.parse()?;
294 let v: HeaderValue = HeaderValue::from_str(value)?;
295 self.headers.insert(n, v);
296 Ok(self)
297 }
298
299 pub fn add_header(self, name: &str, value: &str) -> Self {
309 if name.eq_ignore_ascii_case("ISTag")
310 && let Err(e) = validate_istag(value)
311 {
312 trace!("ignoring invalid ISTag passed to add_header: {}", e);
313 return self;
314 }
315
316 self.try_add_header(name, value)
317 .expect("invalid response header name or value")
318 }
319
320 pub fn try_set_istag(mut self, istag: &str) -> IcapResult<Self> {
328 let name = HeaderName::from_static("istag");
329 let val = istag_header_value(istag)?;
330 self.headers.insert(name, val);
331 Ok(self)
332 }
333
334 pub fn with_body(mut self, body: &[u8]) -> Self {
336 self.body = body.to_vec();
337 self
338 }
339
340 pub fn with_body_string(mut self, body: &str) -> Self {
342 self.body = body.as_bytes().to_vec();
343 self
344 }
345
346 pub fn to_raw(&self) -> IcapResult<Vec<u8>> {
353 let require_istag = self.status_code.is_success();
354
355 if require_istag {
356 let istag = self
357 .headers
358 .get("ISTag")
359 .ok_or_else(|| Error::missing_header("ISTag"))?
360 .to_str()?;
361 validate_istag(istag)?;
362 } else if let Some(v) = self.headers.get("ISTag") {
363 let s = v.to_str()?;
364 validate_istag(s)?;
365 }
366
367 let mut owned: Option<Self> = None;
368
369 match self.status_code {
370 StatusCode::NO_CONTENT => {
371 if !self.body.is_empty() {
372 return Err(Error::body("204 must not carry a body"));
373 }
374 match self.headers.get("Encapsulated") {
375 None => {
376 ensure_owned_response(&mut owned, self).headers.insert(
377 HeaderName::from_static("encapsulated"),
378 HeaderValue::from_static("null-body=0"),
379 );
380 }
381 Some(v) if v.as_bytes() != b"null-body=0".as_slice() => {
382 return Err(Error::header("204 requires Encapsulated: null-body=0"));
383 }
384 Some(_) => {}
385 }
386 }
387 StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
388 if !self.headers.contains_key("Encapsulated") {
389 if self.body.is_empty() {
390 return Err(Error::missing_header(
391 "Encapsulated missing and cannot infer for 2xx with empty body; \
392 set it explicitly or use Response::with_http_response(...)",
393 ));
394 }
395 if looks_like_http_resp(&self.body) {
396 let enc = compute_enc_for_res_body(&self.body)?;
397 let hv = HeaderValue::from_str(&enc)?;
398 ensure_owned_response(&mut owned, self)
399 .headers
400 .insert(HeaderName::from_static("encapsulated"), hv);
401 } else {
402 return Err(Error::header(
403 "Encapsulated missing and body is not an embedded HTTP/1.x".to_string(),
404 ));
405 }
406 }
407 }
408 _ => {
409 if !self.headers.contains_key("Encapsulated") {
410 if self.body.is_empty() {
411 ensure_owned_response(&mut owned, self).headers.insert(
412 HeaderName::from_static("encapsulated"),
413 HeaderValue::from_static("null-body=0"),
414 );
415 } else if looks_like_http_resp(&self.body) {
416 let enc = compute_enc_for_res_body(&self.body)?;
417 let hv = HeaderValue::from_str(&enc)?;
418 ensure_owned_response(&mut owned, self)
419 .headers
420 .insert(HeaderName::from_static("encapsulated"), hv);
421 } else {
422 ensure_owned_response(&mut owned, self).headers.insert(
423 HeaderName::from_static("encapsulated"),
424 HeaderValue::from_static("opt-body=0"),
425 );
426 }
427 }
428 }
429 }
430
431 let resp_ref = owned.as_ref().unwrap_or(self);
432 if !resp_ref.body.is_empty()
433 && let Some(enc_val) = resp_ref
434 .headers
435 .get("Encapsulated")
436 .and_then(|v| v.to_str().ok())
437 && parse_encapsulated_value(enc_val).is_ok_and(|enc| enc.null_body.is_some())
438 {
439 return Err(Error::body("Encapsulated: null-body must not carry a body"));
440 }
441 if let Some(offset) = resp_ref.use_original_body {
442 if resp_ref.status_code != StatusCode::PARTIAL_CONTENT {
443 return Err(Error::header(
444 "use-original-body is only valid on 206 Partial Content",
445 ));
446 }
447 let enc_val = resp_ref
448 .headers
449 .get("Encapsulated")
450 .and_then(|v| v.to_str().ok())
451 .ok_or_else(|| Error::missing_header("Encapsulated"))?;
452 let enc = parse_encapsulated_value(enc_val)?;
453 if enc.req_body.or(enc.res_body).or(enc.opt_body).is_none() {
454 return Err(Error::header(
455 "use-original-body requires an encapsulated body offset",
456 ));
457 }
458 trace!(offset, "serializing 206 use-original-body marker");
459 }
460 Ok(crate::protocol::serialize_icap_response(resp_ref))
461 }
462
463 pub fn remove_header(&mut self, name: &str) -> Option<HeaderValue> {
465 self.headers.remove(name)
466 }
467
468 pub fn with_http_request(mut self, http: &http::Request<Vec<u8>>) -> IcapResult<Self> {
471 let bytes = serialize_http_request(http);
472
473 let enc = compute_enc_for_req_body(&bytes)?;
474 let hv = HeaderValue::from_str(&enc)?;
475
476 self.body = bytes;
477 self.use_original_body = None;
478 self.headers
479 .insert(HeaderName::from_static("encapsulated"), hv);
480 Ok(self)
481 }
482
483 pub fn with_http_response(mut self, http: &http::Response<Vec<u8>>) -> IcapResult<Self> {
486 let bytes = serialize_http_response(http);
487
488 let enc = compute_enc_for_res_body(&bytes)?;
489 let hv = HeaderValue::from_str(&enc)?;
490
491 self.body = bytes;
492 self.use_original_body = None;
493 self.headers
494 .insert(HeaderName::from_static("encapsulated"), hv);
495 Ok(self)
496 }
497
498 pub fn with_http_request_head_and_original_body(
503 mut self,
504 head: &http::Request<()>,
505 offset: usize,
506 ) -> IcapResult<Self> {
507 let mut builder = http::Request::builder()
508 .method(head.method().clone())
509 .uri(head.uri().clone())
510 .version(head.version());
511 if let Some(headers) = builder.headers_mut() {
512 headers.extend(head.headers().clone());
513 }
514 let http = builder
515 .body(Vec::new())
516 .map_err(|e| Error::body(format!("build embedded HTTP request head: {e}")))?;
517 let bytes = serialize_http_request(&http);
518 let hdr_end = find_double_crlf(&bytes)
519 .ok_or_else(|| Error::header("embedded HTTP request missing CRLFCRLF"))?;
520 self.body = bytes;
521 self.use_original_body = Some(offset);
522 self.headers.insert(
523 HeaderName::from_static("encapsulated"),
524 HeaderValue::from_str(&format!("req-hdr=0, req-body={hdr_end}"))?,
525 );
526 Ok(self)
527 }
528
529 pub fn with_http_response_head_and_original_body(
534 mut self,
535 head: &http::Response<()>,
536 offset: usize,
537 ) -> IcapResult<Self> {
538 let mut builder = http::Response::builder()
539 .status(head.status())
540 .version(head.version());
541 if let Some(headers) = builder.headers_mut() {
542 headers.extend(head.headers().clone());
543 }
544 let http = builder
545 .body(Vec::new())
546 .map_err(|e| Error::body(format!("build embedded HTTP response head: {e}")))?;
547 let bytes = serialize_http_response(&http);
548 let hdr_end = find_double_crlf(&bytes)
549 .ok_or_else(|| Error::header("embedded HTTP response missing CRLFCRLF"))?;
550 self.body = bytes;
551 self.use_original_body = Some(offset);
552 self.headers.insert(
553 HeaderName::from_static("encapsulated"),
554 HeaderValue::from_str(&format!("res-hdr=0, res-body={hdr_end}"))?,
555 );
556 Ok(self)
557 }
558}
559
560impl<D> fmt::Display for Response<D> {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563 writeln!(
564 f,
565 "{} {} {}",
566 self.version,
567 self.status_code.as_str(),
568 self.status_text
569 )?;
570 for (name, value) in &self.headers {
571 writeln!(
572 f,
573 "{}: {}",
574 name.as_str(),
575 value.to_str().unwrap_or_default()
576 )?;
577 }
578 if !self.body.is_empty() {
579 writeln!(f, "\n{}", String::from_utf8_lossy(&self.body))?;
580 }
581 Ok(())
582 }
583}
584
585#[inline]
586fn looks_like_http_resp(body: &[u8]) -> bool {
587 body.starts_with(b"HTTP/1.0") || body.starts_with(b"HTTP/1.1")
588}
589#[inline]
590fn compute_enc_for_res_body(body: &[u8]) -> IcapResult<String> {
591 let hdr_end =
592 find_double_crlf(body).ok_or_else(|| Error::header("embedded HTTP missing CRLFCRLF"))?;
593 if body.len() > hdr_end {
594 Ok(format!("res-hdr=0, res-body={hdr_end}"))
595 } else {
596 Ok("res-hdr=0".to_string())
597 }
598}
599#[inline]
600fn compute_enc_for_req_body(body: &[u8]) -> IcapResult<String> {
601 let hdr_end =
602 find_double_crlf(body).ok_or_else(|| Error::header("embedded HTTP missing CRLFCRLF"))?;
603 if body.len() > hdr_end {
604 Ok(format!("req-hdr=0, req-body={hdr_end}"))
605 } else {
606 Ok("req-hdr=0".to_string())
607 }
608}
609
610fn parse_response_head_parts(raw: &[u8]) -> IcapResult<ParsedResponseHead> {
611 let head = parse_icap_response_head(raw)?;
612
613 Ok(ParsedResponseHead {
614 response: Response {
615 version: head.version,
616 status_code: head.status_code,
617 status_text: head.status_text,
618 headers: head.headers,
619 use_original_body: None,
620 body: Vec::new(),
621 chunk_trailers: HeaderMap::new(),
622 direction: PhantomData,
623 },
624 header_end: head.header_end,
625 encapsulated_value: head.encapsulated_value,
626 })
627}
628
629struct ParsedResponseHead {
630 response: ParsedResponse,
631 header_end: usize,
632 encapsulated_value: Option<String>,
633}
634
635pub(crate) fn parse_icap_response(raw: &[u8]) -> IcapResult<ParsedResponse> {
636 trace!(len = raw.len(), "parse_icap_response");
637 let ParsedResponseHead {
638 mut response,
639 header_end,
640 encapsulated_value,
641 } = parse_response_head_parts(raw)?;
642
643 let mut body = raw[header_end..].to_vec();
644 trace!(body_len = body.len(), "parsed body");
645 if response.status_code.is_success() {
646 let enc_val = encapsulated_value.as_deref().or_else(|| {
647 response
648 .headers
649 .get("Encapsulated")
650 .and_then(|v| v.to_str().ok())
651 });
652
653 match response.status_code {
654 StatusCode::NO_CONTENT => {
655 let Some(enc_val) = enc_val else {
656 if !body.is_empty() {
660 return Err(Error::body("204 must not carry a body"));
661 }
662 return Ok(response);
663 };
664 if !enc_val.trim().eq_ignore_ascii_case("null-body=0") {
665 return Err(Error::header("204 requires Encapsulated: null-body=0"));
666 }
667 if !body.is_empty() {
668 return Err(Error::body("204 must not carry a body"));
669 }
670 }
671 StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
672 let enc_val = enc_val.ok_or_else(|| Error::missing_header("Encapsulated"))?;
673 let enc = parse_encapsulated_value(enc_val)?;
674 let (use_original_body, trailers) =
675 dechunk_response_body_if_needed(&enc, &mut body)?;
676 response.use_original_body = use_original_body;
677 response.chunk_trailers = trailers;
678 if response.use_original_body.is_some()
679 && response.status_code != StatusCode::PARTIAL_CONTENT
680 {
681 return Err(Error::header(
682 "use-original-body is only valid on 206 Partial Content",
683 ));
684 }
685 validate_encapsulated_offsets(&enc, body.len())?;
686 }
687 _ => {}
688 }
689 }
690
691 response.body = body;
692 Ok(response)
693}
694
695fn validate_encapsulated_offsets(enc: &Encapsulated, enc_len: usize) -> IcapResult<()> {
696 for off in encapsulated_offsets(enc) {
697 if off > enc_len {
698 return Err(Error::header(format!(
699 "Encapsulated offset {off} out of range (len={enc_len})"
700 )));
701 }
702 }
703 Ok(())
704}
705
706fn dechunk_response_body_if_needed(
707 enc: &Encapsulated,
708 body: &mut Vec<u8>,
709) -> IcapResult<(Option<usize>, HeaderMap)> {
710 let Some(body_start) = enc.req_body.or(enc.res_body).or(enc.opt_body) else {
711 return Ok((None, HeaderMap::new()));
712 };
713
714 if body_start > body.len() {
715 return Err(Error::header(format!(
716 "Encapsulated body offset {body_start} out of range (len={})",
717 body.len()
718 )));
719 }
720 if parse_one_chunk(body, body_start).is_none() {
721 return Err(Error::body(
722 "missing ICAP chunked entity body at Encapsulated body offset",
723 ));
724 }
725
726 let mut chunked = &body[body_start..];
727 let (decoded, use_original_body, trailers) =
728 dechunk_icap_entity_with_use_original_body(&mut chunked)
729 .map_err(|e| Error::body(format!("dechunk ICAP entity: {e}")))?;
730 body.splice(body_start.., decoded);
731 Ok((use_original_body, trailers))
732}
733
734fn encapsulated_offsets(enc: &Encapsulated) -> impl Iterator<Item = usize> {
735 [
736 enc.req_hdr,
737 enc.res_hdr,
738 enc.req_body,
739 enc.res_body,
740 enc.opt_body,
741 enc.null_body,
742 ]
743 .into_iter()
744 .flatten()
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use http::HeaderValue;
751
752 #[inline]
753 fn icap_bytes(s: &str) -> Vec<u8> {
754 s.as_bytes().to_vec()
755 }
756
757 #[test]
758 fn response_add_get_remove_header() {
759 let mut resp = Response::new(StatusCode::OK, "OK").add_header("Service", "Test");
760 assert!(resp.has_header("Service"));
761 assert_eq!(
762 resp.get_header("Service").unwrap(),
763 &HeaderValue::from_static("Test")
764 );
765
766 let removed = resp.remove_header("Service");
767 assert!(removed.is_some());
768 assert!(!resp.has_header("Service"));
769 }
770
771 #[test]
772 fn success_shortcuts_set_status_and_istag() {
773 let ok = Response::ok_with_istag("ok-1").expect("valid ISTag");
774 assert_eq!(ok.status_code, StatusCode::OK);
775 assert_eq!(ok.status_text, "OK");
776 assert_eq!(
777 ok.get_header("ISTag").unwrap(),
778 &HeaderValue::from_static("\"ok-1\"")
779 );
780
781 let no_content = Response::no_content_with_istag("no-change-1").expect("valid ISTag");
782 assert_eq!(no_content.status_code, StatusCode::NO_CONTENT);
783 assert_eq!(no_content.status_text, "No Content");
784
785 let partial = Response::partial_content_with_istag("partial-1").expect("valid ISTag");
786 assert_eq!(partial.status_code, StatusCode::PARTIAL_CONTENT);
787 assert_eq!(partial.status_text, "Partial Content");
788 }
789
790 #[test]
791 fn success_shortcuts_validate_istag() {
792 let err = Response::ok_with_istag("BAD TAG").expect_err("shortcut must validate ISTag");
793 assert!(matches!(
794 err,
795 Error::Protocol(ProtocolError::InvalidISTag(_))
796 ));
797 }
798
799 #[test]
800 fn last_duplicate_header_wins() {
801 let raw = icap_bytes(
802 "ICAP/1.0 204 No Content\r\n\
803 ISTag: a\r\n\
804 ISTag: b\r\n\
805 Encapsulated: null-body=0\r\n\
806 \r\n",
807 );
808 let r = parse_icap_response(&raw).expect("parse");
809 assert_eq!(
810 r.get_header("ISTag").unwrap(),
811 &HeaderValue::from_static("b")
812 );
813 }
814
815 #[test]
816 fn parse_errors_on_empty() {
817 let err = parse_icap_response(b"").unwrap_err();
818 assert!(err.to_string().contains("Empty response"));
819 }
820
821 #[test]
822 fn parse_errors_on_invalid_status_code_token() {
823 let raw = icap_bytes(
824 "ICAP/1.0 ABC OK\r\n\
825 \r\n",
826 );
827 let err = parse_icap_response(&raw).unwrap_err();
828 assert!(err.to_string().contains("Invalid status code"));
829 }
830
831 #[test]
832 fn add_header_istag_rejects_invalid_but_does_not_panic() {
833 let resp = Response::new(StatusCode::OK, "OK").add_header("ISTag", "BAD TAG WITH SPACE");
834 assert!(resp.get_header("ISTag").is_none());
835 }
836
837 #[test]
838 fn try_add_header_rejects_invalid_header_input() {
839 let err = Response::new(StatusCode::OK, "OK")
840 .try_add_header("Bad Header", "value")
841 .expect_err("invalid header name should be rejected");
842
843 assert!(matches!(err, Error::Protocol(ProtocolError::HeaderName(_))));
844 }
845
846 #[test]
847 fn try_add_header_rejects_invalid_istag() {
848 let err = Response::new(StatusCode::OK, "OK")
849 .try_add_header("ISTag", "BAD TAG WITH SPACE")
850 .expect_err("invalid ISTag should be rejected");
851
852 assert!(matches!(
853 err,
854 Error::Protocol(ProtocolError::InvalidISTag(_))
855 ));
856 }
857
858 #[test]
859 fn add_header_istag_accepts_valid_value() {
860 let resp = Response::new(StatusCode::OK, "OK").add_header("ISTag", "ok-Tag.123");
861 assert_eq!(
862 resp.get_header("ISTag").unwrap(),
863 &HeaderValue::from_static("\"ok-Tag.123\"")
864 );
865 }
866
867 #[test]
868 fn try_set_istag_quotes_base64_like_token_for_wire() {
869 let resp = Response::no_content_with_istag("QUJD+/8=").expect("valid base64-like ISTag");
870 assert_eq!(
871 resp.get_header("ISTag").unwrap(),
872 &HeaderValue::from_static("\"QUJD+/8=\"")
873 );
874
875 let raw = String::from_utf8(resp.to_raw().expect("serialize 204")).expect("utf8");
876 assert!(raw.contains("\r\nISTag: \"QUJD+/8=\"\r\n"));
877 }
878
879 #[test]
880 fn try_set_istag_preserves_already_quoted_value() {
881 let resp = Response::no_content_with_istag("\"QUJD+/8=\"").expect("valid quoted ISTag");
882 assert_eq!(
883 resp.get_header("ISTag").unwrap(),
884 &HeaderValue::from_static("\"QUJD+/8=\"")
885 );
886 }
887
888 #[test]
889 fn to_raw_errors_if_istag_missing() {
890 let resp = Response::new(StatusCode::OK, "OK").add_header("Service", "X");
891 let err = resp.to_raw().unwrap_err();
892 assert!(matches!(err, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "ISTag"));
893 }
894
895 #[test]
896 fn to_raw_ok_when_istag_is_valid() {
897 let resp = Response::new(StatusCode::OK, "OK")
898 .try_set_istag("ok-Tag.123")
899 .unwrap()
900 .add_header("Encapsulated", "res-hdr=0")
901 .with_body_string("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
902 let _bytes = resp
903 .to_raw()
904 .expect("to_raw should succeed with valid ISTag");
905 }
906
907 #[test]
910 fn to_raw_autogenerates_encapsulated_for_404_without_body() {
911 let resp = Response::new(StatusCode::NOT_FOUND, "Not Found");
912 let raw = resp.to_raw().expect("serialize 404");
913 let s = String::from_utf8(raw).unwrap();
914 assert!(s.contains("Encapsulated: null-body=0"));
915 assert!(
916 !s.to_lowercase().contains("istag:"),
917 "no ISTag required on non-2xx"
918 );
919 }
920
921 #[test]
922 fn to_raw_autogenerates_opt_body_for_non_http_body_on_error() {
923 let resp =
924 Response::new(StatusCode::INTERNAL_SERVER_ERROR, "Internal").with_body_string("oops");
925 let raw = resp.to_raw().expect("serialize 500 with body");
926 let s = String::from_utf8(raw).unwrap();
927 assert!(s.contains("Encapsulated: opt-body=0"));
928 }
929
930 #[test]
931 fn status_line_has_single_reason() {
932 let bytes = Response::new(http::StatusCode::OK, "OK")
933 .try_set_istag("x")
934 .unwrap()
935 .add_header("Encapsulated", "null-body=0")
936 .to_raw()
937 .unwrap();
938
939 let line = std::str::from_utf8(&bytes).unwrap().lines().next().unwrap();
940 assert_eq!(line, "ICAP/1.0 200 OK");
941 }
942
943 #[test]
944 fn to_raw_chunks_only_embedded_http_entity_body() {
945 let http = http::Response::builder()
946 .status(http::StatusCode::OK)
947 .version(http::Version::HTTP_11)
948 .header("Content-Length", "5")
949 .body(b"hello".to_vec())
950 .unwrap();
951
952 let raw = Response::new(StatusCode::OK, "OK")
953 .try_set_istag("x")
954 .unwrap()
955 .with_http_response(&http)
956 .unwrap()
957 .to_raw()
958 .unwrap();
959
960 let icap_header_end = find_double_crlf(&raw).expect("ICAP header end");
961 assert_eq!(&raw[icap_header_end..icap_header_end + 5], b"HTTP/");
962
963 let text = String::from_utf8_lossy(&raw);
964 assert!(text.contains("Encapsulated: res-hdr=0, res-body="));
965 assert!(text.contains("\r\n5\r\nhello\r\n0\r\n\r\n"));
966 }
967
968 #[test]
969 fn parse_rfc_wire_dechunks_embedded_http_entity_body() {
970 let raw = b"ICAP/1.0 200 OK\r\n\
971 ISTag: x\r\n\
972 Encapsulated: res-hdr=0, res-body=38\r\n\
973 \r\n\
974 HTTP/1.1 200 OK\r\n\
975 Content-Length: 5\r\n\
976 \r\n\
977 5\r\n\
978 hello\r\n\
979 0\r\n\
980 \r\n";
981
982 let parsed = parse_icap_response(raw).expect("parse RFC wire response");
983 assert_eq!(
984 parsed.body,
985 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
986 );
987 }
988
989 #[test]
990 fn to_raw_206_serializes_use_original_body_zero_chunk_extension() {
991 let http = http::Response::builder()
992 .status(http::StatusCode::OK)
993 .version(http::Version::HTTP_11)
994 .header("Content-Length", "5")
995 .body(())
996 .unwrap();
997
998 let raw = Response::new(StatusCode::PARTIAL_CONTENT, "Partial Content")
999 .try_set_istag("x")
1000 .unwrap()
1001 .with_http_response_head_and_original_body(&http, 0)
1002 .unwrap()
1003 .to_raw()
1004 .unwrap();
1005 let text = String::from_utf8(raw).unwrap();
1006
1007 assert!(text.starts_with("ICAP/1.0 206 Partial Content\r\n"));
1008 assert!(text.contains("Encapsulated: res-hdr=0, res-body="));
1009 assert!(text.ends_with("\r\n0; use-original-body=0\r\n\r\n"));
1010 }
1011
1012 #[test]
1013 fn parse_206_extracts_use_original_body_offset() {
1014 let raw = b"ICAP/1.0 206 Partial Content\r\n\
1015 ISTag: x\r\n\
1016 Encapsulated: res-hdr=0, res-body=38\r\n\
1017 \r\n\
1018 HTTP/1.1 200 OK\r\n\
1019 Content-Length: 5\r\n\
1020 \r\n\
1021 0; use-original-body=0\r\n\
1022 \r\n";
1023
1024 let parsed = parse_icap_response(raw).expect("parse 206 partial content");
1025
1026 assert_eq!(parsed.status_code, StatusCode::PARTIAL_CONTENT);
1027 assert_eq!(parsed.use_original_body_offset(), Some(0));
1028 assert_eq!(parsed.body, b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n");
1029 }
1030
1031 #[test]
1032 fn parse_rejects_use_original_body_on_200() {
1033 let raw = b"ICAP/1.0 200 OK\r\n\
1034 ISTag: x\r\n\
1035 Encapsulated: res-hdr=0, res-body=38\r\n\
1036 \r\n\
1037 HTTP/1.1 200 OK\r\n\
1038 Content-Length: 5\r\n\
1039 \r\n\
1040 0; use-original-body=0\r\n\
1041 \r\n";
1042
1043 let err = parse_icap_response(raw).expect_err("use-original-body requires 206");
1044
1045 assert!(err.to_string().contains("206 Partial Content"));
1046 }
1047
1048 #[test]
1049 fn parse_rejects_legacy_unchunked_entity_body_after_body_offset() {
1050 let raw = b"ICAP/1.0 200 OK\r\n\
1051 ISTag: x\r\n\
1052 Encapsulated: res-hdr=0, res-body=38\r\n\
1053 \r\n\
1054 HTTP/1.1 200 OK\r\n\
1055 Content-Length: 5\r\n\
1056 \r\n\
1057 hello";
1058
1059 let err = parse_icap_response(raw).expect_err("legacy unchunked body must be rejected");
1060 assert!(
1061 err.to_string().to_lowercase().contains("chunked"),
1062 "expected chunked framing error, got: {err}"
1063 );
1064 }
1065
1066 #[test]
1067 fn parse_accepts_c_icap_204_without_encapsulated() {
1068 let raw = b"ICAP/1.0 204 Unmodified\r\n\
1069 Server: C-ICAP/0.5.10\r\n\
1070 ISTag: \"CI0001-XXXXXXXXX\"\r\n\
1071 \r\n";
1072
1073 let response = parse_icap_response(raw).expect("c-icap bare 204 should parse");
1074
1075 assert_eq!(response.status_code, StatusCode::NO_CONTENT);
1076 assert!(response.body.is_empty());
1077 }
1078}
1079
1080#[cfg(test)]
1081mod response_wire_parser_tests {
1082 use super::*;
1086 use http::HeaderValue;
1087 use rstest::rstest;
1088
1089 #[inline]
1090 fn icap_bytes(s: &str) -> Vec<u8> {
1091 s.as_bytes().to_vec()
1092 }
1093
1094 #[test]
1095 fn version_must_be_icap_1_0() {
1096 let raw = icap_bytes(
1097 "ICAP/2.0 200 OK\r\n\
1098 ISTag: x\r\n\
1099 Encapsulated: null-body=0\r\n\
1100 \r\n",
1101 );
1102 let err = parse_icap_response(&raw).unwrap_err();
1103 assert!(
1104 matches!(err, Error::Protocol(ProtocolError::InvalidField { field: ProtocolField::Version, value: ref v, .. }) if v == "ICAP/2.0"),
1105 "expected InvalidVersion(\"ICAP/2.0\"), got: {err:?}"
1106 );
1107 }
1108
1109 #[test]
1110 fn supports_multiword_reason_phrase() {
1111 let raw = icap_bytes(
1112 "ICAP/1.0 405 Method Not Allowed\r\n\
1113 ISTag: x\r\n\
1114 Encapsulated: null-body=0\r\n\
1115 \r\n",
1116 );
1117 let r = parse_icap_response(&raw).expect("parse ok");
1118 assert_eq!(r.status_code, StatusCode::METHOD_NOT_ALLOWED);
1119 assert_eq!(r.status_text, "Method Not Allowed");
1120 }
1121
1122 #[test]
1125 fn istag_required_for_2xx() {
1126 let raw = b"ICAP/1.0 200 OK\r\nEncapsulated: null-body=0\r\n\r\n";
1127 let err = parse_icap_response(raw).unwrap_err();
1128 assert!(err.to_string().to_lowercase().contains("istag"));
1129 }
1130
1131 #[test]
1132 fn istag_may_be_absent_in_404() {
1133 let raw = b"ICAP/1.0 404 Not Found\r\n\r\n";
1134 let r = parse_icap_response(raw).expect("lenient for non-2xx");
1135 assert_eq!(r.status_code, StatusCode::NOT_FOUND);
1136 assert!(r.get_header("ISTag").is_none());
1137 }
1138
1139 #[rstest]
1140 #[case("ok-Tag.123".to_string(), true)] #[case("helloo.1755855904-1755855904181".to_string(), true)] #[case("x".to_string(), true)] #[case("A".repeat(32), true)] #[case(r#""5BDEEEA9-12E4-2""#.to_string(), true)] #[case(r#""ABC"#.to_string(), false)] #[case(format!(r#""{}""#, "A".repeat(33)), false)] #[case(r#""ABC_DEF""#.to_string(), true)] #[case(r#""QUJDREUrLw==""#.to_string(), true)] #[case("QUJDREUrLw==".to_string(), true)] #[case("TAG 1".to_string(), false)] #[case("TAG_1".to_string(), true)] #[case("TAG+1".to_string(), true)] #[case("TAG/1".to_string(), true)] #[case("TAG#1".to_string(), true)] #[case("TAG@1".to_string(), false)] fn istag_validate_cases(#[case] value: String, #[case] ok: bool) {
1157 assert_eq!(
1159 validate_istag(&value).is_ok(),
1160 ok,
1161 "validate_istag failed for value={value:?}"
1162 );
1163
1164 let raw = format!(
1166 "ICAP/1.0 200 OK\r\n\
1167 ISTag: {value}\r\n\
1168 Encapsulated: null-body=0\r\n\
1169 \r\n",
1170 );
1171
1172 match (ok, parse_icap_response(raw.as_bytes())) {
1173 (true, Ok(resp)) => {
1174 assert_eq!(
1175 resp.get_header("ISTag").unwrap(),
1176 &HeaderValue::from_str(&value).unwrap(),
1177 "parsed ISTag differs for value={value:?}"
1178 );
1179 }
1180 (false, Err(e)) => {
1181 let msg = e.to_string().to_lowercase();
1182 assert!(
1183 msg.contains("istag"),
1184 "expected ISTag-related error, got: {msg}"
1185 );
1186 }
1187 (true, Err(e)) => panic!("expected parse OK for valid ISTag={value:?}, got error: {e}"),
1188 (false, Ok(_)) => panic!("expected parse error for invalid ISTag={value:?}"),
1189 }
1190 }
1191
1192 #[test]
1195 fn encapsulated_required_for_200() {
1196 let raw = icap_bytes(
1197 "ICAP/1.0 200 OK\r\n\
1198 ISTag: x\r\n\
1199 \r\n",
1200 );
1201 let err = parse_icap_response(&raw).unwrap_err();
1202 assert!(
1203 err.to_string().to_lowercase().contains("encapsulated"),
1204 "expected missing Encapsulated; got {err}"
1205 );
1206 }
1207
1208 #[test]
1209 fn no_duplicate_encapsulated_headers() {
1210 let raw = icap_bytes(
1211 "ICAP/1.0 200 OK\r\n\
1212 ISTag: x\r\n\
1213 Encapsulated: res-hdr=0, res-body=100\r\n\
1214 Encapsulated: req-hdr=0\r\n\
1215 \r\n",
1216 );
1217 let err = parse_icap_response(&raw).unwrap_err();
1218 let m = err.to_string().to_lowercase();
1219 assert!(
1220 m.contains("duplicate") || m.contains("encapsulated"),
1221 "expected duplicate Encapsulated error; got: {m}"
1222 );
1223 }
1224
1225 #[test]
1226 fn duplicate_encapsulated_parts_are_rejected() {
1227 let raw = icap_bytes(
1228 "ICAP/1.0 200 OK\r\n\
1229 ISTag: x\r\n\
1230 Encapsulated: res-hdr=0, res-hdr=10\r\n\
1231 \r\n\
1232 HTTP/1.1 200 OK\r\n\
1233 Content-Length: 0\r\n\
1234 \r\n",
1235 );
1236 let err = parse_icap_response(&raw).unwrap_err();
1237 let m = err.to_string().to_lowercase();
1238 assert!(
1239 m.contains("duplicate") && m.contains("encapsulated"),
1240 "expected duplicate Encapsulated part error; got: {m}"
1241 );
1242 }
1243
1244 #[test]
1245 fn invalid_encapsulated_tokens_are_rejected() {
1246 let raw = icap_bytes(
1247 "ICAP/1.0 200 OK\r\n\
1248 ISTag: x\r\n\
1249 Encapsulated: totally-wrong=abc, res-body=-5\r\n\
1250 \r\n",
1251 );
1252 let err = parse_icap_response(&raw).unwrap_err();
1253 let m = err.to_string().to_lowercase();
1254 assert!(
1255 m.contains("encapsulated") || m.contains("invalid") || m.contains("parse"),
1256 "expected invalid Encapsulated; got: {m}"
1257 );
1258 }
1259
1260 #[test]
1261 fn encapsulated_offsets_must_be_monotonic_and_in_range() {
1262 let raw = icap_bytes(
1263 "ICAP/1.0 200 OK\r\n\
1264 ISTag: x\r\n\
1265 Encapsulated: res-hdr=50, res-body=10\r\n\
1266 \r\n\
1267 HTTP/1.1 200 OK\r\n\
1268 Content-Length: 0\r\n\
1269 \r\n",
1270 );
1271 let err = parse_icap_response(&raw).unwrap_err();
1272 assert!(
1273 err.to_string().to_lowercase().contains("offset"),
1274 "expected offsets validation error; got: {err}"
1275 );
1276 }
1277
1278 #[test]
1281 fn valid_minimal_204() {
1282 let raw = icap_bytes(
1283 "ICAP/1.0 204 No Content\r\n\
1284 ISTag: x\r\n\
1285 Encapsulated: null-body=0\r\n\
1286 \r\n",
1287 );
1288 let r = parse_icap_response(&raw).expect("parse ok");
1289 assert_eq!(r.status_code, StatusCode::NO_CONTENT);
1290 assert_eq!(
1291 r.get_header("Encapsulated").unwrap(),
1292 &HeaderValue::from_static("null-body=0")
1293 );
1294 assert!(r.body.is_empty(), "204 must not carry a body");
1295 }
1296
1297 #[test]
1298 fn rfc_204_must_not_have_body_bytes() {
1299 let raw = icap_bytes(
1300 "ICAP/1.0 204 No Content\r\n\
1301 ISTag: x\r\n\
1302 Encapsulated: null-body=0\r\n\
1303 \r\n\
1304 ILLEGAL_BODY",
1305 );
1306 let err = parse_icap_response(&raw).unwrap_err();
1307 let m = err.to_string().to_lowercase();
1308 assert!(
1309 m.contains("204")
1310 && (m.contains("no body")
1311 || m.contains("null-body")
1312 || m.contains("must not carry a body")),
1313 "expected 204-with-body error; got: {m}"
1314 );
1315 }
1316
1317 #[test]
1320 fn supports_100_continue() {
1321 let raw = icap_bytes(
1322 "ICAP/1.0 100 Continue\r\n\
1323 ISTag: x\r\n\
1324 Encapsulated: null-body=0\r\n\
1325 \r\n",
1326 );
1327 let r = parse_icap_response(&raw).expect("parse ok");
1328 assert_eq!(r.status_code, StatusCode::CONTINUE);
1329 }
1330
1331 #[test]
1332 fn supports_404_not_found() {
1333 let raw = icap_bytes(
1334 "ICAP/1.0 404 ICAP Service not found\r\n\
1335 \r\n",
1336 );
1337 let r = parse_icap_response(&raw).expect("parse ok");
1338 assert_eq!(r.status_code, StatusCode::NOT_FOUND);
1339 assert!(r.status_text.to_lowercase().contains("not"));
1340 }
1341
1342 #[test]
1343 fn header_lookup_is_case_insensitive() {
1344 let raw = icap_bytes(
1345 "ICAP/1.0 200 OK\r\n\
1346 isTag: X\r\n\
1347 eNcaPsulated: null-body=0\r\n\
1348 \r\n",
1349 );
1350 let r = parse_icap_response(&raw).expect("parse ok");
1351 assert_eq!(
1352 r.get_header("ISTag").unwrap(),
1353 &HeaderValue::from_static("X")
1354 );
1355 assert_eq!(
1356 r.get_header("Encapsulated").unwrap(),
1357 &HeaderValue::from_static("null-body=0")
1358 );
1359 }
1360
1361 #[test]
1364 fn error_on_incomplete_headers() {
1365 let raw = icap_bytes("ICAP/1.0 200 OK\r\nISTag: x\r\n");
1366 let err = parse_icap_response(&raw).unwrap_err();
1367 assert!(
1368 err.to_string().to_lowercase().contains("headers"),
1369 "expected incomplete headers error; got {err}"
1370 );
1371 }
1372
1373 #[test]
1374 fn allows_empty_reason_phrase() {
1375 let raw = icap_bytes(
1376 "ICAP/1.0 200 \r\n\
1377 ISTag: x\r\n\
1378 Encapsulated: null-body=0\r\n\
1379 \r\n",
1380 );
1381 let r = parse_icap_response(&raw).expect("parse ok");
1382 assert_eq!(r.status_code, StatusCode::OK);
1383 assert_eq!(r.status_text, "");
1384 }
1385
1386 #[test]
1387 fn ok_minimal_200_with_res_hdr_skeleton() {
1388 let raw = icap_bytes(
1389 "ICAP/1.0 200 OK\r\n\
1390 ISTag: x\r\n\
1391 Encapsulated: res-hdr=0\r\n\
1392 \r\n\
1393 HTTP/1.1 200 OK\r\n\
1394 Content-Length: 0\r\n\
1395 \r\n",
1396 );
1397 let r = parse_icap_response(&raw).expect("parse ok");
1398 assert_eq!(r.status_code, StatusCode::OK);
1399 }
1400}