866d0536c8
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
257 lines
7.7 KiB
Rust
257 lines
7.7 KiB
Rust
//! 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());
|
|
}
|
|
} |