open-standards/wai/wai-rs/tests/ffi_robustness.rs
Transaction Science 2502f70493 wai: gate codec-dependent test/example targets on codecs
Follow-up to the neural/codecs decouple: a few test files (jpegai_e2e,
fuzz_smoke, ffi_robustness) and the three codec examples (opus_check, dl_check,
jpeg_test) use the codec chain / C FFI, so they no longer compile under
`--no-default-features --features neural`. Gate them on `codecs` (jpegai_e2e
needs both neural and codecs). `--features neural` now builds every target
clean; the default (codecs-on) build is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 17:36:39 -04:00

285 lines
8.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Robustness tests for the C FFI surface. These exercise exactly the
//! mistakes an SDK consumer is most likely to make when calling
//! `libwai.dylib` from Python/Node/Swift/etc.: null pointers, zero
//! lengths, mismatched dimensions, truncated payloads, malformed magic.
//!
//! Every wai_* function MUST return a non-zero status code (never panic
//! into C — that's undefined behavior) and leave its output `WaiBuffer*`
//! cleared on any error.
//!
//! Exercises the codec C FFI (`wai::ffi`), so it requires the `codecs` feature.
#![cfg(feature = "codecs")]
use std::ptr;
use wai::ffi::*;
// ---- helpers --------------------------------------------------------
fn empty_buf() -> WaiBuffer {
WaiBuffer { data: ptr::null_mut(), len: 0, cap: 0 }
}
fn synth_rgb(h: u32, w: u32) -> Vec<u8> {
let mut v = vec![0u8; (h * w * 3) as usize];
for i in 0..h {
for j in 0..w {
let p = ((i * w + j) * 3) as usize;
v[p] = ((i + j) as u8).wrapping_mul(4);
v[p + 1] = (i as u8).wrapping_mul(8);
v[p + 2] = (j as u8).wrapping_mul(8);
}
}
v
}
// ---- null / mismatched inputs --------------------------------------
#[test]
fn png_encode_null_out() {
let rgb = synth_rgb(8, 8);
let rc = unsafe {
wai_image_png_encode(rgb.as_ptr(), rgb.len(), 8, 8, ptr::null_mut())
};
assert_eq!(rc, WAI_ERR_INVALID_ARG, "null out must reject");
}
#[test]
fn png_encode_len_mismatch() {
let rgb = synth_rgb(8, 8); // 192 bytes
let mut out = empty_buf();
let rc = unsafe {
// claim 16×16 but pass 8×8 buffer
wai_image_png_encode(rgb.as_ptr(), rgb.len(), 16, 16, &mut out)
};
assert_eq!(rc, WAI_ERR_INVALID_ARG, "len mismatch must reject");
assert!(out.data.is_null(), "out must be empty on error");
}
#[test]
fn png_encode_null_data_with_nonzero_len() {
let mut out = empty_buf();
let rc = unsafe {
wai_image_png_encode(ptr::null(), 100, 8, 8, &mut out)
};
assert_eq!(rc, WAI_ERR_INVALID_ARG);
assert!(out.data.is_null());
}
#[test]
fn png_encode_zero_dimensions_overflow_safe() {
let mut out = empty_buf();
// h*w*3 of u32::MAX should overflow detection, not produce UB
let rc = unsafe {
wai_image_png_encode(ptr::null(), 0, u32::MAX, u32::MAX, &mut out)
};
assert_eq!(rc, WAI_ERR_INVALID_ARG, "huge dims must be caught");
}
#[test]
fn png_decode_garbage_bytes() {
let garbage = b"not a png file at all".to_vec();
let mut out = empty_buf();
let mut h = 0u32; let mut w = 0u32;
let rc = unsafe {
wai_image_png_decode(garbage.as_ptr(), garbage.len(),
&mut out, &mut h, &mut w)
};
assert_ne!(rc, WAI_OK, "garbage must not decode as PNG");
assert!(out.data.is_null());
assert_eq!((h, w), (0, 0), "dimensions must be zeroed on error");
}
#[test]
fn png_decode_truncated_png() {
// start of a valid PNG signature but cut off
let trunc = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00];
let mut out = empty_buf();
let mut h = 0u32; let mut w = 0u32;
let rc = unsafe {
wai_image_png_decode(trunc.as_ptr(), trunc.len(),
&mut out, &mut h, &mut w)
};
assert_ne!(rc, WAI_OK);
}
#[test]
fn png_decode_null_size_out() {
let mut out = empty_buf();
let rc = unsafe {
wai_image_png_decode(b"x".as_ptr(), 1, &mut out, ptr::null_mut(), ptr::null_mut())
};
assert_eq!(rc, WAI_ERR_INVALID_ARG);
}
// ---- audio: FLAC / Opus -------------------------------------------
#[test]
fn flac_decode_garbage() {
let garbage = vec![0u8; 64];
let mut out = empty_buf();
let mut sr = 0u32;
let rc = unsafe {
wai_audio_flac_decode(garbage.as_ptr(), garbage.len(), &mut out, &mut sr)
};
assert_ne!(rc, WAI_OK);
assert_eq!(sr, 0);
}
#[test]
fn opus_encode_invalid_sr() {
let samples = vec![0f32; 1000];
let mut out = empty_buf();
// 44100 isn't an opus sr; must fail cleanly (not panic)
let rc = unsafe {
wai_audio_opus_encode(samples.as_ptr(), samples.len(),
44100, 64_000, &mut out)
};
assert_eq!(rc, WAI_ERR_CODEC);
assert!(out.data.is_null());
}
#[test]
fn opus_decode_garbage() {
let garbage = vec![0u8; 16];
let mut out = empty_buf();
let mut sr = 0u32;
let rc = unsafe {
wai_audio_opus_decode(garbage.as_ptr(), garbage.len(), &mut out, &mut sr)
};
assert_ne!(rc, WAI_OK);
}
// ---- text: zstd / xz ----------------------------------------------
#[test]
fn zstd_decode_garbage() {
let garbage = b"this is not a zstd stream".to_vec();
let mut out = empty_buf();
let rc = unsafe {
wai_text_zstd_decode(garbage.as_ptr(), garbage.len(), &mut out)
};
assert_ne!(rc, WAI_OK);
}
#[test]
fn xz_decode_garbage() {
let garbage = vec![0u8; 32];
let mut out = empty_buf();
let rc = unsafe {
wai_text_xz_decode(garbage.as_ptr(), garbage.len(), &mut out)
};
assert_ne!(rc, WAI_OK);
}
#[test]
fn zstd_round_trip_empty_input() {
let mut enc = empty_buf();
let rc = unsafe { wai_text_zstd_encode(ptr::null(), 0, 19, &mut enc) };
assert_eq!(rc, WAI_OK, "encoding zero bytes is valid");
let mut dec = empty_buf();
let rc = unsafe { wai_text_zstd_decode(enc.data, enc.len, &mut dec) };
assert_eq!(rc, WAI_OK);
assert_eq!(dec.len, 0);
unsafe { wai_buffer_free(enc); wai_buffer_free(dec); }
}
// ---- envelope ------------------------------------------------------
#[test]
fn envelope_unpack_bad_magic() {
let bad = b"NOTWAI...".to_vec();
let mut m = empty_buf();
let mut p = empty_buf();
let rc = unsafe {
wai_envelope_unpack(bad.as_ptr(), bad.len(), &mut m, &mut p)
};
assert_eq!(rc, WAI_ERR_CONTAINER);
assert!(m.data.is_null() && p.data.is_null());
}
#[test]
fn envelope_unpack_truncated() {
// just the magic, nothing else
let trunc = b"WAI1".to_vec();
let mut m = empty_buf();
let mut p = empty_buf();
let rc = unsafe {
wai_envelope_unpack(trunc.as_ptr(), trunc.len(), &mut m, &mut p)
};
assert_ne!(rc, WAI_OK);
}
#[test]
fn envelope_pack_bad_manifest_json() {
let bad_json = std::ffi::CString::new("{not a valid json}").unwrap();
let mut out = empty_buf();
let rc = unsafe {
wai_envelope_pack(bad_json.as_ptr(), ptr::null(), 0, &mut out)
};
assert_eq!(rc, WAI_ERR_CONTAINER);
assert!(out.data.is_null());
}
#[test]
fn envelope_pack_null_manifest() {
let mut out = empty_buf();
let rc = unsafe {
wai_envelope_pack(ptr::null(), ptr::null(), 0, &mut out)
};
assert_eq!(rc, WAI_ERR_INVALID_ARG);
}
// ---- video ---------------------------------------------------------
#[test]
fn av1_encode_zero_fps() {
let rgb = synth_rgb(16, 16);
let mut out = empty_buf();
let rc = unsafe {
wai_video_av1_encode(rgb.as_ptr(), rgb.len(),
1, 16, 16, 0, 1, 0, 50, &mut out)
};
assert_eq!(rc, WAI_ERR_INVALID_ARG, "zero fps_num must be rejected");
}
#[test]
fn av1_decode_garbage() {
let garbage = vec![0u8; 64];
let mut out = empty_buf();
let mut n = 0u32; let mut h = 0u32; let mut w = 0u32;
let mut fn_ = 0u32; let mut fd_ = 0u32;
let rc = unsafe {
wai_video_av1_decode(garbage.as_ptr(), garbage.len(),
&mut out, &mut n, &mut h, &mut w, &mut fn_, &mut fd_)
};
assert_ne!(rc, WAI_OK);
}
// ---- happy-path sanity (1 per codec to confirm we didn't break it) -
#[test]
fn png_round_trip_happy() {
let rgb = synth_rgb(16, 16);
let mut enc = empty_buf();
let rc = unsafe { wai_image_png_encode(rgb.as_ptr(), rgb.len(), 16, 16, &mut enc) };
assert_eq!(rc, WAI_OK);
let mut dec = empty_buf();
let mut h = 0u32; let mut w = 0u32;
let rc = unsafe { wai_image_png_decode(enc.data, enc.len, &mut dec, &mut h, &mut w) };
assert_eq!(rc, WAI_OK);
assert_eq!((h, w), (16, 16));
let got = unsafe { std::slice::from_raw_parts(dec.data, dec.len) };
assert_eq!(got, rgb.as_slice(), "PNG happy-path must be bit-exact");
unsafe { wai_buffer_free(enc); wai_buffer_free(dec); }
}
#[test]
fn last_error_returns_something_after_failure() {
let mut out = empty_buf();
// force an error
let _ = unsafe { wai_image_png_decode(b"x".as_ptr(), 1, &mut out,
std::ptr::null_mut(), std::ptr::null_mut()) };
let p = wai_last_error();
assert!(!p.is_null(), "wai_last_error must be set after a failed call");
}