1#![warn(missing_docs)]
3
4use std::fmt;
5use std::io::Read;
6use std::iter::FusedIterator;
7use std::ops::Deref;
8
9use bytes::Bytes;
10use flate2::bufread::ZlibDecoder;
11use scroll::{ctx::TryFromCtx, Endian, Pread};
12
13use crate::context::Unreal4Context;
14use crate::error::{Unreal4Error, Unreal4ErrorKind};
15use crate::logs::Unreal4LogEntry;
16
17#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
18struct AnsiString(String);
19
20impl AnsiString {
21 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24}
25
26impl AsRef<str> for AnsiString {
27 fn as_ref(&self) -> &str {
28 &self.0
29 }
30}
31
32impl Deref for AnsiString {
33 type Target = str;
34
35 fn deref(&self) -> &Self::Target {
36 &self.0
37 }
38}
39
40impl fmt::Display for AnsiString {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 self.0.fmt(f)
43 }
44}
45
46impl TryFromCtx<'_, Endian> for AnsiString {
47 type Error = scroll::Error;
48
49 fn try_from_ctx(data: &[u8], context: Endian) -> Result<(Self, usize), Self::Error> {
50 let mut offset = 0;
51
52 let len = data.gread_with::<u32>(&mut offset, context)?;
54 let bytes = data.gread_with::<&[u8]>(&mut offset, len as usize)?;
55
56 let mut string = String::from_utf8_lossy(bytes).into_owned();
58 let actual_len = string.trim_end_matches('\0').len();
59 string.truncate(actual_len);
60
61 Ok((Self(string), offset))
62 }
63}
64
65#[allow(dead_code)]
66#[derive(Clone, Debug, Pread)]
67struct Unreal4Header {
68 pub directory_name: AnsiString,
69 pub file_name: AnsiString,
70 pub uncompressed_size: i32,
71 pub file_count: i32,
72}
73
74#[derive(Clone, Debug)]
76struct Unreal4FileMeta {
77 index: usize,
79 file_name: AnsiString,
81 offset: usize,
83 len: usize,
85}
86
87impl TryFromCtx<'_, usize> for Unreal4FileMeta {
88 type Error = scroll::Error;
89
90 fn try_from_ctx(data: &[u8], file_offset: usize) -> Result<(Self, usize), Self::Error> {
91 let mut offset = 0;
92
93 let index = data.gread_with::<i32>(&mut offset, scroll::LE)? as usize;
94 let file_name = data.gread_with(&mut offset, scroll::LE)?;
95 let len = data.gread_with::<i32>(&mut offset, scroll::LE)? as usize;
96
97 let file_meta = Unreal4FileMeta {
98 index,
99 file_name,
100 offset: file_offset + offset,
101 len,
102 };
103
104 if len > 0 {
106 data.gread_with::<&[u8]>(&mut offset, len)?;
107 }
108
109 Ok((file_meta, offset))
110 }
111}
112
113fn gread_files(
114 bytes: &[u8],
115 count: usize,
116 offset: &mut usize,
117) -> Result<Vec<Unreal4FileMeta>, Unreal4Error> {
118 if count > bytes.len() / 12 {
120 return Err(Unreal4ErrorKind::BadData.into());
121 }
122 let mut files = Vec::with_capacity(count);
123 for _ in 0..count {
124 let file_offset = *offset;
125 files.push(bytes.gread_with(offset, file_offset)?);
126 }
127 Ok(files)
128}
129
130#[derive(Debug)]
132pub struct Unreal4Crash {
133 bytes: Bytes,
134 header: Unreal4Header,
135 files: Vec<Unreal4FileMeta>,
136}
137
138impl Unreal4Crash {
139 fn from_bytes(bytes: Bytes) -> Result<Self, Unreal4Error> {
140 let mut offset = 0;
141
142 let (header, files) = if bytes.starts_with(b"CR1") {
143 offset = 3;
147
148 let header = bytes.gread_with::<Unreal4Header>(&mut offset, scroll::LE)?;
149 let files = gread_files(&bytes, header.file_count as usize, &mut offset)?;
150
151 (header, files)
152 } else {
153 let file_count = bytes.pread_with::<i32>(bytes.len() - 4, scroll::LE)? as usize;
158
159 bytes.gread_with::<Unreal4Header>(&mut offset, scroll::LE)?;
161
162 let files = gread_files(&bytes, file_count, &mut offset)?;
163 let header = bytes.gread_with(&mut offset, scroll::LE)?;
164
165 (header, files)
166 };
167
168 if offset != bytes.len() {
169 return Err(Unreal4ErrorKind::TrailingData.into());
170 }
171
172 Ok(Unreal4Crash {
173 bytes,
174 header,
175 files,
176 })
177 }
178
179 pub fn parse(slice: &[u8]) -> Result<Self, Unreal4Error> {
184 Self::parse_with_limit(slice, usize::MAX)
185 }
186
187 pub fn parse_with_limit(slice: &[u8], limit: usize) -> Result<Self, Unreal4Error> {
192 if slice.is_empty() {
193 return Err(Unreal4ErrorKind::Empty.into());
194 }
195
196 let mut decompressed = Vec::new();
197 let decoder = &mut ZlibDecoder::new(slice);
198
199 decoder
200 .take(limit as u64)
201 .read_to_end(&mut decompressed)
202 .map_err(|e| Unreal4Error::new(Unreal4ErrorKind::BadCompression, e))?;
203
204 if !matches!(decoder.read(&mut [0; 1]), Ok(0)) {
207 return Err(Unreal4ErrorKind::TooLarge.into());
208 }
209
210 Self::from_bytes(decompressed.into())
211 }
212
213 pub fn name(&self) -> &str {
215 &self.header.file_name
216 }
217
218 pub fn directory_name(&self) -> &str {
220 &self.header.directory_name
221 }
222
223 pub fn files(&self) -> Unreal4FileIterator<'_> {
225 Unreal4FileIterator {
226 inner: self.files.iter(),
227 bytes: &self.bytes,
228 }
229 }
230
231 pub fn file_count(&self) -> usize {
233 self.files.len()
234 }
235
236 pub fn file_by_index(&self, index: usize) -> Option<Unreal4File> {
238 self.files().nth(index)
239 }
240
241 pub fn file_by_type(&self, ty: Unreal4FileType) -> Option<Unreal4File> {
245 self.files().find(|f| f.ty() == ty)
246 }
247
248 pub fn native_crash(&self) -> Option<Unreal4File> {
250 self.files().find(|f| {
251 f.ty() == Unreal4FileType::Minidump || f.ty() == Unreal4FileType::AppleCrashReport
252 })
253 }
254
255 pub fn context(&self) -> Result<Option<Unreal4Context>, Unreal4Error> {
260 match self.file_by_type(Unreal4FileType::Context) {
261 Some(file) => Unreal4Context::parse(file.data()).map(Some),
262 None => Ok(None),
263 }
264 }
265
266 pub fn logs(&self, limit: usize) -> Result<Vec<Unreal4LogEntry>, Unreal4Error> {
268 match self.file_by_type(Unreal4FileType::Log) {
269 Some(file) => Unreal4LogEntry::parse(file.data(), limit),
270 None => Ok(Vec::new()),
271 }
272 }
273}
274
275#[non_exhaustive]
277#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
278pub enum Unreal4FileType {
279 Minidump,
281 AppleCrashReport,
283 Log,
285 Config,
287 Context,
289 Unknown,
291}
292
293impl Unreal4FileType {
294 pub fn name(self) -> &'static str {
296 match self {
297 Unreal4FileType::Minidump => "minidump",
298 Unreal4FileType::AppleCrashReport => "applecrashreport",
299 Unreal4FileType::Log => "log",
300 Unreal4FileType::Config => "config",
301 Unreal4FileType::Context => "context",
302 Unreal4FileType::Unknown => "unknown",
303 }
304 }
305}
306
307impl fmt::Display for Unreal4FileType {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 write!(f, "{}", self.name())
310 }
311}
312
313#[derive(Debug)]
317pub struct Unreal4File {
318 index: usize,
320 file_name: String,
322 bytes: Bytes,
324}
325
326impl Unreal4File {
327 fn from_meta(meta: &Unreal4FileMeta, bytes: &Bytes) -> Self {
329 Unreal4File {
330 index: meta.index,
331 file_name: meta.file_name.as_str().to_owned(),
332 bytes: bytes.slice(meta.offset..meta.offset + meta.len),
333 }
334 }
335
336 pub fn index(&self) -> usize {
338 self.index
339 }
340
341 pub fn name(&self) -> &str {
343 &self.file_name
344 }
345
346 pub fn data(&self) -> &[u8] {
348 &self.bytes
349 }
350
351 pub fn ty(&self) -> Unreal4FileType {
353 if self.name() == "CrashReportClient.ini" {
354 Unreal4FileType::Config
355 } else if self.name() == "CrashContext.runtime-xml" {
356 Unreal4FileType::Context
357 } else if self.name().ends_with(".log") {
358 Unreal4FileType::Log
359 } else if self.data().starts_with(b"MDMP") {
360 Unreal4FileType::Minidump
361 } else if self.data().starts_with(b"Incident Identifier:") {
362 Unreal4FileType::AppleCrashReport
363 } else {
364 Unreal4FileType::Unknown
365 }
366 }
367}
368
369pub struct Unreal4FileIterator<'a> {
371 inner: std::slice::Iter<'a, Unreal4FileMeta>,
372 bytes: &'a Bytes,
373}
374
375impl Iterator for Unreal4FileIterator<'_> {
376 type Item = Unreal4File;
377
378 fn next(&mut self) -> Option<Self::Item> {
379 let meta = self.inner.next()?;
380 Some(Unreal4File::from_meta(meta, self.bytes))
381 }
382
383 fn size_hint(&self) -> (usize, Option<usize>) {
384 self.inner.size_hint()
385 }
386
387 fn count(self) -> usize {
388 self.inner.count()
389 }
390
391 fn nth(&mut self, n: usize) -> Option<Self::Item> {
392 let meta = self.inner.nth(n)?;
393 Some(Unreal4File::from_meta(meta, self.bytes))
394 }
395}
396
397impl DoubleEndedIterator for Unreal4FileIterator<'_> {
398 fn next_back(&mut self) -> Option<Self::Item> {
399 let meta = self.inner.next_back()?;
400 Some(Unreal4File::from_meta(meta, self.bytes))
401 }
402}
403
404impl FusedIterator for Unreal4FileIterator<'_> {}
405
406impl ExactSizeIterator for Unreal4FileIterator<'_> {}
407
408#[cfg(test)]
409mod tests {
410 use std::{error::Error, fs::File};
411 use symbolic_testutils::fixture;
412
413 use super::*;
414
415 #[test]
416 fn test_parse_empty_buffer() {
417 let crash = &[];
418
419 let result = Unreal4Crash::parse(crash);
420
421 assert!(matches!(
422 result.expect_err("empty crash").kind(),
423 Unreal4ErrorKind::Empty
424 ));
425 }
426
427 #[test]
428 fn test_parse_invalid_input() {
429 let crash = &[0u8; 1];
430
431 let result = Unreal4Crash::parse(crash);
432 let error = result.expect_err("empty crash");
433 assert_eq!(error.kind(), Unreal4ErrorKind::BadCompression);
434
435 let source = error.source().expect("error source");
436 assert_eq!(source.to_string(), "corrupt deflate stream");
437 }
438
439 const DECOMPRESSED_SIZE: usize = 440752;
441
442 #[test]
443 fn test_parse_too_large() {
444 let mut file = File::open(fixture("unreal/unreal_crash")).expect("example file opens");
445 let mut file_content = Vec::new();
446 file.read_to_end(&mut file_content).expect("fixture file");
447
448 let result = Unreal4Crash::parse_with_limit(&file_content, DECOMPRESSED_SIZE - 1);
449 let error = result.expect_err("too large");
450 assert_eq!(error.kind(), Unreal4ErrorKind::TooLarge);
451 }
452
453 #[test]
454 fn test_parse_fits_exact() {
455 let mut file = File::open(fixture("unreal/unreal_crash")).expect("example file opens");
456 let mut file_content = Vec::new();
457 file.read_to_end(&mut file_content).expect("fixture file");
458
459 Unreal4Crash::parse_with_limit(&file_content, DECOMPRESSED_SIZE)
460 .expect("file fits decompression buffer");
461 }
462}