pub(crate) mod path;
pub(crate) mod sqpk;
use crate::Platform;
use crate::Result;
use crate::chunk::Chunk;
use crate::chunk::adir::AddDirectory;
use crate::chunk::aply::{ApplyOption, ApplyOptionKind};
use crate::chunk::ddir::DeleteDirectory;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use tracing::{debug, trace, warn};
const MAX_CACHED_FDS: usize = 256;
pub struct ApplyContext {
pub(crate) game_path: PathBuf,
pub(crate) platform: Platform,
pub(crate) ignore_missing: bool,
pub(crate) ignore_old_mismatch: bool,
file_cache: HashMap<PathBuf, File>,
}
impl ApplyContext {
pub fn new(game_path: impl Into<PathBuf>) -> Self {
Self {
game_path: game_path.into(),
platform: Platform::Win32,
ignore_missing: false,
ignore_old_mismatch: false,
file_cache: HashMap::new(),
}
}
#[must_use]
pub fn game_path(&self) -> &std::path::Path {
&self.game_path
}
#[must_use]
pub fn platform(&self) -> Platform {
self.platform
}
#[must_use]
pub fn ignore_missing(&self) -> bool {
self.ignore_missing
}
#[must_use]
pub fn ignore_old_mismatch(&self) -> bool {
self.ignore_old_mismatch
}
#[must_use]
pub fn with_platform(mut self, platform: Platform) -> Self {
self.platform = platform;
self
}
#[must_use]
pub fn with_ignore_missing(mut self, v: bool) -> Self {
self.ignore_missing = v;
self
}
#[must_use]
pub fn with_ignore_old_mismatch(mut self, v: bool) -> Self {
self.ignore_old_mismatch = v;
self
}
pub(crate) fn open_cached(&mut self, path: PathBuf) -> std::io::Result<&mut File> {
use std::collections::hash_map::Entry;
if self.file_cache.len() >= MAX_CACHED_FDS && !self.file_cache.contains_key(&path) {
self.file_cache.clear();
}
match self.file_cache.entry(path) {
Entry::Occupied(e) => Ok(e.into_mut()),
Entry::Vacant(e) => {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(e.key())?;
Ok(e.insert(file))
}
}
}
pub(crate) fn evict_cached(&mut self, path: &Path) {
self.file_cache.remove(path);
}
pub(crate) fn clear_file_cache(&mut self) {
self.file_cache.clear();
}
}
pub trait Apply {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()>;
}
impl Apply for Chunk {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
match self {
Chunk::FileHeader(_) | Chunk::ApplyFreeSpace(_) | Chunk::EndOfFile => Ok(()),
Chunk::Sqpk(c) => c.apply(ctx),
Chunk::ApplyOption(c) => c.apply(ctx),
Chunk::AddDirectory(c) => c.apply(ctx),
Chunk::DeleteDirectory(c) => c.apply(ctx),
}
}
}
impl Apply for ApplyOption {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
debug!(kind = ?self.kind, value = self.value, "apply option");
match self.kind {
ApplyOptionKind::IgnoreMissing => ctx.ignore_missing = self.value,
ApplyOptionKind::IgnoreOldMismatch => ctx.ignore_old_mismatch = self.value,
}
Ok(())
}
}
impl Apply for AddDirectory {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
trace!(name = %self.name, "create directory");
std::fs::create_dir_all(ctx.game_path.join(&self.name))?;
Ok(())
}
}
impl Apply for DeleteDirectory {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
match std::fs::remove_dir(ctx.game_path.join(&self.name)) {
Ok(()) => {
trace!(name = %self.name, "delete directory");
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && ctx.ignore_missing => {
warn!(name = %self.name, "delete directory: not found, ignored");
Ok(())
}
Err(e) => Err(e.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_eviction_clears_when_full() {
let tmp = tempfile::tempdir().unwrap();
let mut ctx = ApplyContext::new(tmp.path());
for i in 0..MAX_CACHED_FDS {
ctx.open_cached(tmp.path().join(format!("{i}.dat")))
.unwrap();
}
assert_eq!(ctx.file_cache.len(), MAX_CACHED_FDS);
ctx.open_cached(tmp.path().join("new.dat")).unwrap();
assert_eq!(ctx.file_cache.len(), 1);
}
#[test]
fn game_path_returns_install_root() {
let tmp = tempfile::tempdir().unwrap();
let ctx = ApplyContext::new(tmp.path());
assert_eq!(ctx.game_path(), tmp.path());
}
#[test]
fn with_platform_overrides_default() {
let ctx = ApplyContext::new("/irrelevant").with_platform(Platform::Ps4);
assert_eq!(ctx.platform(), Platform::Ps4);
}
#[test]
fn with_ignore_old_mismatch_toggles_flag() {
let ctx = ApplyContext::new("/irrelevant").with_ignore_old_mismatch(true);
assert!(ctx.ignore_old_mismatch());
let ctx = ctx.with_ignore_old_mismatch(false);
assert!(!ctx.ignore_old_mismatch());
}
}