1use bytemuck::{Pod, Zeroable};
6
7use crate::{DrivenError, Result};
8
9#[repr(u8)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RuleOperation {
13 TemplateDefine = 0,
15 Instantiate = 1,
17 PatchText = 2,
19 PatchMeta = 3,
21 Remove = 4,
23 BatchStart = 5,
25 BatchCommit = 6,
27 AddSection = 7,
29 Reorder = 8,
31 FullSync = 9,
33 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#[repr(C)]
58#[derive(Debug, Clone, Copy, Pod, Zeroable)]
59pub struct OperationHeader {
60 pub op: u8,
62 pub flags: u8,
64 pub target_id: u16,
66 pub payload_len: u32,
68}
69
70impl OperationHeader {
71 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 pub const fn size() -> usize {
83 std::mem::size_of::<Self>()
84 }
85
86 pub fn operation(&self) -> RuleOperation {
88 RuleOperation::from(self.op)
89 }
90}
91
92#[derive(Debug)]
94pub struct HtipDelivery {
95 operations: Vec<(OperationHeader, Vec<u8>)>,
97 in_batch: bool,
99 sequence: u32,
101}
102
103impl HtipDelivery {
104 pub fn new() -> Self {
106 Self {
107 operations: Vec::new(),
108 in_batch: false,
109 sequence: 0,
110 }
111 }
112
113 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 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 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 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 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 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 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 let end = OperationHeader::new(RuleOperation::EndStream, 0, 0);
172 output.extend_from_slice(bytemuck::bytes_of(&end));
173
174 output
175 }
176
177 pub fn clear(&mut self) {
179 self.operations.clear();
180 self.in_batch = false;
181 }
182
183 pub fn len(&self) -> usize {
185 self.operations.len()
186 }
187
188 pub fn is_empty(&self) -> bool {
190 self.operations.is_empty()
191 }
192
193 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#[derive(Debug)]
207pub struct HtipReader<'a> {
208 data: &'a [u8],
210 pos: usize,
212}
213
214impl<'a> HtipReader<'a> {
215 pub fn new(data: &'a [u8]) -> Self {
217 Self { data, pos: 0 }
218 }
219
220 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 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 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); 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}