Skip to main content

windows_eventlog_native/
event.rs

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