1use serde::{Deserialize, Serialize};
20
21use crate::hir::IrRule;
22
23pub const HIR_SCHEMA_VERSION: u32 = 1;
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct HirCacheHeader {
30 pub ir_schema_version: u32,
32 pub rsigma_version: String,
35}
36
37impl HirCacheHeader {
38 pub fn current() -> Self {
40 Self {
41 ir_schema_version: HIR_SCHEMA_VERSION,
42 rsigma_version: env!("CARGO_PKG_VERSION").to_string(),
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct HirCache {
51 pub header: HirCacheHeader,
52 pub rules: Vec<IrRule>,
53}
54
55#[derive(Debug, thiserror::Error)]
57pub enum CacheError {
58 #[error("HIR cache encode failed: {0}")]
59 Encode(String),
60 #[error("HIR cache decode failed: {0}")]
61 Decode(String),
62 #[error("HIR cache schema mismatch: blob is v{found}, this build expects v{expected}")]
63 SchemaMismatch { expected: u32, found: u32 },
64}
65
66pub fn encode_rules(rules: &[IrRule]) -> Result<Vec<u8>, CacheError> {
69 let mut buf = Vec::new();
70 ciborium::into_writer(&HirCacheHeader::current(), &mut buf)
71 .map_err(|e| CacheError::Encode(e.to_string()))?;
72 ciborium::into_writer(&rules, &mut buf).map_err(|e| CacheError::Encode(e.to_string()))?;
73 Ok(buf)
74}
75
76pub fn decode_rules(bytes: &[u8]) -> Result<Vec<IrRule>, CacheError> {
79 let mut cursor = std::io::Cursor::new(bytes);
80 let header: HirCacheHeader =
81 ciborium::from_reader(&mut cursor).map_err(|e| CacheError::Decode(e.to_string()))?;
82 if header.ir_schema_version != HIR_SCHEMA_VERSION {
83 return Err(CacheError::SchemaMismatch {
84 expected: HIR_SCHEMA_VERSION,
85 found: header.ir_schema_version,
86 });
87 }
88 ciborium::from_reader(&mut cursor).map_err(|e| CacheError::Decode(e.to_string()))
89}
90
91pub fn to_json(rules: &[IrRule]) -> Result<String, CacheError> {
93 let cache = HirCache {
94 header: HirCacheHeader::current(),
95 rules: rules.to_vec(),
96 };
97 serde_json::to_string_pretty(&cache).map_err(|e| CacheError::Encode(e.to_string()))
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::hir::{IrRule, IrRuleMetadata};
104 use rsigma_parser::LogSource;
105
106 fn sample_rule(title: &str) -> IrRule {
107 IrRule {
108 metadata: IrRuleMetadata {
109 title: title.to_string(),
110 ..Default::default()
111 },
112 logsource: LogSource::default(),
113 sigma_version: None,
114 detections: Default::default(),
115 conditions: Vec::new(),
116 }
117 }
118
119 #[test]
120 fn round_trips_through_postcard() {
121 let rules = vec![sample_rule("a"), sample_rule("b")];
122 let blob = encode_rules(&rules).unwrap();
123 let decoded = decode_rules(&blob).unwrap();
124 assert_eq!(decoded, rules);
125 }
126
127 #[test]
128 fn rejects_schema_mismatch() {
129 let rules = vec![sample_rule("a")];
130 let mut blob = Vec::new();
132 let header = HirCacheHeader {
133 ir_schema_version: HIR_SCHEMA_VERSION + 1,
134 rsigma_version: "test".to_string(),
135 };
136 ciborium::into_writer(&header, &mut blob).unwrap();
137 ciborium::into_writer(&rules, &mut blob).unwrap();
138
139 match decode_rules(&blob) {
140 Err(CacheError::SchemaMismatch { expected, found }) => {
141 assert_eq!(expected, HIR_SCHEMA_VERSION);
142 assert_eq!(found, HIR_SCHEMA_VERSION + 1);
143 }
144 other => panic!("expected schema mismatch, got {other:?}"),
145 }
146 }
147
148 #[test]
149 fn json_export_is_readable() {
150 let rules = vec![sample_rule("json")];
151 let json = to_json(&rules).unwrap();
152 assert!(json.contains("\"ir_schema_version\""));
153 assert!(json.contains("\"json\""));
154 }
155}