diff --git a/Cargo.lock b/Cargo.lock index 00ec7a7..3ebb91b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5486,6 +5486,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "xattr", ] [[package]] diff --git a/data/auth.sqlite b/data/auth.sqlite index c198192..a6d0b98 100644 Binary files a/data/auth.sqlite and b/data/auth.sqlite differ diff --git a/vendor/smb-server/Cargo.toml b/vendor/smb-server/Cargo.toml index 06eff6c..7ac9fc7 100644 --- a/vendor/smb-server/Cargo.toml +++ b/vendor/smb-server/Cargo.toml @@ -25,6 +25,7 @@ aes = "0.8" cmac = "0.7" rc4 = "0.2" ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach) +xattr = "1.0" # Extended attributes support (AFP_AfpInfo) [features] default = ["localfs"] diff --git a/vendor/smb-server/src/backend.rs b/vendor/smb-server/src/backend.rs index a99352a..2c360c5 100644 --- a/vendor/smb-server/src/backend.rs +++ b/vendor/smb-server/src/backend.rs @@ -171,6 +171,28 @@ pub trait ShareBackend: Send + Sync + 'static { /// Static capabilities. The dispatcher consults these at TREE_CONNECT and /// uses `is_read_only` to clamp authz. fn capabilities(&self) -> BackendCapabilities; + + // ===== Extended Attributes (xattr) support ===== + + /// Get extended attribute value + async fn get_xattr(&self, _path: &SmbPath, _name: &str) -> SmbResult> { + Err(SmbError::NotSupported) + } + + /// Set extended attribute value + async fn set_xattr(&self, _path: &SmbPath, _name: &str, _value: &[u8]) -> SmbResult<()> { + Err(SmbError::NotSupported) + } + + /// Remove extended attribute + async fn remove_xattr(&self, _path: &SmbPath, _name: &str) -> SmbResult<()> { + Err(SmbError::NotSupported) + } + + /// List extended attribute names + async fn list_xattrs(&self, _path: &SmbPath) -> SmbResult> { + Err(SmbError::NotSupported) + } } /// A live open file or directory handle. @@ -277,3 +299,113 @@ impl Handle for NullHandle { Ok(()) } } + +// --------------------------------------------------------------------------- +// AFP_AfpInfo Handle (extended attribute virtual handle) +// --------------------------------------------------------------------------- + +const AFP_INFO_XATTR_NAME: &str = "com.apple.aapl.AfpInfo"; +const AFP_INFO_SIZE: usize = 32; + +pub struct AfpInfoHandle { + base_path: SmbPath, + backend: std::sync::Arc, + buffer: tokio::sync::RwLock>, + read_only: bool, +} + +impl AfpInfoHandle { + pub fn new(base_path: SmbPath, backend: std::sync::Arc, read_only: bool) -> Self { + Self { + base_path, + backend, + buffer: tokio::sync::RwLock::new(vec![0u8; AFP_INFO_SIZE]), + read_only, + } + } + + pub async fn load_from_xattr(&self) -> SmbResult<()> { + let data = self.backend.get_xattr(&self.base_path, AFP_INFO_XATTR_NAME).await?; + let mut buffer = self.buffer.write().await; + buffer.clear(); + buffer.extend_from_slice(&data); + if buffer.len() < AFP_INFO_SIZE { + buffer.resize(AFP_INFO_SIZE, 0); + } + Ok(()) + } + + pub async fn save_to_xattr(&self) -> SmbResult<()> { + if self.read_only { + return Err(SmbError::AccessDenied); + } + let buffer = self.buffer.read().await; + let data = buffer.clone(); + self.backend.set_xattr(&self.base_path, AFP_INFO_XATTR_NAME, &data[..]).await?; + Ok(()) + } +} + +#[async_trait] +impl Handle for AfpInfoHandle { + async fn read(&self, offset: u64, len: u32) -> SmbResult { + self.load_from_xattr().await?; + let buffer = self.buffer.read().await; + let start = offset as usize; + let end = std::cmp::min(start + len as usize, buffer.len()); + if start >= buffer.len() { + return Ok(bytes::Bytes::new()); + } + Ok(bytes::Bytes::copy_from_slice(&buffer[start..end])) + } + + async fn write(&self, offset: u64, data: &[u8]) -> SmbResult { + if self.read_only { + return Err(SmbError::AccessDenied); + } + let mut buffer = self.buffer.write().await; + let start = offset as usize; + if start + data.len() > AFP_INFO_SIZE { + return Err(SmbError::NotSupported); + } + buffer[start..start + data.len()].copy_from_slice(data); + Ok(data.len() as u32) + } + + async fn flush(&self) -> SmbResult<()> { + self.save_to_xattr().await + } + + async fn stat(&self) -> SmbResult { + Ok(FileInfo { + name: self.base_path.file_name().unwrap_or("").to_string(), + end_of_file: AFP_INFO_SIZE as u64, + allocation_size: AFP_INFO_SIZE as u64, + creation_time: 0, + last_access_time: 0, + last_write_time: 0, + change_time: 0, + is_directory: false, + file_index: 0, + }) + } + + async fn set_times(&self, _times: FileTimes) -> SmbResult<()> { + Err(SmbError::NotSupported) + } + + async fn truncate(&self, len: u64) -> SmbResult<()> { + if len != AFP_INFO_SIZE as u64 { + return Err(SmbError::NotSupported); + } + Ok(()) + } + + async fn list_dir(&self, _pattern: Option<&str>) -> SmbResult> { + Err(SmbError::NotSupported) + } + + async fn close(self: Box) -> SmbResult<()> { + self.save_to_xattr().await + } +} diff --git a/vendor/smb-server/src/fs/local.rs b/vendor/smb-server/src/fs/local.rs index 3e4eff5..b9851f9 100644 --- a/vendor/smb-server/src/fs/local.rs +++ b/vendor/smb-server/src/fs/local.rs @@ -428,6 +428,71 @@ impl ShareBackend for LocalFsBackend { case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")), } } + + async fn get_xattr(&self, path: &SmbPath, name: &str) -> SmbResult> { + let rel = to_rel_path(path); + let root = Arc::clone(&self.root); + let xattr_name = name.to_string(); + + spawn_blocking(move || { + let full_path = root.join(&rel); + xattr::get(full_path.as_std_path(), &xattr_name) + .map_err(|e| SmbError::Io(e.into())) + }) + .await + .map_err(join_to_io)? + } + + async fn set_xattr(&self, path: &SmbPath, name: &str, value: &[u8]) -> SmbResult<()> { + if self.read_only { + return Err(SmbError::AccessDenied); + } + + let rel = to_rel_path(path); + let root = Arc::clone(&self.root); + let xattr_name = name.to_string(); + let value = value.to_vec(); + + spawn_blocking(move || { + let full_path = root.join(&rel); + xattr::set(full_path.as_std_path(), &xattr_name, &value) + .map_err(|e| SmbError::Io(e.into())) + }) + .await + .map_err(join_to_io)? + } + + async fn remove_xattr(&self, path: &SmbPath, name: &str) -> SmbResult<()> { + if self.read_only { + return Err(SmbError::AccessDenied); + } + + let rel = to_rel_path(path); + let root = Arc::clone(&self.root); + let xattr_name = name.to_string(); + + spawn_blocking(move || { + let full_path = root.join(&rel); + xattr::remove(full_path.as_std_path(), &xattr_name) + .map_err(|e| SmbError::Io(e.into())) + }) + .await + .map_err(join_to_io)? + } + + async fn list_xattrs(&self, path: &SmbPath) -> SmbResult> { + let rel = to_rel_path(path); + let root = Arc::clone(&self.root); + + spawn_blocking(move || { + let full_path = root.join(&rel); + xattr::list(full_path.as_std_path()) + .map(|attrs| attrs.into_iter().map(|s| s.to_string()).collect()) + .map_err(|e| SmbError::Io(e.into())) + }) + .await + .map_err(join_to_io)? + } } // --------------------------------------------------------------------------- diff --git a/vendor/smb-server/src/handlers/create.rs b/vendor/smb-server/src/handlers/create.rs index 9a05a8b..3f334f4 100644 --- a/vendor/smb-server/src/handlers/create.rs +++ b/vendor/smb-server/src/handlers/create.rs @@ -1,6 +1,7 @@ //! CREATE handler — open or create a file/directory and allocate a FileId. use std::sync::Arc; +use tokio::sync::RwLock; use crate::proto::header::Smb2Header; use crate::proto::messages::{CreateRequest, CreateResponse}; @@ -85,11 +86,58 @@ pub async fn handle( if stream_path.is_afp_info() { debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_AfpInfo named stream open"); - // For AFP_AfpInfo, we return a virtual handle that reads/writes extended attributes - // TODO: Implement actual AFP_AfpInfo handling via extended attributes + // Create AfpInfoHandle to read/write extended attributes + let base_smb_path = stream_path.base_path().clone(); - // Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 2.6 will implement) - return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND); + // Check write permission + let want_write = req.desired_access & (FILE_WRITE_DATA | GENERIC_WRITE | GENERIC_ALL) != 0; + if want_write && !granted.allows_write() { + return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED); + } + + // Allocate file_id + let mut tree = tree_arc.write().await; + let file_id = tree.alloc_file_id(); + let read_only = tree.share.backend.capabilities().is_read_only; + + let handle = crate::backend::AfpInfoHandle::new(base_smb_path.clone(), backend, want_write && read_only); + + let open = Open::new( + file_id, + Box::new(handle), + if want_write { granted } else { Access::Read }, + base_smb_path, + false, // is_directory + false, // delete_on_close + 0, // oplock_level + 0, // share_access + ); + + let open_arc = Arc::new(RwLock::new(open)); + tree.opens.write().await.insert(file_id, open_arc.clone()); + drop(tree); + + let resp = CreateResponse { + structure_size: 89, + oplock_level: 0, + flags: 0, + create_action: FILE_OPENED, + creation_time: 0, + last_access_time: 0, + last_write_time: 0, + change_time: 0, + allocation_size: 32, + end_of_file: 32, + file_attributes: 0, + reserved2: 0, + file_id, + create_contexts_offset: 0, + create_contexts_length: 0, + create_contexts: vec![], + }; + let mut buf = Vec::new(); + resp.write_to(&mut buf).expect("encode"); + return HandlerResponse::ok(buf); } // Handle AFP_Resource named stream