//! SMB Share Snapshot Manager (MS-SMB2 §2.2.31 / MS-FSCC §2.3.7) //! //! Implements FSCTL_SRV_SNAPSHOT_CREATE/READ/WRITE/DELETE operations //! for Windows VSS (Volume Shadow Copy Service) support. use std::collections::HashMap; use std::fmt::Write; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::SystemTime; use crate::ntstatus; /// Snapshot entry stored in SnapshotManager #[derive(Debug, Clone)] pub struct SnapshotEntry { /// Unique snapshot ID (GMT token format: @GMT-YYYY.MM.DD-HH.MM.SS) pub snapshot_id: String, /// Share name this snapshot belongs to pub share_name: String, /// Creation timestamp pub created_at: SystemTime, /// Snapshot state pub state: SnapshotState, /// Snapshot metadata (JSON) pub metadata: Option, } /// Snapshot state enum #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SnapshotState { Created, Active, Deleting, Deleted, } /// SMB Snapshot request types #[derive(Debug, Clone)] pub enum SnapshotRequest { Create { share_name: String, snapshot_name: Option, }, Read { share_name: String, snapshot_id: String, }, Write { share_name: String, snapshot_id: String, data: Vec, }, Delete { share_name: String, snapshot_id: String, }, } /// SMB Snapshot response types #[derive(Debug, Clone)] pub enum SnapshotResponse { Create { snapshot_id: String, created_at: SystemTime, }, Read { snapshot_id: String, data: Vec, }, Write { bytes_written: u32, }, Delete { success: bool, }, List { snapshots: Vec, }, } const SNAPSHOTS_DIR: &str = ".snapshots"; const SNAPSHOTS_FILE: &str = "snapshots.json"; /// Snapshot manager - manages share snapshots pub struct SnapshotManager { /// Snapshots indexed by (share_name, snapshot_id) snapshots: RwLock>, /// Optional file-system path for persistence storage_path: Option, } impl SnapshotManager { pub fn new() -> Self { Self { snapshots: RwLock::new(HashMap::new()), storage_path: None, } } pub fn with_storage_path(path: PathBuf) -> Self { let manager = Self { snapshots: RwLock::new(HashMap::new()), storage_path: Some(path), }; manager.load_snapshots(); manager } fn snapshots_file_path(&self) -> Option { self.storage_path.as_ref().map(|p| p.join(SNAPSHOTS_DIR).join(SNAPSHOTS_FILE)) } fn load_snapshots(&self) { let path = match self.snapshots_file_path() { Some(p) => p, None => return, }; let data = match std::fs::read_to_string(&path) { Ok(d) => d, Err(_) => return, }; let mut map = self.snapshots.write().unwrap(); for line in data.lines() { let parts: Vec<&str> = line.splitn(5, '|').collect(); if parts.len() < 4 { continue; } let share_name = parts[0].to_string(); let snapshot_id = parts[1].to_string(); let secs: u64 = match parts[2].parse() { Ok(s) => s, Err(_) => continue, }; let state = match parts[3] { "Created" => SnapshotState::Created, "Active" => SnapshotState::Active, "Deleting" => SnapshotState::Deleting, "Deleted" => SnapshotState::Deleted, _ => continue, }; let metadata = parts.get(4).filter(|m| !m.is_empty()).map(|m| m.to_string()); let created_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs); let entry = SnapshotEntry { snapshot_id, share_name: share_name.clone(), created_at, state, metadata, }; map.insert((share_name, entry.snapshot_id.clone()), entry); } } fn save_snapshots(&self) { let path = match self.snapshots_file_path() { Some(p) => p, None => return, }; if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } let mut output = String::new(); { let map = self.snapshots.read().unwrap(); for entry in map.values() { let secs = entry.created_at .duration_since(std::time::UNIX_EPOCH) .unwrap_or(std::time::Duration::ZERO) .as_secs(); let state_str = match entry.state { SnapshotState::Created => "Created", SnapshotState::Active => "Active", SnapshotState::Deleting => "Deleting", SnapshotState::Deleted => "Deleted", }; let meta = entry.metadata.as_deref().unwrap_or(""); writeln!(output, "{}|{}|{}|{}|{}", entry.share_name, entry.snapshot_id, secs, state_str, meta).ok(); } } let _ = std::fs::write(&path, &output); } /// Create a new snapshot for a share pub fn create_snapshot( &self, share_name: &str, snapshot_name: Option<&str>, ) -> Result { let now = SystemTime::now(); let snapshot_id = match snapshot_name { Some(name) => name.to_string(), None => Self::gmt_token_from_time(now), }; let entry = SnapshotEntry { snapshot_id: snapshot_id.clone(), share_name: share_name.to_string(), created_at: now, state: SnapshotState::Active, metadata: None, }; self.snapshots .write() .unwrap() .insert((share_name.to_string(), snapshot_id.clone()), entry.clone()); self.save_snapshots(); Ok(entry) } /// Read snapshot metadata pub fn read_snapshot( &self, share_name: &str, snapshot_id: &str, ) -> Result { self.snapshots .read() .unwrap() .get(&(share_name.to_string(), snapshot_id.to_string())) .cloned() .ok_or(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND) } /// Delete a snapshot pub fn delete_snapshot( &self, share_name: &str, snapshot_id: &str, ) -> Result<(), u32> { let mut snapshots = self.snapshots.write().unwrap(); let entry = snapshots .get_mut(&(share_name.to_string(), snapshot_id.to_string())) .ok_or(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND)?; if entry.state == SnapshotState::Deleting { return Err(ntstatus::STATUS_SNAPSHOT_OPERATION_IN_PROGRESS); } entry.state = SnapshotState::Deleted; snapshots.remove(&(share_name.to_string(), snapshot_id.to_string())); self.save_snapshots(); Ok(()) } /// List all snapshots for a share pub fn list_snapshots(&self, share_name: &str) -> Vec { self.snapshots .read() .unwrap() .iter() .filter(|((s, _), _)| s == share_name) .map(|(_, entry)| entry.clone()) .collect() } /// Convert SystemTime to GMT token format (@GMT-YYYY.MM.DD-HH.MM.SS) pub fn gmt_token_from_time(time: SystemTime) -> String { use std::time::UNIX_EPOCH; let duration = time .duration_since(UNIX_EPOCH) .unwrap_or(std::time::Duration::ZERO); let total_secs = duration.as_secs(); let hours = total_secs / 3600; let minutes = (total_secs % 3600) / 60; let seconds = total_secs % 60; // Simplified: use days since epoch (not calendar date) let days = hours / 24; let year = 1970 + days / 365; let remaining_days = days % 365; let month = remaining_days / 30 + 1; let day = remaining_days % 30 + 1; let hour = hours % 24; format!( "@GMT-{:04}.{:02}.{:02}-{:02}.{:02}.{:02}", year, month, day, hour, minutes, seconds ) } /// Parse GMT token to SystemTime pub fn time_from_gmt_token(token: &str) -> Option { use std::time::UNIX_EPOCH; if !token.starts_with("@GMT-") { return None; } let parts: Vec<&str> = token[5..].split('-').collect(); if parts.len() != 2 { return None; } let date_parts: Vec<&str> = parts[0].split('.').collect(); let time_parts: Vec<&str> = parts[1].split('.').collect(); if date_parts.len() != 3 || time_parts.len() != 3 { return None; } let year: u64 = date_parts[0].parse().ok()?; let month: u64 = date_parts[1].parse().ok()?; let day: u64 = date_parts[2].parse().ok()?; let hour: u64 = time_parts[0].parse().ok()?; let minute: u64 = time_parts[1].parse().ok()?; let second: u64 = time_parts[2].parse().ok()?; // Simplified calculation (not calendar-aware) let days_since_epoch = (year - 1970) * 365 + month * 30 + day; let total_secs = days_since_epoch * 86400 + hour * 3600 + minute * 60 + second; Some(UNIX_EPOCH + std::time::Duration::from_secs(total_secs)) } } impl Default for SnapshotManager { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use std::time::UNIX_EPOCH; #[test] fn test_create_snapshot() { let manager = SnapshotManager::new(); let entry = manager.create_snapshot("test_share", Some("snapshot1")).unwrap(); assert_eq!(entry.share_name, "test_share"); assert_eq!(entry.snapshot_id, "snapshot1"); assert_eq!(entry.state, SnapshotState::Active); } #[test] fn test_read_snapshot() { let manager = SnapshotManager::new(); manager.create_snapshot("test_share", Some("snapshot1")).unwrap(); let entry = manager.read_snapshot("test_share", "snapshot1").unwrap(); assert_eq!(entry.snapshot_id, "snapshot1"); } #[test] fn test_delete_snapshot() { let manager = SnapshotManager::new(); manager.create_snapshot("test_share", Some("snapshot1")).unwrap(); manager.delete_snapshot("test_share", "snapshot1").unwrap(); assert!(manager.read_snapshot("test_share", "snapshot1").is_err()); } #[test] fn test_list_snapshots() { let manager = SnapshotManager::new(); manager.create_snapshot("share1", Some("snap1")).unwrap(); manager.create_snapshot("share1", Some("snap2")).unwrap(); manager.create_snapshot("share2", Some("snap3")).unwrap(); let snapshots = manager.list_snapshots("share1"); assert_eq!(snapshots.len(), 2); } #[test] fn test_gmt_token_format() { let time = UNIX_EPOCH + std::time::Duration::from_secs(1609459200); // 2021-01-01 00:00:00 let token = SnapshotManager::gmt_token_from_time(time); assert!(token.starts_with("@GMT-")); assert!(token.contains("2021")); } #[test] fn test_gmt_token_parse() { let token = "@GMT-2021.01.01-00.00.00"; let time = SnapshotManager::time_from_gmt_token(token).unwrap(); let token2 = SnapshotManager::gmt_token_from_time(time); assert!(token2.starts_with("@GMT-")); } }