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
+257
View 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());
}
}