Skip to main content

rsigma_parser/
version.rs

1//! Sigma specification version targeting (the `sigma-version` attribute).
2//!
3//! A Sigma document declares the specification MAJOR version it targets via the
4//! optional top-level `sigma-version` attribute (for example `sigma-version: 3`).
5//! Only the major is significant, because breaking spec changes occur only at
6//! major bumps. When the attribute is absent, the document resolves to a fixed
7//! floor ([`SPEC_VERSION_FLOOR`]): a constant defined by the specification rather
8//! than "the latest a tool supports", so an absent attribute means the same
9//! thing on every tool and the existing corpus is never silently reinterpreted.
10//!
11//! Version-sensitive interpretation is gated on the resolved major. The first
12//! such behavior is array-matching bracket semantics: a trailing `[...]` on a
13//! field path is an array selector only at [`SPEC_VERSION_ARRAY_MATCHING`] or
14//! later; below it, brackets are literal field-name characters. See
15//! [`array_matching_enabled`].
16
17use yaml_serde::Value;
18
19/// The fixed floor an absent `sigma-version` resolves to: the v2.x line that is
20/// current immediately before array matching (the first versioned breaking
21/// change). Existing rules carry no `sigma-version`, so they resolve here and
22/// keep their pre-array-matching semantics.
23pub const SPEC_VERSION_FLOOR: u32 = 2;
24
25/// The major in which array-matching bracket selectors become active. A rule
26/// must declare `sigma-version: 3` (or higher) to read `field[any]`, `args[0]`,
27/// and the other selectors as array selectors rather than literal field names.
28pub const SPEC_VERSION_ARRAY_MATCHING: u32 = 3;
29
30/// The highest specification major this build implements. A document declaring a
31/// major above this targets semantics the tool does not know, and should be
32/// rejected or skipped rather than interpreted under older rules.
33pub const SPEC_VERSION_SUPPORTED: u32 = 3;
34
35/// Resolve a declared major to its effective value: the declared major, or the
36/// fixed floor ([`SPEC_VERSION_FLOOR`]) when absent (`None`).
37#[must_use]
38pub fn resolve_major(declared: Option<u32>) -> u32 {
39    declared.unwrap_or(SPEC_VERSION_FLOOR)
40}
41
42/// Whether array-matching bracket selectors are enabled at the resolved major.
43#[must_use]
44pub fn array_matching_enabled(declared: Option<u32>) -> bool {
45    resolve_major(declared) >= SPEC_VERSION_ARRAY_MATCHING
46}
47
48/// Whether a declared major exceeds what this build supports
49/// ([`SPEC_VERSION_SUPPORTED`]). An absent version (`None`) is always supported,
50/// since it resolves to the floor.
51#[must_use]
52pub fn is_unsupported(declared: Option<u32>) -> bool {
53    matches!(declared, Some(major) if major > SPEC_VERSION_SUPPORTED)
54}
55
56/// Extract the specification major from a `sigma-version` YAML value.
57///
58/// Accepts an integer major (`3`), a float whose integer part is the major
59/// (`2.1` -> `2`), or a release string (`"3"`, `"2.1.0"`, `"v3"`). Only the
60/// major component is read. Returns `None` when the value cannot be interpreted
61/// as a version, so the caller can warn and treat it as absent.
62#[must_use]
63pub fn major_from_value(value: &Value) -> Option<u32> {
64    match value {
65        Value::Number(n) => {
66            if let Some(u) = n.as_u64() {
67                u32::try_from(u).ok()
68            } else if let Some(i) = n.as_i64() {
69                u32::try_from(i).ok()
70            } else if let Some(f) = n.as_f64() {
71                // A float like `2.1`: the integer part is the major.
72                if f.is_finite() && f >= 0.0 {
73                    Some(f.trunc() as u32)
74                } else {
75                    None
76                }
77            } else {
78                None
79            }
80        }
81        Value::String(s) => major_from_str(s),
82        _ => None,
83    }
84}
85
86/// Parse the major component out of a release string: the leading integer of the
87/// dotted version, ignoring an optional `v`/`V` prefix (`"v3.1"` -> `3`).
88#[must_use]
89pub fn major_from_str(s: &str) -> Option<u32> {
90    let head = s
91        .trim()
92        .trim_start_matches(['v', 'V'])
93        .split('.')
94        .next()
95        .unwrap_or("");
96    head.parse::<u32>().ok()
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn floor_and_gating() {
105        assert_eq!(resolve_major(None), SPEC_VERSION_FLOOR);
106        assert_eq!(resolve_major(Some(3)), 3);
107        assert!(!array_matching_enabled(None));
108        assert!(!array_matching_enabled(Some(2)));
109        assert!(array_matching_enabled(Some(3)));
110        assert!(array_matching_enabled(Some(4)));
111    }
112
113    #[test]
114    fn unsupported_major() {
115        assert!(!is_unsupported(None));
116        assert!(!is_unsupported(Some(SPEC_VERSION_SUPPORTED)));
117        assert!(is_unsupported(Some(SPEC_VERSION_SUPPORTED + 1)));
118    }
119
120    #[test]
121    fn major_parsing() {
122        assert_eq!(major_from_str("3"), Some(3));
123        assert_eq!(major_from_str("2.1.0"), Some(2));
124        assert_eq!(major_from_str("v3.1"), Some(3));
125        assert_eq!(major_from_str(" 2 "), Some(2));
126        assert_eq!(major_from_str("abc"), None);
127        assert_eq!(major_from_str(""), None);
128    }
129}