Skip to main content

driven/fusion/
template_module.rs

1//! Pre-Compiled Template Module (.dtm format)
2//!
3//! Binary template format for instant loading.
4
5use bytemuck::{Pod, Zeroable};
6
7use crate::{DrivenError, Result};
8
9use super::{FUSION_MAGIC, FUSION_VERSION};
10
11/// Fusion header (64 bytes)
12#[repr(C)]
13#[derive(Debug, Clone, Copy, Pod, Zeroable)]
14pub struct FusionHeader {
15    /// Magic: "DRVF" (4 bytes)
16    pub magic: [u8; 4],
17    /// Version (2 bytes)
18    pub version: u16,
19    /// Flags (2 bytes)
20    pub flags: u16,
21    /// Number of slots (4 bytes)
22    pub slot_count: u32,
23    /// String table offset (4 bytes)
24    pub string_table_offset: u32,
25    /// Slot table offset (4 bytes)
26    pub slot_table_offset: u32,
27    /// Content offset (4 bytes)
28    pub content_offset: u32,
29    /// Template ID hash (8 bytes)
30    pub template_hash: u64,
31    /// Source file hash (8 bytes)
32    pub source_hash: u64,
33    /// Timestamp (8 bytes)
34    pub timestamp: u64,
35    /// Reserved (16 bytes)
36    pub _reserved: [u8; 16],
37}
38
39impl FusionHeader {
40    /// Create a new header
41    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    /// Parse from bytes
61    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    /// Header size
78    pub const fn size() -> usize {
79        std::mem::size_of::<Self>()
80    }
81
82    /// Serialize to bytes
83    pub fn to_bytes(&self) -> &[u8] {
84        bytemuck::bytes_of(self)
85    }
86}
87
88/// Template slot (16 bytes)
89#[repr(C)]
90#[derive(Debug, Clone, Copy, Pod, Zeroable)]
91pub struct TemplateSlot {
92    /// Slot ID
93    pub slot_id: u16,
94    /// Slot type
95    pub slot_type: u8,
96    /// Flags
97    pub flags: u8,
98    /// Name string index
99    pub name: u32,
100    /// Content offset
101    pub content_offset: u32,
102    /// Content length
103    pub content_length: u32,
104}
105
106impl TemplateSlot {
107    /// Size in bytes
108    pub const fn size() -> usize {
109        std::mem::size_of::<Self>()
110    }
111}
112
113/// Slot types
114#[repr(u8)]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum SlotType {
117    /// Text content
118    Text = 0,
119    /// Variable placeholder
120    Variable = 1,
121    /// Conditional block
122    Conditional = 2,
123    /// Loop block
124    Loop = 3,
125    /// Include directive
126    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/// Fusion module (compiled template)
143#[derive(Debug)]
144pub struct FusionModule<'a> {
145    /// Header
146    pub header: &'a FusionHeader,
147    /// Raw data
148    data: &'a [u8],
149}
150
151impl<'a> FusionModule<'a> {
152    /// Load from bytes (zero-copy)
153    pub fn from_bytes(data: &'a [u8]) -> Result<Self> {
154        let header = FusionHeader::from_bytes(data)?;
155        Ok(Self { header, data })
156    }
157
158    /// Get all slots
159    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    /// Get slot by ID
173    pub fn get_slot(&self, slot_id: u16) -> Option<&'a TemplateSlot> {
174        self.slots().find(|s| s.slot_id == slot_id)
175    }
176
177    /// Get slot content
178    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    /// Template hash
190    pub fn template_hash(&self) -> u64 {
191        self.header.template_hash
192    }
193
194    /// Source hash for cache invalidation
195    pub fn source_hash(&self) -> u64 {
196        self.header.source_hash
197    }
198
199    /// Check if cache is stale
200    pub fn is_stale(&self, current_source_hash: u64) -> bool {
201        self.header.source_hash != current_source_hash
202    }
203}
204
205/// Fusion module builder
206#[derive(Debug)]
207pub struct FusionBuilder {
208    /// Slots
209    slots: Vec<BuiltSlot>,
210    /// String table
211    strings: Vec<String>,
212    /// Content buffer
213    content: Vec<u8>,
214    /// Template hash
215    template_hash: u64,
216    /// Source hash
217    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    /// Create a new builder
229    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    /// Add a text slot
240    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    /// Add a variable slot
251    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    /// Build the fusion module
262    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        // Build string table
270        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        // Build slot table
279        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        // Calculate offsets
296        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        // Assemble output
301        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}