use crate::error::Error;
use c2pa_structured_text::{find_delimiter, BEGIN, END};
pub(crate) const SIGNATURE: &str = "WEBVTT";
pub(crate) const NOTE: &str = "NOTE";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractionResult {
pub reference: String,
pub offset: usize,
pub length: usize,
}
pub(crate) fn is_webvtt(text: &str) -> bool {
let body = text.strip_prefix('\u{feff}').unwrap_or(text);
match body.strip_prefix(SIGNATURE) {
Some(rest) => rest.is_empty() || rest.starts_with([' ', '\t', '\n', '\r']),
None => false,
}
}
pub fn extract_manifest(text: &str) -> Result<ExtractionResult, Error> {
if !is_webvtt(text) {
return Err(Error::NotVtt);
}
let bytes = text.as_bytes();
let mut found: Option<ExtractionResult> = None;
let mut search = 0;
while let Some(rel) = find_delimiter(&bytes[search..], BEGIN) {
let begin = search + rel;
let after_begin = begin + BEGIN.len();
let end = match find_delimiter(&bytes[after_begin..], END) {
Some(r) => after_begin + r,
None => return Err(Error::NotFound),
};
let after_end = end + END.len();
let line_start = text[..begin].rfind('\n').map_or(0, |p| p + 1);
if text[line_start..begin].trim() == NOTE {
let reference = text[after_begin..end].trim().to_string();
if reference.is_empty() {
return Err(Error::EmptyReference);
}
if found.is_some() {
return Err(Error::MultipleManifests);
}
let line_end = text[after_end..]
.find('\n')
.map_or(text.len(), |p| after_end + p + 1);
found = Some(ExtractionResult {
reference,
offset: line_start,
length: line_end - line_start,
});
}
search = after_end;
}
found.ok_or(Error::NotFound)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "WEBVTT\n\nNOTE -----BEGIN C2PA MANIFEST----- https://example.com/m.c2pa -----END C2PA MANIFEST-----\n\n00:00:00.000 --> 00:00:05.000\nHello world\n";
#[test]
fn extract_reference_and_range() {
let r = extract_manifest(SAMPLE).unwrap();
assert_eq!(r.reference, "https://example.com/m.c2pa");
assert_eq!(r.offset, "WEBVTT\n\n".len());
let expected_line =
"NOTE -----BEGIN C2PA MANIFEST----- https://example.com/m.c2pa -----END C2PA MANIFEST-----\n";
assert_eq!(r.length, expected_line.len());
assert_eq!(&SAMPLE[r.offset..r.offset + r.length], expected_line);
}
#[test]
fn not_vtt() {
assert!(matches!(
extract_manifest("not a vtt file"),
Err(Error::NotVtt)
));
}
#[test]
fn accepts_bom_and_header_text() {
let vtt = "\u{feff}WEBVTT - Title\n\nNOTE -----BEGIN C2PA MANIFEST----- urn:x -----END C2PA MANIFEST-----\n";
assert_eq!(extract_manifest(vtt).unwrap().reference, "urn:x");
}
#[test]
fn no_manifest() {
let plain = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nHello\n";
assert!(matches!(extract_manifest(plain), Err(Error::NotFound)));
}
#[test]
fn empty_reference() {
let vtt = "WEBVTT\n\nNOTE -----BEGIN C2PA MANIFEST----- -----END C2PA MANIFEST-----\n";
assert!(matches!(extract_manifest(vtt), Err(Error::EmptyReference)));
}
#[test]
fn multiple_manifests() {
let vtt = "WEBVTT\n\nNOTE -----BEGIN C2PA MANIFEST----- https://a -----END C2PA MANIFEST-----\n\nNOTE -----BEGIN C2PA MANIFEST----- https://b -----END C2PA MANIFEST-----\n";
assert!(matches!(
extract_manifest(vtt),
Err(Error::MultipleManifests)
));
}
#[test]
fn ignores_delimiter_in_cue_text() {
let vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\n-----BEGIN C2PA MANIFEST----- nope -----END C2PA MANIFEST-----\n";
assert!(matches!(extract_manifest(vtt), Err(Error::NotFound)));
}
#[test]
fn crlf_length_includes_terminator() {
let vtt = "WEBVTT\r\n\r\nNOTE -----BEGIN C2PA MANIFEST----- urn:x -----END C2PA MANIFEST-----\r\n\r\n";
let r = extract_manifest(vtt).unwrap();
assert!(vtt[r.offset..r.offset + r.length].ends_with("\r\n"));
}
}