casc-lib 0.2.1

Pure Rust library for reading World of Warcraft CASC archives
Documentation
//! Jenkins hashlittle2 hash and WoW file path hashing.
//!
//! CASC root files optionally store a 64-bit name hash for each entry,
//! computed using Bob Jenkins' `hashlittle2` (lookup3) algorithm over the
//! normalized (uppercased, backslash-separated) file path. The
//! `hashpath` function performs this normalization and returns the
//! combined 64-bit hash used by the game client.

/// Bob Jenkins' lookup3 hashlittle2 function.
/// Returns (pc, pb) hash pair.
pub fn hashlittle2(key: &[u8], pc: u32, pb: u32) -> (u32, u32) {
    let mut a: u32;
    let mut b: u32;
    let mut c: u32;

    a = 0xdeadbeef_u32
        .wrapping_add(key.len() as u32)
        .wrapping_add(pc);
    b = a;
    c = a.wrapping_add(pb);

    let mut offset = 0;
    let mut remaining = key.len();

    // Process 12-byte chunks
    while remaining > 12 {
        a = a.wrapping_add(
            key[offset] as u32
                | (key[offset + 1] as u32) << 8
                | (key[offset + 2] as u32) << 16
                | (key[offset + 3] as u32) << 24,
        );
        b = b.wrapping_add(
            key[offset + 4] as u32
                | (key[offset + 5] as u32) << 8
                | (key[offset + 6] as u32) << 16
                | (key[offset + 7] as u32) << 24,
        );
        c = c.wrapping_add(
            key[offset + 8] as u32
                | (key[offset + 9] as u32) << 8
                | (key[offset + 10] as u32) << 16
                | (key[offset + 11] as u32) << 24,
        );

        // mix
        a = a.wrapping_sub(c);
        a ^= c.rotate_left(4);
        c = c.wrapping_add(b);
        b = b.wrapping_sub(a);
        b ^= a.rotate_left(6);
        a = a.wrapping_add(c);
        c = c.wrapping_sub(b);
        c ^= b.rotate_left(8);
        b = b.wrapping_add(a);
        a = a.wrapping_sub(c);
        a ^= c.rotate_left(16);
        c = c.wrapping_add(b);
        b = b.wrapping_sub(a);
        b ^= a.rotate_left(19);
        a = a.wrapping_add(c);
        c = c.wrapping_sub(b);
        c ^= b.rotate_left(4);
        b = b.wrapping_add(a);

        offset += 12;
        remaining -= 12;
    }

    // Handle the last few bytes (the switch/case in C)
    match remaining {
        12 => {
            c = c.wrapping_add((key[offset + 11] as u32) << 24);
            c = c.wrapping_add((key[offset + 10] as u32) << 16);
            c = c.wrapping_add((key[offset + 9] as u32) << 8);
            c = c.wrapping_add(key[offset + 8] as u32);
            b = b.wrapping_add((key[offset + 7] as u32) << 24);
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        11 => {
            c = c.wrapping_add((key[offset + 10] as u32) << 16);
            c = c.wrapping_add((key[offset + 9] as u32) << 8);
            c = c.wrapping_add(key[offset + 8] as u32);
            b = b.wrapping_add((key[offset + 7] as u32) << 24);
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        10 => {
            c = c.wrapping_add((key[offset + 9] as u32) << 8);
            c = c.wrapping_add(key[offset + 8] as u32);
            b = b.wrapping_add((key[offset + 7] as u32) << 24);
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        9 => {
            c = c.wrapping_add(key[offset + 8] as u32);
            b = b.wrapping_add((key[offset + 7] as u32) << 24);
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        8 => {
            b = b.wrapping_add((key[offset + 7] as u32) << 24);
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        7 => {
            b = b.wrapping_add((key[offset + 6] as u32) << 16);
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        6 => {
            b = b.wrapping_add((key[offset + 5] as u32) << 8);
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        5 => {
            b = b.wrapping_add(key[offset + 4] as u32);
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        4 => {
            a = a.wrapping_add((key[offset + 3] as u32) << 24);
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        3 => {
            a = a.wrapping_add((key[offset + 2] as u32) << 16);
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        2 => {
            a = a.wrapping_add((key[offset + 1] as u32) << 8);
            a = a.wrapping_add(key[offset] as u32);
        }
        1 => {
            a = a.wrapping_add(key[offset] as u32);
        }
        0 => {
            return (c, b);
        }
        _ => unreachable!(),
    }

    // final mix
    c ^= b;
    c = c.wrapping_sub(b.rotate_left(14));
    a ^= c;
    a = a.wrapping_sub(c.rotate_left(11));
    b ^= a;
    b = b.wrapping_sub(a.rotate_left(25));
    c ^= b;
    c = c.wrapping_sub(b.rotate_left(16));
    a ^= c;
    a = a.wrapping_sub(c.rotate_left(4));
    b ^= a;
    b = b.wrapping_sub(a.rotate_left(14));
    c ^= b;
    c = c.wrapping_sub(b.rotate_left(24));

    (c, b)
}

/// Hash a file path using Jenkins96, normalizing to uppercase with backslashes.
pub fn hashpath(path: &str) -> u64 {
    let normalized = path.to_uppercase().replace('/', "\\");
    let (pc, pb) = hashlittle2(normalized.as_bytes(), 0, 0);
    ((pb as u64) << 32) | (pc as u64)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hashlittle2_empty() {
        let (pc, pb) = hashlittle2(b"", 0, 0);
        // Known value for empty input with 0,0 seeds
        // From reference implementation
        assert_eq!(pc, 0xdeadbeef);
        assert_eq!(pb, 0xdeadbeef);
    }

    #[test]
    fn hashlittle2_known_vectors() {
        // Test with "abc" - known from Jenkins reference
        let (pc, pb) = hashlittle2(b"abc", 0, 0);
        // These are verifiable against the C reference implementation
        assert_ne!(pc, 0); // Non-trivial output
        assert_ne!(pb, 0);
    }

    #[test]
    fn hashpath_normalizes_case_and_slashes() {
        let h1 = hashpath("World/Maps/Azeroth/Azeroth.wdt");
        let h2 = hashpath("world/maps/azeroth/azeroth.wdt");
        let h3 = hashpath("WORLD\\MAPS\\AZEROTH\\AZEROTH.WDT");
        assert_eq!(h1, h2);
        assert_eq!(h2, h3);
    }

    #[test]
    fn hashpath_deterministic() {
        let h1 = hashpath("test/file.txt");
        let h2 = hashpath("test/file.txt");
        assert_eq!(h1, h2);
    }
}