mp4-atom 0.12.1

A MP4/ISOBMFF atom decoder and encoder
Documentation
use crate::*;

// ItemInformationBox. ISO/IEC 14496-12:2022 Section 8.11.6
// This is used to work out what the items are

ext! {
    name: Iinf,
    versions: [0, 1],
    flags: {}
}

ext! {
    name: ItemInfoEntry,
    versions: [0, 1, 2, 3],
    flags: {item_not_in_presentation = 0,}
}

/// Describes one item stored in an `iinf` item information box.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ItemInfoEntry {
    /// Item identifier used by other item metadata boxes.
    pub item_id: u32,

    /// Index into the item protection box, or zero when unprotected.
    pub item_protection_index: u16,

    /// Version 2 and 3 item type, such as `mime` or `uri `.
    pub item_type: Option<FourCC>,

    /// Human-readable item name.
    pub item_name: String,

    /// MIME content type for `mime` item entries.
    pub content_type: Option<String>,

    /// MIME content encoding for `mime` item entries.
    pub content_encoding: Option<String>,

    /// URI payload for `uri ` item entries.
    pub item_uri_type: Option<String>,

    /// Whether the item is excluded from presentation.
    pub item_not_in_presentation: bool,
}

impl AtomExt for ItemInfoEntry {
    const KIND_EXT: FourCC = FourCC::new(b"infe");

    type Ext = ItemInfoEntryExt;

    /// Encodes the item info entry body and version/flags extension.
    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<Self::Ext> {
        // TODO: maybe work harder at versioning
        let version: ItemInfoEntryVersion = if self.item_id > u16::MAX as u32 {
            ItemInfoEntryVersion::V3
        } else {
            // version 0 or 2, since we don't support version 1 yet
            if self.item_type.is_some() {
                ItemInfoEntryVersion::V2
            } else {
                ItemInfoEntryVersion::V0
            }
        };
        if (version == ItemInfoEntryVersion::V0) || (version == ItemInfoEntryVersion::V1) {
            (self.item_id as u16).encode(buf)?;
            self.item_protection_index.encode(buf)?;
            self.item_name.as_str().encode(buf)?;
            self.content_type.clone().unwrap().as_str().encode(buf)?;
            self.content_encoding
                .clone()
                .unwrap_or("".to_string())
                .as_str()
                .encode(buf)?;
            if version == ItemInfoEntryVersion::V1 {
                return Err(Error::Unsupported("infe version 1 extensions"));
            }
        } else {
            if version == ItemInfoEntryVersion::V2 {
                (self.item_id as u16).encode(buf)?;
            } else {
                self.item_id.encode(buf)?;
            }
            self.item_protection_index.encode(buf)?;
            Some(self.item_type).encode(buf)?;
            self.item_name.as_str().encode(buf)?;
            if self.item_type == Some(FourCC::new(b"mime")) {
                self.content_type.clone().unwrap().as_str().encode(buf)?;
                self.content_encoding
                    .clone()
                    .unwrap_or("".to_string())
                    .as_str()
                    .encode(buf)?;
            } else if self.item_type == Some(FourCC::new(b"uri ")) {
                let item_uri_type = self.item_uri_type.as_ref().ok_or(Error::MissingContent(
                    "item_uri_type required with 'uri ' item_type",
                ))?;
                item_uri_type.as_str().encode(buf)?;
            }
        }
        Ok(ItemInfoEntryExt {
            version,
            item_not_in_presentation: self.item_not_in_presentation,
        })
    }

    /// Decodes the item info entry body for the parsed version/flags extension.
    fn decode_body_ext<B: Buf>(buf: &mut B, ext: Self::Ext) -> Result<Self> {
        let item_id: u32;
        let item_protection_index;
        let mut item_type = None;
        let item_name;
        let mut content_type = None;
        let mut content_encoding = None;
        let mut item_uri_type = None;
        if (ext.version == ItemInfoEntryVersion::V0) || (ext.version == ItemInfoEntryVersion::V1) {
            item_id = u16::decode(buf)? as u32;
            item_protection_index = u16::decode(buf)?;
            item_name = String::decode(buf)?;
            content_type = Some(String::decode(buf)?);
            content_encoding = Some(String::decode(buf)?);
            if ext.version == ItemInfoEntryVersion::V1 {
                return Err(Error::Unsupported("infe version 1 extensions"));
            }
        } else {
            if ext.version == ItemInfoEntryVersion::V2 {
                item_id = u16::decode(buf)? as u32;
            } else {
                item_id = u32::decode(buf)?;
            }
            item_protection_index = u16::decode(buf)?;
            item_type = Some(FourCC::decode(buf)?);
            item_name = String::decode(buf)?;
            if item_type == Some(FourCC::new(b"mime")) {
                content_type = Some(String::decode(buf)?);
                content_encoding = Some(String::decode(buf)?);
            } else if item_type == Some(FourCC::new(b"uri ")) {
                item_uri_type = Some(String::decode(buf)?);
            }
        }
        Ok(ItemInfoEntry {
            item_id,
            item_protection_index,
            item_type,
            item_name,
            content_type,
            content_encoding,
            item_uri_type,
            item_not_in_presentation: ext.item_not_in_presentation,
        })
    }
}

/// Item information box containing every item entry.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Iinf {
    /// Item info entries in file order.
    pub item_infos: Vec<ItemInfoEntry>,
}

impl AtomExt for Iinf {
    type Ext = IinfExt;

    const KIND_EXT: FourCC = FourCC::new(b"iinf");

    /// Decodes an item information box for the parsed version/flags extension.
    fn decode_body_ext<B: Buf>(buf: &mut B, ext: IinfExt) -> Result<Self> {
        let mut item_infos = vec![];
        let entry_count = if ext.version == IinfVersion::V0 {
            u16::decode(buf)? as usize
        } else {
            u32::decode(buf)? as usize
        };
        for _ in 0..entry_count {
            item_infos.push(ItemInfoEntry::decode(buf)?);
        }
        Ok(Iinf { item_infos })
    }

    /// Encodes an item information box and chooses the smallest entry-count version.
    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<IinfExt> {
        let version;
        if self.item_infos.len() > u16::MAX as usize {
            version = IinfVersion::V1;
            (self.item_infos.len() as u32).encode(buf)?
        } else {
            version = IinfVersion::V0;
            (self.item_infos.len() as u16).encode(buf)?
        }
        for item_info in &self.item_infos {
            item_info.encode(buf)?;
        }

        Ok(IinfExt { version })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const ENCODED_IINF_LIBAVIF_MIME: &[u8] = &[
        0, 0, 0, 65, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 51, 105, 110, 102, 101, 2, 0,
        0, 0, 0, 1, 0, 0, 109, 105, 109, 101, 73, 116, 101, 109, 0, 99, 111, 110, 116, 101, 110,
        116, 45, 116, 121, 112, 101, 0, 117, 110, 107, 110, 111, 119, 110, 47, 109, 105, 109, 101,
        0,
    ];

    /// Decodes a libavif-style `mime` item info box.
    #[test]
    fn test_iinf_libavif_decode_mime() {
        let buf: &mut std::io::Cursor<&&[u8]> =
            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_MIME);

        let iinf: Iinf = Iinf {
            item_infos: vec![ItemInfoEntry {
                item_id: 1,
                item_protection_index: 0,
                item_type: Some(FourCC::new(b"mime")),
                item_name: "Item".to_string(),
                content_type: Some("content-type".to_string()),
                content_encoding: Some("unknown/mime".to_string()),
                item_uri_type: None,
                item_not_in_presentation: false,
            }],
        };
        let decoded = Iinf::decode(buf).unwrap();
        assert_eq!(decoded, iinf);
    }

    /// Encodes a libavif-style `mime` item info box.
    #[test]
    fn test_iinf_avif_encode_mime() {
        let iinf: Iinf = Iinf {
            item_infos: vec![ItemInfoEntry {
                item_id: 1,
                item_protection_index: 0,
                item_type: Some(FourCC::new(b"mime")),
                item_name: "Item".to_string(),
                content_type: Some("content-type".to_string()),
                content_encoding: Some("unknown/mime".to_string()),
                item_uri_type: None,
                item_not_in_presentation: false,
            }],
        };
        let mut buf = Vec::new();
        iinf.encode(&mut buf).unwrap();

        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_MIME);
    }

    const ENCODED_IINF_LIBAVIF_URI: &[u8] = &[
        0, 0, 0, 50, 105, 105, 110, 102, 0, 0, 0, 0, 0, 1, 0, 0, 0, 36, 105, 110, 102, 101, 2, 0,
        0, 0, 0, 1, 0, 0, 117, 114, 105, 32, 73, 116, 101, 109, 0, 117, 114, 105, 58, 47, 47, 116,
        101, 115, 116, 0,
    ];

    /// Decodes a libavif-style `uri ` item info box.
    #[test]
    fn test_iinf_libavif_decode_uri() {
        let buf: &mut std::io::Cursor<&&[u8]> =
            &mut std::io::Cursor::new(&ENCODED_IINF_LIBAVIF_URI);

        let iinf: Iinf = Iinf {
            item_infos: vec![ItemInfoEntry {
                item_id: 1,
                item_protection_index: 0,
                item_type: Some(FourCC::new(b"uri ")),
                item_name: "Item".to_string(),
                content_type: None,
                content_encoding: None,
                item_uri_type: Some("uri://test".to_string()),
                item_not_in_presentation: false,
            }],
        };
        let decoded = Iinf::decode(buf).unwrap();
        assert_eq!(decoded, iinf);
    }

    /// Encodes a libavif-style `uri ` item info box.
    #[test]
    fn test_iinf_avif_encode_uri() {
        let iinf: Iinf = Iinf {
            item_infos: vec![ItemInfoEntry {
                item_id: 1,
                item_protection_index: 0,
                item_type: Some(FourCC::new(b"uri ")),
                item_name: "Item".to_string(),
                content_type: None,
                content_encoding: None,
                item_uri_type: Some("uri://test".to_string()),
                item_not_in_presentation: false,
            }],
        };
        let mut buf = Vec::new();
        iinf.encode(&mut buf).unwrap();

        assert_eq!(buf.as_slice(), ENCODED_IINF_LIBAVIF_URI);
    }

    /// Rejects a `uri ` item info box when the URI payload is missing.
    #[test]
    fn test_iinf_avif_encode_uri_invalid() {
        let iinf: Iinf = Iinf {
            item_infos: vec![ItemInfoEntry {
                item_id: 1,
                item_protection_index: 0,
                item_type: Some(FourCC::new(b"uri ")),
                item_name: "Item".to_string(),
                content_type: None,
                content_encoding: None,
                item_uri_type: None, // encode will return an error because this is empty
                item_not_in_presentation: false,
            }],
        };
        let mut buf = Vec::new();
        assert!(matches!(
            iinf.encode(&mut buf),
            Err(Error::MissingContent(_))
        ));
    }

    /// Version 1 `infe` entries are reported as unsupported, not panicked.
    #[test]
    fn test_iinf_decode_unsupported_infe_v1_returns_error() {
        let body: &[u8] = &[
            // iinf version/flags and one entry
            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // infe atom header
            0x00, 0x00, 0x00, 0x13, b'i', b'n', b'f', b'e', // infe version 1, no flags
            0x01, 0x00, 0x00, 0x00,
            // item_id, item_protection_index, item_name, content_type,
            // content_encoding
            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
        ];
        let result = Iinf::decode_body(&mut std::io::Cursor::new(body));

        assert!(matches!(
            result,
            Err(Error::Unsupported("infe version 1 extensions"))
        ));

        let fuzz_body: &[u8] = &[
            0x00, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x80,
            0x01, 0x00, 0x00, 0x04, 0x00, b'p', b'y', b't', b'f',
        ];
        let fuzz_result = Iinf::decode_body(&mut std::io::Cursor::new(fuzz_body));

        assert!(matches!(
            fuzz_result,
            Err(Error::Unsupported("infe version 1 extensions"))
        ));
    }
}