use std::{
collections::BTreeMap,
fs,
io::Read,
path::{Path, PathBuf},
time::UNIX_EPOCH,
};
use serde::{Deserialize, Serialize};
use crate::error::Result;
const CRAFT_CACHE_VERSION: u32 = 1;
const FILE_COMPARE_BUFFER_SIZE: usize = 64 * 1024;
#[derive(Debug, Clone, Serialize)]
pub struct FileStamp {
pub path: String,
pub size: u64,
pub modified_unix_ms: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedCraftTarget {
pub fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CraftCacheState {
pub version: u32,
pub jobs: BTreeMap<String, CachedCraftTarget>,
}
#[derive(Debug, Clone)]
pub struct TargetCachePaths {
pub root: PathBuf,
pub edited_texture: PathBuf,
pub encoded_texture: PathBuf,
}
impl Default for CraftCacheState {
fn default() -> Self {
Self {
version: CRAFT_CACHE_VERSION,
jobs: BTreeMap::new(),
}
}
}
impl CraftCacheState {
pub fn load(path: &Path) -> Self {
let Ok(payload) = fs::read_to_string(path) else {
return Self::default();
};
let Ok(state) = serde_json::from_str::<Self>(&payload) else {
return Self::default();
};
if state.version == CRAFT_CACHE_VERSION {
state
} else {
Self::default()
}
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, serde_json::to_string_pretty(self)?)?;
Ok(())
}
pub fn fingerprint_for(&self, target_name: &str) -> Option<&str> {
self.jobs
.get(target_name)
.map(|entry| entry.fingerprint.as_str())
}
pub fn update(&mut self, target_name: &str, fingerprint: String) {
self.jobs
.insert(target_name.to_string(), CachedCraftTarget { fingerprint });
}
}
pub fn capture_file_stamp(path: &Path) -> Result<FileStamp> {
let metadata = fs::metadata(path)?;
let modified_unix_ms = metadata
.modified()
.ok()
.and_then(|value| value.duration_since(UNIX_EPOCH).ok())
.map(|value| value.as_millis())
.unwrap_or_default();
Ok(FileStamp {
path: path.display().to_string(),
size: metadata.len(),
modified_unix_ms,
})
}
pub fn hash_serialized<T: Serialize>(value: &T) -> Result<String> {
let payload = serde_json::to_vec(value)?;
Ok(format!("{:x}", md5::compute(payload)))
}
pub fn files_are_identical(left: &Path, right: &Path) -> Result<bool> {
let left_metadata = fs::metadata(left)?;
let right_metadata = fs::metadata(right)?;
if left_metadata.len() != right_metadata.len() {
return Ok(false);
}
let mut left_file = fs::File::open(left)?;
let mut right_file = fs::File::open(right)?;
let mut left_buffer = vec![0_u8; FILE_COMPARE_BUFFER_SIZE];
let mut right_buffer = vec![0_u8; FILE_COMPARE_BUFFER_SIZE];
loop {
let left_read = left_file.read(&mut left_buffer)?;
let right_read = right_file.read(&mut right_buffer)?;
if left_read != right_read {
return Ok(false);
}
if left_read == 0 {
return Ok(true);
}
if left_buffer[..left_read] != right_buffer[..right_read] {
return Ok(false);
}
}
}
pub fn target_cache_paths(
cache_root: &Path,
target_name: &str,
edited_extension: &str,
encoded_output_path: &Path,
) -> TargetCachePaths {
let target_slug = target_name
.chars()
.map(|value| {
if value.is_ascii_alphanumeric() || value == '-' || value == '_' {
value
} else {
'_'
}
})
.collect::<String>();
let edited_extension = edited_extension.trim_start_matches('.');
let encoded_extension = encoded_output_path
.extension()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty())
.unwrap_or("bin");
let root = cache_root.join("targets").join(target_slug);
TargetCachePaths {
edited_texture: root.join(format!("edited.{edited_extension}")),
encoded_texture: root.join(format!("encoded.{encoded_extension}")),
root,
}
}