use super::{ApplyContext, PathCacheKey, PathKind};
use crate::{Platform, Result, ZiPatchError};
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) -> Result<&'static str> {
match platform {
Platform::Win32 => Ok("win32"),
Platform::Ps3 => Ok("ps3"),
Platform::Ps4 => Ok("ps4"),
Platform::Unknown(id) => Err(ZiPatchError::UnsupportedPlatform(id)),
}
}
fn build_dat_path(ctx: &ApplyContext, main_id: u16, sub_id: u16, file_id: u32) -> Result<PathBuf> {
let platform = platform_str(ctx.platform)?;
Ok(ctx
.game_path
.join("sqpack")
.join(expansion_folder(sub_id))
.join(format!("{main_id:02x}{sub_id:04x}.{platform}.dat{file_id}")))
}
pub(crate) fn dat_path(
ctx: &mut ApplyContext,
main_id: u16,
sub_id: u16,
file_id: u32,
) -> Result<PathBuf> {
let key = PathCacheKey {
main_id,
sub_id,
file_id,
kind: PathKind::Dat,
};
if let Some(cached) = ctx.path_cache.get(&key) {
return Ok(cached.clone());
}
let path = build_dat_path(ctx, main_id, sub_id, file_id)?;
ctx.path_cache.insert(key, path.clone());
Ok(path)
}
fn build_index_path(
ctx: &ApplyContext,
main_id: u16,
sub_id: u16,
file_id: u32,
) -> Result<PathBuf> {
let platform = platform_str(ctx.platform)?;
let suffix = if file_id == 0 {
String::new()
} else {
file_id.to_string()
};
Ok(ctx
.game_path
.join("sqpack")
.join(expansion_folder(sub_id))
.join(format!(
"{main_id:02x}{sub_id:04x}.{platform}.index{suffix}"
)))
}
pub(crate) fn index_path(
ctx: &mut ApplyContext,
main_id: u16,
sub_id: u16,
file_id: u32,
) -> Result<PathBuf> {
let key = PathCacheKey {
main_id,
sub_id,
file_id,
kind: PathKind::Index,
};
if let Some(cached) = ctx.path_cache.get(&key) {
return Ok(cached.clone());
}
let path = build_index_path(ctx, main_id, sub_id, file_id)?;
ctx.path_cache.insert(key, path.clone());
Ok(path)
}
#[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 mut c = ctx("/game");
let result = dat_path(&mut c, 0x00, 0x0000, 0).unwrap();
assert_eq!(
result,
PathBuf::from("/game/sqpack/ffxiv/000000.win32.dat0")
);
}
#[test]
fn dat_expansion_1() {
let mut c = ctx("/game");
let result = dat_path(&mut c, 0x04, 0x0100, 2).unwrap();
assert_eq!(result, PathBuf::from("/game/sqpack/ex1/040100.win32.dat2"));
}
#[test]
fn index_expansion_0_file_id_0() {
let mut c = ctx("/game");
let result = index_path(&mut c, 0x00, 0x0000, 0).unwrap();
assert_eq!(
result,
PathBuf::from("/game/sqpack/ffxiv/000000.win32.index")
);
}
#[test]
fn index_expansion_0_file_id_1() {
let mut c = ctx("/game");
let result = index_path(&mut c, 0x00, 0x0000, 1).unwrap();
assert_eq!(
result,
PathBuf::from("/game/sqpack/ffxiv/000000.win32.index1")
);
}
#[test]
fn index_expansion_1_file_id_0() {
let mut c = ctx("/game");
let result = index_path(&mut c, 0x04, 0x0100, 0).unwrap();
assert_eq!(result, PathBuf::from("/game/sqpack/ex1/040100.win32.index"));
}
#[test]
fn index_expansion_1_file_id_1() {
let mut c = ctx("/game");
let result = index_path(&mut c, 0x04, 0x0100, 1).unwrap();
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(&mut ctx, 0x00, 0x0000, 0).unwrap(),
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(&mut ctx, 0x00, 0x0000, 0).unwrap(),
PathBuf::from("/game/sqpack/ffxiv/000000.ps4.dat0")
);
}
#[test]
fn dat_expansion_2() {
let mut c = ctx("/game");
let result = dat_path(&mut c, 0x08, 0x0200, 0).unwrap();
assert_eq!(result, PathBuf::from("/game/sqpack/ex2/080200.win32.dat0"));
}
#[test]
fn dat_path_returns_unsupported_platform_for_unknown() {
let mut c = ApplyContext::new("/game");
c.platform = Platform::Unknown(99);
let err = dat_path(&mut c, 0x00, 0x0000, 0)
.expect_err("unknown platform must abort dat_path resolution");
match err {
ZiPatchError::UnsupportedPlatform(id) => assert_eq!(
id, 99,
"error must carry the raw platform_id for diagnostics"
),
other => panic!("expected UnsupportedPlatform(99), got {other:?}"),
}
}
#[test]
fn index_path_returns_unsupported_platform_for_unknown() {
let mut c = ApplyContext::new("/game");
c.platform = Platform::Unknown(7);
let err = index_path(&mut c, 0x00, 0x0000, 1)
.expect_err("unknown platform must abort index_path resolution");
match err {
ZiPatchError::UnsupportedPlatform(id) => assert_eq!(id, 7),
other => panic!("expected UnsupportedPlatform(7), got {other:?}"),
}
}
#[test]
fn dat_path_populates_and_serves_from_cache() {
let mut c = ctx("/game");
assert!(
c.path_cache.is_empty(),
"fresh ApplyContext starts with an empty path cache"
);
let p1 = dat_path(&mut c, 0x04, 0x0100, 2).unwrap();
assert_eq!(c.path_cache.len(), 1, "first call must populate the cache");
let p2 = dat_path(&mut c, 0x04, 0x0100, 2).unwrap();
assert_eq!(p1, p2, "cache hit must produce an equal PathBuf");
assert_eq!(
c.path_cache.len(),
1,
"second call for the same key must not grow the cache"
);
}
#[test]
fn dat_and_index_paths_share_cache_under_different_kinds() {
let mut c = ctx("/game");
let d = dat_path(&mut c, 0x00, 0x0000, 0).unwrap();
let i = index_path(&mut c, 0x00, 0x0000, 0).unwrap();
assert_ne!(d, i, "dat and index paths must differ");
assert_eq!(
c.path_cache.len(),
2,
"PathKind discriminates: dat and index must occupy separate cache slots"
);
}
#[test]
fn path_cache_unsupported_platform_does_not_insert() {
let mut c = ApplyContext::new("/game");
c.platform = Platform::Unknown(42);
assert!(dat_path(&mut c, 0x00, 0x0000, 0).is_err());
assert!(
c.path_cache.is_empty(),
"unsupported-platform error must not insert into the path cache"
);
}
#[test]
fn invalidate_path_cache_drops_every_entry() {
let mut c = ctx("/game");
let _ = dat_path(&mut c, 0x04, 0x0100, 2).unwrap();
let _ = index_path(&mut c, 0x04, 0x0100, 0).unwrap();
assert_eq!(c.path_cache.len(), 2);
c.invalidate_path_cache();
assert!(
c.path_cache.is_empty(),
"invalidate_path_cache must empty the entire map"
);
}
#[test]
fn index_path_populates_and_serves_from_cache() {
let mut c = ctx("/game");
assert!(
c.path_cache.is_empty(),
"fresh ApplyContext starts with an empty path cache"
);
let p1 = index_path(&mut c, 0x04, 0x0100, 0).unwrap();
assert_eq!(c.path_cache.len(), 1, "first call must populate the cache");
let p2 = index_path(&mut c, 0x04, 0x0100, 0).unwrap();
assert_eq!(p1, p2, "cache hit must return an equal PathBuf");
assert_eq!(
c.path_cache.len(),
1,
"second call for the same key must not grow the cache"
);
}
}