1 | mod args; |
2 | mod error; |
3 | mod metadata; |
4 | mod rdl; |
5 | mod rust; |
6 | mod tokens; |
7 | mod tree; |
8 | mod winmd; |
9 | |
10 | pub use error::{Error, Result}; |
11 | use tree::Tree; |
12 | |
13 | enum ArgKind { |
14 | None, |
15 | Input, |
16 | Output, |
17 | Filter, |
18 | Config, |
19 | } |
20 | |
21 | pub fn bindgen<I, S>(args: I) -> Result<String> |
22 | where |
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 | |
127 | fn 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 | |
163 | fn 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 | |
184 | fn 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 | |
188 | fn 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 | |
192 | fn 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 | |
205 | fn 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 | |
219 | fn 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 | |
223 | fn 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 | |
231 | fn 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 | |
253 | fn extension(path: &str) -> &str { |
254 | path.rsplit_once('.' ).map_or(default:"" , |(_, extension: &str)| extension) |
255 | } |
256 | |
257 | fn directory(path: &str) -> &str { |
258 | path.rsplit_once(&['/' , ' \\' ]).map_or(default:"" , |(directory: &str, _)| directory) |
259 | } |
260 | |