Skip to main content

stix_ffi/
handles.rs

1//! Opaque handles (`Pattern`, `Bundle`) and the plain `MatchOutcome` value.
2
3/// Opaque handle around a parsed pattern AST.
4#[derive(Debug, Clone)]
5pub struct Pattern {
6    inner: stix::pattern::Pattern,
7}
8
9impl Pattern {
10    pub(crate) fn new(inner: stix::pattern::Pattern) -> Self {
11        Pattern { inner }
12    }
13
14    pub(crate) fn inner(&self) -> &stix::pattern::Pattern {
15        &self.inner
16    }
17
18    /// The pattern's AST serialized as compact JSON.
19    pub fn to_json(&self) -> String {
20        // Serialization of the AST is infallible in practice; fall back to "null".
21        serde_json::to_string(&self.inner).unwrap_or_else(|_| "null".to_string())
22    }
23
24    /// The pattern lowered to the IR, serialized as compact JSON.
25    pub fn ir_json(&self) -> String {
26        // As with `to_json`, serialization is infallible in practice.
27        serde_json::to_string(&self.lowered()).unwrap_or_else(|_| "null".to_string())
28    }
29
30    /// The IR as a human-readable listing; see `Program::to_listing`.
31    pub fn ir_listing(&self) -> String {
32        self.lowered().to_listing()
33    }
34
35    /// Canonical pattern text, rendered back from the IR.
36    pub fn canonical(&self) -> String {
37        stix::pattern::ir::render(&self.lowered())
38    }
39
40    fn lowered(&self) -> stix::pattern::ir::Program {
41        stix::pattern::ir::lower(&self.inner)
42    }
43}
44
45/// Opaque handle around an imported bundle.
46#[derive(Debug, Clone)]
47pub struct Bundle {
48    inner: stix::model::Bundle,
49}
50
51impl Bundle {
52    pub(crate) fn new(inner: stix::model::Bundle) -> Self {
53        Bundle { inner }
54    }
55
56    pub(crate) fn inner(&self) -> &stix::model::Bundle {
57        &self.inner
58    }
59
60    /// Number of objects in the bundle.
61    pub fn object_count(&self) -> usize {
62        self.inner.objects.len()
63    }
64
65    /// The object at `index` serialized as JSON, or `None` if out of range.
66    pub fn object_json(&self, index: usize) -> Option<String> {
67        let obj = self.inner.objects.get(index)?;
68        Some(serde_json::to_string(obj).unwrap_or_else(|_| "null".to_string()))
69    }
70}
71
72/// The outcome of a match: whether it matched and which observation indices bound.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct MatchOutcome {
75    /// Whether the pattern matched.
76    pub matched: bool,
77    /// The indices of the observations that participated in the match.
78    pub observations: Vec<u64>,
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn pattern_to_json_round_trips() {
87        let inner = stix::parse("[ipv4-addr:value = '1.2.3.4']").unwrap();
88        let handle = Pattern::new(inner.clone());
89        let json = handle.to_json();
90        let back: stix::pattern::Pattern = serde_json::from_str(&json).unwrap();
91        assert_eq!(back, inner);
92    }
93
94    #[test]
95    fn bundle_object_access() {
96        let raw = r#"{"type":"bundle","id":"bundle--1","objects":[
97            {"type":"ipv4-addr","id":"ipv4-addr--1","value":"1.2.3.4"}
98        ]}"#;
99        let inner = stix::model::Bundle::from_json_str(raw).unwrap();
100        let handle = Bundle::new(inner);
101        assert_eq!(handle.object_count(), 1);
102        let obj_json = handle.object_json(0).unwrap();
103        assert!(obj_json.contains("ipv4-addr--1"));
104        assert!(handle.object_json(5).is_none());
105    }
106
107    #[test]
108    fn match_outcome_fields() {
109        let o = MatchOutcome {
110            matched: true,
111            observations: vec![0, 2],
112        };
113        assert!(o.matched);
114        assert_eq!(o.observations, vec![0, 2]);
115    }
116
117    const TWO_OBSERVATIONS: &str = "[file:size > 1024] OR [file:name = 'a']";
118
119    #[test]
120    fn pattern_ir_json_has_blocks_and_main() {
121        let handle = Pattern::new(stix::parse(TWO_OBSERVATIONS).unwrap());
122        let ir: serde_json::Value = serde_json::from_str(&handle.ir_json()).unwrap();
123        assert_eq!(ir["schema_version"], 1);
124        assert_eq!(ir["blocks"].as_array().unwrap().len(), 2);
125        assert_eq!(ir["main"]["kind"], "main");
126    }
127
128    #[test]
129    fn pattern_ir_listing_names_blocks() {
130        let handle = Pattern::new(stix::parse(TWO_OBSERVATIONS).unwrap());
131        let listing = handle.ir_listing();
132        assert!(listing.contains("block b1 (comparison):"), "{listing}");
133        assert!(listing.contains("block main (observation):"), "{listing}");
134    }
135
136    #[test]
137    fn pattern_canonical_normalizes_spacing() {
138        let handle = Pattern::new(stix::parse("[file:size>1024]  OR [file:name='a']").unwrap());
139        assert_eq!(handle.canonical(), TWO_OBSERVATIONS);
140    }
141}