Skip to main content

rama_utils/include_dir/
mod.rs

1//! An extension to the `include_str!()` and `include_bytes!()` macro for
2//! embedding an entire directory tree into your binary.
3//!
4//! # Environment Variables
5//!
6//! You might
7//! want to read the [*Environment Variables*][cargo-vars] section of *The
8//! Cargo Book* for a list of variables provided by `cargo`.
9//!
10//! For example you might want to use the `$CARGO_MANIFEST_DIR` or `$OUT_DIR`
11//! variables. In specific to include a folder relative to your crate you might
12//! use `include_dir!("$CARGO_MANIFEST_DIR/assets")`.
13//!
14//! By default paths are assumed to be relative to the file where the macro
15//! is executed from.
16//! # Compile Time Considerations
17//!
18//! While the `include_dir!()` macro executes relatively quickly, it expands
19//! to a fairly large amount of code (all your files are essentially embedded
20//! as Rust byte strings) and this may have a flow-on effect on the build
21//! process.
22//!
23//! In particular, including a large number or files or files which are
24//! particularly big may cause the compiler to use large amounts of RAM or spend
25//! a long time parsing your crate.
26//!
27//! As one data point, this crate's `target/` directory contained 620 files with
28//! a total of 64 MB, with a full build taking about 1.5 seconds and 200MB of
29//! RAM to generate a 7MB binary.
30//!
31//! Using `include_dir!("target/")` increased the compile time to 5 seconds
32//! and used 730MB of RAM, generating a 72MB binary.
33//!
34//! [tracked-env]: https://github.com/rust-lang/rust/issues/74690
35//! [track-path]: https://github.com/rust-lang/rust/issues/73921
36//! [cargo-vars]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
37
38mod dir;
39mod dir_entry;
40mod file;
41mod metadata;
42
43pub use self::{dir::Dir, dir_entry::DirEntry, file::File, metadata::Metadata};
44
45#[doc(inline)]
46pub use ::rama_macros::include_dir;
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_relative_dir() {
54        static ASSETS: Dir = include_dir!("../../../test-files");
55
56        let entry = ASSETS.get_entry("index.html").unwrap();
57        let file = entry.as_file().unwrap();
58
59        assert!(file.contents_utf8().unwrap().contains("<b>HTML!</b>"));
60
61        _ = file.metadata().unwrap();
62    }
63
64    #[test]
65    fn test_absolute_dir() {
66        static ASSETS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src");
67
68        let entry = ASSETS.get_entry("include_dir/dir.rs").unwrap();
69        let file = entry.as_file().unwrap();
70        assert!(file.contents_utf8().unwrap().contains("fn get_entry"));
71
72        _ = file.metadata().unwrap();
73
74        let entry = ASSETS.get_entry("macros").unwrap();
75        _ = entry.as_dir().unwrap();
76    }
77
78    #[test]
79    fn test_absolute_with_relative_dir() {
80        static ASSETS: Dir = include_dir!("$CARGO_MANIFEST_DIR/../test-files");
81
82        let entry = ASSETS.get_entry("index.html").unwrap();
83        let file = entry.as_file().unwrap();
84
85        assert!(file.contents_utf8().unwrap().contains("<b>HTML!</b>"));
86
87        _ = file.metadata().unwrap();
88    }
89
90    #[test]
91    fn test_extract_rejects_absolute_paths() {
92        let malicious_file = File::new("/etc/passwd", b"malicious content");
93        let malicious_entry = DirEntry::File(malicious_file);
94        let entries = [malicious_entry];
95        let malicious_dir = Dir::new("test", &entries);
96
97        let temp_dir = std::env::temp_dir().join("test_extract_absolute");
98        let result = malicious_dir.extract(&temp_dir);
99
100        assert!(result.is_err());
101        let err = result.unwrap_err();
102        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
103    }
104
105    #[test]
106    fn test_extract_rejects_parent_traversal() {
107        let malicious_file = File::new("../../../etc/passwd", b"malicious content");
108        let malicious_entry = DirEntry::File(malicious_file);
109        let entries = [malicious_entry];
110        let malicious_dir = Dir::new("test", &entries);
111
112        let temp_dir = std::env::temp_dir().join("test_extract_traversal");
113        let result = malicious_dir.extract(&temp_dir);
114
115        assert!(result.is_err());
116        let err = result.unwrap_err();
117        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
118    }
119
120    #[test]
121    fn test_extract_allows_safe_paths() {
122        let safe_file = File::new("subdir/safe.txt", b"safe content");
123        let safe_entry = DirEntry::File(safe_file);
124        let entries = [safe_entry];
125        let safe_dir = Dir::new("test", &entries);
126
127        let temp_dir = tempfile::tempdir().unwrap();
128        safe_dir.extract(temp_dir.path()).unwrap();
129    }
130
131    #[test]
132    fn test_extract_rejects_mixed_traversal() {
133        let malicious_file = File::new("subdir/../../etc/passwd", b"malicious content");
134        let malicious_entry = DirEntry::File(malicious_file);
135        let entries = [malicious_entry];
136        let malicious_dir = Dir::new("test", &entries);
137
138        let temp_dir = std::env::temp_dir().join("test_extract_mixed");
139        let result = malicious_dir.extract(&temp_dir);
140
141        assert!(result.is_err());
142        let err = result.unwrap_err();
143        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
144    }
145
146    #[cfg(unix)]
147    #[test]
148    fn test_extract_rejects_symlink_escape() {
149        let temp_dir = tempfile::tempdir().unwrap();
150        let root = temp_dir.path().join("root");
151        let outside = temp_dir.path().join("outside");
152        std::fs::create_dir(&root).unwrap();
153        std::fs::create_dir(&outside).unwrap();
154        std::os::unix::fs::symlink(outside.join("escaped.txt"), root.join("link.txt")).unwrap();
155
156        let malicious_file = File::new("link.txt", b"malicious content");
157        let malicious_entry = DirEntry::File(malicious_file);
158        let entries = [malicious_entry];
159        let malicious_dir = Dir::new("test", &entries);
160
161        let result = malicious_dir.extract(&root);
162
163        assert!(result.is_err());
164        let err = result.unwrap_err();
165        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
166        assert!(!outside.join("escaped.txt").exists());
167    }
168}