1use std::fmt;
2
3use serde::Serialize;
4
5use crate::error::{Result, SigmaParserError};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub enum SpecialChar {
18 WildcardMulti,
20 WildcardSingle,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub enum StringPart {
27 Plain(String),
28 Special(SpecialChar),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54pub struct SigmaString {
55 pub parts: Vec<StringPart>,
56 pub original: String,
57}
58
59impl SigmaString {
60 pub fn new(s: &str) -> Self {
62 let mut parts: Vec<StringPart> = Vec::new();
63 let mut acc = String::new();
64 let mut escaped = false;
65
66 for c in s.chars() {
67 if escaped {
68 if c == '*' || c == '?' || c == '\\' {
69 acc.push(c);
70 } else {
71 acc.push('\\');
73 acc.push(c);
74 }
75 escaped = false;
76 } else if c == '\\' {
77 escaped = true;
78 } else if c == '*' {
79 if !acc.is_empty() {
80 parts.push(StringPart::Plain(std::mem::take(&mut acc)));
81 }
82 parts.push(StringPart::Special(SpecialChar::WildcardMulti));
83 } else if c == '?' {
84 if !acc.is_empty() {
85 parts.push(StringPart::Plain(std::mem::take(&mut acc)));
86 }
87 parts.push(StringPart::Special(SpecialChar::WildcardSingle));
88 } else {
89 acc.push(c);
90 }
91 }
92
93 if escaped {
94 acc.push('\\');
95 }
96 if !acc.is_empty() {
97 parts.push(StringPart::Plain(acc));
98 }
99
100 SigmaString {
101 parts,
102 original: s.to_string(),
103 }
104 }
105
106 pub fn from_raw(s: &str) -> Self {
108 SigmaString {
109 parts: if s.is_empty() {
110 Vec::new()
111 } else {
112 vec![StringPart::Plain(s.to_string())]
113 },
114 original: s.to_string(),
115 }
116 }
117
118 pub fn is_plain(&self) -> bool {
120 self.parts.iter().all(|p| matches!(p, StringPart::Plain(_)))
121 }
122
123 pub fn contains_wildcards(&self) -> bool {
125 self.parts
126 .iter()
127 .any(|p| matches!(p, StringPart::Special(_)))
128 }
129
130 pub fn as_plain(&self) -> Option<String> {
132 if !self.is_plain() {
133 return None;
134 }
135 Some(
136 self.parts
137 .iter()
138 .filter_map(|p| match p {
139 StringPart::Plain(s) => Some(s.as_str()),
140 _ => None,
141 })
142 .collect(),
143 )
144 }
145}
146
147impl fmt::Display for SigmaString {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.original)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize)]
162pub enum SigmaValue {
163 String(SigmaString),
165 Integer(i64),
167 Float(f64),
169 Bool(bool),
171 Null,
173}
174
175impl SigmaValue {
176 pub fn from_yaml(v: &yaml_serde::Value) -> Self {
178 match v {
179 yaml_serde::Value::String(s) => SigmaValue::String(SigmaString::new(s)),
180 yaml_serde::Value::Number(n) => {
181 if let Some(i) = n.as_i64() {
182 SigmaValue::Integer(i)
183 } else if let Some(f) = n.as_f64() {
184 SigmaValue::Float(f)
185 } else {
186 SigmaValue::Null
187 }
188 }
189 yaml_serde::Value::Bool(b) => SigmaValue::Bool(*b),
190 yaml_serde::Value::Null => SigmaValue::Null,
191 _ => SigmaValue::String(SigmaString::new(&format!("{v:?}"))),
192 }
193 }
194
195 pub fn from_raw_string(s: &str) -> Self {
197 SigmaValue::String(SigmaString::from_raw(s))
198 }
199}
200
201impl fmt::Display for SigmaValue {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 match self {
204 SigmaValue::String(s) => write!(f, "{s}"),
205 SigmaValue::Integer(n) => write!(f, "{n}"),
206 SigmaValue::Float(n) => write!(f, "{n}"),
207 SigmaValue::Bool(b) => write!(f, "{b}"),
208 SigmaValue::Null => write!(f, "null"),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
219pub enum TimespanUnit {
220 Second,
221 Minute,
222 Hour,
223 Day,
224 Week,
225 Month,
226 Year,
227}
228
229impl fmt::Display for TimespanUnit {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 let c = match self {
232 TimespanUnit::Second => "s",
233 TimespanUnit::Minute => "m",
234 TimespanUnit::Hour => "h",
235 TimespanUnit::Day => "d",
236 TimespanUnit::Week => "w",
237 TimespanUnit::Month => "M",
238 TimespanUnit::Year => "y",
239 };
240 write!(f, "{c}")
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248pub struct Timespan {
249 pub count: u64,
250 pub unit: TimespanUnit,
251 pub seconds: u64,
253 pub original: String,
255}
256
257impl Timespan {
258 pub fn parse(s: &str) -> Result<Self> {
263 if s.len() < 2 {
264 return Err(SigmaParserError::InvalidTimespan(s.to_string()));
265 }
266 let split_pos = match s.char_indices().next_back() {
267 Some((idx, _)) => idx,
268 None => return Err(SigmaParserError::InvalidTimespan(s.to_string())),
269 };
270 let (count_str, unit_str) = s.split_at(split_pos);
271 let count: u64 = count_str
272 .parse()
273 .map_err(|_| SigmaParserError::InvalidTimespan(s.to_string()))?;
274
275 let (unit, multiplier) = match unit_str {
276 "s" => (TimespanUnit::Second, 1u64),
277 "m" => (TimespanUnit::Minute, 60),
278 "h" => (TimespanUnit::Hour, 3600),
279 "d" => (TimespanUnit::Day, 86400),
280 "w" => (TimespanUnit::Week, 604800),
281 "M" => (TimespanUnit::Month, 2_629_746), "y" => (TimespanUnit::Year, 31_556_952), _ => return Err(SigmaParserError::InvalidTimespan(s.to_string())),
284 };
285
286 let seconds = count
287 .checked_mul(multiplier)
288 .ok_or_else(|| SigmaParserError::InvalidTimespan(s.to_string()))?;
289
290 Ok(Timespan {
291 count,
292 unit,
293 seconds,
294 original: s.to_string(),
295 })
296 }
297}
298
299impl fmt::Display for Timespan {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(f, "{}", self.original)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_sigma_string_plain() {
311 let s = SigmaString::new("hello world");
312 assert!(s.is_plain());
313 assert!(!s.contains_wildcards());
314 assert_eq!(s.as_plain(), Some("hello world".to_string()));
315 }
316
317 #[test]
318 fn test_sigma_string_wildcards() {
319 let s = SigmaString::new("*admin*");
320 assert!(!s.is_plain());
321 assert!(s.contains_wildcards());
322 assert_eq!(s.parts.len(), 3);
323 assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
324 assert_eq!(s.parts[1], StringPart::Plain("admin".to_string()));
325 assert_eq!(s.parts[2], StringPart::Special(SpecialChar::WildcardMulti));
326 }
327
328 #[test]
329 fn test_sigma_string_escaped_wildcard_is_literal() {
330 let s = SigmaString::new(r"C:\Windows\*");
333 assert!(!s.contains_wildcards()); assert!(s.is_plain());
335 assert_eq!(s.as_plain(), Some(r"C:\Windows*".to_string()));
337 }
338
339 #[test]
340 fn test_sigma_string_unescaped_wildcard_in_path() {
341 let s = SigmaString::new(r"C:\Windows*");
343 assert!(s.contains_wildcards());
344 assert_eq!(s.parts.len(), 2);
345 assert_eq!(s.parts[0], StringPart::Plain(r"C:\Windows".to_string()));
346 assert_eq!(s.parts[1], StringPart::Special(SpecialChar::WildcardMulti));
347 }
348
349 #[test]
350 fn test_sigma_string_leading_wildcard_path() {
351 let s = SigmaString::new(r"*\cmd.exe");
353 assert!(s.contains_wildcards());
354 assert_eq!(s.parts.len(), 2);
355 assert_eq!(s.parts[0], StringPart::Special(SpecialChar::WildcardMulti));
356 assert_eq!(s.parts[1], StringPart::Plain(r"\cmd.exe".to_string()));
357 }
358
359 #[test]
360 fn test_sigma_string_escaped_wildcard() {
361 let s = SigmaString::new(r"test\*value");
362 assert!(s.is_plain());
363 assert_eq!(s.as_plain(), Some("test*value".to_string()));
364 }
365
366 #[test]
367 fn test_sigma_string_single_wildcard() {
368 let s = SigmaString::new("user?admin");
369 assert!(s.contains_wildcards());
370 assert_eq!(s.parts.len(), 3);
371 }
372
373 #[test]
374 fn test_timespan_parse() {
375 let ts = Timespan::parse("1h").unwrap();
376 assert_eq!(ts.count, 1);
377 assert_eq!(ts.unit, TimespanUnit::Hour);
378 assert_eq!(ts.seconds, 3600);
379
380 let ts = Timespan::parse("15s").unwrap();
381 assert_eq!(ts.count, 15);
382 assert_eq!(ts.unit, TimespanUnit::Second);
383 assert_eq!(ts.seconds, 15);
384
385 let ts = Timespan::parse("30m").unwrap();
386 assert_eq!(ts.seconds, 1800);
387
388 let ts = Timespan::parse("7d").unwrap();
389 assert_eq!(ts.seconds, 604800);
390 }
391
392 #[test]
393 fn test_timespan_invalid() {
394 assert!(Timespan::parse("x").is_err());
395 assert!(Timespan::parse("1x").is_err());
396 assert!(Timespan::parse("").is_err());
397 }
398
399 #[test]
400 fn test_timespan_multibyte_utf8_no_panic() {
401 assert!(Timespan::parse("1\u{00e9}").is_err());
403 assert!(Timespan::parse("\u{1f600}s").is_err());
404 assert!(Timespan::parse("5\u{00fc}").is_err());
405 }
406
407 #[test]
408 fn test_timespan_overflow_no_panic() {
409 assert!(Timespan::parse("99999999999999999d").is_err());
411 assert!(Timespan::parse("999999999999999999h").is_err());
412 }
413}