// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //! Parser for the JPEG XL Frame Index box (`jxli`), as specified in //! the JPEG XL container specification. //! //! The frame index box provides a seek table for animated JXL files, //! listing keyframe byte offsets in the codestream, timestamps, and //! frame counts. use std::num::NonZero; use byteorder::{BigEndian, ReadBytesExt}; use crate::error::{Error, Result}; use crate::icc::read_varint_from_reader; use crate::util::NewWithCapacity; /// A single entry in the frame index. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FrameIndexEntry { /// Absolute byte offset of this keyframe in the codestream. /// (Accumulated from the delta-coded OFFi values.) pub codestream_offset: u64, /// Duration in ticks from this indexed frame to the next indexed frame /// (or end of stream for the last entry). A tick lasts TNUM/TDEN seconds. pub duration_ticks: u64, /// Number of displayed frames from this indexed frame to the next indexed /// frame (or end of stream for the last entry). pub frame_count: u64, } /// Parsed contents of a Frame Index box (`jxli`). #[derive(Debug, Clone, PartialEq, Eq)] pub struct FrameIndexBox { /// Tick numerator. A tick lasts `tnum / tden` seconds. pub tnum: u32, /// Tick denominator (non-zero per spec). pub tden: NonZero, /// Indexed frame entries. pub entries: Vec, } impl FrameIndexBox { /// Returns the number of indexed frames. pub fn num_frames(&self) -> usize { self.entries.len() } /// Returns the duration of one tick in seconds. pub fn tick_duration_secs(&self) -> f64 { self.tnum as f64 / self.tden.get() as f64 } /// Finds the index entry for the keyframe at or before the given /// codestream byte offset. pub fn entry_for_offset(&self, offset: u64) -> Option<&FrameIndexEntry> { // Entries are sorted by codestream_offset (monotonically increasing). match self .entries .binary_search_by_key(&offset, |e| e.codestream_offset) { Ok(i) => Some(&self.entries[i]), Err(0) => None, Err(i) => Some(&self.entries[i - 1]), } } /// Parse a frame index box from its raw content bytes (after the box header). pub fn parse(data: &[u8]) -> Result { let mut reader = data; let nf = read_varint_from_reader(&mut reader)?; if nf > u32::MAX as u64 { return Err(Error::InvalidBox); } let nf = nf as usize; let tnum = reader .read_u32::() .map_err(|_| Error::InvalidBox)?; let tden = NonZero::new( reader .read_u32::() .map_err(|_| Error::InvalidBox)?, ) .ok_or(Error::InvalidBox)?; // Each entry requires at least 3 bytes (three varints, min 1 byte each). // Cap the pre-allocation to avoid OOM from a crafted NF value. // Use new_with_capacity to return Err on allocation failure instead of aborting. let mut entries = Vec::new_with_capacity(nf.min(reader.len() / 3))?; let mut absolute_offset: u64 = 0; for _ in 0..nf { let off_delta = read_varint_from_reader(&mut reader)?; let duration_ticks = read_varint_from_reader(&mut reader)?; let frame_count = read_varint_from_reader(&mut reader)?; absolute_offset = absolute_offset .checked_add(off_delta) .ok_or(Error::InvalidBox)?; entries.push(FrameIndexEntry { codestream_offset: absolute_offset, duration_ticks, frame_count, }); } Ok(FrameIndexBox { tnum, tden, entries, }) } } #[cfg(test)] mod tests { use super::*; use crate::util::test::{build_frame_index_content, encode_varint}; fn build_frame_index(tnum: u32, tden: u32, entries: &[(u64, u64, u64)]) -> Vec { build_frame_index_content(tnum, tden, entries) } #[test] fn test_parse_empty_index() { let data = build_frame_index(1, 1000, &[]); let index = FrameIndexBox::parse(&data).unwrap(); assert_eq!(index.num_frames(), 0); assert_eq!(index.tnum, 1); assert_eq!(index.tden.get(), 1000); } #[test] fn test_parse_single_entry() { // One frame at offset 0, duration 100 ticks, 1 frame let data = build_frame_index(1, 1000, &[(0, 100, 1)]); let index = FrameIndexBox::parse(&data).unwrap(); assert_eq!(index.num_frames(), 1); assert_eq!( index.entries[0], FrameIndexEntry { codestream_offset: 0, duration_ticks: 100, frame_count: 1, } ); } #[test] fn test_parse_multiple_entries_delta_coding() { // Three frames with delta-coded offsets: // OFF0=100 (absolute: 100), T0=50, F0=2 // OFF1=200 (absolute: 300), T1=50, F1=2 // OFF2=150 (absolute: 450), T2=30, F2=1 let data = build_frame_index(1, 1000, &[(100, 50, 2), (200, 50, 2), (150, 30, 1)]); let index = FrameIndexBox::parse(&data).unwrap(); assert_eq!(index.num_frames(), 3); assert_eq!(index.entries[0].codestream_offset, 100); assert_eq!(index.entries[1].codestream_offset, 300); assert_eq!(index.entries[2].codestream_offset, 450); assert_eq!(index.entries[0].duration_ticks, 50); assert_eq!(index.entries[1].duration_ticks, 50); assert_eq!(index.entries[2].duration_ticks, 30); } #[test] fn test_parse_large_varint() { // Test with a value that requires multiple varint bytes let mut data = Vec::new(); data.extend(encode_varint(1)); // NF = 1 data.extend(1u32.to_be_bytes()); // TNUM data.extend(1000u32.to_be_bytes()); // TDEN data.extend(encode_varint(0x1234_5678_9ABC)); // large offset data.extend(encode_varint(42)); data.extend(encode_varint(1)); let index = FrameIndexBox::parse(&data).unwrap(); assert_eq!(index.entries[0].codestream_offset, 0x1234_5678_9ABC); } #[test] fn test_entry_for_offset() { let data = build_frame_index(1, 1000, &[(100, 50, 2), (200, 50, 2), (150, 30, 1)]); let index = FrameIndexBox::parse(&data).unwrap(); // Absolute offsets: 100, 300, 450 // Before first entry assert!(index.entry_for_offset(50).is_none()); // Exact match assert_eq!(index.entry_for_offset(100).unwrap().codestream_offset, 100); // Between entries assert_eq!(index.entry_for_offset(200).unwrap().codestream_offset, 100); assert_eq!(index.entry_for_offset(350).unwrap().codestream_offset, 300); // Exact match on last assert_eq!(index.entry_for_offset(450).unwrap().codestream_offset, 450); // Past last assert_eq!(index.entry_for_offset(999).unwrap().codestream_offset, 450); } #[test] fn test_zero_tden_rejected() { let data = build_frame_index(1, 0, &[]); assert!(FrameIndexBox::parse(&data).is_err()); } #[test] fn test_truncated_data() { // Just NF=1, no TNUM/TDEN let data = encode_varint(1); assert!(FrameIndexBox::parse(&data).is_err()); } #[test] fn test_huge_nf_no_oom() { // Crafted input: NF claims billions of entries but the data is tiny. // This must not OOM -- Vec::with_capacity should be bounded by data length. let mut data = Vec::new(); data.extend(encode_varint(u32::MAX as u64)); // NF = 4 billion data.extend(1u32.to_be_bytes()); // TNUM data.extend(1000u32.to_be_bytes()); // TDEN // No actual entry data -- parse should fail gracefully, not OOM. assert!(FrameIndexBox::parse(&data).is_err()); } #[test] fn test_tick_duration() { let data = build_frame_index(1, 1000, &[]); let index = FrameIndexBox::parse(&data).unwrap(); assert!((index.tick_duration_secs() - 0.001).abs() < 1e-9); } }