zipatch-rs 1.0.0

Parser for FFXIV ZiPatch patch files
Documentation
use super::ApplyContext;
use crate::Platform;
use std::path::PathBuf;

pub(crate) fn expansion_folder_id(id: u16) -> String {
    if id == 0 {
        "ffxiv".to_string()
    } else {
        format!("ex{id}")
    }
}

fn expansion_folder(sub_id: u16) -> String {
    expansion_folder_id(sub_id >> 8)
}

fn platform_str(platform: Platform) -> &'static str {
    // Unknown platforms fall back to win32 paths — best effort so partial
    // applies do not silently misroute to a different folder layout.
    match platform {
        Platform::Win32 | Platform::Unknown(_) => "win32",
        Platform::Ps3 => "ps3",
        Platform::Ps4 => "ps4",
    }
}

#[must_use]
pub(crate) fn dat_path(ctx: &ApplyContext, main_id: u16, sub_id: u16, file_id: u32) -> PathBuf {
    ctx.game_path
        .join("sqpack")
        .join(expansion_folder(sub_id))
        .join(format!(
            "{:02x}{:04x}.{}.dat{}",
            main_id,
            sub_id,
            platform_str(ctx.platform),
            file_id
        ))
}

#[must_use]
pub(crate) fn index_path(ctx: &ApplyContext, main_id: u16, sub_id: u16, file_id: u32) -> PathBuf {
    let suffix = if file_id == 0 {
        String::new()
    } else {
        file_id.to_string()
    };
    ctx.game_path
        .join("sqpack")
        .join(expansion_folder(sub_id))
        .join(format!(
            "{:02x}{:04x}.{}.index{}",
            main_id,
            sub_id,
            platform_str(ctx.platform),
            suffix
        ))
}

#[must_use]
pub(crate) fn generic_path(ctx: &ApplyContext, relative_path: &str) -> PathBuf {
    ctx.game_path.join(relative_path)
}

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

    fn ctx(game: &str) -> ApplyContext {
        ApplyContext::new(game)
    }

    #[test]
    fn dat_expansion_0() {
        let result = dat_path(&ctx("/game"), 0x00, 0x0000, 0);
        assert_eq!(
            result,
            PathBuf::from("/game/sqpack/ffxiv/000000.win32.dat0")
        );
    }

    #[test]
    fn dat_expansion_1() {
        let result = dat_path(&ctx("/game"), 0x04, 0x0100, 2);
        assert_eq!(result, PathBuf::from("/game/sqpack/ex1/040100.win32.dat2"));
    }

    #[test]
    fn index_expansion_0_file_id_0() {
        let result = index_path(&ctx("/game"), 0x00, 0x0000, 0);
        assert_eq!(
            result,
            PathBuf::from("/game/sqpack/ffxiv/000000.win32.index")
        );
    }

    #[test]
    fn index_expansion_0_file_id_1() {
        let result = index_path(&ctx("/game"), 0x00, 0x0000, 1);
        assert_eq!(
            result,
            PathBuf::from("/game/sqpack/ffxiv/000000.win32.index1")
        );
    }

    #[test]
    fn index_expansion_1_file_id_0() {
        let result = index_path(&ctx("/game"), 0x04, 0x0100, 0);
        assert_eq!(result, PathBuf::from("/game/sqpack/ex1/040100.win32.index"));
    }

    #[test]
    fn index_expansion_1_file_id_1() {
        let result = index_path(&ctx("/game"), 0x04, 0x0100, 1);
        assert_eq!(
            result,
            PathBuf::from("/game/sqpack/ex1/040100.win32.index1")
        );
    }

    #[test]
    fn dat_ps3_platform() {
        let mut ctx = ApplyContext::new("/game");
        ctx.platform = Platform::Ps3;
        assert_eq!(
            dat_path(&ctx, 0x00, 0x0000, 0),
            PathBuf::from("/game/sqpack/ffxiv/000000.ps3.dat0")
        );
    }

    #[test]
    fn dat_ps4_platform() {
        let mut ctx = ApplyContext::new("/game");
        ctx.platform = Platform::Ps4;
        assert_eq!(
            dat_path(&ctx, 0x00, 0x0000, 0),
            PathBuf::from("/game/sqpack/ffxiv/000000.ps4.dat0")
        );
    }

    #[test]
    fn dat_expansion_2() {
        // sub_id >> 8 == 2 → "ex2"
        let result = dat_path(&ctx("/game"), 0x08, 0x0200, 0);
        assert_eq!(result, PathBuf::from("/game/sqpack/ex2/080200.win32.dat0"));
    }
}