SPEC §4, §7.1 step 2: a sink tries an envelope's capabilities in the energy
order unless its deployer configures another: a decode the platform answers
supported and power-efficient first, a learned capability (NeuralIntegerExact,
NeuralEntropyExact, NeuralFloat, Behavioral) with no energy budget after every
other, the envelope's order within each. The order changes which capability
decodes, never a reason. An energy budget is a decode figure measured on a
named reference sink under OnChipCounter or CalibratedInstrument, in a record
named by its digest; no registration states one yet (REGISTRY, Energy budgets;
the export's optional energy_budget, schema only).
wai-rs: energy_order (DispatchPolicy, Budgets, EnergyBudget, is_learned);
Wai::dispatch_verified and WaiMulti::pick apply it, dispatch_with and pick_with
take another order, the HTTP client's default order and the C ABI's
wai_envelope_select follow it. Tests that are about pins state the manifest
order they run in.
browser-sink §4.7: objects sharing a choice are alternatives; the page decodes
the first in the energy order that routes (no_alternative otherwise).
browser_sink::choose_report, the page's chooseReport (wai-live-web, binding.js),
a choices section in live-conformance/browser (existing sections unchanged),
checked by wai-rs, the page's corpus run and browser_sink_verify.py (34 of 34
mutations caught).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 11b2556827)
595 lines
26 KiB
Rust
595 lines
26 KiB
Rust
//! C ABI for SDK consumers. Stable, language-agnostic surface — every
|
|
//! other-language binding (Python ctypes, Node N-API, Swift, JNI, etc.)
|
|
//! talks to this. The Rust crate is built as `cdylib`/`staticlib` so it
|
|
//! drops in as `libwai.so`/`libwai.dylib`/`wai.dll`.
|
|
//!
|
|
//! This module (feature `ffi`, which `codecs` implies) is the envelope surface:
|
|
//! buffers, errors, packing and unpacking `WAI1`/`WAI2`, and SPEC §7.1 selection.
|
|
//! It needs no codec library. The codec wrappers are [`crate::ffi_codecs`]
|
|
//! (feature `codecs`, re-exported here) and the neural sink is
|
|
//! [`crate::ffi_neural`] (features `neural` and `ffi`).
|
|
//!
|
|
//! ## Status codes (returned by every wai_* function)
|
|
//! - `0` = ok
|
|
//! - `1` = invalid argument (null pointer where one shouldn't be, etc.)
|
|
//! - `2` = encode/decode failed (codec-level error; details in
|
|
//! `wai_last_error`)
|
|
//! - `3` = container/envelope error (bad magic, truncated, bad JSON)
|
|
//! - `4` = internal panic (a bug in the wrapper — we catch it instead
|
|
//! of unwinding into C, which would be UB)
|
|
//! - `5` = a pin was refused (SPEC §3.1); `wai_last_error` starts with the reason
|
|
//! (`prior_not_held`, `form_required`, …)
|
|
//! - `6` = `intent = replicate` is not sound for the capability decoded through
|
|
//! (SPEC §7); `wai_last_error` starts with the reason
|
|
//! - `7` = no rendition of a `WAI2` envelope can be picked (SPEC §7.1);
|
|
//! `wai_last_error` gives each entry's index and reason
|
|
//!
|
|
//! ## Memory model
|
|
//! All byte buffers crossing the boundary are heap-allocated here and
|
|
//! must be freed with `wai_buffer_free`. Errors are returned as a
|
|
//! non-zero status code; `wai_last_error` returns a null-terminated
|
|
//! UTF-8 string of the most recent error per thread.
|
|
|
|
use std::cell::RefCell;
|
|
use std::ffi::{c_char, c_int, CString};
|
|
use std::panic;
|
|
use std::slice;
|
|
|
|
|
|
pub const WAI_OK: c_int = 0;
|
|
pub const WAI_ERR_INVALID_ARG: c_int = 1;
|
|
pub const WAI_ERR_CODEC: c_int = 2;
|
|
pub const WAI_ERR_CONTAINER: c_int = 3;
|
|
pub const WAI_ERR_PANIC: c_int = 4;
|
|
/// A pin was refused (SPEC §3.1); `wai_last_error` starts with the reason.
|
|
pub const WAI_ERR_PRIOR: c_int = 5;
|
|
/// `intent = replicate` is not sound for the capability decoded through (SPEC §7).
|
|
pub const WAI_ERR_UNSOUND_REPLICATE: c_int = 6;
|
|
/// No rendition of a `WAI2` envelope can be picked (SPEC §7.1).
|
|
pub const WAI_ERR_NO_RENDITION: c_int = 7;
|
|
|
|
#[cfg(feature = "codecs")]
|
|
pub use crate::ffi_codecs::*;
|
|
|
|
thread_local! {
|
|
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
pub(crate) fn set_err(e: impl ToString) {
|
|
let cs = CString::new(e.to_string())
|
|
.unwrap_or_else(|_| CString::new("invalid utf-8 error").unwrap());
|
|
LAST_ERROR.with(|c| *c.borrow_mut() = Some(cs));
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct WaiBuffer {
|
|
pub data: *mut u8,
|
|
pub len: usize,
|
|
pub cap: usize,
|
|
}
|
|
|
|
impl WaiBuffer {
|
|
pub(crate) fn from_vec(mut v: Vec<u8>) -> Self {
|
|
let data = v.as_mut_ptr();
|
|
let len = v.len();
|
|
let cap = v.capacity();
|
|
std::mem::forget(v);
|
|
WaiBuffer { data, len, cap }
|
|
}
|
|
pub(crate) fn empty() -> Self { WaiBuffer { data: std::ptr::null_mut(), len: 0, cap: 0 } }
|
|
}
|
|
|
|
/// Free a buffer previously returned by any `wai_*_encode` /
|
|
/// `wai_*_decode` / `wai_envelope_*` call. Safe to call with a null
|
|
/// or zero-cap buffer.
|
|
///
|
|
/// # Safety
|
|
/// `buf.data` must have been produced by this crate.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_buffer_free(buf: WaiBuffer) {
|
|
if !buf.data.is_null() && buf.cap > 0 {
|
|
drop(unsafe { Vec::from_raw_parts(buf.data, buf.len, buf.cap) });
|
|
}
|
|
}
|
|
|
|
/// Returns a pointer to the most recent error message for this thread,
|
|
/// or null if there is none. The pointer is valid until the next FFI
|
|
/// call on this thread that sets an error.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn wai_last_error() -> *const c_char {
|
|
LAST_ERROR.with(|c| match &*c.borrow() {
|
|
Some(cs) => cs.as_ptr(),
|
|
None => std::ptr::null(),
|
|
})
|
|
}
|
|
|
|
// ---- generic helpers ------------------------------------------------
|
|
pub(crate) unsafe fn input_slice<'a>(data: *const u8, len: usize) -> Option<&'a [u8]> {
|
|
match (data.is_null(), len) {
|
|
(true, 0) => Some(&[]),
|
|
(true, _) => None, // null but non-zero len
|
|
(false, _) => Some(unsafe { slice::from_raw_parts(data, len) }),
|
|
}
|
|
}
|
|
|
|
pub(crate) unsafe fn write_buf(out: *mut WaiBuffer, v: Vec<u8>) -> bool {
|
|
if out.is_null() { return false; }
|
|
unsafe { *out = WaiBuffer::from_vec(v); }
|
|
true
|
|
}
|
|
|
|
pub(crate) unsafe fn clear_buf(out: *mut WaiBuffer) {
|
|
if !out.is_null() {
|
|
unsafe { *out = WaiBuffer::empty(); }
|
|
}
|
|
}
|
|
|
|
/// Wraps an FFI body. Validates the `out` buffer pointer is non-null,
|
|
/// runs the closure inside `catch_unwind` (so a Rust panic returns
|
|
/// `WAI_ERR_PANIC` instead of unwinding into C — which would be UB),
|
|
/// and clears `out` to an empty buffer on any error path.
|
|
pub(crate) fn ffi_buf_call<F>(out: *mut WaiBuffer, f: F) -> c_int
|
|
where F: FnOnce() -> std::result::Result<Vec<u8>, (c_int, String)> + panic::UnwindSafe
|
|
{
|
|
if out.is_null() {
|
|
set_err("output buffer pointer is null");
|
|
return WAI_ERR_INVALID_ARG;
|
|
}
|
|
match panic::catch_unwind(f) {
|
|
Ok(Ok(v)) => unsafe {
|
|
if write_buf(out, v) { WAI_OK }
|
|
else { set_err("write to out failed"); WAI_ERR_INVALID_ARG }
|
|
}
|
|
Ok(Err((code, msg))) => {
|
|
set_err(msg);
|
|
unsafe { clear_buf(out); }
|
|
code
|
|
}
|
|
Err(p) => {
|
|
let msg = if let Some(s) = p.downcast_ref::<&str>() { (*s).to_string() }
|
|
else if let Some(s) = p.downcast_ref::<String>() { s.clone() }
|
|
else { "panic with non-string payload".into() };
|
|
set_err(format!("internal panic: {msg}"));
|
|
unsafe { clear_buf(out); }
|
|
WAI_ERR_PANIC
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- envelope: wrap an existing payload in a WAI container ----------
|
|
/// The C error for an envelope the emit path refused: a candidate is the
|
|
/// caller's argument (`WAI_ERR_INVALID_ARG`), a serialisation failure the
|
|
/// container's (`WAI_ERR_CONTAINER`).
|
|
fn emit_err(e: crate::container::EmitError) -> (c_int, String) {
|
|
match e {
|
|
crate::container::EmitError::Candidate(_) | crate::container::EmitError::IntPayload { .. } => (WAI_ERR_INVALID_ARG, e.to_string()),
|
|
_ => (WAI_ERR_CONTAINER, e.to_string()),
|
|
}
|
|
}
|
|
|
|
/// What decoding an integer payload will cost (`wai_int_payload_declared`,
|
|
/// `extensions/integer-payloads.md` §7, *Memory and work*): the decoded output's
|
|
/// bytes (`output_known` 1 where they are filled), the symbols its streams code, and,
|
|
/// where `cost_known` is 1, the most bytes the reference decode holds at once beyond
|
|
/// the payload and the parsed set (`working_bytes`) and the networks'
|
|
/// multiply-accumulates, `macs_hi·2^64 + macs_lo`.
|
|
#[repr(C)]
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub struct WaiIntDeclared {
|
|
pub output_bytes: u64,
|
|
pub output_known: u8,
|
|
pub symbols: u64,
|
|
pub working_bytes: u64,
|
|
pub macs_lo: u64,
|
|
pub macs_hi: u64,
|
|
pub cost_known: u8,
|
|
}
|
|
|
|
impl WaiIntDeclared {
|
|
/// The figures `container::declared_size` reads from the header alone.
|
|
fn from_size(d: crate::container::DeclaredSize) -> Self {
|
|
let macs = d.macs.unwrap_or(0);
|
|
WaiIntDeclared {
|
|
output_bytes: d.output_bytes.unwrap_or(0),
|
|
output_known: d.output_bytes.is_some() as u8,
|
|
symbols: d.symbols,
|
|
working_bytes: d.working_bytes.unwrap_or(0),
|
|
macs_lo: macs as u64,
|
|
macs_hi: (macs >> 64) as u64,
|
|
cost_known: (d.working_bytes.is_some() && d.macs.is_some()) as u8,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The members of `capability`'s parameter set read from `set_path`: inside it where
|
|
/// it is a directory, by the names the set lists them under; the file itself for a set
|
|
/// of one file; else the file named for the member beside it. A member that cannot be
|
|
/// read is absent, and the cost then refuses with `set_incomplete`.
|
|
#[cfg(feature = "neural_int")]
|
|
fn read_set(capability: &str, set_path: &std::path::Path) -> std::collections::BTreeMap<String, Vec<u8>> {
|
|
let members = crate::int_payload::set_members(capability);
|
|
members.iter()
|
|
.filter_map(|m| {
|
|
let f = if set_path.is_dir() {
|
|
set_path.join(m)
|
|
} else if members.len() == 1 {
|
|
set_path.to_path_buf()
|
|
} else {
|
|
set_path.parent().map(|d| d.join(m)).unwrap_or_else(|| std::path::PathBuf::from(m))
|
|
};
|
|
std::fs::read(f).ok().map(|b| (m.to_string(), b))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Every figure of the declared cost, over the set read from `set_path`.
|
|
#[cfg(feature = "neural_int")]
|
|
fn cost_with_set(capability: &str, payload: &[u8], set_path: &str) -> std::result::Result<WaiIntDeclared, (c_int, String)> {
|
|
use crate::int_payload::{declared_cost, resolve};
|
|
let files = read_set(capability, std::path::Path::new(set_path));
|
|
let view: std::collections::BTreeMap<String, &[u8]> = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect();
|
|
let params = resolve(capability, None, &[crate::pin::HeldBytes::Set { listing: b"", files: &view }], None)
|
|
.map_err(|r| (WAI_ERR_INVALID_ARG, format!("{}: {r}", r.code())))?;
|
|
let c = declared_cost(capability, payload, ¶ms).map_err(|e| (WAI_ERR_INVALID_ARG, e.to_string()))?;
|
|
Ok(WaiIntDeclared {
|
|
output_bytes: c.output_bytes, output_known: 1, symbols: c.symbols, working_bytes: c.working_bytes,
|
|
macs_lo: c.macs as u64, macs_hi: (c.macs >> 64) as u64, cost_known: 1,
|
|
})
|
|
}
|
|
|
|
#[cfg(not(feature = "neural_int"))]
|
|
fn cost_with_set(capability: &str, _: &[u8], _: &str) -> std::result::Result<WaiIntDeclared, (c_int, String)> {
|
|
Err((WAI_ERR_INVALID_ARG, format!("{capability}: this build reads no parameter set (feature neural_int)")))
|
|
}
|
|
|
|
/// Read what decoding the payload of an integer capability will cost before decoding
|
|
/// it, so a sink can refuse a payload past its own size policy first
|
|
/// (`extensions/integer-payloads.md` §7, *Memory and work*). `capability` is the
|
|
/// envelope's capability (or the `WAI2` component's). The payload is parsed as a sink
|
|
/// parses it: one that is not the format SPEC §5 pins `capability` to is
|
|
/// `WAI_ERR_INVALID_ARG`, with its code in `wai_last_error`, and so is a capability
|
|
/// with no pinned integer format.
|
|
///
|
|
/// With `set_path` null, only the header is read: a `wai.neural.int_hyper` image's
|
|
/// size is its synthesis network's, so `output_known` is 0 for it, and `cost_known` is
|
|
/// 1 only for a `WIV1` with keyframe 0, which runs no network. With `set_path` naming
|
|
/// the capability's parameter set — a directory holding its members by the names §9
|
|
/// gives, or the file of a one-file set — every figure is filled
|
|
/// (`int_payload::declared_cost`), and the set is checked against the payload as a
|
|
/// decode checks it first (`set_incomplete`, `model_mismatch`, `geometry_mismatch`,
|
|
/// …). The set is read as given, unverified: a pin is verified when the envelope is
|
|
/// decoded. A build without `neural_int` reads no set.
|
|
///
|
|
/// # Safety
|
|
/// `capability` NUL-terminated; `payload` valid for `payload_len` bytes; `set_path`
|
|
/// null or NUL-terminated UTF-8; `out` writable.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_int_payload_declared(
|
|
capability: *const c_char,
|
|
payload: *const u8, payload_len: usize,
|
|
set_path: *const c_char,
|
|
out: *mut WaiIntDeclared,
|
|
) -> c_int {
|
|
if capability.is_null() || out.is_null() {
|
|
set_err("int_payload_declared: capability/out null");
|
|
return WAI_ERR_INVALID_ARG;
|
|
}
|
|
let r = panic::catch_unwind(|| -> std::result::Result<WaiIntDeclared, (c_int, String)> {
|
|
let cap = unsafe { std::ffi::CStr::from_ptr(capability) }.to_string_lossy().to_string();
|
|
let p = unsafe { input_slice(payload, payload_len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG, "payload null + len>0".into()))?;
|
|
let header = match crate::container::declared_size(&cap, p) {
|
|
Ok(Some(d)) => WaiIntDeclared::from_size(d),
|
|
Ok(None) => return Err((WAI_ERR_INVALID_ARG, format!("{cap} has no pinned integer payload format"))),
|
|
Err(e) => return Err((WAI_ERR_INVALID_ARG, e.to_string())),
|
|
};
|
|
if set_path.is_null() {
|
|
return Ok(header);
|
|
}
|
|
let set = unsafe { std::ffi::CStr::from_ptr(set_path) }.to_str()
|
|
.map_err(|_| (WAI_ERR_INVALID_ARG, "set_path is not UTF-8".to_string()))?;
|
|
cost_with_set(&cap, p, set)
|
|
});
|
|
match r {
|
|
Ok(Ok(d)) => { unsafe { *out = d; } WAI_OK }
|
|
Ok(Err((code, msg))) => { set_err(msg); code }
|
|
Err(_) => { set_err("internal panic in int_payload_declared"); WAI_ERR_PANIC }
|
|
}
|
|
}
|
|
|
|
/// Builds a complete WAI file from a codec-produced payload + manifest
|
|
/// fields. `manifest_json` is a UTF-8 JSON object overriding/supplying
|
|
/// any fields; `payload` is the bytes from one of the codec encoders.
|
|
/// Refuses (`WAI_ERR_INVALID_ARG`) a manifest that names a candidate capability
|
|
/// as its capability or fallback, which an encoder MUST NOT emit (SPEC §5,
|
|
/// *Candidate capabilities*), and a payload that is not the format SPEC §5 pins its
|
|
/// integer capability or fallback to (*Payloads of the WAI-native integer codecs*):
|
|
/// it emits through `Wai::to_emit_bytes`.
|
|
///
|
|
/// # Safety
|
|
/// `manifest_json` NUL-terminated; `payload` valid for `payload_len` bytes; `out` writable.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_pack(
|
|
manifest_json: *const c_char,
|
|
payload: *const u8, payload_len: usize,
|
|
out: *mut WaiBuffer,
|
|
) -> c_int {
|
|
ffi_buf_call(out, || {
|
|
if manifest_json.is_null() {
|
|
return Err((WAI_ERR_INVALID_ARG, "null manifest_json".into()));
|
|
}
|
|
let s = unsafe { std::ffi::CStr::from_ptr(manifest_json) }
|
|
.to_string_lossy().to_string();
|
|
let manifest: crate::container::Manifest = serde_json::from_str(&s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("manifest parse: {e}")))?;
|
|
let pl = unsafe { input_slice(payload, payload_len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG, "payload null + len>0".into()))?
|
|
.to_vec();
|
|
crate::container::Wai::new(manifest, pl).to_emit_bytes().map_err(emit_err)
|
|
})
|
|
}
|
|
|
|
// ---- v1.1: multi-rendition envelope FFI ------------------------
|
|
// Multi-rendition is a separate magic ("WAI2") with multiple payloads
|
|
// in one envelope. The C ABI keeps it consumer-friendly by serializing
|
|
// the rendition metadata (capability + kind + payload offset/length)
|
|
// as a JSON sidecar and packing every rendition payload contiguously
|
|
// into a single buffer — the caller indexes into the buffer using the
|
|
// JSON. No array-of-structs ownership games.
|
|
|
|
/// Detect WAI version from a byte stream. Returns 1 (WAI1), 2 (WAI2),
|
|
/// or 0 with an error message in `wai_last_error` for unknown magic.
|
|
///
|
|
/// # Safety
|
|
/// `bytes` valid for `len` bytes.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_detect_version(
|
|
bytes: *const u8, len: usize,
|
|
) -> c_int {
|
|
let s = match unsafe { input_slice(bytes, len) } {
|
|
Some(s) => s,
|
|
None => { set_err("bytes null + len>0"); return 0; }
|
|
};
|
|
match crate::container::detect_version(s) {
|
|
Ok(v) => v as c_int,
|
|
Err(e) => { set_err(e); 0 }
|
|
}
|
|
}
|
|
|
|
/// Pack a WAI2 multi-rendition envelope.
|
|
///
|
|
/// - `manifest_json` must declare a `renditions` array of length `n`.
|
|
/// - `payload_data` is an array of `n` byte-buffer pointers.
|
|
/// - `payload_lens` is the matching array of `n` lengths.
|
|
/// - The i-th payload pairs with `manifest.renditions[i]`.
|
|
///
|
|
/// Refuses (`WAI_ERR_INVALID_ARG`) a table with a component, in either role,
|
|
/// whose capability is a candidate (SPEC §5, *Candidate capabilities*), or whose
|
|
/// payload is not the format SPEC §5 pins its integer capability to: it emits
|
|
/// through `WaiMulti::to_emit_bytes`.
|
|
///
|
|
/// # Safety
|
|
/// All array pointers must point to valid memory of the declared length.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_pack_multi(
|
|
manifest_json: *const c_char,
|
|
payload_data: *const *const u8,
|
|
payload_lens: *const usize,
|
|
n_payloads: usize,
|
|
out: *mut WaiBuffer,
|
|
) -> c_int {
|
|
ffi_buf_call(out, || {
|
|
if manifest_json.is_null() {
|
|
return Err((WAI_ERR_INVALID_ARG, "null manifest_json".into()));
|
|
}
|
|
let mj = unsafe { std::ffi::CStr::from_ptr(manifest_json) }
|
|
.to_string_lossy().to_string();
|
|
let manifest: crate::container::ManifestV2 = serde_json::from_str(&mj)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("manifest parse: {e}")))?;
|
|
if manifest.renditions.len() != n_payloads {
|
|
return Err((WAI_ERR_INVALID_ARG, format!(
|
|
"manifest has {} renditions but n_payloads={n_payloads}",
|
|
manifest.renditions.len())));
|
|
}
|
|
if n_payloads > 0 && (payload_data.is_null() || payload_lens.is_null()) {
|
|
return Err((WAI_ERR_INVALID_ARG,
|
|
"payload_data/payload_lens null with n_payloads>0".into()));
|
|
}
|
|
// Collect rendition payloads. Every member of the manifest — the component
|
|
// identity members, and any a later revision defines — is kept as written.
|
|
let mut payloads: Vec<Vec<u8>> = Vec::with_capacity(n_payloads);
|
|
for i in 0..n_payloads {
|
|
let ptr = unsafe { *payload_data.add(i) };
|
|
let len = unsafe { *payload_lens.add(i) };
|
|
let s = unsafe { input_slice(ptr, len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG,
|
|
format!("payload {i} null with len>0")))?;
|
|
payloads.push(s.to_vec());
|
|
}
|
|
crate::container::WaiMulti::from_parts(manifest, payloads)
|
|
.map_err(|e| (WAI_ERR_INVALID_ARG, e.to_string()))?
|
|
.to_emit_bytes().map_err(emit_err)
|
|
})
|
|
}
|
|
|
|
/// Parse a WAI2 envelope. Writes three outputs:
|
|
///
|
|
/// - `out_manifest` — the manifest JSON exactly as the envelope carries it
|
|
/// (UTF-8 bytes, no NUL), so members this library does not model survive
|
|
/// - `out_renditions_json` — a JSON array with one object per component:
|
|
/// `capability`, `kind`, `offset` (into `out_payload_block`), `length`, and
|
|
/// `selectable` (whether SPEC §7.1 may pick it: a companion, or a companion-only
|
|
/// capability, never is), plus `id`, `sha256`, `prior`, `role`, `companion_of`
|
|
/// and `companion_of_id` when the entry carries them. A C sink selects with
|
|
/// `wai_envelope_select` rather than by hand.
|
|
/// - `out_payload_block` — every rendition's payload bytes, packed
|
|
/// contiguously. The caller indexes via the JSON above.
|
|
///
|
|
/// This avoids exposing a C struct array (and the ownership pain that
|
|
/// comes with it). Any JSON parser the consumer already has decodes
|
|
/// the rendition table in two lines.
|
|
///
|
|
/// # Safety
|
|
/// `bytes` valid for `len` bytes; `out_manifest` writable; `out_renditions_json` writable; `out_payload_block` writable.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_unpack_multi(
|
|
bytes: *const u8, len: usize,
|
|
out_manifest: *mut WaiBuffer,
|
|
out_renditions_json: *mut WaiBuffer,
|
|
out_payload_block: *mut WaiBuffer,
|
|
) -> c_int {
|
|
if out_manifest.is_null() || out_renditions_json.is_null()
|
|
|| out_payload_block.is_null()
|
|
{
|
|
set_err("unpack_multi: one of the out pointers is null");
|
|
return WAI_ERR_INVALID_ARG;
|
|
}
|
|
match panic::catch_unwind(|| -> std::result::Result<(Vec<u8>, Vec<u8>, Vec<u8>), (c_int, String)> {
|
|
let s = unsafe { input_slice(bytes, len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
|
|
let m = crate::container::WaiMulti::from_bytes(s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
|
|
let mb = crate::container::manifest_bytes(s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?
|
|
.to_vec();
|
|
// build rendition table JSON + concatenated payload block
|
|
let mut table: Vec<serde_json::Value> = Vec::with_capacity(m.renditions.len());
|
|
let mut block: Vec<u8> = Vec::new();
|
|
for (meta, payload) in m.manifest.renditions.iter().zip(m.renditions.iter()) {
|
|
let mut e = serde_json::json!({
|
|
"capability": meta.capability,
|
|
"kind": meta.kind,
|
|
"offset": block.len(),
|
|
"length": payload.len(),
|
|
"selectable": meta.is_selectable(),
|
|
});
|
|
let o = e.as_object_mut().unwrap();
|
|
let mut put = |k: &str, v: Option<serde_json::Value>| if let Some(v) = v { o.insert(k.into(), v); };
|
|
put("id", meta.id.clone().map(Into::into));
|
|
put("sha256", meta.sha256.clone().map(Into::into));
|
|
put("prior", meta.prior.as_ref().and_then(|p| serde_json::to_value(p).ok()));
|
|
put("role", meta.role.clone().map(Into::into));
|
|
put("companion_of", meta.companion_of.clone().map(Into::into));
|
|
put("companion_of_id", meta.companion_of_id.clone().map(Into::into));
|
|
table.push(e);
|
|
block.extend_from_slice(payload);
|
|
}
|
|
let tj = serde_json::to_vec(&serde_json::Value::Array(table))
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("table serialize: {e}")))?;
|
|
Ok((mb, tj, block))
|
|
}) {
|
|
Ok(Ok((mb, tj, block))) => unsafe {
|
|
*out_manifest = WaiBuffer::from_vec(mb);
|
|
*out_renditions_json = WaiBuffer::from_vec(tj);
|
|
*out_payload_block = WaiBuffer::from_vec(block);
|
|
WAI_OK
|
|
}
|
|
Ok(Err((code, msg))) => {
|
|
set_err(msg);
|
|
unsafe {
|
|
clear_buf(out_manifest);
|
|
clear_buf(out_renditions_json);
|
|
clear_buf(out_payload_block);
|
|
}
|
|
code
|
|
}
|
|
Err(_) => {
|
|
set_err("internal panic in envelope_unpack_multi");
|
|
unsafe {
|
|
clear_buf(out_manifest);
|
|
clear_buf(out_renditions_json);
|
|
clear_buf(out_payload_block);
|
|
}
|
|
WAI_ERR_PANIC
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses a WAI envelope. Writes the manifest JSON exactly as the envelope
|
|
/// carries it to `out_manifest` (so `security`, and members this library does not
|
|
/// model, survive) and the raw payload to `out_payload`.
|
|
///
|
|
/// # Safety
|
|
/// `bytes` valid for `len` bytes; `out_manifest` writable; `out_payload` writable.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_unpack(
|
|
bytes: *const u8, len: usize,
|
|
out_manifest: *mut WaiBuffer, out_payload: *mut WaiBuffer,
|
|
) -> c_int {
|
|
if out_manifest.is_null() || out_payload.is_null() {
|
|
set_err("envelope_unpack: out_manifest/out_payload null");
|
|
return WAI_ERR_INVALID_ARG;
|
|
}
|
|
match panic::catch_unwind(|| -> std::result::Result<(Vec<u8>, Vec<u8>), (c_int, String)> {
|
|
let s = unsafe { input_slice(bytes, len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
|
|
let wai = crate::container::Wai::from_bytes(s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
|
|
let mj = crate::container::manifest_bytes(s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?
|
|
.to_vec();
|
|
Ok((mj, wai.payload))
|
|
}) {
|
|
Ok(Ok((mj, pl))) => unsafe {
|
|
*out_manifest = WaiBuffer::from_vec(mj);
|
|
*out_payload = WaiBuffer::from_vec(pl);
|
|
WAI_OK
|
|
}
|
|
Ok(Err((code, msg))) => {
|
|
set_err(msg);
|
|
unsafe { clear_buf(out_manifest); clear_buf(out_payload); }
|
|
code
|
|
}
|
|
Err(_) => {
|
|
set_err("internal panic in envelope_unpack");
|
|
unsafe { clear_buf(out_manifest); clear_buf(out_payload); }
|
|
WAI_ERR_PANIC
|
|
}
|
|
}
|
|
}
|
|
|
|
/// SPEC §7.1 selection for a `WAI2` envelope at a sink that advertises the
|
|
/// capabilities in `advertised_json` (a NUL-terminated JSON array of capability
|
|
/// strings). Writes the index of the picked rendition to `out_index`: the first
|
|
/// entry, in SPEC §4's energy order (table order, but an entry whose capability is
|
|
/// learned and carries no energy budget after every other), that is selectable (never a companion, nor a capability that is not
|
|
/// dispatchable under SPEC §4: companion-only, a candidate, a record or a
|
|
/// derivation), advertised, and whose payload matches its `sha256`. This call holds no
|
|
/// parameter set, so an entry whose pin applies is skipped (`prior_not_held`); a
|
|
/// sink that holds parameter sets decodes through `wai_neural_decode_ex`, which
|
|
/// verifies each pin. Returns `WAI_ERR_NO_RENDITION` when nothing is picked, with
|
|
/// each entry's index and reason in `wai_last_error`.
|
|
///
|
|
/// # Safety
|
|
/// `envelope` valid for `envelope_len` bytes; `advertised_json` NUL-terminated;
|
|
/// `out_index` writable.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wai_envelope_select(
|
|
envelope: *const u8, envelope_len: usize,
|
|
advertised_json: *const c_char,
|
|
out_index: *mut u32,
|
|
) -> c_int {
|
|
if out_index.is_null() || advertised_json.is_null() {
|
|
set_err("envelope_select: out_index/advertised_json null");
|
|
return WAI_ERR_INVALID_ARG;
|
|
}
|
|
let r = panic::catch_unwind(|| -> std::result::Result<u32, (c_int, String)> {
|
|
let s = unsafe { input_slice(envelope, envelope_len) }
|
|
.ok_or((WAI_ERR_INVALID_ARG, "envelope null + len>0".into()))?;
|
|
let adv = unsafe { std::ffi::CStr::from_ptr(advertised_json) }.to_string_lossy().to_string();
|
|
let advertised: Vec<String> = serde_json::from_str(&adv)
|
|
.map_err(|e| (WAI_ERR_INVALID_ARG, format!("advertised_json: {e}")))?;
|
|
let m = crate::container::WaiMulti::from_bytes(s)
|
|
.map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
|
|
match m.pick(|c| advertised.iter().any(|a| a == c), crate::container::no_parameter_sets()) {
|
|
Ok(p) => Ok(p.index as u32),
|
|
Err(skips) => Err((WAI_ERR_NO_RENDITION, skips.iter()
|
|
.map(|(i, k)| format!("{i}: {k}")).collect::<Vec<_>>().join("; "))),
|
|
}
|
|
});
|
|
match r {
|
|
Ok(Ok(i)) => { unsafe { *out_index = i; } WAI_OK }
|
|
Ok(Err((code, msg))) => { set_err(msg); code }
|
|
Err(_) => { set_err("internal panic in envelope_select"); WAI_ERR_PANIC }
|
|
}
|
|
}
|