#![allow(dead_code)]
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExportMode {
Embedded,
ExternalRef,
Mixed,
}
impl ExportMode {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Embedded => "Embedded",
Self::ExternalRef => "External Reference",
Self::Mixed => "Mixed",
}
}
#[must_use]
pub const fn may_embed(&self) -> bool {
matches!(self, Self::Embedded | Self::Mixed)
}
}
impl std::fmt::Display for ExportMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone)]
pub struct AafExportConfig {
mode: ExportMode,
version_major: u16,
version_minor: u16,
external_threshold: u64,
application_name: Option<String>,
custom_properties: HashMap<String, String>,
}
impl AafExportConfig {
#[must_use]
pub fn new() -> Self {
Self {
mode: ExportMode::Embedded,
version_major: 1,
version_minor: 2,
external_threshold: 10 * 1024 * 1024, application_name: None,
custom_properties: HashMap::new(),
}
}
#[must_use]
pub fn with_mode(mut self, mode: ExportMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_version(mut self, major: u16, minor: u16) -> Self {
self.version_major = major;
self.version_minor = minor;
self
}
#[must_use]
pub fn with_external_threshold(mut self, bytes: u64) -> Self {
self.external_threshold = bytes;
self
}
#[must_use]
pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
self.application_name = Some(name.into());
self
}
pub fn add_property(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.custom_properties.insert(key.into(), value.into());
}
#[must_use]
pub fn mode(&self) -> ExportMode {
self.mode
}
#[must_use]
pub fn version(&self) -> (u16, u16) {
(self.version_major, self.version_minor)
}
#[must_use]
pub fn external_threshold(&self) -> u64 {
self.external_threshold
}
#[must_use]
pub fn application_name(&self) -> Option<&str> {
self.application_name.as_deref()
}
#[must_use]
pub fn custom_properties(&self) -> &HashMap<String, String> {
&self.custom_properties
}
}
impl Default for AafExportConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct AafExporter {
config: AafExportConfig,
session_id: Uuid,
}
impl AafExporter {
#[must_use]
pub fn new(config: AafExportConfig) -> Self {
Self {
config,
session_id: Uuid::new_v4(),
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(AafExportConfig::new())
}
#[must_use]
pub fn session_id(&self) -> Uuid {
self.session_id
}
#[must_use]
pub fn config(&self) -> &AafExportConfig {
&self.config
}
#[must_use]
pub fn write_header(&self) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::with_capacity(256);
let magic = b"AAF-OxiMedia ";
buf.extend_from_slice(magic);
buf.extend_from_slice(&self.config.version_major.to_be_bytes());
buf.extend_from_slice(&self.config.version_minor.to_be_bytes());
buf.extend_from_slice(self.session_id.as_bytes());
let mode_byte: u8 = match self.config.mode {
ExportMode::Embedded => 0,
ExportMode::ExternalRef => 1,
ExportMode::Mixed => 2,
};
buf.push(mode_byte);
if let Some(name) = &self.config.application_name {
let name_bytes = name.as_bytes();
let len = name_bytes.len().min(u16::MAX as usize) as u16;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(&name_bytes[..len as usize]);
} else {
buf.extend_from_slice(&0u16.to_be_bytes());
}
let prop_count = self.config.custom_properties.len().min(u16::MAX as usize) as u16;
buf.extend_from_slice(&prop_count.to_be_bytes());
for (k, v) in &self.config.custom_properties {
let kb = k.as_bytes();
let kl = kb.len().min(u16::MAX as usize) as u16;
buf.extend_from_slice(&kl.to_be_bytes());
buf.extend_from_slice(&kb[..kl as usize]);
let vb = v.as_bytes();
let vl = vb.len().min(u16::MAX as usize) as u16;
buf.extend_from_slice(&vl.to_be_bytes());
buf.extend_from_slice(&vb[..vl as usize]);
}
buf
}
#[must_use]
pub fn min_header_size(&self) -> usize {
41
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_export_mode_label() {
assert_eq!(ExportMode::Embedded.label(), "Embedded");
assert_eq!(ExportMode::ExternalRef.label(), "External Reference");
assert_eq!(ExportMode::Mixed.label(), "Mixed");
}
#[test]
fn test_export_mode_may_embed() {
assert!(ExportMode::Embedded.may_embed());
assert!(ExportMode::Mixed.may_embed());
assert!(!ExportMode::ExternalRef.may_embed());
}
#[test]
fn test_export_mode_display() {
assert_eq!(format!("{}", ExportMode::Mixed), "Mixed");
}
#[test]
fn test_config_defaults() {
let cfg = AafExportConfig::new();
assert_eq!(cfg.mode(), ExportMode::Embedded);
assert_eq!(cfg.version(), (1, 2));
assert_eq!(cfg.external_threshold(), 10 * 1024 * 1024);
assert!(cfg.application_name().is_none());
assert!(cfg.custom_properties().is_empty());
}
#[test]
fn test_config_builders() {
let cfg = AafExportConfig::new()
.with_mode(ExportMode::ExternalRef)
.with_version(2, 0)
.with_external_threshold(5_000_000)
.with_application_name("TestApp");
assert_eq!(cfg.mode(), ExportMode::ExternalRef);
assert_eq!(cfg.version(), (2, 0));
assert_eq!(cfg.external_threshold(), 5_000_000);
assert_eq!(cfg.application_name(), Some("TestApp"));
}
#[test]
fn test_config_custom_properties() {
let mut cfg = AafExportConfig::new();
cfg.add_property("vendor", "OxiMedia");
cfg.add_property("project", "TestProject");
assert_eq!(cfg.custom_properties().len(), 2);
assert_eq!(cfg.custom_properties().get("vendor").unwrap(), "OxiMedia");
}
#[test]
fn test_exporter_creation() {
let exporter = AafExporter::with_defaults();
assert_eq!(exporter.config().mode(), ExportMode::Embedded);
}
#[test]
fn test_exporter_session_id_unique() {
let a = AafExporter::with_defaults();
let b = AafExporter::with_defaults();
assert_ne!(a.session_id(), b.session_id());
}
#[test]
fn test_write_header_magic() {
let exporter = AafExporter::with_defaults();
let header = exporter.write_header();
assert!(header.len() >= exporter.min_header_size());
assert_eq!(&header[..16], b"AAF-OxiMedia ");
}
#[test]
fn test_write_header_version() {
let cfg = AafExportConfig::new().with_version(3, 7);
let exporter = AafExporter::new(cfg);
let header = exporter.write_header();
assert_eq!(u16::from_be_bytes([header[16], header[17]]), 3);
assert_eq!(u16::from_be_bytes([header[18], header[19]]), 7);
}
#[test]
fn test_write_header_mode_byte() {
for (mode, expected) in [
(ExportMode::Embedded, 0u8),
(ExportMode::ExternalRef, 1u8),
(ExportMode::Mixed, 2u8),
] {
let cfg = AafExportConfig::new().with_mode(mode);
let exporter = AafExporter::new(cfg);
let header = exporter.write_header();
assert_eq!(header[36], expected, "mode byte mismatch for {mode}");
}
}
#[test]
fn test_write_header_with_app_name() {
let cfg = AafExportConfig::new().with_application_name("MyApp");
let exporter = AafExporter::new(cfg);
let header = exporter.write_header();
let name_len = u16::from_be_bytes([header[37], header[38]]);
assert_eq!(name_len, 5);
assert_eq!(&header[39..44], b"MyApp");
}
#[test]
fn test_write_header_no_app_name() {
let exporter = AafExporter::with_defaults();
let header = exporter.write_header();
let name_len = u16::from_be_bytes([header[37], header[38]]);
assert_eq!(name_len, 0);
}
#[test]
fn test_config_default_trait() {
let cfg = AafExportConfig::default();
assert_eq!(cfg.mode(), ExportMode::Embedded);
}
#[test]
fn test_min_header_size() {
let exporter = AafExporter::with_defaults();
assert_eq!(exporter.min_header_size(), 41);
}
}