proc_include_dir_as_map/
lib.rs1#![forbid(unsafe_code)]
2
3use std::{
4 collections::HashMap,
5 fs::File,
6 io::Read,
7 path::{Path, PathBuf},
8};
9
10use proc_macro::TokenStream;
11use quote::quote;
12
13#[allow(dead_code)]
14type DirMap = HashMap<String, Vec<u8>>;
15
16#[allow(dead_code)]
17fn file_to_bytes(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
18 let mut data = Vec::new();
19 let mut file = File::open(path)?;
20 file.read_to_end(&mut data)?;
21 Ok(data)
22}
23
24#[allow(dead_code)]
25fn dir_to_map(root: &Path, base: &Path) -> Result<DirMap, Box<dyn std::error::Error>> {
26 let mut paths = HashMap::new();
27 for entry in std::fs::read_dir(base)? {
28 let entry = entry?;
29 let path = entry.path();
30 let metadata = std::fs::metadata(&path)?;
31 if metadata.is_file() {
32 let data = file_to_bytes(&path)?;
33 let rel = path.strip_prefix(root)?.to_str().unwrap();
34 paths.insert(rel.to_string(), data);
35 } else if metadata.is_dir() {
36 let dirmap = dir_to_map(root, &path)?;
37 paths.extend(dirmap);
38 } else {
39 panic!("{:?} is not a file or directory", entry);
40 }
41 }
42 Ok(paths)
43}
44
45fn env_expand_dir(raw: &str) -> PathBuf {
46 let mut copy = raw;
47 let mut root = String::new();
48 while let Some(pos) = copy.find('$') {
49 let (head, tail) = copy.split_at(pos);
50 let token = &tail[1..];
51 let end = token
52 .find(|ch: char| !ch.is_alphanumeric() && ch != '_')
53 .unwrap_or(token.len());
54 let env = &token[..end];
55 let val = std::env::var(env)
56 .unwrap_or_else(|_| panic!("{:?} is not a valid environment variable", env));
57 copy = token.strip_prefix(env).unwrap();
58 root.push_str(head);
59 root.push_str(&val);
60 }
61 root.push_str(copy);
62 std::fs::canonicalize(&root).unwrap_or_else(|_| panic!("{:?} is not a valid directory", root))
63}
64
65fn strip_quotes(tokens: TokenStream) -> String {
66 let mut raw = tokens.to_string();
67 if !raw.starts_with('"') || !raw.ends_with('"') || raw.len() < 3 {
68 panic!("directory must be a non-empty string");
69 }
70 raw.pop();
71 raw.remove(0);
72 raw
73}
74
75#[cfg(any(not(debug_assertions), feature = "always-embed"))]
76fn internal_dir_as_map(input: TokenStream) -> TokenStream {
77 let raw = strip_quotes(input);
78 let root = env_expand_dir(&raw);
79 let data =
80 dir_to_map(&root, &root).unwrap_or_else(|_| panic!("{:?} contains invalid entries", root));
81
82 let it = data.iter().map(|(k, v)| {
83 let u = v.iter();
84 quote! {
85 (String::from(#k), Vec::from([ #( #u ),* ]))
86 }
87 });
88
89 let output = quote! {
90 DirMap::from([ #( #it ),* ])
91 };
92
93 output.into()
94}
95
96#[cfg(all(debug_assertions, not(feature = "always-embed")))]
97fn internal_dir_as_map(input: TokenStream) -> TokenStream {
98 let raw = strip_quotes(input);
99 let root = env_expand_dir(&raw);
100 let root = root
101 .to_str()
102 .unwrap_or_else(|| panic!("{:?} cannot be converted to utf-8", root));
103
104 let output = quote! {
105 {
106 use std::{
107 collections::HashMap,
108 fs::File,
109 io::Read,
110 path::Path
111 };
112 fn file_to_bytes(path: &Path) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
113 let mut data = Vec::new();
114 let mut file = File::open(path)?;
115 file.read_to_end(&mut data)?;
116 Ok(data)
117 }
118 fn dir_to_map(root: &Path, base: &Path) -> Result<DirMap, Box<dyn std::error::Error>> {
119 let mut paths = HashMap::new();
120 for entry in std::fs::read_dir(base)? {
121 let entry = entry?;
122 let path = entry.path();
123 let metadata = std::fs::metadata(&path)?;
124 if metadata.is_file() {
125 let data = file_to_bytes(&path)?;
126 let rel = path.strip_prefix(root)?.to_str().unwrap();
127 paths.insert(rel.to_string(), data);
128 } else if metadata.is_dir() {
129 let dirmap = dir_to_map(root, &path)?;
130 paths.extend(dirmap);
131 } else {
132 panic!("{:?} is not a file or directory", entry);
133 }
134 }
135 Ok(paths)
136 }
137 dir_to_map(Path::new(#root), Path::new(#root)).unwrap_or_else(|_| panic!("{:?} contains invalid entries", #root))
138 }
139 };
140
141 output.into()
142}
143
144#[proc_macro]
173pub fn include_dir_as_map(input: TokenStream) -> TokenStream {
174 internal_dir_as_map(input)
175}
176
177#[cfg(test)]
178mod tests {
179 use crate::*;
180
181 #[test]
182 fn valid_dir() {
183 let root = Path::new(".");
184 let _data = dir_to_map(root, root).unwrap();
185 }
186
187 #[test]
188 #[should_panic]
189 fn invalid_dir() {
190 let root = Path::new("invalid_path_for_testing");
191 let _data = dir_to_map(root, root).unwrap();
192 }
193
194 #[test]
195 fn valid_env_var() {
196 let path = env_expand_dir("$CARGO_MANIFEST_DIR");
197 let ok = path.ends_with("proc");
198 assert_eq!(ok, true);
199 }
200
201 #[test]
202 fn valid_env_var_2() {
203 std::env::set_var("SRC_DIR", "src");
204 let path = env_expand_dir("$CARGO_MANIFEST_DIR/$SRC_DIR");
205 let ok = path.ends_with("src");
206 assert_eq!(ok, true);
207 }
208
209 #[test]
210 #[should_panic]
211 fn invalid_env_var() {
212 let _path = env_expand_dir("$INVALID_ENV_VAR_FOR_TESTING");
213 }
214}