Skip to main content

windows_eventlog_native/
event.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, TimeZone, Utc};
4use quick_xml::escape::resolve_xml_entity;
5use quick_xml::events::Event as XmlEvent;
6use quick_xml::{Reader, XmlVersion};
7
8use crate::error::{Error, Result};
9
10/// A parsed Windows event.
11///
12/// `xml` is the raw RenderedXml payload from `EvtRender`; every other field is derived
13/// from it. Keep `xml` around when the caller wants to inspect UserData / raw payload
14/// fields we did not lift into `data`.
15#[derive(Debug, Clone)]
16pub struct Event {
17    pub record_id: u64,
18    pub event_id: u32,
19    pub time_created: DateTime<Utc>,
20    pub provider: String,
21    pub channel: String,
22    pub xml: String,
23    /// Flattened `<EventData><Data Name="…">value</Data></EventData>` map.
24    /// Nameless `<Data>` elements land under keys `Data_0`, `Data_1`, ….
25    pub data: HashMap<String, String>,
26}
27
28/// Return the rendered XML for the event (same as `evt.xml`, provided per spec).
29pub fn rendered_xml(e: &Event) -> String {
30    e.xml.clone()
31}
32
33/// Parse a RenderedXml Event blob into an `Event`.
34///
35/// This is a hand-rolled quick-xml pass instead of pulling in `serde-xml-rs` — the XML
36/// shape is stable and small (`<Event><System>…</System><EventData>…</EventData></Event>`).
37pub fn parse_event_xml(xml: &str, default_channel: &str) -> Result<Event> {
38    let mut reader = Reader::from_str(xml);
39
40    let mut record_id: u64 = 0;
41    let mut event_id: u32 = 0;
42    let mut time_created: Option<DateTime<Utc>> = None;
43    let mut provider = String::new();
44    let mut channel = String::from(default_channel);
45    let mut data: HashMap<String, String> = HashMap::new();
46
47    // State machine: which <Data Name="…"> are we currently inside, and are we inside
48    // <EventData>? We also need to swallow the text nodes of <System>/<EventID>, etc.
49    enum State {
50        Idle,
51        InSystemElement(&'static str), // event_id | record_id | channel_text
52        InEventData,
53        InDataNamed(String),
54        InDataAnon(usize),
55    }
56
57    fn apply_text(
58        state: &State,
59        txt: &str,
60        event_id: &mut u32,
61        record_id: &mut u64,
62        channel: &mut String,
63        data: &mut HashMap<String, String>,
64    ) {
65        match state {
66            State::InSystemElement("event_id") => {
67                *event_id = txt.trim().parse().unwrap_or(0);
68            }
69            State::InSystemElement("record_id") => {
70                *record_id = txt.trim().parse().unwrap_or(0);
71            }
72            State::InSystemElement("channel") => {
73                *channel = txt.trim().to_string();
74            }
75            State::InDataNamed(name) => {
76                data.entry(name.clone())
77                    .and_modify(|value| value.push_str(txt))
78                    .or_insert_with(|| txt.to_string());
79            }
80            State::InDataAnon(index) => {
81                let key = format!("Data_{index}");
82                data.entry(key)
83                    .and_modify(|value| value.push_str(txt))
84                    .or_insert_with(|| txt.to_string());
85            }
86            _ => {}
87        }
88    }
89
90    let mut state = State::Idle;
91    let mut anon_counter = 0usize;
92    let mut in_event_data = false;
93
94    let mut buf = Vec::new();
95    loop {
96        match reader.read_event_into(&mut buf) {
97            Ok(XmlEvent::Start(e)) => {
98                let name = e.name();
99                let local = std::str::from_utf8(name.as_ref()).unwrap_or("");
100                match local {
101                    "EventData" => {
102                        in_event_data = true;
103                        state = State::InEventData;
104                    }
105                    "Data" if in_event_data => {
106                        let mut named: Option<String> = None;
107                        for attr in e.attributes().flatten() {
108                            if attr.key.as_ref() == b"Name" {
109                                if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
110                                    named = Some(v.into_owned());
111                                }
112                            }
113                        }
114                        state = match named {
115                            Some(n) => State::InDataNamed(n),
116                            None => {
117                                let idx = anon_counter;
118                                anon_counter += 1;
119                                State::InDataAnon(idx)
120                            }
121                        };
122                    }
123                    "Provider" => {
124                        for attr in e.attributes().flatten() {
125                            if attr.key.as_ref() == b"Name" {
126                                if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
127                                    provider = v.into_owned();
128                                }
129                            }
130                        }
131                    }
132                    "EventID" => state = State::InSystemElement("event_id"),
133                    "EventRecordID" => state = State::InSystemElement("record_id"),
134                    "Channel" => state = State::InSystemElement("channel"),
135                    "TimeCreated" => {
136                        for attr in e.attributes().flatten() {
137                            if attr.key.as_ref() == b"SystemTime" {
138                                if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
139                                    // Format: 2024-01-15T12:34:56.7891234Z
140                                    if let Ok(t) = DateTime::parse_from_rfc3339(&v) {
141                                        time_created = Some(t.with_timezone(&Utc));
142                                    } else if let Ok(t) = chrono::NaiveDateTime::parse_from_str(
143                                        v.trim_end_matches('Z'),
144                                        "%Y-%m-%dT%H:%M:%S%.f",
145                                    ) {
146                                        time_created = Some(Utc.from_utc_datetime(&t));
147                                    }
148                                }
149                            }
150                        }
151                    }
152                    _ => {}
153                }
154            }
155            Ok(XmlEvent::Empty(e)) => {
156                // Self-closed <Data Name="X"/> is legal and means empty string.
157                let name = e.name();
158                let local = std::str::from_utf8(name.as_ref()).unwrap_or("");
159                if local == "Data" && in_event_data {
160                    let mut named: Option<String> = None;
161                    for attr in e.attributes().flatten() {
162                        if attr.key.as_ref() == b"Name" {
163                            if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
164                                named = Some(v.into_owned());
165                            }
166                        }
167                    }
168                    match named {
169                        Some(n) => {
170                            data.insert(n, String::new());
171                        }
172                        None => {
173                            data.insert(format!("Data_{}", anon_counter), String::new());
174                            anon_counter += 1;
175                        }
176                    }
177                } else if local == "TimeCreated" {
178                    for attr in e.attributes().flatten() {
179                        if attr.key.as_ref() == b"SystemTime" {
180                            if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
181                                if let Ok(t) = DateTime::parse_from_rfc3339(&v) {
182                                    time_created = Some(t.with_timezone(&Utc));
183                                }
184                            }
185                        }
186                    }
187                } else if local == "Provider" {
188                    for attr in e.attributes().flatten() {
189                        if attr.key.as_ref() == b"Name" {
190                            if let Ok(v) = attr.normalized_value(XmlVersion::Implicit1_0) {
191                                provider = v.into_owned();
192                            }
193                        }
194                    }
195                }
196            }
197            Ok(XmlEvent::Text(t)) => {
198                let txt = t.xml10_content().unwrap_or_default();
199                apply_text(
200                    &state,
201                    &txt,
202                    &mut event_id,
203                    &mut record_id,
204                    &mut channel,
205                    &mut data,
206                );
207            }
208            Ok(XmlEvent::GeneralRef(reference)) => {
209                let resolved = match reference.resolve_char_ref() {
210                    Ok(Some(character)) => character.to_string(),
211                    Ok(None) => {
212                        let name = std::str::from_utf8(reference.as_ref()).unwrap_or_default();
213                        resolve_xml_entity(name).unwrap_or_default().to_string()
214                    }
215                    Err(_) => String::new(),
216                };
217                apply_text(
218                    &state,
219                    &resolved,
220                    &mut event_id,
221                    &mut record_id,
222                    &mut channel,
223                    &mut data,
224                );
225            }
226            Ok(XmlEvent::End(e)) => {
227                let name = e.name();
228                let local = std::str::from_utf8(name.as_ref()).unwrap_or("");
229                if local == "EventData" {
230                    in_event_data = false;
231                    state = State::Idle;
232                } else if matches!(local, "EventID" | "EventRecordID" | "Channel" | "Data") {
233                    state = if in_event_data {
234                        State::InEventData
235                    } else {
236                        State::Idle
237                    };
238                }
239            }
240            Ok(XmlEvent::Eof) => break,
241            Err(e) => return Err(Error::Xml(e.to_string())),
242            _ => {}
243        }
244        buf.clear();
245    }
246
247    Ok(Event {
248        record_id,
249        event_id,
250        time_created: time_created.unwrap_or_else(Utc::now),
251        provider,
252        channel,
253        xml: xml.to_string(),
254        data,
255    })
256}
257
258/// Convert a Win32 `FILETIME` (100ns intervals since 1601-01-01 UTC) to `DateTime<Utc>`.
259///
260/// Kept public for callers that already hold a FILETIME from `EvtRender` with the
261/// `EvtRenderEventValues` context (a future path — for XML rendering we use the
262/// `TimeCreated/@SystemTime` string instead).
263pub fn filetime_to_utc(ft: u64) -> Result<DateTime<Utc>> {
264    // 11_644_473_600 = seconds between 1601-01-01 and 1970-01-01.
265    const SEC_1601_TO_1970: i64 = 11_644_473_600;
266    let ticks = ft as i128;
267    let secs = (ticks / 10_000_000) as i64 - SEC_1601_TO_1970;
268    let nsecs = ((ticks % 10_000_000) * 100) as u32;
269    Utc.timestamp_opt(secs, nsecs)
270        .single()
271        .ok_or(Error::BadFileTime(ft))
272}