use bytemuck::{Pod, Zeroable};
use crate::{DrivenError, Result};
use super::{FUSION_MAGIC, FUSION_VERSION};
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct FusionHeader {
pub magic: [u8; 4],
pub version: u16,
pub flags: u16,
pub slot_count: u32,
pub string_table_offset: u32,
pub slot_table_offset: u32,
pub content_offset: u32,
pub template_hash: u64,
pub source_hash: u64,
pub timestamp: u64,
pub _reserved: [u8; 16],
}
impl FusionHeader {
pub fn new(slot_count: u32, template_hash: u64, source_hash: u64) -> Self {
Self {
magic: *FUSION_MAGIC,
version: FUSION_VERSION,
flags: 0,
slot_count,
string_table_offset: Self::size() as u32,
slot_table_offset: 0,
content_offset: 0,
template_hash,
source_hash,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
_reserved: [0; 16],
}
}
pub fn from_bytes(data: &[u8]) -> Result<&Self> {
if data.len() < Self::size() {
return Err(DrivenError::InvalidBinary("Fusion header too small".into()));
}
let header: &Self = bytemuck::from_bytes(&data[..Self::size()]);
if &header.magic != FUSION_MAGIC {
return Err(DrivenError::InvalidBinary(
"Invalid fusion magic bytes".into(),
));
}
Ok(header)
}
pub const fn size() -> usize {
std::mem::size_of::<Self>()
}
pub fn to_bytes(&self) -> &[u8] {
bytemuck::bytes_of(self)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct TemplateSlot {
pub slot_id: u16,
pub slot_type: u8,
pub flags: u8,
pub name: u32,
pub content_offset: u32,
pub content_length: u32,
}
impl TemplateSlot {
pub const fn size() -> usize {
std::mem::size_of::<Self>()
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotType {
Text = 0,
Variable = 1,
Conditional = 2,
Loop = 3,
Include = 4,
}
impl From<u8> for SlotType {
fn from(v: u8) -> Self {
match v {
0 => SlotType::Text,
1 => SlotType::Variable,
2 => SlotType::Conditional,
3 => SlotType::Loop,
4 => SlotType::Include,
_ => SlotType::Text,
}
}
}
#[derive(Debug)]
pub struct FusionModule<'a> {
pub header: &'a FusionHeader,
data: &'a [u8],
}
impl<'a> FusionModule<'a> {
pub fn from_bytes(data: &'a [u8]) -> Result<Self> {
let header = FusionHeader::from_bytes(data)?;
Ok(Self { header, data })
}
pub fn slots(&self) -> impl Iterator<Item = &'a TemplateSlot> {
let offset = self.header.slot_table_offset as usize;
let count = self.header.slot_count as usize;
if offset + count * TemplateSlot::size() > self.data.len() {
return [].iter();
}
let slot_data = &self.data[offset..offset + count * TemplateSlot::size()];
let slots: &[TemplateSlot] = bytemuck::cast_slice(slot_data);
slots.iter()
}
pub fn get_slot(&self, slot_id: u16) -> Option<&'a TemplateSlot> {
self.slots().find(|s| s.slot_id == slot_id)
}
pub fn slot_content(&self, slot: &TemplateSlot) -> Option<&'a [u8]> {
let start = self.header.content_offset as usize + slot.content_offset as usize;
let end = start + slot.content_length as usize;
if end > self.data.len() {
return None;
}
Some(&self.data[start..end])
}
pub fn template_hash(&self) -> u64 {
self.header.template_hash
}
pub fn source_hash(&self) -> u64 {
self.header.source_hash
}
pub fn is_stale(&self, current_source_hash: u64) -> bool {
self.header.source_hash != current_source_hash
}
}
#[derive(Debug)]
pub struct FusionBuilder {
slots: Vec<BuiltSlot>,
strings: Vec<String>,
content: Vec<u8>,
template_hash: u64,
source_hash: u64,
}
#[derive(Debug)]
struct BuiltSlot {
slot_type: SlotType,
name: String,
content: Vec<u8>,
}
impl FusionBuilder {
pub fn new(template_hash: u64, source_hash: u64) -> Self {
Self {
slots: Vec::new(),
strings: Vec::new(),
content: Vec::new(),
template_hash,
source_hash,
}
}
pub fn add_text(&mut self, name: &str, content: &str) -> u16 {
let slot_id = self.slots.len() as u16;
self.slots.push(BuiltSlot {
slot_type: SlotType::Text,
name: name.to_string(),
content: content.as_bytes().to_vec(),
});
slot_id
}
pub fn add_variable(&mut self, name: &str) -> u16 {
let slot_id = self.slots.len() as u16;
self.slots.push(BuiltSlot {
slot_type: SlotType::Variable,
name: name.to_string(),
content: Vec::new(),
});
slot_id
}
pub fn build(self) -> Vec<u8> {
let mut header = FusionHeader::new(
self.slots.len() as u32,
self.template_hash,
self.source_hash,
);
let mut string_bytes = Vec::new();
let mut string_offsets = Vec::new();
for slot in &self.slots {
string_offsets.push(string_bytes.len() as u32);
string_bytes.extend_from_slice(slot.name.as_bytes());
}
let mut slot_bytes = Vec::new();
let mut content_bytes = Vec::new();
for (i, slot) in self.slots.iter().enumerate() {
let slot_entry = TemplateSlot {
slot_id: i as u16,
slot_type: slot.slot_type as u8,
flags: 0,
name: string_offsets.get(i).copied().unwrap_or(0),
content_offset: content_bytes.len() as u32,
content_length: slot.content.len() as u32,
};
slot_bytes.extend_from_slice(bytemuck::bytes_of(&slot_entry));
content_bytes.extend_from_slice(&slot.content);
}
header.string_table_offset = FusionHeader::size() as u32;
header.slot_table_offset = header.string_table_offset + string_bytes.len() as u32;
header.content_offset = header.slot_table_offset + slot_bytes.len() as u32;
let mut output = Vec::new();
output.extend_from_slice(header.to_bytes());
output.extend_from_slice(&string_bytes);
output.extend_from_slice(&slot_bytes);
output.extend_from_slice(&content_bytes);
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_size() {
assert_eq!(FusionHeader::size(), 64);
}
#[test]
fn test_slot_size() {
assert_eq!(TemplateSlot::size(), 16);
}
#[test]
fn test_roundtrip() {
let mut builder = FusionBuilder::new(12345, 67890);
builder.add_text("greeting", "Hello, World!");
builder.add_variable("name");
let bytes = builder.build();
let module = FusionModule::from_bytes(&bytes).unwrap();
assert_eq!(module.header.slot_count, 2);
assert_eq!(module.template_hash(), 12345);
}
}