c2pa_structured_text/
extract.rs1use crate::codec;
8use crate::error::Error;
9
10const BEGIN: &str = "-----BEGIN C2PA MANIFEST-----";
11const END: &str = "-----END C2PA MANIFEST-----";
12const DATA_URI_PREFIX: &str = "data:application/c2pa;base64,";
13
14pub(crate) struct Block {
24 pub reference: String,
25 pub line_start: usize,
26 pub line_end: usize,
27}
28
29pub(crate) fn locate_block(text: &str) -> Result<Block, Error> {
30 let bytes = text.as_bytes();
31
32 let begin_pos = find_delimiter(bytes, BEGIN).ok_or(Error::NotFound)?;
33 let after_begin = begin_pos + BEGIN.len();
34
35 let end_pos = find_delimiter(&bytes[after_begin..], END)
36 .map(|pos| after_begin + pos)
37 .ok_or(Error::NotFound)?;
38
39 let after_end = end_pos + END.len();
40 if find_delimiter(&bytes[after_end..], BEGIN).is_some() {
41 return Err(Error::MultipleBlocks);
42 }
43
44 let reference = text[after_begin..end_pos].trim().to_string();
45 if reference.is_empty() {
46 return Err(Error::EmptyReference);
47 }
48
49 let line_start = text[..begin_pos].rfind('\n').map_or(0, |p| p + 1);
50 let line_end = text[after_end..]
51 .find('\n')
52 .map_or(text.len(), |p| after_end + p + 1);
53
54 Ok(Block {
55 reference,
56 line_start,
57 line_end,
58 })
59}
60
61#[derive(Debug)]
64pub struct ExtractionResult {
65 pub reference: String,
66 pub offset: usize,
67 pub length: usize,
68}
69
70pub fn extract_manifest(text: &str) -> Result<ExtractionResult, Error> {
76 let block = locate_block(text)?;
77 Ok(ExtractionResult {
78 reference: block.reference,
79 offset: block.line_start,
80 length: block.line_end - block.line_start,
81 })
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum Reference {
88 Url(String),
89 Embedded(Vec<u8>),
90}
91
92pub fn classify_reference(reference: &str) -> Result<Reference, Error> {
98 if let Some(b64) = reference.strip_prefix(DATA_URI_PREFIX) {
99 let bytes = codec::decode(b64).map_err(Error::ManifestDecode)?;
100 Ok(Reference::Embedded(bytes))
101 } else if looks_like_uri(reference) {
102 Ok(Reference::Url(reference.to_string()))
103 } else {
104 Err(Error::MalformedReference(reference.to_string()))
105 }
106}
107
108fn looks_like_uri(reference: &str) -> bool {
109 match reference.find(':') {
113 Some(0) | None => false,
114 Some(colon) => {
115 let scheme = &reference.as_bytes()[..colon];
116 scheme[0].is_ascii_alphabetic()
117 && scheme
118 .iter()
119 .all(|&c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.'))
120 }
121 }
122}
123
124fn find_delimiter(haystack: &[u8], needle: &str) -> Option<usize> {
125 let needle = needle.as_bytes();
126 haystack.windows(needle.len()).position(|w| w == needle)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn single_line_python() {
135 let text = "# -----BEGIN C2PA MANIFEST----- https://example.com/m.c2pa -----END C2PA MANIFEST-----\nprint('hello')\n";
136 let result = extract_manifest(text).unwrap();
137 assert_eq!(result.reference, "https://example.com/m.c2pa");
138 assert_eq!(result.offset, 0);
139 }
140
141 #[test]
142 fn front_matter() {
143 let text = "---\n-----BEGIN C2PA MANIFEST-----\nhttps://example.com/m.c2pa\n-----END C2PA MANIFEST-----\ntitle: doc\n---\n";
144 let result = extract_manifest(text).unwrap();
145 assert_eq!(result.reference, "https://example.com/m.c2pa");
146 assert_eq!(result.offset, 4);
148 }
149
150 #[test]
151 fn not_found() {
152 assert!(matches!(
153 extract_manifest("no manifest here"),
154 Err(Error::NotFound)
155 ));
156 }
157
158 #[test]
159 fn empty_reference() {
160 let text = "# -----BEGIN C2PA MANIFEST----- -----END C2PA MANIFEST-----\n";
161 assert!(matches!(extract_manifest(text), Err(Error::EmptyReference)));
162 }
163
164 #[test]
165 fn multiple_blocks() {
166 let text = "# -----BEGIN C2PA MANIFEST----- https://a.com -----END C2PA MANIFEST-----\n# -----BEGIN C2PA MANIFEST----- https://b.com -----END C2PA MANIFEST-----\n";
167 assert!(matches!(extract_manifest(text), Err(Error::MultipleBlocks)));
168 }
169
170 #[test]
171 fn classify_url() {
172 assert_eq!(
173 classify_reference("https://example.com/m.c2pa").unwrap(),
174 Reference::Url("https://example.com/m.c2pa".to_string())
175 );
176 }
177
178 #[test]
179 fn classify_data_uri() {
180 let r = classify_reference("data:application/c2pa;base64,Zm9vYmFy").unwrap();
182 assert_eq!(r, Reference::Embedded(b"foobar".to_vec()));
183 }
184
185 #[test]
186 fn classify_rejects_bare_text() {
187 assert!(matches!(
188 classify_reference("not a reference"),
189 Err(Error::MalformedReference(_))
190 ));
191 }
192}