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}