Skip to content

Commit f752d7a

Browse files
CopilotSeriousBug
andcommitted
Add zstd compression support
Co-authored-by: SeriousBug <1008124+SeriousBug@users.noreply.github.com>
1 parent 332fc19 commit f752d7a

13 files changed

Lines changed: 114 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ rust-embed-for-web-utils = { version = "11.2.1", path = "utils" }
1919
chrono = { version = "0.4", default-features = false }
2020
flate2 = "1.0"
2121
brotli = "6.0"
22+
zstd = "0.13"
2223
actix-web = "4.4"
2324

2425
[features]
@@ -46,6 +47,11 @@ name = "gzip"
4647
path = "tests/gzip.rs"
4748
required-features = ["always-embed"]
4849

50+
[[test]]
51+
name = "zstd"
52+
path = "tests/zstd.rs"
53+
required-features = ["always-embed"]
54+
4955
[[test]]
5056
name = "include-exclude"
5157
path = "tests/include-exclude.rs"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ executable in exchange for better performance at runtime. In particular:
2424
or decompress anything at runtime.
2525
- If the compression makes little difference, for example a jpeg file won't
2626
compress much further if at all, then the compressed version is not included.
27-
- You can also disable this behavior by adding an attribute `#[gzip = false]` and `#[br = false]`
27+
- You can also disable this behavior by adding an attribute `#[gzip = false]`, `#[br = false]`, or `#[zstd = false]`
2828
When disabled, the compressed files won't be included for that embed.
2929
- Some metadata that is useful for web headers like `ETag` and `Last-Modified`
3030
are computed ahead of time and embedded into the executable. This makes it
@@ -68,8 +68,8 @@ The path for the `folder` is resolved relative to where `Cargo.toml` is.
6868

6969
### Disabling compression
7070

71-
You can add `#[gzip = false]` and/or `#[br = false]` attributes to your embed to
72-
disable gzip and brotli compression for the files in that embed.
71+
You can add `#[gzip = false]`, `#[br = false]`, and/or `#[zstd = false]` attributes to your embed to
72+
disable gzip, brotli, and/or zstd compression for the files in that embed.
7373
`rust-embed-for-web` will only include compressed files where the compression
7474
actually makes files smaller so files that won't compress well like images or
7575
archives already don't include their compressed versions. However you can

impl/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ walkdir = "2.4.0"
2727
# Compression
2828
flate2 = "1.0"
2929
brotli = "6.0"
30+
zstd = "0.13"
3031

3132
globset = { version = "0.4", optional = true }
3233

impl/src/attributes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub(crate) fn read_attribute_config(ast: &syn::DeriveInput) -> Config {
4242
"exclude" => parse_str(attribute).map(|v| config.add_exclude(v)),
4343
"gzip" => parse_bool(attribute).map(|v| config.set_gzip(v)),
4444
"br" => parse_bool(attribute).map(|v| config.set_br(v)),
45+
"zstd" => parse_bool(attribute).map(|v| config.set_zstd(v)),
4546
_ => None,
4647
};
4748
}

impl/src/compress.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::io::{BufReader, Write};
22

33
use brotli::enc::BrotliEncoderParams;
44
use flate2::{write::GzEncoder, Compression};
5+
use zstd::stream::write::Encoder as ZstdEncoder;
56

67
/// Only include the compressed version if it is at least this much smaller than
78
/// the uncompressed version.
@@ -39,3 +40,21 @@ pub(crate) fn compress_br(data: &[u8]) -> Option<Vec<u8>> {
3940
None
4041
}
4142
}
43+
44+
pub(crate) fn compress_zstd(data: &[u8]) -> Option<Vec<u8>> {
45+
let mut data_zstd: Vec<u8> = Vec::new();
46+
let mut encoder = ZstdEncoder::new(&mut data_zstd, 3)
47+
.expect("Failed to create zstd encoder");
48+
encoder
49+
.write_all(data)
50+
.expect("Failed to compress zstd data");
51+
encoder
52+
.finish()
53+
.expect("Failed to finish compression of zstd data");
54+
55+
if data_zstd.len() < ((data.len() as f64) * COMPRESSION_INCLUDE_THRESHOLD) as usize {
56+
Some(data_zstd)
57+
} else {
58+
None
59+
}
60+
}

impl/src/embed.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use proc_macro2::TokenStream as TokenStream2;
22
use rust_embed_for_web_utils::{get_files, Config, DynamicFile, EmbedableFile, FileEntry};
33

4-
use crate::compress::{compress_br, compress_gzip};
4+
use crate::compress::{compress_br, compress_gzip, compress_zstd};
55

66
/// Anything that can be embedded into the program.
77
///
@@ -70,6 +70,11 @@ impl<'t> MakeEmbed for EmbedDynamicFile<'t> {
7070
} else {
7171
None::<Vec<u8>>.make_embed()
7272
};
73+
let data_zstd = if self.config.should_zstd() {
74+
compress_zstd(&data).make_embed()
75+
} else {
76+
None::<Vec<u8>>.make_embed()
77+
};
7378
let data = data.make_embed();
7479
let hash = file.hash().make_embed();
7580
let etag = file.etag().make_embed();
@@ -83,6 +88,7 @@ impl<'t> MakeEmbed for EmbedDynamicFile<'t> {
8388
#data,
8489
#data_gzip,
8590
#data_br,
91+
#data_zstd,
8692
#hash,
8793
#etag,
8894
#last_modified,

impl/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fn impl_rust_embed_for_web(ast: &syn::DeriveInput) -> TokenStream2 {
8787
}
8888
}
8989

90-
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, gzip, br))]
90+
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, gzip, br, zstd))]
9191
/// A folder that is embedded into your program.
9292
///
9393
/// For example:

tests/compression.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ struct Embed;
1111
fn html_files_are_compressed() {
1212
assert!(Embed::get("index.html").unwrap().data_gzip().is_some());
1313
assert!(Embed::get("index.html").unwrap().data_br().is_some());
14+
assert!(Embed::get("index.html").unwrap().data_zstd().is_some());
1415
}
1516

1617
#[test]
@@ -20,6 +21,7 @@ fn image_files_are_not_compressed() {
2021
.data_gzip()
2122
.is_none());
2223
assert!(Embed::get("images/flower.jpg").unwrap().data_br().is_none());
24+
assert!(Embed::get("images/flower.jpg").unwrap().data_zstd().is_none());
2325
}
2426

2527
#[test]
@@ -42,3 +44,11 @@ fn compression_br_roundtrip() {
4244
let decompressed_body = String::from_utf8_lossy(&decompressed[..]);
4345
assert!(decompressed_body.starts_with("<!DOCTYPE html>"));
4446
}
47+
48+
#[test]
49+
fn compression_zstd_roundtrip() {
50+
let compressed = Embed::get("index.html").unwrap().data_zstd().unwrap();
51+
let decompressed = zstd::bulk::decompress(&compressed, 1024 * 1024).unwrap();
52+
let decompressed_body = String::from_utf8_lossy(&decompressed[..]);
53+
assert!(decompressed_body.starts_with("<!DOCTYPE html>"));
54+
}

tests/zstd.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use rust_embed_for_web::{EmbedableFile, RustEmbed};
2+
3+
#[derive(RustEmbed)]
4+
#[folder = "examples/public/"]
5+
struct DefaultZstd;
6+
7+
#[derive(RustEmbed)]
8+
#[folder = "examples/public/"]
9+
#[zstd = false]
10+
struct FalseZstd;
11+
12+
#[derive(RustEmbed)]
13+
#[folder = "examples/public/"]
14+
#[zstd = true]
15+
struct TrueZstd;
16+
17+
#[test]
18+
fn zstd_is_used_by_default() {
19+
let file = DefaultZstd::get("index.html").unwrap();
20+
assert!(file.data_zstd().is_some());
21+
}
22+
23+
#[test]
24+
fn zstd_is_used_when_enabled() {
25+
let file = TrueZstd::get("index.html").unwrap();
26+
assert!(file.data_zstd().is_some());
27+
}
28+
29+
#[test]
30+
fn zstd_is_not_available_when_disabled() {
31+
let file = FalseZstd::get("index.html").unwrap();
32+
assert!(file.data_zstd().is_none());
33+
}
34+
35+
#[test]
36+
fn image_files_dont_get_zstd_compressed() {
37+
let file = DefaultZstd::get("images/flower.jpg").unwrap();
38+
assert!(file.data_zstd().is_none());
39+
}

utils/src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub struct Config {
99
exclude: Vec<GlobMatcher>,
1010
gzip: bool,
1111
br: bool,
12+
zstd: bool,
1213
}
1314

1415
impl Default for Config {
@@ -20,6 +21,7 @@ impl Default for Config {
2021
exclude: vec![],
2122
gzip: true,
2223
br: true,
24+
zstd: true,
2325
}
2426
}
2527
}
@@ -56,6 +58,10 @@ impl Config {
5658
self.br = status;
5759
}
5860

61+
pub fn set_zstd(&mut self, status: bool) {
62+
self.zstd = status;
63+
}
64+
5965
#[cfg(feature = "include-exclude")]
6066
pub fn get_includes(&self) -> &Vec<GlobMatcher> {
6167
&self.include
@@ -99,4 +105,8 @@ impl Config {
99105
pub fn should_br(&self) -> bool {
100106
self.br
101107
}
108+
109+
pub fn should_zstd(&self) -> bool {
110+
self.zstd
111+
}
102112
}

0 commit comments

Comments
 (0)