c2pa_structured_text/
extract.rs1use 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}