Skip to main content

mp4_atom/moov/udta/
mod.rs

1mod chpl;
2mod cprt;
3mod kind;
4mod rtng;
5mod skip;
6
7pub use chpl::*;
8pub use cprt::*;
9pub use kind::*;
10pub use rtng::*;
11pub use skip::*;
12
13use crate::*;
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Udta {
18    /// Zero or one, a Nero-style chapter list (not part of the ISO/QuickTime spec).
19    pub chpl: Option<Chpl>,
20    /// Zero or more, one per language (ISO/IEC 14496-12 §8.10.2).
21    pub cprt: Vec<Cprt>,
22    /// Zero or more, one per role/kind label (ISO/IEC 14496-12 §8.10.4).
23    pub kind: Vec<Kind>,
24    /// Zero or one (ISO/IEC 14496-12 §8.11.1).
25    pub meta: Option<Meta>,
26    /// Zero or more, one per language (3GPP TS 26.244 clause 8).
27    pub rtng: Vec<Rtng>,
28}
29
30impl Atom for Udta {
31    const KIND: FourCC = FourCC::new(b"udta");
32
33    fn decode_body<B: Buf>(buf: &mut B) -> Result<Self> {
34        let mut chpl = None;
35        let mut cprt = Vec::new();
36        let mut kind = Vec::new();
37        let mut meta = None;
38        let mut rtng = Vec::new();
39
40        // `udta` is a free-form user-data container. QuickTime writes a
41        // track-name `name` box here, whose fourcc collides with the iTunes
42        // `ilst` Name item in the global atom table, so dispatch children by
43        // header against the udta namespace only — the same shape as
44        // `Ilst::decode_body` — rather than through `Any`. Unknown children go
45        // to `decode_unknown` (a warning by default, an error under strict).
46        while let Some(header) = Header::decode_maybe(buf)? {
47            let size = header.size.unwrap_or(buf.remaining());
48            if size > buf.remaining() {
49                // A child whose declared size exceeds what remains is truncated.
50                return Err(Error::OutOfBounds);
51            }
52            // Multiplicity follows the specs: `cprt` and `kind` are "zero or
53            // more" (ISO/IEC 14496-12 §8.10.2 — one copyright notice per
54            // language — and §8.10.4 — one box per role value), and 3GPP
55            // TS 26.244 clause 8 admits each asset box (`rtng` included) once
56            // per language, so repeats accumulate. `meta` is "zero or one" per
57            // container (ISO/IEC 14496-12 §8.11.1), so a second one is
58            // malformed.
59            match header.kind {
60                Chpl::KIND => {
61                    if chpl.is_some() {
62                        return Err(Error::DuplicateBox(Chpl::KIND));
63                    }
64                    chpl = Some(Chpl::decode_atom(&header, buf)?);
65                }
66                Cprt::KIND => cprt.push(Cprt::decode_atom(&header, buf)?),
67                Kind::KIND => kind.push(Kind::decode_atom(&header, buf)?),
68                Meta::KIND => {
69                    if meta.is_some() {
70                        return Err(Error::DuplicateBox(Meta::KIND));
71                    }
72                    meta = Some(Meta::decode_atom(&header, buf)?);
73                }
74                Rtng::KIND => rtng.push(Rtng::decode_atom(&header, buf)?),
75                // `free`/`skip` are padding boxes; drop them.
76                Free::KIND | Skip::KIND => {
77                    buf.advance(size);
78                }
79                unknown => {
80                    let body = Vec::decode(&mut buf.slice(size))?;
81                    buf.advance(size);
82                    Self::decode_unknown(&Any::Unknown(unknown, body))?;
83                }
84            }
85        }
86        // Drop any sub-header padding remainder after the children.
87        skip_trailing_padding(buf);
88
89        Ok(Udta {
90            chpl,
91            cprt,
92            kind,
93            meta,
94            rtng,
95        })
96    }
97
98    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
99        self.chpl.encode(buf)?;
100        self.cprt.encode(buf)?;
101        self.meta.encode(buf)?;
102        self.kind.encode(buf)?;
103        self.rtng.encode(buf)?;
104        Ok(())
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_udta_empty() {
114        let expected = Udta {
115            chpl: None,
116            cprt: vec![],
117            meta: None,
118            kind: vec![],
119            rtng: vec![],
120        };
121
122        let mut buf = Vec::new();
123        expected.encode(&mut buf).unwrap();
124
125        let mut buf = buf.as_ref();
126        let output = Udta::decode(&mut buf).unwrap();
127        assert_eq!(output, expected);
128    }
129
130    #[test]
131    fn test_udta() {
132        let expected = Udta {
133            chpl: None,
134            cprt: vec![Cprt {
135                language: "und".into(),
136                notice: "MIT or Apache".into(),
137            }],
138            meta: Some(Meta {
139                hdlr: Hdlr {
140                    handler: FourCC::new(b"fake"),
141                    name: "".into(),
142                },
143                items: vec![],
144            }),
145            kind: vec![Kind {
146                scheme_uri: "http://www.w3.org/TR/html5/".into(),
147                value: "".into(),
148            }],
149            rtng: vec![Rtng {
150                entity: b"BBFC".into(),
151                criteria: b"PG13".into(),
152                language: "eng".into(),
153                rating_info: "test info".into(),
154            }],
155        };
156
157        let mut buf = Vec::new();
158        expected.encode(&mut buf).unwrap();
159
160        let mut buf = buf.as_ref();
161        let output = Udta::decode(&mut buf).unwrap();
162        assert_eq!(output, expected);
163    }
164
165    // From MPEG File Format Conformance, isobmff/02_dref_edts_img.mp4
166    const ENCODED_UDTA_WITH_CPRT: &[u8] = &[
167        0x00, 0x00, 0x00, 0x70, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x68, 0x63, 0x70, 0x72,
168        0x74, 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x45, 0x4e, 0x53, 0x54, 0x20, 0x49, 0x73, 0x6f,
169        0x4d, 0x65, 0x64, 0x69, 0x61, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e,
170        0x63, 0x65, 0x20, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x20, 0x2d, 0x20, 0x45, 0x4e, 0x53, 0x54,
171        0x20, 0x28, 0x63, 0x29, 0x20, 0x32, 0x30, 0x30, 0x36, 0x20, 0x2d, 0x20, 0x52, 0x69, 0x67,
172        0x68, 0x74, 0x73, 0x20, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x20, 0x66, 0x6f,
173        0x72, 0x20, 0x49, 0x53, 0x4f, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e,
174        0x63, 0x65, 0x20, 0x75, 0x73, 0x65, 0x00,
175    ];
176
177    #[test]
178    fn test_udta_cprt() {
179        let mut buf = std::io::Cursor::new(ENCODED_UDTA_WITH_CPRT);
180
181        let udta = Udta::decode(&mut buf).expect("failed to decode udta");
182
183        assert_eq!(
184            udta,
185            Udta {
186                cprt: vec![Cprt { language: "und".into(), notice: "ENST IsoMedia Conformance Files - ENST (c) 2006 - Rights released for ISO Conformance use".into() }],
187                ..Default::default()
188            }
189        );
190
191        let mut buf = Vec::new();
192        udta.encode(&mut buf).unwrap();
193
194        assert_eq!(buf, ENCODED_UDTA_WITH_CPRT);
195    }
196
197    // From MPEG File Format Conformance, nalu/hevc/hev1_clg1_header.mp4
198    const ENCODED_UDTA_WITH_KIND: &[u8] = &[
199        0x00, 0x00, 0x00, 0x31, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x29, 0x6b, 0x69, 0x6e,
200        0x64, 0x00, 0x00, 0x00, 0x00, 0x75, 0x72, 0x6e, 0x3a, 0x6d, 0x70, 0x65, 0x67, 0x3a, 0x64,
201        0x61, 0x73, 0x68, 0x3a, 0x72, 0x6f, 0x6c, 0x65, 0x3a, 0x32, 0x30, 0x31, 0x31, 0x00, 0x6d,
202        0x61, 0x69, 0x6e, 0x00,
203    ];
204
205    #[test]
206    fn test_udta_kind() {
207        let mut buf = std::io::Cursor::new(ENCODED_UDTA_WITH_KIND);
208
209        let udta = Udta::decode(&mut buf).expect("failed to decode udta");
210
211        assert_eq!(
212            udta,
213            Udta {
214                kind: vec![Kind {
215                    scheme_uri: "urn:mpeg:dash:role:2011".into(),
216                    value: "main".into()
217                }],
218                ..Default::default()
219            }
220        );
221
222        let mut buf = Vec::new();
223        udta.encode(&mut buf).unwrap();
224
225        assert_eq!(buf, ENCODED_UDTA_WITH_KIND);
226    }
227
228    // A track-level QuickTime `udta` holding an empty (4-null-byte) track-name
229    // `name` box. Its fourcc collides with the iTunes `ilst` Name item in the
230    // global atom table.
231    const ENCODED_UDTA_WITH_QT_NAME: &[u8] = &[
232        0x00, 0x00, 0x00, 0x14, 0x75, 0x64, 0x74, 0x61, // udta, size 20
233        0x00, 0x00, 0x00, 0x0c, 0x6e, 0x61, 0x6d, 0x65, // name, size 12
234        0x00, 0x00, 0x00, 0x00, // body: empty QuickTime track-name string
235    ];
236
237    // A sub-header padding remainder after a `udta`'s children must be skipped
238    // as padding while the real children still decode.
239    #[test]
240    fn test_udta_trailing_padding() {
241        let mut buf = ENCODED_UDTA_WITH_CPRT.to_vec();
242        buf.extend_from_slice(&[0, 0, 0, 0]);
243        let size = (buf.len() as u32).to_be_bytes();
244        buf[0..4].copy_from_slice(&size);
245
246        let udta = Udta::decode(&mut buf.as_slice()).expect("trailing padding must be tolerated");
247        assert_eq!(
248            udta.cprt.len(),
249            1,
250            "the real child still decodes, the padding is dropped"
251        );
252    }
253
254    #[test]
255    fn test_udta_quicktime_name_not_misparsed() {
256        // The udta-namespace dispatch must NOT route `name` to the global
257        // `Any` table's ilst Name item (which would UnderDecode the 4-byte
258        // body). Under `cfg(test)` an unknown child is an error, proving the
259        // atom is treated as unknown-to-udta rather than misparsed as ilst.
260        let mut buf = std::io::Cursor::new(ENCODED_UDTA_WITH_QT_NAME);
261        match Udta::decode(&mut buf) {
262            Err(Error::UnexpectedBox(kind)) => assert_eq!(kind, FourCC::new(b"name")),
263            other => panic!("expected UnexpectedBox(name), got {other:?}"),
264        }
265    }
266
267    // Repeated `cprt`/`kind`/`rtng` children are spec-legal — ISO/IEC 14496-12
268    // declares both CopyrightBox (§8.10.2) and KindBox (§8.10.4) as quantity
269    // "zero or more" (one copyright per language, one kind per role value),
270    // and 3GPP TS 26.244 clause 8 admits "zero or more sub-boxes of each kind,
271    // zero or one for each language" of the asset boxes, `rtng` included.
272    // Repeats must accumulate — not error, not overwrite each other.
273    #[test]
274    fn test_udta_repeated_children() {
275        let expected = Udta {
276            chpl: None,
277            cprt: vec![
278                Cprt {
279                    language: "eng".into(),
280                    notice: "All rights reserved".into(),
281                },
282                Cprt {
283                    language: "fra".into(),
284                    notice: "Tous droits réservés".into(),
285                },
286            ],
287            kind: vec![
288                Kind {
289                    scheme_uri: "urn:mpeg:dash:role:2011".into(),
290                    value: "main".into(),
291                },
292                Kind {
293                    scheme_uri: "urn:mpeg:dash:role:2011".into(),
294                    value: "caption".into(),
295                },
296            ],
297            meta: None,
298            rtng: vec![
299                Rtng {
300                    entity: b"BBFC".into(),
301                    criteria: b"PG13".into(),
302                    language: "eng".into(),
303                    rating_info: "parental guidance".into(),
304                },
305                Rtng {
306                    entity: b"BBFC".into(),
307                    criteria: b"PG13".into(),
308                    language: "fra".into(),
309                    rating_info: "accord parental".into(),
310                },
311            ],
312        };
313
314        let mut buf = Vec::new();
315        expected.encode(&mut buf).unwrap();
316
317        let mut buf = buf.as_ref();
318        let output = Udta::decode(&mut buf).unwrap();
319        assert_eq!(output, expected);
320    }
321
322    // `meta` is quantity "zero or one" per container (ISO/IEC 14496-12
323    // §8.11.1) — unlike the repeatable children above, a second one is
324    // malformed and must be rejected, not silently overwritten.
325    #[test]
326    fn test_udta_duplicate_meta() {
327        let meta = Meta {
328            hdlr: Hdlr {
329                handler: FourCC::new(b"fake"),
330                name: "".into(),
331            },
332            items: vec![],
333        };
334        let mut body = Vec::new();
335        meta.encode(&mut body).unwrap();
336        meta.encode(&mut body).unwrap();
337
338        let mut bytes = Vec::new();
339        bytes.extend_from_slice(&((body.len() + 8) as u32).to_be_bytes());
340        bytes.extend_from_slice(b"udta");
341        bytes.extend_from_slice(&body);
342
343        match Udta::decode(&mut bytes.as_slice()) {
344            Err(Error::DuplicateBox(kind)) => assert_eq!(kind, Meta::KIND),
345            other => panic!("expected DuplicateBox(meta), got {other:?}"),
346        }
347    }
348}