Skip to main content

crashrustler/
backtrace.rs

1use crate::crash_rustler::CrashRustler;
2use crate::types::*;
3
4impl CrashRustler {
5    /// Adds a binary image to the list. Deduplicates by checking attempted_binary_images.
6    /// Sets current_binary_image to "name @ base_address" for tracking.
7    /// Returns true if the image was added (not a duplicate).
8    /// Equivalent to -[CrashReport _extractBinaryImageInfoFromSymbolOwner:withMemory:]
9    pub fn add_binary_image(&mut self, image: BinaryImage) -> bool {
10        let key = format!("{} @ 0x{:x}", image.name, image.base_address);
11        self.current_binary_image = Some(key.clone());
12
13        if self.attempted_binary_images.contains(&key) {
14            return false;
15        }
16        self.attempted_binary_images.insert(key);
17        self.binary_images.push(image);
18        true
19    }
20
21    /// Post-processes binary images: enriches metadata, sorts by base address,
22    /// and assigns sequential indices. Called lazily on first access.
23    /// Equivalent to the post-processing logic in -[CrashReport binaryImages]
24    pub fn finalize_binary_images(&mut self) {
25        if self.binary_image_post_processing_complete {
26            return;
27        }
28
29        // Enrich images: fill in missing identifiers from path
30        for image in &mut self.binary_images {
31            if image.identifier.is_none()
32                && let Some(last) = image.path.rsplit('/').next()
33            {
34                image.identifier = Some(last.to_string());
35            }
36        }
37
38        // Sort by base address
39        self.binary_images.sort_by_key(|img| img.base_address);
40
41        // Track max identifier length for formatting
42        self.max_binary_identifier_length = self
43            .binary_images
44            .iter()
45            .map(|img| img.identifier.as_deref().unwrap_or("").len() as u32)
46            .max()
47            .unwrap_or(0);
48
49        self.binary_image_post_processing_complete = true;
50    }
51
52    /// Finds the binary image containing the given address.
53    /// Linear search: returns first image where base_address <= addr < end_address.
54    /// Equivalent to -[CrashReport binaryImageDictionaryForAddress:]
55    pub fn binary_image_for_address(&self, address: u64) -> Option<&BinaryImage> {
56        self.binary_images
57            .iter()
58            .find(|img| address >= img.base_address && address < img.end_address)
59    }
60
61    /// Finds the binary image with the given executable path.
62    /// Equivalent to -[CrashReport binaryImageDictionaryForPath:]
63    pub fn binary_image_for_path(&self, path: &str) -> Option<&BinaryImage> {
64        self.binary_images.iter().find(|img| img.path == path)
65    }
66
67    /// Formats a single binary image line for the crash report.
68    /// Format: `startAddr - endAddr [+]identifier version <UUID> path`
69    /// Apple images get "+" prefix. 32-bit uses 10-digit hex, 64-bit uses 18-digit.
70    /// Equivalent to -[CrashReport _appendToDescription:binaryImageDict:force64BitMode:]
71    pub fn format_binary_image_line(&self, image: &BinaryImage, force_64bit: bool) -> String {
72        let is_apple = Self::path_is_apple(&image.path)
73            || image
74                .identifier
75                .as_deref()
76                .is_some_and(Self::bundle_identifier_is_apple);
77        let apple_marker = if is_apple { "+" } else { " " };
78
79        let identifier = image.identifier.as_deref().unwrap_or("???");
80
81        let version_str = match (&image.version, image.identifier.as_deref()) {
82            (Some(ver), _) => {
83                let sanitized = Self::sanitize_version(Some(ver));
84                format!("({sanitized})")
85            }
86            _ => String::new(),
87        };
88
89        let uuid_str = image
90            .uuid
91            .as_deref()
92            .map(|u| format!("<{u}>"))
93            .unwrap_or_default();
94
95        let end_addr = if image.end_address > 0 {
96            image.end_address - 1
97        } else {
98            0
99        };
100
101        if self.is_64_bit || force_64bit {
102            format!(
103                "    0x{:018x} - 0x{:018x} {apple_marker}{identifier} {version_str} {uuid_str} {}",
104                image.base_address, end_addr, image.path
105            )
106        } else {
107            format!(
108                "    0x{:010x} - 0x{:010x} {apple_marker}{identifier} {version_str} {uuid_str} {}",
109                image.base_address, end_addr, image.path
110            )
111        }
112    }
113
114    /// Formats all binary images as a human-readable crash report section.
115    /// Equivalent to -[CrashReport binaryImagesDescription]
116    pub fn binary_images_description(&self) -> String {
117        let mut result = String::new();
118
119        if self.binary_images.is_empty() {
120            result.push_str("Binary images description not available.\n");
121            return result;
122        }
123
124        result.push_str("Binary Images:\n");
125        for image in &self.binary_images {
126            let line = self.format_binary_image_line(image, false);
127            result.push_str(&line);
128            result.push('\n');
129        }
130        result
131    }
132
133    // =========================================================================
134    // Backtrace and thread methods
135    // =========================================================================
136
137    /// Adds a thread backtrace from sampled data. Determines crashed thread
138    /// by matching thread port or thread ID. Detects special crash patterns:
139    /// exec failure (___NEW_PROCESS_COULD_NOT_BE_EXECD___), ObjC messaging
140    /// crashes (objc_msgSend*), dyld fatal errors, and SIGABRT in abort/__abort.
141    /// Equivalent to -[CrashReport _extractBacktraceInfoUsingSymbolicator:]
142    pub fn add_thread_backtrace(&mut self, backtrace: ThreadBacktrace) {
143        let thread_idx = self.backtraces.len() as i32;
144
145        // Determine if this is the crashed thread
146        if self.crashed_thread_number < 0 {
147            if backtrace.is_crashed {
148                self.crashed_thread_number = thread_idx;
149            } else if let Some(tid) = backtrace.thread_id
150                && self.thread_id == Some(tid)
151            {
152                self.crashed_thread_number = thread_idx;
153            }
154        }
155
156        // Detect special patterns in frame 0 of crashed thread
157        if self.crashed_thread_number == thread_idx
158            && let Some(frame) = backtrace.frames.first()
159            && let Some(ref sym) = frame.symbol_name
160        {
161            if sym == "___NEW_PROCESS_COULD_NOT_BE_EXECD___" {
162                self.exec_failure_error = Some(String::new());
163            } else if sym.starts_with("objc_msgSend") {
164                self.objc_selector_name = Some(sym.clone());
165            } else if sym.starts_with("dyld_fatal_error") && self.dyld_error_string.is_none() {
166                self.extract_legacy_dyld_error_string = true;
167            }
168            // SIGABRT in abort/__abort → override crashed thread
169            if self.signal == 6 && (sym == "abort" || sym == "__abort") {
170                self.crashed_thread_number = thread_idx;
171            }
172        }
173
174        self.backtraces.push(backtrace);
175    }
176
177    /// Formats all thread backtraces as human-readable crash report text.
178    /// Each thread: "Thread N:" or "Thread N Crashed:". Each frame:
179    /// "frameNum  identifier  address  symbolName + offset"
180    /// Uses 10-digit hex for 32-bit, 18-digit for 64-bit addresses.
181    /// Equivalent to -[CrashReport backtraceDescription]
182    pub fn backtrace_description(&self) -> String {
183        if self.backtraces.is_empty() {
184            return "Backtrace not available\n".to_string();
185        }
186
187        let mut result = String::new();
188
189        for (thread_idx, bt) in self.backtraces.iter().enumerate() {
190            let crashed_marker = if thread_idx as i32 == self.crashed_thread_number {
191                " Crashed"
192            } else {
193                ""
194            };
195
196            result.push_str(&format!("Thread {thread_idx}{crashed_marker}:"));
197            if let Some(ref name) = bt.thread_name {
198                let clean_name = name.replace('\n', " ");
199                result.push_str(&format!(" {clean_name}"));
200            }
201            result.push('\n');
202
203            for frame in &bt.frames {
204                let identifier = if let Some(img) = self.binary_image_for_address(frame.address) {
205                    let id = img.identifier.as_deref().unwrap_or("???");
206                    if id.len() < 30 {
207                        format!("{id:<30}")
208                    } else {
209                        id.to_string()
210                    }
211                } else {
212                    format!("{:<30}", "???")
213                };
214
215                let addr_str = if self.is_64_bit {
216                    format!("0x{:018x}", frame.address)
217                } else {
218                    format!("0x{:010x}", frame.address)
219                };
220
221                result.push_str(&format!(
222                    "{}  {}  {}",
223                    frame.frame_number, identifier, addr_str
224                ));
225
226                if let Some(ref sym) = frame.symbol_name {
227                    result.push_str(&format!(" {} + 0x{:x}", sym, frame.symbol_offset));
228                } else if let Some(img) = self.binary_image_for_address(frame.address) {
229                    let offset = frame.address - img.base_address;
230                    result.push_str(&format!(" 0x{:x} + {offset}", img.base_address));
231                }
232
233                if let Some(ref file) = frame.source_file
234                    && let Some(line) = frame.source_line
235                {
236                    result.push_str(&format!(" ({file}:{line})"));
237                }
238
239                result.push('\n');
240            }
241            result.push('\n');
242        }
243
244        result
245    }
246
247    /// Formats the crashed thread's register state as human-readable text.
248    /// Supports x86_THREAD_STATE (flavor 7) with 32/64-bit sub-flavors,
249    /// x86_THREAD_STATE32 (flavor 1 on x86), ARM_THREAD_STATE64 (flavor 6),
250    /// and ARM_THREAD_STATE (flavor 1 on ARM with sub-flavor dispatch).
251    /// Equivalent to -[CrashReport threadStateDescription]
252    pub fn thread_state_description(&self) -> String {
253        let thread_label = if self.crashed_thread_number >= 0 {
254            format!("Thread {}", self.crashed_thread_number)
255        } else {
256            "Unknown thread".to_string()
257        };
258
259        let regs = &self.thread_state.registers;
260        let flavor = self.thread_state.flavor;
261
262        // ARM_THREAD_STATE64 (flavor 6): x0-x28, fp, lr, sp, pc, cpsr
263        // 33 registers * 2 u32s each + cpsr(1) + pad(1) = 68 u32s
264        if flavor == 6 && regs.len() >= 68 {
265            return self.format_arm64_regs(&thread_label, regs, 0);
266        }
267
268        // x86_THREAD_STATE (flavor 7) with sub-flavor check
269        if flavor == 7 && !regs.is_empty() {
270            let sub_flavor = regs[0];
271            if sub_flavor == 1 && regs.len() >= 18 {
272                // 32-bit: eax, ebx, ecx, edx, edi, esi, ebp, esp, ss, efl, eip, cs, ds, es, fs, gs
273                return format!(
274                    "{thread_label} crashed with X86 Thread State (32-bit):\n  \
275                     eax: 0x{:08x}  ebx: 0x{:08x}  ecx: 0x{:08x}  edx: 0x{:08x}\n  \
276                     edi: 0x{:08x}  esi: 0x{:08x}  ebp: 0x{:08x}  esp: 0x{:08x}\n  \
277                     ss: 0x{:08x}   efl: 0x{:08x}  eip: 0x{:08x}  cs: 0x{:08x}\n  \
278                     ds: 0x{:08x}   es: 0x{:08x}   fs: 0x{:08x}   gs: 0x{:08x}\n",
279                    regs[2],
280                    regs[3],
281                    regs[4],
282                    regs[5],
283                    regs[6],
284                    regs[7],
285                    regs[8],
286                    regs[9],
287                    regs[10],
288                    regs[11],
289                    regs[12],
290                    regs[13],
291                    regs[14],
292                    regs[15],
293                    regs[16],
294                    regs[17]
295                );
296            }
297            // 64-bit state (sub_flavor != 1): registers are 64-bit, stored as pairs of u32
298            if regs.len() >= 44 {
299                let r = |idx: usize| -> u64 {
300                    let base = 2 + idx * 2;
301                    (regs[base] as u64) | ((regs[base + 1] as u64) << 32)
302                };
303                return format!(
304                    "{thread_label} crashed with X86 Thread State (64-bit):\n  \
305                     rax: 0x{:016x}  rbx: 0x{:016x}  rcx: 0x{:016x}  rdx: 0x{:016x}\n  \
306                     rdi: 0x{:016x}  rsi: 0x{:016x}  rbp: 0x{:016x}  rsp: 0x{:016x}\n  \
307                     r8:  0x{:016x}  r9:  0x{:016x}  r10: 0x{:016x}  r11: 0x{:016x}\n  \
308                     r12: 0x{:016x}  r13: 0x{:016x}  r14: 0x{:016x}  r15: 0x{:016x}\n  \
309                     rip: 0x{:016x}  rfl: 0x{:016x}\n",
310                    r(0),
311                    r(1),
312                    r(2),
313                    r(3),
314                    r(4),
315                    r(5),
316                    r(6),
317                    r(7),
318                    r(8),
319                    r(9),
320                    r(10),
321                    r(11),
322                    r(12),
323                    r(13),
324                    r(14),
325                    r(15),
326                    r(16),
327                    r(17)
328                );
329            }
330        }
331
332        // Flavor 1 collision: ARM_THREAD_STATE (unified) vs x86_THREAD_STATE32
333        if flavor == 1 && !regs.is_empty() {
334            if self.is_arm_cpu() {
335                let sub_flavor = regs[0];
336                if sub_flavor == 2 && regs.len() >= 70 {
337                    // ARM_THREAD_STATE sub_flavor=2 (ARM64): 2-word header + 68 ARM64 regs
338                    return self.format_arm64_regs(&thread_label, regs, 2);
339                }
340                if sub_flavor == 1 && regs.len() >= 19 {
341                    // ARM_THREAD_STATE sub_flavor=1 (ARM32): r0-r15, cpsr
342                    // 2 header words + 17 register words
343                    return format!(
344                        "{thread_label} crashed with ARM Thread State (32-bit):\n  \
345                         r0:  0x{:08x}  r1:  0x{:08x}  r2:  0x{:08x}  r3:  0x{:08x}\n  \
346                         r4:  0x{:08x}  r5:  0x{:08x}  r6:  0x{:08x}  r7:  0x{:08x}\n  \
347                         r8:  0x{:08x}  r9:  0x{:08x}  r10: 0x{:08x}  r11: 0x{:08x}\n  \
348                         r12: 0x{:08x}  sp:  0x{:08x}  lr:  0x{:08x}  pc:  0x{:08x}\n  \
349                         cpsr: 0x{:08x}\n",
350                        regs[2],
351                        regs[3],
352                        regs[4],
353                        regs[5],
354                        regs[6],
355                        regs[7],
356                        regs[8],
357                        regs[9],
358                        regs[10],
359                        regs[11],
360                        regs[12],
361                        regs[13],
362                        regs[14],
363                        regs[15],
364                        regs[16],
365                        regs[17],
366                        regs[18]
367                    );
368                }
369            } else if regs.len() >= 16 {
370                // x86_THREAD_STATE32 (flavor 1)
371                return format!(
372                    "{thread_label} crashed with X86 Thread State (32-bit):\n  \
373                     eax: 0x{:08x}  ebx: 0x{:08x}  ecx: 0x{:08x}  edx: 0x{:08x}\n  \
374                     edi: 0x{:08x}  esi: 0x{:08x}  ebp: 0x{:08x}  esp: 0x{:08x}\n  \
375                     ss: 0x{:08x}   efl: 0x{:08x}  eip: 0x{:08x}  cs: 0x{:08x}\n  \
376                     ds: 0x{:08x}   es: 0x{:08x}   fs: 0x{:08x}   gs: 0x{:08x}\n",
377                    regs[0],
378                    regs[1],
379                    regs[2],
380                    regs[3],
381                    regs[4],
382                    regs[5],
383                    regs[6],
384                    regs[7],
385                    regs[8],
386                    regs[9],
387                    regs[10],
388                    regs[11],
389                    regs[12],
390                    regs[13],
391                    regs[14],
392                    regs[15]
393                );
394            }
395        }
396
397        format!(
398            "{thread_label} crashed with unknown flavor {}, state count {}\n",
399            flavor,
400            regs.len()
401        )
402    }
403
404    /// Formats ARM64 register state (x0-x28, fp, lr, sp, pc, cpsr) from a u32 slice.
405    /// `offset` is the starting index in `regs` (0 for flavor 6, 2 for flavor 1/sub2).
406    fn format_arm64_regs(&self, thread_label: &str, regs: &[u32], offset: usize) -> String {
407        let r = |idx: usize| -> u64 {
408            let base = offset + idx * 2;
409            (regs[base] as u64) | ((regs[base + 1] as u64) << 32)
410        };
411        let cpsr = regs[offset + 66];
412        format!(
413            "{thread_label} crashed with ARM Thread State (64-bit):\n  \
414             x0:  0x{:016x}  x1:  0x{:016x}  x2:  0x{:016x}  x3:  0x{:016x}\n  \
415             x4:  0x{:016x}  x5:  0x{:016x}  x6:  0x{:016x}  x7:  0x{:016x}\n  \
416             x8:  0x{:016x}  x9:  0x{:016x}  x10: 0x{:016x}  x11: 0x{:016x}\n  \
417             x12: 0x{:016x}  x13: 0x{:016x}  x14: 0x{:016x}  x15: 0x{:016x}\n  \
418             x16: 0x{:016x}  x17: 0x{:016x}  x18: 0x{:016x}  x19: 0x{:016x}\n  \
419             x20: 0x{:016x}  x21: 0x{:016x}  x22: 0x{:016x}  x23: 0x{:016x}\n  \
420             x24: 0x{:016x}  x25: 0x{:016x}  x26: 0x{:016x}  x27: 0x{:016x}\n  \
421             x28: 0x{:016x}  fp:  0x{:016x}  lr:  0x{:016x}  sp:  0x{:016x}\n  \
422             pc:  0x{:016x}  cpsr: 0x{:08x}\n",
423            r(0),
424            r(1),
425            r(2),
426            r(3),
427            r(4),
428            r(5),
429            r(6),
430            r(7),
431            r(8),
432            r(9),
433            r(10),
434            r(11),
435            r(12),
436            r(13),
437            r(14),
438            r(15),
439            r(16),
440            r(17),
441            r(18),
442            r(19),
443            r(20),
444            r(21),
445            r(22),
446            r(23),
447            r(24),
448            r(25),
449            r(26),
450            r(27),
451            r(28),
452            r(29),
453            r(30),
454            r(31),
455            r(32),
456            cpsr
457        )
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use crate::test_helpers::*;
464    use crate::types::*;
465
466    // =========================================================================
467    // 9. binary_images — Image management
468    // =========================================================================
469    mod binary_images {
470        use super::*;
471
472        fn make_image(name: &str, base: u64, end: u64) -> BinaryImage {
473            BinaryImage {
474                name: name.into(),
475                path: format!("/usr/lib/{name}"),
476                uuid: Some("AAAA-BBBB-CCCC".into()),
477                base_address: base,
478                end_address: end,
479                arch: Some("x86_64".into()),
480                identifier: None,
481                version: Some("1.0".into()),
482            }
483        }
484
485        #[test]
486        fn add_binary_image_normal() {
487            let mut cr = make_test_cr();
488            let img = make_image("libfoo.dylib", 0x1000, 0x2000);
489            assert!(cr.add_binary_image(img));
490            assert_eq!(cr.binary_images.len(), 1);
491        }
492
493        #[test]
494        fn add_binary_image_duplicate() {
495            let mut cr = make_test_cr();
496            let img1 = make_image("libfoo.dylib", 0x1000, 0x2000);
497            let img2 = make_image("libfoo.dylib", 0x1000, 0x2000);
498            assert!(cr.add_binary_image(img1));
499            assert!(!cr.add_binary_image(img2));
500            assert_eq!(cr.binary_images.len(), 1);
501        }
502
503        #[test]
504        fn finalize_binary_images_sorts_by_address() {
505            let mut cr = make_test_cr();
506            cr.add_binary_image(make_image("libB.dylib", 0x3000, 0x4000));
507            cr.add_binary_image(make_image("libA.dylib", 0x1000, 0x2000));
508            cr.finalize_binary_images();
509            assert_eq!(cr.binary_images[0].base_address, 0x1000);
510            assert_eq!(cr.binary_images[1].base_address, 0x3000);
511        }
512
513        #[test]
514        fn finalize_binary_images_fills_identifier_from_path() {
515            let mut cr = make_test_cr();
516            let mut img = make_image("libfoo.dylib", 0x1000, 0x2000);
517            img.identifier = None;
518            cr.add_binary_image(img);
519            cr.finalize_binary_images();
520            assert_eq!(
521                cr.binary_images[0].identifier.as_deref(),
522                Some("libfoo.dylib")
523            );
524        }
525
526        #[test]
527        fn finalize_binary_images_max_identifier_length() {
528            let mut cr = make_test_cr();
529            let mut img1 = make_image("short", 0x1000, 0x2000);
530            img1.identifier = Some("short".into());
531            let mut img2 = make_image("much_longer_name.dylib", 0x3000, 0x4000);
532            img2.identifier = Some("much_longer_name.dylib".into());
533            cr.add_binary_image(img1);
534            cr.add_binary_image(img2);
535            cr.finalize_binary_images();
536            assert_eq!(cr.max_binary_identifier_length, 22);
537        }
538
539        #[test]
540        fn finalize_binary_images_idempotent() {
541            let mut cr = make_test_cr();
542            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
543            cr.finalize_binary_images();
544            cr.finalize_binary_images(); // second call does nothing
545            assert_eq!(cr.binary_images.len(), 1);
546        }
547
548        #[test]
549        fn binary_image_for_address_found() {
550            let mut cr = make_test_cr();
551            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
552            assert!(cr.binary_image_for_address(0x1500).is_some());
553        }
554
555        #[test]
556        fn binary_image_for_address_not_found() {
557            let mut cr = make_test_cr();
558            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
559            assert!(cr.binary_image_for_address(0x3000).is_none());
560        }
561
562        #[test]
563        fn binary_image_for_address_boundaries() {
564            let mut cr = make_test_cr();
565            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
566            // base_address is inclusive
567            assert!(cr.binary_image_for_address(0x1000).is_some());
568            // end_address is exclusive
569            assert!(cr.binary_image_for_address(0x2000).is_none());
570            // Just below end
571            assert!(cr.binary_image_for_address(0x1FFF).is_some());
572        }
573
574        #[test]
575        fn binary_image_for_path_found() {
576            let mut cr = make_test_cr();
577            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
578            assert!(cr.binary_image_for_path("/usr/lib/libfoo.dylib").is_some());
579        }
580
581        #[test]
582        fn binary_image_for_path_not_found() {
583            let mut cr = make_test_cr();
584            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
585            assert!(cr.binary_image_for_path("/usr/lib/libbar.dylib").is_none());
586        }
587
588        #[test]
589        fn format_binary_image_line_apple() {
590            let cr = make_test_cr();
591            let mut img = make_image("libSystem.B.dylib", 0x1000, 0x2000);
592            img.identifier = Some("libSystem.B.dylib".into());
593            let line = cr.format_binary_image_line(&img, false);
594            assert!(line.contains("+libSystem.B.dylib"));
595        }
596
597        #[test]
598        fn format_binary_image_line_non_apple() {
599            let cr = make_test_cr();
600            let mut img = make_image("libfoo.dylib", 0x1000, 0x2000);
601            img.path = "/Applications/Foo.app/Contents/Frameworks/libfoo.dylib".into();
602            img.identifier = Some("libfoo.dylib".into());
603            let line = cr.format_binary_image_line(&img, false);
604            assert!(line.contains(" libfoo.dylib"));
605            assert!(!line.contains("+libfoo.dylib"));
606        }
607
608        #[test]
609        fn format_binary_image_line_32bit() {
610            let mut cr = make_test_cr();
611            cr.is_64_bit = false;
612            let img = make_image("libfoo.dylib", 0x1000, 0x2000);
613            let line = cr.format_binary_image_line(&img, false);
614            // 32-bit uses 10-digit hex: "0x" + 10 digits
615            assert!(line.contains("0x0000001000"));
616        }
617
618        #[test]
619        fn format_binary_image_line_64bit() {
620            let cr = make_test_cr();
621            let img = make_image("libfoo.dylib", 0x1000, 0x2000);
622            let line = cr.format_binary_image_line(&img, false);
623            // 64-bit uses 18-digit hex
624            assert!(line.contains("0x000000000000001000"));
625        }
626
627        #[test]
628        fn format_binary_image_line_missing_uuid() {
629            let cr = make_test_cr();
630            let mut img = make_image("libfoo.dylib", 0x1000, 0x2000);
631            img.uuid = None;
632            let line = cr.format_binary_image_line(&img, false);
633            assert!(!line.contains('<'));
634        }
635
636        #[test]
637        fn format_binary_image_line_missing_version() {
638            let cr = make_test_cr();
639            let mut img = make_image("libfoo.dylib", 0x1000, 0x2000);
640            img.version = None;
641            let line = cr.format_binary_image_line(&img, false);
642            assert!(!line.contains('('));
643        }
644
645        #[test]
646        fn binary_images_description_empty() {
647            let cr = make_test_cr();
648            let desc = cr.binary_images_description();
649            assert!(desc.contains("not available"));
650        }
651
652        #[test]
653        fn binary_images_description_populated() {
654            let mut cr = make_test_cr();
655            cr.add_binary_image(make_image("libfoo.dylib", 0x1000, 0x2000));
656            let desc = cr.binary_images_description();
657            assert!(desc.starts_with("Binary Images:"));
658            assert!(desc.contains("libfoo.dylib"));
659        }
660    }
661
662    // =========================================================================
663    // 10. backtrace_methods — Thread backtrace
664    // =========================================================================
665    mod backtrace_methods {
666        use super::*;
667
668        fn make_frame(num: u32, sym: Option<&str>, addr: u64) -> BacktraceFrame {
669            BacktraceFrame {
670                frame_number: num,
671                image_name: "libfoo.dylib".into(),
672                address: addr,
673                symbol_name: sym.map(|s| s.into()),
674                symbol_offset: 42,
675                source_file: None,
676                source_line: None,
677            }
678        }
679
680        fn make_bt(
681            thread_num: u32,
682            is_crashed: bool,
683            frames: Vec<BacktraceFrame>,
684        ) -> ThreadBacktrace {
685            ThreadBacktrace {
686                thread_number: thread_num,
687                thread_name: None,
688                thread_id: None,
689                is_crashed,
690                frames,
691            }
692        }
693
694        #[test]
695        fn add_thread_backtrace_sets_crashed_on_is_crashed() {
696            let mut cr = make_test_cr();
697            cr.crashed_thread_number = -1;
698            let bt = make_bt(0, true, vec![make_frame(0, Some("main"), 0x1000)]);
699            cr.add_thread_backtrace(bt);
700            assert_eq!(cr.crashed_thread_number, 0);
701        }
702
703        #[test]
704        fn add_thread_backtrace_sets_crashed_on_thread_id_match() {
705            let mut cr = make_test_cr();
706            cr.crashed_thread_number = -1;
707            cr.thread_id = Some(999);
708            let mut bt = make_bt(0, false, vec![make_frame(0, Some("main"), 0x1000)]);
709            bt.thread_id = Some(999);
710            cr.add_thread_backtrace(bt);
711            assert_eq!(cr.crashed_thread_number, 0);
712        }
713
714        #[test]
715        fn add_thread_backtrace_exec_failure_pattern() {
716            let mut cr = make_test_cr();
717            cr.crashed_thread_number = -1;
718            let bt = make_bt(
719                0,
720                true,
721                vec![make_frame(
722                    0,
723                    Some("___NEW_PROCESS_COULD_NOT_BE_EXECD___"),
724                    0x1000,
725                )],
726            );
727            cr.add_thread_backtrace(bt);
728            assert!(cr.exec_failure_error.is_some());
729        }
730
731        #[test]
732        fn add_thread_backtrace_objc_msgsend_pattern() {
733            let mut cr = make_test_cr();
734            cr.crashed_thread_number = -1;
735            let bt = make_bt(0, true, vec![make_frame(0, Some("objc_msgSend"), 0x1000)]);
736            cr.add_thread_backtrace(bt);
737            assert_eq!(cr.objc_selector_name, Some("objc_msgSend".into()));
738        }
739
740        #[test]
741        fn add_thread_backtrace_dyld_fatal_error_pattern() {
742            let mut cr = make_test_cr();
743            cr.crashed_thread_number = -1;
744            let bt = make_bt(
745                0,
746                true,
747                vec![make_frame(0, Some("dyld_fatal_error"), 0x1000)],
748            );
749            cr.add_thread_backtrace(bt);
750            assert!(cr.extract_legacy_dyld_error_string);
751        }
752
753        #[test]
754        fn add_thread_backtrace_sigabrt_abort_pattern() {
755            let mut cr = make_test_cr();
756            cr.crashed_thread_number = -1;
757            cr.signal = 6; // SIGABRT
758            let bt = make_bt(0, true, vec![make_frame(0, Some("abort"), 0x1000)]);
759            cr.add_thread_backtrace(bt);
760            assert_eq!(cr.crashed_thread_number, 0);
761        }
762
763        #[test]
764        fn backtrace_description_empty() {
765            let cr = make_test_cr();
766            assert!(cr.backtrace_description().contains("not available"));
767        }
768
769        #[test]
770        fn backtrace_description_single_thread() {
771            let mut cr = make_test_cr();
772            cr.crashed_thread_number = -1;
773            let bt = make_bt(0, false, vec![make_frame(0, Some("main"), 0x1000)]);
774            cr.add_thread_backtrace(bt);
775            let desc = cr.backtrace_description();
776            assert!(desc.contains("Thread 0:"));
777            assert!(desc.contains("main + 0x2a"));
778        }
779
780        #[test]
781        fn backtrace_description_crashed_thread_marker() {
782            let mut cr = make_test_cr();
783            cr.crashed_thread_number = -1;
784            let bt = make_bt(0, true, vec![make_frame(0, Some("crash_fn"), 0x1000)]);
785            cr.add_thread_backtrace(bt);
786            let desc = cr.backtrace_description();
787            assert!(desc.contains("Thread 0 Crashed:"));
788        }
789
790        #[test]
791        fn backtrace_description_32bit_addresses() {
792            let mut cr = make_test_cr();
793            cr.is_64_bit = false;
794            cr.crashed_thread_number = -1;
795            let bt = make_bt(0, false, vec![make_frame(0, Some("fn"), 0x1000)]);
796            cr.add_thread_backtrace(bt);
797            let desc = cr.backtrace_description();
798            // 32-bit: "0x" + 10 hex digits
799            assert!(desc.contains("0x0000001000"));
800        }
801
802        #[test]
803        fn backtrace_description_64bit_addresses() {
804            let mut cr = make_test_cr();
805            cr.crashed_thread_number = -1;
806            let bt = make_bt(0, false, vec![make_frame(0, Some("fn"), 0x1000)]);
807            cr.add_thread_backtrace(bt);
808            let desc = cr.backtrace_description();
809            // 64-bit: "0x" + 18 hex digits
810            assert!(desc.contains("0x000000000000001000"));
811        }
812
813        #[test]
814        fn backtrace_description_source_file_line() {
815            let mut cr = make_test_cr();
816            cr.crashed_thread_number = -1;
817            let mut frame = make_frame(0, Some("fn"), 0x1000);
818            frame.source_file = Some("main.c".into());
819            frame.source_line = Some(42);
820            let bt = make_bt(0, false, vec![frame]);
821            cr.add_thread_backtrace(bt);
822            let desc = cr.backtrace_description();
823            assert!(desc.contains("(main.c:42)"));
824        }
825
826        #[test]
827        fn thread_state_description_flavor7_sub1_32bit() {
828            let mut cr = make_test_cr();
829            cr.crashed_thread_number = 0;
830            // flavor 7, sub_flavor 1 (32-bit), need 18 regs total (2 header + 16 regs)
831            let mut regs = vec![0u32; 18];
832            regs[0] = 1; // sub_flavor
833            regs[1] = 0; // padding
834            regs[2] = 0xAAAA_AAAA; // eax
835            cr.thread_state = ThreadState {
836                flavor: 7,
837                registers: regs,
838            };
839            let desc = cr.thread_state_description();
840            assert!(desc.contains("32-bit"));
841            assert!(desc.contains("eax: 0xaaaaaaaa"));
842        }
843
844        #[test]
845        fn thread_state_description_flavor7_64bit() {
846            let mut cr = make_test_cr();
847            cr.crashed_thread_number = 0;
848            // flavor 7, sub_flavor != 1 (64-bit), need 44 regs
849            let mut regs = vec![0u32; 44];
850            regs[0] = 4; // sub_flavor (64-bit)
851            regs[2] = 0xDEAD_BEEF; // rax low
852            regs[3] = 0x0000_0001; // rax high
853            cr.thread_state = ThreadState {
854                flavor: 7,
855                registers: regs,
856            };
857            let desc = cr.thread_state_description();
858            assert!(desc.contains("64-bit"));
859            assert!(desc.contains("rax: 0x00000001deadbeef"));
860        }
861
862        #[test]
863        fn thread_state_description_flavor1() {
864            let mut cr = make_test_cr();
865            cr.crashed_thread_number = 0;
866            let mut regs = vec![0u32; 16];
867            regs[0] = 0xBBBB_BBBB; // eax
868            cr.thread_state = ThreadState {
869                flavor: 1,
870                registers: regs,
871            };
872            let desc = cr.thread_state_description();
873            assert!(desc.contains("32-bit"));
874            assert!(desc.contains("eax: 0xbbbbbbbb"));
875        }
876
877        #[test]
878        fn thread_state_description_unknown_flavor() {
879            let mut cr = make_test_cr();
880            cr.crashed_thread_number = 0;
881            cr.thread_state = ThreadState {
882                flavor: 99,
883                registers: vec![],
884            };
885            let desc = cr.thread_state_description();
886            assert!(desc.contains("unknown flavor 99"));
887        }
888
889        #[test]
890        fn thread_state_description_arm64_flavor6() {
891            let mut cr = make_test_cr_arm64();
892            cr.crashed_thread_number = 0;
893            // ARM_THREAD_STATE64 (flavor 6): 68 u32s
894            // 33 registers * 2 + cpsr + pad = 68
895            let mut regs = vec![0u32; 68];
896            regs[0] = 0xCAFE_BABE; // x0 low
897            regs[1] = 0x0000_0001; // x0 high
898            regs[64] = 0xDEAD_0000; // pc low
899            regs[65] = 0x0000_FFFF; // pc high
900            regs[66] = 0x8000_0000; // cpsr
901            cr.thread_state = ThreadState {
902                flavor: 6,
903                registers: regs,
904            };
905            let desc = cr.thread_state_description();
906            assert!(desc.contains("ARM Thread State (64-bit)"));
907            assert!(desc.contains("x0:  0x00000001cafebabe"));
908            assert!(desc.contains("pc:  0x0000ffffdead0000"));
909            assert!(desc.contains("cpsr: 0x80000000"));
910        }
911
912        #[test]
913        fn thread_state_description_arm_thread_state_sub2() {
914            let mut cr = make_test_cr_arm64();
915            cr.crashed_thread_number = 0;
916            // ARM_THREAD_STATE (flavor 1), sub_flavor=2 (ARM64): 2 header + 68 = 70
917            let mut regs = vec![0u32; 70];
918            regs[0] = 2; // sub_flavor
919            regs[1] = 0; // padding
920            regs[2] = 0x1111_2222; // x0 low (offset 2)
921            regs[3] = 0x3333_4444; // x0 high
922            regs[68] = 0xAAAA_BBBB; // cpsr (offset 2 + 66)
923            cr.thread_state = ThreadState {
924                flavor: 1,
925                registers: regs,
926            };
927            let desc = cr.thread_state_description();
928            assert!(desc.contains("ARM Thread State (64-bit)"));
929            assert!(desc.contains("x0:  0x3333444411112222"));
930            assert!(desc.contains("cpsr: 0xaaaabbbb"));
931        }
932
933        #[test]
934        fn thread_state_description_arm32() {
935            let mut cr = make_test_cr();
936            cr.cpu_type = CpuType::ARM;
937            cr.crashed_thread_number = 0;
938            // ARM_THREAD_STATE (flavor 1), sub_flavor=1 (ARM32): 2 header + 17 = 19
939            let mut regs = vec![0u32; 19];
940            regs[0] = 1; // sub_flavor
941            regs[1] = 0; // padding
942            regs[2] = 0xDEAD_BEEF; // r0
943            regs[17] = 0x1234_5678; // pc
944            regs[18] = 0x6000_0010; // cpsr
945            cr.thread_state = ThreadState {
946                flavor: 1,
947                registers: regs,
948            };
949            let desc = cr.thread_state_description();
950            assert!(desc.contains("ARM Thread State (32-bit)"));
951            assert!(desc.contains("r0:  0xdeadbeef"));
952            assert!(desc.contains("pc:  0x12345678"));
953            assert!(desc.contains("cpsr: 0x60000010"));
954        }
955    }
956}