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