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