use crate::boxes::*;
fn rd_u8(buf: &[u8], pos: &mut usize) -> Option<u8> {
let v = *buf.get(*pos)?;
*pos += 1;
Some(v)
}
fn rd_u16(buf: &[u8], pos: &mut usize) -> Option<u16> {
if *pos + 2 > buf.len() {
return None;
}
let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
*pos += 2;
Some(v)
}
fn rd_u32(buf: &[u8], pos: &mut usize) -> Option<u32> {
if *pos + 4 > buf.len() {
return None;
}
let v = u32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
*pos += 4;
Some(v)
}
fn rd_cstr(buf: &[u8], pos: &mut usize) -> Option<String> {
let start = *pos;
let rel = buf[start..].iter().position(|&b| b == 0)?;
let s = String::from_utf8_lossy(&buf[start..start + rel]).into_owned();
*pos = start + rel + 1;
Some(s)
}
fn rd_id(buf: &[u8], pos: &mut usize, version: u8) -> Option<u32> {
if version == 0 {
rd_u16(buf, pos).map(u32::from)
} else {
rd_u32(buf, pos)
}
}
fn wr_id(out: &mut Vec<u8>, value: u32, version: u8) {
if version == 0 {
out.extend_from_slice(&(value as u16).to_be_bytes());
} else {
out.extend_from_slice(&value.to_be_bytes());
}
}
fn rd_fullbox(buf: &[u8], pos: &mut usize) -> Option<(u8, u32)> {
if *pos + 4 > buf.len() {
return None;
}
let version = buf[*pos];
let flags = u32::from_be_bytes([0, buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
*pos += 4;
Some((version, flags))
}
fn wr_fullbox(out: &mut Vec<u8>, version: u8, flags: u32) {
out.push(version);
out.extend_from_slice(&flags.to_be_bytes()[1..]);
}
fn wrap(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut v = Vec::with_capacity(8 + body.len());
v.extend_from_slice(&total.to_be_bytes());
v.extend_from_slice(fourcc);
v.extend_from_slice(body);
v
}
fn each_child<F: FnMut([u8; 4], &[u8])>(body: &[u8], mut f: F) {
let mut pos = 0usize;
while pos + 8 <= body.len() {
let size32 = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
let mut fourcc = [0u8; 4];
fourcc.copy_from_slice(&body[pos + 4..pos + 8]);
let (total, hdr_len) = if size32 == 1 {
if pos + 16 > body.len() {
break;
}
let large = u64::from_be_bytes([
body[pos + 8],
body[pos + 9],
body[pos + 10],
body[pos + 11],
body[pos + 12],
body[pos + 13],
body[pos + 14],
body[pos + 15],
]);
(large as usize, 16usize)
} else if size32 == 0 {
(body.len() - pos, 8usize)
} else {
(size32 as usize, 8usize)
};
let end = match pos.checked_add(total) {
Some(e) if total >= hdr_len && e <= body.len() => e,
_ => break,
};
f(fourcc, &body[pos + hdr_len..end]);
pos = end;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FparEntry {
pub block_count: u16,
pub block_size: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FparBox {
pub version: u8,
pub item_id: u32,
pub packet_payload_size: u16,
pub fec_encoding_id: u8,
pub fec_instance_id: u16,
pub max_source_block_length: u16,
pub encoding_symbol_length: u16,
pub max_number_of_encoding_symbols: u16,
pub scheme_specific_info: String,
pub entries: Vec<FparEntry>,
}
pub fn parse_fpar_box(body: &[u8]) -> Option<FparBox> {
let mut p = 0usize;
let (version, _flags) = rd_fullbox(body, &mut p)?;
let item_id = rd_id(body, &mut p, version)?;
let packet_payload_size = rd_u16(body, &mut p)?;
let _reserved = rd_u8(body, &mut p)?;
let fec_encoding_id = rd_u8(body, &mut p)?;
let fec_instance_id = rd_u16(body, &mut p)?;
let max_source_block_length = rd_u16(body, &mut p)?;
let encoding_symbol_length = rd_u16(body, &mut p)?;
let max_number_of_encoding_symbols = rd_u16(body, &mut p)?;
let scheme_specific_info = rd_cstr(body, &mut p)?;
let entry_count = rd_id(body, &mut p, version)? as usize;
let mut entries = Vec::with_capacity(entry_count.min(body.len()));
for _ in 0..entry_count {
let block_count = rd_u16(body, &mut p)?;
let block_size = rd_u32(body, &mut p)?;
entries.push(FparEntry {
block_count,
block_size,
});
}
Some(FparBox {
version,
item_id,
packet_payload_size,
fec_encoding_id,
fec_instance_id,
max_source_block_length,
encoding_symbol_length,
max_number_of_encoding_symbols,
scheme_specific_info,
entries,
})
}
pub fn build_fpar_box(b: &FparBox) -> Vec<u8> {
let mut body = Vec::new();
wr_fullbox(&mut body, b.version, 0);
wr_id(&mut body, b.item_id, b.version);
body.extend_from_slice(&b.packet_payload_size.to_be_bytes());
body.push(0); body.push(b.fec_encoding_id);
body.extend_from_slice(&b.fec_instance_id.to_be_bytes());
body.extend_from_slice(&b.max_source_block_length.to_be_bytes());
body.extend_from_slice(&b.encoding_symbol_length.to_be_bytes());
body.extend_from_slice(&b.max_number_of_encoding_symbols.to_be_bytes());
body.extend_from_slice(b.scheme_specific_info.as_bytes());
body.push(0); wr_id(&mut body, b.entries.len() as u32, b.version);
for e in &b.entries {
body.extend_from_slice(&e.block_count.to_be_bytes());
body.extend_from_slice(&e.block_size.to_be_bytes());
}
wrap(&FPAR, &body)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReservoirEntry {
pub item_id: u32,
pub symbol_count: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReservoirBox {
pub version: u8,
pub entries: Vec<ReservoirEntry>,
}
pub fn parse_reservoir_box(body: &[u8]) -> Option<ReservoirBox> {
let mut p = 0usize;
let (version, _flags) = rd_fullbox(body, &mut p)?;
let entry_count = rd_id(body, &mut p, version)? as usize;
let mut entries = Vec::with_capacity(entry_count.min(body.len()));
for _ in 0..entry_count {
let item_id = rd_id(body, &mut p, version)?;
let symbol_count = rd_u32(body, &mut p)?;
entries.push(ReservoirEntry {
item_id,
symbol_count,
});
}
Some(ReservoirBox { version, entries })
}
pub fn build_reservoir_box(fourcc: &[u8; 4], b: &ReservoirBox) -> Vec<u8> {
let mut body = Vec::new();
wr_fullbox(&mut body, b.version, 0);
wr_id(&mut body, b.entries.len() as u32, b.version);
for e in &b.entries {
wr_id(&mut body, e.item_id, b.version);
body.extend_from_slice(&e.symbol_count.to_be_bytes());
}
wrap(fourcc, &body)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PartitionEntry {
pub fpar: Option<FparBox>,
pub fecr: Option<ReservoirBox>,
pub fire: Option<ReservoirBox>,
}
pub fn parse_paen_box(body: &[u8]) -> PartitionEntry {
let mut out = PartitionEntry {
fpar: None,
fecr: None,
fire: None,
};
each_child(body, |fourcc, child| match fourcc {
FPAR => out.fpar = parse_fpar_box(child),
FECR => out.fecr = parse_reservoir_box(child),
FIRE => out.fire = parse_reservoir_box(child),
_ => {}
});
out
}
pub fn build_paen_box(e: &PartitionEntry) -> Vec<u8> {
let mut body = Vec::new();
if let Some(fpar) = &e.fpar {
body.extend_from_slice(&build_fpar_box(fpar));
}
if let Some(fecr) = &e.fecr {
body.extend_from_slice(&build_reservoir_box(&FECR, fecr));
}
if let Some(fire) = &e.fire {
body.extend_from_slice(&build_reservoir_box(&FIRE, fire));
}
wrap(&PAEN, &body)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionGroup {
pub group_ids: Vec<u32>,
pub hint_track_ids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SegrBox {
pub session_groups: Vec<SessionGroup>,
}
pub fn parse_segr_box(body: &[u8]) -> Option<SegrBox> {
let mut p = 0usize;
let num = rd_u16(body, &mut p)? as usize;
let mut session_groups = Vec::with_capacity(num.min(body.len()));
for _ in 0..num {
let entry_count = rd_u8(body, &mut p)? as usize;
let mut group_ids = Vec::with_capacity(entry_count.min(body.len()));
for _ in 0..entry_count {
group_ids.push(rd_u32(body, &mut p)?);
}
let num_channels = rd_u16(body, &mut p)? as usize;
let mut hint_track_ids = Vec::with_capacity(num_channels.min(body.len()));
for _ in 0..num_channels {
hint_track_ids.push(rd_u32(body, &mut p)?);
}
session_groups.push(SessionGroup {
group_ids,
hint_track_ids,
});
}
Some(SegrBox { session_groups })
}
pub fn build_segr_box(b: &SegrBox) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(b.session_groups.len() as u16).to_be_bytes());
for g in &b.session_groups {
body.push(g.group_ids.len() as u8);
for id in &g.group_ids {
body.extend_from_slice(&id.to_be_bytes());
}
body.extend_from_slice(&(g.hint_track_ids.len() as u16).to_be_bytes());
for id in &g.hint_track_ids {
body.extend_from_slice(&id.to_be_bytes());
}
}
wrap(&SEGR, &body)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GroupName {
pub group_id: u32,
pub group_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GitnBox {
pub entries: Vec<GroupName>,
}
pub fn parse_gitn_box(body: &[u8]) -> Option<GitnBox> {
let mut p = 0usize;
let (_version, _flags) = rd_fullbox(body, &mut p)?;
let entry_count = rd_u16(body, &mut p)? as usize;
let mut entries = Vec::with_capacity(entry_count.min(body.len()));
for _ in 0..entry_count {
let group_id = rd_u32(body, &mut p)?;
let group_name = rd_cstr(body, &mut p)?;
entries.push(GroupName {
group_id,
group_name,
});
}
Some(GitnBox { entries })
}
pub fn build_gitn_box(b: &GitnBox) -> Vec<u8> {
let mut body = Vec::new();
wr_fullbox(&mut body, 0, 0);
body.extend_from_slice(&(b.entries.len() as u16).to_be_bytes());
for e in &b.entries {
body.extend_from_slice(&e.group_id.to_be_bytes());
body.extend_from_slice(e.group_name.as_bytes());
body.push(0); }
wrap(&GITN, &body)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FiinBox {
pub partition_entries: Vec<PartitionEntry>,
pub session_info: Option<SegrBox>,
pub group_id_to_name: Option<GitnBox>,
}
pub fn parse_fiin_box(body: &[u8]) -> Option<FiinBox> {
let mut p = 0usize;
let (_version, _flags) = rd_fullbox(body, &mut p)?;
let _entry_count = rd_u16(body, &mut p)?;
let mut partition_entries = Vec::new();
let mut session_info = None;
let mut group_id_to_name = None;
each_child(&body[p..], |fourcc, child| match fourcc {
PAEN => partition_entries.push(parse_paen_box(child)),
SEGR => session_info = parse_segr_box(child),
GITN => group_id_to_name = parse_gitn_box(child),
_ => {}
});
Some(FiinBox {
partition_entries,
session_info,
group_id_to_name,
})
}
pub fn build_fiin_box(b: &FiinBox) -> Vec<u8> {
let mut body = Vec::new();
wr_fullbox(&mut body, 0, 0);
body.extend_from_slice(&(b.partition_entries.len() as u16).to_be_bytes());
for e in &b.partition_entries {
body.extend_from_slice(&build_paen_box(e));
}
if let Some(segr) = &b.session_info {
body.extend_from_slice(&build_segr_box(segr));
}
if let Some(gitn) = &b.group_id_to_name {
body.extend_from_slice(&build_gitn_box(gitn));
}
wrap(&FIIN, &body)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FeciBox {
pub fec_encoding_id: u8,
pub fec_instance_id: u16,
pub source_block_number: u16,
pub encoding_symbol_id: u16,
}
pub fn parse_feci_box(body: &[u8]) -> Option<FeciBox> {
let mut p = 0usize;
let fec_encoding_id = rd_u8(body, &mut p)?;
let fec_instance_id = rd_u16(body, &mut p)?;
let source_block_number = rd_u16(body, &mut p)?;
let encoding_symbol_id = rd_u16(body, &mut p)?;
Some(FeciBox {
fec_encoding_id,
fec_instance_id,
source_block_number,
encoding_symbol_id,
})
}
pub fn build_feci_box(b: &FeciBox) -> Vec<u8> {
let mut body = Vec::with_capacity(7);
body.push(b.fec_encoding_id);
body.extend_from_slice(&b.fec_instance_id.to_be_bytes());
body.extend_from_slice(&b.source_block_number.to_be_bytes());
body.extend_from_slice(&b.encoding_symbol_id.to_be_bytes());
wrap(&FECI, &body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_child_rejects_overflowing_largesize() {
let mut body = Vec::new();
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(b"junk");
body.extend_from_slice(&u64::MAX.to_be_bytes());
let mut seen = 0usize;
each_child(&body, |_, _| seen += 1);
assert_eq!(seen, 0, "overflowing largesize child must be dropped");
}
fn unwrap_box<'a>(bytes: &'a [u8], fourcc: &[u8; 4]) -> &'a [u8] {
assert!(bytes.len() >= 8);
let total = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
assert_eq!(total, bytes.len(), "size field must equal total length");
assert_eq!(&bytes[4..8], fourcc, "fourcc mismatch");
&bytes[8..]
}
#[test]
fn fpar_v0_round_trips() {
let b = FparBox {
version: 0,
item_id: 0x1234,
packet_payload_size: 1400,
fec_encoding_id: 129,
fec_instance_id: 7,
max_source_block_length: 64,
encoding_symbol_length: 1024,
max_number_of_encoding_symbols: 255,
scheme_specific_info: "AAEC".to_string(),
entries: vec![
FparEntry {
block_count: 3,
block_size: 4096,
},
FparEntry {
block_count: 1,
block_size: 512,
},
],
};
let bytes = build_fpar_box(&b);
let body = unwrap_box(&bytes, &FPAR);
assert_eq!(parse_fpar_box(body).unwrap(), b);
}
#[test]
fn fpar_v1_uses_32bit_ids() {
let b = FparBox {
version: 1,
item_id: 0x0001_0000,
packet_payload_size: 1200,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 100,
encoding_symbol_length: 1316,
max_number_of_encoding_symbols: 0,
scheme_specific_info: String::new(),
entries: vec![FparEntry {
block_count: 0x1_0000_u32 as u16, block_size: 0xDEAD_BEEF,
}],
};
let bytes = build_fpar_box(&b);
let body = unwrap_box(&bytes, &FPAR);
let parsed = parse_fpar_box(body).unwrap();
assert_eq!(parsed.item_id, 0x0001_0000);
assert_eq!(parsed.entries[0].block_size, 0xDEAD_BEEF);
}
#[test]
fn fpar_empty_scheme_info_terminator_present() {
let b = FparBox {
version: 0,
item_id: 1,
packet_payload_size: 0,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 0,
encoding_symbol_length: 0,
max_number_of_encoding_symbols: 0,
scheme_specific_info: String::new(),
entries: vec![],
};
let bytes = build_fpar_box(&b);
let body = unwrap_box(&bytes, &FPAR);
assert_eq!(body.len(), 21);
assert_eq!(parse_fpar_box(body).unwrap(), b);
}
#[test]
fn fpar_unterminated_string_rejected() {
let mut body = Vec::new();
wr_fullbox(&mut body, 0, 0);
body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.push(0); body.push(0); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(b"no terminator here"); assert!(parse_fpar_box(&body).is_none());
}
#[test]
fn reservoir_fecr_round_trips_both_versions() {
for version in [0u8, 1u8] {
let b = ReservoirBox {
version,
entries: vec![
ReservoirEntry {
item_id: 10,
symbol_count: 100,
},
ReservoirEntry {
item_id: 20,
symbol_count: 0,
},
],
};
let bytes = build_reservoir_box(&FECR, &b);
let body = unwrap_box(&bytes, &FECR);
assert_eq!(parse_reservoir_box(body).unwrap(), b);
}
}
#[test]
fn reservoir_fire_uses_distinct_fourcc() {
let b = ReservoirBox {
version: 0,
entries: vec![ReservoirEntry {
item_id: 5,
symbol_count: 50,
}],
};
let bytes = build_reservoir_box(&FIRE, &b);
let body = unwrap_box(&bytes, &FIRE);
assert_eq!(parse_reservoir_box(body).unwrap(), b);
}
#[test]
fn paen_with_all_three_children_round_trips() {
let e = PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 1,
packet_payload_size: 1400,
fec_encoding_id: 1,
fec_instance_id: 0,
max_source_block_length: 32,
encoding_symbol_length: 512,
max_number_of_encoding_symbols: 64,
scheme_specific_info: "x".to_string(),
entries: vec![FparEntry {
block_count: 2,
block_size: 1024,
}],
}),
fecr: Some(ReservoirBox {
version: 0,
entries: vec![ReservoirEntry {
item_id: 2,
symbol_count: 8,
}],
}),
fire: Some(ReservoirBox {
version: 1,
entries: vec![ReservoirEntry {
item_id: 3,
symbol_count: 9,
}],
}),
};
let bytes = build_paen_box(&e);
let body = unwrap_box(&bytes, &PAEN);
assert_eq!(parse_paen_box(body), e);
}
#[test]
fn paen_fpar_only_round_trips() {
let e = PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 1,
packet_payload_size: 0,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 0,
encoding_symbol_length: 0,
max_number_of_encoding_symbols: 0,
scheme_specific_info: String::new(),
entries: vec![],
}),
fecr: None,
fire: None,
};
let bytes = build_paen_box(&e);
let body = unwrap_box(&bytes, &PAEN);
let parsed = parse_paen_box(body);
assert_eq!(parsed, e);
assert!(parsed.fecr.is_none() && parsed.fire.is_none());
}
#[test]
fn segr_round_trips() {
let b = SegrBox {
session_groups: vec![
SessionGroup {
group_ids: vec![1, 2, 3],
hint_track_ids: vec![10, 11],
},
SessionGroup {
group_ids: vec![],
hint_track_ids: vec![20],
},
],
};
let bytes = build_segr_box(&b);
let body = unwrap_box(&bytes, &SEGR);
assert_eq!(parse_segr_box(body).unwrap(), b);
}
#[test]
fn gitn_round_trips_utf8_names() {
let b = GitnBox {
entries: vec![
GroupName {
group_id: 1,
group_name: "base".to_string(),
},
GroupName {
group_id: 2,
group_name: "enhancement-層".to_string(),
},
],
};
let bytes = build_gitn_box(&b);
let body = unwrap_box(&bytes, &GITN);
assert_eq!(parse_gitn_box(body).unwrap(), b);
}
#[test]
fn fiin_full_round_trips() {
let b = FiinBox {
partition_entries: vec![
PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 1,
packet_payload_size: 1400,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 16,
encoding_symbol_length: 1024,
max_number_of_encoding_symbols: 16,
scheme_specific_info: String::new(),
entries: vec![FparEntry {
block_count: 1,
block_size: 16384,
}],
}),
fecr: Some(ReservoirBox {
version: 0,
entries: vec![ReservoirEntry {
item_id: 2,
symbol_count: 4,
}],
}),
fire: None,
},
PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 3,
packet_payload_size: 1200,
fec_encoding_id: 1,
fec_instance_id: 1,
max_source_block_length: 8,
encoding_symbol_length: 512,
max_number_of_encoding_symbols: 8,
scheme_specific_info: "AA==".to_string(),
entries: vec![],
}),
fecr: None,
fire: None,
},
],
session_info: Some(SegrBox {
session_groups: vec![SessionGroup {
group_ids: vec![1],
hint_track_ids: vec![100, 101],
}],
}),
group_id_to_name: Some(GitnBox {
entries: vec![GroupName {
group_id: 1,
group_name: "main".to_string(),
}],
}),
};
let bytes = build_fiin_box(&b);
let body = unwrap_box(&bytes, &FIIN);
assert_eq!(parse_fiin_box(body).unwrap(), b);
}
#[test]
fn fiin_entry_count_ignored_for_walk() {
let b = FiinBox {
partition_entries: vec![
PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 1,
packet_payload_size: 0,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 0,
encoding_symbol_length: 0,
max_number_of_encoding_symbols: 0,
scheme_specific_info: String::new(),
entries: vec![],
}),
fecr: None,
fire: None,
},
PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 2,
packet_payload_size: 0,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 0,
encoding_symbol_length: 0,
max_number_of_encoding_symbols: 0,
scheme_specific_info: String::new(),
entries: vec![],
}),
fecr: None,
fire: None,
},
],
session_info: None,
group_id_to_name: None,
};
let mut bytes = build_fiin_box(&b);
bytes[12] = 0xFF;
bytes[13] = 0xFF;
let body = unwrap_box(&bytes, &FIIN);
let parsed = parse_fiin_box(body).unwrap();
assert_eq!(parsed.partition_entries.len(), 2);
}
#[test]
fn feci_round_trips() {
let b = FeciBox {
fec_encoding_id: 129,
fec_instance_id: 0x1234,
source_block_number: 0x5678,
encoding_symbol_id: 0x9ABC,
};
let bytes = build_feci_box(&b);
let body = unwrap_box(&bytes, &FECI);
assert_eq!(body.len(), 7);
assert_eq!(parse_feci_box(body).unwrap(), b);
}
#[test]
fn feci_truncated_rejected() {
assert!(parse_feci_box(&[0x81, 0x00]).is_none());
}
}