use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct BinaryConfig {
pub project_root: PathBuf,
pub output_dir: PathBuf,
}
impl Default for BinaryConfig {
fn default() -> Self {
Self {
project_root: PathBuf::from("."),
output_dir: PathBuf::from(".dx/serializer"),
}
}
}
impl BinaryConfig {
pub fn with_root<P: AsRef<Path>>(root: P) -> Self {
let root = root.as_ref().to_path_buf();
Self {
output_dir: root.join(".dx/serializer"),
project_root: root,
}
}
}
pub fn hash_path(relative_path: &str) -> String {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for byte in relative_path.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("{:08x}", hash as u32)
}
pub fn get_binary_path<P: AsRef<Path>>(source_path: P, config: &BinaryConfig) -> PathBuf {
let source = source_path.as_ref();
let relative = source
.strip_prefix(&config.project_root)
.unwrap_or(source)
.to_string_lossy()
.replace('\\', "/");
let hash = hash_path(&relative);
let filename = source
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "dx".to_string());
config
.output_dir
.join(&hash)
.join(format!("{}.dx", filename))
}
pub fn write_binary<P: AsRef<Path>>(
source_path: P,
binary_data: &[u8],
config: &BinaryConfig,
) -> std::io::Result<PathBuf> {
let output_path = get_binary_path(&source_path, config);
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = fs::File::create(&output_path)?;
file.write_all(binary_data)?;
Ok(output_path)
}
pub fn read_binary<P: AsRef<Path>>(
source_path: P,
config: &BinaryConfig,
) -> std::io::Result<Vec<u8>> {
let binary_path = get_binary_path(&source_path, config);
fs::read(binary_path)
}
pub fn is_cache_valid<P: AsRef<Path>>(source_path: P, config: &BinaryConfig) -> bool {
let source = source_path.as_ref();
let binary_path = get_binary_path(source, config);
let binary_meta = match fs::metadata(&binary_path) {
Ok(m) => m,
Err(_) => return false,
};
let source_meta = match fs::metadata(source) {
Ok(m) => m,
Err(_) => return false,
};
match (source_meta.modified(), binary_meta.modified()) {
(Ok(src_time), Ok(bin_time)) => bin_time >= src_time,
_ => false,
}
}
pub fn get_manifest(config: &BinaryConfig) -> std::io::Result<Vec<(String, PathBuf)>> {
let mut manifest = Vec::new();
if !config.output_dir.exists() {
return Ok(manifest);
}
for entry in fs::read_dir(&config.output_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let hash = path.file_name().map(|s| s.to_string_lossy().to_string());
for file_entry in fs::read_dir(&path)? {
let file_entry = file_entry?;
let file_path = file_entry.path();
if file_path.extension().map(|e| e == "dx").unwrap_or(false) {
if let Some(h) = &hash {
manifest.push((h.clone(), file_path));
}
}
}
}
}
Ok(manifest)
}
pub fn clean_stale(config: &BinaryConfig) -> std::io::Result<usize> {
let mut cleaned = 0;
if !config.output_dir.exists() {
return Ok(0);
}
for entry in fs::read_dir(&config.output_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let mut is_empty = true;
for file_entry in fs::read_dir(&path)? {
let file_entry = file_entry?;
let file_path = file_entry.path();
if file_path.extension().map(|e| e == "dx").unwrap_or(false) {
is_empty = false;
}
}
if is_empty {
fs::remove_dir(&path)?;
cleaned += 1;
}
}
}
Ok(cleaned)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_path_consistency() {
let hash1 = hash_path("config.dx");
let hash2 = hash_path("config.dx");
assert_eq!(hash1, hash2);
}
#[test]
fn test_hash_path_uniqueness() {
let hash1 = hash_path("config.dx");
let hash2 = hash_path("src/config.dx");
let hash3 = hash_path("lib/config.dx");
assert_ne!(hash1, hash2);
assert_ne!(hash2, hash3);
assert_ne!(hash1, hash3);
}
#[test]
fn test_get_binary_path() {
let config = BinaryConfig::with_root("/project");
let path = get_binary_path("/project/config.dx", &config);
assert!(path.to_string_lossy().contains(".dx/serializer"));
assert!(path.to_string_lossy().ends_with(".dx"));
}
#[test]
fn test_binary_path_different_dirs() {
let config = BinaryConfig::with_root("/project");
let path1 = get_binary_path("/project/config.dx", &config);
let path2 = get_binary_path("/project/src/config.dx", &config);
assert_ne!(path1, path2);
assert!(path1.file_name() == path2.file_name()); }
}