Implement SMB AFP_Resource Stream via AppleDouble files (Phase 3 complete)
This commit is contained in:
BIN
data/auth.sqlite
BIN
data/auth.sqlite
Binary file not shown.
197
vendor/smb-server/src/backend.rs
vendored
197
vendor/smb-server/src/backend.rs
vendored
@@ -409,3 +409,200 @@ impl Handle for AfpInfoHandle {
|
|||||||
self.save_to_xattr().await
|
self.save_to_xattr().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AFP_Resource Handle (AppleDouble file ._filename)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// AppleDouble file format (._filename)
|
||||||
|
/// Reference: https://developer.apple.com/library/archive/documentation/macpdf/Inside_Macintosh/Inside_Macintosh.pdf
|
||||||
|
pub struct AfpResourceHandle {
|
||||||
|
base_path: SmbPath,
|
||||||
|
backend: std::sync::Arc<dyn ShareBackend>,
|
||||||
|
resource_path: SmbPath,
|
||||||
|
data: tokio::sync::RwLock<Option<Vec<u8>>>,
|
||||||
|
read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AfpResourceHandle {
|
||||||
|
pub fn new(base_path: SmbPath, backend: std::sync::Arc<dyn ShareBackend>, read_only: bool) -> Self {
|
||||||
|
// AppleDouble file: ._filename in same directory
|
||||||
|
let file_name = base_path.file_name().unwrap_or("");
|
||||||
|
let parent = base_path.parent();
|
||||||
|
let resource_name = format!("._{}", file_name);
|
||||||
|
|
||||||
|
let resource_path = match parent {
|
||||||
|
Some(p) => p.join(&resource_name).unwrap_or_else(|_| base_path.clone()),
|
||||||
|
None => SmbPath::root(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base_path,
|
||||||
|
backend,
|
||||||
|
resource_path,
|
||||||
|
data: tokio::sync::RwLock::new(None),
|
||||||
|
read_only,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_from_file(&self) -> SmbResult<()> {
|
||||||
|
// Try to open ._filename file
|
||||||
|
let opts = OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: false,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: false,
|
||||||
|
non_directory: true,
|
||||||
|
delete_on_close: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.backend.open(&self.resource_path, opts).await {
|
||||||
|
Ok(handle) => {
|
||||||
|
let info = handle.stat().await?;
|
||||||
|
let size = info.end_of_file as usize;
|
||||||
|
let data = handle.read(0, size as u32).await?;
|
||||||
|
let mut buffer = self.data.write().await;
|
||||||
|
*buffer = Some(data.to_vec());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(SmbError::NotFound | SmbError::PathNotFound) => {
|
||||||
|
// ._file doesn't exist, initialize empty buffer
|
||||||
|
let mut buffer = self.data.write().await;
|
||||||
|
*buffer = Some(vec![]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_to_file(&self) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = self.data.read().await;
|
||||||
|
if let Some(data) = buffer.as_ref() {
|
||||||
|
if data.is_empty() {
|
||||||
|
// Don't create empty ._file
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let opts = OpenOptions {
|
||||||
|
read: false,
|
||||||
|
write: true,
|
||||||
|
intent: OpenIntent::OverwriteOrCreate,
|
||||||
|
directory: false,
|
||||||
|
non_directory: true,
|
||||||
|
delete_on_close: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = self.backend.open(&self.resource_path, opts).await?;
|
||||||
|
handle.write(0, data).await?;
|
||||||
|
handle.flush().await?;
|
||||||
|
let _ = handle.close().await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handle for AfpResourceHandle {
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> SmbResult<bytes::Bytes> {
|
||||||
|
self.load_from_file().await?;
|
||||||
|
let buffer = self.data.read().await;
|
||||||
|
|
||||||
|
match buffer.as_ref() {
|
||||||
|
Some(data) => {
|
||||||
|
let start = offset as usize;
|
||||||
|
let end = std::cmp::min(start + len as usize, data.len());
|
||||||
|
if start >= data.len() {
|
||||||
|
return Ok(bytes::Bytes::new());
|
||||||
|
}
|
||||||
|
Ok(bytes::Bytes::copy_from_slice(&data[start..end]))
|
||||||
|
}
|
||||||
|
None => Ok(bytes::Bytes::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = self.data.write().await;
|
||||||
|
match buffer.as_mut() {
|
||||||
|
Some(buf) => {
|
||||||
|
let start = offset as usize;
|
||||||
|
// Extend buffer if needed
|
||||||
|
if start + data.len() > buf.len() {
|
||||||
|
buf.resize(start + data.len(), 0);
|
||||||
|
}
|
||||||
|
buf[start..start + data.len()].copy_from_slice(data);
|
||||||
|
Ok(data.len() as u32)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*buffer = Some(vec![0u8; offset as usize + data.len()]);
|
||||||
|
if let Some(buf) = buffer.as_mut() {
|
||||||
|
buf[offset as usize..].copy_from_slice(data);
|
||||||
|
}
|
||||||
|
Ok(data.len() as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) -> SmbResult<()> {
|
||||||
|
self.save_to_file().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stat(&self) -> SmbResult<FileInfo> {
|
||||||
|
self.load_from_file().await?;
|
||||||
|
let buffer = self.data.read().await;
|
||||||
|
|
||||||
|
let size = match buffer.as_ref() {
|
||||||
|
Some(data) => data.len() as u64,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(FileInfo {
|
||||||
|
name: format!("._{}", self.base_path.file_name().unwrap_or("")),
|
||||||
|
end_of_file: size,
|
||||||
|
allocation_size: size,
|
||||||
|
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 self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = self.data.write().await;
|
||||||
|
match buffer.as_mut() {
|
||||||
|
Some(buf) => {
|
||||||
|
buf.resize(len as usize, 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*buffer = Some(vec![0u8; len as usize]);
|
||||||
|
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_file().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
85
vendor/smb-server/src/handlers/create.rs
vendored
85
vendor/smb-server/src/handlers/create.rs
vendored
@@ -7,7 +7,7 @@ use crate::proto::header::Smb2Header;
|
|||||||
use crate::proto::messages::{CreateRequest, CreateResponse};
|
use crate::proto::messages::{CreateRequest, CreateResponse};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::backend::{OpenIntent, OpenOptions};
|
use crate::backend::{OpenIntent, OpenOptions, FileInfo};
|
||||||
use crate::builder::Access;
|
use crate::builder::Access;
|
||||||
use crate::conn::state::{Connection, Open};
|
use crate::conn::state::{Connection, Open};
|
||||||
use crate::dispatch::HandlerResponse;
|
use crate::dispatch::HandlerResponse;
|
||||||
@@ -144,11 +144,86 @@ pub async fn handle(
|
|||||||
if stream_path.is_afp_resource() {
|
if stream_path.is_afp_resource() {
|
||||||
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_Resource named stream open");
|
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_Resource named stream open");
|
||||||
|
|
||||||
// For AFP_Resource, we return a virtual handle that reads/writes ._ file
|
// Create AfpResourceHandle to read/write AppleDouble file (._filename)
|
||||||
// TODO: Implement actual AFP_Resource handling via AppleDouble files
|
let base_smb_path = stream_path.base_path().clone();
|
||||||
|
|
||||||
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 3 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::AfpResourceHandle::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);
|
||||||
|
|
||||||
|
// Load resource fork size for response
|
||||||
|
let open_lock = open_arc.read().await;
|
||||||
|
let info = match open_lock.handle.as_ref() {
|
||||||
|
Some(h) => h.stat().await.unwrap_or(FileInfo {
|
||||||
|
name: "".to_string(),
|
||||||
|
end_of_file: 0,
|
||||||
|
allocation_size: 0,
|
||||||
|
creation_time: 0,
|
||||||
|
last_access_time: 0,
|
||||||
|
last_write_time: 0,
|
||||||
|
change_time: 0,
|
||||||
|
is_directory: false,
|
||||||
|
file_index: 0,
|
||||||
|
}),
|
||||||
|
None => FileInfo {
|
||||||
|
name: "".to_string(),
|
||||||
|
end_of_file: 0,
|
||||||
|
allocation_size: 0,
|
||||||
|
creation_time: 0,
|
||||||
|
last_access_time: 0,
|
||||||
|
last_write_time: 0,
|
||||||
|
change_time: 0,
|
||||||
|
is_directory: false,
|
||||||
|
file_index: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
drop(open_lock);
|
||||||
|
|
||||||
|
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: info.allocation_size,
|
||||||
|
end_of_file: info.end_of_file,
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown named stream type
|
// Unknown named stream type
|
||||||
|
|||||||
Reference in New Issue
Block a user