Skip to main content

c2pa_structured_text/
extract.rs

1use crate::error::Error;
2
3const BEGIN: &str = "-----BEGIN C2PA MANIFEST-----";
4const END: &str = "-----END C2PA MANIFEST-----";
5
6#[derive(Debug)]
7pub struct ExtractionResult {
8    pub reference: String,
9    pub offset: usize,
10    pub length: usize,
11}
12
13pub fn extract_manifest(text: &str) -> Result<ExtractionResult, Error> {
14    let bytes = text.as_bytes();
15
16    let begin_pos = match find_delimiter(bytes, BEGIN) {
17        Some(pos) => pos,
18        None => return Err(Error::NotFound),
19    };
20
21    let after_begin = begin_pos + BEGIN.len();
22
23    let end_pos = match find_delimiter(&bytes[after_begin..], END) {
24        Some(pos) => after_begin + pos,
25        None => return Err(Error::NotFound),
26    };
27
28    if find_delimiter(&bytes[end_pos + END.len()..], BEGIN).is_some() {
29        return Err(Error::MultipleBlocks);
30    }
31
32    let reference = text[after_begin..end_pos].trim().to_string();
33
34    if reference.is_empty() {
35        return Err(Error::EmptyReference);
36    }
37
38    let line_start = text[..begin_pos].rfind('\n').map_or(0, |p| p + 1);
39    let line_end = text[end_pos + END.len()..]
40        .find('\n')
41        .map_or(text.len(), |p| end_pos + END.len() + p + 1);
42
43    Ok(ExtractionResult {
44        reference,
45        offset: line_start,
46        length: line_end - line_start,
47    })
48}
49
50fn find_delimiter(haystack: &[u8], needle: &str) -> Option<usize> {
51    let needle = needle.as_bytes();
52    haystack
53        .windows(needle.len())
54        .position(|w| w == needle)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn single_line_python() {
63        let text = "# -----BEGIN C2PA MANIFEST----- https://example.com/m.c2pa -----END C2PA MANIFEST-----\nprint('hello')\n";
64        let result = extract_manifest(text).unwrap();
65        assert_eq!(result.reference, "https://example.com/m.c2pa");
66        assert_eq!(result.offset, 0);
67    }
68
69    #[test]
70    fn front_matter() {
71        let text = "---\n-----BEGIN C2PA MANIFEST-----\nhttps://example.com/m.c2pa\n-----END C2PA MANIFEST-----\ntitle: doc\n---\n";
72        let result = extract_manifest(text).unwrap();
73        assert_eq!(result.reference, "https://example.com/m.c2pa");
74    }
75
76    #[test]
77    fn not_found() {
78        assert!(matches!(
79            extract_manifest("no manifest here"),
80            Err(Error::NotFound)
81        ));
82    }
83
84    #[test]
85    fn empty_reference() {
86        let text = "# -----BEGIN C2PA MANIFEST-----  -----END C2PA MANIFEST-----\n";
87        assert!(matches!(
88            extract_manifest(text),
89            Err(Error::EmptyReference)
90        ));
91    }
92
93    #[test]
94    fn multiple_blocks() {
95        let text = "# -----BEGIN C2PA MANIFEST----- https://a.com -----END C2PA MANIFEST-----\n# -----BEGIN C2PA MANIFEST----- https://b.com -----END C2PA MANIFEST-----\n";
96        assert!(matches!(
97            extract_manifest(text),
98            Err(Error::MultipleBlocks)
99        ));
100    }
101}