Skip to main content

crates.io docs.rs CI OpenSSF Scorecard License

Overview

Implements the Embedding Manifests into Unstructured Text section of the C2PA Technical Specification, which carries a C2PA Manifest Store inside a Unicode text stream as a run of non-rendering variation selectors, so provenance survives copy and paste between systems that have no file.

Hello world.<U+FEFF><variation-selector run carrying magic|version|length|manifest>

The specification describes this method as one that should only be used where no other embedding method is feasible. For source code, configuration, and markup, prefer c2pa-structured-text. This crate exists for the case the spec carves out: text with no container.

This crate owns two things:

  1. The frame — encode, embed, locate, and strip the C2PATextManifestWrapper, including the specified deterministic padding.
  2. The hard binding — the exact c2pa.hash.data coverage for unstructured text, with compute and verify.

Signature verification, certificate trust, and assertion validation are not reimplemented here.

Not certified or conformance-tested by the C2PA. It implements the embedding and hard binding as specified.

Zero dependencies by default

[dependencies]
c2pa-unstructured-text = "0.1"

The frame and the binding algorithm pull nothing in. Hashing and NFC are injected through two traits, so a host that already provides them supplies its own:

use c2pa_unstructured_text::hardbinding::{Algorithm, Hasher, Normalizer};

struct HostCrypto;
impl Hasher for HostCrypto {
    fn digest(&self, alg: Algorithm, data: &[u8]) -> Vec<u8> { todo!("call the runtime") }
}
struct HostNfc;
impl Normalizer for HostNfc {
    fn nfc(&self, text: &str) -> String { todo!("call the runtime") }
}

That matters at the edge. A Cloudflare Worker or a browser already has SHA-2 and String.prototype.normalize, so shipping Unicode tables into the bundle is pure waste. Enable hard-binding to get ready-made implementations instead; it adds convenience, never capability.

Embed and extract

use c2pa_unstructured_text::wrapper;

let asset = wrapper::embed("Hello world.", b"manifest-bytes").unwrap();

let found = wrapper::extract(&asset).unwrap();
assert_eq!(found.payload, b"manifest-bytes");
assert_eq!(wrapper::strip(&asset, found.range()).unwrap(), "Hello world.");

The hard binding

The exclusion range covers the U+FEFF marker together with the selector run, including any trailing padding. Offsets are into the text as stored, before normalization. A validator removes the excluded bytes first, normalizes what remains to NFC, then hashes.

# #[cfg(feature = "hard-binding")] {
use c2pa_unstructured_text::hardbinding::{
    compute_data_hash, verify_data_hash, Algorithm, RustCrypto, UnicodeNfc,
};
use c2pa_unstructured_text::wrapper;

let asset = wrapper::embed("Hello world.", b"manifest-bytes").unwrap();
let binding =
    compute_data_hash(&asset, Algorithm::Sha256, &RustCrypto, &UnicodeNfc).unwrap();

assert!(verify_data_hash(&asset, &binding, &RustCrypto, &UnicodeNfc).is_ok());
# }

Normalizing before computing offsets would shift every one of them whenever the stored text is not already NFC, which is why the order is fixed this way.

Deterministic padding

The wrapper's byte length depends on the manifest's byte distribution, since low bytes encode to three UTF-8 bytes and the rest to four. That is circular when the exclusion length has to go inside the manifest being measured. The specification breaks the cycle with a target that depends only on the manifest size, and fixes the padding byte values so compliant generators emit byte-identical wrappers:

use c2pa_unstructured_text::wrapper;

assert_eq!(wrapper::target_length(16), 125);
assert_eq!(wrapper::encode(b"c2pa-manifest-01").unwrap().len(), 114);
// The 11-byte gap decomposes as one 0x00 then two 0x10.
assert_eq!(wrapper::padding(11).unwrap(), vec![0x00, 0x10, 0x10]);
assert_eq!(wrapper::encode_padded(b"c2pa-manifest-01").unwrap().len(), 125);

This is the part of the method implementations most easily diverge on, so it is covered by vectors cross-checked against the C2PA public test corpus.

Locating

The specification defines two failure codes here, and the crate reports both:

outcome code meaning
no wrapper, no candidate (none) the text is unsigned
candidate detected, none decodes manifest.text.corruptedWrapper magic found, frame malformed
more than one valid wrapper manifest.text.multipleWrappers rejected

Only the first means the text carries no provenance, which is what is_no_manifest_located() reports — an integrator needs to tell "carries no provenance" from "carried provenance that was rejected":

use c2pa_unstructured_text::{wrapper, Error};

let err = wrapper::extract("no wrapper here").unwrap_err();
assert_eq!(err, Error::NotFound);
assert!(err.is_no_manifest_located());
assert_eq!(err.code(), None);

A candidate that fails to decode beside a valid wrapper is skipped, not reported: letting stray bytes carrying the magic invalidate an otherwise good wrapper would hand anyone who can append to the text a denial of service.

The specification is in tension here. Placement rule 5 says a validator "may encounter multiple wrappers" and that selection "is governed by the exclusions field", while the status-code section makes more than one valid wrapper a failure. This crate follows the explicit failure code.

Comparison with the structured-text binding

Both crates expose the same shape, so a dispatcher can treat them alike, but coverage differs deliberately.

unstructured (this crate) structured
carrier variation selectors ASCII armour comment
normalization NFC, after exclusion none
exclusion marker + selector run + padding the manifest block line
default deps none none

Features

  • hard-bindingRustCrypto and UnicodeNfc implementations (pulls sha2 and unicode-normalization).
  • checksum-v2 — a v2 frame carrying a truncated hash over the header and payload, so a mangled carrier is rejected rather than decoded to wrong bytes. A WritersLogic extension, not part of the specified frame.

No feature is enabled by default.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

c2pa_unstructured_text-0.1.0.tar.gz (39.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

c2pa_unstructured_text-0.1.0-cp39-abi3-win_amd64.whl (214.8 kB view details)

Uploaded CPython 3.9+Windows x86-64

c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (348.0 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (341.4 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_11_0_arm64.whl (313.2 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl (321.2 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file c2pa_unstructured_text-0.1.0.tar.gz.

File metadata

  • Download URL: c2pa_unstructured_text-0.1.0.tar.gz
  • Upload date:
  • Size: 39.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for c2pa_unstructured_text-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c520fc52da4c752695ebfbd00c8f32594bebcd167128cb582c8ff053ca238bd0
MD5 c851ca4c3a7d0ab564d276ea2a69e4a5
BLAKE2b-256 233417115450fbb49dc2dc63e427e9a40f2b2801db142ab9019caa4b8e1d5add

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0.tar.gz:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file c2pa_unstructured_text-0.1.0-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for c2pa_unstructured_text-0.1.0-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 33845d3b564d99698e3306534ac0ef34139bc6f571da43090aba0561d185092c
MD5 45a66dcb6467b348c2276cb7adb78dac
BLAKE2b-256 f6f9fe7001495e7165d89e4846102c4b6eef2374aaa0db90ef7ba684a02c591d

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0-cp39-abi3-win_amd64.whl:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b22531a68995b2721c531eee9c2dfc5e43360f9b2132954c5183b4215ef83254
MD5 2f19ecfef784dce5ea618384c21240ef
BLAKE2b-256 c202b1574c2772ae46737b2cacb9c756448aab56475d6ba50ca71797431320e3

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 fe216bf566404335a65ec3f02b39c3c46fbdbe202fa5fa3b18e7682c90fa8e2a
MD5 8975b3b60734c0558c47426cc2bbf3e9
BLAKE2b-256 484ea2d8271c8ba59074d457b86282434ea55fea3559c6048f4815777b75286d

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 6720421d59e47dca207c435a5da6a0ce3f5ac8614b328a4f01e6494fb9e75c8c
MD5 7f1962b09883c60d09dc7d5dfe602d23
BLAKE2b-256 aff971786657e372898935e5022145d2dedf5a880eed0ebfe423fb0c40ccf280

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 259ed6e10f3b214b19e1906c54de920b7c33833ee05ef5e4f59fdbc8e929641e
MD5 07bf118e04710043a40ab3727e12beb3
BLAKE2b-256 04808903892193daf1519ecd1adf8a54c948d94c9b246b77822c7368f11318db

See more details on using hashes here.

Provenance

The following attestation bundles were made for c2pa_unstructured_text-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on writerslogic/c2pa-unstructured-text

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

6 files

This release

0.1.0 This release

6 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page