check_reconstruction_binding compared only the hash, so a binding of another decoderId, format or size than the exact decoder computes was reported as Differs. reconstruction-binding §2 forbids reporting a binding as failed when the verifier decoded some other way than its descriptor says. Such a binding is now BindingCheck::OtherReconstruction, with the path that ran; for the exact decoder that is any decoderId but 2. wai_neural_decode_ex dropped the path and the capability, so a native sink could not state its decoder entry's variant. wai_decoded_t gains `path` (WAI_DECODE_PATH_SOLE, _EXACT or _FLOAT) and `fallback`, 1 when a WAI1 envelope decoded through its fallback. include/wai.h is regenerated. The neural gates counted the two JPEG-AI cases' skips as passes, so CI ran only the one-path case. That step now selects its two committed-input cases by name, one of them a binding over int_mlicpp checked under its descriptor. A new job in the 24.04 image fetches the pinned jpegai/v1 bundle and runs the exact decoder's case with WAI_REQUIRE_SOTA_VECTORS=1, requiring it by name and refusing a skip. The float decode needs the reference-software assets, which no job holds, so its case runs locally only; the test header and determinism-tiers §5 say so. The C ABI step runs the path and fallback test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
150 lines
7.8 KiB
Rust
150 lines
7.8 KiB
Rust
//! The neural C ABI reports a refused pin as `WAI_ERR_PRIOR` with its reason first
|
|
//! in `wai_last_error`, an unsound `replicate` under the strict flag as
|
|
//! `WAI_ERR_UNSOUND_REPLICATE`, decodes a `WAI2` envelope through the
|
|
//! rendition whose pin the registry holds, and reports the decode path that ran and
|
|
//! whether a `WAI1` decoded through its fallback — all on the committed int_mlicpp
|
|
//! vector, with no model to fetch.
|
|
#![cfg(all(feature = "neural", feature = "ffi"))]
|
|
|
|
use std::ffi::{CStr, CString};
|
|
use std::path::{Path, PathBuf};
|
|
use std::ptr;
|
|
|
|
use wai::codecs::DeclaredTier;
|
|
use wai::container::{Prior, Rendition, Supersedes, Wai, WaiMulti};
|
|
use wai::ffi::*;
|
|
use wai::ffi_neural::*;
|
|
|
|
fn dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_mlicpp")
|
|
}
|
|
|
|
fn last_error() -> String {
|
|
let p = wai_last_error();
|
|
if p.is_null() { String::new() } else { unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned() }
|
|
}
|
|
|
|
fn decoded() -> WaiDecoded {
|
|
WaiDecoded {
|
|
kind: 0, index: 0, data: WaiBuffer { data: ptr::null_mut(), len: 0, cap: 0 },
|
|
width: 0, height: 0, sample_rate: 0, n_frames: 0, fps_x_1000: 0, path: 0, fallback: 0,
|
|
}
|
|
}
|
|
|
|
fn fnv_of(d: &WaiDecoded) -> String {
|
|
let rgb = unsafe { std::slice::from_raw_parts(d.data.data, d.data.len) };
|
|
format!("{:016x}", wai::int_hyper_synth::fnv1a(rgb))
|
|
}
|
|
|
|
#[test]
|
|
fn tampered_model_is_wai_err_prior() {
|
|
let env = std::fs::read(dir().join("stream.wai")).unwrap();
|
|
let tampered = std::env::temp_dir().join(format!("wai-ffi-tampered-{}.wmb", std::process::id()));
|
|
let mut b = std::fs::read(dir().join("model.wmb")).unwrap();
|
|
b[20] ^= 1;
|
|
std::fs::write(&tampered, &b).unwrap();
|
|
let path = CString::new(tampered.to_str().unwrap()).unwrap();
|
|
let mut rgb = WaiBuffer { data: ptr::null_mut(), len: 0, cap: 0 };
|
|
let (mut w, mut h) = (0u32, 0u32);
|
|
let code = unsafe { wai_neural_decode_image(env.as_ptr(), env.len(), path.as_ptr(), &mut rgb, &mut w, &mut h) };
|
|
std::fs::remove_file(&tampered).ok();
|
|
assert_eq!(code, WAI_ERR_PRIOR);
|
|
assert!(last_error().starts_with("prior_not_held: "), "{}", last_error());
|
|
// The true model decodes.
|
|
let path = CString::new(dir().join("model.wmb").to_str().unwrap()).unwrap();
|
|
assert_eq!(unsafe { wai_neural_decode_image(env.as_ptr(), env.len(), path.as_ptr(), &mut rgb, &mut w, &mut h) }, WAI_OK);
|
|
assert_eq!((w, h), (64, 64));
|
|
unsafe { wai_buffer_free(rgb) };
|
|
}
|
|
|
|
/// A failed decode leaves `*out` empty, whatever it held before: a caller that
|
|
/// frees `out->data` after any status frees nothing it did not get.
|
|
#[test]
|
|
fn failed_decode_ex_clears_out() {
|
|
let env = std::fs::read(dir().join("stream.wai")).unwrap();
|
|
let empty = CString::new("{}").unwrap();
|
|
let mut out = WaiDecoded {
|
|
kind: 7, index: 9, data: WaiBuffer { data: 0x5a5a as *mut u8, len: 5, cap: 5 },
|
|
width: 1, height: 1, sample_rate: 1, n_frames: 1, fps_x_1000: 1, path: 3, fallback: 1,
|
|
};
|
|
let code = unsafe { wai_neural_decode_ex(env.as_ptr(), env.len(), empty.as_ptr(), 0, &mut out) };
|
|
assert_ne!(code, WAI_OK);
|
|
assert_eq!((out.kind, out.index, out.width, out.height, out.path, out.fallback), (0, 0, 0, 0, 0, 0));
|
|
assert!(out.data.data.is_null() && out.data.len == 0 && out.data.cap == 0);
|
|
unsafe { wai_buffer_free(out.data) };
|
|
// A bad argument after the null check clears it too.
|
|
let mut out2 = WaiDecoded { kind: 3, ..decoded() };
|
|
let code = unsafe { wai_neural_decode_ex(env.as_ptr(), env.len(), ptr::null(), 0, &mut out2) };
|
|
assert_eq!(code, WAI_ERR_INVALID_ARG);
|
|
assert_eq!(out2.kind, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn strict_replicate_is_wai_err_unsound_replicate() {
|
|
let mut env = Wai::from_bytes(&std::fs::read(dir().join("stream.wai")).unwrap()).unwrap();
|
|
env.manifest.conditioning.prior = None;
|
|
let bytes = env.to_bytes().unwrap();
|
|
let reg = CString::new(format!(r#"{{"wai.neural.int_mlicpp":["{}"]}}"#, dir().join("model.wmb").display())).unwrap();
|
|
let mut out = decoded();
|
|
let code = unsafe { wai_neural_decode_ex(bytes.as_ptr(), bytes.len(), reg.as_ptr(), WAI_DECODE_STRICT, &mut out) };
|
|
assert_eq!(code, WAI_ERR_UNSOUND_REPLICATE);
|
|
assert!(last_error().contains("replicate_unpinned"), "{}", last_error());
|
|
// Without the strict flag the unpinned envelope decodes, through its own capability's
|
|
// one decode path, and says so.
|
|
assert_eq!(unsafe { wai_neural_decode_ex(bytes.as_ptr(), bytes.len(), reg.as_ptr(), 0, &mut out) }, WAI_OK);
|
|
assert_eq!((out.kind, out.width, out.height), (WAI_DECODED_IMAGE, 64, 64));
|
|
assert_eq!((out.path, out.fallback), (WAI_DECODE_PATH_SOLE, 0));
|
|
unsafe { wai_buffer_free(out.data) };
|
|
}
|
|
|
|
/// `wai_neural_decode_ex` reports the decode path that ran and whether a `WAI1` envelope
|
|
/// decoded through its fallback (extensions/determinism-tiers.md §5), which a native sink
|
|
/// states in its receipt (extensions/jwp-receipts.md §2.4.12, a decoder entry's `variant`
|
|
/// and `path`).
|
|
#[test]
|
|
fn decode_ex_reports_the_path_and_the_fallback() {
|
|
let mut env = Wai::from_bytes(&std::fs::read(dir().join("stream.wai")).unwrap()).unwrap();
|
|
env.manifest.conditioning.prior = None;
|
|
let reg = CString::new(format!(r#"{{"wai.neural.int_mlicpp":["{}"]}}"#, dir().join("model.wmb").display())).unwrap();
|
|
let own = env.to_bytes().unwrap();
|
|
let mut out = decoded();
|
|
assert_eq!(unsafe { wai_neural_decode_ex(own.as_ptr(), own.len(), reg.as_ptr(), 0, &mut out) }, WAI_OK);
|
|
assert_eq!((out.kind, out.path, out.fallback), (WAI_DECODED_IMAGE, WAI_DECODE_PATH_SOLE, 0));
|
|
let want = fnv_of(&out);
|
|
unsafe { wai_buffer_free(out.data) };
|
|
// The same payload under a capability the registry does not hold, with int_mlicpp as
|
|
// its fallback: the same picture, through the fallback, and the result says so.
|
|
env.manifest.model_requirement.capability = "wai.neural.int_hyper".into();
|
|
env.manifest.model_requirement.fallback = Some("wai.neural.int_mlicpp".into());
|
|
let via = env.to_bytes().unwrap();
|
|
let mut out = decoded();
|
|
assert_eq!(unsafe { wai_neural_decode_ex(via.as_ptr(), via.len(), reg.as_ptr(), 0, &mut out) }, WAI_OK, "{}", last_error());
|
|
assert_eq!((out.kind, out.path, out.fallback), (WAI_DECODED_IMAGE, WAI_DECODE_PATH_SOLE, 1));
|
|
assert_eq!(fnv_of(&out), want);
|
|
unsafe { wai_buffer_free(out.data) };
|
|
}
|
|
|
|
#[test]
|
|
fn wai2_decode_ex_picks_the_held_pin() {
|
|
let pl = Wai::from_bytes(&std::fs::read(dir().join("stream.wai")).unwrap()).unwrap().payload;
|
|
let committed = wai::pin::sha256_hex(&std::fs::read(dir().join("model.wmb")).unwrap());
|
|
let det = "int-rans/p16+int-mlicpp/i64";
|
|
let env = WaiMulti::new("image", "replicate", serde_json::Value::Null, vec![
|
|
Rendition::media("wai.neural.int_mlicpp", "int_mlicpp", pl.clone())
|
|
.with_prior(Prior::new("next", "5c".repeat(32), det).with_tier(DeclaredTier::DecodeEquivalence)
|
|
.with_supersedes(Supersedes::new(committed.clone()))),
|
|
Rendition::media("wai.neural.int_mlicpp", "int_mlicpp", pl)
|
|
.with_prior(Prior::new("committed", committed, det).with_tier(DeclaredTier::DecodeEquivalence)),
|
|
]).to_bytes().unwrap();
|
|
let reg = CString::new(format!(r#"{{"wai.neural.int_mlicpp":["{}"]}}"#, dir().join("model.wmb").display())).unwrap();
|
|
let mut out = decoded();
|
|
let code = unsafe { wai_neural_decode_ex(env.as_ptr(), env.len(), reg.as_ptr(), WAI_DECODE_STRICT, &mut out) };
|
|
assert_eq!(code, WAI_OK, "{}", last_error());
|
|
assert_eq!((out.index, out.kind), (1, WAI_DECODED_IMAGE));
|
|
assert_eq!(fnv_of(&out), "7c0b9a4b3423d775");
|
|
unsafe { wai_buffer_free(out.data) };
|
|
// With no set held, nothing is picked.
|
|
let empty = CString::new("{}").unwrap();
|
|
let mut out = decoded();
|
|
assert_eq!(unsafe { wai_neural_decode_ex(env.as_ptr(), env.len(), empty.as_ptr(), 0, &mut out) }, WAI_ERR_NO_RENDITION);
|
|
}
|