// 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.

#[cfg(feature = "file_io")]
use std::path::{Path, PathBuf};
use std::{
    collections::{HashMap, HashSet},
    io::{Cursor, Read, Seek},
};

use async_generic::async_generic;
use log::error;

#[cfg(feature = "file_io")]
use crate::jumbf_io::{get_supported_file_extension, save_jumbf_to_file};
use crate::{
    assertion::{Assertion, AssertionBase, AssertionData, AssertionDecodeError},
    assertions::{
        labels::{self, CLAIM},
        BmffHash, BoxHash, CertificateStatus, DataBox, DataHash, Ingredient, Relationship,
        TimeStamp, User, UserCbor,
    },
    asset_io::{
        CAIRead, CAIReadWrite, HashBlockObjectType, HashObjectPositions, RemoteRefEmbedType,
    },
    claim::{
        check_ocsp_status, check_ocsp_status_async, Claim, ClaimAssertion, ClaimAssetData,
        RemoteManifest,
    },
    context::{Context, ProgressPhase},
    cose_sign::{cose_sign, cose_sign_async},
    cose_validator::{verify_cose, verify_cose_async},
    crypto::{
        asn1::rfc3161::TstInfo,
        cose::{
            cert_chain_from_sign1, fetch_and_check_ocsp_response,
            fetch_and_check_ocsp_response_async, parse_cose_sign1, CertificateTrustPolicy,
            TimeStampStorage,
        },
        hash::sha256,
        ocsp::OcspResponse,
        time_stamp::verify_time_stamp,
    },
    dynamic_assertion::{
        AsyncDynamicAssertion, DynamicAssertion, DynamicAssertionContent, PartialClaim,
    },
    error::{Error, Result},
    hash_utils::{hash_by_alg, vec_compare, verify_by_alg},
    hashed_uri::HashedUri,
    jumbf::{
        self,
        boxes::*,
        labels::{
            assertion_label_from_uri, manifest_label_from_uri, manifest_label_to_parts,
            to_assertion_uri, to_manifest_uri, ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE,
        },
    },
    jumbf_io::{
        get_assetio_handler, is_bmff_format, load_jumbf_from_stream, object_locations_from_stream,
        save_jumbf_to_stream,
    },
    log_item,
    manifest_store_report::ManifestStoreReport,
    maybe_send_sync::MaybeSend,
    settings::{builder::OcspFetchScope, get_thread_local_settings, Settings, MAX_ASSERTIONS},
    status_tracker::{ErrorBehavior, StatusTracker},
    utils::{
        hash_utils::HashRange,
        io_utils::{self, insert_data_at, stream_len},
        is_zero,
    },
    validation_results::validation_codes::{
        ASSERTION_CBOR_INVALID, ASSERTION_JSON_INVALID, ASSERTION_MISSING, CLAIM_MALFORMED,
    },
    validation_status::{self, ALGORITHM_UNSUPPORTED},
    AsyncSigner, Signer, ValidationResults, ValidationState,
};

const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests
#[cfg(feature = "fetch_remote_manifests")]
const DEFAULT_MANIFEST_RESPONSE_SIZE: usize = 10 * 1024 * 1024; // 10 MB

/// Maximum depth of nested ingredient references allowed during validation.
///
/// Prevents stack overflow when processing a manifest store that contains a deep
/// linear chain of ingredient references (A → B → C → … → N). With the default
/// 8 MB thread stack, ~3 000 levels causes an unrecoverable stack overflow (exit 134).
/// A limit of 200 uses only ~540 KB of stack, providing a large safety margin while
/// accommodating any realistic provenance chain depth.
const MAX_INGREDIENT_DEPTH: usize = 200;

pub(crate) struct ManifestHashes {
    pub manifest_box_hash: Vec<u8>,
    pub signature_box_hash: Vec<u8>,
}

// internal struct to pass around info needed to optimally complete validation
#[derive(Default)]
pub(crate) struct StoreValidationInfo<'a> {
    pub redactions: Vec<String>, // list of redactions found in claim hierarchy
    pub ingredient_references: HashMap<String, HashSet<String>>, // mapping in ingredients to list of claims that reference it
    pub manifest_map: HashMap<String, &'a Claim>, // list of the addressable items in ingredient, saves re-parsing the items during validation
    pub binding_claim: String,                    // name of the claim that has the hash binding
    pub timestamps: HashMap<String, TstInfo>,     // list of timestamp assertions for each claim
    pub update_manifest_label: Option<String>,    // label of the update manifest if it exists
    pub manifest_store_range: Option<HashRange>, // range of the manifest store in the asset for data hash exclusions
    pub certificate_statuses: HashMap<String, Vec<Vec<u8>>>, // list of certificate status assertions for each serial
}

/// A `Store` maintains a list of `Claim` structs.
///
/// Typically, this list of `Claim`s represents all of the claims in an asset.
#[derive(Debug)]
pub struct Store {
    claims_map: HashMap<String, Claim>,
    claims: Vec<String>, // maintains order of claims
    manifest_box_hash_cache: HashMap<String, (Vec<u8>, Vec<u8>)>,
    label: String,
    provenance_path: Option<String>,
    ctp: CertificateTrustPolicy,
    remote_url: Option<String>,
    embedded: bool,
}

struct ManifestInfo<'a> {
    pub desc_box: &'a JUMBFDescriptionBox,
    pub sbox: &'a JUMBFSuperBox,
}

impl Default for Store {
    fn default() -> Self {
        Self::from_context(&Context::new())
    }
}

impl Store {
    /// Create a new, empty claims store with default settings.
    pub fn new() -> Self {
        Store {
            claims_map: HashMap::new(),
            manifest_box_hash_cache: HashMap::new(),
            claims: Vec::new(),
            label: MANIFEST_STORE_EXT.to_string(),
            ctp: CertificateTrustPolicy::default(),
            provenance_path: None,
            remote_url: None,
            embedded: false,
        }
    }

    /// Create a new, empty claims store with the specified settings.
    pub fn from_context(context: &Context) -> Self {
        let mut store = Store::new();
        let settings = context.settings();

        // load the trust handler settings, don't worry about status as these are checked during setting generation
        if let Some(ta) = &settings.trust.trust_anchors {
            let _v = store.add_trust(ta.as_bytes());
        }

        if let Some(pa) = &settings.trust.user_anchors {
            let _v = store.add_user_trust_anchors(pa.as_bytes());
        }

        if let Some(tc) = &settings.trust.trust_config {
            let _v = store.add_trust_config(tc.as_bytes());
        }

        if let Some(al) = &settings.trust.allowed_list {
            let _v = store.add_trust_allowed_list(al.as_bytes());
        }

        store
    }

    // Append new Store to the current Store preserving the order from the new Store
    pub(crate) fn append_store(&mut self, store: &Store) {
        for claim in store.claims() {
            self.insert_restored_claim(claim.label().to_string(), claim.clone());
        }
    }

    /// Return label for the store
    #[allow(dead_code)] // doesn't harm to have this
    pub fn label(&self) -> &str {
        &self.label
    }

    /// Returns the remote url of the manifest if this [`Store`] was obtained remotely.
    pub fn remote_url(&self) -> Option<&str> {
        self.remote_url.as_deref()
    }

    /// Returns if the [`Store`] was created from an embedded manifest.
    pub fn is_embedded(&self) -> bool {
        self.embedded
    }

    /// Load set of trust anchors used for certificate validation. [u8] containing the
    /// trust anchors is passed in the trust_vec variable.
    pub fn add_trust(&mut self, trust_vec: &[u8]) -> Result<()> {
        Ok(self.ctp.add_trust_anchors(trust_vec)?)
    }

    // Load set of user trust anchors used for certificate validation. [u8] to the
    /// user trust anchors is passed in the trust_vec variable.  This can be called multiple times
    /// if there are additional trust stores.
    pub fn add_user_trust_anchors(&mut self, trust_vec: &[u8]) -> Result<()> {
        Ok(self.ctp.add_user_trust_anchors(trust_vec)?)
    }

    pub fn add_trust_config(&mut self, trust_vec: &[u8]) -> Result<()> {
        self.ctp.add_valid_ekus(trust_vec);
        Ok(())
    }

    pub fn add_trust_allowed_list(&mut self, allowed_vec: &[u8]) -> Result<()> {
        Ok(self.ctp.add_end_entity_credentials(allowed_vec)?)
    }

    /// Get the provenance if available.
    /// If loaded from an existing asset it will be provenance from the last claim.
    /// If a new claim is committed that will be the provenance claim
    pub fn provenance_path(&self) -> Option<String> {
        self.provenance_path.as_ref().cloned()
    }

    // set the path of the current provenance claim
    pub fn set_provenance_path(&mut self, claim: &Claim) {
        let path = claim.to_claim_uri();
        self.provenance_path = Some(path);
    }

    /// get the list of claims for this store in the order they were added
    pub fn claims(&self) -> Vec<&Claim> {
        self.claims
            .iter()
            .filter_map(|l| self.claims_map.get(l))
            .collect()
    }

    /// the JUMBF manifest box hash (spec 1.2) and signature box hash (2.x)
    pub(crate) fn get_manifest_box_hashes(&self, claim: &Claim) -> ManifestHashes {
        if let Some((mbh, sbh)) = self.manifest_box_hash_cache.get(claim.label()) {
            ManifestHashes {
                manifest_box_hash: mbh.clone(),
                signature_box_hash: sbh.clone(),
            }
        } else {
            ManifestHashes {
                manifest_box_hash: Store::calc_manifest_box_hash(claim, None, claim.alg())
                    .unwrap_or_default(),
                signature_box_hash: Claim::calc_sig_box_hash(claim, claim.alg())
                    .unwrap_or_default(),
            }
        }
    }

    // remove a claim from the store
    pub(crate) fn remove_claim(&mut self, label: &str) -> Option<Claim> {
        self.claims.retain(|l| l != label);
        self.claims_map.remove(label)
    }

    /// Add a new Claim to this Store. The function
    /// will return the label of the claim.
    pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> {
        // make sure there is no pending unsigned claim
        if let Some(pc) = self.provenance_claim() {
            if pc.signature_val().is_empty() {
                return Err(Error::ClaimUnsigned);
            }
        }
        // verify the claim is valid
        claim.build()?;

        // update the provenance path
        self.set_provenance_path(&claim);

        let claim_label = claim.label().to_string();

        // add ingredients claims to the store claims
        // replace any existing claims with the same label
        for ingredient in claim.claim_ingredients() {
            if self
                .claims_map
                .insert(ingredient.label().to_string(), ingredient.clone())
                .is_none()
            {
                self.claims.push(ingredient.label().to_string());
            }
        }

        // add to new claim to list of claims
        self.claims.push(claim_label.clone());
        self.claims_map.insert(claim_label.clone(), claim);

        Ok(claim_label)
    }

    /// Add a new update manifest to this Store. The manifest label
    /// may be updated to reflect is position in the manifest Store
    /// if there are conflicting label names.  The function
    /// will return the label of the claim used
    #[allow(unused)]
    pub fn update_manifest_test(&mut self, claim: &Claim) -> Result<()> {
        use crate::{
            assertions::{labels::CLAIM_THUMBNAIL, Actions},
            claim::ALLOWED_UPDATE_MANIFEST_ACTIONS,
        };

        // check for disallowed assertions
        if claim.has_assertion_type(labels::DATA_HASH)
            || claim.has_assertion_type(labels::BOX_HASH)
            || claim.has_assertion_type(labels::BMFF_HASH)
            || claim.has_assertion_type(labels::COLLECTION_HASH)
        {
            return Err(Error::ClaimInvalidContent);
        }

        // must have exactly one ingredient
        let ingredient_assertions = claim.ingredient_assertions();
        if ingredient_assertions.len() != 1 {
            return Err(Error::ClaimInvalidContent);
        }

        let ingredient = Ingredient::from_assertion(ingredient_assertions[0].assertion())?;

        // must have a parent relationship
        if ingredient.relationship != Relationship::ParentOf {
            return Err(Error::IngredientNotFound);
        }

        // make sure ingredient c2pa.manifest points to provenance claim
        if let Some(c2pa_manifest) = ingredient.c2pa_manifest() {
            // the manifest should refer to provenance claim
            if let Some(pc) = self.provenance_claim() {
                if !c2pa_manifest.url().contains(pc.label()) {
                    return Err(Error::IngredientNotFound);
                }
            } else {
                // when called from builder, there will be no provenance claim yet
                // so we cannot verify the manifest url, but we just created it.
                // return Err(Error::IngredientNotFound);
            }
        } else {
            return Err(Error::IngredientNotFound);
        }

        // must be one of the allowed actions
        for aa in claim.action_assertions() {
            let actions = Actions::from_assertion(aa.assertion())?;
            for action in actions.actions() {
                if !ALLOWED_UPDATE_MANIFEST_ACTIONS
                    .iter()
                    .any(|a| *a == action.action())
                {
                    return Err(Error::ClaimInvalidContent);
                }
            }
        }

        // thumbnail assertions are not allowed
        if claim
            .claim_assertion_store()
            .iter()
            .any(|ca| ca.label_raw().contains(CLAIM_THUMBNAIL))
        {
            return Err(Error::OtherError(
                "only one claim thumbnail assertion allowed".into(),
            ));
        }

        Ok(())
    }

    /// Add a new update manifest to this Store. The manifest label
    /// may be updated to reflect is position in the manifest Store
    /// if there are conflicting label names.  The function
    /// will return the label of the claim used
    #[allow(unused)]
    pub fn commit_update_manifest(&mut self, mut claim: Claim) -> Result<String> {
        self.update_manifest_test(&claim)?;

        claim.set_update_manifest(true);

        self.commit_claim(claim)
    }

    /// Get Claim by label
    // Returns Option<&Claim>
    pub fn get_claim(&self, label: &str) -> Option<&Claim> {
        self.claims_map.get(label)
    }

    /// Returns true if `uri` (absolute or relative to `claim_label`) appears in the
    /// redaction list of any claim in the store — i.e. it was intentionally removed
    /// by a parent manifest.
    pub(crate) fn is_uri_redacted(&self, claim_label: &str, uri: &str) -> bool {
        let abs_uri = jumbf::labels::to_absolute_uri(claim_label, uri);
        self.claims_map
            .values()
            .any(|c| c.redactions().is_some_and(|r| r.contains(&abs_uri)))
    }

    /// Get Claim by label
    // Returns Option<&Claim>
    pub fn get_claim_mut(&mut self, label: &str) -> Option<&mut Claim> {
        self.claims_map.get_mut(label)
    }

    /// returns a Claim given a jumbf uri
    pub fn get_claim_from_uri(&self, uri: &str) -> Result<&Claim> {
        let claim_label = Store::manifest_label_from_path(uri);
        self.get_claim(&claim_label)
            .ok_or_else(|| Error::ClaimMissing {
                label: claim_label.to_owned(),
            })
    }

    /// returns a ClaimAssertion given a jumbf uri, resolving to the right claim in the store
    pub fn get_claim_assertion_from_uri(&self, uri: &str) -> Result<&ClaimAssertion> {
        // first find the right claim and then look for the assertion there
        let claim = self.get_claim_from_uri(uri)?;
        let (label, instance) = Claim::assertion_label_from_link(uri);
        claim
            .get_claim_assertion(&label, instance)
            .ok_or_else(|| Error::AssertionMissing {
                url: uri.to_owned(),
            })
    }

    /// Returns an Assertion referenced by JUMBF URI.  The URI should be absolute and include
    /// the desired Claim in the path. If you need to specify the Claim for this URI use
    /// get_assertion_from_uri_and_claim.
    /// uri - The JUMBF URI for desired Assertion.
    pub fn get_assertion_from_uri(&self, uri: &str) -> Option<&Assertion> {
        let claim_label = Store::manifest_label_from_path(uri);
        let (assertion_label, instance) = Claim::assertion_label_from_link(uri);

        if let Some(claim) = self.get_claim(&claim_label) {
            claim.get_assertion(&assertion_label, instance)
        } else {
            None
        }
    }

    /// Returns an Assertion referenced by JUMBF URI. Only the Claim specified by target_claim_label
    /// will be searched.  The target_claim_label can be a Claim label or JUMBF URI.
    /// uri - The JUMBF URI for desired Assertion.
    /// target_claim_label - Label or URI of the Claim to search for the case when the URI is a relative path.
    pub fn get_assertion_from_uri_and_claim(
        &self,
        uri: &str,
        target_claim_label: &str,
    ) -> Option<&Assertion> {
        let (assertion_label, instance) = Claim::assertion_label_from_link(uri);

        let label = Store::manifest_label_from_path(target_claim_label);

        if let Some(claim) = self.get_claim(&label) {
            claim.get_assertion(&assertion_label, instance)
        } else {
            None
        }
    }

    /// Returns a DataBox referenced by JUMBF URI if it exists.
    ///
    /// Relative paths will use the provenance claim to resolve the DataBox.d
    pub fn get_data_box_from_uri_and_claim(
        &self,
        hr: &HashedUri,
        target_claim_label: &str,
    ) -> Option<&DataBox> {
        match jumbf::labels::manifest_label_from_uri(&hr.url()) {
            Some(label) => self.get_claim(&label), // use the manifest label from the thumbnail uri
            None => self.get_claim(target_claim_label), //  relative so use the target claim label
        }
        .and_then(|claim| claim.get_databox(hr))
    }

    // Returns placeholder that will be searched for and replaced
    // with actual signature data.
    fn sign_claim_placeholder(claim: &Claim, min_reserve_size: usize) -> Vec<u8> {
        let placeholder_str = format!("signature placeholder:{}", claim.label());
        let mut placeholder = sha256(placeholder_str.as_bytes());

        use std::cmp::max;
        placeholder.resize(max(placeholder.len(), min_reserve_size), 0);

        placeholder
    }

    /// Return OCSP info if available
    // Currently only called from manifest_store behind a feature flag but this is allowable
    // anywhere so allow dead code here for future uses to compile
    #[allow(dead_code)]
    #[async_generic]
    pub fn get_ocsp_status(&self, context: &Context) -> Option<String> {
        let claim = self
            .provenance_claim()
            .ok_or(Error::ProvenanceMissing)
            .ok()?;

        let sig = claim.signature_val();
        let data = claim.data().ok()?;
        let mut validation_log =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        let sign1 = parse_cose_sign1(sig, &data, &mut validation_log).ok()?;
        let ocsp_status = if _sync {
            check_ocsp_status(
                &sign1,
                &data,
                &self.ctp,
                None,
                None,
                &mut validation_log,
                context,
            )
        } else {
            check_ocsp_status_async(
                &sign1,
                &data,
                &self.ctp,
                None,
                None,
                &mut validation_log,
                context,
            )
            .await
        };
        if let Ok(info) = ocsp_status {
            if let Some(revoked_at) = &info.revoked_at {
                Some(format!(
                    "Certificate Status: Revoked, revoked at: {revoked_at}"
                ))
            } else {
                Some(format!(
                    "Certificate Status: Good, next update: {}",
                    info.next_update
                ))
            }
        } else {
            None
        }
    }

    /// Sign the claim and return signature.
    #[async_generic(async_signature(
        &self,
        claim: &Claim,
        signer: &dyn AsyncSigner,
        box_size: usize,
        settings: &Settings,
    ))]
    pub fn sign_claim(
        &self,
        claim: &Claim,
        signer: &dyn Signer,
        box_size: usize,
        settings: &Settings,
    ) -> Result<Vec<u8>> {
        let claim_bytes = claim.data()?;

        // no verification of timestamp trust while signing
        let mut adjusted_settings = settings.clone();
        adjusted_settings.verify.verify_timestamp_trust = false;

        let tss = if claim.version() > 1 {
            TimeStampStorage::V2_sigTst2_CTT
        } else {
            TimeStampStorage::V1_sigTst
        };

        let result = if _sync {
            if signer.direct_cose_handling() {
                // Let the signer do all the COSE processing and return the structured COSE data.
                return signer.sign(&claim_bytes); // do not verify remote signers (we never did)
            } else {
                cose_sign(signer, &claim_bytes, box_size, tss, &adjusted_settings)
            }
        } else {
            if signer.direct_cose_handling() {
                // Let the signer do all the COSE processing and return the structured COSE data.
                return signer.sign(claim_bytes.clone()).await;
            // do not verify remote signers (we never did)
            } else {
                cose_sign_async(signer, &claim_bytes, box_size, tss, settings).await
            }
        };
        match result {
            Ok(sig) => {
                // Sanity check: Ensure that this signature is valid.
                let verify_after_sign = settings.verify.verify_after_sign;

                if verify_after_sign {
                    let mut cose_log =
                        StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

                    let result = if _sync {
                        verify_cose(
                            &sig,
                            &claim_bytes,
                            b"",
                            false,
                            &self.ctp,
                            None,
                            &mut cose_log,
                            &adjusted_settings,
                        )
                    } else {
                        verify_cose_async(
                            &sig,
                            &claim_bytes,
                            b"",
                            false,
                            &self.ctp,
                            None,
                            &mut cose_log,
                            &adjusted_settings,
                        )
                        .await
                    };
                    if let Err(err) = result {
                        error!("Signature that was just generated does not validate: {err:#?}");
                        return Err(err);
                    }
                }

                Ok(sig)
            }
            Err(e) => Err(e),
        }
    }

    /// Retrieves all manifest labels that need to fetch ocsp responses.
    pub fn get_manifest_labels_for_ocsp(&self, settings: &Settings) -> Vec<String> {
        let labels = match settings.builder.certificate_status_fetch {
            Some(ocsp_fetch) => match ocsp_fetch {
                OcspFetchScope::All => self.claims.clone(),
                OcspFetchScope::Active => {
                    if let Some(active_label) = self.provenance_label() {
                        vec![active_label]
                    } else {
                        Vec::new()
                    }
                }
            },
            None => Vec::new(),
        };

        match settings.builder.certificate_status_should_override {
            Some(should_override) => {
                if !should_override {
                    labels
                        .into_iter()
                        .filter(|label| {
                            self.claims_map
                                .get(label)
                                .is_some_and(|claim| !claim.has_ocsp_vals())
                        })
                        .collect()
                } else {
                    labels
                }
            }
            _ => Vec::new(),
        }
    }

    /// return the current provenance claim label if available
    pub fn provenance_label(&self) -> Option<String> {
        self.provenance_path()
            .map(|provenance| Store::manifest_label_from_path(&provenance))
    }

    /// return the current provenance claim if available
    pub fn provenance_claim(&self) -> Option<&Claim> {
        match self.provenance_path() {
            Some(provenance) => {
                let claim_label = Store::manifest_label_from_path(&provenance);
                self.get_claim(&claim_label)
            }
            None => None,
        }
    }

    /// return the current provenance claim as mutable if available
    pub fn provenance_claim_mut(&mut self) -> Option<&mut Claim> {
        match self.provenance_path() {
            Some(provenance) => {
                let claim_label = Store::manifest_label_from_path(&provenance);
                self.get_claim_mut(&claim_label)
            }
            None => None,
        }
    }

    // add a restored claim
    pub(crate) fn insert_restored_claim(&mut self, label: String, claim: Claim) {
        self.set_provenance_path(&claim);
        self.claims_map.insert(label.clone(), claim);
        self.claims.push(label);
    }

    /// Build a flat ingredient [`Store`] for `claim` by walking nested ingredient assertions.
    ///
    /// Used to materialize the JUMBF bytes for a single ingredient's provenance chain without
    /// eagerly serializing every ingredient when constructing a [`Reader`](crate::Reader).
    pub(crate) fn build_flat_ingredient_store(store: &Store, claim: &Claim) -> Result<Store> {
        let mut ingredient_store = Store::new();
        let mut visited = HashSet::new();
        let mut path = Vec::new();

        fn collect_flat(
            store: &Store,
            claim: &Claim,
            ingredient_store: &mut Store,
            visited: &mut HashSet<String>,
            path: &mut Vec<String>,
        ) -> Result<()> {
            let claim_label = claim.label().to_string();

            if visited.contains(&claim_label) {
                return Ok(());
            }

            // Cycle detection
            if path.iter().any(|p| p == &claim_label) {
                return Ok(());
            }

            path.push(claim_label.clone());

            for ing_assertion in claim.ingredient_assertions() {
                let ingredient = Ingredient::from_assertion(ing_assertion.assertion())?;
                let manifest_uri = ingredient
                    .active_manifest
                    .as_ref()
                    .or(ingredient.c2pa_manifest.as_ref());
                if let Some(manifest_uri) = manifest_uri {
                    let ingredient_label = Store::manifest_label_from_path(&manifest_uri.url());
                    if let Some(ingredient_claim) = store.get_claim(&ingredient_label) {
                        collect_flat(store, ingredient_claim, ingredient_store, visited, path)?;
                    }
                }
            }

            // Post-order: add after all children
            ingredient_store.insert_restored_claim(claim_label.clone(), claim.clone());
            visited.insert(claim_label);
            path.pop();

            Ok(())
        }

        collect_flat(store, claim, &mut ingredient_store, &mut visited, &mut path)?;

        Ok(ingredient_store)
    }

    // replace a claim if it already exists
    pub(crate) fn replace_claim_or_insert(&mut self, label: String, claim: Claim) {
        if self.get_claim(&label).is_some() {
            self.claims_map.insert(label.clone(), claim);
        } else {
            self.insert_restored_claim(label, claim);
        }
    }

    fn add_assertion_to_jumbf_store(
        store: &mut CAIAssertionStore,
        claim_assertion: &ClaimAssertion,
    ) -> Result<()> {
        // Grab assertion data object.
        let d = claim_assertion.assertion().decode_data();

        match d {
            AssertionData::Json(_) => {
                let mut json_data = CAIJSONAssertionBox::new(&claim_assertion.label());
                json_data.add_json(claim_assertion.assertion().data().to_vec());
                if let Some(salt) = claim_assertion.salt() {
                    json_data.set_salt(salt.clone())?;
                }
                store.add_assertion(Box::new(json_data));
            }
            AssertionData::Binary(_) => {
                // TODO: Handle other binary box types if needed.
                let mut data = JumbfEmbeddedFileBox::new(&claim_assertion.label());
                data.add_data(
                    claim_assertion.assertion().data().to_vec(),
                    claim_assertion.assertion().mime_type(),
                    None,
                );
                if let Some(salt) = claim_assertion.salt() {
                    data.set_salt(salt.clone())?;
                }
                store.add_assertion(Box::new(data));
            }
            AssertionData::Cbor(_) => {
                let mut cbor_data = CAICBORAssertionBox::new(&claim_assertion.label());
                cbor_data.add_cbor(claim_assertion.assertion().data().to_vec());
                if let Some(salt) = claim_assertion.salt() {
                    cbor_data.set_salt(salt.clone())?;
                }
                store.add_assertion(Box::new(cbor_data));
            }
            AssertionData::Uuid(s, _) => {
                let mut uuid_data = CAIUUIDAssertionBox::new(&claim_assertion.label());
                uuid_data.add_uuid(s, claim_assertion.assertion().data().to_vec())?;
                if let Some(salt) = claim_assertion.salt() {
                    uuid_data.set_salt(salt.clone())?;
                }
                store.add_assertion(Box::new(uuid_data));
            }
        }
        Ok(())
    }

    // look for old style hashing to determine if this is a pre 1.0 claim
    fn is_old_assertion(alg: &str, data: &[u8], original_hash: &[u8]) -> bool {
        let old_hash = hash_by_alg(alg, data, None);
        vec_compare(&old_hash, original_hash)
    }

    fn get_assertion_from_jumbf_store(
        claim: &Claim,
        assertion_box: &JUMBFSuperBox,
        label: &str,
        check_for_legacy_assertion: bool,
        validation_log: &mut StatusTracker,
    ) -> Result<ClaimAssertion> {
        let assertion_desc_box = assertion_box.desc_box();

        let (raw_label, instance) = Claim::assertion_label_from_link(label);
        let instance_label = Claim::label_with_instance(&raw_label, instance);
        let (assertion_hashed_uri, claim_assertion_type) = claim
            .assertion_hashed_uri_from_label(&instance_label)
            .ok_or_else(|| {
                log_item!(
                    label.to_owned(),
                    "error loading assertion",
                    "get_assertion_from_jumbf_store"
                )
                .validation_status(ASSERTION_MISSING)
                .failure_as_err(
                    validation_log,
                    Error::AssertionMissing {
                        url: instance_label.to_string(),
                    },
                )
            })?;

        let alg = match assertion_hashed_uri.alg() {
            Some(ref a) => a.clone(),
            None => claim.alg().to_string(),
        };

        // get salt value if set
        let salt = assertion_desc_box.get_salt();

        let result = match assertion_desc_box.uuid().as_ref() {
            CAI_JSON_ASSERTION_UUID => {
                let json_box = assertion_box
                    .data_box_as_json_box(0)
                    .ok_or(Error::JumbfBoxNotFound)?;

                let assertion = Assertion::from_data_json(&raw_label, json_box.json())?;

                // make sure it is JSON
                if let Err(e) = serde_json::from_slice::<serde_json::Value>(json_box.json()) {
                    log_item!(
                        label.to_owned(),
                        "invalid assertion json",
                        "get_assertion_from_jumbf_store"
                    )
                    .validation_status(ASSERTION_JSON_INVALID)
                    .failure(
                        validation_log,
                        Error::AssertionDecoding(
                            AssertionDecodeError::from_assertion_and_json_err(&assertion, e),
                        ),
                    )?;
                }

                let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?;
                Ok(ClaimAssertion::new(
                    assertion,
                    instance,
                    &hash,
                    &alg,
                    salt,
                    claim_assertion_type,
                ))
            }
            CAI_EMBEDDED_FILE_UUID => {
                let ef_box = assertion_box
                    .data_box_as_embedded_media_type_box(0)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let data_box = assertion_box
                    .data_box_as_embedded_file_content_box(1)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let media_type = ef_box.media_type();
                let assertion =
                    Assertion::from_data_binary(&raw_label, &media_type, data_box.data());
                let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?;
                Ok(ClaimAssertion::new(
                    assertion,
                    instance,
                    &hash,
                    &alg,
                    salt,
                    claim_assertion_type,
                ))
            }
            CAI_CBOR_ASSERTION_UUID => {
                let cbor_box = assertion_box
                    .data_box_as_cbor_box(0)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let assertion = Assertion::from_data_cbor(&raw_label, cbor_box.cbor());

                // make sure it is CBOR
                if let Err(e) = c2pa_cbor::from_slice::<c2pa_cbor::Value>(cbor_box.cbor()) {
                    log_item!(
                        label.to_owned(),
                        "invalid assertion cbor",
                        "get_assertion_from_jumbf_store"
                    )
                    .validation_status(ASSERTION_CBOR_INVALID)
                    .failure(
                        validation_log,
                        Error::AssertionDecoding(
                            AssertionDecodeError::from_assertion_and_cbor_err(&assertion, e),
                        ),
                    )?;
                }

                let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?;
                Ok(ClaimAssertion::new(
                    assertion,
                    instance,
                    &hash,
                    &alg,
                    salt,
                    claim_assertion_type,
                ))
            }
            CAI_UUID_ASSERTION_UUID => {
                let uuid_box = assertion_box
                    .data_box_as_uuid_box(0)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let uuid_str = hex::encode(uuid_box.uuid());
                let assertion = Assertion::from_data_uuid(&raw_label, &uuid_str, uuid_box.data());

                // if a redaction then make sure the data is zeros
                if uuid_str == C2PA_REDACTION_UUID {
                    let data = uuid_box.data();
                    if !is_zero(data) {
                        let assertion_absolute_uri =
                            to_assertion_uri(claim.label(), &instance_label);
                        log_item!(
                            assertion_absolute_uri,
                            "redacted assertion data must be zeros or empty",
                            "get_assertion_from_jumbf_store"
                        )
                        .validation_status(validation_status::ASSERTION_NOT_REDACTED)
                        .failure(
                            validation_log,
                            Error::OtherError(
                                "redacted assertion data must be zeros or empty".into(),
                            ),
                        )?;
                    }
                }

                let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?;
                Ok(ClaimAssertion::new(
                    assertion,
                    instance,
                    &hash,
                    &alg,
                    salt,
                    claim_assertion_type,
                ))
            }
            _ => Err(Error::JumbfCreationError),
        };

        if check_for_legacy_assertion {
            // make sure this is not pre 1.0 data
            match result {
                Ok(r) => {
                    // look for old style hashing
                    if Store::is_old_assertion(
                        &alg,
                        r.assertion().data(),
                        &assertion_hashed_uri.hash(),
                    ) {
                        Err(Error::PrereleaseError)
                    } else {
                        Ok(r)
                    }
                }
                Err(e) => Err(e),
            }
        } else {
            result
        }
    }

    pub(crate) fn to_jumbf_internal(&self, min_reserve_size: usize) -> Result<Vec<u8>> {
        // Create the CAI block.
        let mut cai_block = Cai::new();

        // Add claims and assertions in this store to the JUMBF store.
        for claim in self.claims() {
            let cai_store = Store::build_manifest_box(claim, min_reserve_size)?;

            // add the completed CAI store into the CAI block.
            cai_block.add_box(Box::new(cai_store));
        }

        // Write it to memory.
        let mut mem_box: Vec<u8> = Vec::new();
        cai_block.write_box(&mut mem_box)?;

        if mem_box.is_empty() {
            Err(Error::JumbfCreationError)
        } else {
            Ok(mem_box)
        }
    }

    fn build_manifest_box(claim: &Claim, min_reserve_size: usize) -> Result<CAIManifest> {
        // box label
        let label = claim.label();

        let manifest_type = if claim.update_manifest() {
            ManifestType::UpdateManifest
        } else {
            ManifestType::Manifest
        };

        let mut cai_store = CAIManifest::new(label, manifest_type, claim.compressed());

        for manifest_box in claim.get_box_order() {
            match *manifest_box {
                ASSERTIONS => {
                    let mut a_store = CAIAssertionStore::new();

                    // add assertions to CAI assertion store.
                    let cas = claim.claim_assertion_store();
                    for assertion in cas {
                        Store::add_assertion_to_jumbf_store(&mut a_store, assertion)?;
                    }

                    cai_store.add_box(Box::new(a_store)); // add the assertion store to the manifest
                }
                CLAIM => {
                    let mut cb = CAIClaimBox::new(claim.version());

                    // Add the Claim json
                    let claim_cbor_bytes = claim.data()?;
                    let c_cbor = JUMBFCBORContentBox::new(claim_cbor_bytes);
                    cb.add_claim(Box::new(c_cbor));

                    cai_store.add_box(Box::new(cb)); // add claim to manifest
                }
                SIGNATURE => {
                    // create a signature and add placeholder data to the CAI store.
                    let mut sigb = CAISignatureBox::new();
                    let signed_data = match claim.signature_val().is_empty() {
                        false => claim.signature_val().clone(), // existing claims have sig values
                        true => Store::sign_claim_placeholder(claim, min_reserve_size), /* empty is the new sig to be replaced */
                    };

                    let sigc = JUMBFCBORContentBox::new(signed_data);
                    sigb.add_signature(Box::new(sigc));

                    cai_store.add_box(Box::new(sigb)); // add signature to manifest
                }
                CREDENTIALS => {
                    // add vc_store if needed
                    if !claim.get_verifiable_credentials().is_empty() && claim.version() < 2 {
                        let mut vc_store = CAIVerifiableCredentialStore::new();

                        // Add assertions to CAI assertion store.
                        let vcs = claim.get_verifiable_credentials_store();
                        for (uri, assertion_data) in vcs {
                            if let AssertionData::Json(j) = assertion_data {
                                let id = Claim::vc_id(j)?;
                                let mut json_data = CAIJSONAssertionBox::new(&id);
                                json_data.add_json(j.as_bytes().to_vec());

                                if let Some(salt) = uri.salt() {
                                    json_data.set_salt(salt.clone())?;
                                }

                                vc_store.add_credential(Box::new(json_data));
                            } else {
                                return Err(Error::BadParam("VC data must be JSON".to_string()));
                            }
                        }
                        cai_store.add_box(Box::new(vc_store)); // add the CAI assertion store to manifest
                    }
                }
                DATABOXES => {
                    // Add the data boxes
                    if !claim.databoxes().is_empty() {
                        let mut databoxes = CAIDataboxStore::new();

                        for (uri, db) in claim.databoxes() {
                            let db_cbor_bytes = c2pa_cbor::to_vec(db)
                                .map_err(|err| Error::AssertionEncoding(err.to_string()))?;

                            let (link, instance) = Claim::assertion_label_from_link(&uri.url());
                            let label = Claim::label_with_instance(&link, instance);

                            let mut db_cbor = CAICBORAssertionBox::new(&label);
                            db_cbor.add_cbor(db_cbor_bytes);

                            if let Some(salt) = uri.salt() {
                                db_cbor.set_salt(salt.clone())?;
                            }

                            databoxes.add_databox(Box::new(db_cbor));
                        }

                        cai_store.add_box(Box::new(databoxes)); // add claim to manifest
                    }
                }
                _ => return Err(Error::ClaimInvalidContent),
            }
        }

        Ok(cai_store)
    }

    // calculate the hash of the manifest JUMBF box
    pub fn calc_manifest_box_hash(
        claim: &Claim,
        salt: Option<Vec<u8>>,
        alg: &str,
    ) -> Result<Vec<u8>> {
        let mut hash_bytes = Vec::with_capacity(4096);

        // build box
        let mut cai_store = Store::build_manifest_box(claim, 0)?;

        // add salt if requested
        if let Some(salt) = salt {
            cai_store.set_salt(salt)?;
        }

        // box content as Vec
        cai_store.super_box().write_box_payload(&mut hash_bytes)?;

        Ok(hash_by_alg(alg, &hash_bytes, None))
    }

    fn manifest_map<'a>(sb: &'a JUMBFSuperBox) -> Result<HashMap<String, ManifestInfo<'a>>> {
        let mut box_info: HashMap<String, ManifestInfo<'a>> = HashMap::new();
        for i in 0..sb.data_box_count() {
            let sbox = sb.data_box_as_superbox(i).ok_or(Error::JumbfBoxNotFound)?;
            let desc_box = sbox.desc_box();

            let label = desc_box.uuid();

            let mi = ManifestInfo { desc_box, sbox };

            box_info.insert(label, mi);
        }

        Ok(box_info)
    }

    // Compare two version labels
    // base_version_label - is the source label
    // desired_version_label - is the label to compare to the base
    // returns true if desired version is <= base version
    fn check_label_version(base_version_label: &str, desired_version_label: &str) -> bool {
        labels::version(desired_version_label) <= labels::version(base_version_label)
    }

    #[inline]
    pub fn from_jumbf(buffer: &[u8], validation_log: &mut StatusTracker) -> Result<Store> {
        // Legacy path: no Context available; read thread-local settings for backward compatibility.
        let max_manifest_size = get_thread_local_settings()
            .core
            .max_decompressed_manifest_size_in_mb
            .saturating_mul(1024 * 1024);
        Self::from_jumbf_impl(Store::new(), buffer, validation_log, max_manifest_size)
    }

    #[inline]
    pub fn from_jumbf_with_context(
        buffer: &[u8],
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Store> {
        let max_manifest_size = context
            .settings()
            .core
            .max_decompressed_manifest_size_in_mb
            .saturating_mul(1024 * 1024);
        Self::from_jumbf_impl(
            Store::from_context(context),
            buffer,
            validation_log,
            max_manifest_size,
        )
    }

    fn from_jumbf_impl(
        mut store: Store,
        buffer: &[u8],
        validation_log: &mut StatusTracker,
        max_manifest_size: usize,
    ) -> Result<Store> {
        if buffer.is_empty() {
            return Err(Error::JumbfNotFound);
        }

        // setup a cursor for reading the buffer...
        let mut buf_reader = Cursor::new(buffer);

        // this loads up all the boxes...
        let super_box = BoxReader::read_super_box(&mut buf_reader)?;

        // this loads up all the boxes...
        let cai_block = Cai::from(super_box);

        // check the CAI Block
        let desc_box = cai_block.desc_box();
        if desc_box.uuid() != CAI_BLOCK_UUID {
            log_item!("JUMBF", "c2pa box not found", "from_jumbf").failure_no_throw(
                validation_log,
                Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound),
            );

            return Err(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound));
        }

        let num_stores = cai_block.data_box_count();
        for idx in 0..num_stores {
            let store_box = CAIManifest::from(
                cai_block
                    .data_box_as_superbox(idx)
                    .ok_or(Error::JumbfBoxNotFound)?,
                max_manifest_size,
            )?;
            let cai_store_box = store_box.super_box();
            let cai_store_desc_box = cai_store_box.desc_box();

            // ignore unknown boxes per the spec
            if cai_store_desc_box.uuid() != CAI_UPDATE_MANIFEST_UUID
                && cai_store_desc_box.uuid() != CAI_MANIFEST_UUID
            {
                continue;
            }

            // remember the order of the boxes to insure the box hashes can be regenerated
            let mut box_order: Vec<&str> = Vec::new();

            // make sure there are not multiple claim boxes
            let mut claim_box_cnt = 0;
            for i in 0..cai_store_box.data_box_count() {
                let sbox = cai_store_box
                    .data_box_as_superbox(i)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let desc_box = sbox.desc_box();

                if desc_box.uuid() == CAI_CLAIM_UUID {
                    claim_box_cnt += 1;
                }

                if claim_box_cnt > 1 {
                    log_item!("JUMBF", "c2pa multiple claim boxes found", "from_jumbf")
                        .validation_status(validation_status::CLAIM_MULTIPLE)
                        .failure_no_throw(
                            validation_log,
                            Error::InvalidClaim(InvalidClaimError::C2paMultipleClaimBoxes),
                        );

                    return Err(Error::InvalidClaim(
                        InvalidClaimError::C2paMultipleClaimBoxes,
                    ));
                }

                let (box_label, _instance) =
                    Claim::box_name_label_instance(desc_box.label().as_ref());
                match box_label.as_ref() {
                    ASSERTIONS => box_order.push(ASSERTIONS),
                    CLAIM => box_order.push(CLAIM),
                    SIGNATURE => box_order.push(SIGNATURE),
                    CREDENTIALS => box_order.push(CREDENTIALS),
                    DATABOXES => box_order.push(DATABOXES),
                    _ => {
                        log_item!("JUMBF", "unrecognized manifest box", "from_jumbf")
                            .validation_status(validation_status::CLAIM_MULTIPLE)
                            .failure(
                                validation_log,
                                Error::InvalidClaim(InvalidClaimError::ClaimBoxData),
                            )?;
                    }
                }
            }

            let is_update_manifest = cai_store_desc_box.uuid() == CAI_UPDATE_MANIFEST_UUID;

            // get map of boxes in this manifest
            let manifest_boxes = Store::manifest_map(cai_store_box)?;

            // retrieve the claim & validate
            let claim_superbox = manifest_boxes
                .get(CAI_CLAIM_UUID)
                .ok_or(Error::InvalidClaim(
                    InvalidClaimError::ClaimSuperboxNotFound,
                ))?
                .sbox;
            let claim_desc_box = manifest_boxes
                .get(CAI_CLAIM_UUID)
                .ok_or(Error::InvalidClaim(
                    InvalidClaimError::ClaimDescriptionBoxNotFound,
                ))?
                .desc_box;

            // check if version is supported
            let claim_box_ver = claim_desc_box.label();
            if !Self::check_label_version(&Claim::build_version_support(), &claim_box_ver) {
                return Err(Error::InvalidClaim(InvalidClaimError::ClaimVersionTooNew));
            }

            // check box contents
            if claim_desc_box.uuid() == CAI_CLAIM_UUID {
                // must be have only one claim
                if claim_superbox.data_box_count() > 1 {
                    return Err(Error::InvalidClaim(InvalidClaimError::DuplicateClaimBox {
                        label: claim_desc_box.label(),
                    }));
                }
                // better be, but just in case...

                let cbor_box = match claim_superbox.data_box_as_cbor_box(0) {
                    Some(c) => c,
                    None => {
                        // check for old claims for reporting
                        match claim_superbox.data_box_as_json_box(0) {
                            Some(_c) => {
                                log_item!("JUMBF", "error loading claim data", "from_jumbf")
                                    .failure_no_throw(validation_log, Error::PrereleaseError);

                                return Err(Error::PrereleaseError);
                            }
                            None => {
                                log_item!("JUMBF", "error loading claim data", "from_jumbf")
                                    .failure_no_throw(
                                        validation_log,
                                        Error::InvalidClaim(InvalidClaimError::ClaimBoxData),
                                    );

                                return Err(Error::InvalidClaim(InvalidClaimError::ClaimBoxData));
                            }
                        }
                    }
                };

                if cbor_box.box_uuid() != JUMBF_CBOR_UUID {
                    return Err(Error::InvalidClaim(
                        InvalidClaimError::ClaimDescriptionBoxInvalid,
                    ));
                }
            }

            // retrieve the signature
            let sig_superbox = manifest_boxes
                .get(CAI_SIGNATURE_UUID)
                .ok_or(Error::InvalidClaim(
                    InvalidClaimError::ClaimSignatureBoxNotFound,
                ))?
                .sbox;
            let sig_desc_box = manifest_boxes
                .get(CAI_SIGNATURE_UUID)
                .ok_or(Error::InvalidClaim(
                    InvalidClaimError::ClaimSignatureDescriptionBoxNotFound,
                ))?
                .desc_box;

            // check box contents
            if sig_desc_box.uuid() == CAI_SIGNATURE_UUID {
                // better be, but just in case...
                let sig_box = sig_superbox
                    .data_box_as_cbor_box(0)
                    .ok_or(Error::JumbfBoxNotFound)?;
                if sig_box.box_uuid() != JUMBF_CBOR_UUID {
                    return Err(Error::InvalidClaim(
                        InvalidClaimError::ClaimSignatureDescriptionBoxInvalid,
                    ));
                }
            }
            // save signature to be validated on load
            let sig_data = sig_superbox
                .data_box_as_cbor_box(0)
                .ok_or(Error::JumbfBoxNotFound)?;

            // Create a new Claim object from jumbf data after validations
            let cbor_box = claim_superbox
                .data_box_as_cbor_box(0)
                .ok_or(Error::JumbfBoxNotFound)?;
            let mut claim = Claim::from_data(&cai_store_desc_box.label(), cbor_box.cbor())
                .map_err(|e| {
                    log_item!(CLAIM, "CLAIM CBOR could not be decoded", "from_jumbf")
                        .validation_status(CLAIM_MALFORMED)
                        .failure_as_err(validation_log, e)
                })?;

            // the claim must have an algorithm to be able to process internal hashes
            if claim.alg_raw().is_none() {
                return Err(log_item!(
                    claim.label().to_owned(),
                    "no hashing algorithm found for claim",
                    "from_jumbf"
                )
                .validation_status(ALGORITHM_UNSUPPORTED)
                .failure_as_err(validation_log, Error::UnknownAlgorithm));
            }

            // make sure box version label match the read Claim
            if claim.version() > 1 {
                match labels::version(&claim_box_ver) {
                    v if claim.version() >= v => (),
                    _ => return Err(Error::InvalidClaim(InvalidClaimError::ClaimBoxVersion)),
                }
            }

            // set the  type of manifest
            claim.set_update_manifest(is_update_manifest);

            // set order to process JUMBF boxes
            claim.set_box_order(box_order);

            // retrieve & set signature for each claim
            claim.set_signature_val(sig_data.cbor().clone()); // load the stored signature

            // set the compression status
            claim.set_compressed_manifest(store_box.compressed_store);

            // retrieve the assertion store
            let assertion_store_box = manifest_boxes
                .get(CAI_ASSERTION_STORE_UUID)
                .ok_or(Error::InvalidClaim(
                    InvalidClaimError::AssertionStoreSuperboxNotFound,
                ))?
                .sbox;

            let num_assertions = assertion_store_box.data_box_count();

            // Reject manifests that embed more assertions than the configured limit to
            // prevent unbounded memory and CPU consumption on untrusted input.
            if num_assertions > MAX_ASSERTIONS {
                return Err(Error::TooManyAssertions {
                    max: MAX_ASSERTIONS,
                });
            }

            // loop over all assertions in assertion store...
            let mut check_for_legacy_assertion = true;
            for idx in 0..num_assertions {
                let assertion_box = assertion_store_box
                    .data_box_as_superbox(idx)
                    .ok_or(Error::JumbfBoxNotFound)?;
                let assertion_desc_box = assertion_box.desc_box();

                // Add assertions to claim after validation
                let label = assertion_desc_box.label();
                match Store::get_assertion_from_jumbf_store(
                    &claim,
                    assertion_box,
                    &label,
                    check_for_legacy_assertion,
                    validation_log,
                ) {
                    Ok(assertion) => {
                        claim.put_assertion_store(assertion); // restore assertion data to claim
                        check_for_legacy_assertion = false; // only need to check once
                    }
                    Err(e) => {
                        // if this is an old manifest always return
                        if std::mem::discriminant(&e)
                            == std::mem::discriminant(&Error::PrereleaseError)
                        {
                            log_item!("JUMBF", "error loading assertion", "from_jumbf")
                                .failure_no_throw(validation_log, e);

                            return Err(Error::PrereleaseError);
                        }
                        return Err(e);
                    }
                }
            }

            // load vc_store if available
            if let Some(mi) = manifest_boxes.get(CAI_VERIFIABLE_CREDENTIALS_STORE_UUID) {
                let vc_store = mi.sbox;
                let num_vcs = vc_store.data_box_count();

                // VC stores should not be in a 2.x claim
                if claim.version() > 1 {
                    return Err(Error::InvalidClaim(InvalidClaimError::UnsupportedFeature(
                        "Verifiable Credentials Store > v1 claim".to_string(),
                    )));
                }

                for idx in 0..num_vcs {
                    let vc_box = vc_store
                        .data_box_as_superbox(idx)
                        .ok_or(Error::JumbfBoxNotFound)?;
                    let vc_json = vc_box
                        .data_box_as_json_box(0)
                        .ok_or(Error::JumbfBoxNotFound)?;
                    let vc_desc_box = vc_box.desc_box();
                    let _id = vc_desc_box.label();

                    let json_str = String::from_utf8(vc_json.json().to_vec())
                        .map_err(|_| InvalidClaimError::VerifiableCredentialStoreInvalid)?;

                    let salt = vc_desc_box.get_salt();

                    claim.put_verifiable_credential(&json_str, salt)?;
                }
            }

            // load databox store if available
            if let Some(mi) = manifest_boxes.get(CAI_DATABOXES_STORE_UUID) {
                let databox_store = mi.sbox;
                let num_databoxes = databox_store.data_box_count();

                for idx in 0..num_databoxes {
                    let db_box = databox_store
                        .data_box_as_superbox(idx)
                        .ok_or(Error::JumbfBoxNotFound)?;
                    let db_cbor = db_box
                        .data_box_as_cbor_box(0)
                        .ok_or(Error::JumbfBoxNotFound)?;
                    let db_desc_box = db_box.desc_box();
                    let label = db_desc_box.label();

                    let salt = db_desc_box.get_salt();

                    claim.put_databox(&label, db_cbor.cbor(), salt)?;
                }
            }

            // save the hash of the loaded manifest for ingredient validation
            // and the signature box for Ingredient_v3
            store.manifest_box_hash_cache.insert(
                claim.label().to_owned(),
                (
                    Store::calc_manifest_box_hash(&claim, None, claim.alg())?,
                    Claim::calc_sig_box_hash(&claim, claim.alg())?,
                ),
            );

            // add claim to store
            store.insert_restored_claim(cai_store_desc_box.label(), claim);
        }

        Ok(store)
    }

    // Get the store label from jumbf path
    pub fn manifest_label_from_path(claim_path: &str) -> String {
        if let Some(s) = jumbf::labels::manifest_label_from_uri(claim_path) {
            s
        } else {
            claim_path.to_owned()
        }
    }

    // recursively walk the ingredients and validate
    #[async_generic]
    fn ingredient_checks(
        store: &Store,
        claim: &Claim,
        svi: &StoreValidationInfo<'_>,
        validation_log: &mut StatusTracker,
        depth: usize,
        context: &Context,
    ) -> Result<()> {
        if depth >= MAX_INGREDIENT_DEPTH {
            return Err(Error::InvalidAsset(format!(
                "ingredient chain depth ({depth}) exceeds maximum ({MAX_INGREDIENT_DEPTH})"
            )));
        }

        let settings = context.settings();

        // Pre-count verifiable ingredients so we can emit accurate step/total values.
        let total_ingredients = claim.ingredient_assertions().len() as u32;
        let mut ingredient_step = 0u32;

        // walk the ingredients
        for i in claim.ingredient_assertions() {
            ingredient_step += 1;
            context.check_progress(
                ProgressPhase::VerifyingIngredient,
                ingredient_step,
                total_ingredients,
            )?;

            // allow for zero out ingredient assertions
            if is_zero(i.assertion().data()) {
                continue;
            }

            let ingredient_assertion = Ingredient::from_assertion(i.assertion()).map_err(|e| {
                log_item!(
                    i.label().clone(),
                    "ingredient assertion could not be parsed",
                    "ingredient_checks"
                )
                .validation_status(validation_status::ASSERTION_INGREDIENT_MALFORMED)
                .failure_as_err(validation_log, e)
            })?;

            // we don't care about InputTo ingredients
            if ingredient_assertion.relationship == Relationship::InputTo {
                continue;
            }

            validation_log
                .push_ingredient_uri(jumbf::labels::to_assertion_uri(claim.label(), &i.label()));

            // is this an ingredient
            if let Some(c2pa_manifest) = ingredient_assertion.c2pa_manifest() {
                // if this is a v3 ingredient then it must have validation report indicating it was validated
                if let Some(ingredient_version) = ingredient_assertion.version() {
                    if ingredient_version >= 3 && ingredient_assertion.validation_results.is_none()
                    {
                        log_item!(
                            jumbf::labels::to_assertion_uri(claim.label(), &i.label()),
                            "ingredient V3 must have validation results",
                            "ingredient_checks"
                        )
                        .validation_status(validation_status::ASSERTION_INGREDIENT_MALFORMED)
                        .failure(
                            validation_log,
                            Error::HashMismatch(
                                "ingredient V3 missing validation status".to_string(),
                            ),
                        )?;
                    }
                }

                let label = Store::manifest_label_from_path(&c2pa_manifest.url());

                if let Some(ingredient) = store.get_claim(&label) {
                    let alg = match c2pa_manifest.alg() {
                        Some(a) => a,
                        None => ingredient.alg().to_owned(),
                    };

                    // are we evaluating a 2.x manifest, then use those rule
                    let ingredient_version = ingredient.version();
                    let has_redactions = svi.redactions.iter().any(|r| r.contains(&label));

                    // allow the extra ingredient trust checks
                    // these checks are to prevent the trust spoofing
                    let check_ingredient_trust: bool = settings.verify.verify_trust;

                    // get the 1.1-1.2 box hash
                    let ingredient_hashes = store.get_manifest_box_hashes(ingredient);

                    // since no redactions we can try manifest match method
                    let mut pre_v1_3_hash = false;
                    let manifests_match = if !has_redactions {
                        // test for 1.1 hash then 1.0 version
                        if !vec_compare(&c2pa_manifest.hash(), &ingredient_hashes.manifest_box_hash)
                        {
                            // try legacy hash
                            pre_v1_3_hash = true;
                            verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None)
                        } else {
                            true
                        }
                    } else {
                        false
                    };

                    // since the manifest hashes are equal we can short circuit the rest of the validation
                    // we can only do this for post 1.3 Claims since manfiest box hashing was not available
                    if manifests_match && !pre_v1_3_hash {
                        log_item!(
                            c2pa_manifest.url(),
                            "ingredient hash matched",
                            "ingredient_checks"
                        )
                        .validation_status(validation_status::INGREDIENT_MANIFEST_VALIDATED)
                        .success(validation_log);
                    }

                    // if mismatch is not because of a redaction this is a hard error
                    if !manifests_match && !has_redactions {
                        log_item!(
                            c2pa_manifest.url(),
                            "ingredient hash incorrect",
                            "ingredient_checks"
                        )
                        .validation_status(validation_status::INGREDIENT_MANIFEST_MISMATCH)
                        .failure(
                            validation_log,
                            Error::HashMismatch(
                                "ingredient hash does not match found ingredient".to_string(),
                            ),
                        )?;
                    }

                    // if manifest hash did not match because of redaction and this is a V2 or greater claim then we
                    // must try the signature validation method before proceeding
                    if !manifests_match && has_redactions && ingredient_version > 1 {
                        let claim_signature =
                            ingredient_assertion.signature().ok_or_else(|| {
                                log_item!(
                                    c2pa_manifest.url(),
                                    "ingredient claimSignature missing",
                                    "ingredient_checks"
                                )
                                .validation_status(
                                    validation_status::INGREDIENT_CLAIM_SIGNATURE_MISSING,
                                )
                                .failure_as_err(
                                    validation_log,
                                    Error::HashMismatch(
                                        "ingredient claimSignature missing".to_string(),
                                    ),
                                )
                            })?;

                        // compare the signature box hashes
                        if vec_compare(
                            &claim_signature.hash(),
                            &ingredient_hashes.signature_box_hash,
                        ) {
                            log_item!(
                                c2pa_manifest.url(),
                                "ingredient claimSignature validated",
                                "ingredient_checks"
                            )
                            .validation_status(
                                validation_status::INGREDIENT_CLAIM_SIGNATURE_VALIDATED,
                            )
                            .informational(validation_log);
                        } else {
                            log_item!(
                                c2pa_manifest.url(),
                                "ingredient claimSignature mismatch",
                                "ingredient_checks"
                            )
                            .validation_status(
                                validation_status::INGREDIENT_CLAIM_SIGNATURE_MISMATCH,
                            )
                            .failure(
                                validation_log,
                                Error::HashMismatch(
                                    "ingredient claimSignature mismatch".to_string(),
                                ),
                            )?;
                        }
                    }

                    if _sync {
                        Claim::verify_claim(
                            ingredient,
                            svi,
                            check_ingredient_trust,
                            &store.ctp,
                            validation_log,
                            context,
                        )?;
                    } else {
                        Claim::verify_claim_async(
                            ingredient,
                            svi,
                            check_ingredient_trust,
                            &store.ctp,
                            validation_log,
                            context,
                        )
                        .await?;
                    }

                    // recurse nested ingredients
                    if _sync {
                        Store::ingredient_checks(
                            store,
                            ingredient,
                            svi,
                            validation_log,
                            depth.saturating_add(1),
                            context,
                        )?;
                    } else {
                        Box::pin(Store::ingredient_checks_async(
                            store,
                            ingredient,
                            svi,
                            validation_log,
                            depth.saturating_add(1),
                            context,
                        ))
                        .await?;
                    }
                } else {
                    log_item!(label.clone(), "ingredient not found", "ingredient_checks")
                        .validation_status(validation_status::INGREDIENT_MANIFEST_MISSING)
                        .failure(
                            validation_log,
                            Error::ClaimVerification(format!("ingredient: {label} is missing")),
                        )?;
                }
            } else {
                let title = ingredient_assertion.title.unwrap_or("no title".into());
                let description = format!("{title}: ingredient does not have provenance");
                log_item!(
                    jumbf::labels::to_assertion_uri(claim.label(), &i.label()),
                    description,
                    "ingredient_checks"
                )
                .validation_status(validation_status::INGREDIENT_PROVENANCE_UNKNOWN)
                .informational(validation_log);
            }
            validation_log.pop_ingredient_uri();
        }

        Ok(())
    }

    fn get_store_validation_info<'a>(
        &'a self,
        claim: &'a Claim,
        asset_data: Option<&mut ClaimAssetData<'_>>,
        validation_log: &mut StatusTracker,
    ) -> Result<StoreValidationInfo<'a>> {
        let mut svi = StoreValidationInfo::default();
        Store::get_claim_referenced_manifests(claim, self, &mut svi, true, validation_log)?;

        // find the manifest with the hash binding
        svi.binding_claim = self.get_hash_binding_manifest(claim).ok_or_else(|| {
            log_item!(
                to_manifest_uri(claim.label()),
                "could not find manifest with hard binding",
                "get_store_validation_info"
            )
            .validation_status(validation_status::HARD_BINDINGS_MISSING)
            .failure_as_err(validation_log, Error::ClaimMissingHardBinding)
        })?;

        // save the update manifest label if it exists
        if claim.update_manifest() {
            svi.update_manifest_label = Some(claim.label().to_owned());
        }

        // get the manifest offset position
        if let Some(asset_data) = asset_data {
            let locations = match asset_data {
                #[cfg(feature = "file_io")]
                ClaimAssetData::Path(path) => {
                    let format =
                        get_supported_file_extension(path).ok_or(Error::UnsupportedType)?;
                    let mut reader = std::fs::File::open(path)?;

                    object_locations_from_stream(&format, &mut reader)
                }
                ClaimAssetData::Bytes(items, typ) => {
                    let format = typ.to_owned();
                    let mut reader = Cursor::new(items);

                    object_locations_from_stream(&format, &mut reader)
                }
                ClaimAssetData::Stream(reader, typ) => {
                    let format = typ.to_owned();
                    let positions = object_locations_from_stream(&format, reader);
                    reader.rewind()?;
                    positions
                }
                ClaimAssetData::StreamFragment(reader, _read1, typ) => {
                    let format = typ.to_owned();
                    object_locations_from_stream(&format, reader)
                }
                #[cfg(feature = "file_io")]
                ClaimAssetData::StreamFragments(reader, _path_bufs, typ) => {
                    let format = typ.to_owned();
                    object_locations_from_stream(&format, reader)
                }
            };

            if let Ok(locations) = locations {
                if let Some(manifest_loc) = locations
                    .iter()
                    .find(|o| o.htype == HashBlockObjectType::Cai)
                {
                    svi.manifest_store_range = Some(HashRange::new(
                        manifest_loc.offset as u64,
                        manifest_loc.length as u64,
                    ));
                }
            }
        }

        for found_claim in svi.manifest_map.values() {
            // get the timestamp assertions
            let timestamp_assertions = found_claim.timestamp_assertions();
            for ta in timestamp_assertions {
                let timestamp_assertion =
                    TimeStamp::from_assertion(ta.assertion()).map_err(|_e| {
                        log_item!(
                            ta.label(),
                            "could not parse timestamp assertion",
                            "get_claim_referenced_manifests"
                        )
                        .validation_status(validation_status::ASSERTION_TIMESTAMP_MALFORMED)
                        .failure_as_err(
                            validation_log,
                            Error::ValidationRule("timestamp assertion malformed".into()),
                        )
                    })?;

                // save the valid timestamps stored in the StoreValidationInfo
                // we only use valid timestamps, otherwise just ignore
                for (referenced_claim, time_stamp_token) in timestamp_assertion.as_ref() {
                    let mut tmp_log = StatusTracker::default();
                    if let Some(rc) = svi.manifest_map.get(referenced_claim) {
                        if let Ok(sign1) = rc.cose_sign1() {
                            if let Ok(tst_info) = verify_time_stamp(
                                time_stamp_token,
                                &sign1.signature,
                                &self.ctp,
                                &mut tmp_log,
                                // no trust checks for leagacy timestamps
                                rc.version() != 1,
                            ) {
                                svi.timestamps.insert(rc.label().to_owned(), tst_info);
                            }
                        }
                    }
                }
            }

            // get the certificate status assertions
            let certificate_status_assertions = found_claim.certificate_status_assertions();
            for csa in certificate_status_assertions {
                let certificate_status_assertion =
                    CertificateStatus::from_assertion(csa.assertion())?;

                // The certificate status assertion carries OCSP responses for
                // this manifest's own signing certificate, so bind each
                // response to that certificate's chain (RFC 6960 §4.1.1).
                let signing_cert_chain = found_claim
                    .cose_sign1()
                    .ok()
                    .and_then(|sign1| cert_chain_from_sign1(&sign1).ok())
                    .unwrap_or_default();

                // save the ocsp_ders stored in the StoreValidationInfo
                for ocsp_der in certificate_status_assertion.as_ref() {
                    if let Ok(response) = OcspResponse::from_der_checked(
                        ocsp_der,
                        &signing_cert_chain,
                        None,
                        validation_log,
                    ) {
                        let ocsp_ders = svi
                            .certificate_statuses
                            .entry(response.certificate_serial_num)
                            .or_insert(Vec::new());
                        ocsp_ders.push(response.ocsp_der);
                    }
                }
            }
        }

        Ok(svi)
    }

    /// Verify Store
    /// store: Store to validate
    /// xmp_str: String containing entire XMP block of the asset
    /// asset_bytes: bytes of the asset to be verified
    /// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned
    #[async_generic(async_signature(
        store: &Store,
        mut asset_data: Option<&mut ClaimAssetData<'_>>,
        validation_log: &mut StatusTracker,
        context: &Context,
    ))]
    pub fn verify_store(
        store: &Store,
        mut asset_data: Option<&mut ClaimAssetData<'_>>,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<()> {
        context.check_progress(ProgressPhase::VerifyingManifest, 1, 1)?;
        let claim = match store.provenance_claim() {
            Some(c) => c,
            None => {
                log_item!("Unknown", "could not find active manifest", "verify_store")
                    .validation_status(validation_status::CLAIM_MISSING)
                    .failure_no_throw(validation_log, Error::ProvenanceMissing);

                return Err(Error::ProvenanceMissing);
            }
        };

        // get info needed to complete validation
        let svi =
            store.get_store_validation_info(claim, asset_data.as_deref_mut(), validation_log)?;

        if _sync {
            // verify the provenance claim
            Claim::verify_claim(claim, &svi, true, &store.ctp, validation_log, context)?;

            Store::ingredient_checks(store, claim, &svi, validation_log, 0, context)?;
        } else {
            Claim::verify_claim_async(claim, &svi, true, &store.ctp, validation_log, context)
                .await?;

            Store::ingredient_checks_async(store, claim, &svi, validation_log, 0, context).await?;
        }

        // verify the asset hash binding once for the whole store, on the binding manifest
        if let Some(data) = asset_data {
            if let Some(binding_claim) = store.get_claim(&svi.binding_claim) {
                Claim::verify_hash_binding(binding_claim, data, &svi, validation_log, context)?;
            }
        }

        Ok(())
    }

    /// Verifies the store and returns [`Error::InvalidManifest`] if the validation state is
    /// invalid. Pass `asset_data` to also verify hash bindings against the asset.
    #[async_generic(async_signature(
        &mut self,
        asset_data: Option<&mut ClaimAssetData<'_>>,
        context: &Context,
    ))]
    pub(crate) fn verify_store_strict(
        &mut self,
        asset_data: Option<&mut ClaimAssetData<'_>>,
        context: &Context,
    ) -> Result<()> {
        let mut validation_log = StatusTracker::default();
        if _sync {
            Store::verify_store(self, asset_data, &mut validation_log, context)?;
        } else {
            Store::verify_store_async(self, asset_data, &mut validation_log, context).await?;
        }

        let validation_results = ValidationResults::from_store(self, &validation_log);
        if validation_results.validation_state() == ValidationState::Invalid {
            return Err(Error::InvalidManifest(validation_results));
        }

        Ok(())
    }

    // generate a list of AssetHashes based on the location of objects in the stream
    fn generate_data_hashes_for_stream<R, F>(
        stream: &mut R,
        alg: &str,
        block_locations: &mut Vec<HashObjectPositions>,
        calc_hashes: bool,
        progress: &mut F,
    ) -> Result<Vec<DataHash>>
    where
        R: Read + Seek + ?Sized,
        F: FnMut(u32, u32) -> Result<()>,
    {
        let stream_len = stream_len(stream)?;
        stream.rewind()?;

        let mut hashes: Vec<DataHash> = Vec::new();

        // Create a DataHash regardless of whether JUMBF is found or not...
        // For remote manifests with no embedded JUMBF, creates a hash with no exclusions,
        // because there is nothing to exclude from the hashing (since nothing is embedded)
        let mut dh = DataHash::new("jumbf manifest", alg);

        // sort blocks by offset
        block_locations.sort_by_key(|a| a.offset);

        // generate default data hash that excludes jumbf block
        // find the first jumbf block (ours are always in order)
        // find the first block after the jumbf blocks
        let mut block_start: usize = 0;
        let mut block_end: usize = 0;
        let mut found_jumbf = false;
        for item in block_locations {
            // find start of jumbf
            if !found_jumbf && item.htype == HashBlockObjectType::Cai {
                block_start = item.offset;
                found_jumbf = true;
            }

            // find start of block after jumbf blocks
            if found_jumbf && item.htype == HashBlockObjectType::Cai {
                block_end = item.offset + item.length;
            }

            // add explict exclusion ranges
            if item.htype == HashBlockObjectType::OtherExclusion {
                dh.add_exclusion(HashRange::new(item.offset as u64, item.length as u64));
            }
        }

        if found_jumbf {
            // add exclusion for embedded jumbf
            if calc_hashes {
                if block_end > block_start && (block_end as u64) <= stream_len {
                    dh.add_exclusion(HashRange::new(
                        block_start as u64,
                        (block_end - block_start) as u64,
                    ));
                }

                // this check is only valid on the final sized asset
                //
                // a case may occur where there is no existing manifest in the stream and the
                // asset handler creates a placeholder beyond the length of the stream
                if block_end as u64 > stream_len + (block_end - block_start) as u64 {
                    return Err(Error::BadParam(
                        "data hash exclusions out of range".to_string(),
                    ));
                }
            } else if block_end > block_start {
                dh.add_exclusion(HashRange::new(
                    block_start as u64,
                    (block_end - block_start) as u64,
                ));
            }
        }

        // Generate or set placeholder hash
        if calc_hashes {
            // Second signing pass: calcultate the actual real hash
            dh.gen_hash_from_stream_with_progress(stream, progress)?;
        } else {
            // First signing pass: zero-filled placeholder hash (to get to end size)
            match alg {
                "sha256" => dh.set_hash([0u8; 32].to_vec()),
                "sha384" => dh.set_hash([0u8; 48].to_vec()),
                "sha512" => dh.set_hash([0u8; 64].to_vec()),
                _ => return Err(Error::UnsupportedType),
            }
        }

        hashes.push(dh);

        Ok(hashes)
    }

    fn generate_bmff_data_hash_for_stream(alg: &str) -> Result<BmffHash> {
        // The spec has mandatory BMFF exclusion ranges for certain atoms.
        // The function makes sure those are included.

        let mut dh = BmffHash::new("jumbf manifest", alg, None);
        dh.set_default_exclusions();

        // fill in temporary hash
        match alg {
            "sha256" => dh.set_hash([0u8; 32].to_vec()),
            "sha384" => dh.set_hash([0u8; 48].to_vec()),
            "sha512" => dh.set_hash([0u8; 64].to_vec()),
            _ => return Err(Error::UnsupportedType),
        }

        Ok(dh)
    }

    // This function generates the BMFF hash for the 'mdat' boxes. This is used
    // in the case where the SDK is automatically generating the Merkle tree leaves.
    // If the user is supplying their own BmffHash they can specify the Merkle
    // tree leaves themselves and this function will not be called.
    fn generate_bmff_mdat_hashes(
        asset_stream: &mut dyn CAIRead,
        bmff_hash: &mut BmffHash,
        settings: &Settings,
    ) -> Result<()> {
        if let Some(merkle_chunk_size) = settings.core.merkle_tree_chunk_size_in_kb {
            bmff_hash.add_merkle_map_for_mdats(
                asset_stream,
                merkle_chunk_size,
                settings.core.merkle_tree_max_proofs,
            )?;
        }
        Ok(())
    }

    /// This function is used to pre-generate a manifest with place holders for the final
    /// DataHash and Manifest Signature.  The DataHash will reserve space for at least 10
    /// Exclusion ranges.  The Signature box reserved size is based on the size required by
    /// the Signer you plan to use.  This function is not needed when using Box Hash. This function is used
    /// in conjunction with `get_data_hashed_embeddable_manifest`.  The manifest returned
    /// from `get_data_hashed_embeddable_manifest` will have a size that matches this function.
    /// Note: This function does not support dynamic assertions. Use `get_placeholder`
    /// if you need dynamic assertion support.
    pub fn get_data_hashed_manifest_placeholder(
        &mut self,
        reserve_size: usize,
        format: &str,
    ) -> Result<Vec<u8>> {
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

        // if user did not supply a hash
        if pc.hash_assertions().is_empty() {
            // create placeholder DataHash large enough for 10 Exclusions
            let mut ph = DataHash::new("jumbf manifest", pc.alg());
            for _ in 0..10 {
                ph.add_exclusion(HashRange::new(0u64, 2u64));
            }
            let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
            let mut stream = Cursor::new(data);
            ph.gen_hash_from_stream(&mut stream)?;

            pc.add_assertion(&ph)?;
        }

        let jumbf_bytes = self.to_jumbf_internal(reserve_size)?;

        let composed = Self::get_composed_manifest(&jumbf_bytes, format)?;

        Ok(composed)
    }

    /// This function is used to get a placeholder manifest with dynamic assertion support.
    /// The placeholder is then injected into the asset before calculating hashes.
    /// Unlike [`data_hashed_placeholder`], this function supports dynamic assertions
    /// (e.g., CAWG identity assertions) by accepting a signer.
    ///
    /// # Arguments
    /// * `context` - The context to use.
    /// # Returns
    /// * The bytes of the `c2pa_manifest` placeholder.
    /// # Errors
    /// * Returns an [`Error`] if the placeholder cannot be created.
    pub fn get_placeholder(&mut self, _format: &str, context: &Context) -> Result<Vec<u8>> {
        let signer = context.signer()?;
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

        // if user did not supply a hash
        if pc.hash_assertions().is_empty() {
            return Err(Error::BadParam(
                "Claim must have a hard binding assertion".to_string(),
            ));
        };

        // add dynamic assertions to the store
        let dynamic_assertions = signer.dynamic_assertions();
        let _da_uris = self.add_dynamic_assertion_placeholders(&dynamic_assertions)?;

        self.to_jumbf_internal(signer.reserve_size())
    }

    /// Signs an already hashed manifest with dynamic assertion support.
    ///
    /// # Arguments
    /// * `signer` - The signer to use.
    /// * `settings` - The settings to use.
    /// # Returns
    /// * The signed manifest bytes.
    /// # Errors
    /// * Returns an [`Error`] if the placeholder cannot be signed.
    pub fn sign_manifest(&mut self, signer: &dyn Signer, context: &Context) -> Result<Vec<u8>> {
        let settings = context.settings();
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;

        // if user did not supply a hash
        if pc.hash_assertions().is_empty() {
            return Err(Error::BadParam(
                "Claim must have a valid hard binding assertion".to_string(),
            ));
        };

        // Write dynamic assertions only if placeholders were added during placeholder generation.
        // We check if the dynamic assertion labels exist in the claim - if not, placeholders
        // weren't added and we should skip writing to avoid size mismatches.
        let dynamic_assertions = signer.dynamic_assertions();
        if !dynamic_assertions.is_empty() {
            // Check if placeholders exist for these dynamic assertions
            let has_placeholders = {
                dynamic_assertions
                    .iter()
                    .all(|da| pc.assertion_hashed_uri_from_label(&da.label()).is_some())
            };

            if has_placeholders {
                let mut preliminary_claim = PartialClaim::default();
                {
                    for assertion in pc.assertions() {
                        preliminary_claim.add_assertion(assertion);
                    }
                }

                // Drop pc before calling write_dynamic_assertions
                let _ = pc;

                let _modified =
                    self.write_dynamic_assertions(&dynamic_assertions, &mut preliminary_claim)?;

                // Get pc again
                let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
                let sig = self.sign_claim(pc, signer, signer.reserve_size(), settings)?;

                let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
                pc.set_signature_val(sig);

                let jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;

                if context.settings().verify.verify_after_sign {
                    self.verify_store_strict(None, context)?;
                }

                return Ok(jumbf_bytes);
            }
        }

        context.check_progress(ProgressPhase::Signing, 1, 1)?;

        // No dynamic assertions - sign directly
        let sig = self.sign_claim(pc, signer, signer.reserve_size(), settings)?;
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.set_signature_val(sig);

        let jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;

        if context.settings().verify.verify_after_sign {
            self.verify_store_strict(None, context)?;
        }

        Ok(jumbf_bytes)
    }

    fn prep_embeddable_store(
        &mut self,
        dh: &DataHash,
        asset_reader: Option<&mut dyn CAIRead>,
        context: &Context,
    ) -> Result<()> {
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

        // make sure there are data hashes present before generating
        if pc.hash_assertions().is_empty() {
            return Err(Error::BadParam(
                "Claim must have hash binding assertion".to_string(),
            ));
        }

        // don't allow BMFF assertions to be present
        if !pc.bmff_hash_assertions().is_empty() {
            return Err(Error::BadParam(
                "BMFF assertions not supported in embeddable manifests".to_string(),
            ));
        }

        let mut adjusted_dh = DataHash::new("jumbf manifest", pc.alg());
        adjusted_dh.exclusions.clone_from(&dh.exclusions);
        adjusted_dh.hash.clone_from(&dh.hash);

        if let Some(reader) = asset_reader {
            // calc hashes
            let mut cb = |step, total| context.check_progress(ProgressPhase::Hashing, step, total);
            adjusted_dh.gen_hash_from_stream_with_progress(reader, &mut cb)?;
        }

        // update the placeholder hash
        pc.update_data_hash(adjusted_dh)?;

        Ok(())
    }

    fn finish_embeddable_store(&mut self, jumbf_bytes: &[u8], format: &str) -> Result<Vec<u8>> {
        Self::get_composed_manifest(jumbf_bytes, format)
    }

    /// Returns a finalized, signed manifest.  The manifest are only supported
    /// for cases when the client has provided a data hash content hash binding.  Note,
    /// this function will not work for cases like BMFF where the position
    /// of the content is also encoded.  This function is not compatible with
    /// BMFF hash binding.  If a BMFF data hash or box hash is detected that is
    /// an error.  The DataHash placeholder assertion will be  adjusted to the contain
    /// the correct values.  If the asset_reader value is supplied it will also perform
    /// the hash calculations, otherwise the function uses the caller supplied values.
    /// It is an error if `get_data_hashed_manifest_placeholder` was not called first
    /// as this call inserts the DataHash placeholder assertion to reserve space for the
    /// actual hash values not required when using BoxHashes.
    #[async_generic(async_signature(
        &mut self,
        dh: &DataHash,
        signer: &dyn AsyncSigner,
        format: &str,
        asset_reader: Option<&mut dyn CAIRead>,
        context: &Context,
    ))]
    pub fn get_data_hashed_embeddable_manifest(
        &mut self,
        dh: &DataHash,
        signer: &dyn Signer,
        format: &str,
        asset_reader: Option<&mut dyn CAIRead>,
        context: &Context,
    ) -> Result<Vec<u8>> {
        self.prep_embeddable_store(dh, asset_reader, context)?;

        // Write dynamic assertions only if placeholders were added during placeholder generation.
        // We check if the dynamic assertion labels exist in the claim - if not, placeholders
        // weren't added and we should skip writing to avoid size mismatches.
        let dynamic_assertions = signer.dynamic_assertions();
        if !dynamic_assertions.is_empty() {
            // Check if placeholders exist for these dynamic assertions
            let has_placeholders = {
                let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
                dynamic_assertions
                    .iter()
                    .all(|da| pc.assertion_hashed_uri_from_label(&da.label()).is_some())
            };

            if has_placeholders {
                let mut preliminary_claim = PartialClaim::default();
                {
                    let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
                    for assertion in pc.assertions() {
                        preliminary_claim.add_assertion(assertion);
                    }
                }
                if _sync {
                    self.write_dynamic_assertions(&dynamic_assertions, &mut preliminary_claim)?;
                } else {
                    self.write_dynamic_assertions_async(
                        &dynamic_assertions,
                        &mut preliminary_claim,
                    )
                    .await?;
                }
            }
        }

        context.check_progress(ProgressPhase::Signing, 1, 1)?;

        // sign contents
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
        let sig = if _sync {
            self.sign_claim(pc, signer, signer.reserve_size(), context.settings())?
        } else {
            self.sign_claim_async(pc, signer, signer.reserve_size(), context.settings())
                .await?
        };

        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.set_signature_val(sig);

        let jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;

        if context.settings().verify.verify_after_sign {
            if _sync {
                self.verify_store_strict(None, context)?;
            } else {
                self.verify_store_strict_async(None, context).await?;
            }
        }

        self.finish_embeddable_store(&jumbf_bytes, format)
    }

    /// Returns a finalized, signed manifest.  The client is required to have
    /// included the necessary box hash assertion with the pregenerated hashes.
    #[async_generic(async_signature(
        &mut self,
        signer: &dyn AsyncSigner,
        context: &Context,
    ))]
    pub fn get_box_hashed_embeddable_manifest(
        &mut self,
        signer: &dyn Signer,
        context: &Context,
    ) -> Result<Vec<u8>> {
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;

        // make sure there is only one
        if pc.hash_assertions().len() != 1 {
            return Err(Error::BadParam(
                "Claim must have exactly one hash binding assertion".to_string(),
            ));
        }

        // only allow box hash assertions to be present
        if pc.box_hash_assertions().is_empty() {
            return Err(Error::BadParam("Missing box hash assertion".to_string()));
        }

        context.check_progress(ProgressPhase::Signing, 1, 1)?;

        // sign contents
        let sig = if _sync {
            self.sign_claim(pc, signer, signer.reserve_size(), context.settings())?
        } else {
            self.sign_claim_async(pc, signer, signer.reserve_size(), context.settings())
                .await?
        };

        // save the signature back to the provenance claim so it gets included in the manifest
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.set_signature_val(sig);

        self.to_jumbf_internal(signer.reserve_size())
    }

    /// Returns the supplied manifest composed to be directly compatible with the desired format.
    /// For example, if format is JPEG function will return the set of APP11 segments that contains
    /// the manifest.  Similarly for PNG it would be the PNG chunk complete with header and  CRC.
    pub fn get_composed_manifest(manifest_bytes: &[u8], format: &str) -> Result<Vec<u8>> {
        if let Some(h) = get_assetio_handler(format) {
            if let Some(composed_data_handler) = h.composed_data_ref() {
                return composed_data_handler.compose_manifest(manifest_bytes, format);
            }
        }
        Err(Error::UnsupportedType)
    }

    /// Inserts placeholders for dynamic assertions to be updated later.
    #[async_generic(async_signature(
        &mut self,
        dyn_assertions: &[Box<dyn AsyncDynamicAssertion>],
    ))]
    pub(crate) fn add_dynamic_assertion_placeholders(
        &mut self,
        dyn_assertions: &[Box<dyn DynamicAssertion>],
    ) -> Result<Vec<HashedUri>> {
        if dyn_assertions.is_empty() {
            return Ok(Vec::new());
        }

        // Two passes since we are accessing two fields in self.
        let mut assertions = Vec::new();
        for da in dyn_assertions.iter() {
            let reserve_size = da.reserve_size()?;
            let data1 = c2pa_cbor::ser::to_vec_packed(&vec![0; reserve_size])?;
            let cbor_delta = data1.len() - reserve_size;
            let da_data = c2pa_cbor::ser::to_vec_packed(&vec![0; reserve_size - cbor_delta])?;
            assertions.push(UserCbor::new(&da.label(), da_data));
        }

        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        // add dynamic assertions (respects created_assertion_labels settings)
        assertions.iter().map(|a| pc.add_assertion(a)).collect()
    }

    /// Write the dynamic assertions to the manifest.
    /// Note: This assumes each dynamic assertion label is unique (no instance suffixes).
    /// Multiple dynamic assertions with different labels are supported.
    #[async_generic(async_signature(
        &mut self,
        dyn_assertions: &[Box<dyn AsyncDynamicAssertion>],
        preliminary_claim: &mut PartialClaim,
    ))]
    #[allow(unused_variables)]
    fn write_dynamic_assertions(
        &mut self,
        dyn_assertions: &[Box<dyn DynamicAssertion>],
        preliminary_claim: &mut PartialClaim,
    ) -> Result<bool> {
        if dyn_assertions.is_empty() {
            return Ok(false);
        }

        let mut final_assertions = Vec::new();

        for da in dyn_assertions.iter() {
            // Use the dynamic assertion's label directly.
            // This assumes each dynamic assertion label is unique (no instance suffixes needed).
            let label = da.label();

            let da_size = da.reserve_size()?;
            let da_data = if _sync {
                da.content(&label, Some(da_size), preliminary_claim)?
            } else {
                da.content(&label, Some(da_size), preliminary_claim).await?
            };

            match da_data {
                DynamicAssertionContent::Cbor(data) => {
                    final_assertions.push(UserCbor::new(&label, data).to_assertion()?);
                }
                DynamicAssertionContent::Json(data) => {
                    final_assertions.push(User::new(&label, &data).to_assertion()?);
                }
                DynamicAssertionContent::Binary(format, data) => {
                    //final_assertions.push(EmbeddedData::to_binary_assertion(&EmbeddedData::new(&label, format, data))?);
                }
            }
        }

        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        for assertion in final_assertions {
            pc.replace_assertion(assertion)?;
        }

        // clear the provenance claim data since the contents are now different
        pc.clear_data();

        Ok(true)
    }

    #[cfg(feature = "file_io")]
    fn add_merkmap_for_rendition(
        &mut self,
        fragments: &[std::path::PathBuf],
        local_id: usize,
        unique_id: usize,
        output_dir: &Path,
        settings: &Settings,
    ) -> Result<()> {
        if fragments.is_empty() {
            return Err(Error::BadParam(
                "at least one fragment path must be provided".to_string(),
            ));
        }

        // get the provenance claim changing mutability
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.clear_data(); // clear since we are reusing an existing claim

        // get the BMFF hash assertion if it exists or create a new one if not
        let mut bmff_hash = if let Some(bmff_hash_assertion) = pc.bmff_hash_assertions().first() {
            BmffHash::from_assertion(bmff_hash_assertion.assertion())?
        } else {
            let mut bmff_hash = BmffHash::new("jumbf manifest", pc.alg(), None);

            bmff_hash.set_default_exclusions();

            if pc.version() < 2 {
                bmff_hash.set_bmff_version(2); // backcompat support
            }

            pc.add_assertion(&bmff_hash)?;
            bmff_hash
        };
        bmff_hash.clear_hash(); // hash is not used when using fragmented Merkle tree approach

        // generate fragments and produce Merkle tree
        bmff_hash.add_merkle_for_fragmented(
            settings.core.merkle_tree_max_proofs,
            pc.alg(),
            fragments,
            output_dir,
            local_id,
            unique_id,
        )?;

        // update the BMFF hash assertion with the new Merkle tree information
        pc.update_bmff_hash(bmff_hash)?;

        /*
        // add in the BMFF assertion
        pc.add_assertion(&bmff_hash)?;

        // 3) Generate in memory CAI jumbf block
        // and write preliminary jumbf store to file
        // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
        data = self.to_jumbf_internal(reserve_size)?;
        let jumbf_size = data.len();
        save_jumbf_to_file(&data, &dest_path, Some(&dest_path))?;

        // generate actual hash values
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability

        let bmff_hashes = pc.bmff_hash_assertions();

        if !bmff_hashes.is_empty() {
            let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0].assertion())?;
            bmff_hash.update_fragmented_inithash(&dest_path)?;
            pc.update_bmff_hash(bmff_hash)?;
        }

        // regenerate the jumbf because the cbor changed
        data = self.to_jumbf_internal(reserve_size)?;
        if jumbf_size != data.len() {
            return Err(Error::JumbfCreationError);
        }
        */

        Ok(())
    }

    /// Embed the claims store as jumbf into fragmented assets.
    #[cfg(feature = "file_io")]
    pub fn save_to_bmff_fragmented<P: AsRef<Path>>(
        &mut self,
        init_paths: &[PathBuf],
        fragment_glob: P,
        output_path: P,
        signer: &dyn Signer,
        context: &Context,
    ) -> Result<()> {
        if init_paths.is_empty() {
            return Err(Error::BadParam(
                "at least one init segment path must be provided".to_string(),
            ));
        }

        let mut output_map = HashMap::new();

        // make sure output path is not a file
        if output_path.as_ref().is_file() {
            return Err(crate::Error::BadParam(
                "output_path must be a folder".to_string(),
            ));
        }

        // mak sure we can make the output folder
        if !output_path.as_ref().exists() {
            // ensure the path exists
            std::fs::create_dir_all(output_path.as_ref()).map_err(|e| {
                Error::BadParam(format!(
                    "failed to create output directory for fragments: {e}"
                ))
            })?;
        }

        // add dynamic assertions placeholders to the store
        let dynamic_assertions = signer.dynamic_assertions();
        if !dynamic_assertions.is_empty() {
            self.add_dynamic_assertion_placeholders(&dynamic_assertions)?;
        }

        // add a Merkle tree map for each init segment and its associated fragments
        for (i, init_path) in init_paths.iter().enumerate() {
            // make sure it is a supported BMFF format
            match get_supported_file_extension(init_path.as_ref()) {
                Some(ext) => {
                    if !is_bmff_format(&ext) {
                        return Err(Error::UnsupportedType);
                    }
                }
                None => return Err(Error::UnsupportedType),
            }

            // build the list of fragments for this init segment based on the glob pattern
            let mut fragments = Vec::new();
            let init_dir = init_path
                .parent()
                .ok_or(Error::BadParam(
                    "failed to get parent directory for init segment".to_string(),
                ))?
                .to_path_buf();
            let frag_glob = init_dir.join(fragment_glob.as_ref());
            let frag_glob_str = frag_glob
                .to_str()
                .ok_or(Error::BadParam("glob pattern is not valid".to_string()))?; // segment match pattern

            // grab the fragments that go with this init segment
            for entry in glob::glob(frag_glob_str)
                .map_err(|e| Error::BadParam(format!("glob pattern is not valid: {e}")))?
            {
                match entry {
                    Ok(path) => fragments.push(path),
                    Err(e) => {
                        return Err(Error::BadParam(format!(
                            "error processing glob pattern: {e}"
                        )))
                    }
                }
            }

            let new_output_path = output_path.as_ref().join(
                init_dir
                    .file_name()
                    .ok_or(Error::BadParam("init segment bad file name".to_string()))?,
            );

            // add the Merkle tree map for this rendition
            // creating fragments in the output location
            let unique_id = i + 1;
            let local_id = i + 1;
            self.add_merkmap_for_rendition(
                &fragments,
                local_id, // local id for this rendition (same as unique since we are only doing one rendition per claim for now)
                unique_id, // unique id for this rendition
                &new_output_path,
                context.settings(),
            )?;

            output_map.insert(init_path.to_owned(), (unique_id, local_id));
        }

        // now save the manifest to each output init segment (the manifest is the same for each segment per the spec to allow related rendtions to be validated as a set)
        let mut output_files = Vec::new();
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability
        let bmff_hashes = pc.bmff_hash_assertions();
        let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0].assertion())?;
        let unsigned_jumbf = self.to_jumbf_internal(signer.reserve_size())?;
        for (init_path, (unique_id, local_id)) in output_map.iter() {
            let init_name = PathBuf::from(init_path.file_name().ok_or(Error::BadParam(
                "failed to get file name for init segment".to_string(),
            ))?);
            let init_dir = PathBuf::from(
                init_path
                    .parent()
                    .ok_or(Error::BadParam(
                        "failed to get parent directory for init segment".to_string(),
                    ))?
                    .file_name()
                    .ok_or(Error::BadParam(
                        "failed to get file name for init segment".to_string(),
                    ))?,
            );

            let output_file = output_path.as_ref().join(init_dir).join(init_name);

            // add manifest a placeholder that will be replaced with the final manifest after signing,
            // but we need to add it now to properly calculate the BMFF hash for each init segment
            save_jumbf_to_file(&unsigned_jumbf, init_path, Some(&output_file))?;

            // update the initHash for each init segment
            bmff_hash.update_fragmented_inithash(*unique_id, *local_id, &output_file)?;

            output_files.push(output_file);
        }

        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability

        // update the BMFF hash in the final BMFFHash assertion containing the unique initHash values for each rendition
        pc.update_bmff_hash(bmff_hash)?;

        // Write dynamic assertions only if placeholders were added during placeholder generation.
        // We check if the dynamic assertion labels exist in the claim - if not, placeholders
        // weren't added and we should skip writing to avoid size mismatches.
        let dynamic_assertions = signer.dynamic_assertions();
        if !dynamic_assertions.is_empty() {
            // Check if placeholders exist for these dynamic assertions
            let has_placeholders = {
                let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
                dynamic_assertions
                    .iter()
                    .all(|da| pc.assertion_hashed_uri_from_label(&da.label()).is_some())
            };

            if has_placeholders {
                let mut preliminary_claim = PartialClaim::default();
                {
                    let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
                    for assertion in pc.assertions() {
                        preliminary_claim.add_assertion(assertion);
                    }
                }

                self.write_dynamic_assertions(&dynamic_assertions, &mut preliminary_claim)?;
            }
        }

        // sign the claim
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
        let sig = self.sign_claim(pc, signer, signer.reserve_size(), context.settings())?;

        // update the provenance claim with the signature so it gets saved in the manifest
        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.set_signature_val(sig);

        // regenerate the JUMBF with the signature
        let final_jumbf = self.to_jumbf_internal(signer.reserve_size())?;
        if final_jumbf.len() != unsigned_jumbf.len() {
            return Err(Error::JumbfCreationError);
        }

        // write the signed manifest to each output init segment
        for output_file in output_files.iter() {
            save_jumbf_to_file(&final_jumbf, output_file, Some(output_file))?;
        }

        Ok(())
    }

    /// Embed the claims store as JUMBF into a stream. Updates XMP with provenance
    /// record.
    ///
    /// When called, the stream should contain an asset matching `format`.
    /// On return, the stream will contain the new manifest signed with `signer`.
    ///
    /// This directly modifies the asset in stream. Back up the stream first if
    /// you need to preserve it.
    ///
    /// This can also handle remote signing if `direct_cose_handling()` is `true`.
    #[allow(unused_variables)]
    #[async_generic(async_signature(
        &mut self,
        format: &str,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
        signer: &dyn AsyncSigner,
        context: &Context,
    ))]
    pub(crate) fn save_to_stream(
        &mut self,
        format: &str,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
        signer: &dyn Signer,
        context: &Context,
    ) -> Result<Vec<u8>> {
        let settings = context.settings();
        let dynamic_assertions = signer.dynamic_assertions();

        // Add dynamic assertion placeholders (URIs no longer needed, we use da.label() directly)
        if _sync {
            self.add_dynamic_assertion_placeholders(&dynamic_assertions)?;
        } else {
            self.add_dynamic_assertion_placeholders_async(&dynamic_assertions)
                .await?;
        }

        let threshold = settings.core.backing_store_memory_threshold_in_mb;

        let input_len = io_utils::stream_len(input_stream)?;
        let mut intermediate_stream = io_utils::stream_with_fs_fallback(threshold, input_len)?;

        #[allow(unused_mut)] // Not mutable in the non-async case.
        self.start_save_stream(
            format,
            input_stream,
            &mut intermediate_stream,
            signer.reserve_size(),
            context,
        )?;
        intermediate_stream.rewind()?;

        let mut preliminary_claim = PartialClaim::default();
        {
            let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
            for assertion in pc.assertions() {
                preliminary_claim.add_assertion(assertion);
            }
        }

        // Now add the dynamic assertions and update the JUMBF.
        let modified = if _sync {
            self.write_dynamic_assertions(&dynamic_assertions, &mut preliminary_claim)
        } else {
            self.write_dynamic_assertions_async(&dynamic_assertions, &mut preliminary_claim)
                .await
        }?;

        context.check_progress(ProgressPhase::Signing, 1, 1)?;

        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
        let sig = if _sync {
            self.sign_claim(pc, signer, signer.reserve_size(), settings)
        } else {
            self.sign_claim_async(pc, signer, signer.reserve_size(), settings)
                .await
        }?;

        // update the signature
        let pc: &mut Claim = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
        pc.set_signature_val(sig.clone());

        // update the JUMBF with the signature
        let jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?;

        output_stream.rewind()?;
        match self.finish_save_stream(jumbf_bytes, format, &mut intermediate_stream, output_stream)
        {
            Ok((s, m)) => {
                output_stream.flush()?;
                output_stream.rewind()?;

                context.check_progress(ProgressPhase::Embedding, 1, 1)?;

                if context.settings().verify.verify_after_sign {
                    let output_len = stream_len(output_stream)?;
                    let validate_hash = context.settings().verify.verify_after_sign_hash;
                    let mut asset_data = if output_len > 0 && validate_hash {
                        Some(ClaimAssetData::Stream(output_stream, format))
                    } else {
                        None
                    };
                    if _sync {
                        self.verify_store_strict(asset_data.as_mut(), context)?;
                    } else {
                        self.verify_store_strict_async(asset_data.as_mut(), context)
                            .await?;
                    }
                }
                Ok(m)
            }
            Err(e) => Err(e),
        }
    }

    /// Start the save process for a stream, this will prepare the intermediate stream
    /// and return the JUMBF data that will be used to embed the manifest.
    fn start_save_stream(
        &mut self,
        format: &str,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
        reserve_size: usize,
        context: &Context,
    ) -> Result<Vec<u8>> {
        let settings = context.settings();
        let threshold = settings.core.backing_store_memory_threshold_in_mb;

        let input_len = io_utils::stream_len(input_stream)?;
        let mut intermediate_stream = io_utils::stream_with_fs_fallback(threshold, input_len)?;

        let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

        // Add remote reference XMP if needed and strip out existing manifest
        // We don't need to strip manifests if we are replacing an existing one
        let (url, remove_manifests) = match pc.remote_manifest() {
            RemoteManifest::NoRemote => (None, false),
            RemoteManifest::SideCar => (None, true),
            RemoteManifest::Remote(url) => (Some(url), true),
            RemoteManifest::EmbedWithRemote(url) => (Some(url), false),
        };

        let io_handler = get_assetio_handler(format);

        let is_bmff = is_bmff_format(format);
        // fast_path applies to all formats: when there is no XMP embed and no manifest removal,
        // we can pass input_stream directly to the write/hash steps, skipping one full-file copy.
        let fast_path = url.is_none() && !remove_manifests;

        context.check_progress(ProgressPhase::Writing, 1, 2)?;

        if let Some(io_handler) = &io_handler {
            // Do not assume the handler supports XMP or removing manifests unless we need it to
            if let Some(url) = url {
                let external_ref_writer = io_handler
                    .remote_ref_writer_ref()
                    .ok_or(Error::XmpNotSupported)?;

                if remove_manifests {
                    let manifest_writer = io_handler
                        .get_writer(format)
                        .ok_or(Error::UnsupportedType)?;

                    let mut tmp_stream = io_utils::stream_with_fs_fallback(threshold, input_len)?;
                    manifest_writer.remove_cai_store_from_stream(input_stream, &mut tmp_stream)?;

                    // add external ref if possible
                    tmp_stream.rewind()?;
                    external_ref_writer.embed_reference_to_stream(
                        &mut tmp_stream,
                        &mut intermediate_stream,
                        RemoteRefEmbedType::Xmp(url),
                    )?;
                } else {
                    // add external ref if possible
                    external_ref_writer.embed_reference_to_stream(
                        input_stream,
                        &mut intermediate_stream,
                        RemoteRefEmbedType::Xmp(url),
                    )?;
                }
            } else if remove_manifests {
                let manifest_writer = io_handler
                    .get_writer(format)
                    .ok_or(Error::UnsupportedType)?;

                manifest_writer
                    .remove_cai_store_from_stream(input_stream, &mut intermediate_stream)?;
            } else if !fast_path {
                // XMP or manifest-removal was NOT the trigger — but fast_path is false,
                // which can only happen if remove_manifests is true (already handled above).
                // This branch is a safety net; in practice it should be unreachable here.
                input_stream.rewind()?;
                std::io::copy(input_stream, &mut intermediate_stream)?;
            }
        } else {
            // No format handler — only sidecar mode (no_embed=true, no remote URL) is allowed.
            if url.is_some() {
                return Err(Error::XmpNotSupported);
            }
            if !remove_manifests {
                // The caller did not set no_embed=true, so they expected JUMBF to be embedded
                // in the output stream, which is impossible without a format handler.
                return Err(Error::UnsupportedType);
            }
            // Sidecar: output is a verbatim copy of the input; JUMBF is returned as sidecar data
            input_stream.rewind()?;
            std::io::copy(input_stream, &mut intermediate_stream)?;
        }

        // `source_is_intermediate` tracks whether intermediate_stream (true) or
        // input_stream (false) should be used as the asset source for this call.
        // When fast_path is true we skip the intermediate copy for ALL formats, not
        // just BMFF, saving one full-file read+write pass.
        let mut source_is_intermediate = !fast_path;

        let mut data;
        let jumbf_size;

        // Check to see if manifest compression is requested, BMFF is not supported for compression since the manifest
        // needs to be in a specific location and compression would change the size of the manifest which
        // would break the offsets
        if pc.compressed() {
            // If compression is desired use BoxHashing for compatibile formats, otherwise fall back to regular hashing.
            match io_handler.and_then(|h| h.asset_box_hash_ref()) {
                Some(box_hash_handler) if !is_bmff => {
                    // if the user already has a box hash assertion we use that and ignore the compression setting
                    if pc.box_hash_assertions().is_empty() {
                        // no user box hash assertion, so use box hashing
                        let mut bh = BoxHash { boxes: Vec::new() };

                        if !source_is_intermediate {
                            // Box-hash generation requires a seekable intermediate; populate it
                            // from input_stream so generate_box_hash_from_stream_with_progress
                            // can make multiple passes over the data.
                            input_stream.rewind()?;
                            std::io::copy(input_stream, &mut intermediate_stream)?;
                            source_is_intermediate = true;
                        }

                        let mut cb = |step, total| {
                            context.check_progress(ProgressPhase::Hashing, step, total)
                        };
                        bh.generate_box_hash_from_stream_with_progress(
                            &mut intermediate_stream,
                            pc.alg(),
                            box_hash_handler,
                            false,
                            &mut cb,
                        )?;

                        // add the box hash assertion to the claim
                        pc.add_assertion(&bh)?;

                        intermediate_stream.rewind()?;
                    } else {
                        pc.set_compressed_manifest(false);
                    }
                }
                _ => {
                    // already have a box hash assertion so no compression will be performed
                    pc.set_compressed_manifest(false);
                }
            }
        }

        if is_bmff {
            // 2) Get hash ranges if needed, do not generate for update manifests
            let mut needs_hash = false;
            if !pc.update_manifest() && pc.bmff_hash_assertions().is_empty() {
                if source_is_intermediate {
                    intermediate_stream.rewind()?;
                } else {
                    input_stream.rewind()?;
                }
                let mut bmff_hash = Store::generate_bmff_data_hash_for_stream(pc.alg())?;

                if pc.version() < 2 {
                    bmff_hash.set_bmff_version(2); // backcompat support
                }

                // add Merkle mdats if requested
                if source_is_intermediate {
                    Store::generate_bmff_mdat_hashes(
                        &mut intermediate_stream,
                        &mut bmff_hash,
                        settings,
                    )?;
                } else {
                    Store::generate_bmff_mdat_hashes(input_stream, &mut bmff_hash, settings)?;
                }

                // insert Merkle UUID boxes at the correct location if required
                if let Some(merkle_uuid_boxes) = &bmff_hash.merkle_uuid_boxes {
                    let mut temp_stream = io_utils::stream_with_fs_fallback(threshold, input_len)?;

                    if !source_is_intermediate {
                        // Merkle insertion requires a writable source; populate intermediate
                        // from input_stream so we can call insert_data_at on it.
                        input_stream.rewind()?;
                        std::io::copy(input_stream, &mut intermediate_stream)?;
                        source_is_intermediate = true;
                    }

                    intermediate_stream.rewind()?;
                    insert_data_at(
                        &mut intermediate_stream,
                        &mut temp_stream,
                        bmff_hash.merkle_uuid_boxes_insertion_point,
                        merkle_uuid_boxes,
                    )?;

                    // this is the new intermediate stream with the UUID Merkle boxes inserted
                    temp_stream.rewind()?;
                    intermediate_stream = temp_stream;
                }
                pc.add_assertion(&bmff_hash)?;

                needs_hash = true;
            }

            // 3) Generate in memory CAI jumbf block
            // and write preliminary jumbf store to file
            // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned
            data = self.to_jumbf_internal(reserve_size)?;
            jumbf_size = data.len();
            // write the jumbf to the output stream if we are embedding the manifest
            if !remove_manifests {
                if source_is_intermediate {
                    intermediate_stream.rewind()?;
                    save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?;
                } else {
                    input_stream.rewind()?;
                    save_jumbf_to_stream(format, input_stream, output_stream, &data)?;
                }
            } else {
                // just copy the asset to the output stream without an embedded manifest (may be stripping one out here)
                // remove_manifests → fast_path is false → source_is_intermediate is true
                intermediate_stream.rewind()?;
                std::io::copy(&mut intermediate_stream, output_stream)?;
            }

            // Signal that the write pass is done; hash readback begins next.
            context.check_progress(ProgressPhase::Writing, 2, 2)?;

            // generate actual hash values
            let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability

            if !pc.update_manifest() {
                let bmff_hashes = pc.bmff_hash_assertions();

                if !bmff_hashes.is_empty() && needs_hash {
                    let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0].assertion())?;

                    output_stream.rewind()?;
                    let mut cb =
                        |step, total| context.check_progress(ProgressPhase::Hashing, step, total);
                    bmff_hash.gen_hash_from_stream_with_progress(output_stream, &mut cb)?;
                    pc.update_bmff_hash(bmff_hash)?;
                }
            }
        } else {
            // we will not do automatic hashing if we detect a box hash present
            let mut needs_hashing = false;
            if pc.hash_assertions().is_empty() {
                // 2) Get hash ranges if needed, do not generate for update manifests.
                // When fast_path is true, source_is_intermediate is false and we read
                // input_stream directly, skipping the intermediate copy pass.
                let mut hash_ranges = if io_handler.is_none() {
                    Vec::new()
                } else if source_is_intermediate {
                    object_locations_from_stream(format, &mut intermediate_stream)?
                } else {
                    object_locations_from_stream(format, input_stream)?
                };
                let hashes: Vec<DataHash> = if pc.update_manifest() {
                    Vec::new()
                } else if source_is_intermediate {
                    Store::generate_data_hashes_for_stream(
                        &mut intermediate_stream,
                        pc.alg(),
                        &mut hash_ranges,
                        false,
                        &mut |_, _| Ok(()),
                    )?
                } else {
                    Store::generate_data_hashes_for_stream(
                        input_stream,
                        pc.alg(),
                        &mut hash_ranges,
                        false,
                        &mut |_, _| Ok(()),
                    )?
                };

                // add the placeholder data hashes to provenance claim so that the required space is reserved
                for mut hash in hashes {
                    // add padding to account for possible cbor expansion of final DataHash
                    let padding: Vec<u8> = vec![0x0; 10];
                    hash.add_padding(padding);

                    pc.add_assertion(&hash)?;
                }
                needs_hashing = true;
            }

            // 3) Generate in memory CAI jumbf block
            data = self.to_jumbf_internal(reserve_size)?;
            jumbf_size = data.len();

            // Embed the JUMBF only when we have a format handler that supports it.
            // Without a handler the output is always a verbatim copy of the asset and
            // the manifest is returned as sidecar data.
            if !remove_manifests && io_handler.is_some() {
                if source_is_intermediate {
                    intermediate_stream.rewind()?;
                    save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?;
                } else {
                    input_stream.rewind()?;
                    save_jumbf_to_stream(format, input_stream, output_stream, &data)?;
                }
            } else {
                // just copy the asset to the output stream without an embedded manifest
                // remove_manifests → fast_path is false → source_is_intermediate is true
                intermediate_stream.rewind()?;
                std::io::copy(&mut intermediate_stream, output_stream)?;
            }

            // Signal that the asset write pass is complete, before the hash
            // readback pass begins.  This separates "Writing" (streaming
            // input → output with placeholder JUMBF) from "Hashing" (reading
            // output to compute the final content-hash binding).
            context.check_progress(ProgressPhase::Writing, 2, 2)?;

            // 4)  determine final object locations and patch the asset hashes with correct offset
            // replace the source with correct asset hashes so that the claim hash will be correct
            if needs_hashing {
                let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

                // Flush before seeking: FFI-backed streams (and any stream that
                // buffers writes) must be flushed before seeking back to read.
                output_stream.flush()?;

                // get the final hash ranges, but not for update manifests.
                // When there is no handler use empty ranges so the whole stream is hashed.
                output_stream.rewind()?;
                let mut new_hash_ranges = if io_handler.is_some() {
                    object_locations_from_stream(format, output_stream)?
                } else {
                    Vec::new()
                };
                if !pc.update_manifest() {
                    // if we removed the manifest fixup the hash range to be empty
                    if remove_manifests {
                        new_hash_ranges.iter_mut().for_each(|h| {
                            if h.htype == HashBlockObjectType::Cai
                                || h.htype == HashBlockObjectType::OtherExclusion
                            {
                                h.offset = 0;
                                h.length = 0;
                            }
                        });
                    }

                    let mut cb =
                        |step, total| context.check_progress(ProgressPhase::Hashing, step, total);
                    let updated_hashes = Store::generate_data_hashes_for_stream(
                        output_stream,
                        pc.alg(),
                        &mut new_hash_ranges,
                        true,
                        &mut cb,
                    )?;

                    // patch existing claim hash with updated data
                    for hash in updated_hashes {
                        pc.update_data_hash(hash)?;
                    }
                }
            }
        }

        // regenerate the jumbf because the cbor changed
        data = self.to_jumbf_internal(reserve_size)?;
        if jumbf_size != data.len() {
            return Err(Error::JumbfCreationError);
        }

        Ok(data) // return JUMBF data
    }

    fn finish_save_stream(
        &self,
        jumbf_bytes: Vec<u8>,
        format: &str,
        input_stream: &mut dyn CAIRead,
        output_stream: &mut dyn CAIReadWrite,
    ) -> Result<(Vec<u8>, Vec<u8>)> {
        // re-save to file
        let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
        match pc.remote_manifest() {
            RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => {
                save_jumbf_to_stream(format, input_stream, output_stream, &jumbf_bytes)?;
            }
            RemoteManifest::SideCar | RemoteManifest::Remote(_) => {
                // just copy the asset to the output stream without an embedded manifest (may be stripping one out here)
                std::io::copy(input_stream, output_stream)?;
            }
        }

        output_stream.flush()?;
        Ok((pc.signature_val().to_vec(), jumbf_bytes))
    }

    // fetch remote manifest if possible
    #[cfg(feature = "fetch_remote_manifests")]
    #[async_generic]
    fn fetch_remote_manifest(url: &str, context: &Context) -> Result<Vec<u8>> {
        //const MANIFEST_CONTENT_TYPE: &str = "application/x-c2pa-manifest-store"; // todo verify once these are served

        context.check_progress(ProgressPhase::FetchingRemoteManifest, 1, 1)?;

        let request = http::Request::get(url).body(Vec::new())?;
        let response = if _sync {
            context.resolver().http_resolve(request)
        } else {
            context.resolver_async().http_resolve_async(request).await
        };

        match response {
            Ok(response) => {
                if response.status() == 200 {
                    let len = response
                        .headers()
                        .get(http::header::CONTENT_LENGTH)
                        .and_then(|content_length| content_length.to_str().ok())
                        .and_then(|content_length| content_length.parse().ok())
                        .unwrap_or(DEFAULT_MANIFEST_RESPONSE_SIZE); // todo figure out good max to accept
                    let body = response.into_body();

                    let mut response_bytes: Vec<u8> = Vec::with_capacity(len);

                    let len64 = u64::try_from(len)
                        .map_err(|_err| Error::BadParam("value out of range".to_string()))?;

                    body.take(len64)
                        .read_to_end(&mut response_bytes)
                        .map_err(|_err| {
                            Error::RemoteManifestFetch("error reading content stream".to_string())
                        })?;

                    Ok(response_bytes)
                } else {
                    Err(Error::RemoteManifestFetch(format!(
                        "fetch failed: code: {}, status: {}",
                        response.status().as_u16(),
                        response.status().as_str()
                    )))
                }
            }
            Err(err) => Err(Error::RemoteManifestFetch(err.to_string())),
        }
    }

    /// Handles remote manifests when file_io/fetch_remote_manifests feature is enabled
    #[async_generic]
    fn handle_remote_manifest(ext_ref: &str, _context: &Context) -> Result<Vec<u8>> {
        // verify provenance path is remote url
        if Store::is_valid_remote_url(ext_ref) {
            #[cfg(feature = "fetch_remote_manifests")]
            {
                // Everything except browser wasm if fetch_remote_manifests is enabled
                if _context.settings().verify.remote_manifest_fetch {
                    if _sync {
                        Store::fetch_remote_manifest(ext_ref, _context)
                    } else {
                        Store::fetch_remote_manifest_async(ext_ref, _context).await
                    }
                } else {
                    Err(Error::RemoteManifestUrl(ext_ref.to_owned()))
                }
            }

            #[cfg(not(feature = "fetch_remote_manifests"))]
            Err(Error::RemoteManifestUrl(ext_ref.to_owned()))
        } else {
            Err(Error::JumbfNotFound)
        }
    }

    /// load jumbf given a stream
    ///
    /// This handles, embedded and remote manifests
    ///
    /// asset_type -  mime type of the stream
    /// stream - a readable stream of an asset
    ///
    /// Returns a tuple (jumbf_bytes, remote_url), returning a remote_url only
    /// if it was used to fetch the jumbf_bytes.
    #[async_generic(async_signature(
        asset_type: &str,
        stream: &mut dyn CAIRead,
        context: &Context
    ))]
    pub fn load_jumbf_from_stream(
        asset_type: &str,
        stream: &mut dyn CAIRead,
        context: &Context,
    ) -> Result<(Vec<u8>, Option<String>)> {
        match load_jumbf_from_stream(asset_type, stream) {
            Ok(manifest_bytes) => Ok((manifest_bytes, None)),
            Err(Error::JumbfNotFound) => {
                stream.rewind()?;
                if let Some(ext_ref) =
                    crate::utils::xmp_inmemory_utils::XmpInfo::from_source(stream, asset_type)
                        .provenance
                {
                    let jumbf = if _sync {
                        Store::handle_remote_manifest(&ext_ref, context)?
                    } else {
                        Store::handle_remote_manifest_async(&ext_ref, context).await?
                    };
                    Ok((jumbf, Some(ext_ref)))
                } else {
                    Err(Error::JumbfNotFound)
                }
            }
            Err(e) => Err(e),
        }
    }

    /// Returns embedded remote manifest URL if available
    /// asset_type: extensions or mime type of the data
    /// data: byte array containing the asset
    #[allow(unused)] // we don't use this anywhere now, but we should!
    pub fn get_remote_manifest_url(asset_type: &str, data: &[u8]) -> Option<String> {
        let mut buf_reader = Cursor::new(data);

        if let Some(ext_ref) =
            crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut buf_reader, asset_type)
                .provenance
        {
            // make sure it parses
            let _u = url::Url::parse(&ext_ref).ok()?;
            Some(ext_ref)
        } else {
            None
        }
    }

    /// check the input url to see if it is a supported remotes URI
    pub fn is_valid_remote_url(url: &str) -> bool {
        match url::Url::parse(url) {
            Ok(u) => u.scheme() == "http" || u.scheme() == "https",
            Err(_) => false,
        }
    }

    /// Load store from a stream
    #[async_generic]
    pub fn from_stream(
        format: &str,
        mut stream: impl Read + Seek + MaybeSend,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Self> {
        let (manifest_bytes, remote_url) = if _sync {
            Store::load_jumbf_from_stream(format, &mut stream, context)
        } else {
            Store::load_jumbf_from_stream_async(format, &mut stream, context).await
        }
        .inspect_err(|e| {
            log_item!("asset", "error loading file", "load_from_asset")
                .failure_no_throw(validation_log, e);
        })?;

        let store = if _sync {
            Self::from_manifest_data_and_stream(
                &manifest_bytes,
                format,
                &mut stream,
                validation_log,
                context,
            )
        } else {
            Self::from_manifest_data_and_stream_async(
                &manifest_bytes,
                format,
                &mut stream,
                validation_log,
                context,
            )
            .await
        };

        let mut store = store?;
        if remote_url.is_none() {
            store.embedded = true;
        } else {
            store.remote_url = remote_url;
        }

        Ok(store)
    }

    /// Load store from a manifest data and stream
    #[async_generic]
    pub fn from_manifest_data_and_stream(
        c2pa_data: &[u8],
        format: &str,
        mut stream: impl Read + Seek + MaybeSend,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Self> {
        stream.rewind()?;

        // First we convert the JUMBF into a usable store.
        let store = Store::from_jumbf_with_context(c2pa_data, validation_log, context)
            .inspect_err(|e| {
                log_item!("asset", "error loading file", "load_from_asset")
                    .failure_no_throw(validation_log, e);
            })?;

        if context.settings().verify.verify_after_reading {
            stream.rewind()?;
            let mut asset_data = ClaimAssetData::Stream(&mut stream, format);
            if _sync {
                Store::verify_store(&store, Some(&mut asset_data), validation_log, context)
            } else {
                Store::verify_store_async(&store, Some(&mut asset_data), validation_log, context)
                    .await
            }?;
        }
        Ok(store)
    }

    /// Load Store from a init and fragments
    /// asset_type: asset extension or mime type
    /// init_segment: reader for the file containing the initialization segments
    /// fragments: list of paths to the fragments to verify
    /// verify: if true will run verification checks when loading, all fragments must verify for Ok status
    /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
    #[cfg(feature = "file_io")]
    pub fn load_from_file_and_fragments(
        asset_type: &str,
        init_segment: &mut dyn CAIRead,
        fragments: &Vec<PathBuf>,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Store> {
        let manifest_bytes = Store::load_jumbf_from_stream(asset_type, init_segment, context)?.0;

        let store = Store::from_jumbf_with_context(&manifest_bytes, validation_log, context)?;
        let verify = context.settings().verify.verify_after_reading;

        if verify {
            init_segment.rewind()?;
            // verify store and claims
            Store::verify_store(
                &store,
                Some(&mut ClaimAssetData::StreamFragments(
                    init_segment,
                    fragments,
                    asset_type,
                )),
                validation_log,
                context,
            )?;
        }

        Ok(store)
    }

    /// Load Store from a stream and fragment stream
    ///
    /// asset_type: asset extension or mime type
    /// stream: reference to initial segment asset
    /// fragment: reference to fragment asset
    /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned
    #[async_generic(async_signature(
        format: &str,
        mut stream: impl Read + Seek + MaybeSend,
        mut fragment: impl Read + Seek + MaybeSend,
        validation_log: &mut StatusTracker,
        context: &Context,
    ))]
    pub fn load_fragment_from_stream(
        format: &str,
        mut stream: impl Read + Seek + MaybeSend,
        mut fragment: impl Read + Seek + MaybeSend,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Store> {
        let manifest_bytes = if _sync {
            Store::load_jumbf_from_stream(format, &mut stream, context)?.0
        } else {
            Store::load_jumbf_from_stream_async(format, &mut stream, context)
                .await?
                .0
        };

        let store = Store::from_jumbf_with_context(&manifest_bytes, validation_log, context)?;
        let verify = context.settings().verify.verify_after_reading;

        if verify {
            let mut fragment = ClaimAssetData::StreamFragment(&mut stream, &mut fragment, format);
            if _sync {
                Store::verify_store(&store, Some(&mut fragment), validation_log, context)
            } else {
                Store::verify_store_async(&store, Some(&mut fragment), validation_log, context)
                    .await
            }?;
        };
        Ok(store)
    }

    // get the manifest that should be used for hash binding checks
    fn get_hash_binding_manifest(&self, claim: &Claim) -> Option<String> {
        // is this claim valid
        if !claim.update_manifest() && !claim.hash_assertions().is_empty() {
            return Some(claim.label().to_owned());
        }

        // walk the update manifests until you find an acceptable claim
        for i in claim.ingredient_assertions() {
            let ingredient = Ingredient::from_assertion(i.assertion()).ok()?;
            if ingredient.relationship == Relationship::ParentOf {
                if let Some(parent_uri) = ingredient.c2pa_manifest() {
                    let parent_label = manifest_label_from_uri(&parent_uri.url())?;
                    if let Some(parent) = self.get_claim(&parent_label) {
                        // recurse until we find
                        if parent.update_manifest() {
                            return self.get_hash_binding_manifest(parent);
                        } else if !parent.hash_assertions().is_empty() {
                            return Some(parent.label().to_owned());
                        }
                    }
                }
            }
        }
        None
    }

    // determine if the only changes are redacted assertions
    fn manifest_differs_by_redaction(
        c1: &Claim,
        c2: &Claim,
        redactions: &[String],
    ) -> Option<Vec<String>> {
        if let Ok(d1) = c1.data() {
            if let Ok(d2) = c2.data() {
                if d1 != d2 {
                    return None;
                }
            } else {
                return None;
            }
        } else {
            return None;
        }

        if c1.signature_val() != c2.signature_val() {
            return None;
        }

        if c1.databoxes() != c2.databoxes() {
            return None;
        }

        // get the assertion store differences
        let c1_set: HashSet<&ClaimAssertion> = c1.claim_assertion_store().iter().collect();
        let c2_set: HashSet<&ClaimAssertion> = c2.claim_assertion_store().iter().collect();

        let differences = c1_set.symmetric_difference(&c2_set).collect::<Vec<_>>();

        // are the assertion differences listed in the redaction list
        let mut redact_matches = 0;
        let mut redactions_to_remove = Vec::new();
        for difference in &differences {
            let difference_uri = to_assertion_uri(c1.label(), &difference.label());

            // was the difference in the redacted list
            if redactions
                .iter()
                .any(|redaction_uri| redaction_uri.as_str() == difference_uri.as_str())
            {
                redact_matches += 1;
                redactions_to_remove.push(difference_uri);
            }
        }

        // if all mismatches are redactions we are good
        if redact_matches == differences.len() {
            return Some(redactions_to_remove);
        }

        None
    }

    // build ingredient lists for the Claim in the specified Store
    // the referenced_ingredients map the ingredient to the claims that reference it
    // the found_redactions are any redactions found
    fn get_claim_referenced_manifests<'a>(
        claim: &'a Claim,
        store: &'a Store,
        svi: &mut StoreValidationInfo<'a>,
        recurse: bool,
        validation_log: &mut StatusTracker,
    ) -> Result<()> {
        Self::get_claim_referenced_manifests_impl(
            claim,
            store,
            svi,
            recurse,
            validation_log,
            &mut Vec::new(),
        )
    }

    fn get_claim_referenced_manifests_impl<'a>(
        claim: &'a Claim,
        store: &'a Store,
        svi: &mut StoreValidationInfo<'a>,
        recurse: bool,
        validation_log: &mut StatusTracker,
        claim_label_path: &mut Vec<&'a str>,
    ) -> Result<()> {
        // `claim_label_path` is the chain of claims currently being walked
        // recursively: each entry pushes on entry (line below) and pops on
        // return (end of function), so its length is the current recursion
        // depth — not the total number of ingredients seen. A claim with a
        // million sibling ingredients still recurses at depth 1 per sibling.
        if claim_label_path.len() >= MAX_INGREDIENT_DEPTH {
            return Err(Error::InvalidAsset(format!(
                "ingredient chain depth ({}) exceeds maximum ({})",
                claim_label_path.len(),
                MAX_INGREDIENT_DEPTH
            )));
        }

        let claim_label = claim.label();

        if svi.manifest_map.contains_key(claim_label) {
            return Ok(());
        }

        claim_label_path.push(claim_label);

        // add in current redactions
        if let Some(c_redactions) = claim.redactions() {
            svi.redactions.extend(c_redactions.iter().cloned());
        }

        // save the addressible claims for quicker lookup
        svi.manifest_map.insert(claim_label.to_owned(), claim);

        for i in claim.ingredient_assertions() {
            let ingredient_assertion = Ingredient::from_assertion(i.assertion())?;

            // get correct hashed URI
            let c2pa_manifest = match ingredient_assertion.c2pa_manifest() {
                Some(m) => m, // > v2 ingredient assertion
                None => continue,
            };

            // is this an ingredient
            let ingredient_label = Store::manifest_label_from_path(&c2pa_manifest.url());

            if let Some(ingredient) = store.get_claim(&ingredient_label) {
                if claim_label_path.contains(&ingredient.label()) {
                    return Err(log_item!(
                        jumbf::labels::to_assertion_uri(claim_label, &i.label()),
                        "ingredient cannot be cyclic",
                        "ingredient_checks"
                    )
                    .validation_status(validation_status::ASSERTION_INGREDIENT_MALFORMED)
                    .failure_as_err(
                        validation_log,
                        Error::CyclicIngredients {
                            claim_label_path: claim_label_path
                                .iter()
                                .map(|&label| label.to_owned())
                                .collect(),
                        },
                    ));
                }

                // build mapping of ingredients and those claims that reference it
                svi.ingredient_references
                    .entry(ingredient_label.clone())
                    .or_insert(HashSet::from_iter(vec![claim_label.to_owned()]))
                    .insert(claim_label.to_owned());

                // recurse nested ingredients
                if recurse {
                    Store::get_claim_referenced_manifests_impl(
                        ingredient,
                        store,
                        svi,
                        recurse,
                        validation_log,
                        claim_label_path,
                    )?;
                }
            } else {
                log_item!(
                    ingredient_label.clone(),
                    "ingredient manifest missing",
                    "get_claim_referenced_manifests"
                )
                .validation_status(validation_status::INGREDIENT_MANIFEST_MISSING)
                .failure(
                    validation_log,
                    Error::ClaimMissing {
                        label: ingredient_label,
                    },
                )?;
            }
        }

        claim_label_path.pop();

        Ok(())
    }

    /// Load Store from memory and add its content as a claim ingredient
    /// claim: claim to add an ingredient
    /// provenance_label: label of the provenance claim used as key into ingredient map
    /// data: jumbf data block
    /// returns new Store with ingredients loaded, claim is modified to include resolved
    /// ingredients conflicts
    pub fn load_ingredient_to_claim(
        claim: &mut Claim,
        data: &[u8],
        redactions: Option<Vec<String>>,
        context: &Context,
    ) -> Result<Store> {
        // constants for ingredient conflict reasons
        const CONFLICTING_MANIFEST: usize = 1; // Conflicts with another C2PA Manifest

        let mut to_both = Vec::new();
        let mut to_remove_from_incoming = Vec::new();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let i_store = Store::from_jumbf_with_context(data, &mut report, context)?;

        let empty_store = Store::default();

        // make sure the claims stores are compatible
        let ingredient_pc = i_store.provenance_claim().ok_or(Error::OtherError(
            "ingredient missing provenace claim".into(),
        ))?;
        if claim.version() < ingredient_pc.version() {
            return Err(Error::OtherError("ingredient version too new".into()));
        }

        // get list of referenced manifests and redactions from ingredient provenance claim
        let mut validation_log = StatusTracker::default();
        let mut svi = StoreValidationInfo::default();
        Store::get_claim_referenced_manifests(
            ingredient_pc,
            &i_store,
            &mut svi,
            true,
            &mut validation_log,
        )?;

        // resolve conflicts
        // for 2.x perform ingredients conflict handling by making new label if needed
        let skip_resolution = context
            .settings()
            .verify
            .skip_ingredient_conflict_resolution;

        if claim.version() > 1 && !skip_resolution {
            // if the hashes match then the values are OK to add so remove form conflict list
            // matching manifests are automatically deduped in a later step
            let potential_conflicts: Vec<_> = i_store
                .claims()
                .iter()
                .filter_map(|i_claim| {
                    for c in claim.claim_ingredients() {
                        if c.label() == i_claim.label() {
                            let i_ingredient_hashes = i_store.get_manifest_box_hashes(i_claim);
                            let current_claim_hashes = empty_store.get_manifest_box_hashes(c);

                            // if they match there is no conflict
                            if !vec_compare(
                                &current_claim_hashes.manifest_box_hash,
                                &i_ingredient_hashes.manifest_box_hash,
                            ) {
                                return Some(c.label().to_owned());
                            }
                        }
                    }

                    None
                })
                .collect();

            if !potential_conflicts.is_empty() {
                // get info about conflicting Claim from current claim
                let mut claim_redactions: Vec<String> = redactions.clone().unwrap_or_default();
                for c in claim.claim_ingredients() {
                    if let Some(r) = c.redactions() {
                        claim_redactions.extend(r.iter().cloned());
                    }
                }

                let combined_redactions = HashSet::<_>::from_iter(
                    vec![claim_redactions.clone(), svi.redactions.clone()]
                        .into_iter()
                        .flatten(),
                )
                .into_iter()
                .collect::<Vec<String>>();

                // do any of the conflicting manifests contain redactions
                for conflict_label in potential_conflicts {
                    // Step 1: was the conflict because of a redaction from the either the current
                    // claim or the incoming store

                    let conflict = i_store
                        .get_claim(&conflict_label)
                        .ok_or(Error::IngredientNotFound)?;

                    // can only resolve conflict if the changes were redaction differences
                    if let Some(curr_claim_ingredient_conflict) = claim
                        .claim_ingredients()
                        .iter()
                        .find(|c| c.label() == conflict_label)
                    {
                        if let Some(mut differences) = Store::manifest_differs_by_redaction(
                            curr_claim_ingredient_conflict,
                            conflict,
                            &combined_redactions,
                        ) {
                            if !claim_redactions.is_empty() && svi.redactions.is_empty() {
                                // if redactions were only in the claim we can skip bringing the ingredient
                                to_remove_from_incoming.push(conflict_label.clone());
                            } else if claim_redactions.is_empty() && !svi.redactions.is_empty() {
                                // if redactions were only from the incoming ingredient replace claim
                                // noting to do here since the incoming claim will just overwrite the current claim
                                continue;
                            } else {
                                to_both.append(&mut differences);
                            }
                        } else {
                            let new_version = match claim
                                .claim_ingredient_store()
                                .keys()
                                .filter_map(|label| match manifest_label_to_parts(label) {
                                    Some(mp) => mp.version,
                                    None => None,
                                })
                                .max()
                            {
                                Some(last_conflict_version) => last_conflict_version + 1,
                                None => {
                                    return Err(Error::OtherError(
                                        "ingredient label malformed".into(),
                                    ))
                                }
                            };

                            // make new ingredient label
                            let mut new_mp = manifest_label_to_parts(&conflict_label)
                                .ok_or(Error::OtherError("ingredient label malformed".into()))?;
                            new_mp.version = Some(new_version);
                            new_mp.reason = Some(CONFLICTING_MANIFEST);
                            let new_label = new_mp.to_string();

                            // update ingredient manifest label to new label
                            let mut fixup_claim = i_store
                                .get_claim(conflict.label())
                                .ok_or(Error::IngredientNotFound)?
                                .clone();
                            fixup_claim.set_conflict_label(new_label.clone());

                            // add relabeled manifest to store as new ingredient
                            claim.add_ingredient_data(
                                vec![fixup_claim],
                                None,
                                &svi.ingredient_references,
                            )?;
                        }
                    }
                }
            }
        }

        // make necessary changes to the incoming store
        let mut i_store_mut = Store::from_jumbf_with_context(data, &mut report, context)?;
        let mut final_redactions = HashSet::new();
        if let Some(redactions) = redactions {
            for r in redactions {
                final_redactions.insert(r);
            }
        }

        // For each redaction that targets an ingredient assertion, remove the corresponding
        // entry from ingredient_references. If no other claim still references that ingredient
        // claim, drop it from the incoming store entirely.
        for redaction_uri in &final_redactions {
            let assertion_label = match assertion_label_from_uri(redaction_uri) {
                Some(l) => l,
                None => continue,
            };

            // Only act on ingredient assertions.
            if !assertion_label.starts_with(labels::INGREDIENT) {
                continue;
            }

            // The redaction URI encodes which claim owns the assertion.
            let claim_label = match manifest_label_from_uri(redaction_uri) {
                Some(l) => l,
                None => continue,
            };

            // Find the owning claim and then the specific assertion within it.
            let Some(owning_claim) = i_store.get_claim(&claim_label) else {
                continue;
            };

            let Some(ia) = owning_claim
                .ingredient_assertions()
                .into_iter()
                .find(|a| a.label() == assertion_label)
            else {
                continue;
            };

            // Recover the referenced manifest URL from active_manifest or c2pa_manifest.
            let Ok(ingredient_assertion) = Ingredient::from_assertion(ia.assertion()) else {
                continue;
            };

            let Some(manifest_ref) = ingredient_assertion.c2pa_manifest() else {
                continue;
            };

            let ingredient_label = Store::manifest_label_from_path(&manifest_ref.url());

            let now_unreferenced =
                if let Some(refs) = svi.ingredient_references.get_mut(&ingredient_label) {
                    refs.remove(&claim_label);
                    refs.is_empty()
                } else {
                    false
                };

            if now_unreferenced {
                svi.ingredient_references.remove(&ingredient_label);
                to_remove_from_incoming.push(ingredient_label);
            }
        }

        // remove the claims from the incoming store as to not overwrite the current claim
        for label in to_remove_from_incoming {
            i_store_mut.remove_claim(&label);
        }

        // if there are redactions in both apply the current redaction to incoming claim
        if !to_both.is_empty() {
            for r in to_both.iter() {
                final_redactions.insert(r.to_owned());
            }

            // copy the redactions differences from current to incoming claim
            final_redactions.retain(|f| !svi.redactions.contains(f));
        }

        let claims_to_add: Vec<Claim> = i_store_mut.claims().into_iter().cloned().collect();
        claim.add_ingredient_data(
            claims_to_add,
            Some(final_redactions.into_iter().collect()),
            &svi.ingredient_references,
        )?;
        Ok(i_store)
    }

    /// Fetches ocsp response ders from the specified manifests.
    ///
    /// # Arguments
    /// * `manifest_labels` - Vector of manifest labels to check for ocsp responses
    /// * `validation_log` - Status tracker for logging validation events
    ///
    /// # Returns
    /// A `Result` containing tuples of manifest labels and their associated ocsp response
    #[async_generic(async_signature(
        &self,
        manifest_labels: Vec<String>,
        validation_log: &mut StatusTracker,
        context: &Context,
    ))]
    pub fn get_ocsp_response_ders(
        &self,
        manifest_labels: Vec<String>,
        validation_log: &mut StatusTracker,
        context: &Context,
    ) -> Result<Vec<(String, Vec<u8>)>> {
        let mut oscp_response_ders = Vec::new();

        let mut adjusted_settings = context.settings().clone();
        let original_trust_val = adjusted_settings.verify.verify_timestamp_trust;

        for manifest_label in manifest_labels {
            if let Some(claim) = self.claims_map.get(&manifest_label) {
                let sig = claim.signature_val().clone();
                let data = claim.data()?;

                // no timestamp trust checks for 1.x manifests
                if claim.version() == 1 {
                    adjusted_settings.verify.verify_timestamp_trust = false;
                }

                let sign1 = parse_cose_sign1(&sig, &data, validation_log)?;
                let ocsp_response_der = if _sync {
                    fetch_and_check_ocsp_response(
                        &sign1,
                        &data,
                        &self.ctp,
                        None,
                        validation_log,
                        context,
                    )?
                    .ocsp_der
                } else {
                    fetch_and_check_ocsp_response_async(
                        &sign1,
                        &data,
                        &self.ctp,
                        None,
                        validation_log,
                        context,
                    )
                    .await?
                    .ocsp_der
                };

                if !ocsp_response_der.is_empty() {
                    oscp_response_ders.push((manifest_label, ocsp_response_der));
                }
            }
            adjusted_settings.verify.verify_timestamp_trust = original_trust_val;
        }

        Ok(oscp_response_ders)
    }
}

impl std::fmt::Display for Store {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let report = &ManifestStoreReport::from_store(self).unwrap_or_default();
        f.write_str(&format!("{}", report))
    }
}

/// `InvalidClaimError` provides additional detail on error cases for [`Store::from_jumbf`].
#[derive(Debug, thiserror::Error)]
pub enum InvalidClaimError {
    /// The "c2pa" block was not found in the asset.
    #[error("\"c2pa\" block not found")]
    C2paBlockNotFound,

    #[error("\"c2pa\" multiple claim boxes found in manifest")]
    C2paMultipleClaimBoxes,

    /// The claim superbox was not found.
    #[error("claim superbox not found")]
    ClaimSuperboxNotFound,

    /// The claim description box was not found.
    #[error("claim description box not found")]
    ClaimDescriptionBoxNotFound,

    /// More than one claim description box was found.
    #[error("more than one claim description box was found for {label}")]
    DuplicateClaimBox { label: String },

    /// The expected data not found in claim box.
    #[error("claim cbor box not valid")]
    ClaimBoxData,

    /// The claim has a version that is newer than supported by this crate.
    #[error("claim version is too new, not supported")]
    ClaimVersionTooNew,

    /// The claim has a version does not match JUMBF box label.
    #[error("claim version does not match JUMBF box label")]
    ClaimBoxVersion,

    /// The claim description box could not be parsed.
    #[error("claim description box was invalid")]
    ClaimDescriptionBoxInvalid,

    /// The claim signature box was not found.
    #[error("claim signature box was not found")]
    ClaimSignatureBoxNotFound,

    /// The claim signature description box was not found.
    #[error("claim signature description box was not found")]
    ClaimSignatureDescriptionBoxNotFound,

    /// The claim signature description box was invalid.
    #[error("claim signature description box was invalid")]
    ClaimSignatureDescriptionBoxInvalid,

    /// The assertion store superbox was not found.
    #[error("assertion store superbox not found")]
    AssertionStoreSuperboxNotFound,

    /// The verifiable credentials store could not be read.
    #[error("the verifiable credentials store could not be read")]
    VerifiableCredentialStoreInvalid,

    /// The feature is not supported by version
    #[error("the manifest contained a feature not support by version")]
    UnsupportedFeature(String),

    /// The assertion store does not contain the expected number of assertions.
    #[error(
        "unexpected number of assertions in assertion store (expected {expected}, found {found})"
    )]
    AssertionCountMismatch { expected: usize, found: usize },
}

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

    #[cfg(feature = "file_io")]
    use std::fs;
    use std::{
        io::{Read, Seek, SeekFrom, Write},
        sync::{
            atomic::{AtomicBool, Ordering},
            Arc,
        },
    };

    use c2pa_macros::c2pa_test_async;
    #[cfg(feature = "file_io")]
    use memchr::memmem;
    use serde::Serialize;
    #[cfg(feature = "file_io")]
    use sha2::Sha256;
    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
    use wasm_bindgen_test::wasm_bindgen_test;

    use super::*;
    #[cfg(feature = "file_io")]
    use crate::{
        assertion::AssertionJson,
        assertions::{labels::BOX_HASH, BoxHash},
        hashed_uri::HashedUri,
        jumbf_io::{get_assetio_handler_from_path, load_jumbf_from_file, save_jumbf_to_file},
        utils::{
            hash_utils::Hasher,
            io_utils::tempdirectory,
            test::{temp_dir_path, write_jpeg_placeholder_file, TEST_USER_ASSERTION},
            test_signer::test_cawg_signer,
        },
    };
    use crate::{
        assertions::{Action, Actions, Uuid},
        claim::AssertionStoreJsonFormat,
        status_tracker::{LogItem, StatusTracker},
        utils::{
            patch::patch_bytes,
            test::{create_test_claim, create_test_streams, fixture_path},
            test_signer::{async_test_signer, test_signer},
        },
        ClaimGeneratorInfo, DigitalSourceType, SigningAlg,
    };

    fn create_editing_claim(claim: &mut Claim) -> Result<&mut Claim> {
        let uuid_str = "deadbeefdeadbeefdeadbeefdeadbeef";

        // add a binary thumbnail assertion  ('deadbeefadbeadbe')
        let some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        let actions = Actions::new().add_action(Action::new("c2pa.created"));

        claim.add_assertion(&actions)?;

        let uuid_assertion = Uuid::new("test uuid", uuid_str.to_string(), some_binary_data);

        claim.add_assertion(&uuid_assertion)?;

        Ok(claim)
    }

    fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> {
        let actions = Actions::new()
            .add_action(Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty));

        claim.add_assertion(&actions)?;

        Ok(claim)
    }

    #[test]
    fn test_jumbf_generation() {
        let context = Context::new();

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v1_unit_test");

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();
        claim_capture.add_claim_generator_info(cgi.clone());

        let signer = test_signer(SigningAlg::Ps256);

        store.commit_claim(claim_capture).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // should not have any
        assert!(!report.has_any_error());

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim.get_claim_assertion(&label, instance).unwrap();
            }
        }
    }

    #[test]
    fn test_claim_v2_generation() {
        let context = Context::new();

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 2);
        create_capture_claim(&mut claim_capture).unwrap();
        claim_capture.add_claim_generator_info(cgi.clone());

        let signer = test_signer(SigningAlg::Ps256);

        store.commit_claim(claim_capture).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // should not have any
        assert!(!report.has_any_error());

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim.get_claim_assertion(&label, instance).unwrap();
            }
        }
    }

    #[test]
    fn test_bad_claim_v2_generation() {
        let mut context = Context::new();
        context.settings_mut().verify.verify_after_sign = false;

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 2);
        create_capture_claim(&mut claim_capture).unwrap();
        claim_capture.add_claim_generator_info(cgi.clone());

        // add second action to claim which is not allowed
        let action = Actions::new().add_action(Action::new("c2pa.opened"));
        claim_capture.add_assertion(&action).unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        store.commit_claim(claim_capture).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let _new_store = Store::from_stream(format, &mut output_stream, &mut report, &context);

        // should have action errors
        assert!(report.has_any_error());
        assert!(report.has_error(Error::ValidationRule(
            "only first action can be created or opened".to_string()
        )));
    }

    #[test]
    #[cfg(feature = "file_io")]
    #[ignore = "we need to make this work again"]
    fn test_unknown_asset_type_generation() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let (_format, mut input_stream, mut output_stream) =
            create_test_streams("unsupported_type.txt");
        let format = "text/plain";
        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store = Store::from_stream(
            format,
            &mut output_stream,
            &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
            &context,
        )
        .unwrap();

        // can  we get by the ingredient data back

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim.get_claim_assertion(&label, instance).unwrap();
            }
        }
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_detects_unverifiable_signature() {
        let context = crate::context::Context::new();

        struct BadSigner {}

        impl Signer for BadSigner {
            fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
                Ok(b"not a valid signature".to_vec())
            }

            fn alg(&self) -> SigningAlg {
                SigningAlg::Ps256
            }

            fn certs(&self) -> Result<Vec<Vec<u8>>> {
                Ok(Vec::new())
            }

            fn reserve_size(&self) -> usize {
                42
            }
        }

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        let mut store = Store::from_context(&context);

        let claim = create_test_claim().unwrap();

        let signer = BadSigner {};

        // JUMBF generation should fail because this signature won't validate.
        store.commit_claim(claim).unwrap();

        // TO DO: This generates a log spew when running this test.
        // I don't have time to fix this right now.
        // [(date) ERROR c2pa::store] Signature that was just generated does not validate: CoseCbor

        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .unwrap_err();
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_sign_with_expired_cert() {
        use crate::{create_signer, SigningAlg};

        let context = Context::new();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        let mut store = Store::from_context(&context);

        let claim = create_test_claim().unwrap();

        let signcert_path = fixture_path("rsa-pss256_key-expired.pub");
        let pkey_path = fixture_path("rsa-pss256-expired.pem");
        let signer =
            create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None).unwrap();

        store.commit_claim(claim).unwrap();

        let r = store.save_to_stream(
            format,
            &mut input_stream,
            &mut output_stream,
            &signer,
            &context,
        );
        assert!(r.is_err());
        assert_eq!(
            r.err().unwrap().to_string(),
            "the certificate was not valid at time of signing"
        );
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_jumbf_replacement_generation() {
        let context = Context::new();
        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();
        store.commit_claim(claim1).unwrap();

        // do we generate JUMBF
        let jumbf_bytes = store.to_jumbf_internal(512).unwrap();
        assert!(!jumbf_bytes.is_empty());

        // test adding to actual image
        let ap = fixture_path("prerelease.jpg");
        let temp_dir = tempdirectory().expect("temp dir");
        let op = temp_dir_path(&temp_dir, "replacement_test.jpg");

        // grab jumbf from original
        let original_jumbf = load_jumbf_from_file(&ap).unwrap();

        // replace with new jumbf
        save_jumbf_to_file(&jumbf_bytes, &ap, Some(&op)).unwrap();

        let saved_jumbf = load_jumbf_from_file(&op).unwrap();

        // saved data should be the new data
        assert_eq!(&jumbf_bytes, &saved_jumbf);

        // original data should not be in file anymore check for first 1k
        let buf = fs::read(&op).unwrap();
        assert_eq!(memmem::find(&buf, &original_jumbf[0..1024]), None);
    }

    #[c2pa_test_async]
    //#[ignore] // this is not generating the expected error. Needs investigation.
    async fn test_jumbf_generation_async() -> Result<()> {
        let context = crate::context::Context::new();

        // Verify after sign is causing UnreferencedManifest errors here, since the manifests don't reference each other.
        //no_verify_after_sign();

        let signer = async_test_signer(SigningAlg::Ps256);

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = crate::utils::test::create_test_claim()?;

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2)?;

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture)?;

        // Test generate JUMBF
        // Get labels for label test
        let claim1_label = claim1.label().to_string();
        let capture = claim_capture.label().to_string();
        let claim2_label = claim2.label().to_string();

        store.commit_claim(claim1)?;
        store
            .save_to_stream_async(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .await?;
        store.commit_claim(claim_capture)?;
        let mut temp_stream = Cursor::new(Vec::new());
        output_stream.rewind()?;
        store
            .save_to_stream_async(
                format,
                &mut output_stream,
                &mut temp_stream,
                &signer,
                &context,
            )
            .await?;
        store.commit_claim(claim2)?;
        temp_stream.rewind()?;
        output_stream.rewind()?;
        store
            .save_to_stream_async(
                format,
                &mut temp_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .await
            .unwrap();

        // test finding claims by label
        let c1 = store.get_claim(&claim1_label);
        let c2 = store.get_claim(&capture);
        let c3 = store.get_claim(&claim2_label);
        assert_eq!(&claim1_label, c1.unwrap().label());
        assert_eq!(&capture, c2.unwrap().label());
        assert_eq!(claim2_label, c3.unwrap().label());

        // Do we generate JUMBF
        let jumbf_bytes = store.to_jumbf_internal(signer.reserve_size())?;
        assert!(!jumbf_bytes.is_empty());

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        // make sure we can read from new file
        let mut report = StatusTracker::default();

        output_stream.rewind()?;
        let _new_store =
            Store::from_stream_async(format, &mut output_stream, &mut report, &context).await?;

        assert!(!report.has_any_error());
        Ok(())
    }

    #[test]
    fn test_png_jumbf_generation() {
        let mut context = Context::new();
        context.settings_mut().verify.verify_after_sign = false;

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .unwrap();

        store.commit_claim(claim_capture).unwrap();
        output_stream.rewind().unwrap();
        let mut temp_stream = Cursor::new(Vec::new());
        store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut temp_stream,
                &signer,
                &context,
            )
            .unwrap();

        store.commit_claim(claim2).unwrap();
        temp_stream.rewind().unwrap();
        output_stream.rewind().unwrap();
        store
            .save_to_stream(
                format,
                &mut temp_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // can  we get by the ingredient data back
        let _some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            // println!(
            //     "Claim: {} \n{}",
            //     claim.label(),
            //     claim
            //         .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
            //         .expect("could not restore from json")
            // );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_png_compressed_jumbf_generation() {
        let mut context = Context::new();
        context.settings_mut().verify.verify_after_sign = false;

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();
        claim2.set_compressed_manifest(true);

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();
        claim_capture.set_compressed_manifest(true);

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .unwrap();

        store.commit_claim(claim_capture).unwrap();
        output_stream.rewind().unwrap();
        let mut temp_stream = Cursor::new(Vec::new());
        store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut temp_stream,
                &signer,
                &context,
            )
            .unwrap();

        store.commit_claim(claim2).unwrap();
        temp_stream.rewind().unwrap();
        output_stream.rewind().unwrap();
        store
            .save_to_stream(
                format,
                &mut temp_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // can  we get by the ingredient data back
        let _some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            // println!(
            //     "Claim: {} \n{}",
            //     claim.label(),
            //     claim
            //         .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
            //         .expect("could not restore from json")
            // );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_get_data_boxes() {
        // Create a new claim.
        use crate::jumbf::labels::to_relative_uri;
        let claim1 = create_test_claim().unwrap();

        for (uri, db) in claim1.databoxes() {
            // test full path
            assert!(claim1.get_databox(uri).is_some());

            // test with relative path
            let rel_path = to_relative_uri(&uri.url());
            let rel_hr = HashedUri::new(rel_path, uri.alg(), &uri.hash());
            assert!(claim1.get_databox(&rel_hr).is_some());

            // test values
            assert_eq!(db, claim1.get_databox(uri).unwrap());
        }
    }

    /*  reenable this test once we place for large test files
        #[test]
        #[cfg(feature = "file_io")]
        fn test_arw_jumbf_generation() {
            let ap = fixture_path("sample1.arw");
            let temp_dir = tempdirectory().expect("temp dir");
            let op = temp_dir_path(&temp_dir, "ssample1.arw");

            // Create claims store.
            let mut store = Store::new();

            // Create a new claim.
            let claim1 = create_test_claim().unwrap();

            // Create a new claim.
            let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
            create_editing_claim(&mut claim2).unwrap();

            // Create a 3rd party claim
            let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
            create_capture_claim(&mut claim_capture).unwrap();

            // Do we generate JUMBF?
            let signer = test_signer(SigningAlg::Ps256);

            // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits
            store.commit_claim(claim1).unwrap();
            store.save_to_asset(&ap, signer.as_ref(), &op).unwrap();
            store.commit_claim(claim_capture).unwrap();
            store.save_to_asset(&op, signer.as_ref(), &op).unwrap();
            store.commit_claim(claim2).unwrap();
            store.save_to_asset(&op, signer.as_ref(), &op).unwrap();

            // write to new file
            println!("Provenance: {}\n", store.provenance_path().unwrap());

            let mut report = StatusTracker::default();

            // read from new file
            let new_store = Store::load_from_asset(&op, true, &mut report).unwrap();

            // can  we get by the ingredient data back
            let _some_binary_data: Vec<u8> = vec![
                0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
                0x0b, 0x0e,
            ];

            // dump store and compare to original
            for claim in new_store.claims() {
                let _restored_json = claim
                    .to_json(AssertionStoreJsonFormat::OrderedList, false)
                    .unwrap();
                let _orig_json = store
                    .get_claim(claim.label())
                    .unwrap()
                    .to_json(AssertionStoreJsonFormat::OrderedList, false)
                    .unwrap();

                println!(
                    "Claim: {} \n{}",
                    claim.label(),
                    claim
                        .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                        .expect("could not restore from json")
                );

                for hashed_uri in claim.assertions() {
                    let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                    claim
                        .get_claim_assertion(&label, instance)
                        .expect("Should find assertion");
                }
            }
        }
        #[test]
        #[cfg(feature = "file_io")]
        fn test_nef_jumbf_generation() {
            let ap = fixture_path("sample1.nef");
            let temp_dir = tempdirectory().expect("temp dir");
            let op = temp_dir_path(&temp_dir, "ssample1.nef");

            // Create claims store.
            let mut store = Store::new();

            // Create a new claim.
            let claim1 = create_test_claim().unwrap();

            // Create a new claim.
            let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
            create_editing_claim(&mut claim2).unwrap();

            // Create a 3rd party claim
            let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
            create_capture_claim(&mut claim_capture).unwrap();

            // Do we generate JUMBF?
            let signer = test_signer(SigningAlg::Ps256);

            // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits
            store.commit_claim(claim1).unwrap();
            store.save_to_asset(&ap, signer.as_ref(), &op).unwrap();
            store.commit_claim(claim_capture).unwrap();
            store.save_to_asset(&op, signer.as_ref(), &op).unwrap();
            store.commit_claim(claim2).unwrap();
            store.save_to_asset(&op, signer.as_ref(), &op).unwrap();

            // write to new file
            println!("Provenance: {}\n", store.provenance_path().unwrap());

            let mut report = StatusTracker::default();

            // read from new file
            let new_store = Store::load_from_asset(&op, true, &mut report).unwrap();

            // can  we get by the ingredient data back
            let _some_binary_data: Vec<u8> = vec![
                0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
                0x0b, 0x0e,
            ];

            // dump store and compare to original
            for claim in new_store.claims() {
                let _restored_json = claim
                    .to_json(AssertionStoreJsonFormat::OrderedList, false)
                    .unwrap();
                let _orig_json = store
                    .get_claim(claim.label())
                    .unwrap()
                    .to_json(AssertionStoreJsonFormat::OrderedList, false)
                    .unwrap();

                println!(
                    "Claim: {} \n{}",
                    claim.label(),
                    claim
                        .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                        .expect("could not restore from json")
                );

                for hashed_uri in claim.assertions() {
                    let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                    claim
                        .get_claim_assertion(&label, instance)
                        .expect("Should find assertion");
                }
            }
        }
    */
    #[test]
    fn test_wav_jumbf_generation() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.wav");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();
        store.commit_claim(claim_capture).unwrap();
        output_stream.rewind().unwrap();
        let mut temp_stream = Cursor::new(Vec::new());
        store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut temp_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();
        store.commit_claim(claim2).unwrap();
        output_stream.rewind().unwrap();
        store
            .save_to_stream(
                format,
                &mut temp_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // can  we get by the ingredient data back
        let _some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_avi_jumbf_generation() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("test.avi");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .unwrap();
        store.commit_claim(claim_capture).unwrap();
        output_stream.rewind().unwrap();
        let mut temp_stream = Cursor::new(Vec::new());
        store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut temp_stream,
                &signer,
                &context,
            )
            .unwrap();
        store.commit_claim(claim2).unwrap();
        temp_stream.rewind().unwrap();
        output_stream.rewind().unwrap();
        store
            .save_to_stream(
                format,
                &mut temp_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // can  we get by the ingredient data back
        let _some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_webp_jumbf_generation() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.webp");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Create a new claim.
        let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
        create_editing_claim(&mut claim2).unwrap();

        // Create a 3rd party claim
        let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
        create_capture_claim(&mut claim_capture).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                &signer,
                &context,
            )
            .unwrap();
        store.commit_claim(claim_capture).unwrap();
        output_stream.rewind().unwrap();
        let mut temp_stream = Cursor::new(Vec::new());
        store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut temp_stream,
                &signer,
                &context,
            )
            .unwrap();
        store.commit_claim(claim2).unwrap();
        temp_stream.rewind().unwrap();
        output_stream.rewind().unwrap();
        store
            .save_to_stream(
                format,
                &mut temp_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // write to new file
        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // can  we get by the ingredient data back
        let _some_binary_data: Vec<u8> = vec![
            0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d,
            0x0b, 0x0e,
        ];

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_heic() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heic");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_avif() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.avif");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[test]
    fn test_heif() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heif");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // read from new stream
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        // dump store and compare to original
        for claim in new_store.claims() {
            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    /*  todo: disable until we can generate a valid file with no xmp
    #[test]
    fn test_manifest_no_xmp() {
        let ap = fixture_path("CAICAI_NO_XMP.jpg");
        assert!(Store::load_from_asset(&ap, true, None).is_ok());
    }
    */

    #[test]
    #[ignore = "This is not generating the expected error. Needs investigation."]
    fn test_manifest_bad_sig() {
        let context = Context::new();
        let (format, mut input_stream, _output_stream) = create_test_streams("CIE-sig-CA.jpg");
        let tracker = &mut StatusTracker::default();
        let result = Store::from_stream(format, &mut input_stream, tracker, &context);
        assert!(result.is_ok());
        println!("Error report: {tracker:?}");
        assert!(tracker.has_error(Error::AssertionInvalidRedaction));
    }

    #[test]
    fn test_unsupported_type_without_external_manifest() {
        let context = Context::new();
        let (format, mut input_stream, _output_stream) =
            create_test_streams("unsupported_type.txt");
        let mut report = StatusTracker::default();
        let result = Store::from_stream(format, &mut input_stream, &mut report, &context);
        assert!(matches!(result, Err(Error::UnsupportedType)));
        println!("Error report: {report:?}");
        assert!(!report.logged_items().is_empty());

        assert!(report.has_error(Error::UnsupportedType));
    }

    fn make_brotli_bomb_jumbf(decompressed_size: usize) -> Vec<u8> {
        use brotli::enc::BrotliEncoderParams;

        use crate::{
            jumbf::boxes::{Cai, JUMBFBrotliContentBox, JUMBFSuperBox, CAI_MANIFEST_UUID},
            store::BMFFBox,
        };

        let payload = vec![0u8; decompressed_size];
        let mut compressed = Vec::new();
        brotli::BrotliCompress(
            &mut Cursor::new(payload.as_slice()),
            &mut compressed,
            &BrotliEncoderParams::default(),
        )
        .expect("brotli compress");

        let brotli_box = JUMBFBrotliContentBox::new(compressed);
        let mut manifest_sbox = JUMBFSuperBox::new("test.manifest", Some(CAI_MANIFEST_UUID));
        manifest_sbox.add_data_box(Box::new(brotli_box));

        let mut cai = Cai::new();
        cai.add_box(Box::new(manifest_sbox));

        let mut bytes = Vec::new();
        cai.super_box()
            .write_box(&mut bytes)
            .expect("serialize cai block");
        bytes
    }

    #[test]
    fn test_from_jumbf_rejects_brotli_bomb_via_thread_local() {
        crate::settings::set_settings_value("core.max_decompressed_manifest_size_in_mb", 1usize)
            .unwrap();

        let result = Store::from_jumbf(
            &make_brotli_bomb_jumbf(2 * 1024 * 1024),
            &mut StatusTracker::default(),
        );

        crate::settings::reset_default_settings().unwrap();
        assert!(result.is_err());
    }

    #[test]
    fn test_from_jumbf_with_context_rejects_brotli_bomb() {
        let settings = Settings::default()
            .with_value("core.max_decompressed_manifest_size_in_mb", 1usize)
            .unwrap();
        let context = Context::new().with_settings(settings).unwrap();

        let result = Store::from_jumbf_with_context(
            &make_brotli_bomb_jumbf(2 * 1024 * 1024),
            &mut StatusTracker::default(),
            &context,
        );

        assert!(result.is_err());
    }

    #[test]
    fn test_bad_jumbf() {
        // test bad jumbf
        let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg");
        let mut report = StatusTracker::default();
        let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());

        // error report
        println!("Error report: {report:?}");
        assert!(!report.logged_items().is_empty());

        assert!(report.has_error(Error::PrereleaseError));
    }

    #[test]
    fn test_detect_byte_change() {
        // test bad jumbf
        let (format, mut input_stream, _output_stream) = create_test_streams("XCA.jpg");
        let mut report = StatusTracker::default();
        Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap();

        // error report
        println!("Error report: {report:?}");
        assert!(!report.logged_items().is_empty());

        assert!(report.has_status(validation_status::ASSERTION_DATAHASH_MISMATCH));
    }

    // #[test]
    // #[cfg(feature = "file_io")]
    // fn test_file_not_found() {
    //     let ap = fixture_path("this_does_not_exist.jpg");
    //     let mut report = StatusTracker::default();
    //     let _result = Store::load_from_asset(&ap, true, &mut report);

    //     println!(
    //         "Error report for {}: {:?}",
    //         ap.display(),
    //         report.logged_items()
    //     );

    //     assert!(!report.logged_items().is_empty());

    //     let errors: Vec<&LogItem> = report.filter_errors().collect();
    //     assert!(errors[0].err_val.as_ref().unwrap().starts_with("IoError"));
    // }

    #[test]
    fn test_old_manifest() {
        let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg");
        let mut report = StatusTracker::default();
        let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());

        println!("Error report: {report:?}");

        assert!(!report.logged_items().is_empty());

        let errors: Vec<&LogItem> = report.filter_errors().collect();
        assert!(errors[0]
            .err_val
            .as_ref()
            .unwrap()
            .starts_with("Prerelease"));
    }

    #[test]
    fn test_verifiable_credentials() {
        use crate::utils::test::create_test_store_v1;

        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        let signer = test_signer(SigningAlg::Ps256);

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // read back in
        output_stream.rewind().unwrap();
        let restored_store = Store::from_stream(
            format,
            &mut output_stream,
            &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
            &context,
        )
        .unwrap();

        let pc = restored_store.provenance_claim().unwrap();

        let vc = pc.get_verifiable_credentials();

        assert!(!vc.is_empty());
        match &vc[0] {
            AssertionData::Json(s) => {
                assert!(s.contains("did:nppa:eb1bb9934d9896a374c384521410c7f14"))
            }
            _ => panic!("expected JSON assertion data"),
        }
    }

    #[test]
    fn test_data_box_creation() {
        use crate::utils::test::create_test_store_v1;

        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        let signer = test_signer(SigningAlg::Ps256);

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // read back in
        output_stream.rewind().unwrap();
        let restored_store = Store::from_stream(
            format,
            &mut output_stream,
            &mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
            &context,
        )
        .unwrap();

        let pc = restored_store.provenance_claim().unwrap();

        let databoxes = pc.databoxes();

        assert!(!databoxes.is_empty());

        for (uri, db) in databoxes {
            println!(
                "URI: {}, data: {}",
                uri.url(),
                String::from_utf8_lossy(&db.data)
            );
        }
    }

    /// loads a fixture, replaces some bytes in memory and returns a validation report
    fn patch_and_report(
        fixture_name: &str,
        search_bytes: &[u8],
        replace_bytes: &[u8],
    ) -> StatusTracker {
        // Create test streams from fixture
        let (format, input_stream, _output_stream) = create_test_streams(fixture_name);

        // Get the data from the input stream and patch it
        let mut data = input_stream.into_inner();
        patch_bytes(&mut data, search_bytes, replace_bytes).expect("patch_bytes");

        // Create new stream from patched data
        let mut patched_stream = std::io::Cursor::new(data);

        let mut report = StatusTracker::default();
        let _r = Store::from_stream(format, &mut patched_stream, &mut report, &Context::new()); // errs are in report
        println!("report: {report:?}");
        report
    }

    #[test]
    fn test_update_manifest_v1() {
        use crate::{hashed_uri::HashedUri, utils::test::create_test_store_v1};

        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");
        let signer = test_signer(SigningAlg::Ps256);

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        // read back in
        output_stream.rewind().unwrap();
        let restored_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
        let pc = restored_store.provenance_claim().unwrap();

        // should be a regular manifest
        assert!(!pc.update_manifest());

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest"), 1);
        output_stream.rewind().unwrap();
        let mut new_store = Store::load_ingredient_to_claim(
            &mut claim,
            &load_jumbf_from_stream(format, &mut output_stream).unwrap(),
            None,
            &context,
        )
        .unwrap();

        let ingredient_hashes = new_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );

        let ingredient = Ingredient::new_v2("update_manifest.jpg", "image/jpeg")
            .set_parent()
            .set_c2pa_manifest_from_hashed_uri(Some(parent_hashed_uri));

        claim.add_assertion(&ingredient).unwrap();

        new_store.commit_update_manifest(claim).unwrap();
        output_stream.rewind().unwrap();
        let mut output_stream2 = std::io::Cursor::new(Vec::new());
        new_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut output_stream2,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // read back in store with update manifest
        output_stream2.rewind().unwrap();
        let um_store =
            Store::from_stream(format, &mut output_stream2, &mut report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should not have any errors
        assert!(!report.has_any_error());
    }

    ///Test for Update Manifest V2
    #[test]
    fn test_update_manifest_v2() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri,
            utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults,
        };

        let context = crate::context::Context::new();

        let signer = test_signer(SigningAlg::Ps256);

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        // read back in
        output_stream.rewind().unwrap();
        let ingredient_vec = output_stream.get_ref().clone();
        let mut ingredient_stream = Cursor::new(ingredient_vec);
        let restored_store =
            Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();
        let pc = restored_store.provenance_claim().unwrap();

        // should be a regular manifest
        assert!(!pc.update_manifest());

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim.add_claim_generator_info(cgi);

        ingredient_stream.rewind().unwrap();
        let (manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut ingredient_stream, &context).unwrap();
        let mut new_store =
            Store::load_ingredient_to_claim(&mut claim, &manifest_bytes, None, &context).unwrap();

        let ingredient_hashes = new_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&restored_store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let em = Action::new("c2pa.edited.metadata");
        let actions = Actions::new().add_action(opened).add_action(em);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        /* sample of adding timestamp assertion

        // lets add a timestamp for old manifest
        let timestamp = send_timestamp_request(pc.signature_val()).unwrap();
        crate::crypto::time_stamp::verify_time_stamp(&timestamp, pc.signature_val()).unwrap();
        let timestamp_assertion = crate::assertions::TimeStamp::new(pc.label(), &timestamp);
        claim.add_assertion(&timestamp_assertion).unwrap();
        */

        new_store.commit_update_manifest(claim).unwrap();
        output_stream.rewind().unwrap();
        let mut output_stream2 = std::io::Cursor::new(Vec::new());
        new_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut output_stream2,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        output_stream2.rewind().unwrap();
        let um_store =
            Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should not have any errors
        assert!(!um_report.has_any_error());
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_update_manifest_v2_bmff() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri, ClaimGeneratorInfo,
            ValidationResults,
        };

        let context = crate::context::Context::new();

        let signer = test_signer(SigningAlg::Ps256);

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read in the store
        let mut store =
            Store::from_stream(format, &mut input_stream, &mut report, &context).unwrap();
        let pc = store.provenance_claim().unwrap();

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim.add_claim_generator_info(cgi);

        let ingredient_hashes = store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let em = Action::new("c2pa.edited.metadata");
        let actions = Actions::new().add_action(opened).add_action(em);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        store.commit_update_manifest(claim).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        output_stream.rewind().unwrap();
        let mut um_store =
            Store::from_stream(format, &mut output_stream, &mut um_report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should have valid bmff hash binding
        assert!(um_report.has_status(validation_status::ASSERTION_BMFFHASH_MATCH));

        // now add a new oridinary manifest to restore the original manifest (collapsed manifest)

        // create a new ordinary claim
        let mut claim2 = Claim::new("adobe unit test", Some("ordinary_manifest_vendor"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim2.add_claim_generator_info(cgi);

        // make update PC claim the parent of the ordinary claim
        let update_pc = um_store.provenance_claim().unwrap();
        let ingredient_hashes = um_store.get_manifest_box_hashes(update_pc);
        let parent_hashed_uri = HashedUri::new(
            um_store.provenance_path().unwrap(),
            Some(update_pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(update_pc.label()),
            Some(update_pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&um_store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim2.add_assertion(&ingredient).unwrap();

        // create mandatory opened action
        let ingredient = claim2.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim2.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim2.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let editted = Action::new("c2pa.edited");
        let actions = Actions::new().add_action(opened).add_action(editted);

        // add action
        claim2.add_assertion(&actions).unwrap();

        um_store.commit_claim(claim2).unwrap();
        output_stream.rewind().unwrap();
        let mut output_stream2 = Cursor::new(Vec::new());
        um_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut output_stream2,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut collapsed_report =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        output_stream2.rewind().unwrap();
        let collapsed_store =
            Store::from_stream(format, &mut output_stream2, &mut collapsed_report, &context)
                .unwrap();

        let cm = collapsed_store.provenance_claim().unwrap();
        assert!(!cm.update_manifest());

        // should have valid bmff hash binding
        assert!(collapsed_report.has_status(validation_status::ASSERTION_BMFFHASH_MATCH));
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_update_manifest_with_timestamp_assertion() {
        // add timestamp assertion to update manifest
        let (format, mut input_stream, _output_stream) = create_test_streams("update_manifest.jpg");

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let restored_store =
            Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap();
        let pc = restored_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(pc.update_manifest());
    }

    fn add_ingredient_assertion_to_claim(
        store: &Store,
        claim: &mut Claim,
        action: &str,
        relationship: Relationship,
        instance: Option<usize>,
    ) -> Result<()> {
        use crate::{hashed_uri::HashedUri, jumbf::labels::to_signature_uri, ValidationResults};

        let report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        let pc = store.provenance_claim().unwrap();

        let ingredient_hashes = store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(store, &report);

        let ingredient = Ingredient::new_v3(relationship)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let mut ingredient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        if let Some(instance) = instance {
            ingredient_uri.push_str(&format!("__{instance}"));
        }
        let ingredient_hashed_uri = HashedUri::new(
            ingredient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let a = Action::new(action)
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let actions = Actions::new().add_action(a);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        Ok(())
    }

    #[test]
    fn test_ingredient_conflict_with_current_manifest() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri,
            utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults,
        };

        let context = crate::context::Context::new();

        let signer = test_signer(SigningAlg::Ps256);

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        // read back in
        output_stream.rewind().unwrap();
        let ingredient_vec = output_stream.get_ref().clone();
        let mut ingredient_stream = Cursor::new(ingredient_vec.clone());
        let restored_store =
            Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();
        let pc = restored_store.provenance_claim().unwrap();

        // should be a regular manifest
        assert!(!pc.update_manifest());

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim.add_claim_generator_info(cgi);

        // created redacted uri
        let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);

        let (manifest_bytes, _) = Store::load_jumbf_from_stream(
            format,
            &mut Cursor::new(ingredient_vec.clone()),
            &context,
        )
        .unwrap();
        let mut redacted_store = Store::load_ingredient_to_claim(
            &mut claim,
            &manifest_bytes,
            Some(vec![redacted_uri.clone()]),
            &context,
        )
        .unwrap();

        let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&restored_store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let em = Action::new("c2pa.edited.metadata");
        let actions = Actions::new().add_action(opened).add_action(em);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        redacted_store.commit_update_manifest(claim).unwrap();
        output_stream.rewind().unwrap();
        let mut output_stream2 = std::io::Cursor::new(Vec::new());
        redacted_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut output_stream2,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        output_stream2.rewind().unwrap();
        let um_store =
            Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should not have any errors
        assert!(!um_report.has_any_error());

        // add ingredient again without redaction to make sure conflict is resolved with current redaction
        let mut new_claim = Claim::new("adobe unit test", Some("merged_manifests"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        new_claim.add_claim_generator_info(cgi);

        // load original ingredient without redaction
        let mut original_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let (original_manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context)
                .unwrap();
        let original_store =
            Store::from_jumbf(&original_manifest_bytes, &mut original_log).unwrap();
        Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &context)
            .unwrap();
        // add original ingredient assertion as component to create conflict
        add_ingredient_assertion_to_claim(
            &original_store,
            &mut new_claim,
            "c2pa.opened",
            Relationship::ParentOf,
            None,
        )
        .unwrap();

        // load ingredient with redaction and try to redact again
        // it should not cause any issues and should retain the redacted ingredient assertion without error, and the conflict should be resolved with the current redaction
        output_stream2.rewind().unwrap();
        let mut redacted_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let (redacted_manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap();
        let redacted_store =
            Store::from_jumbf(&redacted_manifest_bytes, &mut redacted_log).unwrap();
        Store::load_ingredient_to_claim(
            &mut new_claim,
            &redacted_manifest_bytes,
            Some(vec![redacted_uri.clone()]),
            &context,
        )
        .unwrap();
        // add redacted ingredient assertion as component
        add_ingredient_assertion_to_claim(
            &redacted_store,
            &mut new_claim,
            "c2pa.placed",
            Relationship::ComponentOf,
            Some(1),
        )
        .unwrap();

        // save output of the conflict store to see the conflict assertion
        output_stream2.rewind().unwrap();
        let mut output_stream3 = std::io::Cursor::new(Vec::new());
        let mut final_store = Store::from_context(&Context::new());

        final_store.commit_claim(new_claim).unwrap();
        final_store
            .save_to_stream(
                format,
                &mut output_stream2,
                &mut output_stream3,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // the confict_store is adjusted to remove the conflicting claim
        output_stream3.rewind().unwrap();
        let mut resolved_store_report =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let resolved_store = Store::from_stream(
            format,
            &mut output_stream3,
            &mut resolved_store_report,
            &context,
        )
        .unwrap();

        // make sure the redaction stuck
        let redacted_claim = resolved_store.get_claim(pc.label()).unwrap();
        assert!(redacted_claim
            .get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
            .is_none());
    }

    #[test]
    fn test_ingredient_conflict_with_incoming_manifest() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri,
            utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults,
        };

        let context = Context::new();

        let signer = test_signer(SigningAlg::Ps256);

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        // read back in
        output_stream.rewind().unwrap();
        let ingredient_vec = output_stream.get_ref().clone();
        let mut ingredient_stream = Cursor::new(ingredient_vec.clone());
        let restored_store =
            Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();

        let pc = restored_store.provenance_claim().unwrap();

        // should be a regular manifest
        assert!(!pc.update_manifest());

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim.add_claim_generator_info(cgi);

        // created redacted uri
        let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);

        let (manifest_bytes, _) = Store::load_jumbf_from_stream(
            format,
            &mut Cursor::new(ingredient_vec.clone()),
            &context,
        )
        .unwrap();

        let mut redacted_store = Store::load_ingredient_to_claim(
            &mut claim,
            &manifest_bytes,
            Some(vec![redacted_uri]),
            &context,
        )
        .unwrap();

        let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&restored_store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let em = Action::new("c2pa.edited.metadata");
        let actions = Actions::new().add_action(opened).add_action(em);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        redacted_store.commit_update_manifest(claim).unwrap();
        output_stream.rewind().unwrap();
        let mut output_stream2 = std::io::Cursor::new(Vec::new());
        redacted_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut output_stream2,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        output_stream2.rewind().unwrap();
        let um_store =
            Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should not have any errors
        assert!(!um_report.has_any_error());

        // add ingredient again without redaction to make sure conflict is resolved with current redaction
        let mut new_claim = Claim::new("adobe unit test", Some("update_manifest_2"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        new_claim.add_claim_generator_info(cgi);

        // load original ingredient without redaction
        let (original_manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context)
                .unwrap();
        Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &context)
            .unwrap();

        // the confict_store is adjusted to remove the conflicting claim
        let not_redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
        assert!(not_redacted_claim
            .get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
            .is_some());

        // load ingredient with redaction
        output_stream2.rewind().unwrap();

        let (redacted_manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap();
        Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &context)
            .unwrap();

        // the confict_store is adjusted to remove the conflicting claim
        let redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
        assert!(redacted_claim
            .get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
            .is_none());
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_ingredient_conflicting_redactions_to_same_manifest() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri,
            utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults,
        };

        let context = Context::new();

        let signer = test_signer(SigningAlg::Ps256);

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("earth_apollo17.jpg");

        // get default store with default claim
        let mut store = create_test_store_v1().unwrap();

        // save to output
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        // read back in
        output_stream.rewind().unwrap();
        let restored_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
        let pc = restored_store.provenance_claim().unwrap();

        // should be a regular manifest
        assert!(!pc.update_manifest());

        // create a new update manifest
        let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        claim.add_claim_generator_info(cgi.clone());

        // created redacted uri
        let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);

        output_stream.rewind().unwrap();
        let ingredient_vec = load_jumbf_from_stream(format, &mut output_stream).unwrap();
        let mut redacted_store = Store::load_ingredient_to_claim(
            &mut claim,
            &ingredient_vec,
            Some(vec![redacted_uri]),
            &context,
        )
        .unwrap();

        let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes.manifest_box_hash,
        );
        let signature_hashed_uri = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes.signature_box_hash,
        );

        let validation_results = ValidationResults::from_store(&restored_store, &report);

        let ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri),
                Some(signature_hashed_uri),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results)); // mandatory for v3

        claim.add_assertion(&ingredient).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient = claim.ingredient_assertions()[0];
        let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
        let ingredient_hashed_uri = HashedUri::new(
            ingregient_uri,
            Some(claim.alg().to_owned()),
            ingredient.hash(),
        );

        let opened = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri])
            .unwrap();
        let em = Action::new("c2pa.edited.metadata");
        let actions = Actions::new().add_action(opened).add_action(em);

        // add action (this is optional for update manifest)
        claim.add_assertion(&actions).unwrap();

        redacted_store.commit_update_manifest(claim).unwrap();
        output_stream.rewind().unwrap();
        let mut op_output = std::io::Cursor::new(Vec::new());
        redacted_store
            .save_to_stream(
                format,
                &mut output_stream,
                &mut op_output,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // read back in store with update manifest
        op_output.rewind().unwrap();
        let um_store =
            Store::from_stream(format, &mut op_output, &mut um_report, &context).unwrap();

        let um = um_store.provenance_claim().unwrap();

        // should be an update manifest
        assert!(um.update_manifest());

        // should not have any errors
        assert!(!um_report.has_any_error());

        // save a different redaction to the same manifest
        let mut claim2 = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        claim2.add_claim_generator_info(cgi);

        // created redacted uri
        let redacted_uri2 = to_assertion_uri(pc.label(), TEST_USER_ASSERTION);

        let mut redacted_store2 = Store::load_ingredient_to_claim(
            &mut claim2,
            &ingredient_vec,
            Some(vec![redacted_uri2]),
            &context,
        )
        .unwrap();

        let ingredient_hashes2 = restored_store.get_manifest_box_hashes(pc);
        let parent_hashed_uri2 = HashedUri::new(
            restored_store.provenance_path().unwrap(),
            Some(pc.alg().to_string()),
            &ingredient_hashes2.manifest_box_hash,
        );
        let signature_hashed_uri2 = HashedUri::new(
            to_signature_uri(pc.label()),
            Some(pc.alg().to_string()),
            &ingredient_hashes2.signature_box_hash,
        );

        let validation_results2 = ValidationResults::from_store(&restored_store, &report);

        let ingredient2 = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(
                Some(parent_hashed_uri2),
                Some(signature_hashed_uri2),
            ) // mandatory for v3
            .set_validation_results(Some(validation_results2)); // mandatory for v3

        claim2.add_assertion(&ingredient2).unwrap();

        // create mandatory opened action (optional for update manifest)
        let ingredient2 = claim2.ingredient_assertions()[0];
        let ingregient_uri2 = to_assertion_uri(claim2.label(), &ingredient2.label());
        let ingredient_hashed_uri2 = HashedUri::new(
            ingregient_uri2,
            Some(claim2.alg().to_owned()),
            ingredient2.hash(),
        );

        let opened2 = Action::new("c2pa.opened")
            .set_parameter("ingredients", vec![ingredient_hashed_uri2])
            .unwrap();
        let em2 = Action::new("c2pa.edited.metadata");
        let actions2 = Actions::new().add_action(opened2).add_action(em2);

        // add action (this is optional for update manifest)
        claim2.add_assertion(&actions2).unwrap();

        redacted_store2.commit_update_manifest(claim2).unwrap();
        output_stream.rewind().unwrap();
        let mut op2_output = std::io::Cursor::new(Vec::new());
        redacted_store2
            .save_to_stream(
                format,
                &mut output_stream,
                &mut op2_output,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // add ingredient again without redaction to make sure conflict is resolved with current redaction
        let mut new_claim = Claim::new("adobe unit test", Some("update_manifest_2"), 2);
        // ClaimGeneratorInfo is mandatory in Claim V2
        let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
        new_claim.add_claim_generator_info(cgi);

        // load ingredient with SCHEMA_ORG redaction
        op_output.rewind().unwrap();
        Store::load_ingredient_to_claim(
            &mut new_claim,
            &load_jumbf_from_stream(format, &mut op_output).unwrap(),
            None,
            &context,
        )
        .unwrap();

        // load original ingredient with TEST_USER_ASSERTION redaction
        op2_output.rewind().unwrap();
        Store::load_ingredient_to_claim(
            &mut new_claim,
            &load_jumbf_from_stream(format, &mut op2_output).unwrap(),
            None,
            &context,
        )
        .unwrap();

        // Check that both redactions are present
        let redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
        assert!(redacted_claim
            .get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
            .is_none());

        assert!(redacted_claim
            .get_assertion(TEST_USER_ASSERTION, 0)
            .is_none());
    }

    #[test]
    fn test_claim_decoding() {
        // modify a required field label in the claim - causes failure to read claim from cbor
        let report = patch_and_report("C.jpg", b"claim_generator", b"claim_generatur");
        assert!(!report.logged_items().is_empty());
        assert!(report.logged_items()[0]
            .err_val
            .as_ref()
            .unwrap()
            .starts_with("ClaimDecoding"))
    }

    #[test]
    fn test_claim_modified() {
        // replace the title that is inside the claim data - should cause signature to not match
        let report = patch_and_report("C.jpg", b"C.jpg", b"X.jpg");
        assert!(!report.logged_items().is_empty());
        // note in the older validation statuses, this was an error, but now it is informational
        assert!(report.has_status(validation_status::TIMESTAMP_MISMATCH));
    }

    #[test]
    fn test_assertion_hash_mismatch() {
        // modifies content of an action assertion - causes an assertion hashuri mismatch
        let report = patch_and_report("CA.jpg", b"brightnesscontrast", b"brightnesscontraxx");
        let first_error = report.filter_errors().next().cloned().unwrap();

        assert_eq!(
            first_error.validation_status.as_deref(),
            Some(validation_status::ASSERTION_HASHEDURI_MISMATCH)
        );
    }

    #[test]
    fn test_claim_missing() {
        // patch jumbf url from c2pa_manifest field in an ingredient to cause claim_missing
        // note this includes hex for Jumbf blocks, so may need some manual tweaking
        const SEARCH_BYTES: &[u8] =
            b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuid:";
        const REPLACE_BYTES: &[u8] =
            b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuix:";
        let report = patch_and_report("CIE-sig-CA.jpg", SEARCH_BYTES, REPLACE_BYTES);

        assert!(report.has_status(validation_status::ASSERTION_HASHEDURI_MISMATCH));
        assert!(report.has_status(validation_status::INGREDIENT_MANIFEST_MISSING));
    }

    #[test]
    fn test_display() {
        let context = Context::new();
        let (format, mut input_stream, _output_stream) = create_test_streams("CA.jpg");

        let mut report = StatusTracker::default();
        let store = Store::from_stream(format, &mut input_stream, &mut report, &context)
            .expect("from_stream");

        assert!(!report.has_any_error());
        println!("store = {store}");
    }

    #[test]
    fn test_no_alg() {
        let (format, mut input_stream, _output_stream) = create_test_streams("no_alg.jpg");
        let mut report = StatusTracker::default();
        let _store = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());

        assert!(report.has_status(ALGORITHM_UNSUPPORTED));
    }

    /// sample of adding timestamp assertion
    /*fn send_timestamp_request(message: &[u8]) -> Result<Vec<u8>> {
        let url = "http://timestamp.digicert.com";

        let body = crate::crypto::time_stamp::default_rfc3161_message(message)?;
        let headers = None;

        let bytes =
            crate::crypto::time_stamp::default_rfc3161_request(url, headers, &body, message)
                .map_err(|_e| Error::OtherError("timestamp token not found".into()))?;

        let token = crate::crypto::cose::timestamptoken_from_timestamprsp(&bytes)
            .ok_or(Error::OtherError("timestamp token not found".into()))?;

        Ok(token)
    }*/

    #[test]
    fn test_legacy_ingredient_hash() {
        let context = Context::new();
        // test 1.0 ingredient hash
        let (format, input_stream, _output_stream) =
            create_test_streams("legacy_ingredient_hash.jpg");
        let mut report = StatusTracker::default();
        let store =
            Store::from_stream(format, input_stream, &mut report, &context).expect("from_stream");
        println!("store = {store}");
    }

    #[test]
    #[ignore = "this test is not generating the expected errors - the test image cert has expired"]
    fn test_bmff_legacy() {
        let mut context = Context::new();
        context.settings_mut().verify.verify_trust = false;

        let (format, mut input_stream, _output_stream) = create_test_streams("legacy.mp4");
        // test 1.0 bmff hash
        let mut report = StatusTracker::default();
        let store = Store::from_stream(format, &mut input_stream, &mut report, &context);
        println!("store = {report:#?}");
        // expect action error
        assert!(store.is_err());
        assert!(report.has_error(Error::ValidationRule(
            "opened, placed and removed items must have ingredient(s) parameters".into()
        )));
        assert!(report.filter_errors().count() == 2);
    }

    #[test]
    fn test_bmff_fragments() {
        let context = Context::new();
        let init_stream_path = fixture_path("dashinit.mp4");
        let segment_stream_path = fixture_path("dash1.m4s");

        let init_stream = std::fs::File::open(init_stream_path).unwrap();
        let init_stream = std::io::BufReader::new(init_stream);
        let segment_stream = std::fs::File::open(segment_stream_path).unwrap();
        let segment_stream = std::io::BufReader::new(segment_stream);

        let mut report = StatusTracker::default();
        let store = Store::load_fragment_from_stream(
            "mp4",
            init_stream,
            segment_stream,
            &mut report,
            &context,
        )
        .expect("load_from_asset");
        println!("store = {store}");
    }

    #[test]
    fn test_bmff_jumbf_generation() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[test]
    fn test_bmff_jumbf_generation_qt() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("c.mov");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[test]
    fn test_bmff_jumbf_generation_claim_v1() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = crate::utils::test::create_test_claim_v1().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[cfg(feature = "file_io")]
    #[test]
    fn test_jumbf_generation_with_bmffv3_fixed_block_size() {
        // use Merkle tree with 1024 byte chunks
        let context = crate::context::Context::new();

        // use Merkle tree with 1024 byte chunks
        crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) =
            create_test_streams("BigBuckBunny_320x180.mp4");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[test]
    fn test_jumbf_generation_with_bmffv3_fixed_block_size_no_proof() {
        let context = crate::context::Context::new();

        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        // use Merkle tree with 1024 byte chunks an 0 proofs (no UUID boxes)
        crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();
        crate::settings::set_settings_value("core.merkle_tree_max_proofs", 0).unwrap();

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[test]
    fn test_jumbf_generation_with_bmffv3_fixed_block_size_stream() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        // use Merkle tree with 1024 byte chunks
        crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        // can we read back in
        output_stream.set_position(0);
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        println!("store = {new_store}");
    }

    #[test]
    fn test_bmff_jumbf_stream_generation() {
        let mut context = Context::new();
        context.settings_mut().verify.verify_after_reading = false;

        // test adding to actual image
        let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list.
        store.commit_claim(claim1).unwrap();

        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        let mut report = StatusTracker::default();

        output_stream.set_position(0);

        let (manifest_bytes, _) =
            Store::load_jumbf_from_stream(format, &mut input_stream, &context).unwrap();

        let _new_store = {
            Store::from_manifest_data_and_stream(
                &manifest_bytes,
                format,
                &mut output_stream,
                &mut report,
                &context,
            )
            .unwrap()
        };
        println!("report = {report:#?}");
        assert!(!report.has_any_error());
    }

    #[test]
    fn test_removed_jumbf() {
        let context = Context::new();
        let (format, mut input_stream, _output_stream) = create_test_streams("no_manifest.jpg");

        let mut report = StatusTracker::default();

        // can we read back in
        let result = Store::from_stream(format, &mut input_stream, &mut report, &context);

        assert!(result.is_err());
        assert!(matches!(result, Err(Error::JumbfNotFound)));
        assert!(report.has_error(Error::JumbfNotFound));
    }

    // #[test]
    // #[cfg(feature = "file_io")]
    // fn test_external_manifest_sidecar() {
    //     // test adding to actual image
    //     let ap = fixture_path("libpng-test.png");
    //     let temp_dir = tempdirectory().expect("temp dir");
    //     let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png");

    //     let sidecar = op.with_extension(MANIFEST_STORE_EXT);

    //     // Create claims store.
    //     let mut store = Store::new();

    //     // Create a new claim.
    //     let mut claim = create_test_claim().unwrap();

    //     // set claim for side car generation
    //     claim.set_external_manifest();

    //     // Do we generate JUMBF?
    //     let signer = test_signer(SigningAlg::Ps256);

    //     store.commit_claim(claim).unwrap();

    //     let saved_manifest = store.save_to_asset(&ap, signer.as_ref(), &op).unwrap();

    //     assert!(sidecar.exists());

    //     // load external manifest
    //     let loaded_manifest = std::fs::read(sidecar).unwrap();

    //     // compare returned to external
    //     assert_eq!(saved_manifest, loaded_manifest);

    //     // test auto loading of sidecar with validation
    //     let mut validation_log =
    //         StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
    //     Store::load_from_asset(&op, true, &mut validation_log).unwrap();
    // }

    // generalize test for multipe file types
    fn external_manifest_test(file_name: &str) {
        use crate::utils::test::{run_file_test, TestFileSetup};

        run_file_test(file_name, |setup: &TestFileSetup| {
            let context = crate::context::Context::new();

            // Create claims store.
            let mut store = Store::from_context(&context);

            // Create a new claim.
            let mut claim = create_test_claim().unwrap();

            // Do we generate JUMBF?
            let signer = test_signer(SigningAlg::Ps256);

            // set claim for side car with remote manifest embedding generation
            claim.set_remote_manifest(setup.sidecar_url()).unwrap();

            store.commit_claim(claim).unwrap();

            // Use streams from TestFileSetup - same pattern as create_test_streams
            let (format, mut input_stream, mut output_stream) = setup.create_streams();
            println!("format = {format}");
            let saved_manifest = store
                .save_to_stream(
                    format,
                    &mut input_stream,
                    &mut output_stream,
                    signer.as_ref(),
                    &context,
                )
                .unwrap();

            // load the jumbf back into a store
            let mut asset_reader = std::fs::File::open(&setup.output_path).unwrap();
            let ext_ref = crate::utils::xmp_inmemory_utils::XmpInfo::from_source(
                &mut asset_reader,
                setup.extension(),
            )
            .provenance
            .unwrap();

            // cases might be different on different filesystems
            assert_eq!(ext_ref.to_lowercase(), setup.sidecar_url().to_lowercase());

            // make sure it validates using streams with external manifest data
            let mut validation_log =
                StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
            let mut validation_stream = std::fs::File::open(&setup.output_path).unwrap();
            Store::from_manifest_data_and_stream(
                &saved_manifest,
                format,
                &mut validation_stream,
                &mut validation_log,
                &context,
            )
            .unwrap();
        });
    }

    #[test]
    fn test_external_manifest_embedded_png() {
        external_manifest_test("libpng-test.png");
    }

    #[test]
    fn test_external_manifest_embedded_tiff() {
        external_manifest_test("TUSCANY.TIF");
    }

    #[test]
    fn test_external_manifest_embedded_webp() {
        external_manifest_test("sample1.webp");
    }

    #[test]
    fn test_user_guid_external_manifest_embedded() {
        let context = crate::context::Context::new();

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let mut claim = create_test_claim().unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // start with base url
        let fp = format!("file:/{}", "temp_sidecar.c2pa");
        let url = url::Url::parse(&fp).unwrap();

        let url_string: String = url.into();

        // set claim for side car with remote manifest embedding generation
        claim.set_embed_remote_manifest(url_string.clone()).unwrap();

        store.commit_claim(claim).unwrap();

        let saved_manifest = store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // Get the data from output stream to check for external reference
        output_stream.rewind().unwrap();
        let output_data = output_stream.get_ref().clone();
        let mut output_reader = Cursor::new(output_data);

        let ext_ref =
            crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut output_reader, format)
                .provenance
                .unwrap();

        assert_eq!(ext_ref, url_string);

        // make sure it validates using the manifest data and stream
        let mut validation_log =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        output_stream.rewind().unwrap();
        Store::from_manifest_data_and_stream(
            &saved_manifest,
            format,
            &mut output_stream,
            &mut validation_log,
            &context,
        )
        .unwrap();
    }

    #[c2pa_test_async]
    async fn test_jumbf_generation_stream() {
        let context = crate::context::Context::new();

        let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec();
        // convert buffer to cursor with Read/Write/Seek capability
        let mut buf_io = Cursor::new(file_buffer);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = async_test_signer(SigningAlg::Ps256);

        store.commit_claim(claim1).unwrap();

        let result: Vec<u8> = Vec::new();
        let mut result_stream = Cursor::new(result);

        store
            .save_to_stream_async(
                "image/jpeg",
                &mut buf_io,
                &mut result_stream,
                signer.as_ref(),
                &context,
            )
            .await
            .unwrap();

        // rewind the result stream to read from it
        result_stream.rewind().unwrap();

        // make sure we can read from new file
        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream_async("image/jpeg", &mut result_stream, &mut report, &context)
                .await
                .unwrap();

        assert!(!report.has_any_error());
        // std::fs::write("target/test.jpg", result).unwrap();
    }

    #[test]
    fn test_tiff_jumbf_generation() {
        let context = crate::context::Context::new();

        // Create test streams from fixture
        let (format, mut input_stream, mut output_stream) = create_test_streams("TUSCANY.TIF");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits
        store.commit_claim(claim1).unwrap();
        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut output_stream,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        println!("Provenance: {}\n", store.provenance_path().unwrap());

        let mut report = StatusTracker::default();

        // read from new file
        output_stream.rewind().unwrap();
        let new_store =
            Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());

        // dump store and compare to original
        for claim in new_store.claims() {
            let _restored_json = claim
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();
            let _orig_json = store
                .get_claim(claim.label())
                .unwrap()
                .to_json(AssertionStoreJsonFormat::OrderedList, false)
                .unwrap();

            println!(
                "Claim: {} \n{}",
                claim.label(),
                claim
                    .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
                    .expect("could not restore from json")
            );

            for hashed_uri in claim.assertions() {
                let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
                claim
                    .get_claim_assertion(&label, instance)
                    .expect("Should find assertion");
            }
        }
    }

    #[c2pa_test_async]
    #[cfg(feature = "file_io")]
    async fn test_boxhash_embeddable_manifest_async() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let ap = fixture_path("boxhash.jpg");
        let box_hash_path = fixture_path("boxhash.json");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let mut claim = create_test_claim().unwrap();

        // add box hash for CA.jpg
        let box_hash_data = std::fs::read(box_hash_path).unwrap();
        let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap();
        let box_hash = BoxHash::from_json_assertion(&assertion).unwrap();
        claim.add_assertion(&box_hash).unwrap();

        store.commit_claim(claim).unwrap();

        // Do we generate JUMBF?
        let signer = async_test_signer(SigningAlg::Ps256);

        // get the embeddable manifest
        let em = store
            .get_box_hashed_embeddable_manifest_async(&signer, &context)
            .await
            .unwrap();

        // get composed version for embedding to JPEG
        let cm = Store::get_composed_manifest(&em, "image/jpeg").unwrap();

        // insert manifest into output asset
        let jpeg_io = get_assetio_handler_from_path(&ap).unwrap();
        let ol = jpeg_io.get_object_locations(&ap).unwrap();

        let cai_loc = ol
            .iter()
            .find(|o| o.htype == HashBlockObjectType::Cai)
            .unwrap();

        // remove any existing manifest
        jpeg_io.read_cai_store(&ap).unwrap();

        // build new asset in memory inserting new manifest
        let outbuf = Vec::new();
        let mut out_stream = Cursor::new(outbuf);
        let mut input_file = std::fs::File::open(&ap).unwrap();

        // write before
        let mut before = vec![0u8; cai_loc.offset];
        input_file.read_exact(before.as_mut_slice()).unwrap();
        out_stream.write_all(&before).unwrap();

        // write composed bytes
        out_stream.write_all(&cm).unwrap();

        // write bytes after
        let mut after_buf = Vec::new();
        input_file.read_to_end(&mut after_buf).unwrap();
        out_stream.write_all(&after_buf).unwrap();

        out_stream.rewind().unwrap();

        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream_async("image/jpeg", &mut out_stream, &mut report, &context)
                .await
                .unwrap();

        assert!(!report.has_any_error());
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_boxhash_embeddable_manifest() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let ap = fixture_path("boxhash.jpg");
        let box_hash_path = fixture_path("boxhash.json");

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let mut claim = create_test_claim().unwrap();

        // add box hash for CA.jpg
        let box_hash_data = std::fs::read(box_hash_path).unwrap();
        let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap();
        let box_hash = BoxHash::from_json_assertion(&assertion).unwrap();
        claim.add_assertion(&box_hash).unwrap();

        store.commit_claim(claim).unwrap();

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // get the embeddable manifest
        let em = store
            .get_box_hashed_embeddable_manifest(signer.as_ref(), &context)
            .unwrap();

        // get composed version for embedding to JPEG
        let cm = Store::get_composed_manifest(&em, "jpg").unwrap();

        // insert manifest into output asset
        let jpeg_io = get_assetio_handler_from_path(&ap).unwrap();
        let ol = jpeg_io.get_object_locations(&ap).unwrap();

        let cai_loc = ol
            .iter()
            .find(|o| o.htype == HashBlockObjectType::Cai)
            .unwrap();

        // remove any existing manifest
        jpeg_io.read_cai_store(&ap).unwrap();

        // build new asset in memory inserting new manifest
        let outbuf = Vec::new();
        let mut out_stream = Cursor::new(outbuf);
        let mut input_file = std::fs::File::open(&ap).unwrap();

        // write before
        let mut before = vec![0u8; cai_loc.offset];
        input_file.read_exact(before.as_mut_slice()).unwrap();
        out_stream.write_all(&before).unwrap();

        // write composed bytes
        out_stream.write_all(&cm).unwrap();

        // write bytes after
        let mut after_buf = Vec::new();
        input_file.read_to_end(&mut after_buf).unwrap();
        out_stream.write_all(&after_buf).unwrap();

        out_stream.rewind().unwrap();

        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream("image/jpeg", &mut out_stream, &mut report, &context).unwrap();

        assert!(!report.has_any_error());
    }

    #[c2pa_test_async]
    #[cfg(feature = "file_io")]
    async fn test_datahash_embeddable_manifest_async() {
        let context = crate::context::Context::new();

        // test adding to actual image
        use std::io::SeekFrom;

        let ap = fixture_path("cloud.jpg");

        // Do we generate JUMBF?
        let signer = async_test_signer(SigningAlg::Ps256);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim = create_test_claim().unwrap();

        store.commit_claim(claim).unwrap();

        // get a placeholder the manifest
        let placeholder = store
            .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg")
            .unwrap();

        let temp_dir = tempdirectory().unwrap();
        let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
        let mut output_file = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(true)
            .open(&output)
            .unwrap();

        // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder)
        let offset =
            write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();

        // build manifest to insert in the hole

        // create an hash exclusion for the manifest
        let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
        let exclusions = vec![exclusion];

        let mut dh = DataHash::new("source_hash", "sha256");
        dh.exclusions = Some(exclusions);

        // get the embeddable manifest, letting API do the hashing
        output_file.rewind().unwrap();
        let cm = store
            .get_data_hashed_embeddable_manifest_async(
                &dh,
                &signer,
                "jpeg",
                Some(&mut output_file),
                &context,
            )
            .await
            .unwrap();

        // path in new composed manifest
        output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
        output_file.write_all(&cm).unwrap();

        output_file.rewind().unwrap();
        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream_async("image/jpeg", &mut output_file, &mut report, &context)
                .await
                .unwrap();

        assert!(!report.has_any_error());
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_datahash_embeddable_manifest() {
        let context = crate::context::Context::new();

        // test adding to actual image

        use std::io::SeekFrom;
        let ap = fixture_path("cloud.jpg");

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim = create_test_claim().unwrap();

        store.commit_claim(claim).unwrap();

        // get a placeholder the manifest
        let placeholder = store
            .get_data_hashed_manifest_placeholder(Signer::reserve_size(&signer), "jpeg")
            .unwrap();

        let temp_dir = tempdirectory().unwrap();
        let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
        let mut output_file = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(true)
            .open(&output)
            .unwrap();

        // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder)
        let offset =
            write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();

        // build manifest to insert in the hole

        // create an hash exclusion for the manifest
        let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
        let exclusions = vec![exclusion];

        let mut dh = DataHash::new("source_hash", "sha256");
        dh.exclusions = Some(exclusions);

        // get the embeddable manifest, letting API do the hashing
        output_file.rewind().unwrap();
        let cm = store
            .get_data_hashed_embeddable_manifest(
                &dh,
                signer.as_ref(),
                "jpeg",
                Some(&mut output_file),
                &context,
            )
            .unwrap();

        // path in new composed manifest
        output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
        output_file.write_all(&cm).unwrap();

        output_file.rewind().unwrap();
        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap();

        assert!(!report.has_any_error());
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_datahash_embeddable_manifest_user_hashed() {
        let context = crate::context::Context::new();

        use std::io::SeekFrom;

        use sha2::Digest;

        // test adding to actual image
        let ap = fixture_path("cloud.jpg");

        let mut hasher = Hasher::SHA256(Sha256::new());

        // Do we generate JUMBF?
        let signer = test_signer(SigningAlg::Ps256);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim = create_test_claim().unwrap();

        store.commit_claim(claim).unwrap();

        // get a placeholder for the manifest
        let placeholder = store
            .get_data_hashed_manifest_placeholder(Signer::reserve_size(&signer), "jpeg")
            .unwrap();

        let temp_dir = tempdirectory().unwrap();
        let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
        let mut output_file = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(true)
            .open(&output)
            .unwrap();

        // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder)
        let offset =
            write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, Some(&mut hasher))
                .unwrap();

        // create target data hash
        // create an hash exclusion for the manifest
        let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
        let exclusions = vec![exclusion];

        //input_file.rewind().unwrap();
        let mut dh = DataHash::new("source_hash", "sha256");
        dh.hash = Hasher::finalize(hasher);
        dh.exclusions = Some(exclusions);

        // get the embeddable manifest, using user hashing
        let cm = store
            .get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None, &context)
            .unwrap();

        // path in new composed manifest
        output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
        output_file.write_all(&cm).unwrap();

        output_file.rewind().unwrap();
        let mut report = StatusTracker::default();
        let _new_store =
            Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap();

        assert!(!report.has_any_error());
    }

    #[test]
    fn test_dynamic_assertions() {
        let context = crate::context::Context::new();

        #[derive(Serialize)]
        struct TestAssertion {
            my_tag: String,
        }

        #[derive(Debug)]
        struct TestDynamicAssertion {}

        impl DynamicAssertion for TestDynamicAssertion {
            fn label(&self) -> String {
                "com.mycompany.myassertion".to_string()
            }

            fn reserve_size(&self) -> Result<usize> {
                let assertion = TestAssertion {
                    my_tag: "some value I will replace".to_string(),
                };
                Ok(c2pa_cbor::to_vec(&assertion)?.len())
            }

            fn content(
                &self,
                _label: &str,
                _size: Option<usize>,
                claim: &PartialClaim,
            ) -> Result<DynamicAssertionContent> {
                assert!(claim
                    .assertions()
                    .inspect(|a| {
                        dbg!(a);
                    })
                    .any(|a| a.url().contains("c2pa.hash")));

                let assertion = TestAssertion {
                    my_tag: "some value I will replace".to_string(),
                };

                Ok(DynamicAssertionContent::Cbor(
                    c2pa_cbor::to_vec(&assertion).unwrap(),
                ))
            }
        }

        /// This is an signer wrapped around a local temp signer,
        /// that implements the dynamic assertion trait.
        struct DynamicSigner(Box<dyn Signer>);

        impl DynamicSigner {
            fn new() -> Self {
                Self(test_signer(SigningAlg::Ps256))
            }
        }

        impl crate::Signer for DynamicSigner {
            fn sign(&self, data: &[u8]) -> crate::error::Result<Vec<u8>> {
                self.0.sign(data)
            }

            fn alg(&self) -> SigningAlg {
                self.0.alg()
            }

            fn certs(&self) -> crate::Result<Vec<Vec<u8>>> {
                self.0.certs()
            }

            fn reserve_size(&self) -> usize {
                self.0.reserve_size()
            }

            fn time_authority_url(&self) -> Option<String> {
                self.0.time_authority_url()
            }

            fn ocsp_val(&self) -> Option<Vec<u8>> {
                self.0.ocsp_val()
            }

            // Returns our dynamic assertion here.
            fn dynamic_assertions(
                &self,
            ) -> Vec<Box<dyn crate::dynamic_assertion::DynamicAssertion>> {
                vec![Box::new(TestDynamicAssertion {})]
            }
        }

        let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec();
        // convert buffer to cursor with Read/Write/Seek capability
        let mut buf_io = Cursor::new(file_buffer);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = DynamicSigner::new();

        store.commit_claim(claim1).unwrap();

        let result: Vec<u8> = Vec::new();
        let mut result_stream = Cursor::new(result);

        store
            .save_to_stream("jpeg", &mut buf_io, &mut result_stream, &signer, &context)
            .unwrap();

        // rewind the result stream to read from it
        result_stream.rewind().unwrap();

        // make sure we can read from new file
        let mut report = StatusTracker::default();
        let new_store =
            Store::from_stream("image/jpeg", &mut result_stream, &mut report, &context).unwrap();

        println!("new_store: {new_store}");

        assert!(!report.has_any_error());
        // std::fs::write("target/test.jpg", result).unwrap();
    }

    #[c2pa_test_async]
    async fn test_async_dynamic_assertions() {
        use async_trait::async_trait;

        let context = crate::context::Context::new();

        #[derive(Serialize)]
        struct TestAssertion {
            my_tag: String,
        }

        #[derive(Debug)]
        struct TestDynamicAssertion {}

        #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
        #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
        impl AsyncDynamicAssertion for TestDynamicAssertion {
            fn label(&self) -> String {
                "com.mycompany.myassertion".to_string()
            }

            fn reserve_size(&self) -> Result<usize> {
                let assertion = TestAssertion {
                    my_tag: "some value I will replace".to_string(),
                };
                Ok(c2pa_cbor::to_vec(&assertion)?.len())
            }

            async fn content(
                &self,
                _label: &str,
                _size: Option<usize>,
                claim: &PartialClaim,
            ) -> Result<DynamicAssertionContent> {
                assert!(claim
                    .assertions()
                    .inspect(|a| {
                        dbg!(a);
                    })
                    .any(|a| a.url().contains("c2pa.hash")));

                let assertion = TestAssertion {
                    my_tag: "some value I will replace".to_string(),
                };

                Ok(DynamicAssertionContent::Cbor(
                    c2pa_cbor::to_vec(&assertion).unwrap(),
                ))
            }
        }

        /// This is an async signer wrapped around a local temp signer,
        /// that implements the dynamic assertion trait.
        struct DynamicSigner(Box<dyn AsyncSigner>);

        impl DynamicSigner {
            fn new() -> Self {
                Self(async_test_signer(SigningAlg::Ps256))
            }
        }

        #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
        #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
        impl crate::AsyncSigner for DynamicSigner {
            async fn sign(&self, data: Vec<u8>) -> crate::error::Result<Vec<u8>> {
                self.0.sign(data).await
            }

            fn alg(&self) -> SigningAlg {
                self.0.alg()
            }

            fn certs(&self) -> crate::Result<Vec<Vec<u8>>> {
                self.0.certs()
            }

            fn reserve_size(&self) -> usize {
                self.0.reserve_size()
            }

            fn time_authority_url(&self) -> Option<String> {
                self.0.time_authority_url()
            }

            async fn ocsp_val(&self) -> Option<Vec<u8>> {
                self.0.ocsp_val().await
            }

            // Returns our dynamic assertion here.
            fn dynamic_assertions(
                &self,
            ) -> Vec<Box<dyn crate::dynamic_assertion::AsyncDynamicAssertion>> {
                vec![Box::new(TestDynamicAssertion {})]
            }
        }

        let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec();
        // convert buffer to cursor with Read/Write/Seek capability
        let mut buf_io = Cursor::new(file_buffer);

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim1 = create_test_claim().unwrap();

        let signer = DynamicSigner::new();

        store.commit_claim(claim1).unwrap();

        let result: Vec<u8> = Vec::new();
        let mut result_stream = Cursor::new(result);

        store
            .save_to_stream_async("jpeg", &mut buf_io, &mut result_stream, &signer, &context)
            .await
            .unwrap();

        result_stream.rewind().unwrap();

        // make sure we can read from new file
        let mut report = StatusTracker::default();
        let new_store = Store::from_stream_async("jpeg", &mut result_stream, &mut report, &context)
            .await
            .unwrap();

        println!("new_store: {new_store}");

        let result = result_stream.into_inner();

        Store::verify_store_async(
            &new_store,
            Some(&mut ClaimAssetData::Bytes(&result, "jpg")),
            &mut report,
            &context,
        )
        .await
        .unwrap();

        assert!(!report.has_any_error());
        // std::fs::write("target/test.jpg", result).unwrap();
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_fragmented_jumbf_generation() {
        let mut context = crate::context::Context::new();
        context.settings_mut().verify.verify_after_reading = true;

        // test adding to actual image

        let tempdir = tempdirectory().expect("temp dir");
        let output_path = tempdir.path();

        // search folders for init segments
        let mut inits = Vec::new();
        for item in glob::glob(&fixture_path("bunny/**/BigBuckBunny_2s_init.mp4").to_string_lossy())
            .unwrap()
            .flatten()
        {
            inits.push(item);
        }

        // Create claims store.
        let mut store = Store::from_context(&context);

        // Create a new claim.
        let claim = create_test_claim().unwrap();
        store.commit_claim(claim).unwrap();

        // Do we generate JUMBF?
        let signer = test_cawg_signer(SigningAlg::Ps256, &[labels::SCHEMA_ORG_INTERNAL]).unwrap();

        store
            .save_to_bmff_fragmented(
                &inits,
                &PathBuf::from("BigBuckBunny_2s*.m4s"),
                &output_path.to_path_buf(),
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // verify the fragments
        for init_path in &inits {
            let init_name = PathBuf::from(init_path.file_name().unwrap_or_default());
            let init_dir = PathBuf::from(init_path.parent().unwrap().file_name().unwrap());

            let mut output_file = output_path.to_path_buf();
            output_file = output_file.join(&init_dir).join(&init_name);

            let mut init_stream = std::fs::File::open(&output_file).unwrap();

            // build the list of fragments for this init segment based on the glob pattern
            let mut fragments = Vec::new();
            let frag_glob = output_path.join(&init_dir).join("BigBuckBunny_2s*.m4s");
            let frag_glob_str = frag_glob.to_str().unwrap();

            // grab the fragments that go with this init segment
            for entry in glob::glob(frag_glob_str).unwrap() {
                fragments.push(entry.unwrap());
            }

            // check fragments individually
            for entry in &fragments {
                let mut validation_log = StatusTracker::default();

                let mut fragment_stream = std::fs::File::open(entry).unwrap();
                let _manifest = Store::load_fragment_from_stream(
                    "mp4",
                    &mut init_stream,
                    &mut fragment_stream,
                    &mut validation_log,
                    &context,
                )
                .unwrap();
                init_stream.seek(std::io::SeekFrom::Start(0)).unwrap();
                assert!(!validation_log.has_any_error());
            }

            // check all fragments together with the init
            let mut validation_log = StatusTracker::default();
            init_stream.rewind().unwrap();
            let _manifest = Store::load_from_file_and_fragments(
                "mp4",
                &mut init_stream,
                &fragments,
                &mut validation_log,
                &context,
            )
            .unwrap();

            assert!(!validation_log.has_any_error());
        }
    }

    #[test]
    #[cfg(feature = "file_io")]
    fn test_bogus_cert() {
        use crate::builder::{Builder, BuilderIntent};

        // bypass auto sig check
        crate::settings::set_settings_value("verify.verify_after_sign", false).unwrap();
        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();

        let png = include_bytes!("../tests/fixtures/libpng-test.png"); // Randomly generated local Ed25519
        let ed25519 = include_bytes!("../tests/fixtures/certs/ed25519.pem");
        let certs = include_bytes!("../tests/fixtures/certs/es256.pub");

        let mut context = Context::new();
        // bypass auto signature checks
        context.settings_mut().verify.verify_after_sign = false;
        context.settings_mut().verify.verify_trust = false;

        let mut builder = Builder::from_context(context);
        builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));

        let signer =
            crate::create_signer::from_keys(certs, ed25519, SigningAlg::Ed25519, None).unwrap();
        let mut dst = Cursor::new(Vec::new());

        builder
            .sign(&signer, "image/png", &mut Cursor::new(png), &mut dst)
            .unwrap();

        let reader = crate::Reader::default()
            .with_stream("image/png", &mut dst)
            .unwrap();

        assert_eq!(reader.validation_state(), crate::ValidationState::Invalid);
    }

    #[test]
    /// Test that we can we load a store from JUMBF and then convert it back to the identical JUMBF.
    fn test_from_and_to_jumbf() {
        let context = crate::context::Context::new();

        // test adding to actual image
        let ap = fixture_path("C.jpg");

        let mut stream = std::fs::File::open(&ap).unwrap();
        let format = "image/jpeg";

        let (manifest_bytes, _remote_url) =
            Store::load_jumbf_from_stream(format, &mut stream, &context).unwrap();

        let store = Store::from_jumbf_with_context(
            &manifest_bytes,
            &mut StatusTracker::default(),
            &context,
        )
        .unwrap();

        let jumbf = store
            .to_jumbf_internal(0)
            .expect("Failed to convert store to JUMBF");

        assert_eq!(jumbf, manifest_bytes);
    }

    #[c2pa_test_async]
    async fn test_store_load_fragment_from_stream_async() {
        let context = crate::context::Context::new();
        // Use the dash fixtures that are known to work with fragment loading
        // These are the same files used in test_bmff_fragments
        let init_segment = include_bytes!("../tests/fixtures/dashinit.mp4");
        let fragment = include_bytes!("../tests/fixtures/dash1.m4s");

        let mut init_stream = Cursor::new(init_segment);
        let mut fragment_stream = Cursor::new(fragment);

        let format = "mp4";
        let mut validation_log = StatusTracker::default();

        // Test the async fragment loading (this is what we're actually testing)
        let result = Store::load_fragment_from_stream_async(
            format,
            &mut init_stream,
            &mut fragment_stream,
            &mut validation_log,
            &context,
        )
        .await;

        // Same validation as test_fragmented_jumbf_generation - but allow expected certificate trust errors
        match result {
            Ok(_manifest) => {
                // Verify that we successfully loaded a store from the fragment
                // The store should contain the manifest data from the fragment

                // Check for validation errors, but allow expected certificate trust errors
                if validation_log.has_any_error() {
                    let errors: Vec<_> = validation_log.filter_errors().collect();
                    let has_unexpected_errors = errors.iter().any(|item| {
                        // Allow certificate trust errors (these are expected for test fixtures)
                        // Check if the error is a CertificateTrustError
                        if let Some(err_val) = &item.err_val {
                            if err_val.contains("CertificateTrustError") {
                                return false; // This error is expected
                            }
                        }

                        // Any other errors are unexpected
                        true
                    });

                    if has_unexpected_errors {
                        panic!("Validation log contains unexpected errors: {validation_log:?}",);
                    }
                    // Certificate trust errors are OK for test fixtures
                }
            }
            Err(e) => {
                // Errors are NOT acceptable - this should work with fragments that contain manifest data
                panic!("Failed to load fragment from stream: {e:?}");
            }
        }
    }

    #[test]
    fn test_stream_context_handling_for_c_ffi_layer() {
        // Building a stream wrapper that simulates C FFI behavior

        /// Stream wrapper that simulates the streams like it would be in C FFI layer.
        /// Needed to repro a use-after-free bug from the FFI layer.
        struct FlushTrackingStream<T: CAIReadWrite> {
            inner: T,
            buffer: Vec<u8>,
            flush_called: Arc<AtomicBool>,
            dropped: Arc<AtomicBool>,
        }

        impl<T: CAIReadWrite> FlushTrackingStream<T> {
            fn new(inner: T, flush_called: Arc<AtomicBool>, dropped: Arc<AtomicBool>) -> Self {
                Self {
                    inner,
                    buffer: Vec::new(),
                    flush_called,
                    dropped,
                }
            }
        }

        impl<T: CAIReadWrite> Read for FlushTrackingStream<T> {
            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
                self.inner.read(buf)
            }
        }

        impl<T: CAIReadWrite> Write for FlushTrackingStream<T> {
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                // Simulate buffering behavior
                if !self.buffer.is_empty() {
                    // Write previous buffer first
                    self.inner.write_all(&self.buffer)?;
                }
                // Keep the current write in the buffer
                self.buffer = buf.to_vec();
                Ok(buf.len())
            }

            fn flush(&mut self) -> std::io::Result<()> {
                // Mark that flush was called
                self.flush_called.store(true, Ordering::SeqCst);

                // Actually write the buffered data
                if !self.buffer.is_empty() {
                    self.inner.write_all(&self.buffer)?;
                    self.buffer.clear();
                }
                self.inner.flush()
            }
        }

        impl<T: CAIReadWrite> Seek for FlushTrackingStream<T> {
            fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
                // Check if we're trying to seek without flushing first
                // Simulates a crash scenario where the stream is accessed
                // after data should have been written but wasn't
                if !self.buffer.is_empty() && !self.flush_called.load(Ordering::SeqCst) {
                    // This is a use-after-free error simulation for the way the FFI layer handles streams
                    return Err(std::io::Error::other(
                        "Simulated use-after-free: attempting to seek with unflushed buffer (similar to accessing freed FFI context)",
                    ));
                }
                self.inner.seek(pos)
            }

            fn rewind(&mut self) -> std::io::Result<()> {
                // Suffer the same troubles as seek
                if !self.buffer.is_empty() && !self.flush_called.load(Ordering::SeqCst) {
                    return Err(std::io::Error::other(
                        "Simulated use-after-free: attempting to rewind with unflushed buffer (similar to accessing freed FFI context)",
                    ));
                }
                self.inner.rewind()
            }

            fn stream_position(&mut self) -> std::io::Result<u64> {
                self.inner.stream_position()
            }
        }

        impl<T: CAIReadWrite> Drop for FlushTrackingStream<T> {
            fn drop(&mut self) {
                self.dropped.store(true, Ordering::SeqCst);
                // Dropping without flush frees the context
            }
        }

        // The actual test starts here...
        let context = Context::new();

        let (format, mut input_stream, output_stream) = create_test_streams("earth_apollo17.jpg");

        let flush_called = Arc::new(AtomicBool::new(false));
        let dropped = Arc::new(AtomicBool::new(false));

        let mut tracking_stream =
            FlushTrackingStream::new(output_stream, flush_called.clone(), dropped.clone());

        let mut store = Store::from_context(&context);

        let cgi = ClaimGeneratorInfo::new("flush_test");
        let mut claim = Claim::new("test", Some("flush_test"), 1);
        create_capture_claim(&mut claim).unwrap();
        claim.add_claim_generator_info(cgi);

        let signer = test_signer(SigningAlg::Ps256);

        store.commit_claim(claim).unwrap();

        store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut tracking_stream,
                signer.as_ref(),
                &context,
            )
            .expect("save_to_stream should succeed and properly flush the stream");

        // Verify that flush was called: if not, we would have crashed on rewind
        // when used through the C FFI layers with streams
        assert!(
            flush_called.load(Ordering::SeqCst),
            "flush() must be called during save_to_stream to prevent use-after-free in FFI scenarios"
        );

        // Verify buffer is empty (data was actually written), nothing dangling
        assert!(
            tracking_stream.buffer.is_empty(),
            "buffer should be empty after flush"
        );
    }

    /// Another test for stream context issues, with unsafe pointers
    #[test]
    fn test_stream_context_handling_for_c_ffi_layer_no_use_after_free() {
        use std::io::Cursor;

        // This struct simulates an FFI stream context that could be freed any time
        #[repr(C)]
        struct StreamContext {
            data: Cursor<Vec<u8>>,
            // Used to detect if memory has been freed
            magic: u64,
        }

        impl StreamContext {
            fn new() -> Self {
                Self {
                    data: Cursor::new(Vec::new()),
                    // Magic number to detect valid context (not unintialized)
                    magic: 0xdeadbeefcafebabe,
                }
            }

            fn mark_freed(&mut self) {
                // 0xCCCCCCCCCCCCCCCC is a value used by some compilers
                // to flag uninitialized memory, so we'll do the same here
                self.magic = 0xcccccccccccccccc;
            }

            fn check_valid(&self) -> std::io::Result<()> {
                if self.magic != 0xdeadbeefcafebabe {
                    return Err(std::io::Error::other(
                        format!(
                            "Use-after-free detected: StreamContext magic is 0x{:X} (expected 0xDEADBEEFCAFEBABE)",
                            self.magic
                        ),
                    ));
                }
                Ok(())
            }
        }

        struct UnsafeStream {
            context: *mut StreamContext,
            flush_called: Arc<AtomicBool>,
        }

        impl UnsafeStream {
            fn new(flush_called: Arc<AtomicBool>) -> Self {
                let context = Box::into_raw(Box::new(StreamContext::new()));
                Self {
                    context,
                    flush_called,
                }
            }

            unsafe fn context_ref(&self) -> &StreamContext {
                &*self.context
            }

            unsafe fn context_mut(&mut self) -> &mut StreamContext {
                &mut *self.context
            }
        }

        impl Read for UnsafeStream {
            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
                unsafe {
                    self.context_ref().check_valid()?;
                    self.context_mut().data.read(buf)
                }
            }
        }

        impl Write for UnsafeStream {
            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                unsafe {
                    self.context_ref().check_valid()?;
                    self.context_mut().data.write(buf)
                }
            }

            fn flush(&mut self) -> std::io::Result<()> {
                unsafe {
                    self.context_ref().check_valid()?;
                    self.flush_called.store(true, Ordering::SeqCst);
                    self.context_mut().data.flush()
                }
            }
        }

        impl Seek for UnsafeStream {
            fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
                unsafe {
                    // This is where the crash would happen in the FFI layer
                    // if the context was freed before flush
                    self.context_ref().check_valid()?;
                    self.context_mut().data.seek(pos)
                }
            }

            fn rewind(&mut self) -> std::io::Result<()> {
                unsafe {
                    self.context_ref().check_valid()?;
                    self.context_mut().data.rewind()
                }
            }

            fn stream_position(&mut self) -> std::io::Result<u64> {
                unsafe {
                    self.context_ref().check_valid()?;
                    self.context_mut().data.stream_position()
                }
            }
        }

        impl Drop for UnsafeStream {
            fn drop(&mut self) {
                unsafe {
                    if !self.context.is_null() {
                        // Simulate what happens in FFI: if flush wasn't called,
                        // mark the context as freed aka uninitialized memory
                        if !self.flush_called.load(Ordering::SeqCst) {
                            (*self.context).mark_freed();
                        }
                        // Clean up the context
                        drop(Box::from_raw(self.context));
                    }
                }
            }
        }

        unsafe impl Send for UnsafeStream {}
        unsafe impl Sync for UnsafeStream {}

        let context = Context::new();

        let (format, mut input_stream, _) = create_test_streams("earth_apollo17.jpg");

        let flush_called = Arc::new(AtomicBool::new(false));
        let mut unsafe_stream = UnsafeStream::new(flush_called.clone());

        let mut store = Store::from_context(&context);

        let cgi = ClaimGeneratorInfo::new("unsafe_flush_test");
        let mut claim = Claim::new("test", Some("unsafe_flush_test"), 1);
        create_capture_claim(&mut claim).unwrap();
        claim.add_claim_generator_info(cgi);

        let signer = test_signer(SigningAlg::Ps256);

        store.commit_claim(claim).unwrap();

        // Save to stream: without the flush calls, this triggers
        // a use-after-free error when trying to rewind the stream,
        // because the stream context is freed before the flush is called
        let result = store.save_to_stream(
            format,
            &mut input_stream,
            &mut unsafe_stream,
            signer.as_ref(),
            &context,
        );

        // flushing should prevent the lifetimes issues at C FFI level
        assert!(
            result.is_ok(),
            "save_to_stream should succeed. Error: {:?}",
            result.err()
        );

        // Verify that flush was called
        assert!(flush_called.load(Ordering::SeqCst));
    }

    #[test]
    fn test_ingredient_depth_limit_get_claim_referenced_manifests() {
        let store = Store::new();
        let claim = Claim::new("depth_test", Some("contentauth"), 2);
        let mut svi = StoreValidationInfo::default();
        let mut validation_log =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);

        // Pre-fill claim_label_path to exactly MAX_INGREDIENT_DEPTH entries so the
        // depth guard fires immediately without needing a real ingredient chain.
        let labels: Vec<String> = (0..MAX_INGREDIENT_DEPTH)
            .map(|i| format!("claim_{i}"))
            .collect();
        let mut claim_label_path: Vec<&str> = labels.iter().map(String::as_str).collect();

        let result = Store::get_claim_referenced_manifests_impl(
            &claim,
            &store,
            &mut svi,
            true,
            &mut validation_log,
            &mut claim_label_path,
        );

        assert!(
            matches!(result, Err(Error::InvalidAsset(_))),
            "expected Err(InvalidAsset) for depth >= MAX_INGREDIENT_DEPTH, got: {result:?}"
        );
    }

    #[test]
    fn test_ingredient_depth_limit_ingredient_checks() {
        use crate::context::Context;

        let store = Store::new();
        let claim = Claim::new("depth_test", Some("contentauth"), 2);
        let svi = StoreValidationInfo::default();
        let mut validation_log =
            StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let context = Context::new();

        let result = Store::ingredient_checks(
            &store,
            &claim,
            &svi,
            &mut validation_log,
            MAX_INGREDIENT_DEPTH,
            &context,
        );

        assert!(
            matches!(result, Err(Error::InvalidAsset(_))),
            "expected Err(InvalidAsset) for depth >= MAX_INGREDIENT_DEPTH, got: {result:?}"
        );
    }

    // Verify that when an ingredient assertion is included in final_redactions, the claim it
    // referenced is removed from the incoming store and from svi.ingredient_references.
    #[test]
    fn test_redacted_ingredient_assertion_removes_claim() {
        use crate::{
            hashed_uri::HashedUri, jumbf::labels::to_signature_uri,
            utils::test::create_test_store_v1, ClaimGeneratorInfo, ValidationResults,
        };

        let context = Context::new();
        let signer = test_signer(SigningAlg::Ps256);

        // --- Build M1: a simple signed manifest with no sub-ingredients. ---
        let (format, mut input_stream, mut m1_output) = create_test_streams("earth_apollo17.jpg");
        let mut m1_store = create_test_store_v1().unwrap();
        m1_store
            .save_to_stream(
                format,
                &mut input_stream,
                &mut m1_output,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        m1_output.rewind().unwrap();
        let m1_vec = m1_output.get_ref().clone();
        let mut m1_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let m1_store_loaded = Store::from_stream(
            format,
            Cursor::new(m1_vec.clone()),
            &mut m1_report,
            &context,
        )
        .unwrap();
        let m1_pc = m1_store_loaded.provenance_claim().unwrap();
        let m1_label = m1_pc.label().to_owned();

        // --- Build M2: an update manifest whose ingredient assertion points to M1. ---
        let mut m2_claim = Claim::new("test", Some("m2_ingredient_redact"), 2);
        m2_claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));

        let (m1_jumbf, _) =
            Store::load_jumbf_from_stream(format, &mut Cursor::new(m1_vec.clone()), &context)
                .unwrap();
        let mut m2_store =
            Store::load_ingredient_to_claim(&mut m2_claim, &m1_jumbf, None, &context).unwrap();

        let m1_hashes = m1_store_loaded.get_manifest_box_hashes(m1_pc);
        let parent_uri = HashedUri::new(
            m1_store_loaded.provenance_path().unwrap(),
            Some(m1_pc.alg().to_string()),
            &m1_hashes.manifest_box_hash,
        );
        let sig_uri = HashedUri::new(
            to_signature_uri(m1_pc.label()),
            Some(m1_pc.alg().to_string()),
            &m1_hashes.signature_box_hash,
        );

        let m1_validation = ValidationResults::from_store(&m1_store_loaded, &m1_report);
        let parent_ingredient = Ingredient::new_v3(Relationship::ParentOf)
            .set_active_manifests_and_signature_from_hashed_uri(Some(parent_uri), Some(sig_uri))
            .set_validation_results(Some(m1_validation));
        m2_claim.add_assertion(&parent_ingredient).unwrap();

        // An action referencing the ingredient is required by the update manifest schema.
        let ia = m2_claim.ingredient_assertions()[0];
        let ia_hashed_uri = HashedUri::new(
            to_assertion_uri(m2_claim.label(), &ia.label()),
            Some(m2_claim.alg().to_owned()),
            ia.hash(),
        );
        let actions = Actions::new().add_action(
            Action::new("c2pa.opened")
                .set_parameter("ingredients", vec![ia_hashed_uri])
                .unwrap(),
        );
        m2_claim.add_assertion(&actions).unwrap();

        m2_store.commit_update_manifest(m2_claim).unwrap();
        m1_output.rewind().unwrap();
        let mut m2_output = Cursor::new(Vec::new());
        m2_store
            .save_to_stream(
                format,
                &mut m1_output,
                &mut m2_output,
                signer.as_ref(),
                &context,
            )
            .unwrap();

        // Load M2 back to discover the exact labels and ingredient assertion label.
        m2_output.rewind().unwrap();
        let m2_vec = m2_output.get_ref().clone();
        let mut m2_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
        let m2_store_loaded = Store::from_stream(
            format,
            Cursor::new(m2_vec.clone()),
            &mut m2_report,
            &context,
        )
        .unwrap();
        let m2_pc = m2_store_loaded.provenance_claim().unwrap();
        let m2_label = m2_pc.label().to_owned();

        // Build a redaction URI targeting M2's ingredient assertion (which points to M1).
        let m2_ingredient_assertions = m2_pc.ingredient_assertions();
        assert!(
            !m2_ingredient_assertions.is_empty(),
            "M2 should have an ingredient assertion"
        );
        let ingredient_redaction_uri =
            to_assertion_uri(m2_pc.label(), &m2_ingredient_assertions[0].label());

        // --- Load M2 into M3 with the ingredient assertion redacted. ---
        let mut m3_claim = Claim::new("test", Some("m3_ingredient_redact"), 2);
        m3_claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));

        let (m2_jumbf, _) =
            Store::load_jumbf_from_stream(format, &mut Cursor::new(m2_vec), &context).unwrap();
        Store::load_ingredient_to_claim(
            &mut m3_claim,
            &m2_jumbf,
            Some(vec![ingredient_redaction_uri]),
            &context,
        )
        .unwrap();

        // M2 (the provenance claim of the incoming store) must still be present.
        assert!(
            m3_claim.claim_ingredient(&m2_label).is_some(),
            "M2 should remain in M3's ingredients"
        );

        // M1 must have been removed: its only reference (M2's ingredient assertion) was redacted.
        assert!(
            m3_claim.claim_ingredient(&m1_label).is_none(),
            "M1 should be removed because its only referencing assertion was redacted"
        );
    }
}
