~repos /rust-embed

#rust#proc-macro#http

git clone https://pyrossh.dev/repos/rust-embed.git

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


48814079 pyros2097

10 years ago
Initial commit
.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.9.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.9.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,398 @@
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.9.0] - 2025-10-31
11
+
12
+ - Ignore paths that couldn't be canonicalized. Thanks to zvolin <mac.zwolinski@gmail.com>.
13
+
14
+ ## [8.9.0] - 2025-10-18
15
+
16
+ - fix export of RustEmbed macro. Thanks to LorrensP-2158466 <lorrens.pantelis@student.uhasselt.be>.
17
+
18
+ ## [8.7.2] - 2025-05-14
19
+
20
+ - Update repo links to sub-packages
21
+
22
+ ## [8.7.1] - 2025-05-05
23
+
24
+ - Update documentation and repo links
25
+
26
+ ## [8.7.0] - 2025-04-10
27
+
28
+ - add deterministic timestamps flag for deterministic builds [#259](https://github.com/pyrossh/rust-embed/pull/259). Thanks to [daywalker90](https://github.com/daywalker90)
29
+
30
+
31
+ ## [8.6.0] - 2025-02-25
32
+
33
+ - Update include-flate to 0.3 [#246](https://github.com/pyrossh/rust-embed/pull/246). Thanks to [krant](https://github.com/krant)
34
+ - refactor: remove redundant reference and closure [#250](https://github.com/pyrossh/rust-embed/pull/250). Thanks to [hamirmahal](https://github.com/hamirmahal)
35
+ - refactor: replace map().unwrap_or_else(). [#250](https://github.com/pyrossh/rust-embed/pull/255). Thanks to [hamirmahal](https://github.com/hamirmahal)
36
+ - Compatible with Axum 0.7.9 [#253](https://github.com/pyrossh/rust-embed/pull/253). Thanks to [wkmyws](https://github.com/wkmyws)
37
+ - Add allow_missing option to derive macro [#256](https://github.com/pyrossh/rust-embed/pull/256). Thanks to [lirannl](https://github.com/lirannl)
38
+
39
+
40
+ ## [8.5.0] - 2024-07-09
41
+
42
+ - 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)
43
+ - Increase minimum rust-version to v1.7.0.0
44
+
45
+ ## [8.4.0] - 2024-05-11
46
+
47
+ - Re-export RustEmbed as Embed [#245](https://github.com/pyrossh/rust-embed/pull/245/files). Thanks to [pyrossh](https://github.com/pyrossh)
48
+ - 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)
49
+ - Add `metadata_only` attribute [#241](https://github.com/pyrossh/rust-embed/pull/241/files). Thanks to [ddfisher](https://github.com/ddfisher)
50
+ - 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)
51
+ - Eliminate unnecessary `to_path` call [#239](https://github.com/pyrossh/rust-embed/pull/239/files). Thanks to [smoelius](https://github.com/smoelius)
52
+
53
+ ## [8.3.0] - 2024-02-26
54
+
55
+ - Fix symbolic links in debug builds [#235](https://github.com/pyrossh/rust-embed/pull/235/files). Thanks to [Buckram123](https://github.com/Buckram123)
56
+
57
+ ## [8.2.0] - 2023-12-29
58
+
59
+ - Fix naming collisions in macros [#230](https://github.com/pyrossh/rust-embed/pull/230/files). Thanks to [hwittenborn](https://github.com/hwittenborn)
60
+
61
+ ## [8.1.0] - 2023-12-08
62
+
63
+ - Add created to file metadata. [#225](https://github.com/pyrossh/rust-embed/pull/225/files). Thanks to [ngalaiko](https://github.com/ngalaiko)
64
+
65
+ ## [8.0.0] - 2023-08-23
66
+
67
+ - 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)
68
+
69
+ ## [6.8.1] - 2023-06-30
70
+
71
+ - Fix failing compilation under compression feature [#182](https://github.com/pyrossh/rust-embed/issues/182). Thanks to [osiewicz](https://github.com/osiewicz)
72
+
73
+ ## [6.8.0] - 2023-06-30
74
+
75
+ - Update `include-flate` to v0.2 [#182](https://github.com/pyrossh/rust-embed/issues/182)
76
+
77
+ ## [6.7.0] - 2023-06-09
78
+
79
+ - Update `syn` to v2.0 [#211](https://github.com/pyrossh/rust-embed/issues/211)
80
+
81
+ ## [6.6.1] - 2023-03-25
82
+
83
+ - Fix mime-guess feature not working properly [#209](https://github.com/pyrossh/rust-embed/issues/209)
84
+
85
+ ## [6.6.0] - 2023-03-05
86
+
87
+ - sort_by_file_name() requires walkdir v2.3.2 [#206](https://github.com/pyrossh/rust-embed/issues/206)
88
+ - Add `mime-guess` feature to statically store mimetype [#192](https://github.com/pyrossh/rust-embed/issues/192)
89
+
90
+ ## [6.4.2] - 2022-10-20
91
+
92
+ - Fail the proc macro if include/exclude are used without the feature [#187](https://github.com/pyrossh/rust-embed/issues/187)
93
+
94
+ ## [6.4.1] - 2022-09-13
95
+
96
+ - Update sha2 dependency version in utils crate [#186](https://github.com/pyrossh/rust-embed/issues/186)
97
+
98
+ ## [6.4.0] - 2022-04-15
99
+
100
+ - Order files by filename [#171](https://github.com/pyros2097/rust-embed/issues/171). Thanks to [apognu](https://github.com/apognu)
101
+
102
+ ## [6.3.0] - 2021-11-28
103
+
104
+ - Fixed a security issue in debug mode [#159](https://github.com/pyros2097/rust-embed/issues/159). Thanks to [5225225](https://github.com/5225225)
105
+
106
+ ## [6.2.0] - 2021-09-01
107
+
108
+ - Fixed `include-exclude` feature when using cargo v2 resolver
109
+
110
+ ## [6.1.0] - 2021-08-31
111
+
112
+ - Added `include-exclude` feature by [mbme](https://github.com/mbme)
113
+
114
+ ## [6.0.1] - 2021-08-21
115
+
116
+ - Added doc comments to macro generated functions
117
+
118
+ ## [6.0.0] - 2021-08-01
119
+
120
+ Idea came about from [Cody Casterline](https://github.com/NfNitLoop)
121
+
122
+ - 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
123
+ a `metadata` field which has theses 2 properties associated to the file `hash` and `last_modified`;
124
+
125
+ ## [5.9.0] - 2021-01-18
126
+
127
+ ### Added
128
+
129
+ - Added path prefix attribute
130
+
131
+ ## [5.8.0] - 2021-01-06
132
+
133
+ ### Fixed
134
+
135
+ - Fixed compiling with latest version of syn
136
+
137
+ ## [5.7.0] - 2020-12-08
138
+
139
+ ### Fixed
140
+
141
+ - Fix feature flag typo
142
+
143
+ ## [5.6.0] - 2020-07-19
144
+
145
+ ### Fixed
146
+
147
+ - Fixed windows path error in release mode
148
+
149
+ ### Changed
150
+
151
+ - Using github actions for CI now
152
+
153
+ ## [5.5.1] - 2020-03-19
154
+
155
+ ### Fixed
156
+
157
+ - Fixed warnings in latest nightly
158
+
159
+ ## [5.5.0] - 2020-02-26
160
+
161
+ ### Fixed
162
+
163
+ - Fixed the `folder` directory being relative to the current directory.
164
+ It is now relative to `Cargo.toml`.
165
+
166
+ ## [5.4.0] - 2020-02-24
167
+
168
+ ### Changed
169
+
170
+ - using rust-2018 edition now
171
+ - code cleanup
172
+ - updated examples and crates
173
+
174
+ ## [5.3.0] - 2020-02-15
175
+
176
+ ### Added
177
+
178
+ - `compression` feature for compressing embedded files
179
+
180
+ ## [5.2.0] - 2019-12-05
181
+
182
+ ## Changed
183
+
184
+ - updated syn and quote crate to 1.x
185
+
186
+ ## [5.1.0] - 2019-07-09
187
+
188
+ ## Fixed
189
+
190
+ - error when debug code tries to import the utils crate
191
+
192
+ ## [5.0.1] - 2019-07-07
193
+
194
+ ## Changed
195
+
196
+ - derive is allowed only on unit structs now
197
+
198
+ ## [5.0.0] - 2019-07-05
199
+
200
+ ## Added
201
+
202
+ - proper error message stating only unit structs are supported
203
+
204
+ ## Fixed
205
+
206
+ - windows latest build
207
+
208
+ ## [4.5.0] - 2019-06-29
209
+
210
+ ## Added
211
+
212
+ - allow rust embed derive to take env variables in the folder path
213
+
214
+ ## [4.4.0] - 2019-05-11
215
+
216
+ ### Fixed
217
+
218
+ - a panic when struct has doc comments
219
+
220
+ ### Added
221
+
222
+ - a warp example
223
+
224
+ ## [4.3.0] - 2019-01-10
225
+
226
+ ### Fixed
227
+
228
+ - debug_embed feature was not working at all
229
+
230
+ ### Added
231
+
232
+ - a test run for debug_embed feature
233
+
234
+ ## [4.2.0] - 2018-12-02
235
+
236
+ ### Changed
237
+
238
+ - return `Cow<'static, [u8]>` to preserve static lifetime
239
+
240
+ ## [4.1.0] - 2018-10-24
241
+
242
+ ### Added
243
+
244
+ - `iter()` method to list files
245
+
246
+ ## [4.0.0] - 2018-10-11
247
+
248
+ ### Changed
249
+
250
+ - avoid vector allocation by returning `impl AsRef<[u8]>`
251
+
252
+ ## [3.0.2] - 2018-09-05
253
+
254
+ ### Added
255
+
256
+ - appveyor for testing on windows
257
+
258
+ ### Fixed
259
+
260
+ - handle paths in windows correctly
261
+
262
+ ## [3.0.1] - 2018-07-24
263
+
264
+ ### Added
265
+
266
+ - panic if the folder cannot be found
267
+
268
+ ## [3.0.0] - 2018-06-01
269
+
270
+ ### Changed
271
+
272
+ - 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).
273
+
274
+ ```rust
275
+ #[folder("assets/")]
276
+ ```
277
+
278
+ to
279
+
280
+ ```rust
281
+ #[folder = "assets/"]
282
+ ```
283
+
284
+ ### Removed
285
+
286
+ - log dependecy as we are not using it anymore
287
+
288
+ ## [2.0.0] - 2018-05-26
289
+
290
+ ### Changed
291
+
292
+ - Reimplemented the macro for release to use include_bytes for perf sake. Thanks to [lukad](https://github.com/lukad).
293
+
294
+ ## [1.1.1] - 2018-03-19
295
+
296
+ ### Changed
297
+
298
+ - Fixed usage error message
299
+
300
+ ## [1.1.0] - 2018-03-19
301
+
302
+ ### Added
303
+
304
+ - Release mode for custom derive
305
+
306
+ ### Changed
307
+
308
+ - Fixed tests in travis
309
+
310
+ ## [1.0.0] - 2018-03-18
311
+
312
+ ### Changed
313
+
314
+ - Converted the rust-embed macro `embed!` into a Rust Custom Derive Macro `#[derive(RustEmbed)]` which implements get on the struct
315
+
316
+ ```rust
317
+ let asset = embed!("examples/public/")
318
+ ```
319
+
320
+ to
321
+
322
+ ```rust
323
+ #[derive(RustEmbed)]
324
+ #[folder = "examples/public/"]
325
+ struct Asset;
326
+ ```
327
+
328
+ ## [0.5.2] - 2018-03-16
329
+
330
+ ### Added
331
+
332
+ - rouille example
333
+
334
+ ## [0.5.1] - 2018-03-16
335
+
336
+ ### Removed
337
+
338
+ - the plugin attribute from crate
339
+
340
+ ## [0.5.0] - 2018-03-16
341
+
342
+ ### Added
343
+
344
+ - rocket example
345
+
346
+ ### Changed
347
+
348
+ - Converted the rust-embed executable into a macro `embed!` which now loads files at compile time during release and from the fs during dev.
349
+
350
+ ## [0.4.0] - 2017-03-2
351
+
352
+ ### Changed
353
+
354
+ - `generate_assets` to public again
355
+
356
+ ## [0.3.5] - 2017-03-2
357
+
358
+ ### Added
359
+
360
+ - rust-embed prefix to all logs
361
+
362
+ ## [0.3.4] - 2017-03-2
363
+
364
+ ### Changed
365
+
366
+ - the lib to be plugin again
367
+
368
+ ## [0.3.3] - 2017-03-2
369
+
370
+ ### Changed
371
+
372
+ - the lib to be proc-macro from plugin
373
+
374
+ ## [0.3.2] - 2017-03-2
375
+
376
+ ### Changed
377
+
378
+ - lib name from `rust-embed` to `rust_embed`
379
+
380
+ ## [0.3.1] - 2017-03-2
381
+
382
+ ### Removed
383
+
384
+ - hyper example
385
+
386
+ ## [0.3.0] - 2017-02-26
387
+
388
+ ### Added
389
+
390
+ - rust-embed executable which generates rust code to embed resource files into your rust executable
391
+ it creates a file like assets.rs that contains the code for your assets.
392
+
393
+ ## [0.2.0] - 2017-03-16
394
+
395
+ ### Added
396
+
397
+ - rust-embed executable which generates rust code to embed resource files into your rust executable
398
+ 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.9.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.9.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>> {
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>> {
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() -> Filenames;
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,58 @@
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
+ }
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.9.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
+ }