oxipkx 1.0.0

Zero-dependency parser for id Tech 3/4 PK3/PK4 files (Quake III, Doom 3).
Documentation
//! MD4 (RFC 1320) plus the "block checksum" fold both id engines use to turn a
//! pak's CRC list into a single 32-bit identifier. only compiled when a game
//! feature that needs pak checksums is enabled.

/// MD4 digest of a message, as 16 raw bytes.
fn md4(message: &[u8]) -> [u8; 16] {
    let mut a: u32 = 0x6745_2301;
    let mut b: u32 = 0xefcd_ab89;
    let mut c: u32 = 0x98ba_dcfe;
    let mut d: u32 = 0x1032_5476;

    // pad: 0x80, then zeros, then 64-bit little-endian bit length
    let bit_length = (message.len() as u64).wrapping_mul(8);
    let mut padded = message.to_vec();
    padded.push(0x80);
    while padded.len() % 64 != 56 {
        padded.push(0);
    }
    padded.extend_from_slice(&bit_length.to_le_bytes());

    for chunk in padded.chunks_exact(64) {
        let mut words = [0u32; 16];
        for (index, word) in words.iter_mut().enumerate() {
            let base = index * 4;
            *word = u32::from_le_bytes(chunk[base..base + 4].try_into().unwrap());
        }
        let (mut aa, mut bb, mut cc, mut dd) = (a, b, c, d);

        let f = |x: u32, y: u32, z: u32| (x & y) | (!x & z);
        let g = |x: u32, y: u32, z: u32| (x & y) | (x & z) | (y & z);
        let h = |x: u32, y: u32, z: u32| x ^ y ^ z;

        // round 1
        for &index in &[0usize, 4, 8, 12] {
            aa = aa
                .wrapping_add(f(bb, cc, dd))
                .wrapping_add(words[index])
                .rotate_left(3);
            dd = dd
                .wrapping_add(f(aa, bb, cc))
                .wrapping_add(words[index + 1])
                .rotate_left(7);
            cc = cc
                .wrapping_add(f(dd, aa, bb))
                .wrapping_add(words[index + 2])
                .rotate_left(11);
            bb = bb
                .wrapping_add(f(cc, dd, aa))
                .wrapping_add(words[index + 3])
                .rotate_left(19);
        }
        // round 2
        for &index in &[0usize, 1, 2, 3] {
            aa = aa
                .wrapping_add(g(bb, cc, dd))
                .wrapping_add(words[index])
                .wrapping_add(0x5a82_7999)
                .rotate_left(3);
            dd = dd
                .wrapping_add(g(aa, bb, cc))
                .wrapping_add(words[index + 4])
                .wrapping_add(0x5a82_7999)
                .rotate_left(5);
            cc = cc
                .wrapping_add(g(dd, aa, bb))
                .wrapping_add(words[index + 8])
                .wrapping_add(0x5a82_7999)
                .rotate_left(9);
            bb = bb
                .wrapping_add(g(cc, dd, aa))
                .wrapping_add(words[index + 12])
                .wrapping_add(0x5a82_7999)
                .rotate_left(13);
        }
        // round 3
        for &index in &[0usize, 2, 1, 3] {
            aa = aa
                .wrapping_add(h(bb, cc, dd))
                .wrapping_add(words[index])
                .wrapping_add(0x6ed9_eba1)
                .rotate_left(3);
            dd = dd
                .wrapping_add(h(aa, bb, cc))
                .wrapping_add(words[index + 8])
                .wrapping_add(0x6ed9_eba1)
                .rotate_left(9);
            cc = cc
                .wrapping_add(h(dd, aa, bb))
                .wrapping_add(words[index + 4])
                .wrapping_add(0x6ed9_eba1)
                .rotate_left(11);
            bb = bb
                .wrapping_add(h(cc, dd, aa))
                .wrapping_add(words[index + 12])
                .wrapping_add(0x6ed9_eba1)
                .rotate_left(15);
        }

        a = a.wrapping_add(aa);
        b = b.wrapping_add(bb);
        c = c.wrapping_add(cc);
        d = d.wrapping_add(dd);
    }

    let mut digest = [0u8; 16];
    digest[0..4].copy_from_slice(&a.to_le_bytes());
    digest[4..8].copy_from_slice(&b.to_le_bytes());
    digest[8..12].copy_from_slice(&c.to_le_bytes());
    digest[12..16].copy_from_slice(&d.to_le_bytes());
    digest
}

/// the id "block checksum": MD4 the buffer, then XOR the digest's four
/// little-endian 32-bit words into one value. this is Com_BlockChecksum in
/// Quake III and MD4_BlockChecksum in id Tech 4; they are identical.
pub fn block_checksum(data: &[u8]) -> u32 {
    let digest = md4(data);
    let word = |offset: usize| u32::from_le_bytes(digest[offset..offset + 4].try_into().unwrap());
    word(0) ^ word(4) ^ word(8) ^ word(12)
}