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