quake-util 0.4.0

A utility library for using Quake file formats
Documentation
use crate::error;
use crate::lump::{kind, Lump};
use crate::wad;
use std::io::Cursor;
use std::iter::repeat;
use std::mem::{size_of, size_of_val};
use std::vec::Vec;
use wad::repr::Head;

fn image_bytes() -> Vec<u8> {
    let width: u32 = 64;
    let height: u32 = 128;
    let pix_ct: usize = (width * height).try_into().unwrap();

    let mut image = Vec::new();
    image.extend(width.to_le_bytes());
    image.extend(height.to_le_bytes());
    image.extend(repeat(0u8).take(pix_ct));

    image
}

fn miptex_bytes(name: [u8; 16]) -> Vec<u8> {
    let width: u32 = 512;
    let height: u32 = 32;
    let mip0_sz = width * height;
    let mut miptex = Vec::new();

    miptex.extend(name);
    miptex.extend(width.to_le_bytes());
    miptex.extend(height.to_le_bytes());

    let mut offset: u32 = miptex.len().try_into().unwrap();

    for i in 0..4 {
        miptex.extend(offset.to_le_bytes());
        let mip_sz = mip0_sz >> (2 * i);
        offset += mip_sz;
    }

    for i in 0..4 {
        let mip_sz = mip0_sz >> (2 * i);
        miptex.extend(repeat(0u8).take(mip_sz.try_into().unwrap()));
    }

    miptex
}

fn palette_bytes() -> Vec<u8> {
    let mut palette = Vec::new();

    for i in 0..768i32 {
        palette.push((i & 0xff).try_into().unwrap());
    }

    palette
}

fn entry_bytes(offset: u32, length: u32, kind: u8, name: [u8; 16]) -> Vec<u8> {
    let mut entry = Vec::new();

    entry.extend(offset.to_le_bytes());
    entry.extend(length.to_le_bytes());
    entry.extend(length.to_le_bytes());
    entry.push(kind);
    entry.push(0u8);
    entry.extend([0; 2]);
    entry.extend(name);

    entry
}

fn compressed_entry_bytes(offset: u32) -> Vec<u8> {
    let mut entry = Vec::new();

    entry.extend(offset.to_le_bytes());
    entry.extend([0; 8]);
    entry.push(kind::FLAT);
    entry.push(1u8);
    entry.extend([0; 2]);
    entry.extend(b"compressed\0\0\0\0\0\0");

    entry
}

fn duplicate_entry_wad_bytes() -> Vec<u8> {
    let mut wad = Vec::new();
    let entry_count = 2u32;
    let directory_offset = 12u32;
    let name = *b"same_name\0\0\0\0\0\0\0";

    wad.extend(b"WAD2");
    wad.extend(entry_count.to_le_bytes());
    wad.extend(directory_offset.to_le_bytes());

    let entry1 =
        entry_bytes(wad.len().try_into().unwrap(), 0, kind::FLAT, name);
    wad.extend(entry1);

    let entry2 =
        entry_bytes(wad.len().try_into().unwrap(), 0, kind::FLAT, name);
    wad.extend(entry2);

    wad
}

fn good_wad_bytes() -> Vec<u8> {
    let mut wad = Vec::new();
    let image_name = *b"image\0\0\0\0\0\0\0\0\0\0\0";
    let miptex_name = *b"miptex\0\0\0\0\0\0\0\0\0\0";
    let palette_name = *b"palette\0\0\0\0\0\0\0\0\0";
    let flat_name = *b"flat\0\0\0\0\0\0\0\0\0\0\0\0";
    let conchars_name = *b"CONCHARS\0\0\0\0\0\0\0\0";
    let entry_count: u32 = 5;
    let image = image_bytes();
    let miptex = miptex_bytes(miptex_name);
    let palette = palette_bytes();
    let flat = [0u8; 123];
    let conchars = [0u8; 128 * 128];
    let directory_offset: u32 = (image.len()
        + miptex.len()
        + palette.len()
        + flat.len()
        + conchars.len()
        + size_of::<Head>())
    .try_into()
    .unwrap();

    wad.extend(b"WAD2");
    wad.extend(entry_count.to_le_bytes());
    wad.extend(directory_offset.to_le_bytes());

    let image_offset: u32 = wad.len().try_into().unwrap();
    wad.extend(&image);

    let miptex_offset: u32 = wad.len().try_into().unwrap();
    wad.extend(&miptex);

    let palette_offset: u32 = wad.len().try_into().unwrap();
    wad.extend(&palette);

    let flat_offset: u32 = wad.len().try_into().unwrap();
    wad.extend(&flat);

    let conchars_offset: u32 = wad.len().try_into().unwrap();
    wad.extend(&conchars);

    wad.extend(entry_bytes(
        image_offset,
        image.len().try_into().unwrap(),
        kind::SBAR,
        image_name,
    ));
    wad.extend(entry_bytes(
        miptex_offset,
        miptex.len().try_into().unwrap(),
        kind::MIPTEX,
        miptex_name,
    ));
    wad.extend(entry_bytes(
        palette_offset,
        palette.len().try_into().unwrap(),
        kind::PALETTE,
        palette_name,
    ));
    wad.extend(entry_bytes(
        flat_offset,
        flat.len().try_into().unwrap(),
        kind::FLAT,
        flat_name,
    ));
    wad.extend(entry_bytes(
        conchars_offset,
        conchars.len().try_into().unwrap(),
        kind::MIPTEX,
        conchars_name,
    ));

    wad
}

#[test]
fn parse_good_wad() {
    let mut wad_file = Cursor::new(good_wad_bytes());
    let (mut parser, warnings) = wad::Parser::new(&mut wad_file).unwrap();
    let dir = parser.directory();
    let panic_dir = || panic!("{:?}", dir);
    let image_entry = dir.get("image").unwrap_or_else(panic_dir);
    let miptex_entry = dir.get("miptex").unwrap_or_else(panic_dir);
    let palette_entry = dir.get("palette").unwrap_or_else(panic_dir);
    let flat_entry = dir.get("flat").unwrap_or_else(panic_dir);
    let conchars_entry = dir.get("CONCHARS").unwrap_or_else(panic_dir);

    assert_eq!(warnings.len(), 0);
    assert_eq!(image_entry.kind(), kind::SBAR);
    assert_eq!(miptex_entry.kind(), kind::MIPTEX);
    assert_eq!(palette_entry.kind(), kind::PALETTE);
    assert_eq!(flat_entry.kind(), kind::FLAT);
    assert_eq!(conchars_entry.kind(), kind::MIPTEX);

    {
        let image_lump = parser.parse_image(image_entry).unwrap();
        let (width, height) = (image_lump.width(), image_lump.height());
        assert_eq!(width, 64);
        assert_eq!(height, 128);
        assert_eq!(
            width * height,
            image_lump.pixels().len().try_into().unwrap()
        );
    }

    {
        let miptex_lump = parser.parse_mip_texture(miptex_entry).unwrap();
        assert_eq!(miptex_lump.mip(0).width(), 512);
        assert_eq!(miptex_lump.mip(1).width(), 256);
        assert_eq!(miptex_lump.mip(2).width(), 128);
        assert_eq!(miptex_lump.mip(3).width(), 64);
        assert_eq!(miptex_lump.mip(0).height(), 32);
        assert_eq!(miptex_lump.mip(1).height(), 16);
        assert_eq!(miptex_lump.mip(2).height(), 8);
        assert_eq!(miptex_lump.mip(3).height(), 4);
    }

    {
        let palette_lump = parser.parse_palette(palette_entry).unwrap();
        let mut val = 0u8;

        for i in 0..256usize {
            assert_eq!(
                (*palette_lump)[i],
                [val, val.wrapping_add(1), val.wrapping_add(2)]
            );
            val = val.wrapping_add(3);
        }
    }

    {
        let flat_lump = parser.read_raw(flat_entry).unwrap();
        assert_eq!(flat_lump.len(), 123);
    }

    for (entry_name, entry) in dir {
        assert!(match &entry_name[..] {
            "image" => matches!(
                parser.parse_inferred(&entry).unwrap(),
                Lump::StatusBar(_)
            ),
            "miptex" => matches!(
                parser.parse_inferred(&entry).unwrap(),
                Lump::MipTexture(_)
            ),
            "palette" => matches!(
                parser.parse_inferred(&entry).unwrap(),
                Lump::Palette(_)
            ),
            "flat" =>
                matches!(parser.parse_inferred(&entry).unwrap(), Lump::Flat(_)),
            "CONCHARS" =>
                matches!(parser.parse_inferred(&entry).unwrap(), Lump::Flat(_)),
            name => panic!("Unexpected entry name `{name}`"),
        });
    }
}

#[test]
fn parse_duplicate_entry() {
    let mut wad_file = Cursor::new(duplicate_entry_wad_bytes());
    let (_, warnings) = wad::Parser::new(&mut wad_file).unwrap();

    assert_eq!(warnings.len(), 1);
}

#[test]
fn parse_bad_magic_wad() {
    let mut wad_file = Cursor::new(b"WART\0\0\0\0\0\0\0\0");
    let e = wad::Parser::new(&mut wad_file).unwrap_err();

    assert!(matches!(e, error::BinParse::Parse(_)));
}

#[test]
fn parse_bad_short_wad() {
    let mut wad_file = Cursor::new(b"WAD2");
    let e = wad::Parser::new(&mut wad_file).unwrap_err();

    assert!(matches!(e, error::BinParse::Io(_)));
}

#[test]
fn parse_bad_directory() {
    let mut wad_file = Cursor::new(b"WAD2\x01\0\0\0\0\0\0\0");
    let e = wad::Parser::new(&mut wad_file).unwrap_err();

    assert!(matches!(e, error::BinParse::Io(_)));
}

#[test]
fn parse_bad_compression_entry() {
    let mut wad = Vec::<u8>::new();
    wad.extend(b"WAD2\x01\0\0\0\x0c\0\0\0");
    wad.extend(compressed_entry_bytes(12));
    let mut wad_file = Cursor::new(wad);
    let e = wad::Parser::new(&mut wad_file).unwrap_err();

    assert!(matches!(e, error::BinParse::Parse(_)));
}

#[test]
fn parse_bad_lumps() {
    for kind in [kind::SBAR, kind::MIPTEX, kind::PALETTE, kind::FLAT] {
        let mut wad = Vec::<u8>::new();
        let bad_lump = b"\xBA\xDB\xAD";
        wad.extend(b"WAD2");
        wad.extend((1u32).to_le_bytes());

        wad.extend(
            <u32>::try_from(size_of::<Head>() + size_of_val(bad_lump))
                .unwrap()
                .to_le_bytes(),
        );

        wad.extend(bad_lump);

        wad.extend(entry_bytes(
            wad.len().try_into().unwrap(),
            666,
            kind,
            *b"BAD_BAD_BAD_BAD\0",
        ));

        let mut wad_file = Cursor::new(wad);
        let (mut parser, _) = wad::Parser::new(&mut wad_file).unwrap();
        let dir = parser.directory();
        let entry = dir.get("BAD_BAD_BAD_BAD").unwrap();

        assert!(matches!(
            parser.parse_inferred(entry),
            Err(error::BinParse::Io(_))
        ));
    }
}