Skip to main content

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}