use crate::codec;
use crate::error::Error;
const BEGIN: &str = "-----BEGIN C2PA MANIFEST-----";
const END: &str = "-----END C2PA MANIFEST-----";
const DATA_URI_PREFIX: &str = "data:application/c2pa;base64,";
pub(crate) struct Block {
pub reference: String,
pub line_start: usize,
pub line_end: usize,
}
pub(crate) fn locate_block(text: &str) -> Result<Block, Error> {
let bytes = text.as_bytes();
let begin_pos = find_delimiter(bytes, BEGIN).ok_or(Error::NotFound)?;
let after_begin = begin_pos + BEGIN.len();
let end_pos = find_delimiter(&bytes[after_begin..], END)
.map(|pos| after_begin + pos)
.ok_or(Error::NotFound)?;
let after_end = end_pos + END.len();
if find_delimiter(&bytes[after_end..], BEGIN).is_some() {
return Err(Error::MultipleBlocks);
}
let reference = text[after_begin..end_pos].trim().to_string();
if reference.is_empty() {
return Err(Error::EmptyReference);
}
let line_start = text[..begin_pos].rfind('\n').map_or(0, |p| p + 1);
let line_end = text[after_end..]
.find('\n')
.map_or(text.len(), |p| after_end + p + 1);
Ok(Block {
reference,
line_start,
line_end,
})
}
#[derive(Debug)]
pub struct ExtractionResult {
pub reference: String,
pub offset: usize,
pub length: usize,
}
pub fn extract_manifest(text: &str) -> Result<ExtractionResult, Error> {
let block = locate_block(text)?;
Ok(ExtractionResult {
reference: block.reference,
offset: block.line_start,
length: block.line_end - block.line_start,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Reference {
Url(String),
Embedded(Vec<u8>),
}
pub fn classify_reference(reference: &str) -> Result<Reference, Error> {
if let Some(b64) = reference.strip_prefix(DATA_URI_PREFIX) {
let bytes = codec::decode(b64).map_err(Error::ManifestDecode)?;
Ok(Reference::Embedded(bytes))
} else if looks_like_uri(reference) {
Ok(Reference::Url(reference.to_string()))
} else {
Err(Error::MalformedReference(reference.to_string()))
}
}
fn looks_like_uri(reference: &str) -> bool {
match reference.find(':') {
Some(0) | None => false,
Some(colon) => {
let scheme = &reference.as_bytes()[..colon];
scheme[0].is_ascii_alphabetic()
&& scheme
.iter()
.all(|&c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.'))
}
}
}
fn find_delimiter(haystack: &[u8], needle: &str) -> Option<usize> {
let needle = needle.as_bytes();
haystack.windows(needle.len()).position(|w| w == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_line_python() {
let text = "# -----BEGIN C2PA MANIFEST----- https://example.com/m.c2pa -----END C2PA MANIFEST-----\nprint('hello')\n";
let result = extract_manifest(text).unwrap();
assert_eq!(result.reference, "https://example.com/m.c2pa");
assert_eq!(result.offset, 0);
}
#[test]
fn front_matter() {
let text = "---\n-----BEGIN C2PA MANIFEST-----\nhttps://example.com/m.c2pa\n-----END C2PA MANIFEST-----\ntitle: doc\n---\n";
let result = extract_manifest(text).unwrap();
assert_eq!(result.reference, "https://example.com/m.c2pa");
assert_eq!(result.offset, 4);
}
#[test]
fn not_found() {
assert!(matches!(
extract_manifest("no manifest here"),
Err(Error::NotFound)
));
}
#[test]
fn empty_reference() {
let text = "# -----BEGIN C2PA MANIFEST----- -----END C2PA MANIFEST-----\n";
assert!(matches!(extract_manifest(text), Err(Error::EmptyReference)));
}
#[test]
fn multiple_blocks() {
let text = "# -----BEGIN C2PA MANIFEST----- https://a.com -----END C2PA MANIFEST-----\n# -----BEGIN C2PA MANIFEST----- https://b.com -----END C2PA MANIFEST-----\n";
assert!(matches!(extract_manifest(text), Err(Error::MultipleBlocks)));
}
#[test]
fn classify_url() {
assert_eq!(
classify_reference("https://example.com/m.c2pa").unwrap(),
Reference::Url("https://example.com/m.c2pa".to_string())
);
}
#[test]
fn classify_data_uri() {
let r = classify_reference("data:application/c2pa;base64,Zm9vYmFy").unwrap();
assert_eq!(r, Reference::Embedded(b"foobar".to_vec()));
}
#[test]
fn classify_rejects_bare_text() {
assert!(matches!(
classify_reference("not a reference"),
Err(Error::MalformedReference(_))
));
}
}