1#![doc = include_str!("../readme.md")]
2
3pub mod emit;
5mod error;
6pub mod formatter;
8pub mod implib;
11mod reader;
12mod writer;
13
14use emit::*;
15use std::collections::{BTreeMap, HashMap, HashSet};
16use std::path::{Path, PathBuf};
17use syn::spanned::Spanned;
18use windows_metadata as metadata;
19
20pub use error::Error;
21use proc_macro2::{Literal, Span, TokenStream};
22use quote::quote;
23pub use reader::Reader;
24pub use writer::Writer;
25
26pub(crate) const METADATA_NAMESPACE: &str = "Windows.Win32.Metadata";
28
29pub(crate) struct PseudoAttr {
31 pub short: &'static str,
32 pub metadata: &'static str,
33 pub prop: Option<&'static str>,
35}
36
37pub(crate) const PSEUDO_ATTRS: &[PseudoAttr] = &[
39 PseudoAttr {
40 short: "retval",
41 metadata: "RetValAttribute",
42 prop: None,
43 },
44 PseudoAttr {
45 short: "iid_is",
46 metadata: "ComOutPtrAttribute",
47 prop: None,
48 },
49 PseudoAttr {
50 short: "len_param",
51 metadata: "NativeArrayInfoAttribute",
52 prop: Some("CountParamIndex"),
53 },
54 PseudoAttr {
55 short: "len_const",
56 metadata: "NativeArrayInfoAttribute",
57 prop: Some("CountConst"),
58 },
59 PseudoAttr {
60 short: "size_param",
61 metadata: "MemorySizeAttribute",
62 prop: Some("BytesParamIndex"),
63 },
64 PseudoAttr {
65 short: "reserved",
66 metadata: "ReservedAttribute",
67 prop: None,
68 },
69 PseudoAttr {
70 short: "noreturn",
71 metadata: "DoesNotReturnAttribute",
72 prop: None,
73 },
74 PseudoAttr {
75 short: "scoped",
76 metadata: "ScopedEnumAttribute",
77 prop: None,
78 },
79 PseudoAttr {
80 short: "encoding",
81 metadata: "NativeEncodingAttribute",
82 prop: None,
83 },
84];
85
86pub(crate) fn pseudo_by_short(short: &str) -> Option<&'static PseudoAttr> {
87 PSEUDO_ATTRS.iter().find(|p| p.short == short)
88}
89
90pub(crate) fn pseudo_for_metadata(name: &str, arg_names: &[String]) -> Option<&'static PseudoAttr> {
93 let mut fallback = None;
94 for pseudo in PSEUDO_ATTRS.iter().filter(|p| p.metadata == name) {
95 match pseudo.prop {
96 Some(prop) if arg_names.len() == 1 && arg_names[0] == prop => return Some(pseudo),
97 None => fallback = Some(pseudo),
98 _ => {}
99 }
100 }
101 fallback
102}
103
104pub fn reader() -> Reader {
106 Reader::new()
107}
108
109pub fn item_names(path: impl AsRef<Path>, namespace: &str) -> Result<Vec<String>, Error> {
111 reader::item_names(path, namespace)
112}
113
114pub fn writer() -> Writer {
116 Writer::new()
117}
118
119pub struct ArchInput {
121 pub rdl_dir: PathBuf,
122 pub winmd: PathBuf,
123 pub bits: i32,
124}
125
126pub fn merge_arch_rdl(
128 inputs: &[ArchInput],
129 seed: Option<&Path>,
130 output_dir: impl AsRef<Path>,
131) -> Result<(), Error> {
132 let output_dir = output_dir.as_ref();
133
134 if inputs.is_empty() {
135 return Err(writer_err!(
136 "merge_arch_rdl requires at least one arch input"
137 ));
138 }
139
140 let seed = seed
142 .map(|seed| {
143 let name = seed
144 .file_name()
145 .ok_or_else(|| writer_err!("invalid seed path `{}`", seed.display()))?
146 .to_os_string();
147 let text = std::fs::read(seed)
148 .map_err(|e| writer_err!("failed to read seed `{}`: {e}", seed.display()))?;
149 Ok::<_, Error>((name, seed.to_path_buf(), text))
150 })
151 .transpose()?;
152
153 let temp = std::env::temp_dir().join(format!(
155 "win32-arch-merge-{}-{}",
156 std::process::id(),
157 std::time::SystemTime::now()
158 .duration_since(std::time::UNIX_EPOCH)
159 .map_or(0, |d| d.as_nanos())
160 ));
161 std::fs::create_dir_all(&temp)
162 .map_err(|e| writer_err!("failed to create temp dir `{}`: {e}", temp.display()))?;
163 let _scratch = ScratchDir(temp.clone());
164 let merged = temp.join("Windows.Win32.merged.winmd");
165 let mut merger = metadata::merge();
166 for input in inputs {
167 merger.arch_input(&input.winmd, input.bits);
168 }
169 merger
170 .output(&merged)
171 .merge()
172 .map_err(|e| writer_err!("arch-merge failed: {e}"))?;
173
174 let mut map = HashMap::<String, String>::new();
176 for input in inputs {
177 for entry in std::fs::read_dir(&input.rdl_dir)
178 .map_err(|e| writer_err!("failed to read `{}`: {e}", input.rdl_dir.display()))?
179 .flatten()
180 {
181 let path = entry.path();
182 if path.extension().is_none_or(|x| x != "rdl")
183 || path.file_name() == seed.as_ref().map(|(name, _, _)| name.as_os_str())
184 {
185 continue;
186 }
187 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
188 continue;
189 };
190 for name in reader::item_names(&path, "Windows.Win32")? {
191 map.entry(name).or_insert_with(|| stem.to_string());
192 }
193 }
194 }
195
196 writer()
197 .input(&merged)
198 .partition(map)
199 .output(output_dir)
200 .write()?;
201
202 if let Some((_, seed_path, seed_text)) = seed {
204 write_to_file(seed_path, seed_text)?;
205 }
206
207 Ok(())
208}
209
210struct ScratchDir(PathBuf);
212
213impl Drop for ScratchDir {
214 fn drop(&mut self) {
215 let _ = std::fs::remove_dir_all(&self.0);
216 }
217}
218
219pub fn expand_input_paths<P: AsRef<Path>>(
220 inputs: &[P],
221 ext1: &str,
222 ext2: &str,
223) -> Result<(Vec<PathBuf>, Vec<PathBuf>), Error> {
224 let mut paths1 = vec![];
225 let mut paths2 = vec![];
226
227 for input in inputs {
228 let path = input.as_ref();
229 let display = path.to_string_lossy();
230
231 if path.is_dir() {
232 let prev_total = paths1.len() + paths2.len();
233
234 for entry_path in path
235 .read_dir()
236 .map_err(|_| Error::new("failed to read directory", &display, 0, 0))?
237 .flatten()
238 .map(|entry| entry.path())
239 {
240 if entry_path.is_file() {
241 if entry_path
242 .extension()
243 .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1))
244 {
245 paths1.push(entry_path);
246 } else if entry_path
247 .extension()
248 .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2))
249 {
250 paths2.push(entry_path);
251 }
252 }
253 }
254
255 if paths1.len() + paths2.len() == prev_total {
256 let message = if ext1 == ext2 {
257 format!("failed to find .{ext1} files in directory")
258 } else {
259 format!("failed to find .{ext1} or .{ext2} files in directory")
260 };
261 return Err(Error::new(&message, &display, 0, 0));
262 }
263 } else if path
264 .extension()
265 .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1))
266 {
267 paths1.push(path.to_path_buf());
268 } else if path
269 .extension()
270 .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2))
271 {
272 paths2.push(path.to_path_buf());
273 } else {
274 let message = if ext1 == ext2 {
275 format!("expected .{ext1} file")
276 } else {
277 format!("expected .{ext1} or .{ext2} file")
278 };
279 return Err(Error::new(&message, &display, 0, 0));
280 }
281 }
282
283 Ok((paths1, paths2))
284}
285
286pub fn expand_input_files<P: AsRef<Path>>(
288 inputs: &[P],
289 extension: &str,
290) -> Result<Vec<PathBuf>, Error> {
291 Ok(expand_input_paths(inputs, extension, extension)?.0)
292}
293
294pub fn write_to_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), Error> {
295 let path = path.as_ref();
296 let display = path.to_string_lossy();
297 if let Some(parent) = path.parent() {
298 std::fs::create_dir_all(parent)
299 .map_err(|_| writer_err!("failed to create directory `{display}`"))?;
300 }
301
302 std::fs::write(path, contents).map_err(|_| writer_err!("failed to write file `{display}`"))
303}
304
305macro_rules! writer_err {
306 ($($arg:tt)*) => {
307 Error::new(&format!($($arg)*), "", 0, 0)
308 };
309}
310
311use writer_err;
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn prop_bound_pseudo_requires_sole_argument() {
319 let sole = ["CountParamIndex".to_string()];
320 let pseudo = pseudo_for_metadata("NativeArrayInfoAttribute", &sole)
321 .expect("single-property NativeArrayInfo should map to a short pseudo");
322 assert_eq!(pseudo.short, "len_param");
323
324 let extra = ["CountParamIndex".to_string(), "CountConst".to_string()];
325 assert!(
326 pseudo_for_metadata("NativeArrayInfoAttribute", &extra).is_none(),
327 "a multi-valued property-bound attribute must fall back to the fully-qualified spelling"
328 );
329 }
330
331 #[test]
332 fn property_less_pseudo_matches_by_name() {
333 let pseudo = pseudo_for_metadata("RetValAttribute", &[])
334 .expect("RetValAttribute should map to a pseudo");
335 assert_eq!(pseudo.short, "retval");
336 }
337}