c2pa_unstructured_text/lib.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//! C2PA manifest embedding and hard binding for unstructured text.
6//!
7//! Implements the *Embedding Manifests into Unstructured Text* section of the
8//! C2PA Technical Specification, which carries a C2PA Manifest Store inside a
9//! Unicode text stream as a run of non-rendering variation selectors, so that
10//! provenance survives copy and paste between systems that have no file.
11//!
12//! The specification describes this method as one that *should only be used
13//! where no other embedding method is feasible*. For source code, configuration,
14//! and markup, prefer the structured-text method in
15//! [`c2pa-structured-text`](https://crates.io/crates/c2pa-structured-text); for
16//! HTML, prefer the dedicated HTML method.
17//!
18//! # Scope
19//!
20//! - **Frame** ([`wrapper`]): encode, embed, locate, and strip the
21//! `C2PATextManifestWrapper`, plus the specified deterministic padding.
22//! - **Hard binding** ([`hardbinding`]): the exact `c2pa.hash.data` coverage,
23//! with compute and verify.
24//!
25//! Signature verification, certificate trust, and assertion validation are not
26//! implemented here.
27//!
28//! # Zero dependencies by default
29//!
30//! The frame and the binding algorithm pull nothing in. Hashing and NFC are
31//! injected through [`hardbinding::Hasher`] and [`hardbinding::Normalizer`], so
32//! a host that already provides them (a Cloudflare Worker, a browser) supplies
33//! its own. Enabling `hard-binding` adds ready-made implementations; it adds
34//! convenience, never capability.
35//!
36//! # Examples
37//!
38//! Embed a Manifest Store and recover it:
39//!
40//! ```
41//! use c2pa_unstructured_text::wrapper;
42//!
43//! let asset = wrapper::embed("Hello world.", b"manifest-bytes").unwrap();
44//! assert_eq!(wrapper::extract(&asset).unwrap().payload, b"manifest-bytes");
45//! ```
46//!
47//! The wrapper is invisible, so the visible text is unchanged:
48//!
49//! ```
50//! use c2pa_unstructured_text::wrapper;
51//!
52//! let asset = wrapper::embed("Hello world.", b"m").unwrap();
53//! let w = wrapper::extract(&asset).unwrap();
54//! assert_eq!(wrapper::strip(&asset, w.range()).unwrap(), "Hello world.");
55//! ```
56//!
57//! Bind the visible text:
58//!
59//! ```
60//! # #[cfg(feature = "hard-binding")] {
61//! use c2pa_unstructured_text::hardbinding::{
62//! compute_data_hash, verify_data_hash, Algorithm, RustCrypto, UnicodeNfc,
63//! };
64//! use c2pa_unstructured_text::wrapper;
65//!
66//! let asset = wrapper::embed("Hello world.", b"manifest-bytes").unwrap();
67//! let binding =
68//! compute_data_hash(&asset, Algorithm::Sha256, &RustCrypto, &UnicodeNfc).unwrap();
69//!
70//! assert!(verify_data_hash(&asset, &binding, &RustCrypto, &UnicodeNfc).is_ok());
71//! # }
72//! ```
73//!
74//! # Relationship to the structured-text binding
75//!
76//! Both crates expose the same shape, so a dispatcher can treat them alike, but
77//! the coverage rules differ deliberately. A.9 hashes the raw file bytes with no
78//! normalization, because structured text is byte-stable on disk. A.8 removes
79//! the wrapper and then normalizes to NFC, because the text is clipboard
80//! portable and may arrive in any normalization form. Offsets are into the text
81//! as stored in both cases.
82//!
83//! # Features
84//!
85//! - `hard-binding` — [`hardbinding::RustCrypto`] and
86//! [`hardbinding::UnicodeNfc`] (pulls `sha2` and
87//! `unicode-normalization`).
88//! - `checksum-v2` — a v2 frame carrying a truncated hash over the header and
89//! payload, so a mangled carrier is rejected rather than decoded to wrong
90//! bytes. A WritersLogic extension, not part of the specified frame.
91//!
92//! No feature is enabled by default.
93
94#![forbid(unsafe_code)]
95
96pub mod error;
97pub mod hardbinding;
98pub mod vs;
99pub mod wrapper;
100
101#[cfg(target_arch = "wasm32")]
102mod wasm;
103
104#[cfg(all(feature = "python", not(target_arch = "wasm32")))]
105mod python;
106
107pub use error::Error;
108pub use hardbinding::{Algorithm, DataHash, Exclusion, Hasher, Normalizer};
109pub use wrapper::{Wrapper, MAGIC, MARKER, VERSION};