actix_web_rust_embed_responder/
compress.rs

1use std::{
2    collections::HashMap,
3    io::{BufReader, Write},
4    sync::RwLock,
5};
6
7use brotli::enc::BrotliEncoderParams;
8use flate2::Compression;
9use lazy_static::lazy_static;
10use regex::Regex;
11
12/// When should the server try sending a compressed response?
13#[derive(Default)]
14pub enum Compress {
15    /// Never compress responses, even if a precompressed response is available.
16    Never,
17    ///  Only use a compressed response if a precompressed response is available.
18    ///
19    /// With this option, compression won't be performed "on-the-fly".
20    /// This significantly reduces the CPU usage, but will increase the amount of data transferred.
21    ///
22    /// This option will only work with `rust-embed-for-web` and only if compression has not been disabled.
23    /// With `rust-embed`, or if the `rust-embed-for-web` resource is tagged with `#[gzip = "false"]` this is equivalent to Never.
24    ///
25    #[default]
26    IfPrecompressed,
27    /// Perform on-the-fly compression if the file mime type is well known to be compressible.
28    ///
29    /// This option allows you to use compression with `rust-embed-for-web` when the resource is tagged with `#[gzip = "false"]`.
30    /// This will use some CPU to compress the file on the fly before responding. Compressed versions are cached in memory.
31    ///
32    IfWellKnown,
33    /// With this option set, the file is always compressed (as long as the client supports it).
34    ///
35    /// This is usually not a good idea unless you know that all the files embedded are compressible.
36    /// File formats that are already compressed will not compress any further (such as image or video files),
37    /// in which case trying to use compression is just a waste of CPU time.
38    ///
39    Always,
40}
41
42/// This is basically a list of text mime types, plus javascript, json, and xml.
43pub(crate) fn is_well_known_compressible_mime_type(mime_type: &str) -> bool {
44    lazy_static! {
45        static ref RE: Regex =
46            Regex::new(r#"^text/.*|application/(javascript|json5?|(ld|jsonml)[+]json|xml)$"#)
47                .unwrap();
48    }
49    RE.is_match(mime_type)
50}
51
52// Putting the data into cache could potentially fail. That's okay if it does
53// happen, we have no way of handling that and we might as well just keep
54// serving files.
55#[allow(unused_must_use)]
56/// Compresses data with gzip encoding.
57///
58/// The compressed files are cached based on the hash values provided.
59/// Since we already have the hashes precomputed in rust-embed and rust-embed-for-web,
60/// we just reuse that instead of trying to hash the data this function gets.
61pub(crate) fn compress_data_gzip(hash: &str, data: &[u8]) -> Vec<u8> {
62    lazy_static! {
63        static ref CACHED_GZIP_DATA: RwLock<HashMap<String, Vec<u8>>> = RwLock::new(HashMap::new());
64    }
65
66    if let Some(data_gzip) = CACHED_GZIP_DATA
67        .read()
68        .ok()
69        .and_then(|cached| cached.get(hash).map(ToOwned::to_owned))
70    {
71        return data_gzip;
72    }
73
74    let mut compressed: Vec<u8> = Vec::new();
75    flate2::write::GzEncoder::new(&mut compressed, Compression::default())
76        .write_all(data)
77        .unwrap();
78    CACHED_GZIP_DATA
79        .write()
80        .map(|mut cached| cached.insert(hash.to_string(), compressed.clone()));
81    compressed
82}
83
84// Putting the data into cache could potentially fail. That's okay if it does
85// happen, we have no way of handling that and we might as well just keep
86// serving files.
87#[allow(unused_must_use)]
88/// Compresses data with gzip encoding.
89///
90/// The compressed files are cached based on the hash values provided.
91/// Since we already have the hashes precomputed in rust-embed and rust-embed-for-web,
92/// we just reuse that instead of trying to hash the data this function gets.
93pub(crate) fn compress_data_br(hash: &str, data: &[u8]) -> Vec<u8> {
94    lazy_static! {
95        static ref CACHED_BR_DATA: RwLock<HashMap<String, Vec<u8>>> = RwLock::new(HashMap::new());
96    }
97
98    if let Some(data_gzip) = CACHED_BR_DATA
99        .read()
100        .ok()
101        .and_then(|cached| cached.get(hash).map(ToOwned::to_owned))
102    {
103        return data_gzip;
104    }
105
106    let mut data_read = BufReader::new(data);
107    let mut compressed: Vec<u8> = Vec::new();
108    brotli::BrotliCompress(
109        &mut data_read,
110        &mut compressed,
111        &BrotliEncoderParams::default(),
112    )
113    .expect("Failed to compress br data");
114    CACHED_BR_DATA
115        .write()
116        .map(|mut cached| cached.insert(hash.to_string(), compressed.clone()));
117    compressed
118}
119
120// Putting the data into cache could potentially fail. That's okay if it does
121// happen, we have no way of handling that and we might as well just keep
122// serving files.
123#[allow(unused_must_use)]
124/// Compresses data with zstd encoding.
125///
126/// The compressed files are cached based on the hash values provided.
127/// Since we already have the hashes precomputed in rust-embed and rust-embed-for-web,
128/// we just reuse that instead of trying to hash the data this function gets.
129#[cfg(feature = "compression-zstd")]
130pub(crate) fn compress_data_zstd(hash: &str, data: &[u8]) -> Vec<u8> {
131    lazy_static! {
132        static ref CACHED_ZSTD_DATA: RwLock<HashMap<String, Vec<u8>>> = RwLock::new(HashMap::new());
133    }
134
135    if let Some(data_zstd) = CACHED_ZSTD_DATA
136        .read()
137        .ok()
138        .and_then(|cached| cached.get(hash).map(ToOwned::to_owned))
139    {
140        return data_zstd;
141    }
142
143    let compressed = zstd::encode_all(data, 0).expect("Failed to compress zstd data");
144    CACHED_ZSTD_DATA
145        .write()
146        .map(|mut cached| cached.insert(hash.to_string(), compressed.clone()));
147    compressed
148}
149
150#[allow(unused_imports)]
151mod test {
152    use crate::compress::is_well_known_compressible_mime_type;
153    use crate::{compress_data_br, compress_data_gzip};
154    use std::io::Write;
155    use std::time::Instant;
156
157    #[test]
158    fn html_file_is_compressible() {
159        assert!(is_well_known_compressible_mime_type("text/html"))
160    }
161
162    #[test]
163    fn css_file_is_compressible() {
164        assert!(is_well_known_compressible_mime_type("text/css"))
165    }
166
167    #[test]
168    fn javascript_file_is_compressible() {
169        assert!(is_well_known_compressible_mime_type(
170            "application/javascript"
171        ))
172    }
173
174    #[test]
175    fn json_file_is_compressible() {
176        assert!(is_well_known_compressible_mime_type("application/json"))
177    }
178
179    #[test]
180    fn xml_file_is_compressible() {
181        assert!(is_well_known_compressible_mime_type("application/xml"))
182    }
183
184    #[test]
185    fn jpg_file_not_compressible() {
186        assert!(!is_well_known_compressible_mime_type("image/jpeg"))
187    }
188
189    #[test]
190    fn zip_file_not_compressible() {
191        assert!(!is_well_known_compressible_mime_type("application/zip"))
192    }
193
194    #[test]
195    fn gzip_roundtrip() {
196        let source = b"x123";
197        let compressed = compress_data_gzip("foo", source);
198        let mut decompressed = Vec::new();
199        flate2::write::GzDecoder::new(&mut decompressed)
200            .write_all(&compressed)
201            .unwrap();
202        assert_eq!(source, &decompressed[..]);
203    }
204
205    #[test]
206    fn br_roundtrip() {
207        let source = b"x123";
208        let compressed = compress_data_br("bar", source);
209        let mut decompressed = Vec::new();
210        brotli::BrotliDecompress(&mut &compressed[..], &mut decompressed).unwrap();
211        assert_eq!(source, &decompressed[..]);
212    }
213
214    #[test]
215    fn compression_is_cached() {
216        let source = b"Et quos non sed magnam reiciendis praesentium quod libero. Architecto optio tempora iure aspernatur rerum voluptatem quas. Eos ut atque quas perspiciatis dolorem quidem. Cum et quo et. Voluptatum ut est id eligendi illum inventore. Est non rerum vel rem. Molestiae similique alias nihil harum qui. Consectetur et dolores autem. Magnam et saepe ad reprehenderit. Repellendus vel excepturi eaque esse error. Deserunt est impedit totam nostrum sunt. Eligendi magnam distinctio odit iste molestias est id. Deserunt odit similique magnam repudiandae aut saepe. Dolores laboriosam consectetur quos dolores ea. Non quod veniam quisquam molestias aut deserunt tempora. Mollitia consequuntur facilis doloremque provident eligendi similique possimus. Deleniti facere quam fugiat porro. Tenetur cupiditate eum consequatur beatae dolorum. Veniam voluptatem qui eum quasi corrupti. Quis necessitatibus maxime eum numquam ipsam ducimus expedita maiores. Aliquid voluptas non aut. Tempore dicta ut aperiam ipsum ut et esse explicabo.";
217
218        let first_start = Instant::now();
219        compress_data_gzip("lorem", source);
220        let first = first_start.elapsed();
221        let second_start = Instant::now();
222        compress_data_gzip("lorem", source);
223        let second = second_start.elapsed();
224
225        // Check that the second call was faster
226        assert!(first > second);
227    }
228
229    #[test]
230    fn br_compression_is_cached() {
231        let source = b"Et quos non sed magnam reiciendis praesentium quod libero. Architecto optio tempora iure aspernatur rerum voluptatem quas. Eos ut atque quas perspiciatis dolorem quidem. Cum et quo et. Voluptatum ut est id eligendi illum inventore. Est non rerum vel rem. Molestiae similique alias nihil harum qui. Consectetur et dolores autem. Magnam et saepe ad reprehenderit. Repellendus vel excepturi eaque esse error. Deserunt est impedit totam nostrum sunt. Eligendi magnam distinctio odit iste molestias est id. Deserunt odit similique magnam repudiandae aut saepe. Dolores laboriosam consectetur quos dolores ea. Non quod veniam quisquam molestias aut deserunt tempora. Mollitia consequuntur facilis doloremque provident eligendi similique possimus. Deleniti facere quam fugiat porro. Tenetur cupiditate eum consequatur beatae dolorum. Veniam voluptatem qui eum quasi corrupti. Quis necessitatibus maxime eum numquam ipsam ducimus expedita maiores. Aliquid voluptas non aut. Tempore dicta ut aperiam ipsum ut et esse explicabo.";
232
233        let first_start = Instant::now();
234        compress_data_br("lorem-br", source);
235        let first = first_start.elapsed();
236        let second_start = Instant::now();
237        compress_data_br("lorem-br", source);
238        let second = second_start.elapsed();
239
240        // Check that the second call was faster
241        assert!(first > second);
242    }
243
244    #[test]
245    #[cfg(feature = "compression-zstd")]
246    fn zstd_roundtrip() {
247        let source = b"x123";
248        let compressed = crate::compress_data_zstd("foo", source);
249        let decompressed = zstd::decode_all(&compressed[..]).unwrap();
250        assert_eq!(source, &decompressed[..]);
251    }
252
253    #[test]
254    #[cfg(feature = "compression-zstd")]
255    fn zstd_compression_is_cached() {
256        let source = b"Et quos non sed magnam reiciendis praesentium quod libero. Architecto optio tempora iure aspernatur rerum voluptatem quas. Eos ut atque quas perspiciatis dolorem quidem. Cum et quo et. Voluptatum ut est id eligendi illum inventore. Est non rerum vel rem. Molestiae similique alias nihil harum qui. Consectetur et dolores autem. Magnam et saepe ad reprehenderit. Repellendus vel excepturi eaque esse error. Deserunt est impedit totam nostrum sunt. Eligendi magnam distinctio odit iste molestias est id. Deserunt odit similique magnam repudiandae aut saepe. Dolores laboriosam consectetur quos dolores ea. Non quod veniam quisquam molestias aut deserunt tempora. Mollitia consequuntur facilis doloremque provident eligendi similique possimus. Deleniti facere quam fugiat porro. Tenetur cupiditate eum consequatur beatae dolorum. Veniam voluptatem qui eum quasi corrupti. Quis necessitatibus maxime eum numquam ipsam ducimus expedita maiores. Aliquid voluptas non aut. Tempore dicta ut aperiam ipsum ut et esse explicabo.";
257
258        let first_start = Instant::now();
259        crate::compress_data_zstd("lorem-zstd", source);
260        let first = first_start.elapsed();
261        let second_start = Instant::now();
262        crate::compress_data_zstd("lorem-zstd", source);
263        let second = second_start.elapsed();
264
265        // Check that the second call was faster
266        assert!(first > second);
267    }
268}