1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(test, allow(clippy::unwrap_used))]
3
4use std::fmt::{self, Display, Formatter};
69use std::str::FromStr;
70use std::sync::LazyLock;
71
72use indexmap::IndexMap;
73use salvo_core::http::body::ResBody;
74use salvo_core::http::header::{
75 ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, VARY,
76};
77use salvo_core::http::{self, Mime, StatusCode, mime};
78use salvo_core::{Depot, FlowCtrl, Handler, Request, Response, async_trait};
79
80mod encoder;
81mod stream;
82use encoder::Encoder;
83use stream::EncodeStream;
84
85#[non_exhaustive]
87#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
88pub enum CompressionLevel {
89 Fastest,
91 Minsize,
93 #[default]
95 Default,
96 Precise(u32),
101}
102
103#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash)]
105#[non_exhaustive]
106pub enum CompressionAlgo {
107 #[cfg(feature = "brotli")]
109 #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
110 Brotli,
111
112 #[cfg(feature = "deflate")]
114 #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
115 Deflate,
116
117 #[cfg(feature = "gzip")]
119 #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
120 Gzip,
121
122 #[cfg(feature = "zstd")]
124 #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
125 Zstd,
126}
127
128impl FromStr for CompressionAlgo {
129 type Err = String;
130
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
132 match s {
133 #[cfg(feature = "brotli")]
134 "br" => Ok(Self::Brotli),
135 #[cfg(feature = "brotli")]
136 "brotli" => Ok(Self::Brotli),
137
138 #[cfg(feature = "deflate")]
139 "deflate" => Ok(Self::Deflate),
140
141 #[cfg(feature = "gzip")]
142 "gzip" => Ok(Self::Gzip),
143
144 #[cfg(feature = "zstd")]
145 "zstd" => Ok(Self::Zstd),
146 _ => Err(format!("unknown compression algorithm: {s}")),
147 }
148 }
149}
150
151impl Display for CompressionAlgo {
152 #[allow(unreachable_patterns)]
153 #[allow(unused_variables)]
154 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155 match self {
156 #[cfg(feature = "brotli")]
157 Self::Brotli => write!(f, "br"),
158 #[cfg(feature = "deflate")]
159 Self::Deflate => write!(f, "deflate"),
160 #[cfg(feature = "gzip")]
161 Self::Gzip => write!(f, "gzip"),
162 #[cfg(feature = "zstd")]
163 Self::Zstd => write!(f, "zstd"),
164 _ => unreachable!(),
165 }
166 }
167}
168
169impl From<CompressionAlgo> for HeaderValue {
170 #[inline]
171 fn from(algo: CompressionAlgo) -> Self {
172 match algo {
173 #[cfg(feature = "brotli")]
174 CompressionAlgo::Brotli => Self::from_static("br"),
175 #[cfg(feature = "deflate")]
176 CompressionAlgo::Deflate => Self::from_static("deflate"),
177 #[cfg(feature = "gzip")]
178 CompressionAlgo::Gzip => Self::from_static("gzip"),
179 #[cfg(feature = "zstd")]
180 CompressionAlgo::Zstd => Self::from_static("zstd"),
181 }
182 }
183}
184
185#[derive(Clone, Debug)]
187#[non_exhaustive]
188pub struct Compression {
189 pub algos: IndexMap<CompressionAlgo, CompressionLevel>,
191 pub content_types: Vec<Mime>,
193 pub min_length: usize,
201 pub force_priority: bool,
204}
205
206static DEFAULT_CONTENT_TYPES: LazyLock<Vec<Mime>> = LazyLock::new(|| {
207 vec![
208 mime::TEXT_STAR,
209 mime::APPLICATION_JAVASCRIPT,
210 mime::APPLICATION_JSON,
211 mime::IMAGE_SVG,
212 "application/wasm".parse().expect("invalid mime type"),
213 "application/xml".parse().expect("invalid mime type"),
214 "application/rss+xml".parse().expect("invalid mime type"),
215 ]
216});
217
218impl Default for Compression {
219 fn default() -> Self {
220 #[allow(unused_mut)]
221 let mut algos = IndexMap::new();
222 #[cfg(feature = "zstd")]
223 algos.insert(CompressionAlgo::Zstd, CompressionLevel::Default);
224 #[cfg(feature = "gzip")]
225 algos.insert(CompressionAlgo::Gzip, CompressionLevel::Default);
226 #[cfg(feature = "deflate")]
227 algos.insert(CompressionAlgo::Deflate, CompressionLevel::Default);
228 #[cfg(feature = "brotli")]
229 algos.insert(CompressionAlgo::Brotli, CompressionLevel::Default);
230 Self {
231 algos,
232 content_types: DEFAULT_CONTENT_TYPES.clone(),
233 min_length: 1024,
234 force_priority: false,
235 }
236 }
237}
238
239impl Compression {
240 #[inline]
242 #[must_use]
243 pub fn new() -> Self {
244 Default::default()
245 }
246
247 #[inline]
249 #[must_use]
250 pub fn disable_all(mut self) -> Self {
251 self.algos.clear();
252 self
253 }
254
255 #[cfg(feature = "gzip")]
257 #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
258 #[inline]
259 #[must_use]
260 pub fn enable_gzip(mut self, level: CompressionLevel) -> Self {
261 self.algos.insert(CompressionAlgo::Gzip, level);
262 self
263 }
264 #[cfg(feature = "gzip")]
266 #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
267 #[inline]
268 #[must_use]
269 pub fn disable_gzip(mut self) -> Self {
270 self.algos.shift_remove(&CompressionAlgo::Gzip);
271 self
272 }
273 #[cfg(feature = "zstd")]
275 #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
276 #[inline]
277 #[must_use]
278 pub fn enable_zstd(mut self, level: CompressionLevel) -> Self {
279 self.algos.insert(CompressionAlgo::Zstd, level);
280 self
281 }
282 #[cfg(feature = "zstd")]
284 #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
285 #[inline]
286 #[must_use]
287 pub fn disable_zstd(mut self) -> Self {
288 self.algos.shift_remove(&CompressionAlgo::Zstd);
289 self
290 }
291 #[cfg(feature = "brotli")]
293 #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
294 #[inline]
295 #[must_use]
296 pub fn enable_brotli(mut self, level: CompressionLevel) -> Self {
297 self.algos.insert(CompressionAlgo::Brotli, level);
298 self
299 }
300 #[cfg(feature = "brotli")]
302 #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
303 #[inline]
304 #[must_use]
305 pub fn disable_brotli(mut self) -> Self {
306 self.algos.shift_remove(&CompressionAlgo::Brotli);
307 self
308 }
309
310 #[cfg(feature = "deflate")]
312 #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
313 #[inline]
314 #[must_use]
315 pub fn enable_deflate(mut self, level: CompressionLevel) -> Self {
316 self.algos.insert(CompressionAlgo::Deflate, level);
317 self
318 }
319
320 #[cfg(feature = "deflate")]
322 #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))]
323 #[inline]
324 #[must_use]
325 pub fn disable_deflate(mut self) -> Self {
326 self.algos.shift_remove(&CompressionAlgo::Deflate);
327 self
328 }
329
330 #[inline]
336 #[must_use]
337 pub fn min_length(mut self, size: usize) -> Self {
338 self.min_length = size;
339 self
340 }
341 #[inline]
343 #[must_use]
344 pub fn force_priority(mut self, force_priority: bool) -> Self {
345 self.force_priority = force_priority;
346 self
347 }
348
349 #[inline]
351 #[must_use]
352 pub fn content_types(mut self, content_types: &[Mime]) -> Self {
353 self.content_types = content_types.to_vec();
354 self
355 }
356
357 fn negotiate(
358 &self,
359 req: &Request,
360 res: &Response,
361 ) -> Option<(CompressionAlgo, CompressionLevel)> {
362 if !self.content_types.is_empty() {
363 let content_type = res
364 .headers()
365 .get(CONTENT_TYPE)
366 .and_then(|v| v.to_str().ok())
367 .unwrap_or_default();
368 if content_type.is_empty() {
369 return None;
370 }
371 if let Ok(content_type) = content_type.parse::<Mime>() {
372 if !self.content_types.iter().any(|citem| {
373 citem.type_() == content_type.type_()
374 && (citem.subtype() == "*" || citem.subtype() == content_type.subtype())
375 }) {
376 return None;
377 }
378 } else {
379 return None;
380 }
381 }
382 let header = req
383 .headers()
384 .get(ACCEPT_ENCODING)
385 .and_then(|v| v.to_str().ok())?;
386
387 let accept_list = http::parse_accept_encoding(header);
388
389 let wildcard_q: Option<u8> = accept_list
395 .iter()
396 .find(|(name, _)| name == "*")
397 .map(|(_, q)| *q);
398
399 let parsed: smallvec::SmallVec<[(CompressionAlgo, u8); 4]> = accept_list
405 .iter()
406 .filter(|(name, _)| name != "*")
407 .filter_map(|(name, q)| name.parse::<CompressionAlgo>().ok().map(|algo| (algo, *q)))
408 .collect();
409
410 let is_accepted =
411 |algo: &CompressionAlgo| -> bool { parsed.iter().any(|(a, q)| a == algo && *q > 0) };
412 let is_rejected =
413 |algo: &CompressionAlgo| -> bool { parsed.iter().any(|(a, q)| a == algo && *q == 0) };
414
415 if self.force_priority {
416 self.algos
418 .iter()
419 .find(|(algo, _)| {
420 !is_rejected(algo) && (is_accepted(algo) || wildcard_q.is_some_and(|q| q > 0))
421 })
422 .map(|(algo, level)| (*algo, *level))
423 } else {
424 let result = parsed
426 .iter()
427 .filter(|(_, q)| *q > 0)
428 .find_map(|(algo, _)| self.algos.get(algo).map(|level| (*algo, *level)));
429
430 if result.is_some() {
431 return result;
432 }
433
434 if wildcard_q.is_some_and(|q| q > 0) {
436 self.algos
437 .iter()
438 .find(|(algo, _)| !is_rejected(algo))
439 .map(|(algo, level)| (*algo, *level))
440 } else {
441 None
442 }
443 }
444 }
445}
446
447#[async_trait]
448impl Handler for Compression {
449 async fn handle(
450 &self,
451 req: &mut Request,
452 depot: &mut Depot,
453 res: &mut Response,
454 ctrl: &mut FlowCtrl,
455 ) {
456 ctrl.call_next(req, depot, res).await;
457 if ctrl.is_ceased() || res.headers().contains_key(CONTENT_ENCODING) {
458 return;
459 }
460
461 if let Some(StatusCode::SWITCHING_PROTOCOLS | StatusCode::NO_CONTENT) = res.status_code {
462 return;
463 }
464
465 match res.take_body() {
466 ResBody::None => {
467 return;
468 }
469 ResBody::Once(bytes) => {
470 if self.min_length > 0 && bytes.len() < self.min_length {
471 res.body(ResBody::Once(bytes));
472 return;
473 }
474 if let Some((algo, level)) = self.negotiate(req, res) {
475 res.stream(EncodeStream::new(algo, level, Some(bytes)));
476 res.headers_mut().insert(CONTENT_ENCODING, algo.into());
477 } else {
478 res.body(ResBody::Once(bytes));
479 return;
480 }
481 }
482 ResBody::Chunks(chunks) => {
483 if self.min_length > 0 {
484 let len: usize = chunks.iter().map(|c| c.len()).sum();
485 if len < self.min_length {
486 res.body(ResBody::Chunks(chunks));
487 return;
488 }
489 }
490 if let Some((algo, level)) = self.negotiate(req, res) {
491 res.stream(EncodeStream::new(algo, level, chunks));
492 res.headers_mut().insert(CONTENT_ENCODING, algo.into());
493 } else {
494 res.body(ResBody::Chunks(chunks));
495 return;
496 }
497 }
498 ResBody::Hyper(body) => {
499 if let Some((algo, level)) = self.negotiate(req, res) {
500 res.stream(EncodeStream::new(algo, level, body));
501 res.headers_mut().insert(CONTENT_ENCODING, algo.into());
502 } else {
503 res.body(ResBody::Hyper(body));
504 return;
505 }
506 }
507 ResBody::Stream(body) => {
508 let body = body.into_inner();
509 if let Some((algo, level)) = self.negotiate(req, res) {
510 res.stream(EncodeStream::new(algo, level, body));
511 res.headers_mut().insert(CONTENT_ENCODING, algo.into());
512 } else {
513 res.body(ResBody::stream(body));
514 return;
515 }
516 }
517 body => {
518 res.body(body);
519 return;
520 }
521 }
522 res.headers_mut().remove(CONTENT_LENGTH);
523 res.headers_mut()
524 .append(VARY, HeaderValue::from_static("accept-encoding"));
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use salvo_core::prelude::*;
531 use salvo_core::test::{ResponseExt, TestClient};
532
533 use super::*;
534
535 #[handler]
536 async fn hello() -> &'static str {
537 "hello"
538 }
539
540 #[tokio::test]
541 async fn test_gzip() {
542 let comp_handler = Compression::new().min_length(1);
543 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
544
545 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
546 .add_header(ACCEPT_ENCODING, "gzip", true)
547 .send(router)
548 .await;
549 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
550 let content = res.take_string().await.unwrap();
551 assert_eq!(content, "hello");
552 }
553
554 #[tokio::test]
555 async fn test_brotli() {
556 let comp_handler = Compression::new().min_length(1);
557 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
558
559 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
560 .add_header(ACCEPT_ENCODING, "br", true)
561 .send(router)
562 .await;
563 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
564 let content = res.take_string().await.unwrap();
565 assert_eq!(content, "hello");
566 }
567
568 #[tokio::test]
569 async fn test_deflate() {
570 let comp_handler = Compression::new().min_length(1);
571 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
572
573 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
574 .add_header(ACCEPT_ENCODING, "deflate", true)
575 .send(router)
576 .await;
577 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "deflate");
578 let content = res.take_string().await.unwrap();
579 assert_eq!(content, "hello");
580 }
581
582 #[tokio::test]
583 async fn test_zstd() {
584 let comp_handler = Compression::new().min_length(1);
585 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
586
587 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
588 .add_header(ACCEPT_ENCODING, "zstd", true)
589 .send(router)
590 .await;
591 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "zstd");
592 let content = res.take_string().await.unwrap();
593 assert_eq!(content, "hello");
594 }
595
596 #[tokio::test]
597 async fn test_min_length_not_compress() {
598 let comp_handler = Compression::new().min_length(10);
599 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
600
601 let res = TestClient::get("http://127.0.0.1:5801/hello")
602 .add_header(ACCEPT_ENCODING, "gzip", true)
603 .send(router)
604 .await;
605 assert!(res.headers().get(CONTENT_ENCODING).is_none());
606 }
607
608 #[tokio::test]
609 async fn test_min_length_should_compress() {
610 let comp_handler = Compression::new().min_length(1);
611 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
612
613 let res = TestClient::get("http://127.0.0.1:5801/hello")
614 .add_header(ACCEPT_ENCODING, "gzip", true)
615 .send(router)
616 .await;
617 assert!(res.headers().get(CONTENT_ENCODING).is_some());
618 }
619
620 #[handler]
621 async fn hello_html(res: &mut Response) {
622 res.render(Text::Html("<html><body>hello</body></html>"));
623 }
624 #[tokio::test]
625 async fn test_content_types_should_compress() {
626 let comp_handler = Compression::new()
627 .min_length(1)
628 .content_types(&[mime::TEXT_HTML]);
629 let router =
630 Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello_html));
631
632 let res = TestClient::get("http://127.0.0.1:5801/hello")
633 .add_header(ACCEPT_ENCODING, "gzip", true)
634 .send(router)
635 .await;
636 assert!(res.headers().get(CONTENT_ENCODING).is_some());
637 }
638
639 #[tokio::test]
640 async fn test_content_types_not_compress() {
641 let comp_handler = Compression::new()
642 .min_length(1)
643 .content_types(&[mime::APPLICATION_JSON]);
644 let router =
645 Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello_html));
646
647 let res = TestClient::get("http://127.0.0.1:5801/hello")
648 .add_header(ACCEPT_ENCODING, "gzip", true)
649 .send(router)
650 .await;
651 assert!(res.headers().get(CONTENT_ENCODING).is_none());
652 }
653
654 #[tokio::test]
655 async fn test_q_value_preference() {
656 let comp_handler = Compression::new().min_length(1);
658 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
659
660 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
661 .add_header(ACCEPT_ENCODING, "gzip;q=0.5, br;q=1.0", true)
662 .send(router)
663 .await;
664 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
665 let content = res.take_string().await.unwrap();
666 assert_eq!(content, "hello");
667 }
668
669 #[tokio::test]
670 async fn test_q_value_zero_rejects_algo() {
671 let comp_handler = Compression::new().min_length(1);
673 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
674
675 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
676 .add_header(ACCEPT_ENCODING, "gzip;q=0, br", true)
677 .send(router)
678 .await;
679 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
680 let content = res.take_string().await.unwrap();
681 assert_eq!(content, "hello");
682 }
683
684 #[tokio::test]
685 async fn test_identity_only_no_compression() {
686 let comp_handler = Compression::new().min_length(1);
688 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
689
690 let res = TestClient::get("http://127.0.0.1:5801/hello")
691 .add_header(ACCEPT_ENCODING, "identity", true)
692 .send(router)
693 .await;
694 assert!(res.headers().get(CONTENT_ENCODING).is_none());
695 }
696
697 #[tokio::test]
698 async fn test_wildcard_uses_server_algo() {
699 let comp_handler = Compression::new()
701 .disable_all()
702 .enable_gzip(CompressionLevel::Default)
703 .min_length(1);
704 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
705
706 let res = TestClient::get("http://127.0.0.1:5801/hello")
707 .add_header(ACCEPT_ENCODING, "*", true)
708 .send(router)
709 .await;
710 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
711 }
712
713 #[tokio::test]
714 async fn test_wildcard_excludes_rejected_algo() {
715 let comp_handler = Compression::new()
717 .disable_all()
718 .enable_gzip(CompressionLevel::Default)
719 .enable_brotli(CompressionLevel::Default)
720 .min_length(1);
721 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
722
723 let res = TestClient::get("http://127.0.0.1:5801/hello")
724 .add_header(ACCEPT_ENCODING, "*, gzip;q=0", true)
725 .send(router)
726 .await;
727 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
728 }
729
730 #[tokio::test]
731 async fn test_multiple_wildcards_take_highest_q() {
732 let comp_handler = Compression::new()
737 .disable_all()
738 .enable_gzip(CompressionLevel::Default)
739 .min_length(1);
740 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
741
742 let res = TestClient::get("http://127.0.0.1:5801/hello")
743 .add_header(ACCEPT_ENCODING, "*;q=1, *;q=0", true)
744 .send(router)
745 .await;
746 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "gzip");
747 }
748
749 #[tokio::test]
750 async fn test_single_content_encoding_header() {
751 let comp_handler = Compression::new().min_length(1);
753 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
754
755 let res = TestClient::get("http://127.0.0.1:5801/hello")
756 .add_header(ACCEPT_ENCODING, "gzip", true)
757 .send(router)
758 .await;
759 let count = res.headers().get_all(CONTENT_ENCODING).iter().count();
760 assert_eq!(count, 1, "must have exactly one Content-Encoding header");
761 }
762
763 #[tokio::test]
764 async fn test_force_priority() {
765 let comp_handler = Compression::new()
766 .disable_all()
767 .enable_brotli(CompressionLevel::Default)
768 .enable_gzip(CompressionLevel::Default)
769 .min_length(1)
770 .force_priority(true);
771 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
772
773 let mut res = TestClient::get("http://127.0.0.1:5801/hello")
774 .add_header(ACCEPT_ENCODING, "gzip, br", true)
775 .send(router)
776 .await;
777 assert_eq!(res.headers().get(CONTENT_ENCODING).unwrap(), "br");
778 let content = res.take_string().await.unwrap();
779 assert_eq!(content, "hello");
780 }
781
782 #[test]
784 fn test_compression_level_default() {
785 let level: CompressionLevel = Default::default();
786 assert_eq!(level, CompressionLevel::Default);
787 }
788
789 #[test]
790 fn test_compression_level_fastest() {
791 let level = CompressionLevel::Fastest;
792 assert_eq!(level, CompressionLevel::Fastest);
793 }
794
795 #[test]
796 fn test_compression_level_minsize() {
797 let level = CompressionLevel::Minsize;
798 assert_eq!(level, CompressionLevel::Minsize);
799 }
800
801 #[test]
802 fn test_compression_level_precise() {
803 let level = CompressionLevel::Precise(5);
804 assert_eq!(level, CompressionLevel::Precise(5));
805 }
806
807 #[test]
808 fn test_compression_level_clone() {
809 let level = CompressionLevel::Fastest;
810 let cloned = level;
811 assert_eq!(level, cloned);
812 }
813
814 #[test]
815 fn test_compression_level_copy() {
816 let level = CompressionLevel::Default;
817 let copied = level;
818 assert_eq!(level, copied);
819 }
820
821 #[test]
822 fn test_compression_level_debug() {
823 let level = CompressionLevel::Fastest;
824 let debug_str = format!("{level:?}");
825 assert!(debug_str.contains("Fastest"));
826 }
827
828 #[cfg(feature = "gzip")]
830 #[test]
831 fn test_compression_algo_gzip_from_str() {
832 let algo: CompressionAlgo = "gzip".parse().unwrap();
833 assert_eq!(algo, CompressionAlgo::Gzip);
834 }
835
836 #[cfg(feature = "brotli")]
837 #[test]
838 fn test_compression_algo_brotli_from_str() {
839 let algo: CompressionAlgo = "br".parse().unwrap();
840 assert_eq!(algo, CompressionAlgo::Brotli);
841
842 let algo: CompressionAlgo = "brotli".parse().unwrap();
843 assert_eq!(algo, CompressionAlgo::Brotli);
844 }
845
846 #[cfg(feature = "deflate")]
847 #[test]
848 fn test_compression_algo_deflate_from_str() {
849 let algo: CompressionAlgo = "deflate".parse().unwrap();
850 assert_eq!(algo, CompressionAlgo::Deflate);
851 }
852
853 #[cfg(feature = "zstd")]
854 #[test]
855 fn test_compression_algo_zstd_from_str() {
856 let algo: CompressionAlgo = "zstd".parse().unwrap();
857 assert_eq!(algo, CompressionAlgo::Zstd);
858 }
859
860 #[test]
861 fn test_compression_algo_unknown_from_str() {
862 let result: Result<CompressionAlgo, _> = "unknown".parse();
863 assert!(result.is_err());
864 assert!(
865 result
866 .unwrap_err()
867 .contains("unknown compression algorithm")
868 );
869 }
870
871 #[cfg(feature = "gzip")]
872 #[test]
873 fn test_compression_algo_gzip_display() {
874 let algo = CompressionAlgo::Gzip;
875 assert_eq!(format!("{algo}"), "gzip");
876 }
877
878 #[cfg(feature = "brotli")]
879 #[test]
880 fn test_compression_algo_brotli_display() {
881 let algo = CompressionAlgo::Brotli;
882 assert_eq!(format!("{algo}"), "br");
883 }
884
885 #[cfg(feature = "deflate")]
886 #[test]
887 fn test_compression_algo_deflate_display() {
888 let algo = CompressionAlgo::Deflate;
889 assert_eq!(format!("{algo}"), "deflate");
890 }
891
892 #[cfg(feature = "zstd")]
893 #[test]
894 fn test_compression_algo_zstd_display() {
895 let algo = CompressionAlgo::Zstd;
896 assert_eq!(format!("{algo}"), "zstd");
897 }
898
899 #[cfg(feature = "gzip")]
900 #[test]
901 fn test_compression_algo_into_header_value() {
902 let algo = CompressionAlgo::Gzip;
903 let header: HeaderValue = algo.into();
904 assert_eq!(header, "gzip");
905 }
906
907 #[test]
908 fn test_compression_algo_debug() {
909 #[cfg(feature = "gzip")]
910 {
911 let algo = CompressionAlgo::Gzip;
912 let debug_str = format!("{algo:?}");
913 assert!(debug_str.contains("Gzip"));
914 }
915 }
916
917 #[test]
918 fn test_compression_algo_clone() {
919 #[cfg(feature = "gzip")]
920 {
921 let algo = CompressionAlgo::Gzip;
922 let cloned = algo;
923 assert_eq!(algo, cloned);
924 }
925 }
926
927 #[test]
928 fn test_compression_algo_hash() {
929 use std::collections::HashSet;
930 #[cfg(feature = "gzip")]
931 {
932 let mut set = HashSet::new();
933 set.insert(CompressionAlgo::Gzip);
934 assert!(set.contains(&CompressionAlgo::Gzip));
935 }
936 }
937
938 #[test]
940 fn test_compression_new() {
941 let comp = Compression::new();
942 assert!(!comp.algos.is_empty());
943 assert!(!comp.content_types.is_empty());
944 assert_eq!(comp.min_length, 1024);
945 assert!(!comp.force_priority);
946 }
947
948 #[test]
949 fn test_compression_default() {
950 let comp = Compression::default();
951 assert!(!comp.algos.is_empty());
952 }
953
954 #[test]
955 fn test_compression_disable_all() {
956 let comp = Compression::new().disable_all();
957 assert!(comp.algos.is_empty());
958 }
959
960 #[cfg(feature = "gzip")]
961 #[test]
962 fn test_compression_enable_gzip() {
963 let comp = Compression::new()
964 .disable_all()
965 .enable_gzip(CompressionLevel::Fastest);
966 assert!(comp.algos.contains_key(&CompressionAlgo::Gzip));
967 assert_eq!(
968 comp.algos.get(&CompressionAlgo::Gzip),
969 Some(&CompressionLevel::Fastest)
970 );
971 }
972
973 #[cfg(feature = "gzip")]
974 #[test]
975 fn test_compression_disable_gzip() {
976 let comp = Compression::new().disable_gzip();
977 assert!(!comp.algos.contains_key(&CompressionAlgo::Gzip));
978 }
979
980 #[cfg(feature = "brotli")]
981 #[test]
982 fn test_compression_enable_brotli() {
983 let comp = Compression::new()
984 .disable_all()
985 .enable_brotli(CompressionLevel::Minsize);
986 assert!(comp.algos.contains_key(&CompressionAlgo::Brotli));
987 }
988
989 #[cfg(feature = "brotli")]
990 #[test]
991 fn test_compression_disable_brotli() {
992 let comp = Compression::new().disable_brotli();
993 assert!(!comp.algos.contains_key(&CompressionAlgo::Brotli));
994 }
995
996 #[cfg(feature = "zstd")]
997 #[test]
998 fn test_compression_enable_zstd() {
999 let comp = Compression::new()
1000 .disable_all()
1001 .enable_zstd(CompressionLevel::Default);
1002 assert!(comp.algos.contains_key(&CompressionAlgo::Zstd));
1003 }
1004
1005 #[cfg(feature = "zstd")]
1006 #[test]
1007 fn test_compression_disable_zstd() {
1008 let comp = Compression::new().disable_zstd();
1009 assert!(!comp.algos.contains_key(&CompressionAlgo::Zstd));
1010 }
1011
1012 #[cfg(feature = "deflate")]
1013 #[test]
1014 fn test_compression_enable_deflate() {
1015 let comp = Compression::new()
1016 .disable_all()
1017 .enable_deflate(CompressionLevel::Default);
1018 assert!(comp.algos.contains_key(&CompressionAlgo::Deflate));
1019 }
1020
1021 #[cfg(feature = "deflate")]
1022 #[test]
1023 fn test_compression_disable_deflate() {
1024 let comp = Compression::new().disable_deflate();
1025 assert!(!comp.algos.contains_key(&CompressionAlgo::Deflate));
1026 }
1027
1028 #[test]
1029 fn test_compression_min_length() {
1030 let comp = Compression::new().min_length(1024);
1031 assert_eq!(comp.min_length, 1024);
1032 }
1033
1034 #[test]
1035 fn test_compression_force_priority() {
1036 let comp = Compression::new().force_priority(true);
1037 assert!(comp.force_priority);
1038 }
1039
1040 #[test]
1041 fn test_compression_content_types() {
1042 let comp = Compression::new().content_types(&[mime::TEXT_PLAIN, mime::TEXT_HTML]);
1043 assert_eq!(comp.content_types.len(), 2);
1044 assert!(comp.content_types.contains(&mime::TEXT_PLAIN));
1045 assert!(comp.content_types.contains(&mime::TEXT_HTML));
1046 }
1047
1048 #[test]
1049 fn test_compression_debug() {
1050 let comp = Compression::new();
1051 let debug_str = format!("{comp:?}");
1052 assert!(debug_str.contains("Compression"));
1053 assert!(debug_str.contains("algos"));
1054 assert!(debug_str.contains("content_types"));
1055 }
1056
1057 #[test]
1058 fn test_compression_clone() {
1059 let comp = Compression::new().min_length(100);
1060 let cloned = comp.clone();
1061 assert_eq!(comp.min_length, cloned.min_length);
1062 assert_eq!(comp.algos.len(), cloned.algos.len());
1063 }
1064
1065 #[tokio::test]
1067 async fn test_no_accept_encoding_header() {
1068 let comp_handler = Compression::new().min_length(1);
1069 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
1070
1071 let res = TestClient::get("http://127.0.0.1:5801/hello")
1072 .send(router)
1073 .await;
1074 assert!(res.headers().get(CONTENT_ENCODING).is_none());
1075 }
1076
1077 #[tokio::test]
1078 async fn test_unsupported_encoding() {
1079 let comp_handler = Compression::new().min_length(1);
1080 let router = Router::with_hoop(comp_handler).push(Router::with_path("hello").get(hello));
1081
1082 let res = TestClient::get("http://127.0.0.1:5801/hello")
1083 .add_header(ACCEPT_ENCODING, "unknown", true)
1084 .send(router)
1085 .await;
1086 assert!(res.headers().get(CONTENT_ENCODING).is_none());
1087 }
1088
1089 #[tokio::test]
1090 async fn test_empty_response() {
1091 #[handler]
1092 async fn empty() {}
1093
1094 let comp_handler = Compression::new();
1095 let router = Router::with_hoop(comp_handler).push(Router::with_path("empty").get(empty));
1096
1097 let res = TestClient::get("http://127.0.0.1:5801/empty")
1098 .add_header(ACCEPT_ENCODING, "gzip", true)
1099 .send(router)
1100 .await;
1101 assert!(res.headers().get(CONTENT_ENCODING).is_none());
1102 }
1103
1104 #[tokio::test]
1105 async fn test_chained_configuration() {
1106 #[cfg(all(feature = "gzip", feature = "brotli"))]
1107 {
1108 let comp_handler = Compression::new()
1109 .disable_all()
1110 .enable_gzip(CompressionLevel::Fastest)
1111 .enable_brotli(CompressionLevel::Default)
1112 .min_length(1)
1113 .force_priority(false)
1114 .content_types(&[mime::TEXT_PLAIN]);
1115
1116 assert_eq!(comp_handler.algos.len(), 2);
1117 assert_eq!(comp_handler.min_length, 1);
1118 assert!(!comp_handler.force_priority);
1119 assert_eq!(comp_handler.content_types.len(), 1);
1120 }
1121 }
1122}