c2pa_structured_text/bridge.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//! Validation bridge to `c2pa-rs`.
6//!
7//! This crate owns locating and extracting the manifest block and defining the
8//! structured-text hard binding. It does **not** implement COSE signature
9//! verification, certificate trust, or assertion validation; those are
10//! delegated to `c2pa-rs`.
11//!
12//! The bridge extracts the reference, resolves it to a C2PA Manifest Store
13//! (decoding a `data:` URI inline, or fetching a URL with the `remote`
14//! feature), and hands the store plus the text stream to [`c2pa::Reader`],
15//! which validates the signature, trust chain, and the `c2pa.hash.data` hard
16//! binding against the raw bytes. Because the manifest block is excluded from
17//! the hash, `c2pa-rs`'s raw-byte data-hash validation matches the binding this
18//! crate computes.
19
20use std::io::Cursor;
21
22use c2pa::{Context, Reader};
23
24use crate::error::Error;
25use crate::extract::{classify_reference, extract_manifest, Reference};
26
27/// The default media type used when handing a text stream to `c2pa-rs`.
28pub const DEFAULT_FORMAT: &str = "text/plain";
29
30/// An error from the validation bridge.
31#[derive(Debug)]
32pub enum BridgeError {
33 /// Locating, extracting, or classifying the reference failed.
34 Extract(Error),
35 /// The reference is a URL but network resolution is not available. Enable
36 /// the `remote` feature, or resolve it yourself and call
37 /// [`validate_with_manifest`].
38 RemoteNotEnabled(String),
39 /// Resolving a remote manifest over the network failed.
40 #[cfg(feature = "remote")]
41 Fetch(String),
42 /// `c2pa-rs` rejected the manifest data or errored during validation.
43 C2pa(c2pa::Error),
44}
45
46impl std::fmt::Display for BridgeError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Extract(e) => write!(f, "manifest extraction failed: {e}"),
50 Self::RemoteNotEnabled(url) => {
51 write!(
52 f,
53 "reference is a URL ({url}); enable the `remote` feature to resolve it"
54 )
55 }
56 #[cfg(feature = "remote")]
57 Self::Fetch(msg) => write!(f, "remote manifest fetch failed: {msg}"),
58 Self::C2pa(e) => write!(f, "c2pa validation error: {e}"),
59 }
60 }
61}
62
63impl std::error::Error for BridgeError {}
64
65impl From<Error> for BridgeError {
66 fn from(e: Error) -> Self {
67 BridgeError::Extract(e)
68 }
69}
70
71impl From<c2pa::Error> for BridgeError {
72 fn from(e: c2pa::Error) -> Self {
73 BridgeError::C2pa(e)
74 }
75}
76
77/// Validate structured `text` against a C2PA Manifest Store the caller has
78/// already resolved (from a `data:` URI or its own fetch).
79///
80/// Returns the [`c2pa::Reader`]; call [`Reader::validation_state`] or
81/// [`Reader::validation_results`] for the verdict. The reader validates the
82/// signature and trust chain, and the `c2pa.hash.data` binding over the raw
83/// bytes of `text` (with the manifest block excluded by the assertion's
84/// exclusion range).
85pub fn validate_with_manifest(
86 text: &str,
87 c2pa_data: &[u8],
88 format: &str,
89) -> Result<Reader, BridgeError> {
90 let reader = Reader::from_context(Context::default()).with_manifest_data_and_stream(
91 c2pa_data,
92 format,
93 Cursor::new(text.as_bytes()),
94 )?;
95 Ok(reader)
96}
97
98/// Extract the reference from `text`, resolve it, and validate.
99///
100/// A `data:application/c2pa;base64,` reference is decoded inline. A URL
101/// reference is fetched when the `remote` feature is enabled; otherwise this
102/// returns [`BridgeError::RemoteNotEnabled`] and the caller should fetch the
103/// bytes and use [`validate_with_manifest`].
104pub fn validate(text: &str, format: &str) -> Result<Reader, BridgeError> {
105 let extracted = extract_manifest(text)?;
106 match classify_reference(&extracted.reference)? {
107 Reference::Embedded(bytes) => validate_with_manifest(text, &bytes, format),
108 Reference::Url(url) => {
109 #[cfg(feature = "remote")]
110 {
111 let bytes = resolve_url(&url)?;
112 validate_with_manifest(text, &bytes, format)
113 }
114 #[cfg(not(feature = "remote"))]
115 {
116 Err(BridgeError::RemoteNotEnabled(url))
117 }
118 }
119 }
120}
121
122/// Fetch a remote C2PA Manifest Store over HTTP(S).
123#[cfg(feature = "remote")]
124pub fn resolve_url(url: &str) -> Result<Vec<u8>, BridgeError> {
125 use std::io::Read;
126
127 let resp = ureq::get(url)
128 .call()
129 .map_err(|e| BridgeError::Fetch(e.to_string()))?;
130 let mut bytes = Vec::new();
131 resp.into_reader()
132 .take(64 * 1024 * 1024)
133 .read_to_end(&mut bytes)
134 .map_err(|e| BridgeError::Fetch(e.to_string()))?;
135 Ok(bytes)
136}