sentinel_proxy/static_files/
compression.rs1use anyhow::Result;
7use bytes::Bytes;
8use flate2::write::GzEncoder;
9use flate2::Compression;
10use http::{header, Request};
11use std::io::Write;
12use tracing::trace;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ContentEncoding {
21 Identity,
22 Gzip,
23 Brotli,
24}
25
26impl ContentEncoding {
27 pub fn as_str(&self) -> &'static str {
29 match self {
30 ContentEncoding::Identity => "identity",
31 ContentEncoding::Gzip => "gzip",
32 ContentEncoding::Brotli => "br",
33 }
34 }
35}
36
37pub fn should_compress(content_type: &str) -> bool {
43 content_type.starts_with("text/")
44 || content_type.contains("javascript")
45 || content_type.contains("json")
46 || content_type.contains("xml")
47 || content_type.contains("svg")
48 || content_type == "application/wasm"
49}
50
51pub fn negotiate_encoding<B>(req: &Request<B>) -> ContentEncoding {
53 if let Some(accept_encoding) = req.headers().get(header::ACCEPT_ENCODING) {
54 if let Ok(accept_str) = accept_encoding.to_str() {
55 if accept_str.contains("br") {
57 trace!(
58 accept_encoding = %accept_str,
59 selected = "brotli",
60 "Negotiated content encoding"
61 );
62 return ContentEncoding::Brotli;
63 }
64 if accept_str.contains("gzip") {
66 trace!(
67 accept_encoding = %accept_str,
68 selected = "gzip",
69 "Negotiated content encoding"
70 );
71 return ContentEncoding::Gzip;
72 }
73 }
74 }
75 trace!(selected = "identity", "No compression encoding accepted");
76 ContentEncoding::Identity
77}
78
79pub fn compress_content(content: &Bytes, encoding: ContentEncoding) -> Result<Bytes> {
81 let original_size = content.len();
82
83 match encoding {
84 ContentEncoding::Gzip => {
85 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
86 encoder.write_all(content)?;
87 let compressed = encoder.finish()?;
88 let compressed_size = compressed.len();
89
90 trace!(
91 encoding = "gzip",
92 original_size = original_size,
93 compressed_size = compressed_size,
94 ratio = format!("{:.1}%", (compressed_size as f64 / original_size as f64) * 100.0),
95 "Compressed content"
96 );
97
98 Ok(Bytes::from(compressed))
99 }
100 ContentEncoding::Brotli => {
101 let mut compressed = Vec::new();
102 {
103 let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 4, 22);
104 encoder.write_all(content)?;
105 }
106 let compressed_size = compressed.len();
107
108 trace!(
109 encoding = "brotli",
110 original_size = original_size,
111 compressed_size = compressed_size,
112 ratio = format!("{:.1}%", (compressed_size as f64 / original_size as f64) * 100.0),
113 "Compressed content"
114 );
115
116 Ok(Bytes::from(compressed))
117 }
118 ContentEncoding::Identity => {
119 trace!(
120 encoding = "identity",
121 size = original_size,
122 "No compression applied"
123 );
124 Ok(content.clone())
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_should_compress() {
135 assert!(should_compress("text/html"));
136 assert!(should_compress("text/css"));
137 assert!(should_compress("application/javascript"));
138 assert!(should_compress("application/json"));
139 assert!(should_compress("image/svg+xml"));
140 assert!(should_compress("application/wasm"));
141
142 assert!(!should_compress("image/png"));
143 assert!(!should_compress("image/jpeg"));
144 assert!(!should_compress("application/octet-stream"));
145 }
146
147 #[test]
148 fn test_content_encoding_as_str() {
149 assert_eq!(ContentEncoding::Identity.as_str(), "identity");
150 assert_eq!(ContentEncoding::Gzip.as_str(), "gzip");
151 assert_eq!(ContentEncoding::Brotli.as_str(), "br");
152 }
153
154 #[test]
155 fn test_compress_content_gzip() {
156 let content = Bytes::from("Hello, World!");
157 let compressed = compress_content(&content, ContentEncoding::Gzip).unwrap();
158
159 assert!(!compressed.is_empty());
161 }
162
163 #[test]
164 fn test_compress_content_identity() {
165 let content = Bytes::from("Hello, World!");
166 let result = compress_content(&content, ContentEncoding::Identity).unwrap();
167 assert_eq!(result, content);
168 }
169}