ips_file.rs

  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}