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
22 changes: 21 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,29 @@ Use the `Reader` constructor to read C2PA data from a stream. This constructor e
The parameters are:

- `context` - A `Context` (or any `IContextProvider`) that configures SDK behavior. See [Configuring the SDK with Context and Settings](context-settings.md).
- `<FORMAT>` - A MIME string format for the stream; must be one of the [supported file formats](supported-formats.md).
- `<FORMAT>` - A MIME type or file extension for the stream, for example `image/jpeg` or `jpg`; see the [supported file formats](https://github.com/contentauth/c2pa-rs/blob/main/docs/supported-formats.md). Pass an empty string to take the format from the stream's leading bytes instead.
- `<STREAM>` - An open readable iostream.

Passing an empty string (or use a format-less overload of a `Reader`) asks the native core library to guess the format (the read will still fail if the format can't be guessed or is unsupported):

```cpp
c2pa::Context context;
std::ifstream ifs("asset.bin", std::ios::binary);
auto reader = c2pa::Reader(std::make_shared<c2pa::Context>(), ifs);
```

The used stream must support seeking, since it is rewound after inspection. A file path with no extension is read the same way.

Prefer passing the format when you know it. When you do pass one, it is reconciled against the container detected in the leading bytes:

- Both identify the same container: your format is used, keeping the more specific spelling (`dng` stays `dng` rather than widening to `image/tiff`).
- They identify different containers: the detected container wins.
- Detection finds no container: the format is used as given.

If detection detects no container and no format was supplied, the read fails with an unsupported-type error. Note that the format is a hint, not a constraint: the library does not reject a format merely because it is absent from `Reader::supported_mime_types()`, and an unrecognized format on recognizable bytes still reads.

`Builder` always requires an explicit format, because the container type decides how the asset is written and hashed. Signing with a blank format, or to a destination path with no extension, throws.

For example:

```cpp
Expand Down
42 changes: 38 additions & 4 deletions include/c2pa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ namespace c2pa
class C2PA_CPP_API Reader
{
private:
C2paReader *c2pa_reader;
C2paReader *c2pa_reader = nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

std::unique_ptr<std::ifstream> owned_stream; // Owns file stream when created from path
std::unique_ptr<CppIStream> cpp_stream; // Wraps stream for C API; destroyed before owned_stream
std::shared_ptr<IContextProvider> context_ref;
Expand Down Expand Up @@ -795,17 +795,37 @@ namespace c2pa
/// @details The Reader retains a shared reference to the context, keeping it
/// alive for the lifetime of the Reader.
/// @param context Shared context provider.
/// @param format The mime format of the stream.
/// @param stream The input stream to read from.
/// @throws C2paException if context is null or context->is_valid() returns false.
/// @param format The mime type or file extension of the stream. Treated as a hint and
/// reconciled against the container detected in the leading bytes: the same
/// container keeps @p format (so "dng" is not widened to "image/tiff"), a
/// different one wins over @p format, and if nothing is detected @p format is
/// used as given. Pass an empty string to rely on detection alone. A format
/// absent from supported_mime_types() is not rejected here.
/// @param stream The input stream to read from. Must support seeking; it is rewound

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we enforce the seeking requirement in any way, or do we just end up erroring and say it's the user's fault? If we throw an error, I think the comment below needs updating.

/// before inspection.
/// @throws C2paException if context is null or context->is_valid() returns false, or if
/// no container is detected and @p format does not name a supported one.
Reader(std::shared_ptr<IContextProvider> context, const std::string &format, std::istream &stream);

/// @brief Create a Reader from a shared context and stream, guessing the format from content.
/// @details Equivalent to passing an empty format. Detection is best effort; prefer the
/// overload taking a format when the format is known.
/// The stream is rewound before inspection, so it must support seeking.
/// @param context Shared context provider.
/// @param stream The input stream to read from. Must support seeking.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, for consistency. Also noticed this a few more times below.

Suggested change
/// @param stream The input stream to read from. Must support seeking.
/// @param stream The input stream to read from. Must support seeking; it is rewound before inspection.

/// @throws C2paException if context is null, context->is_valid() returns false, or the
/// container type cannot be determined from the bytes.
Reader(std::shared_ptr<IContextProvider> context, std::istream &stream);

/// @brief Create a Reader from a shared context and file path.
/// @details The Reader retains a shared reference to the context, keeping it
/// alive for the lifetime of the Reader.
/// @param context Shared context provider.
/// @param source_path The path to the file to read.
/// @throws C2paException if context is null or context->is_valid() returns false.
/// @note The extension is used as a hint only. With no extension, the container is
/// guessed from the leading bytes (best-effort). A wrong or unrecognized
/// extension is accepted, since a detected container overrides it.
Reader(std::shared_ptr<IContextProvider> context, const std::filesystem::path &source_path);

/// @brief Create a Reader from a shared context, image stream, and external JUMBF manifest.
Expand Down Expand Up @@ -877,10 +897,24 @@ namespace c2pa

/// @brief Try to create a Reader from a shared context and stream when the asset may lack C2PA data.
/// @details The Reader retains a shared reference to the context if C2PA data is found.
/// @param context Shared context provider.
/// @param format The mime type or file extension of the stream, used as a hint and
/// reconciled against the detected container. See the corresponding constructor.
/// @param stream The input stream to read from. Must support seeking.
/// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none.
/// @throws C2paException for errors other than a missing manifest.
static std::optional<Reader> from_asset(std::shared_ptr<IContextProvider> context, const std::string &format, std::istream &stream);

/// @brief Try to create a Reader from a shared context and stream, guessing the format from content.
/// @details The Reader retains a shared reference to the context if C2PA data is found.
/// Detection is best effort; prefer the overload taking a format when it is known.
/// @param context Shared context provider.
/// @param stream The input stream to read from. Must support seeking.
/// @return A Reader if JUMBF (c2pa/manifest) data is present; std::nullopt if none.
/// @throws C2paException for errors other than a missing manifest. Content whose
/// container cannot be identified raises an error rather than returning nullopt.
static std::optional<Reader> from_asset(std::shared_ptr<IContextProvider> context, std::istream &stream);

// Non-copyable
Reader(const Reader&) = delete;

Expand Down
52 changes: 39 additions & 13 deletions src/c2pa_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,27 +222,31 @@ namespace c2pa

std::vector<unsigned char> Builder::sign(const std::string &format, std::istream &source, std::ostream &dest, Signer &signer)
{
const std::string normalized_format = detail::normalize_format(format);

// Caller's source/dest streams must outlive this call
// Stream wrappers are stack locals that wrap the caller's streams
CppIStream c_source(source);
CppOStream c_dest(dest);
const unsigned char *c2pa_manifest_bytes = nullptr;

// c2pa_builder_sign() uses streams synchronously and completes before returning
auto result = c2pa_builder_sign(builder, format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes);
auto result = c2pa_builder_sign(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

std::vector<unsigned char> Builder::sign(const std::string &format, std::istream &source, std::iostream &dest, Signer &signer)
{
const std::string normalized_format = detail::normalize_format(format);

// Caller's source/dest streams must outlive this call
// Stream wrappers are stack locals that wrap the caller's streams
CppIStream c_source(source);
CppIOStream c_dest(dest);
const unsigned char *c2pa_manifest_bytes = nullptr;

// c2pa_builder_sign() uses streams synchronously and completes before returning
auto result = c2pa_builder_sign(builder, format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes);
auto result = c2pa_builder_sign(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

Expand All @@ -254,6 +258,9 @@ namespace c2pa
/// @throws C2pa::C2paException for errors encountered by the C2PA library.
std::vector<unsigned char> Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path, Signer &signer)
{
const std::string format =
detail::resolve_format(detail::extract_file_extension(dest_path));

auto source = detail::open_file_binary<std::ifstream>(source_path);
// Ensure the destination directory exists
auto dest_dir = dest_path.parent_path();
Expand All @@ -270,23 +277,27 @@ namespace c2pa
{
throw std::runtime_error("Failed to open destination file: " + dest_path.string());
}
auto format = detail::extract_file_extension(dest_path);
auto result = sign(format, *source, dest, signer);
return result;
}

std::vector<unsigned char> Builder::sign(const std::string &format, std::istream &source, std::iostream &dest)
{
const std::string normalized_format = detail::normalize_format(format);

CppIStream c_source(source);
CppIOStream c_dest(dest);
const unsigned char *c2pa_manifest_bytes = nullptr;

auto result = c2pa_builder_sign_context(builder, format.c_str(), c_source.c_stream, c_dest.c_stream, &c2pa_manifest_bytes);
auto result = c2pa_builder_sign_context(builder, normalized_format.c_str(), c_source.c_stream, c_dest.c_stream, &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

std::vector<unsigned char> Builder::sign(const std::filesystem::path &source_path, const std::filesystem::path &dest_path)
{
const std::string format =
detail::resolve_format(detail::extract_file_extension(dest_path));

auto source = detail::open_file_binary<std::ifstream>(source_path);
auto dest_dir = dest_path.parent_path();
if (!std::filesystem::exists(dest_dir))
Expand All @@ -302,7 +313,6 @@ namespace c2pa
{
throw std::runtime_error("Failed to open destination file: " + dest_path.string());
}
auto format = detail::extract_file_extension(dest_path);
auto result = sign(format, *source, dest);
return result;
}
Expand Down Expand Up @@ -386,30 +396,36 @@ namespace c2pa

std::vector<unsigned char> Builder::data_hashed_placeholder(uintptr_t reserve_size, const std::string &format)
{
const std::string normalized_format = detail::normalize_format(format);

const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_builder_data_hashed_placeholder(builder, reserve_size, format.c_str(), &c2pa_manifest_bytes);
auto result = c2pa_builder_data_hashed_placeholder(builder, reserve_size, normalized_format.c_str(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

std::vector<unsigned char> Builder::sign_data_hashed_embeddable(Signer &signer, const std::string &data_hash, const std::string &format, std::istream *asset)
{
const std::string normalized_format = detail::normalize_format(format);

int64_t result;
const unsigned char *c2pa_manifest_bytes = nullptr;
if (asset)
{
CppIStream c_asset(*asset);
result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), format.c_str(), c_asset.c_stream, &c2pa_manifest_bytes);
result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), normalized_format.c_str(), c_asset.c_stream, &c2pa_manifest_bytes);
}
else
{
result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), format.c_str(), nullptr, &c2pa_manifest_bytes);
result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), normalized_format.c_str(), nullptr, &c2pa_manifest_bytes);
}
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

bool Builder::needs_placeholder(const std::string &format)
{
int result = c2pa_builder_needs_placeholder(builder, format.c_str());
const std::string resolved_format = detail::resolve_format(format);

int result = c2pa_builder_needs_placeholder(builder, resolved_format.c_str());
if (result < 0)
{
throw C2paException();
Expand All @@ -419,8 +435,11 @@ namespace c2pa

std::vector<unsigned char> Builder::placeholder(const std::string &format)
{
// The format selects the hash assertion written into the builder.
const std::string normalized_format = detail::normalize_format(format);

const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_builder_placeholder(builder, format.c_str(), &c2pa_manifest_bytes);
auto result = c2pa_builder_placeholder(builder, normalized_format.c_str(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

Expand All @@ -443,8 +462,11 @@ namespace c2pa

void Builder::update_hash_from_stream(const std::string &format, std::istream &stream)
{
// The format decides which hash assertion is updated.
const std::string resolved_format = detail::resolve_format(format);

CppIStream c_stream(stream);
int result = c2pa_builder_update_hash_from_stream(builder, format.c_str(), c_stream.c_stream);
int result = c2pa_builder_update_hash_from_stream(builder, resolved_format.c_str(), c_stream.c_stream);
if (result < 0)
{
throw C2paException();
Expand All @@ -453,15 +475,19 @@ namespace c2pa

std::vector<unsigned char> Builder::sign_embeddable(const std::string &format)
{
const std::string normalized_format = detail::normalize_format(format);

const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_builder_sign_embeddable(builder, format.c_str(), &c2pa_manifest_bytes);
auto result = c2pa_builder_sign_embeddable(builder, normalized_format.c_str(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

std::vector<unsigned char> Builder::format_embeddable(const std::string &format, std::vector<unsigned char> &data)
{
const std::string normalized_format = detail::normalize_format(format);

const unsigned char *c2pa_manifest_bytes = nullptr;
auto result = c2pa_format_embeddable(format.c_str(), data.data(), data.size(), &c2pa_manifest_bytes);
auto result = c2pa_format_embeddable(normalized_format.c_str(), data.data(), data.size(), &c2pa_manifest_bytes);
return detail::to_byte_vector(c2pa_manifest_bytes, result);
}

Expand Down
4 changes: 1 addition & 3 deletions src/c2pa_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ namespace c2pa
if (!raw) {
throw C2paException("Signer is not valid");
}
// On error the signer may not have been consumed by the C API,
// surface an error

if (c2pa_context_builder_set_signer(context_builder, raw) != 0) {
throw C2paException();
}
Expand All @@ -204,7 +203,6 @@ namespace c2pa
throw C2paException("Failed to create HTTP resolver");
}
if (c2pa_context_builder_set_http_resolver(context_builder, resolver) != 0) {
c2pa_free(resolver);
Comment thread
tmathern marked this conversation as resolved.
throw C2paException();
}
return *this;
Expand Down
40 changes: 40 additions & 0 deletions src/c2pa_internal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
#ifndef C2PA_INTERNAL_HPP
#define C2PA_INTERNAL_HPP

#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include <memory>

Expand Down Expand Up @@ -242,6 +245,43 @@ inline std::string extract_file_extension(const std::filesystem::path &path) noe
return ext.empty() ? "" : ext.substr(1);
}

/// @brief Format asking the library to determine the container type from content.
/// @details The C API rejects a null format, so absent must be spelled empty.
inline constexpr const char *kDetectFormatFromContent = "";
Comment thread
tmathern marked this conversation as resolved.

/// @brief ASCII whitespace ignored around a format.
/// @details Formats are MIME types or extensions, so non-ASCII spaces are
/// deliberately excluded.
inline constexpr const char *kFormatWhitespace = " \t\n\r\f\v";

/// @brief Trim and lowercase a caller-supplied format; empty when @p format is blank.
/// @details Empty means detection from content, so this alone is enough wherever
/// a blank format is allowed to request that.
[[nodiscard]] inline std::string normalize_format(const std::string &format) {
const auto first = format.find_first_not_of(kFormatWhitespace);
if (first == std::string::npos) {
return {}; // Empty or all whitespace.
}
const auto last = format.find_last_not_of(kFormatWhitespace);
std::string normalized(std::string_view(format).substr(first, last - first + 1));
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalized;
}

/// @brief Resolve a format that must be stated explicitly.
/// These paths never infer the container type from content,
/// so a blank format is rejected.
/// @return @p format trimmed and lowercased.
/// @throws C2paException if @p format is blank.
[[nodiscard]] inline std::string resolve_format(const std::string &format) {
std::string normalized = normalize_format(format);
if (normalized.empty()) {
throw C2paException("An explicit format is required.");
}
return normalized;
}

/// @brief Convert C string result to C++ string with cleanup
/// @param c_result Raw C string from C API
/// @return C++ string (throws if null)
Expand Down
Loading
Loading