Skip to main content

mp4_atom/meta/
iinf.rs

1use crate::*;
2
3// ItemInformationBox. ISO/IEC 14496-12:2022 Section 8.11.6
4// This is used to work out what the items are
5
6ext! {
7    name: Iinf,
8    versions: [0, 1],
9    flags: {}
10}
11
12ext! {
13    name: ItemInfoEntry,
14    versions: [0, 1, 2, 3],
15    flags: {item_not_in_presentation = 0,}
16}
17
18/// Describes one item stored in an `iinf` item information box.
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct ItemInfoEntry {
22    /// Item identifier used by other item metadata boxes.
23    pub item_id: u32,
24
25    /// Index into the item protection box, or zero when unprotected.
26    pub item_protection_index: u16,
27
28    /// Version 2 and 3 item type, such as `mime` or `uri `.
29    pub item_type: Option<FourCC>,
30
31    /// Human-readable item name.
32    pub item_name: String,
33
34    /// MIME content type for `mime` item entries.
35    pub content_type: Option<String>,
36
37    /// MIME content encoding for `mime` item entries.
38    pub content_encoding: Option<String>,
39
40    /// URI payload for `uri ` item entries.
41    pub item_uri_type: Option<String>,
42
43    /// Whether the item is excluded from presentation.
44    pub item_not_in_presentation: bool,
45}
46
47impl AtomExt for ItemInfoEntry {
48    const KIND_EXT: FourCC = FourCC::new(b"infe");
49
50    type Ext = ItemInfoEntryExt;
51
52    /// Encodes the item info entry body and version/flags extension.
53    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<Self::Ext> {
54        // TODO: maybe work harder at versioning
55        let version: ItemInfoEntryVersion = if self.item_id > u16::MAX as u32 {
56            ItemInfoEntryVersion::V3
57        } else {
58            // version 0 or 2, since we don't support version 1 yet
59            if self.item_type.is_some() {
60                ItemInfoEntryVersion::V2
61            } else {
62                ItemInfoEntryVersion::V0
63            }
64        };
65        if (version == ItemInfoEntryVersion::V0) || (version == ItemInfoEntryVersion::V1) {
66            (self.item_id as u16).encode(buf)?;
67            self.item_protection_index.encode(buf)?;
68            self.item_name.as_str().encode(buf)?;
69            self.content_type.clone().unwrap().as_str().encode(buf)?;
70            self.content_encoding
71                .clone()
72                .unwrap_or("".to_string())
73                .as_str()
74                .encode(buf)?;
75            if version == ItemInfoEntryVersion::V1 {
76                return Err(Error::Unsupported("infe version 1 extensions"));
77            }
78        } else {
79            if version == ItemInfoEntryVersion::V2 {
80                (self.item_id as u16).encode(buf)?;
81            } else {
82                self.item_id.encode(buf)?;
83            }
84            self.item_protection_index.encode(buf)?;
85            Some(self.item_type).encode(buf)?;
86            self.item_name.as_str().encode(buf)?;
87            if self.item_type == Some(FourCC::new(b"mime")) {
88                self.content_type.clone().unwrap().as_str().encode(buf)?;
89                self.content_encoding
90                    .clone()
91                    .unwrap_or("".to_string())
92                    .as_str()
93                    .encode(buf)?;
94            } else if self.item_type == Some(FourCC::new(b"uri ")) {
95                let item_uri_type = self.item_uri_type.as_ref().ok_or(Error::MissingContent(
96                    "item_uri_type required with 'uri ' item_type",
97                ))?;
98                item_uri_type.as_str().encode(buf)?;
99            }
100        }
101        Ok(ItemInfoEntryExt {
102            version,
103            item_not_in_presentation: self.item_not_in_presentation,
104        })
105    }
106
107    /// Decodes the item info entry body for the parsed version/flags extension.
108    fn decode_body_ext<B: Buf>(buf: &mut B, ext: Self::Ext) -> Result<Self> {
109        let item_id: u32;
110        let item_protection_index;
111        let mut item_type = None;
112        let item_name;
113        let mut content_type = None;
114        let mut content_encoding = None;
115        let mut item_uri_type = None;
116        if (ext.version == ItemInfoEntryVersion::V0) || (ext.version == ItemInfoEntryVersion::V1) {
117            item_id = u16::decode(buf)? as u32;
118            item_protection_index = u16::decode(buf)?;
119            item_name = String::decode(buf)?;
120            content_type = Some(String::decode(buf)?);
121            content_encoding = Some(String::decode(buf)?);
122            if ext.version == ItemInfoEntryVersion::V1 {
123                return Err(Error::Unsupported("infe version 1 extensions"));
124            }
125        } else {
126            if ext.version == ItemInfoEntryVersion::V2 {
127                item_id = u16::decode(buf)? as u32;
128            } else {
129                item_id = u32::decode(buf)?;
130            }
131            item_protection_index = u16::decode(buf)?;
132            item_type = Some(FourCC::decode(buf)?);
133            item_name = String::decode(buf)?;
134            if item_type == Some(FourCC::new(b"mime")) {
135                content_type = Some(String::decode(buf)?);
136                content_encoding = Some(String::decode(buf)?);
137            } else if item_type == Some(FourCC::new(b"uri ")) {
138                item_uri_type = Some(String::decode(buf)?);
139            }
140        }
141        Ok(ItemInfoEntry {
142            item_id,
143            item_protection_index,
144            item_type,
145            item_name,
146            content_type,
147            content_encoding,
148            item_uri_type,
149            item_not_in_presentation: ext.item_not_in_presentation,
150        })
151    }
152}
153
154/// Item information box containing every item entry.
155#[derive(Debug, Clone, PartialEq, Eq)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct Iinf {
158    /// Item info entries in file order.
159    pub item_infos: Vec<ItemInfoEntry>,
160}
161
162impl AtomExt for Iinf {
163    type Ext = IinfExt;
164
165    const KIND_EXT: FourCC = FourCC::new(b"iinf");
166
167    /// Decodes an item information box for the parsed version/flags extension.
168    fn decode_body_ext<B: Buf>(buf: &mut B, ext: IinfExt) -> Result<Self> {
169        let mut item_infos = vec![];
170        let entry_count = if ext.version == IinfVersion::V0 {
171            u16::decode(buf)? as usize
172        } else {
173            u32::decode(buf)? as usize
174        };
175        for _ in 0..entry_count {
176            item_infos.push(ItemInfoEntry::decode(buf)?);
177        }
178        Ok(Iinf { item_infos })
179    }
180
181    /// Encodes an item information box and chooses the smallest entry-count version.
182    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<IinfExt> {
183        let version;
184        if self.item_infos.len() > u16::MAX as usize {
185            version = IinfVersion::V1;
186            (self.item_infos.len() as u32).encode(buf)?
187        } else {
188            version = IinfVersion::V0;
189            (self.item_infos.len() as u16).encode(buf)?
190        }
191        for item_info in &self.item_infos {
192            item_info.encode(buf)?;
193        }
194
195        Ok(IinfExt { version })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    const ENCODED_IINF_LIBAVIF_MIME: &[u8] = &[
204        0, 0, 0, 65, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 51, 105, 110, 102, 101, 2, 0,
205        0, 0, 0, 1, 0, 0, 109, 105, 109, 101, 73, 116, 101, 109, 0, 99, 111, 110, 116, 101, 110,
206        116, 45, 116, 121, 112, 101, 0, 117, 110, 107, 110, 111, 119, 110, 47, 109, 105, 109, 101,
207        0,
208    ];
209
210    /// Decodes a libavif-style `mime` item info box.
211    #[test]
212    fn test_iinf_libavif_decode_mime() {
213        let buf: &mut std::io::Cursor<&&[u8]> =
214            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_MIME);
215
216        let iinf: Iinf = Iinf {
217            item_infos: vec![ItemInfoEntry {
218                item_id: 1,
219                item_protection_index: 0,
220                item_type: Some(FourCC::new(b"mime")),
221                item_name: "Item".to_string(),
222                content_type: Some("content-type".to_string()),
223                content_encoding: Some("unknown/mime".to_string()),
224                item_uri_type: None,
225                item_not_in_presentation: false,
226            }],
227        };
228        let decoded = Iinf::decode(buf).unwrap();
229        assert_eq!(decoded, iinf);
230    }
231
232    /// Encodes a libavif-style `mime` item info box.
233    #[test]
234    fn test_iinf_avif_encode_mime() {
235        let iinf: Iinf = Iinf {
236            item_infos: vec![ItemInfoEntry {
237                item_id: 1,
238                item_protection_index: 0,
239                item_type: Some(FourCC::new(b"mime")),
240                item_name: "Item".to_string(),
241                content_type: Some("content-type".to_string()),
242                content_encoding: Some("unknown/mime".to_string()),
243                item_uri_type: None,
244                item_not_in_presentation: false,
245            }],
246        };
247        let mut buf = Vec::new();
248        iinf.encode(&mut buf).unwrap();
249
250        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_MIME);
251    }
252
253    const ENCODED_IINF_LIBAVIF_URI: &[u8] = &[
254        0, 0, 0, 50, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 36, 105, 110, 102, 101, 2, 0,
255        0, 0, 0, 1, 0, 0, 117, 114, 105, 32, 73, 116, 101, 109, 0, 117, 114, 105, 58, 47, 47, 116,
256        101, 115, 116, 0,
257    ];
258
259    /// Decodes a libavif-style `uri ` item info box.
260    #[test]
261    fn test_iinf_libavif_decode_uri() {
262        let buf: &mut std::io::Cursor<&&[u8]> =
263            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_URI);
264
265        let iinf: Iinf = Iinf {
266            item_infos: vec![ItemInfoEntry {
267                item_id: 1,
268                item_protection_index: 0,
269                item_type: Some(FourCC::new(b"uri ")),
270                item_name: "Item".to_string(),
271                content_type: None,
272                content_encoding: None,
273                item_uri_type: Some("uri://test".to_string()),
274                item_not_in_presentation: false,
275            }],
276        };
277        let decoded = Iinf::decode(buf).unwrap();
278        assert_eq!(decoded, iinf);
279    }
280
281    /// Encodes a libavif-style `uri ` item info box.
282    #[test]
283    fn test_iinf_avif_encode_uri() {
284        let iinf: Iinf = Iinf {
285            item_infos: vec![ItemInfoEntry {
286                item_id: 1,
287                item_protection_index: 0,
288                item_type: Some(FourCC::new(b"uri ")),
289                item_name: "Item".to_string(),
290                content_type: None,
291                content_encoding: None,
292                item_uri_type: Some("uri://test".to_string()),
293                item_not_in_presentation: false,
294            }],
295        };
296        let mut buf = Vec::new();
297        iinf.encode(&mut buf).unwrap();
298
299        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_URI);
300    }
301
302    /// Rejects a `uri ` item info box when the URI payload is missing.
303    #[test]
304    fn test_iinf_avif_encode_uri_invalid() {
305        let iinf: Iinf = Iinf {
306            item_infos: vec![ItemInfoEntry {
307                item_id: 1,
308                item_protection_index: 0,
309                item_type: Some(FourCC::new(b"uri ")),
310                item_name: "Item".to_string(),
311                content_type: None,
312                content_encoding: None,
313                item_uri_type: None, // encode will return an error because this is empty
314                item_not_in_presentation: false,
315            }],
316        };
317        let mut buf = Vec::new();
318        assert!(matches!(
319            iinf.encode(&mut buf),
320            Err(Error::MissingContent(_))
321        ));
322    }
323
324    /// Version 1 `infe` entries are reported as unsupported, not panicked.
325    #[test]
326    fn test_iinf_decode_unsupported_infe_v1_returns_error() {
327        let body: &[u8] = &[
328            // iinf version/flags and one entry
329            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // infe atom header
330            0x00, 0x00, 0x00, 0x13, b'i', b'n', b'f', b'e', // infe version 1, no flags
331            0x01, 0x00, 0x00, 0x00,
332            // item_id, item_protection_index, item_name, content_type,
333            // content_encoding
334            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
335        ];
336        let result = Iinf::decode_body(&mut std::io::Cursor::new(body));
337
338        assert!(matches!(
339            result,
340            Err(Error::Unsupported("infe version 1 extensions"))
341        ));
342
343        let fuzz_body: &[u8] = &[
344            0x00, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x80,
345            0x01, 0x00, 0x00, 0x04, 0x00, b'p', b'y', b't', b'f',
346        ];
347        let fuzz_result = Iinf::decode_body(&mut std::io::Cursor::new(fuzz_body));
348
349        assert!(matches!(
350            fuzz_result,
351            Err(Error::Unsupported("infe version 1 extensions"))
352        ));
353    }
354}