use std::ffi::OsString;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self_encryption::DataMap;
use tempfile::NamedTempFile;
use crate::data::error::{Error, Result};
pub const DATAMAP_EXTENSION: &str = "datamap";
const MAX_COLLISION_ATTEMPTS: u32 = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollisionPolicy {
Overwrite,
NumericSuffix,
}
pub fn datamap_filename_for(original_name: &str) -> String {
let sanitized = sanitize_filename(original_name);
if sanitized.is_empty() {
format!("datamap.{DATAMAP_EXTENSION}")
} else {
format!("{sanitized}.{DATAMAP_EXTENSION}")
}
}
pub fn original_name_from_datamap(path: &Path) -> Option<OsString> {
let basename = path.file_name()?.to_str()?;
let stripped = basename.strip_suffix(&format!(".{DATAMAP_EXTENSION}"))?;
if stripped.is_empty() {
None
} else {
Some(OsString::from(stripped))
}
}
pub fn write_datamap(
dir: &Path,
original_name: &str,
dm: &DataMap,
policy: CollisionPolicy,
) -> Result<PathBuf> {
fs::create_dir_all(dir)?;
let bytes = rmp_serde::to_vec(dm)
.map_err(|e| Error::Serialization(format!("DataMap msgpack encode failed: {e}")))?;
let base_filename = datamap_filename_for(original_name);
let target = reserve_target_path(dir, &base_filename, policy)?;
write_atomic(dir, &target, &bytes)?;
Ok(target)
}
pub fn read_datamap(path: &Path) -> Result<DataMap> {
let bytes = fs::read(path)?;
if bytes.first() == Some(&b'{') {
serde_json::from_slice::<DataMap>(&bytes)
.map_err(|e| Error::Serialization(format!("DataMap JSON decode failed: {e}")))
} else {
rmp_serde::from_slice::<DataMap>(&bytes)
.map_err(|e| Error::Serialization(format!("DataMap msgpack decode failed: {e}")))
}
}
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_alphanumeric() || matches!(c, ' ' | '-' | '_' | '.' | '(' | ')') {
c
} else {
'_'
}
})
.collect::<String>()
.trim()
.to_string()
}
fn reserve_target_path(
dir: &Path,
base_filename: &str,
policy: CollisionPolicy,
) -> Result<PathBuf> {
match policy {
CollisionPolicy::Overwrite => Ok(dir.join(base_filename)),
CollisionPolicy::NumericSuffix => {
let stem = base_filename
.strip_suffix(&format!(".{DATAMAP_EXTENSION}"))
.unwrap_or(base_filename);
for attempt in 0..MAX_COLLISION_ATTEMPTS {
let candidate = if attempt == 0 {
base_filename.to_string()
} else {
format!("{stem}-{}.{DATAMAP_EXTENSION}", attempt + 1)
};
let path = dir.join(&candidate);
if !path.exists() {
return Ok(path);
}
}
Err(Error::Storage(format!(
"Unable to reserve a free datamap filename after {MAX_COLLISION_ATTEMPTS} attempts in {}",
dir.display()
)))
}
}
}
fn write_atomic(dir: &Path, target: &Path, bytes: &[u8]) -> Result<()> {
let mut tmp = NamedTempFile::new_in(dir)?;
tmp.write_all(bytes)?;
tmp.as_file().sync_all()?;
tmp.persist(target).map_err(|e| Error::Io(e.error))?;
#[cfg(unix)]
{
if let Ok(dir_handle) = fs::File::open(dir) {
let _ = dir_handle.sync_all();
}
}
Ok(())
}