Skip to main content

crashrustler/
analysis.rs

1use std::collections::BTreeMap;
2
3use crate::crash_rustler::CrashRustler;
4use crate::types::*;
5
6impl CrashRustler {
7    /// Returns true if the crash was due to a bad memory access.
8    /// EXC_BAD_ACCESS (type 1) is a bad memory access UNLESS it's a general
9    /// protection fault (code 0xd) with address 0.
10    /// Equivalent to -[CrashReport _crashedDueToBadMemoryAccess]
11    ///
12    /// # Examples
13    ///
14    /// ```
15    /// use crashrustler::{CrashRustler, CpuType};
16    ///
17    /// let mut cr = CrashRustler::default();
18    /// cr.exception_type = 1; // EXC_BAD_ACCESS
19    /// cr.exception_code = vec![2, 0xdeadbeef];
20    /// assert!(cr.crashed_due_to_bad_memory_access());
21    ///
22    /// // GPF with null address on x86 is NOT a bad memory access
23    /// cr.cpu_type = CpuType::X86_64;
24    /// cr.exception_code = vec![0xd, 0x0];
25    /// assert!(!cr.crashed_due_to_bad_memory_access());
26    /// ```
27    pub fn crashed_due_to_bad_memory_access(&self) -> bool {
28        if self.exception_type != 1 {
29            return false;
30        }
31        // GPF with null address is NOT treated as bad memory access (x86 only)
32        if self.is_x86_cpu()
33            && self.exception_code.first() == Some(&0xd)
34            && self.exception_code.get(1) == Some(&0)
35        {
36            return false;
37        }
38        true
39    }
40
41    /// Extracts the crashing address from exception codes or exception state.
42    /// For bad memory access: uses exception_code\[1\].
43    /// For code sign killed: extracts cr2 from exception state registers.
44    /// Equivalent to the address extraction in -[CrashReport _extractVMMap]
45    pub fn extract_crashing_address(&mut self) {
46        if self.crashed_due_to_bad_memory_access() {
47            if let Some(&addr) = self.exception_code.get(1) {
48                self.crashing_address = addr as u64;
49            }
50        } else if self.is_code_sign_killed() {
51            let state = &self.exception_state.state;
52            if self.is_arm_cpu() {
53                // ARM64 exception state: FAR (Fault Address Register) at state[0..1] as u64
54                if state.len() >= 2 {
55                    self.crashing_address = (state[0] as u64) | ((state[1] as u64) << 32);
56                }
57            } else {
58                // x86 exception state: cr2 location depends on sub-flavor
59                if !state.is_empty() {
60                    if state[0] == 1 {
61                        // 32-bit exception state: cr2 at index 4
62                        if let Some(&cr2) = state.get(4) {
63                            self.crashing_address = cr2 as u64;
64                        }
65                    } else {
66                        // 64-bit exception state: cr2 at indices 4-5
67                        if state.len() > 5 {
68                            self.crashing_address = (state[4] as u64) | ((state[5] as u64) << 32);
69                        }
70                    }
71                }
72            }
73        }
74    }
75
76    /// Sanitizes a file path by replacing user-specific components.
77    /// Replaces /Users/username/ with /Users/USER/ for privacy.
78    /// Equivalent to _CRCopySanitizedPath used by -[CrashReport cleansePaths]
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use crashrustler::CrashRustler;
84    ///
85    /// assert_eq!(
86    ///     CrashRustler::sanitize_path("/Users/alice/Library/MyApp"),
87    ///     "/Users/USER/Library/MyApp"
88    /// );
89    /// assert_eq!(
90    ///     CrashRustler::sanitize_path("/System/Library/Frameworks/AppKit"),
91    ///     "/System/Library/Frameworks/AppKit"
92    /// );
93    /// ```
94    pub fn sanitize_path(path: &str) -> String {
95        if let Some(rest) = path.strip_prefix("/Users/")
96            && let Some(slash_pos) = rest.find('/')
97        {
98            return format!("/Users/USER/{}", &rest[slash_pos + 1..]);
99        }
100        path.to_string()
101    }
102
103    /// Sanitizes all file paths in the crash report for privacy.
104    /// Preserves original executable path as reopen_path before sanitizing.
105    /// Sanitizes: executable_path, all binary image paths, VM map paths.
106    /// Equivalent to -[CrashReport cleansePaths]
107    pub fn cleanse_paths(&mut self) {
108        // Save original path for relaunch
109        if self.executable_path.is_some() && self.reopen_path.is_none() {
110            self.reopen_path = self.executable_path.clone();
111        }
112
113        // Sanitize executable path
114        if let Some(ref path) = self.executable_path {
115            let sanitized = Self::sanitize_path(path);
116            if sanitized != *path {
117                self.executable_path = Some(sanitized);
118            }
119        }
120
121        // Sanitize binary image paths
122        for image in &mut self.binary_images {
123            image.path = Self::sanitize_path(&image.path);
124        }
125
126        // Sanitize VM map paths
127        if let Some(ref vm_map) = self.vm_map_string {
128            let sanitized_lines: Vec<String> = vm_map
129                .lines()
130                .map(|line| {
131                    if let Some(slash_pos) = line.find('/') {
132                        let path = &line[slash_pos..];
133                        let sanitized = Self::sanitize_path(path);
134                        format!("{}{}", &line[..slash_pos], sanitized)
135                    } else {
136                        line.to_string()
137                    }
138                })
139                .collect();
140            self.vm_map_string = Some(sanitized_lines.join("\n"));
141        }
142    }
143
144    /// Sets work queue limits from pre-queried sysctl values.
145    /// Bit 0 of `flags`: constrained thread limit hit.
146    /// Bit 1 of `flags`: total thread limit hit.
147    /// The caller (exc_handler) is responsible for querying
148    /// `kern.wq_max_constrained_threads` and `kern.wq_max_threads`.
149    /// Equivalent to -[CrashReport _extractWorkQueueLimitsFromData:]
150    pub fn extract_work_queue_limits_from_flags(
151        &mut self,
152        flags: u32,
153        wq_max_constrained_threads: Option<u32>,
154        wq_max_threads: Option<u32>,
155    ) {
156        if flags & 3 == 0 {
157            return;
158        }
159        let mut limits = WorkQueueLimits {
160            min_threads: None,
161            max_threads: None,
162        };
163        if flags & 1 != 0 {
164            limits.min_threads = wq_max_constrained_threads;
165        }
166        if flags & 2 != 0 {
167            limits.max_threads = wq_max_threads;
168        }
169        self.work_queue_limits = Some(limits);
170    }
171
172    /// Reads the App Store receipt from the application bundle path.
173    /// Sets has_receipt and adam_id if a valid receipt is found.
174    /// Equivalent to -[CrashReport _readAppStoreReceipt]
175    pub fn set_app_store_receipt(&mut self, adam_id: Option<String>, version_id: Option<String>) {
176        if adam_id.is_some() {
177            self.has_receipt = true;
178            self.adam_id = adam_id;
179            self.software_version_external_identifier = version_id;
180        }
181    }
182
183    // =========================================================================
184    // Dictionary/plist and classification methods
185    // =========================================================================
186
187    /// Builds the main problem dictionary for crash reporting.
188    /// Populates ~40+ keys including app info, exception details, thread state,
189    /// binary images, VM map, notes, timestamps, and more.
190    /// Equivalent to -[CrashReport problemDictionary]
191    pub fn problem_dictionary(&self) -> BTreeMap<String, PlistValue> {
192        let mut d = BTreeMap::new();
193
194        if let Some(name) = self.process_name()
195            && !name.is_empty()
196        {
197            d.insert("app_name".into(), PlistValue::String(name.into()));
198        }
199        if self.pid != 0 {
200            d.insert("app_pid".into(), PlistValue::Int(self.pid as i64));
201        }
202        if let Some(path) = self.executable_path()
203            && !path.is_empty()
204        {
205            d.insert("app_path".into(), PlistValue::String(path.into()));
206        }
207        if let Some(id) = self.process_identifier()
208            && !id.is_empty()
209        {
210            d.insert("app_bundle_id".into(), PlistValue::String(id.into()));
211        }
212        let ver = self.app_version();
213        if !ver.is_empty() {
214            d.insert("app_version".into(), PlistValue::String(ver));
215        }
216        let build_ver = self.app_build_version();
217        if !build_ver.is_empty() {
218            d.insert("app_build_version".into(), PlistValue::String(build_ver));
219        }
220
221        // Build version dictionary fields
222        if let Some(v) = self.build_version_dictionary.get("ProjectName")
223            && !v.is_empty()
224        {
225            d.insert("project_name".into(), PlistValue::String(v.clone()));
226        }
227        if let Some(v) = self.build_version_dictionary.get("SourceVersion")
228            && !v.is_empty()
229        {
230            d.insert(
231                "project_source_version".into(),
232                PlistValue::String(v.clone()),
233            );
234        }
235        if let Some(v) = self.build_version_dictionary.get("BuildVersion")
236            && !v.is_empty()
237        {
238            d.insert(
239                "project_build_version".into(),
240                PlistValue::String(v.clone()),
241            );
242        }
243
244        d.insert("arch".into(), PlistValue::String(self.short_arch_name()));
245        d.insert("arch_translated".into(), PlistValue::Bool(!self.is_native));
246        d.insert("arch_64".into(), PlistValue::Bool(self.is_64_bit));
247
248        if let Some(name) = self.parent_process_name()
249            && !name.is_empty()
250        {
251            d.insert("parent_name".into(), PlistValue::String(name.into()));
252        }
253        if self.ppid != 0 {
254            d.insert("parent_pid".into(), PlistValue::Int(self.ppid as i64));
255        }
256        if self.r_process_pid != -1 {
257            if let Some(name) = self.responsible_process_name()
258                && !name.is_empty()
259            {
260                d.insert(
261                    "responsible_process_name".into(),
262                    PlistValue::String(name.into()),
263                );
264            }
265            d.insert(
266                "responsible_process_pid".into(),
267                PlistValue::Int(self.r_process_pid as i64),
268            );
269        }
270
271        if let Some(ref date) = self.date {
272            d.insert("date".into(), PlistValue::String(date.clone()));
273        }
274
275        // OS version fields
276        if let Some(v) = self.os_version_dictionary.get("ProductVersion")
277            && !v.is_empty()
278        {
279            d.insert("os_version".into(), PlistValue::String(v.clone()));
280        }
281        if let Some(v) = self.os_version_dictionary.get("BuildVersion")
282            && !v.is_empty()
283        {
284            d.insert("os_build".into(), PlistValue::String(v.clone()));
285        }
286        if let Some(v) = self.os_version_dictionary.get("ProductName")
287            && !v.is_empty()
288        {
289            d.insert("os_product".into(), PlistValue::String(v.clone()));
290        }
291
292        d.insert("report_version".into(), PlistValue::String("12".into()));
293
294        if self.awake_system_uptime != 0 {
295            d.insert(
296                "awake_system_uptime".into(),
297                PlistValue::Int(Self::reduce_to_two_sig_figures(self.awake_system_uptime) as i64),
298            );
299        }
300
301        let sig_name = self.signal_name();
302        if !sig_name.is_empty() {
303            d.insert("signal_name".into(), PlistValue::String(sig_name));
304        }
305        if self.crashed_thread_number >= 0 {
306            d.insert(
307                "crashing_thread_index".into(),
308                PlistValue::Int(self.crashed_thread_number as i64),
309            );
310        }
311
312        let exc_type = self.exception_type_description();
313        if !exc_type.is_empty() {
314            d.insert("exception_type".into(), PlistValue::String(exc_type));
315        }
316        let exc_codes = self.exception_codes_description();
317        if !exc_codes.is_empty() {
318            d.insert("exception_codes".into(), PlistValue::String(exc_codes));
319        }
320
321        if self.performing_autopsy {
322            d.insert(
323                "exception_notes".into(),
324                PlistValue::Array(vec![{
325                    let mut m = BTreeMap::new();
326                    m.insert(
327                        "note".into(),
328                        PlistValue::String("EXC_CORPSE_NOTIFY".into()),
329                    );
330                    m
331                }]),
332            );
333        }
334
335        if self.is_code_sign_killed() {
336            d.insert("cs_killed".into(), PlistValue::Bool(true));
337            if let Some(ref msgs) = self.code_sign_invalid_messages_description {
338                d.insert("kernel_messages".into(), PlistValue::String(msgs.clone()));
339            }
340        }
341
342        d.insert(
343            "system_integrity_protection".into(),
344            PlistValue::Bool(self.is_rootless_enabled()),
345        );
346
347        if let Some(ref info) = self.application_specific_info
348            && !info.is_empty()
349        {
350            d.insert(
351                "crash_info_message".into(),
352                PlistValue::String(info.clone()),
353            );
354        }
355        if let Some(ref sel) = self.objc_selector_name
356            && !sel.is_empty()
357        {
358            d.insert("objc_selector".into(), PlistValue::String(sel.clone()));
359        }
360        if !self.application_specific_signature_strings.is_empty() {
361            let arr = self
362                .application_specific_signature_strings
363                .iter()
364                .map(|s| {
365                    let mut m = BTreeMap::new();
366                    m.insert("signature".into(), PlistValue::String(s.clone()));
367                    m
368                })
369                .collect();
370            d.insert("crash_info_signatures".into(), PlistValue::Array(arr));
371        }
372        if !self.application_specific_backtraces.is_empty() {
373            let arr = self
374                .application_specific_backtraces
375                .iter()
376                .map(|s| {
377                    let mut m = BTreeMap::new();
378                    m.insert("backtrace".into(), PlistValue::String(s.clone()));
379                    m
380                })
381                .collect();
382            d.insert("crash_info_thread_strings".into(), PlistValue::Array(arr));
383        }
384
385        if let Some(ref err) = self.internal_error
386            && !err.is_empty()
387        {
388            d.insert("internal_error".into(), PlistValue::String(err.clone()));
389        }
390        if let Some(ref err) = self.dyld_error_string
391            && !err.is_empty()
392        {
393            d.insert("dyld_error".into(), PlistValue::String(err.clone()));
394        }
395        if let Some(ref info) = self.dyld_error_info {
396            d.insert("dyld_error_info".into(), PlistValue::String(info.clone()));
397        }
398
399        // Thread state
400        let ts = self.thread_state_description();
401        d.insert("crashing_thread_state".into(), PlistValue::String(ts));
402
403        // VM map
404        if let Some(ref vm) = self.vm_map_string
405            && !vm.is_empty()
406        {
407            d.insert("vm_map".into(), PlistValue::String(vm.clone()));
408        }
409        if let Some(ref vs) = self.vm_summary_string
410            && !vs.is_empty()
411        {
412            d.insert("vm_summary".into(), PlistValue::String(vs.clone()));
413        }
414
415        if let Some(ref uuid) = self.sleep_wake_uuid
416            && !uuid.is_empty()
417        {
418            d.insert(
419                "sleep_wake_uuid_string".into(),
420                PlistValue::String(uuid.clone()),
421            );
422        }
423        if let Some(ref uuid) = self.anon_uuid {
424            d.insert("anon_uuid".into(), PlistValue::String(uuid.clone()));
425        }
426        if !self.ext_mod_info.dictionary.is_empty() {
427            let mut ext = BTreeMap::new();
428            for (k, v) in &self.ext_mod_info.dictionary {
429                ext.insert(k.clone(), PlistValue::String(v.clone()));
430            }
431            d.insert(
432                "external_modification_summary".into(),
433                PlistValue::Dict(ext),
434            );
435        }
436        if let Some(ref rosetta) = self.rosetta_info
437            && !rosetta.is_empty()
438        {
439            d.insert(
440                "rosetta_threads_string".into(),
441                PlistValue::String(rosetta.clone()),
442            );
443        }
444
445        d
446    }
447
448    /// Builds a filtered dictionary for crash signature generation.
449    /// Contains a subset of problem dictionary keys needed for signature matching.
450    /// Filters thread plists and binary images to only referenced images.
451    /// Equivalent to -[CrashReport preSignatureDictionary]
452    pub fn pre_signature_dictionary(&self) -> BTreeMap<String, PlistValue> {
453        let mut d = BTreeMap::new();
454
455        if let Some(name) = self.process_name()
456            && !name.is_empty()
457        {
458            d.insert("app_name".into(), PlistValue::String(name.into()));
459        }
460        if let Some(id) = self.process_identifier()
461            && !id.is_empty()
462        {
463            d.insert("app_bundle_id".into(), PlistValue::String(id.into()));
464        }
465        let build_ver = self.app_build_version();
466        if !build_ver.is_empty() {
467            d.insert("app_build_version".into(), PlistValue::String(build_ver));
468        }
469        let ver = self.app_version();
470        if !ver.is_empty() {
471            d.insert("app_version".into(), PlistValue::String(ver));
472        }
473        d.insert("arch".into(), PlistValue::String(self.short_arch_name()));
474
475        if let Some(v) = self.os_version_dictionary.get("BuildVersion")
476            && !v.is_empty()
477        {
478            d.insert("os_build".into(), PlistValue::String(v.clone()));
479        }
480
481        d.insert("report_version".into(), PlistValue::String("12".into()));
482
483        let sig_name = self.signal_name();
484        if !sig_name.is_empty() {
485            d.insert("signal_name".into(), PlistValue::String(sig_name));
486        }
487        let exc_type = self.exception_type_description();
488        if !exc_type.is_empty() {
489            d.insert("exception_type".into(), PlistValue::String(exc_type));
490        }
491        if let Some(ref sel) = self.objc_selector_name
492            && !sel.is_empty()
493        {
494            d.insert("objc_selector".into(), PlistValue::String(sel.clone()));
495        }
496        if !self.application_specific_signature_strings.is_empty() {
497            let arr = self
498                .application_specific_signature_strings
499                .iter()
500                .map(|s| {
501                    let mut m = BTreeMap::new();
502                    m.insert("signature".into(), PlistValue::String(s.clone()));
503                    m
504                })
505                .collect();
506            d.insert("crash_info_signatures".into(), PlistValue::Array(arr));
507        }
508        if let Some(ref err) = self.internal_error
509            && !err.is_empty()
510        {
511            d.insert("internal_error".into(), PlistValue::String(err.clone()));
512        }
513        if let Some(ref info) = self.dyld_error_info {
514            d.insert("dyld_error_info".into(), PlistValue::String(info.clone()));
515        }
516        if let Some(ref adam) = self.adam_id
517            && !adam.is_empty()
518        {
519            d.insert("mas_adam_id".into(), PlistValue::String(adam.clone()));
520        }
521        if let Some(ref ext_id) = self.software_version_external_identifier
522            && !ext_id.is_empty()
523        {
524            d.insert("mas_external_id".into(), PlistValue::String(ext_id.clone()));
525        }
526        if self.work_queue_limits.is_some() {
527            let mut wq = BTreeMap::new();
528            if let Some(ref limits) = self.work_queue_limits {
529                if let Some(min) = limits.min_threads {
530                    wq.insert("min_threads".into(), PlistValue::Int(min as i64));
531                }
532                if let Some(max) = limits.max_threads {
533                    wq.insert("max_threads".into(), PlistValue::Int(max as i64));
534                }
535            }
536            d.insert("wq_limits_reached".into(), PlistValue::Dict(wq));
537        }
538
539        // Filter crashed thread backtrace for presignature
540        if !self.fatal_dyld_error_on_launch
541            && self.crashed_thread_number >= 0
542            && (self.crashed_thread_number as usize) < self.backtraces.len()
543        {
544            let bt = &self.backtraces[self.crashed_thread_number as usize];
545            let filtered = self.filter_thread_for_presignature(bt);
546            d.insert("crashed_thread".into(), PlistValue::Dict(filtered));
547        }
548
549        d
550    }
551
552    /// Builds a minimal context dictionary with date and sleep/wake UUID.
553    /// Equivalent to -[CrashReport contextDictionary]
554    pub fn context_dictionary(&self) -> BTreeMap<String, PlistValue> {
555        let mut d = BTreeMap::new();
556        if let Some(ref date) = self.date {
557            d.insert("date".into(), PlistValue::String(date.clone()));
558        }
559        if let Some(ref uuid) = self.sleep_wake_uuid
560            && !uuid.is_empty()
561        {
562            d.insert(
563                "sleep_wake_uuid_string".into(),
564                PlistValue::String(uuid.clone()),
565            );
566        }
567        d
568    }
569
570    /// Combines problemDictionary, preSignatureDictionary, and contextDictionary
571    /// into a single dictionary with keys "report", "presignature", "context".
572    /// Equivalent to -[CrashReport descriptionDictionary]
573    pub fn description_dictionary(&self) -> BTreeMap<String, PlistValue> {
574        let mut d = BTreeMap::new();
575        d.insert("report".into(), PlistValue::Dict(self.problem_dictionary()));
576        d.insert(
577            "presignature".into(),
578            PlistValue::Dict(self.pre_signature_dictionary()),
579        );
580        d.insert(
581            "context".into(),
582            PlistValue::Dict(self.context_dictionary()),
583        );
584        d
585    }
586
587    /// Filters a thread backtrace for presignature use.
588    /// Extracts only symbol, symbol_offset, binary_image_offset,
589    /// binary_image_identifier, and binary_image_index per frame.
590    /// Equivalent to -[CrashReport filterThreadPlistForPresignature:withBinaryImagesSet:]
591    fn filter_thread_for_presignature(&self, bt: &ThreadBacktrace) -> BTreeMap<String, PlistValue> {
592        let mut result = BTreeMap::new();
593        let mut frames = Vec::new();
594
595        for frame in &bt.frames {
596            let mut fd = BTreeMap::new();
597            if let Some(ref sym) = frame.symbol_name {
598                fd.insert("symbol".into(), PlistValue::String(sym.clone()));
599            }
600            if frame.symbol_offset != 0 {
601                fd.insert(
602                    "symbol_offset".into(),
603                    PlistValue::Int(frame.symbol_offset as i64),
604                );
605            }
606            if let Some(img) = self.binary_image_for_address(frame.address) {
607                let offset = frame.address - img.base_address;
608                fd.insert("binary_image_offset".into(), PlistValue::Int(offset as i64));
609                if let Some(ref id) = img.identifier {
610                    fd.insert(
611                        "binary_image_identifier".into(),
612                        PlistValue::String(id.clone()),
613                    );
614                }
615            }
616            frames.push(fd);
617        }
618
619        result.insert("backtrace".into(), PlistValue::Array(frames));
620        if let Some(ref name) = bt.thread_name {
621            result.insert("thread_name".into(), PlistValue::String(name.clone()));
622        }
623        result
624    }
625
626    /// Filters a binary image for presignature. If UUID exists, returns
627    /// just index+uuid. Otherwise returns index + bundle metadata + path.
628    /// Equivalent to -[CrashReport filteredBinaryImagePlistForPresignature:]
629    pub fn filtered_binary_image_for_presignature(
630        &self,
631        image: &BinaryImage,
632        index: usize,
633    ) -> BTreeMap<String, PlistValue> {
634        let mut d = BTreeMap::new();
635        d.insert("index".into(), PlistValue::Int(index as i64));
636
637        if let Some(ref uuid) = image.uuid {
638            d.insert("uuid".into(), PlistValue::String(uuid.clone()));
639        } else {
640            if let Some(ref ver) = image.version {
641                d.insert("bundle_version".into(), PlistValue::String(ver.clone()));
642            }
643            if let Some(ref id) = image.identifier {
644                d.insert("bundle_id".into(), PlistValue::String(id.clone()));
645            }
646            d.insert("path".into(), PlistValue::String(image.path.clone()));
647        }
648        d
649    }
650
651    /// Converts binary images to plist format array.
652    /// Equivalent to -[CrashReport _binaryImagesPlist]
653    pub fn binary_images_plist(&self) -> Vec<BTreeMap<String, PlistValue>> {
654        self.binary_images
655            .iter()
656            .enumerate()
657            .map(|(i, img)| {
658                let mut d = BTreeMap::new();
659                d.insert("index".into(), PlistValue::Int(i as i64));
660                d.insert(
661                    "StartAddress".into(),
662                    PlistValue::Int(img.base_address as i64),
663                );
664                d.insert(
665                    "Size".into(),
666                    PlistValue::Int((img.end_address - img.base_address) as i64),
667                );
668                d.insert("path".into(), PlistValue::String(img.path.clone()));
669                d.insert("name".into(), PlistValue::String(img.name.clone()));
670                if let Some(ref uuid) = img.uuid {
671                    d.insert("uuid".into(), PlistValue::String(uuid.clone()));
672                }
673                if let Some(ref id) = img.identifier {
674                    d.insert("bundle_id".into(), PlistValue::String(id.clone()));
675                }
676                if let Some(ref ver) = img.version {
677                    d.insert("bundle_version".into(), PlistValue::String(ver.clone()));
678                }
679                if let Some(ref arch) = img.arch {
680                    d.insert("arch".into(), PlistValue::String(arch.clone()));
681                }
682                d
683            })
684            .collect()
685    }
686
687    /// Parses rosettaInfo string into plist-compatible thread array.
688    /// Splits by newlines, detects Thread headers with Crashed markers,
689    /// parses hex address + image path + symbol/offset from each frame.
690    /// Equivalent to -[CrashReport _rosettaThreadsPlist]
691    pub fn rosetta_threads_plist(&self) -> Vec<BTreeMap<String, PlistValue>> {
692        let rosetta = match &self.rosetta_info {
693            Some(r) if !r.is_empty() => r,
694            _ => return Vec::new(),
695        };
696
697        let mut threads: Vec<BTreeMap<String, PlistValue>> = Vec::new();
698        let mut current_frames: Vec<BTreeMap<String, PlistValue>> = Vec::new();
699        let mut current_thread = BTreeMap::new();
700        current_thread.insert("backtrace".into(), PlistValue::Array(Vec::new()));
701
702        for line in rosetta.lines() {
703            if line.starts_with("Thread") {
704                // Flush previous thread if it had frames
705                if (!current_frames.is_empty() || threads.is_empty()) && !current_frames.is_empty()
706                {
707                    current_thread.insert("backtrace".into(), PlistValue::Array(current_frames));
708                    threads.push(current_thread);
709                }
710                current_frames = Vec::new();
711                current_thread = BTreeMap::new();
712                if line.contains("Crashed") {
713                    current_thread.insert("crashed".into(), PlistValue::Bool(true));
714                }
715            } else {
716                // Parse frame: "0xADDR imagepath symbol + offset"
717                let trimmed = line.trim();
718                if trimmed.is_empty() {
719                    continue;
720                }
721                // Try to parse hex address at start
722                if let Some(hex_str) = trimmed.strip_prefix("0x") {
723                    let addr_end = hex_str
724                        .find(|c: char| !c.is_ascii_hexdigit())
725                        .unwrap_or(hex_str.len());
726                    if let Ok(addr) = u64::from_str_radix(&hex_str[..addr_end], 16) {
727                        let mut frame = BTreeMap::new();
728                        frame.insert("address".into(), PlistValue::Int(addr as i64));
729
730                        // Parse rest: skip whitespace, get image path, symbol + offset
731                        let rest = hex_str[addr_end..].trim_start();
732                        if let Some(space_pos) = rest.find(' ') {
733                            let image_path = &rest[..space_pos];
734                            let after_image = rest[space_pos..].trim_start();
735
736                            if let Some(last_component) = image_path.rsplit('/').next() {
737                                frame.insert(
738                                    "binary_image_identifier".into(),
739                                    PlistValue::String(last_component.into()),
740                                );
741                            }
742
743                            // Check for "symbol + offset" pattern
744                            if let Some(plus_pos) = after_image.rfind(" + ") {
745                                let symbol = after_image[..plus_pos].trim();
746                                let offset_str = after_image[plus_pos + 3..].trim();
747                                if !symbol.is_empty() {
748                                    frame
749                                        .insert("symbol".into(), PlistValue::String(symbol.into()));
750                                    if let Ok(off) = offset_str.parse::<u64>() {
751                                        frame.insert(
752                                            "symbol_offset".into(),
753                                            PlistValue::Int(off as i64),
754                                        );
755                                    }
756                                } else if let Ok(off) = offset_str.parse::<u64>() {
757                                    frame.insert(
758                                        "binary_image_offset".into(),
759                                        PlistValue::Int(off as i64),
760                                    );
761                                }
762                            }
763                        }
764                        current_frames.push(frame);
765                    }
766                }
767            }
768        }
769
770        // Flush last thread
771        if !current_frames.is_empty() {
772            current_thread.insert("backtrace".into(), PlistValue::Array(current_frames));
773            threads.push(current_thread);
774        }
775
776        if threads.is_empty() {
777            return Vec::new();
778        }
779        threads
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use crate::test_helpers::*;
786    use crate::types::*;
787
788    // =========================================================================
789    // 11. crash_analysis — Extraction methods
790    // =========================================================================
791    mod crash_analysis {
792        use super::*;
793
794        #[test]
795        fn crashed_due_to_bad_memory_access_not_type1() {
796            let mut cr = make_test_cr();
797            cr.exception_type = 3; // EXC_ARITHMETIC
798            assert!(!cr.crashed_due_to_bad_memory_access());
799        }
800
801        #[test]
802        fn crashed_due_to_bad_memory_access_type1() {
803            let mut cr = make_test_cr();
804            cr.exception_type = 1;
805            cr.exception_code = vec![2, 0x42];
806            assert!(cr.crashed_due_to_bad_memory_access());
807        }
808
809        #[test]
810        fn crashed_due_to_bad_memory_access_gpf_null_false() {
811            let mut cr = make_test_cr();
812            cr.exception_type = 1;
813            cr.exception_code = vec![0xd, 0]; // GPF with null address
814            assert!(!cr.crashed_due_to_bad_memory_access());
815        }
816
817        #[test]
818        fn extract_crashing_address_bad_memory_access() {
819            let mut cr = make_test_cr();
820            cr.exception_type = 1;
821            cr.exception_code = vec![2, 0xDEAD];
822            cr.extract_crashing_address();
823            assert_eq!(cr.crashing_address, 0xDEAD);
824        }
825
826        #[test]
827        fn extract_crashing_address_code_sign_killed_32bit() {
828            let mut cr = make_test_cr();
829            cr.exception_type = 5; // not bad access
830            cr.cs_status = 0x100_0000;
831            cr.exception_state = ExceptionState {
832                state: vec![1, 0, 0, 0, 0xCAFE, 0], // sub_flavor=1, cr2 at index 4
833                count: 6,
834            };
835            cr.extract_crashing_address();
836            assert_eq!(cr.crashing_address, 0xCAFE);
837        }
838
839        #[test]
840        fn extract_crashing_address_code_sign_killed_64bit() {
841            let mut cr = make_test_cr();
842            cr.exception_type = 5;
843            cr.cs_status = 0x100_0000;
844            cr.exception_state = ExceptionState {
845                state: vec![4, 0, 0, 0, 0xBEEF, 0x0001], // sub_flavor=4, cr2 at 4-5
846                count: 6,
847            };
848            cr.extract_crashing_address();
849            assert_eq!(cr.crashing_address, (0x0001u64 << 32) | 0xBEEF);
850        }
851
852        #[test]
853        fn crashed_due_to_bad_memory_access_gpf_null_arm64_is_true() {
854            // On ARM64, code 0xd with null address is NOT a GPF — it's a real bad access
855            let mut cr = make_test_cr_arm64();
856            cr.exception_type = 1;
857            cr.exception_code = vec![0xd, 0];
858            assert!(cr.crashed_due_to_bad_memory_access());
859        }
860
861        #[test]
862        fn extract_crashing_address_code_sign_killed_arm64() {
863            let mut cr = make_test_cr_arm64();
864            cr.exception_type = 5;
865            cr.cs_status = 0x100_0000;
866            // ARM64 exception state: FAR at state[0..1]
867            cr.exception_state = ExceptionState {
868                state: vec![0xDEAD_0000, 0x0000_FFFF, 0, 0],
869                count: 4,
870            };
871            cr.extract_crashing_address();
872            assert_eq!(cr.crashing_address, 0x0000_FFFF_DEAD_0000);
873        }
874
875        #[test]
876        fn cleanse_paths_sanitizes_executable_path() {
877            let mut cr = make_test_cr();
878            cr.executable_path = Some("/Users/kurtis/app/bin".into());
879            cr.cleanse_paths();
880            assert_eq!(cr.executable_path.as_deref(), Some("/Users/USER/app/bin"));
881        }
882
883        #[test]
884        fn cleanse_paths_preserves_reopen_path() {
885            let mut cr = make_test_cr();
886            cr.executable_path = Some("/Users/kurtis/app/bin".into());
887            cr.reopen_path = None;
888            cr.cleanse_paths();
889            // reopen_path should be set to original before sanitization
890            assert_eq!(cr.reopen_path.as_deref(), Some("/Users/kurtis/app/bin"));
891        }
892
893        #[test]
894        fn cleanse_paths_sanitizes_binary_image_paths() {
895            let mut cr = make_test_cr();
896            cr.binary_images.push(BinaryImage {
897                name: "libfoo.dylib".into(),
898                path: "/Users/kurtis/lib/libfoo.dylib".into(),
899                uuid: None,
900                base_address: 0x1000,
901                end_address: 0x2000,
902                arch: None,
903                identifier: None,
904                version: None,
905            });
906            cr.cleanse_paths();
907            assert_eq!(cr.binary_images[0].path, "/Users/USER/lib/libfoo.dylib");
908        }
909
910        #[test]
911        fn cleanse_paths_sanitizes_vm_map_lines() {
912            let mut cr = make_test_cr();
913            cr.vm_map_string =
914                Some("region 0x1000 /Users/kurtis/lib/libfoo.dylib\nother line".into());
915            cr.cleanse_paths();
916            let vm = cr.vm_map_string.as_ref().unwrap();
917            assert!(vm.contains("/Users/USER/lib/libfoo.dylib"));
918            assert_eq!(vm.lines().nth(1).unwrap(), "other line");
919        }
920
921        #[test]
922        fn set_app_store_receipt_with_adam_id() {
923            let mut cr = make_test_cr();
924            cr.set_app_store_receipt(Some("12345".into()), Some("67890".into()));
925            assert!(cr.has_receipt);
926            assert_eq!(cr.adam_id, Some("12345".into()));
927            assert_eq!(
928                cr.software_version_external_identifier,
929                Some("67890".into())
930            );
931        }
932
933        #[test]
934        fn set_app_store_receipt_without_adam_id() {
935            let mut cr = make_test_cr();
936            cr.set_app_store_receipt(None, Some("67890".into()));
937            assert!(!cr.has_receipt);
938            assert!(cr.adam_id.is_none());
939        }
940    }
941
942    // =========================================================================
943    // 12. dictionary_methods — Plist output
944    // =========================================================================
945    mod dictionary_methods {
946        use super::*;
947
948        #[test]
949        fn problem_dictionary_has_expected_keys() {
950            let cr = make_test_cr();
951            let d = cr.problem_dictionary();
952            assert!(d.contains_key("app_name"));
953            assert!(d.contains_key("app_pid"));
954            assert!(d.contains_key("app_path"));
955            assert!(d.contains_key("arch"));
956            assert!(d.contains_key("arch_translated"));
957            assert!(d.contains_key("arch_64"));
958            assert!(d.contains_key("report_version"));
959            assert!(d.contains_key("system_integrity_protection"));
960            assert!(d.contains_key("crashing_thread_state"));
961        }
962
963        #[test]
964        fn problem_dictionary_values() {
965            let cr = make_test_cr();
966            let d = cr.problem_dictionary();
967            match d.get("app_name") {
968                Some(PlistValue::String(s)) => assert_eq!(s, "TestApp"),
969                _ => panic!("unexpected app_name type"),
970            }
971            match d.get("app_pid") {
972                Some(PlistValue::Int(n)) => assert_eq!(*n, 1234),
973                _ => panic!("unexpected app_pid type"),
974            }
975        }
976
977        #[test]
978        fn pre_signature_dictionary_has_expected_keys() {
979            let cr = make_test_cr();
980            let d = cr.pre_signature_dictionary();
981            assert!(d.contains_key("app_name"));
982            assert!(d.contains_key("arch"));
983            assert!(d.contains_key("report_version"));
984            assert!(d.contains_key("exception_type"));
985            assert!(d.contains_key("signal_name"));
986        }
987
988        #[test]
989        fn pre_signature_dictionary_filtered() {
990            let cr = make_test_cr();
991            let d = cr.pre_signature_dictionary();
992            // Keys that should NOT be in presignature
993            assert!(!d.contains_key("app_pid"));
994            assert!(!d.contains_key("crashing_thread_state"));
995            assert!(!d.contains_key("vm_map"));
996        }
997
998        #[test]
999        fn context_dictionary_has_date_and_uuid() {
1000            let mut cr = make_test_cr();
1001            cr.date = Some("2024-01-01".into());
1002            cr.sleep_wake_uuid = Some("UUID-123".into());
1003            let d = cr.context_dictionary();
1004            assert!(d.contains_key("date"));
1005            assert!(d.contains_key("sleep_wake_uuid_string"));
1006            assert_eq!(d.len(), 2);
1007        }
1008
1009        #[test]
1010        fn context_dictionary_date_only() {
1011            let mut cr = make_test_cr();
1012            cr.date = Some("2024-01-01".into());
1013            let d = cr.context_dictionary();
1014            assert!(d.contains_key("date"));
1015            assert!(!d.contains_key("sleep_wake_uuid_string"));
1016        }
1017
1018        #[test]
1019        fn description_dictionary_has_three_keys() {
1020            let cr = make_test_cr();
1021            let d = cr.description_dictionary();
1022            assert!(d.contains_key("report"));
1023            assert!(d.contains_key("presignature"));
1024            assert!(d.contains_key("context"));
1025        }
1026
1027        #[test]
1028        fn binary_images_plist_empty() {
1029            let cr = make_test_cr();
1030            assert!(cr.binary_images_plist().is_empty());
1031        }
1032
1033        #[test]
1034        fn binary_images_plist_populated() {
1035            let mut cr = make_test_cr();
1036            cr.binary_images.push(BinaryImage {
1037                name: "libfoo.dylib".into(),
1038                path: "/usr/lib/libfoo.dylib".into(),
1039                uuid: Some("UUID".into()),
1040                base_address: 0x1000,
1041                end_address: 0x2000,
1042                arch: Some("x86_64".into()),
1043                identifier: Some("libfoo.dylib".into()),
1044                version: Some("1.0".into()),
1045            });
1046            let plist = cr.binary_images_plist();
1047            assert_eq!(plist.len(), 1);
1048            assert!(plist[0].contains_key("StartAddress"));
1049            assert!(plist[0].contains_key("Size"));
1050            assert!(plist[0].contains_key("uuid"));
1051            assert!(plist[0].contains_key("bundle_id"));
1052        }
1053
1054        #[test]
1055        fn filtered_binary_image_for_presignature_with_uuid() {
1056            let cr = make_test_cr();
1057            let img = BinaryImage {
1058                name: "libfoo.dylib".into(),
1059                path: "/usr/lib/libfoo.dylib".into(),
1060                uuid: Some("UUID-123".into()),
1061                base_address: 0x1000,
1062                end_address: 0x2000,
1063                arch: None,
1064                identifier: Some("libfoo.dylib".into()),
1065                version: Some("1.0".into()),
1066            };
1067            let d = cr.filtered_binary_image_for_presignature(&img, 0);
1068            assert!(d.contains_key("uuid"));
1069            assert!(!d.contains_key("path"));
1070        }
1071
1072        #[test]
1073        fn filtered_binary_image_for_presignature_without_uuid() {
1074            let cr = make_test_cr();
1075            let img = BinaryImage {
1076                name: "libfoo.dylib".into(),
1077                path: "/usr/lib/libfoo.dylib".into(),
1078                uuid: None,
1079                base_address: 0x1000,
1080                end_address: 0x2000,
1081                arch: None,
1082                identifier: Some("libfoo.dylib".into()),
1083                version: Some("1.0".into()),
1084            };
1085            let d = cr.filtered_binary_image_for_presignature(&img, 0);
1086            assert!(!d.contains_key("uuid"));
1087            assert!(d.contains_key("path"));
1088            assert!(d.contains_key("bundle_id"));
1089            assert!(d.contains_key("bundle_version"));
1090        }
1091
1092        #[test]
1093        fn rosetta_threads_plist_empty() {
1094            let cr = make_test_cr();
1095            assert!(cr.rosetta_threads_plist().is_empty());
1096        }
1097
1098        #[test]
1099        fn rosetta_threads_plist_single_thread() {
1100            let mut cr = make_test_cr();
1101            cr.rosetta_info = Some("Thread 0:\n0x1000 /usr/lib/libfoo.dylib main + 42\n".into());
1102            let threads = cr.rosetta_threads_plist();
1103            assert_eq!(threads.len(), 1);
1104        }
1105
1106        #[test]
1107        fn rosetta_threads_plist_crashed_marker() {
1108            let mut cr = make_test_cr();
1109            cr.rosetta_info =
1110                Some("Thread 0 Crashed:\n0x1000 /usr/lib/libfoo.dylib main + 42\n".into());
1111            let threads = cr.rosetta_threads_plist();
1112            assert_eq!(threads.len(), 1);
1113            assert!(threads[0].contains_key("crashed"));
1114        }
1115
1116        #[test]
1117        fn rosetta_threads_plist_frame_with_symbol_offset() {
1118            let mut cr = make_test_cr();
1119            cr.rosetta_info = Some("Thread 0:\n0x1000 /usr/lib/libfoo.dylib main + 42\n".into());
1120            let threads = cr.rosetta_threads_plist();
1121            let bt = threads[0].get("backtrace").unwrap();
1122            if let PlistValue::Array(frames) = bt {
1123                assert_eq!(frames.len(), 1);
1124                assert!(frames[0].contains_key("symbol"));
1125                match frames[0].get("symbol") {
1126                    Some(PlistValue::String(s)) => assert_eq!(s, "main"),
1127                    _ => panic!("expected symbol string"),
1128                }
1129                match frames[0].get("symbol_offset") {
1130                    Some(PlistValue::Int(n)) => assert_eq!(*n, 42),
1131                    _ => panic!("expected symbol_offset int"),
1132                }
1133            } else {
1134                panic!("expected backtrace array");
1135            }
1136        }
1137    }
1138}