Skip to main content

windows_erg/evt/
query.rs

1//! Event query building and XPath query construction.
2
3use super::types::EventLevel;
4use std::borrow::Cow;
5
6/// Query builder for constructing flexible event queries.
7///
8/// Supports building XPath queries with convenience methods while allowing
9/// raw XPath for advanced scenarios.
10#[derive(Debug, Clone)]
11pub struct QueryBuilder {
12    xpath: Option<String>,
13    level: Option<EventLevel>,
14    provider: Option<String>,
15    event_id: Option<u32>,
16    reverse: bool,
17    max_results: Option<usize>,
18    include_event_data: bool,
19    parse_message: bool,
20}
21
22impl QueryBuilder {
23    /// Create a new query builder.
24    pub fn new() -> Self {
25        QueryBuilder {
26            xpath: None,
27            level: None,
28            provider: None,
29            event_id: None,
30            reverse: false,
31            max_results: None,
32            include_event_data: false,
33            parse_message: false,
34        }
35    }
36
37    /// Set a raw XPath query expression.
38    ///
39    /// When set, this takes precedence over other builder fields.
40    /// Example: `"Event/System[EventID=4688]"`
41    pub fn xpath(mut self, xpath: impl Into<Cow<'static, str>>) -> Self {
42        self.xpath = Some(xpath.into().into_owned());
43        self
44    }
45
46    /// Filter by event level (1=Critical to 5=Verbose).
47    pub fn level(mut self, level: EventLevel) -> Self {
48        self.level = Some(level);
49        self
50    }
51
52    /// Filter by provider/source name.
53    pub fn provider(mut self, name: impl Into<Cow<'static, str>>) -> Self {
54        self.provider = Some(name.into().into_owned());
55        self
56    }
57
58    /// Filter by specific event ID.
59    pub fn event_id(mut self, id: u32) -> Self {
60        self.event_id = Some(id);
61        self
62    }
63
64    /// Query in reverse order (newest to oldest).
65    ///
66    /// Note: Not supported on Debug/Analytic channels or .evt files.
67    pub fn reverse(mut self) -> Self {
68        self.reverse = true;
69        self
70    }
71
72    /// Limit maximum number of results returned.
73    pub fn max_results(mut self, count: usize) -> Self {
74        self.max_results = Some(count);
75        self
76    }
77
78    /// Parse EventData fields into the data HashMap.
79    ///
80    /// When enabled, extracts <Data Name="..."> fields from event XML.
81    /// Common field names are cached as static strings for performance.
82    pub fn with_event_data(mut self) -> Self {
83        self.include_event_data = true;
84        self
85    }
86
87    /// Parse event message using publisher metadata.
88    ///
89    /// When enabled, formats the event message using the provider's message template.
90    /// Returns None if publisher metadata is unavailable.
91    pub fn with_message(mut self) -> Self {
92        self.parse_message = true;
93        self
94    }
95
96    /// Check if EventData parsing is enabled.
97    pub fn should_parse_event_data(&self) -> bool {
98        self.include_event_data
99    }
100
101    /// Check if message parsing is enabled.
102    pub fn should_parse_message(&self) -> bool {
103        self.parse_message
104    }
105
106    /// Build the final XPath query string.
107    ///
108    /// Returns the XPath expression that will be passed to Windows Event Log API.
109    /// If raw XPath was set, returns that. Otherwise, builds XPath from builder fields.
110    pub fn build_xpath(&self) -> String {
111        if let Some(xpath) = &self.xpath {
112            return xpath.clone();
113        }
114
115        let mut conditions = Vec::new();
116
117        // Add event level condition
118        if let Some(level) = self.level {
119            conditions.push(format!("Level <= {}", level as u8));
120        }
121
122        // Add provider condition
123        if let Some(ref provider) = self.provider {
124            conditions.push(format!(
125                "System/Provider/@Name='{}'",
126                escape_xpath_string(provider)
127            ));
128        }
129
130        // Add event ID condition
131        if let Some(id) = self.event_id {
132            conditions.push(format!("EventID={}", id));
133        }
134
135        // Build XPath expression
136        if conditions.is_empty() {
137            "Event".to_string()
138        } else {
139            format!("Event/System[{}]", conditions.join(" and "))
140        }
141    }
142
143    /// Get whether query should be reversed.
144    pub fn is_reverse(&self) -> bool {
145        self.reverse
146    }
147
148    /// Get maximum results limit if set.
149    pub fn max_results_limit(&self) -> Option<usize> {
150        self.max_results
151    }
152}
153
154impl Default for QueryBuilder {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160/// Escape special characters for XPath string literals.
161fn escape_xpath_string(s: &str) -> String {
162    // XPath doesn't have standard escape sequences; handle single/double quotes
163    // by wroting the string if it contains quotes
164    if s.contains('\'') && s.contains('"') {
165        // Has both - need to use concat()
166        // For now, prefer double quotes and escape any double quotes
167        s.replace('"', "&quot;")
168    } else if s.contains('\'') {
169        // Use double quotes
170        s.to_string()
171    } else {
172        // Use single quotes (preferred)
173        s.to_string()
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_query_builder_empty() {
183        let builder = QueryBuilder::new();
184        assert_eq!(builder.build_xpath(), "Event");
185    }
186
187    #[test]
188    fn test_query_builder_with_level() {
189        let builder = QueryBuilder::new().level(EventLevel::Error);
190        let xpath = builder.build_xpath();
191        assert!(xpath.contains("Level <= 3"));
192        assert!(xpath.contains("Event/System"));
193    }
194
195    #[test]
196    fn test_query_builder_with_provider() {
197        let builder = QueryBuilder::new().provider("Security");
198        let xpath = builder.build_xpath();
199        assert!(xpath.contains("System/Provider/@Name='Security'"));
200    }
201
202    #[test]
203    fn test_query_builder_with_event_id() {
204        let builder = QueryBuilder::new().event_id(4688);
205        let xpath = builder.build_xpath();
206        assert!(xpath.contains("EventID=4688"));
207    }
208
209    #[test]
210    fn test_query_builder_combined() {
211        let builder = QueryBuilder::new()
212            .level(EventLevel::Warning)
213            .provider("Application")
214            .event_id(1000);
215        let xpath = builder.build_xpath();
216        assert!(xpath.contains("Level <= 4"));
217        assert!(xpath.contains("System/Provider/@Name='Application'"));
218        assert!(xpath.contains("EventID=1000"));
219        assert!(xpath.contains(" and "));
220    }
221
222    #[test]
223    fn test_query_builder_with_raw_xpath() {
224        let builder = QueryBuilder::new()
225            .xpath("Event/System[EventID=4728]")
226            .level(EventLevel::Error); // Should be ignored
227        let xpath = builder.build_xpath();
228        assert_eq!(xpath, "Event/System[EventID=4728]");
229    }
230
231    #[test]
232    fn test_query_builder_reverse() {
233        let builder = QueryBuilder::new().reverse();
234        assert!(builder.is_reverse());
235
236        let builder2 = QueryBuilder::new();
237        assert!(!builder2.is_reverse());
238    }
239
240    #[test]
241    fn test_query_builder_max_results() {
242        let builder = QueryBuilder::new().max_results(100);
243        assert_eq!(builder.max_results_limit(), Some(100));
244
245        let builder2 = QueryBuilder::new();
246        assert_eq!(builder2.max_results_limit(), None);
247    }
248}