Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WASI to c2patool #945

Merged
merged 3 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ jobs:
CC: /opt/wasi-sdk/bin/clang
WASI_SDK_PATH: /opt/wasi-sdk
RUST_MIN_STACK: 16777216
run: cargo +nightly test --target wasm32-wasip2 -p c2pa -p c2pa-crypto -p cawg-identity --all-features
run: cargo +nightly test --target wasm32-wasip2 -p c2pa -p c2pa-crypto -p cawg-identity -p c2patool --all-features

test-direct-minimal-versions:
name: Unit tests with minimum versions of direct dependencies
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test-wasi:
ifeq ($(PLATFORM),mac)
$(eval CC := /opt/homebrew/opt/llvm/bin/clang)
endif
CC=$(CC) CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime -S cli -S http --dir ." cargo +nightly test --target wasm32-wasip2 -p c2pa -p c2pa-crypto -p cawg-identity --all-features
CC=$(CC) CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime -S cli -S http --dir ." cargo +nightly test --target wasm32-wasip2 -p c2pa -p c2pa-crypto -p cawg-identity -p c2patool --all-features
rm -r sdk/Users

# Full local validation, build and test all features including wasm
Expand Down
12 changes: 9 additions & 3 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ serde_json = "1.0"
tempfile = "3.3"
treeline = "0.1.0"
pem = "3.0.3"
openssl = { version = "0.10.61", features = ["vendored"] }
reqwest = { version = "0.12.4", features = ["blocking"] }
url = "2.5.0"

[target.'cfg(not(target_os = "wasi"))'.dependencies]
reqwest = { version = "0.12.4", features = ["blocking"] }

[target.'cfg(target_os = "wasi")'.dependencies]
wasi = "0.14"

[dev-dependencies]
mockall = "0.13.0"

[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
assert_cmd = "2.0.14"
httpmock = "0.7.0"
predicates = "3.1"
mockall = "0.13.0"

[package.metadata.binstall]
# Use defaults
10 changes: 9 additions & 1 deletion cli/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,12 @@ c2patool /Downloads/1080p_out/avc1/init.mp4 \

### Additional option

The `--fragments_glob` option is only available with the `fragment` subcommand and specifies the glob pattern to find the fragments of the asset. The path is automatically set to be the same as the "init" segment, so the pattern must match only segment file names, not full paths.
The `--fragments_glob` option is only available with the `fragment` subcommand and specifies the glob pattern to find the fragments of the asset. The path is automatically set to be the same as the "init" segment, so the pattern must match only segment file names, not full paths.

## WASI

The wasm created for wasm32-wasip2 can be run directly with [wasmtime](https://docs.wasmtime.dev/). It also can be transpiled to a JS + core Wasm for JavaScript execution using [jco](https://bytecodealliance.github.io/jco/transpiling.html).
```
wasmtime -S cli -S http --dir . c2patool.wasm [OPTIONS] <PATH> [COMMAND]
```

19 changes: 13 additions & 6 deletions cli/src/callback_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,16 @@ mod test {

use super::*;

fn sign_cert_path() -> PathBuf {
#[cfg(not(target_os = "wasi"))]
return PathBuf::from(env!("CARGO_MANIFEST_DIR"));
#[cfg(target_os = "wasi")]
return PathBuf::from("/");
}

#[test]
fn test_signing_succeeds_returns_bytes() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/es256_certs.pem");

let sign_config = SignConfig {
Expand All @@ -220,7 +227,7 @@ mod test {

#[test]
fn test_signing_succeeds_returns_error_embedding() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/es256_certs.pem");

let sign_config = SignConfig {
Expand Down Expand Up @@ -280,7 +287,7 @@ mod test {

#[test]
fn test_try_from_succeeds_for_valid_sign_config() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/es256_certs.pem");

let expected_alg = SigningAlg::Es256;
Expand All @@ -301,7 +308,7 @@ mod test {

#[test]
fn test_callback_signer_error_file_not_found() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/NOT-HERE");

let sign_config = SignConfig {
Expand All @@ -319,7 +326,7 @@ mod test {

#[test]
fn test_callback_signer_error_invalid_cert() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/test.json");

let sign_config = SignConfig {
Expand All @@ -337,7 +344,7 @@ mod test {

#[test]
fn test_callback_signer_valid_sign_certs() {
let mut sign_cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut sign_cert_path = sign_cert_path();
sign_cert_path.push("sample/es256_certs.pem");

let sign_config = SignConfig {
Expand Down
112 changes: 107 additions & 5 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,108 @@ fn load_trust_resource(resource: &TrustResource) -> Result<String> {
Ok(data)
}
TrustResource::Url(url) => {
#[cfg(not(target_os = "wasi"))]
let data = reqwest::blocking::get(url.to_string())?
.text()
.with_context(|| format!("Failed to read trust resource from URL: {}", url))?;

#[cfg(target_os = "wasi")]
let data = blocking_get(&url.to_string())?;
Ok(data)
}
}
}

#[cfg(target_os = "wasi")]
fn blocking_get(url: &str) -> Result<String> {
use std::io::Read;

use url::Url;
use wasi::http::{
outgoing_handler,
types::{Fields, OutgoingRequest, Scheme},
};

let parsed_url =
Url::parse(url).map_err(|e| Error::ResourceNotFound(format!("invalid URL: {}", e)))?;
let path_with_query = parsed_url[url::Position::BeforeHost..].to_string();
let request = OutgoingRequest::new(Fields::new());
request.set_path_with_query(Some(&path_with_query)).unwrap();

// Set the scheme based on the URL.
let scheme = match parsed_url.scheme() {
"http" => Scheme::Http,
"https" => Scheme::Https,
_ => return Err(anyhow!("unsupported URL scheme".to_string(),)),
};

request.set_scheme(Some(&scheme)).unwrap();

match outgoing_handler::handle(request, None) {
Ok(resp) => {
resp.subscribe().block();

let response = resp
.get()
.expect("HTTP request response missing")
.expect("HTTP request response requested more than once")
.expect("HTTP request failed");

if response.status() == 200 {
let raw_header = response.headers().get("Content-Length");
if raw_header.first().map(|val| val.is_empty()).unwrap_or(true) {
return Err(anyhow!("url returned no content length".to_string()));
}

let str_parsed_header = match std::str::from_utf8(raw_header.first().unwrap()) {
Ok(s) => s,
Err(e) => {
return Err(anyhow!(format!(
"error parsing content length header: {}",
e
)))
}
};

let content_length: usize = match str_parsed_header.parse() {
Ok(s) => s,
Err(e) => {
return Err(anyhow!(format!(
"error parsing content length header: {}",
e
)))
}
};

let body = {
let mut buf = Vec::with_capacity(content_length);
let response_body = response
.consume()
.expect("failed to get incoming request body");
let mut stream = response_body
.stream()
.expect("failed to get response body stream");
stream
.read_to_end(&mut buf)
.expect("failed to read response body");
buf
};

let body_string = std::str::from_utf8(&body)
.map_err(|e| anyhow!(format!("invalid UTF-8: {}", e)))?;
Ok(body_string.to_string())
} else {
Err(anyhow!(format!(
"fetch failed: code: {}",
response.status(),
)))
}
}

Err(e) => Err(anyhow!(e.to_string())),
}
}

fn configure_sdk(args: &CliArgs) -> Result<()> {
const TA: &str = r#"{"trust": { "trust_anchors": replacement_val } }"#;
const AL: &str = r#"{"trust": { "allowed_list": replacement_val } }"#;
Expand Down Expand Up @@ -699,6 +792,8 @@ fn main() -> Result<()> {
pub mod tests {
#![allow(clippy::unwrap_used)]

use tempfile::TempDir;

use super::*;

const CONFIG: &str = r#"{
Expand All @@ -714,12 +809,19 @@ pub mod tests {
]
}"#;

fn tempdirectory() -> Result<TempDir> {
#[cfg(target_os = "wasi")]
return TempDir::new_in("/").map_err(Into::into);

#[cfg(not(target_os = "wasi"))]
return tempfile::tempdir().map_err(Into::into);
}

#[test]
fn test_manifest_config() {
const SOURCE_PATH: &str = "tests/fixtures/earth_apollo17.jpg";
const OUTPUT_PATH: &str = "../target/tmp/unit_out.jpg";
create_dir_all("../target/tmp").expect("create_dir");
std::fs::remove_file(OUTPUT_PATH).ok(); // remove output file if it exists
let tempdir = tempdirectory().unwrap();
let output_path = tempdir.path().join("unit_out.jpg");
let mut builder = Builder::from_json(CONFIG).expect("from_json");

let signer = SignConfig::from_json(CONFIG)
Expand All @@ -729,10 +831,10 @@ pub mod tests {
.expect("get_signer");

let _result = builder
.sign_file(signer.as_ref(), SOURCE_PATH, OUTPUT_PATH)
.sign_file(signer.as_ref(), SOURCE_PATH, &output_path)
.expect("embed");

let ms = Reader::from_file(OUTPUT_PATH)
let ms = Reader::from_file(output_path)
.expect("from_file")
.to_string();
println!("{}", ms);
Expand Down
11 changes: 9 additions & 2 deletions cli/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
A permanent key and cert should be provided in the manifest definition or in the environment variables.\n");

let signer = create_signer::from_keys(DEFAULT_CERTS, DEFAULT_KEY, alg, tsa_url)
.context("Invalid certification data")?;
.context("Invalid certification dato")?;

Ok(signer)
}
Expand All @@ -129,7 +129,14 @@
let mut sign_config = SignConfig::from_json(CONFIG).expect("from_json");
sign_config.set_base_path("sample");

let signer = sign_config.signer().expect("get signer");
let signer = match sign_config.signer() {
Ok(signer) => signer,
Err(e) => {
println!("Error: {}", e);
panic!();

Check warning on line 136 in cli/src/signer.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/signer.rs#L134-L136

Added lines #L134 - L136 were not covered by tests
}
};

assert_eq!(signer.alg(), SigningAlg::Es256);
}

Expand Down
1 change: 1 addition & 0 deletions cli/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(not(target_os = "wasi"))]
// Copyright 2022 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
Expand Down
13 changes: 6 additions & 7 deletions sdk/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ use log::error;

#[cfg(feature = "v1_api")]
use crate::jumbf_io::save_jumbf_to_memory;
#[cfg(feature = "file_io")]
use crate::jumbf_io::{
get_file_extension, get_supported_file_extension, load_jumbf_from_file, save_jumbf_to_file,
};
#[cfg(all(feature = "v1_api", feature = "file_io"))]
use crate::jumbf_io::{object_locations, remove_jumbf_from_file};
#[cfg(all(feature = "file_io", feature = "v1_api"))]
use crate::utils::io_utils::tempdirectory;
use crate::{
assertion::{
Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeErrorCause,
Expand Down Expand Up @@ -76,13 +82,6 @@ use crate::{
};
#[cfg(feature = "v1_api")]
use crate::{external_manifest::ManifestPatchCallback, RemoteSigner};
#[cfg(feature = "file_io")]
use crate::{
jumbf_io::{
get_file_extension, get_supported_file_extension, load_jumbf_from_file, save_jumbf_to_file,
},
utils::io_utils::tempdirectory,
};

const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests

Expand Down