1use anylog::LogEntry;
2use chrono::{DateTime, Utc};
3use lazy_static::lazy_static;
4use regex::Regex;
5#[cfg(any(test, feature = "serde"))]
6use time::format_description::well_known::Rfc3339;
7
8use crate::error::Unreal4Error;
9use crate::Unreal4ErrorKind;
10
11#[cfg(test)]
12use similar_asserts::assert_eq;
13
14lazy_static! {
15 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();
19}
20
21#[cfg(feature = "serde")]
22fn serialize_timestamp<S: serde::Serializer>(
23 timestamp: &Option<time::OffsetDateTime>,
24 serializer: S,
25) -> Result<S::Ok, S::Error> {
26 use serde::ser::Error;
27 match timestamp {
28 Some(timestamp) => serializer.serialize_str(&match timestamp.format(&Rfc3339) {
29 Ok(s) => s,
30 Err(_) => return Err(S::Error::custom("failed formatting `OffsetDateTime`")),
31 }),
32 None => serializer.serialize_none(),
33 }
34}
35
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
38pub struct Unreal4LogEntry {
39 #[cfg_attr(
41 feature = "serde",
42 serde(
43 skip_serializing_if = "Option::is_none",
44 serialize_with = "serialize_timestamp"
45 )
46 )]
47 pub timestamp: Option<time::OffsetDateTime>,
48
49 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
51 pub component: Option<String>,
52
53 pub message: String,
55}
56
57fn parse_log_datetime(text: &str) -> Option<time::OffsetDateTime> {
58 let captures = LOG_FIRST_LINE.captures(text)?;
59
60 let month = time::Month::try_from(captures["month"].parse::<u8>().ok()?).ok()?;
62 let date = time::Date::from_calendar_date(
63 captures["year"].parse::<i32>().ok()? + 2000,
64 month,
65 captures["day"].parse::<u8>().ok()?,
66 )
67 .ok()?;
68
69 let datetime = date
70 .with_hms(
71 captures["hour"].parse::<u8>().ok()?,
72 captures["minute"].parse::<u8>().ok()?,
73 captures["second"].parse::<u8>().ok()?,
74 )
75 .ok()?;
76
77 Some(datetime.assume_utc())
79}
80
81fn convert_chrono(dt: DateTime<Utc>) -> Option<time::OffsetDateTime> {
82 time::OffsetDateTime::from_unix_timestamp(dt.timestamp()).ok()
83}
84
85impl Unreal4LogEntry {
86 pub fn parse(log_slice: &[u8], limit: usize) -> Result<Vec<Self>, Unreal4Error> {
89 let mut fallback_timestamp = None;
90 let logs_utf8 = std::str::from_utf8(log_slice)
91 .map_err(|e| Unreal4Error::new(Unreal4ErrorKind::InvalidLogEntry, e))?;
92
93 if let Some(first_line) = logs_utf8.lines().next() {
94 fallback_timestamp = parse_log_datetime(first_line);
97 }
98
99 let mut logs: Vec<_> = logs_utf8
100 .lines()
101 .rev()
102 .take(limit + 1) .map(|line| {
104 let entry = LogEntry::parse(line.as_bytes());
105 let (component, message) = entry.component_and_message();
106 fallback_timestamp = entry
110 .utc_timestamp()
111 .and_then(convert_chrono)
112 .or(fallback_timestamp);
113
114 Unreal4LogEntry {
115 timestamp: fallback_timestamp,
116 component: component.map(Into::into),
117 message: message.into(),
118 }
119 })
120 .collect();
121
122 logs.pop();
126 logs.reverse();
127 Ok(logs)
128 }
129}
130
131#[test]
132fn test_parse_logs_no_entries_with_timestamp() {
133 let log_bytes = br"Log file open, 12/13/18 15:54:53
134LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
135LogWindows: File 'aqProf.dll' does not exist";
136
137 let logs = Unreal4LogEntry::parse(log_bytes, 1000).expect("logs");
138
139 assert_eq!(logs.len(), 2);
140 assert_eq!(logs[1].component.as_ref().expect("component"), "LogWindows");
141 assert_eq!(
142 logs[1]
143 .timestamp
144 .expect("timestamp")
145 .format(&Rfc3339)
146 .unwrap(),
147 "2018-12-13T15:54:53Z"
148 );
149 assert_eq!(logs[1].message, "File 'aqProf.dll' does not exist");
150}
151
152#[test]
153fn test_parse_logs_invalid_time() {
154 let log_bytes = br"Log file open, 12/13/18 99:99:99
155LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
156LogWindows: File 'aqProf.dll' does not exist";
157
158 let logs = Unreal4LogEntry::parse(log_bytes, 1000).expect("logs");
159
160 assert_eq!(logs.len(), 2);
161 assert_eq!(logs[1].timestamp, None);
162}