vfs_shadow/
lib.rs

1//! # vfs-shadow
2//!
3//! This crate allows embedding files from a directory into a virtual filesystem (VFS) during
4//! compile time.
5//! The macro [`load_into_vfs!`] takes a path to a directory and a [filesystem](vfs::FileSystem),
6//! then loads the contents of the directory into the provided filesystem.
7//!
8//! ## Usage
9//! Use the [`load_into_vfs!`] macro to load files from a directory into a
10//! [`MemoryFS`](vfs::impls::memory::MemoryFS) (or any other filesystem that implements
11//! [`vfs::FileSystem`]):
12//! ```
13//! use vfs_shadow::load_into_vfs;
14//! use vfs::{MemoryFS, FileSystem};
15//!
16//! // Load files into a MemoryFS
17//! let fs = load_into_vfs!("example/vfs", MemoryFS::new()).unwrap();
18//!
19//! // Interact with the embedded files
20//! assert!(fs.exists("/data.json").is_ok());
21//! ```
22//!
23//! You can also pass a reference to an existing filesystem:
24//! ```
25//! use vfs_shadow::load_into_vfs;
26//! use vfs::{MemoryFS, FileSystem};
27//!
28//! let fs = MemoryFS::new();
29//! load_into_vfs!("example/vfs", &fs).unwrap();
30//!
31//! // Use the filesystem
32//! assert!(fs.exists("/data.json").is_ok());
33//! ```
34//!
35//! In both cases, the directory at `example/vfs` is included in the final binary, and its contents
36//! are copied into the provided filesystem.
37//!
38//! ### Path Resolution
39//! The path provided to `load_into_vfs!` is relative to the manifest directory.
40//! This can change when [`proc_macro::Span::source_file`] stabilizes in future.
41//!
42//! ## Return Value
43//! The macro returns a [`vfs::VfsResult<()>`].
44//! If an error occurs while copying files into the filesystem, the operation will fail, and
45//! execution will stop.
46
47use proc_macro::TokenStream;
48use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
49use proc_macro_error2::proc_macro_error;
50use quote::quote;
51use std::path::PathBuf;
52use syn::{
53    parse::{Parse, ParseStream},
54    Token,
55};
56
57mod files;
58
59/// Embeds files from a directory into a virtual filesystem at compile time.
60///
61/// For general usage of the `load_into_vfs!` macro, please refer to the
62/// [crate-level documentation](crate).
63///
64/// ## Pseudo Signature
65/// ```rust,ignore
66/// load_into_vfs!<FS: vfs::FileSystem>(path: &str, fs: &FS) -> vfs::VfsResult<FS>
67/// ```
68///
69/// ## Implementation Details
70/// The `load_into_vfs!` macro is designed to load the contents of a directory into a virtual
71/// filesystem at compile time.
72/// Here’s how it works:
73///
74/// 1. **Directory Walking**:
75///     During compile time, the specified directory is recursively walked.
76///     The macro collects the absolute paths of all entries within the directory, along with
77///     whether each entry is a file or a directory.
78///
79/// 2. **Trait Generation**:
80///     The macro generates a private trait called `LoadIntoFileSystem`, which is implemented for
81///     any type that implements the [`vfs::FileSystem`] trait.
82///     This trait includes a single function, `load_into_vfs`, which is generated at compile time.
83///
84/// 3. **Directory Handling**:
85///     For directories, the generated code will create the corresponding directory in the VFS
86///     using:
87///     ```rust,ignore
88///     self.create_dir(#vfs_path)?;
89///     ```
90///
91/// 4. **File Handling**:
92///     For files, the macro generates a block of code to include the file contents as bytes and
93///     write them into the VFS:
94///     ```rust,ignore
95///     {
96///         static BYTES: &[u8] = ::std::include_bytes!(#real_path);
97///         let mut file = self.create_file(#vfs_path)?;
98///         file.write_all(BYTES)?;
99///     }
100///     ```
101///     This ensures the file's bytes are included in the binary and written to the VFS at runtime.
102///
103/// 5. **Returning the Filesystem**:
104///     After processing all entries, the macro returns the filesystem wrapped in a
105///     [`vfs::VfsResult`], allowing further interactions with the virtual filesystem.
106#[proc_macro]
107#[proc_macro_error]
108pub fn load_into_vfs(input: TokenStream) -> TokenStream {
109    let args = syn::parse_macro_input!(input as Args);
110
111    let fs = &args.fs;
112    let dir_entries = files::dir_entries(&args.path.0, args.path.1);
113    let load_into_vfs = load_into_vfs_tokens(&dir_entries);
114
115    quote! {{
116        use ::vfs::FileSystem;
117
118        trait LoadIntoFileSystem: FileSystem {
119            #load_into_vfs
120        }
121
122        impl<FS> LoadIntoFileSystem for FS where FS: FileSystem {}
123        let fs = #fs;
124        fs.load_into_vfs().map(|_| fs)
125    }}
126    .into()
127}
128
129struct Args {
130    path: (PathBuf, Span2),
131    fs: syn::Expr,
132}
133
134impl Parse for Args {
135    fn parse(input: ParseStream) -> syn::Result<Self> {
136        let path: syn::LitStr = input.parse()?;
137        let path = (PathBuf::from(path.value()), path.span());
138        input.parse::<Token![,]>()?;
139        let fs: syn::Expr = input.parse()?;
140
141        if !input.is_empty() {
142            return Err(input.error("unexpected tokens"));
143        }
144
145        Ok(Args { path, fs })
146    }
147}
148
149fn load_into_vfs_tokens(dir_entries: &[files::DirEntry]) -> TokenStream2 {
150    let mut instructions: Vec<TokenStream2> = Vec::with_capacity(dir_entries.len());
151    for files::DirEntry {
152        real_path,
153        vfs_path,
154        file_type,
155    } in dir_entries
156    {
157        let real_path = real_path.display().to_string();
158        let vfs_path = vfs_path.display().to_string();
159
160        if file_type.is_dir() {
161            instructions.push(quote!(self.create_dir(#vfs_path)?;));
162        }
163
164        if file_type.is_file() {
165            instructions.push(quote! {{
166                static BYTES: &[u8] = ::std::include_bytes!(#real_path);
167                let mut file = self.create_file(#vfs_path)?;
168                file.write_all(BYTES)?;
169            }});
170        }
171    }
172
173    quote! {
174        fn load_into_vfs(&self) -> ::vfs::error::VfsResult<()> {
175            #(#instructions)*
176            Ok(())
177        }
178    }
179}