#![cfg(feature = "encode")]
use webpx::*;
#[cfg(feature = "streaming")]
fn rgba_fixture(width: u32, height: u32) -> Vec<u8> {
let mut rgba = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
rgba.extend_from_slice(&[
(x * 40 + 10) as u8,
(y * 50 + 20) as u8,
(x * 17 + y * 19 + 30) as u8,
255,
]);
}
}
rgba
}
#[cfg(feature = "streaming")]
fn encode_lossless_rgba(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
Encoder::new_rgba(rgba, width, height)
.lossless(true)
.encode(Unstoppable)
.expect("lossless encode should succeed")
}
#[test]
fn argb_lossless_encode_must_not_mutate_shared_input() {
let argb = vec![0x00_ff_00_00u32];
let original = argb.clone();
Encoder::new_argb(&argb, 1, 1)
.lossless(true)
.encode(Unstoppable)
.expect("lossless argb encode should succeed");
assert_eq!(
argb, original,
"safe ARGB encoding mutated data passed through an immutable slice"
);
}
#[test]
fn yuv_encode_must_not_mutate_shared_planes() {
let width = 8;
let height = 8;
let y: Vec<u8> = (0..(width * height)).map(|v| v as u8).collect();
let u: Vec<u8> = (0..((width / 2) * (height / 2)))
.map(|v| (64 + v) as u8)
.collect();
let v: Vec<u8> = (0..((width / 2) * (height / 2)))
.map(|v| (128 + v) as u8)
.collect();
let a = vec![0u8; (width * height) as usize];
let original_y = y.clone();
let original_u = u.clone();
let original_v = v.clone();
let planes = YuvPlanesRef {
y: &y,
y_stride: width as usize,
u: &u,
u_stride: (width / 2) as usize,
v: &v,
v_stride: (width / 2) as usize,
a: Some(&a),
a_stride: width as usize,
width,
height,
};
Encoder::new_yuv(planes)
.encode(Unstoppable)
.expect("yuv encode should succeed");
assert_eq!(
y, original_y,
"safe YUV encoding mutated the Y plane passed through an immutable slice"
);
assert_eq!(
u, original_u,
"safe YUV encoding mutated the U plane passed through an immutable slice"
);
assert_eq!(
v, original_v,
"safe YUV encoding mutated the V plane passed through an immutable slice"
);
}
#[test]
fn yuv_encode_must_reject_planes_shorter_than_stride_height() {
let width = 2;
let height = 2;
let hidden_tail = [16u8, 235, 235, 235];
let u = [128u8];
let v = [128u8];
let planes = YuvPlanesRef {
y: &hidden_tail[..1],
y_stride: width as usize,
u: &u,
u_stride: 1,
v: &v,
v_stride: 1,
a: None,
a_stride: 0,
width,
height,
};
let result = Encoder::new_yuv(planes).encode(Unstoppable);
assert!(
result.is_err(),
"safe YUV encoding accepted a Y plane shorter than y_stride * height"
);
}
mod stride_overflow {
use super::*;
const NEGATIVE_BOUNDARY: u32 = (i32::MAX as u32) + 1;
#[test]
fn argb_zero_copy_rejects_stride_above_i32_max() {
let argb: Vec<u32> = vec![0; 64];
let r = Encoder::new_argb_stride(&argb, 1, 1, NEGATIVE_BOUNDARY).encode(Unstoppable);
assert!(
r.is_err(),
"ARGB stride > i32::MAX must be rejected before the cast",
);
}
#[test]
fn rgba_stride_above_i32_max_is_rejected() {
let data = vec![0u8; 16];
let r = Encoder::new_rgba_stride(&data, 1, 1, NEGATIVE_BOUNDARY).encode(Unstoppable);
assert!(
r.is_err(),
"RGBA stride > i32::MAX must be rejected before the cast",
);
}
#[test]
fn yuv_stride_above_i32_max_is_rejected() {
if usize::BITS < 64 {
return;
}
let big_stride = (i32::MAX as usize) + 1;
let y = vec![0u8; big_stride * 2];
let u = vec![0u8; 1];
let v = vec![0u8; 1];
let planes = YuvPlanesRef {
y: &y,
y_stride: big_stride,
u: &u,
u_stride: 1,
v: &v,
v_stride: 1,
a: None,
a_stride: 0,
width: 2,
height: 2,
};
let r = Encoder::new_yuv(planes).encode(Unstoppable);
assert!(
r.is_err(),
"Y stride > i32::MAX must be rejected by validate_yuv_planes",
);
}
#[cfg(feature = "streaming")]
#[cfg(all(feature = "streaming", feature = "decode"))]
#[test]
fn streaming_with_buffer_rejects_stride_above_i32_max() {
let mut buf = vec![0u8; 64];
let r = StreamingDecoder::with_buffer(&mut buf, (i32::MAX as usize) + 1, ColorMode::Rgba);
assert!(
r.is_err(),
"StreamingDecoder::with_buffer must reject stride > i32::MAX",
);
}
}
#[cfg(all(feature = "streaming", feature = "decode"))]
#[test]
fn streaming_decoder_with_buffer_safe_usage() {
let width = 16;
let height = 16;
let rgba = rgba_fixture(width, height);
let webp = encode_lossless_rgba(&rgba, width, height);
let stride = (width * 4) as usize;
let mut output = vec![0u8; stride * height as usize];
let mut decoder = StreamingDecoder::with_buffer(&mut output, stride, ColorMode::Rgba)
.expect("streaming decoder with external buffer");
decoder.append(&webp).expect("streaming decode append");
let (decoded, decoded_width, decoded_height) =
decoder.finish().expect("finish on caller-owned buffer");
assert_eq!((decoded_width, decoded_height), (width, height));
assert_eq!(decoded, rgba);
}
#[cfg(all(feature = "streaming", feature = "decode"))]
#[test]
fn streaming_get_partial_respects_external_buffer_minimum_extent() {
let width = 1;
let height = 2;
let rgba = rgba_fixture(width, height);
let webp = encode_lossless_rgba(&rgba, width, height);
let stride = 8;
let row_bytes = width as usize * 4;
let expected_partial_len = stride * (height as usize - 1) + row_bytes;
let mut output = vec![0xaa; expected_partial_len];
let mut decoder = StreamingDecoder::with_buffer(&mut output, stride, ColorMode::Rgba)
.expect("streaming decoder with tightly-sized external buffer");
decoder.append(&webp).expect("streaming decode append");
let (partial, partial_width, partial_rows) = decoder
.get_partial()
.expect("complete decode should be visible");
assert_eq!((partial_width, partial_rows), (width, height));
assert_eq!(partial.len(), expected_partial_len);
assert_eq!(partial[partial.len() - 1], rgba[rgba.len() - 1]);
let (decoded, decoded_width, decoded_height) =
decoder.finish().expect("finish on caller-owned buffer");
assert_eq!((decoded_width, decoded_height), (width, height));
assert_eq!(decoded, rgba);
}
#[cfg(all(feature = "streaming", feature = "decode"))]
#[test]
fn streaming_decoder_new_rejects_yuv_modes() {
for mode in [ColorMode::Yuv420, ColorMode::Yuva420] {
let r = StreamingDecoder::new(mode);
match r {
Err(at) => {
let (err, _) = at.decompose();
assert!(
matches!(err, Error::InvalidInput(_)),
"{:?} must surface InvalidInput, not OutOfMemory",
mode,
);
}
Ok(_) => panic!("StreamingDecoder::new accepted {:?}", mode),
}
}
}
#[test]
fn encoder_constructors_dont_panic_on_huge_width() {
let small = [0u8; 16];
let _e1 = Encoder::new_rgba(&small, u32::MAX, 1);
let _e2 = Encoder::new_bgra(&small, u32::MAX, 1);
let _e3 = Encoder::new_rgb(&small, u32::MAX, 1);
let _e4 = Encoder::new_bgr(&small, u32::MAX, 1);
let r = Encoder::new_rgba(&small, 1_073_741_824, 1).encode(Unstoppable);
assert!(r.is_err(), "huge width must produce a clean error");
}
#[test]
fn from_pixels_stride_with_huge_pixel_stride_rejected() {
use rgb::RGBA8;
let pixels: Vec<RGBA8> = vec![RGBA8::new(0, 0, 0, 0); 16];
let huge_pixel_stride: u32 = (u32::MAX / 4) + 1;
let r = Encoder::from_pixels_stride(&pixels, 1, 1, huge_pixel_stride).encode(Unstoppable);
assert!(
r.is_err(),
"from_pixels_stride must reject pixel-strides that overflow u32::MAX when scaled by bpp",
);
}
#[test]
fn yuv_planes_new_checked_rejects_out_of_range_dimensions() {
assert!(YuvPlanes::new_checked(u32::MAX, 1, false).is_none());
assert!(YuvPlanes::new_checked(1, u32::MAX, false).is_none());
assert!(YuvPlanes::new_checked(u32::MAX, u32::MAX, true).is_none());
assert!(YuvPlanes::new_checked(64, 48, false).is_some());
}
#[cfg(feature = "icc")]
#[test]
fn metadata_get_with_tight_max_metadata_bytes_rejects_oversize_chunk() {
let rgba = vec![255u8; 4 * 4 * 4];
let webp = Encoder::new_rgba(&rgba, 4, 4)
.quality(80.0)
.encode(Unstoppable)
.expect("encode");
let icc = vec![0xa5u8; 1024];
let webp_with_icc = webpx::embed_icc(&webp, &icc).expect("embed icc");
let tight = Limits::none().with_max_metadata_bytes(512);
let r = webpx::get_icc_profile_with_limits(&webp_with_icc, &tight);
match r {
Err(at) => {
let (err, _) = at.decompose();
assert!(
matches!(err, Error::LimitExceeded(_)),
"max_metadata_bytes overflow must surface LimitExceeded, got {:?}",
err
);
}
Ok(_) => panic!("max_metadata_bytes=512 accepted a 1 KiB ICC chunk"),
}
let loose = Limits::none().with_max_metadata_bytes(4096);
let icc_back = webpx::get_icc_profile_with_limits(&webp_with_icc, &loose)
.expect("loose cap should succeed")
.expect("ICC chunk must be present");
assert_eq!(icc_back, icc);
}
#[cfg(feature = "animation")]
#[test]
fn animation_decoder_max_frames_rejects_huge_declared_frame_count() {
let mut frame_a = vec![0u8; 8 * 8 * 4];
for px in frame_a.chunks_mut(4) {
px.copy_from_slice(&[255, 0, 0, 255]);
}
let mut frame_b = vec![0u8; 8 * 8 * 4];
for px in frame_b.chunks_mut(4) {
px.copy_from_slice(&[0, 255, 0, 255]);
}
let mut frame_c = vec![0u8; 8 * 8 * 4];
for px in frame_c.chunks_mut(4) {
px.copy_from_slice(&[0, 0, 255, 255]);
}
let mut enc = AnimationEncoder::new(8, 8).expect("encoder");
enc.add_frame_rgba(&frame_a, 0).expect("add frame 0");
enc.add_frame_rgba(&frame_b, 100).expect("add frame 1");
enc.add_frame_rgba(&frame_c, 200).expect("add frame 2");
let webp = enc.finish(300).expect("finish");
let tight = Limits::none().with_max_frames(2);
let r = AnimationDecoder::with_options_limits(&webp, ColorMode::Rgba, true, &tight);
match r {
Err(at) => {
let (err, _) = at.decompose();
assert!(
matches!(err, Error::LimitExceeded(_)),
"max_frames=2 must surface LimitExceeded for a 3-frame animation, got {:?}",
err
);
}
Ok(_) => panic!("max_frames=2 accepted a 3-frame animation"),
}
}
#[cfg(feature = "animation")]
#[test]
fn animation_decoder_max_animation_ms_rejects_long_animations() {
let mut frame_a = vec![0u8; 8 * 8 * 4];
for px in frame_a.chunks_mut(4) {
px.copy_from_slice(&[200, 50, 50, 255]);
}
let mut frame_b = vec![0u8; 8 * 8 * 4];
for px in frame_b.chunks_mut(4) {
px.copy_from_slice(&[50, 200, 50, 255]);
}
let mut frame_c = vec![0u8; 8 * 8 * 4];
for px in frame_c.chunks_mut(4) {
px.copy_from_slice(&[50, 50, 200, 255]);
}
let mut enc = AnimationEncoder::new(8, 8).expect("encoder");
enc.add_frame_rgba(&frame_a, 0).expect("add frame 0");
enc.add_frame_rgba(&frame_b, 5_000).expect("add frame 1");
enc.add_frame_rgba(&frame_c, 60_000).expect("add frame 2");
let webp = enc.finish(120_000).expect("finish");
let tight = Limits::none().with_max_animation_ms(30_000);
let mut dec = AnimationDecoder::with_options_limits(&webp, ColorMode::Rgba, true, &tight)
.expect("decoder construction succeeds; max_animation_ms is checked in decode_all");
let r = dec.decode_all();
match r {
Err(at) => {
let (err, _) = at.decompose();
assert!(
matches!(err, Error::LimitExceeded(_)),
"max_animation_ms=30s must reject a 120s animation, got {:?}",
err
);
}
Ok(_) => panic!("max_animation_ms=30s accepted a 120s animation"),
}
}
#[cfg(feature = "decode")]
#[test]
fn decoder_max_pixels_rejects_oversize_canvas() {
let rgba = vec![255u8; 64 * 64 * 4];
let webp = Encoder::new_rgba(&rgba, 64, 64)
.quality(80.0)
.encode(Unstoppable)
.expect("encode");
let tight = Limits::none().with_max_pixels(1024);
let cfg = DecoderConfig::new().limits(tight);
let dec = Decoder::new(&webp).expect("decoder").config(cfg);
let r = dec.decode_rgba();
match r {
Err(at) => {
let (err, _) = at.decompose();
assert!(
matches!(err, Error::LimitExceeded(_)),
"max_pixels=1024 must reject a 4096-pixel image, got {:?}",
err
);
}
Ok(_) => panic!("max_pixels=1024 accepted a 4096-pixel image"),
}
}