crashrustler/
accessors.rs1use std::collections::{HashMap, HashSet};
2
3use crate::crash_rustler::CrashRustler;
4use crate::types::*;
5
6impl CrashRustler {
7 pub fn date(&self) -> Option<&str> {
10 self.date.as_deref()
11 }
12
13 pub fn task(&self) -> u32 {
16 self.task
17 }
18
19 pub fn pid(&self) -> i32 {
22 self.pid
23 }
24
25 pub fn cpu_type(&self) -> CpuType {
28 self.cpu_type
29 }
30
31 pub fn process_name(&self) -> Option<&str> {
34 self.process_name.as_deref()
35 }
36
37 pub fn process_identifier(&self) -> Option<&str> {
40 self.bundle_identifier().or_else(|| self.process_name())
41 }
42
43 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 pub fn display_name(&self) -> Option<&str> {
57 self.process_name()
60 }
61
62 pub fn parent_process_name(&self) -> Option<&str> {
65 self.parent_process_name.as_deref()
66 }
67
68 pub fn responsible_process_name(&self) -> Option<&str> {
71 self.responsible_process_name.as_deref()
72 }
73
74 pub fn process_version_dictionary(&self) -> &HashMap<String, String> {
79 &self.process_version_dictionary
80 }
81
82 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 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 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 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 pub fn adam_id(&self) -> Option<&str> {
130 self.adam_id.as_deref()
131 }
132
133 pub fn binary_uuid(&self) -> Option<&str> {
136 self.binary_uuid.as_deref()
137 }
138
139 pub fn executable_path(&self) -> Option<&str> {
142 self.executable_path.as_deref()
143 }
144
145 pub fn reopen_path(&self) -> Option<&str> {
148 self.reopen_path
149 .as_deref()
150 .or(self.executable_path.as_deref())
151 }
152
153 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 pub fn environment(&self) -> &HashMap<String, String> {
164 &self.environment
165 }
166
167 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 pub fn is_translated(&self) -> bool {
185 !self.is_native
186 }
187
188 pub fn is_user_visible_app(&self) -> bool {
193 if self.exec_failure_error.is_some() {
195 return false;
196 }
197
198 let process = self.process_name().unwrap_or("");
199 if process == "WebProcess" {
201 return false;
202 }
203
204 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 if bundle_id == "com.apple.finder" {
220 return true;
221 }
222 }
223
224 self.executable_path.is_some()
226 }
227
228 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 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 pub fn sleep_wake_uuid(&self) -> &str {
276 self.sleep_wake_uuid.as_deref().unwrap_or("")
277 }
278
279 pub fn is_code_sign_killed(&self) -> bool {
283 self.cs_status & 0x100_0000 != 0
284 }
285
286 pub fn is_rootless_enabled(&self) -> bool {
289 true
290 }
291
292 pub fn is_app_store_app(&self) -> bool {
295 self.has_receipt && self.adam_id.is_some()
296 }
297
298 pub(crate) fn is_x86_cpu(&self) -> bool {
300 matches!(self.cpu_type.0, 7 | 0x0100_0007)
301 }
302
303 pub(crate) fn is_arm_cpu(&self) -> bool {
305 matches!(self.cpu_type.0, 12 | 0x0100_000c)
306 }
307
308 pub fn application_specific_dialog_mode(&self) -> Option<&str> {
311 self.application_specific_dialog_mode.as_deref()
312 }
313
314 pub fn set_thread(&mut self, thread: u32) {
317 self.thread = thread;
318 }
322
323 pub fn set_current_binary_image(&mut self, image: Option<String>) {
326 self.current_binary_image = image;
327 }
328
329 pub fn sandbox_container(&self) -> Option<&str> {
332 self.sandbox_container.as_deref()
333 }
334
335 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 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; 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 cr.has_receipt = false;
516 cr.adam_id = None;
517 assert!(!cr.is_app_store_app());
518 cr.has_receipt = true;
520 cr.adam_id = None;
521 assert!(!cr.is_app_store_app());
522 cr.has_receipt = false;
524 cr.adam_id = Some("12345".into());
525 assert!(!cr.is_app_store_app());
526 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 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}