1mod args;
2mod error;
3mod metadata;
4mod rdl;
5mod rust;
6mod tokens;
7mod tree;
8mod winmd;
9
10pub use error::{Error, Result};
11use tree::Tree;
12
13enum ArgKind {
14 None,
15 Input,
16 Output,
17 Filter,
18 Config,
19}
20
21pub fn bindgen<I, S>(args: I) -> Result<String>
22where
23 I: IntoIterator<Item = S>,
24 S: AsRef<str>,
25{
26 let time = std::time::Instant::now();
27 let args = args::expand(args)?;
28
29 let mut kind = ArgKind::None;
30 let mut output = None;
31 let mut input = Vec::<&str>::new();
32 let mut include = Vec::<&str>::new();
33 let mut exclude = Vec::<&str>::new();
34 let mut config = std::collections::BTreeMap::<&str, &str>::new();
35 let mut format = false;
36
37 for arg in &args {
38 if arg.starts_with('-') {
39 kind = ArgKind::None;
40 }
41
42 match kind {
43 ArgKind::None => match arg.as_str() {
44 "-i" | "--in" => kind = ArgKind::Input,
45 "-o" | "--out" => kind = ArgKind::Output,
46 "-f" | "--filter" => kind = ArgKind::Filter,
47 "--config" => kind = ArgKind::Config,
48 "--format" => format = true,
49 _ => return Err(Error::new(&format!("invalid option `{arg}`"))),
50 },
51 ArgKind::Output => {
52 if output.is_none() {
53 output = Some(arg.as_str());
54 } else {
55 return Err(Error::new("too many outputs"));
56 }
57 }
58 ArgKind::Input => input.push(arg.as_str()),
59 ArgKind::Filter => {
60 if let Some(rest) = arg.strip_prefix('!') {
61 exclude.push(rest);
62 } else {
63 include.push(arg.as_str());
64 }
65 }
66 ArgKind::Config => {
67 if let Some((key, value)) = arg.split_once('=') {
68 config.insert(key, value);
69 } else {
70 config.insert(arg, "");
71 }
72 }
73 }
74 }
75
76 if format {
77 if output.is_some() || !include.is_empty() || !exclude.is_empty() {
78 return Err(Error::new("`--format` cannot be combined with `--out` or `--filter`"));
79 }
80
81 let input = filter_input(&input, &["rdl"])?;
82
83 if input.is_empty() {
84 return Err(Error::new("no .rdl inputs"));
85 }
86
87 for path in &input {
88 read_file_text(path).and_then(|source| rdl::File::parse_str(&source)).and_then(|file| write_to_file(path, file.fmt())).map_err(|err| err.with_path(path))?;
89 }
90
91 return Ok(String::new());
92 }
93
94 let Some(output) = output else {
95 return Err(Error::new("no output"));
96 };
97
98 // This isn't strictly necessary but avoids a common newbie pitfall where all metadata
99 // would be generated when building a component for a specific API.
100 if include.is_empty() {
101 return Err(Error::new("at least one `--filter` must be specified"));
102 }
103
104 let output = canonicalize(output)?;
105
106 let input = read_input(&input)?;
107 let reader = metadata::Reader::filter(input, &include, &exclude);
108
109 winmd::verify(reader)?;
110
111 match extension(&output) {
112 "rdl" => rdl::from_reader(reader, config, &output)?,
113 "winmd" => winmd::from_reader(reader, config, &output)?,
114 "rs" => rust::from_reader(reader, config, &output)?,
115 _ => return Err(Error::new("output extension must be one of winmd/rdl/rs")),
116 }
117
118 let elapsed = time.elapsed().as_secs_f32();
119
120 if elapsed > 0.1 {
121 Ok(format!(" Finished writing `{}` in {:.2}s", output, time.elapsed().as_secs_f32()))
122 } else {
123 Ok(format!(" Finished writing `{}`", output,))
124 }
125}
126
127fn filter_input(input: &[&str], extensions: &[&str]) -> Result<Vec<String>> {
128 fn try_push(path: &str, extensions: &[&str], results: &mut Vec<String>) -> Result<()> {
129 // First canonicalize input so that the extension check below will match the case of the path.
130 let path = canonicalize(path)?;
131
132 if extensions.contains(&extension(&path)) {
133 results.push(path);
134 }
135
136 Ok(())
137 }
138
139 let mut results = vec![];
140
141 for input in input {
142 let path = std::path::Path::new(input);
143
144 if !path.exists() {
145 return Err(Error::new("failed to read input").with_path(input));
146 }
147
148 if path.is_dir() {
149 for entry in path.read_dir().map_err(|_| Error::new("failed to read directory").with_path(input))?.flatten() {
150 let path = entry.path();
151
152 if path.is_file() {
153 try_push(&path.to_string_lossy(), extensions, &mut results)?;
154 }
155 }
156 } else {
157 try_push(&path.to_string_lossy(), extensions, &mut results)?;
158 }
159 }
160 Ok(results)
161}
162
163fn read_input(input: &[&str]) -> Result<Vec<metadata::File>> {
164 let input: Vec = filter_input(input, &["winmd", "rdl"])?;
165 let mut results: Vec = vec![];
166
167 if cfg!(feature = "metadata") {
168 results.push(metadata::File::new(bytes:std::include_bytes!("../default/Windows.winmd").to_vec()).unwrap());
169 results.push(metadata::File::new(bytes:std::include_bytes!("../default/Windows.Win32.winmd").to_vec()).unwrap());
170 results.push(metadata::File::new(bytes:std::include_bytes!("../default/Windows.Wdk.winmd").to_vec()).unwrap());
171 } else if input.is_empty() {
172 return Err(Error::new(message:"no inputs"));
173 }
174
175 for input: &String in &input {
176 let file: File = if extension(path:input) == "winmd" { read_winmd_file(path:input)? } else { read_rdl_file(path:input)? };
177
178 results.push(file);
179 }
180
181 Ok(results)
182}
183
184fn read_file_text(path: &str) -> Result<String> {
185 std::fs::read_to_string(path).map_err(|_| Error::new(message:"failed to read text file"))
186}
187
188fn read_file_bytes(path: &str) -> Result<Vec<u8>> {
189 std::fs::read(path).map_err(|_| Error::new(message:"failed to read binary file"))
190}
191
192fn read_file_lines(path: &str) -> Result<Vec<String>> {
193 use std::io::BufRead;
194 fn error(path: &str) -> Error {
195 Error::new(message:"failed to read lines").with_path(path)
196 }
197 let file: BufReader = std::io::BufReader::new(inner:std::fs::File::open(path).map_err(|_| error(path))?);
198 let mut lines: Vec = vec![];
199 for line: Result in file.lines() {
200 lines.push(line.map_err(|_| error(path))?);
201 }
202 Ok(lines)
203}
204
205fn read_rdl_file(path: &str) -> Result<metadata::File> {
206 read_file_text(path)
207 .and_then(|source| rdl::File::parse_str(&source))
208 .and_then(|file| file.into_winmd())
209 .map(|bytes| {
210 // TODO: Write bytes to file if you need to debug the intermediate .winmd file like so:
211 _ = write_to_file("temp.winmd", &bytes);
212
213 // Unwrapping here is fine since `rdl_to_winmd` should have produced a valid winmd
214 metadata::File::new(bytes).unwrap()
215 })
216 .map_err(|err: Error| err.with_path(path))
217}
218
219fn read_winmd_file(path: &str) -> Result<metadata::File> {
220 read_file_bytes(path).and_then(|bytes: Vec| metadata::File::new(bytes).ok_or_else(|| Error::new(message:"failed to read .winmd format").with_path(path)))
221}
222
223fn write_to_file<C: AsRef<[u8]>>(path: &str, contents: C) -> Result<()> {
224 if let Some(parent: &Path) = std::path::Path::new(path).parent() {
225 std::fs::create_dir_all(parent).map_err(|_| Error::new(message:"failed to create directory").with_path(path))?;
226 }
227
228 std::fs::write(path, contents).map_err(|_| Error::new(message:"failed to write file").with_path(path))
229}
230
231fn canonicalize(value: &str) -> Result<String> {
232 let temp: bool = !std::path::Path::new(value).exists();
233
234 // `std::fs::canonicalize` only works if the file exists so we temporarily create it here.
235 if temp {
236 write_to_file(path:value, contents:"")?;
237 }
238
239 let path: PathBuf = std::fs::canonicalize(value).map_err(|_| Error::new(message:"failed to find path").with_path(value))?;
240
241 if temp {
242 std::fs::remove_file(value).map_err(|_| Error::new(message:"failed to remove temporary file").with_path(value))?;
243 }
244
245 let path: String = path.to_string_lossy().trim_start_matches(r"\\?\").to_string();
246
247 match path.rsplit_once(delimiter:'.') {
248 Some((file: &str, extension: &str)) => Ok(format!("{file}.{}", extension.to_lowercase())),
249 _ => Ok(path),
250 }
251}
252
253fn extension(path: &str) -> &str {
254 path.rsplit_once('.').map_or(default:"", |(_, extension: &str)| extension)
255}
256
257fn directory(path: &str) -> &str {
258 path.rsplit_once(&['/', '\\']).map_or(default:"", |(directory: &str, _)| directory)
259}
260