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            buf.encode_atom(*index, |buf| data.encode(buf))?;
119        }
120
121        Ok(())
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_ilst() {
131        let expected = Ilst {
132            year: Year("src_year".to_string()).into(),
133            ..Default::default()
134        };
135
136        let mut buf = Vec::new();
137        expected.encode(&mut buf).unwrap();
138
139        let mut buf = buf.as_ref();
140        let decoded = Ilst::decode(&mut buf).unwrap();
141        assert_eq!(decoded, expected);
142    }
143
144    #[test]
145    fn test_ilst_empty() {
146        let expected = Ilst::default();
147        let mut buf = Vec::new();
148        expected.encode(&mut buf).unwrap();
149
150        let mut buf = buf.as_ref();
151        let decoded = Ilst::decode(&mut buf).unwrap();
152        assert_eq!(decoded, expected);
153    }
154
155    // Build `[size:u32][fourcc][body]`.
156    fn atom_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
157        let mut v = Vec::with_capacity(8 + body.len());
158        v.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
159        v.extend_from_slice(fourcc);
160        v.extend_from_slice(body);
161        v
162    }
163
164    // An iTunes `cprt` item (as written by FFmpeg for `-metadata copyright`)
165    // wraps a `data` atom. Pre-fix, Ilst dispatched it through the global
166    // `Any` table where the fourcc collides with the ISO CopyrightBox, so the
167    // whole decode failed with UnderDecode(cprt).
168    #[test]
169    fn test_ilst_itunes_cprt() {
170        let text = b"(c) 2026 Example";
171        let mut data_body = Vec::new();
172        data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8
173        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
174        data_body.extend_from_slice(text);
175        let cprt_item = atom_box(b"cprt", &atom_box(b"data", &data_body));
176        let encoded = atom_box(b"ilst", &cprt_item);
177
178        let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap();
179        assert_eq!(
180            decoded,
181            Ilst {
182                cprt: Some(Copyright {
183                    country_indicator: 0,
184                    language_indicator: 0,
185                    text: "(c) 2026 Example".into(),
186                }),
187                ..Default::default()
188            }
189        );
190
191        // The encoder writes the same long-style `data` layout back.
192        let mut reencoded = Vec::new();
193        decoded.encode(&mut reencoded).unwrap();
194        assert_eq!(reencoded, encoded);
195    }
196
197    // Unknown ilst items still go through decode_unknown (an error in
198    // strict/test builds, a warning otherwise).
199    #[test]
200    fn test_ilst_unknown_item() {
201        let mut data_body = Vec::new();
202        data_body.extend_from_slice(&1u32.to_be_bytes());
203        data_body.extend_from_slice(&0u32.to_be_bytes());
204        data_body.extend_from_slice(b"Some Artist");
205        let art_item = atom_box(b"\xa9ART", &atom_box(b"data", &data_body));
206        let encoded = atom_box(b"ilst", &art_item);
207
208        let result = Ilst::decode(&mut encoded.as_slice());
209        match result {
210            Err(Error::UnexpectedBox(kind)) => assert_eq!(kind, FourCC::new(b"\xa9ART")),
211            other => panic!("expected UnexpectedBox, got {other:?}"),
212        }
213    }
214
215    // Apple's `mdta`-keyed metadata scheme (used alongside a sibling `keys`
216    // box) keys each `ilst` item by a raw 1-based index rather than a
217    // well-known fourcc — the index is stored where a fourcc would be, with
218    // its top byte zero (so it can't collide with a real ASCII/Latin1 tag).
219    #[test]
220    fn test_ilst_mdta_item() {
221        let mut data_body = Vec::new();
222        data_body.extend_from_slice(&1u32.to_be_bytes()); // type indicator: UTF-8
223        data_body.extend_from_slice(&0u32.to_be_bytes()); // country + language
224        data_body.extend_from_slice(b"Apple");
225        let item = atom_box(&1u32.to_be_bytes(), &atom_box(b"data", &data_body));
226        let encoded = atom_box(b"ilst", &item);
227
228        let decoded = Ilst::decode(&mut encoded.as_slice()).unwrap();
229        assert_eq!(
230            decoded,
231            Ilst {
232                mdta: vec![(
233                    1,
234                    IlstData {
235                        country_indicator: 0,
236                        language_indicator: 0,
237                        value: IlstDataValue::Utf8("Apple".into()),
238                    }
239                )],
240                ..Default::default()
241            }
242        );
243
244        // The encoder writes the same long-style `data` layout back.
245        let mut reencoded = Vec::new();
246        decoded.encode(&mut reencoded).unwrap();
247        assert_eq!(reencoded, encoded);
248    }
249
250    // An out-of-range index would encode as a FourCC whose top byte is
251    // nonzero, which could look like (or collide with) a real ASCII/Latin1
252    // tag on the next decode -- reject it instead of silently corrupting it.
253    #[test]
254    fn test_ilst_mdta_index_out_of_range_rejected() {
255        let ilst = Ilst {
256            mdta: vec![(
257                MDTA_INDEX_LIMIT,
258                IlstData {
259                    country_indicator: 0,
260                    language_indicator: 0,
261                    value: IlstDataValue::Utf8("Apple".into()),
262                },
263            )],
264            ..Default::default()
265        };
266
267        let mut buf = Vec::new();
268        let err = ilst.encode(&mut buf).unwrap_err();
269        assert!(matches!(err, Error::Unsupported(_)));
270    }
271}