Skip to main content

dump/
dump.rs

1//! Dumps every piece of AutoIt extraction information for an input file.
2
3use std::{env, fs, process::ExitCode};
4
5use autoit::{Artifact, AutoItBinary, RecognitionFailure, Record, Script, StringFinding};
6
7fn main() -> ExitCode {
8    let mut args = env::args_os();
9    let _program = args.next();
10    let mut path = None;
11    for arg in args {
12        if path.is_none() {
13            path = Some(arg);
14        } else {
15            eprintln!("usage: dump <autoit-exe-or-a3x>");
16            return ExitCode::FAILURE;
17        }
18    }
19    let Some(path) = path else {
20        eprintln!("usage: dump <autoit-exe-or-a3x>");
21        return ExitCode::FAILURE;
22    };
23
24    let data = match fs::read(&path) {
25        Ok(data) => data,
26        Err(err) => {
27            eprintln!("failed to read input: {err}");
28            return ExitCode::FAILURE;
29        }
30    };
31
32    match AutoItBinary::try_parse(&data) {
33        Ok(binary) => {
34            print_text(&binary);
35            ExitCode::SUCCESS
36        }
37        Err(err) if err.recognition_failure() == Some(RecognitionFailure::NotRecognized) => {
38            eprintln!("not recognized as AutoIt");
39            ExitCode::FAILURE
40        }
41        Err(err) => {
42            eprintln!("failed to parse AutoIt payload: {err}");
43            ExitCode::FAILURE
44        }
45    }
46}
47
48fn print_text(binary: &AutoItBinary) {
49    print_overview(binary);
50    print_container(binary);
51    print_observations(binary);
52    print_records(binary.records());
53    print_record_diagnostics(binary);
54    print_scripts(binary.scripts());
55    print_artifacts(binary.artifacts());
56    print_strings(binary.strings());
57}
58
59fn print_overview(binary: &AutoItBinary) {
60    section("overview");
61    field("input kind", format!("{:?}", binary.input_kind()));
62    field("encoding", opt_debug(binary.encoding()));
63    field("records", binary.records().len().to_string());
64    field("scripts", binary.scripts().len().to_string());
65    field("artifacts", binary.artifacts().len().to_string());
66    field("strings", binary.strings().len().to_string());
67    field(
68        "record diagnostics",
69        binary.record_diagnostics().len().to_string(),
70    );
71}
72
73fn print_container(binary: &AutoItBinary) {
74    let container = binary.container();
75    section("container");
76    field("input kind", format!("{:?}", container.input_kind()));
77    field("encoding", opt_debug(container.encoding()));
78    field(
79        "autoit signature offset",
80        container
81            .autoit_signature_offset()
82            .map_or_else(|| "none".to_string(), |offset| format!("{offset}")),
83    );
84    field(
85        "version marker",
86        container.version_marker().map_or_else(
87            || "none".to_string(),
88            |marker| format!("{:?} @ offset {}", marker.encoding, marker.offset),
89        ),
90    );
91    field(
92        "payload stream offsets",
93        if container.payload_stream_offsets().is_empty() {
94            "none".to_string()
95        } else {
96            format!("{:?}", container.payload_stream_offsets())
97        },
98    );
99
100    match container.pe_script_resource() {
101        None => field("pe script resource", "none".to_string()),
102        Some(resource) => {
103            println!("  pe script resource:");
104            subfield(
105                "type",
106                match resource.type_name {
107                    Some(name) => format!("{} ({name})", resource.type_id),
108                    None => resource.type_id.to_string(),
109                },
110            );
111            subfield("name", escape_inline(resource.name));
112            subfield(
113                "language id",
114                resource
115                    .language_id
116                    .map_or_else(|| "none".to_string(), |id| id.to_string()),
117            );
118            subfield("rva", format!("{:#x}", resource.rva));
119            subfield("file offset", resource.offset.to_string());
120            subfield("size", format!("{} bytes", resource.size));
121        }
122    }
123
124    if container.packed_markers().is_empty() {
125        field("packed markers", "none".to_string());
126    } else {
127        println!("  packed markers:");
128        for marker in container.packed_markers() {
129            println!("    {} @ offset {}", marker.name, marker.offset);
130        }
131    }
132}
133
134fn print_observations(binary: &AutoItBinary) {
135    let observations = binary.observations().entries();
136    section(&format!("observations ({})", observations.len()));
137    for observation in observations {
138        println!("  {observation:?}");
139    }
140}
141
142fn print_records(records: &[Record]) {
143    section(&format!("records ({})", records.len()));
144    for record in records {
145        println!("  #{} @ offset {}", record.index(), record.offset());
146        subfield("subtype", escape_inline(record.subtype()));
147        subfield("name", escape_inline(record.name()));
148        let profile = record.profile();
149        subfield(
150            "profile",
151            format!(
152                "encoding={:?} encryption={:?} compression={:?}",
153                profile.encoding, profile.encryption, profile.compression
154            ),
155        );
156        subfield(
157            "compressed",
158            format!(
159                "{} (compressed_size={}, uncompressed_size={})",
160                record.compressed(),
161                record.compressed_size(),
162                record.uncompressed_size()
163            ),
164        );
165        subfield(
166            "checksum",
167            format!(
168                "{:#010x} ({})",
169                record.checksum(),
170                if record.checksum_valid() {
171                    "valid"
172                } else {
173                    "invalid"
174                }
175            ),
176        );
177        subfield(
178            "timestamps",
179            format!(
180                "created={} last_write={}",
181                record.creation_time(),
182                record.last_write_time()
183            ),
184        );
185        subfield(
186            "data sizes",
187            format!(
188                "encrypted={} decrypted={} decompressed={} payload={}",
189                record.encrypted_data().len(),
190                record.decrypted_data().len(),
191                record
192                    .decompressed_data()
193                    .map_or_else(|| "none".to_string(), |data| data.len().to_string()),
194                record.payload_data().len()
195            ),
196        );
197        subfield(
198            "decompression",
199            format!("{:?}", record.decompression_status()),
200        );
201    }
202}
203
204fn print_record_diagnostics(binary: &AutoItBinary) {
205    let diagnostics = binary.record_diagnostics();
206    if diagnostics.is_empty() {
207        return;
208    }
209    section(&format!("record diagnostics ({})", diagnostics.len()));
210    for diagnostic in diagnostics {
211        println!(
212            "  record #{} @ offset {}: {:?}",
213            diagnostic.record_index, diagnostic.offset, diagnostic.reason
214        );
215    }
216}
217
218fn print_scripts(scripts: &[Script]) {
219    section(&format!("scripts ({})", scripts.len()));
220    for script in scripts {
221        println!(
222            "  record #{} {}",
223            script.record_index(),
224            escape_inline(script.name())
225        );
226        subfield("kind", format!("{:?}", script.kind()));
227        subfield("bytes", script.bytes().len().to_string());
228        subfield(
229            "timestamps",
230            format!(
231                "created={} last_write={}",
232                script.creation_time(),
233                script.last_write_time()
234            ),
235        );
236        subfield(
237            "text encoding",
238            script.text().map_or_else(
239                || "none".to_string(),
240                |text| format!("{:?}", text.encoding()),
241            ),
242        );
243        subfield("decode error", opt_debug(script.decode_error()));
244        match script.token_stream() {
245            None => subfield("token stream", "none".to_string()),
246            Some(tokens) => subfield(
247                "token stream",
248                format!(
249                    "line_count={} tokens={}",
250                    tokens.line_count(),
251                    tokens.tokens().len()
252                ),
253            ),
254        }
255        match script.source_text() {
256            None => subfield("source", "<unavailable>".to_string()),
257            Some(source) => {
258                println!("    source ({} chars):", source.chars().count());
259                print_block(source);
260            }
261        }
262    }
263}
264
265fn print_artifacts(artifacts: &[Artifact]) {
266    section(&format!("artifacts ({})", artifacts.len()));
267    for artifact in artifacts {
268        println!(
269            "  record #{} {}",
270            artifact.record_index(),
271            escape_inline(artifact.name())
272        );
273        subfield("subtype", escape_inline(artifact.subtype()));
274        subfield("bytes", artifact.bytes().len().to_string());
275        subfield(
276            "timestamps",
277            format!(
278                "created={} last_write={}",
279                artifact.creation_time(),
280                artifact.last_write_time()
281            ),
282        );
283        subfield("checksum valid", artifact.checksum_valid().to_string());
284        subfield(
285            "decompression",
286            format!("{:?}", artifact.decompression_status()),
287        );
288    }
289}
290
291fn print_strings(strings: &[StringFinding]) {
292    section(&format!("strings ({})", strings.len()));
293    for found in strings {
294        println!(
295            "  record #{} offset {} {:?}: {}",
296            found.record_index(),
297            found.offset(),
298            found.encoding(),
299            escape_inline(found.value())
300        );
301    }
302}
303
304/// Prints a section header.
305fn section(title: &str) {
306    println!("== {title} ==");
307}
308
309/// Prints a top-level `label: value` line.
310fn field(label: &str, value: String) {
311    println!("  {label}: {value}");
312}
313
314/// Prints an indented `label: value` line under a record/script/artifact entry.
315fn subfield(label: &str, value: String) {
316    println!("    {label}: {value}");
317}
318
319/// Prints a multi-line text block indented under its owning entry.
320fn print_block(text: &str) {
321    for line in text.lines() {
322        println!("    | {line}");
323    }
324}
325
326/// Formats an optional `Debug` value, rendering `None` as `none`.
327fn opt_debug<T>(value: Option<T>) -> String
328where
329    T: core::fmt::Debug,
330{
331    value.map_or_else(|| "none".to_string(), |value| format!("{value:?}"))
332}
333
334/// Renders a string on a single line, escaping control characters so embedded
335/// newlines, tabs, and other control bytes cannot corrupt the layout.
336fn escape_inline(value: &str) -> String {
337    let mut out = String::new();
338    for ch in value.chars() {
339        match ch {
340            '\n' => out.push_str("\\n"),
341            '\r' => out.push_str("\\r"),
342            '\t' => out.push_str("\\t"),
343            ch if ch.is_control() => out.push_str(format!("\\u{{{:04x}}}", u32::from(ch)).as_str()),
344            ch => out.push(ch),
345        }
346    }
347    out
348}