Skip to main content

crashrustler/
formatting.rs

1use crate::crash_rustler::CrashRustler;
2
3impl CrashRustler {
4    /// Returns the human-readable POSIX signal name for the crash signal.
5    /// Maps signals 0-31 to their standard names (SIGHUP, SIGINT, etc.).
6    /// Unknown signals are formatted as "Signal N".
7    /// Equivalent to -[CrashReport signalName]
8    ///
9    /// # Examples
10    ///
11    /// ```
12    /// use crashrustler::CrashRustler;
13    ///
14    /// let mut cr = CrashRustler::default();
15    /// cr.signal = 11;
16    /// assert_eq!(cr.signal_name(), "SIGSEGV");
17    ///
18    /// cr.signal = 6;
19    /// assert_eq!(cr.signal_name(), "SIGABRT");
20    ///
21    /// cr.signal = 99;
22    /// assert_eq!(cr.signal_name(), "Signal 99");
23    /// ```
24    pub fn signal_name(&self) -> String {
25        match self.signal {
26            0 => String::new(),
27            1 => "SIGHUP".into(),
28            2 => "SIGINT".into(),
29            3 => "SIGQUIT".into(),
30            4 => "SIGILL".into(),
31            5 => "SIGTRAP".into(),
32            6 => "SIGABRT".into(),
33            7 => "SIGEMT".into(),
34            8 => "SIGFPE".into(),
35            9 => "SIGKILL".into(),
36            10 => "SIGBUS".into(),
37            11 => "SIGSEGV".into(),
38            12 => "SIGSYS".into(),
39            13 => "SIGPIPE".into(),
40            14 => "SIGALRM".into(),
41            15 => "SIGTERM".into(),
42            16 => "SIGURG".into(),
43            17 => "SIGSTOP".into(),
44            18 => "SIGTSTP".into(),
45            19 => "SIGCONT".into(),
46            20 => "SIGCHLD".into(),
47            21 => "SIGTTIN".into(),
48            22 => "SIGTTOU".into(),
49            23 => "SIGIO".into(),
50            24 => "SIGXCPU".into(),
51            25 => "SIGXFSZ".into(),
52            26 => "SIGVTALRM".into(),
53            27 => "SIGPROF".into(),
54            28 => "SIGWINCH".into(),
55            29 => "SIGINFO".into(),
56            30 => "SIGUSR1".into(),
57            31 => "SIGUSR2".into(),
58            n => format!("Signal {n}"),
59        }
60    }
61
62    /// Returns the human-readable Mach exception type name.
63    /// Maps standard exception types 1-13 to names like EXC_BAD_ACCESS.
64    /// Unknown types are formatted as hex.
65    /// Equivalent to -[CrashReport exceptionTypeDescription]
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use crashrustler::CrashRustler;
71    ///
72    /// let mut cr = CrashRustler::default();
73    /// cr.exception_type = 1;
74    /// assert_eq!(cr.exception_type_description(), "EXC_BAD_ACCESS");
75    ///
76    /// cr.exception_type = 6;
77    /// assert_eq!(cr.exception_type_description(), "EXC_BREAKPOINT");
78    /// ```
79    pub fn exception_type_description(&self) -> String {
80        match self.exception_type {
81            1 => "EXC_BAD_ACCESS".into(),
82            2 => "EXC_BAD_INSTRUCTION".into(),
83            3 => "EXC_ARITHMETIC".into(),
84            4 => "EXC_EMULATION".into(),
85            5 => "EXC_SOFTWARE".into(),
86            6 => "EXC_BREAKPOINT".into(),
87            7 => "EXC_SYSCALL".into(),
88            8 => "EXC_MACH_SYSCALL".into(),
89            9 => "EXC_RPC_ALERT".into(),
90            10 => "EXC_CRASH".into(),
91            11 => "EXC_RESOURCE".into(),
92            12 => "EXC_GUARD".into(),
93            13 => "EXC_CORPSE_NOTIFY".into(),
94            n => format!("{n:08X}"),
95        }
96    }
97
98    /// Returns a human-readable description of the exception codes.
99    /// For EXC_BAD_ACCESS: maps code\[0\] to KERN_PROTECTION_FAILURE or
100    /// KERN_INVALID_ADDRESS with the faulting address from code\[1\].
101    /// For EXC_ARITHMETIC: maps code\[0\]=1 to EXC_I386_DIV.
102    /// Otherwise formats all codes as hex values joined by commas.
103    /// Equivalent to -[CrashReport exceptionCodesDescription]
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use crashrustler::{CrashRustler, CpuType};
109    ///
110    /// let mut cr = CrashRustler::default();
111    /// cr.exception_type = 1; // EXC_BAD_ACCESS
112    /// cr.exception_code = vec![2, 0x7fff_dead_beef]; // KERN_PROTECTION_FAILURE
113    /// assert_eq!(
114    ///     cr.exception_codes_description(),
115    ///     "KERN_PROTECTION_FAILURE at 0x00007fffdeadbeef"
116    /// );
117    ///
118    /// cr.exception_code = vec![1, 0x0]; // KERN_INVALID_ADDRESS
119    /// assert_eq!(
120    ///     cr.exception_codes_description(),
121    ///     "KERN_INVALID_ADDRESS at 0x0000000000000000"
122    /// );
123    /// ```
124    pub fn exception_codes_description(&self) -> String {
125        if self.exception_code.is_empty() {
126            return String::new();
127        }
128
129        let code0 = self.exception_code[0];
130
131        // EXC_BAD_ACCESS special formatting
132        if self.exception_type == 1 {
133            if code0 == 2 && self.exception_code.len() > 1 {
134                return format!(
135                    "KERN_PROTECTION_FAILURE at 0x{:016x}",
136                    self.exception_code[1] as u64
137                );
138            }
139            if code0 == 1 && self.exception_code.len() > 1 {
140                return format!(
141                    "KERN_INVALID_ADDRESS at 0x{:016x}",
142                    self.exception_code[1] as u64
143                );
144            }
145            // GPF is x86-specific
146            if code0 == 0xd && self.is_x86_cpu() {
147                return "EXC_I386_GPFLT".into();
148            }
149        }
150
151        // EXC_ARITHMETIC special formatting
152        if self.exception_type == 3 {
153            if self.is_x86_cpu() && code0 == 1 {
154                return "EXC_I386_DIV (divide by zero)".into();
155            }
156            if self.is_arm_cpu() {
157                return match code0 {
158                    1 => "EXC_ARM_FP_IO (invalid operation)".into(),
159                    2 => "EXC_ARM_FP_DZ (divide by zero)".into(),
160                    3 => "EXC_ARM_FP_OF (overflow)".into(),
161                    4 => "EXC_ARM_FP_UF (underflow)".into(),
162                    5 => "EXC_ARM_FP_IX (inexact)".into(),
163                    6 => "EXC_ARM_FP_ID (input denormal)".into(),
164                    _ => format!("0x{:016x}", code0 as u64),
165                };
166            }
167        }
168
169        // Default: format all codes as hex
170        self.exception_code
171            .iter()
172            .map(|c| format!("0x{:016x}", *c as u64))
173            .collect::<Vec<_>>()
174            .join(", ")
175    }
176
177    /// Returns the CPU type as a human-readable string.
178    /// Maps: X86, PPC, X86-64, PPC-64, or hex for unknown.
179    /// Equivalent to -[CrashReport _cpuTypeDescription]
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use crashrustler::{CrashRustler, CpuType};
185    ///
186    /// let mut cr = CrashRustler::default();
187    /// cr.cpu_type = CpuType::ARM64;
188    /// assert_eq!(cr.cpu_type_description(), "ARM-64");
189    /// assert_eq!(cr.short_arch_name(), "arm64");
190    ///
191    /// cr.cpu_type = CpuType::X86_64;
192    /// assert_eq!(cr.cpu_type_description(), "X86-64");
193    /// assert_eq!(cr.short_arch_name(), "x86_64");
194    /// ```
195    pub fn cpu_type_description(&self) -> String {
196        match self.cpu_type.0 {
197            7 => "X86".into(),
198            18 => "PPC".into(),
199            12 => "ARM".into(),
200            0x100_0007 => "X86-64".into(),
201            0x100_0012 => "PPC-64".into(),
202            0x100_000c => "ARM-64".into(),
203            n => format!("{n:08X}"),
204        }
205    }
206
207    /// Returns the short architecture name for the CPU type.
208    /// Maps: i386, ppc, x86_64, ppc64, or falls back to cpu_type_description.
209    /// Equivalent to -[CrashReport _shortArchName]
210    pub fn short_arch_name(&self) -> String {
211        match self.cpu_type.0 {
212            7 => "i386".into(),
213            18 => "ppc".into(),
214            12 => "arm".into(),
215            0x100_0007 => "x86_64".into(),
216            0x100_0012 => "ppc64".into(),
217            0x100_000c => "arm64".into(),
218            _ => self.cpu_type_description(),
219        }
220    }
221
222    /// Normalizes whitespace in a string by splitting on whitespace/newline
223    /// characters and rejoining with single spaces.
224    /// Returns empty string for None input.
225    /// Equivalent to -[CrashReport _spacifyString:]
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use crashrustler::CrashRustler;
231    ///
232    /// assert_eq!(
233    ///     CrashRustler::spacify_string(Some("hello   world\n\tfoo")),
234    ///     "hello world foo"
235    /// );
236    /// assert_eq!(CrashRustler::spacify_string(None), "");
237    /// ```
238    pub fn spacify_string(s: Option<&str>) -> String {
239        match s {
240            None => String::new(),
241            Some(s) => s.split_whitespace().collect::<Vec<_>>().join(" "),
242        }
243    }
244
245    /// Replaces newlines in a string with padded newlines for crash report
246    /// formatting alignment. Strips leading newline if present.
247    /// Equivalent to -[CrashReport stringByPaddingNewlinesInString:]
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use crashrustler::CrashRustler;
253    ///
254    /// assert_eq!(
255    ///     CrashRustler::string_by_padding_newlines("line1\nline2\nline3"),
256    ///     "line1\n    line2\n    line3"
257    /// );
258    /// ```
259    pub fn string_by_padding_newlines(s: &str) -> String {
260        let result = s.replace('\n', "\n    ");
261        if let Some(stripped) = result.strip_prefix('\n') {
262            stripped.to_string()
263        } else {
264            result
265        }
266    }
267
268    /// Trims whitespace from a string. If the trimmed result is empty,
269    /// returns "[column N]" where N is the original string length.
270    /// This preserves column-alignment information for empty content.
271    /// Equivalent to -[CrashReport stringByTrimmingColumnSensitiveWhitespacesInString:]
272    pub fn string_by_trimming_column_sensitive_whitespace(s: &str) -> String {
273        let trimmed = s.trim();
274        if trimmed.is_empty() {
275            format!("[column {}]", s.len())
276        } else {
277            trimmed.to_string()
278        }
279    }
280
281    /// Returns true if the given path is an Apple system path.
282    /// Checks: /System, /usr/lib, /usr/bin, /usr/sbin, /bin, /sbin.
283    /// Equivalent to -[CrashReport pathIsApple:]
284    pub fn path_is_apple(path: &str) -> bool {
285        path.starts_with("/System")
286            || path.starts_with("/usr/lib")
287            || path.starts_with("/usr/bin")
288            || path.starts_with("/usr/sbin")
289            || path.starts_with("/bin")
290            || path.starts_with("/sbin")
291    }
292
293    /// Returns true if the bundle identifier belongs to Apple.
294    /// Checks: "com.apple." prefix, "commpage" prefix, or equals "Ozone"/"Motion".
295    /// Equivalent to -[CrashReport bundleIdentifierIsApple:]
296    pub fn bundle_identifier_is_apple(bundle_id: &str) -> bool {
297        bundle_id.starts_with("com.apple.")
298            || bundle_id.starts_with("commpage")
299            || bundle_id == "Ozone"
300            || bundle_id == "Motion"
301    }
302
303    /// Returns true if this is an Apple application, checking both
304    /// the executable path and the bundle identifier.
305    /// Equivalent to -[CrashReport isAppleApplication]
306    pub fn is_apple_application(&self) -> bool {
307        if let Some(path) = self.executable_path()
308            && Self::path_is_apple(path)
309        {
310            return true;
311        }
312        if let Some(bundle_id) = self.bundle_identifier() {
313            return Self::bundle_identifier_is_apple(bundle_id);
314        }
315        false
316    }
317
318    /// Appends an error message to the internal error log.
319    /// Creates the log on first call, appends with newline on subsequent calls.
320    /// Equivalent to -[CrashReport recordInternalError:]
321    pub fn record_internal_error(&mut self, error: &str) {
322        match &mut self.internal_error {
323            Some(existing) => {
324                existing.push('\n');
325                existing.push_str(error);
326            }
327            None => {
328                self.internal_error = Some(error.to_string());
329            }
330        }
331    }
332
333    /// Reduces a u64 value to two significant figures.
334    /// Used for approximate memory statistics in crash reports.
335    /// Equivalent to -[CrashReport reduceToTwoSigFigures:]
336    pub fn reduce_to_two_sig_figures(value: u64) -> u64 {
337        if value == 0 {
338            return 0;
339        }
340        let digits = (value as f64).log10() as u32 + 1;
341        if digits <= 2 {
342            return value;
343        }
344        let divisor = 10u64.pow(digits - 2);
345        (value / divisor) * divisor
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use crate::crash_rustler::CrashRustler;
352    use crate::test_helpers::*;
353    use crate::types::*;
354
355    // =========================================================================
356    // 5. descriptions — Signal, exception, CPU descriptions
357    // =========================================================================
358    mod descriptions {
359        use super::*;
360
361        #[test]
362        fn signal_name_zero() {
363            let mut cr = make_test_cr();
364            cr.signal = 0;
365            assert_eq!(cr.signal_name(), "");
366        }
367
368        #[test]
369        fn signal_name_all_named() {
370            let expected = [
371                (1, "SIGHUP"),
372                (2, "SIGINT"),
373                (3, "SIGQUIT"),
374                (4, "SIGILL"),
375                (5, "SIGTRAP"),
376                (6, "SIGABRT"),
377                (7, "SIGEMT"),
378                (8, "SIGFPE"),
379                (9, "SIGKILL"),
380                (10, "SIGBUS"),
381                (11, "SIGSEGV"),
382                (12, "SIGSYS"),
383                (13, "SIGPIPE"),
384                (14, "SIGALRM"),
385                (15, "SIGTERM"),
386                (16, "SIGURG"),
387                (17, "SIGSTOP"),
388                (18, "SIGTSTP"),
389                (19, "SIGCONT"),
390                (20, "SIGCHLD"),
391                (21, "SIGTTIN"),
392                (22, "SIGTTOU"),
393                (23, "SIGIO"),
394                (24, "SIGXCPU"),
395                (25, "SIGXFSZ"),
396                (26, "SIGVTALRM"),
397                (27, "SIGPROF"),
398                (28, "SIGWINCH"),
399                (29, "SIGINFO"),
400                (30, "SIGUSR1"),
401                (31, "SIGUSR2"),
402            ];
403            let mut cr = make_test_cr();
404            for (sig, name) in expected {
405                cr.signal = sig;
406                assert_eq!(cr.signal_name(), name, "signal={sig}");
407            }
408        }
409
410        #[test]
411        fn signal_name_unknown() {
412            let mut cr = make_test_cr();
413            cr.signal = 32;
414            assert_eq!(cr.signal_name(), "Signal 32");
415            cr.signal = 100;
416            assert_eq!(cr.signal_name(), "Signal 100");
417        }
418
419        #[test]
420        fn exception_type_description_all_known() {
421            let cases = [
422                (1, "EXC_BAD_ACCESS"),
423                (2, "EXC_BAD_INSTRUCTION"),
424                (3, "EXC_ARITHMETIC"),
425                (4, "EXC_EMULATION"),
426                (5, "EXC_SOFTWARE"),
427                (6, "EXC_BREAKPOINT"),
428                (7, "EXC_SYSCALL"),
429                (8, "EXC_MACH_SYSCALL"),
430                (9, "EXC_RPC_ALERT"),
431                (10, "EXC_CRASH"),
432                (11, "EXC_RESOURCE"),
433                (12, "EXC_GUARD"),
434                (13, "EXC_CORPSE_NOTIFY"),
435            ];
436            let mut cr = make_test_cr();
437            for (et, desc) in cases {
438                cr.exception_type = et;
439                assert_eq!(cr.exception_type_description(), desc, "type={et}");
440            }
441        }
442
443        #[test]
444        fn exception_type_description_unknown() {
445            let mut cr = make_test_cr();
446            cr.exception_type = 99;
447            assert_eq!(cr.exception_type_description(), "00000063");
448        }
449
450        #[test]
451        fn exception_codes_description_empty() {
452            let mut cr = make_test_cr();
453            cr.exception_code = vec![];
454            assert_eq!(cr.exception_codes_description(), "");
455        }
456
457        #[test]
458        fn exception_codes_bad_access_protection_failure() {
459            let mut cr = make_test_cr();
460            cr.exception_type = 1;
461            cr.exception_code = vec![2, 0x7fff_0000_1234];
462            assert_eq!(
463                cr.exception_codes_description(),
464                "KERN_PROTECTION_FAILURE at 0x00007fff00001234"
465            );
466        }
467
468        #[test]
469        fn exception_codes_bad_access_invalid_address() {
470            let mut cr = make_test_cr();
471            cr.exception_type = 1;
472            cr.exception_code = vec![1, 0x42];
473            assert_eq!(
474                cr.exception_codes_description(),
475                "KERN_INVALID_ADDRESS at 0x0000000000000042"
476            );
477        }
478
479        #[test]
480        fn exception_codes_bad_access_gpf() {
481            let mut cr = make_test_cr();
482            cr.exception_type = 1;
483            cr.exception_code = vec![0xd];
484            assert_eq!(cr.exception_codes_description(), "EXC_I386_GPFLT");
485        }
486
487        #[test]
488        fn exception_codes_arithmetic_div_zero() {
489            let mut cr = make_test_cr();
490            cr.exception_type = 3;
491            cr.exception_code = vec![1];
492            assert_eq!(
493                cr.exception_codes_description(),
494                "EXC_I386_DIV (divide by zero)"
495            );
496        }
497
498        #[test]
499        fn exception_codes_generic_hex() {
500            let mut cr = make_test_cr();
501            cr.exception_type = 5; // EXC_SOFTWARE
502            cr.exception_code = vec![0xdead, 0xbeef];
503            assert_eq!(
504                cr.exception_codes_description(),
505                "0x000000000000dead, 0x000000000000beef"
506            );
507        }
508
509        #[test]
510        fn cpu_type_description_known() {
511            let mut cr = make_test_cr();
512            let cases = [
513                (CpuType::X86, "X86"),
514                (CpuType::POWERPC, "PPC"),
515                (CpuType::X86_64, "X86-64"),
516                (CpuType::POWERPC64, "PPC-64"),
517            ];
518            for (ct, desc) in cases {
519                cr.cpu_type = ct;
520                assert_eq!(cr.cpu_type_description(), desc);
521            }
522        }
523
524        #[test]
525        fn cpu_type_description_unknown() {
526            let mut cr = make_test_cr();
527            cr.cpu_type = CpuType(999);
528            assert_eq!(cr.cpu_type_description(), "000003E7");
529        }
530
531        #[test]
532        fn short_arch_name_known() {
533            let mut cr = make_test_cr();
534            let cases = [
535                (CpuType::X86, "i386"),
536                (CpuType::POWERPC, "ppc"),
537                (CpuType::X86_64, "x86_64"),
538                (CpuType::POWERPC64, "ppc64"),
539            ];
540            for (ct, name) in cases {
541                cr.cpu_type = ct;
542                assert_eq!(cr.short_arch_name(), name);
543            }
544        }
545
546        #[test]
547        fn short_arch_name_unknown_falls_back() {
548            let mut cr = make_test_cr();
549            cr.cpu_type = CpuType(999);
550            // Falls back to cpu_type_description
551            assert_eq!(cr.short_arch_name(), "000003E7");
552        }
553
554        #[test]
555        fn cpu_type_description_arm() {
556            let mut cr = make_test_cr();
557            cr.cpu_type = CpuType::ARM;
558            assert_eq!(cr.cpu_type_description(), "ARM");
559            cr.cpu_type = CpuType::ARM64;
560            assert_eq!(cr.cpu_type_description(), "ARM-64");
561        }
562
563        #[test]
564        fn short_arch_name_arm() {
565            let mut cr = make_test_cr();
566            cr.cpu_type = CpuType::ARM;
567            assert_eq!(cr.short_arch_name(), "arm");
568            cr.cpu_type = CpuType::ARM64;
569            assert_eq!(cr.short_arch_name(), "arm64");
570        }
571
572        #[test]
573        fn exception_codes_bad_access_gpf_not_on_arm64() {
574            let mut cr = make_test_cr_arm64();
575            cr.exception_type = 1;
576            cr.exception_code = vec![0xd];
577            // ARM64 does not recognize 0xd as GPF — falls through to hex
578            assert_eq!(cr.exception_codes_description(), "0x000000000000000d");
579        }
580
581        #[test]
582        fn exception_codes_arithmetic_arm64_fp_dz() {
583            let mut cr = make_test_cr_arm64();
584            cr.exception_type = 3;
585            cr.exception_code = vec![2];
586            assert_eq!(
587                cr.exception_codes_description(),
588                "EXC_ARM_FP_DZ (divide by zero)"
589            );
590        }
591
592        #[test]
593        fn exception_codes_arithmetic_arm64_fp_io() {
594            let mut cr = make_test_cr_arm64();
595            cr.exception_type = 3;
596            cr.exception_code = vec![1];
597            assert_eq!(
598                cr.exception_codes_description(),
599                "EXC_ARM_FP_IO (invalid operation)"
600            );
601        }
602
603        #[test]
604        fn exception_codes_arithmetic_arm64_unknown() {
605            let mut cr = make_test_cr_arm64();
606            cr.exception_type = 3;
607            cr.exception_code = vec![99];
608            assert_eq!(cr.exception_codes_description(), "0x0000000000000063");
609        }
610    }
611
612    // =========================================================================
613    // 6. string_utils — Static string formatting
614    // =========================================================================
615    mod string_utils {
616        use super::*;
617
618        #[test]
619        fn spacify_string_none() {
620            assert_eq!(CrashRustler::spacify_string(None), "");
621        }
622
623        #[test]
624        fn spacify_string_empty() {
625            assert_eq!(CrashRustler::spacify_string(Some("")), "");
626        }
627
628        #[test]
629        fn spacify_string_normal() {
630            assert_eq!(
631                CrashRustler::spacify_string(Some("hello world")),
632                "hello world"
633            );
634        }
635
636        #[test]
637        fn spacify_string_multi_whitespace() {
638            assert_eq!(
639                CrashRustler::spacify_string(Some("hello   world   foo")),
640                "hello world foo"
641            );
642        }
643
644        #[test]
645        fn spacify_string_tabs_newlines() {
646            assert_eq!(
647                CrashRustler::spacify_string(Some("hello\t\nworld")),
648                "hello world"
649            );
650        }
651
652        #[test]
653        fn string_by_padding_newlines_no_newlines() {
654            assert_eq!(CrashRustler::string_by_padding_newlines("hello"), "hello");
655        }
656
657        #[test]
658        fn string_by_padding_newlines_with_newlines() {
659            assert_eq!(
660                CrashRustler::string_by_padding_newlines("line1\nline2\nline3"),
661                "line1\n    line2\n    line3"
662            );
663        }
664
665        #[test]
666        fn string_by_padding_newlines_leading_newline() {
667            assert_eq!(
668                CrashRustler::string_by_padding_newlines("\nline1"),
669                "    line1"
670            );
671        }
672
673        #[test]
674        fn trimming_column_sensitive_normal() {
675            assert_eq!(
676                CrashRustler::string_by_trimming_column_sensitive_whitespace("  hello  "),
677                "hello"
678            );
679        }
680
681        #[test]
682        fn trimming_column_sensitive_all_whitespace() {
683            assert_eq!(
684                CrashRustler::string_by_trimming_column_sensitive_whitespace("     "),
685                "[column 5]"
686            );
687        }
688
689        #[test]
690        fn trimming_column_sensitive_empty() {
691            assert_eq!(
692                CrashRustler::string_by_trimming_column_sensitive_whitespace(""),
693                "[column 0]"
694            );
695        }
696
697        #[test]
698        fn path_is_apple_system() {
699            assert!(CrashRustler::path_is_apple(
700                "/System/Library/Frameworks/AppKit.framework"
701            ));
702        }
703
704        #[test]
705        fn path_is_apple_usr_lib() {
706            assert!(CrashRustler::path_is_apple("/usr/lib/libSystem.B.dylib"));
707        }
708
709        #[test]
710        fn path_is_apple_usr_bin() {
711            assert!(CrashRustler::path_is_apple("/usr/bin/file"));
712        }
713
714        #[test]
715        fn path_is_apple_usr_sbin() {
716            assert!(CrashRustler::path_is_apple("/usr/sbin/notifyd"));
717        }
718
719        #[test]
720        fn path_is_apple_bin() {
721            assert!(CrashRustler::path_is_apple("/bin/sh"));
722        }
723
724        #[test]
725        fn path_is_apple_sbin() {
726            assert!(CrashRustler::path_is_apple("/sbin/launchd"));
727        }
728
729        #[test]
730        fn path_is_apple_applications_not_apple() {
731            assert!(!CrashRustler::path_is_apple("/Applications/Foo.app"));
732        }
733
734        #[test]
735        fn bundle_identifier_is_apple_com_apple() {
736            assert!(CrashRustler::bundle_identifier_is_apple("com.apple.Safari"));
737        }
738
739        #[test]
740        fn bundle_identifier_is_apple_commpage() {
741            assert!(CrashRustler::bundle_identifier_is_apple("commpage"));
742            assert!(CrashRustler::bundle_identifier_is_apple("commpage64"));
743        }
744
745        #[test]
746        fn bundle_identifier_is_apple_ozone() {
747            assert!(CrashRustler::bundle_identifier_is_apple("Ozone"));
748        }
749
750        #[test]
751        fn bundle_identifier_is_apple_motion() {
752            assert!(CrashRustler::bundle_identifier_is_apple("Motion"));
753        }
754
755        #[test]
756        fn bundle_identifier_is_apple_not_apple() {
757            assert!(!CrashRustler::bundle_identifier_is_apple(
758                "com.google.Chrome"
759            ));
760            assert!(!CrashRustler::bundle_identifier_is_apple(
761                "org.mozilla.firefox"
762            ));
763        }
764
765        #[test]
766        fn reduce_to_two_sig_figures_zero() {
767            assert_eq!(CrashRustler::reduce_to_two_sig_figures(0), 0);
768        }
769
770        #[test]
771        fn reduce_to_two_sig_figures_small() {
772            assert_eq!(CrashRustler::reduce_to_two_sig_figures(1), 1);
773            assert_eq!(CrashRustler::reduce_to_two_sig_figures(42), 42);
774            assert_eq!(CrashRustler::reduce_to_two_sig_figures(99), 99);
775        }
776
777        #[test]
778        fn reduce_to_two_sig_figures_three_digits() {
779            assert_eq!(CrashRustler::reduce_to_two_sig_figures(100), 100);
780            assert_eq!(CrashRustler::reduce_to_two_sig_figures(123), 120);
781            assert_eq!(CrashRustler::reduce_to_two_sig_figures(999), 990);
782        }
783
784        #[test]
785        fn reduce_to_two_sig_figures_large() {
786            assert_eq!(CrashRustler::reduce_to_two_sig_figures(12345), 12000);
787        }
788
789        #[test]
790        fn sanitize_path_user_path() {
791            assert_eq!(
792                CrashRustler::sanitize_path("/Users/kurtis/foo/bar"),
793                "/Users/USER/foo/bar"
794            );
795        }
796
797        #[test]
798        fn sanitize_path_system_unchanged() {
799            assert_eq!(
800                CrashRustler::sanitize_path("/System/Library/Frameworks/foo"),
801                "/System/Library/Frameworks/foo"
802            );
803        }
804
805        #[test]
806        fn sanitize_path_users_no_trailing_slash() {
807            // /Users/kurtis (no slash after username) — no substitution
808            assert_eq!(
809                CrashRustler::sanitize_path("/Users/kurtis"),
810                "/Users/kurtis"
811            );
812        }
813    }
814
815    // =========================================================================
816    // 13. mac_roman — Character encoding
817    // =========================================================================
818    mod mac_roman {
819        use crate::crash_rustler::mac_roman_to_char;
820
821        #[test]
822        fn ascii_passthrough() {
823            for b in 0..0x80u8 {
824                assert_eq!(mac_roman_to_char(b), b as char);
825            }
826        }
827
828        #[test]
829        fn known_characters() {
830            // 0x80 = Ä
831            assert_eq!(mac_roman_to_char(0x80), '\u{00C4}');
832            // 0xCA = non-breaking space
833            assert_eq!(mac_roman_to_char(0xCA), '\u{00A0}');
834            // 0xDB = €
835            assert_eq!(mac_roman_to_char(0xDB), '\u{20AC}');
836        }
837    }
838}