use std::io::Cursor;
use oxideav_core::{Demuxer, ReadSeek};
use oxideav_mkv::ebml::{write_element_id, write_vint};
use oxideav_mkv::ids;
fn elem_uint(id: u32, value: u64) -> Vec<u8> {
let n = if value == 0 {
1
} else {
(64 - value.leading_zeros()).div_ceil(8) as usize
};
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(n as u64, 0));
for i in (0..n).rev() {
out.push(((value >> (i * 8)) & 0xFF) as u8);
}
out
}
fn elem_str(id: u32, s: &str) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(s.len() as u64, 0));
out.extend_from_slice(s.as_bytes());
out
}
fn elem_bytes(id: u32, bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(bytes.len() as u64, 0));
out.extend_from_slice(bytes);
out
}
fn elem_float_be_f64(id: u32, value: f64) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(8, 0));
out.extend_from_slice(&value.to_be_bytes());
out
}
fn elem_master(id: u32, body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(id));
out.extend_from_slice(&write_vint(body.len() as u64, 0));
out.extend_from_slice(body);
out
}
fn simple_block(track: u8, tc_offset: i16, keyframe: bool, payload: u8) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&write_vint(track as u64, 0));
body.extend_from_slice(&tc_offset.to_be_bytes());
body.push(if keyframe { 0x80 } else { 0x00 });
body.push(payload);
let mut out = Vec::new();
out.extend_from_slice(&write_element_id(ids::SIMPLE_BLOCK));
out.extend_from_slice(&write_vint(body.len() as u64, 0));
out.extend_from_slice(&body);
out
}
fn attached_file(uid: u64, filename: &str, mime: &str, data: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&elem_uint(ids::FILE_UID, uid));
body.extend_from_slice(&elem_str(ids::FILE_NAME, filename));
body.extend_from_slice(&elem_str(ids::FILE_MIME_TYPE, mime));
body.extend_from_slice(&elem_bytes(ids::FILE_DATA, data));
elem_master(ids::ATTACHED_FILE, &body)
}
fn attached_file_with_description(
uid: u64,
filename: &str,
mime: &str,
description: &str,
data: &[u8],
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&elem_str(ids::FILE_DESCRIPTION, description));
body.extend_from_slice(&elem_uint(ids::FILE_UID, uid));
body.extend_from_slice(&elem_str(ids::FILE_NAME, filename));
body.extend_from_slice(&elem_str(ids::FILE_MIME_TYPE, mime));
body.extend_from_slice(&elem_bytes(ids::FILE_DATA, data));
elem_master(ids::ATTACHED_FILE, &body)
}
fn build_mkv_with_attachments() -> Vec<u8> {
let mut ebml_body = Vec::new();
ebml_body.extend_from_slice(&elem_uint(ids::EBML_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_READ_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_ID_LENGTH, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_SIZE_LENGTH, 8));
ebml_body.extend_from_slice(&elem_str(ids::EBML_DOC_TYPE, "matroska"));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_VERSION, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_READ_VERSION, 2));
let ebml_header = elem_master(ids::EBML_HEADER, &ebml_body);
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 1000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let track_entry = elem_master(ids::TRACK_ENTRY, &track_body);
let tracks = elem_master(ids::TRACKS, &track_entry);
let mut atts_body = Vec::new();
atts_body.extend_from_slice(&attached_file(
0xA1,
"subs.ttf",
"application/x-truetype-font",
b"FONTPAYLOAD!",
));
atts_body.extend_from_slice(&attached_file(0xA2, "cover.png", "image/png", b"PNGDATA"));
let attachments = elem_master(ids::ATTACHMENTS, &atts_body);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&attachments);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut out = Vec::new();
out.extend_from_slice(&ebml_header);
out.extend_from_slice(&segment);
out
}
#[test]
fn attachments_surface_in_metadata() {
let bytes = build_mkv_with_attachments();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mkv::demux::open(rs, &oxideav_core::NullCodecResolver).expect("demux open");
let md: Vec<(String, String)> = dmx.metadata().to_vec();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(get("attachment:1:filename").as_deref(), Some("subs.ttf"));
assert_eq!(
get("attachment:1:mime_type").as_deref(),
Some("application/x-truetype-font")
);
assert_eq!(get("attachment:1:size_bytes").as_deref(), Some("12"));
assert_eq!(get("attachment:2:filename").as_deref(), Some("cover.png"));
assert_eq!(get("attachment:2:mime_type").as_deref(), Some("image/png"));
assert_eq!(get("attachment:2:size_bytes").as_deref(), Some("7"));
}
#[test]
fn typed_attachments_expose_filename_mime_uid_and_size() {
let bytes = build_mkv_with_attachments();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx =
oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver).expect("demux open");
let atts = dmx.attachments();
assert_eq!(atts.len(), 2, "two AttachedFiles were written");
assert_eq!(atts[0].index, 1);
assert_eq!(atts[0].filename, "subs.ttf");
assert_eq!(atts[0].mime_type, "application/x-truetype-font");
assert_eq!(atts[0].description, "");
assert_eq!(atts[0].uid, 0xA1);
assert_eq!(atts[0].data_size, 12);
assert_eq!(atts[1].index, 2);
assert_eq!(atts[1].filename, "cover.png");
assert_eq!(atts[1].mime_type, "image/png");
assert_eq!(atts[1].description, "");
assert_eq!(atts[1].uid, 0xA2);
assert_eq!(atts[1].data_size, 7);
assert_ne!(atts[0].data_offset, atts[1].data_offset);
}
#[test]
fn attachment_data_returns_full_payload_on_demand() {
let bytes = build_mkv_with_attachments();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx =
oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver).expect("demux open");
let cover = dmx.attachment_data(2).expect("read cover.png");
assert_eq!(cover, b"PNGDATA");
let font = dmx.attachment_data(1).expect("read subs.ttf");
assert_eq!(font, b"FONTPAYLOAD!");
let pkt = dmx.next_packet().expect("first packet still readable");
assert_eq!(pkt.data, vec![0xAA]);
}
#[test]
fn attachment_data_rejects_invalid_indices() {
let bytes = build_mkv_with_attachments();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx =
oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver).expect("demux open");
assert!(
dmx.attachment_data(0).is_err(),
"index 0 is invalid (attachments are 1-based)"
);
assert!(
dmx.attachment_data(99).is_err(),
"index past the last attachment must error"
);
assert_eq!(dmx.attachment_data(1).unwrap(), b"FONTPAYLOAD!");
}
#[test]
fn file_description_surfaces_in_both_views() {
let mut ebml_body = Vec::new();
ebml_body.extend_from_slice(&elem_uint(ids::EBML_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_READ_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_ID_LENGTH, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_SIZE_LENGTH, 8));
ebml_body.extend_from_slice(&elem_str(ids::EBML_DOC_TYPE, "matroska"));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_VERSION, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_READ_VERSION, 2));
let ebml_header = elem_master(ids::EBML_HEADER, &ebml_body);
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 1000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let track_entry = elem_master(ids::TRACK_ENTRY, &track_body);
let tracks = elem_master(ids::TRACKS, &track_entry);
let mut atts_body = Vec::new();
atts_body.extend_from_slice(&attached_file_with_description(
0x1234,
"lyrics.txt",
"text/plain",
"Song lyrics in plain UTF-8",
b"line one\nline two",
));
let attachments = elem_master(ids::ATTACHMENTS, &atts_body);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&attachments);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx =
oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver).expect("demux open");
let md: Vec<(String, String)> = dmx.metadata().to_vec();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(
get("attachment:1:description").as_deref(),
Some("Song lyrics in plain UTF-8")
);
let atts = dmx.attachments();
assert_eq!(atts.len(), 1);
assert_eq!(atts[0].description, "Song lyrics in plain UTF-8");
assert_eq!(atts[0].uid, 0x1234);
assert_eq!(atts[0].data_size, b"line one\nline two".len() as u64);
let payload = dmx.attachment_data(1).unwrap();
assert_eq!(payload, b"line one\nline two");
}
#[test]
fn typed_attachments_empty_when_no_attachments_element() {
let mut ebml_body = Vec::new();
ebml_body.extend_from_slice(&elem_uint(ids::EBML_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_READ_VERSION, 1));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_ID_LENGTH, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_MAX_SIZE_LENGTH, 8));
ebml_body.extend_from_slice(&elem_str(ids::EBML_DOC_TYPE, "matroska"));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_VERSION, 4));
ebml_body.extend_from_slice(&elem_uint(ids::EBML_DOC_TYPE_READ_VERSION, 2));
let ebml_header = elem_master(ids::EBML_HEADER, &ebml_body);
let mut info_body = Vec::new();
info_body.extend_from_slice(&elem_uint(ids::TIMECODE_SCALE, 1_000_000));
info_body.extend_from_slice(&elem_float_be_f64(ids::DURATION, 1000.0));
let info = elem_master(ids::INFO, &info_body);
let mut track_body = Vec::new();
track_body.extend_from_slice(&elem_uint(ids::TRACK_NUMBER, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_UID, 1));
track_body.extend_from_slice(&elem_uint(ids::TRACK_TYPE, ids::TRACK_TYPE_AUDIO));
track_body.extend_from_slice(&elem_str(ids::CODEC_ID, "A_PCM/INT/LIT"));
let mut audio_body = Vec::new();
audio_body.extend_from_slice(&elem_float_be_f64(ids::SAMPLING_FREQUENCY, 48_000.0));
audio_body.extend_from_slice(&elem_uint(ids::CHANNELS, 2));
audio_body.extend_from_slice(&elem_uint(ids::BIT_DEPTH, 16));
track_body.extend_from_slice(&elem_master(ids::AUDIO, &audio_body));
let track_entry = elem_master(ids::TRACK_ENTRY, &track_body);
let tracks = elem_master(ids::TRACKS, &track_entry);
let mut cluster_body = Vec::new();
cluster_body.extend_from_slice(&elem_uint(ids::TIMECODE, 0));
cluster_body.extend_from_slice(&simple_block(1, 0, true, 0xAA));
let cluster = elem_master(ids::CLUSTER, &cluster_body);
let mut seg_body = Vec::new();
seg_body.extend_from_slice(&info);
seg_body.extend_from_slice(&tracks);
seg_body.extend_from_slice(&cluster);
let segment = elem_master(ids::SEGMENT, &seg_body);
let mut bytes = Vec::new();
bytes.extend_from_slice(&ebml_header);
bytes.extend_from_slice(&segment);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let mut dmx =
oxideav_mkv::demux::open_typed(rs, &oxideav_core::NullCodecResolver).expect("demux open");
assert!(dmx.attachments().is_empty());
assert!(dmx.attachment_data(1).is_err());
}