Skip to main content

crashrustler/
accessors.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::crash_rustler::CrashRustler;
4use crate::types::*;
5
6impl CrashRustler {
7    /// Returns the crash date.
8    /// Equivalent to -[CrashReport date]
9    pub fn date(&self) -> Option<&str> {
10        self.date.as_deref()
11    }
12
13    /// Returns the Mach task port.
14    /// Equivalent to -[CrashReport task]
15    pub fn task(&self) -> u32 {
16        self.task
17    }
18
19    /// Returns the process ID of the crashed process.
20    /// Equivalent to -[CrashReport pid]
21    pub fn pid(&self) -> i32 {
22        self.pid
23    }
24
25    /// Returns the CPU type of the crashed process.
26    /// Equivalent to -[CrashReport cpuType]
27    pub fn cpu_type(&self) -> CpuType {
28        self.cpu_type
29    }
30
31    /// Returns the process name.
32    /// Equivalent to -[CrashReport processName]
33    pub fn process_name(&self) -> Option<&str> {
34        self.process_name.as_deref()
35    }
36
37    /// Returns the bundle identifier if available, otherwise the process name.
38    /// Equivalent to -[CrashReport processIdentifier]
39    pub fn process_identifier(&self) -> Option<&str> {
40        self.bundle_identifier().or_else(|| self.process_name())
41    }
42
43    /// Returns the bundle identifier from ls_application_information.
44    /// Equivalent to -[CrashReport bundleIdentifier]
45    /// Retrieves `CFBundleIdentifier` from the LS application information dictionary.
46    pub fn bundle_identifier(&self) -> Option<&str> {
47        self.ls_application_information
48            .as_ref()
49            .and_then(|dict| dict.get("CFBundleIdentifier"))
50            .map(|s| s.as_str())
51    }
52
53    /// Returns the display name of the application.
54    /// Falls back to processName if the display name is not available.
55    /// Equivalent to -[CrashReport displayName]
56    pub fn display_name(&self) -> Option<&str> {
57        // In the ObjC implementation, this looks up kLSDisplayNameKey from
58        // _lsApplicationInformation. Falls back to processName if empty.
59        self.process_name()
60    }
61
62    /// Returns the parent process name.
63    /// Equivalent to -[CrashReport parentProcessName]
64    pub fn parent_process_name(&self) -> Option<&str> {
65        self.parent_process_name.as_deref()
66    }
67
68    /// Returns the responsible process name.
69    /// Equivalent to -[CrashReport responsibleProcessName]
70    pub fn responsible_process_name(&self) -> Option<&str> {
71        self.responsible_process_name.as_deref()
72    }
73
74    /// Returns the process version dictionary, lazily populating it if needed.
75    /// Contains keys "shortVersion" (CFBundleShortVersionString) and "version"
76    /// (CFBundleVersion). Tries LS info, resource fork, then binary images.
77    /// Equivalent to -[CrashReport processVersionDictionary]
78    pub fn process_version_dictionary(&self) -> &HashMap<String, String> {
79        &self.process_version_dictionary
80    }
81
82    /// Sanitizes a version string by removing parentheses.
83    /// Returns empty string if input is None or empty.
84    /// Equivalent to -[CrashReport _sanitizeVersion:]
85    pub(crate) fn sanitize_version(version: Option<&str>) -> String {
86        match version {
87            Some(v) if !v.is_empty() => v.replace(['(', ')'], ""),
88            _ => String::new(),
89        }
90    }
91
92    /// Returns the application build version (CFBundleVersion),
93    /// sanitized to remove parentheses.
94    /// Equivalent to -[CrashReport appBuildVersion]
95    pub fn app_build_version(&self) -> String {
96        Self::sanitize_version(
97            self.process_version_dictionary
98                .get("version")
99                .map(|s| s.as_str()),
100        )
101    }
102
103    /// Returns the application short version string (CFBundleShortVersionString),
104    /// sanitized to remove parentheses.
105    /// Equivalent to -[CrashReport appVersion]
106    pub fn app_version(&self) -> String {
107        Self::sanitize_version(
108            self.process_version_dictionary
109                .get("shortVersion")
110                .map(|s| s.as_str()),
111        )
112    }
113
114    /// Returns a formatted version string: "shortVersion (buildVersion)"
115    /// or just the build version if no short version is available.
116    /// Equivalent to -[CrashReport processVersion]
117    pub fn process_version(&self) -> String {
118        let short = self.app_version();
119        let build = self.app_build_version();
120        if short.is_empty() {
121            build
122        } else {
123            format!("{short} ({build})")
124        }
125    }
126
127    /// Returns the App Store Adam ID.
128    /// Equivalent to -[CrashReport adamID]
129    pub fn adam_id(&self) -> Option<&str> {
130        self.adam_id.as_deref()
131    }
132
133    /// Returns the UUID of the main binary.
134    /// Equivalent to -[CrashReport binaryUUID]
135    pub fn binary_uuid(&self) -> Option<&str> {
136        self.binary_uuid.as_deref()
137    }
138
139    /// Returns the path to the executable.
140    /// Equivalent to -[CrashReport executablePath]
141    pub fn executable_path(&self) -> Option<&str> {
142        self.executable_path.as_deref()
143    }
144
145    /// Returns the reopen path. Falls back to executable_path if not set.
146    /// Equivalent to -[CrashReport reopenPath]
147    pub fn reopen_path(&self) -> Option<&str> {
148        self.reopen_path
149            .as_deref()
150            .or(self.executable_path.as_deref())
151    }
152
153    /// Returns true if a dyld error string is present.
154    /// Equivalent to -[CrashReport isDyldError]
155    pub fn is_dyld_error(&self) -> bool {
156        self.dyld_error_string
157            .as_ref()
158            .is_some_and(|s| !s.is_empty())
159    }
160
161    /// Returns the environment variable dictionary.
162    /// Equivalent to -[CrashReport environment]
163    pub fn environment(&self) -> &HashMap<String, String> {
164        &self.environment
165    }
166
167    /// Returns the notes array. Lazily populates with translocated process
168    /// and OS update notes on first access.
169    /// Equivalent to -[CrashReport notes]
170    pub fn notes(&mut self) -> Vec<String> {
171        let mut result = Vec::new();
172        if self.is_translocated_process {
173            result.push("Translocated Process".to_string());
174        }
175        if let Some(ref build) = self.in_update_previous_os_build {
176            result.push(format!("Occurred during OS Update from build: {build}"));
177        }
178        result
179    }
180
181    /// Returns true if the process is running under Rosetta translation.
182    /// This is the inverse of is_native.
183    /// Equivalent to -[CrashReport isTranslated]
184    pub fn is_translated(&self) -> bool {
185        !self.is_native
186    }
187
188    /// Determines if the crashed app is a user-visible foreground application.
189    /// Checks against known background apps, exec failures, LSUIElement,
190    /// LSBackgroundOnly, and CFBundlePackageType=XPC!.
191    /// Equivalent to -[CrashReport isUserVisibleApp]
192    pub fn is_user_visible_app(&self) -> bool {
193        // Return false for exec failures
194        if self.exec_failure_error.is_some() {
195            return false;
196        }
197
198        let process = self.process_name().unwrap_or("");
199        // WebProcess is always hidden (unless Apple internal)
200        if process == "WebProcess" {
201            return false;
202        }
203
204        // Known background bundle identifiers
205        let excluded_bundles: HashSet<&str> = [
206            "com.apple.iChatAgent",
207            "com.apple.dashboard.client",
208            "com.apple.InterfaceBuilder.IBCocoaTouchPlugin.IBCocoaTouchTool",
209            "com.apple.WebKit.PluginHost",
210        ]
211        .into_iter()
212        .collect();
213
214        if let Some(bundle_id) = self.bundle_identifier() {
215            if excluded_bundles.contains(bundle_id) {
216                return false;
217            }
218            // Finder is always user-visible
219            if bundle_id == "com.apple.finder" {
220                return true;
221            }
222        }
223
224        // Default: assume visible if we have an executable
225        self.executable_path.is_some()
226    }
227
228    /// Returns true if the crash is due to a missing user library.
229    /// Requires: isDyldError AND path not under /System/ AND fatalDyldErrorOnLaunch.
230    /// Equivalent to -[CrashReport isUserMissingLibrary]
231    pub fn is_user_missing_library(&self) -> bool {
232        if !self.is_dyld_error() {
233            return false;
234        }
235        let path = self.executable_path().unwrap_or("");
236        if path.starts_with("/System/") {
237            return false;
238        }
239        self.fatal_dyld_error_on_launch
240    }
241
242    /// Determines if the app should be offered a relaunch option.
243    /// Returns false for excluded bundles, dyld errors, code sign kills,
244    /// and WebProcess. Otherwise delegates to is_user_visible_app.
245    /// Equivalent to -[CrashReport allowRelaunch]
246    pub fn allow_relaunch(&self) -> bool {
247        let excluded_bundles: HashSet<&str> = [
248            "com.apple.iChatAgent",
249            "com.apple.dashboard.client",
250            "com.apple.InterfaceBuilder.IBCocoaTouchPlugin.IBCocoaTouchTool",
251            "com.apple.WebKit.PluginHost",
252        ]
253        .into_iter()
254        .collect();
255
256        if let Some(bundle_id) = self.bundle_identifier()
257            && excluded_bundles.contains(bundle_id)
258        {
259            return false;
260        }
261
262        if self.is_dyld_error() || self.is_code_sign_killed() {
263            return false;
264        }
265
266        if self.process_name() == Some("WebProcess") {
267            return false;
268        }
269
270        self.is_user_visible_app()
271    }
272
273    /// Returns the sleep/wake UUID, or empty string if not set.
274    /// Equivalent to -[CrashReport sleepWakeUUID]
275    pub fn sleep_wake_uuid(&self) -> &str {
276        self.sleep_wake_uuid.as_deref().unwrap_or("")
277    }
278
279    /// Returns true if the process was killed due to a code signing violation.
280    /// Checks bit 0x1000000 (CS_KILLED) in cs_status.
281    /// Equivalent to -[CrashReport isCodeSignKilled]
282    pub fn is_code_sign_killed(&self) -> bool {
283        self.cs_status & 0x100_0000 != 0
284    }
285
286    /// Returns true. Rootless (SIP) is always enabled on Sierra+.
287    /// Equivalent to -[CrashReport isRootlessEnabled]
288    pub fn is_rootless_enabled(&self) -> bool {
289        true
290    }
291
292    /// Returns true if the app has a receipt and an Adam ID (App Store app).
293    /// Equivalent to -[CrashReport isAppStoreApp]
294    pub fn is_app_store_app(&self) -> bool {
295        self.has_receipt && self.adam_id.is_some()
296    }
297
298    /// Returns true if the crash target has an x86 or x86_64 CPU type.
299    pub(crate) fn is_x86_cpu(&self) -> bool {
300        matches!(self.cpu_type.0, 7 | 0x0100_0007)
301    }
302
303    /// Returns true if the crash target has an ARM or ARM64 CPU type.
304    pub(crate) fn is_arm_cpu(&self) -> bool {
305        matches!(self.cpu_type.0, 12 | 0x0100_000c)
306    }
307
308    /// Returns the application-specific dialog mode.
309    /// Equivalent to -[CrashReport applicationSpecificDialogMode]
310    pub fn application_specific_dialog_mode(&self) -> Option<&str> {
311        self.application_specific_dialog_mode.as_deref()
312    }
313
314    /// Sets the thread port and refreshes thread state.
315    /// Equivalent to -[CrashReport setThread:]
316    pub fn set_thread(&mut self, thread: u32) {
317        self.thread = thread;
318        // In the ObjC implementation, this calls thread_get_state to refresh
319        // the _threadState registers. In Rust, the caller would need to
320        // provide the new state separately since we don't have Mach APIs.
321    }
322
323    /// Sets the current binary image being processed.
324    /// Equivalent to -[CrashReport setCurrentBinaryImage:]
325    pub fn set_current_binary_image(&mut self, image: Option<String>) {
326        self.current_binary_image = image;
327    }
328
329    /// Returns the sandbox container path.
330    /// Equivalent to -[CrashReport sandboxContainer]
331    pub fn sandbox_container(&self) -> Option<&str> {
332        self.sandbox_container.as_deref()
333    }
334
335    /// Sets the sandbox container path.
336    /// Equivalent to -[CrashReport setSandboxContainer:]
337    pub fn set_sandbox_container(&mut self, path: Option<String>) {
338        self.sandbox_container = path;
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use crate::test_helpers::*;
345    use crate::*;
346
347    mod accessors {
348        use super::*;
349
350        #[test]
351        fn process_identifier_falls_back_to_process_name() {
352            // ls_application_information is None, so bundle_identifier() returns None
353            let cr = make_test_cr();
354            assert_eq!(cr.process_identifier(), Some("TestApp"));
355        }
356
357        #[test]
358        fn bundle_identifier_from_ls_info() {
359            let mut cr = make_test_cr();
360            let mut info = std::collections::HashMap::new();
361            info.insert("CFBundleIdentifier".into(), "com.example.TestApp".into());
362            cr.ls_application_information = Some(info);
363            assert_eq!(cr.bundle_identifier(), Some("com.example.TestApp"));
364        }
365
366        #[test]
367        fn bundle_identifier_none_without_ls_info() {
368            let cr = make_test_cr();
369            assert_eq!(cr.bundle_identifier(), None);
370        }
371
372        #[test]
373        fn bundle_identifier_none_without_key() {
374            let mut cr = make_test_cr();
375            let info = std::collections::HashMap::new();
376            cr.ls_application_information = Some(info);
377            assert_eq!(cr.bundle_identifier(), None);
378        }
379
380        #[test]
381        fn process_identifier_prefers_bundle_identifier() {
382            let mut cr = make_test_cr();
383            let mut info = std::collections::HashMap::new();
384            info.insert("CFBundleIdentifier".into(), "com.example.TestApp".into());
385            cr.ls_application_information = Some(info);
386            assert_eq!(cr.process_identifier(), Some("com.example.TestApp"));
387        }
388
389        #[test]
390        fn display_name_falls_back_to_process_name() {
391            let cr = make_test_cr();
392            assert_eq!(cr.display_name(), Some("TestApp"));
393        }
394
395        #[test]
396        fn reopen_path_falls_back_to_executable_path() {
397            let cr = make_test_cr();
398            assert_eq!(cr.reopen_path(), cr.executable_path());
399        }
400
401        #[test]
402        fn reopen_path_uses_own_value_when_set() {
403            let mut cr = make_test_cr();
404            cr.reopen_path = Some("/custom/path".into());
405            assert_eq!(cr.reopen_path(), Some("/custom/path"));
406        }
407
408        #[test]
409        fn is_dyld_error_none() {
410            let cr = make_test_cr();
411            assert!(!cr.is_dyld_error());
412        }
413
414        #[test]
415        fn is_dyld_error_empty_string() {
416            let mut cr = make_test_cr();
417            cr.dyld_error_string = Some(String::new());
418            assert!(!cr.is_dyld_error());
419        }
420
421        #[test]
422        fn is_dyld_error_some_string() {
423            let mut cr = make_test_cr();
424            cr.dyld_error_string = Some("dyld: Library not loaded".into());
425            assert!(cr.is_dyld_error());
426        }
427
428        #[test]
429        fn sleep_wake_uuid_none_returns_empty() {
430            let cr = make_test_cr();
431            assert_eq!(cr.sleep_wake_uuid(), "");
432        }
433
434        #[test]
435        fn sleep_wake_uuid_returns_value() {
436            let mut cr = make_test_cr();
437            cr.sleep_wake_uuid = Some("ABC-123".into());
438            assert_eq!(cr.sleep_wake_uuid(), "ABC-123");
439        }
440
441        #[test]
442        fn notes_empty() {
443            let mut cr = make_test_cr();
444            assert!(cr.notes().is_empty());
445        }
446
447        #[test]
448        fn notes_translocated() {
449            let mut cr = make_test_cr();
450            cr.is_translocated_process = true;
451            let notes = cr.notes();
452            assert_eq!(notes.len(), 1);
453            assert_eq!(notes[0], "Translocated Process");
454        }
455
456        #[test]
457        fn notes_os_update() {
458            let mut cr = make_test_cr();
459            cr.in_update_previous_os_build = Some("21A5248p".into());
460            let notes = cr.notes();
461            assert_eq!(notes.len(), 1);
462            assert!(notes[0].contains("21A5248p"));
463        }
464
465        #[test]
466        fn notes_both() {
467            let mut cr = make_test_cr();
468            cr.is_translocated_process = true;
469            cr.in_update_previous_os_build = Some("21A5248p".into());
470            let notes = cr.notes();
471            assert_eq!(notes.len(), 2);
472        }
473
474        #[test]
475        fn is_translated() {
476            let mut cr = make_test_cr();
477            cr.is_native = true;
478            assert!(!cr.is_translated());
479            cr.is_native = false;
480            assert!(cr.is_translated());
481        }
482    }
483
484    mod boolean_flags {
485        use super::*;
486
487        #[test]
488        fn is_code_sign_killed_without_bit() {
489            let mut cr = make_test_cr();
490            cr.cs_status = 0;
491            assert!(!cr.is_code_sign_killed());
492            cr.cs_status = 0xFF_FFFF; // all bits except the kill bit
493            assert!(!cr.is_code_sign_killed());
494        }
495
496        #[test]
497        fn is_code_sign_killed_with_bit() {
498            let mut cr = make_test_cr();
499            cr.cs_status = 0x100_0000;
500            assert!(cr.is_code_sign_killed());
501            cr.cs_status = 0x1FF_FFFF;
502            assert!(cr.is_code_sign_killed());
503        }
504
505        #[test]
506        fn is_rootless_enabled_always_true() {
507            let cr = make_test_cr();
508            assert!(cr.is_rootless_enabled());
509        }
510
511        #[test]
512        fn is_app_store_app_combinations() {
513            let mut cr = make_test_cr();
514            // Neither
515            cr.has_receipt = false;
516            cr.adam_id = None;
517            assert!(!cr.is_app_store_app());
518            // Receipt only
519            cr.has_receipt = true;
520            cr.adam_id = None;
521            assert!(!cr.is_app_store_app());
522            // Adam ID only
523            cr.has_receipt = false;
524            cr.adam_id = Some("12345".into());
525            assert!(!cr.is_app_store_app());
526            // Both
527            cr.has_receipt = true;
528            cr.adam_id = Some("12345".into());
529            assert!(cr.is_app_store_app());
530        }
531
532        #[test]
533        fn is_user_visible_app_exec_failure() {
534            let mut cr = make_test_cr();
535            cr.exec_failure_error = Some(String::new());
536            assert!(!cr.is_user_visible_app());
537        }
538
539        #[test]
540        fn is_user_visible_app_webprocess() {
541            let mut cr = make_test_cr();
542            cr.process_name = Some("WebProcess".into());
543            assert!(!cr.is_user_visible_app());
544        }
545
546        #[test]
547        fn is_user_visible_app_has_executable() {
548            let cr = make_test_cr();
549            assert!(cr.is_user_visible_app());
550        }
551
552        #[test]
553        fn is_user_visible_app_no_executable() {
554            let mut cr = make_test_cr();
555            cr.executable_path = None;
556            assert!(!cr.is_user_visible_app());
557        }
558
559        #[test]
560        fn is_user_missing_library_not_dyld_error() {
561            let cr = make_test_cr();
562            assert!(!cr.is_user_missing_library());
563        }
564
565        #[test]
566        fn is_user_missing_library_system_path() {
567            let mut cr = make_test_cr();
568            cr.dyld_error_string = Some("error".into());
569            cr.executable_path = Some("/System/Library/foo".into());
570            cr.fatal_dyld_error_on_launch = true;
571            assert!(!cr.is_user_missing_library());
572        }
573
574        #[test]
575        fn is_user_missing_library_fatal() {
576            let mut cr = make_test_cr();
577            cr.dyld_error_string = Some("error".into());
578            cr.fatal_dyld_error_on_launch = true;
579            assert!(cr.is_user_missing_library());
580        }
581
582        #[test]
583        fn is_user_missing_library_not_fatal() {
584            let mut cr = make_test_cr();
585            cr.dyld_error_string = Some("error".into());
586            cr.fatal_dyld_error_on_launch = false;
587            assert!(!cr.is_user_missing_library());
588        }
589
590        #[test]
591        fn allow_relaunch_dyld_error() {
592            let mut cr = make_test_cr();
593            cr.dyld_error_string = Some("error".into());
594            assert!(!cr.allow_relaunch());
595        }
596
597        #[test]
598        fn allow_relaunch_code_sign_killed() {
599            let mut cr = make_test_cr();
600            cr.cs_status = 0x100_0000;
601            assert!(!cr.allow_relaunch());
602        }
603
604        #[test]
605        fn allow_relaunch_webprocess() {
606            let mut cr = make_test_cr();
607            cr.process_name = Some("WebProcess".into());
608            assert!(!cr.allow_relaunch());
609        }
610
611        #[test]
612        fn allow_relaunch_normal_app() {
613            let cr = make_test_cr();
614            assert!(cr.allow_relaunch());
615        }
616
617        #[test]
618        fn is_apple_application_apple_path() {
619            let mut cr = make_test_cr();
620            cr.executable_path = Some("/System/Library/Frameworks/foo".into());
621            assert!(cr.is_apple_application());
622        }
623
624        #[test]
625        fn is_apple_application_non_apple() {
626            let cr = make_test_cr();
627            // /Applications is NOT an Apple path
628            assert!(!cr.is_apple_application());
629        }
630    }
631
632    mod version_methods {
633        use super::*;
634
635        #[test]
636        fn sanitize_version_none() {
637            assert_eq!(CrashRustler::sanitize_version(None), "");
638        }
639
640        #[test]
641        fn sanitize_version_empty() {
642            assert_eq!(CrashRustler::sanitize_version(Some("")), "");
643        }
644
645        #[test]
646        fn sanitize_version_normal() {
647            assert_eq!(CrashRustler::sanitize_version(Some("1.2.3")), "1.2.3");
648        }
649
650        #[test]
651        fn sanitize_version_with_parens() {
652            assert_eq!(CrashRustler::sanitize_version(Some("(1.2.3)")), "1.2.3");
653        }
654
655        #[test]
656        fn app_version_empty_dict() {
657            let cr = make_test_cr();
658            assert_eq!(cr.app_version(), "");
659        }
660
661        #[test]
662        fn app_version_populated() {
663            let mut cr = make_test_cr();
664            cr.process_version_dictionary
665                .insert("shortVersion".into(), "2.1".into());
666            assert_eq!(cr.app_version(), "2.1");
667        }
668
669        #[test]
670        fn app_build_version_empty_dict() {
671            let cr = make_test_cr();
672            assert_eq!(cr.app_build_version(), "");
673        }
674
675        #[test]
676        fn app_build_version_populated() {
677            let mut cr = make_test_cr();
678            cr.process_version_dictionary
679                .insert("version".into(), "100".into());
680            assert_eq!(cr.app_build_version(), "100");
681        }
682
683        #[test]
684        fn process_version_short_and_build() {
685            let mut cr = make_test_cr();
686            cr.process_version_dictionary
687                .insert("shortVersion".into(), "2.1".into());
688            cr.process_version_dictionary
689                .insert("version".into(), "100".into());
690            assert_eq!(cr.process_version(), "2.1 (100)");
691        }
692
693        #[test]
694        fn process_version_build_only() {
695            let mut cr = make_test_cr();
696            cr.process_version_dictionary
697                .insert("version".into(), "100".into());
698            assert_eq!(cr.process_version(), "100");
699        }
700
701        #[test]
702        fn process_version_both_empty() {
703            let cr = make_test_cr();
704            assert_eq!(cr.process_version(), "");
705        }
706    }
707}