use crate::c2pa_formats::util::le_u32;
use crate::c2pa_formats::{AssetFormat, DataHashExclusion, FormatError};
const FMT: AssetFormat = AssetFormat::Riff;
const RIFF: &[u8; 4] = b"RIFF";
const CHUNK_C2PA: &[u8; 4] = b"C2PA";
fn check_riff(data: &[u8]) -> Result<(), FormatError> {
if data.len() < 12 || &data[..4] != RIFF {
return Err(FormatError::InvalidStructure {
format: FMT,
detail: "missing RIFF header",
});
}
Ok(())
}
struct Chunk {
id: [u8; 4],
start: usize,
data_start: usize,
data_len: usize,
end: usize,
}
fn walk_chunks(data: &[u8], mut f: impl FnMut(&Chunk)) -> Result<(), FormatError> {
check_riff(data)?;
let mut pos = 12;
while pos + 8 <= data.len() {
let mut id = [0u8; 4];
id.copy_from_slice(&data[pos..pos + 4]);
let len = le_u32(data, pos + 4).ok_or(FormatError::Truncated(FMT))? as usize;
let data_start = pos + 8;
let data_end = data_start
.checked_add(len)
.filter(|&e| e <= data.len())
.ok_or(FormatError::Truncated(FMT))?;
let end = if len % 2 == 1 && data_end < data.len() {
data_end + 1
} else {
data_end
};
f(&Chunk {
id,
start: pos,
data_start,
data_len: len,
end,
});
pos = end;
}
Ok(())
}
pub(crate) fn extract(data: &[u8]) -> Result<Option<Vec<u8>>, FormatError> {
let mut found = None;
walk_chunks(data, |c| {
if &c.id == CHUNK_C2PA && found.is_none() {
found = Some(data[c.data_start..c.data_start + c.data_len].to_vec());
}
})?;
Ok(found)
}
#[cfg(test)]
pub(crate) fn strip(asset: &[u8]) -> Result<Vec<u8>, FormatError> {
check_riff(asset)?;
let mut out = Vec::with_capacity(asset.len());
out.extend_from_slice(&asset[..12]);
let mut cursor = 12;
walk_chunks(asset, |c| {
if &c.id == CHUNK_C2PA {
out.extend_from_slice(&asset[cursor..c.start]);
cursor = c.end;
}
})?;
out.extend_from_slice(&asset[cursor..]);
let riff_size = (out.len() - 8) as u32;
out[4..8].copy_from_slice(&riff_size.to_le_bytes());
Ok(out)
}
#[cfg(test)]
pub(crate) fn build_c2pa_chunk(manifest_store: &[u8]) -> Result<Vec<u8>, FormatError> {
if manifest_store.len() > u32::MAX as usize {
return Err(FormatError::ManifestTooLarge {
format: FMT,
max: u32::MAX as usize,
got: manifest_store.len(),
});
}
let mut out = Vec::with_capacity(manifest_store.len() + 9);
out.extend_from_slice(CHUNK_C2PA);
out.extend_from_slice(&(manifest_store.len() as u32).to_le_bytes());
out.extend_from_slice(manifest_store);
if manifest_store.len() % 2 == 1 {
out.push(0);
}
Ok(out)
}
#[cfg(test)]
pub(crate) fn embed(asset: &[u8], manifest_store: &[u8]) -> Result<Vec<u8>, FormatError> {
let carrier = build_c2pa_chunk(manifest_store)?;
let clean = strip(asset)?;
let mut out = Vec::with_capacity(clean.len() + carrier.len() + 1);
out.extend_from_slice(&clean);
if out.len() % 2 == 1 {
out.push(0);
}
out.extend_from_slice(&carrier);
let riff_size = (out.len() - 8) as u32;
out[4..8].copy_from_slice(&riff_size.to_le_bytes());
Ok(out)
}
pub(crate) fn exclusions(data: &[u8]) -> Result<Vec<DataHashExclusion>, FormatError> {
let mut ex = Vec::new();
walk_chunks(data, |c| {
if &c.id == CHUNK_C2PA {
ex.push(DataHashExclusion {
start: c.start,
length: c.end - c.start,
});
}
})?;
Ok(ex)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::c2pa_formats::tests::dummy_manifest_store;
fn tiny_wav() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"WAVE");
body.extend_from_slice(b"fmt ");
body.extend_from_slice(&16u32.to_le_bytes());
body.extend_from_slice(&[1, 0, 1, 0, 0x44, 0xAC, 0, 0, 0x88, 0x58, 1, 0, 2, 0, 16, 0]);
let mut v = Vec::from(*RIFF);
v.extend_from_slice(&(body.len() as u32).to_le_bytes());
v.extend_from_slice(&body);
v
}
#[test]
fn roundtrip() {
let store = dummy_manifest_store();
let embedded = embed(&tiny_wav(), &store).unwrap();
assert_eq!(
extract(&embedded).unwrap().as_deref(),
Some(store.as_slice())
);
}
#[test]
fn riff_size_updated() {
let store = dummy_manifest_store();
let embedded = embed(&tiny_wav(), &store).unwrap();
let declared = le_u32(&embedded, 4).unwrap() as usize;
assert_eq!(declared, embedded.len() - 8);
}
#[test]
fn bare_asset_has_no_manifest() {
assert_eq!(extract(&tiny_wav()).unwrap(), None);
}
#[test]
fn re_embed_replaces_manifest() {
let first = embed(&tiny_wav(), b"FIRST-manifest-store").unwrap();
let second = embed(&first, b"SECOND-manifest-store").unwrap();
assert_eq!(
extract(&second).unwrap().as_deref(),
Some(&b"SECOND-manifest-store"[..])
);
let mut c2pa_count = 0;
walk_chunks(&second, |c| {
if &c.id == CHUNK_C2PA {
c2pa_count += 1;
}
})
.unwrap();
assert_eq!(c2pa_count, 1, "re-embed must leave exactly one C2PA chunk");
let declared = le_u32(&second, 4).unwrap() as usize;
assert_eq!(declared, second.len() - 8);
}
#[test]
fn rejects_non_riff() {
assert!(matches!(
extract(b"NOTRIFFNOTRIFF"),
Err(FormatError::InvalidStructure { .. })
));
}
}