mallardscript/
lib.rs

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
14/// Compile MallardScript input path to DuckyScript output file.
15pub 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    // Expand our input path.
25    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    // Handle Circular Dependencies.
35    // Do not compile input, if we've already compiled it before.
36    if imports_visited.contains_key(input_path) {
37        return Err(anyhow!("Circular dependency detected."));
38    } else {
39        // Mark import as visited.
40        imports_visited.insert(
41            input_path_expanded
42                .clone()
43                .into_os_string()
44                .into_string()
45                .unwrap(),
46            true,
47        );
48    }
49
50    // Load input contents.
51    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    // Parse input contents into AST.
60    let program_ast = mallardscript::parser::parse_document(input_contents)
61        .with_context(|| ("Unable to parse input."))?;
62
63    // Process AST.
64    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
78/// Compile MallardScript simple command to DuckyScript output file.
79fn 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        // Process all other statement commands.
95        write_statement(output_file, indentation, format!("{}\n", command_name))?;
96    }
97
98    Ok(())
99}
100
101/// Compile MallardScript statement.
102fn 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                // Collect all command key statement command key values.
148                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            // Process all variable statements.
207            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            // Process all variable statements.
217            write_statement(
218                output_file,
219                indentation,
220                format!("${} = {}\n", variable.name, variable.assignment),
221            )?;
222        }
223        mallardscript::ast::Statement::CommandImport(command) => {
224            // Compile import file.
225            // Make sure to get the current working directory so imports can resolve locally.
226            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            // Add a new line after import file compilation.
241            write_statement(output_file, indentation, String::from("\n"))?;
242        }
243        mallardscript::ast::Statement::BlockIf(block) => {
244            // Process block if statement.
245            write_statement(
246                output_file,
247                indentation,
248                format!("IF {} THEN\n", block.expression),
249            )?;
250
251            // Process block if statement, true case statements.
252            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            // Add ELSE statement.
264            if !block.statements_false.is_empty() {
265                write_statement(output_file, indentation, String::from("ELSE\n"))?;
266
267                // Process block if statement, false case statements.
268                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            // Add the END_IF statement.
281            write_statement(output_file, indentation, String::from("END_IF\n"))?;
282        }
283        mallardscript::ast::Statement::BlockWhile(block) => {
284            // Process block while statement.
285            write_statement(
286                output_file,
287                indentation,
288                format!("WHILE {}\n", block.expression),
289            )?;
290
291            // Process block while statement statements.
292            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            // Add the END_WHILE statement.
304            write_statement(output_file, indentation, String::from("END_WHILE\n"))?;
305        }
306        mallardscript::ast::Statement::End { .. } => {
307            log::info!("Processing End.");
308
309            // Remove statement end line from end of file.
310            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
330/// Write a statement line to the output file.
331/// This also adds indentation for the statement line.
332fn 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}