Skip to main content

mp4_atom/meta/ilst/
mod.rs

1mod covr;
2mod cprt;
3mod data;
4mod desc;
5mod name;
6mod tool;
7mod year;
8
9pub use covr::*;
10pub use cprt::*;
11pub use desc::*;
12pub use name::*;
13pub use tool::*;
14pub use year::*;
15
16use crate::*;
17
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Ilst {
21    pub name: Option<Name>,
22    pub year: Option<Year>, // Called day in the spec
23    pub covr: Option<Covr>,
24    pub desc: Option<Desc>,
25    pub ctoo: Option<Tool>,      // 4CC: "©too"
26    pub cprt: Option<Copyright>, // iTunes item, NOT the ISO CopyrightBox
27}
28
29impl Atom for Ilst {
30    const KIND: FourCC = FourCC::new(b"ilst");
31
32    fn decode_body<B: Buf>(buf: &mut B) -> Result<Self> {
33        let mut name = None;
34        let mut year = None;
35        let mut covr = None;
36        let mut desc = None;
37        let mut ctoo = None;
38        let mut cprt = None;
39
40        // `ilst` children live in the iTunes metadata namespace, which reuses
41        // fourccs of unrelated ISO atoms — an ilst `cprt` item wraps a `data`
42        // atom while the ISO CopyrightBox is a FullBox with a language code.
43        // Dispatch by header against the ilst namespace only, never through
44        // the global `Any` table.
45        while let Some(header) = Header::decode_maybe(buf)? {
46            let size = header.size.unwrap_or(buf.remaining());
47            if size > buf.remaining() {
48                // Truncated child: stop and let the caller's remaining-bytes
49                // check report it, matching `Any::decode_maybe`.
50                break;
51            }
52            match header.kind {
53                Name::KIND => name = Some(Name::decode_atom(&header, buf)?),
54                Year::KIND => year = Some(Year::decode_atom(&header, buf)?),
55                Covr::KIND => covr = Some(Covr::decode_atom(&header, buf)?),
56                Desc::KIND => desc = Some(Desc::decode_atom(&header, buf)?),
57                Tool::KIND => ctoo = Some(Tool::decode_atom(&header, buf)?),
58                Copyright::KIND => cprt = Some(Copyright::decode_atom(&header, buf)?),
59                kind => {
60                    let body = Vec::decode(&mut buf.slice(size))?;
61                    buf.advance(size);
62                    Self::decode_unknown(&Any::Unknown(kind, body))?;
63                }
64            }
65        }
66
67        Ok(Ilst {
68            name,
69            year,
70            covr,
71            desc,
72            ctoo,
73            cprt,
74        })
75    }
76
77    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
78        self.name.encode(buf)?;
79        self.year.encode(buf)?;
80        self.covr.encode(buf)?;
81        self.desc.encode(buf)?;
82        self.ctoo.encode(buf)?;
83        self.cprt.encode(buf)?;
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_ilst() {
94        let expected = Ilst {
95            year: Year("src_year".to_string()).into(),
96            ..Default::default()
97        };
98
99        let mut buf = Vec::new();
100        expected.encode(&mut buf).unwrap();
101
102        let mut buf = buf.as_ref();
103        let decoded = Ilst::decode(&mut buf).unwrap();
104        assert_eq!(decoded, expected);
105    }
106
107    #[test]
108    fn test_ilst_empty() {
109        let expected = Ilst::default();
110        let mut buf = Vec::new();
111        expected.encode(&mut buf).unwrap();
112
113        let mut buf = buf.as_ref();
114        let decoded = Ilst::decode(&mut buf).unwrap();
115        assert_eq!(decoded, expected);
116    }
117
118    // Build `[size:u32][fourcc][body]`.
119    fn atom_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
120        let mut v = Vec::with_capacity(8 + body.len());
121        v.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
122        v.extend_from_slice(fourcc);
123        v.extend_from_slice(body);
124        v
125    }
126
127    // An iTunes `cprt` item (as written by FFmpeg for `-metadata copyright`)
128    // wraps a `data` atom. Pre-fix, Ilst dispatched it through the global
129    // `Any` table where the fourcc collides with the ISO CopyrightBox, so the
130    // whole decode failed with UnderDecode(cprt).
131    #[test]
132    fn test_ilst_itunes_cprt() {
133        let text = b"(c) 2026 Example";
134        let mut data_body = Vec::new();
135        data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8
136        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
137        data_body.extend_from_slice(text);
138        let cprt_item = atom_box(b"cprt", &atom_box(b"data", &data_body));
139        let encoded = atom_box(b"ilst", &cprt_item);
140
141        let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap();
142        assert_eq!(
143            decoded,
144            Ilst {
145                cprt: Some(Copyright {
146                    country_indicator: 0,
147                    language_indicator: 0,
148                    text: "(c) 2026 Example".into(),
149                }),
150                ..Default::default()
151            }
152        );
153
154        // The encoder writes the same long-style `data` layout back.
155        let mut reencoded = Vec::new();
156        decoded.encode(&mut reencoded).unwrap();
157        assert_eq!(reencoded, encoded);
158    }
159
160    // Unknown ilst items still go through decode_unknown (an error in
161    // strict/test builds, a warning otherwise).
162    #[test]
163    fn test_ilst_unknown_item() {
164        let mut data_body = Vec::new();
165        data_body.extend_from_slice(&1u32.to_be_bytes());
166        data_body.extend_from_slice(&0u32.to_be_bytes());
167        data_body.extend_from_slice(b"Some Artist");
168        let art_item = atom_box(b"\xa9ART", &atom_box(b"data", &data_body));
169        let encoded = atom_box(b"ilst", &art_item);
170
171        let result = Ilst::decode(&mut encoded.as_slice());
172        match result {
173            Err(Error::UnexpectedBox(kind)) => assert_eq!(kind, FourCC::new(b"\xa9ART")),
174            other => panic!("expected UnexpectedBox, got {other:?}"),
175        }
176    }
177}