open-standards/wai/wai-rs/tests/ffi_robustness.rs
Transaction Science 78169bc6c2
All checks were successful
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 19s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 19s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 23s
wai-ledger service / does this change touch it (push) Successful in 15s
wai-sink-android native sink library / does this change touch it (push) Successful in 19s
WAI Studio / does this change touch it (push) Successful in 18s
wai-sink-swift native sink package / does this change touch it (push) Successful in 18s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m51s
wai-quantum studio and desktop / does this change touch it (push) Successful in 3m19s
wai-quantum for JavaScript / does this change touch it (push) Successful in 3m24s
wai-audit collector and player integration / does this change touch it (push) Successful in 3m41s
wai-app and Create / wai-app · does this change touch it (push) Successful in 3m55s
wai-fleet service / does this change touch it (push) Successful in 3m31s
WAI operation catalogue / does this change touch it (push) Successful in 3m33s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 15s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m29s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 2m49s
wai-ledger service / service · linux (push) Successful in 2m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m3s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m57s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m9s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m53s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 34s
wai-quantum for JavaScript / javascript · linux (push) Successful in 3m6s
wai-quantum studio and desktop / studio · browser (push) Successful in 6m8s
wai-audit collector and player integration / collector · linux (push) Successful in 4m15s
wai-fleet service / service · linux (push) Successful in 4m32s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 13m23s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 15m38s
WAI Studio / Studio in WebKit · linux (push) Successful in 24m45s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 3m17s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m53s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m30s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 48s
wai-ledger service / service · macos (push) Successful in 1m4s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m1s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m22s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m58s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m49s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m20s
wai-fleet service / service · macos (push) Successful in 44s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m43s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m45s
wai-rs: the codec wrappers and the default build pass clippy
clippy 1.99.1, warnings denied, every target, with the default features (the C codec libraries), on macOS arm64: 18 findings. div_ceil for a rounded-up division, while let for a loop that breaks on None, !is_empty, as_chunks, an array for a vec! that is only borrowed, slice::from_ref, parentheses dropped, a private long type named. av1_decode and av1_decode_within keep their public signatures spelled out, allowed with the reason; wai_bench_diff's CodecStats is the report's record whole, read by name.

No behaviour changes. tests/ffi_robustness, tests/media_decode_bounds and the codecs' library tests pass with the default features. No workflow holds this build to clippy: the change note says so.

Co-Authored-By: Claude <[email protected]>
2026-11-11 00:11:17 -05: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 = [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 = [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 = [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 = [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");
}