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            let content_type = self.content_type.as_deref().ok_or(Error::MissingContent(
70                "content_type required for infe version 0 or 1",
71            ))?;
72            content_type.encode(buf)?;
73            self.content_encoding.as_deref().unwrap_or("").encode(buf)?;
74            if version == ItemInfoEntryVersion::V1 {
75                return Err(Error::Unsupported("infe version 1 extensions"));
76            }
77        } else {
78            if version == ItemInfoEntryVersion::V2 {
79                (self.item_id as u16).encode(buf)?;
80            } else {
81                self.item_id.encode(buf)?;
82            }
83            self.item_protection_index.encode(buf)?;
84            Some(self.item_type).encode(buf)?;
85            self.item_name.as_str().encode(buf)?;
86            if self.item_type == Some(FourCC::new(b"mime")) {
87                let content_type = self.content_type.as_deref().ok_or(Error::MissingContent(
88                    "content_type required with 'mime' item_type",
89                ))?;
90                content_type.encode(buf)?;
91                self.content_encoding.as_deref().unwrap_or("").encode(buf)?;
92            } else if self.item_type == Some(FourCC::new(b"uri ")) {
93                let item_uri_type = self.item_uri_type.as_ref().ok_or(Error::MissingContent(
94                    "item_uri_type required with 'uri ' item_type",
95                ))?;
96                item_uri_type.as_str().encode(buf)?;
97            }
98        }
99        Ok(ItemInfoEntryExt {
100            version,
101            item_not_in_presentation: self.item_not_in_presentation,
102        })
103    }
104
105    /// Decodes the item info entry body for the parsed version/flags extension.
106    fn decode_body_ext<B: Buf>(buf: &mut B, ext: Self::Ext) -> Result<Self> {
107        let item_id: u32;
108        let item_protection_index;
109        let mut item_type = None;
110        let item_name;
111        let mut content_type = None;
112        let mut content_encoding = None;
113        let mut item_uri_type = None;
114        if (ext.version == ItemInfoEntryVersion::V0) || (ext.version == ItemInfoEntryVersion::V1) {
115            item_id = u16::decode(buf)? as u32;
116            item_protection_index = u16::decode(buf)?;
117            item_name = String::decode(buf)?;
118            content_type = Some(String::decode(buf)?);
119            content_encoding = Some(String::decode(buf)?);
120            if ext.version == ItemInfoEntryVersion::V1 {
121                return Err(Error::Unsupported("infe version 1 extensions"));
122            }
123        } else {
124            if ext.version == ItemInfoEntryVersion::V2 {
125                item_id = u16::decode(buf)? as u32;
126            } else {
127                item_id = u32::decode(buf)?;
128            }
129            item_protection_index = u16::decode(buf)?;
130            item_type = Some(FourCC::decode(buf)?);
131            item_name = String::decode(buf)?;
132            if item_type == Some(FourCC::new(b"mime")) {
133                content_type = Some(String::decode(buf)?);
134                content_encoding = Some(String::decode(buf)?);
135            } else if item_type == Some(FourCC::new(b"uri ")) {
136                item_uri_type = Some(String::decode(buf)?);
137            }
138        }
139        Ok(ItemInfoEntry {
140            item_id,
141            item_protection_index,
142            item_type,
143            item_name,
144            content_type,
145            content_encoding,
146            item_uri_type,
147            item_not_in_presentation: ext.item_not_in_presentation,
148        })
149    }
150}
151
152/// Item information box containing every item entry.
153#[derive(Debug, Clone, PartialEq, Eq)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Iinf {
156    /// Item info entries in file order.
157    pub item_infos: Vec<ItemInfoEntry>,
158}
159
160impl AtomExt for Iinf {
161    type Ext = IinfExt;
162
163    const KIND_EXT: FourCC = FourCC::new(b"iinf");
164
165    /// Decodes an item information box for the parsed version/flags extension.
166    fn decode_body_ext<B: Buf>(buf: &mut B, ext: IinfExt) -> Result<Self> {
167        let mut item_infos = vec![];
168        let entry_count = if ext.version == IinfVersion::V0 {
169            u16::decode(buf)? as usize
170        } else {
171            u32::decode(buf)? as usize
172        };
173        for _ in 0..entry_count {
174            item_infos.push(ItemInfoEntry::decode(buf)?);
175        }
176        Ok(Iinf { item_infos })
177    }
178
179    /// Encodes an item information box and chooses the smallest entry-count version.
180    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<IinfExt> {
181        let version;
182        if self.item_infos.len() > u16::MAX as usize {
183            version = IinfVersion::V1;
184            (self.item_infos.len() as u32).encode(buf)?
185        } else {
186            version = IinfVersion::V0;
187            (self.item_infos.len() as u16).encode(buf)?
188        }
189        for item_info in &self.item_infos {
190            item_info.encode(buf)?;
191        }
192
193        Ok(IinfExt { version })
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    const ENCODED_IINF_LIBAVIF_MIME: &[u8] = &[
202        0, 0, 0, 65, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 51, 105, 110, 102, 101, 2, 0,
203        0, 0, 0, 1, 0, 0, 109, 105, 109, 101, 73, 116, 101, 109, 0, 99, 111, 110, 116, 101, 110,
204        116, 45, 116, 121, 112, 101, 0, 117, 110, 107, 110, 111, 119, 110, 47, 109, 105, 109, 101,
205        0,
206    ];
207
208    /// Decodes a libavif-style `mime` item info box.
209    #[test]
210    fn test_iinf_libavif_decode_mime() {
211        let buf: &mut std::io::Cursor<&&[u8]> =
212            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_MIME);
213
214        let iinf: Iinf = Iinf {
215            item_infos: vec![ItemInfoEntry {
216                item_id: 1,
217                item_protection_index: 0,
218                item_type: Some(FourCC::new(b"mime")),
219                item_name: "Item".to_string(),
220                content_type: Some("content-type".to_string()),
221                content_encoding: Some("unknown/mime".to_string()),
222                item_uri_type: None,
223                item_not_in_presentation: false,
224            }],
225        };
226        let decoded = Iinf::decode(buf).unwrap();
227        assert_eq!(decoded, iinf);
228    }
229
230    /// Encodes a libavif-style `mime` item info box.
231    #[test]
232    fn test_iinf_avif_encode_mime() {
233        let iinf: Iinf = Iinf {
234            item_infos: vec![ItemInfoEntry {
235                item_id: 1,
236                item_protection_index: 0,
237                item_type: Some(FourCC::new(b"mime")),
238                item_name: "Item".to_string(),
239                content_type: Some("content-type".to_string()),
240                content_encoding: Some("unknown/mime".to_string()),
241                item_uri_type: None,
242                item_not_in_presentation: false,
243            }],
244        };
245        let mut buf = Vec::new();
246        iinf.encode(&mut buf).unwrap();
247
248        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_MIME);
249    }
250
251    const ENCODED_IINF_LIBAVIF_URI: &[u8] = &[
252        0, 0, 0, 50, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 36, 105, 110, 102, 101, 2, 0,
253        0, 0, 0, 1, 0, 0, 117, 114, 105, 32, 73, 116, 101, 109, 0, 117, 114, 105, 58, 47, 47, 116,
254        101, 115, 116, 0,
255    ];
256
257    const ENCODED_IINF_LIBAVIF_URI_V3: &[u8] = &[
258        0, 0, 0, 67, b'i', b'i', b'n', b'f', 0, 0, 0, 0, 0, 1, 0, 0, 0, 53, b'i', b'n', b'f', b'e',
259        3, 0, 0, 0, 0, 1, 0, 0, 0, 0, b'm', b'i', b'm', b'e', b'I', b't', b'e', b'm', 0, b'c',
260        b'o', b'n', b't', b'e', b'n', b't', b'-', b't', b'y', b'p', b'e', 0, b'u', b'n', b'k',
261        b'n', b'o', b'w', b'n', b'/', b'm', b'i', b'm', b'e', 0,
262    ];
263
264    /// Decodes an item info box version 3 entry.
265    #[test]
266    fn test_iinf_libavif_decode_mime_v3() {
267        let buf: &mut std::io::Cursor<&&[u8]> =
268            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_URI_V3);
269
270        let iinf: Iinf = Iinf {
271            item_infos: vec![ItemInfoEntry {
272                item_id: 65536,
273                item_protection_index: 0,
274                item_type: Some(FourCC::new(b"mime")),
275                item_name: "Item".to_string(),
276                content_type: Some("content-type".to_string()),
277                content_encoding: Some("unknown/mime".to_string()),
278                item_uri_type: None,
279                item_not_in_presentation: false,
280            }],
281        };
282        let decoded = Iinf::decode(buf).unwrap();
283        assert_eq!(decoded, iinf);
284    }
285
286    /// Encodes a item info box version 3 entry.
287    #[test]
288    fn test_iinf_avif_encode_mime_v3() {
289        let iinf: Iinf = Iinf {
290            item_infos: vec![ItemInfoEntry {
291                item_id: 65536,
292                item_protection_index: 0,
293                item_type: Some(FourCC::new(b"mime")),
294                item_name: "Item".to_string(),
295                content_type: Some("content-type".to_string()),
296                content_encoding: Some("unknown/mime".to_string()),
297                item_uri_type: None,
298                item_not_in_presentation: false,
299            }],
300        };
301        let mut buf = Vec::new();
302        iinf.encode(&mut buf).unwrap();
303
304        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_URI_V3);
305    }
306
307    /// Decodes a libavif-style `uri ` item info box.
308    #[test]
309    fn test_iinf_libavif_decode_uri() {
310        let buf: &mut std::io::Cursor<&&[u8]> =
311            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_URI);
312
313        let iinf: Iinf = Iinf {
314            item_infos: vec![ItemInfoEntry {
315                item_id: 1,
316                item_protection_index: 0,
317                item_type: Some(FourCC::new(b"uri ")),
318                item_name: "Item".to_string(),
319                content_type: None,
320                content_encoding: None,
321                item_uri_type: Some("uri://test".to_string()),
322                item_not_in_presentation: false,
323            }],
324        };
325        let decoded = Iinf::decode(buf).unwrap();
326        assert_eq!(decoded, iinf);
327    }
328
329    /// Encodes a libavif-style `uri ` item info box.
330    #[test]
331    fn test_iinf_avif_encode_uri() {
332        let iinf: Iinf = Iinf {
333            item_infos: vec![ItemInfoEntry {
334                item_id: 1,
335                item_protection_index: 0,
336                item_type: Some(FourCC::new(b"uri ")),
337                item_name: "Item".to_string(),
338                content_type: None,
339                content_encoding: None,
340                item_uri_type: Some("uri://test".to_string()),
341                item_not_in_presentation: false,
342            }],
343        };
344        let mut buf = Vec::new();
345        iinf.encode(&mut buf).unwrap();
346
347        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_URI);
348    }
349
350    /// Rejects a `uri ` item info box when the URI payload is missing.
351    #[test]
352    fn test_iinf_avif_encode_uri_invalid() {
353        let iinf: Iinf = Iinf {
354            item_infos: vec![ItemInfoEntry {
355                item_id: 1,
356                item_protection_index: 0,
357                item_type: Some(FourCC::new(b"uri ")),
358                item_name: "Item".to_string(),
359                content_type: None,
360                content_encoding: None,
361                item_uri_type: None, // encode will return an error because this is empty
362                item_not_in_presentation: false,
363            }],
364        };
365        let mut buf = Vec::new();
366        assert!(matches!(
367            iinf.encode(&mut buf),
368            Err(Error::MissingContent(_))
369        ));
370    }
371
372    /// Rejects a version 0 item info entry when its required content type is missing.
373    #[test]
374    fn test_iinf_encode_v0_without_content_type_returns_error() {
375        let iinf = Iinf {
376            item_infos: vec![ItemInfoEntry {
377                item_id: 1,
378                item_protection_index: 0,
379                item_type: None,
380                item_name: "Item".to_string(),
381                content_type: None,
382                content_encoding: None,
383                item_uri_type: None,
384                item_not_in_presentation: false,
385            }],
386        };
387        let mut buf = Vec::new();
388
389        assert!(matches!(
390            iinf.encode(&mut buf),
391            Err(Error::MissingContent(_))
392        ));
393    }
394
395    /// Rejects a `mime` item info entry when its required content type is missing.
396    #[test]
397    fn test_iinf_encode_mime_without_content_type_returns_error() {
398        let iinf = Iinf {
399            item_infos: vec![ItemInfoEntry {
400                item_id: 1,
401                item_protection_index: 0,
402                item_type: Some(FourCC::new(b"mime")),
403                item_name: "Item".to_string(),
404                content_type: None,
405                content_encoding: None,
406                item_uri_type: None,
407                item_not_in_presentation: false,
408            }],
409        };
410        let mut buf = Vec::new();
411
412        assert!(matches!(
413            iinf.encode(&mut buf),
414            Err(Error::MissingContent(_))
415        ));
416    }
417
418    /// Version 1 `infe` entries are reported as unsupported, not panicked.
419    #[test]
420    fn test_iinf_decode_unsupported_infe_v1_returns_error() {
421        let body: &[u8] = &[
422            // iinf version/flags and one entry
423            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // infe atom header
424            0x00, 0x00, 0x00, 0x13, b'i', b'n', b'f', b'e', // infe version 1, no flags
425            0x01, 0x00, 0x00, 0x00,
426            // item_id, item_protection_index, item_name, content_type,
427            // content_encoding
428            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
429        ];
430        let result = Iinf::decode_body(&mut std::io::Cursor::new(body));
431
432        assert!(matches!(
433            result,
434            Err(Error::Unsupported("infe version 1 extensions"))
435        ));
436
437        let fuzz_body: &[u8] = &[
438            0x00, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x80,
439            0x01, 0x00, 0x00, 0x04, 0x00, b'p', b'y', b't', b'f',
440        ];
441        let fuzz_result = Iinf::decode_body(&mut std::io::Cursor::new(fuzz_body));
442
443        assert!(matches!(
444            fuzz_result,
445            Err(Error::UnexpectedBox(kind)) if kind == FourCC::new(b"\0\0A\x80")
446        ));
447    }
448}