use super::ion::{IonValue, IonWriter};
use super::symbols::KfxSymbol;
pub struct SerializedEntity<'a> {
pub id: u32,
pub entity_type: u32,
pub data: Vec<u8>,
pub raw: Option<&'a [u8]>,
}
impl SerializedEntity<'_> {
pub fn len(&self) -> usize {
self.data.len() + self.raw.map_or(0, <[u8]>::len)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
const CONTAINER_MAGIC: &[u8; 4] = b"CONT";
const ENTITY_MAGIC: &[u8; 4] = b"ENTY";
#[allow(clippy::vec_init_then_push)]
pub fn serialize_container(
container_id: &str,
entities: &[SerializedEntity],
symtab_ion: &[u8],
format_caps_ion: &[u8],
) -> Vec<u8> {
let mut entity_table = Vec::new();
let mut current_offset = 0u64;
for entity in entities {
entity_table.extend_from_slice(&entity.id.to_le_bytes());
entity_table.extend_from_slice(&entity.entity_type.to_le_bytes());
entity_table.extend_from_slice(¤t_offset.to_le_bytes());
entity_table.extend_from_slice(&(entity.len() as u64).to_le_bytes());
current_offset += entity.len() as u64;
}
let mut sha = sha1_smol::Sha1::new();
for entity in entities {
sha.update(&entity.data);
if let Some(raw) = entity.raw {
sha.update(raw);
}
}
let payload_sha1 = sha.digest().to_string();
const HEADER_SIZE: usize = 18;
let entity_table_offset = HEADER_SIZE;
let symtab_offset = entity_table_offset + entity_table.len();
let format_caps_offset = symtab_offset + symtab_ion.len();
let mut container_info_fields = Vec::new();
container_info_fields.push((
KfxSymbol::Bccontid as u64,
IonValue::String(container_id.to_string()),
));
container_info_fields.push((KfxSymbol::Bccomprtype as u64, IonValue::Int(0)));
container_info_fields.push((KfxSymbol::Bcdrmscheme as u64, IonValue::Int(0)));
container_info_fields.push((KfxSymbol::Bcchunksize as u64, IonValue::Int(4096)));
container_info_fields.push((
KfxSymbol::Bcindextaboffset as u64,
IonValue::Int(entity_table_offset as i64),
));
container_info_fields.push((
KfxSymbol::Bcindextablength as u64,
IonValue::Int(entity_table.len() as i64),
));
container_info_fields.push((
KfxSymbol::Bcdocsymboloffset as u64,
IonValue::Int(symtab_offset as i64),
));
container_info_fields.push((
KfxSymbol::Bcdocsymbollength as u64,
IonValue::Int(symtab_ion.len() as i64),
));
if !format_caps_ion.is_empty() {
container_info_fields.push((
KfxSymbol::Bcfcapabilitiesoffset as u64,
IonValue::Int(format_caps_offset as i64),
));
container_info_fields.push((
KfxSymbol::Bcfcapabilitieslength as u64,
IonValue::Int(format_caps_ion.len() as i64),
));
}
let mut ion_writer = IonWriter::new();
ion_writer.write_bvm();
ion_writer.write_value(&IonValue::Struct(container_info_fields));
let container_info_data = ion_writer.into_bytes();
let container_info_offset = format_caps_offset + format_caps_ion.len();
let kfxgen_info = format!(
r#"[{{key:"kfxgen_package_version",value:"boko-{}"}},{{key:"kfxgen_application_version",value:"boko"}},{{key:"kfxgen_payload_sha1",value:"{}"}},{{key:"kfxgen_acr",value:"{}"}}]"#,
env!("CARGO_PKG_VERSION"),
payload_sha1,
container_id
);
let header_len = container_info_offset + container_info_data.len() + kfxgen_info.len();
let mut output = Vec::with_capacity(header_len + current_offset as usize);
output.extend_from_slice(CONTAINER_MAGIC);
output.extend_from_slice(&2u16.to_le_bytes()); output.extend_from_slice(&(header_len as u32).to_le_bytes());
output.extend_from_slice(&(container_info_offset as u32).to_le_bytes());
output.extend_from_slice(&(container_info_data.len() as u32).to_le_bytes());
output.extend_from_slice(&entity_table);
output.extend_from_slice(symtab_ion);
output.extend_from_slice(format_caps_ion);
output.extend_from_slice(&container_info_data);
output.extend_from_slice(kfxgen_info.as_bytes());
for entity in entities {
output.extend_from_slice(&entity.data);
if let Some(raw) = entity.raw {
output.extend_from_slice(raw);
}
}
output
}
pub fn create_entity_data(value: &IonValue) -> Vec<u8> {
let header_fields = vec![
(KfxSymbol::Bccomprtype as u64, IonValue::Int(0)),
(KfxSymbol::Bcdrmscheme as u64, IonValue::Int(0)),
];
let mut header_writer = IonWriter::new();
header_writer.write_bvm();
header_writer.write_value(&IonValue::Struct(header_fields));
let header_ion = header_writer.into_bytes();
let mut content_writer = IonWriter::new();
content_writer.write_bvm();
content_writer.write_value(value);
let content_ion = content_writer.into_bytes();
let header_len = 10 + header_ion.len();
let mut data = Vec::with_capacity(header_len + content_ion.len());
data.extend_from_slice(ENTITY_MAGIC);
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&(header_len as u32).to_le_bytes());
data.extend_from_slice(&header_ion);
data.extend_from_slice(&content_ion);
data
}
pub fn create_raw_media_header() -> Vec<u8> {
let header_fields = vec![
(KfxSymbol::Bccomprtype as u64, IonValue::Int(0)),
(KfxSymbol::Bcdrmscheme as u64, IonValue::Int(0)),
];
let mut header_writer = IonWriter::new();
header_writer.write_bvm();
header_writer.write_value(&IonValue::Struct(header_fields));
let header_ion = header_writer.into_bytes();
let header_len = 10 + header_ion.len();
let mut data = Vec::with_capacity(header_len);
data.extend_from_slice(ENTITY_MAGIC);
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&(header_len as u32).to_le_bytes());
data.extend_from_slice(&header_ion);
data
}
pub fn serialize_annotated_ion(annotation_id: u64, value: &IonValue) -> Vec<u8> {
let annotated = IonValue::Annotated(vec![annotation_id], Box::new(value.clone()));
let mut writer = IonWriter::new();
writer.write_bvm();
writer.write_value(&annotated);
writer.into_bytes()
}
pub fn generate_container_id(seed: &str) -> String {
let digest = sha1_smol::Sha1::from(seed.as_bytes()).digest().bytes();
let mut state = u128::from_be_bytes(digest[..16].try_into().expect("sha1 digest >= 16 bytes"));
const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut id = String::from("CR!");
for _ in 0..28 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let idx = ((state >> 56) as usize) % CHARS.len();
id.push(CHARS[idx] as char);
}
id
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_container_id_format() {
let id = generate_container_id("test-seed");
assert!(id.starts_with("CR!"));
assert_eq!(id.len(), 31);
let suffix = &id[3..];
assert!(
suffix
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()),
"Container ID should only contain uppercase alphanumeric: {}",
id
);
}
#[test]
fn test_create_entity_data() {
let value = IonValue::Struct(vec![(
KfxSymbol::Title as u64,
IonValue::String("Test".into()),
)]);
let data = create_entity_data(&value);
assert_eq!(&data[..4], b"ENTY");
assert_eq!(u16::from_le_bytes([data[4], data[5]]), 1);
}
#[test]
fn test_create_raw_media_header() {
let header = create_raw_media_header();
assert_eq!(&header[..4], b"ENTY");
let header_len = u32::from_le_bytes([header[6], header[7], header[8], header[9]]);
assert_eq!(header_len as usize, header.len());
}
#[test]
fn test_serialize_annotated_ion() {
let value = IonValue::List(vec![IonValue::String("symbol1".into())]);
let data = serialize_annotated_ion(3, &value);
assert_eq!(&data[..4], &[0xe0, 0x01, 0x00, 0xea]);
}
}