Skip to main content

driven/streaming/
htip_delivery.rs

1//! HTIP-Inspired Rule Delivery
2//!
3//! Binary operation stream for rule synchronization.
4
5use bytemuck::{Pod, Zeroable};
6
7use crate::{DrivenError, Result};
8
9/// Rule operation types (similar to HTIP opcodes)
10#[repr(u8)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RuleOperation {
13    /// Define a new rule template
14    TemplateDefine = 0,
15    /// Instantiate a template with bindings
16    Instantiate = 1,
17    /// Update rule text content
18    PatchText = 2,
19    /// Update rule metadata
20    PatchMeta = 3,
21    /// Remove a rule
22    Remove = 4,
23    /// Begin batch transaction
24    BatchStart = 5,
25    /// Commit batch transaction
26    BatchCommit = 6,
27    /// Add a new section
28    AddSection = 7,
29    /// Reorder rules
30    Reorder = 8,
31    /// Full sync (replace all)
32    FullSync = 9,
33    /// End of stream marker
34    EndStream = 255,
35}
36
37impl From<u8> for RuleOperation {
38    fn from(v: u8) -> Self {
39        match v {
40            0 => RuleOperation::TemplateDefine,
41            1 => RuleOperation::Instantiate,
42            2 => RuleOperation::PatchText,
43            3 => RuleOperation::PatchMeta,
44            4 => RuleOperation::Remove,
45            5 => RuleOperation::BatchStart,
46            6 => RuleOperation::BatchCommit,
47            7 => RuleOperation::AddSection,
48            8 => RuleOperation::Reorder,
49            9 => RuleOperation::FullSync,
50            255 => RuleOperation::EndStream,
51            _ => RuleOperation::EndStream,
52        }
53    }
54}
55
56/// Operation header (8 bytes)
57#[repr(C)]
58#[derive(Debug, Clone, Copy, Pod, Zeroable)]
59pub struct OperationHeader {
60    /// Operation type
61    pub op: u8,
62    /// Flags
63    pub flags: u8,
64    /// Target rule ID
65    pub target_id: u16,
66    /// Payload length
67    pub payload_len: u32,
68}
69
70impl OperationHeader {
71    /// Create a new operation header
72    pub fn new(op: RuleOperation, target_id: u16, payload_len: u32) -> Self {
73        Self {
74            op: op as u8,
75            flags: 0,
76            target_id,
77            payload_len,
78        }
79    }
80
81    /// Size in bytes
82    pub const fn size() -> usize {
83        std::mem::size_of::<Self>()
84    }
85
86    /// Get operation type
87    pub fn operation(&self) -> RuleOperation {
88        RuleOperation::from(self.op)
89    }
90}
91
92/// HTIP-style rule delivery system
93#[derive(Debug)]
94pub struct HtipDelivery {
95    /// Pending operations
96    operations: Vec<(OperationHeader, Vec<u8>)>,
97    /// In batch mode
98    in_batch: bool,
99    /// Sequence number
100    sequence: u32,
101}
102
103impl HtipDelivery {
104    /// Create a new delivery instance
105    pub fn new() -> Self {
106        Self {
107            operations: Vec::new(),
108            in_batch: false,
109            sequence: 0,
110        }
111    }
112
113    /// Start a batch operation
114    pub fn begin_batch(&mut self) {
115        self.operations.push((
116            OperationHeader::new(RuleOperation::BatchStart, 0, 0),
117            Vec::new(),
118        ));
119        self.in_batch = true;
120    }
121
122    /// Commit the current batch
123    pub fn commit_batch(&mut self) {
124        if self.in_batch {
125            self.operations.push((
126                OperationHeader::new(RuleOperation::BatchCommit, 0, 0),
127                Vec::new(),
128            ));
129            self.in_batch = false;
130        }
131    }
132
133    /// Add a patch text operation
134    pub fn patch_text(&mut self, rule_id: u16, new_text: &[u8]) {
135        let header = OperationHeader::new(RuleOperation::PatchText, rule_id, new_text.len() as u32);
136        self.operations.push((header, new_text.to_vec()));
137        self.sequence += 1;
138    }
139
140    /// Add a patch metadata operation
141    pub fn patch_meta(&mut self, rule_id: u16, metadata: &[u8]) {
142        let header = OperationHeader::new(RuleOperation::PatchMeta, rule_id, metadata.len() as u32);
143        self.operations.push((header, metadata.to_vec()));
144        self.sequence += 1;
145    }
146
147    /// Add a remove operation
148    pub fn remove(&mut self, rule_id: u16) {
149        let header = OperationHeader::new(RuleOperation::Remove, rule_id, 0);
150        self.operations.push((header, Vec::new()));
151        self.sequence += 1;
152    }
153
154    /// Add a full sync operation
155    pub fn full_sync(&mut self, data: &[u8]) {
156        let header = OperationHeader::new(RuleOperation::FullSync, 0, data.len() as u32);
157        self.operations.push((header, data.to_vec()));
158        self.sequence += 1;
159    }
160
161    /// Serialize all operations to bytes
162    pub fn serialize(&self) -> Vec<u8> {
163        let mut output = Vec::new();
164
165        for (header, payload) in &self.operations {
166            output.extend_from_slice(bytemuck::bytes_of(header));
167            output.extend_from_slice(payload);
168        }
169
170        // End marker
171        let end = OperationHeader::new(RuleOperation::EndStream, 0, 0);
172        output.extend_from_slice(bytemuck::bytes_of(&end));
173
174        output
175    }
176
177    /// Clear all pending operations
178    pub fn clear(&mut self) {
179        self.operations.clear();
180        self.in_batch = false;
181    }
182
183    /// Get operation count
184    pub fn len(&self) -> usize {
185        self.operations.len()
186    }
187
188    /// Check if empty
189    pub fn is_empty(&self) -> bool {
190        self.operations.is_empty()
191    }
192
193    /// Get current sequence number
194    pub fn sequence(&self) -> u32 {
195        self.sequence
196    }
197}
198
199impl Default for HtipDelivery {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205/// HTIP stream reader
206#[derive(Debug)]
207pub struct HtipReader<'a> {
208    /// Input data
209    data: &'a [u8],
210    /// Current position
211    pos: usize,
212}
213
214impl<'a> HtipReader<'a> {
215    /// Create a new reader
216    pub fn new(data: &'a [u8]) -> Self {
217        Self { data, pos: 0 }
218    }
219
220    /// Read next operation
221    pub fn next(&mut self) -> Result<Option<(RuleOperation, u16, &'a [u8])>> {
222        if self.pos + OperationHeader::size() > self.data.len() {
223            return Ok(None);
224        }
225
226        let header_bytes = &self.data[self.pos..self.pos + OperationHeader::size()];
227        // Copy to aligned buffer to avoid alignment issues
228        let mut aligned = [0u8; 8];
229        aligned.copy_from_slice(header_bytes);
230        let header: OperationHeader = bytemuck::cast(aligned);
231        self.pos += OperationHeader::size();
232
233        let op = header.operation();
234        if op == RuleOperation::EndStream {
235            return Ok(None);
236        }
237
238        let payload_len = header.payload_len as usize;
239        if self.pos + payload_len > self.data.len() {
240            return Err(DrivenError::InvalidBinary("Truncated payload".into()));
241        }
242
243        let payload = &self.data[self.pos..self.pos + payload_len];
244        self.pos += payload_len;
245
246        Ok(Some((op, header.target_id, payload)))
247    }
248
249    /// Read all operations
250    pub fn read_all(&mut self) -> Result<Vec<(RuleOperation, u16, Vec<u8>)>> {
251        let mut ops = Vec::new();
252        while let Some((op, id, payload)) = self.next()? {
253            ops.push((op, id, payload.to_vec()));
254        }
255        Ok(ops)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_operation_header_size() {
265        assert_eq!(OperationHeader::size(), 8);
266    }
267
268    #[test]
269    fn test_roundtrip() {
270        let mut delivery = HtipDelivery::new();
271
272        delivery.begin_batch();
273        delivery.patch_text(1, b"hello");
274        delivery.patch_text(2, b"world");
275        delivery.commit_batch();
276
277        let bytes = delivery.serialize();
278
279        let mut reader = HtipReader::new(&bytes);
280        let ops = reader.read_all().unwrap();
281
282        assert_eq!(ops.len(), 4); // BatchStart, 2x PatchText, BatchCommit
283        assert_eq!(ops[0].0, RuleOperation::BatchStart);
284        assert_eq!(ops[1].0, RuleOperation::PatchText);
285        assert_eq!(ops[1].1, 1);
286        assert_eq!(ops[1].2, b"hello");
287    }
288}