Implement SMB AFP_AfpInfo read/write via xattr (Phase 2.8 complete)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

This commit is contained in:
Warren
2026-06-22 15:16:59 +08:00
parent 25991c71b2
commit 1c8c47d5fa
6 changed files with 251 additions and 4 deletions

1
Cargo.lock generated
View File

@@ -5486,6 +5486,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
"xattr",
] ]
[[package]] [[package]]

Binary file not shown.

View File

@@ -25,6 +25,7 @@ aes = "0.8"
cmac = "0.7" cmac = "0.7"
rc4 = "0.2" rc4 = "0.2"
ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach) ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach)
xattr = "1.0" # Extended attributes support (AFP_AfpInfo)
[features] [features]
default = ["localfs"] default = ["localfs"]

View File

@@ -171,6 +171,28 @@ pub trait ShareBackend: Send + Sync + 'static {
/// Static capabilities. The dispatcher consults these at TREE_CONNECT and /// Static capabilities. The dispatcher consults these at TREE_CONNECT and
/// uses `is_read_only` to clamp authz. /// uses `is_read_only` to clamp authz.
fn capabilities(&self) -> BackendCapabilities; fn capabilities(&self) -> BackendCapabilities;
// ===== Extended Attributes (xattr) support =====
/// Get extended attribute value
async fn get_xattr(&self, _path: &SmbPath, _name: &str) -> SmbResult<Vec<u8>> {
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<Vec<String>> {
Err(SmbError::NotSupported)
}
} }
/// A live open file or directory handle. /// A live open file or directory handle.
@@ -277,3 +299,113 @@ impl Handle for NullHandle {
Ok(()) 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<dyn ShareBackend>,
buffer: tokio::sync::RwLock<Vec<u8>>,
read_only: bool,
}
impl AfpInfoHandle {
pub fn new(base_path: SmbPath, backend: std::sync::Arc<dyn ShareBackend>, 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<bytes::Bytes> {
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<u32> {
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<FileInfo> {
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<Vec<DirEntry>> {
Err(SmbError::NotSupported)
}
async fn close(self: Box<Self>) -> SmbResult<()> {
self.save_to_xattr().await
}
}

View File

@@ -428,6 +428,71 @@ impl ShareBackend for LocalFsBackend {
case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")), case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")),
} }
} }
async fn get_xattr(&self, path: &SmbPath, name: &str) -> SmbResult<Vec<u8>> {
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<Vec<String>> {
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)?
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -1,6 +1,7 @@
//! CREATE handler — open or create a file/directory and allocate a FileId. //! CREATE handler — open or create a file/directory and allocate a FileId.
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock;
use crate::proto::header::Smb2Header; use crate::proto::header::Smb2Header;
use crate::proto::messages::{CreateRequest, CreateResponse}; use crate::proto::messages::{CreateRequest, CreateResponse};
@@ -85,11 +86,58 @@ pub async fn handle(
if stream_path.is_afp_info() { if stream_path.is_afp_info() {
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_AfpInfo named stream open"); 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 // Create AfpInfoHandle to read/write extended attributes
// TODO: Implement actual AFP_AfpInfo handling via extended attributes let base_smb_path = stream_path.base_path().clone();
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 2.6 will implement) // Check write permission
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND); 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 // Handle AFP_Resource named stream