1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use anylog::LogEntry;
use chrono::{DateTime, TimeZone, Utc};
use lazy_static::lazy_static;
use regex::Regex;
use crate::error::Unreal4Error;
use crate::Unreal4ErrorKind;
#[cfg(test)]
use similar_asserts::assert_eq;
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();
}
#[cfg_attr(feature = "serde", derive(serde_::Serialize))]
#[cfg_attr(feature = "serde", serde(crate = "serde_"))]
pub struct Unreal4LogEntry {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub timestamp: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub component: Option<String>,
pub message: String,
}
fn parse_log_datetime(text: &str) -> Option<DateTime<Utc>> {
let captures = LOG_FIRST_LINE.captures(text)?;
Utc.ymd_opt(
captures["year"].parse::<i32>().unwrap() + 2000,
captures["month"].parse::<u32>().unwrap(),
captures["day"].parse::<u32>().unwrap(),
)
.latest()?
.and_hms_opt(
captures["hour"].parse::<u32>().unwrap(),
captures["minute"].parse::<u32>().unwrap(),
captures["second"].parse::<u32>().unwrap(),
)
}
impl Unreal4LogEntry {
pub fn parse(log_slice: &[u8], limit: usize) -> Result<Vec<Self>, Unreal4Error> {
let mut fallback_timestamp = None;
let logs_utf8 = std::str::from_utf8(log_slice)
.map_err(|e| Unreal4Error::new(Unreal4ErrorKind::InvalidLogEntry, e))?;
if let Some(first_line) = logs_utf8.lines().next() {
fallback_timestamp = parse_log_datetime(&first_line);
}
let mut logs: Vec<_> = logs_utf8
.lines()
.rev()
.take(limit + 1)
.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.pop();
logs.reverse();
Ok(logs)
}
}
#[test]
fn test_parse_logs_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 = Unreal4LogEntry::parse(log_bytes, 1000).expect("logs");
assert_eq!(logs.len(), 2);
assert_eq!(logs[1].component.as_ref().expect("component"), "LogWindows");
assert_eq!(
logs[1].timestamp.expect("timestamp").to_rfc3339(),
"2018-12-13T15:54:53+00:00"
);
assert_eq!(logs[1].message, "File 'aqProf.dll' does not exist");
}
#[test]
fn test_parse_logs_invalid_time() {
let log_bytes = br"Log file open, 12/13/18 99:99:99
LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
LogWindows: File 'aqProf.dll' does not exist";
let logs = Unreal4LogEntry::parse(log_bytes, 1000).expect("logs");
assert_eq!(logs.len(), 2);
assert_eq!(logs[1].timestamp, None);
}