use crate::c2pa_formats::util::be_u32;
use crate::c2pa_formats::{AssetFormat, DataHashExclusion, FormatError};
const FMT: AssetFormat = AssetFormat::Midi;
const C2PA_MANUFACTURER_ID: [u8; 3] = [0x00, 0x43, 0x32];
const MAX_EVENTS_PER_FILE: usize = 16 * 1024 * 1024;
fn invalid(detail: &'static str) -> FormatError {
FormatError::InvalidStructure {
format: FMT,
detail,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct C2paEvent {
track: usize,
store_start: usize,
store_len: usize,
}
fn read_vlq(data: &[u8], pos: &mut usize, end: usize) -> Result<u32, FormatError> {
let mut value = 0u32;
for _ in 0..4 {
if *pos >= end {
return Err(FormatError::Truncated(FMT));
}
let byte = data[*pos];
*pos += 1;
value = (value << 7) | u32::from(byte & 0x7f);
if byte & 0x80 == 0 {
return Ok(value);
}
}
Err(invalid("variable-length quantity exceeds four bytes"))
}
struct Parsed {
format: u16,
tracks: Vec<(usize, usize)>,
}
fn parse_chunks(data: &[u8]) -> Result<Parsed, FormatError> {
if data.get(..4) != Some(b"MThd") {
return Err(invalid("missing MThd header chunk"));
}
let header_len = be_u32(data, 4).ok_or(FormatError::Truncated(FMT))? as usize;
if header_len < 6 {
return Err(invalid("MThd chunk shorter than six bytes"));
}
let header_end = 8usize
.checked_add(header_len)
.filter(|end| *end <= data.len())
.ok_or(FormatError::Truncated(FMT))?;
let format = u16::from_be_bytes([data[8], data[9]]);
let declared_tracks = u16::from_be_bytes([data[10], data[11]]) as usize;
if format > 2 {
return Err(FormatError::UnsupportedVariant {
format: FMT,
detail: "MThd format is not 0, 1, or 2",
});
}
if declared_tracks == 0 || (format == 0 && declared_tracks != 1) {
return Err(invalid("MThd track count is inconsistent with its format"));
}
let mut tracks = Vec::with_capacity(declared_tracks);
let mut pos = header_end;
while pos < data.len() {
let kind = data.get(pos..pos + 4).ok_or(FormatError::Truncated(FMT))?;
let len = be_u32(data, pos + 4).ok_or(FormatError::Truncated(FMT))? as usize;
let body = pos + 8;
let end = body
.checked_add(len)
.filter(|end| *end <= data.len())
.ok_or(FormatError::Truncated(FMT))?;
if kind == b"MTrk" {
tracks.push((body, end));
}
pos = end;
}
if tracks.len() != declared_tracks {
return Err(invalid(
"MTrk chunk count differs from the MThd track count",
));
}
Ok(Parsed { format, tracks })
}
fn scan_track(
data: &[u8],
track: usize,
(start, end): (usize, usize),
events: &mut usize,
found: &mut Vec<C2paEvent>,
) -> Result<(), FormatError> {
let mut pos = start;
let mut running_status: Option<u8> = None;
loop {
if pos >= end {
return Err(invalid("track ends without an End of Track event"));
}
*events += 1;
if *events > MAX_EVENTS_PER_FILE {
return Err(FormatError::UnsupportedVariant {
format: FMT,
detail: "event count exceeds the verifier bound",
});
}
read_vlq(data, &mut pos, end)?;
let first = *data.get(pos).ok_or(FormatError::Truncated(FMT))?;
match first {
0xff => {
pos += 1;
let kind = *data
.get(pos)
.filter(|_| pos < end)
.ok_or(FormatError::Truncated(FMT))?;
pos += 1;
let len = read_vlq(data, &mut pos, end)? as usize;
let body_end = pos
.checked_add(len)
.filter(|e| *e <= end)
.ok_or(FormatError::Truncated(FMT))?;
running_status = None;
if kind == 0x2f {
if len != 0 {
return Err(invalid("End of Track event carries data"));
}
if body_end != end {
return Err(invalid("bytes follow the End of Track event"));
}
return Ok(());
}
if kind == 0x7f && data[pos..body_end].starts_with(&C2PA_MANUFACTURER_ID) {
found.push(C2paEvent {
track,
store_start: pos + C2PA_MANUFACTURER_ID.len(),
store_len: len - C2PA_MANUFACTURER_ID.len(),
});
}
pos = body_end;
}
0xf0 | 0xf7 => {
pos += 1;
let len = read_vlq(data, &mut pos, end)? as usize;
pos = pos
.checked_add(len)
.filter(|e| *e <= end)
.ok_or(FormatError::Truncated(FMT))?;
running_status = None;
}
0xf1..=0xfe => {
return Err(invalid("system common or real-time message in a track"));
}
0x80..=0xef => {
running_status = Some(first);
pos += 1;
pos = skip_channel_data(first, pos, end)?;
}
_ => {
let status = running_status.ok_or(invalid("data byte without running status"))?;
pos = skip_channel_data(status, pos, end)?;
}
}
}
}
fn skip_channel_data(status: u8, pos: usize, end: usize) -> Result<usize, FormatError> {
let count = match status & 0xf0 {
0xc0 | 0xd0 => 1,
_ => 2,
};
pos.checked_add(count)
.filter(|e| *e <= end)
.ok_or(FormatError::Truncated(FMT))
}
fn locate(data: &[u8]) -> Result<Option<C2paEvent>, FormatError> {
let parsed = parse_chunks(data)?;
let mut found = Vec::new();
let mut events = 0usize;
for (index, span) in parsed.tracks.iter().enumerate() {
scan_track(data, index, *span, &mut events, &mut found)?;
}
let designated = match parsed.format {
1 => parsed.tracks.len() - 1,
_ => 0,
};
Ok(match found.as_slice() {
[only] if only.track == designated => Some(*only),
_ => None,
})
}
pub(crate) fn extract(data: &[u8]) -> Result<Option<Vec<u8>>, FormatError> {
Ok(locate(data)?
.map(|event| data[event.store_start..event.store_start + event.store_len].to_vec()))
}
pub(crate) fn exclusions(data: &[u8]) -> Result<Vec<DataHashExclusion>, FormatError> {
Ok(locate(data)?
.map(|event| DataHashExclusion {
start: event.store_start,
length: event.store_len,
})
.into_iter()
.collect())
}
#[cfg(test)]
pub(crate) fn sample_asset(format: u16, tracks: usize, c2pa: &[(usize, &[u8])]) -> Vec<u8> {
fn vlq(mut value: u32) -> Vec<u8> {
let mut out = vec![(value & 0x7f) as u8];
value >>= 7;
while value > 0 {
out.insert(0, ((value & 0x7f) as u8) | 0x80);
value >>= 7;
}
out
}
let mut out = b"MThd".to_vec();
out.extend_from_slice(&6u32.to_be_bytes());
out.extend_from_slice(&format.to_be_bytes());
out.extend_from_slice(&(tracks as u16).to_be_bytes());
out.extend_from_slice(&96u16.to_be_bytes());
for track in 0..tracks {
let mut body = vec![0x00, 0x90, 0x3c, 0x40, 0x60, 0x3c, 0x00];
for (target, store) in c2pa {
if *target == track {
body.push(0x00);
body.extend_from_slice(&[0xff, 0x7f]);
body.extend(vlq((store.len() + 3) as u32));
body.extend_from_slice(&C2PA_MANUFACTURER_ID);
body.extend_from_slice(store);
}
}
body.extend_from_slice(&[0x10, 0xff, 0x2f, 0x00]);
out.extend_from_slice(b"MTrk");
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
out.extend(body);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const STORE: &[u8] = b"jumbf-store-bytes";
#[test]
fn designated_track_follows_the_smf_format() {
for (format, tracks, target) in [(0u16, 1usize, 0usize), (1, 3, 2), (2, 3, 0)] {
let asset = sample_asset(format, tracks, &[(target, STORE)]);
assert_eq!(
extract(&asset).unwrap().as_deref(),
Some(STORE),
"format {format}"
);
let spans = exclusions(&asset).unwrap();
assert_eq!(spans.len(), 1, "one exclusion");
let span = spans[0];
assert_eq!(&asset[span.start..span.start + span.length], STORE);
assert_eq!(&asset[span.start - 3..span.start], &C2PA_MANUFACTURER_ID);
}
}
#[test]
fn a_c2pa_event_outside_the_designated_track_is_not_located() {
let asset = sample_asset(1, 3, &[(0, STORE)]);
assert_eq!(extract(&asset).unwrap(), None);
assert!(exclusions(&asset).unwrap().is_empty());
}
#[test]
fn two_c2pa_events_locate_no_manifest() {
let asset = sample_asset(1, 3, &[(2, STORE), (1, b"other")]);
assert_eq!(extract(&asset).unwrap(), None);
let same_track = sample_asset(0, 1, &[(0, STORE), (0, STORE)]);
assert_eq!(extract(&same_track).unwrap(), None);
}
#[test]
fn malformed_files_are_rejected() {
let asset = sample_asset(0, 1, &[(0, STORE)]);
assert!(extract(&asset[..asset.len() - 2]).is_err());
let mut wrong_count = asset.clone();
wrong_count[11] = 2;
assert!(extract(&wrong_count).is_err());
assert!(extract(b"RIFF....").is_err());
let mut no_status = sample_asset(0, 1, &[]);
let first_event = 14 + 8 + 1;
no_status[first_event] = 0x3c;
assert!(extract(&no_status).is_err());
}
#[test]
fn a_file_without_c2pa_has_no_manifest() {
let asset = sample_asset(1, 2, &[]);
assert_eq!(extract(&asset).unwrap(), None);
}
}