1extern crate anyhow;
2extern crate pest_duckyscript;
3
4use anyhow::{anyhow, Context, Result};
5use pest_duckyscript::mallardscript;
6use std::{
7 collections::HashMap,
8 io::{Seek, Write},
9 path::PathBuf,
10};
11
12static INDENTATION_SIZE: usize = 2;
13
14pub fn compile(
16 current_directory: PathBuf,
17 input_path: &str,
18 output_file: &std::fs::File,
19 indentation: usize,
20 imports_visited: &mut HashMap<String, bool>,
21) -> Result<()> {
22 log::info!("Compiling '{}'.", input_path);
23
24 let input_path_expanded = std::fs::canonicalize(current_directory.join(input_path))
26 .with_context(|| {
27 format!(
28 "Unable to find file input '{}' from '{}'.",
29 input_path,
30 current_directory.display()
31 )
32 })?;
33
34 if imports_visited.contains_key(input_path) {
37 return Err(anyhow!("Circular dependency detected."));
38 } else {
39 imports_visited.insert(
41 input_path_expanded
42 .clone()
43 .into_os_string()
44 .into_string()
45 .unwrap(),
46 true,
47 );
48 }
49
50 let input_contents = std::fs::read_to_string(&input_path_expanded).with_context(|| {
52 format!(
53 "Unable to load file input '{}' from '{}'.",
54 input_path_expanded.display(),
55 current_directory.display()
56 )
57 })?;
58
59 let program_ast = mallardscript::parser::parse_document(input_contents)
61 .with_context(|| ("Unable to parse input."))?;
62
63 for statement in program_ast {
65 compile_statement(
66 input_path,
67 input_path_expanded.clone(),
68 statement,
69 output_file,
70 indentation,
71 imports_visited,
72 )?;
73 }
74
75 Ok(())
76}
77
78fn compile_simple_statement(
80 output_file: &std::fs::File,
81 indentation: usize,
82 command_name: String,
83 command_value: Option<String>,
84) -> Result<()> {
85 if let Some(value) = command_value {
86 log::info!("Processing '{} {}'.", command_name, value);
87
88 write_statement(
89 output_file,
90 indentation,
91 format!("{} {}\n", command_name, value),
92 )?;
93 } else {
94 write_statement(output_file, indentation, format!("{}\n", command_name))?;
96 }
97
98 Ok(())
99}
100
101fn compile_statement(
103 input_path: &str,
104 input_path_expanded: PathBuf,
105 statement: mallardscript::ast::Statement,
106 mut output_file: &std::fs::File,
107 indentation: usize,
108 imports_visited: &mut HashMap<String, bool>,
109) -> Result<()> {
110 match statement {
111 mallardscript::ast::Statement::CommandDefaultDelay(command) => {
112 compile_simple_statement(
113 output_file,
114 indentation,
115 String::from("DEFAULTDELAY"),
116 command.value.into(),
117 )?;
118 }
119 mallardscript::ast::Statement::CommandDefine(command) => {
120 compile_simple_statement(
121 output_file,
122 indentation,
123 String::from("DEFINE"),
124 command.value.into(),
125 )?;
126 }
127 mallardscript::ast::Statement::CommandDelay(command) => {
128 compile_simple_statement(
129 output_file,
130 indentation,
131 String::from("DELAY"),
132 command.value.into(),
133 )?;
134 }
135 mallardscript::ast::Statement::CommandExfil(command) => {
136 compile_simple_statement(
137 output_file,
138 indentation,
139 String::from("EXFIL"),
140 command.name.into(),
141 )?;
142 }
143 mallardscript::ast::Statement::CommandKey(command) => {
144 fn collect_command_key_values(
145 command_key: mallardscript::ast::StatementCommandKey,
146 ) -> Vec<String> {
147 let mut command_key_statements_reduced = command_key.statements.into_iter().fold(
149 vec![] as Vec<String>,
150 |mut accumulation, statement| {
151 if let mallardscript::ast::Statement::CommandKey(statement_command_key) =
152 statement
153 {
154 accumulation.extend(collect_command_key_values(statement_command_key));
155 } else if let mallardscript::ast::Statement::CommandKeyValue(
156 statement_command_key_value,
157 ) = statement
158 {
159 accumulation.push(statement_command_key_value.name);
160 }
161
162 accumulation
163 },
164 );
165
166 if !command_key.remaining_keys.is_empty() {
167 command_key_statements_reduced.push(command_key.remaining_keys);
168 }
169
170 command_key_statements_reduced
171 }
172
173 let command_reduced = collect_command_key_values(command).join(" ");
174 compile_simple_statement(output_file, indentation, command_reduced, None)?;
175 }
176 mallardscript::ast::Statement::CommandRem(command) => {
177 compile_simple_statement(
178 output_file,
179 indentation,
180 String::from("REM"),
181 command.value.into(),
182 )?;
183 }
184 mallardscript::ast::Statement::CommandString(command) => {
185 compile_simple_statement(
186 output_file,
187 indentation,
188 String::from("STRING"),
189 command.value.into(),
190 )?;
191 }
192 mallardscript::ast::Statement::CommandStringln(command) => {
193 compile_simple_statement(
194 output_file,
195 indentation,
196 String::from("STRINGLN"),
197 command.value.into(),
198 )?;
199 }
200 mallardscript::ast::Statement::SingleCommand(command) => {
201 compile_simple_statement(output_file, indentation, command.name, None)?;
202 }
203 mallardscript::ast::Statement::VariableDeclaration(variable) => {
204 log::info!("Processing '${} = {}'.", variable.name, variable.assignment);
205
206 write_statement(
208 output_file,
209 indentation,
210 format!("VAR ${} = {}\n", variable.name, variable.assignment),
211 )?;
212 }
213 mallardscript::ast::Statement::VariableAssignment(variable) => {
214 log::info!("Processing '${} = {}'.", variable.name, variable.assignment);
215
216 write_statement(
218 output_file,
219 indentation,
220 format!("${} = {}\n", variable.name, variable.assignment),
221 )?;
222 }
223 mallardscript::ast::Statement::CommandImport(command) => {
224 let mut new_current_directory = input_path_expanded;
227 new_current_directory.pop();
228 compile(
229 new_current_directory,
230 &command.value,
231 output_file,
232 indentation,
233 imports_visited,
234 )
235 .context(format!(
236 "Unable to import file '{}' from '{}'.",
237 command.value, input_path
238 ))?;
239
240 write_statement(output_file, indentation, String::from("\n"))?;
242 }
243 mallardscript::ast::Statement::BlockIf(block) => {
244 write_statement(
246 output_file,
247 indentation,
248 format!("IF {} THEN\n", block.expression),
249 )?;
250
251 for statement in block.statements_true {
253 compile_statement(
254 input_path,
255 input_path_expanded.clone(),
256 statement,
257 output_file,
258 indentation + INDENTATION_SIZE,
259 imports_visited,
260 )?;
261 }
262
263 if !block.statements_false.is_empty() {
265 write_statement(output_file, indentation, String::from("ELSE\n"))?;
266
267 for statement in block.statements_false {
269 compile_statement(
270 input_path,
271 input_path_expanded.clone(),
272 statement,
273 output_file,
274 indentation + INDENTATION_SIZE,
275 imports_visited,
276 )?;
277 }
278 }
279
280 write_statement(output_file, indentation, String::from("END_IF\n"))?;
282 }
283 mallardscript::ast::Statement::BlockWhile(block) => {
284 write_statement(
286 output_file,
287 indentation,
288 format!("WHILE {}\n", block.expression),
289 )?;
290
291 for statement in block.statements {
293 compile_statement(
294 input_path,
295 input_path_expanded.clone(),
296 statement,
297 output_file,
298 indentation + INDENTATION_SIZE,
299 imports_visited,
300 )?;
301 }
302
303 write_statement(output_file, indentation, String::from("END_WHILE\n"))?;
305 }
306 mallardscript::ast::Statement::End { .. } => {
307 log::info!("Processing End.");
308
309 output_file
311 .set_len(
312 output_file
313 .metadata()
314 .unwrap()
315 .len()
316 .checked_sub("\n".len() as u64)
317 .unwrap(),
318 )
319 .unwrap();
320 output_file.seek(std::io::SeekFrom::End(0))?;
321 }
322 mallardscript::ast::Statement::CommandKeyValue { .. } => {
323 return Err(anyhow!("Provided statement CommandKeyValue not supported at top level commands. These should be nested under CommandKey statements."));
324 }
325 }
326
327 Ok(())
328}
329
330fn write_statement(
333 mut output_file: &std::fs::File,
334 indentation: usize,
335 line: String,
336) -> Result<()> {
337 output_file
338 .write_all(format!("{}{}", " ".repeat(indentation), line).as_bytes())
339 .context("Unable to write to output file.")?;
340
341 Ok(())
342}