ips_file.rs

  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}