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:
141
vendor/smb-server/src/proto/messages/aapl.rs
vendored
Normal file
141
vendor/smb-server/src/proto/messages/aapl.rs
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Apple SMB Extensions (AAPL Create Context)
|
||||
//!
|
||||
//! Reference: Apple SMB Extensions protocol (MS-SMB2 §2.2.13.2)
|
||||
//! and Samba vfs_fruit.c implementation.
|
||||
|
||||
// AAPL Create Context Command Codes
|
||||
pub const SMB2_CRTCTX_AAPL_SERVER_QUERY: u32 = 1;
|
||||
pub const SMB2_CRTCTX_AAPL_RESOLVE_ID: u32 = 2;
|
||||
|
||||
// AAPL Server Query Request/Response Bitmap
|
||||
pub const SMB2_CRTCTX_AAPL_SERVER_CAPS: u64 = 1;
|
||||
pub const SMB2_CRTCTX_AAPL_VOLUME_CAPS: u64 = 2;
|
||||
pub const SMB2_CRTCTX_AAPL_MODEL_INFO: u64 = 4;
|
||||
|
||||
// AAPL Client/Server Capabilities Bitmap
|
||||
pub const SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR: u64 = 1;
|
||||
pub const SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE: u64 = 2;
|
||||
pub const SMB2_CRTCTX_AAPL_UNIX_BASED: u64 = 4;
|
||||
pub const SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE: u64 = 8;
|
||||
|
||||
// AAPL Volume Capabilities Bitmap
|
||||
pub const SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID: u64 = 1;
|
||||
pub const SMB2_CRTCTX_AAPL_CASE_SENSITIVE: u64 = 2;
|
||||
pub const SMB2_CRTCTX_AAPL_FULL_SYNC: u64 = 4;
|
||||
|
||||
/// AAPL Create Context Request (24 bytes)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AaplCreateContextRequest {
|
||||
pub command: u32,
|
||||
pub reserved: u32,
|
||||
pub request_bitmap: u64,
|
||||
pub client_caps: u64,
|
||||
}
|
||||
|
||||
impl AaplCreateContextRequest {
|
||||
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != 24 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
command: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
|
||||
reserved: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
|
||||
request_bitmap: u64::from_le_bytes([
|
||||
data[8], data[9], data[10], data[11],
|
||||
data[12], data[13], data[14], data[15],
|
||||
]),
|
||||
client_caps: u64::from_le_bytes([
|
||||
data[16], data[17], data[18], data[19],
|
||||
data[20], data[21], data[22], data[23],
|
||||
]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// AAPL Create Context Response (variable length)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AaplCreateContextResponse {
|
||||
pub command: u32,
|
||||
pub reserved: u32,
|
||||
pub request_bitmap: u64,
|
||||
pub server_caps: u64,
|
||||
pub volume_caps: u64,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl AaplCreateContextResponse {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
buf.extend_from_slice(&self.command.to_le_bytes());
|
||||
buf.extend_from_slice(&self.reserved.to_le_bytes());
|
||||
buf.extend_from_slice(&self.request_bitmap.to_le_bytes());
|
||||
|
||||
if self.request_bitmap & SMB2_CRTCTX_AAPL_SERVER_CAPS != 0 {
|
||||
buf.extend_from_slice(&self.server_caps.to_le_bytes());
|
||||
}
|
||||
|
||||
if self.request_bitmap & SMB2_CRTCTX_AAPL_VOLUME_CAPS != 0 {
|
||||
buf.extend_from_slice(&self.volume_caps.to_le_bytes());
|
||||
}
|
||||
|
||||
if self.request_bitmap & SMB2_CRTCTX_AAPL_MODEL_INFO != 0 {
|
||||
let model_utf16: Vec<u16> = self.model.encode_utf16().collect();
|
||||
buf.extend_from_slice(&(model_utf16.len() as u32 * 2).to_le_bytes());
|
||||
for ch in model_utf16 {
|
||||
buf.extend_from_slice(&ch.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn new_server_query(
|
||||
request_bitmap: u64,
|
||||
client_caps: u64,
|
||||
server_caps: u64,
|
||||
volume_caps: u64,
|
||||
model: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
command: SMB2_CRTCTX_AAPL_SERVER_QUERY,
|
||||
reserved: 0,
|
||||
request_bitmap,
|
||||
server_caps,
|
||||
volume_caps,
|
||||
model: model.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_aapl_request_parse() {
|
||||
let data = [
|
||||
1u8, 0, 0, 0, // command = SERVER_QUERY
|
||||
0, 0, 0, 0, // reserved
|
||||
7, 0, 0, 0, 0, 0, 0, 0, // request_bitmap = SERVER_CAPS | VOLUME_CAPS | MODEL_INFO
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // client_caps
|
||||
];
|
||||
let req = AaplCreateContextRequest::from_bytes(&data).unwrap();
|
||||
assert_eq!(req.command, SMB2_CRTCTX_AAPL_SERVER_QUERY);
|
||||
assert_eq!(req.request_bitmap, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aapl_response_encode() {
|
||||
let resp = AaplCreateContextResponse::new_server_query(
|
||||
SMB2_CRTCTX_AAPL_SERVER_CAPS | SMB2_CRTCTX_AAPL_VOLUME_CAPS,
|
||||
0,
|
||||
SMB2_CRTCTX_AAPL_UNIX_BASED | SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
|
||||
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
|
||||
"MacBookPro",
|
||||
);
|
||||
let bytes = resp.to_bytes();
|
||||
assert!(!bytes.is_empty());
|
||||
assert_eq!(bytes[0..4], [1, 0, 0, 0]); // command
|
||||
}
|
||||
}
|
||||
257
vendor/smb-server/src/proto/messages/afp_info.rs
vendored
Normal file
257
vendor/smb-server/src/proto/messages/afp_info.rs
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
//! AFP_AfpInfo Stream (Apple SMB Extensions)
|
||||
//!
|
||||
//! Reference: Apple SMB Extensions protocol (MS-SMB2 §2.2.13.2)
|
||||
//! and Samba MacExtensions.h implementation.
|
||||
|
||||
pub const AFP_INFO_SIZE: usize = 60;
|
||||
pub const AFP_SIGNATURE: u32 = 0x41465000; // "AFP\0"
|
||||
pub const AFP_VERSION: u32 = 0x00010000;
|
||||
|
||||
pub const AFP_OFF_FINDER_INFO: usize = 16;
|
||||
pub const AFP_FINDER_SIZE: usize = 32;
|
||||
|
||||
pub const AFPINFO_STREAM_NAME: &[u8] = b"AFP_AfpInfo";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct FinderInfo {
|
||||
pub file_type: [u8; 4],
|
||||
pub creator_type: [u8; 4],
|
||||
pub finder_flags: u16,
|
||||
pub location: (u16, u16),
|
||||
pub reserved: u16,
|
||||
pub extended_flags: u16,
|
||||
pub extended_location: (u16, u16),
|
||||
pub reserved2: [u8; 10],
|
||||
}
|
||||
|
||||
impl FinderInfo {
|
||||
pub fn default_file() -> Self {
|
||||
Self {
|
||||
file_type: [0, 0, 0, 0],
|
||||
creator_type: [0, 0, 0, 0],
|
||||
finder_flags: 0,
|
||||
location: (0, 0),
|
||||
reserved: 0,
|
||||
extended_flags: 0,
|
||||
extended_location: (0, 0),
|
||||
reserved2: [0; 10],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_directory() -> Self {
|
||||
Self {
|
||||
file_type: [0, 0, 0, 0],
|
||||
creator_type: [0, 0, 0, 0],
|
||||
finder_flags: 0,
|
||||
location: (0, 0),
|
||||
reserved: 0,
|
||||
extended_flags: 0,
|
||||
extended_location: (0, 0),
|
||||
reserved2: [0; 10],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(data: &[u8; 32]) -> Self {
|
||||
let file_type = [data[0], data[1], data[2], data[3]];
|
||||
let creator_type = [data[4], data[5], data[6], data[7]];
|
||||
let finder_flags = u16::from_be_bytes([data[8], data[9]]);
|
||||
let location = (
|
||||
u16::from_be_bytes([data[10], data[11]]),
|
||||
u16::from_be_bytes([data[12], data[13]]),
|
||||
);
|
||||
let reserved = u16::from_be_bytes([data[14], data[15]]);
|
||||
let extended_flags = u16::from_be_bytes([data[16], data[17]]);
|
||||
let extended_location = (
|
||||
u16::from_be_bytes([data[18], data[19]]),
|
||||
u16::from_be_bytes([data[20], data[21]]),
|
||||
);
|
||||
let reserved2 = [
|
||||
data[22], data[23], data[24], data[25],
|
||||
data[26], data[27], data[28], data[29],
|
||||
data[30], data[31],
|
||||
];
|
||||
Self {
|
||||
file_type,
|
||||
creator_type,
|
||||
finder_flags,
|
||||
location,
|
||||
reserved,
|
||||
extended_flags,
|
||||
extended_location,
|
||||
reserved2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
let mut data = [0u8; 32];
|
||||
data[0..4].copy_from_slice(&self.file_type);
|
||||
data[4..8].copy_from_slice(&self.creator_type);
|
||||
data[8..10].copy_from_slice(&self.finder_flags.to_be_bytes());
|
||||
data[10..12].copy_from_slice(&self.location.0.to_be_bytes());
|
||||
data[12..14].copy_from_slice(&self.location.1.to_be_bytes());
|
||||
data[14..16].copy_from_slice(&self.reserved.to_be_bytes());
|
||||
data[16..18].copy_from_slice(&self.extended_flags.to_be_bytes());
|
||||
data[18..20].copy_from_slice(&self.extended_location.0.to_be_bytes());
|
||||
data[20..22].copy_from_slice(&self.extended_location.1.to_be_bytes());
|
||||
data[22..32].copy_from_slice(&self.reserved2);
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AfpInfo {
|
||||
pub signature: u32,
|
||||
pub version: u32,
|
||||
pub reserved1: u32,
|
||||
pub backup_time: u32,
|
||||
pub finder_info: FinderInfo,
|
||||
pub prodos_info: [u8; 6],
|
||||
pub reserved2: [u8; 6],
|
||||
}
|
||||
|
||||
impl AfpInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signature: AFP_SIGNATURE,
|
||||
version: AFP_VERSION,
|
||||
reserved1: 0,
|
||||
backup_time: 0,
|
||||
finder_info: FinderInfo::default_file(),
|
||||
prodos_info: [0; 6],
|
||||
reserved2: [0; 6],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < AFP_INFO_SIZE {
|
||||
return None;
|
||||
}
|
||||
let signature = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if signature != AFP_SIGNATURE {
|
||||
return None;
|
||||
}
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
if version != AFP_VERSION {
|
||||
return None;
|
||||
}
|
||||
let reserved1 = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
|
||||
let backup_time = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
|
||||
let finder_bytes: [u8; 32] = data[16..48].try_into().ok()?;
|
||||
let finder_info = FinderInfo::from_bytes(&finder_bytes);
|
||||
let prodos_info: [u8; 6] = data[48..54].try_into().ok()?;
|
||||
let reserved2: [u8; 6] = data[54..60].try_into().ok()?;
|
||||
Some(Self {
|
||||
signature,
|
||||
version,
|
||||
reserved1,
|
||||
backup_time,
|
||||
finder_info,
|
||||
prodos_info,
|
||||
reserved2,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut data = Vec::with_capacity(AFP_INFO_SIZE);
|
||||
data.extend_from_slice(&self.signature.to_le_bytes());
|
||||
data.extend_from_slice(&self.version.to_le_bytes());
|
||||
data.extend_from_slice(&self.reserved1.to_le_bytes());
|
||||
data.extend_from_slice(&self.backup_time.to_le_bytes());
|
||||
data.extend_from_slice(&self.finder_info.to_bytes());
|
||||
data.extend_from_slice(&self.prodos_info);
|
||||
data.extend_from_slice(&self.reserved2);
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SambaAfpInfo {
|
||||
pub afp: AfpInfo,
|
||||
pub create_time: u32,
|
||||
}
|
||||
|
||||
impl SambaAfpInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
afp: AfpInfo::new(),
|
||||
create_time: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < AFP_INFO_SIZE + 4 {
|
||||
return None;
|
||||
}
|
||||
let afp = AfpInfo::from_bytes(&data[..AFP_INFO_SIZE])?;
|
||||
let create_time = u32::from_le_bytes([
|
||||
data[60], data[61], data[62], data[63],
|
||||
]);
|
||||
Some(Self {
|
||||
afp,
|
||||
create_time,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut data = self.afp.to_bytes();
|
||||
data.extend_from_slice(&self.create_time.to_le_bytes());
|
||||
data
|
||||
}
|
||||
|
||||
pub fn get_create_time(&self) -> u32 {
|
||||
self.create_time
|
||||
}
|
||||
|
||||
pub fn set_create_time(&mut self, time: u32) {
|
||||
self.create_time = time;
|
||||
}
|
||||
|
||||
pub fn get_finder_info(&self) -> &FinderInfo {
|
||||
&self.afp.finder_info
|
||||
}
|
||||
|
||||
pub fn set_finder_info(&mut self, finder: FinderInfo) {
|
||||
self.afp.finder_info = finder;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_afp_info_roundtrip() {
|
||||
let afp = AfpInfo::new();
|
||||
let bytes = afp.to_bytes();
|
||||
assert_eq!(bytes.len(), AFP_INFO_SIZE);
|
||||
let decoded = AfpInfo::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.signature, AFP_SIGNATURE);
|
||||
assert_eq!(decoded.version, AFP_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_samba_afp_info_roundtrip() {
|
||||
let mut samba = SambaAfpInfo::new();
|
||||
samba.set_create_time(12345678);
|
||||
let bytes = samba.to_bytes();
|
||||
assert_eq!(bytes.len(), AFP_INFO_SIZE + 4);
|
||||
let decoded = SambaAfpInfo::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.get_create_time(), 12345678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_finder_info_roundtrip() {
|
||||
let finder = FinderInfo::default_file();
|
||||
let bytes = finder.to_bytes();
|
||||
assert_eq!(bytes.len(), 32);
|
||||
let decoded = FinderInfo::from_bytes(&bytes);
|
||||
assert_eq!(decoded.file_type, finder.file_type);
|
||||
assert_eq!(decoded.creator_type, finder.creator_type);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature() {
|
||||
let data = [0u8; 60];
|
||||
assert!(AfpInfo::from_bytes(&data).is_none());
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,7 @@ impl CreateContext {
|
||||
pub const NAME_RQLS: &'static [u8; 4] = b"RqLs"; // REQUEST_LEASE
|
||||
pub const NAME_DH2Q: &'static [u8; 4] = b"DH2Q"; // DURABLE_HANDLE_REQUEST_V2
|
||||
pub const NAME_DH2C: &'static [u8; 4] = b"DH2C"; // DURABLE_HANDLE_RECONNECT_V2
|
||||
pub const NAME_AAPL: &'static [u8; 4] = b"AAPL"; // Apple SMB Extensions (MS-SMB2 §2.2.13.2)
|
||||
|
||||
/// Parse a chain of create-contexts from the raw chain bytes.
|
||||
///
|
||||
|
||||
14
vendor/smb-server/src/proto/messages/mod.rs
vendored
14
vendor/smb-server/src/proto/messages/mod.rs
vendored
@@ -7,6 +7,8 @@
|
||||
//! The crate does **not** implement command behavior — it only encodes/decodes
|
||||
//! the wire bytes. The server crate owns dispatch and state.
|
||||
|
||||
pub mod aapl;
|
||||
pub mod afp_info;
|
||||
pub mod cancel;
|
||||
pub mod change_notify;
|
||||
pub mod close;
|
||||
@@ -29,6 +31,18 @@ pub mod tree_connect;
|
||||
pub mod tree_disconnect;
|
||||
pub mod write;
|
||||
|
||||
pub use aapl::{
|
||||
AaplCreateContextRequest, AaplCreateContextResponse, SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
|
||||
SMB2_CRTCTX_AAPL_FULL_SYNC, SMB2_CRTCTX_AAPL_MODEL_INFO, SMB2_CRTCTX_AAPL_SERVER_CAPS,
|
||||
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE, SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR, SMB2_CRTCTX_AAPL_UNIX_BASED,
|
||||
SMB2_CRTCTX_AAPL_VOLUME_CAPS,
|
||||
};
|
||||
pub use afp_info::{
|
||||
AfpInfo, FinderInfo, SambaAfpInfo, AFP_FINDER_SIZE, AFP_INFO_SIZE, AFP_OFF_FINDER_INFO,
|
||||
AFP_SIGNATURE, AFP_VERSION, AFPINFO_STREAM_NAME,
|
||||
};
|
||||
pub use cancel::CancelRequest;
|
||||
pub use change_notify::{ChangeNotifyRequest, ChangeNotifyResponse};
|
||||
pub use close::{CloseRequest, CloseResponse};
|
||||
|
||||
Reference in New Issue
Block a user