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 data::*;
12pub use desc::*;
13pub use name::*;
14pub use tool::*;
15pub use year::*;
16
17use crate::*;
18
19/// Below this value, a child's raw fourcc looks like a big-endian 1-based
20/// index (top byte zero) rather than 4 ASCII/Latin1 tag characters — the
21/// scheme Apple's `mdta`-keyed metadata (see [`Keys`]) uses for `ilst` items.
22const MDTA_INDEX_LIMIT: u32 = 0x0100_0000;
23
24#[derive(Debug, Clone, PartialEq, Default)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Ilst {
27    pub name: Option<Name>,
28    pub year: Option<Year>, // Called day in the spec
29    pub covr: Option<Covr>,
30    pub desc: Option<Desc>,
31    pub ctoo: Option<Tool>,      // 4CC: "©too"
32    pub cprt: Option<Copyright>, // iTunes item, NOT the ISO CopyrightBox
33
34    /// Apple `mdta`-keyed items, keyed by their 1-based index into the
35    /// sibling [`Keys`] box's entries.
36    pub mdta: Vec<(u32, IlstData)>,
37}
38
39impl Atom for Ilst {
40    const KIND: FourCC = FourCC::new(b"ilst");
41
42    fn decode_body<B: Buf>(buf: &mut B) -> Result<Self> {
43        let mut name = None;
44        let mut year = None;
45        let mut covr = None;
46        let mut desc = None;
47        let mut ctoo = None;
48        let mut cprt = None;
49        let mut mdta = vec![];
50
51        // `ilst` children live in the iTunes metadata namespace, which reuses
52        // fourccs of unrelated ISO atoms — an ilst `cprt` item wraps a `data`
53        // atom while the ISO CopyrightBox is a FullBox with a language code.
54        // Dispatch by header against the ilst namespace only, never through
55        // the global `Any` table.
56        while let Some(header) = Header::decode_maybe(buf)? {
57            let size = header.size.unwrap_or(buf.remaining());
58            if size > buf.remaining() {
59                // Truncated child: stop and let the caller's remaining-bytes
60                // check report it, matching `Any::decode_maybe`.
61                break;
62            }
63            match header.kind {
64                Name::KIND => name = Some(Name::decode_atom(&header, buf)?),
65                Year::KIND => year = Some(Year::decode_atom(&header, buf)?),
66                Covr::KIND => covr = Some(Covr::decode_atom(&header, buf)?),
67                Desc::KIND => desc = Some(Desc::decode_atom(&header, buf)?),
68                Tool::KIND => ctoo = Some(Tool::decode_atom(&header, buf)?),
69                Copyright::KIND => cprt = Some(Copyright::decode_atom(&header, buf)?),
70                kind => {
71                    let index = u32::from(kind);
72
73                    let mdta_item = if index < MDTA_INDEX_LIMIT {
74                        IlstData::decode(&mut buf.slice(size)).ok()
75                    } else {
76                        None
77                    };
78
79                    if let Some(data) = mdta_item {
80                        mdta.push((index, data))
81                    } else {
82                        let body = Vec::decode(&mut buf.slice(size))?;
83                        Self::decode_unknown(&Any::Unknown(kind, body))?;
84                    }
85
86                    buf.advance(size);
87                }
88            }
89        }
90
91        Ok(Ilst {
92            name,
93            year,
94            covr,
95            desc,
96            ctoo,
97            cprt,
98            mdta,
99        })
100    }
101
102    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
103        self.name.encode(buf)?;
104        self.year.encode(buf)?;
105        self.covr.encode(buf)?;
106        self.desc.encode(buf)?;
107        self.ctoo.encode(buf)?;
108        self.cprt.encode(buf)?;
109
110        for (index, data) in &self.mdta {
111            if *index == 0 || *index >= MDTA_INDEX_LIMIT {
112                // A top-byte-nonzero index would encode as a FourCC that
113                // looks like (or collides with) a real ASCII/Latin1 tag,
114                // silently changing meaning on the next decode.
115                return Err(Error::Unsupported("mdta index out of range"));
116            }
117
118            let start = buf.len();
119            0u32.encode(buf)?; // size placeholder
120            FourCC::from(*index).encode(buf)?;
121            data.encode(buf)?;
122
123            let size: u32 = (buf.len() - start)
124                .try_into()
125                .map_err(|_| Error::TooLarge(FourCC::from(*index)))?;
126            buf.set_slice(start, &size.to_be_bytes());
127        }
128
129        Ok(())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_ilst() {
139        let expected = Ilst {
140            year: Year("src_year".to_string()).into(),
141            ..Default::default()
142        };
143
144        let mut buf = Vec::new();
145        expected.encode(&mut buf).unwrap();
146
147        let mut buf = buf.as_ref();
148        let decoded = Ilst::decode(&mut buf).unwrap();
149        assert_eq!(decoded, expected);
150    }
151
152    #[test]
153    fn test_ilst_empty() {
154        let expected = Ilst::default();
155        let mut buf = Vec::new();
156        expected.encode(&mut buf).unwrap();
157
158        let mut buf = buf.as_ref();
159        let decoded = Ilst::decode(&mut buf).unwrap();
160        assert_eq!(decoded, expected);
161    }
162
163    // Build `[size:u32][fourcc][body]`.
164    fn atom_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
165        let mut v = Vec::with_capacity(8 + body.len());
166        v.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
167        v.extend_from_slice(fourcc);
168        v.extend_from_slice(body);
169        v
170    }
171
172    // An iTunes `cprt` item (as written by FFmpeg for `-metadata copyright`)
173    // wraps a `data` atom. Pre-fix, Ilst dispatched it through the global
174    // `Any` table where the fourcc collides with the ISO CopyrightBox, so the
175    // whole decode failed with UnderDecode(cprt).
176    #[test]
177    fn test_ilst_itunes_cprt() {
178        let text = b"(c) 2026 Example";
179        let mut data_body = Vec::new();
180        data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8
181        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
182        data_body.extend_from_slice(text);
183        let cprt_item = atom_box(b"cprt", &atom_box(b"data", &data_body));
184        let encoded = atom_box(b"ilst", &cprt_item);
185
186        let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap();
187        assert_eq!(
188            decoded,
189            Ilst {
190                cprt: Some(Copyright {
191                    country_indicator: 0,
192                    language_indicator: 0,
193                    text: "(c) 2026 Example".into(),
194                }),
195                ..Default::default()
196            }
197        );
198
199        // The encoder writes the same long-style `data` layout back.
200        let mut reencoded = Vec::new();
201        decoded.encode(&mut reencoded).unwrap();
202        assert_eq!(reencoded, encoded);
203    }
204
205    // Unknown ilst items still go through decode_unknown (an error in
206    // strict/test builds, a warning otherwise).
207    #[test]
208    fn test_ilst_unknown_item() {
209        let mut data_body = Vec::new();
210        data_body.extend_from_slice(&1u32.to_be_bytes());
211        data_body.extend_from_slice(&0u32.to_be_bytes());
212        data_body.extend_from_slice(b"Some Artist");
213        let art_item = atom_box(b"\xa9ART", &atom_box(b"data", &data_body));
214        let encoded = atom_box(b"ilst", &art_item);
215
216        let result = Ilst::decode(&mut encoded.as_slice());
217        match result {
218            Err(Error::UnexpectedBox(kind)) => assert_eq!(kind, FourCC::new(b"\xa9ART")),
219            other => panic!("expected UnexpectedBox, got {other:?}"),
220        }
221    }
222
223    // Apple's `mdta`-keyed metadata scheme (used alongside a sibling `keys`
224    // box) keys each `ilst` item by a raw 1-based index rather than a
225    // well-known fourcc — the index is stored where a fourcc would be, with
226    // its top byte zero (so it can't collide with a real ASCII/Latin1 tag).
227    #[test]
228    fn test_ilst_mdta_item() {
229        let mut data_body = Vec::new();
230        data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8
231        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
232        data_body.extend_from_slice(b"Apple");
233        let item = atom_box(&1u32.to_be_bytes(), &atom_box(b"data", &data_body));
234        let encoded = atom_box(b"ilst", &item);
235
236        let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap();
237        assert_eq!(
238            decoded,
239            Ilst {
240                mdta: vec![(
241                    1,
242                    IlstData {
243                        country_indicator: 0,
244                        language_indicator: 0,
245                        value: IlstDataValue::Utf8("Apple".into()),
246                    }
247                )],
248                ..Default::default()
249            }
250        );
251
252        // The encoder writes the same long-style `data` layout back.
253        let mut reencoded = Vec::new();
254        decoded.encode(&mut reencoded).unwrap();
255        assert_eq!(reencoded, encoded);
256    }
257
258    // An out-of-range index would encode as a FourCC whose top byte is
259    // nonzero, which could look like (or collide with) a real ASCII/Latin1
260    // tag on the next decode -- reject it instead of silently corrupting it.
261    #[test]
262    fn test_ilst_mdta_index_out_of_range_rejected() {
263        let ilst = Ilst {
264            mdta: vec![(
265                MDTA_INDEX_LIMIT,
266                IlstData {
267                    country_indicator: 0,
268                    language_indicator: 0,
269                    value: IlstDataValue::Utf8("Apple".into()),
270                },
271            )],
272            ..Default::default()
273        };
274
275        let mut buf = Vec::new();
276        let err = ilst.encode(&mut buf).unwrap_err();
277        assert!(matches!(err, Error::Unsupported(_)));
278    }
279}