// Copyright 2022 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

use std::{
    cmp::min,
    collections::HashMap,
    fs::{File, OpenOptions},
    io::{Cursor, Read, Seek, SeekFrom, Write},
    path::Path,
};

use atree::{Arena, Token};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};

use crate::{
    assertions::{BmffMerkleMap, ExclusionsMap},
    asset_io::{
        rename_or_move, AssetIO, AssetPatch, CAIRead, CAIReadWrite, CAIReader, CAIWriter,
        ComposedManifestRef, HashObjectPositions, RemoteRefEmbed, RemoteRefEmbedType,
    },
    error::{Error, Result},
    status_tracker::{ErrorBehavior, StatusTracker},
    store::Store,
    utils::{
        hash_utils::{vec_compare, HashRange},
        io_utils::{patch_stream, stream_len, tempfile_builder, ReaderUtils},
        patch::patch_bytes,
        xmp_inmemory_utils::{add_provenance, MIN_XMP},
    },
};

pub struct BmffIO {
    #[allow(dead_code)]
    bmff_format: String, // can be used for specialized BMFF cases
}

const MAX_BOX_DEPTH: usize = 32; // reasonable BMFF box depth, to prevent stack overflow

const HEADER_SIZE: u64 = 8; // 4 byte type + 4 byte size
const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large size

const C2PA_UUID: [u8; 16] = [
    0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4, 0x81,
];
const XMP_UUID: [u8; 16] = [
    0xbe, 0x7a, 0xcf, 0xcb, 0x97, 0xa9, 0x42, 0xe8, 0x9c, 0x71, 0x99, 0x94, 0x91, 0xe3, 0xaf, 0xac,
];
pub(crate) const MANIFEST: &str = "manifest";
pub(crate) const MERKLE: &str = "merkle";
const ORIGINAL: &str = "original";
const UPDATE: &str = "update";

// ISO IEC 14496-12_2022 FullBoxes
const FULL_BOX_TYPES: &[&str; 80] = &[
    "pdin", "mvhd", "tkhd", "mdhd", "hdlr", "nmhd", "elng", "stsd", "stdp", "stts", "ctts", "cslg",
    "stss", "stsh", "stdp", "elst", "dref", "stsz", "stz2", "stsc", "stco", "co64", "padb", "subs",
    "saiz", "saio", "mehd", "trex", "mfhd", "tfhd", "trun", "tfra", "mfro", "tfdt", "leva", "trep",
    "assp", "sbgp", "sgpd", "csgp", "cprt", "tsel", "kind", "meta", "xml ", "bxml", "iloc", "pitm",
    "ipro", "infe", "iinf", "iref", "ipma", "schm", "fiin", "fpar", "fecr", "gitn", "fire", "stri",
    "stsg", "stvi", "csch", "sidx", "ssix", "prft", "srpp", "vmhd", "smhd", "srat", "chnl", "dmix",
    "txtC", "mime", "uri ", "uriI", "hmhd", "sthd", "vvhd", "medc",
];

static SUPPORTED_TYPES: [&str; 15] = [
    "avif",
    "heif",
    "heic",
    "mp4",
    "m4a",
    "mov",
    "m4v",
    "application/mp4",
    "audio/mp4",
    "image/avif",
    "image/heic",
    "image/heif",
    "video/mp4",
    "video/quicktime",
    "video/x-m4v",
];

macro_rules! boxtype {
    ($( $name:ident => $value:expr ),*) => {
        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
        pub enum BoxType {
            $( $name, )*
            UnknownBox(u32),
        }

        impl From<u32> for BoxType {
            fn from(t: u32) -> BoxType {
                match t {
                    $( $value => BoxType::$name, )*
                    _ => BoxType::UnknownBox(t),
                }
            }
        }

        impl From<BoxType> for u32 {
            fn from(t: BoxType) -> u32 {
                match t {
                    $( BoxType::$name => $value, )*
                    BoxType::UnknownBox(t) => t,
                }
            }
        }
    }
}

boxtype! {
    Empty => 0x0000_0000,
    UuidBox => 0x75756964,
    FtypBox => 0x66747970,
    MvhdBox => 0x6d766864,
    MfhdBox => 0x6d666864,
    FreeBox => 0x66726565,
    MdatBox => 0x6d646174,
    MoovBox => 0x6d6f6f76,
    MvexBox => 0x6d766578,
    MehdBox => 0x6d656864,
    TrexBox => 0x74726578,
    EmsgBox => 0x656d7367,
    MoofBox => 0x6d6f6f66,
    TkhdBox => 0x746b6864,
    TfhdBox => 0x74666864,
    EdtsBox => 0x65647473,
    MdiaBox => 0x6d646961,
    ElstBox => 0x656c7374,
    MfraBox => 0x6d667261,
    MdhdBox => 0x6d646864,
    HdlrBox => 0x68646c72,
    MinfBox => 0x6d696e66,
    VmhdBox => 0x766d6864,
    StblBox => 0x7374626c,
    StsdBox => 0x73747364,
    SttsBox => 0x73747473,
    CttsBox => 0x63747473,
    StssBox => 0x73747373,
    StscBox => 0x73747363,
    StszBox => 0x7374737A,
    StcoBox => 0x7374636F,
    Co64Box => 0x636F3634,
    TrakBox => 0x7472616b,
    TrafBox => 0x74726166,
    TrefBox => 0x74726566,
    TregBox => 0x74726567,
    TrunBox => 0x7472756E,
    UdtaBox => 0x75647461,
    DinfBox => 0x64696e66,
    DrefBox => 0x64726566,
    UrlBox  => 0x75726C20,
    SmhdBox => 0x736d6864,
    Avc1Box => 0x61766331,
    AvcCBox => 0x61766343,
    Hev1Box => 0x68657631,
    HvcCBox => 0x68766343,
    Mp4aBox => 0x6d703461,
    EsdsBox => 0x65736473,
    Tx3gBox => 0x74783367,
    VpccBox => 0x76706343,
    Vp09Box => 0x76703039,
    MetaBox => 0x6D657461,
    SchiBox => 0x73636869,
    IlocBox => 0x696C6F63,
    MfroBox => 0x6d66726f,
    TfraBox => 0x74667261,
    SaioBox => 0x7361696f
}

struct BoxHeaderLite {
    pub name: BoxType,
    pub size: u64,
    pub fourcc: String,
    pub large_size: bool,
}

impl BoxHeaderLite {
    pub fn new(name: BoxType, size: u64, fourcc: &str) -> Self {
        Self {
            name,
            size,
            fourcc: fourcc.to_string(),
            large_size: false,
        }
    }

    pub fn read<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<Self> {
        let box_start = reader.stream_position()?;

        // Create and read to buf.
        let mut buf = [0u8; 8]; // 8 bytes for box header.
        reader.read_exact(&mut buf)?;

        // Get size.
        let mut s = [0u8; 4];
        s.clone_from_slice(&buf[0..4]);
        let size = u32::from_be_bytes(s);

        // Get box type string.
        let mut t = [0u8; 4];
        t.clone_from_slice(&buf[4..8]);
        let fourcc = String::from_utf8_lossy(&buf[4..8]).to_string();
        let typ = u32::from_be_bytes(t);

        // Get largesize if size is 1
        if size == 1 {
            reader.read_exact(&mut buf)?;
            let largesize = u64::from_be_bytes(buf);

            Ok(BoxHeaderLite {
                name: BoxType::from(typ),
                size: largesize,
                fourcc,
                large_size: true,
            })
        } else if size == 0 {
            // special case to indicate the size goes to the end of the file
            let end_of_stream = stream_len(reader)?;
            let actual_size = end_of_stream - box_start;

            Ok(BoxHeaderLite {
                name: BoxType::from(typ),
                size: actual_size,
                fourcc,
                large_size: false,
            })
        } else {
            Ok(BoxHeaderLite {
                name: BoxType::from(typ),
                size: size as u64,
                fourcc,
                large_size: false,
            })
        }
    }

    pub fn write<W: Write>(&self, writer: &mut W) -> Result<u64> {
        if self.size > u32::MAX as u64 {
            writer.write_u32::<BigEndian>(1)?;
            writer.write_u32::<BigEndian>(self.name.into())?;
            writer.write_u64::<BigEndian>(self.size)?;
            Ok(16)
        } else {
            writer.write_u32::<BigEndian>(self.size as u32)?;
            writer.write_u32::<BigEndian>(self.name.into())?;
            Ok(8)
        }
    }
}

fn write_box_uuid_extension<W: Write>(w: &mut W, uuid: &[u8; 16]) -> Result<u64> {
    w.write_all(uuid)?;
    Ok(16)
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct BoxInfo {
    path: String,
    parent: Option<Token>,
    pub offset: u64,
    pub size: u64,
    box_type: BoxType,
    user_type: Option<Vec<u8>>,
    version: Option<u8>,
    flags: Option<u32>,
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct BoxInfoLite {
    pub path: String,
    pub offset: u64,
    pub size: u64,
}

impl BoxInfoLite {
    pub fn start(&self) -> u64 {
        self.offset
    }

    pub fn end(&self) -> u64 {
        self.offset + self.size
    }

    pub fn size(&self) -> u64 {
        self.size
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct FourCC {
    pub value: [u8; 4],
}

impl From<u32> for FourCC {
    fn from(number: u32) -> Self {
        FourCC {
            value: number.to_be_bytes(),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct FileTypeBox {
    pub major_brand: FourCC,
    pub minor_version: u32,
    pub compatible_brands: Vec<FourCC>,
}

fn read_ftyp_box<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<FileTypeBox> {
    let start = reader.stream_position()?;

    let header = BoxHeaderLite::read(reader)
        .map_err(|err| Error::InvalidAsset(format!("Bad BMFF {err}")))?;

    if header.name != BoxType::FtypBox {
        // when no ftyp is present ISOBMFF parsers typically treat the file as if it had an ftyp box with major_brand of 'mp41' and minor_version of 0, so we will do the same for our purposes
        return Ok(FileTypeBox {
            major_brand: From::from(0x6d703431), // 'mp41'
            minor_version: 0,
            compatible_brands: Vec::new(),
        });
    }

    let size = header.size;

    if size < 16 || size % 4 != 0 {
        return Err(Error::InvalidAsset(
            "ftyp size too small or not aligned".to_string(),
        ));
    }

    let brand_count = (size - 16) / 4; // header + major + minor
    let major = reader.read_u32::<BigEndian>()?;
    let minor = reader.read_u32::<BigEndian>()?;

    let mut brands = Vec::new();
    for _ in 0..brand_count {
        let b = reader.read_u32::<BigEndian>()?;
        brands.push(From::from(b));
    }

    skip_bytes_to(reader, start + size)?;

    Ok(FileTypeBox {
        major_brand: From::from(major),
        minor_version: minor,
        compatible_brands: brands,
    })
}

fn read_box_header_ext<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<(u8, u32)> {
    let version = reader.read_u8()?;
    let flags = reader.read_u24::<BigEndian>()?;
    Ok((version, flags))
}

/// Detect whether a `meta` box omits the FullBox version/flags header.
///
/// Per ISO 14496-12 §8.11.1, `meta` is a FullBox whose first child must
/// be `hdlr`. However, Apple QuickTime (and iOS AVAssetWriter even with
/// `isom` brand) writes `meta` as a plain Box without the 4-byte
/// version+flags field. We detect this by peeking at the next 8 bytes:
/// if bytes 4..8 are `hdlr`, the data is already a child box header so
/// no FullBox header is present. This matches the approach used by FFmpeg
/// and Bento4.
fn meta_box_lacks_fullbox_header<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<bool> {
    let pos = reader.stream_position()?;
    let mut buf = [0u8; 8];
    let ok = reader.read_exact(&mut buf).is_ok();
    reader.seek(SeekFrom::Start(pos))?;

    if !ok {
        return Ok(false);
    }

    Ok(&buf[4..8] == b"hdlr")
}

fn write_box_header_ext<W: Write>(w: &mut W, v: u8, f: u32) -> Result<u64> {
    w.write_u8(v)?;
    w.write_u24::<BigEndian>(f)?;
    Ok(4)
}

fn box_start<R: Read + Seek + ?Sized>(reader: &mut R, is_large: bool) -> Result<u64> {
    if is_large {
        Ok(reader.stream_position()? - HEADER_SIZE_LARGE)
    } else {
        Ok(reader.stream_position()? - HEADER_SIZE)
    }
}

fn _skip_bytes<R: Read + Seek + ?Sized>(reader: &mut R, size: u64) -> Result<()> {
    // `size as i64` on a value greater than i64::MAX wraps to a negative
    // number, which would seek *backward* rather than skip forward. Compute
    // the absolute target with checked_add on u64 and seek to it via
    // `SeekFrom::Start`, which takes a u64 directly.
    let current = reader.stream_position()?;
    let target = current
        .checked_add(size)
        .ok_or_else(|| Error::InvalidAsset("BMFF skip past u64::MAX".to_string()))?;
    reader.seek(SeekFrom::Start(target))?;
    Ok(())
}

fn skip_bytes_to<R: Read + Seek + ?Sized>(reader: &mut R, pos: u64) -> Result<u64> {
    let pos = reader.seek(SeekFrom::Start(pos))?;
    Ok(pos)
}

pub(crate) fn write_c2pa_box<W: Write>(
    w: &mut W,
    data: &[u8],
    purpose: &str,
    merkle_data: &[u8],
    merkle_offset: u64,
) -> Result<()> {
    let purpose_size = purpose.len() + 1;

    let box_size = if purpose == MERKLE {
        merkle_data.len()
    } else {
        8
    };
    let size = 8 + 16 + 4 + purpose_size + box_size + data.len(); // header + UUID + version/flags + data + zero terminated purpose + merkle data
    let bh = BoxHeaderLite::new(BoxType::UuidBox, size as u64, "uuid");

    // write out header
    bh.write(w)?;

    // write out c2pa extension UUID
    write_box_uuid_extension(w, &C2PA_UUID)?;

    // write out version and flags
    let version: u8 = 0;
    let flags: u32 = 0;
    write_box_header_ext(w, version, flags)?;

    // write with appropriate purpose
    w.write_all(purpose.as_bytes())?;
    w.write_u8(0)?;
    if purpose == MERKLE {
        // write merkle cbor
        w.write_all(merkle_data)?;
    } else {
        // write merkle offset
        w.write_u64::<BigEndian>(merkle_offset)?;
    }

    // write out data
    w.write_all(data)?;

    Ok(())
}

fn write_xmp_box<W: Write>(w: &mut W, data: &[u8]) -> Result<()> {
    let size = 8 + 16 + data.len(); // header + UUID + data
    let bh = BoxHeaderLite::new(BoxType::UuidBox, size as u64, "uuid");

    // write out header
    bh.write(w)?;

    // write out XMP extension UUID
    write_box_uuid_extension(w, &XMP_UUID)?;

    // write out data
    w.write_all(data)?;

    Ok(())
}

#[allow(unused_imports)]
fn write_free_box<W: Write>(w: &mut W, size: usize) -> Result<()> {
    if size < 8 {
        return Err(Error::BadParam("cannot adjust free space".to_string()));
    }

    let zeros = vec![0u8; size - 8];
    let bh = BoxHeaderLite::new(BoxType::FreeBox, size as u64, "free");

    // write out header
    bh.write(w)?;

    // write out header
    w.write_all(&zeros)?;

    Ok(())
}

fn add_token_to_cache(bmff_path_map: &mut HashMap<String, Vec<Token>>, path: String, token: Token) {
    if let Some(token_list) = bmff_path_map.get_mut(&path) {
        token_list.push(token);
    } else {
        let token_list = vec![token];
        bmff_path_map.insert(path, token_list);
    }
}

fn path_from_token(bmff_tree: &Arena<BoxInfo>, current_node_token: &Token) -> Result<String> {
    let ancestors = current_node_token.ancestors(bmff_tree);
    let mut path = bmff_tree[*current_node_token].data.path.clone();

    for parent in ancestors {
        path = format!("{}/{}", parent.data.path, path);
    }

    if path.is_empty() {
        path = "/".to_string();
    }

    Ok(path)
}

fn get_top_level_box_offsets(
    bmff_tree: &Arena<BoxInfo>,
    bmff_path_map: &HashMap<String, Vec<Token>>,
) -> Vec<u64> {
    let mut tl_offsets = Vec::new();

    for (p, t) in bmff_path_map {
        // look for top level offsets
        if p.matches('/').count() == 1 {
            for token in t {
                if let Some(box_info) = bmff_tree.get(*token) {
                    tl_offsets.push(box_info.data.offset);
                }
            }
        }
    }

    tl_offsets
}

fn get_top_level_boxes(
    bmff_tree: &Arena<BoxInfo>,
    bmff_path_map: &HashMap<String, Vec<Token>>,
) -> Vec<BoxInfoLite> {
    let mut tl_boxes = Vec::new();

    for (p, t) in bmff_path_map {
        // look for top level offsets
        if p.matches('/').count() == 1 {
            for token in t {
                if let Some(box_info) = bmff_tree.get(*token) {
                    tl_boxes.push(BoxInfoLite {
                        path: box_info.data.path.clone(),
                        offset: box_info.data.offset,
                        size: box_info.data.size,
                    });
                }
            }
        }
    }

    tl_boxes
}

pub fn bmff_to_jumbf_exclusions<R>(
    mut reader: &mut R,
    bmff_exclusions: &[ExclusionsMap],
    bmff_v2: bool,
) -> Result<Vec<HashRange>>
where
    R: Read + Seek + ?Sized,
{
    let size = stream_len(reader)?;
    reader.rewind()?;

    let ftyp = read_ftyp_box(reader)?;
    reader.rewind()?;

    // create root node
    let root_box = BoxInfo {
        path: "".to_string(),
        offset: 0,
        size,
        box_type: BoxType::Empty,
        parent: None,
        user_type: None,
        version: None,
        flags: None,
    };

    let (mut bmff_tree, root_token) = Arena::with_data(root_box);
    let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

    // build layout of the BMFF structure
    let mut rl = 0usize;
    build_bmff_tree(
        reader,
        size,
        &mut bmff_tree,
        &root_token,
        &mut bmff_map,
        &mut rl,
        &ftyp,
    )?;

    // get top level box offsets
    let mut tl_offsets = get_top_level_box_offsets(&bmff_tree, &bmff_map);
    tl_offsets.sort();

    let mut exclusions = Vec::new();

    for bmff_exclusion in bmff_exclusions {
        if let Some(box_token_list) = bmff_map.get(&bmff_exclusion.xpath) {
            for box_token in box_token_list {
                let box_info = &bmff_tree[*box_token].data;

                let box_start = box_info.offset;
                let box_length = box_info.size;

                let exclusion_start = box_start;
                let exclusion_length = box_length;

                // adjust exclusion bounds as needed

                // check the length
                if let Some(desired_length) = bmff_exclusion.length {
                    if desired_length != box_length {
                        continue;
                    }
                }

                // check the version
                if let Some(desired_version) = bmff_exclusion.version {
                    if let Some(box_version) = box_info.version {
                        if desired_version != box_version {
                            continue;
                        }
                    }
                }

                // check the flags
                if let Some(desired_flag_bytes) = &bmff_exclusion.flags {
                    let mut temp_bytes = [0u8; 4];
                    if desired_flag_bytes.len() >= 3 {
                        temp_bytes[0] = desired_flag_bytes[0];
                        temp_bytes[1] = desired_flag_bytes[1];
                        temp_bytes[2] = desired_flag_bytes[2];
                    }
                    let desired_flags = u32::from_be_bytes(temp_bytes);

                    if let Some(box_flags) = box_info.flags {
                        let exact = bmff_exclusion.exact.unwrap_or(true);

                        if exact {
                            if desired_flags != box_flags {
                                continue;
                            }
                        } else {
                            // bitwise match
                            if (desired_flags | box_flags) != desired_flags {
                                continue;
                            }
                        }
                    }
                }

                // check data match
                if let Some(data_map_vec) = &bmff_exclusion.data {
                    let mut should_add = true;

                    for data_map in data_map_vec {
                        // move to the start of exclusion
                        skip_bytes_to(
                            reader,
                            box_start
                                .checked_add(data_map.offset)
                                .ok_or(Error::InvalidAsset(
                                    "BMFF data map offset overflow".to_string(),
                                ))?,
                        )?;
                        // match the data
                        let buf = reader.read_to_vec(data_map.value.len() as u64)?;

                        // does not match so skip
                        if !vec_compare(&data_map.value, &buf) {
                            should_add = false;
                            break;
                        }
                    }
                    if !should_add {
                        continue;
                    }
                }

                // reduce range if desired
                if let Some(subset_vec) = &bmff_exclusion.subset {
                    for subset in subset_vec {
                        // if the subset offset is past the end of the box, skip
                        if subset.offset > exclusion_length {
                            continue;
                        }

                        let new_start = exclusion_start + subset.offset;
                        let new_length = if subset.length == 0 {
                            exclusion_length - subset.offset
                        } else {
                            min(subset.length, exclusion_length - subset.offset)
                        };

                        let exclusion = HashRange::new(new_start, new_length);

                        exclusions.push(exclusion);
                    }
                } else {
                    // exclude box in its entirty
                    let exclusion = HashRange::new(exclusion_start, exclusion_length);

                    exclusions.push(exclusion);

                    // for BMFF V2 hashes we do not add hash offsets for top level boxes
                    // that are completely excluded, so remove from BMFF V2 hash offset calc
                    if let Some(pos) = tl_offsets.iter().position(|x| *x == exclusion_start) {
                        tl_offsets.remove(pos);
                    }
                }
            }
        }
    }

    // add remaining top level offsets to be included when generating BMFF V2 hashes
    // note: this is technically not an exclusion but a replacement with a new range of bytes to be hashed
    if bmff_v2 {
        for tl_start in tl_offsets {
            let mut exclusion = HashRange::new(tl_start, 1u64);
            exclusion.set_bmff_offset(tl_start);

            exclusions.push(exclusion);
        }
    }

    Ok(exclusions)
}

// `iloc`, `stco`, `co64`, `mfro`, `saio`, `sidx`, `tdhd`, and `tfra` elements contain absolute file offsets so they need to be adjusted based on whether content was added or removed.
fn adjust_known_offsets<W: Write + CAIRead + ?Sized>(
    mut output: &mut W,
    bmff_tree: &Arena<BoxInfo>,
    bmff_path_map: &HashMap<String, Vec<Token>>,
    adjust: i64,
) -> Result<()> {
    let start_pos = output.stream_position()?; // save starting point

    // handle 32 bit offsets
    if let Some(stco_list) = bmff_path_map.get("/moov/trak/mdia/minf/stbl/stco") {
        for stco_token in stco_list {
            let stco_box_info = &bmff_tree[*stco_token].data;
            if stco_box_info.box_type != BoxType::StcoBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read stco box and patch
            output.seek(SeekFrom::Start(stco_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::StcoBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (_version, _flags) = read_box_header_ext(output)?; // box extensions

            // get count of offsets
            let entry_count = output.read_u32::<BigEndian>()?;

            // read and patch offsets
            let entry_start_pos = output.stream_position()?;
            let mut entries: Vec<u32> = Vec::new();
            for _e in 0..entry_count {
                let offset = output.read_u32::<BigEndian>()?;
                let new_offset = if adjust < 0 {
                    offset
                        - u32::try_from(adjust.abs()).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                } else {
                    offset
                        + u32::try_from(adjust).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                };
                entries.push(new_offset);
            }

            // write updated offsets
            output.seek(SeekFrom::Start(entry_start_pos))?;
            for e in entries {
                output.write_u32::<BigEndian>(e)?;
            }
        }
    }

    // handle 64 offsets
    if let Some(co64_list) = bmff_path_map.get("/moov/trak/mdia/minf/stbl/co64") {
        for co64_token in co64_list {
            let co64_box_info = &bmff_tree[*co64_token].data;
            if co64_box_info.box_type != BoxType::Co64Box {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read co64 box and patch
            output.seek(SeekFrom::Start(co64_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::Co64Box {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (_version, _flags) = read_box_header_ext(output)?; // box extensions

            // get count of offsets
            let entry_count = output.read_u32::<BigEndian>()?;

            // read and patch offsets
            let entry_start_pos = output.stream_position()?;
            let mut entries: Vec<u64> = Vec::new();
            for _e in 0..entry_count {
                let offset = output.read_u64::<BigEndian>()?;
                let new_offset = if adjust < 0 {
                    offset
                        - u64::try_from(adjust.abs()).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                } else {
                    offset
                        + u64::try_from(adjust).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                };
                entries.push(new_offset);
            }

            // write updated offsets
            output.seek(SeekFrom::Start(entry_start_pos))?;
            for e in entries {
                output.write_u64::<BigEndian>(e)?;
            }
        }
    }

    // handle meta iloc
    if let Some(iloc_list) = bmff_path_map.get("/meta/iloc") {
        for iloc_token in iloc_list {
            let iloc_box_info = &bmff_tree[*iloc_token].data;
            if iloc_box_info.box_type != BoxType::IlocBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read iloc box and patch
            output.seek(SeekFrom::Start(iloc_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::IlocBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (version, _flags) = read_box_header_ext(output)?; // box extensions

            // read next 16 bits (in file byte order)
            let mut iloc_header = [0u8, 2];
            output.read_exact(&mut iloc_header)?;

            // get offset size (high nibble)
            let offset_size: u8 = (iloc_header[0] & 0xf0) >> 4;

            // get length size (low nibble)
            let length_size: u8 = iloc_header[0] & 0x0f;

            // get box offset size (high nibble)
            let base_offset_size: u8 = (iloc_header[1] & 0xf0) >> 4;

            // get index size (low nibble)
            let index_size: u8 = iloc_header[1] & 0x0f;

            // get item count
            let item_count = match version {
                _v if version < 2 => output.read_u16::<BigEndian>()? as u32,
                _v if version == 2 => output.read_u32::<BigEndian>()?,
                _ => {
                    return Err(Error::InvalidAsset(
                        "Bad BMFF unknown iloc format".to_string(),
                    ))
                }
            };

            // walk the iloc items and patch
            for _i in 0..item_count {
                // read item id
                let _item_id = match version {
                    _v if version < 2 => output.read_u16::<BigEndian>()? as u32,
                    2 => output.read_u32::<BigEndian>()?,
                    _ => {
                        return Err(Error::InvalidAsset(
                            "Bad BMFF: unknown iloc item".to_string(),
                        ))
                    }
                };

                // read construction method
                let construction_method = if version == 1 || version == 2 {
                    let mut cm_bytes = [0u8, 2];
                    output.read_exact(&mut cm_bytes)?;

                    // lower nibble of 2nd byte
                    cm_bytes[1] & 0x0f
                } else {
                    0
                };

                // read data reference index
                let _data_reference_index = output.read_u16::<BigEndian>()?;

                let base_offset_file_pos = output.stream_position()?;
                let base_offset = match base_offset_size {
                    0 => 0_u64,
                    4 => output.read_u32::<BigEndian>()? as u64,
                    8 => output.read_u64::<BigEndian>()?,
                    _ => {
                        return Err(Error::InvalidAsset(
                            "Bad BMFF: unknown iloc offset size".to_string(),
                        ))
                    }
                };

                // patch the offsets if needed
                if construction_method == 0 {
                    // file offset construction method
                    if base_offset_size == 4 {
                        let new_offset = if adjust < 0 {
                            u32::try_from(base_offset).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })? - u32::try_from(adjust.abs()).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                        } else {
                            u32::try_from(base_offset).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })? + u32::try_from(adjust).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                        };

                        output.seek(SeekFrom::Start(base_offset_file_pos))?;
                        output.write_u32::<BigEndian>(new_offset)?;
                    }

                    if base_offset_size == 8 {
                        let new_offset = if adjust < 0 {
                            base_offset
                                - u64::try_from(adjust.abs()).map_err(|_| {
                                    Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                                })?
                        } else {
                            base_offset
                                + u64::try_from(adjust).map_err(|_| {
                                    Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                                })?
                        };

                        output.seek(SeekFrom::Start(base_offset_file_pos))?;
                        output.write_u64::<BigEndian>(new_offset)?;
                    }
                }

                // read extent count
                let extent_count = output.read_u16::<BigEndian>()?;

                // consume the extents
                for _e in 0..extent_count {
                    let _extent_index = if version == 1 || (version == 2 && index_size > 0) {
                        match base_offset_size {
                            4 => Some(output.read_u32::<BigEndian>()? as u64),
                            8 => Some(output.read_u64::<BigEndian>()?),
                            _ => None,
                        }
                    } else {
                        None
                    };

                    let extent_offset_file_pos = output.stream_position()?;
                    let extent_offset = match offset_size {
                        0 => 0_u64,
                        4 => output.read_u32::<BigEndian>()? as u64,
                        8 => output.read_u64::<BigEndian>()?,
                        _ => {
                            return Err(Error::InvalidAsset(
                                "Bad BMFF: unknown iloc extent_offset size".to_string(),
                            ))
                        }
                    };

                    // no base offset so just adjust the raw extent_offset value
                    if construction_method == 0 && base_offset == 0 && extent_offset != 0 {
                        output.seek(SeekFrom::Start(extent_offset_file_pos))?;
                        match offset_size {
                            4 => {
                                let new_offset = if adjust < 0 {
                                    extent_offset as u32
                                        - u32::try_from(adjust.abs()).map_err(|_| {
                                            Error::InvalidAsset(
                                                "Bad BMFF offset adjustment".to_string(),
                                            )
                                        })?
                                } else {
                                    extent_offset as u32
                                        + u32::try_from(adjust).map_err(|_| {
                                            Error::InvalidAsset(
                                                "Bad BMFF offset adjustment".to_string(),
                                            )
                                        })?
                                };
                                output.write_u32::<BigEndian>(new_offset)?;
                            }
                            8 => {
                                let new_offset = if adjust < 0 {
                                    extent_offset
                                        - u64::try_from(adjust.abs()).map_err(|_| {
                                            Error::InvalidAsset(
                                                "Bad BMFF offset adjustment".to_string(),
                                            )
                                        })?
                                } else {
                                    extent_offset
                                        + u64::try_from(adjust).map_err(|_| {
                                            Error::InvalidAsset(
                                                "Bad BMFF offset adjustment".to_string(),
                                            )
                                        })?
                                };
                                output.write_u64::<BigEndian>(new_offset)?;
                            }
                            _ => {
                                return Err(Error::InvalidAsset(
                                    "Bad BMFF: unknown extent_offset format".to_string(),
                                ))
                            }
                        }
                    }

                    let _extent_length = match length_size {
                        0 => 0_u64,
                        4 => output.read_u32::<BigEndian>()? as u64,
                        8 => output.read_u64::<BigEndian>()?,
                        _ => {
                            return Err(Error::InvalidAsset(
                                "Bad BMFF: unknown iloc offset size".to_string(),
                            ))
                        }
                    };
                }
            }
        }
    }

    // map to store track to moof mapping
    let mut track_id_to_moof_mapping = HashMap::new();

    // handle moof traf tfhd
    if let Some(tfhd_list) = bmff_path_map.get("/moof/traf/tfhd") {
        for tfhd_token in tfhd_list {
            let tfhd_box_info = &bmff_tree[*tfhd_token].data;
            if tfhd_box_info.box_type != BoxType::TfhdBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read box and patch
            output.seek(SeekFrom::Start(tfhd_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::TfhdBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (_version, tf_flags) = read_box_header_ext(output)?; // box extensions

            // track ID
            let track_id = output.read_u32::<BigEndian>()?;

            // get to outter moof box
            let ancestors = tfhd_token.ancestors(bmff_tree);
            for ancestor in ancestors {
                if ancestor.data.path == "moof" {
                    track_id_to_moof_mapping.insert(track_id, ancestor.data.offset);
                }
            }

            // fix up base offset and write out if flags indicate to do so
            if tf_flags & 1 == 1 {
                let base_data_offset_pos = output.stream_position()?;
                let mut base_data_offset = output.read_u64::<BigEndian>()?;

                base_data_offset = if adjust < 0 {
                    base_data_offset
                        - u64::try_from(adjust.abs()).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                } else {
                    base_data_offset
                        + u64::try_from(adjust).map_err(|_| {
                            Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                        })?
                };

                output.seek(SeekFrom::Start(base_data_offset_pos))?;
                output.write_u64::<BigEndian>(base_data_offset)?;
            }

            // ignore rest of fields
        }
    }

    // handle mfra tfra
    if let Some(tfra_list) = bmff_path_map.get("/mfra/tfra") {
        for tfra_token in tfra_list {
            let tfra_box_info = &bmff_tree[*tfra_token].data;
            if tfra_box_info.box_type != BoxType::TfraBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read iloc box and patch
            output.seek(SeekFrom::Start(tfra_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::TfraBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (version, _flags) = read_box_header_ext(output)?; // box extensions

            // track ID
            let track_id = output.read_u32::<BigEndian>()?;

            // tfr flags
            let tfra_info = output.read_u32::<BigEndian>()?;
            let length_size_of_traf_num = (tfra_info >> 4) & 0x03;
            let length_size_of_trun_num = (tfra_info >> 2) & 0x03;
            let length_size_of_sample_num = tfra_info & 0x03;

            // num entries
            let num_entries = output.read_u32::<BigEndian>()?;

            // get the moof boxes
            // fix up the offsets in the entry list
            for _entries in 0..num_entries {
                if version == 1 {
                    let _time = output.read_u64::<BigEndian>()?;

                    // write out mapped value of the moof position for this track
                    let moof_offset = track_id_to_moof_mapping
                        .get(&track_id)
                        .ok_or(Error::InvalidAsset("Bad BMFF".to_string()))?;
                    output.write_u64::<BigEndian>(*moof_offset)?;
                } else {
                    let _time = output.read_u32::<BigEndian>()?;

                    // write out mapped value of the moof position for this track
                    let moof_offset_u64 = track_id_to_moof_mapping
                        .get(&track_id)
                        .ok_or(Error::InvalidAsset("Bad BMFF".to_string()))?;

                    let moof_offset = u32::try_from(*moof_offset_u64).map_err(|_e| {
                        Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                    })?;
                    output.write_u32::<BigEndian>(moof_offset)?;
                }

                // read extra stuff to move the position
                let traf_num_bytes = length_size_of_traf_num + 1;
                output.read_to_vec(traf_num_bytes as u64)?;
                let trun_num_bytes = length_size_of_trun_num + 1;
                output.read_to_vec(trun_num_bytes as u64)?;
                let sample_num_bytes = length_size_of_sample_num + 1;
                output.read_to_vec(sample_num_bytes as u64)?;
            }
        }
    }

    // handle moov trak mdia minf stbl saio
    if let Some(saio_list) = bmff_path_map.get("/moov/trak/mdia/minf/stbl/saio") {
        for saio_token in saio_list {
            let saio_box_info = &bmff_tree[*saio_token].data;
            if saio_box_info.box_type != BoxType::SaioBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read saio box and patch
            output.seek(SeekFrom::Start(saio_box_info.offset))?;

            // read header
            let header = BoxHeaderLite::read(output)
                .map_err(|_err| Error::InvalidAsset("Bad BMFF".to_string()))?;
            if header.name != BoxType::SaioBox {
                return Err(Error::InvalidAsset("Bad BMFF".to_string()));
            }

            // read extended header
            let (version, flags) = read_box_header_ext(output)?; // box extensions
            if (flags & 1) == 1 {
                let _aux_info_type = output.read_u32::<BigEndian>()?;
                let _aux_info_type_parameter = output.read_u32::<BigEndian>()?;
            }

            // get count of offsets
            let entry_count = output.read_u32::<BigEndian>()?;

            // read and patch offsets
            let entry_start_pos = output.stream_position()?;
            let mut entries: Vec<u64> = Vec::new();
            for _e in 0..entry_count {
                if version == 0 {
                    let offset = output.read_u32::<BigEndian>()?;
                    let new_offset = if adjust < 0 {
                        offset
                            - u32::try_from(adjust.abs()).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                    } else {
                        offset
                            + u32::try_from(adjust).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                    };
                    entries.push(new_offset as u64);
                } else {
                    let offset = output.read_u64::<BigEndian>()?;
                    let new_offset = if adjust < 0 {
                        offset
                            - u64::try_from(adjust.abs()).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                    } else {
                        offset
                            + u64::try_from(adjust).map_err(|_| {
                                Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                            })?
                    };
                    entries.push(new_offset);
                }
            }

            // write updated offsets
            output.seek(SeekFrom::Start(entry_start_pos))?;
            for e in entries {
                if version == 0 {
                    let e32 = u32::try_from(e).map_err(|_| {
                        Error::InvalidAsset("Bad BMFF offset adjustment".to_string())
                    })?;
                    output.write_u32::<BigEndian>(e32)?;
                } else {
                    output.write_u64::<BigEndian>(e)?;
                }
            }
        }
    }

    // restore seek point
    output.seek(SeekFrom::Start(start_pos))?;
    output.flush()?;

    Ok(())
}

#[allow(clippy::only_used_in_recursion)]
pub(crate) fn build_bmff_tree<R: Read + Seek + ?Sized>(
    reader: &mut R,
    end: u64,
    bmff_tree: &mut Arena<BoxInfo>,
    current_node: &Token,
    bmff_path_map: &mut HashMap<String, Vec<Token>>,
    recursion_level: &mut usize,
    ftyp: &FileTypeBox,
) -> Result<()> {
    *recursion_level += 1;
    if *recursion_level > MAX_BOX_DEPTH {
        return Err(Error::InvalidAsset(
            "Boxes are too deply nested, unsupported asset".to_string(),
        ));
    }

    let start = reader.stream_position()?;
    let mut current = start;
    while current < end {
        // Get box header.
        let header = match BoxHeaderLite::read(reader) {
            Ok(h) => h,
            Err(_) => {
                // if we can't read a header, just return what we have so far since some files have trailing data after the last box
                skip_bytes_to(reader, end)?;
                break;
            }
        };

        // Break if size zero BoxHeader
        let mut s = header.size;
        if s == 0 {
            break;
        }

        let box_end = current
            .checked_add(s)
            .ok_or_else(|| Error::InvalidAsset("BMFF box size overflow".to_string()))?;
        if box_end > end {
            if BoxType::MdatBox == header.name {
                // for mdat boxes that extend beyond the end of the file we will just set the size to the remaining bytes in the file since
                // some files have malformed mdat sizes but we can still hash the content by treating it as a truncated box
                s = end - current;
            } else {
                return Err(Error::InvalidAsset(
                    "Box size extends beyond asset bounds".to_string(),
                ));
            }
        }

        // Match and parse the supported atom boxes.
        match header.name {
            BoxType::UuidBox => {
                let start = box_start(reader, header.large_size)?;

                let mut extended_type = [0u8; 16]; // 16 bytes of UUID
                reader.read_exact(&mut extended_type)?;

                // if this is a C2PA ContentProvenanceBox it is a FullBox so it has version and flags
                let (version, flags) = if extended_type == C2PA_UUID {
                    let (v, f) = read_box_header_ext(reader)?;
                    (Some(v), Some(f))
                } else {
                    (None, None)
                };

                let b = BoxInfo {
                    path: header.fourcc.clone(),
                    offset: start,
                    size: s,
                    box_type: BoxType::UuidBox,
                    parent: Some(*current_node),
                    user_type: Some(extended_type.to_vec()),
                    version,
                    flags,
                };

                let new_token = current_node.append(bmff_tree, b);

                let path = path_from_token(bmff_tree, &new_token)?;
                add_token_to_cache(bmff_path_map, path, new_token);

                // position seek pointer
                skip_bytes_to(reader, start + s)?;
            }
            // container box types
            BoxType::MoovBox
            | BoxType::TrakBox
            | BoxType::MdiaBox
            | BoxType::MinfBox
            | BoxType::StblBox
            | BoxType::MoofBox
            | BoxType::TrafBox
            | BoxType::EdtsBox
            | BoxType::UdtaBox
            | BoxType::DinfBox
            | BoxType::TrefBox
            | BoxType::TregBox
            | BoxType::MvexBox
            | BoxType::MfraBox
            | BoxType::MetaBox
            | BoxType::SchiBox => {
                let start = box_start(reader, header.large_size)?;

                let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) {
                    // FullBox has version and flags after the header, but for some boxes like QT "meta"
                    // the version and flags are not present even though it is technically a full box,
                    // so we need to conditionally read the extended header based on the box type and in
                    // the case of "meta" we peek at the data to detect the QuickTime exception.
                    let (version, flags) = if BoxType::MetaBox == header.name
                        && meta_box_lacks_fullbox_header(reader)?
                    {
                        (None, None)
                    } else {
                        let (v, f) = read_box_header_ext(reader)?;
                        (Some(v), Some(f))
                    };

                    BoxInfo {
                        path: header.fourcc.clone(),
                        offset: start,
                        size: s,
                        box_type: header.name,
                        parent: Some(*current_node),
                        user_type: None,
                        version,
                        flags,
                    }
                } else {
                    BoxInfo {
                        path: header.fourcc.clone(),
                        offset: start,
                        size: s,
                        box_type: header.name,
                        parent: Some(*current_node),
                        user_type: None,
                        version: None,
                        flags: None,
                    }
                };

                let new_token = bmff_tree.new_node(b);
                current_node
                    .append_node(bmff_tree, new_token)
                    .map_err(|_err| Error::InvalidAsset("Bad BMFF Graph".to_string()))?;

                let path = path_from_token(bmff_tree, &new_token)?;
                add_token_to_cache(bmff_path_map, path, new_token);

                // consume all sub-boxes
                let mut current = reader.stream_position()?;
                let end = start + s;
                while current < end {
                    build_bmff_tree(
                        reader,
                        end,
                        bmff_tree,
                        &new_token,
                        bmff_path_map,
                        recursion_level,
                        ftyp,
                    )?;
                    current = reader.stream_position()?;
                }

                // position seek pointer
                skip_bytes_to(reader, start + s)?;
            }
            _ => {
                let start = box_start(reader, header.large_size)?;

                let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) {
                    let (version, flags) = read_box_header_ext(reader)?; // box extensions
                    BoxInfo {
                        path: header.fourcc.clone(),
                        offset: start,
                        size: s,
                        box_type: header.name,
                        parent: Some(*current_node),
                        user_type: None,
                        version: Some(version),
                        flags: Some(flags),
                    }
                } else {
                    BoxInfo {
                        path: header.fourcc.clone(),
                        offset: start,
                        size: s,
                        box_type: header.name,
                        parent: Some(*current_node),
                        user_type: None,
                        version: None,
                        flags: None,
                    }
                };

                let new_token = current_node.append(bmff_tree, b);

                let path = path_from_token(bmff_tree, &new_token)?;
                add_token_to_cache(bmff_path_map, path, new_token);

                // position seek pointer
                skip_bytes_to(reader, start + s)?;
            }
        }
        current = reader.stream_position()?;
    }

    *recursion_level -= 1;

    Ok(())
}

fn get_uuid_box_purpose<R: Read + Seek + ?Sized>(
    reader: &mut R,
    box_info: &atree::Node<BoxInfo>,
) -> Result<(String, u64)> {
    if box_info.data.box_type == BoxType::UuidBox {
        let mut data_len = box_info
            .data
            .size
            .checked_sub(HEADER_SIZE)
            .and_then(|n| n.checked_sub(16))
            .ok_or_else(|| Error::InvalidAsset("UUID box too small".to_string()))?;

        // set reader to start of box contents
        skip_bytes_to(reader, box_info.data.offset + HEADER_SIZE + 16)?;

        // Fullbox => 8 bits for version 24 bits for flags
        let (_version, _flags) = read_box_header_ext(reader)?;
        data_len = data_len.checked_sub(4).ok_or_else(|| {
            Error::InvalidAsset("UUID box too small for FullBox header".to_string())
        })?;

        // get the purpose (null-terminated string bounded by the box)
        let mut purpose_bytes = Vec::with_capacity(64);
        loop {
            // Guard prevents reading past the box boundary, which would cause
            // data_len to underflow (wrapping to u64::MAX) and the caller to
            // attempt a u64::MAX-byte allocation.
            if data_len == 0 {
                return Err(Error::InvalidAsset(
                    "UUID box purpose field missing null terminator".to_string(),
                ));
            }
            let mut buf = [0; 1];
            reader.read_exact(&mut buf)?;
            data_len -= 1;
            if buf[0] == 0x00 {
                break;
            }
            purpose_bytes.push(buf[0]);
        }

        let purpose = String::from_utf8_lossy(&purpose_bytes);

        return Ok((purpose.to_string(), data_len));
    }

    Err(Error::C2PAValidation(
        "C2PA UUID box does not contain a purpose".to_string(),
    ))
}

fn get_uuid_token(
    reader: &mut dyn CAIRead,
    bmff_tree: &Arena<BoxInfo>,
    bmff_map: &HashMap<String, Vec<Token>>,
    uuid: &[u8; 16],
    purpose: Option<&[&str]>,
) -> Result<Token> {
    if let Some(uuid_list) = bmff_map.get("/uuid") {
        for uuid_token in uuid_list {
            let box_info = &bmff_tree[*uuid_token];

            // make sure it is UUID box
            if box_info.data.box_type == BoxType::UuidBox {
                if let Some(found_uuid) = &box_info.data.user_type {
                    // make sure uuids match
                    if vec_compare(uuid, found_uuid) {
                        // if C2PA_UUID also check against purpose if present
                        if vec_compare(&C2PA_UUID, uuid) {
                            let (box_purpose, _) = get_uuid_box_purpose(reader, box_info)?;

                            // if there is a purpose, match it
                            if let Some(target_purposes) = purpose {
                                for target_purpose in target_purposes {
                                    if box_purpose == *target_purpose {
                                        return Ok(*uuid_token);
                                    }
                                }
                                continue;
                            }
                        }
                        return Ok(*uuid_token);
                    }
                }
            }
        }
    }
    Err(Error::NotFound)
}

#[allow(dead_code)]
pub(crate) struct C2PABmffBoxes {
    pub manifest_bytes: Option<Vec<u8>>,
    pub original_bytes: Option<Vec<u8>>,
    pub update_bytes: Option<Vec<u8>>,
    pub manifest_box_bytes: Option<Vec<u8>>,
    pub update_box_bytes: Option<Vec<u8>>,
    pub bmff_merkle: Vec<BmffMerkleMap>,
    pub bmff_merkle_box_infos: Vec<BoxInfoLite>,
    pub box_infos: Vec<BoxInfoLite>,
    pub xmp: Option<String>,
    pub manifest_box_offset: Option<u64>,
    pub update_box_offset: Option<u64>,
    pub first_aux_uuid_offset: u64,
    pub xmp_box_offset: u64,
    pub xmp_box_size: u64,
}

fn c2pa_boxes_from_tree_and_map<R: Read + Seek + ?Sized>(
    mut reader: &mut R,
    bmff_tree: &Arena<BoxInfo>,
    bmff_map: &HashMap<String, Vec<Token>>,
) -> Result<C2PABmffBoxes> {
    let mut manifest_bytes: Option<Vec<u8>> = None;
    let mut original_bytes: Option<Vec<u8>> = None;
    let mut update_bytes: Option<Vec<u8>> = None;
    let mut manifest_box_bytes: Option<Vec<u8>> = None;
    let mut update_box_bytes: Option<Vec<u8>> = None;
    let mut xmp: Option<String> = None;
    let mut manifest_box_offset = None;
    let mut update_box_offset = None;
    let mut first_aux_uuid_offset = 0u64;
    let mut merkle_boxes: Vec<BmffMerkleMap> = Vec::new();
    let mut merkle_box_infos: Vec<BoxInfoLite> = Vec::new();
    let mut xmp_box_offset = 0;
    let mut xmp_box_size = 0;

    // grab top level (for now) C2PA box
    if let Some(uuid_list) = bmff_map.get("/uuid") {
        let mut manifest_store_cnt = 0;
        let mut update_store_cnt = 0;

        for uuid_token in uuid_list {
            let box_info = &bmff_tree[*uuid_token];

            // make sure it is UUID box
            if box_info.data.box_type == BoxType::UuidBox {
                if let Some(uuid) = &box_info.data.user_type {
                    // make sure it is a C2PA ContentProvenanceBox box
                    if vec_compare(&C2PA_UUID, uuid) {
                        let (purpose, mut data_len) = get_uuid_box_purpose(reader, box_info)?;

                        // is the purpose manifest?
                        if purpose == MANIFEST || purpose == ORIGINAL || purpose == UPDATE {
                            // offset to first aux uuid with purpose merkle
                            let mut buf = [0u8; 8];
                            reader.read_exact(&mut buf)?;
                            data_len = data_len.checked_sub(8).ok_or_else(|| {
                                Error::InvalidAsset(
                                    "UUID box too small for merkle offset field".to_string(),
                                )
                            })?;

                            // read the manifest box contents
                            let manifest = reader.read_to_vec(data_len)?;

                            // read the entire manifest box
                            skip_bytes_to(reader, box_info.data.offset)?;
                            let box_bytes = Some(reader.read_to_vec(box_info.data.size)?);

                            if purpose == MANIFEST {
                                manifest_bytes = Some(manifest);
                                manifest_box_offset = Some(box_info.data.offset);
                                manifest_box_bytes = box_bytes;
                                manifest_store_cnt += 1;
                                // offset to first aux uuid
                                first_aux_uuid_offset = u64::from_be_bytes(buf);
                            } else if purpose == ORIGINAL {
                                original_bytes = Some(manifest);
                                manifest_box_offset = Some(box_info.data.offset);
                                manifest_box_bytes = box_bytes;
                                manifest_store_cnt += 1;
                                // offset to first aux uuid
                                first_aux_uuid_offset = u64::from_be_bytes(buf);
                            } else if purpose == UPDATE {
                                update_bytes = Some(manifest);
                                update_box_offset = Some(box_info.data.offset);
                                update_box_bytes = box_bytes;
                                update_store_cnt += 1;
                            }

                            if manifest_store_cnt > 1 || update_store_cnt > 1 {
                                return Err(Error::TooManyManifestStores);
                            }
                        } else if purpose == MERKLE {
                            let merkle = reader.read_to_vec(data_len)?;

                            // use this method since it will strip trailing zeros padding if there
                            let mut deserializer = c2pa_cbor::de::Deserializer::from_slice(&merkle);
                            let mm: BmffMerkleMap =
                                serde::Deserialize::deserialize(&mut deserializer)?;
                            merkle_boxes.push(mm);
                            merkle_box_infos.push(BoxInfoLite {
                                path: box_info.data.path.clone(),
                                offset: box_info.data.offset,
                                size: box_info.data.size,
                            });
                        }
                    } else if vec_compare(&XMP_UUID, uuid) {
                        let data_len = box_info
                            .data
                            .size
                            .checked_sub(HEADER_SIZE)
                            .and_then(|n| n.checked_sub(16))
                            .ok_or_else(|| {
                                Error::InvalidAsset("XMP UUID box too small".to_string())
                            })?;

                        // set reader to start of box contents
                        skip_bytes_to(reader, box_info.data.offset + HEADER_SIZE + 16)?;

                        let xmp_vec = reader.read_to_vec(data_len)?;
                        if let Ok(xmp_string) = String::from_utf8(xmp_vec) {
                            xmp = Some(xmp_string);
                            xmp_box_offset = box_info.data.offset;
                            xmp_box_size = box_info.data.size;
                        }
                    }
                }
            }
        }
    }

    // get position ordered list of boxes
    let mut box_infos: Vec<BoxInfoLite> = get_top_level_boxes(bmff_tree, bmff_map);
    box_infos.sort_by_key(|a| a.offset);

    Ok(C2PABmffBoxes {
        manifest_bytes,
        original_bytes,
        update_bytes,
        manifest_box_bytes,
        update_box_bytes,
        bmff_merkle: merkle_boxes,
        bmff_merkle_box_infos: merkle_box_infos,
        box_infos,
        xmp,
        manifest_box_offset,
        update_box_offset,
        first_aux_uuid_offset,
        xmp_box_offset,
        xmp_box_size,
    })
}

pub(crate) fn read_bmff_c2pa_boxes<R: Read + Seek + ?Sized>(
    reader: &mut R,
) -> Result<C2PABmffBoxes> {
    let size = stream_len(reader)?;
    reader.rewind()?;

    let ftyp = read_ftyp_box(reader)?;
    reader.rewind()?;

    // create root node
    let root_box = BoxInfo {
        path: "".to_string(),
        offset: 0,
        size,
        box_type: BoxType::Empty,
        parent: None,
        user_type: None,
        version: None,
        flags: None,
    };

    let (mut bmff_tree, root_token) = Arena::with_data(root_box);
    let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

    // build layout of the BMFF structure
    let mut rl = 0usize;
    build_bmff_tree(
        reader,
        size,
        &mut bmff_tree,
        &root_token,
        &mut bmff_map,
        &mut rl,
        &ftyp,
    )?;
    c2pa_boxes_from_tree_and_map(reader, &bmff_tree, &bmff_map)
}

impl CAIReader for BmffIO {
    fn read_cai(&self, reader: &mut dyn CAIRead) -> Result<Vec<u8>> {
        reader.seek(SeekFrom::Start(4))?;

        let mut header = [0u8; 4];
        reader.read_exact(&mut header)?;

        if header[..4] != *b"ftyp" {
            return Err(BmffError::InvalidFileSignature {
                reason: format!(
                    "invalid BMFF structure: expected box type \"ftyp\" at offset 4, found {}",
                    String::from_utf8_lossy(&header[..4])
                ),
            }
            .into());
        }

        let c2pa_boxes = read_bmff_c2pa_boxes(reader)?;

        // is this an update manifest?
        if let Some(original_bytes) = c2pa_boxes.original_bytes {
            if let Some(update_bytes) = c2pa_boxes.update_bytes {
                let mut validation_log = StatusTracker::default();

                // combine original Store and update Store to single logical manifest Store
                let mut original_store = Store::from_jumbf(&original_bytes, &mut validation_log)?;
                let update_store = Store::from_jumbf(&update_bytes, &mut validation_log)?;

                original_store.append_store(&update_store);

                return original_store.to_jumbf_internal(0);
            } else {
                return Err(Error::C2PAValidation(
                    "original manifest without update manifest".to_string(),
                ));
            }
        }

        c2pa_boxes.manifest_bytes.ok_or(Error::JumbfNotFound)
    }

    // Get XMP block
    fn read_xmp(&self, reader: &mut dyn CAIRead) -> Option<String> {
        let c2pa_boxes = read_bmff_c2pa_boxes(reader).ok()?;

        c2pa_boxes.xmp
    }
}

impl AssetIO for BmffIO {
    fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> {
        Some(self)
    }

    fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
        let mut f = File::open(asset_path)?;
        self.read_cai(&mut f)
    }

    fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
        let mut input_stream = std::fs::OpenOptions::new()
            .read(true)
            .open(asset_path)
            .map_err(Error::IoError)?;

        let mut temp_file = tempfile_builder("c2pa_temp")?;

        self.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;

        // copy temp file to asset
        rename_or_move(temp_file, asset_path)
    }

    fn get_object_locations(
        &self,
        _asset_path: &std::path::Path,
    ) -> Result<Vec<HashObjectPositions>> {
        let vec: Vec<HashObjectPositions> = Vec::new();
        Ok(vec)
    }

    fn remove_cai_store(&self, asset_path: &Path) -> Result<()> {
        let mut input_file = std::fs::File::open(asset_path)?;

        let mut temp_file = tempfile_builder("c2pa_temp")?;

        self.remove_cai_store_from_stream(&mut input_file, &mut temp_file)?;

        // copy temp file to asset
        rename_or_move(temp_file, asset_path)
    }

    fn new(asset_type: &str) -> Self
    where
        Self: Sized,
    {
        BmffIO {
            bmff_format: asset_type.to_string(),
        }
    }

    fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO> {
        Box::new(BmffIO::new(asset_type))
    }

    fn get_reader(&self) -> &dyn CAIReader {
        self
    }

    fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
        Some(Box::new(BmffIO::new(asset_type)))
    }

    fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> {
        Some(self)
    }

    fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> {
        Some(self)
    }

    fn supported_types(&self) -> &[&str] {
        &SUPPORTED_TYPES
    }
}

impl CAIWriter for BmffIO {
    fn write_cai(
        &self,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
        store_bytes: &[u8],
    ) -> Result<()> {
        let size = stream_len(input_stream)?;
        input_stream.rewind()?;

        let ftyp = read_ftyp_box(input_stream)?;
        input_stream.rewind()?;

        // create root node
        let root_box = BoxInfo {
            path: "".to_string(),
            offset: 0,
            size,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        };

        let (mut bmff_tree, root_token) = Arena::with_data(root_box);
        let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

        // build layout of the BMFF structure
        let mut rl = 0usize;
        build_bmff_tree(
            input_stream,
            size,
            &mut bmff_tree,
            &root_token,
            &mut bmff_map,
            &mut rl,
            &ftyp,
        )?;

        // figure out what state we are in
        let c2pa_boxes = c2pa_boxes_from_tree_and_map(input_stream, &bmff_tree, &bmff_map)?;
        let has_manifest = c2pa_boxes.manifest_bytes.is_some();
        let has_original = c2pa_boxes.original_bytes.is_some();
        let has_update = c2pa_boxes.update_bytes.is_some();
        // if the incoming Store has an update manifest we must split it into original and update stores
        let mut validation_log =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let (pc, is_update) = if let Ok(store) = Store::from_jumbf(store_bytes, &mut validation_log)
        {
            let pc = store
                .provenance_claim()
                .ok_or(Error::BadParam("no provenance claim".to_string()))?;
            let is_update = pc.update_manifest();
            (Some(pc.clone()), is_update)
        } else {
            (None, false)
        };

        // "original" manifest store and "update" manifest store can only appear together
        if has_original && !has_update || !has_original && has_update {
            return Err(Error::BadParam(
                "BMFF save failure, found original manifest store without update manifest store"
                    .to_string(),
            ));
        }

        // if is an ordinary manifest store then it should not have an update manifest store
        if has_manifest && has_update {
            return Err(Error::BadParam(
                "BMFF save failure, found manifest store with update manifest store".to_string(),
            ));
        }

        // if we already have an "original" manifest store and an "update" manifest store
        // then we can just apppend to the update store
        if has_original && has_update && is_update {
            let update_manifest_bytes = &c2pa_boxes
                .update_bytes
                .ok_or(Error::BadParam("no update manifest".to_string()))?;
            let update_box_offset = c2pa_boxes
                .update_box_offset
                .ok_or(Error::BadParam("no update manifest".to_string()))?;
            let update_box_size = c2pa_boxes
                .update_box_bytes
                .ok_or(Error::BadParam("no update manifest".to_string()))?
                .len();
            let pc = pc.ok_or(Error::BadParam("no provenance manifest".to_string()))?;

            let mut update_store = Store::from_jumbf(update_manifest_bytes, &mut validation_log)?;
            // add new update manfiest or replace existing one if the is a finalization pass
            update_store.replace_claim_or_insert(pc.label().to_string(), pc);

            let new_update_bytes = update_store.to_jumbf_internal(0)?;
            let mut new_update_box = Vec::new();
            write_c2pa_box(&mut new_update_box, &new_update_bytes, UPDATE, &[], 0)?;

            patch_stream(
                input_stream,
                output_stream,
                update_box_offset,
                update_box_size as u64,
                &new_update_box,
            )?;

            return Ok(());
        }

        // if we have an ordinary manifest store and we are adding a new update manifest
        // then we need to split off incoming provenance claim into and add to update new update manifest
        if has_manifest && !has_update && is_update {
            let pc = pc.ok_or(Error::BadParam("no provenance manifest".to_string()))?;

            let mut update_store = Store::new();
            update_store.insert_restored_claim(pc.label().to_string(), pc);
            let new_update_bytes = update_store.to_jumbf_internal(0)?;

            // patch the purpose of the original manifest store
            let mut manifest_box_bytes = c2pa_boxes
                .manifest_box_bytes
                .ok_or(Error::BadParam("no original manifest".to_string()))?
                .clone();
            let manifest_box_offset = c2pa_boxes
                .manifest_box_offset
                .ok_or(Error::BadParam("no original manifest offset".to_string()))?;

            // update the manifest purpose
            patch_bytes(
                &mut manifest_box_bytes,
                MANIFEST.as_bytes(),
                ORIGINAL.as_bytes(),
            )?;

            // write the stream with manifest bytes containing updated manifest PURPOSE
            patch_stream(
                input_stream,
                output_stream,
                manifest_box_offset,
                manifest_box_bytes.len() as u64,
                &manifest_box_bytes,
            )?;

            // append new update manifest store to end of stream
            let mut update_manifest = Vec::new();
            write_c2pa_box(&mut update_manifest, &new_update_bytes, UPDATE, &[], 0)?;
            output_stream.seek(SeekFrom::End(0))?;
            output_stream.write_all(&update_manifest)?;

            return Ok(());
        }

        // since we reached this point we must have an ordinary manifest store so we may need to truncate off
        // the update manifest
        // get ftyp location
        // start after ftyp
        let ftyp_token = bmff_map.get("/ftyp").ok_or(Error::UnsupportedType)?; // todo check ftyps to make sure we support any special format requirements
        let ftyp_info = &bmff_tree[ftyp_token[0]].data;
        let ftyp_offset = ftyp_info.offset;
        let ftyp_size = ftyp_info.size;

        // get position to insert c2pa primary manifest store
        let (c2pa_start, c2pa_length) = match get_uuid_token(
            input_stream,
            &bmff_tree,
            &bmff_map,
            &C2PA_UUID,
            Some(&[MANIFEST, ORIGINAL]),
        ) {
            Ok(c2pa_token) => {
                let uuid_info = &bmff_tree[c2pa_token].data;

                (uuid_info.offset, Some(uuid_info.size))
            }
            Err(Error::NotFound) => ((ftyp_offset + ftyp_size), None),
            Err(e) => return Err(e),
        };

        let mut new_c2pa_box: Vec<u8> = Vec::with_capacity(store_bytes.len() * 2);
        let merkle_data: &[u8] = &[]; // not yet supported
        write_c2pa_box(&mut new_c2pa_box, store_bytes, MANIFEST, merkle_data, 0)?;
        let new_c2pa_box_size = new_c2pa_box.len();

        let (start, end) = if let Some(c2pa_length) = c2pa_length {
            let start = usize::try_from(c2pa_start)
                .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label

            let end = usize::try_from(c2pa_start + c2pa_length)
                .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;

            (start, end)
        } else {
            // insert new c2pa
            let end = usize::try_from(c2pa_start)
                .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;

            (end, end)
        };

        // write content before ContentProvenanceBox
        input_stream.rewind()?;
        let mut before_manifest = input_stream.take(start as u64);
        std::io::copy(&mut before_manifest, output_stream)?;

        // write ContentProvenanceBox
        output_stream.write_all(&new_c2pa_box)?;

        // calc offset adjustments. Use i64 so a box larger than i32::MAX
        // (2 GiB) does not silently truncate and corrupt embedded offsets.
        let new_c2pa_box_size_i64 = i64::try_from(new_c2pa_box_size)
            .map_err(|_| Error::InvalidAsset("C2PA box too large".to_string()))?;
        let offset_adjust: i64 = if end == 0 {
            new_c2pa_box_size_i64
        } else {
            // value could be negative if box is truncated
            let existing_c2pa_box_size = end - start;
            let existing_i64 = i64::try_from(existing_c2pa_box_size)
                .map_err(|_| Error::InvalidAsset("existing C2PA box too large".to_string()))?;
            new_c2pa_box_size_i64 - existing_i64
        };

        // write content after ContentProvenanceBox
        // since we reached this point we must have an ordinary manifest store so we may need to truncate off
        // the update manifest
        input_stream.seek(SeekFrom::Start(end as u64))?;
        if has_update {
            let update_offset = c2pa_boxes
                .update_box_offset
                .ok_or(Error::BadParam("no update manifest".to_string()))?;
            let len_to_update = update_offset - end as u64;
            let mut truncating_reader = input_stream.take(len_to_update);
            std::io::copy(&mut truncating_reader, output_stream)?;
        } else {
            std::io::copy(input_stream, output_stream)?;
        }

        // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets.
        if offset_adjust != 0 {
            // create root node
            let root_box = BoxInfo {
                path: "".to_string(),
                offset: 0,
                size,
                box_type: BoxType::Empty,
                parent: None,
                user_type: None,
                version: None,
                flags: None,
            };

            // map box layout of current output file
            let (mut output_bmff_tree, root_token) = Arena::with_data(root_box);
            let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

            let size = stream_len(output_stream)?;
            output_stream.rewind()?;
            let mut rl = 0usize;
            build_bmff_tree(
                output_stream,
                size,
                &mut output_bmff_tree,
                &root_token,
                &mut output_bmff_map,
                &mut rl,
                &ftyp,
            )?;

            // adjust offsets based on current layout
            output_stream.rewind()?;
            adjust_known_offsets(
                output_stream,
                &output_bmff_tree,
                &output_bmff_map,
                offset_adjust,
            )?;
        }

        Ok(())
    }

    fn get_object_locations_from_stream(
        &self,
        _input_stream: &mut dyn CAIRead,
    ) -> Result<Vec<HashObjectPositions>> {
        let vec: Vec<HashObjectPositions> = Vec::new();
        Ok(vec)
    }

    fn remove_cai_store_from_stream(
        &self,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
    ) -> Result<()> {
        let size = stream_len(input_stream)?;
        input_stream.rewind()?;

        let ftyp = read_ftyp_box(input_stream)?;
        input_stream.rewind()?;

        // create root node
        let root_box = BoxInfo {
            path: "".to_string(),
            offset: 0,
            size,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        };

        let (mut bmff_tree, root_token) = Arena::with_data(root_box);
        let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

        // build layout of the BMFF structure
        let mut rl = 0usize;
        build_bmff_tree(
            input_stream,
            size,
            &mut bmff_tree,
            &root_token,
            &mut bmff_map,
            &mut rl,
            &ftyp,
        )?;

        // get position of c2pa manifest
        let (c2pa_start, c2pa_length) =
            match get_uuid_token(input_stream, &bmff_tree, &bmff_map, &C2PA_UUID, None) {
                Ok(c2pa_token) => {
                    let uuid_info = &bmff_tree[c2pa_token].data;

                    (uuid_info.offset, Some(uuid_info.size))
                }
                Err(Error::NotFound) => {
                    input_stream.rewind()?;
                    std::io::copy(input_stream, output_stream)?;
                    return Ok(()); // no box to remove, propagate source to output
                }
                Err(e) => return Err(e),
            };

        let (start, end) = if let Some(c2pa_length) = c2pa_length {
            let start = usize::try_from(c2pa_start)
                .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label

            let end = usize::try_from(c2pa_start + c2pa_length)
                .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;

            (start, end)
        } else {
            return Err(Error::InvalidAsset("value out of range".to_string()));
        };

        // write content before ContentProvenanceBox
        input_stream.rewind()?;
        let mut before_manifest = input_stream.take(start as u64);
        std::io::copy(&mut before_manifest, output_stream)?;

        // calc offset adjustments — value will be negative since the box is
        // being truncated. Use i64 so boxes larger than i32::MAX (2 GiB) do
        // not truncate and shift `stco`/`co64`/`iloc` offsets to garbage.
        let existing_c2pa_box_size = end - start;
        let offset_adjust: i64 = -i64::try_from(existing_c2pa_box_size)
            .map_err(|_| Error::InvalidAsset("existing C2PA box too large".to_string()))?;

        // write content after ContentProvenanceBox
        input_stream.seek(SeekFrom::Start(end as u64))?;
        std::io::copy(input_stream, output_stream)?;

        // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets.

        // create root node
        let root_box = BoxInfo {
            path: "".to_string(),
            offset: 0,
            size,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        };

        // map box layout of current output file
        let (mut output_bmff_tree, root_token) = Arena::with_data(root_box);
        let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

        let size = stream_len(output_stream)?;
        output_stream.rewind()?;
        let mut rl = 0usize;
        build_bmff_tree(
            output_stream,
            size,
            &mut output_bmff_tree,
            &root_token,
            &mut output_bmff_map,
            &mut rl,
            &ftyp,
        )?;

        // adjust offsets based on current layout
        output_stream.rewind()?;
        adjust_known_offsets(
            output_stream,
            &output_bmff_tree,
            &output_bmff_map,
            offset_adjust,
        )
    }
}

impl AssetPatch for BmffIO {
    fn patch_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> {
        let mut asset = OpenOptions::new()
            .write(true)
            .read(true)
            .create(false)
            .open(asset_path)?;
        let size = stream_len(&mut asset)?;
        asset.rewind()?;

        let ftyp = read_ftyp_box(&mut asset)?;
        asset.rewind()?;

        // create root node
        let root_box = BoxInfo {
            path: "".to_string(),
            offset: 0,
            size,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        };

        let (mut bmff_tree, root_token) = Arena::with_data(root_box);
        let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

        // build layout of the BMFF structure
        let mut rl = 0usize;
        build_bmff_tree(
            &mut asset,
            size,
            &mut bmff_tree,
            &root_token,
            &mut bmff_map,
            &mut rl,
            &ftyp,
        )?;

        // get position to insert c2pa
        let (c2pa_start, c2pa_length) = if let Some(uuid_tokens) = bmff_map.get("/uuid") {
            let uuid_info = &bmff_tree[uuid_tokens[0]].data;

            // is this a C2PA manifest
            let is_c2pa = if let Some(uuid) = &uuid_info.user_type {
                // make sure it is a C2PA box
                vec_compare(&C2PA_UUID, uuid)
            } else {
                false
            };

            if is_c2pa {
                (uuid_info.offset, Some(uuid_info.size))
            } else {
                (0, None)
            }
        } else {
            return Err(Error::InvalidAsset(
                "patch_cai_store found no manifest store to patch.".to_string(),
            ));
        };

        if let Some(manifest_length) = c2pa_length {
            let mut new_c2pa_box: Vec<u8> = Vec::with_capacity(store_bytes.len() * 2);
            let merkle_data: &[u8] = &[]; // not yet supported
            write_c2pa_box(&mut new_c2pa_box, store_bytes, MANIFEST, merkle_data, 0)?;
            let new_c2pa_box_size = new_c2pa_box.len();

            if new_c2pa_box_size as u64 == manifest_length {
                asset.seek(SeekFrom::Start(c2pa_start))?;
                asset.write_all(&new_c2pa_box)?;
                Ok(())
            } else {
                Err(Error::InvalidAsset(
                    "patch_cai_store store size mismatch.".to_string(),
                ))
            }
        } else {
            Err(Error::InvalidAsset(
                "patch_cai_store store size mismatch.".to_string(),
            ))
        }
    }
}

impl ComposedManifestRef for BmffIO {
    fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>> {
        let mut new_c2pa_box: Vec<u8> = Vec::with_capacity(manifest_data.len() * 2);
        write_c2pa_box(&mut new_c2pa_box, manifest_data, MANIFEST, &[], 0)?;
        Ok(new_c2pa_box)
    }
}

impl RemoteRefEmbed for BmffIO {
    #[allow(unused_variables)]
    fn embed_reference(
        &self,
        asset_path: &Path,
        embed_ref: crate::asset_io::RemoteRefEmbedType,
    ) -> Result<()> {
        match embed_ref {
            crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => {
                let output_buf = Vec::new();
                let mut output_stream = Cursor::new(output_buf);

                // block so that source file is closed after embed
                {
                    let mut source_stream = std::fs::File::open(asset_path)?;
                    self.embed_reference_to_stream(
                        &mut source_stream,
                        &mut output_stream,
                        RemoteRefEmbedType::Xmp(manifest_uri),
                    )?;
                }

                // write will replace exisiting contents
                std::fs::write(asset_path, output_stream.into_inner())?;
                Ok(())
            }
            crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType),
            crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType),
            crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType),
        }
    }

    fn embed_reference_to_stream(
        &self,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
        embed_ref: RemoteRefEmbedType,
    ) -> Result<()> {
        match embed_ref {
            crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => {
                let size = stream_len(input_stream)?;
                input_stream.rewind()?;

                let ftyp = read_ftyp_box(input_stream)?;
                input_stream.rewind()?;

                // create root node
                let root_box = BoxInfo {
                    path: "".to_string(),
                    offset: 0,
                    size,
                    box_type: BoxType::Empty,
                    parent: None,
                    user_type: None,
                    version: None,
                    flags: None,
                };

                let (mut bmff_tree, root_token) = Arena::with_data(root_box);
                let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

                // build layout of the BMFF structure
                let mut rl = 0usize;
                build_bmff_tree(
                    input_stream,
                    size,
                    &mut bmff_tree,
                    &root_token,
                    &mut bmff_map,
                    &mut rl,
                    &ftyp,
                )?;

                let c2pa_boxes = c2pa_boxes_from_tree_and_map(input_stream, &bmff_tree, &bmff_map)?;

                let xmp = match &c2pa_boxes.xmp {
                    Some(xmp) => add_provenance(xmp, &manifest_uri)?,
                    None => {
                        let xmp = MIN_XMP.to_string();
                        add_provenance(&xmp, &manifest_uri)?
                    }
                };

                // get position to insert xmp
                let (xmp_start, xmp_length) = match &c2pa_boxes.xmp {
                    Some(_xmp) => (c2pa_boxes.xmp_box_offset, Some(c2pa_boxes.xmp_box_size)),
                    None => {
                        // get ftyp location
                        // start after ftyp
                        let ftyp_token = bmff_map.get("/ftyp").ok_or(Error::UnsupportedType)?; // todo check ftyps to make sure we support any special format requirements
                        let ftyp_info = &bmff_tree[ftyp_token[0]].data;
                        let ftyp_offset = ftyp_info.offset;
                        let ftyp_size = ftyp_info.size;

                        ((ftyp_offset + ftyp_size), None)
                    }
                };

                let mut new_xmp_box: Vec<u8> = Vec::with_capacity(xmp.len() * 2);
                write_xmp_box(&mut new_xmp_box, xmp.as_bytes())?;
                let new_xmp_box_size = new_xmp_box.len();

                let (start, end) = if let Some(xmp_length) = xmp_length {
                    let start = usize::try_from(xmp_start)
                        .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; // get beginning of chunk which starts 4 bytes before label

                    let end = usize::try_from(xmp_start + xmp_length)
                        .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;

                    (start, end)
                } else {
                    // insert new c2pa
                    let end = usize::try_from(xmp_start)
                        .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?;

                    (end, end)
                };

                // write content before XMP box
                input_stream.rewind()?;
                let mut before_xmp = input_stream.take(start as u64);
                std::io::copy(&mut before_xmp, output_stream)?;

                // write ContentProvenanceBox
                output_stream.write_all(&new_xmp_box)?;

                // calc offset adjustments. Use i64 so XMP boxes larger than
                // i32::MAX (2 GiB) do not silently truncate.
                let new_xmp_box_size_i64 = i64::try_from(new_xmp_box_size)
                    .map_err(|_| Error::InvalidAsset("XMP box too large".to_string()))?;
                let offset_adjust: i64 = if end == 0 {
                    new_xmp_box_size_i64
                } else {
                    // value could be negative if box is truncated
                    let existing_xmp_box_size = end - start;
                    let existing_i64 = i64::try_from(existing_xmp_box_size).map_err(|_| {
                        Error::InvalidAsset("existing XMP box too large".to_string())
                    })?;
                    new_xmp_box_size_i64 - existing_i64
                };

                // write content after XMP box
                input_stream.seek(SeekFrom::Start(end as u64))?;
                std::io::copy(input_stream, output_stream)?;

                // Manipulating the UUID box means we may need some patch offsets if they are file absolute offsets.

                // create root node
                let root_box = BoxInfo {
                    path: "".to_string(),
                    offset: 0,
                    size,
                    box_type: BoxType::Empty,
                    parent: None,
                    user_type: None,
                    version: None,
                    flags: None,
                };

                // map box layout of current output file
                let (mut output_bmff_tree, root_token) = Arena::with_data(root_box);
                let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

                let size = stream_len(output_stream)?;
                output_stream.rewind()?;
                let mut rl = 0usize;
                build_bmff_tree(
                    output_stream,
                    size,
                    &mut output_bmff_tree,
                    &root_token,
                    &mut output_bmff_map,
                    &mut rl,
                    &ftyp,
                )?;

                // adjust offsets based on current layout
                output_stream.rewind()?;
                adjust_known_offsets(
                    output_stream,
                    &output_bmff_tree,
                    &output_bmff_map,
                    offset_adjust,
                )
            }
            crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType),
            crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType),
            crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType),
        }
    }
}

// inject a placeholder free box of free_size at the end of the ftyp box. This is used to reserve
// space for a manifest box when one does not already exist in the file.
// Returns the location of the injected placeholder box.  This function assumes the file does not have
// an existing manifest store and that the placeholder box will be replaced with the manifest store during the first update pass.
#[allow(dead_code)]
pub(crate) fn inject_placeholder(
    input_stream: &mut dyn CAIRead,
    output_stream: &mut dyn CAIReadWrite,
    free_size: usize,
) -> Result<u64> {
    let size = stream_len(input_stream)?;
    input_stream.rewind()?;

    let ftyp = read_ftyp_box(input_stream)?;
    input_stream.rewind()?;

    // create root node
    let root_box = BoxInfo {
        path: "".to_string(),
        offset: 0,
        size,
        box_type: BoxType::Empty,
        parent: None,
        user_type: None,
        version: None,
        flags: None,
    };

    let (mut bmff_tree, root_token) = Arena::with_data(root_box);
    let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

    // build layout of the BMFF structure
    let mut rl = 0usize;
    build_bmff_tree(
        input_stream,
        size,
        &mut bmff_tree,
        &root_token,
        &mut bmff_map,
        &mut rl,
        &ftyp,
    )?;

    // figure out what state we are in
    let c2pa_boxes = c2pa_boxes_from_tree_and_map(input_stream, &bmff_tree, &bmff_map)?;
    let has_manifest = c2pa_boxes.manifest_bytes.is_some();
    let has_original = c2pa_boxes.original_bytes.is_some();
    let has_update = c2pa_boxes.update_bytes.is_some();

    if has_manifest || has_original || has_update {
        return Err(Error::InvalidAsset(
            "inject_placeholder should only be called on files without existing manifest stores"
                .to_string(),
        ));
    }

    // since we reached this point we must have an ordinary manifest store so we may need to truncate off
    // the update manifest
    // get ftyp location
    // start after ftyp
    let ftyp_token = bmff_map.get("/ftyp").ok_or(Error::UnsupportedType)?; // todo check ftyps to make sure we support any special format requirements
    let ftyp_info = &bmff_tree[ftyp_token[0]].data;
    let ftyp_offset = ftyp_info.offset;
    let ftyp_size = ftyp_info.size;

    // create free box bytes
    let mut free_box_bytes = Vec::with_capacity(free_size + 8);
    write_free_box(&mut free_box_bytes, free_size)?;

    // insertion point
    let start = ftyp_offset + ftyp_size;

    // write content before free box
    input_stream.rewind()?;
    let mut before_free = input_stream.take(start);
    std::io::copy(&mut before_free, output_stream)?;

    // write free box
    output_stream.write_all(&free_box_bytes)?;

    // write content after free box
    std::io::copy(input_stream, output_stream)?;

    // calc offset adjustments — use i64 so a placeholder larger than
    // i32::MAX (2 GiB) does not truncate.
    let offset_adjust: i64 = i64::try_from(free_box_bytes.len())
        .map_err(|_| Error::InvalidAsset("placeholder box too large".to_string()))?;

    // Manipulating the free box means we may need some patch offsets if they are file absolute offsets.
    if offset_adjust != 0 {
        // create root node
        let root_box = BoxInfo {
            path: "".to_string(),
            offset: 0,
            size,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        };

        // map box layout of current output file
        let (mut output_bmff_tree, root_token) = Arena::with_data(root_box);
        let mut output_bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

        let size = stream_len(output_stream)?;
        output_stream.rewind()?;
        let mut rl = 0usize;
        build_bmff_tree(
            output_stream,
            size,
            &mut output_bmff_tree,
            &root_token,
            &mut output_bmff_map,
            &mut rl,
            &ftyp,
        )?;

        // adjust offsets based on current layout
        output_stream.rewind()?;
        adjust_known_offsets(
            output_stream,
            &output_bmff_tree,
            &output_bmff_map,
            offset_adjust,
        )?;
    }

    Ok(start)
}

// write manifest into free box location.  Used inconjunction with inject_placeholder to first
// inject a free box to reserve space for the manifest and then write the manifest into the
// free box during the first update pass. This function assumes the manifest box will be the
// same size or smaller than the placeholder free box. If the manifest box is smaller than the
// placeholder free box then the remaining free space will be converted to a smaller free box.
// If the manifest box is larger than the placeholder free box then an error will be returned.
// manifest_bytes should be the bytes of the manifest box including the header. free_box_start is
// the file offset of the beginning of the free box to be replaced by the manifest box.
#[allow(dead_code)]
pub(crate) fn inject_manifest_into_free_box(
    stream: &mut dyn CAIReadWrite,
    manifest_bytes: &[u8],
    free_box_start: u64,
) -> Result<()> {
    let size = stream_len(stream)?;
    stream.rewind()?;

    let ftyp = read_ftyp_box(stream)?;
    stream.rewind()?;

    // create root node
    let root_box = BoxInfo {
        path: "".to_string(),
        offset: 0,
        size,
        box_type: BoxType::Empty,
        parent: None,
        user_type: None,
        version: None,
        flags: None,
    };

    let (mut bmff_tree, root_token) = Arena::with_data(root_box);
    let mut bmff_map: HashMap<String, Vec<Token>> = HashMap::new();

    // build layout of the BMFF structure
    let mut rl = 0usize;
    build_bmff_tree(
        stream,
        size,
        &mut bmff_tree,
        &root_token,
        &mut bmff_map,
        &mut rl,
        &ftyp,
    )?;

    // get the matching free box
    let free_tokens = bmff_map.get("/free").ok_or(Error::BadParam(
        "Did not find free box to inject manifest".to_string(),
    ))?;

    // find the free box that starts at the expected location
    let free_token = free_tokens
        .iter()
        .find(|token| {
            let free_info = &bmff_tree[**token].data;
            free_info.offset == free_box_start
        })
        .ok_or(Error::BadParam(
            "Did not find free box to inject manifest at expected location".to_string(),
        ))?;

    let free_info = &bmff_tree[*free_token].data;

    if manifest_bytes.len() as u64 > free_info.size {
        return Err(Error::BadParam(
            "Manifest size is larger than free box".to_string(),
        ));
    }

    // write manifest into free box location
    stream.seek(SeekFrom::Start(free_info.offset))?;
    stream.write_all(manifest_bytes)?;

    // convert remaining free space to a smaller free box if needed
    let remaining_free_space = free_info.size - manifest_bytes.len() as u64;
    if remaining_free_space > 8 {
        // need at least 8 bytes to write another free box
        let mut new_free_box = Vec::with_capacity(remaining_free_space as usize);
        write_free_box(&mut new_free_box, remaining_free_space as usize)?;
        stream.write_all(&new_free_box)?;
    } else {
        Err(Error::BadParam(
            "Not enough space to create new free box".to_string(),
        ))?;
    }
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum BmffError {
    #[error("invalid file signature: {reason}")]
    InvalidFileSignature { reason: String },
}

// ---------------------------------------------------------------------------
// Native BMFF sample reader.
//
// This is a small, hardened reimplementation of the narrow slice of the `mp4`
// crate that `BmffHash` verification relies on for the timed-media, track-based
// Merkle path: enumerate a file's tracks and read the bytes of an individual
// sample. It parses only the sample-table boxes needed to map a sample id to a
// byte range (`stsc`, `stco`/`co64`, `stsz`) plus `tkhd` for the track id, and
// every offset/size computation is checked so a crafted asset yields an
// `InvalidAsset` error rather than a panic. It intentionally does not touch the
// `emsg`, `stts`, `ctts`, or `stsd` boxes, which is where the `mp4` crate's
// panics lived.
// ---------------------------------------------------------------------------

/// One `stsc` run, with the 1-based `first_sample` derived at parse time (it is
/// not stored on disk).
#[derive(Clone, Copy, Debug)]
pub(crate) struct StscRun {
    pub first_chunk: u32,
    pub samples_per_chunk: u32,
    pub first_sample: u32,
}

/// The parsed sample tables for a single track, sufficient to locate and read
/// any sample's bytes.
#[derive(Debug)]
pub(crate) struct SampleTrack {
    track_id: u32,
    stsc: Vec<StscRun>,
    chunk_offsets: Vec<u64>,
    // `stsz.sample_size`: when non-zero every sample has this fixed size and
    // `sample_sizes` is empty; when zero, `sample_sizes` holds one entry per
    // sample.
    fixed_sample_size: u32,
    sample_sizes: Vec<u32>,
    sample_count: u32,
}

impl SampleTrack {
    pub(crate) fn track_id(&self) -> u32 {
        self.track_id
    }

    pub(crate) fn sample_count(&self) -> u32 {
        self.sample_count
    }

    pub(crate) fn stsc_runs(&self) -> &[StscRun] {
        &self.stsc
    }

    /// Returns the index of the `stsc` run governing `sample_id` (the last run
    /// whose `first_sample <= sample_id`). Mirrors the historical behavior of
    /// the `mp4` crate's `stsc_index`.
    pub(crate) fn stsc_index(&self, sample_id: u32) -> Result<usize> {
        if self.stsc.is_empty() {
            return Err(Error::InvalidAsset("BMFF has no stsc entries".to_string()));
        }

        // The runs are ordered by `first_sample`, so the governing run is the
        // last one whose `first_sample <= sample_id`. Tracking the running match
        // (rather than indexing `i - 1`/`len - 1`) keeps this free of any
        // subtraction that could underflow.
        let mut governing = None;
        for (i, run) in self.stsc.iter().enumerate() {
            if run.first_sample <= sample_id {
                governing = Some(i);
            } else {
                break;
            }
        }

        governing
            .ok_or_else(|| Error::InvalidAsset("BMFF sample precedes first stsc entry".to_string()))
    }

    /// Returns the size in bytes of `sample_id` (1-based).
    fn sample_size(&self, sample_id: u32) -> Result<u32> {
        if self.fixed_sample_size > 0 {
            return Ok(self.fixed_sample_size);
        }

        let index = (sample_id as usize)
            .checked_sub(1)
            .ok_or_else(|| Error::InvalidAsset("BMFF sample id must be non-zero".to_string()))?;

        self.sample_sizes
            .get(index)
            .copied()
            .ok_or_else(|| Error::InvalidAsset("BMFF stsz entry not found".to_string()))
    }

    /// Reads the bytes of `sample_id` (1-based) from `reader`. Returns
    /// `Ok(None)` when the sample or its chunk is not present in the sample
    /// tables (matching the `mp4` crate, which the caller treats as a Merkle
    /// location miss); returns `Err(InvalidAsset)` for malformed tables or a
    /// sample that would read past the end of the stream.
    fn read_sample<R: Read + Seek + ?Sized>(
        &self,
        reader: &mut R,
        sample_id: u32,
        stream_len: u64,
    ) -> Result<Option<Vec<u8>>> {
        if sample_id == 0 || sample_id > self.sample_count {
            return Ok(None);
        }

        let run = &self.stsc[self.stsc_index(sample_id)?];

        if run.samples_per_chunk == 0 {
            return Err(Error::InvalidAsset(
                "stsc samples_per_chunk must be non-zero".to_string(),
            ));
        }

        // chunk_id (1-based) = (sample_id - first_sample) / samples_per_chunk + first_chunk.
        let chunk_id = sample_id
            .checked_sub(run.first_sample)
            .map(|n| n / run.samples_per_chunk)
            .and_then(|n| n.checked_add(run.first_chunk))
            .ok_or_else(|| Error::InvalidAsset("BMFF stsc chunk_id overflow".to_string()))?;

        let chunk_index = match (chunk_id as usize).checked_sub(1) {
            Some(i) => i,
            None => {
                return Err(Error::InvalidAsset(
                    "BMFF stsc chunk_id is zero".to_string(),
                ))
            }
        };

        let chunk_offset = match self.chunk_offsets.get(chunk_index) {
            Some(o) => *o,
            None => return Ok(None),
        };

        // Sum the sizes of the samples preceding this one within its chunk.
        let first_sample_in_chunk = sample_id
            - sample_id
                .checked_sub(run.first_sample)
                .ok_or_else(|| Error::InvalidAsset("BMFF stsc underflow".to_string()))?
                % run.samples_per_chunk;

        // Offset of this sample within its chunk = total size of the samples
        // preceding it. For fixed-size samples that is a single product; only the
        // variable-size case needs to walk the entries (and the stsz box size
        // bounds that walk). This keeps read_sample O(1) for the common
        // fixed-size case instead of O(samples-per-chunk), which would make the
        // caller's per-sample loop quadratic over a single-chunk track.
        let preceding_samples = sample_id - first_sample_in_chunk;
        let mut intra_chunk_offset = (preceding_samples as u64)
            .checked_mul(self.fixed_sample_size as u64)
            .ok_or_else(|| Error::InvalidAsset("BMFF intra-chunk offset overflow".to_string()))?;

        if self.fixed_sample_size == 0 {
            for i in first_sample_in_chunk..sample_id {
                intra_chunk_offset = intra_chunk_offset
                    .checked_add(self.sample_size(i)? as u64)
                    .ok_or_else(|| {
                        Error::InvalidAsset("BMFF intra-chunk offset overflow".to_string())
                    })?;
            }
        }

        let sample_size = self.sample_size(sample_id)?;
        let offset = chunk_offset
            .checked_add(intra_chunk_offset)
            .ok_or_else(|| Error::InvalidAsset("BMFF sample offset overflow".to_string()))?;

        // Bound the read against the stream before allocating, so a crafted size
        // cannot drive an unbounded allocation (memory amplification).
        let end = offset
            .checked_add(sample_size as u64)
            .ok_or_else(|| Error::InvalidAsset("BMFF sample end overflow".to_string()))?;

        if end > stream_len {
            return Err(Error::InvalidAsset(
                "BMFF sample extends beyond end of stream".to_string(),
            ));
        }

        reader.seek(SeekFrom::Start(offset))?;
        let mut buf = vec![0u8; sample_size as usize];
        reader.read_exact(&mut buf)?;

        Ok(Some(buf))
    }
}

/// A parsed BMFF file exposing its tracks' sample tables, and the ability to
/// read individual samples. Replaces the `mp4` crate for the timed-media
/// Merkle-verification path.
#[derive(Debug)]
pub(crate) struct BmffSampleReader {
    tracks: HashMap<u32, SampleTrack>,
    stream_len: u64,
}

impl BmffSampleReader {
    /// Parses `reader`, extracting the sample tables of every track under
    /// `moov`. Does not retain the reader; sample bytes are read on demand via
    /// [`BmffSampleReader::read_sample`] using a caller-supplied reader.
    pub(crate) fn from_stream<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<Self> {
        let stream_len = stream_len(reader)?;

        let mut tracks = HashMap::new();

        if let Some((moov_start, moov_end)) = find_box(reader, 0, stream_len, BoxType::MoovBox)? {
            let mut pos = moov_start;
            while pos < moov_end {
                let (name, content_start, box_end) = read_child(reader, pos, moov_end)?;
                if name == BoxType::TrakBox {
                    if let Some(track) = parse_trak(reader, content_start, box_end)? {
                        // A track cannot contain more samples than the stream has
                        // bytes. This bounds the `stsz` sample_count (which the
                        // fixed-size branch does not otherwise cap) so a crafted
                        // value cannot drive an enormous sample-iteration loop.
                        if track.sample_count as u64 > stream_len {
                            return Err(Error::InvalidAsset(
                                "BMFF stsz sample count exceeds stream length".to_string(),
                            ));
                        }

                        tracks.insert(track.track_id, track);
                    }
                }
                pos = box_end;
            }
        }

        Ok(Self { tracks, stream_len })
    }

    pub(crate) fn tracks(&self) -> &HashMap<u32, SampleTrack> {
        &self.tracks
    }

    /// Reads sample `sample_id` (1-based) of `track_id` from `reader`. `reader`
    /// must be the same stream the tables were parsed from.
    pub(crate) fn read_sample<R: Read + Seek + ?Sized>(
        &self,
        reader: &mut R,
        track_id: u32,
        sample_id: u32,
    ) -> Result<Option<Vec<u8>>> {
        match self.tracks.get(&track_id) {
            Some(track) => track.read_sample(reader, sample_id, self.stream_len),
            None => Ok(None),
        }
    }
}

/// Reads the box header at `pos` and returns `(name, content_start, box_end)`,
/// validating that the box lies within `[pos, end)` and is at least header-sized.
fn read_child<R: Read + Seek + ?Sized>(
    reader: &mut R,
    pos: u64,
    end: u64,
) -> Result<(BoxType, u64, u64)> {
    reader.seek(SeekFrom::Start(pos))?;
    let header = BoxHeaderLite::read(reader)?;
    let content_start = reader.stream_position()?;

    let box_end = pos
        .checked_add(header.size)
        .ok_or_else(|| Error::InvalidAsset("BMFF box size overflow".to_string()))?;

    if box_end > end || box_end < content_start {
        return Err(Error::InvalidAsset(
            "BMFF box size out of bounds".to_string(),
        ));
    }

    Ok((header.name, content_start, box_end))
}

/// Scans the children in `[start, end)` for the first box of `target`, returning
/// its `(content_start, box_end)`.
fn find_box<R: Read + Seek + ?Sized>(
    reader: &mut R,
    start: u64,
    end: u64,
    target: BoxType,
) -> Result<Option<(u64, u64)>> {
    let mut pos = start;
    while pos < end {
        let (name, content_start, box_end) = read_child(reader, pos, end)?;
        if name == target {
            return Ok(Some((content_start, box_end)));
        }
        pos = box_end;
    }
    Ok(None)
}

/// Parses a `trak` box (children in `[start, end)`) into a [`SampleTrack`].
///
/// The `Ok(None)` vs `Err` distinction is deliberate: `Ok(None)` means this trak
/// is not a readable sample track (a required box such as `tkhd`/`mdia`/`minf`/
/// `stbl`/`stsc`/`stsz` or a chunk-offset table is simply absent), so it is
/// skipped rather than failing the whole parse — real files carry hint/metadata
/// tracks without full sample tables. A box that is *present but malformed*
/// yields `Err` from the corresponding `parse_*` helper. A caller that needs a
/// specific track therefore sees a missing one as "not found" (its `local_id`
/// won't be in the map) and a corrupt one as a hard error.
fn parse_trak<R: Read + Seek + ?Sized>(
    reader: &mut R,
    start: u64,
    end: u64,
) -> Result<Option<SampleTrack>> {
    let (tkhd_start, tkhd_end) = match find_box(reader, start, end, BoxType::TkhdBox)? {
        Some(r) => r,
        None => return Ok(None),
    };
    let track_id = parse_tkhd_track_id(reader, tkhd_start, tkhd_end)?;

    // Descend moov/trak/mdia/minf/stbl.
    let (mdia_start, mdia_end) = match find_box(reader, start, end, BoxType::MdiaBox)? {
        Some(r) => r,
        None => return Ok(None),
    };
    let (minf_start, minf_end) = match find_box(reader, mdia_start, mdia_end, BoxType::MinfBox)? {
        Some(r) => r,
        None => return Ok(None),
    };
    let (stbl_start, stbl_end) = match find_box(reader, minf_start, minf_end, BoxType::StblBox)? {
        Some(r) => r,
        None => return Ok(None),
    };

    let (stsc_start, stsc_end) = match find_box(reader, stbl_start, stbl_end, BoxType::StscBox)? {
        Some(r) => r,
        None => return Ok(None),
    };
    let stsc = parse_stsc(reader, stsc_start, stsc_end)?;

    let chunk_offsets =
        if let Some((s, e)) = find_box(reader, stbl_start, stbl_end, BoxType::StcoBox)? {
            parse_stco(reader, s, e)?
        } else if let Some((s, e)) = find_box(reader, stbl_start, stbl_end, BoxType::Co64Box)? {
            parse_co64(reader, s, e)?
        } else {
            return Ok(None);
        };

    let (stsz_start, stsz_end) = match find_box(reader, stbl_start, stbl_end, BoxType::StszBox)? {
        Some(r) => r,
        None => return Ok(None),
    };
    let (fixed_sample_size, sample_sizes, sample_count) = parse_stsz(reader, stsz_start, stsz_end)?;

    Ok(Some(SampleTrack {
        track_id,
        stsc,
        chunk_offsets,
        fixed_sample_size,
        sample_sizes,
        sample_count,
    }))
}

/// Reads the `track_id` field of a `tkhd` box (its content spans
/// `[start, end)`), honoring the version-dependent field layout.
fn parse_tkhd_track_id<R: Read + Seek + ?Sized>(
    reader: &mut R,
    start: u64,
    end: u64,
) -> Result<u32> {
    reader.seek(SeekFrom::Start(start))?;
    let (version, _flags) = read_box_header_ext(reader)?;

    // Skip creation_time + modification_time: 8 bytes for v0, 16 for v1.
    let skip = if version == 1 { 16u64 } else { 8u64 };
    let track_id_pos = reader
        .stream_position()?
        .checked_add(skip)
        .ok_or_else(|| Error::InvalidAsset("BMFF tkhd overflow".to_string()))?;

    if track_id_pos.checked_add(4).map(|p| p > end).unwrap_or(true) {
        return Err(Error::InvalidAsset("BMFF tkhd too small".to_string()));
    }

    reader.seek(SeekFrom::Start(track_id_pos))?;
    Ok(reader.read_u32::<BigEndian>()?)
}

/// Rejects a declared entry count whose `entry_size`-byte entries, starting at
/// `entries_start`, would read past `end` (guards against memory-amplification),
/// returning the count as a `usize` when it fits.
fn bounded_entry_count(
    declared: u32,
    entries_start: u64,
    end: u64,
    entry_size: u64,
) -> Result<usize> {
    // Callers pass a fixed, non-zero entry size, but guard the division anyway so
    // this helper can never divide by zero.
    if entry_size == 0 {
        return Err(Error::InvalidAsset(
            "BMFF sample-table entry size must be non-zero".to_string(),
        ));
    }

    let available = end.saturating_sub(entries_start);
    let max_entries = available / entry_size;

    if declared as u64 > max_entries {
        return Err(Error::InvalidAsset(
            "BMFF sample-table entry count exceeds box size".to_string(),
        ));
    }

    Ok(declared as usize)
}

fn parse_stsc<R: Read + Seek + ?Sized>(
    reader: &mut R,
    start: u64,
    end: u64,
) -> Result<Vec<StscRun>> {
    reader.seek(SeekFrom::Start(start))?;
    let (_version, _flags) = read_box_header_ext(reader)?;
    let entry_count = reader.read_u32::<BigEndian>()?;

    // Each stsc entry is 12 bytes: first_chunk, samples_per_chunk, sample_description_index.
    let count = bounded_entry_count(entry_count, reader.stream_position()?, end, 12)?;

    let mut raw = Vec::with_capacity(count);
    for _ in 0..count {
        let first_chunk = reader.read_u32::<BigEndian>()?;
        let samples_per_chunk = reader.read_u32::<BigEndian>()?;
        let _sample_description_index = reader.read_u32::<BigEndian>()?;
        raw.push((first_chunk, samples_per_chunk));
    }

    // Derive the 1-based first_sample for each run: run i covers
    // (next.first_chunk - this.first_chunk) * this.samples_per_chunk samples.
    let mut runs = Vec::with_capacity(raw.len());
    let mut first_sample: u32 = 1;
    for i in 0..raw.len() {
        let (first_chunk, samples_per_chunk) = raw[i];
        runs.push(StscRun {
            first_chunk,
            samples_per_chunk,
            first_sample,
        });

        if i + 1 < raw.len() {
            let next_first_chunk = raw[i + 1].0;
            first_sample = next_first_chunk
                .checked_sub(first_chunk)
                .and_then(|n| n.checked_mul(samples_per_chunk))
                .and_then(|n| n.checked_add(first_sample))
                .ok_or_else(|| {
                    Error::InvalidAsset("BMFF stsc first_sample overflow".to_string())
                })?;
        }
    }

    Ok(runs)
}

fn parse_stco<R: Read + Seek + ?Sized>(reader: &mut R, start: u64, end: u64) -> Result<Vec<u64>> {
    reader.seek(SeekFrom::Start(start))?;
    let (_version, _flags) = read_box_header_ext(reader)?;
    let entry_count = reader.read_u32::<BigEndian>()?;
    let count = bounded_entry_count(entry_count, reader.stream_position()?, end, 4)?;

    let mut offsets = Vec::with_capacity(count);
    for _ in 0..count {
        offsets.push(reader.read_u32::<BigEndian>()? as u64);
    }
    Ok(offsets)
}

fn parse_co64<R: Read + Seek + ?Sized>(reader: &mut R, start: u64, end: u64) -> Result<Vec<u64>> {
    reader.seek(SeekFrom::Start(start))?;
    let (_version, _flags) = read_box_header_ext(reader)?;
    let entry_count = reader.read_u32::<BigEndian>()?;
    let count = bounded_entry_count(entry_count, reader.stream_position()?, end, 8)?;

    let mut offsets = Vec::with_capacity(count);
    for _ in 0..count {
        offsets.push(reader.read_u64::<BigEndian>()?);
    }
    Ok(offsets)
}

/// Returns `(fixed_sample_size, sample_sizes, sample_count)`.
fn parse_stsz<R: Read + Seek + ?Sized>(
    reader: &mut R,
    start: u64,
    end: u64,
) -> Result<(u32, Vec<u32>, u32)> {
    reader.seek(SeekFrom::Start(start))?;
    let (_version, _flags) = read_box_header_ext(reader)?;
    let sample_size = reader.read_u32::<BigEndian>()?;
    let sample_count = reader.read_u32::<BigEndian>()?;

    if sample_size > 0 {
        // Fixed size: no per-sample table follows.
        return Ok((sample_size, Vec::new(), sample_count));
    }

    // Variable sizes: `sample_count` u32 entries follow the two u32 header fields.
    let entries_start = start
        .checked_add(4 + 4 + 4)
        .ok_or_else(|| Error::InvalidAsset("BMFF stsz overflow".to_string()))?;
    let available = end.saturating_sub(entries_start);

    if sample_count as u64 > available / 4 {
        return Err(Error::InvalidAsset(
            "BMFF stsz sample count exceeds box size".to_string(),
        ));
    }

    let mut sizes = Vec::with_capacity(sample_count as usize);
    for _ in 0..sample_count {
        sizes.push(reader.read_u32::<BigEndian>()?);
    }

    Ok((0, sizes, sample_count))
}

#[cfg(test)]
pub mod tests {
    #![allow(clippy::expect_used)]
    #![allow(clippy::panic)]
    #![allow(clippy::unwrap_used)]

    use super::*;
    use crate::utils::{
        io_utils::tempdirectory,
        test::{fixture_path, temp_dir_path},
    };

    #[test]
    fn test_read_deep_nesting() {
        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();

        let ap = fixture_path("nested_moov_1000.mp4");
        let mut input_stream = std::fs::File::open(&ap).unwrap();

        let bmff = BmffIO::new("mp4");
        let cai = bmff.read_cai(&mut input_stream);

        assert!(cai.is_err());
    }

    #[test]
    fn test_read_mp4() {
        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();

        let ap = fixture_path("video1.mp4");
        let mut input_stream = std::fs::File::open(&ap).unwrap();

        let bmff = BmffIO::new("mp4");
        let cai = bmff.read_cai(&mut input_stream).unwrap();

        assert!(!cai.is_empty());
    }

    #[test]
    fn test_xmp_write() {
        let data = "some test data";
        let source = fixture_path("video1.mp4");

        let temp_dir = tempdirectory().unwrap();
        let output = temp_dir_path(&temp_dir, "video1-out.mp4");

        std::fs::copy(source, &output).unwrap();

        let bmff = BmffIO::new("mp4");

        let eh = bmff.remote_ref_writer_ref().unwrap();

        eh.embed_reference(&output, RemoteRefEmbedType::Xmp(data.to_string()))
            .unwrap();

        let mut output_stream = std::fs::File::open(&output).unwrap();
        let xmp = bmff.get_reader().read_xmp(&mut output_stream).unwrap();

        let loaded = crate::utils::xmp_inmemory_utils::extract_provenance(&xmp).unwrap();

        assert_eq!(&loaded, data);
    }

    #[test]
    fn test_truncated_c2pa_write_mp4() {
        let test_data = "some test data".as_bytes();
        let source = fixture_path("video1.mp4");

        let mut success = false;
        if let Ok(temp_dir) = tempdirectory() {
            let output = temp_dir_path(&temp_dir, "mp4_test.mp4");

            if let Ok(_size) = std::fs::copy(source, &output) {
                let bmff = BmffIO::new("mp4");

                //let test_data =  bmff.read_cai_store(&source).unwrap();
                if let Ok(()) = bmff.save_cai_store(&output, test_data) {
                    if let Ok(read_test_data) = bmff.read_cai_store(&output) {
                        assert!(vec_compare(test_data, &read_test_data));
                        success = true;
                    }
                }
            }
        }
        assert!(success)
    }

    #[test]
    fn test_expanded_c2pa_write_mp4() {
        let mut more_data = "some more test data".as_bytes().to_vec();
        let source = fixture_path("video1.mp4");

        let mut success = false;
        if let Ok(temp_dir) = tempdirectory() {
            let output = temp_dir_path(&temp_dir, "mp4_test.mp4");

            if let Ok(_size) = std::fs::copy(&source, &output) {
                let bmff = BmffIO::new("mp4");

                if let Ok(mut test_data) = bmff.read_cai_store(&source) {
                    test_data.append(&mut more_data);
                    if let Ok(()) = bmff.save_cai_store(&output, &test_data) {
                        if let Ok(read_test_data) = bmff.read_cai_store(&output) {
                            assert!(vec_compare(&test_data, &read_test_data));
                            success = true;
                        }
                    }
                }
            }
        }
        assert!(success)
    }

    #[test]
    fn test_patch_c2pa_write_mp4() {
        let test_data = "some test data".as_bytes();
        let source = fixture_path("video1.mp4");

        let mut success = false;
        if let Ok(temp_dir) = tempdirectory() {
            let output = temp_dir_path(&temp_dir, "mp4_test.mp4");

            if let Ok(_size) = std::fs::copy(source, &output) {
                let bmff = BmffIO::new("mp4");

                if let Ok(source_data) = bmff.read_cai_store(&output) {
                    // create replacement data of same size
                    let mut new_data = vec![0u8; source_data.len()];
                    new_data[..test_data.len()].copy_from_slice(test_data);
                    bmff.patch_cai_store(&output, &new_data).unwrap();

                    let replaced = bmff.read_cai_store(&output).unwrap();

                    assert_eq!(new_data, replaced);

                    success = true;
                }
            }
        }
        assert!(success)
    }

    #[test]
    fn test_remove_c2pa() {
        let source = fixture_path("video1.mp4");

        let temp_dir = tempdirectory().unwrap();
        let output = temp_dir_path(&temp_dir, "mp4_test.mp4");

        std::fs::copy(source, &output).unwrap();
        let bmff_io = BmffIO::new("mp4");

        bmff_io.remove_cai_store(&output).unwrap();

        // read back in asset, JumbfNotFound is expected since it was removed
        match bmff_io.read_cai_store(&output) {
            Err(Error::JumbfNotFound) => (),
            _ => unreachable!(),
        }
    }

    #[test]
    fn test_bmff_large_size_overflow_does_not_panic() {
        // Craft a 32-byte MP4: 16-byte ftyp box followed by a 16-byte large-size box
        // that claims 0xFFFFFFFFFFFFFFF0 bytes. When current=16 (after ftyp), the
        // unchecked addition 16 + 0xFFFFFFFFFFFFFFF0 overflows u64 in debug mode
        // (panic exit 101) and silently wraps to bypass the bounds check in release mode.
        let mut data: Vec<u8> = Vec::new();
        // ftyp box (16 bytes): size=16, type='ftyp', major_brand='mp41', minor_version=0
        data.extend_from_slice(&16u32.to_be_bytes());
        data.extend_from_slice(b"ftyp");
        data.extend_from_slice(b"mp41");
        data.extend_from_slice(&0u32.to_be_bytes());
        // large-size box (16 bytes): size=1 signals largesize, type='mdat', largesize=MAX-15
        data.extend_from_slice(&1u32.to_be_bytes());
        data.extend_from_slice(b"mdat");
        data.extend_from_slice(&0xfffffffffffffff0u64.to_be_bytes());

        let bmff_io = BmffIO::new("mp4");
        let mut source = Cursor::new(data);
        assert!(matches!(
            bmff_io.read_cai(&mut source),
            Err(Error::InvalidAsset(_))
        ));
    }

    #[test]
    fn test_bmff_datamap_offset_overflow_returns_error() {
        // A crafted C2PA manifest can embed a DataMap.offset of u64::MAX, which when
        // added to any non-zero box_start overflows u64 and panics. Verify that
        // bmff_to_jumbf_exclusions returns Err instead of panicking.
        let mut data: Vec<u8> = Vec::new();
        // ftyp box (16 bytes): size=16, type='ftyp', major_brand='mp41', minor_version=0
        data.extend_from_slice(&16u32.to_be_bytes());
        data.extend_from_slice(b"ftyp");
        data.extend_from_slice(b"mp41");
        data.extend_from_slice(&0u32.to_be_bytes());
        // mdat box (8 bytes): size=8, type='mdat'
        data.extend_from_slice(&8u32.to_be_bytes());
        data.extend_from_slice(b"mdat");

        let mut exclusion = ExclusionsMap::new("/mdat".to_string());
        exclusion.data = Some(vec![crate::assertions::DataMap {
            offset: u64::MAX,
            value: vec![0],
        }]);

        let mut reader = Cursor::new(data);
        assert!(matches!(
            bmff_to_jumbf_exclusions(&mut reader, &[exclusion], false),
            Err(Error::InvalidAsset(_))
        ));
    }

    // Regression tests for the UUID box purpose field u64 underflow vulnerability.
    // Without the data_len == 0 guard, reading past the end of the box would decrement
    // data_len from 0 to u64::MAX (wrapping), causing the caller to attempt a
    // u64::MAX-byte allocation (OOM crash).

    #[test]
    fn test_uuid_purpose_missing_null_terminator_rejected() {
        // Box layout: HEADER(8) + UUID(16) + FullBox(4) + payload(8) = 36 bytes total.
        // Payload is 8 non-null bytes — no null terminator — so the parser must hit
        // data_len == 0 and return an error instead of underflowing.
        let size: u64 = 36;
        let (arena, token) = Arena::with_data(BoxInfo {
            path: "/uuid".to_string(),
            parent: None,
            offset: 0,
            size,
            box_type: BoxType::UuidBox,
            user_type: Some(C2PA_UUID.to_vec()),
            version: None,
            flags: None,
        });
        let node = &arena[token];

        // Reader bytes starting at offset 0:
        //   [0..7]   HEADER (8 bytes) — skipped by skip_bytes_to(offset + 8 + 16)
        //   [8..23]  UUID extension (16 bytes)
        //   [24..27] FullBox version(1)+flags(3) = 0x00000000
        //   [28..35] purpose bytes — 8 non-null bytes, no terminator
        let mut bytes = vec![0u8; 8 + 16]; // header + UUID
        bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // FullBox
        bytes.extend_from_slice(b"manifest"); // 8 bytes, no null terminator

        let mut reader = Cursor::new(bytes);
        assert!(matches!(
            get_uuid_box_purpose(&mut reader, node),
            Err(Error::InvalidAsset(_))
        ));
    }

    #[test]
    fn test_uuid_purpose_box_too_small_rejected() {
        // size = 20 < HEADER_SIZE(8) + UUID(16) = 24; the checked_sub chain must return
        // Err(InvalidAsset) before any read occurs.
        let (arena, token) = Arena::with_data(BoxInfo {
            path: "/uuid".to_string(),
            parent: None,
            offset: 0,
            size: 20,
            box_type: BoxType::UuidBox,
            user_type: Some(C2PA_UUID.to_vec()),
            version: None,
            flags: None,
        });
        let node = &arena[token];

        let mut reader = Cursor::new(vec![0u8; 20]);
        assert!(matches!(
            get_uuid_box_purpose(&mut reader, node),
            Err(Error::InvalidAsset(_))
        ));
    }

    // Regression: `_skip_bytes(reader, size)` used `size as i64`, which wraps
    // a u64 > i64::MAX to a negative i64 and would seek *backward* instead of
    // forward. The fix routes through the current position + checked_add +
    // SeekFrom::Start so the seek direction is always forward for any u64
    // that fits inside the file, and any value that would overflow u64
    // surfaces as InvalidAsset.
    #[test]
    fn test_skip_bytes_large_u64_does_not_seek_backward() {
        // A small in-memory buffer where we can't actually seek that far
        // forward — we only care that the function does not produce a
        // *backward* seek and that its result is a well-formed forward seek
        // to a position >= the starting position (or a controlled error).
        let mut buf = vec![0u8; 32];
        let mut reader = Cursor::new(&mut buf);
        reader.seek(SeekFrom::Start(4)).unwrap();

        // Size just above i64::MAX — the pre-fix `size as i64` cast would
        // wrap this to a negative value. `SeekFrom::Start(u64)` handles the
        // large target correctly; the actual stream_position after such a
        // seek is well-defined (some backends allow seeking past EOF).
        // The security property under test is *no backward seek*.
        let before = reader.stream_position().unwrap();
        let _ = _skip_bytes(&mut reader, (i64::MAX as u64) + 1);
        let after = reader.stream_position().unwrap();
        assert!(
            after >= before,
            "skip_bytes must never seek backward — before={before}, after={after}",
        );
    }

    #[test]
    fn test_skip_bytes_u64_max_returns_error_not_backward_seek() {
        // A skip that would push the target past u64::MAX must surface as an
        // `InvalidAsset` error, not wrap into a backward seek.
        let mut buf = vec![0u8; 16];
        let mut reader = Cursor::new(&mut buf);
        reader.seek(SeekFrom::Start(8)).unwrap();

        let result = _skip_bytes(&mut reader, u64::MAX);
        assert!(
            matches!(result, Err(Error::InvalidAsset(_))),
            "expected InvalidAsset for skip past u64::MAX, got {result:?}",
        );
    }

    // Regression: `offset_adjust` used to be an `i32` that received a
    // `usize as i32` cast. For a C2PA / free box larger than i32::MAX
    // (2 GiB), the cast truncated to a bogus value (potentially negative),
    // corrupting every stco/co64/iloc offset patched via
    // `adjust_known_offsets`. Widening `adjust_known_offsets` to accept `i64`
    // and guarding every producer with `i64::try_from(usize)` makes the
    // adjustment either faithful or a clean `InvalidAsset` error.
    #[test]
    fn test_adjust_known_offsets_accepts_over_i32_max_positive() {
        // Empty tree / path map → adjust_known_offsets should not touch
        // anything, and must accept an i64 far beyond i32::MAX without a
        // truncation panic or an integer overflow.
        let (arena, _root) = Arena::with_data(BoxInfo {
            path: "".to_string(),
            offset: 0,
            size: 0,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        });
        let map: HashMap<String, Vec<Token>> = HashMap::new();
        let mut cursor = Cursor::new(Vec::<u8>::new());
        let big_adjust: i64 = (i32::MAX as i64) + 1;
        adjust_known_offsets(&mut cursor, &arena, &map, big_adjust).unwrap();
    }

    #[test]
    fn test_adjust_known_offsets_accepts_below_neg_i32_min_negative() {
        // Same invariant on the negative side — the pre-fix i32 signature
        // could not represent a shrink adjustment for a box larger than
        // 2 GiB (its magnitude); the i64 signature can, and empty maps keep
        // the function a no-op.
        let (arena, _root) = Arena::with_data(BoxInfo {
            path: "".to_string(),
            offset: 0,
            size: 0,
            box_type: BoxType::Empty,
            parent: None,
            user_type: None,
            version: None,
            flags: None,
        });
        let map: HashMap<String, Vec<Token>> = HashMap::new();
        let mut cursor = Cursor::new(Vec::<u8>::new());
        let big_neg: i64 = (i32::MIN as i64) - 1;
        adjust_known_offsets(&mut cursor, &arena, &map, big_neg).unwrap();
    }

    /// The native sample reader must parse our real MP4/MOV fixtures (which carry
    /// full sample tables, multiple tracks, and codec sample entries) and read a
    /// sample, so the parser's strict bounds don't reject legitimate assets.
    #[test]
    fn native_reader_parses_real_assets() {
        for name in ["video1.mp4", "c.mov", "BigBuckBunny_320x180.mp4"] {
            let bytes = std::fs::read(fixture_path(name))
                .unwrap_or_else(|e| panic!("cannot read fixture {name}: {e}"));
            let mut reader = Cursor::new(bytes);

            let media = BmffSampleReader::from_stream(&mut reader)
                .unwrap_or_else(|e| panic!("native reader failed to parse {name}: {e}"));
            assert!(
                !media.tracks().is_empty(),
                "{name} should expose at least one track"
            );

            // Read sample 1 of the first track that has samples; it must succeed
            // and return non-empty bytes.
            let mut read_any = false;
            for (track_id, track) in media.tracks() {
                if track.sample_count() > 0 {
                    let sample = media
                        .read_sample(&mut reader, *track_id, 1)
                        .unwrap_or_else(|e| panic!("{name} read_sample failed: {e}"))
                        .unwrap_or_else(|| panic!("{name} track {track_id} sample 1 missing"));
                    assert!(!sample.is_empty(), "{name} sample 1 should be non-empty");
                    read_any = true;
                    break;
                }
            }
            assert!(read_any, "{name} should have a track with samples");
        }
    }

    /// A zero entry size must not divide-by-zero; it is rejected instead.
    #[test]
    fn bounded_entry_count_rejects_zero_entry_size() {
        assert!(bounded_entry_count(1, 0, 100, 0).is_err());
    }

    // --- Direct SampleTrack unit tests -------------------------------------
    //
    // These exercise the offset/size arithmetic and its defensive guards
    // directly, including branches that the public verify path cannot reach
    // (e.g. a degenerate `stsc` that the parser's `first_sample` derivation
    // never actually produces).

    fn one_run_track(
        first_chunk: u32,
        samples_per_chunk: u32,
        chunk_offsets: Vec<u64>,
        fixed_sample_size: u32,
        sample_sizes: Vec<u32>,
        sample_count: u32,
    ) -> SampleTrack {
        SampleTrack {
            track_id: 1,
            stsc: vec![StscRun {
                first_chunk,
                samples_per_chunk,
                first_sample: 1,
            }],
            chunk_offsets,
            fixed_sample_size,
            sample_sizes,
            sample_count,
        }
    }

    #[test]
    fn read_sample_out_of_range_is_none() {
        let track = one_run_track(1, 1, vec![0], 4, vec![], 2);
        let mut reader = Cursor::new(vec![0u8; 64]);
        // sample_id 0 and sample_id > sample_count both yield Ok(None).
        assert!(track.read_sample(&mut reader, 0, 64).unwrap().is_none());
        assert!(track.read_sample(&mut reader, 3, 64).unwrap().is_none());
    }

    #[test]
    fn read_sample_zero_samples_per_chunk_errs() {
        let track = one_run_track(1, 0, vec![0], 4, vec![], 1);
        let mut reader = Cursor::new(vec![0u8; 64]);
        assert!(track.read_sample(&mut reader, 1, 64).is_err());
    }

    #[test]
    fn read_sample_zero_first_chunk_errs() {
        // first_chunk 0 makes chunk_id 0, which has no valid 0-based index.
        let track = one_run_track(0, 1, vec![0], 4, vec![], 1);
        let mut reader = Cursor::new(vec![0u8; 64]);
        let err = track.read_sample(&mut reader, 1, 64).unwrap_err();
        assert!(matches!(err, Error::InvalidAsset(ref m) if m.contains("chunk_id is zero")));
    }

    #[test]
    fn read_sample_missing_chunk_offset_is_none() {
        // stsc references chunk 1 but the offset table is empty.
        let track = one_run_track(1, 1, vec![], 4, vec![], 1);
        let mut reader = Cursor::new(vec![0u8; 64]);
        assert!(track.read_sample(&mut reader, 1, 64).unwrap().is_none());
    }

    #[test]
    fn read_sample_beyond_eof_errs() {
        // Fixed size 4, chunk offset 60, stream len 62 -> read of [60,64) exceeds.
        let track = one_run_track(1, 1, vec![60], 4, vec![], 1);
        let mut reader = Cursor::new(vec![0u8; 62]);
        let err = track.read_sample(&mut reader, 1, 62).unwrap_err();
        assert!(matches!(err, Error::InvalidAsset(ref m) if m.contains("beyond end of stream")));
    }

    #[test]
    fn read_sample_fixed_size_reads_correct_bytes() {
        // Two 4-byte samples in one chunk at offset 8; sample 2 is bytes [12,16).
        let mut data = vec![0u8; 8];
        data.extend_from_slice(b"AAAABBBB");
        let track = one_run_track(1, 2, vec![8], 4, vec![], 2);
        let mut reader = Cursor::new(data);
        assert_eq!(
            track.read_sample(&mut reader, 1, 16).unwrap().unwrap(),
            b"AAAA"
        );
        assert_eq!(
            track.read_sample(&mut reader, 2, 16).unwrap().unwrap(),
            b"BBBB"
        );
    }

    #[test]
    fn read_sample_variable_size_reads_correct_bytes() {
        // Variable sizes [3, 5] in one chunk at offset 4; sample 2 is bytes [7,12).
        let mut data = vec![0u8; 4];
        data.extend_from_slice(b"aaabbbbb");
        let track = one_run_track(1, 2, vec![4], 0, vec![3, 5], 2);
        let mut reader = Cursor::new(data);
        assert_eq!(
            track.read_sample(&mut reader, 1, 12).unwrap().unwrap(),
            b"aaa"
        );
        assert_eq!(
            track.read_sample(&mut reader, 2, 12).unwrap().unwrap(),
            b"bbbbb"
        );
    }

    #[test]
    fn stsc_index_empty_errs() {
        let track = SampleTrack {
            track_id: 1,
            stsc: vec![],
            chunk_offsets: vec![],
            fixed_sample_size: 4,
            sample_sizes: vec![],
            sample_count: 0,
        };
        let err = track.stsc_index(1).unwrap_err();
        assert!(matches!(err, Error::InvalidAsset(ref m) if m == "BMFF has no stsc entries"));
    }

    #[test]
    fn stsc_index_precedes_first_entry_errs() {
        // A degenerate run whose first_sample is greater than the queried id.
        let track = SampleTrack {
            track_id: 1,
            stsc: vec![StscRun {
                first_chunk: 1,
                samples_per_chunk: 1,
                first_sample: 5,
            }],
            chunk_offsets: vec![0],
            fixed_sample_size: 4,
            sample_sizes: vec![],
            sample_count: 10,
        };
        let err = track.stsc_index(2).unwrap_err();
        assert!(
            matches!(err, Error::InvalidAsset(ref m) if m.contains("precedes first stsc entry"))
        );
    }

    #[test]
    fn stsc_index_selects_governing_run() {
        let track = SampleTrack {
            track_id: 1,
            stsc: vec![
                StscRun {
                    first_chunk: 1,
                    samples_per_chunk: 2,
                    first_sample: 1,
                },
                StscRun {
                    first_chunk: 3,
                    samples_per_chunk: 1,
                    first_sample: 5,
                },
            ],
            chunk_offsets: vec![0, 0, 0],
            fixed_sample_size: 4,
            sample_sizes: vec![],
            sample_count: 10,
        };
        assert_eq!(track.stsc_index(1).unwrap(), 0);
        assert_eq!(track.stsc_index(4).unwrap(), 0);
        assert_eq!(track.stsc_index(5).unwrap(), 1);
        assert_eq!(track.stsc_index(9).unwrap(), 1);
    }

    #[test]
    fn sample_size_variable_out_of_range_errs() {
        let track = one_run_track(1, 1, vec![0], 0, vec![4], 1);
        // sample_id 2 has no stsz entry.
        assert!(track.sample_size(2).is_err());
        // sample_id 0 underflows the 1-based index.
        assert!(track.sample_size(0).is_err());
    }

    #[test]
    fn parse_tkhd_track_id_version_1() {
        // FullBox header (version 1), then 16 bytes of times, then track_id.
        let mut b = vec![1u8, 0, 0, 0]; // version=1, flags=0
        b.extend_from_slice(&[0u8; 16]); // creation+modification (64-bit each)
        b.extend_from_slice(&42u32.to_be_bytes()); // track_id
        let mut reader = Cursor::new(b);
        let id = parse_tkhd_track_id(&mut reader, 0, 24).unwrap();
        assert_eq!(id, 42);
    }

    #[test]
    fn parse_tkhd_track_id_too_small_errs() {
        let mut b = vec![0u8, 0, 0, 0]; // version=0, flags=0
        b.extend_from_slice(&[0u8; 4]); // truncated before track_id
        let mut reader = Cursor::new(b);
        assert!(parse_tkhd_track_id(&mut reader, 0, 8).is_err());
    }

    #[test]
    fn read_sample_end_overflow_errs() {
        // A chunk offset at u64::MAX makes offset + size overflow.
        let track = one_run_track(1, 1, vec![u64::MAX], 4, vec![], 1);
        let mut reader = Cursor::new(vec![0u8; 16]);
        let err = track.read_sample(&mut reader, 1, 16).unwrap_err();
        assert!(matches!(err, Error::InvalidAsset(ref m) if m.contains("overflow")));
    }

    #[test]
    fn read_sample_offset_overflow_errs() {
        // chunk_offset near u64::MAX plus a preceding variable sample overflows
        // the intra-chunk addition into the base offset.
        let track = one_run_track(1, 2, vec![u64::MAX], 0, vec![1, 1], 2);
        let mut reader = Cursor::new(vec![0u8; 16]);
        let err = track.read_sample(&mut reader, 2, 16).unwrap_err();
        assert!(matches!(err, Error::InvalidAsset(ref m) if m.contains("overflow")));
    }

    #[test]
    fn parse_stsz_variable_count_exceeds_box_errs() {
        // version/flags(4) + sample_size=0(4) + sample_count=1000(4); box ends
        // right after the header, so 1000 entries cannot fit.
        let mut b = vec![0u8; 4];
        b.extend_from_slice(&0u32.to_be_bytes()); // sample_size = 0 (variable)
        b.extend_from_slice(&1000u32.to_be_bytes()); // sample_count
        let end = b.len() as u64;
        let mut reader = Cursor::new(b);
        assert!(parse_stsz(&mut reader, 0, end).is_err());
    }

    #[test]
    fn read_child_rejects_box_extending_past_parent() {
        // A box header claiming size 1000 inside a 20-byte parent range.
        let mut b = 1000u32.to_be_bytes().to_vec();
        b.extend_from_slice(b"free");
        b.extend_from_slice(&[0u8; 12]);
        let mut reader = Cursor::new(b);
        assert!(read_child(&mut reader, 0, 20).is_err());
    }

    #[test]
    fn from_stream_without_moov_has_no_tracks() {
        // Just an ftyp box, no moov: parses to an empty track set.
        let ftyp = {
            let payload = b"isom\x00\x00\x00\x00isom";
            let size = (8 + payload.len()) as u32;
            let mut v = size.to_be_bytes().to_vec();
            v.extend_from_slice(b"ftyp");
            v.extend_from_slice(payload);
            v
        };
        let mut reader = Cursor::new(ftyp);
        let media = BmffSampleReader::from_stream(&mut reader).unwrap();
        assert!(media.tracks().is_empty());
    }
}
