~repos /rust-embed

#rust#proc-macro#http

git clone https://pyrossh.dev/repos/rust-embed.git
Discussions: https://groups.google.com/g/rust-embed-devs

rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.


.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ target
2
+ Cargo.lock
3
+
4
+ .vscode
5
+ .idea
.pants-ignore ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "ignore": [{ "id": "sonatype-2021-4646", "reason": "We are handling it this case. Sonatype doesn't recognize that." }]
3
+ }
Cargo.toml ADDED
@@ -0,0 +1,113 @@
1
+ [package]
2
+ name = "rust-embed"
3
+ version = "8.11.0"
4
+ description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev"
5
+ readme = "README.md"
6
+ documentation = "https://docs.rs/rust-embed"
7
+ repository = "https://pyrossh.dev/repos/rust-embed"
8
+ license = "MIT"
9
+ keywords = ["http", "rocket", "static", "web", "server"]
10
+ categories = ["web-programming", "filesystem"]
11
+ authors = ["pyrossh"]
12
+ edition = "2018"
13
+ rust-version = "1.70.0"
14
+
15
+ [[example]]
16
+ name = "warp"
17
+ path = "examples/warp.rs"
18
+ required-features = ["warp-ex"]
19
+
20
+ [[example]]
21
+ name = "actix"
22
+ path = "examples/actix.rs"
23
+ required-features = ["actix"]
24
+
25
+ [[example]]
26
+ name = "rocket"
27
+ path = "examples/rocket.rs"
28
+ required-features = ["rocket"]
29
+
30
+ [[example]]
31
+ name = "axum"
32
+ path = "examples/axum.rs"
33
+ required-features = ["axum-ex"]
34
+
35
+ [[example]]
36
+ name = "axum-spa"
37
+ path = "examples/axum-spa/main.rs"
38
+ required-features = ["axum-ex"]
39
+
40
+ [[example]]
41
+ name = "poem"
42
+ path = "examples/poem.rs"
43
+ required-features = ["poem-ex"]
44
+
45
+ [[example]]
46
+ name = "salvo"
47
+ path = "examples/salvo.rs"
48
+ required-features = ["salvo-ex"]
49
+
50
+ [[test]]
51
+ name = "interpolated_path"
52
+ path = "tests/interpolated_path.rs"
53
+ required-features = ["interpolate-folder-path"]
54
+
55
+ [[test]]
56
+ name = "include_exclude"
57
+ path = "tests/include_exclude.rs"
58
+ required-features = ["include-exclude"]
59
+
60
+ [[test]]
61
+ name = "mime_guess"
62
+ path = "tests/mime_guess.rs"
63
+ required-features = ["mime-guess"]
64
+
65
+ [dependencies]
66
+ walkdir = "2.3.2"
67
+ rust-embed-impl = { version = "8.9.0", path = "impl" }
68
+ rust-embed-utils = { version = "8.9.0", path = "utils" }
69
+
70
+ include-flate = { version = "0.3", optional = true }
71
+ actix-web = { version = "4", optional = true }
72
+ mime_guess = { version = "2.0.5", optional = true }
73
+ hex = { version = "0.4.3", optional = true }
74
+ tokio = { version = "1.0", optional = true, features = [
75
+ "macros",
76
+ "rt-multi-thread",
77
+ ] }
78
+ warp = { version = "0.3", default-features = false, optional = true }
79
+ rocket = { version = "0.5.0-rc.2", default-features = false, optional = true }
80
+ axum = { version = "0.8", default-features = false, features = [
81
+ "http1",
82
+ "tokio",
83
+ ], optional = true }
84
+ poem = { version = "1.3.30", default-features = false, features = [
85
+ "server",
86
+ ], optional = true }
87
+ salvo = { version = "0.16", default-features = false, optional = true }
88
+
89
+ [dev-dependencies]
90
+ sha2 = "0.10"
91
+
92
+ [features]
93
+ debug-embed = ["rust-embed-impl/debug-embed", "rust-embed-utils/debug-embed"]
94
+ interpolate-folder-path = ["rust-embed-impl/interpolate-folder-path"]
95
+ compression = ["rust-embed-impl/compression", "include-flate"]
96
+ mime-guess = ["rust-embed-impl/mime-guess", "rust-embed-utils/mime-guess"]
97
+ include-exclude = [
98
+ "rust-embed-impl/include-exclude",
99
+ "rust-embed-utils/include-exclude",
100
+ ]
101
+ actix = ["actix-web", "mime_guess"]
102
+ warp-ex = ["warp", "tokio", "mime_guess"]
103
+ axum-ex = ["axum", "tokio", "mime_guess"]
104
+ poem-ex = ["poem", "tokio", "mime_guess", "hex"]
105
+ salvo-ex = ["salvo", "tokio", "mime_guess", "hex"]
106
+ deterministic-timestamps = ["rust-embed-impl/deterministic-timestamps"]
107
+
108
+
109
+ [badges]
110
+ maintenance = { status = "passively-maintained" }
111
+
112
+ [workspace]
113
+ members = ["impl", "utils"]
LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 pyros2097
3
+ Copyright (c) 2018 pyros2097
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
README.md ADDED
@@ -0,0 +1,177 @@
1
+ # rust-embed
2
+
3
+ Rust macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev.
4
+
5
+ You can use this to embed your css, js and images into a single executable which can be deployed to your servers. Also it makes it easy to build a very small docker image for you to deploy.
6
+
7
+ ## Installation
8
+
9
+ ```toml
10
+ [dependencies]
11
+ rust-embed="8.11.0"
12
+ ```
13
+
14
+ ## Documentation
15
+
16
+ You need to add the custom derive macro RustEmbed to your struct with an attribute `folder` which is the path to your static folder.
17
+
18
+ The path resolution works as follows:
19
+
20
+ - In `debug` and when `debug-embed` feature is not enabled, the folder path is resolved relative to where the binary is run from.
21
+ - In `release` or when `debug-embed` feature is enabled, the folder path is resolved relative to where `Cargo.toml` is.
22
+
23
+ ```rust
24
+ #[derive(Embed)]
25
+ #[folder = "examples/public/"]
26
+ struct Asset;
27
+ ```
28
+
29
+ The macro will generate the following code:
30
+
31
+ ```rust
32
+ impl Asset {
33
+ pub fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
34
+ ...
35
+ }
36
+
37
+ pub fn iter() -> impl Iterator<Item = Cow<'static, str>> {
38
+ ...
39
+ }
40
+ }
41
+ impl RustEmbed for Asset {
42
+ fn get(file_path: &str) -> Option<rust_embed::EmbeddedFile> {
43
+ ...
44
+ }
45
+ fn iter() -> impl Iterator<Item = Cow<'static, str>> {
46
+ ...
47
+ }
48
+ }
49
+
50
+ // Where EmbeddedFile contains these fields,
51
+ pub struct EmbeddedFile {
52
+ pub data: Cow<'static, [u8]>,
53
+ pub metadata: Metadata,
54
+ }
55
+ pub struct Metadata {
56
+ hash: [u8; 32],
57
+ last_modified: Option<u64>,
58
+ created: Option<u64>,
59
+ }
60
+ ```
61
+
62
+ ## Methods
63
+ * `get(file_path: &str) -> Option<rust_embed::EmbeddedFile>`
64
+
65
+ Given a relative path from the assets folder returns the `EmbeddedFile` if found.
66
+ If the feature `debug-embed` is enabled or the binary compiled in release mode the bytes have been embeded in the binary and a `Option<rust_embed::EmbeddedFile>` is returned.
67
+ Otherwise the bytes are read from the file system on each call and a `Option<rust_embed::EmbeddedFile>` is returned.
68
+
69
+ * `iter()`
70
+
71
+ Iterates the files in this assets folder.
72
+ If the feature `debug-embed` is enabled or the binary compiled in release mode a static array to the list of relative paths to the files is returned.
73
+ Otherwise the files are listed from the file system on each call.
74
+
75
+ ## Attributes
76
+ * `prefix`
77
+
78
+ You can add `#[prefix = "my_prefix/"]` to the `RustEmbed` struct to add a prefix
79
+ to all of the file paths. This prefix will be required on `get` calls, and will
80
+ be included in the file paths returned by `iter`.
81
+
82
+ * `metadata_only`
83
+
84
+ You can add `#[metadata_only = true]` to the `RustEmbed` struct to exclude file contents from the
85
+ binary. Only file paths and metadata will be embedded.
86
+
87
+ * `allow_missing`
88
+
89
+ You can add `#[allow_missing = true]` to the `RustEmbed` struct to allow the embedded folder to be missing.
90
+ In that case, RustEmbed will be empty.
91
+
92
+ ## Features
93
+
94
+ * `debug-embed`: Always embed the files in the binary, even in debug mode.
95
+ * `compression`: Compress each file when embedding into the binary. Compression is done via [include-flate](https://crates.io/crates/include-flate).
96
+ * `deterministic-timestamps`: Overwrite embedded files' timestamps with `0` to preserve deterministic builds with `debug-embed` or release mode.
97
+ * `interpolate-folder-path`: Allow environment variables to be used in the `folder` path. This will pull the `foo` directory relative to your `Cargo.toml` file.
98
+ ```rust
99
+ #[derive(Embed)]
100
+ #[folder = "$CARGO_MANIFEST_DIR/foo"]
101
+ struct Asset;
102
+ ```
103
+ * `include-exclude`: Filter files to be embedded with multiple `#[include = "*.txt"]` and `#[exclude = "*.jpg"]` attributes.
104
+ Matching is done on relative file paths, via [globset](https://crates.io/crates/globset). `exclude` attributes have higher priority than `include` attributes.
105
+ ```rust
106
+ use rust_embed::Embed;
107
+
108
+ #[derive(Embed)]
109
+ #[folder = "examples/public/"]
110
+ #[include = "*.html"]
111
+ #[include = "images/*"]
112
+ #[exclude = "*.txt"]
113
+ struct Asset;
114
+ ```
115
+
116
+ ## Usage
117
+
118
+ ```rust
119
+ use rust_embed::Embed;
120
+
121
+ #[derive(Embed)]
122
+ #[folder = "examples/public/"]
123
+ #[prefix = "prefix/"]
124
+ struct Asset;
125
+
126
+ fn main() {
127
+ let index_html = Asset::get("prefix/index.html").unwrap();
128
+ println!("{:?}", std::str::from_utf8(index_html.data.as_ref()));
129
+
130
+ for file in Asset::iter() {
131
+ println!("{}", file.as_ref());
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Integrations
137
+
138
+ 1. [Poem](https://github.com/poem-web/poem) for poem framework under feature flag "embed"
139
+ 2. [warp_embed](https://docs.rs/warp-embed/latest/warp_embed/) for warp framework
140
+
141
+ ## Examples
142
+
143
+ ```sh
144
+ cargo run --example basic # dev mode where it reads from the fs
145
+ cargo run --example basic --release # release mode where it reads from binary
146
+ cargo run --example actix --features actix # https://github.com/actix/actix-web
147
+ cargo run --example rocket --features rocket # https://github.com/SergioBenitez/Rocket
148
+ cargo run --example warp --features warp-ex # https://github.com/seanmonstar/warp
149
+ cargo run --example axum --features axum-ex # https://github.com/tokio-rs/axum
150
+ cargo run --example poem --features poem-ex # https://github.com/poem-web/poem
151
+ cargo run --example salvo --features salvo-ex # https://github.com/salvo-rs/salvo
152
+ ```
153
+
154
+ ## Testing
155
+
156
+ ```sh
157
+ cargo test --test lib
158
+ cargo test --test lib --features "debug-embed"
159
+ cargo test --test lib --features "compression" --release
160
+ cargo test --test mime_guess --features "mime-guess"
161
+ cargo test --test mime_guess --features "mime-guess" --release
162
+ cargo test --test interpolated_path --features "interpolate-folder-path"
163
+ cargo test --test interpolated_path --features "interpolate-folder-path" --release
164
+ cargo test --test custom_crate_path
165
+ cargo test --test custom_crate_path --release
166
+ cargo build --example basic
167
+ cargo build --example rocket --features rocket
168
+ cargo build --example actix --features actix
169
+ cargo build --example axum --features axum-ex
170
+ cargo build --example warp --features warp-ex
171
+ cargo test --test lib --release
172
+ cargo build --example basic --release
173
+ cargo build --example rocket --features rocket --release
174
+ cargo build --example actix --features actix --release
175
+ cargo build --example axum --features axum-ex --release
176
+ cargo build --example warp --features warp-ex --release
177
+ ```
changelog.md ADDED
@@ -0,0 +1,406 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
+
8
+ Thanks to [Mark Drobnak](https://github.com/AzureMarker) for the changelog.
9
+
10
+ ## [8.11.0] - 2026-01-14
11
+
12
+ - Fix RustEmbed::iter() signature as it was breaking the static lifetime.
13
+
14
+ ## [8.10.0] - 2026-01-13
15
+
16
+ - RustEmbed::iter(): replace concrete return type with generic iterator, to make it easier to implement an empty fake embed. Thanks to Johannes Altmanninger <aclopte@gmail.com>.
17
+
18
+ ## [8.9.0] - 2025-10-31
19
+
20
+ - Ignore paths that couldn't be canonicalized. Thanks to zvolin <mac.zwolinski@gmail.com>.
21
+
22
+ ## [8.9.0] - 2025-10-18
23
+
24
+ - fix export of RustEmbed macro. Thanks to LorrensP-2158466 <lorrens.pantelis@student.uhasselt.be>.
25
+
26
+ ## [8.7.2] - 2025-05-14
27
+
28
+ - Update repo links to sub-packages
29
+
30
+ ## [8.7.1] - 2025-05-05
31
+
32
+ - Update documentation and repo links
33
+
34
+ ## [8.7.0] - 2025-04-10
35
+
36
+ - add deterministic timestamps flag for deterministic builds [#259](https://github.com/pyrossh/rust-embed/pull/259). Thanks to [daywalker90](https://github.com/daywalker90)
37
+
38
+
39
+ ## [8.6.0] - 2025-02-25
40
+
41
+ - Update include-flate to 0.3 [#246](https://github.com/pyrossh/rust-embed/pull/246). Thanks to [krant](https://github.com/krant)
42
+ - refactor: remove redundant reference and closure [#250](https://github.com/pyrossh/rust-embed/pull/250). Thanks to [hamirmahal](https://github.com/hamirmahal)
43
+ - refactor: replace map().unwrap_or_else(). [#250](https://github.com/pyrossh/rust-embed/pull/255). Thanks to [hamirmahal](https://github.com/hamirmahal)
44
+ - Compatible with Axum 0.7.9 [#253](https://github.com/pyrossh/rust-embed/pull/253). Thanks to [wkmyws](https://github.com/wkmyws)
45
+ - Add allow_missing option to derive macro [#256](https://github.com/pyrossh/rust-embed/pull/256). Thanks to [lirannl](https://github.com/lirannl)
46
+
47
+
48
+ ## [8.5.0] - 2024-07-09
49
+
50
+ - Allow users to specify a custom path to the rust_embed crate in generated code[#232](https://github.com/pyrossh/rust-embed/pull/232). Thanks to [Wulf](https://github.com/Wulf)
51
+ - Increase minimum rust-version to v1.7.0.0
52
+
53
+ ## [8.4.0] - 2024-05-11
54
+
55
+ - Re-export RustEmbed as Embed [#245](https://github.com/pyrossh/rust-embed/pull/245/files). Thanks to [pyrossh](https://github.com/pyrossh)
56
+ - Do not build glob matchers repeatedly when include-exclude feature is enabled [#244](https://github.com/pyrossh/rust-embed/pull/244/files). Thanks to [osiewicz](https://github.com/osiewicz)
57
+ - Add `metadata_only` attribute [#241](https://github.com/pyrossh/rust-embed/pull/241/files). Thanks to [ddfisher](https://github.com/ddfisher)
58
+ - Replace `expect` with a safer alternative that returns `None` instead [#240](https://github.com/pyrossh/rust-embed/pull/240/files). Thanks to [costinsin](https://github.com/costinsin)
59
+ - Eliminate unnecessary `to_path` call [#239](https://github.com/pyrossh/rust-embed/pull/239/files). Thanks to [smoelius](https://github.com/smoelius)
60
+
61
+ ## [8.3.0] - 2024-02-26
62
+
63
+ - Fix symbolic links in debug builds [#235](https://github.com/pyrossh/rust-embed/pull/235/files). Thanks to [Buckram123](https://github.com/Buckram123)
64
+
65
+ ## [8.2.0] - 2023-12-29
66
+
67
+ - Fix naming collisions in macros [#230](https://github.com/pyrossh/rust-embed/pull/230/files). Thanks to [hwittenborn](https://github.com/hwittenborn)
68
+
69
+ ## [8.1.0] - 2023-12-08
70
+
71
+ - Add created to file metadata. [#225](https://github.com/pyrossh/rust-embed/pull/225/files). Thanks to [ngalaiko](https://github.com/ngalaiko)
72
+
73
+ ## [8.0.0] - 2023-08-23
74
+
75
+ - Store file contents statically and use binary search for lookup. [#217](https://github.com/pyrossh/rust-embed/pull/217/files). Thanks to [osiewicz](https://github.com/osiewicz)
76
+
77
+ ## [6.8.1] - 2023-06-30
78
+
79
+ - Fix failing compilation under compression feature [#182](https://github.com/pyrossh/rust-embed/issues/182). Thanks to [osiewicz](https://github.com/osiewicz)
80
+
81
+ ## [6.8.0] - 2023-06-30
82
+
83
+ - Update `include-flate` to v0.2 [#182](https://github.com/pyrossh/rust-embed/issues/182)
84
+
85
+ ## [6.7.0] - 2023-06-09
86
+
87
+ - Update `syn` to v2.0 [#211](https://github.com/pyrossh/rust-embed/issues/211)
88
+
89
+ ## [6.6.1] - 2023-03-25
90
+
91
+ - Fix mime-guess feature not working properly [#209](https://github.com/pyrossh/rust-embed/issues/209)
92
+
93
+ ## [6.6.0] - 2023-03-05
94
+
95
+ - sort_by_file_name() requires walkdir v2.3.2 [#206](https://github.com/pyrossh/rust-embed/issues/206)
96
+ - Add `mime-guess` feature to statically store mimetype [#192](https://github.com/pyrossh/rust-embed/issues/192)
97
+
98
+ ## [6.4.2] - 2022-10-20
99
+
100
+ - Fail the proc macro if include/exclude are used without the feature [#187](https://github.com/pyrossh/rust-embed/issues/187)
101
+
102
+ ## [6.4.1] - 2022-09-13
103
+
104
+ - Update sha2 dependency version in utils crate [#186](https://github.com/pyrossh/rust-embed/issues/186)
105
+
106
+ ## [6.4.0] - 2022-04-15
107
+
108
+ - Order files by filename [#171](https://github.com/pyros2097/rust-embed/issues/171). Thanks to [apognu](https://github.com/apognu)
109
+
110
+ ## [6.3.0] - 2021-11-28
111
+
112
+ - Fixed a security issue in debug mode [#159](https://github.com/pyros2097/rust-embed/issues/159). Thanks to [5225225](https://github.com/5225225)
113
+
114
+ ## [6.2.0] - 2021-09-01
115
+
116
+ - Fixed `include-exclude` feature when using cargo v2 resolver
117
+
118
+ ## [6.1.0] - 2021-08-31
119
+
120
+ - Added `include-exclude` feature by [mbme](https://github.com/mbme)
121
+
122
+ ## [6.0.1] - 2021-08-21
123
+
124
+ - Added doc comments to macro generated functions
125
+
126
+ ## [6.0.0] - 2021-08-01
127
+
128
+ Idea came about from [Cody Casterline](https://github.com/NfNitLoop)
129
+
130
+ - Breaking change the `Asset::get()` api has changed and now returns an `EmbeddedFile` which contains a `data` field which is the bytes of the file and
131
+ a `metadata` field which has theses 2 properties associated to the file `hash` and `last_modified`;
132
+
133
+ ## [5.9.0] - 2021-01-18
134
+
135
+ ### Added
136
+
137
+ - Added path prefix attribute
138
+
139
+ ## [5.8.0] - 2021-01-06
140
+
141
+ ### Fixed
142
+
143
+ - Fixed compiling with latest version of syn
144
+
145
+ ## [5.7.0] - 2020-12-08
146
+
147
+ ### Fixed
148
+
149
+ - Fix feature flag typo
150
+
151
+ ## [5.6.0] - 2020-07-19
152
+
153
+ ### Fixed
154
+
155
+ - Fixed windows path error in release mode
156
+
157
+ ### Changed
158
+
159
+ - Using github actions for CI now
160
+
161
+ ## [5.5.1] - 2020-03-19
162
+
163
+ ### Fixed
164
+
165
+ - Fixed warnings in latest nightly
166
+
167
+ ## [5.5.0] - 2020-02-26
168
+
169
+ ### Fixed
170
+
171
+ - Fixed the `folder` directory being relative to the current directory.
172
+ It is now relative to `Cargo.toml`.
173
+
174
+ ## [5.4.0] - 2020-02-24
175
+
176
+ ### Changed
177
+
178
+ - using rust-2018 edition now
179
+ - code cleanup
180
+ - updated examples and crates
181
+
182
+ ## [5.3.0] - 2020-02-15
183
+
184
+ ### Added
185
+
186
+ - `compression` feature for compressing embedded files
187
+
188
+ ## [5.2.0] - 2019-12-05
189
+
190
+ ## Changed
191
+
192
+ - updated syn and quote crate to 1.x
193
+
194
+ ## [5.1.0] - 2019-07-09
195
+
196
+ ## Fixed
197
+
198
+ - error when debug code tries to import the utils crate
199
+
200
+ ## [5.0.1] - 2019-07-07
201
+
202
+ ## Changed
203
+
204
+ - derive is allowed only on unit structs now
205
+
206
+ ## [5.0.0] - 2019-07-05
207
+
208
+ ## Added
209
+
210
+ - proper error message stating only unit structs are supported
211
+
212
+ ## Fixed
213
+
214
+ - windows latest build
215
+
216
+ ## [4.5.0] - 2019-06-29
217
+
218
+ ## Added
219
+
220
+ - allow rust embed derive to take env variables in the folder path
221
+
222
+ ## [4.4.0] - 2019-05-11
223
+
224
+ ### Fixed
225
+
226
+ - a panic when struct has doc comments
227
+
228
+ ### Added
229
+
230
+ - a warp example
231
+
232
+ ## [4.3.0] - 2019-01-10
233
+
234
+ ### Fixed
235
+
236
+ - debug_embed feature was not working at all
237
+
238
+ ### Added
239
+
240
+ - a test run for debug_embed feature
241
+
242
+ ## [4.2.0] - 2018-12-02
243
+
244
+ ### Changed
245
+
246
+ - return `Cow<'static, [u8]>` to preserve static lifetime
247
+
248
+ ## [4.1.0] - 2018-10-24
249
+
250
+ ### Added
251
+
252
+ - `iter()` method to list files
253
+
254
+ ## [4.0.0] - 2018-10-11
255
+
256
+ ### Changed
257
+
258
+ - avoid vector allocation by returning `impl AsRef<[u8]>`
259
+
260
+ ## [3.0.2] - 2018-09-05
261
+
262
+ ### Added
263
+
264
+ - appveyor for testing on windows
265
+
266
+ ### Fixed
267
+
268
+ - handle paths in windows correctly
269
+
270
+ ## [3.0.1] - 2018-07-24
271
+
272
+ ### Added
273
+
274
+ - panic if the folder cannot be found
275
+
276
+ ## [3.0.0] - 2018-06-01
277
+
278
+ ### Changed
279
+
280
+ - The derive attribute style so we don't need `attr_literals` and it can be used in stable rust now. Thanks to [Mcat12](https://github.com/Mcat12).
281
+
282
+ ```rust
283
+ #[folder("assets/")]
284
+ ```
285
+
286
+ to
287
+
288
+ ```rust
289
+ #[folder = "assets/"]
290
+ ```
291
+
292
+ ### Removed
293
+
294
+ - log dependecy as we are not using it anymore
295
+
296
+ ## [2.0.0] - 2018-05-26
297
+
298
+ ### Changed
299
+
300
+ - Reimplemented the macro for release to use include_bytes for perf sake. Thanks to [lukad](https://github.com/lukad).
301
+
302
+ ## [1.1.1] - 2018-03-19
303
+
304
+ ### Changed
305
+
306
+ - Fixed usage error message
307
+
308
+ ## [1.1.0] - 2018-03-19
309
+
310
+ ### Added
311
+
312
+ - Release mode for custom derive
313
+
314
+ ### Changed
315
+
316
+ - Fixed tests in travis
317
+
318
+ ## [1.0.0] - 2018-03-18
319
+
320
+ ### Changed
321
+
322
+ - Converted the rust-embed macro `embed!` into a Rust Custom Derive Macro `#[derive(RustEmbed)]` which implements get on the struct
323
+
324
+ ```rust
325
+ let asset = embed!("examples/public/")
326
+ ```
327
+
328
+ to
329
+
330
+ ```rust
331
+ #[derive(RustEmbed)]
332
+ #[folder = "examples/public/"]
333
+ struct Asset;
334
+ ```
335
+
336
+ ## [0.5.2] - 2018-03-16
337
+
338
+ ### Added
339
+
340
+ - rouille example
341
+
342
+ ## [0.5.1] - 2018-03-16
343
+
344
+ ### Removed
345
+
346
+ - the plugin attribute from crate
347
+
348
+ ## [0.5.0] - 2018-03-16
349
+
350
+ ### Added
351
+
352
+ - rocket example
353
+
354
+ ### Changed
355
+
356
+ - Converted the rust-embed executable into a macro `embed!` which now loads files at compile time during release and from the fs during dev.
357
+
358
+ ## [0.4.0] - 2017-03-2
359
+
360
+ ### Changed
361
+
362
+ - `generate_assets` to public again
363
+
364
+ ## [0.3.5] - 2017-03-2
365
+
366
+ ### Added
367
+
368
+ - rust-embed prefix to all logs
369
+
370
+ ## [0.3.4] - 2017-03-2
371
+
372
+ ### Changed
373
+
374
+ - the lib to be plugin again
375
+
376
+ ## [0.3.3] - 2017-03-2
377
+
378
+ ### Changed
379
+
380
+ - the lib to be proc-macro from plugin
381
+
382
+ ## [0.3.2] - 2017-03-2
383
+
384
+ ### Changed
385
+
386
+ - lib name from `rust-embed` to `rust_embed`
387
+
388
+ ## [0.3.1] - 2017-03-2
389
+
390
+ ### Removed
391
+
392
+ - hyper example
393
+
394
+ ## [0.3.0] - 2017-02-26
395
+
396
+ ### Added
397
+
398
+ - rust-embed executable which generates rust code to embed resource files into your rust executable
399
+ it creates a file like assets.rs that contains the code for your assets.
400
+
401
+ ## [0.2.0] - 2017-03-16
402
+
403
+ ### Added
404
+
405
+ - rust-embed executable which generates rust code to embed resource files into your rust executable
406
+ it creates a file like assets.rs that contains the code for your assets.
examples/actix.rs ADDED
@@ -0,0 +1,31 @@
1
+ use actix_web::{web, App, HttpResponse, HttpServer, Responder};
2
+ use mime_guess::from_path;
3
+ use rust_embed::Embed;
4
+
5
+ #[derive(Embed)]
6
+ #[folder = "examples/public/"]
7
+ struct Asset;
8
+
9
+ fn handle_embedded_file(path: &str) -> HttpResponse {
10
+ match Asset::get(path) {
11
+ Some(content) => HttpResponse::Ok()
12
+ .content_type(from_path(path).first_or_octet_stream().as_ref())
13
+ .body(content.data.into_owned()),
14
+ None => HttpResponse::NotFound().body("404 Not Found"),
15
+ }
16
+ }
17
+
18
+ #[actix_web::get("/")]
19
+ async fn index() -> impl Responder {
20
+ handle_embedded_file("index.html")
21
+ }
22
+
23
+ #[actix_web::get("/dist/{_:.*}")]
24
+ async fn dist(path: web::Path<String>) -> impl Responder {
25
+ handle_embedded_file(path.as_str())
26
+ }
27
+
28
+ #[actix_web::main]
29
+ async fn main() -> std::io::Result<()> {
30
+ HttpServer::new(|| App::new().service(index).service(dist)).bind("127.0.0.1:8000")?.run().await
31
+ }
examples/axum-spa/README.md ADDED
@@ -0,0 +1 @@
1
+ A small example for hosting single page application (SPA) files with [axum](https://github.com/tokio-rs/axum) and [rust-embed](https://github.com/pyrossh/rust-embed).
examples/axum-spa/assets/index.html ADDED
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Single Page Application</title>
8
+ </head>
9
+
10
+ <body>
11
+ I'm the body!
12
+ <script src="./js/script.js"></script>
13
+ </body>
14
+
15
+ </html>
examples/axum-spa/assets/js/script.js ADDED
@@ -0,0 +1,3 @@
1
+ var elem = document.createElement("div");
2
+ elem.innerHTML = "I'm the JS script!<br>" + new Date();
3
+ document.body.appendChild(elem);
examples/axum-spa/main.rs ADDED
@@ -0,0 +1,57 @@
1
+ use axum::{
2
+ http::{header, StatusCode, Uri},
3
+ response::{Html, IntoResponse, Response},
4
+ routing::Router,
5
+ };
6
+ use rust_embed::Embed;
7
+ use std::net::SocketAddr;
8
+
9
+ static INDEX_HTML: &str = "index.html";
10
+
11
+ #[derive(Embed)]
12
+ #[folder = "examples/axum-spa/assets/"]
13
+ struct Assets;
14
+
15
+ #[tokio::main]
16
+ async fn main() {
17
+ let app = Router::new().fallback(static_handler);
18
+
19
+ let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
20
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
21
+ println!("listening on {}", addr);
22
+ axum::serve(listener, app.into_make_service()).await.unwrap();
23
+ }
24
+
25
+ async fn static_handler(uri: Uri) -> impl IntoResponse {
26
+ let path = uri.path().trim_start_matches('/');
27
+
28
+ if path.is_empty() || path == INDEX_HTML {
29
+ return index_html().await;
30
+ }
31
+
32
+ match Assets::get(path) {
33
+ Some(content) => {
34
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
35
+
36
+ ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
37
+ }
38
+ None => {
39
+ if path.contains('.') {
40
+ return not_found().await;
41
+ }
42
+
43
+ index_html().await
44
+ }
45
+ }
46
+ }
47
+
48
+ async fn index_html() -> Response {
49
+ match Assets::get(INDEX_HTML) {
50
+ Some(content) => Html(content.data).into_response(),
51
+ None => not_found().await,
52
+ }
53
+ }
54
+
55
+ async fn not_found() -> Response {
56
+ (StatusCode::NOT_FOUND, "404").into_response()
57
+ }
examples/axum.rs ADDED
@@ -0,0 +1,65 @@
1
+ use axum::{
2
+ extract::Path,
3
+ http::{header, StatusCode},
4
+ response::{Html, IntoResponse, Response},
5
+ routing::{get, Router},
6
+ };
7
+ use rust_embed::Embed;
8
+ use std::net::SocketAddr;
9
+
10
+ #[tokio::main]
11
+ async fn main() {
12
+ // Define our app routes, including a fallback option for anything not matched.
13
+ let app = Router::new()
14
+ .route("/", get(index_handler))
15
+ .route("/index.html", get(index_handler))
16
+ .route("/dist/{*file}", get(static_handler))
17
+ .fallback_service(get(not_found));
18
+
19
+ // Start listening on the given address.
20
+ let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
21
+ println!("listening on {}", addr);
22
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
23
+ axum::serve(listener, app.into_make_service()).await.unwrap();
24
+ }
25
+
26
+ // We use static route matchers ("/" and "/index.html") to serve our home
27
+ // page.
28
+ async fn index_handler() -> impl IntoResponse {
29
+ static_handler(Path("index.html".to_string())).await
30
+ }
31
+
32
+ // We use a wildcard matcher ("/dist/*file") to match against everything
33
+ // within our defined assets directory. This is the directory on our Asset
34
+ // struct below, where folder = "examples/public/".
35
+ async fn static_handler(Path(path): Path<String>) -> impl IntoResponse {
36
+ StaticFile(path)
37
+ }
38
+
39
+ // Finally, we use a fallback route for anything that didn't match.
40
+ async fn not_found() -> Html<&'static str> {
41
+ Html("<h1>404</h1><p>Not Found</p>")
42
+ }
43
+
44
+ #[derive(Embed)]
45
+ #[folder = "examples/public/"]
46
+ struct Asset;
47
+
48
+ pub struct StaticFile<T>(pub T);
49
+
50
+ impl<T> IntoResponse for StaticFile<T>
51
+ where
52
+ T: Into<String>,
53
+ {
54
+ fn into_response(self) -> Response {
55
+ let path = self.0.into();
56
+
57
+ match Asset::get(path.as_str()) {
58
+ Some(content) => {
59
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
60
+ ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
61
+ }
62
+ None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
63
+ }
64
+ }
65
+ }
examples/basic.rs ADDED
@@ -0,0 +1,10 @@
1
+ use rust_embed::Embed;
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ struct Asset;
6
+
7
+ fn main() {
8
+ let index_html = Asset::get("index.html").unwrap();
9
+ println!("{:?}", std::str::from_utf8(index_html.data.as_ref()));
10
+ }
examples/poem.rs ADDED
@@ -0,0 +1,65 @@
1
+ use poem::{
2
+ async_trait,
3
+ http::{header, Method, StatusCode},
4
+ listener::TcpListener,
5
+ Endpoint, Request, Response, Result, Route, Server,
6
+ };
7
+ #[tokio::main]
8
+ async fn main() -> Result<(), std::io::Error> {
9
+ let app = Route::new().at("/", StaticEmbed).at("/index.html", StaticEmbed).nest("/dist", StaticEmbed);
10
+
11
+ let listener = TcpListener::bind("127.0.0.1:3000");
12
+ let server = Server::new(listener);
13
+ server.run(app).await?;
14
+ Ok(())
15
+ }
16
+
17
+ #[derive(rust_embed::Embed)]
18
+ #[folder = "examples/public/"]
19
+ struct Asset;
20
+ pub(crate) struct StaticEmbed;
21
+
22
+ #[async_trait]
23
+ impl Endpoint for StaticEmbed {
24
+ type Output = Response;
25
+
26
+ async fn call(&self, req: Request) -> Result<Self::Output> {
27
+ if req.method() != Method::GET {
28
+ return Ok(StatusCode::METHOD_NOT_ALLOWED.into());
29
+ }
30
+
31
+ let mut path = req.uri().path().trim_start_matches('/').trim_end_matches('/').to_string();
32
+ if path.starts_with("dist/") {
33
+ path = path.replace("dist/", "");
34
+ } else if path.is_empty() {
35
+ path = "index.html".to_string();
36
+ }
37
+ let path = path.as_ref();
38
+
39
+ match Asset::get(path) {
40
+ Some(content) => {
41
+ let hash = hex::encode(content.metadata.sha256_hash());
42
+ // if etag is matched, return 304
43
+ if req
44
+ .headers()
45
+ .get(header::IF_NONE_MATCH)
46
+ .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
47
+ .unwrap_or(false)
48
+ {
49
+ return Ok(StatusCode::NOT_MODIFIED.into());
50
+ }
51
+
52
+ // otherwise, return 200 with etag hash
53
+ let body: Vec<u8> = content.data.into();
54
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
55
+ Ok(
56
+ Response::builder()
57
+ .header(header::CONTENT_TYPE, mime.as_ref())
58
+ .header(header::ETAG, hash)
59
+ .body(body),
60
+ )
61
+ }
62
+ None => Ok(Response::builder().status(StatusCode::NOT_FOUND).finish()),
63
+ }
64
+ }
65
+ }
examples/public/images/doc.txt ADDED
@@ -0,0 +1 @@
1
+ Testing 1 2 3
examples/public/images/flower.jpg ADDED
Binary file
examples/public/images/llama.png ADDED
Binary file
examples/public/index.html ADDED
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link rel="stylesheet" type="text/css" href="dist/main.css" />
5
+ <script src="dist/main.js"></script>
6
+ </head>
7
+ <body>
8
+ <div class="bor">
9
+ <table bgcolor=#f0f0f0 cellspacing=0 class="hh">
10
+ <tr>
11
+ <td><img src="dist/images/llama.png" align="left"></td>
12
+ <td>
13
+ <table cellspacing=0>
14
+ <tr><td><b>pyros2097</b></td></tr>
15
+ <tr><td>Gdx Developer</td></tr>
16
+ <tr><td>
17
+ "Awesomeness we can has"
18
+ </td></tr>
19
+ </table>
20
+ </td>
21
+ </tr>
22
+ <tr>
23
+ </tr>
24
+ </table>
25
+ </div>
26
+ </body>
27
+ </html>
examples/public/main.css ADDED
@@ -0,0 +1,14 @@
1
+ body {
2
+ color:#000000;
3
+ font-family:sans-serif;
4
+ font-size: small;
5
+ font-style: italic;
6
+ font-weight: bold;
7
+ }
8
+
9
+ table.hh {
10
+ border:1px solid gray;
11
+ }
12
+
13
+
14
+ bor { border: 1px;}
examples/public/main.js ADDED
@@ -0,0 +1 @@
1
+ console.log("Awesomeness we can has")
examples/public/symlinks/main.js ADDED
@@ -0,0 +1 @@
1
+ ../main.js
examples/rocket.rs ADDED
@@ -0,0 +1,38 @@
1
+ #[macro_use]
2
+ extern crate rocket;
3
+
4
+ use rocket::http::ContentType;
5
+ use rocket::response::content::RawHtml;
6
+ use rust_embed::Embed;
7
+
8
+ use std::borrow::Cow;
9
+ use std::ffi::OsStr;
10
+ use std::path::PathBuf;
11
+
12
+ #[derive(Embed)]
13
+ #[folder = "examples/public/"]
14
+ struct Asset;
15
+
16
+ #[get("/")]
17
+ fn index() -> Option<RawHtml<Cow<'static, [u8]>>> {
18
+ let asset = Asset::get("index.html")?;
19
+ Some(RawHtml(asset.data))
20
+ }
21
+
22
+ #[get("/dist/<file..>")]
23
+ fn dist(file: PathBuf) -> Option<(ContentType, Cow<'static, [u8]>)> {
24
+ let filename = file.display().to_string();
25
+ let asset = Asset::get(&filename)?;
26
+ let content_type = file
27
+ .extension()
28
+ .and_then(OsStr::to_str)
29
+ .and_then(ContentType::from_extension)
30
+ .unwrap_or(ContentType::Bytes);
31
+
32
+ Some((content_type, asset.data))
33
+ }
34
+
35
+ #[rocket::launch]
36
+ fn rocket() -> _ {
37
+ rocket::build().mount("/", routes![index, dist])
38
+ }
examples/salvo.rs ADDED
@@ -0,0 +1,48 @@
1
+ use salvo::http::{header, StatusCode};
2
+ use salvo::prelude::*;
3
+
4
+ #[tokio::main]
5
+ async fn main() -> Result<(), std::io::Error> {
6
+ let router = Router::new()
7
+ .push(Router::with_path("dist/<**>").get(static_embed))
8
+ .push(Router::with_path("/<**>").get(static_embed));
9
+
10
+ let listener = TcpListener::bind("127.0.0.1:3000");
11
+ Server::new(listener).serve(router).await;
12
+ Ok(())
13
+ }
14
+
15
+ #[derive(rust_embed::Embed)]
16
+ #[folder = "examples/public/"]
17
+ struct Asset;
18
+
19
+ #[fn_handler]
20
+ async fn static_embed(req: &mut Request, res: &mut Response) {
21
+ let mut path: String = req.get_param("**").unwrap_or_default();
22
+ if path.is_empty() {
23
+ path = "index.html".into();
24
+ }
25
+
26
+ match Asset::get(&path) {
27
+ Some(content) => {
28
+ let hash = hex::encode(content.metadata.sha256_hash());
29
+ // if etag is matched, return 304
30
+ if req
31
+ .headers()
32
+ .get(header::IF_NONE_MATCH)
33
+ .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
34
+ .unwrap_or(false)
35
+ {
36
+ res.set_status_code(StatusCode::NOT_MODIFIED);
37
+ return;
38
+ }
39
+
40
+ // otherwise, return 200 with etag hash
41
+ let body: Vec<u8> = content.data.into();
42
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
43
+ res.headers_mut().insert(header::ETAG, hash.parse().unwrap());
44
+ res.render_binary(mime.as_ref().parse().unwrap(), &body);
45
+ }
46
+ None => res.set_status_code(StatusCode::NOT_FOUND),
47
+ }
48
+ }
examples/warp.rs ADDED
@@ -0,0 +1,32 @@
1
+ use rust_embed::Embed;
2
+ use warp::{http::header::HeaderValue, path::Tail, reply::Response, Filter, Rejection, Reply};
3
+
4
+ #[derive(Embed)]
5
+ #[folder = "examples/public/"]
6
+ struct Asset;
7
+
8
+ #[tokio::main]
9
+ async fn main() {
10
+ let index_html = warp::path::end().and_then(serve_index);
11
+ let dist = warp::path("dist").and(warp::path::tail()).and_then(serve);
12
+
13
+ let routes = index_html.or(dist);
14
+ warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
15
+ }
16
+
17
+ async fn serve_index() -> Result<impl Reply, Rejection> {
18
+ serve_impl("index.html")
19
+ }
20
+
21
+ async fn serve(path: Tail) -> Result<impl Reply, Rejection> {
22
+ serve_impl(path.as_str())
23
+ }
24
+
25
+ fn serve_impl(path: &str) -> Result<impl Reply, Rejection> {
26
+ let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?;
27
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
28
+
29
+ let mut res = Response::new(asset.data.into());
30
+ res.headers_mut().insert("content-type", HeaderValue::from_str(mime.as_ref()).unwrap());
31
+ Ok(res)
32
+ }
impl/Cargo.toml ADDED
@@ -0,0 +1,40 @@
1
+ [package]
2
+ name = "rust-embed-impl"
3
+ version = "8.11.0"
4
+ description = "Rust Custom Derive Macro which loads files into the rust binary at compile time during release and loads the file from the fs during dev"
5
+ readme = "readme.md"
6
+ documentation = "https://docs.rs/rust-embed"
7
+ repository = "https://pyrossh.dev/repos/rust-embed"
8
+ license = "MIT"
9
+ keywords = ["http", "rocket", "static", "web", "server"]
10
+ categories = ["web-programming::http-server"]
11
+ authors = ["pyrossh"]
12
+ edition = "2018"
13
+
14
+ [lib]
15
+ proc-macro = true
16
+
17
+ [dependencies]
18
+ rust-embed-utils = { version = "8.11.0", path = "../utils" }
19
+
20
+ syn = { version = "2", default-features = false, features = [
21
+ "derive",
22
+ "parsing",
23
+ "proc-macro",
24
+ "printing",
25
+ ] }
26
+ quote = "1"
27
+ proc-macro2 = "1"
28
+ walkdir = "2.3.1"
29
+
30
+ [dependencies.shellexpand]
31
+ version = "3"
32
+ optional = true
33
+
34
+ [features]
35
+ debug-embed = []
36
+ interpolate-folder-path = ["shellexpand"]
37
+ compression = []
38
+ mime-guess = ["rust-embed-utils/mime-guess"]
39
+ include-exclude = ["rust-embed-utils/include-exclude"]
40
+ deterministic-timestamps = []
impl/license ADDED
@@ -0,0 +1 @@
1
+ ../license
impl/readme.md ADDED
@@ -0,0 +1,3 @@
1
+ # Rust Embed Implementation
2
+
3
+ The implementation of the rust-embed macro lies here.
impl/src/lib.rs ADDED
@@ -0,0 +1,421 @@
1
+ #![recursion_limit = "1024"]
2
+ #![forbid(unsafe_code)]
3
+ #[macro_use]
4
+ extern crate quote;
5
+ extern crate proc_macro;
6
+
7
+ use proc_macro::TokenStream;
8
+ use proc_macro2::TokenStream as TokenStream2;
9
+ use rust_embed_utils::PathMatcher;
10
+ use std::{
11
+ collections::BTreeMap,
12
+ env,
13
+ io::ErrorKind,
14
+ iter::FromIterator,
15
+ path::{Path, PathBuf},
16
+ };
17
+ use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue};
18
+
19
+ fn embedded(
20
+ ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String],
21
+ metadata_only: bool, crate_path: &syn::Path,
22
+ ) -> syn::Result<TokenStream2> {
23
+ extern crate rust_embed_utils;
24
+
25
+ let mut match_values = BTreeMap::new();
26
+ let mut list_values = Vec::<String>::new();
27
+
28
+ let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
29
+ let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
30
+ let matcher = PathMatcher::new(&includes, &excludes);
31
+ for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), matcher) {
32
+ match_values.insert(
33
+ rel_path.clone(),
34
+ embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only, crate_path)?,
35
+ );
36
+
37
+ list_values.push(if let Some(prefix) = prefix {
38
+ format!("{}{}", prefix, rel_path)
39
+ } else {
40
+ rel_path
41
+ });
42
+ }
43
+
44
+ let array_len = list_values.len();
45
+
46
+ // If debug-embed is on, unconditionally include the code below. Otherwise,
47
+ // make it conditional on cfg(not(debug_assertions)).
48
+ let not_debug_attr = if cfg!(feature = "debug-embed") {
49
+ quote! {}
50
+ } else {
51
+ quote! { #[cfg(not(debug_assertions))]}
52
+ };
53
+
54
+ let handle_prefix = if let Some(prefix) = prefix {
55
+ quote! {
56
+ let file_path = file_path.strip_prefix(#prefix)?;
57
+ }
58
+ } else {
59
+ TokenStream2::new()
60
+ };
61
+ let match_values = match_values.into_iter().map(|(path, bytes)| {
62
+ quote! {
63
+ (#path, #bytes),
64
+ }
65
+ });
66
+ let value_type = if cfg!(feature = "compression") {
67
+ quote! { fn() -> #crate_path::EmbeddedFile }
68
+ } else {
69
+ quote! { #crate_path::EmbeddedFile }
70
+ };
71
+ let get_value = if cfg!(feature = "compression") {
72
+ quote! {|idx| (ENTRIES[idx].1)()}
73
+ } else {
74
+ quote! {|idx| ENTRIES[idx].1.clone()}
75
+ };
76
+ Ok(quote! {
77
+ #not_debug_attr
78
+ impl #ident {
79
+ /// Get an embedded file and its metadata.
80
+ pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
81
+ #handle_prefix
82
+ let key = file_path.replace("\\", "/");
83
+ const ENTRIES: &'static [(&'static str, #value_type)] = &[
84
+ #(#match_values)*];
85
+ let position = ENTRIES.binary_search_by_key(&key.as_str(), |entry| entry.0);
86
+ position.ok().map(#get_value)
87
+
88
+ }
89
+
90
+ fn names() -> ::std::slice::Iter<'static, &'static str> {
91
+ const ITEMS: [&str; #array_len] = [#(#list_values),*];
92
+ ITEMS.iter()
93
+ }
94
+
95
+ /// Iterates over the file paths in the folder.
96
+ pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static {
97
+ Self::names().map(|x| ::std::borrow::Cow::from(*x))
98
+ }
99
+ }
100
+
101
+ #not_debug_attr
102
+ impl #crate_path::RustEmbed for #ident {
103
+ fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
104
+ #ident::get(file_path)
105
+ }
106
+ fn iter() -> #crate_path::Filenames {
107
+ #crate_path::Filenames::Embedded(#ident::names())
108
+ }
109
+ }
110
+ })
111
+ }
112
+
113
+ fn dynamic(
114
+ ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool, crate_path: &syn::Path,
115
+ ) -> TokenStream2 {
116
+ let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
117
+ (
118
+ quote! { let file_path = file_path.strip_prefix(#prefix)?; },
119
+ quote! { ::std::borrow::Cow::Owned(format!("{}{}", #prefix, e.rel_path)) },
120
+ )
121
+ } else {
122
+ (TokenStream2::new(), quote! { ::std::borrow::Cow::from(e.rel_path) })
123
+ };
124
+
125
+ let declare_includes = quote! {
126
+ const INCLUDES: &[&str] = &[#(#includes),*];
127
+ };
128
+
129
+ let declare_excludes = quote! {
130
+ const EXCLUDES: &[&str] = &[#(#excludes),*];
131
+ };
132
+
133
+ // In metadata_only mode, we still need to read file contents to generate the
134
+ // file hash, but then we drop the file data.
135
+ let strip_contents = metadata_only.then_some(quote! {
136
+ .map(|mut file| { file.data = ::std::default::Default::default(); file })
137
+ });
138
+
139
+ let non_canonical_folder_path = Path::new(&folder_path);
140
+ let canonical_folder_path = non_canonical_folder_path
141
+ .canonicalize()
142
+ .or_else(|err| match err {
143
+ err if err.kind() == ErrorKind::NotFound => Ok(non_canonical_folder_path.to_owned()),
144
+ err => Err(err),
145
+ })
146
+ .expect("folder path must resolve to an absolute path");
147
+ let canonical_folder_path = canonical_folder_path.to_str().expect("absolute folder path must be valid unicode");
148
+
149
+ quote! {
150
+ #[cfg(debug_assertions)]
151
+ impl #ident {
152
+
153
+
154
+ fn matcher() -> #crate_path::utils::PathMatcher {
155
+ #declare_includes
156
+ #declare_excludes
157
+ static PATH_MATCHER: ::std::sync::OnceLock<#crate_path::utils::PathMatcher> = ::std::sync::OnceLock::new();
158
+ PATH_MATCHER.get_or_init(|| #crate_path::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone()
159
+ }
160
+ /// Get an embedded file and its metadata.
161
+ pub fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
162
+ #handle_prefix
163
+
164
+ let rel_file_path = file_path.replace("\\", "/");
165
+ let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path);
166
+
167
+ // Make sure the path requested does not escape the folder path
168
+ let canonical_file_path = file_path.canonicalize().ok()?;
169
+ if !canonical_file_path.starts_with(#canonical_folder_path) {
170
+ // Tried to request a path that is not in the embedded folder
171
+
172
+ // TODO: Currently it allows "path_traversal_attack" for the symlink files
173
+ // For it to be working properly we need to get absolute path first
174
+ // and check that instead if it starts with `canonical_folder_path`
175
+ // https://doc.rust-lang.org/std/path/fn.absolute.html (currently nightly)
176
+ // Should be allowed only if it was a symlink
177
+ let metadata = ::std::fs::symlink_metadata(&file_path).ok()?;
178
+ if !metadata.is_symlink() {
179
+ return ::std::option::Option::None;
180
+ }
181
+ }
182
+ let path_matcher = Self::matcher();
183
+ if path_matcher.is_path_included(&rel_file_path) {
184
+ #crate_path::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
185
+ } else {
186
+ ::std::option::Option::None
187
+ }
188
+ }
189
+
190
+ /// Iterates over the file paths in the folder.
191
+ pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static {
192
+ use ::std::path::Path;
193
+
194
+
195
+ #crate_path::utils::get_files(::std::string::String::from(#folder_path), Self::matcher())
196
+ .map(|e| #map_iter)
197
+ }
198
+ }
199
+
200
+ #[cfg(debug_assertions)]
201
+ impl #crate_path::RustEmbed for #ident {
202
+ fn get(file_path: &str) -> ::std::option::Option<#crate_path::EmbeddedFile> {
203
+ #ident::get(file_path)
204
+ }
205
+ fn iter() -> #crate_path::Filenames {
206
+ // the return type of iter() is unnamable, so we have to box it
207
+ #crate_path::Filenames::Dynamic(::std::boxed::Box::new(#ident::iter()))
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ fn generate_assets(
214
+ ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<String>, includes: Vec<String>, excludes: Vec<String>,
215
+ metadata_only: bool, crate_path: &syn::Path,
216
+ ) -> syn::Result<TokenStream2> {
217
+ let embedded_impl = embedded(
218
+ ident,
219
+ relative_folder_path,
220
+ absolute_folder_path.clone(),
221
+ prefix.as_deref(),
222
+ &includes,
223
+ &excludes,
224
+ metadata_only,
225
+ crate_path,
226
+ );
227
+ if cfg!(feature = "debug-embed") {
228
+ return embedded_impl;
229
+ }
230
+ let embedded_impl = embedded_impl?;
231
+ let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes, metadata_only, crate_path);
232
+
233
+ Ok(quote! {
234
+ #embedded_impl
235
+ #dynamic_impl
236
+ })
237
+ }
238
+
239
+ fn embed_file(
240
+ folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool, crate_path: &syn::Path,
241
+ ) -> syn::Result<TokenStream2> {
242
+ let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
243
+ let hash = file.metadata.sha256_hash();
244
+ let (last_modified, created) = if cfg!(feature = "deterministic-timestamps") {
245
+ (quote! { ::std::option::Option::Some(0u64) }, quote! { ::std::option::Option::Some(0u64) })
246
+ } else {
247
+ let last_modified = match file.metadata.last_modified() {
248
+ Some(last_modified) => quote! { ::std::option::Option::Some(#last_modified) },
249
+ None => quote! { ::std::option::Option::None },
250
+ };
251
+ let created = match file.metadata.created() {
252
+ Some(created) => quote! { ::std::option::Option::Some(#created) },
253
+ None => quote! { ::std::option::Option::None },
254
+ };
255
+ (last_modified, created)
256
+ };
257
+ #[cfg(feature = "mime-guess")]
258
+ let mimetype_tokens = {
259
+ let mt = file.metadata.mimetype();
260
+ quote! { , #mt }
261
+ };
262
+ #[cfg(not(feature = "mime-guess"))]
263
+ let mimetype_tokens = TokenStream2::new();
264
+
265
+ let embedding_code = if metadata_only {
266
+ quote! {
267
+ const BYTES: &'static [u8] = &[];
268
+ }
269
+ } else if cfg!(feature = "compression") {
270
+ let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
271
+ // Print some debugging information
272
+ let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
273
+ let full_relative_path = full_relative_path.to_string_lossy();
274
+ quote! {
275
+ #crate_path::flate!(static BYTES: [u8] from #full_relative_path);
276
+ }
277
+ } else {
278
+ quote! {
279
+ const BYTES: &'static [u8] = include_bytes!(#full_canonical_path);
280
+ }
281
+ };
282
+ let closure_args = if cfg!(feature = "compression") {
283
+ quote! { || }
284
+ } else {
285
+ quote! {}
286
+ };
287
+ Ok(quote! {
288
+ #closure_args {
289
+ #embedding_code
290
+
291
+ #crate_path::EmbeddedFile {
292
+ data: ::std::borrow::Cow::Borrowed(&BYTES),
293
+ metadata: #crate_path::Metadata::__rust_embed_new([#(#hash),*], #last_modified, #created #mimetype_tokens)
294
+ }
295
+ }
296
+ })
297
+ }
298
+
299
+ /// Find all pairs of the `name = "value"` attribute from the derive input
300
+ fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String> {
301
+ ast
302
+ .attrs
303
+ .iter()
304
+ .filter(|value| value.path().is_ident(attr_name))
305
+ .filter_map(|attr| match &attr.meta {
306
+ Meta::NameValue(MetaNameValue {
307
+ value: Expr::Lit(ExprLit { lit: Lit::Str(val), .. }),
308
+ ..
309
+ }) => Some(val.value()),
310
+ _ => None,
311
+ })
312
+ .collect()
313
+ }
314
+
315
+ fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option<bool> {
316
+ ast
317
+ .attrs
318
+ .iter()
319
+ .find(|value| value.path().is_ident(attr_name))
320
+ .and_then(|attr| match &attr.meta {
321
+ Meta::NameValue(MetaNameValue {
322
+ value: Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }),
323
+ ..
324
+ }) => Some(val.value()),
325
+ _ => None,
326
+ })
327
+ }
328
+
329
+ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
330
+ match ast.data {
331
+ Data::Struct(ref data) => match data.fields {
332
+ Fields::Unit => {}
333
+ _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
334
+ },
335
+ _ => return Err(syn::Error::new_spanned(ast, "RustEmbed can only be derived for unit structs")),
336
+ };
337
+
338
+ let crate_path: syn::Path = find_attribute_values(ast, "crate_path")
339
+ .last()
340
+ .map_or_else(|| syn::parse_str("rust_embed").unwrap(), |v| syn::parse_str(v).unwrap());
341
+
342
+ let mut folder_paths = find_attribute_values(ast, "folder");
343
+ if folder_paths.len() != 1 {
344
+ return Err(syn::Error::new_spanned(
345
+ ast,
346
+ "#[derive(RustEmbed)] must contain one attribute like this #[folder = \"examples/public/\"]",
347
+ ));
348
+ }
349
+ let folder_path = folder_paths.remove(0);
350
+
351
+ let prefix = find_attribute_values(ast, "prefix").into_iter().next();
352
+ let includes = find_attribute_values(ast, "include");
353
+ let excludes = find_attribute_values(ast, "exclude");
354
+ let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false);
355
+ let allow_missing = find_bool_attribute(ast, "allow_missing").unwrap_or(false);
356
+
357
+ #[cfg(not(feature = "include-exclude"))]
358
+ if !includes.is_empty() || !excludes.is_empty() {
359
+ return Err(syn::Error::new_spanned(
360
+ ast,
361
+ "Please turn on the `include-exclude` feature to use the `include` and `exclude` attributes",
362
+ ));
363
+ }
364
+
365
+ #[cfg(feature = "interpolate-folder-path")]
366
+ let folder_path = shellexpand::full(&folder_path)
367
+ .map_err(|v| syn::Error::new_spanned(ast, v.to_string()))?
368
+ .to_string();
369
+
370
+ // Base relative paths on the Cargo.toml location
371
+ let (relative_path, absolute_folder_path) = if Path::new(&folder_path).is_relative() {
372
+ let absolute_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
373
+ .join(&folder_path)
374
+ .to_str()
375
+ .unwrap()
376
+ .to_owned();
377
+ (Some(folder_path.clone()), absolute_path)
378
+ } else {
379
+ if cfg!(feature = "compression") {
380
+ return Err(syn::Error::new_spanned(ast, "`folder` must be a relative path under `compression` feature."));
381
+ }
382
+ (None, folder_path)
383
+ };
384
+
385
+ if !Path::new(&absolute_folder_path).exists() && !allow_missing {
386
+ let mut message = format!(
387
+ "#[derive(RustEmbed)] folder '{}' does not exist. cwd: '{}'",
388
+ absolute_folder_path,
389
+ std::env::current_dir().unwrap().to_str().unwrap()
390
+ );
391
+
392
+ // Add a message about the interpolate-folder-path feature if the path may
393
+ // include a variable
394
+ if absolute_folder_path.contains('$') && cfg!(not(feature = "interpolate-folder-path")) {
395
+ message += "\nA variable has been detected. RustEmbed can expand variables \
396
+ when the `interpolate-folder-path` feature is enabled.";
397
+ }
398
+
399
+ return Err(syn::Error::new_spanned(ast, message));
400
+ };
401
+
402
+ generate_assets(
403
+ &ast.ident,
404
+ relative_path.as_deref(),
405
+ absolute_folder_path,
406
+ prefix,
407
+ includes,
408
+ excludes,
409
+ metadata_only,
410
+ &crate_path,
411
+ )
412
+ }
413
+
414
+ #[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, allow_missing, metadata_only, crate_path))]
415
+ pub fn derive_input_object(input: TokenStream) -> TokenStream {
416
+ let ast = parse_macro_input!(input as DeriveInput);
417
+ match impl_rust_embed(&ast) {
418
+ Ok(ok) => ok.into(),
419
+ Err(e) => e.to_compile_error().into(),
420
+ }
421
+ }
rustfmt.toml ADDED
@@ -0,0 +1,5 @@
1
+ merge_derives = true
2
+ fn_params_layout = "Compressed"
3
+ max_width = 160
4
+ tab_spaces = 2
5
+ reorder_imports = true
src/lib.rs ADDED
@@ -0,0 +1,80 @@
1
+ #![forbid(unsafe_code)]
2
+ #[cfg(feature = "compression")]
3
+ #[cfg_attr(feature = "compression", doc(hidden))]
4
+ pub use include_flate::flate;
5
+
6
+ extern crate rust_embed_impl;
7
+ pub use rust_embed_impl::*;
8
+
9
+ pub use rust_embed_utils::{EmbeddedFile, Metadata};
10
+
11
+ #[doc(hidden)]
12
+ pub extern crate rust_embed_utils as utils;
13
+
14
+ /// A directory of binary assets.
15
+ ///
16
+ /// The files in the specified folder will be embedded into the executable in
17
+ /// release builds. Debug builds will read the data from the file system at
18
+ /// runtime.
19
+ ///
20
+ /// This trait is meant to be derived like so:
21
+ /// ```
22
+ /// use rust_embed::Embed;
23
+ ///
24
+ /// #[derive(Embed)]
25
+ /// #[folder = "examples/public/"]
26
+ /// struct Asset;
27
+ ///
28
+ /// fn main() {}
29
+ /// ```
30
+ pub trait RustEmbed {
31
+ /// Get an embedded file and its metadata.
32
+ ///
33
+ /// If the feature `debug-embed` is enabled or the binary was compiled in
34
+ /// release mode, the file information is embedded in the binary and the file
35
+ /// data is returned as a `Cow::Borrowed(&'static [u8])`.
36
+ ///
37
+ /// Otherwise, the information is read from the file system on each call and
38
+ /// the file data is returned as a `Cow::Owned(Vec<u8>)`.
39
+ fn get(file_path: &str) -> Option<EmbeddedFile>;
40
+
41
+ /// Iterates over the file paths in the folder.
42
+ ///
43
+ /// If the feature `debug-embed` is enabled or the binary is compiled in
44
+ /// release mode, a static array containing the list of relative file paths
45
+ /// is used.
46
+ ///
47
+ /// Otherwise, the files are listed from the file system on each call.
48
+ fn iter() -> impl Iterator<Item = std::borrow::Cow<'static, str>> + 'static;
49
+ }
50
+
51
+ pub use RustEmbed as Embed;
52
+
53
+ /// An iterator over filenames.
54
+ ///
55
+ /// This enum exists for optimization purposes, to avoid boxing the iterator in
56
+ /// some cases. Do not try and match on it, as different variants will exist
57
+ /// depending on the compilation context.
58
+ pub enum Filenames {
59
+ /// Release builds use a named iterator type, which can be stack-allocated.
60
+ #[cfg(any(not(debug_assertions), feature = "debug-embed"))]
61
+ Embedded(std::slice::Iter<'static, &'static str>),
62
+
63
+ /// The debug iterator type is currently unnameable and still needs to be
64
+ /// boxed.
65
+ #[cfg(all(debug_assertions, not(feature = "debug-embed")))]
66
+ Dynamic(Box<dyn Iterator<Item = std::borrow::Cow<'static, str>>>),
67
+ }
68
+
69
+ impl Iterator for Filenames {
70
+ type Item = std::borrow::Cow<'static, str>;
71
+ fn next(&mut self) -> Option<Self::Item> {
72
+ match self {
73
+ #[cfg(any(not(debug_assertions), feature = "debug-embed"))]
74
+ Filenames::Embedded(names) => names.next().map(|x| std::borrow::Cow::from(*x)),
75
+
76
+ #[cfg(all(debug_assertions, not(feature = "debug-embed")))]
77
+ Filenames::Dynamic(boxed) => boxed.next(),
78
+ }
79
+ }
80
+ }
tests/allow_missing.rs ADDED
@@ -0,0 +1,15 @@
1
+ use std::{path::PathBuf, str::FromStr};
2
+
3
+ use rust_embed::Embed;
4
+
5
+ #[derive(Embed)]
6
+ #[folder = "examples/missing/"]
7
+ #[allow_missing = true]
8
+ struct Asset;
9
+
10
+ #[test]
11
+ fn missing_is_empty() {
12
+ let path = PathBuf::from_str("./examples/missing").unwrap();
13
+ assert!(!path.exists());
14
+ assert_eq!(Asset::iter().count(), 0);
15
+ }
tests/custom_crate_path.rs ADDED
@@ -0,0 +1,20 @@
1
+ /// This test checks that the `crate_path` attribute can be used
2
+ /// to specify a custom path to the `rust_embed` crate.
3
+
4
+ mod custom {
5
+ pub mod path {
6
+ pub use rust_embed;
7
+ }
8
+ }
9
+
10
+ // We introduce a 'rust_embed' module here to break compilation in case
11
+ // the `rust_embed` crate is not loaded correctly.
12
+ //
13
+ // To test this, try commenting out the attribute which specifies the
14
+ // the custom crate path -- you should find that the test fails to compile.
15
+ mod rust_embed {}
16
+
17
+ #[derive(custom::path::rust_embed::RustEmbed)]
18
+ #[crate_path = "custom::path::rust_embed"]
19
+ #[folder = "examples/public/"]
20
+ struct Asset;
tests/include_exclude.rs ADDED
@@ -0,0 +1,68 @@
1
+ use rust_embed::Embed;
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ struct AllAssets;
6
+
7
+ #[test]
8
+ fn get_works() {
9
+ assert!(AllAssets::get("index.html").is_some(), "index.html should exist");
10
+ assert!(AllAssets::get("gg.html").is_none(), "gg.html should not exist");
11
+ assert!(AllAssets::get("images/llama.png").is_some(), "llama.png should exist");
12
+ assert!(AllAssets::get("symlinks/main.js").is_some(), "main.js should exist");
13
+ assert_eq!(AllAssets::iter().count(), 7);
14
+ }
15
+
16
+ #[derive(Embed)]
17
+ #[folder = "examples/public/"]
18
+ #[include = "*.html"]
19
+ #[include = "images/*"]
20
+ struct IncludeSomeAssets;
21
+
22
+ #[test]
23
+ fn including_some_assets_works() {
24
+ assert!(IncludeSomeAssets::get("index.html").is_some(), "index.html should exist");
25
+ assert!(IncludeSomeAssets::get("main.js").is_none(), "main.js should not exist");
26
+ assert!(IncludeSomeAssets::get("images/llama.png").is_some(), "llama.png should exist");
27
+ assert_eq!(IncludeSomeAssets::iter().count(), 4);
28
+ }
29
+
30
+ #[derive(Embed)]
31
+ #[folder = "examples/public/"]
32
+ #[exclude = "*.html"]
33
+ #[exclude = "images/*"]
34
+ struct ExcludeSomeAssets;
35
+
36
+ #[test]
37
+ fn excluding_some_assets_works() {
38
+ assert!(ExcludeSomeAssets::get("index.html").is_none(), "index.html should not exist");
39
+ assert!(ExcludeSomeAssets::get("main.js").is_some(), "main.js should exist");
40
+ assert!(ExcludeSomeAssets::get("symlinks/main.js").is_some(), "main.js symlink should exist");
41
+ assert!(ExcludeSomeAssets::get("images/llama.png").is_none(), "llama.png should not exist");
42
+ assert_eq!(ExcludeSomeAssets::iter().count(), 3);
43
+ }
44
+
45
+ #[derive(Embed)]
46
+ #[folder = "examples/public/"]
47
+ #[include = "images/*"]
48
+ #[exclude = "*.txt"]
49
+ struct ExcludePriorityAssets;
50
+
51
+ #[test]
52
+ fn exclude_has_higher_priority() {
53
+ assert!(ExcludePriorityAssets::get("images/doc.txt").is_none(), "doc.txt should not exist");
54
+ assert!(ExcludePriorityAssets::get("images/llama.png").is_some(), "llama.png should exist");
55
+ assert_eq!(ExcludePriorityAssets::iter().count(), 2);
56
+ }
57
+
58
+ #[derive(Embed)]
59
+ #[folder = "examples/public/symlinks"]
60
+ #[include = "main.js"]
61
+ struct IncludeSymlink;
62
+
63
+ #[test]
64
+ fn include_symlink() {
65
+ assert_eq!(IncludeSymlink::iter().count(), 1);
66
+ assert_eq!(IncludeSymlink::iter().next(), Some(std::borrow::Cow::Borrowed("main.js")));
67
+ assert!(IncludeSymlink::get("main.js").is_some())
68
+ }
tests/interpolated_path.rs ADDED
@@ -0,0 +1,37 @@
1
+ use rust_embed::Embed;
2
+
3
+ /// Test doc comment
4
+ #[derive(Embed)]
5
+ #[folder = "$CARGO_MANIFEST_DIR/examples/public/"]
6
+ struct Asset;
7
+
8
+ #[test]
9
+ fn get_works() {
10
+ assert!(Asset::get("index.html").is_some(), "index.html should exist");
11
+ assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
12
+ assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
13
+ }
14
+
15
+ #[test]
16
+ fn iter_works() {
17
+ let mut num_files = 0;
18
+ for file in Asset::iter() {
19
+ assert!(Asset::get(file.as_ref()).is_some());
20
+ num_files += 1;
21
+ }
22
+ assert_eq!(num_files, 7);
23
+ }
24
+
25
+ #[test]
26
+ fn trait_works_generic() {
27
+ trait_works_generic_helper::<Asset>();
28
+ }
29
+ fn trait_works_generic_helper<E: rust_embed::Embed>() {
30
+ let mut num_files = 0;
31
+ for file in E::iter() {
32
+ assert!(E::get(file.as_ref()).is_some());
33
+ num_files += 1;
34
+ }
35
+ assert_eq!(num_files, 7);
36
+ assert!(E::get("gg.html").is_none(), "gg.html should not exist");
37
+ }
tests/lib.rs ADDED
@@ -0,0 +1,72 @@
1
+ use rust_embed::{Embed, RustEmbed};
2
+
3
+ /// Test doc comment
4
+ #[derive(Embed)]
5
+ #[folder = "examples/public/"]
6
+ struct Asset;
7
+
8
+ #[derive(RustEmbed)]
9
+ #[folder = "examples/public/"]
10
+ struct AssetOld;
11
+
12
+ #[test]
13
+ fn get_works() {
14
+ assert!(Asset::get("index.html").is_some(), "index.html should exist");
15
+ assert!(Asset::get("gg.html").is_none(), "gg.html should not exist");
16
+ assert!(Asset::get("images/llama.png").is_some(), "llama.png should exist");
17
+ }
18
+
19
+ // Todo remove this test and rename RustEmbed trait to Embed on a new major release
20
+ #[test]
21
+ fn get_old_name_works() {
22
+ assert!(AssetOld::get("index.html").is_some(), "index.html should exist");
23
+ assert!(AssetOld::get("gg.html").is_none(), "gg.html should not exist");
24
+ assert!(AssetOld::get("images/llama.png").is_some(), "llama.png should exist");
25
+ }
26
+
27
+ /// Using Windows-style path separators (`\`) is acceptable
28
+ #[test]
29
+ fn get_windows_style() {
30
+ assert!(
31
+ Asset::get("images\\llama.png").is_some(),
32
+ "llama.png should be accessible via \"images\\lama.png\""
33
+ );
34
+ }
35
+
36
+ #[test]
37
+ fn iter_works() {
38
+ let mut num_files = 0;
39
+ for file in Asset::iter() {
40
+ assert!(Asset::get(file.as_ref()).is_some());
41
+ num_files += 1;
42
+ }
43
+ assert_eq!(num_files, 7);
44
+ }
45
+
46
+ #[test]
47
+ fn trait_works_generic() {
48
+ trait_works_generic_helper::<Asset>();
49
+ }
50
+ fn trait_works_generic_helper<E: rust_embed::Embed>() {
51
+ let mut num_files = 0;
52
+ for file in E::iter() {
53
+ assert!(E::get(file.as_ref()).is_some());
54
+ num_files += 1;
55
+ }
56
+ assert_eq!(num_files, 7);
57
+ assert!(E::get("gg.html").is_none(), "gg.html should not exist");
58
+ }
59
+
60
+ /// Test that iter() can be boxed into a trait object.
61
+ /// This is required for libraries like i18n-embed that need to return
62
+ /// `Box<dyn Iterator<Item = String>>` from a trait implementation.
63
+ #[test]
64
+ fn iter_can_be_boxed() {
65
+ fn get_boxed_iter<E: rust_embed::Embed>() -> Box<dyn Iterator<Item = String>> {
66
+ Box::new(E::iter().map(|filename| filename.to_string()))
67
+ }
68
+
69
+ let filenames: Vec<String> = get_boxed_iter::<Asset>().collect();
70
+ assert_eq!(filenames.len(), 7);
71
+ assert!(filenames.contains(&"index.html".to_string()));
72
+ }
tests/metadata.rs ADDED
@@ -0,0 +1,56 @@
1
+ use rust_embed::{Embed, EmbeddedFile};
2
+ use sha2::Digest;
3
+ use std::{fs, time::SystemTime};
4
+
5
+ #[derive(Embed)]
6
+ #[folder = "examples/public/"]
7
+ struct Asset;
8
+
9
+ #[test]
10
+ fn hash_is_accurate() {
11
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
12
+ let mut hasher = sha2::Sha256::new();
13
+ hasher.update(index_file.data);
14
+ let expected_hash: [u8; 32] = hasher.finalize().into();
15
+
16
+ assert_eq!(index_file.metadata.sha256_hash(), expected_hash);
17
+ }
18
+
19
+ #[test]
20
+ #[cfg(not(feature = "deterministic-timestamps"))]
21
+ fn last_modified_is_accurate() {
22
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
23
+
24
+ let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
25
+ let expected_datetime_utc = metadata.modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
26
+
27
+ assert_eq!(index_file.metadata.last_modified(), Some(expected_datetime_utc));
28
+ }
29
+
30
+ #[test]
31
+ #[cfg(not(feature = "deterministic-timestamps"))]
32
+ fn create_is_accurate() {
33
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
34
+
35
+ let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
36
+ let expected_datetime_utc = metadata.created().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
37
+
38
+ assert_eq!(index_file.metadata.created(), Some(expected_datetime_utc));
39
+ }
40
+
41
+ #[test]
42
+ #[cfg(feature = "deterministic-timestamps")]
43
+ fn deterministic_timestamps_are_zero() {
44
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
45
+
46
+ assert_eq!(
47
+ index_file.metadata.last_modified(),
48
+ Some(0),
49
+ "last_modified should be 0 with deterministic-timestamps"
50
+ );
51
+ assert_eq!(index_file.metadata.created(), Some(0), "created should be 0 with deterministic-timestamps");
52
+
53
+ let metadata = fs::metadata(format!("{}/examples/public/index.html", env!("CARGO_MANIFEST_DIR"))).unwrap();
54
+ let fs_modified = metadata.modified().unwrap().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
55
+ assert_ne!(fs_modified, 0, "Filesystem modified time should not be 0");
56
+ }
tests/metadata_only.rs ADDED
@@ -0,0 +1,12 @@
1
+ use rust_embed::{Embed, EmbeddedFile};
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ #[metadata_only = true]
6
+ struct Asset;
7
+
8
+ #[test]
9
+ fn file_is_empty() {
10
+ let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
11
+ assert_eq!(index_file.data.len(), 0);
12
+ }
tests/mime_guess.rs ADDED
@@ -0,0 +1,35 @@
1
+ use rust_embed::{Embed, EmbeddedFile};
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ struct Asset;
6
+
7
+ #[test]
8
+ fn html_mime_is_correct() {
9
+ let html_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
10
+ assert_eq!(html_file.metadata.mimetype(), "text/html");
11
+ }
12
+
13
+ #[test]
14
+ fn css_mime_is_correct() {
15
+ let css_file: EmbeddedFile = Asset::get("main.css").expect("main.css exists");
16
+ assert_eq!(css_file.metadata.mimetype(), "text/css");
17
+ }
18
+
19
+ #[test]
20
+ fn js_mime_is_correct() {
21
+ let js_file: EmbeddedFile = Asset::get("main.js").expect("main.js exists");
22
+ assert_eq!(js_file.metadata.mimetype(), "text/javascript");
23
+ }
24
+
25
+ #[test]
26
+ fn jpg_mime_is_correct() {
27
+ let jpg_file: EmbeddedFile = Asset::get("images/flower.jpg").expect("flower.jpg exists");
28
+ assert_eq!(jpg_file.metadata.mimetype(), "image/jpeg");
29
+ }
30
+
31
+ #[test]
32
+ fn png_mime_is_correct() {
33
+ let png_file: EmbeddedFile = Asset::get("images/llama.png").expect("llama.png exists");
34
+ assert_eq!(png_file.metadata.mimetype(), "image/png");
35
+ }
tests/path_traversal_attack.rs ADDED
@@ -0,0 +1,27 @@
1
+ use rust_embed::Embed;
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ struct Assets;
6
+
7
+ /// Prevent attempts to access files outside of the embedded folder.
8
+ /// This is mainly a concern when running in debug mode, since that loads from
9
+ /// the file system at runtime.
10
+ #[test]
11
+ fn path_traversal_attack_fails() {
12
+ assert!(Assets::get("../basic.rs").is_none());
13
+ }
14
+
15
+ #[derive(Embed)]
16
+ #[folder = "examples/axum-spa/"]
17
+ struct AxumAssets;
18
+
19
+ // TODO:
20
+ /// Prevent attempts to access symlinks outside of the embedded folder.
21
+ /// This is mainly a concern when running in debug mode, since that loads from
22
+ /// the file system at runtime.
23
+ #[test]
24
+ #[ignore = "see https://github.com/pyrossh/rust-embed/pull/235"]
25
+ fn path_traversal_attack_symlink_fails() {
26
+ assert!(Assets::get("../public/symlinks/main.js").is_none());
27
+ }
tests/prefix.rs ADDED
@@ -0,0 +1,24 @@
1
+ use rust_embed::Embed;
2
+
3
+ #[derive(Embed)]
4
+ #[folder = "examples/public/"]
5
+ #[prefix = "prefix/"]
6
+ struct Asset;
7
+
8
+ #[test]
9
+ fn get_with_prefix() {
10
+ assert!(Asset::get("prefix/index.html").is_some());
11
+ }
12
+
13
+ #[test]
14
+ fn get_without_prefix() {
15
+ assert!(Asset::get("index.html").is_none());
16
+ }
17
+
18
+ #[test]
19
+ fn iter_values_have_prefix() {
20
+ for file in Asset::iter() {
21
+ assert!(file.starts_with("prefix/"));
22
+ assert!(Asset::get(file.as_ref()).is_some());
23
+ }
24
+ }
utils/Cargo.toml ADDED
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "rust-embed-utils"
3
+ version = "8.11.0"
4
+ description = "Utilities for rust-embed"
5
+ readme = "readme.md"
6
+ documentation = "https://docs.rs/rust-embed"
7
+ repository = "https://pyrossh.dev/repos/rust-embed"
8
+ license = "MIT"
9
+ keywords = ["http", "rocket", "static", "web", "server"]
10
+ categories = ["web-programming::http-server"]
11
+ authors = ["pyrossh"]
12
+ edition = "2018"
13
+
14
+ [dependencies]
15
+ walkdir = "2.3.1"
16
+ sha2 = "0.10.5"
17
+ mime_guess = { version = "2.0.4", optional = true }
18
+
19
+ [dependencies.globset]
20
+ version = "0.4.8"
21
+ optional = true
22
+
23
+ [features]
24
+ debug-embed = []
25
+ mime-guess = ["mime_guess"]
26
+ include-exclude = ["globset"]
utils/license ADDED
@@ -0,0 +1 @@
1
+ ../license
utils/readme.md ADDED
@@ -0,0 +1,3 @@
1
+ # Rust Embed Utilities
2
+
3
+ The utilities used by rust-embed and rust-embed-impl lie here.
utils/src/lib.rs ADDED
@@ -0,0 +1,185 @@
1
+ #![forbid(unsafe_code)]
2
+
3
+ use sha2::Digest;
4
+ use std::borrow::Cow;
5
+ use std::path::Path;
6
+ use std::time::SystemTime;
7
+ use std::{fs, io};
8
+
9
+ #[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
10
+ pub struct FileEntry {
11
+ pub rel_path: String,
12
+ pub full_canonical_path: String,
13
+ }
14
+
15
+ #[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
16
+ pub fn get_files(folder_path: String, matcher: PathMatcher) -> impl Iterator<Item = FileEntry> {
17
+ walkdir::WalkDir::new(&folder_path)
18
+ .follow_links(true)
19
+ .sort_by_file_name()
20
+ .into_iter()
21
+ .filter_map(std::result::Result::ok)
22
+ .filter(|e| e.file_type().is_file())
23
+ .filter_map(move |e| {
24
+ let rel_path = path_to_str(e.path().strip_prefix(&folder_path).unwrap());
25
+ let full_canonical_path = path_to_str(std::fs::canonicalize(e.path()).ok()?);
26
+
27
+ let rel_path = if std::path::MAIN_SEPARATOR == '\\' {
28
+ rel_path.replace('\\', "/")
29
+ } else {
30
+ rel_path
31
+ };
32
+ if matcher.is_path_included(&rel_path) {
33
+ Some(FileEntry { rel_path, full_canonical_path })
34
+ } else {
35
+ None
36
+ }
37
+ })
38
+ }
39
+
40
+ /// A file embedded into the binary
41
+ #[derive(Clone)]
42
+ pub struct EmbeddedFile {
43
+ pub data: Cow<'static, [u8]>,
44
+ pub metadata: Metadata,
45
+ }
46
+
47
+ /// Metadata about an embedded file
48
+ #[derive(Clone)]
49
+ pub struct Metadata {
50
+ hash: [u8; 32],
51
+ last_modified: Option<u64>,
52
+ created: Option<u64>,
53
+ #[cfg(feature = "mime-guess")]
54
+ mimetype: Cow<'static, str>,
55
+ }
56
+
57
+ impl Metadata {
58
+ #[doc(hidden)]
59
+ pub const fn __rust_embed_new(
60
+ hash: [u8; 32], last_modified: Option<u64>, created: Option<u64>, #[cfg(feature = "mime-guess")] mimetype: &'static str,
61
+ ) -> Self {
62
+ Self {
63
+ hash,
64
+ last_modified,
65
+ created,
66
+ #[cfg(feature = "mime-guess")]
67
+ mimetype: Cow::Borrowed(mimetype),
68
+ }
69
+ }
70
+
71
+ /// The SHA256 hash of the file
72
+ pub fn sha256_hash(&self) -> [u8; 32] {
73
+ self.hash
74
+ }
75
+
76
+ /// The last modified date in seconds since the UNIX epoch. If the underlying
77
+ /// platform/file-system does not support this, None is returned.
78
+ pub fn last_modified(&self) -> Option<u64> {
79
+ self.last_modified
80
+ }
81
+
82
+ /// The created data in seconds since the UNIX epoch. If the underlying
83
+ /// platform/file-system does not support this, None is returned.
84
+ pub fn created(&self) -> Option<u64> {
85
+ self.created
86
+ }
87
+
88
+ /// The mime type of the file
89
+ #[cfg(feature = "mime-guess")]
90
+ pub fn mimetype(&self) -> &str {
91
+ &self.mimetype
92
+ }
93
+ }
94
+
95
+ pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
96
+ let data = fs::read(file_path)?;
97
+ let data = Cow::from(data);
98
+
99
+ let mut hasher = sha2::Sha256::new();
100
+ hasher.update(&data);
101
+ let hash: [u8; 32] = hasher.finalize().into();
102
+
103
+ let source_date_epoch = match std::env::var("SOURCE_DATE_EPOCH") {
104
+ Ok(value) => value.parse::<u64>().ok(),
105
+ Err(_) => None,
106
+ };
107
+
108
+ let metadata = fs::metadata(file_path)?;
109
+ let last_modified = metadata
110
+ .modified()
111
+ .ok()
112
+ .and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
113
+ .map(|secs| secs.as_secs());
114
+
115
+ let created = metadata
116
+ .created()
117
+ .ok()
118
+ .and_then(|created| created.duration_since(SystemTime::UNIX_EPOCH).ok())
119
+ .map(|secs| secs.as_secs());
120
+
121
+ #[cfg(feature = "mime-guess")]
122
+ let mimetype = mime_guess::from_path(file_path).first_or_octet_stream().to_string();
123
+
124
+ Ok(EmbeddedFile {
125
+ data,
126
+ metadata: Metadata {
127
+ hash,
128
+ last_modified: source_date_epoch.or(last_modified),
129
+ created: source_date_epoch.or(created),
130
+ #[cfg(feature = "mime-guess")]
131
+ mimetype: mimetype.into(),
132
+ },
133
+ })
134
+ }
135
+
136
+ fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
137
+ p.as_ref().to_str().expect("Path does not have a string representation").to_owned()
138
+ }
139
+
140
+ #[derive(Clone)]
141
+ pub struct PathMatcher {
142
+ #[cfg(feature = "include-exclude")]
143
+ include_matcher: globset::GlobSet,
144
+ #[cfg(feature = "include-exclude")]
145
+ exclude_matcher: globset::GlobSet,
146
+ }
147
+
148
+ #[cfg(feature = "include-exclude")]
149
+ impl PathMatcher {
150
+ pub fn new(includes: &[&str], excludes: &[&str]) -> Self {
151
+ let mut include_matcher = globset::GlobSetBuilder::new();
152
+ for include in includes {
153
+ include_matcher.add(globset::Glob::new(include).unwrap_or_else(|_| panic!("invalid include pattern '{}'", include)));
154
+ }
155
+ let include_matcher = include_matcher
156
+ .build()
157
+ .unwrap_or_else(|_| panic!("Could not compile included patterns matcher"));
158
+
159
+ let mut exclude_matcher = globset::GlobSetBuilder::new();
160
+ for exclude in excludes {
161
+ exclude_matcher.add(globset::Glob::new(exclude).unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude)));
162
+ }
163
+ let exclude_matcher = exclude_matcher
164
+ .build()
165
+ .unwrap_or_else(|_| panic!("Could not compile excluded patterns matcher"));
166
+
167
+ Self {
168
+ include_matcher,
169
+ exclude_matcher,
170
+ }
171
+ }
172
+ pub fn is_path_included(&self, path: &str) -> bool {
173
+ !self.exclude_matcher.is_match(path) && (self.include_matcher.is_empty() || self.include_matcher.is_match(path))
174
+ }
175
+ }
176
+
177
+ #[cfg(not(feature = "include-exclude"))]
178
+ impl PathMatcher {
179
+ pub fn new(_includes: &[&str], _excludes: &[&str]) -> Self {
180
+ Self {}
181
+ }
182
+ pub fn is_path_included(&self, _path: &str) -> bool {
183
+ true
184
+ }
185
+ }