- Remove unused imports in server.rs (Body, HeaderValue, RwLock) - Remove unused imports in forward_acl.rs (tests still need Ipv4Addr) - Remove unused imports in host_key.rs (Read, Write) - Remove unused imports in kex_exchange.rs (HostKeyType) - Remove unused imports in known_hosts.rs (tests need Ipv4Addr) - Remove unused imports in multiplex.rs (Arc) - Auto-fix other unused imports via clippy --fix Tests: 303 passed, 0 failed (4 new tests added)
1062 lines
36 KiB
Rust
1062 lines
36 KiB
Rust
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<tokio::runtime::Runtime>,
|
|
client: Arc<Mutex<smb2::SmbClient>>,
|
|
tree: Arc<Mutex<smb2::Tree>>,
|
|
}
|
|
|
|
impl SmbVfs {
|
|
pub fn new(addr: &str, share: &str, username: &str, password: &str) -> Result<Self, VfsError> {
|
|
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<Self, VfsError> {
|
|
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<dyn VfsBackend> {
|
|
Box::new(self.clone())
|
|
}
|
|
|
|
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, 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<Box<dyn VfsFile>, 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<VfsStat, 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 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<VfsStat, VfsError> {
|
|
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::<std::result::Result<Vec<_>, _>>()
|
|
.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<PathBuf, VfsError> {
|
|
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<PathBuf, 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 _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<tokio::runtime::Runtime>,
|
|
client: Arc<Mutex<smb2::SmbClient>>,
|
|
tree: smb2::Tree,
|
|
path: String,
|
|
mode: FileMode,
|
|
position: u64,
|
|
write_buf: Vec<u8>,
|
|
data: Vec<u8>,
|
|
size: u64,
|
|
file_writer: Option<smb2::FileWriter>,
|
|
file_id: Option<smb2::types::FileId>,
|
|
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<usize, VfsError> {
|
|
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<usize, VfsError> {
|
|
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<u64, VfsError> {
|
|
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<VfsStat, VfsError> {
|
|
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::<std::result::Result<Vec<_>, _>>()
|
|
.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();
|
|
}
|
|
}
|