use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use memmap2::Mmap;
use rkyv::{Archive, Deserialize, Serialize};
use crate::error::{HoldError, Result};
use crate::state::{FileState, GcMetrics, METADATA_VERSION, StateMetadata};
#[cfg(test)]
mod tests;
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
struct StateMetadataV2 {
pub version: u32,
pub files: HashMap<String, FileState>,
pub last_gc_mtime_nanos: Option<u128>,
}
impl From<StateMetadataV2> for StateMetadata {
fn from(v2: StateMetadataV2) -> Self {
StateMetadata {
version: v2.version,
files: v2.files,
last_gc_mtime_nanos: v2.last_gc_mtime_nanos,
gc_metrics: GcMetrics::default(),
}
}
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
struct StateMetadataV3 {
pub version: u32,
pub files: HashMap<String, FileState>,
pub last_gc_mtime_nanos: Option<u128>,
pub gc_metrics: GcMetricsV3,
}
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
struct GcMetricsV3 {
pub runs: u32,
pub seed_initial_size: Option<u64>,
pub recent_initial_sizes: Vec<u64>,
pub recent_bytes_freed: Vec<u64>,
pub last_suggested_cap: Option<u64>,
}
impl From<StateMetadataV3> for StateMetadata {
fn from(v3: StateMetadataV3) -> Self {
StateMetadata {
version: v3.version,
files: v3.files,
last_gc_mtime_nanos: v3.last_gc_mtime_nanos,
gc_metrics: GcMetrics {
runs: v3.gc_metrics.runs,
seed_initial_size: v3.gc_metrics.seed_initial_size,
recent_initial_sizes: v3.gc_metrics.recent_initial_sizes,
recent_bytes_freed: v3.gc_metrics.recent_bytes_freed,
last_suggested_cap: v3.gc_metrics.last_suggested_cap,
recent_final_sizes: Vec::new(),
last_cap_trace: None,
},
}
}
}
pub fn load_metadata(metadata_path: &Path) -> Result<StateMetadata> {
match load_metadata_inner(metadata_path) {
Ok(metadata) => Ok(metadata),
Err(HoldError::DeserializationError { .. }) => {
eprintln!("⚠️ Detected incompatible metadata format from previous cargo-hold version");
eprintln!(" Automatically resetting metadata to use new format...");
if let Err(remove_err) = fs::remove_file(metadata_path) {
eprintln!(" Warning: Could not remove old metadata file: {remove_err}");
}
Ok(StateMetadata::new())
}
Err(other) => Err(other),
}
}
fn load_metadata_inner(metadata_path: &Path) -> Result<StateMetadata> {
if !metadata_path.exists() {
return Ok(StateMetadata::new());
}
let file = File::open(metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
let file_metadata = file.metadata().map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
if file_metadata.len() == 0 {
return Ok(StateMetadata::new());
}
let mmap = unsafe { Mmap::map(&file) }.map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
let metadata = deserialize_metadata(&mmap[..])?;
if metadata.version > METADATA_VERSION {
return Err(HoldError::ConfigError(format!(
"Metadata version {} is newer than supported version {}. Please update cargo-hold.",
metadata.version, METADATA_VERSION
)));
}
let metadata = if metadata.version < METADATA_VERSION {
migrate_metadata(metadata)?
} else {
metadata
};
Ok(metadata)
}
fn migrate_metadata(mut metadata: StateMetadata) -> Result<StateMetadata> {
if metadata.version == 1 {
metadata.version = 2;
}
if metadata.version == 2 {
metadata.gc_metrics = Default::default();
metadata.version = 3;
}
if metadata.version == 3 {
metadata.gc_metrics.recent_final_sizes = Vec::new();
metadata.gc_metrics.last_cap_trace = None;
metadata.version = 4;
}
Ok(metadata)
}
fn deserialize_metadata(bytes: &[u8]) -> Result<StateMetadata> {
match rkyv::from_bytes::<StateMetadata, rkyv::rancor::BoxedError>(bytes) {
Ok(metadata) => Ok(metadata),
Err(primary_err) => {
if let Ok(v3) = rkyv::from_bytes::<StateMetadataV3, rkyv::rancor::BoxedError>(bytes) {
return Ok(StateMetadata::from(v3));
}
if let Ok(v2) = rkyv::from_bytes::<StateMetadataV2, rkyv::rancor::BoxedError>(bytes) {
return Ok(StateMetadata::from(v2));
}
Err(HoldError::DeserializationError(primary_err))
}
}
}
pub fn save_metadata(metadata: &StateMetadata, metadata_path: &Path) -> Result<()> {
if let Some(parent) = metadata_path.parent() {
fs::create_dir_all(parent)
.map_err(|source| HoldError::CreateMetadataDirError(parent.to_path_buf(), source))?;
}
let bytes = rkyv::to_bytes::<rkyv::rancor::BoxedError>(metadata)
.map_err(|e| HoldError::SerializationError(Box::new(e)))?;
let temp_path = metadata_path.with_extension("tmp");
let mut temp_file = File::create(&temp_path).map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
temp_file
.write_all(&bytes)
.map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
temp_file.sync_all().map_err(|source| HoldError::IoError {
path: temp_path.clone(),
source,
})?;
fs::rename(&temp_path, metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
Ok(())
}
pub fn clean_metadata(metadata_path: &Path) -> Result<()> {
if metadata_path.exists() {
fs::remove_file(metadata_path).map_err(|source| HoldError::IoError {
path: metadata_path.to_path_buf(),
source,
})?;
}
Ok(())
}