Skip to main content

mp4_atom/meta/
iloc.rs

1use crate::*;
2
3// ItemInformationBox. ISO/IEC 14496-12:2022 Section 8.11.3
4// This is used to work out where the items are
5
6ext! {
7    name: Iloc,
8    versions: [0, 1, 2],
9    flags: {}
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct ItemLocationExtent {
15    pub item_reference_index: u64,
16    pub offset: u64,
17    pub length: u64,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct ItemLocation {
23    pub item_id: u32,
24    pub construction_method: u8, // enum?
25    pub data_reference_index: u16,
26    pub base_offset: u64,
27    pub extents: Vec<ItemLocationExtent>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct Iloc {
33    pub item_locations: Vec<ItemLocation>,
34}
35
36impl AtomExt for Iloc {
37    type Ext = IlocExt;
38
39    const KIND_EXT: FourCC = FourCC::new(b"iloc");
40
41    fn decode_body_ext<B: Buf>(buf: &mut B, ext: IlocExt) -> Result<Self> {
42        let sizes0 = u8::decode(buf)?;
43        let offset_size = sizes0 >> 4;
44        let length_size = sizes0 & 0x0F;
45        let sizes1 = u8::decode(buf)?;
46        let base_offset_size = sizes1 >> 4;
47        let index_size: u8 = if ext.version == IlocVersion::V1 || ext.version == IlocVersion::V2 {
48            sizes1 & 0x0F
49        } else {
50            0
51        };
52
53        let zero_size_extents = offset_size == 0 && length_size == 0 && index_size == 0;
54
55        let item_count = if ext.version == IlocVersion::V0 || ext.version == IlocVersion::V1 {
56            u16::decode(buf)? as usize
57        } else {
58            u32::decode(buf)? as usize
59        };
60        let mut item_locations = vec![];
61        for _i in 0..item_count {
62            let item_id = if ext.version == IlocVersion::V0 || ext.version == IlocVersion::V1 {
63                u16::decode(buf)? as u32
64            } else {
65                u32::decode(buf)?
66            };
67            let construction_method: u8 =
68                if ext.version == IlocVersion::V1 || ext.version == IlocVersion::V2 {
69                    let construction_method_packed = u16::decode(buf)?;
70                    (construction_method_packed & 0x0f) as u8
71                } else {
72                    0
73                };
74            let data_reference_index = u16::decode(buf)?;
75            let base_offset = match base_offset_size {
76                0 => 0u64,
77                4 => u32::decode(buf)? as u64,
78                8 => u64::decode(buf)?,
79                _ => return Err(Error::Reserved),
80            };
81            let extent_count = u16::decode(buf)?;
82            // A zero-size extent reads no bytes, so a few bytes could otherwise declare
83            // 65,535 of them per item. Only the first may be zero-size: a lone extent with
84            // no offset or length still names the whole data reference.
85            if zero_size_extents && extent_count > 1 {
86                return Err(Error::OutOfBounds);
87            }
88            let mut extents = vec![];
89            for _j in 0..extent_count {
90                let item_reference_index: u64 =
91                    if ext.version == IlocVersion::V1 || ext.version == IlocVersion::V2 {
92                        match index_size {
93                            0 => 0,
94                            4 => u32::decode(buf)? as u64,
95                            8 => u64::decode(buf)?,
96                            _ => return Err(Error::Reserved),
97                        }
98                    } else {
99                        0
100                    };
101                let extent_offset = match offset_size {
102                    0 => 0u64,
103                    4 => u32::decode(buf)? as u64,
104                    8 => u64::decode(buf)?,
105                    _ => return Err(Error::Reserved),
106                };
107                let extent_length = match length_size {
108                    0 => 0u64,
109                    4 => u32::decode(buf)? as u64,
110                    8 => u64::decode(buf)?,
111                    _ => return Err(Error::Reserved),
112                };
113                extents.push(ItemLocationExtent {
114                    item_reference_index,
115                    offset: extent_offset,
116                    length: extent_length,
117                });
118            }
119            item_locations.push(ItemLocation {
120                item_id,
121                construction_method,
122                data_reference_index,
123                base_offset,
124                extents,
125            })
126        }
127        Ok(Iloc { item_locations })
128    }
129
130    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<IlocExt> {
131        let item_count: u32 = self
132            .item_locations
133            .len()
134            .try_into()
135            .map_err(|_| Error::TooLarge(Self::KIND))?;
136
137        let mut base_offset_size = 0u8;
138        let mut offset_size = 4u8;
139        let mut length_size = 4u8;
140        let mut index_size = 0u8;
141        let mut needs_version_1 = false;
142        let mut needs_version_2 = self.item_locations.len() > u16::MAX as usize;
143
144        for item_location in &self.item_locations {
145            if item_location.construction_method > 2 {
146                return Err(Error::InvalidSize);
147            }
148            if item_location.extents.len() > u16::MAX as usize {
149                return Err(Error::TooLarge(Self::KIND));
150            }
151
152            needs_version_1 |= item_location.construction_method != 0;
153            needs_version_2 |= item_location.item_id > u16::MAX as u32;
154
155            if item_location.base_offset > 0 {
156                if item_location.base_offset > u32::MAX as u64 {
157                    base_offset_size = 8u8;
158                } else if base_offset_size != 8 {
159                    base_offset_size = 4u8;
160                }
161            }
162
163            for extent in &item_location.extents {
164                if extent.item_reference_index > 0 {
165                    needs_version_1 = true;
166                    if extent.item_reference_index > u32::MAX as u64 {
167                        index_size = 8;
168                    } else if index_size != 8 {
169                        index_size = 4;
170                    }
171                }
172                if extent.offset > u32::MAX as u64 {
173                    offset_size = 8;
174                }
175                if extent.length > u32::MAX as u64 {
176                    length_size = 8;
177                }
178            }
179        }
180
181        let version = if needs_version_2 {
182            IlocVersion::V2
183        } else if needs_version_1 {
184            IlocVersion::V1
185        } else {
186            IlocVersion::V0
187        };
188
189        let size0 = (offset_size << 4) | length_size;
190        let size1 = (base_offset_size << 4) | index_size;
191        size0.encode(buf)?;
192        size1.encode(buf)?;
193        if version == IlocVersion::V0 || version == IlocVersion::V1 {
194            u16::try_from(item_count)
195                .map_err(|_| Error::TooLarge(Self::KIND))?
196                .encode(buf)?;
197        } else {
198            item_count.encode(buf)?;
199        }
200        for item_location in &self.item_locations {
201            if version == IlocVersion::V0 || version == IlocVersion::V1 {
202                u16::try_from(item_location.item_id)
203                    .map_err(|_| Error::TooLarge(Self::KIND))?
204                    .encode(buf)?;
205            } else {
206                item_location.item_id.encode(buf)?;
207            }
208            if version == IlocVersion::V1 || version == IlocVersion::V2 {
209                (item_location.construction_method as u16).encode(buf)?
210            }
211            item_location.data_reference_index.encode(buf)?;
212            match base_offset_size {
213                0 => {}
214                4 => u32::try_from(item_location.base_offset)
215                    .map_err(|_| Error::TooLarge(Self::KIND))?
216                    .encode(buf)?,
217                8 => item_location.base_offset.encode(buf)?,
218                _ => unreachable!("iloc base_offset_size must be in [0,4,8]"),
219            }
220            u16::try_from(item_location.extents.len())
221                .map_err(|_| Error::TooLarge(Self::KIND))?
222                .encode(buf)?;
223            for extent in &item_location.extents {
224                match index_size {
225                    0 => {}
226                    4 => u32::try_from(extent.item_reference_index)
227                        .map_err(|_| Error::TooLarge(Self::KIND))?
228                        .encode(buf)?,
229                    8 => extent.item_reference_index.encode(buf)?,
230                    _ => unreachable!("iloc index_size must be in [0,4,8]"),
231                }
232                match offset_size {
233                    0 => {}
234                    4 => u32::try_from(extent.offset)
235                        .map_err(|_| Error::TooLarge(Self::KIND))?
236                        .encode(buf)?,
237                    8 => extent.offset.encode(buf)?,
238                    _ => unreachable!("iloc offset_size must be in [0,4,8]"),
239                }
240                match length_size {
241                    0 => {}
242                    4 => u32::try_from(extent.length)
243                        .map_err(|_| Error::TooLarge(Self::KIND))?
244                        .encode(buf)?,
245                    8 => extent.length.encode(buf)?,
246                    _ => unreachable!("iloc length_size must be in [0,4,8]"),
247                }
248            }
249        }
250        Ok(IlocExt { version })
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    const ENCODED_ILOC_LIBAVIF: &[u8] = &[
259        0x00, 0x00, 0x00, 0x1e, 0x69, 0x6c, 0x6f, 0x63, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00,
260        0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x38, 0x00, 0x00, 0x00, 0x1a,
261    ];
262
263    #[test]
264    fn test_iloc_libavif_decode() {
265        let buf: &mut std::io::Cursor<&&[u8]> = &mut std::io::Cursor::new(&ENCODED_ILOC_LIBAVIF);
266
267        let iloc: Iloc = Iloc {
268            item_locations: vec![ItemLocation {
269                item_id: 1,
270                construction_method: 0,
271                data_reference_index: 0,
272                base_offset: 0,
273                extents: vec![ItemLocationExtent {
274                    item_reference_index: 0,
275                    offset: 312,
276                    length: 26,
277                }],
278            }],
279        };
280        let decoded = Iloc::decode(buf).unwrap();
281        assert_eq!(decoded, iloc);
282    }
283
284    // Regression for issue #158: out-of-range *_size nibbles (anything
285    // outside {0, 4, 8} per ISO/IEC 14496-12 ยง8.11.3.3) must return
286    // Err, not panic.
287
288    #[test]
289    fn test_iloc_reserved_base_offset_size() {
290        // version=0, flags=0, sizes0=0x00 (offset_size=0, length_size=0),
291        // sizes1=0xF0 (base_offset_size=15), item_count=1,
292        // item_id=0, data_reference_index=0, then base_offset_size match.
293        let body: &[u8] = &[0x00, 0xF0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
294        let ext = IlocExt {
295            version: IlocVersion::V0,
296        };
297        assert!(matches!(
298            Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext),
299            Err(Error::Reserved)
300        ));
301    }
302
303    #[test]
304    fn test_iloc_reserved_index_size() {
305        // V1: sizes1 low nibble is index_size. base_offset_size=0,
306        // index_size=15. Reach the index_size match by giving an extent.
307        let body: &[u8] = &[
308            0x00, 0x0F, // sizes0=0, sizes1=0x0F
309            0x00, 0x01, // item_count=1
310            0x00, 0x00, // item_id
311            0x00, 0x00, // construction_method
312            0x00, 0x00, // data_reference_index
313            // base_offset_size=0 -> no base_offset
314            0x00, 0x01, // extent_count=1
315        ];
316        let ext = IlocExt {
317            version: IlocVersion::V1,
318        };
319        assert!(matches!(
320            Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext),
321            Err(Error::Reserved)
322        ));
323    }
324
325    #[test]
326    fn test_iloc_reserved_offset_size() {
327        // V0: sizes0 high nibble is offset_size=0xF.
328        let body: &[u8] = &[
329            0xF0, 0x00, // sizes0=0xF0 (offset_size=15), sizes1=0
330            0x00, 0x01, // item_count=1
331            0x00, 0x00, // item_id
332            0x00, 0x00, // data_reference_index
333            0x00, 0x01, // extent_count=1
334        ];
335        let ext = IlocExt {
336            version: IlocVersion::V0,
337        };
338        assert!(matches!(
339            Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext),
340            Err(Error::Reserved)
341        ));
342    }
343
344    #[test]
345    fn test_iloc_reserved_length_size() {
346        // V0: sizes0 low nibble is length_size=0xF, offset_size=0
347        // so the offset_size arm passes through 0 and reaches length_size.
348        let body: &[u8] = &[
349            0x0F, 0x00, // sizes0=0x0F (length_size=15), sizes1=0
350            0x00, 0x01, // item_count=1
351            0x00, 0x00, // item_id
352            0x00, 0x00, // data_reference_index
353            0x00, 0x01, // extent_count=1
354        ];
355        let ext = IlocExt {
356            version: IlocVersion::V0,
357        };
358        assert!(matches!(
359            Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext),
360            Err(Error::Reserved)
361        ));
362    }
363
364    #[test]
365    fn test_iloc_rejects_repeated_zero_size_extents() {
366        // Found by moq's media fuzz targets: every size is 0, so each extent reads no
367        // bytes, and a 28-byte box decoded two items of 65,280 extents each (about 6 MB).
368        const ZERO_SIZE_EXTENTS: &[u8] = &[
369            0x00, 0x00, 0x00, 0x1c, 0x69, 0x6c, 0x6f, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
370            0x00, 0x02, 0xc8, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00,
371        ];
372
373        let err = Iloc::decode(&mut std::io::Cursor::new(ZERO_SIZE_EXTENTS)).unwrap_err();
374        assert!(
375            matches!(err, Error::OverDecode(kind) if kind == Iloc::KIND),
376            "{err:?}"
377        );
378    }
379
380    #[test]
381    fn test_iloc_v1_rejects_repeated_zero_size_extents() {
382        // V1 adds index_size, which is 0 here too.
383        let body: &[u8] = &[
384            0x00, 0x00, // sizes0=0, sizes1=0
385            0x00, 0x01, // item_count=1
386            0x00, 0x01, // item_id
387            0x00, 0x00, // construction_method
388            0x00, 0x00, // data_reference_index
389            0x00, 0x02, // extent_count=2
390        ];
391        let ext = IlocExt {
392            version: IlocVersion::V1,
393        };
394        assert!(matches!(
395            Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext),
396            Err(Error::OutOfBounds)
397        ));
398    }
399
400    #[test]
401    fn test_iloc_single_zero_size_extent_decodes() {
402        // One extent per item with every size 0 still decodes, even at the end of the box.
403        let body: &[u8] = &[
404            0x00, 0x00, // sizes0=0, sizes1=0
405            0x00, 0x02, // item_count=2
406            0x00, 0x01, // item_id
407            0x00, 0x00, // data_reference_index
408            0x00, 0x01, // extent_count=1
409            0x00, 0x02, // item_id
410            0x00, 0x00, // data_reference_index
411            0x00, 0x01, // extent_count=1
412        ];
413        let ext = IlocExt {
414            version: IlocVersion::V0,
415        };
416        let iloc = Iloc::decode_body_ext(&mut std::io::Cursor::new(body), ext).unwrap();
417        let extent = ItemLocationExtent {
418            item_reference_index: 0,
419            offset: 0,
420            length: 0,
421        };
422        assert_eq!(
423            iloc.item_locations
424                .iter()
425                .map(|item| (item.item_id, item.extents.clone()))
426                .collect::<Vec<_>>(),
427            vec![(1, vec![extent.clone()]), (2, vec![extent])]
428        );
429    }
430
431    #[test]
432    fn test_iloc_avif_encode() {
433        let iloc: Iloc = Iloc {
434            item_locations: vec![ItemLocation {
435                item_id: 1,
436                construction_method: 0,
437                data_reference_index: 0,
438                base_offset: 0,
439                extents: vec![ItemLocationExtent {
440                    item_reference_index: 0,
441                    offset: 312,
442                    length: 26,
443                }],
444            }],
445        };
446        let mut buf = Vec::new();
447        iloc.encode(&mut buf).unwrap();
448
449        assert_eq!(buf.as_slice(), ENCODED_ILOC_LIBAVIF);
450    }
451
452    #[test]
453    fn test_iloc_version_1_fields_round_trip() {
454        let iloc = Iloc {
455            item_locations: vec![ItemLocation {
456                item_id: 1,
457                construction_method: 1,
458                data_reference_index: 0,
459                base_offset: 0,
460                extents: vec![ItemLocationExtent {
461                    item_reference_index: u32::MAX as u64 + 1,
462                    offset: u32::MAX as u64 + 1,
463                    length: u32::MAX as u64 + 2,
464                }],
465            }],
466        };
467        let mut buf = Vec::new();
468        iloc.encode(&mut buf).unwrap();
469
470        assert_eq!(buf[8], 1); // version 1
471        assert_eq!(buf[12], 0x88); // 64-bit extent offset and length
472        assert_eq!(buf[13], 0x08); // no base offset, 64-bit extent index
473        assert_eq!(Iloc::decode(&mut buf.as_slice()).unwrap(), iloc);
474    }
475
476    #[test]
477    fn test_iloc_version_2_item_id_round_trip() {
478        let iloc = Iloc {
479            item_locations: vec![ItemLocation {
480                item_id: u16::MAX as u32 + 1,
481                construction_method: 0,
482                data_reference_index: 0,
483                base_offset: 0,
484                extents: vec![],
485            }],
486        };
487        let mut buf = Vec::new();
488        iloc.encode(&mut buf).unwrap();
489
490        assert_eq!(buf[8], 2);
491        assert_eq!(Iloc::decode(&mut buf.as_slice()).unwrap(), iloc);
492    }
493
494    #[test]
495    fn test_iloc_version_2_item_count_round_trip() {
496        let item_location = ItemLocation {
497            item_id: 0,
498            construction_method: 0,
499            data_reference_index: 0,
500            base_offset: 0,
501            extents: vec![],
502        };
503        let iloc = Iloc {
504            item_locations: vec![item_location; u16::MAX as usize + 1],
505        };
506        let mut buf = Vec::new();
507        iloc.encode(&mut buf).unwrap();
508
509        assert_eq!(buf[8], 2);
510        assert_eq!(Iloc::decode(&mut buf.as_slice()).unwrap(), iloc);
511    }
512
513    #[test]
514    fn test_iloc_rejects_unrepresentable_construction_method() {
515        let iloc = Iloc {
516            item_locations: vec![ItemLocation {
517                item_id: 0,
518                construction_method: 3,
519                data_reference_index: 0,
520                base_offset: 0,
521                extents: vec![],
522            }],
523        };
524
525        assert!(matches!(
526            iloc.encode(&mut Vec::new()),
527            Err(Error::InvalidSize)
528        ));
529    }
530
531    #[test]
532    fn test_iloc_rejects_too_many_extents() {
533        let extent = ItemLocationExtent {
534            item_reference_index: 0,
535            offset: 0,
536            length: 0,
537        };
538        let iloc = Iloc {
539            item_locations: vec![ItemLocation {
540                item_id: 0,
541                construction_method: 0,
542                data_reference_index: 0,
543                base_offset: 0,
544                extents: vec![extent; u16::MAX as usize + 1],
545            }],
546        };
547
548        assert!(matches!(
549            iloc.encode(&mut Vec::new()),
550            Err(Error::TooLarge(kind)) if kind == Iloc::KIND
551        ));
552    }
553}