use nom::bytes::complete::{tag, take};
use nom::combinator::{fail, map_res};
use nom::error::context;
use nom::multi::many0;
use nom::number::complete::{
be_f32, be_f64, be_i16, be_i24, be_i32, be_i64, be_u16, be_u24, be_u32, be_u64, u8,
};
use nom::sequence::tuple;
use crate::EntryValue;
use super::BoxHeader;
#[derive(Debug, Clone, PartialEq)]
pub struct IlstBox {
header: BoxHeader,
pub items: Vec<IlstItem>,
}
impl IlstBox {
pub fn parse_box(input: &[u8]) -> nom::IResult<&[u8], IlstBox> {
let (remain, header) = BoxHeader::parse(input)?;
let (remain, items) = many0(IlstItem::parse)(remain)?;
Ok((remain, IlstBox { header, items }))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IlstItem {
size: u32,
index: u32, data_len: u32,
type_set: u8,
type_code: u32,
local: u32,
pub value: EntryValue, }
impl IlstItem {
fn parse<'a>(input: &'a [u8]) -> nom::IResult<&'a [u8], IlstItem> {
let (remain, (size, index, data_len, _, type_set, type_code, local)) =
tuple((be_u32, be_u32, be_u32, tag("data"), u8, be_u24, be_u32))(input)?;
if size < 24 || data_len < 16 {
context("invalid ilst item", fail::<_, (), _>)(remain)?;
}
if size - 24 != data_len - 16 {
context("invalid ilst item", fail::<_, (), _>)(remain)?;
}
let (remain, value) = map_res(take(data_len - 16), |bs: &'a [u8]| {
parse_value(type_code, bs)
})(remain)?;
Ok((
remain,
IlstItem {
size,
index,
data_len,
type_set,
type_code,
local,
value,
},
))
}
}
#[tracing::instrument(skip(data))]
fn parse_value(type_code: u32, data: &[u8]) -> crate::Result<EntryValue> {
use EntryValue::*;
let v = match type_code {
1 => {
let s = String::from_utf8(data.to_vec())?;
Text(s)
}
21 => match data.len() {
1 => data[0].into(),
2 => be_i16(data)?.1.into(),
3 => be_i24(data)?.1.into(),
4 => be_i32(data)?.1.into(),
8 => be_i64(data)?.1.into(),
data_len => {
let data_type = "BE Signed Integer";
tracing::warn!(data_type, data_len, "Invalid ilst item data.");
let msg = format!(
"Invalid ilst item data; \
data type is {data_type} while data len is : {data_len}",
);
return Err(msg.into());
}
},
22 => match data.len() {
1 => data[0].into(),
2 => be_u16(data)?.1.into(),
3 => be_u24(data)?.1.into(),
4 => be_u32(data)?.1.into(),
8 => be_u64(data)?.1.into(),
data_len => {
let data_type = "BE Unsigned Integer";
tracing::warn!(data_type, data_len, "Invalid ilst item data.");
let msg = format!(
"Invalid ilst item data; \
data type is {data_type} while data len is : {data_len}",
);
return Err(msg.into());
}
},
23 => be_f32(data)?.1.into(),
24 => be_f64(data)?.1.into(),
data_type => {
let msg = "Unsupported ilst item data type";
tracing::warn!(data_type, "{}.", msg);
return Err(format!("{}: {data_type}", msg).into());
}
};
Ok(v)
}
#[cfg(test)]
mod tests {
use crate::{bbox::travel_while, testkit::read_sample};
use super::*;
use test_case::test_case;
#[test_case("meta.mov")]
fn ilst_box(path: &str) {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let buf = read_sample(path).unwrap();
let (_, bbox) = travel_while(&buf, |b| b.box_type() != "moov").unwrap();
let bbox = bbox.unwrap();
let (_, bbox) = travel_while(bbox.body_data(), |b| b.box_type() != "meta").unwrap();
let bbox = bbox.unwrap();
let (_, bbox) = travel_while(bbox.body_data(), |b| b.box_type() != "ilst").unwrap();
let bbox = bbox.unwrap();
let (rem, ilst) = IlstBox::parse_box(bbox.data).unwrap();
tracing::info!(?ilst, "ilst");
assert_eq!(rem, b"");
assert_eq!(
ilst.items
.iter()
.map(|x| format!("{x:?}"))
.collect::<Vec<_>>(),
[
"IlstItem { size: 29, index: 1, data_len: 21, type_set: 0, type_code: 1, local: 0, value: Text(\"Apple\") }",
"IlstItem { size: 32, index: 2, data_len: 24, type_set: 0, type_code: 1, local: 0, value: Text(\"iPhone X\") }",
"IlstItem { size: 30, index: 3, data_len: 22, type_set: 0, type_code: 1, local: 0, value: Text(\"12.1.2\") }",
"IlstItem { size: 50, index: 4, data_len: 42, type_set: 0, type_code: 1, local: 0, value: Text(\"+27.1281+100.2508+000.000/\") }",
"IlstItem { size: 49, index: 5, data_len: 41, type_set: 0, type_code: 1, local: 0, value: Text(\"2019-02-12T15:27:12+08:00\") }"
],
);
}
#[test_case("embedded-in-heic.mov")]
fn heic_mov_ilst(path: &str) {
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
let buf = read_sample(path).unwrap();
let (_, moov) = travel_while(&buf, |b| b.box_type() != "moov").unwrap();
let moov = moov.unwrap();
let (_, meta) = travel_while(moov.body_data(), |b| b.box_type() != "meta").unwrap();
let meta = meta.unwrap();
let (_, ilst) = travel_while(meta.body_data(), |b| b.box_type() != "ilst").unwrap();
let ilst = ilst.unwrap();
let (rem, ilst) = IlstBox::parse_box(ilst.data).unwrap();
assert_eq!(rem.len(), 0);
let mut s = ilst
.items
.iter()
.map(|x| format!("{x:?}"))
.collect::<Vec<_>>()
.join("\n");
s.insert(0, '\n');
assert_eq!(
s,
"
IlstItem { size: 33, index: 1, data_len: 25, type_set: 0, type_code: 1, local: 0, value: Text(\"14.235563\") }
IlstItem { size: 25, index: 2, data_len: 17, type_set: 0, type_code: 22, local: 0, value: U8(1) }
IlstItem { size: 60, index: 3, data_len: 52, type_set: 0, type_code: 1, local: 0, value: Text(\"DA1A7EE8-0925-4C9F-9266-DDA3F0BB80F0\") }
IlstItem { size: 28, index: 4, data_len: 20, type_set: 0, type_code: 23, local: 0, value: F32(0.93884003) }
IlstItem { size: 32, index: 5, data_len: 24, type_set: 0, type_code: 21, local: 0, value: I64(4) }
IlstItem { size: 50, index: 6, data_len: 42, type_set: 0, type_code: 1, local: 0, value: Text(\"+22.5797+113.9380+028.396/\") }
IlstItem { size: 29, index: 7, data_len: 21, type_set: 0, type_code: 1, local: 0, value: Text(\"Apple\") }
IlstItem { size: 37, index: 8, data_len: 29, type_set: 0, type_code: 1, local: 0, value: Text(\"iPhone 15 Pro\") }
IlstItem { size: 28, index: 9, data_len: 20, type_set: 0, type_code: 1, local: 0, value: Text(\"17.1\") }
IlstItem { size: 48, index: 10, data_len: 40, type_set: 0, type_code: 1, local: 0, value: Text(\"2023-11-02T19:58:34+0800\") }"
);
}
}