1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Deserialize, Serialize)]
10pub struct IndexedStrings {
11 pub strings: Vec<String>,
12 pub values: Vec<u32>,
13}
14
15#[derive(Debug, Deserialize, Serialize)]
16pub struct NullableIndexedStrings {
17 pub strings: Vec<Option<String>>,
18 pub values: Vec<u32>,
19}
20
21impl IndexedStrings {
22 pub fn get(&self, i: usize) -> &str {
23 &self.strings[self.values[i] as usize]
24 }
25}
26
27impl NullableIndexedStrings {
28 pub fn get(&self, i: usize) -> Option<&str> {
29 self.strings[self.values[i] as usize].as_deref()
30 }
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct CrashPingsResponse {
35 pub channel: IndexedStrings,
36 pub process: IndexedStrings,
37 pub ipc_actor: NullableIndexedStrings,
38 pub clientid: IndexedStrings,
39 pub crashid: Vec<String>,
40 pub version: IndexedStrings,
41 pub os: IndexedStrings,
42 pub osversion: IndexedStrings,
43 pub arch: IndexedStrings,
44 pub date: IndexedStrings,
45 pub reason: NullableIndexedStrings,
46 #[serde(rename = "type")]
47 pub crash_type: NullableIndexedStrings,
48 pub minidump_sha256_hash: Vec<Option<String>>,
49 pub startup_crash: Vec<Option<bool>>,
50 pub build_id: IndexedStrings,
51 pub signature: IndexedStrings,
52}
53
54impl CrashPingsResponse {
55 pub fn len(&self) -> usize {
56 self.crashid.len()
57 }
58
59 pub fn is_empty(&self) -> bool {
60 self.crashid.is_empty()
61 }
62
63 pub fn signature(&self, i: usize) -> &str {
64 self.signature.get(i)
65 }
66
67 pub fn channel(&self, i: usize) -> &str {
68 self.channel.get(i)
69 }
70
71 pub fn os(&self, i: usize) -> &str {
72 self.os.get(i)
73 }
74
75 pub fn process(&self, i: usize) -> &str {
76 self.process.get(i)
77 }
78
79 pub fn version(&self, i: usize) -> &str {
80 self.version.get(i)
81 }
82
83 pub fn arch(&self, i: usize) -> &str {
84 self.arch.get(i)
85 }
86
87 pub fn matches_filters(&self, i: usize, filters: &CrashPingFilters) -> bool {
88 if let Some(ref ch) = filters.channel
89 && !self.channel(i).eq_ignore_ascii_case(ch)
90 {
91 return false;
92 }
93 if let Some(ref os) = filters.os
94 && !self.os(i).eq_ignore_ascii_case(os)
95 {
96 return false;
97 }
98 if let Some(ref proc) = filters.process
99 && !self.process(i).eq_ignore_ascii_case(proc)
100 {
101 return false;
102 }
103 if let Some(ref ver) = filters.version
104 && self.version(i) != ver
105 {
106 return false;
107 }
108 if let Some(ref sig) = filters.signature {
109 let ping_sig = self.signature(i);
110 if let Some(pattern) = sig.strip_prefix('~') {
111 if !ping_sig.to_lowercase().contains(&pattern.to_lowercase()) {
112 return false;
113 }
114 } else if ping_sig != sig {
115 return false;
116 }
117 }
118 if let Some(ref arch) = filters.arch
119 && !self.arch(i).eq_ignore_ascii_case(arch)
120 {
121 return false;
122 }
123 true
124 }
125
126 pub fn facet_value(&self, i: usize, facet: &str) -> String {
127 match facet {
128 "signature" => self.signature(i).to_string(),
129 "channel" => self.channel(i).to_string(),
130 "os" => self.os(i).to_string(),
131 "process" => self.process(i).to_string(),
132 "version" => self.version(i).to_string(),
133 "arch" => self.arch(i).to_string(),
134 "osversion" => self.osversion.get(i).to_string(),
135 "build_id" => self.build_id.get(i).to_string(),
136 "ipc_actor" => self.ipc_actor.get(i).unwrap_or("(none)").to_string(),
137 "reason" => self.reason.get(i).unwrap_or("(none)").to_string(),
138 "type" => self.crash_type.get(i).unwrap_or("(none)").to_string(),
139 _ => "(unknown facet)".to_string(),
140 }
141 }
142}
143
144#[derive(Debug, Serialize, Deserialize)]
147pub struct CrashPingStackResponse {
148 pub stack: Option<Vec<CrashPingFrame>>,
149 pub java_exception: Option<serde_json::Value>,
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct CrashPingFrame {
154 pub function: Option<String>,
155 pub function_offset: Option<String>,
156 pub file: Option<String>,
157 pub line: Option<u32>,
158 pub module: Option<String>,
159 pub module_offset: Option<String>,
160 pub offset: Option<String>,
161 #[serde(default)]
162 pub omitted: Option<serde_json::Value>,
163 #[serde(default)]
164 pub error: Option<String>,
165}
166
167#[derive(Debug, Default)]
170pub struct CrashPingFilters {
171 pub channel: Option<String>,
172 pub os: Option<String>,
173 pub process: Option<String>,
174 pub version: Option<String>,
175 pub signature: Option<String>,
176 pub arch: Option<String>,
177}
178
179#[derive(Debug, Serialize)]
182pub struct CrashPingsSummary {
183 pub date_from: String,
184 pub date_to: String,
185 pub total: usize,
186 pub filtered_total: usize,
187 pub signature_filter: Option<String>,
188 pub facet_name: String,
189 pub items: Vec<CrashPingsItem>,
190}
191
192#[derive(Debug, Serialize)]
193pub struct CrashPingsItem {
194 pub label: String,
195 pub count: usize,
196 pub percentage: f64,
197 pub example_ids: Vec<String>,
198}
199
200#[derive(Debug, Serialize)]
201pub struct CrashPingStackSummary {
202 pub crash_id: String,
203 pub date: String,
204 pub frames: Vec<CrashPingFrame>,
205 pub java_exception: Option<serde_json::Value>,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use serde_json::json;
212
213 fn sample_response_json() -> serde_json::Value {
214 json!({
215 "channel": {
216 "strings": ["release", "beta", "nightly"],
217 "values": [0, 0, 1, 2]
218 },
219 "process": {
220 "strings": ["main", "content", "gpu"],
221 "values": [0, 1, 0, 2]
222 },
223 "ipc_actor": {
224 "strings": [null, "windows-file-dialog"],
225 "values": [0, 1, 0, 0]
226 },
227 "clientid": {
228 "strings": ["client1", "client2", "client3", "client4"],
229 "values": [0, 1, 2, 3]
230 },
231 "crashid": ["crash-1", "crash-2", "crash-3", "crash-4"],
232 "version": {
233 "strings": ["147.0", "148.0"],
234 "values": [0, 0, 1, 1]
235 },
236 "os": {
237 "strings": ["Windows", "Linux", "Mac"],
238 "values": [0, 0, 1, 2]
239 },
240 "osversion": {
241 "strings": ["10.0.19045", "6.1", "15.0"],
242 "values": [0, 0, 1, 2]
243 },
244 "arch": {
245 "strings": ["x86_64", "aarch64"],
246 "values": [0, 0, 0, 1]
247 },
248 "date": {
249 "strings": ["2026-02-12"],
250 "values": [0, 0, 0, 0]
251 },
252 "reason": {
253 "strings": [null, "OOM"],
254 "values": [0, 1, 0, 1]
255 },
256 "type": {
257 "strings": [null, "SIGSEGV"],
258 "values": [0, 1, 0, 0]
259 },
260 "minidump_sha256_hash": ["hash1", null, "hash3", null],
261 "startup_crash": [false, true, false, false],
262 "build_id": {
263 "strings": ["20260210103000", "20260211103000"],
264 "values": [0, 0, 1, 1]
265 },
266 "signature": {
267 "strings": ["OOM | small", "setup_stack_prot", "js::gc::SomeFunc"],
268 "values": [0, 0, 1, 2]
269 }
270 })
271 }
272
273 #[test]
274 fn test_deserialize_response() {
275 let data = sample_response_json();
276 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
277 assert_eq!(resp.len(), 4);
278 assert_eq!(resp.crashid.len(), 4);
279 }
280
281 #[test]
282 fn test_indexed_strings_get() {
283 let data = sample_response_json();
284 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
285 assert_eq!(resp.signature(0), "OOM | small");
286 assert_eq!(resp.signature(1), "OOM | small");
287 assert_eq!(resp.signature(2), "setup_stack_prot");
288 assert_eq!(resp.signature(3), "js::gc::SomeFunc");
289 }
290
291 #[test]
292 fn test_nullable_indexed_strings() {
293 let data = sample_response_json();
294 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
295 assert_eq!(resp.ipc_actor.get(0), None);
296 assert_eq!(resp.ipc_actor.get(1), Some("windows-file-dialog"));
297 }
298
299 #[test]
300 fn test_channel_accessor() {
301 let data = sample_response_json();
302 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
303 assert_eq!(resp.channel(0), "release");
304 assert_eq!(resp.channel(2), "beta");
305 assert_eq!(resp.channel(3), "nightly");
306 }
307
308 #[test]
309 fn test_os_accessor() {
310 let data = sample_response_json();
311 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
312 assert_eq!(resp.os(0), "Windows");
313 assert_eq!(resp.os(2), "Linux");
314 assert_eq!(resp.os(3), "Mac");
315 }
316
317 #[test]
318 fn test_filter_no_filters() {
319 let data = sample_response_json();
320 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
321 let filters = CrashPingFilters::default();
322 for i in 0..resp.len() {
323 assert!(resp.matches_filters(i, &filters));
324 }
325 }
326
327 #[test]
328 fn test_filter_by_channel() {
329 let data = sample_response_json();
330 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
331 let filters = CrashPingFilters {
332 channel: Some("release".to_string()),
333 ..Default::default()
334 };
335 assert!(resp.matches_filters(0, &filters));
336 assert!(resp.matches_filters(1, &filters));
337 assert!(!resp.matches_filters(2, &filters));
338 assert!(!resp.matches_filters(3, &filters));
339 }
340
341 #[test]
342 fn test_filter_by_os() {
343 let data = sample_response_json();
344 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
345 let filters = CrashPingFilters {
346 os: Some("Linux".to_string()),
347 ..Default::default()
348 };
349 assert!(!resp.matches_filters(0, &filters));
350 assert!(resp.matches_filters(2, &filters));
351 }
352
353 #[test]
354 fn test_filter_by_signature_exact() {
355 let data = sample_response_json();
356 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
357 let filters = CrashPingFilters {
358 signature: Some("OOM | small".to_string()),
359 ..Default::default()
360 };
361 assert!(resp.matches_filters(0, &filters));
362 assert!(resp.matches_filters(1, &filters));
363 assert!(!resp.matches_filters(2, &filters));
364 }
365
366 #[test]
367 fn test_filter_by_signature_contains() {
368 let data = sample_response_json();
369 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
370 let filters = CrashPingFilters {
371 signature: Some("~oom".to_string()),
372 ..Default::default()
373 };
374 assert!(resp.matches_filters(0, &filters));
375 assert!(!resp.matches_filters(2, &filters));
376 }
377
378 #[test]
379 fn test_filter_combined() {
380 let data = sample_response_json();
381 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
382 let filters = CrashPingFilters {
383 channel: Some("release".to_string()),
384 os: Some("Windows".to_string()),
385 ..Default::default()
386 };
387 assert!(resp.matches_filters(0, &filters));
388 assert!(resp.matches_filters(1, &filters));
389 assert!(!resp.matches_filters(2, &filters)); assert!(!resp.matches_filters(3, &filters)); }
392
393 #[test]
394 fn test_filter_case_insensitive() {
395 let data = sample_response_json();
396 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
397 let filters = CrashPingFilters {
398 os: Some("windows".to_string()),
399 ..Default::default()
400 };
401 assert!(resp.matches_filters(0, &filters));
402 }
403
404 #[test]
405 fn test_facet_value() {
406 let data = sample_response_json();
407 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
408 assert_eq!(resp.facet_value(0, "signature"), "OOM | small");
409 assert_eq!(resp.facet_value(0, "os"), "Windows");
410 assert_eq!(resp.facet_value(0, "channel"), "release");
411 assert_eq!(resp.facet_value(1, "ipc_actor"), "windows-file-dialog");
412 assert_eq!(resp.facet_value(0, "ipc_actor"), "(none)");
413 }
414
415 #[test]
416 fn test_deserialize_stack_response() {
417 let data = json!({
418 "stack": [
419 {
420 "function": "KiRaiseUserExceptionDispatcher",
421 "function_offset": "0x000000000000003a",
422 "file": null,
423 "line": null,
424 "module": "ntdll.dll",
425 "module_offset": "0x00000000000a14fa",
426 "omitted": null,
427 "error": null,
428 "offset": "0x00007ffbeeef14fa"
429 },
430 {
431 "function": "mozilla::SomeFunc",
432 "function_offset": null,
433 "file": "SomeFile.cpp",
434 "line": 42,
435 "module": "xul.dll",
436 "module_offset": "0x1234",
437 "omitted": null,
438 "error": null,
439 "offset": "0x1234"
440 }
441 ],
442 "java_exception": null
443 });
444 let resp: CrashPingStackResponse = serde_json::from_value(data).unwrap();
445 let stack = resp.stack.unwrap();
446 assert_eq!(stack.len(), 2);
447 assert_eq!(
448 stack[0].function.as_deref(),
449 Some("KiRaiseUserExceptionDispatcher")
450 );
451 assert_eq!(stack[0].module.as_deref(), Some("ntdll.dll"));
452 assert!(stack[0].file.is_none());
453 assert_eq!(stack[1].file.as_deref(), Some("SomeFile.cpp"));
454 assert_eq!(stack[1].line, Some(42));
455 }
456
457 #[test]
458 fn test_deserialize_stack_response_null_stack() {
459 let data = json!({
460 "stack": null,
461 "java_exception": null
462 });
463 let resp: CrashPingStackResponse = serde_json::from_value(data).unwrap();
464 assert!(resp.stack.is_none());
465 }
466
467 #[test]
468 fn test_deserialize_stack_response_with_java_exception() {
469 let data = json!({
470 "stack": null,
471 "java_exception": {"message": "OutOfMemoryError", "frames": []}
472 });
473 let resp: CrashPingStackResponse = serde_json::from_value(data).unwrap();
474 assert!(resp.java_exception.is_some());
475 }
476
477 #[test]
478 fn test_crash_pings_summary() {
479 let summary = CrashPingsSummary {
480 date_from: "2026-02-12".to_string(),
481 date_to: "2026-02-12".to_string(),
482 total: 88808,
483 filtered_total: 4523,
484 signature_filter: Some("OOM | small".to_string()),
485 facet_name: "os".to_string(),
486 items: vec![
487 CrashPingsItem {
488 label: "Windows".to_string(),
489 count: 3900,
490 percentage: 86.24,
491 example_ids: vec!["id1".to_string(), "id2".to_string()],
492 },
493 CrashPingsItem {
494 label: "Linux".to_string(),
495 count: 400,
496 percentage: 8.85,
497 example_ids: vec!["id3".to_string()],
498 },
499 ],
500 };
501 assert_eq!(summary.items.len(), 2);
502 assert_eq!(summary.items[0].label, "Windows");
503 }
504
505 #[test]
506 fn test_filter_by_version() {
507 let data = sample_response_json();
508 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
509 let filters = CrashPingFilters {
510 version: Some("148.0".to_string()),
511 ..Default::default()
512 };
513 assert!(!resp.matches_filters(0, &filters));
514 assert!(resp.matches_filters(2, &filters));
515 assert!(resp.matches_filters(3, &filters));
516 }
517
518 #[test]
519 fn test_filter_by_arch() {
520 let data = sample_response_json();
521 let resp: CrashPingsResponse = serde_json::from_value(data).unwrap();
522 let filters = CrashPingFilters {
523 arch: Some("aarch64".to_string()),
524 ..Default::default()
525 };
526 assert!(!resp.matches_filters(0, &filters));
527 assert!(resp.matches_filters(3, &filters));
528 }
529}