use rustydemon_blp2::{BlpError, BlpFile, ColorEncoding};
mod offsets {
pub const FORMAT_VERSION: usize = 4;
pub const COLOR_ENCODING: usize = 8;
pub const MIP_OFFSET_0: usize = 20;
pub const MIP_SIZE_0: usize = 84;
}
fn blp2_header(color_enc: u8, alpha_size: u8, pf: u8, width: i32, height: i32) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(b"BLP2");
v.extend_from_slice(&1u32.to_le_bytes()); v.push(color_enc);
v.push(alpha_size);
v.push(pf);
v.push(0u8); v.extend_from_slice(&width.to_le_bytes());
v.extend_from_slice(&height.to_le_bytes());
v
}
fn append_mip_tables(v: &mut Vec<u8>, first_offset: u32, first_size: u32) {
v.extend_from_slice(&first_offset.to_le_bytes());
for _ in 1..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
v.extend_from_slice(&first_size.to_le_bytes());
for _ in 1..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
}
fn make_blp2_palette(
width: i32,
height: i32,
alpha_size: u8,
palette_bgra: &[[u8; 4]; 256],
mip_data: Vec<u8>,
) -> Vec<u8> {
const DATA_OFFSET: u32 = 1172;
let mut v = blp2_header(1 , alpha_size, 0, width, height);
append_mip_tables(&mut v, DATA_OFFSET, mip_data.len() as u32);
for entry in palette_bgra {
v.extend_from_slice(entry);
}
assert_eq!(v.len(), 1172);
v.extend_from_slice(&mip_data);
v
}
fn palette_with_one(idx: u8, bgra: [u8; 4]) -> [[u8; 4]; 256] {
let mut p = [[0u8; 4]; 256];
p[idx as usize] = bgra;
p
}
fn make_blp2_dxt(width: i32, height: i32, alpha_size: u8, pf: u8, mip_data: Vec<u8>) -> Vec<u8> {
const DATA_OFFSET: u32 = 148;
let mut v = blp2_header(2 , alpha_size, pf, width, height);
append_mip_tables(&mut v, DATA_OFFSET, mip_data.len() as u32);
v.extend_from_slice(&mip_data);
v
}
fn make_blp2_argb8888(width: i32, height: i32, mip_data: Vec<u8>) -> Vec<u8> {
const DATA_OFFSET: u32 = 148;
let mut v = blp2_header(3 , 8, 2, width, height);
append_mip_tables(&mut v, DATA_OFFSET, mip_data.len() as u32);
v.extend_from_slice(&mip_data);
v
}
fn dxt1_solid_block(r: u8, g: u8, b: u8) -> [u8; 8] {
let r5 = (r >> 3) as u16;
let g6 = (g >> 2) as u16;
let b5 = (b >> 3) as u16;
let rgb565: u16 = (r5 << 11) | (g6 << 5) | b5;
let ep0 = rgb565.max(1); let [lo, hi] = ep0.to_le_bytes();
[lo, hi, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
}
#[test]
fn palette_1x1_red_no_alpha() {
let bgra_red: [u8; 4] = [0x00, 0x00, 0xFF, 0xFF];
let data = make_blp2_palette(1, 1, 0, &palette_with_one(0, bgra_red), vec![0x00]);
let blp = BlpFile::from_bytes(data).unwrap();
assert_eq!(blp.mipmap_count(), 1);
assert_eq!(blp.width, 1);
assert_eq!(blp.height, 1);
assert_eq!(blp.color_encoding, ColorEncoding::Palette);
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (1, 1));
assert_eq!(&pixels, &[255, 0, 0, 255]); }
#[test]
fn palette_2x2_green_alpha8() {
let mut palette = [[0u8; 4]; 256];
palette[1] = [0x00, 0x80, 0x00, 0x00];
let mip_data: Vec<u8> = vec![
1, 1, 1, 1, 0, 64, 128, 255, ];
let data = make_blp2_palette(2, 2, 8, &palette, mip_data);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (2, 2));
assert_eq!(pixels[0..4], [0, 128, 0, 0]); assert_eq!(pixels[4..8], [0, 128, 0, 64]);
assert_eq!(pixels[8..12], [0, 128, 0, 128]);
assert_eq!(pixels[12..16], [0, 128, 0, 255]);
}
#[test]
fn palette_2x2_alpha1() {
let mut palette = [[0u8; 4]; 256];
palette[0] = [0xFF, 0x00, 0x00, 0x00]; let mip_data: Vec<u8> = vec![
0,
0,
0,
0, 0b0000_1010, ];
let data = make_blp2_palette(2, 2, 1, &palette, mip_data);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, _, _) = blp.get_pixels(0).unwrap();
assert_eq!(pixels[3], 0x00); assert_eq!(pixels[7], 0xFF); assert_eq!(pixels[11], 0x00); assert_eq!(pixels[15], 0xFF); }
#[test]
fn palette_4x1_alpha4() {
let mut palette = [[0u8; 4]; 256];
palette[0] = [0x00, 0x00, 0xFF, 0x00]; let mip_data: Vec<u8> = vec![
0, 0, 0, 0, 0xA5, 0xF0, ];
let data = make_blp2_palette(4, 1, 4, &palette, mip_data);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (4, 1));
assert_eq!(pixels[3], 0x50);
assert_eq!(pixels[7], 0xA0);
assert_eq!(pixels[11], 0x00);
assert_eq!(pixels[15], 0xF0);
}
#[test]
fn dxt1_4x4_red() {
let block = dxt1_solid_block(255, 0, 0);
let data = make_blp2_dxt(4, 4, 0, 0 , block.to_vec());
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (4, 4));
for chunk in pixels.chunks_exact(4) {
let r5: u8 = 255 >> 3; let expected_r: u8 = (r5 << 3) | (r5 >> 2); assert_eq!(chunk[0], expected_r, "red channel mismatch");
assert_eq!(chunk[1], 0, "green should be 0");
assert_eq!(chunk[2], 0, "blue should be 0");
assert_eq!(chunk[3], 255, "alpha should be opaque");
}
}
#[test]
fn dxt1_non_power_of_two_2x2() {
let block = dxt1_solid_block(0, 0, 255); let data = make_blp2_dxt(2, 2, 0, 0, block.to_vec());
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (2, 2));
assert_eq!(pixels.len(), 2 * 2 * 4);
for chunk in pixels.chunks_exact(4) {
assert!(chunk[2] > 0, "blue channel should be non-zero");
}
}
#[test]
fn argb8888_1x1_red_bgra_swap() {
let mip_data = vec![0x00u8, 0x00, 0xFF, 0xFF]; let data = make_blp2_argb8888(1, 1, mip_data);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (1, 1));
assert_eq!(&pixels, &[255, 0, 0, 255]); }
#[test]
fn argb8888_2x1_two_colors() {
let mip_data = vec![
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, ];
let data = make_blp2_argb8888(2, 1, mip_data);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (2, 1));
assert_eq!(&pixels[0..4], &[255, 0, 0, 255]); assert_eq!(&pixels[4..8], &[0, 0, 255, 255]); }
#[test]
fn mipmap_count_from_offset_table() {
let palette = [[0u8; 4]; 256];
const BASE: u32 = 1172;
let mut v = blp2_header(1 , 0, 0, 2, 2);
v.extend_from_slice(&BASE.to_le_bytes());
v.extend_from_slice(&(BASE + 4).to_le_bytes());
v.extend_from_slice(&(BASE + 5).to_le_bytes());
for _ in 3..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
v.extend_from_slice(&4u32.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
for _ in 3..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
for entry in &palette {
v.extend_from_slice(entry);
}
v.extend_from_slice(&[0u8, 0, 0, 0, 0, 0]);
let blp = BlpFile::from_bytes(v).unwrap();
assert_eq!(blp.mipmap_count(), 3);
}
#[test]
fn mipmap_level_clamped_to_last() {
let bgra_red: [u8; 4] = [0x00, 0x00, 0xFF, 0xFF];
let data = make_blp2_palette(1, 1, 0, &palette_with_one(0, bgra_red), vec![0x00]);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, w, h) = blp.get_pixels(99).unwrap();
assert_eq!((w, h), (1, 1));
assert_eq!(&pixels, &[255, 0, 0, 255]);
}
#[test]
fn blp1_palette_1x1() {
const DATA_OFFSET: u32 = 1180;
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(b"BLP1");
for val in [1i32, 0, 1, 1, 0, 0] {
v.extend_from_slice(&val.to_le_bytes());
}
v.extend_from_slice(&DATA_OFFSET.to_le_bytes());
for _ in 1..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
v.extend_from_slice(&1u32.to_le_bytes());
for _ in 1..16 {
v.extend_from_slice(&0u32.to_le_bytes());
}
v.extend_from_slice(&[0xFF, 0x00, 0x00, 0xFF]);
for _ in 1..256 {
v.extend_from_slice(&[0u8; 4]);
}
v.push(0x00);
let blp = BlpFile::from_bytes(v).unwrap();
assert_eq!(blp.mipmap_count(), 1);
let (pixels, w, h) = blp.get_pixels(0).unwrap();
assert_eq!((w, h), (1, 1));
assert_eq!(&pixels, &[0, 0, 255, 255]);
}
#[test]
fn error_empty_input() {
assert!(BlpFile::from_bytes(vec![]).is_err());
}
#[test]
fn error_three_bytes() {
assert!(BlpFile::from_bytes(vec![b'B', b'L', b'P']).is_err());
}
#[test]
fn error_invalid_magic() {
let data = b"JUNK\x00\x00\x00\x00".to_vec();
assert!(matches!(
BlpFile::from_bytes(data),
Err(BlpError::InvalidMagic)
));
}
#[test]
fn error_blp2_bad_format_version_0() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0]);
data[offsets::FORMAT_VERSION..offsets::FORMAT_VERSION + 4].copy_from_slice(&0u32.to_le_bytes());
assert!(matches!(
BlpFile::from_bytes(data),
Err(BlpError::InvalidFormatVersion(0))
));
}
#[test]
fn error_blp2_bad_format_version_2() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0]);
data[offsets::FORMAT_VERSION..offsets::FORMAT_VERSION + 4].copy_from_slice(&2u32.to_le_bytes());
assert!(matches!(
BlpFile::from_bytes(data),
Err(BlpError::InvalidFormatVersion(2))
));
}
#[test]
fn error_unknown_color_encoding() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0]);
data[offsets::COLOR_ENCODING] = 9; assert!(matches!(
BlpFile::from_bytes(data),
Err(BlpError::UnsupportedEncoding(9))
));
}
#[test]
fn error_truncated_at_format_version() {
assert!(BlpFile::from_bytes(b"BLP2".to_vec()).is_err());
}
#[test]
fn error_truncated_mid_mip_table() {
let mut data = blp2_header(1, 0, 0, 1, 1);
data.extend_from_slice(&[0u8; 10]); assert!(BlpFile::from_bytes(data).is_err());
}
#[test]
fn error_no_mipmaps_all_offsets_zero() {
let palette_bgra: [u8; 4] = [0u8; 4];
let mut data = blp2_header(1, 0, 0, 1, 1);
data.extend_from_slice(&[0u8; 128 + 1024]);
let blp = BlpFile::from_bytes(data).unwrap();
assert_eq!(blp.mipmap_count(), 0);
assert!(matches!(blp.get_pixels(0), Err(BlpError::NoMipmaps)));
let _ = palette_bgra;
}
#[test]
fn security_mip_offset_beyond_eof() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0x00]);
let eof_plus = (data.len() as u32).wrapping_add(1024);
data[offsets::MIP_OFFSET_0..offsets::MIP_OFFSET_0 + 4].copy_from_slice(&eof_plus.to_le_bytes());
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::OutOfBounds)));
}
#[test]
fn security_mip_size_u32_max() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0x00]);
data[offsets::MIP_SIZE_0..offsets::MIP_SIZE_0 + 4].copy_from_slice(&u32::MAX.to_le_bytes());
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::OutOfBounds)));
}
#[test]
fn security_mip_offset_plus_size_wraps() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0x00]);
let off: u32 = u32::MAX - 1;
let sz: u32 = 4;
data[offsets::MIP_OFFSET_0..offsets::MIP_OFFSET_0 + 4].copy_from_slice(&off.to_le_bytes());
data[offsets::MIP_SIZE_0..offsets::MIP_SIZE_0 + 4].copy_from_slice(&sz.to_le_bytes());
let blp = BlpFile::from_bytes(data).unwrap();
let result = blp.get_pixels(0);
assert!(result.is_err());
}
#[test]
fn security_image_too_large_dimensions() {
let data = make_blp2_palette(32768, 32768, 0, &[[0u8; 4]; 256], vec![0x00]);
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::ImageTooLarge)));
}
#[test]
fn security_palette_mip_data_too_short_for_indices() {
let data = make_blp2_palette(4, 4, 0, &[[0u8; 4]; 256], vec![0u8; 4]);
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::DataTooShort)));
}
#[test]
fn security_palette_missing_alpha8_data() {
let data = make_blp2_palette(2, 2, 8, &[[0u8; 4]; 256], vec![0u8; 4]);
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::DataTooShort)));
}
#[test]
fn security_palette_missing_alpha1_data() {
let data = make_blp2_palette(8, 1, 1, &[[0u8; 4]; 256], vec![0u8; 8]);
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::DataTooShort)));
}
#[test]
fn security_palette_missing_alpha4_data() {
let data = make_blp2_palette(4, 1, 4, &[[0u8; 4]; 256], vec![0u8; 4]);
let blp = BlpFile::from_bytes(data).unwrap();
assert!(matches!(blp.get_pixels(0), Err(BlpError::DataTooShort)));
}
#[test]
fn security_dxt_partial_block_no_panic() {
let data = make_blp2_dxt(4, 4, 0, 0, vec![0u8; 4]);
let blp = BlpFile::from_bytes(data).unwrap();
let result = blp.get_pixels(0);
if let Ok((pixels, w, h)) = result {
assert_eq!(pixels.len(), (w * h * 4) as usize);
}
}
#[test]
fn security_dxt5_empty_data_no_panic() {
let data = make_blp2_dxt(4, 4, 8, 7 , vec![]);
let blp = BlpFile::from_bytes(data).unwrap();
let _ = blp.get_pixels(0); }
#[test]
fn security_dxt3_empty_data_no_panic() {
let data = make_blp2_dxt(4, 4, 8, 1 , vec![]);
let blp = BlpFile::from_bytes(data).unwrap();
let _ = blp.get_pixels(0);
}
#[test]
fn security_jpeg_header_size_too_large() {
const DATA_OFFSET: u32 = 148 + 4; let mut v = blp2_header(0 , 0, 0, 1, 1);
append_mip_tables(&mut v, DATA_OFFSET, 0);
let bad_size: i32 = 64 * 1024 + 1;
v.extend_from_slice(&bad_size.to_le_bytes());
assert!(BlpFile::from_bytes(v).is_err());
}
#[test]
fn security_jpeg_header_size_i32_max() {
let mut v = blp2_header(0 , 0, 0, 1, 1);
append_mip_tables(&mut v, 200, 0);
v.extend_from_slice(&i32::MAX.to_le_bytes());
assert!(BlpFile::from_bytes(v).is_err());
}
#[test]
fn security_all_mip_sizes_u32_max() {
let mut data = make_blp2_palette(1, 1, 0, &[[0u8; 4]; 256], vec![0x00]);
for i in 0..16 {
let off = offsets::MIP_SIZE_0 + i * 4;
data[off..off + 4].copy_from_slice(&u32::MAX.to_le_bytes());
}
let blp = BlpFile::from_bytes(data).unwrap();
for level in 0..16u32 {
let _ = blp.get_pixels(level);
}
}
#[test]
fn security_zero_width() {
let data = make_blp2_palette(0, 1, 0, &[[0u8; 4]; 256], vec![0x00]);
let blp = BlpFile::from_bytes(data).unwrap();
let _ = blp.get_pixels(0);
}
#[test]
fn security_palette_data_exactly_n_pixels_no_alpha() {
let mut palette = [[0u8; 4]; 256];
palette[0] = [0x00, 0xFF, 0x00, 0xFF]; let data = make_blp2_palette(2, 2, 0, &palette, vec![0u8; 4]);
let blp = BlpFile::from_bytes(data).unwrap();
let (pixels, _, _) = blp.get_pixels(0).unwrap();
assert_eq!(pixels.len(), 16);
for chunk in pixels.chunks_exact(4) {
assert_eq!(chunk[3], 0xFF, "alpha must be opaque when alpha_size=0");
}
}