Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,7 @@ harness = false
[[bench]]
name = "read"
harness = false

[[bench]]
name = "fast_sign"
harness = false
214 changes: 214 additions & 0 deletions sdk/benches/fast_sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// Benchmarks comparing standard Builder::sign() vs fast single-pass signing
// for BMFF (MP4), RIFF (WAV), and TIFF formats.

use std::io::Cursor;
use std::path::Path;

use c2pa::{
sign_bmff_fast, sign_riff_fast, sign_tiff_fast, Builder, BuilderIntent, CallbackSigner,
DigitalSourceType, SigningAlg,
};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};

const CERTS: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pub");
const PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pem");

fn create_signer() -> CallbackSigner {
let ed_signer =
|_context: *const (), data: &[u8]| CallbackSigner::ed25519_sign(data, PRIVATE_KEY);
CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS)
}

fn bench_mp4(c: &mut Criterion) {
let source_bytes: &[u8] = include_bytes!("fixtures/100kb.mp4");
let signer = create_signer();
let mut group = c.benchmark_group("sign_mp4_100kb");

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
builder.sign(&signer, "video/mp4", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
sign_bmff_fast(&mut builder, &signer, "video/mp4", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

fn bench_wav(c: &mut Criterion) {
let source_bytes: &[u8] = include_bytes!("fixtures/100kb.wav");
let signer = create_signer();
let mut group = c.benchmark_group("sign_wav_100kb");

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
builder.sign(&signer, "audio/wav", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
sign_riff_fast(&mut builder, &signer, "wav", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

fn bench_tiff(c: &mut Criterion) {
let source_bytes: &[u8] = include_bytes!("fixtures/100kb.tiff");
let signer = create_signer();
let mut group = c.benchmark_group("sign_tiff_100kb");

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
builder.sign(&signer, "image/tiff", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(source_bytes);
let mut dest = Cursor::new(Vec::new());
sign_tiff_fast(&mut builder, &signer, "tiff", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

fn bench_large_mp4(c: &mut Criterion) {
let path = Path::new("/tmp/test_large.mp4");
if !path.exists() {
eprintln!("Skipping large MP4 benchmark: /tmp/test_large.mp4 not found");
return;
}
let source_bytes = std::fs::read(path).unwrap();
let size_mb = source_bytes.len() / (1024 * 1024);
let signer = create_signer();
let mut group = c.benchmark_group(format!("sign_mp4_{size_mb}mb"));
group.sample_size(10);

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
builder.sign(&signer, "video/mp4", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
sign_bmff_fast(&mut builder, &signer, "video/mp4", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

fn bench_large_wav(c: &mut Criterion) {
let path = Path::new("/tmp/test_large.wav");
if !path.exists() {
eprintln!("Skipping large WAV benchmark: /tmp/test_large.wav not found");
return;
}
let source_bytes = std::fs::read(path).unwrap();
let size_mb = source_bytes.len() / (1024 * 1024);
let signer = create_signer();
let mut group = c.benchmark_group(format!("sign_wav_{size_mb}mb"));
group.sample_size(10);

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
builder.sign(&signer, "audio/wav", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
sign_riff_fast(&mut builder, &signer, "wav", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

fn bench_large_tiff(c: &mut Criterion) {
let path = Path::new("/tmp/test_large.tif");
if !path.exists() {
eprintln!("Skipping large TIFF benchmark: /tmp/test_large.tif not found");
return;
}
let source_bytes = std::fs::read(path).unwrap();
let size_mb = source_bytes.len() / (1024 * 1024);
let signer = create_signer();
let mut group = c.benchmark_group(format!("sign_tiff_{size_mb}mb"));
group.sample_size(10);

group.bench_function("standard", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
builder.sign(&signer, "image/tiff", &mut source, &mut dest).unwrap()
})
});

group.bench_function("fast", |b| {
b.iter(|| {
let mut builder = Builder::from_json(include_str!("../tests/fixtures/simple_manifest.json")).unwrap();
builder.set_intent(BuilderIntent::Create(DigitalSourceType::DigitalCapture));
let mut source = Cursor::new(&source_bytes);
let mut dest = Cursor::new(Vec::with_capacity(source_bytes.len() + 65536));
sign_tiff_fast(&mut builder, &signer, "tiff", &mut source, &mut dest).unwrap()
})
});

group.finish();
}

criterion_group!(
benches,
bench_mp4,
bench_wav,
bench_tiff,
bench_large_mp4,
bench_large_wav,
bench_large_tiff
);
criterion_main!(benches);
46 changes: 36 additions & 10 deletions sdk/src/asset_handlers/bmff_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const MAX_BOX_DEPTH: usize = 32; // reasonable BMFF box depth, to prevent stack
const HEADER_SIZE: u64 = 8; // 4 byte type + 4 byte size
const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large size

const C2PA_UUID: [u8; 16] = [
pub(crate) const C2PA_UUID: [u8; 16] = [
0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4, 0x81,
];
const XMP_UUID: [u8; 16] = [
Expand Down Expand Up @@ -265,14 +265,14 @@ fn write_box_uuid_extension<W: Write>(w: &mut W, uuid: &[u8; 16]) -> Result<u64>

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

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -316,6 +316,10 @@ pub(crate) struct FileTypeBox {
pub compatible_brands: Vec<FourCC>,
}

pub(crate) fn read_bmff_ftyp_box<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<FileTypeBox> {
read_ftyp_box(reader)
}

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

Expand Down Expand Up @@ -1514,7 +1518,29 @@ fn get_uuid_box_purpose<R: Read + Seek + ?Sized>(
))
}

fn get_uuid_token(
/// Find a UUID box token by matching the UUID only (no reader or purpose check).
/// Used by the fast_sign module which only needs to locate the C2PA box by UUID.
pub(crate) fn get_uuid_token(
bmff_tree: &Arena<BoxInfo>,
bmff_map: &HashMap<String, Vec<Token>>,
uuid: &[u8; 16],
) -> Option<Token> {
if let Some(uuid_list) = bmff_map.get("/uuid") {
for uuid_token in uuid_list {
let box_info = &bmff_tree[*uuid_token];
if box_info.data.box_type == BoxType::UuidBox {
if let Some(found_uuid) = &box_info.data.user_type {
if vec_compare(uuid, found_uuid) {
return Some(*uuid_token);
}
}
}
}
}
None
}

fn get_uuid_token_with_reader(
reader: &mut dyn CAIRead,
bmff_tree: &Arena<BoxInfo>,
bmff_map: &HashMap<String, Vec<Token>>,
Expand Down Expand Up @@ -2022,7 +2048,7 @@ impl CAIWriter for BmffIO {
let ftyp_size = ftyp_info.size;

// get position to insert c2pa primary manifest store
let (c2pa_start, c2pa_length) = match get_uuid_token(
let (c2pa_start, c2pa_length) = match get_uuid_token_with_reader(
input_stream,
&bmff_tree,
&bmff_map,
Expand Down Expand Up @@ -2184,7 +2210,7 @@ impl CAIWriter for BmffIO {

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

Expand Down
22 changes: 11 additions & 11 deletions sdk/src/asset_handlers/tiff_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ impl IFDEntryType {
// TIFF IFD Entry (value_offset is in target endian)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IfdEntry {
entry_tag: u16,
entry_type: u16,
value_count: u64,
value_offset: u64,
pub(crate) entry_tag: u16,
pub(crate) entry_type: u16,
pub(crate) value_count: u64,
pub(crate) value_offset: u64,
}

// helper enum to know if the IFD requires special handling
Expand All @@ -148,12 +148,12 @@ pub enum IfdType {
// TIFF IFD
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageFileDirectory {
offset: u64,
entry_cnt: u64,
ifd_type: IfdType,
entries: HashMap<u16, IfdEntry>,
next_ifd_offset: Option<u64>,
next_idf_offset_location: u64,
pub(crate) offset: u64,
pub(crate) entry_cnt: u64,
pub(crate) ifd_type: IfdType,
pub(crate) entries: HashMap<u16, IfdEntry>,
pub(crate) next_ifd_offset: Option<u64>,
pub(crate) next_idf_offset_location: u64,
}

impl ImageFileDirectory {
Expand Down Expand Up @@ -366,7 +366,7 @@ impl TiffStructure {
}

// offset are stored in source endianness so to use offset value in Seek calls we must convert to native endianness
fn decode_offset(offset_file_native: u64, endianness: Endianness, big_tiff: bool) -> Result<u64> {
pub(crate) fn decode_offset(offset_file_native: u64, endianness: Endianness, big_tiff: bool) -> Result<u64> {
let offset: u64;
let offset_bytes = offset_file_native.to_ne_bytes();
let offset_reader = Cursor::new(offset_bytes);
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ pub struct Builder {
/// - `urn:c2pa:fa479510-2a7d-c165-7b26-488a267f4c6a`
pub timestamp_manifest_labels: HashSet<String>,

/// If true, use deterministic output (no random salts, preserve caller-set instance_id).
#[serde(default)]
pub deterministic: bool,

/// Container for binary assets (like thumbnails).
#[serde(skip)]
pub(crate) resources: ResourceStore,
Expand Down Expand Up @@ -1741,7 +1745,7 @@ impl Builder {
///
/// This functioin calls [`Builder::to_claim`] internally. Use [`Builder::to_store_with_claim`]
/// if the [`Claim`] is constructed manually.
fn to_store(&self) -> Result<Store> {
pub(crate) fn to_store(&self) -> Result<Store> {
let claim = self.to_claim()?;
self.to_store_with_claim(claim)
}
Expand Down
10 changes: 10 additions & 0 deletions sdk/src/claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,16 @@ impl Claim {
self.add_assertion_impl(assertion_builder, &DefaultSalt::default(), false)
}

/// Add an assertion to this claim with a specific salt generator.
/// This allows callers to control salting behavior, e.g. using NoSalt for deterministic output.
pub(crate) fn add_assertion_with_salt(
&mut self,
assertion_builder: &impl AssertionBase,
salt_generator: &impl SaltGenerator,
) -> Result<C2PAAssertion> {
self.add_assertion_impl(assertion_builder, salt_generator, false)
}

/// Same as add_assertion but forces addition to created_assertions for Claims V2
pub fn add_created_assertion(
&mut self,
Expand Down
Loading