#![warn(missing_docs)]
use std::fmt;
use std::io::{self, Cursor, Read};
use anylog::LogEntry;
use bytes::{Buf, Bytes};
use chrono::{DateTime, TimeZone, Utc};
use compress::zlib;
use failure::Fail;
use lazy_static::lazy_static;
use regex::Regex;
#[cfg(feature = "with-serde")]
use serde::Serialize;
use crate::context::Unreal4Context;
lazy_static! {
static ref LOG_FIRST_LINE: Regex = Regex::new(r"Log file open, (?P<month>\d\d)/(?P<day>\d\d)/(?P<year>\d\d) (?P<hour>\d\d):(?P<minute>\d\d):(?P<second>\d\d)$").unwrap();
}
mod context;
struct Header {
pub directory_name: String,
pub file_name: String,
pub uncompressed_size: i32,
pub file_count: i32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unreal4FileType {
Minidump,
Log,
Config,
Context,
Unknown,
}
impl Unreal4FileType {
pub fn name(self) -> &'static str {
match self {
Unreal4FileType::Minidump => "minidump",
Unreal4FileType::Log => "log",
Unreal4FileType::Config => "config",
Unreal4FileType::Context => "context",
Unreal4FileType::Unknown => "unknown",
}
}
}
impl fmt::Display for Unreal4FileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Clone, Debug)]
pub struct Unreal4CrashFile {
pub index: usize,
pub file_name: String,
pub offset: usize,
pub len: usize,
}
#[cfg_attr(feature = "with-serde", derive(Serialize))]
pub struct Unreal4LogEntry {
#[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))]
pub timestamp: Option<DateTime<Utc>>,
#[cfg_attr(feature = "with-serde", serde(skip_serializing_if = "Option::is_none"))]
pub component: Option<String>,
pub message: String,
}
impl Unreal4CrashFile {
pub fn ty(&self) -> Unreal4FileType {
match self.file_name.as_str() {
"UE4Minidump.dmp" => Unreal4FileType::Minidump,
"minidump.dmp" => Unreal4FileType::Minidump,
"CrashReportClient.ini" => Unreal4FileType::Config,
"CrashContext.runtime-xml" => Unreal4FileType::Context,
name => {
if name.ends_with(".log") {
Unreal4FileType::Log
} else {
Unreal4FileType::Unknown
}
}
}
}
}
#[derive(Fail, Debug)]
pub enum Unreal4Error {
#[fail(display = "unknown bytes format")]
UnknownBytesFormat,
#[fail(display = "empty crash")]
Empty,
#[fail(display = "out of bounds")]
OutOfBounds,
#[fail(display = "bad compression")]
BadCompression(io::Error),
#[fail(display = "invalid log entry")]
InvalidLogEntry(std::str::Utf8Error),
#[fail(display = "invalid xml")]
InvalidXml(elementtree::Error),
}
#[derive(Debug)]
pub struct Unreal4Crash {
bytes: Bytes,
files: Vec<Unreal4CrashFile>,
}
#[derive(Debug, Clone)]
pub enum NativeCrash<'a> {
MiniDump(&'a [u8]),
AppleCrashReport(&'a str),
}
impl Unreal4Crash {
pub fn from_slice(bytes: &[u8]) -> Result<Unreal4Crash, Unreal4Error> {
if bytes.is_empty() {
return Err(Unreal4Error::Empty);
}
let mut zlib_decoder = zlib::Decoder::new(bytes);
let mut decompressed = Vec::new();
zlib_decoder
.read_to_end(&mut decompressed)
.map_err(Unreal4Error::BadCompression)?;
let decompressed = Bytes::from(decompressed);
let file_meta = get_files_from_slice(&decompressed)?;
Ok(Unreal4Crash {
bytes: decompressed,
files: file_meta,
})
}
pub fn files(&self) -> impl Iterator<Item = &Unreal4CrashFile> {
self.files.iter()
}
pub fn file_count(&self) -> usize {
self.files.len() as usize
}
pub fn file_by_index(&self, index: usize) -> Option<&Unreal4CrashFile> {
self.files().find(|f| f.index == index)
}
pub fn file_contents_by_index(&self, index: usize) -> Result<Option<&[u8]>, Unreal4Error> {
match self.file_by_index(index) {
Some(f) => Ok(Some(self.get_file_contents(f)?)),
None => Ok(None),
}
}
pub fn get_native_crash(&self) -> Result<Option<NativeCrash<'_>>, Unreal4Error> {
Ok(self
.get_file_slice(Unreal4FileType::Minidump)?
.and_then(|bytes| {
if bytes.get(..4) == Some(b"MDMP") {
return Some(NativeCrash::MiniDump(bytes));
}
if bytes.get(..20) == Some(b"Incident Identifier:") {
if let Ok(s) = std::str::from_utf8(bytes) {
return Some(NativeCrash::AppleCrashReport(s));
}
}
None
}))
}
pub fn get_minidump_slice(&self) -> Result<Option<&[u8]>, Unreal4Error> {
Ok(self.get_native_crash()?.and_then(|ft| {
if let NativeCrash::MiniDump(md) = ft {
Some(md)
} else {
None
}
}))
}
pub fn get_apple_crash_report(&self) -> Result<Option<&str>, Unreal4Error> {
Ok(self.get_native_crash()?.and_then(|ft| {
if let NativeCrash::AppleCrashReport(s) = ft {
Some(s)
} else {
None
}
}))
}
pub fn get_file_slice(
&self,
file_type: Unreal4FileType,
) -> Result<Option<&[u8]>, Unreal4Error> {
let file = match self.files().find(|f| f.ty() == file_type) {
Some(m) => m,
None => return Ok(None),
};
Ok(Some(self.get_file_contents(file)?))
}
pub fn get_file_contents(&self, file_meta: &Unreal4CrashFile) -> Result<&[u8], Unreal4Error> {
let end = file_meta
.offset
.checked_add(file_meta.len)
.ok_or(Unreal4Error::OutOfBounds)?;
self.bytes
.get(file_meta.offset..end)
.ok_or(Unreal4Error::OutOfBounds)
}
pub fn get_context(&self) -> Result<Option<Unreal4Context>, Unreal4Error> {
Unreal4Context::from_crash(self)
}
pub fn get_logs(&self, limit: usize) -> Result<Vec<Unreal4LogEntry>, Unreal4Error> {
match self.get_file_slice(Unreal4FileType::Log)? {
Some(f) => parse_log_from_slice(f, limit),
None => Ok(Vec::new()),
}
}
}
fn parse_log_from_slice(
log_slice: &[u8],
limit: usize,
) -> Result<Vec<Unreal4LogEntry>, Unreal4Error> {
let mut fallback_timestamp = None;
let logs_utf8 = std::str::from_utf8(log_slice).map_err(Unreal4Error::InvalidLogEntry)?;
if let Some(first_line) = logs_utf8.lines().next() {
if let Some(captures) = LOG_FIRST_LINE.captures(&first_line) {
fallback_timestamp = Some(
Utc.ymd(
captures["year"].parse::<i32>().unwrap() + 2000,
captures["month"].parse::<u32>().unwrap(),
captures["day"].parse::<u32>().unwrap(),
)
.and_hms(
captures["hour"].parse::<u32>().unwrap(),
captures["minute"].parse::<u32>().unwrap(),
captures["second"].parse::<u32>().unwrap(),
),
);
}
}
let mut logs: Vec<_> = logs_utf8
.lines()
.rev()
.take(limit)
.map(|line| {
let entry = LogEntry::parse(line.as_bytes());
let (component, message) = entry.component_and_message();
fallback_timestamp = entry.utc_timestamp().or(fallback_timestamp);
Unreal4LogEntry {
timestamp: fallback_timestamp,
component: component.map(Into::into),
message: message.into(),
}
})
.collect();
logs.reverse();
Ok(logs)
}
fn read_ansi_string(buffer: &mut Cursor<&[u8]>) -> String {
let size = buffer.get_u32_le() as usize;
let dir_name = String::from_utf8_lossy(&Buf::bytes(&buffer)[..size]).into_owned();
buffer.advance(size);
dir_name.trim_end_matches('\0').into()
}
fn read_header(cursor: &mut Cursor<&[u8]>) -> Header {
Header {
directory_name: read_ansi_string(cursor),
file_name: read_ansi_string(cursor),
uncompressed_size: cursor.get_i32_le(),
file_count: cursor.get_i32_le(),
}
}
fn get_files_from_slice(bytes: &Bytes) -> Result<Vec<Unreal4CrashFile>, Unreal4Error> {
let mut rv = vec![];
let file_count = Cursor::new(
&bytes
.get(bytes.len() - 4..)
.ok_or(Unreal4Error::OutOfBounds)?,
)
.get_i32_le();
let mut cursor = Cursor::new(&bytes[..]);
read_header(&mut cursor);
for _ in 0..file_count {
let meta = Unreal4CrashFile {
index: cursor.get_i32_le() as usize,
file_name: read_ansi_string(&mut cursor),
len: cursor.get_i32_le() as usize,
offset: cursor.position() as usize,
};
cursor.advance(meta.len);
rv.push(meta);
}
Ok(rv)
}
#[test]
fn test_from_slice_empty_buffer() {
let crash = &[];
let result = Unreal4Crash::from_slice(crash);
assert!(match result.expect_err("empty crash") {
Unreal4Error::Empty => true,
_ => false,
})
}
#[test]
fn test_from_slice_invalid_input() {
let crash = &[0u8; 1];
let result = Unreal4Crash::from_slice(crash);
let err = match result.expect_err("empty crash") {
Unreal4Error::BadCompression(b) => b.to_string(),
_ => panic!(),
};
assert_eq!("unexpected EOF", err)
}
#[test]
fn test_parse_log_from_slice_no_entries_with_timestamp() {
let log_bytes = br"Log file open, 12/13/18 15:54:53
LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
LogWindows: File 'aqProf.dll' does not exist";
let logs = parse_log_from_slice(log_bytes, 1000).expect("logs");
assert_eq!(logs.len(), 3);
assert_eq!(logs[2].component.as_ref().expect("component"), "LogWindows");
assert_eq!(
logs[2].timestamp.expect("timestamp").to_rfc3339(),
"2018-12-13T15:54:53+00:00"
);
assert_eq!(logs[2].message, "File 'aqProf.dll' does not exist");
}