diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c1872b64..4b9c49778 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index bd878f91a..7cdb005e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,6 @@ dependencies = [ "httpmock", "log", "mockall", - "openssl", "pem 3.0.4", "predicates", "reqwest", @@ -878,6 +877,7 @@ dependencies = [ "tempfile", "treeline", "url", + "wasi 0.14.1+wasi-0.2.3", ] [[package]] diff --git a/Makefile b/Makefile index bd0eaeab0..10298a5a8 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 45a284eb1..b26bde8e8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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 diff --git a/cli/docs/usage.md b/cli/docs/usage.md index de99e8f7f..83282a9a8 100644 --- a/cli/docs/usage.md +++ b/cli/docs/usage.md @@ -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. \ No newline at end of file +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] [COMMAND] +``` + diff --git a/cli/src/callback_signer.rs b/cli/src/callback_signer.rs index d3baae270..bde5d0d24 100644 --- a/cli/src/callback_signer.rs +++ b/cli/src/callback_signer.rs @@ -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 { @@ -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 { @@ -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; @@ -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 { @@ -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 { @@ -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 { diff --git a/cli/src/main.rs b/cli/src/main.rs index 1c3d56203..1c68b6f8c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -252,15 +252,108 @@ fn load_trust_resource(resource: &TrustResource) -> Result { 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 { + 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 } }"#; @@ -699,6 +792,8 @@ fn main() -> Result<()> { pub mod tests { #![allow(clippy::unwrap_used)] + use tempfile::TempDir; + use super::*; const CONFIG: &str = r#"{ @@ -714,12 +809,19 @@ pub mod tests { ] }"#; + fn tempdirectory() -> Result { + #[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) @@ -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); diff --git a/cli/tests/integration.rs b/cli/tests/integration.rs index 04fd18f52..9eb7c33b6 100644 --- a/cli/tests/integration.rs +++ b/cli/tests/integration.rs @@ -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 diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 4bab71879..38669fa01 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -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, @@ -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