use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
const MAX_SAFE_FILENAME_COMPONENT_LEN: usize = 96;
static NEXT_MANAGER_ID: AtomicU64 = AtomicU64::new(1);
pub struct TempPathManager {
base_dir: PathBuf,
quarantine_dir: PathBuf,
manager_id: u64,
counter: AtomicU64,
active_temps: HashMap<PathBuf, TempFileInfo>,
config: TempManagementConfig,
}
#[derive(Debug, Clone)]
pub struct TempManagementConfig {
pub temp_prefix: String,
pub max_temp_age: std::time::Duration,
pub max_active_temps: usize,
pub include_pid_in_name: bool,
pub auto_create_quarantine: bool,
pub temp_file_permissions: Option<u32>,
pub temp_dir_permissions: Option<u32>,
}
impl Default for TempManagementConfig {
fn default() -> Self {
Self {
temp_prefix: "atp_sparse".to_string(),
max_temp_age: std::time::Duration::from_hours(24),
max_active_temps: 1000,
include_pid_in_name: true,
auto_create_quarantine: true,
temp_file_permissions: Some(0o600), temp_dir_permissions: Some(0o700), }
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct TempFileInfo {
created_at: SystemTime,
operation_id: String,
state: PathState,
size: Option<u64>,
committed: bool,
quarantine_reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TempFileSnapshot {
pub created_at: SystemTime,
pub operation_id: String,
pub state: PathState,
pub size: Option<u64>,
pub committed: bool,
pub quarantine_reason: Option<String>,
}
impl TempFileInfo {
fn snapshot(&self) -> TempFileSnapshot {
TempFileSnapshot {
created_at: self.created_at,
operation_id: self.operation_id.clone(),
state: self.state.clone(),
size: self.size,
committed: self.committed,
quarantine_reason: self.quarantine_reason.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathState {
Creating,
Writing,
Verifying,
ReadyToCommit,
Committing,
Committed,
Quarantining,
Quarantined { reason: String },
CleaningUp,
CleanedUp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuarantineReason {
Cancelled,
VerificationFailed,
CommitFailed,
CorruptionDetected,
StaleFile,
UserRequested,
SystemError(String),
}
impl QuarantineReason {
pub fn description(&self) -> String {
match self {
Self::Cancelled => "Operation was cancelled".to_string(),
Self::VerificationFailed => "Verification failed".to_string(),
Self::CommitFailed => "Commit operation failed".to_string(),
Self::CorruptionDetected => "Data corruption detected".to_string(),
Self::StaleFile => "File became stale without completion".to_string(),
Self::UserRequested => "User requested quarantine".to_string(),
Self::SystemError(msg) => format!("System error: {}", msg),
}
}
pub fn severity(&self) -> u8 {
match self {
Self::Cancelled => 20,
Self::UserRequested => 30,
Self::StaleFile => 40,
Self::CommitFailed => 60,
Self::VerificationFailed => 70,
Self::SystemError(_) => 80,
Self::CorruptionDetected => 100,
}
}
}
impl TempPathManager {
pub fn new(base_dir: impl AsRef<Path>) -> Self {
Self::with_config(base_dir, TempManagementConfig::default())
}
pub fn with_config(base_dir: impl AsRef<Path>, config: TempManagementConfig) -> Self {
let base_dir = base_dir.as_ref().to_path_buf();
let quarantine_dir = base_dir.join(".quarantine");
let manager = Self {
base_dir,
quarantine_dir,
manager_id: NEXT_MANAGER_ID.fetch_add(1, Ordering::Relaxed),
counter: AtomicU64::new(1),
active_temps: HashMap::new(),
config,
};
manager.ensure_directories().ok();
manager
}
pub fn create_temp_path(&mut self, operation_id: &str) -> Result<PathBuf, TempManagementError> {
if self.active_temps.len() >= self.config.max_active_temps {
return Err(TempManagementError::TooManyTempFiles);
}
let filename = self.generate_temp_filename(operation_id)?;
let temp_path = self.base_dir.join(filename);
let temp_info = TempFileInfo {
created_at: SystemTime::now(),
operation_id: operation_id.to_string(),
state: PathState::Creating,
size: None,
committed: false,
quarantine_reason: None,
};
self.active_temps.insert(temp_path.clone(), temp_info);
Ok(temp_path)
}
pub fn update_temp_state(
&mut self,
path: &Path,
state: PathState,
) -> Result<(), TempManagementError> {
match self.active_temps.get_mut(path) {
Some(info) => {
info.state = state;
Ok(())
}
None => Err(TempManagementError::TempFileNotFound(path.to_path_buf())),
}
}
pub fn update_temp_size(&mut self, path: &Path, size: u64) -> Result<(), TempManagementError> {
match self.active_temps.get_mut(path) {
Some(info) => {
info.size = Some(size);
Ok(())
}
None => Err(TempManagementError::TempFileNotFound(path.to_path_buf())),
}
}
pub fn mark_committed(
&mut self,
temp_path: &Path,
_final_path: &Path,
) -> Result<(), TempManagementError> {
if let Some(info) = self.active_temps.get_mut(temp_path) {
info.committed = true;
info.state = PathState::Committed;
}
self.active_temps.remove(temp_path);
Ok(())
}
pub fn quarantine_file(
&mut self,
temp_path: &Path,
reason: &str,
) -> Result<PathBuf, TempManagementError> {
self.ensure_quarantine_dir()?;
let quarantine_path = self.generate_quarantine_path(temp_path, reason)?;
fs::rename(temp_path, &quarantine_path)
.map_err(|e| TempManagementError::QuarantineMoveFailed(e.to_string()))?;
if let Some(info) = self.active_temps.get_mut(temp_path) {
info.state = PathState::Quarantined {
reason: reason.to_string(),
};
info.quarantine_reason = Some(reason.to_string());
}
self.active_temps.remove(temp_path);
Ok(quarantine_path)
}
pub fn cleanup_temp_file(&mut self, temp_path: &Path) -> Result<(), TempManagementError> {
if temp_path.exists() {
fs::remove_file(temp_path)
.map_err(|e| TempManagementError::CleanupFailed(e.to_string()))?;
}
if let Some(info) = self.active_temps.get_mut(temp_path) {
info.state = PathState::CleanedUp;
}
self.active_temps.remove(temp_path);
Ok(())
}
pub fn get_temp_info(&self, temp_path: &Path) -> Option<TempFileSnapshot> {
self.active_temps.get(temp_path).map(TempFileInfo::snapshot)
}
pub fn list_active_temps(&self) -> Vec<&PathBuf> {
self.active_temps.keys().collect()
}
pub fn cleanup_stale_files(&mut self) -> Result<Vec<PathBuf>, TempManagementError> {
let now = SystemTime::now();
let max_age = self.config.max_temp_age;
let mut cleaned_files = Vec::new();
let stale_paths: Vec<PathBuf> = self
.active_temps
.iter()
.filter_map(|(path, info)| {
if let Ok(age) = now.duration_since(info.created_at) {
if age > max_age && !info.committed {
Some(path.clone())
} else {
None
}
} else {
Some(path.clone())
}
})
.collect();
for path in stale_paths {
if path.exists() {
if self.config.auto_create_quarantine {
if let Ok(quarantine_path) = self.quarantine_file(&path, "stale_file") {
cleaned_files.push(quarantine_path);
}
} else {
if self.cleanup_temp_file(&path).is_ok() {
cleaned_files.push(path);
}
}
} else {
self.active_temps.remove(&path);
cleaned_files.push(path);
}
}
Ok(cleaned_files)
}
pub fn get_stats(&self) -> TempPathStats {
let active_count = self.active_temps.len();
let total_size = self
.active_temps
.values()
.filter_map(|info| info.size)
.sum();
let state_counts: HashMap<String, usize> = self
.active_temps
.values()
.map(|info| match &info.state {
PathState::Creating => "creating".to_string(),
PathState::Writing => "writing".to_string(),
PathState::Verifying => "verifying".to_string(),
PathState::ReadyToCommit => "ready_to_commit".to_string(),
PathState::Committing => "committing".to_string(),
PathState::Committed => "committed".to_string(),
PathState::Quarantining => "quarantining".to_string(),
PathState::Quarantined { .. } => "quarantined".to_string(),
PathState::CleaningUp => "cleaning_up".to_string(),
PathState::CleanedUp => "cleaned_up".to_string(),
})
.fold(HashMap::new(), |mut acc, state| {
*acc.entry(state).or_insert(0) += 1;
acc
});
TempPathStats {
active_count,
total_size,
state_counts,
base_dir: self.base_dir.clone(),
quarantine_dir: self.quarantine_dir.clone(),
}
}
fn generate_temp_filename(&self, operation_id: &str) -> Result<String, TempManagementError> {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let temp_prefix = safe_filename_component(
&self.config.temp_prefix,
"atp_sparse",
MAX_SAFE_FILENAME_COMPONENT_LEN,
);
let mut filename = format!(
"{}.{}.{}.{}",
temp_prefix, timestamp, self.manager_id, counter
);
if self.config.include_pid_in_name {
let pid = std::process::id();
filename.push_str(&format!(".{}", pid));
}
let safe_operation_id =
safe_filename_component(operation_id, "operation", MAX_SAFE_FILENAME_COMPONENT_LEN);
filename.push_str(&format!(".{}.tmp", safe_operation_id));
Ok(filename)
}
fn generate_quarantine_path(
&self,
original_path: &Path,
reason: &str,
) -> Result<PathBuf, TempManagementError> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let original_name = original_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let safe_original_name =
safe_filename_component(original_name, "unknown", MAX_SAFE_FILENAME_COMPONENT_LEN);
let safe_reason =
safe_filename_component(reason, "quarantine", MAX_SAFE_FILENAME_COMPONENT_LEN);
let quarantine_name = format!("{}.{}.{}", safe_original_name, safe_reason, timestamp);
Ok(self.quarantine_dir.join(quarantine_name))
}
fn ensure_directories(&self) -> Result<(), TempManagementError> {
if !self.base_dir.exists() {
fs::create_dir_all(&self.base_dir)
.map_err(|e| TempManagementError::DirectoryCreation(e.to_string()))?;
}
#[cfg(unix)]
{
if let Some(perms) = self.config.temp_dir_permissions {
use std::os::unix::fs::PermissionsExt;
let permissions = fs::Permissions::from_mode(perms);
fs::set_permissions(&self.base_dir, permissions)
.map_err(|e| TempManagementError::PermissionsSetting(e.to_string()))?;
}
}
if self.config.auto_create_quarantine {
self.ensure_quarantine_dir()?;
}
Ok(())
}
fn ensure_quarantine_dir(&self) -> Result<(), TempManagementError> {
if !self.quarantine_dir.exists() {
fs::create_dir_all(&self.quarantine_dir)
.map_err(|e| TempManagementError::QuarantineDirectoryCreation(e.to_string()))?;
#[cfg(unix)]
{
if let Some(perms) = self.config.temp_dir_permissions {
use std::os::unix::fs::PermissionsExt;
let permissions = fs::Permissions::from_mode(perms);
fs::set_permissions(&self.quarantine_dir, permissions)
.map_err(|e| TempManagementError::PermissionsSetting(e.to_string()))?;
}
}
}
Ok(())
}
}
fn safe_filename_component(input: &str, fallback: &str, max_len: usize) -> String {
let mut component = String::new();
for ch in input.chars() {
let safe = if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
ch
} else {
'_'
};
if component.len() >= max_len {
break;
}
component.push(safe);
}
if component.is_empty() {
fallback.to_string()
} else {
component
}
}
#[derive(Debug, Clone)]
pub struct TempPathStats {
pub active_count: usize,
pub total_size: u64,
pub state_counts: HashMap<String, usize>,
pub base_dir: PathBuf,
pub quarantine_dir: PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum TempManagementError {
#[error("Too many temporary files active")]
TooManyTempFiles,
#[error("Temporary file not found: {0}")]
TempFileNotFound(PathBuf),
#[error("Directory creation failed: {0}")]
DirectoryCreation(String),
#[error("Quarantine directory creation failed: {0}")]
QuarantineDirectoryCreation(String),
#[error("Quarantine move failed: {0}")]
QuarantineMoveFailed(String),
#[error("Cleanup failed: {0}")]
CleanupFailed(String),
#[error("Permissions setting failed: {0}")]
PermissionsSetting(String),
#[error("Invalid filename: {0}")]
InvalidFilename(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn test_temp_path_creation() {
let temp_dir = std::env::temp_dir().join("atp_test_temp_mgmt");
let mut manager = TempPathManager::new(&temp_dir);
let temp_path = manager.create_temp_path("test_operation").unwrap();
assert!(temp_path.to_string_lossy().contains("atp_sparse"));
assert!(temp_path.to_string_lossy().contains("test_operation"));
assert!(manager.get_temp_info(&temp_path).is_some());
assert_eq!(manager.list_active_temps().len(), 1);
manager.cleanup_temp_file(&temp_path).unwrap();
assert!(manager.get_temp_info(&temp_path).is_none());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_temp_paths_are_unique_across_managers() {
let temp_dir = std::env::temp_dir().join("atp_test_temp_manager_uniqueness");
let mut manager_a = TempPathManager::new(&temp_dir);
let mut manager_b = TempPathManager::new(&temp_dir);
let temp_a = manager_a.create_temp_path("same_operation").unwrap();
let temp_b = manager_b.create_temp_path("same_operation").unwrap();
assert_ne!(temp_a, temp_b);
assert_eq!(temp_a.parent(), Some(temp_dir.as_path()));
assert_eq!(temp_b.parent(), Some(temp_dir.as_path()));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_quarantine_reason_is_single_safe_path_component() {
let temp_dir = std::env::temp_dir().join("atp_test_quarantine_reason_safety");
let mut manager = TempPathManager::new(&temp_dir);
let temp_path = manager.create_temp_path("quarantine_reason").unwrap();
File::create(&temp_path).unwrap();
let quarantine_path = manager
.quarantine_file(&temp_path, "../../escape/attempt")
.unwrap();
assert_eq!(
quarantine_path.parent(),
Some(manager.quarantine_dir.as_path())
);
let quarantine_name = quarantine_path
.file_name()
.and_then(|name| name.to_str())
.unwrap();
assert!(quarantine_name.contains("______escape_attempt"));
assert!(!quarantine_name.contains('/'));
assert!(!quarantine_name.contains(".."));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_quarantine_original_name_is_single_safe_path_component() {
let temp_dir = std::env::temp_dir().join("atp_test_quarantine_original_safety");
let manager = TempPathManager::new(&temp_dir);
let original_path = PathBuf::from("bad name..\n\t.tmp");
let quarantine_path = manager
.generate_quarantine_path(&original_path, "../../escape/attempt")
.unwrap();
assert_eq!(
quarantine_path.parent(),
Some(manager.quarantine_dir.as_path())
);
let quarantine_name = quarantine_path
.file_name()
.and_then(|name| name.to_str())
.unwrap();
assert!(quarantine_name.starts_with("bad_name_____tmp."));
assert!(quarantine_name.contains("______escape_attempt"));
assert!(!quarantine_name.contains('/'));
assert!(!quarantine_name.contains(".."));
assert!(!quarantine_name.contains('\n'));
assert!(!quarantine_name.contains('\t'));
}
#[test]
fn test_temp_file_states() {
let temp_dir = std::env::temp_dir().join("atp_test_states");
let mut manager = TempPathManager::new(&temp_dir);
let temp_path = manager.create_temp_path("state_test").unwrap();
let info = manager.get_temp_info(&temp_path).unwrap();
assert_eq!(info.state, PathState::Creating);
manager
.update_temp_state(&temp_path, PathState::Writing)
.unwrap();
let info = manager.get_temp_info(&temp_path).unwrap();
assert_eq!(info.state, PathState::Writing);
manager.update_temp_size(&temp_path, 1024).unwrap();
let info = manager.get_temp_info(&temp_path).unwrap();
assert_eq!(info.size, Some(1024));
manager.cleanup_temp_file(&temp_path).unwrap();
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_quarantine_functionality() {
let temp_dir = std::env::temp_dir().join("atp_test_quarantine");
let mut manager = TempPathManager::new(&temp_dir);
let temp_path = manager.create_temp_path("quarantine_test").unwrap();
File::create(&temp_path).unwrap();
let quarantine_path = manager.quarantine_file(&temp_path, "test_reason").unwrap();
assert!(!temp_path.exists());
assert!(quarantine_path.exists());
assert!(quarantine_path.to_string_lossy().contains("test_reason"));
assert!(manager.get_temp_info(&temp_path).is_none());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_stale_file_cleanup() {
let temp_dir = std::env::temp_dir().join("atp_test_stale");
let mut config = TempManagementConfig::default();
config.max_temp_age = std::time::Duration::from_millis(1); config.auto_create_quarantine = false;
let mut manager = TempPathManager::with_config(&temp_dir, config);
let temp_path = manager.create_temp_path("stale_test").unwrap();
File::create(&temp_path).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
let cleaned = manager.cleanup_stale_files().unwrap();
assert_eq!(cleaned.len(), 1);
assert!(!temp_path.exists());
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_quarantine_reason_properties() {
let cancelled = QuarantineReason::Cancelled;
assert_eq!(cancelled.severity(), 20);
assert!(cancelled.description().contains("cancelled"));
let corruption = QuarantineReason::CorruptionDetected;
assert_eq!(corruption.severity(), 100);
assert!(corruption.description().contains("corruption"));
}
#[test]
fn test_stats_collection() {
let temp_dir = std::env::temp_dir().join("atp_test_stats");
let mut manager = TempPathManager::new(&temp_dir);
let temp1 = manager.create_temp_path("stats_test1").unwrap();
let temp2 = manager.create_temp_path("stats_test2").unwrap();
manager.update_temp_size(&temp1, 1024).unwrap();
manager.update_temp_size(&temp2, 2048).unwrap();
manager
.update_temp_state(&temp2, PathState::Writing)
.unwrap();
let stats = manager.get_stats();
assert_eq!(stats.active_count, 2);
assert_eq!(stats.total_size, 3072);
assert_eq!(stats.state_counts.get("creating"), Some(&1));
assert_eq!(stats.state_counts.get("writing"), Some(&1));
manager.cleanup_temp_file(&temp1).unwrap();
manager.cleanup_temp_file(&temp2).unwrap();
std::fs::remove_dir_all(&temp_dir).ok();
}
}