use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::Mutex; use async_trait::async_trait; use bytes::Bytes; use smb_server::{ BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, OpenIntent, OpenOptions, ShareBackend, SmbError, SmbPath, }; use super::open_flags::OpenFlags; use super::{VfsBackend, VfsError, VfsStat}; const FILETIME_OFFSET: u64 = 116_444_736_000_000_000; pub struct VfsShareBackend { vfs: Arc, root: PathBuf, read_only: bool, } impl VfsShareBackend { pub fn new(vfs: Box, root: PathBuf) -> Self { Self { vfs: Arc::from(vfs), root, read_only: false, } } pub fn read_only(mut self, yes: bool) -> Self { self.read_only = yes; self } } fn resolve_path(root: &Path, smb_path: &SmbPath) -> PathBuf { if smb_path.is_root() { return root.to_path_buf(); } let mut result = root.to_path_buf(); for component in smb_path.components() { result.push(component); } result } fn map_error(e: VfsError) -> SmbError { match e { VfsError::NotFound(_) => SmbError::NotFound, VfsError::PermissionDenied(_) => SmbError::AccessDenied, VfsError::AlreadyExists(_) => SmbError::Exists, VfsError::NotEmpty(_) => SmbError::NotEmpty, VfsError::NotADirectory(_) => SmbError::NotADirectory, VfsError::IsADirectory(_) => SmbError::IsDirectory, VfsError::Unsupported(_) => SmbError::NotSupported, VfsError::Io(msg) => SmbError::Io(std::io::Error::other(msg)), VfsError::UnexpectedEof => SmbError::Io(std::io::Error::other("unexpected eof")), } } fn system_time_to_filetime(t: SystemTime) -> u64 { match t.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => FILETIME_OFFSET + (d.as_secs() * 10_000_000) + (d.subsec_nanos() as u64 / 100), Err(_) => 0, } } fn vfs_stat_to_file_info(stat: &VfsStat, name: &str, path: &Path) -> FileInfo { let name = if name.is_empty() { path.file_name() .and_then(|s| s.to_str()) .unwrap_or("") .to_string() } else { name.to_string() }; FileInfo { name, end_of_file: stat.size, allocation_size: stat.size, creation_time: system_time_to_filetime(stat.mtime), last_access_time: system_time_to_filetime(stat.atime), last_write_time: system_time_to_filetime(stat.mtime), change_time: system_time_to_filetime(stat.mtime), is_directory: stat.is_dir, file_index: 0, } } fn vfs_error_to_io(e: VfsError) -> std::io::Error { std::io::Error::other(e.to_string()) } #[async_trait] impl ShareBackend for VfsShareBackend { async fn open(&self, path: &SmbPath, opts: OpenOptions) -> Result, SmbError> { let full_path = resolve_path(&self.root, path); if opts.directory { match opts.intent { OpenIntent::Create => { if self.vfs.exists(&full_path) { return Err(SmbError::Exists); } self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?; } OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => { if !self.vfs.exists(&full_path) { self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?; } } _ => { if !self.vfs.exists(&full_path) { return Err(SmbError::NotFound); } } } let stat = self.vfs.stat(&full_path).map_err(map_error)?; if !stat.is_dir { return Err(SmbError::NotADirectory); } return Ok(Box::new(VfsHandle::Directory { vfs: self.vfs.clone(), path: full_path, })); } let mut flags = OpenFlags::new(); if opts.read { flags = flags.read(); } if opts.write { flags = flags.write(); } match opts.intent { OpenIntent::Open => {} OpenIntent::Create => { flags = flags.create().exclusive(); } OpenIntent::OpenOrCreate => { flags = flags.create(); } OpenIntent::OverwriteOrCreate => { flags = flags.create().truncate(); } OpenIntent::Truncate => { flags = flags.truncate(); } } if opts.non_directory && self.vfs.exists(&full_path) { let stat = self.vfs.stat(&full_path).map_err(map_error)?; if stat.is_dir { return Err(SmbError::IsDirectory); } } let file = self.vfs.open_file(&full_path, &flags).map_err(map_error)?; Ok(Box::new(VfsHandle::File { file: Mutex::new(file), path: full_path, vfs: self.vfs.clone(), })) } async fn unlink(&self, path: &SmbPath) -> Result<(), SmbError> { let full_path = resolve_path(&self.root, path); if self.vfs.exists(&full_path) { let stat = self.vfs.stat(&full_path).map_err(map_error)?; if stat.is_dir { return self.vfs.remove_dir(&full_path).map_err(map_error); } } self.vfs.remove_file(&full_path).map_err(map_error) } async fn rename(&self, from: &SmbPath, to: &SmbPath) -> Result<(), SmbError> { let from_path = resolve_path(&self.root, from); let to_path = resolve_path(&self.root, to); if self.vfs.exists(&to_path) { return Err(SmbError::Exists); } self.vfs.rename(&from_path, &to_path).map_err(map_error) } fn capabilities(&self) -> BackendCapabilities { BackendCapabilities { is_read_only: self.read_only, case_sensitive: true, } } } enum VfsHandle { File { file: Mutex>, path: PathBuf, vfs: Arc, }, Directory { vfs: Arc, path: PathBuf, }, } #[async_trait] impl Handle for VfsHandle { async fn read(&self, offset: u64, len: u32) -> Result { match self { Self::File { file, .. } => { let mut file = file.lock().await; let mut buf = vec![0u8; len as usize]; let n = file.read_at(&mut buf, offset).map_err(map_error)?; buf.truncate(n); Ok(Bytes::from(buf)) } Self::Directory { .. } => Err(SmbError::NotSupported), } } async fn write(&self, offset: u64, data: &[u8]) -> Result { match self { Self::File { file, .. } => { let mut file = file.lock().await; let n = file.write_at(data, offset).map_err(map_error)?; Ok(n as u32) } Self::Directory { .. } => Err(SmbError::NotSupported), } } async fn flush(&self) -> Result<(), SmbError> { match self { Self::File { file, .. } => { let mut file = file.lock().await; file.flush().map_err(map_error) } Self::Directory { .. } => Ok(()), } } async fn stat(&self) -> Result { match self { Self::File { file, path, .. } => { let mut f = file.lock().await; let vfs_stat = f.stat().map_err(map_error)?; Ok(vfs_stat_to_file_info(&vfs_stat, "", path)) } Self::Directory { vfs, path } => { let vfs_stat = vfs.stat(path).map_err(map_error)?; Ok(vfs_stat_to_file_info(&vfs_stat, "", path)) } } } async fn set_times(&self, times: FileTimes) -> Result<(), SmbError> { let (vfs, path) = match self { Self::File { path, vfs, .. } => (vfs, path), Self::Directory { vfs, path } => (vfs, path), }; let mut stat = VfsStat::new(); if let Some(t) = times.last_write_time { stat.mtime = filetime_to_systemtime(t); } if let Some(t) = times.last_access_time { stat.atime = filetime_to_systemtime(t); } vfs.set_stat(path, &stat).map_err(map_error) } async fn truncate(&self, len: u64) -> Result<(), SmbError> { match self { Self::File { file, .. } => { let mut file = file.lock().await; file.set_len(len).map_err(map_error) } Self::Directory { .. } => Err(SmbError::NotSupported), } } async fn list_dir(&self, pattern: Option<&str>) -> Result, SmbError> { match self { Self::File { .. } => Err(SmbError::NotADirectory), Self::Directory { vfs, path } => { let entries = vfs.read_dir(path).map_err(map_error)?; let mut result: Vec = entries .into_iter() .filter(|entry| { let p = match pattern { None => return true, Some(p) => p, }; if p == "*" || p == "*.*" || p.is_empty() { return true; } smb_match(&entry.name, p) }) .map(|entry| { let info = vfs_stat_to_file_info(&entry.stat, &entry.name, path); DirEntry { info } }) .collect(); for (i, entry) in result.iter_mut().enumerate() { entry.info.file_index = (i + 1) as u64; } Ok(result) } } } async fn close(self: Box) -> Result<(), SmbError> { Ok(()) } } /// Simple SMB wildcard match: `*` matches any sequence, `?` matches one char. fn smb_match(name: &str, pattern: &str) -> bool { let name = name.as_bytes(); let pat = pattern.as_bytes(); let mut ni = 0; let mut pi = 0; let mut star_idx: Option = None; let mut match_idx = 0; while ni < name.len() { if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == name[ni]) { ni += 1; pi += 1; } else if pi < pat.len() && pat[pi] == b'*' { star_idx = Some(pi); match_idx = ni; pi += 1; } else if let Some(si) = star_idx { pi = si + 1; match_idx += 1; ni = match_idx; } else { return false; } } while pi < pat.len() && pat[pi] == b'*' { pi += 1; } pi == pat.len() } fn filetime_to_systemtime(ft: u64) -> SystemTime { if ft < FILETIME_OFFSET { return SystemTime::UNIX_EPOCH; } let delta_secs = (ft - FILETIME_OFFSET) / 10_000_000; let delta_ns = ((ft - FILETIME_OFFSET) % 10_000_000) as u32 * 100; SystemTime::UNIX_EPOCH + std::time::Duration::new(delta_secs, delta_ns) } #[cfg(test)] mod tests { use std::path::PathBuf; use smb_server::{Access, Share, SmbServer}; use crate::vfs::local_fs::LocalFs; use super::*; fn setup() -> (VfsShareBackend, tempfile::TempDir) { let tmp = tempfile::TempDir::new().unwrap(); let vfs = Box::new(LocalFs::new()); let backend = VfsShareBackend::new(vfs, tmp.path().to_path_buf()); (backend, tmp) } fn write_opts() -> OpenOptions { OpenOptions { read: true, write: true, intent: OpenIntent::OpenOrCreate, directory: false, non_directory: false, delete_on_close: false, } } fn read_opts() -> OpenOptions { OpenOptions { read: true, write: false, intent: OpenIntent::Open, directory: false, non_directory: false, delete_on_close: false, } } // ── resolve_path ────────────────────────────────────────────────────── #[test] fn test_resolve_path_root() { let root = PathBuf::from("/srv/share"); let smb = SmbPath::root(); assert_eq!(resolve_path(&root, &smb), root); } #[test] fn test_resolve_path_components() { let root = PathBuf::from("/srv/share"); let smb: SmbPath = "dir\\sub\\file.txt".parse().unwrap(); let expected = PathBuf::from("/srv/share/dir/sub/file.txt"); assert_eq!(resolve_path(&root, &smb), expected); } #[test] fn test_rejects_trailing_backslash() { assert!("dir\\".parse::().is_err()); } // ── FILETIME conversion ─────────────────────────────────────────────── #[test] fn test_system_time_to_filetime() { let epoch = SystemTime::UNIX_EPOCH; let ft = system_time_to_filetime(epoch); assert_eq!(ft, FILETIME_OFFSET); } #[test] fn test_filetime_roundtrip() { let now = SystemTime::now(); let ft = system_time_to_filetime(now); let back = filetime_to_systemtime(ft); let diff = if now > back { now.duration_since(back).unwrap() } else { back.duration_since(now).unwrap() }; assert!(diff.as_millis() < 100); } #[test] fn test_filetime_below_offset_returns_epoch() { let ft = filetime_to_systemtime(FILETIME_OFFSET - 1); assert_eq!(ft, SystemTime::UNIX_EPOCH); } #[test] fn test_filetime_exactly_offset() { let ft = filetime_to_systemtime(FILETIME_OFFSET); assert_eq!(ft, SystemTime::UNIX_EPOCH); } // ── VfsError → SmbError mapping ─────────────────────────────────────── #[test] fn test_map_errors() { assert!(matches!( map_error(VfsError::NotFound("x".into())), SmbError::NotFound )); assert!(matches!( map_error(VfsError::AlreadyExists("x".into())), SmbError::Exists )); assert!(matches!( map_error(VfsError::PermissionDenied("x".into())), SmbError::AccessDenied )); assert!(matches!( map_error(VfsError::NotEmpty("x".into())), SmbError::NotEmpty )); assert!(matches!( map_error(VfsError::NotADirectory("x".into())), SmbError::NotADirectory )); assert!(matches!( map_error(VfsError::IsADirectory("x".into())), SmbError::IsDirectory )); } #[test] fn test_map_error_unsupported() { let result = map_error(VfsError::Unsupported("test".into())); assert!(matches!(result, SmbError::NotSupported)); } #[test] fn test_map_error_unexpected_eof() { let result = map_error(VfsError::UnexpectedEof); assert!(matches!(result, SmbError::Io(_))); } // ── Backend creation / capabilities ─────────────────────────────────── #[test] fn test_backend_default_not_read_only() { let (_backend, _tmp) = setup(); // already tested via setup() — keep for clarity } #[test] fn test_backend_read_only_flag() { let tmp = tempfile::TempDir::new().unwrap(); let vfs = Box::new(LocalFs::new()); let backend = VfsShareBackend::new(vfs, tmp.path().to_path_buf()).read_only(true); assert!(backend.capabilities().is_read_only); } #[test] fn test_backend_case_sensitive() { let (_backend, _tmp) = setup(); assert!(_backend.capabilities().case_sensitive); } // ── VfsHandle File: full lifecycle ──────────────────────────────────── #[tokio::test] async fn test_handle_file_create_write_read_flush_close() { let (backend, _tmp) = setup(); let path: SmbPath = "test_file.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); let n = handle.write(0, b"Hello SMB!").await.unwrap(); assert_eq!(n, 10); handle.flush().await.unwrap(); let bytes = handle.read(0, 100).await.unwrap(); assert_eq!(&bytes[..], b"Hello SMB!"); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_read_after_close_fails() { let (backend, _tmp) = setup(); let path: SmbPath = "read_after_close.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"data").await.unwrap(); handle.flush().await.unwrap(); handle.close().await.unwrap(); // Re-open for read let handle2 = backend.open(&path, read_opts()).await.unwrap(); let bytes = handle2.read(0, 100).await.unwrap(); assert_eq!(&bytes[..], b"data"); handle2.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_stat() { let (backend, _tmp) = setup(); let path: SmbPath = "stat_file.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"1234567890").await.unwrap(); handle.flush().await.unwrap(); let info = handle.stat().await.unwrap(); assert_eq!(info.end_of_file, 10); assert!(!info.is_directory); assert!(!info.name.is_empty()); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_truncate_to_zero() { let (backend, _tmp) = setup(); let path: SmbPath = "trunc_file.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"data to be removed").await.unwrap(); handle.flush().await.unwrap(); handle.truncate(0).await.unwrap(); let info = handle.stat().await.unwrap(); assert_eq!(info.end_of_file, 0); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_truncate_extend() { let (backend, _tmp) = setup(); let path: SmbPath = "extend_file.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"short").await.unwrap(); handle.flush().await.unwrap(); handle.truncate(100).await.unwrap(); let info = handle.stat().await.unwrap(); assert_eq!(info.end_of_file, 100); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_set_times() { let (backend, _tmp) = setup(); let path: SmbPath = "times_file.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"x").await.unwrap(); handle.flush().await.unwrap(); let ft = system_time_to_filetime(SystemTime::now()); let times = FileTimes { creation_time: Some(ft), last_access_time: Some(ft), last_write_time: Some(ft), change_time: Some(ft), }; handle.set_times(times).await.unwrap(); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_list_dir_error() { let (backend, _tmp) = setup(); let path: SmbPath = "not_a_dir.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); let result = handle.list_dir(None).await; assert!(matches!(result, Err(SmbError::NotADirectory))); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_file_write_past_end() { let (backend, _tmp) = setup(); let path: SmbPath = "sparse.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); let n = handle.write(500, b"sparse data").await.unwrap(); assert_eq!(n, 11); handle.flush().await.unwrap(); let info = handle.stat().await.unwrap(); assert_eq!(info.end_of_file, 511); handle.close().await.unwrap(); } // ── VfsHandle Directory: full lifecycle ─────────────────────────────── async fn create_dir_backend(backend: &VfsShareBackend, name: &str) -> Box { let dir_opts = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; backend .open(&name.parse::().unwrap(), dir_opts) .await .unwrap() } #[tokio::test] async fn test_handle_directory_create_stat_list_close() { let (backend, _tmp) = setup(); let handle = create_dir_backend(&backend, "testdir").await; let info = handle.stat().await.unwrap(); assert!(info.is_directory); let entries = handle.list_dir(None).await.unwrap(); assert!(entries.is_empty()); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_directory_list_contains_created_file() { let (backend, _tmp) = setup(); let _dir = create_dir_backend(&backend, "listdir").await; // Create a file inside let file_path: SmbPath = "listdir\\child.txt".parse().unwrap(); let fh = backend.open(&file_path, write_opts()).await.unwrap(); fh.write(0, b"child data").await.unwrap(); fh.flush().await.unwrap(); fh.close().await.unwrap(); // Reopen dir and list let dir_opts = OpenOptions { read: true, write: false, intent: OpenIntent::Open, directory: true, non_directory: false, delete_on_close: false, }; let dir_handle = backend .open(&"listdir".parse().unwrap(), dir_opts) .await .unwrap(); let entries = dir_handle.list_dir(None).await.unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].info.name, "child.txt"); assert!(!entries[0].info.is_directory); assert_eq!(entries[0].info.end_of_file, 10); dir_handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_directory_stat_after_creation() { let (backend, _tmp) = setup(); let handle = create_dir_backend(&backend, "statdir").await; let info = handle.stat().await.unwrap(); assert!(info.is_directory); assert_eq!(info.name, "statdir"); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_directory_read_error() { let (backend, _tmp) = setup(); let handle = create_dir_backend(&backend, "readdir").await; let result = handle.read(0, 10).await; assert!(matches!(result, Err(SmbError::NotSupported))); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_directory_write_error() { let (backend, _tmp) = setup(); let handle = create_dir_backend(&backend, "writedir").await; let result = handle.write(0, b"data").await; assert!(matches!(result, Err(SmbError::NotSupported))); handle.close().await.unwrap(); } #[tokio::test] async fn test_handle_directory_truncate_error() { let (backend, _tmp) = setup(); let handle = create_dir_backend(&backend, "truncdir").await; let result = handle.truncate(0).await; assert!(matches!(result, Err(SmbError::NotSupported))); handle.close().await.unwrap(); } // ── ShareBackend::open — file OpenIntent variants ────────────────────── #[tokio::test] async fn test_open_file_create_new_succeeds() { let (backend, tmp) = setup(); let path: SmbPath = "create_new.txt".parse().unwrap(); let opts = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let handle = backend.open(&path, opts).await.unwrap(); assert!(tmp.path().join("create_new.txt").exists()); handle.close().await.unwrap(); } #[tokio::test] async fn test_open_file_create_existing_fails() { let (backend, _tmp) = setup(); let path: SmbPath = "create_existing.txt".parse().unwrap(); let opts = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, opts).await.unwrap(); h1.close().await.unwrap(); let result = backend.open(&path, opts).await; assert!(matches!(result, Err(SmbError::Exists))); } #[tokio::test] async fn test_open_file_open_or_create_new() { let (backend, _tmp) = setup(); let path: SmbPath = "open_or_create_new.txt".parse().unwrap(); let opts = OpenOptions { read: true, write: true, intent: OpenIntent::OpenOrCreate, directory: false, non_directory: false, delete_on_close: false, }; let handle = backend.open(&path, opts).await.unwrap(); handle.close().await.unwrap(); } #[tokio::test] async fn test_open_file_open_or_create_existing() { let (backend, _tmp) = setup(); let path: SmbPath = "open_or_create_existing.txt".parse().unwrap(); let create = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, create).await.unwrap(); h1.write(0, b"original").await.unwrap(); h1.flush().await.unwrap(); h1.close().await.unwrap(); let open_or_create = OpenOptions { read: true, write: false, intent: OpenIntent::OpenOrCreate, directory: false, non_directory: false, delete_on_close: false, }; let h2 = backend.open(&path, open_or_create).await.unwrap(); let bytes = h2.read(0, 100).await.unwrap(); assert_eq!(&bytes[..], b"original"); h2.close().await.unwrap(); } #[tokio::test] async fn test_open_file_overwrite_create_new() { let (backend, _tmp) = setup(); let path: SmbPath = "overwrite_create.txt".parse().unwrap(); let opts = OpenOptions { read: true, write: true, intent: OpenIntent::OverwriteOrCreate, directory: false, non_directory: false, delete_on_close: false, }; let handle = backend.open(&path, opts).await.unwrap(); handle.close().await.unwrap(); } #[tokio::test] async fn test_open_file_overwrite_create_existing_truncates() { let (backend, _tmp) = setup(); let path: SmbPath = "overwrite_trunc.txt".parse().unwrap(); let create = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, create).await.unwrap(); h1.write(0, b"long data to truncate").await.unwrap(); h1.flush().await.unwrap(); h1.close().await.unwrap(); let overwrite = OpenOptions { read: true, write: true, intent: OpenIntent::OverwriteOrCreate, directory: false, non_directory: false, delete_on_close: false, }; let h2 = backend.open(&path, overwrite).await.unwrap(); let info = h2.stat().await.unwrap(); assert_eq!(info.end_of_file, 0); h2.close().await.unwrap(); } #[tokio::test] async fn test_open_file_truncate_existing() { let (backend, _tmp) = setup(); let path: SmbPath = "trunc_existing.txt".parse().unwrap(); let create = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, create).await.unwrap(); h1.write(0, b"data").await.unwrap(); h1.flush().await.unwrap(); h1.close().await.unwrap(); let trunc = OpenOptions { read: true, write: true, intent: OpenIntent::Truncate, directory: false, non_directory: false, delete_on_close: false, }; let h2 = backend.open(&path, trunc).await.unwrap(); let info = h2.stat().await.unwrap(); assert_eq!(info.end_of_file, 0); h2.close().await.unwrap(); } #[tokio::test] async fn test_open_file_truncate_nonexistent_fails() { let (backend, _tmp) = setup(); let path: SmbPath = "trunc_missing.txt".parse().unwrap(); let opts = OpenOptions { read: true, write: true, intent: OpenIntent::Truncate, directory: false, non_directory: false, delete_on_close: false, }; let result = backend.open(&path, opts).await; assert!(matches!(result, Err(SmbError::NotFound))); } // ── ShareBackend::open — directory OpenIntent variants ──────────────── #[tokio::test] async fn test_open_directory_create_new() { let (backend, _tmp) = setup(); let path: SmbPath = "newdir".parse().unwrap(); let opts = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let handle = backend.open(&path, opts).await.unwrap(); handle.close().await.unwrap(); } #[tokio::test] async fn test_open_directory_create_existing_fails() { let (backend, _tmp) = setup(); let path: SmbPath = "dir_exists".parse().unwrap(); let opts = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, opts).await.unwrap(); h1.close().await.unwrap(); let result = backend.open(&path, opts).await; assert!(matches!(result, Err(SmbError::Exists))); } #[tokio::test] async fn test_open_directory_open_existing() { let (backend, _tmp) = setup(); let path: SmbPath = "open_dir".parse().unwrap(); let create = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, create).await.unwrap(); h1.close().await.unwrap(); let open = OpenOptions { read: true, write: false, intent: OpenIntent::Open, directory: true, non_directory: false, delete_on_close: false, }; let h2 = backend.open(&path, open).await.unwrap(); h2.close().await.unwrap(); } #[tokio::test] async fn test_open_directory_open_or_create_new() { let (backend, tmp) = setup(); let path: SmbPath = "open_or_create_dir".parse().unwrap(); let opts = OpenOptions { read: true, write: false, intent: OpenIntent::OpenOrCreate, directory: true, non_directory: false, delete_on_close: false, }; let handle = backend.open(&path, opts).await.unwrap(); assert!(tmp.path().join("open_or_create_dir").exists()); handle.close().await.unwrap(); } #[tokio::test] async fn test_open_directory_open_or_create_existing() { let (backend, _tmp) = setup(); let path: SmbPath = "open_or_create_dir2".parse().unwrap(); let create = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let h1 = backend.open(&path, create).await.unwrap(); h1.close().await.unwrap(); let open_create = OpenOptions { read: true, write: false, intent: OpenIntent::OpenOrCreate, directory: true, non_directory: false, delete_on_close: false, }; let h2 = backend.open(&path, open_create).await.unwrap(); h2.close().await.unwrap(); } // ── ShareBackend::open — non_directory / directory flag ─────────────── #[tokio::test] async fn test_open_non_directory_flag_on_dir_fails() { let (backend, _tmp) = setup(); let dir_path: SmbPath = "not_a_file_dir".parse().unwrap(); let create_dir = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let dh = backend.open(&dir_path, create_dir).await.unwrap(); dh.close().await.unwrap(); let file_opts = OpenOptions { read: true, write: false, intent: OpenIntent::Open, directory: false, non_directory: true, delete_on_close: false, }; let result = backend.open(&dir_path, file_opts).await; assert!(matches!(result, Err(SmbError::IsDirectory))); } #[tokio::test] async fn test_open_directory_flag_on_file_fails() { let (backend, _tmp) = setup(); let file_path: SmbPath = "not_a_dir_file.txt".parse().unwrap(); let create_file = OpenOptions { read: true, write: true, intent: OpenIntent::Create, directory: false, non_directory: false, delete_on_close: false, }; let fh = backend.open(&file_path, create_file).await.unwrap(); fh.write(0, b"x").await.unwrap(); fh.flush().await.unwrap(); fh.close().await.unwrap(); let dir_opts = OpenOptions { read: true, write: false, intent: OpenIntent::Open, directory: true, non_directory: false, delete_on_close: false, }; let result = backend.open(&file_path, dir_opts).await; assert!(matches!(result, Err(SmbError::NotADirectory))); } // ── ShareBackend::unlink ────────────────────────────────────────────── #[tokio::test] async fn test_unlink_file() { let (backend, _tmp) = setup(); let path: SmbPath = "unlink_me.txt".parse().unwrap(); let handle = backend.open(&path, write_opts()).await.unwrap(); handle.write(0, b"x").await.unwrap(); handle.flush().await.unwrap(); handle.close().await.unwrap(); backend.unlink(&path).await.unwrap(); let result = backend.open(&path, read_opts()).await; assert!(matches!(result, Err(SmbError::NotFound))); } #[tokio::test] async fn test_unlink_directory() { let (backend, _tmp) = setup(); let path: SmbPath = "unlink_dir".parse().unwrap(); let opts = OpenOptions { read: true, write: false, intent: OpenIntent::Create, directory: true, non_directory: false, delete_on_close: false, }; let dh = backend.open(&path, opts).await.unwrap(); dh.close().await.unwrap(); backend.unlink(&path).await.unwrap(); let result = backend.open(&path, read_opts()).await; assert!(matches!(result, Err(SmbError::NotFound))); } #[tokio::test] async fn test_unlink_nonexistent_returns_not_found() { let (backend, _tmp) = setup(); let path: SmbPath = "does_not_exist.txt".parse().unwrap(); let result = backend.unlink(&path).await; assert!(matches!(result, Err(SmbError::NotFound))); } // ── ShareBackend::rename ────────────────────────────────────────────── #[tokio::test] async fn test_rename_file() { let (backend, _tmp) = setup(); let src: SmbPath = "rename_src.txt".parse().unwrap(); let dst: SmbPath = "rename_dst.txt".parse().unwrap(); let handle = backend.open(&src, write_opts()).await.unwrap(); handle.write(0, b"rename me").await.unwrap(); handle.flush().await.unwrap(); handle.close().await.unwrap(); backend.rename(&src, &dst).await.unwrap(); // Source gone assert!(matches!( backend.open(&src, read_opts()).await, Err(SmbError::NotFound) )); // Destination exists with content let dh = backend.open(&dst, read_opts()).await.unwrap(); let bytes = dh.read(0, 100).await.unwrap(); assert_eq!(&bytes[..], b"rename me"); dh.close().await.unwrap(); } #[tokio::test] async fn test_rename_nonexistent_source() { let (backend, _tmp) = setup(); let src: SmbPath = "no_source.txt".parse().unwrap(); let dst: SmbPath = "no_dst.txt".parse().unwrap(); let result = backend.rename(&src, &dst).await; assert!(matches!(result, Err(SmbError::NotFound))); } #[tokio::test] async fn test_rename_existing_target_fails() { let (backend, _tmp) = setup(); let src: SmbPath = "rename_src2.txt".parse().unwrap(); let dst: SmbPath = "rename_dst2.txt".parse().unwrap(); let hs = backend.open(&src, write_opts()).await.unwrap(); hs.close().await.unwrap(); let hd = backend.open(&dst, write_opts()).await.unwrap(); hd.close().await.unwrap(); let result = backend.rename(&src, &dst).await; assert!(matches!(result, Err(SmbError::Exists))); } // ── vfs_stat_to_file_info ───────────────────────────────────────────── #[test] fn test_vfs_stat_to_file_info_with_name() { let stat = VfsStat { size: 42, mode: 0o644, uid: 0, gid: 0, atime: SystemTime::UNIX_EPOCH, mtime: SystemTime::UNIX_EPOCH, is_dir: false, is_symlink: false, }; let info = vfs_stat_to_file_info(&stat, "custom_name", Path::new("/ignored")); assert_eq!(info.name, "custom_name"); assert_eq!(info.end_of_file, 42); assert!(!info.is_directory); } #[test] fn test_vfs_stat_to_file_info_directory() { let stat = VfsStat { size: 0, mode: 0o755, uid: 0, gid: 0, atime: SystemTime::UNIX_EPOCH, mtime: SystemTime::UNIX_EPOCH, is_dir: true, is_symlink: false, }; let info = vfs_stat_to_file_info(&stat, "", Path::new("/share/mydir")); assert_eq!(info.name, "mydir"); assert!(info.is_directory); } #[test] fn test_vfs_stat_to_file_info_alloc_size_equals_end_of_file() { let stat = VfsStat { size: 100, ..Default::default() }; let info = vfs_stat_to_file_info(&stat, "f", Path::new("/x")); assert_eq!(info.allocation_size, 100); } // ── filetime_to_systemtime ──────────────────────────────────────────── #[test] fn test_filetime_zero() { let st = filetime_to_systemtime(0); assert_eq!(st, SystemTime::UNIX_EPOCH); } // ── other ───────────────────────────────────────────────────────────── #[test] fn test_rejects_dotdot() { assert!("a\\..\\b".parse::().is_err()); } #[test] fn test_rejects_forbidden_chars() { for bad in ["ab", "a:b", "a\"b", "a|b", "a?b", "a*b"] { assert!(bad.parse::().is_err()); } } }