1use anyhow::Context as _;
2use collections::HashMap;
3
4use semantic_version::SemanticVersion;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug)]
9pub struct IpsFile {
10 pub header: Header,
11 pub body: Body,
12}
13
14impl IpsFile {
15 pub fn parse(bytes: &[u8]) -> anyhow::Result<IpsFile> {
16 let mut split = bytes.splitn(2, |&b| b == b'\n');
17 let header_bytes = split.next().context("No header found")?;
18 let header: Header = serde_json::from_slice(header_bytes).context("parsing header")?;
19
20 let body_bytes = split.next().context("No body found")?;
21
22 let body: Body = serde_json::from_slice(body_bytes).context("parsing body")?;
23 Ok(IpsFile { header, body })
24 }
25
26 pub fn faulting_thread(&self) -> Option<&Thread> {
27 self.body.threads.get(self.body.faulting_thread? as usize)
28 }
29
30 pub fn app_version(&self) -> Option<SemanticVersion> {
31 self.header.app_version.parse().ok()
32 }
33
34 pub fn timestamp(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
35 chrono::DateTime::parse_from_str(&self.header.timestamp, "%Y-%m-%d %H:%M:%S%.f %#z")
36 .map_err(|e| anyhow::anyhow!(e))
37 }
38
39 pub fn description(&self, panic: Option<&str>) -> String {
40 let mut desc = if self.body.termination.indicator == "Abort trap: 6" {
41 match panic {
42 Some(panic_message) => format!("Panic `{}`", panic_message),
43 None => "Crash `Abort trap: 6` (possible panic)".into(),
44 }
45 } else if let Some(msg) = &self.body.exception.message {
46 format!("Exception `{}`", msg)
47 } else {
48 format!("Crash `{}`", self.body.termination.indicator)
49 };
50 if let Some(thread) = self.faulting_thread() {
51 if let Some(queue) = thread.queue.as_ref() {
52 desc += &format!(
53 " on thread {} ({})",
54 self.body.faulting_thread.unwrap_or_default(),
55 queue
56 );
57 } else {
58 desc += &format!(
59 " on thread {} ({})",
60 self.body.faulting_thread.unwrap_or_default(),
61 thread.name.clone().unwrap_or_default()
62 );
63 }
64 }
65 desc
66 }
67
68 pub fn backtrace_summary(&self) -> String {
69 if let Some(thread) = self.faulting_thread() {
70 let mut frames = thread
71 .frames
72 .iter()
73 .filter_map(|frame| {
74 if let Some(name) = &frame.symbol {
75 if self.is_ignorable_frame(name) {
76 return None;
77 }
78 Some(format!("{:#}", rustc_demangle::demangle(name)))
79 } else if let Some(image) = self.body.used_images.get(frame.image_index) {
80 Some(image.name.clone().unwrap_or("<unknown-image>".into()))
81 } else {
82 Some("<unknown>".into())
83 }
84 })
85 .collect::<Vec<_>>();
86
87 let total = frames.len();
88 if total > 21 {
89 frames = frames.into_iter().take(20).collect();
90 frames.push(format!(" and {} more...", total - 20))
91 }
92 frames.join("\n")
93 } else {
94 "<no backtrace available>".into()
95 }
96 }
97
98 fn is_ignorable_frame(&self, symbol: &String) -> bool {
99 [
100 "pthread_kill",
101 "panic",
102 "backtrace",
103 "rust_begin_unwind",
104 "abort",
105 ]
106 .iter()
107 .any(|s| symbol.contains(s))
108 }
109}
110
111#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
112#[serde(default)]
113pub struct Header {
114 pub app_name: String,
115 pub timestamp: String,
116 pub app_version: String,
117 pub slice_uuid: String,
118 pub build_version: String,
119 pub platform: i64,
120 #[serde(rename = "bundleID", default)]
121 pub bundle_id: String,
122 pub share_with_app_devs: i64,
123 pub is_first_party: i64,
124 pub bug_type: String,
125 pub os_version: String,
126 pub roots_installed: i64,
127 pub name: String,
128 pub incident_id: String,
129}
130#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase", default)]
132pub struct Body {
133 pub uptime: i64,
134 pub proc_role: String,
135 pub version: i64,
136 #[serde(rename = "userID")]
137 pub user_id: i64,
138 pub deploy_version: i64,
139 pub model_code: String,
140 #[serde(rename = "coalitionID")]
141 pub coalition_id: i64,
142 pub os_version: OsVersion,
143 pub capture_time: String,
144 pub code_signing_monitor: i64,
145 pub incident: String,
146 pub pid: i64,
147 pub translated: bool,
148 pub cpu_type: String,
149 #[serde(rename = "roots_installed")]
150 pub roots_installed: i64,
151 #[serde(rename = "bug_type")]
152 pub bug_type: String,
153 pub proc_launch: String,
154 pub proc_start_abs_time: i64,
155 pub proc_exit_abs_time: i64,
156 pub proc_name: String,
157 pub proc_path: String,
158 pub bundle_info: BundleInfo,
159 pub store_info: StoreInfo,
160 pub parent_proc: String,
161 pub parent_pid: i64,
162 pub coalition_name: String,
163 pub crash_reporter_key: String,
164 #[serde(rename = "codeSigningID")]
165 pub code_signing_id: String,
166 #[serde(rename = "codeSigningTeamID")]
167 pub code_signing_team_id: String,
168 pub code_signing_flags: i64,
169 pub code_signing_validation_category: i64,
170 pub code_signing_trust_level: i64,
171 pub instruction_byte_stream: InstructionByteStream,
172 pub sip: String,
173 pub exception: Exception,
174 pub termination: Termination,
175 pub asi: Asi,
176 pub ext_mods: ExtMods,
177 pub faulting_thread: Option<i64>,
178 pub threads: Vec<Thread>,
179 pub used_images: Vec<UsedImage>,
180 pub shared_cache: SharedCache,
181 pub vm_summary: String,
182 pub legacy_info: LegacyInfo,
183 pub log_writing_signature: String,
184 pub trial_info: TrialInfo,
185}
186
187#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", default)]
189pub struct OsVersion {
190 pub train: String,
191 pub build: String,
192 pub release_type: String,
193}
194
195#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", default)]
197pub struct BundleInfo {
198 #[serde(rename = "CFBundleShortVersionString")]
199 pub cfbundle_short_version_string: String,
200 #[serde(rename = "CFBundleVersion")]
201 pub cfbundle_version: String,
202 #[serde(rename = "CFBundleIdentifier")]
203 pub cfbundle_identifier: String,
204}
205
206#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", default)]
208pub struct StoreInfo {
209 pub device_identifier_for_vendor: String,
210 pub third_party: bool,
211}
212
213#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", default)]
215pub struct InstructionByteStream {
216 #[serde(rename = "beforePC")]
217 pub before_pc: String,
218 #[serde(rename = "atPC")]
219 pub at_pc: String,
220}
221
222#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", default)]
224pub struct Exception {
225 pub codes: String,
226 pub raw_codes: Vec<i64>,
227 #[serde(rename = "type")]
228 pub type_field: String,
229 pub subtype: Option<String>,
230 pub signal: String,
231 pub port: Option<i64>,
232 pub guard_id: Option<i64>,
233 pub message: Option<String>,
234}
235
236#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase", default)]
238pub struct Termination {
239 pub flags: i64,
240 pub code: i64,
241 pub namespace: String,
242 pub indicator: String,
243 pub by_proc: String,
244 pub by_pid: i64,
245}
246
247#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase", default)]
249pub struct Asi {
250 #[serde(rename = "libsystem_c.dylib")]
251 pub libsystem_c_dylib: Vec<String>,
252}
253
254#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase", default)]
256pub struct ExtMods {
257 pub caller: ExtMod,
258 pub system: ExtMod,
259 pub targeted: ExtMod,
260 pub warnings: i64,
261}
262
263#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase", default)]
265pub struct ExtMod {
266 #[serde(rename = "thread_create")]
267 pub thread_create: i64,
268 #[serde(rename = "thread_set_state")]
269 pub thread_set_state: i64,
270 #[serde(rename = "task_for_pid")]
271 pub task_for_pid: i64,
272}
273
274#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase", default)]
276pub struct Thread {
277 pub thread_state: HashMap<String, Value>,
278 pub id: i64,
279 pub triggered: Option<bool>,
280 pub name: Option<String>,
281 pub queue: Option<String>,
282 pub frames: Vec<Frame>,
283}
284
285#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase", default)]
287pub struct Frame {
288 pub image_offset: i64,
289 pub symbol: Option<String>,
290 pub symbol_location: Option<i64>,
291 pub image_index: usize,
292}
293
294#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase", default)]
296pub struct UsedImage {
297 pub source: String,
298 pub arch: Option<String>,
299 pub base: i64,
300 #[serde(rename = "CFBundleShortVersionString")]
301 pub cfbundle_short_version_string: Option<String>,
302 #[serde(rename = "CFBundleIdentifier")]
303 pub cfbundle_identifier: Option<String>,
304 pub size: i64,
305 pub uuid: String,
306 pub path: Option<String>,
307 pub name: Option<String>,
308 #[serde(rename = "CFBundleVersion")]
309 pub cfbundle_version: Option<String>,
310}
311
312#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase", default)]
314pub struct SharedCache {
315 pub base: i64,
316 pub size: i64,
317 pub uuid: String,
318}
319
320#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase", default)]
322pub struct LegacyInfo {
323 pub thread_triggered: ThreadTriggered,
324}
325
326#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase", default)]
328pub struct ThreadTriggered {
329 pub name: String,
330 pub queue: String,
331}
332
333#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase", default)]
335pub struct TrialInfo {
336 pub rollouts: Vec<Rollout>,
337 pub experiments: Vec<Value>,
338}
339
340#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase", default)]
342pub struct Rollout {
343 pub rollout_id: String,
344 pub factor_pack_ids: HashMap<String, Value>,
345 pub deployment_id: i64,
346}