ips_file.rs

  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}