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