#![forbid(unsafe_code)]
use bytes::Bytes;
use oximedia_core::{OxiError, OxiResult};
#[derive(Debug, Clone)]
pub struct MatroskaAttachment {
pub uid: u64,
pub filename: String,
pub mime_type: String,
pub data: Bytes,
pub description: Option<String>,
}
impl MatroskaAttachment {
#[must_use]
pub fn new(
uid: u64,
filename: impl Into<String>,
mime_type: impl Into<String>,
data: Bytes,
) -> Self {
Self {
uid,
filename: filename.into(),
mime_type: mime_type.into(),
data,
description: None,
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_image(&self) -> bool {
self.mime_type.starts_with("image/")
}
#[must_use]
pub fn is_font(&self) -> bool {
self.mime_type.starts_with("font/")
|| self.mime_type.starts_with("application/x-font-")
|| self.mime_type.contains("truetype")
|| self.mime_type.contains("opentype")
}
#[must_use]
pub fn extension(&self) -> Option<&str> {
self.filename.rsplit('.').next()
}
}
#[derive(Debug, Clone)]
pub struct MatroskaAttachments {
attachments: Vec<MatroskaAttachment>,
}
impl MatroskaAttachments {
#[must_use]
pub fn new() -> Self {
Self {
attachments: Vec::new(),
}
}
pub fn add(&mut self, attachment: MatroskaAttachment) {
self.attachments.push(attachment);
}
#[must_use]
pub fn attachments(&self) -> &[MatroskaAttachment] {
&self.attachments
}
#[must_use]
pub fn find_by_uid(&self, uid: u64) -> Option<&MatroskaAttachment> {
self.attachments.iter().find(|a| a.uid == uid)
}
#[must_use]
pub fn find_by_filename(&self, filename: &str) -> Option<&MatroskaAttachment> {
self.attachments.iter().find(|a| a.filename == filename)
}
#[must_use]
pub fn images(&self) -> Vec<&MatroskaAttachment> {
self.attachments.iter().filter(|a| a.is_image()).collect()
}
#[must_use]
pub fn fonts(&self) -> Vec<&MatroskaAttachment> {
self.attachments.iter().filter(|a| a.is_font()).collect()
}
#[must_use]
pub fn total_size(&self) -> usize {
self.attachments.iter().map(MatroskaAttachment::size).sum()
}
pub fn validate(&self) -> OxiResult<()> {
let mut uids = std::collections::HashSet::new();
for attachment in &self.attachments {
if !uids.insert(attachment.uid) {
return Err(OxiError::InvalidData(format!(
"Duplicate attachment UID: {}",
attachment.uid
)));
}
}
Ok(())
}
#[must_use]
pub fn len(&self) -> usize {
self.attachments.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.attachments.is_empty()
}
}
impl Default for MatroskaAttachments {
fn default() -> Self {
Self::new()
}
}
pub struct MimeTypes;
impl MimeTypes {
pub const JPEG: &'static str = "image/jpeg";
pub const PNG: &'static str = "image/png";
pub const WEBP: &'static str = "image/webp";
pub const TTF: &'static str = "font/ttf";
pub const OTF: &'static str = "font/otf";
pub const WOFF: &'static str = "font/woff";
pub const WOFF2: &'static str = "font/woff2";
pub const BINARY: &'static str = "application/octet-stream";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matroska_attachment() {
let data = Bytes::from_static(b"test data");
let attachment = MatroskaAttachment::new(1, "cover.jpg", MimeTypes::JPEG, data)
.with_description("Cover art");
assert_eq!(attachment.uid, 1);
assert_eq!(attachment.filename, "cover.jpg");
assert_eq!(attachment.mime_type, MimeTypes::JPEG);
assert_eq!(attachment.size(), 9);
assert!(attachment.is_image());
assert!(!attachment.is_font());
assert_eq!(attachment.extension(), Some("jpg"));
assert_eq!(attachment.description, Some("Cover art".into()));
}
#[test]
fn test_font_detection() {
let data = Bytes::new();
let font = MatroskaAttachment::new(1, "font.ttf", MimeTypes::TTF, data);
assert!(font.is_font());
assert!(!font.is_image());
}
#[test]
fn test_matroska_attachments() {
let mut attachments = MatroskaAttachments::new();
let cover = MatroskaAttachment::new(
1,
"cover.jpg",
MimeTypes::JPEG,
Bytes::from_static(b"image"),
);
let font =
MatroskaAttachment::new(2, "font.ttf", MimeTypes::TTF, Bytes::from_static(b"font"));
attachments.add(cover);
attachments.add(font);
assert_eq!(attachments.len(), 2);
assert_eq!(attachments.images().len(), 1);
assert_eq!(attachments.fonts().len(), 1);
assert_eq!(attachments.total_size(), 9);
let found = attachments.find_by_filename("cover.jpg");
assert!(found.is_some());
assert_eq!(found.expect("operation should succeed").uid, 1);
}
#[test]
fn test_validate_attachments() {
let mut attachments = MatroskaAttachments::new();
assert!(attachments.validate().is_ok());
attachments.add(MatroskaAttachment::new(
1,
"a.jpg",
MimeTypes::JPEG,
Bytes::new(),
));
assert!(attachments.validate().is_ok());
attachments.add(MatroskaAttachment::new(
1,
"b.jpg",
MimeTypes::JPEG,
Bytes::new(),
));
assert!(attachments.validate().is_err());
}
}