1use super::{ModuleInfo, StackFrame, common::deserialize_string_or_number};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct ProcessedCrash {
10 pub uuid: String,
11 #[serde(default)]
12 pub signature: Option<String>,
13 #[serde(default)]
14 pub product: Option<String>,
15 #[serde(default)]
16 pub version: Option<String>,
17 #[serde(default)]
18 pub os_name: Option<String>,
19 #[serde(default, deserialize_with = "deserialize_string_or_number")]
20 pub build: Option<String>,
21 #[serde(default)]
22 pub release_channel: Option<String>,
23 #[serde(default)]
24 pub os_version: Option<String>,
25
26 #[serde(default)]
27 pub crash_info: Option<CrashInfo>,
28 #[serde(default)]
29 pub moz_crash_reason: Option<String>,
30 #[serde(default)]
31 pub abort_message: Option<String>,
32
33 #[serde(default)]
34 pub android_model: Option<String>,
35 #[serde(default)]
36 pub android_version: Option<String>,
37
38 #[serde(default)]
39 pub crashing_thread: Option<usize>,
40 #[serde(default)]
41 pub threads: Option<Vec<Thread>>,
42 #[serde(default)]
43 pub json_dump: Option<serde_json::Value>,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct CrashInfo {
48 #[serde(rename = "type")]
49 pub crash_type: Option<String>,
50 pub address: Option<String>,
51 pub crashing_thread: Option<usize>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct Thread {
56 pub thread: Option<usize>,
57 pub thread_name: Option<String>,
58 pub frames: Vec<StackFrame>,
59}
60
61#[derive(Debug, Clone)]
62pub struct ThreadSummary {
63 pub thread_index: usize,
64 pub thread_name: Option<String>,
65 pub frames: Vec<StackFrame>,
66 pub is_crashing: bool,
67}
68
69#[derive(Debug)]
70pub struct CrashSummary {
71 pub crash_id: String,
72 pub signature: String,
73 pub reason: Option<String>,
74 pub address: Option<String>,
75 pub moz_crash_reason: Option<String>,
76 pub abort_message: Option<String>,
77
78 pub product: String,
79 pub version: String,
80 pub build_id: Option<String>,
81 pub release_channel: Option<String>,
82 pub platform: String,
83
84 pub android_version: Option<String>,
85 pub android_model: Option<String>,
86
87 pub crashing_thread_name: Option<String>,
88 pub frames: Vec<StackFrame>,
89 pub all_threads: Vec<ThreadSummary>,
90 pub modules: Vec<ModuleInfo>,
91}
92
93impl ProcessedCrash {
94 pub fn to_summary(&self, depth: usize, all_threads: bool) -> CrashSummary {
95 let crashing_thread_idx = self
96 .crashing_thread
97 .or_else(|| self.crash_info.as_ref().and_then(|ci| ci.crashing_thread))
98 .or_else(|| {
99 self.json_dump.as_ref().and_then(|jd| {
100 jd.get("crashing_thread")
101 .and_then(|v| v.as_u64())
102 .map(|v| v as usize)
103 })
104 });
105
106 let json_dump_threads: Option<Vec<Thread>> = self
107 .json_dump
108 .as_ref()
109 .and_then(|jd| jd.get("threads"))
110 .and_then(|t| serde_json::from_value(t.clone()).ok());
111
112 let threads_data = self.threads.as_ref().or(json_dump_threads.as_ref());
113
114 let (thread_name, frames, thread_summaries) = if let Some(threads) = threads_data {
115 let mut all_thread_summaries = Vec::new();
116
117 if all_threads {
118 for (idx, thread) in threads.iter().enumerate() {
119 let frames: Vec<StackFrame> =
120 thread.frames.iter().take(depth).cloned().collect();
121 all_thread_summaries.push(ThreadSummary {
122 thread_index: idx,
123 thread_name: thread.thread_name.clone(),
124 frames,
125 is_crashing: Some(idx) == crashing_thread_idx,
126 });
127 }
128 }
129
130 if let Some(idx) = crashing_thread_idx {
131 if let Some(thread) = threads.get(idx) {
132 let frames: Vec<StackFrame> =
133 thread.frames.iter().take(depth).cloned().collect();
134 (thread.thread_name.clone(), frames, all_thread_summaries)
135 } else {
136 (None, Vec::new(), all_thread_summaries)
137 }
138 } else {
139 (None, Vec::new(), all_thread_summaries)
140 }
141 } else {
142 (None, Vec::new(), Vec::new())
143 };
144
145 let modules: Vec<ModuleInfo> = self
146 .json_dump
147 .as_ref()
148 .and_then(|jd| jd.get("modules"))
149 .and_then(|m| serde_json::from_value(m.clone()).ok())
150 .unwrap_or_default();
151
152 let json_dump_crash_info: Option<CrashInfo> = self
153 .json_dump
154 .as_ref()
155 .and_then(|jd| jd.get("crash_info"))
156 .and_then(|ci| serde_json::from_value(ci.clone()).ok());
157
158 let crash_info = self.crash_info.as_ref().or(json_dump_crash_info.as_ref());
159
160 CrashSummary {
161 crash_id: self.uuid.clone(),
162 signature: self
163 .signature
164 .clone()
165 .unwrap_or_else(|| "Unknown".to_string()),
166 reason: crash_info.and_then(|ci| ci.crash_type.clone()),
167 address: crash_info.and_then(|ci| ci.address.clone()),
168 moz_crash_reason: self.moz_crash_reason.clone(),
169 abort_message: self.abort_message.clone(),
170 product: self
171 .product
172 .clone()
173 .unwrap_or_else(|| "Unknown".to_string()),
174 version: self
175 .version
176 .clone()
177 .unwrap_or_else(|| "Unknown".to_string()),
178 build_id: self.build.clone(),
179 release_channel: self.release_channel.clone(),
180 platform: format!(
181 "{}{}",
182 self.os_name.as_deref().unwrap_or("Unknown"),
183 self.os_version
184 .as_ref()
185 .map(|v| format!(" {}", v))
186 .unwrap_or_default()
187 ),
188 android_version: self.android_version.clone(),
189 android_model: self.android_model.clone(),
190 crashing_thread_name: thread_name,
191 frames,
192 all_threads: thread_summaries,
193 modules,
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn sample_crash_json() -> &'static str {
203 r#"{
204 "uuid": "247653e8-7a18-4836-97d1-42a720260120",
205 "signature": "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher",
206 "product": "Fenix",
207 "version": "147.0.1",
208 "os_name": "Android",
209 "os_version": "36",
210 "crashing_thread": 1,
211 "moz_crash_reason": "MOZ_RELEASE_ASSERT(mTimeStretcher->Init())",
212 "crash_info": {
213 "type": "SIGSEGV",
214 "address": "0x0",
215 "crashing_thread": 1
216 },
217 "json_dump": {
218 "modules": [
219 {
220 "filename": "xul.dll",
221 "debug_file": "xul.pdb",
222 "debug_id": "F51BCD2A59EB2A194C4C44205044422E1",
223 "code_id": "69934c4ba31f000",
224 "version": "148.0.0.3"
225 },
226 {
227 "filename": "ntdll.dll",
228 "debug_file": "ntdll.pdb",
229 "debug_id": "180BF1B90AA75697D0EFEA5E5630AC7E1",
230 "code_id": "7ec9c15d1f8000",
231 "version": "6.2.19041.6456"
232 },
233 {
234 "filename": "mozglue.dll",
235 "debug_file": "mozglue.pdb",
236 "debug_id": "AABBCCDD11223344",
237 "code_id": "abc123",
238 "version": "148.0"
239 }
240 ]
241 },
242 "threads": [
243 {
244 "thread": 0,
245 "thread_name": "MainThread",
246 "frames": [
247 {"frame": 0, "function": "main", "file": "main.cpp", "line": 10, "module": "xul.dll"}
248 ]
249 },
250 {
251 "thread": 1,
252 "thread_name": "GraphRunner",
253 "frames": [
254 {"frame": 0, "function": "EnsureTimeStretcher", "file": "AudioDecoderInputTrack.cpp", "line": 624, "module": "xul.dll"},
255 {"frame": 1, "function": "AppendData", "file": "AudioDecoderInputTrack.cpp", "line": 423, "module": "ntdll.dll"}
256 ]
257 }
258 ]
259 }"#
260 }
261
262 #[test]
263 fn test_deserialize_processed_crash() {
264 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
265 assert_eq!(crash.uuid, "247653e8-7a18-4836-97d1-42a720260120");
266 assert_eq!(
267 crash.signature,
268 Some("mozilla::AudioDecoderInputTrack::EnsureTimeStretcher".to_string())
269 );
270 assert_eq!(crash.product, Some("Fenix".to_string()));
271 assert_eq!(crash.version, Some("147.0.1".to_string()));
272 assert_eq!(crash.crashing_thread, Some(1));
273 }
274
275 #[test]
276 fn test_to_summary_basic() {
277 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
278 let summary = crash.to_summary(10, false);
279
280 assert_eq!(summary.crash_id, "247653e8-7a18-4836-97d1-42a720260120");
281 assert_eq!(
282 summary.signature,
283 "mozilla::AudioDecoderInputTrack::EnsureTimeStretcher"
284 );
285 assert_eq!(summary.product, "Fenix");
286 assert_eq!(summary.version, "147.0.1");
287 assert_eq!(summary.reason, Some("SIGSEGV".to_string()));
288 assert_eq!(summary.address, Some("0x0".to_string()));
289 assert_eq!(
290 summary.moz_crash_reason,
291 Some("MOZ_RELEASE_ASSERT(mTimeStretcher->Init())".to_string())
292 );
293 }
294
295 #[test]
296 fn test_to_summary_crashing_thread_frames() {
297 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
298 let summary = crash.to_summary(10, false);
299
300 assert_eq!(
301 summary.crashing_thread_name,
302 Some("GraphRunner".to_string())
303 );
304 assert_eq!(summary.frames.len(), 2);
305 assert_eq!(
306 summary.frames[0].function,
307 Some("EnsureTimeStretcher".to_string())
308 );
309 }
310
311 #[test]
312 fn test_to_summary_depth_limit() {
313 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
314 let summary = crash.to_summary(1, false);
315
316 assert_eq!(summary.frames.len(), 1);
317 assert_eq!(
318 summary.frames[0].function,
319 Some("EnsureTimeStretcher".to_string())
320 );
321 }
322
323 #[test]
324 fn test_to_summary_all_threads() {
325 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
326 let summary = crash.to_summary(10, true);
327
328 assert_eq!(summary.all_threads.len(), 2);
329 assert!(!summary.all_threads[0].is_crashing);
330 assert!(summary.all_threads[1].is_crashing);
331 assert_eq!(
332 summary.all_threads[0].thread_name,
333 Some("MainThread".to_string())
334 );
335 assert_eq!(
336 summary.all_threads[1].thread_name,
337 Some("GraphRunner".to_string())
338 );
339 }
340
341 #[test]
342 fn test_crashing_thread_from_crash_info() {
343 let json = r#"{
345 "uuid": "test-crash",
346 "crash_info": {
347 "type": "SIGSEGV",
348 "crashing_thread": 0
349 },
350 "threads": [
351 {"thread": 0, "thread_name": "Main", "frames": [{"frame": 0, "function": "foo"}]}
352 ]
353 }"#;
354 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
355 let summary = crash.to_summary(10, false);
356
357 assert_eq!(summary.crashing_thread_name, Some("Main".to_string()));
358 }
359
360 #[test]
361 fn test_crashing_thread_from_json_dump() {
362 let json = r#"{
364 "uuid": "test-crash",
365 "json_dump": {
366 "crashing_thread": 0,
367 "threads": [
368 {"thread": 0, "thread_name": "DumpThread", "frames": [{"frame": 0, "function": "bar"}]}
369 ]
370 }
371 }"#;
372 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
373 let summary = crash.to_summary(10, false);
374
375 assert_eq!(summary.crashing_thread_name, Some("DumpThread".to_string()));
376 }
377
378 #[test]
379 fn test_missing_optional_fields() {
380 let json = r#"{"uuid": "minimal-crash"}"#;
381 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
382 let summary = crash.to_summary(10, false);
383
384 assert_eq!(summary.crash_id, "minimal-crash");
385 assert_eq!(summary.signature, "Unknown");
386 assert_eq!(summary.product, "Unknown");
387 assert!(summary.frames.is_empty());
388 assert!(summary.modules.is_empty());
389 }
390
391 #[test]
392 fn test_to_summary_extracts_modules() {
393 let crash: ProcessedCrash = serde_json::from_str(sample_crash_json()).unwrap();
394 let summary = crash.to_summary(10, false);
395
396 assert_eq!(summary.modules.len(), 3);
397 assert_eq!(summary.modules[0].filename, "xul.dll");
398 assert_eq!(summary.modules[0].debug_file, Some("xul.pdb".to_string()));
399 assert_eq!(
400 summary.modules[0].debug_id,
401 Some("F51BCD2A59EB2A194C4C44205044422E1".to_string())
402 );
403 assert_eq!(
404 summary.modules[0].code_id,
405 Some("69934c4ba31f000".to_string())
406 );
407 assert_eq!(summary.modules[0].version, Some("148.0.0.3".to_string()));
408 }
409
410 #[test]
411 fn test_to_summary_modules_missing_json_dump() {
412 let json = r#"{
413 "uuid": "no-json-dump",
414 "threads": [
415 {"thread": 0, "frames": [{"frame": 0, "function": "foo"}]}
416 ]
417 }"#;
418 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
419 let summary = crash.to_summary(10, false);
420
421 assert!(summary.modules.is_empty());
422 }
423
424 #[test]
425 fn test_to_summary_modules_missing_modules_key() {
426 let json = r#"{
427 "uuid": "no-modules",
428 "json_dump": {
429 "crashing_thread": 0,
430 "threads": [
431 {"thread": 0, "frames": [{"frame": 0, "function": "foo"}]}
432 ]
433 }
434 }"#;
435 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
436 let summary = crash.to_summary(10, false);
437
438 assert!(summary.modules.is_empty());
439 }
440
441 #[test]
442 fn test_to_summary_modules_optional_fields() {
443 let json = r#"{
444 "uuid": "partial-modules",
445 "json_dump": {
446 "modules": [
447 {"filename": "bare.dll"}
448 ]
449 }
450 }"#;
451 let crash: ProcessedCrash = serde_json::from_str(json).unwrap();
452 let summary = crash.to_summary(10, false);
453
454 assert_eq!(summary.modules.len(), 1);
455 assert_eq!(summary.modules[0].filename, "bare.dll");
456 assert!(summary.modules[0].debug_file.is_none());
457 assert!(summary.modules[0].debug_id.is_none());
458 assert!(summary.modules[0].code_id.is_none());
459 assert!(summary.modules[0].version.is_none());
460 }
461}