c2pa 0.91.10

Rust SDK for C2PA (Coalition for Content Provenance and Authenticity) implementors
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# Asset handler architecture guide

> [!NOTE]
> This documentation is primarily for SDK developers and contributors, not SDK users or consumers.
> `asset_io.rs` itself (`AssetIO`, `C2paReader`, `C2paWriter`, and friends) is a public,
> documented module — consumers implementing a custom format handler should start with
> its rustdoc rather than this file.

## How asset handlers work

The asset handler system follows a **trait-based plugin architecture** where each file format implements a set of traits defined in `asset_io.rs`, then gets registered in a central dispatch system in `jumbf_io.rs`.

## Dispatch layer

The system uses two `lazy_static` `HashMap`s that map file extension/MIME strings to handler instances:

- **`CAI_READERS`** — Maps every supported extension and MIME type to a `Box<dyn AssetIO>` handler instance. Used for reading manifests, getting object locations, and accessing optional capabilities.
- **`CAI_WRITERS`** — Maps every writable extension/MIME type to a `Box<dyn C2paWriter>` handler instance. Only populated for handlers that return `Some` from `get_writer()`.

At startup, each handler is instantiated, asked for its `supported_types()` (e.g., `["jpg", "jpeg", "image/jpeg"]`), and entries are created in the map for each type. When the SDK needs to process a file, it looks up the extension/MIME in the map and gets the appropriate handler.

### Lookup functions

| Function | Returns | Purpose |
|----------|---------|---------|
| `get_assetio_handler(ext)` | `Option<&dyn AssetIO>` | Full handler with all capabilities |
| `get_assetio_handler_from_path(path)` | `Option<&dyn AssetIO>` | Same, but extracts extension from file path |
| `get_cailoader_handler(asset_type)` | `Option<&dyn C2paReader>` | Stream-based reader only |
| `get_caiwriter_handler(asset_type)` | `Option<&dyn C2paWriter>` | Stream-based writer only |

### Public entry points

| Function | Description |
|----------|-------------|
| `load_jumbf_from_stream()` | Read JUMBF manifest from a stream |
| `load_jumbf_from_memory()` | Read JUMBF manifest from in-memory bytes |
| `load_jumbf_from_file()` | Read JUMBF manifest from a file path |
| `save_jumbf_to_stream()` | Write JUMBF manifest to a stream |
| `save_jumbf_to_memory()` | Write JUMBF manifest to in-memory bytes |
| `save_jumbf_to_file()` | Write JUMBF manifest to a file path |
| `remove_jumbf_from_file()` | Remove C2PA manifest from a file. **Deprecated** — unused within the SDK; slated for removal. |
| `get_supported_types()` | List all supported extensions/MIME types |


## Trait hierarchy

There are **three mandatory traits** and **four optional traits** that a handler can implement.

### Mandatory traits

The mandatory traits are:

- [C2paReader]#manifestreader- 
- [C2paWriter]#manifestwriter 
- [AssetIO]#assetio--the-master-trait

#### C2paReader 

Use `C2paReader` for stream-based reading:

```rust
pub trait C2paReader: Sync + Send {
    fn read_cai(&self, asset_reader: &mut dyn ReadSeek) -> Result<Vec<u8>>;
    fn read_xmp(&self, asset_reader: &mut dyn ReadSeek) -> Option<String>;
}
```

| Method | Responsibility |
|--------|---------------|
| `read_cai` | Extract the raw C2PA JUMBF manifest store bytes from a stream. Return `Error::JumbfNotFound` if none exists. Return `Error::TooManyManifestStores` if more than one manifest is detected. |
| `read_xmp` | Extract XMP metadata as a string. Return `None` if the format doesn't contain XMP or if no XMP is present. |

#### C2paWriter

Use `C2paWriter` for stream-based writing:

```rust
pub trait C2paWriter: Sync + Send {
    fn write_cai(
        &self,
        input_stream: &mut dyn ReadSeek,
        output_stream: &mut dyn ReadWriteSeek,
        store_bytes: &[u8],
    ) -> Result<()>;

    fn get_object_locations_from_stream(
        &self,
        input_stream: &mut dyn ReadSeek,
    ) -> Result<Vec<HashObjectPositions>>;

    fn remove_cai_store_from_stream(
        &self,
        input_stream: &mut dyn ReadSeek,
        output_stream: &mut dyn ReadWriteSeek,
    ) -> Result<()>;
}
```

| Method | Responsibility |
|--------|---------------|
| `write_cai` | Read from input, embed `store_bytes` as the C2PA manifest, write the result to output. Must replace any existing manifest. The output must be a valid file of the same format. |
| `get_object_locations_from_stream` | Return the byte positions and lengths of key regions (CAI manifest, XMP, other) so the hashing system knows what to hash and what to exclude. If no manifest exists yet, insert a placeholder so the offset is known. |
| `remove_cai_store_from_stream` | Rewrite the asset without any C2PA manifest data. The output must remain a valid file. |

#### AssetIO — the master trait

`AssetIO` is the master trait:

```rust
pub trait AssetIO: Sync + Send {
    // Construction
    fn new(asset_type: &str) -> Self where Self: Sized;
    fn get_handler(&self, asset_type: &str) -> Box<dyn AssetIO>;

    // Reader/Writer access
    fn get_reader(&self) -> &dyn C2paReader;
    fn get_writer(&self, _asset_type: &str) -> Option<Box<dyn C2paWriter>> { None }

    // File-based operations (default-derived from the stream-based methods above;
    // most handlers never need to override these — see below)
    fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> { .. }
    fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { .. } // deprecated

    // Metadata
    fn supported_types(&self) -> &[&str];

    // Optional capability accessors (all default to None)
    fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { None }
    fn remote_manifest_url_ref(&self) -> Option<&dyn RemoteManifestUrl> { None }
    fn asset_box_hash_ref(&self) -> Option<&dyn AssetBoxHash> { None }
    fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> { None }
}
```

`read_cai_store` and `get_object_locations` (file-`Path` variants) were removed entirely —
tracing their only production-adjacent callers showed both were dead outside each
handler's own unit tests; use `get_reader().read_cai(...)` /
`get_writer(ext)?.get_object_locations_from_stream(...)` against an opened file instead.

`save_cai_store` and `remove_cai_store` keep default implementations, derived from the
stream-based methods, that open the file, delegate to `C2paWriter`, and move the result
into place via `rename_or_move`:

```rust
fn save_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()> {
    let ext = asset_path.extension().and_then(|e| e.to_str()).unwrap_or_default();
    let writer = self.get_writer(ext).ok_or(Error::UnsupportedType)?;
    let mut input_stream = std::fs::OpenOptions::new()
        .read(true).open(asset_path).map_err(Error::IoError)?;
    let mut temp_file = tempfile_builder("c2pa_temp")?;
    writer.write_cai(&mut input_stream, &mut temp_file, store_bytes)?;
    rename_or_move(temp_file, asset_path)
}
```

Most handlers never override these two — only override when a format needs different
file-level semantics (e.g. `C2paIO`'s sidecar format).
`remove_cai_store` only backs the already-deprecated `jumbf_io::remove_jumbf_from_file`;
it and its default will be removed together in a future release.

### Optional traits

The optional traits are:

- [AssetPatch]#assetpatch 
- [RemoteManifestUrl / WriteXmp]#remotemanifesturl--writexmp 
- [AssetBoxHash]#assetboxhash 
- [ComposedManifestRef]#composedmanifestref-

#### AssetPatch

Use `AssetPatch`for in-place binary patching:

```rust
pub trait AssetPatch {
    fn patch_cai_store(&self, asset_path: &Path, store_bytes: &[u8]) -> Result<()>;
}
```

It optimizes manifest updates by patching bytes in-place without rewriting the whole file. Only works when the new store is the same size as the existing one. This is a performance optimization.

#### RemoteManifestUrl / WriteXmp

Use `RemoteManifestUrl` for writing a remote manifest URL into an asset (the C2PA spec
calls this a remote manifest URI — the SDK's `Builder`/`Claim` layers call the same
concept `remote_url`/`remote_manifest`; this trait is the asset-level mechanics
underneath all of them):

```rust
pub trait RemoteManifestUrl {
    fn write_remote_manifest_url(
        &self,
        source_stream: &mut dyn ReadSeek,
        output_stream: &mut dyn ReadWriteSeek,
        remote_manifest_url: &str,
    ) -> Result<()>;
}
```

Note the direction: this is for a *remote* manifest (referenced by URL, not present in
the asset) — unrelated to *embedding* a manifest store, which is what `C2paWriter::write_cai`
does. It's deliberately narrow: it only covers "point a reader at a manifest hosted
elsewhere." A future non-XMP technique (e.g. a watermark) would be its own separate
trait, not another variant or parameter here — which is also why the old
`RemoteRefEmbedType` enum (and its unused `StegoS`/`StegoB`/`Watermark` variants, plus
the file-`Path`-based `embed_reference` method) is gone; the method just takes the URL
directly.

**Don't implement `RemoteManifestUrl` directly.** Since writing a remote manifest URL is
always "merge it into XMP, then write XMP", implement `WriteXmp` instead —
`RemoteManifestUrl` has a blanket impl for any type that implements `WriteXmp` (and
`C2paReader`, for reading the current XMP):

```rust
pub trait WriteXmp {
    fn write_xmp(
        &self,
        source_stream: &mut dyn ReadSeek,
        output_stream: &mut dyn ReadWriteSeek,
        xmp: &str,
    ) -> Result<()>;
}
```

The blanket impl reads the current XMP via `C2paReader::read_xmp` (falling back to
`MIN_XMP` if there is none), merges in the URL with `xmp_inmemory_utils::add_provenance`,
and calls `write_xmp` with the finished string — every built-in handler that supports
`RemoteManifestUrl` implements `WriteXmp`, not `RemoteManifestUrl`, and none of them call
`add_provenance` themselves anymore. A `write_xmp` impl only needs the format-specific
insertion logic (find/replace the existing XMP block or chunk, or pick an insertion
point if there isn't one yet); one exception is `SvgIO`, which needs a bit more than the
plain XMP string to know *where* in the XML tree to splice — it re-parses the asset
internally via a private helper rather than relying solely on the string handed to it.

#### AssetBoxHash

Use `AssetBoxHash` for box hash support:

```rust
pub trait AssetBoxHash {
    fn get_box_map(&self, input_stream: &mut dyn ReadSeek) -> Result<Vec<BoxMap>>;
}
```

It generates a `BoxMap` describing all hashable regions in the file for `c2pa.hash.boxes` assertions. Each `BoxMap` entry contains:

```rust
pub struct BoxMap {
    pub names: Vec<String>,      // Box/segment name (e.g., "ftyp", "C2PA", "SOI")
    pub alg: Option<String>,     // Hash algorithm (filled by hashing system)
    pub hash: ByteBuf,           // Hash value (filled by hashing system)
    pub excluded: Option<bool>,  // Whether this region is excluded from hashing
    pub pad: ByteBuf,            // Padding (filled by hashing system)
    pub range_start: u64,        // Byte offset from start of file
    pub range_len: u64,          // Length in bytes
}
```

#### ComposedManifestRef 

Use `ComposedManifestRef` pre-composed manifest wrapping

```rust
pub trait ComposedManifestRef {
    fn compose_manifest(&self, manifest_data: &[u8], format: &str) -> Result<Vec<u8>>;
}
```

Wraps raw manifest store bytes into the format-specific container structure. For example:
- **JPEG**: Wraps into multi-segment APP11 data with JPEG XT headers
- **JPEG XL**: Wraps into a single ISOBMFF `jumb` superbox containing the C2PA manifest store
- **PNG**: Wraps into a `caBX` chunk with CRC

> [!NOTE] 
> The embeddable manifest API uses `ComposedManifestRef` to provide a composed manifest ready for direct insertion into an asset. When composed manifests are used, the asset handler is **not** performing the manifest embedding.

## Registration checklist

Adding a new handler requires touching exactly **four files**:

| Step | File | What to do |
|------|------|------------|
| 1 | `sdk/src/asset_handlers/mod.rs` | Add `pub mod your_format_io;` |
| 2 | `sdk/src/jumbf_io.rs` (imports) | Add `use crate::asset_handlers::your_format_io::YourFormatIO;` |
| 3 | `sdk/src/jumbf_io.rs` (registration) | Add `Box::new(YourFormatIO::new(""))` to both `CAI_READERS` and, if supported, `CAI_WRITERS` lazy_static blocks |
| 4 | `sdk/src/jumbf_io.rs` (tests) | Add handler to `test_get_assetio`, `test_get_reader`, `test_get_writer`; add extension to `test_get_supported_list`; add a `test_streams_yourformat` integration test |
| 5 | `docs/supported-formats.md` | Add to the supported formats table |


## Key behaviors and rules

### Format validation

Every handler must validate that the input is actually the expected format before processing. Return `Error::InvalidAsset` with a descriptive message on invalid input. Never silently produce corrupted output.

### Idempotent writes

`write_cai` must handle both:
- **Fresh assets** (no existing manifest) — insert new manifest at the correct location
- **Assets with existing manifests** — remove old manifest, insert new one

The output must always be a valid file of the original format.

### HashObjectPositions contract

The locations returned by `get_object_locations_from_stream` must:

- Be **non-overlapping** byte ranges
- Tag the C2PA manifest region as `HashBlockObjectType::Cai`
- Tag XMP regions as `HashBlockObjectType::Xmp`
- Tag everything else as `HashBlockObjectType::Other`
- If no manifest exists yet, create a placeholder so the hashing system knows where the manifest will go

```rust
pub struct HashObjectPositions {
    pub offset: usize,              // Byte offset from start of file
    pub length: usize,              // Length in bytes
    pub htype: HashBlockObjectType, // Cai, Xmp, Other, or OtherExclusion
}
```

### BoxMap contract

When implementing `AssetBoxHash`, the box map entries must:

- **Cover the contents of all boxes present in the file** — all boxes must be represented in the list
- **Be ordered by offset** — ascending `range_start`
- **Be non-overlapping** — no two entries share byte ranges
- **Label the C2PA manifest entry** with the constant name `C2PA_BOXHASH` (value: `"C2PA"`)
- Only populate `names`, `range_start`, and `range_len` — the hashing system fills in `hash`, `alg`, and `pad`

### Thread safety

All traits require `Sync + Send`. Handlers must be **stateless structs** with no mutable internal state. Pass configuration through the `asset_type` parameter to `new()`.

### Stream semantics

- All stream-based methods receive `&mut dyn ReadSeek` (= `Read + Seek + Send`) for input
- Output streams use `&mut dyn ReadWriteSeek` (= `Read + Seek + Write + Send`)
- Handlers should `rewind()` streams before reading
- Never assume the stream position on entry

### Error conventions

| Error | When to return |
|-------|---------------|
| `Error::JumbfNotFound` | No C2PA manifest exists in the asset |
| `Error::TooManyManifestStores` | More than one C2PA manifest detected |
| `Error::InvalidAsset(String)` | Input is corrupt or not the expected format |
| `Error::UnsupportedType` | Operation not supported for this format |
| `Error::EmbeddingError` | Failed to embed manifest into the asset |
| `Error::IoError(io::Error)` | Underlying I/O failure |

> [!NOTE] 
> Handlers may also return format-specific errors to provide more detailed diagnostics.


## Trait implementation matrix

| Handler | C2paReader | C2paWriter | AssetIO | RemoteManifestUrl | AssetBoxHash | ComposedManifestRef | AssetPatch |
|---------|:---------:|:---------:|:-------:|:--------------:|:------------:|:-------------------:|:----------:|
| **BmffIO** (MP4, HEIF, AVIF, MOV) | Y | Y | Y | Y | -- | -- | Y |
| **JpegIO** (JPG, JPEG) | Y | Y | Y | Y | Y | Y | -- |
| **JpegXlIO** (JXL) | Y | Y | Y | Y | Y | Y | -- |
| **PngIO** (PNG) | Y | Y | Y | Y | Y | Y | -- |
| **TiffIO** (TIF, TIFF, DNG) | Y | Y | Y | Y | -- | Y | Y |
| **RiffIO** (AVI, WAV, WEBP) | Y | Y | Y | Y | -- | -- | Y |
| **SvgIO** (SVG) | Y | Y | Y | Y | -- | -- | Y |
| **Mp3IO** (MP3) | Y | Y | Y | Y | -- | -- | Y |
| **GifIO** (GIF) | Y | Y | Y | Y | Y | Y | Y |
| **C2paIO** (C2PA sidecar) | Y | Y | Y | -- | Y | Y | -- |
| **PdfIO** (PDF) | Y | Y | Y | -- | -- | Y | -- |

### Key observations

- `C2paReader` + `AssetIO` are implemented by **every** handler (minimum requirement)
- `AssetPatch` is a performance optimization — formats that support it can update manifests in-place
- All traits are independent; format capabilities, as defined by the C2PA specification, determine which ones to implement


## Integration test pattern

Every handler should have a `test_streams_*` test in `jumbf_io.rs` that exercises two standard test harnesses:

### test_jumbf

Verifies the full round-trip:
1. Create a test C2PA store and sign it
2. Write JUMBF to the asset stream
3. Read JUMBF back from the output
4. Assert the read-back matches the original
5. Remove the C2PA store from the asset
6. Verify the manifest is gone (`Error::JumbfNotFound`)

### test_remote_ref

Verifies remote manifest URL writing (only applicable if the handler supports `RemoteManifestUrl`):
1. Get the `RemoteManifestUrl` from the handler
2. Write a remote manifest URL into the asset's XMP if supported
3. Read XMP back from the output if supported
4. Extract provenance URL from the XMP
5. Assert it matches the original URL

### Example

```rust
#[test]
fn test_streams_jxl() {
    use crate::asset_handlers::jpegxl_io;
    let container = jpegxl_io::tests::build_test_jxl_container();
    let mut reader = Cursor::new(container);
    test_jumbf("jxl", &mut reader);
    reader.rewind().unwrap();
    test_remote_ref("jxl", &mut reader);
}
```

## Architectural diagram

```mermaid
flowchart TB
    subgraph dispatch["jumbf_io.rs — Dispatch layer"]
        readers["CAI_READERS:<br><pre>HashMap&lt;String, Box&lt;dyn AssetIO&gt;&gt;</pre>"]
        writers["CAI_WRITERS:<br><pre>HashMap&lt;String, Box&tl;dyn C2paWriter&gt;&gt;</pre> "]
        mappings["'jpg' → JpegIO <br> 'png' → PngIO <br> 'jxl' → JpegXlIO <br> 'mp4' → BmffIO  <br> ..."]
    end

    subgraph traits["asset_io.rs — Trait definitions"]
        direction TB
        subgraph mandatory["Mandatory"]
            t1["<b>C2paReader</b>:<br>read_cai, read_xmp"]
            t2["<b>C2paWriter</b>:<br>write_cai, get_object_locations, remove_cai_store"]
            t3["<b>AssetIO</b>:<br>master trait: file-based ops, supported_types, accessors"]
        end
        subgraph optional["Optional"]
            t4["<b>AssetPatch</b>:<br>In-place patching"]
            t5["<b>RemoteManifestUrl</b> (via <b>WriteXmp</b>):<br>Remote manifest URL"]
            t6["<b>AssetBoxHash</b>:<br>Box map for c2pa.hash.boxes"]
            t7["<b>ComposedManifestRef</b>:<br>Pre-composed wrapping"]
        end
    end

    handlers["jpeg_io.rs  jpegxl_io.rs  png_io.rs  bmff_io.rs  tiff_io.rs   riff_io.rs  svg_io.rs  mp3_io.rs  gif_io.rs  c2pa_io.rs  pdf_io.rs"]

    dispatch -->|"Looks up by extension/MIME"| traits
    traits -->|"Implemented by asset_handlers/&lt;format&gt;_io.rs"| handlers
```