Skip to main content

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}