1use bytemuck::{Pod, Zeroable};
6
7use crate::{DrivenError, Result};
8
9use super::{FUSION_MAGIC, FUSION_VERSION};
10
11#[repr(C)]
13#[derive(Debug, Clone, Copy, Pod, Zeroable)]
14pub struct FusionHeader {
15 pub magic: [u8; 4],
17 pub version: u16,
19 pub flags: u16,
21 pub slot_count: u32,
23 pub string_table_offset: u32,
25 pub slot_table_offset: u32,
27 pub content_offset: u32,
29 pub template_hash: u64,
31 pub source_hash: u64,
33 pub timestamp: u64,
35 pub _reserved: [u8; 16],
37}
38
39impl FusionHeader {
40 pub fn new(slot_count: u32, template_hash: u64, source_hash: u64) -> Self {
42 Self {
43 magic: *FUSION_MAGIC,
44 version: FUSION_VERSION,
45 flags: 0,
46 slot_count,
47 string_table_offset: Self::size() as u32,
48 slot_table_offset: 0,
49 content_offset: 0,
50 template_hash,
51 source_hash,
52 timestamp: std::time::SystemTime::now()
53 .duration_since(std::time::UNIX_EPOCH)
54 .unwrap_or_default()
55 .as_secs(),
56 _reserved: [0; 16],
57 }
58 }
59
60 pub fn from_bytes(data: &[u8]) -> Result<&Self> {
62 if data.len() < Self::size() {
63 return Err(DrivenError::InvalidBinary("Fusion header too small".into()));
64 }
65
66 let header: &Self = bytemuck::from_bytes(&data[..Self::size()]);
67
68 if &header.magic != FUSION_MAGIC {
69 return Err(DrivenError::InvalidBinary(
70 "Invalid fusion magic bytes".into(),
71 ));
72 }
73
74 Ok(header)
75 }
76
77 pub const fn size() -> usize {
79 std::mem::size_of::<Self>()
80 }
81
82 pub fn to_bytes(&self) -> &[u8] {
84 bytemuck::bytes_of(self)
85 }
86}
87
88#[repr(C)]
90#[derive(Debug, Clone, Copy, Pod, Zeroable)]
91pub struct TemplateSlot {
92 pub slot_id: u16,
94 pub slot_type: u8,
96 pub flags: u8,
98 pub name: u32,
100 pub content_offset: u32,
102 pub content_length: u32,
104}
105
106impl TemplateSlot {
107 pub const fn size() -> usize {
109 std::mem::size_of::<Self>()
110 }
111}
112
113#[repr(u8)]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum SlotType {
117 Text = 0,
119 Variable = 1,
121 Conditional = 2,
123 Loop = 3,
125 Include = 4,
127}
128
129impl From<u8> for SlotType {
130 fn from(v: u8) -> Self {
131 match v {
132 0 => SlotType::Text,
133 1 => SlotType::Variable,
134 2 => SlotType::Conditional,
135 3 => SlotType::Loop,
136 4 => SlotType::Include,
137 _ => SlotType::Text,
138 }
139 }
140}
141
142#[derive(Debug)]
144pub struct FusionModule<'a> {
145 pub header: &'a FusionHeader,
147 data: &'a [u8],
149}
150
151impl<'a> FusionModule<'a> {
152 pub fn from_bytes(data: &'a [u8]) -> Result<Self> {
154 let header = FusionHeader::from_bytes(data)?;
155 Ok(Self { header, data })
156 }
157
158 pub fn slots(&self) -> impl Iterator<Item = &'a TemplateSlot> {
160 let offset = self.header.slot_table_offset as usize;
161 let count = self.header.slot_count as usize;
162
163 if offset + count * TemplateSlot::size() > self.data.len() {
164 return [].iter();
165 }
166
167 let slot_data = &self.data[offset..offset + count * TemplateSlot::size()];
168 let slots: &[TemplateSlot] = bytemuck::cast_slice(slot_data);
169 slots.iter()
170 }
171
172 pub fn get_slot(&self, slot_id: u16) -> Option<&'a TemplateSlot> {
174 self.slots().find(|s| s.slot_id == slot_id)
175 }
176
177 pub fn slot_content(&self, slot: &TemplateSlot) -> Option<&'a [u8]> {
179 let start = self.header.content_offset as usize + slot.content_offset as usize;
180 let end = start + slot.content_length as usize;
181
182 if end > self.data.len() {
183 return None;
184 }
185
186 Some(&self.data[start..end])
187 }
188
189 pub fn template_hash(&self) -> u64 {
191 self.header.template_hash
192 }
193
194 pub fn source_hash(&self) -> u64 {
196 self.header.source_hash
197 }
198
199 pub fn is_stale(&self, current_source_hash: u64) -> bool {
201 self.header.source_hash != current_source_hash
202 }
203}
204
205#[derive(Debug)]
207pub struct FusionBuilder {
208 slots: Vec<BuiltSlot>,
210 strings: Vec<String>,
212 content: Vec<u8>,
214 template_hash: u64,
216 source_hash: u64,
218}
219
220#[derive(Debug)]
221struct BuiltSlot {
222 slot_type: SlotType,
223 name: String,
224 content: Vec<u8>,
225}
226
227impl FusionBuilder {
228 pub fn new(template_hash: u64, source_hash: u64) -> Self {
230 Self {
231 slots: Vec::new(),
232 strings: Vec::new(),
233 content: Vec::new(),
234 template_hash,
235 source_hash,
236 }
237 }
238
239 pub fn add_text(&mut self, name: &str, content: &str) -> u16 {
241 let slot_id = self.slots.len() as u16;
242 self.slots.push(BuiltSlot {
243 slot_type: SlotType::Text,
244 name: name.to_string(),
245 content: content.as_bytes().to_vec(),
246 });
247 slot_id
248 }
249
250 pub fn add_variable(&mut self, name: &str) -> u16 {
252 let slot_id = self.slots.len() as u16;
253 self.slots.push(BuiltSlot {
254 slot_type: SlotType::Variable,
255 name: name.to_string(),
256 content: Vec::new(),
257 });
258 slot_id
259 }
260
261 pub fn build(self) -> Vec<u8> {
263 let mut header = FusionHeader::new(
264 self.slots.len() as u32,
265 self.template_hash,
266 self.source_hash,
267 );
268
269 let mut string_bytes = Vec::new();
271 let mut string_offsets = Vec::new();
272
273 for slot in &self.slots {
274 string_offsets.push(string_bytes.len() as u32);
275 string_bytes.extend_from_slice(slot.name.as_bytes());
276 }
277
278 let mut slot_bytes = Vec::new();
280 let mut content_bytes = Vec::new();
281
282 for (i, slot) in self.slots.iter().enumerate() {
283 let slot_entry = TemplateSlot {
284 slot_id: i as u16,
285 slot_type: slot.slot_type as u8,
286 flags: 0,
287 name: string_offsets.get(i).copied().unwrap_or(0),
288 content_offset: content_bytes.len() as u32,
289 content_length: slot.content.len() as u32,
290 };
291 slot_bytes.extend_from_slice(bytemuck::bytes_of(&slot_entry));
292 content_bytes.extend_from_slice(&slot.content);
293 }
294
295 header.string_table_offset = FusionHeader::size() as u32;
297 header.slot_table_offset = header.string_table_offset + string_bytes.len() as u32;
298 header.content_offset = header.slot_table_offset + slot_bytes.len() as u32;
299
300 let mut output = Vec::new();
302 output.extend_from_slice(header.to_bytes());
303 output.extend_from_slice(&string_bytes);
304 output.extend_from_slice(&slot_bytes);
305 output.extend_from_slice(&content_bytes);
306
307 output
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_header_size() {
317 assert_eq!(FusionHeader::size(), 64);
318 }
319
320 #[test]
321 fn test_slot_size() {
322 assert_eq!(TemplateSlot::size(), 16);
323 }
324
325 #[test]
326 fn test_roundtrip() {
327 let mut builder = FusionBuilder::new(12345, 67890);
328 builder.add_text("greeting", "Hello, World!");
329 builder.add_variable("name");
330
331 let bytes = builder.build();
332 let module = FusionModule::from_bytes(&bytes).unwrap();
333
334 assert_eq!(module.header.slot_count, 2);
335 assert_eq!(module.template_hash(), 12345);
336 }
337}