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!(
95 "{:.1}%",
96 (compressed_size as f64 / original_size as f64) * 100.0
97 ),
98 "Compressed content"
99 );
100
101 Ok(Bytes::from(compressed))
102 }
103 ContentEncoding::Brotli => {
104 let mut compressed = Vec::new();
105 {
106 let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 4, 22);
107 encoder.write_all(content)?;
108 }
109 let compressed_size = compressed.len();
110
111 trace!(
112 encoding = "brotli",
113 original_size = original_size,
114 compressed_size = compressed_size,
115 ratio = format!(
116 "{:.1}%",
117 (compressed_size as f64 / original_size as f64) * 100.0
118 ),
119 "Compressed content"
120 );
121
122 Ok(Bytes::from(compressed))
123 }
124 ContentEncoding::Identity => {
125 trace!(
126 encoding = "identity",
127 size = original_size,
128 "No compression applied"
129 );
130 Ok(content.clone())
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_should_compress() {
141 assert!(should_compress("text/html"));
142 assert!(should_compress("text/css"));
143 assert!(should_compress("application/javascript"));
144 assert!(should_compress("application/json"));
145 assert!(should_compress("image/svg+xml"));
146 assert!(should_compress("application/wasm"));
147
148 assert!(!should_compress("image/png"));
149 assert!(!should_compress("image/jpeg"));
150 assert!(!should_compress("application/octet-stream"));
151 }
152
153 #[test]
154 fn test_content_encoding_as_str() {
155 assert_eq!(ContentEncoding::Identity.as_str(), "identity");
156 assert_eq!(ContentEncoding::Gzip.as_str(), "gzip");
157 assert_eq!(ContentEncoding::Brotli.as_str(), "br");
158 }
159
160 #[test]
161 fn test_compress_content_gzip() {
162 let content = Bytes::from("Hello, World!");
163 let compressed = compress_content(&content, ContentEncoding::Gzip).unwrap();
164
165 assert!(!compressed.is_empty());
167 }
168
169 #[test]
170 fn test_compress_content_identity() {
171 let content = Bytes::from("Hello, World!");
172 let result = compress_content(&content, ContentEncoding::Identity).unwrap();
173 assert_eq!(result, content);
174 }
175}