plugins/wasm/znippy-plugin-maven-wasm/src/lib.rs
//! The znippy WASM plugin example: maven, compiled to `wasm32-unknown-unknown`
//! and loaded by `znippy_common::plugins::wasm_loader::WasmPlugin`.
//!
//! ```text
//! cargo build --target wasm32-unknown-unknown --release -p znippy-plugin-maven-wasm
//! # → target/wasm32-unknown-unknown/release/znippy_plugin_maven_wasm.wasm
//! WasmPlugin::load(that_path, "maven", 3)
//! ```
//!
//! # What this crate is, and what it deliberately is not
//!
//! It holds **no maven logic**. Every byte of parsing, JAR scanning, classifier
//! recovery and column naming comes from `znippy-plugin-maven`, the same crate
//! the native `NativeMavenPlugin` is built from — this file is only the ABI
//! shim between that logic and the five host imports the loader speaks
//! (`alloc`, `result_len`, `extract`, `plugin_extensions`, `plugin_schema`).
//! `tests/maven_native_wasm_agree.rs` asserts the two surfaces produce the same
//! `ExtensionRow` from the same bytes, which is what makes that claim checkable
//! rather than an assertion in a comment.
//!
//! # What does not cross to wasm32, and why
//!
//! | native only | why it cannot cross |
//! |---|---|
//! | `ljar::decompress_jar_filter` | fans per-entry DEFLATE across the gatling pool; `wasm32-unknown-unknown` has no threads. The wasm path uses the single-threaded `linflate` scan in `znippy_plugin_maven::extract_maven_metadata` — the SAME inflate engine ljar fans out over, so the decoded pom bytes are identical. |
//! | `gatling_map_balanced` batch fan-out (`extract_batch`) | same: no threads. The loader has no batch entry point either; it calls `extract` once per file. |
//! | `znippy_common::plugin::ExtensionRow` / `arrow` | znippy-common carries arrow, io_uring and the gatling engine. The wire form is JSON instead, parsed back into an `ExtensionRow` host-side by `wasm_loader::parse_json_to_row`. |
//! | `resolver.rs` (`ureq`) | a network fetch; there is no socket in this sandbox and the loader offers no host import for one. |
//!
//! The host imports `wasm_loader` *does* provide (`host_decompress`,
//! `host_archive_open`/`_list`/`_entry`/`_close`) are intentionally unused here:
//! `extract` is already handed the whole artifact in linear memory, and the
//! in-wasm `linflate` scan reads only the one `META-INF/maven/**/pom.xml` entry
//! it needs. Routing that through the host would add an ABI round trip and a
//! second decompressor for the same bytes — the opposite of LAW 5. They earn
//! their place for a plugin whose input is a multi-GiB tar.bz2, not this one.
use std::cell::{Cell, RefCell};
use znippy_plugin_maven::row::{fields_to_json, maven_fields, extensions_str, schema_str};
thread_local! {
/// Every buffer handed to the host stays owned here for the life of the
/// instance. The host writes into these pointers and reads results back out
/// of them; a freed buffer would be a use-after-free across the ABI. A
/// `WasmPlugin` instantiates a fresh `Store` per `extract` call, so this
/// never grows beyond one file's worth.
static ALLOCS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
/// Length of the most recent value returned by an export, read by the
/// host's `result_len()` immediately after that export returns.
static RESULT_LEN: Cell<u32> = const { Cell::new(0) };
}
/// Reserve `size` bytes in linear memory and return their address.
///
/// # Safety
/// The returned pointer is valid until the instance is dropped. The host writes
/// exactly `size` bytes there before passing the address back to `extract`.
#[unsafe(no_mangle)]
pub extern "C" fn alloc(size: u32) -> u32 {
ALLOCS.with(|a| {
let mut buf = vec![0u8; size as usize];
let ptr = buf.as_mut_ptr() as u32;
a.borrow_mut().push(buf);
ptr
})
}
/// Byte length of the value the last export returned a pointer to.
#[unsafe(no_mangle)]
pub extern "C" fn result_len() -> u32 {
RESULT_LEN.with(|l| l.get())
}
/// Copy `bytes` into a fresh linear-memory buffer, record its length for
/// `result_len`, and return its address.
fn return_bytes(bytes: &[u8]) -> u32 {
let ptr = alloc(bytes.len() as u32);
// SAFETY: `ptr` was just returned by `alloc`, which reserved exactly
// `bytes.len()` bytes and keeps the buffer alive in ALLOCS.
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
}
RESULT_LEN.with(|l| l.set(bytes.len() as u32));
ptr
}
/// Comma-separated extensions this plugin claims — read once at load time by
/// `WasmPlugin::fetch_extensions`, which then does the per-file match host-side.
#[unsafe(no_mangle)]
pub extern "C" fn plugin_extensions() -> u32 {
return_bytes(extensions_str().as_bytes())
}
/// `name:type` column list — read once at load time by `WasmPlugin::fetch_schema`
/// and parsed into Arrow fields. Same `MAVEN_SCHEMA` the native `schema_fields()`
/// is built from.
#[unsafe(no_mangle)]
pub extern "C" fn plugin_schema() -> u32 {
return_bytes(schema_str().as_bytes())
}
/// Extract maven metadata from one artifact.
///
/// Returns a pointer to a UTF-8 JSON object (`result_len()` bytes long), or to
/// the four bytes `null` when this artifact carries no readable pom — which is
/// what `WasmPlugin::extract_metadata` turns into `None`.
///
/// # Safety
/// `path_ptr`/`data_ptr` must be addresses previously returned by `alloc`, each
/// holding at least the stated number of bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn extract(path_ptr: u32, path_len: u32, data_ptr: u32, data_len: u32) -> u32 {
let path = unsafe { std::slice::from_raw_parts(path_ptr as *const u8, path_len as usize) };
let data = unsafe { std::slice::from_raw_parts(data_ptr as *const u8, data_len as usize) };
let Ok(path) = std::str::from_utf8(path) else {
return return_bytes(b"null");
};
// The ONE maven implementation — the `.pom` / JAR scan shared with the
// native plugin, then the shared field writer.
match znippy_plugin_maven::extract_maven_metadata(path, data) {
Some(coord) => return_bytes(fields_to_json(&maven_fields(coord, path)).as_bytes()),
None => return_bytes(b"null"),
}
}