Skip to main content

c2pa_structured_text/
embed.rs

1// Copyright 2026 WritersLogic. All rights reserved.
2// Licensed under the Apache License, Version 2.0 or the MIT license,
3// at your option.
4
5//! Embedding a C2PA manifest block into structured text.
6//!
7//! The block is placed either at the beginning of the file (recommended), at
8//! the end (when the first line is reserved by the host format, e.g. a shebang
9//! or XML declaration), or inside a front matter section. Placement determines
10//! the hard-binding exclusion range; see [`crate::hardbinding`].
11
12use crate::codec;
13
14const BEGIN: &str = "-----BEGIN C2PA MANIFEST-----";
15const END: &str = "-----END C2PA MANIFEST-----";
16
17/// A manifest reference to embed: either a URI to an external C2PA Manifest
18/// Store (preferred) or the store itself, which is encoded as a
19/// `data:application/c2pa;base64,` URI.
20pub enum ManifestRef<'a> {
21    Url(&'a str),
22    Embedded(&'a [u8]),
23}
24
25impl ManifestRef<'_> {
26    fn render(&self) -> String {
27        match self {
28            ManifestRef::Url(url) => (*url).to_string(),
29            ManifestRef::Embedded(bytes) => {
30                format!("data:application/c2pa;base64,{}", codec::encode(bytes))
31            }
32        }
33    }
34}
35
36fn single_line(reference: &str, comment_prefix: &str, comment_suffix: Option<&str>) -> String {
37    let suffix = comment_suffix.unwrap_or("");
38    format!("{comment_prefix} {BEGIN} {reference} {END} {suffix}")
39        .trim_end()
40        .to_string()
41}
42
43/// Embed a manifest block as the first line of the file using single-line
44/// comment syntax. This is the recommended placement.
45pub fn embed_manifest(
46    text: &str,
47    manifest: ManifestRef<'_>,
48    comment_prefix: &str,
49    comment_suffix: Option<&str>,
50) -> String {
51    let line = single_line(&manifest.render(), comment_prefix, comment_suffix);
52    format!("{line}\n{text}")
53}
54
55/// Embed a manifest block as the last line of the file. Use this when the first
56/// line is reserved by the host format (a shebang `#!/...` or an XML
57/// declaration `<?xml ...?>`), so the `-----END C2PA MANIFEST-----` delimiter
58/// appears on the final line as the specification requires.
59///
60/// The block is separated from preceding content by a single newline; if the
61/// text does not already end in a line terminator one is added first.
62pub fn embed_manifest_at_end(
63    text: &str,
64    manifest: ManifestRef<'_>,
65    comment_prefix: &str,
66    comment_suffix: Option<&str>,
67) -> String {
68    let line = single_line(&manifest.render(), comment_prefix, comment_suffix);
69    if text.is_empty() {
70        return line;
71    }
72    if text.ends_with('\n') {
73        format!("{text}{line}")
74    } else {
75        format!("{text}\n{line}")
76    }
77}
78
79/// Embed a manifest block in multi-line front matter form. `fm_delim` is the
80/// host format's front matter fence (`---` for YAML, `+++` for TOML).
81///
82/// If `text` already opens with `fm_delim` on its first line the C2PA block is
83/// inserted at the top of that existing front matter; otherwise a new front
84/// matter section containing only the block is prepended.
85pub fn embed_front_matter(text: &str, manifest: ManifestRef<'_>, fm_delim: &str) -> String {
86    let reference = manifest.render();
87    let block = format!("{BEGIN}\n{reference}\n{END}");
88
89    let opening = format!("{fm_delim}\n");
90    if let Some(rest) = text.strip_prefix(&opening) {
91        format!("{opening}{block}\n{rest}")
92    } else {
93        format!("{fm_delim}\n{block}\n{fm_delim}\n{text}")
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn embed_url_python() {
103        let text = "print('hello')\n";
104        let result = embed_manifest(
105            text,
106            ManifestRef::Url("https://example.com/m.c2pa"),
107            "#",
108            None,
109        );
110        assert!(result.starts_with("# -----BEGIN C2PA MANIFEST-----"));
111        assert!(result.contains("https://example.com/m.c2pa"));
112        assert!(result.contains("-----END C2PA MANIFEST-----"));
113        assert!(result.ends_with("print('hello')\n"));
114    }
115
116    #[test]
117    fn embed_url_css() {
118        let result = embed_manifest(
119            "body {}",
120            ManifestRef::Url("https://example.com/m.c2pa"),
121            "/*",
122            Some("*/"),
123        );
124        assert!(result.starts_with("/* -----BEGIN C2PA MANIFEST-----"));
125        assert!(result.contains("-----END C2PA MANIFEST----- */"));
126    }
127
128    #[test]
129    fn embed_data_uri() {
130        let bytes = b"test manifest";
131        let result = embed_manifest("content", ManifestRef::Embedded(bytes), "#", None);
132        assert!(result.contains("data:application/c2pa;base64,"));
133    }
134
135    #[test]
136    fn embed_at_end_after_shebang() {
137        let text = "#!/usr/bin/env python3\nprint('hi')\n";
138        let result = embed_manifest_at_end(
139            text,
140            ManifestRef::Url("https://example.com/m.c2pa"),
141            "#",
142            None,
143        );
144        assert!(result.starts_with("#!/usr/bin/env python3"));
145        assert!(result.ends_with("-----END C2PA MANIFEST-----"));
146    }
147
148    #[test]
149    fn embed_at_end_adds_newline_separator() {
150        let result =
151            embed_manifest_at_end("no trailing newline", ManifestRef::Url("u"), "//", None);
152        assert_eq!(
153            result,
154            "no trailing newline\n// -----BEGIN C2PA MANIFEST----- u -----END C2PA MANIFEST-----"
155        );
156    }
157
158    #[test]
159    fn embed_front_matter_into_existing() {
160        let text = "---\ntitle: doc\n---\nbody\n";
161        let result =
162            embed_front_matter(text, ManifestRef::Url("https://example.com/m.c2pa"), "---");
163        assert!(result.starts_with("---\n-----BEGIN C2PA MANIFEST-----\n"));
164        assert!(result.contains("\ntitle: doc\n"));
165    }
166
167    #[test]
168    fn embed_front_matter_creates_section() {
169        let result = embed_front_matter("# Heading\n", ManifestRef::Url("u"), "---");
170        assert!(result.starts_with(
171            "---\n-----BEGIN C2PA MANIFEST-----\nu\n-----END C2PA MANIFEST-----\n---\n"
172        ));
173    }
174}