Add SMB AAPL Extensions Phase 1-6 + VFS xattr support

Phase 1: AAPL Create Context negotiation
Phase 2: AFP_AfpInfo Stream structure (Finder info + creation time)
Phase 2.5: SMB Named Stream Backend (NamedStreamPath)
Phase 2.6: Backend Named Stream Support in handlers
Phase 2.7: VFS Extended Attributes (get/set/remove/list_xattr)
Phase 4: Time Machine share config (time_machine field)
Phase 5: Server/Volume Capabilities
Phase 6: macOS Unicode mapping (private range ↔ ASCII)

Tests: 174 smb-server tests pass, 52 VFS tests pass
This commit is contained in:
Warren
2026-06-22 14:21:53 +08:00
parent 64709ec529
commit 866d0536c8
14 changed files with 906 additions and 5 deletions

View File

@@ -70,6 +70,44 @@ pub async fn handle(
Some(u) => u,
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
// Check for named stream (colon separator)
let has_named_stream = units.iter().any(|&u| u == ':' as u16);
if has_named_stream {
use crate::named_stream::NamedStreamPath;
let stream_path = match NamedStreamPath::parse_from_utf16(&units) {
Ok(p) => p,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
// Handle AFP_AfpInfo named stream
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
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 2.6 will implement)
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND);
}
// Handle AFP_Resource named stream
if stream_path.is_afp_resource() {
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
// TODO: Implement actual AFP_Resource handling via AppleDouble files
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 3 will implement)
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND);
}
// Unknown named stream type
warn!(stream = %stream_path.stream_name(), "unknown named stream type");
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID);
}
let path = match SmbPath::from_utf16(&units) {
Ok(p) => p,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
@@ -248,11 +286,67 @@ pub async fn handle(
tree.opens.write().await.insert(file_id, open_arc.clone());
drop(tree);
// Phase AAPL: Check for AAPL context (Apple SMB Extensions)
let aapl_response_data = if !req.create_contexts.is_empty() {
use crate::proto::messages::CreateContext;
use crate::proto::messages::{
AaplCreateContextRequest, AaplCreateContextResponse,
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_SERVER_CAPS,
SMB2_CRTCTX_AAPL_VOLUME_CAPS, SMB2_CRTCTX_AAPL_MODEL_INFO,
SMB2_CRTCTX_AAPL_UNIX_BASED, SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
};
let contexts = CreateContext::parse_chain(&req.create_contexts).unwrap_or_default();
let aapl_ctx = contexts.iter().find(|ctx| ctx.name == CreateContext::NAME_AAPL);
if let Some(ctx) = aapl_ctx {
if let Some(aapl_req) = AaplCreateContextRequest::from_bytes(&ctx.data) {
if aapl_req.command == SMB2_CRTCTX_AAPL_SERVER_QUERY {
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED | SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR;
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE;
let aapl_resp = AaplCreateContextResponse::new_server_query(
aapl_req.request_bitmap,
aapl_req.client_caps,
server_caps,
volume_caps,
"MarkBase SMB",
);
Some(aapl_resp.to_bytes())
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
};
let create_action = match intent {
OpenIntent::Create => FILE_CREATED,
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => FILE_OPENED,
OpenIntent::Open | OpenIntent::Truncate => FILE_OPENED,
};
// Build response with AAPL context if present
let (create_contexts_offset, create_contexts_length, create_contexts) = if let Some(data) = aapl_response_data {
use crate::proto::messages::CreateContext;
let aapl_ctx = CreateContext {
name: CreateContext::NAME_AAPL.to_vec(),
data,
};
let mut ctx_buf = Vec::new();
CreateContext::encode_chain(&[aapl_ctx], &mut ctx_buf).expect("encode AAPL context");
let offset = 80 + req.name.len() + 8; // After CreateResponse fixed + padding
(offset as u32, ctx_buf.len() as u32, ctx_buf)
} else {
(0, 0, vec![])
};
let resp = CreateResponse {
structure_size: 89,
oplock_level: granted_oplock, // Phase 4: will be dynamic
@@ -267,9 +361,9 @@ pub async fn handle(
file_attributes: info.attributes(),
reserved2: 0,
file_id,
create_contexts_offset: 0,
create_contexts_length: 0,
create_contexts: vec![],
create_contexts_offset,
create_contexts_length,
create_contexts,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("encode");