extendable_assets/filesystem/embed.rs
1use rust_embed::EmbeddedFile;
2
3use crate::filesystem::{Filesystem, FilesystemError};
4
5use async_trait::async_trait;
6
7/// Trait for providing access to embedded files.
8///
9/// This trait exists because `rust-embed` types are not dyn-compatible,
10/// so we need this abstraction layer to allow dynamic dispatch over
11/// different embedded file collections.
12pub trait EmbedFilesystemProvider: Send + Sync {
13 /// Retrieves an embedded file by its path.
14 ///
15 /// # Arguments
16 ///
17 /// * `path` - The path to the embedded file
18 ///
19 /// # Returns
20 ///
21 /// Returns the embedded file if found, or `None` if the path doesn't exist.
22 fn get(&self, path: &str) -> Option<EmbeddedFile>;
23}
24/// A filesystem implementation that reads from embedded files.
25///
26/// This filesystem provides read-only access to files that have been embedded
27/// into the binary at compile time using `rust-embed`.
28pub struct EmbedFilesystem {
29 /// The provider that handles access to the embedded files
30 provider: Box<dyn EmbedFilesystemProvider>,
31 /// Optional root directory prefix to prepend to all file paths.
32 /// When set, all file lookups will be prefixed with this directory path.
33 /// Trailing slashes are automatically normalized.
34 root_dir: String,
35}
36impl EmbedFilesystem {
37 /// Creates a new embedded filesystem with the given provider.
38 ///
39 /// # Arguments
40 ///
41 /// * `provider` - The embedded file provider to use for file access
42 ///
43 /// # Returns
44 ///
45 /// A new `EmbedFilesystem` instance.
46 #[inline]
47 pub fn new(provider: Box<dyn EmbedFilesystemProvider>) -> Self {
48 Self {
49 provider,
50 root_dir: String::new(),
51 }
52 }
53 /// Sets the root directory for this filesystem.
54 ///
55 /// When a root directory is set, all file path lookups will be prefixed
56 /// with this directory path. Trailing slashes and backslashes are
57 /// automatically normalized to ensure consistent path formatting.
58 ///
59 /// # Arguments
60 ///
61 /// * `root_dir` - The root directory path to use as a prefix
62 ///
63 /// # Returns
64 ///
65 /// Self with the root directory configured
66 ///
67 /// # Examples
68 ///
69 /// ```
70 /// # use extendable_assets::EmbedFilesystem;
71 /// # use extendable_assets::EmbedFilesystemProvider;
72 /// struct MockProvider;
73 /// impl EmbedFilesystemProvider for MockProvider {
74 /// fn get(&self, _path: &str) -> Option<rust_embed::EmbeddedFile> { None }
75 /// }
76 /// let fs = EmbedFilesystem::new(Box::new(MockProvider))
77 /// .with_root_dir("assets/");
78 /// ```
79 pub fn with_root_dir(mut self, root_dir: &str) -> Self {
80 self.root_dir = if root_dir.is_empty() {
81 String::new()
82 } else {
83 root_dir.trim_end_matches(['/', '\\']).to_string() + "/"
84 };
85 self
86 }
87}
88#[async_trait]
89impl Filesystem for EmbedFilesystem {
90 /// Reads the contents of an embedded file as bytes.
91 ///
92 /// # Arguments
93 ///
94 /// * `asset_path` - The path to the asset file to read
95 ///
96 /// # Returns
97 ///
98 /// The file contents as a `Vec<u8>` on success, or a `FilesystemError`
99 /// if the file is not found.
100 ///
101 /// # Errors
102 ///
103 /// Returns `FilesystemError::NotFound` if the requested file path
104 /// does not exist in the embedded files.
105 async fn read_bytes(&self, asset_path: &str) -> Result<Vec<u8>, FilesystemError> {
106 let prefixed_path = self.root_dir.clone() + asset_path;
107 // Look up the embedded file using the provider
108 let embedded = self
109 .provider
110 .get(&prefixed_path)
111 .ok_or_else(|| FilesystemError::NotFound(prefixed_path))?;
112
113 // Convert the embedded file data to owned bytes
114 Ok(embedded.data.into_owned())
115 }
116}
117
118#[cfg(test)]
119mod test {
120 use super::*;
121 use std::sync::Arc;
122
123 /// Test implementation of EmbedFilesystemProvider that embeds test files.
124 ///
125 /// This struct uses the `rust_embed::Embed` derive macro to embed all files
126 /// from the `tests/` directory at compile time, making them available for
127 /// testing the embedded filesystem functionality.
128 #[derive(rust_embed::Embed)]
129 #[folder = "$CARGO_MANIFEST_DIR/tests"]
130 struct TestEmbedFsProvider;
131 impl EmbedFilesystemProvider for TestEmbedFsProvider {
132 /// Retrieves an embedded test file by its path.
133 fn get(&self, path: &str) -> Option<EmbeddedFile> {
134 // Delegate to the rust-embed generated static method
135 TestEmbedFsProvider::get(path)
136 }
137 }
138
139 /// Test that the embedded filesystem can successfully read file contents.
140 ///
141 /// This test verifies that:
142 /// 1. The EmbedFilesystem can be constructed with a test provider
143 /// 2. The filesystem can locate and read an embedded test file
144 /// 3. The file contents are returned correctly as bytes
145 /// 4. The async interface works properly with pollster for blocking execution
146 ///
147 /// The test uses a known test file `test_data_0/hello.txt` that should
148 /// contain the text "Hello world\n" to verify the read operation.
149 #[test]
150 fn read_bytes() {
151 // Create an embedded filesystem using our test provider
152 let fs: Arc<dyn Filesystem> = Arc::new(EmbedFilesystem::new(Box::new(TestEmbedFsProvider)));
153
154 // Read the test file contents and verify they match expected value
155 let greeting = pollster::block_on(fs.read_bytes("test_data_0/hello.txt")).unwrap();
156 assert_eq!(greeting, b"Hello world\n");
157 }
158
159 /// Test that the embedded filesystem works correctly with a root directory.
160 ///
161 /// This test verifies that:
162 /// 1. The root directory feature works correctly with trailing slashes
163 /// 2. Path normalization removes excess trailing slashes
164 /// 3. Files can be accessed using relative paths when a root is set
165 /// 4. The correct file contents are returned from the prefixed path
166 ///
167 /// The test uses `test_data_1/hello.txt` as the target file by setting
168 /// `test_data_1` as the root directory and accessing `hello.txt` directly.
169 #[test]
170 fn read_bytes_with_root() {
171 // Create an embedded filesystem using our test provider
172 // Add a root directory, with a bunch of slashes at the end (they should be removed
173 // automatically)
174 let fs: Arc<dyn Filesystem> = Arc::new(
175 EmbedFilesystem::new(Box::new(TestEmbedFsProvider)).with_root_dir("test_data_1///"),
176 );
177
178 // Read the test file contents and verify they match expected value
179 let greeting = pollster::block_on(fs.read_bytes("hello.txt")).unwrap();
180 assert_eq!(greeting, b"Hello earth\n");
181 }
182}