use super::open_flags::OpenFlags; use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsStat}; use smb2::ClientConfig; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SMB_TIMEOUT_SECS: u64 = 30; const FILETIME_TO_UNIX_SECS: u64 = 11_644_473_600; fn filetime_to_systemtime(raw: u64) -> SystemTime { let secs = raw / 10_000_000; if secs > FILETIME_TO_UNIX_SECS { UNIX_EPOCH + Duration::from_secs(secs - FILETIME_TO_UNIX_SECS) } else { UNIX_EPOCH } } fn systemtime_to_filetime(st: SystemTime) -> u64 { let duration = st.duration_since(UNIX_EPOCH).unwrap_or_default(); let secs = duration.as_secs() + FILETIME_TO_UNIX_SECS; let nanos = duration.subsec_nanos() as u64; (secs * 10_000_000) + (nanos / 100) } fn map_smb_error(e: smb2::Error) -> VfsError { match e.kind() { smb2::ErrorKind::NotFound => VfsError::NotFound(e.to_string()), smb2::ErrorKind::AlreadyExists => VfsError::AlreadyExists(e.to_string()), smb2::ErrorKind::AccessDenied => VfsError::PermissionDenied(e.to_string()), smb2::ErrorKind::IsADirectory => VfsError::IsADirectory(e.to_string()), smb2::ErrorKind::NotADirectory => VfsError::NotADirectory(e.to_string()), smb2::ErrorKind::ConnectionLost | smb2::ErrorKind::TimedOut | smb2::ErrorKind::SessionExpired => VfsError::Io(format!("SMB connection error: {}", e)), _ => VfsError::Io(format!("SMB error: {}", e)), } } /// SMB 客户端 VFS 后端 (SMB 2/3) #[derive(Clone)] pub struct SmbVfs { runtime: Arc, client: Arc>, tree: Arc>, } impl SmbVfs { pub fn new(addr: &str, share: &str, username: &str, password: &str) -> Result { Self::new_with_options(addr, share, username, password, true) } pub fn new_with_options( addr: &str, share: &str, username: &str, password: &str, auto_reconnect: bool, ) -> Result { let runtime = Arc::new( tokio::runtime::Builder::new_current_thread() .enable_all() .build() .map_err(|e| VfsError::Io(format!("Failed to create tokio runtime: {}", e)))?, ); let config = ClientConfig { addr: addr.to_string(), timeout: Duration::from_secs(SMB_TIMEOUT_SECS), username: username.to_string(), password: password.to_string(), domain: String::new(), auto_reconnect, compression: true, dfs_enabled: false, dfs_target_overrides: std::collections::HashMap::new(), }; let (client, tree) = runtime.block_on(async { let mut c = smb2::SmbClient::connect(config) .await .map_err(|e| VfsError::Io(format!("SMB connect failed: {}", e)))?; let t = c .connect_share(share) .await .map_err(|e| VfsError::Io(format!("SMB connect_share failed: {}", e)))?; Ok::<_, VfsError>((c, t)) })?; Ok(Self { runtime, client: Arc::new(Mutex::new(client)), tree: Arc::new(Mutex::new(tree)), }) } fn path_to_str(path: &Path) -> String { let s = path.to_string_lossy().to_string(); s.trim_start_matches('/').to_string() } } impl VfsBackend for SmbVfs { fn clone_boxed(&self) -> Box { Box::new(self.clone()) } fn read_dir(&self, path: &Path) -> Result, VfsError> { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; let entries = self .runtime .block_on(client.list_directory(&mut tree, &smb_path)) .map_err(map_smb_error)?; Ok(entries .into_iter() .filter(|e| e.name != "." && e.name != "..") .map(|e| VfsDirEntry { name: e.name, long_name: String::new(), stat: VfsStat { size: e.size, mode: if e.is_directory { 0o755 } else { 0o644 }, uid: 0, gid: 0, atime: filetime_to_systemtime(0), mtime: filetime_to_systemtime(e.modified.0), is_dir: e.is_directory, is_symlink: false, }, }) .collect()) } fn open_file(&self, path: &Path, flags: &OpenFlags) -> Result, VfsError> { let smb_path = Self::path_to_str(path); let _client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; if flags.write || flags.create || flags.truncate { Ok(Box::new(SmbVfsFile { runtime: self.runtime.clone(), client: self.client.clone(), tree: tree.clone(), path: smb_path, mode: FileMode::Write, position: 0, write_buf: Vec::new(), data: Vec::new(), size: 0, file_writer: None, file_id: None, read_chunk_size: DEFAULT_READ_CHUNK_SIZE, })) } else { // Streaming read: open file and store file_id let (file_id, file_size) = { let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?; let tree = self.tree.lock().unwrap(); self.runtime .block_on(tree.open_file(client.connection_mut(), &smb_path)) .map_err(map_smb_error)? }; Ok(Box::new(SmbVfsFile { runtime: self.runtime.clone(), client: self.client.clone(), tree: tree.clone(), path: smb_path, mode: FileMode::Read, position: 0, write_buf: Vec::new(), data: Vec::new(), size: file_size, file_writer: None, file_id: Some(file_id), read_chunk_size: DEFAULT_READ_CHUNK_SIZE, })) } } fn stat(&self, path: &Path) -> Result { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; let info = self .runtime .block_on(client.stat(&mut tree, &smb_path)) .map_err(map_smb_error)?; Ok(VfsStat { size: info.size, mode: if info.is_directory { 0o755 } else { 0o644 }, uid: 0, gid: 0, atime: filetime_to_systemtime(info.accessed.0), mtime: filetime_to_systemtime(info.modified.0), is_dir: info.is_directory, is_symlink: false, }) } fn lstat(&self, path: &Path) -> Result { self.stat(path) } fn create_dir(&self, path: &Path, _mode: u32) -> Result<(), VfsError> { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; self.runtime .block_on(client.create_directory(&mut tree, &smb_path)) .map_err(map_smb_error) } fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> { let mut current = path.to_path_buf(); let mut stack = Vec::new(); while let Some(parent) = current.parent() { if parent.as_os_str().is_empty() || parent == Path::new("/") { break; } stack.push(parent.to_path_buf()); current = parent.to_path_buf(); } for dir in stack.into_iter().rev() { if self.stat(&dir).is_err() { self.create_dir(&dir, mode)?; } } if self.stat(path).is_err() { self.create_dir(path, mode)?; } Ok(()) } fn remove_dir(&self, path: &Path) -> Result<(), VfsError> { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; self.runtime .block_on(client.delete_directory(&mut tree, &smb_path)) .map_err(map_smb_error) } fn remove_file(&self, path: &Path) -> Result<(), VfsError> { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; self.runtime .block_on(client.delete_file(&mut tree, &smb_path)) .map_err(map_smb_error) } fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> { let smb_from = Self::path_to_str(from); let smb_to = Self::path_to_str(to); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; self.runtime .block_on(client.rename(&mut tree, &smb_from, &smb_to)) .map_err(map_smb_error) } fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError> { let smb_path = Self::path_to_str(path); let tree_id = self.tree.lock().unwrap().tree_id; let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let conn = client.connection_mut(); use smb2::client::connection::CompoundOp; use smb2::msg::close::CloseRequest; use smb2::msg::create::{ CreateDisposition, CreateRequest, CreateResponse, ImpersonationLevel, ShareAccess, }; use smb2::msg::query_info::InfoType; use smb2::msg::set_info::SetInfoRequest; use smb2::pack::{ReadCursor, Unpack}; use smb2::types::flags::FileAccessMask; use smb2::types::status::NtStatus; use smb2::types::{Command, CreditCharge, FileId, OplockLevel}; const FILE_BASIC_INFORMATION: u8 = 4; let create_req = CreateRequest { requested_oplock_level: OplockLevel::None, impersonation_level: ImpersonationLevel::Impersonation, desired_access: FileAccessMask::new(FileAccessMask::FILE_WRITE_ATTRIBUTES), file_attributes: 0, share_access: ShareAccess( ShareAccess::FILE_SHARE_READ | ShareAccess::FILE_SHARE_WRITE | ShareAccess::FILE_SHARE_DELETE, ), create_disposition: CreateDisposition::FileOpen, create_options: 0, name: smb_path, create_contexts: vec![], }; let creation_time = 0u64; let last_access_time = systemtime_to_filetime(stat.atime); let last_write_time = systemtime_to_filetime(stat.mtime); let change_time = 0u64; let file_attributes = 0u32; let reserved = 0u32; let mut setinfo_buf = Vec::with_capacity(40); setinfo_buf.extend_from_slice(&creation_time.to_le_bytes()); setinfo_buf.extend_from_slice(&last_access_time.to_le_bytes()); setinfo_buf.extend_from_slice(&last_write_time.to_le_bytes()); setinfo_buf.extend_from_slice(&change_time.to_le_bytes()); setinfo_buf.extend_from_slice(&file_attributes.to_le_bytes()); setinfo_buf.extend_from_slice(&reserved.to_le_bytes()); let setinfo_req = SetInfoRequest { info_type: InfoType::File, file_info_class: FILE_BASIC_INFORMATION, additional_information: 0, file_id: FileId::SENTINEL, buffer: setinfo_buf, }; let close_req = CloseRequest { flags: 0, file_id: FileId::SENTINEL, }; let ops = [ CompoundOp { command: Command::Create, body: &create_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, CompoundOp { command: Command::SetInfo, body: &setinfo_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, CompoundOp { command: Command::Close, body: &close_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, ]; let responses = self.runtime.block_on(async { let frames = conn .execute_compound(&ops) .await .map_err(|e| VfsError::Io(format!("SMB set_stat compound failed: {}", e)))?; let frames: Vec<_> = frames .into_iter() .collect::, _>>() .map_err(|e| VfsError::Io(format!("SMB set_stat waiter error: {}", e)))?; Ok::<_, VfsError>(frames) })?; let create_header = &responses[0].header; let create_body = &responses[0].body; let setinfo_header = &responses[1].header; if create_header.status != NtStatus::SUCCESS { return Err(VfsError::NotFound(format!( "SMB set_stat: file not found ({})", create_header.status ))); } if setinfo_header.status != NtStatus::SUCCESS { let mut cursor = ReadCursor::new(create_body); if let Ok(create_resp) = CreateResponse::unpack(&mut cursor) { let standalone_close = CloseRequest { flags: 0, file_id: create_resp.file_id, }; let _: Result<_, _> = self.runtime.block_on( conn.execute(Command::Close, &standalone_close, Some(tree_id)), ); } return Err(VfsError::Io(format!( "SMB set_stat: SET_INFO failed ({})", setinfo_header.status ))); } Ok(()) } fn read_link(&self, _path: &Path) -> Result { Err(VfsError::Unsupported("SMB read_link".to_string())) } fn create_symlink(&self, _target: &Path, _link: &Path) -> Result<(), VfsError> { Err(VfsError::Unsupported("SMB create_symlink".to_string())) } fn real_path(&self, path: &Path) -> Result { let smb_path = Self::path_to_str(path); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?; let _info = self .runtime .block_on(client.stat(&mut tree, &smb_path)) .map_err(map_smb_error)?; Ok(path.to_path_buf()) } fn exists(&self, path: &Path) -> bool { let smb_path = Self::path_to_str(path); let mut client = match self.client.lock() { Ok(c) => c, Err(_) => return false, }; let mut tree = match self.tree.lock() { Ok(t) => t, Err(_) => return false, }; self.runtime .block_on(client.stat(&mut tree, &smb_path)) .is_ok() } fn hard_link(&self, _original: &Path, _link: &Path) -> Result<(), VfsError> { Err(VfsError::Unsupported("SMB hard_link".to_string())) } } enum FileMode { Read, Write, } struct SmbVfsFile { runtime: Arc, client: Arc>, tree: smb2::Tree, path: String, mode: FileMode, position: u64, write_buf: Vec, data: Vec, size: u64, file_writer: Option, file_id: Option, read_chunk_size: u32, } const DEFAULT_READ_CHUNK_SIZE: u32 = 64 * 1024; // 64KB chunks impl SmbVfsFile { fn ensure_data_loaded(&mut self) -> Result<(), VfsError> { if self.data.is_empty() && self.size > 0 { let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let data = self .runtime .block_on(client.read_file(&mut self.tree, &self.path)) .map_err(map_smb_error)?; self.size = data.len() as u64; self.data = data; } Ok(()) } } impl VfsFile for SmbVfsFile { fn read(&mut self, buf: &mut [u8]) -> Result { if self.position >= self.size { return Ok(0); } // Streaming read using file_id if let Some(file_id) = &self.file_id { let offset = self.position; let to_read = std::cmp::min(buf.len() as u32, self.read_chunk_size); let remaining = self.size - self.position; let actual_read = std::cmp::min(to_read as u64, remaining) as u32; if actual_read == 0 { return Ok(0); } use smb2::msg::read::ReadRequest; use smb2::types::{Command, FileId}; let req = ReadRequest { padding: 0, flags: 0, length: actual_read, offset, file_id: FileId { persistent: file_id.persistent, volatile: file_id.volatile, }, minimum_count: 0, channel: 0, remaining_bytes: 0, read_channel_info: Vec::new(), }; let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?; let tree_id = self.tree.tree_id; let response = self.runtime .block_on(client.connection_mut().execute(Command::Read, &req, Some(tree_id))) .map_err(map_smb_error)?; use smb2::pack::{ReadCursor, Unpack}; use smb2::msg::read::ReadResponse; let mut cursor = ReadCursor::new(&response.body); let read_resp = ReadResponse::unpack(&mut cursor) .map_err(|e| VfsError::Io(format!("Failed to parse ReadResponse: {}", e)))?; let bytes_read = read_resp.data.len(); buf[..bytes_read].copy_from_slice(&read_resp.data); self.position += bytes_read as u64; Ok(bytes_read) } else { // Buffered read (fallback) self.ensure_data_loaded()?; let start = self.position as usize; let available = self.size as usize - start; let to_copy = std::cmp::min(buf.len(), available); buf[..to_copy].copy_from_slice(&self.data[start..start + to_copy]); self.position += to_copy as u64; Ok(to_copy) } } fn write(&mut self, buf: &[u8]) -> Result { if self.file_writer.is_none() { let tree_arc = Arc::new(self.tree.clone()); let conn = { let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; client.connection_mut().clone() }; let writer = self .runtime .block_on(tree_arc.create_file_writer(conn, &self.path)) .map_err(map_smb_error)?; self.file_writer = Some(writer); } if let Some(writer) = &mut self.file_writer { self.runtime .block_on(writer.write_chunk(buf)) .map_err(map_smb_error)?; } self.position += buf.len() as u64; Ok(buf.len()) } fn seek(&mut self, pos: std::io::SeekFrom) -> Result { match pos { std::io::SeekFrom::Start(offset) => { self.position = offset; Ok(offset) } std::io::SeekFrom::End(offset) => { let new_pos = if offset >= 0 { self.size + offset as u64 } else { self.size.saturating_sub((-offset) as u64) }; self.position = new_pos; Ok(new_pos) } std::io::SeekFrom::Current(offset) => { let new_pos = if offset >= 0 { self.position + offset as u64 } else { self.position.saturating_sub((-offset) as u64) }; self.position = new_pos; Ok(new_pos) } } } fn flush(&mut self) -> Result<(), VfsError> { if let FileMode::Write = self.mode { if let Some(writer) = self.file_writer.take() { let total = self .runtime .block_on(writer.finish()) .map_err(map_smb_error)?; self.size = total; } else if !self.write_buf.is_empty() { let data = std::mem::take(&mut self.write_buf); let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; self.runtime .block_on(client.write_file(&mut self.tree, &self.path, &data)) .map_err(map_smb_error)?; self.size = data.len() as u64; } } Ok(()) } fn stat(&mut self) -> Result { let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let info = self .runtime .block_on(client.stat(&mut self.tree, &self.path)) .map_err(map_smb_error)?; Ok(VfsStat { size: info.size, mode: if info.is_directory { 0o755 } else { 0o644 }, uid: 0, gid: 0, atime: filetime_to_systemtime(info.accessed.0), mtime: filetime_to_systemtime(info.modified.0), is_dir: info.is_directory, is_symlink: false, }) } fn set_len(&mut self, size: u64) -> Result<(), VfsError> { if !self.write_buf.is_empty() { self.flush()?; } let path = self.path.clone(); let tree_id = self.tree.tree_id; let mut client = self .client .lock() .map_err(|e| VfsError::Io(e.to_string()))?; let conn = client.connection_mut(); use smb2::client::connection::CompoundOp; use smb2::msg::close::CloseRequest; use smb2::msg::create::{ CreateDisposition, CreateRequest, CreateResponse, ImpersonationLevel, ShareAccess, }; use smb2::msg::query_info::InfoType; use smb2::msg::set_info::SetInfoRequest; use smb2::pack::{ReadCursor, Unpack}; use smb2::types::flags::FileAccessMask; use smb2::types::status::NtStatus; use smb2::types::{Command, CreditCharge, FileId, OplockLevel}; const FILE_END_OF_FILE_INFORMATION: u8 = 14; let create_req = CreateRequest { requested_oplock_level: OplockLevel::None, impersonation_level: ImpersonationLevel::Impersonation, desired_access: FileAccessMask::new( FileAccessMask::FILE_WRITE_DATA | FileAccessMask::SYNCHRONIZE, ), file_attributes: 0, share_access: ShareAccess( ShareAccess::FILE_SHARE_READ | ShareAccess::FILE_SHARE_WRITE | ShareAccess::FILE_SHARE_DELETE, ), create_disposition: CreateDisposition::FileOpen, create_options: 0, name: path, create_contexts: vec![], }; let setinfo_buf = size.to_le_bytes().to_vec(); let setinfo_req = SetInfoRequest { info_type: InfoType::File, file_info_class: FILE_END_OF_FILE_INFORMATION, additional_information: 0, file_id: FileId::SENTINEL, buffer: setinfo_buf, }; let close_req = CloseRequest { flags: 0, file_id: FileId::SENTINEL, }; let ops = [ CompoundOp { command: Command::Create, body: &create_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, CompoundOp { command: Command::SetInfo, body: &setinfo_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, CompoundOp { command: Command::Close, body: &close_req, tree_id: Some(tree_id), credit_charge: CreditCharge(1), }, ]; let responses = self.runtime.block_on(async { let frames = conn .execute_compound(&ops) .await .map_err(|e| VfsError::Io(format!("SMB set_len compound failed: {}", e)))?; let frames: Vec<_> = frames .into_iter() .collect::, _>>() .map_err(|e| VfsError::Io(format!("SMB set_len waiter error: {}", e)))?; Ok::<_, VfsError>(frames) })?; let create_header = &responses[0].header; let create_body = &responses[0].body; let setinfo_header = &responses[1].header; if create_header.status != NtStatus::SUCCESS { return Err(VfsError::NotFound(format!( "SMB set_len: file not found ({})", create_header.status ))); } if setinfo_header.status != NtStatus::SUCCESS { let mut cursor = ReadCursor::new(create_body); if let Ok(create_resp) = CreateResponse::unpack(&mut cursor) { let standalone_close = CloseRequest { flags: 0, file_id: create_resp.file_id, }; let _: Result<_, _> = self.runtime.block_on( conn.execute(Command::Close, &standalone_close, Some(tree_id)), ); } return Err(VfsError::Io(format!( "SMB set_len: SET_INFO failed ({})", setinfo_header.status ))); } self.size = size; if (size as usize) < self.data.len() { self.data.truncate(size as usize); } Ok(()) } } impl Drop for SmbVfsFile { fn drop(&mut self) { // Close file handle for streaming read if let Some(file_id) = self.file_id.take() { if let Ok(mut client) = self.client.lock() { use smb2::msg::close::CloseRequest; use smb2::types::{Command, FileId}; let req = CloseRequest { flags: 0, file_id: FileId { persistent: file_id.persistent, volatile: file_id.volatile, }, }; let tree_id = self.tree.tree_id; let _: Result<_, _> = self.runtime.block_on( client.connection_mut().execute(Command::Close, &req, Some(tree_id)), ); } } // Finish streaming write if let FileMode::Write = self.mode { if let Some(writer) = self.file_writer.take() { let _ = self.runtime.block_on(writer.finish()); } else if !self.write_buf.is_empty() { let data = std::mem::take(&mut self.write_buf); if let Ok(mut client) = self.client.lock() { let _ = self.runtime .block_on(client.write_file(&mut self.tree, &self.path, &data)); } } } } } #[cfg(test)] mod tests { use smb2::types::status::NtStatus; use smb2::types::Command; use super::*; // ── filetime_to_systemtime ────────────────────────────────────────────── #[test] fn test_filetime_conversion() { let raw: u64 = 133604700000000000; let st = filetime_to_systemtime(raw); assert!(st > UNIX_EPOCH); } #[test] fn test_filetime_below_epoch_returns_epoch() { let st = filetime_to_systemtime(0); assert_eq!(st, UNIX_EPOCH); } #[test] fn test_filetime_at_exact_unix_epoch_boundary() { let ft = FILETIME_TO_UNIX_SECS * 10_000_000; let st = filetime_to_systemtime(ft); assert_eq!(st, UNIX_EPOCH); } // ── path_to_str ───────────────────────────────────────────────────────── #[test] fn test_path_to_str() { assert_eq!(SmbVfs::path_to_str(Path::new("foo/bar.txt")), "foo/bar.txt"); assert_eq!( SmbVfs::path_to_str(Path::new("/foo/bar.txt")), "foo/bar.txt" ); assert_eq!(SmbVfs::path_to_str(Path::new("")), ""); assert_eq!(SmbVfs::path_to_str(Path::new("/")), ""); assert_eq!( SmbVfs::path_to_str(Path::new("/a/b/c/d/e/f/g.txt")), "a/b/c/d/e/f/g.txt" ); } // ── map_smb_error — all ErrorKind variants ────────────────────────────── fn proto_err(status: NtStatus) -> smb2::Error { smb2::Error::Protocol { status, command: Command::Read, } } #[test] fn test_map_smb_error_not_found() { let err = proto_err(NtStatus::NO_SUCH_FILE); assert!(matches!(map_smb_error(err), VfsError::NotFound(_))); } #[test] fn test_map_smb_error_already_exists() { let err = proto_err(NtStatus::OBJECT_NAME_COLLISION); assert!(matches!(map_smb_error(err), VfsError::AlreadyExists(_))); } #[test] fn test_map_smb_error_access_denied() { let err = proto_err(NtStatus::ACCESS_DENIED); assert!(matches!(map_smb_error(err), VfsError::PermissionDenied(_))); } #[test] fn test_map_smb_error_is_a_directory() { let err = proto_err(NtStatus::FILE_IS_A_DIRECTORY); assert!(matches!(map_smb_error(err), VfsError::IsADirectory(_))); } #[test] fn test_map_smb_error_not_a_directory() { let err = proto_err(NtStatus::NOT_A_DIRECTORY); assert!(matches!(map_smb_error(err), VfsError::NotADirectory(_))); } #[test] fn test_map_smb_error_disk_full() { let err = proto_err(NtStatus::DISK_FULL); assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_sharing_violation() { let err = proto_err(NtStatus::SHARING_VIOLATION); assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_connection_lost() { let err = smb2::Error::Disconnected; assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_timeout() { let err = smb2::Error::Timeout; assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_session_expired() { let err = smb2::Error::SessionExpired; assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_invalid_data() { let err = smb2::Error::InvalidData { message: "bad data".into(), }; assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_auth() { let err = smb2::Error::Auth { message: "auth fail".into(), }; // AuthRequired falls through to the catch-all _ => VfsError::Io assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_io() { let err = smb2::Error::Io(std::io::Error::other("io error")); assert!(matches!(map_smb_error(err), VfsError::Io(_))); } #[test] fn test_map_smb_error_cancelled() { let err = smb2::Error::Cancelled; assert!(matches!(map_smb_error(err), VfsError::Io(_))); } // VfsFile::set_len returns Unsupported — verified via integration tests // since SmbVfsFile requires a real SMB connection to construct. // ── Integration tests (ignored, require Docker Samba) ─────────────────── /// Integration test: requires Docker Samba container on port 10445. /// Run with: docker compose -f vendor/smb2/tests/docker/internal/docker-compose.yml up -d smb-guest #[test] #[ignore] fn test_smb_vfs_list_root() { let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap(); let entries = vfs.read_dir(Path::new("/")).unwrap(); assert!(!entries.is_empty(), "Expected entries"); } #[test] #[ignore] fn test_smb_vfs_write_read_file() { let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap(); let content = b"Hello SMB VFS!"; let path = Path::new("/smb_vfs_test.txt"); // Write { let flags = OpenFlags::new().write().create().truncate(); let mut file = vfs.open_file(path, &flags).unwrap(); file.write(content).unwrap(); file.flush().unwrap(); } // Read back let flags = OpenFlags::new().read(); let mut file = vfs.open_file(path, &flags).unwrap(); let mut buf = vec![0u8; 1024]; let n = file.read(&mut buf).unwrap(); assert_eq!(&buf[..n], content); // Stat let stat = vfs.stat(path).unwrap(); assert_eq!(stat.size, content.len() as u64); // Cleanup vfs.remove_file(path).unwrap(); assert!(!vfs.exists(path)); } #[test] #[ignore] fn test_smb_vfs_create_remove_dir() { let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap(); let dir_path = Path::new("/smb_vfs_test_dir"); vfs.create_dir(dir_path, 0o755).unwrap(); assert!(vfs.exists(dir_path)); vfs.remove_dir(dir_path).unwrap(); assert!(!vfs.exists(dir_path)); } #[test] #[ignore] fn test_smb_vfs_rename_file() { let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap(); let src = Path::new("/rename_src.txt"); let dst = Path::new("/rename_dst.txt"); // Create source file let flags = OpenFlags::new().write().create().truncate(); { let mut file = vfs.open_file(src, &flags).unwrap(); file.write(b"rename test").unwrap(); file.flush().unwrap(); } // Rename vfs.rename(src, dst).unwrap(); assert!(!vfs.exists(src)); assert!(vfs.exists(dst)); // Cleanup vfs.remove_file(dst).unwrap(); } }