Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Features:
- Add assertions and ingredients to assets.
- Examples and unit tests to demonstrate usage.

<div style={{display: 'none'}}>
<div class="github-only">

For the best experience, read the docs on the [CAI Open Source SDK documentation website](https://opensource.contentauthenticity.org/docs/c2pa-c).

Expand All @@ -21,7 +21,7 @@ If you want to view the documentation in GitHub, see:
- [Supported formats](https://github.com/contentauth/c2pa-rs/blob/main/docs/supported-formats.md)
- [Configuring the SDK using `Context` and `Settings`](docs/context-settings.md)
- [Using Builder intents](docs/intents.md) to ensure spec-compliant manifests
- Using [working stores and archvies](docs/working-stores.md)
- Using [working stores and archives](docs/working-stores.md)
- Selectively constructing manifests by [filtering actions and ingredients](docs/selective-manifests.md)
- [Diagram of public classes in the Python library and their relationships](docs/class-diagram.md)
- [Release notes](docs/release-notes.md)
Expand Down Expand Up @@ -72,7 +72,7 @@ export C2PA_RS_PATH=/path/to/c2pa-rs
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH
```

This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. The release profile is used by default; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`:
This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. By default, the build uses the release profile; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`:

```sh
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH EXTRA_BUILD_ARGS="--debug"
Expand Down
1 change: 0 additions & 1 deletion docs/class-diagram.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Class diagram

This diagram shows the public classes in the Python library and their relationships.
Expand Down
18 changes: 9 additions & 9 deletions docs/context-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,10 @@ The Settings JSON has this top-level structure:

The settings format is **JSON** only. Pass JSON strings to `Settings.from_json()` or `Context.from_json()`, and dictionaries to `Settings.from_dict()` or `Context.from_dict()`. The `from_dict()` methods convert Python dictionaries to a format compatible with the underlying native libraries.

Notes:
- All properties are optional. If you don't specify a value, the SDK uses the default value.
- If you specify a value of `null` (or `None` in a dict), the property is explicitly set to `null`, not the default. This distinction is important when you want to override a default behavior.
- For Boolean values, use JSON Booleans `true`/`false` in JSON strings, or Python `True`/`False` in dicts.
> [!NOTE]
> - All properties are optional. If you don't specify a value, the SDK uses the default value.
> - If you specify a value of `null` (or `None` in a dict), the property is explicitly set to `null`, not the default. This distinction is important when you want to override a default behavior.
> - For Boolean values, use JSON Booleans `true`/`false` in JSON strings, or Python `True`/`False` in dicts.

The settings JSON schema is shared across all C2PA SDKs (Rust, C/C++, Python, and so on). For a complete reference to all properties, see the [SDK object reference - Settings](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema).

Expand Down Expand Up @@ -420,8 +420,8 @@ The following properties default to `true` (verification enabled):

- `verify_after_reading` - Automatically verify manifests when reading assets. Disable only if you want to manually control verification timing.
- `verify_after_sign` - Automatically verify manifests after signing. Recommended to keep enabled to catch signing errors immediately.
- `verify_trust` - Verify signing certificates against configured trust anchors. WARNING: Disabling makes verification non-compliant.
- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. WARNING: Disabling makes verification non-compliant.
- `verify_trust` - Verify signing certificates against configured trust anchors. **Warning:** Disabling makes verification non-compliant.
- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. **Warning:** Disabling makes verification non-compliant.
- `remote_manifest_fetch` - Fetch remote manifests referenced in the asset. Disable in offline or air-gapped environments.

> [!WARNING]
Expand Down Expand Up @@ -628,9 +628,9 @@ C2PA uses a certificate-based trust model to prove who signed an asset. When cre
- **Certificate chain** (`sign_cert`): An X.509 certificate chain in PEM format. The first certificate identifies the signer; subsequent certificates form a chain up to a trusted root. Verifiers use this chain to confirm the signature comes from a trusted source.
- **Timestamp authority URL** (`ta_url`): An optional [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) timestamp server URL. When provided, the SDK requests a trusted timestamp during signing, proving _when_ the signature was made. This keeps signatures verifiable even after the signing certificate expires.

### Signer from settings (recommended)
### Signer from settings

Configure signer credentials directly in settings. This is the most common approach:
Configure signer credentials directly in settings. This is the most common and recommended approach:

```py
ctx = Context.from_dict({
Expand All @@ -649,7 +649,7 @@ with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst:
builder.sign("image/jpeg", src, dst)
```

### Signer on Context (signer object)
### Signer object on Context

Create a `Signer` object and pass it to the `Context`. The signer is **consumed**: the `Signer` object becomes invalid after this call and the `Context` takes ownership.

Expand Down
6 changes: 3 additions & 3 deletions docs/intents.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ for path in image_paths:
builder.sign_file(path, output_path(path), signer)
```

### Using `set_intent` on the Builder
### Using set_intent on the Builder

Call `set_intent` directly on a `Builder` instance for one-off operations or when the intent is determined at runtime. For example:

Expand Down Expand Up @@ -166,7 +166,7 @@ flowchart LR

For `Edit` and `Update` intents, `Builder` looks at the source stream, and if no `parentOf` ingredient has been added manually, it automatically creates one from that stream (and adds the needed action). The source stream *becomes* the parent ingredient. If a `parentOf` ingredient has already been added manually (via `add_ingredient`), `Builder` uses that one instead and does not automatically create one from the source.

### How intent relates to `add_ingredient`
### How intent relates to add_ingredient

The `Builder` intent controls what the `Builder` does with the source stream (source asset) at sign time. The `add_ingredient` method adds other ingredients explicitly. These are separate concerns.

Expand Down Expand Up @@ -208,7 +208,7 @@ from c2pa import (
)
```

### Using `set_intent`
### Using set_intent

Use the `Builder.set_intent` method to specify the intent:

Expand Down
29 changes: 15 additions & 14 deletions docs/native-resources-management.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# Native resource management (ManagedResource class)
# Native resource management

`ManagedResource` is the internal base class used by the C2PA Python SDK to wrap native (Rust/FFI) pointers. When adding new wrappers around native resources `ManagedResource` should be subclassed and follow the documented lifecycle rules.

> [!NOTE]
> `ManagedResource` and the lifecycle machinery described here are internal to the SDK. In most cases, code that reads and writes C2PA data should use the public wrappers (`Reader`, `Builder`, `Signer`, `Context`, `Settings`).

## Why `ManagedResource`?
## Why ManagedResource?

`ManagedResource` is the internal base class responsible for managing native pointers owned by the C2PA Python SDK. It guarantees:

Expand Down Expand Up @@ -44,7 +41,7 @@ Python manages its own objects' memory automatically through garbage collection.

This system works well for pure Python objects, but native memory sits outside of it entirely. The garbage collector sees the Python wrapper object (e.g. a `Reader` instance) and tracks references to it, but it has no visibility into the native memory that the wrapper's `_handle` attribute points to. Memory allocated by native libraries is invisible to the garbage collector: it does not know the size of that native allocation, cannot tell when it is no longer needed, and will not call the native library's `c2pa_free` function to release it. If the Python wrapper of those native resources is collected without first calling `c2pa_free`, the native memory is never released and leaks.

### Why `__del__` is not reliable enough
### Why __del__ is not reliable enough

Python does offer `__del__` as a hook that runs when an object is collected (finalizer), and `ManagedResource` uses it as a fallback to possibly clean up leftover resources at that point. But `__del__` cannot be relied on as the primary cleanup mechanism: its timing is unpredictable (due to being called when the garbage collection runs, which is non-deterministic itself), it may not run at all during interpreter shutdown, and other Python implementations (PyPy, GraalPy) that do not use reference counting make its behavior even less deterministic.

Expand Down Expand Up @@ -87,7 +84,7 @@ Notes:
> The MRO is computed using C3 linearization, which enforces two rules: children appear before their parents, and left-to-right order from the class definition is preserved. For `class Context(ManagedResource, ContextProvider)`:
>
> 1. `Context`: the class itself always comes first.
> 2. `ManagedResource` :first listed parent, nothing else requires it to appear later.
> 2. `ManagedResource`: first listed parent, nothing else requires it to appear later.
> 3. `ContextProvider`: second listed parent, must come after `ManagedResource` to preserve declaration order.
> 4. `ABC`: parent of `ContextProvider`, must come after its child.
> 5. `object`: root of everything (all objects), always last.
Expand All @@ -105,7 +102,7 @@ Therefore, the managed resources have the following principles:
- Each `ManagedResource` holds exactly one `_handle`. `_swap_handle()` replaces it with the pointer a consuming call returned and does not free the old value, since the native side took it (see [Consume-and-swap](#consume-and-swap)).
- `_teardown(free_handle=False)`, `_consume_no_replacement()`, and `_consume_into()` all close or advance the object without calling `c2pa_free`, because ownership moved to the native side.
- Only a few sites free a live handle. Two of them free a pointer this layer still provably owns: normal teardown (`_teardown(free_handle=True)`), and the create-then-validate path, which frees a freshly created pointer if activation fails. The third, `_release_handle()`, is a *guarded* free used only when ownership is genuinely unknown (a consuming call failed without setting an error, or a Python exception was raised before the native side reported anything): if the native side already took the pointer, its address is no longer in the registry and `c2pa_free` is a `-1` no-op, so the free touches no memory. No path frees a pointer known to have been consumed and reallocated (see [Why an ownership-taken failure does not free](#why-an-ownership-taken-failure-does-not-free)).
- `_release()` drops stream wrappers, callbacks, and caches before the native pointer is freed (see [Subclass-specific cleanup with `_release()`](#subclass-specific-cleanup-with-_release)).
- `_release()` drops stream wrappers, callbacks, and caches before the native pointer is freed (see [Subclass-specific cleanup with `_release()`](#subclass-specific-cleanup)).

### Double-free risk mitigations

Expand Down Expand Up @@ -201,7 +198,11 @@ Every public method calls `_ensure_valid_state()` before doing any work, which r

## Ways to clean up

### Context manager (`with` statement)
You can clean up using a `with` statement, explicitly by calling `close()`, or by using destructor fallback.

### Using a "with" statement

The recommended cleanup method is to use a `with` statement, like this:

```python
with Reader("image.jpg") as reader:
Expand All @@ -211,7 +212,7 @@ with Reader("image.jpg") as reader:

When the `with` block exits, `__exit__` calls `close()`, which frees the native pointer. This is the safest approach because cleanup happens even if the code inside the block raises an exception.

### Explicit `.close()`
### Explicit close

```python
reader = Reader("image.jpg")
Expand All @@ -221,9 +222,9 @@ finally:
reader.close()
```

Calling `.close()` directly is equivalent to exiting a `with` block. It is idempotent: calling it multiple times is safe and does nothing after the first call.
Calling `close()` directly is equivalent to exiting a `with` block. It is idempotent: calling it multiple times is safe and does nothing after the first call.

### Destructor fallback (`__del__`)
### Destructor fallback

If neither the context manager nor an explicit `.close()` is used, `__del__` attempts to free the native pointer when Python garbage-collects the object. Per [Why `__del__` is not reliable enough](#why-__del__-is-not-reliable-enough), its timing is unpredictable and it may not run at all, so it is a safety net rather than a primary cleanup mechanism.

Expand Down Expand Up @@ -455,7 +456,7 @@ Always calling the guarded free instead, even where the value is known to be gon

`_release_handle()` (a guarded free) is reserved for the two branches where ownership is not known for certain: a Python exception raised before native reports anything, and a failure that leaves the error slot empty (which no defined native failure is expected to produce). In both, a guarded free is a good default, since it is a real free when the handle is still ours and a `-1` no-op when the native side already took it.

None of this is protected by a lock on the Python side: `ManagedResource` has no thread-safety mechanism of its own, and the retained-vs-consumed guarantee comes entirely from the native pointer registry and its thread-local error slot. As noted under [Which double-free risks this layer guards](#which-double-free-risks-this-layer-guards-and-which-it-leaves-to-the-caller), sharing one instance across threads without external synchronization is the caller's responsibility. This is a different hazard from [Fork safety](#fork-safety), which concerns a forked child process, not a thread within the same process.
None of this is protected by a lock on the Python side: `ManagedResource` has no thread-safety mechanism of its own, and the retained-vs-consumed guarantee comes entirely from the native pointer registry and its thread-local error slot. As noted under [Which double-free risks this layer guards](#double-free-risk-mitigations), sharing one instance across threads without external synchronization is the caller's responsibility. This is a different hazard from [Fork safety](#fork-safety), which concerns a forked child process, not a thread within the same process.

A consuming C FFI function first removes the pointer from its registry, then reconstructs the owned value from it. `untrack_or_return!` runs ahead of `Box::from_raw` in `c2pa_c_ffi`. If the address is unknown or the wrong type, the untrack step fails before ownership is taken and sets an error whose prefix (`UntrackedPointer:` or `WrongPointerType:`) identifies it as a pre-consume rejection. Once the value has been reconstructed, a later failure simply drops it, the same as any owned value going out of scope. The Python side stays defensive (and as generic as possible) rather than assuming any exact behavior: it retains the handle when it recognizes one of those rejection prefixes, and where the outcome is unclear it falls back to the guarded free. A native side that behaved differently would degrade in one of two bounded ways: If it kept a pointer the Python side treated as consumed, nothing would free that pointer and it would leak. If it had already released a pointer the Python side then tried to free, the registry would not find the address and the free would return `-1` without touching memory.

Expand All @@ -477,7 +478,7 @@ self._consume_and_swap(

Activating a handle that is about to be handed to the native library looks backwards, and there are two reasons for it. `_consume_and_swap` needs an active resource to read the handle from and swap the result into. It also puts the intermediate pointer under normal cleanup before anything can go wrong with it: whichever way the consuming call goes, `close()` and `__del__` will free the pointer if the native side did not take it. The alternative, holding the pointer in a local variable across the call, means every failure path has to decide for itself whether to free it.

## Subclass-specific cleanup with `_release()`
## Subclass-specific cleanup

Each subclass can override `_release()` to clean up its own resources before the native pointer is freed. The base implementation does nothing.

Expand Down
22 changes: 11 additions & 11 deletions docs/project-contributions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to the project
# Contributing to the project

The information in this page is primarily for those who wish to contribute to the c2pa-python library project itself, rather than those who simply wish to use it in an application. For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md).
The information in this page is primarily for those who wish to contribute to the c2pa-python library project itself, rather than those who simply wish to use it in an application. For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md).

## Setup

Expand Down Expand Up @@ -28,7 +28,7 @@ pip install -r requirements.txt
pip install -r requirements-dev.txt
```

Download [c2pa-rs library artifacts](https://github.com/contentauth/c2pa-rs/tags) for the current version you want, (for example, as shown below for v0.73.1):
Download [c2pa-rs library artifacts](https://github.com/contentauth/c2pa-rs/tags) for the current version you want (for example, as shown below for v0.73.1):

```bash
python scripts/download_artifacts.py c2pa-v0.73.1
Expand Down Expand Up @@ -58,7 +58,7 @@ pip install -U pytest
python3 -m build --wheel
```

To test local wheels locally, enter this command:
To test the local wheel build, enter this command:

```bash
make test-local-wheel-build
Expand Down Expand Up @@ -94,26 +94,28 @@ make verify-wheel-build

## Testing

The project uses [PyTest](https://docs.pytest.org/) and [unittest](https://docs.python.org/3/library/unittest.html) for testing.
The project uses [pytest](https://docs.pytest.org/) and [unittest](https://docs.python.org/3/library/unittest.html) for testing.

Run tests by following these steps:

1. Activate the virtual environment: `source .venv/bin/activate`
2. (optional) Install dependencies: `pip install -r requirements.txt`
4. Run the tests:
3. Run the tests:
```bash
make test
```
5. Alternatively, install `pytest` (if not already installed) and run it:
4. Alternatively, install `pytest` (if not already installed) and run it:
```bash
pip install pytest
pytest
```
**Warning**: Using `pytest` can lead to issues if you often switch between virtual environments.

> [!WARNING]
> Using `pytest` can lead to issues if you often switch between virtual environments.

### Testing during bindings development

While developing bindings locally, we use [unittest](https://docs.python.org/3/library/unittest.html), since [PyTest](https://docs.pytest.org/) can get confused by virtual environment re-deployments (especially if you bump the version number).
While developing bindings locally, we use [unittest](https://docs.python.org/3/library/unittest.html), since [pytest](https://docs.pytest.org/) can get confused by virtual environment re-deployments (especially if you bump the version number).

To run tests while developing bindings, enter this command:

Expand Down Expand Up @@ -150,5 +152,3 @@ View the output by loading `api-docs/build/html/index.html` in a web browser.
This uses `sphinx-autoapi` to parse `src/c2pa` directly, avoiding imports of native libs.
- Entry script: `scripts/generate_api_docs.py`
- Config: `api-docs/conf.py`; index: `api-docs/index.rst`

Sphinx config is in `api-docs/conf.py`, which uses `index.rst`.
Loading
Loading