Skip to content

Commit

Permalink
feat: Add WASI to c2patool (#945)
Browse files Browse the repository at this point in the history
* feat: Add wasi to c2patool

---------

Co-authored-by: Eric Scouten <[email protected]>
  • Loading branch information
cdmurph32 and scouten-adobe authored Feb 26, 2025
1 parent c7d584d commit 1cc1066
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 25 deletions.
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
1 change: 1 addition & 0 deletions cli/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// specific language governing permissions and limitations under
// each license.

#![cfg(not(target_os = "wasi"))]
use std::{error::Error, fs, fs::create_dir_all, path::PathBuf, process::Command};

// Add methods on commands
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

0 comments on commit 1cc1066

Please sign in to comment.