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