use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tailtalk_packets::afp::{
mangle_name, AFP2_MAX_NAME_LEN, AfpError, CreateFlag, FPByteRangeLockFlags, FPDelete,
FPDirectoryBitmap, FPEnumerate, FPFileBitmap, FPRead, FPSetForkParms, FPVolume,
FPVolumeBitmap, FileType, FinderFlags, FinderInfo, ForkType, VolumeSignature,
};
use encoding_rs::MACINTOSH;
use crate::time_to_afp_v1;
use tracing::{error, info};
#[cfg(unix)]
use xattr;
#[cfg(target_os = "macos")]
const FINDER_INFO_XATTR: &str = "com.apple.FinderInfo";
#[cfg(all(unix, not(target_os = "macos")))]
const FINDER_INFO_XATTR: &str = "user.com.apple.FinderInfo";
#[cfg(windows)]
const FINDER_INFO_STREAM: &str = "com.apple.FinderInfo";
struct TypeCreator {
file_type: [u8; 4],
creator: [u8; 4],
}
fn infer_type_creator_from_content(path: &Path) -> Option<TypeCreator> {
let db = magic_db::load().ok()?;
let magic = db.first_magic_file(path).ok()?;
match magic.mime_type() {
"text/plain" => Some(TypeCreator { file_type: *b"TEXT", creator: *b"ttxt" }),
"application/x-stuffit" => Some(TypeCreator { file_type: *b"SIT!", creator: *b"SIT!" }),
"application/x-stuffit-x" => Some(TypeCreator { file_type: *b"APPL", creator: *b"aust" }),
"application/x-dc42-floppy-image" => Some(TypeCreator { file_type: *b"dImg", creator: *b"dCpy" }),
"application/pdf" => Some(TypeCreator { file_type: *b"PDF ", creator: *b"CARO" }),
"application/mac-binhex40" => Some(TypeCreator { file_type: *b"TEXT", creator: *b"BnHq" }),
"application/mac-binhex" => Some(TypeCreator { file_type: *b"TEXT", creator: *b"BNHQ" }),
"application/x-apple-diskimage" => Some(TypeCreator { file_type: *b"devi", creator: *b"????" }),
"application/x-macbinary" => Some(TypeCreator { file_type: *b"BINA", creator: *b"PSPT" }),
"application/x-dfont" => Some(TypeCreator { file_type: *b"dfil", creator: *b"????" }),
"application/x-apple-rsr" => Some(TypeCreator { file_type: *b"rsrc", creator: *b"????" }),
"application/x-appleworks3" => Some(TypeCreator { file_type: *b"AWWP", creator: *b"pdos" }),
"image/jpeg" => Some(TypeCreator { file_type: *b"JPEG", creator: *b"8BIM" }),
"image/png" => Some(TypeCreator { file_type: *b"PNGf", creator: *b"ogle" }),
"image/x-pict" => Some(TypeCreator { file_type: *b"PICT", creator: *b"ttxt" }),
_ => None,
}
}
fn infer_type_creator_from_extension(path: &Path) -> Option<TypeCreator> {
let stem = path.file_stem().and_then(|s| s.to_str());
let ext = path.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());
if ext.as_deref() == Some("π") {
return Some(TypeCreator { file_type: *b"MPST", creator: *b"MPS " });
}
if ext.as_deref() == Some("rsrc") && stem.is_some_and(|s| s.ends_with(".π")) {
return Some(TypeCreator { file_type: *b"rsrc", creator: *b"MPS " });
}
match ext.as_deref()? {
"txt" => Some(TypeCreator { file_type: *b"TEXT", creator: *b"ttxt" }),
"sit" => Some(TypeCreator { file_type: *b"SIT!", creator: *b"SIT!" }),
"sea" => Some(TypeCreator { file_type: *b"APPL", creator: *b"aust" }),
"img" | "dsk" => Some(TypeCreator { file_type: *b"dImg", creator: *b"dCpy" }),
"pdf" => Some(TypeCreator { file_type: *b"PDF ", creator: *b"CARO" }),
"hqx" => Some(TypeCreator { file_type: *b"TEXT", creator: *b"BnHq" }),
_ => None,
}
}
pub async fn read_finder_info(path: &Path) -> std::io::Result<FinderInfo> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
#[cfg(unix)]
{
match xattr::get(&path, FINDER_INFO_XATTR) {
Ok(Some(data)) if data.len() >= 32 => {
Ok(FinderInfo::from(<[u8; 32]>::try_from(&data[0..32]).unwrap()))
}
Ok(_) => Ok(FinderInfo::default()),
Err(e) => Err(e),
}
}
#[cfg(windows)]
{
let stream_path = format!("{}:{}", path.display(), FINDER_INFO_STREAM);
match std::fs::read(&stream_path) {
Ok(data) if data.len() >= 32 => {
Ok(FinderInfo::from(<[u8; 32]>::try_from(&data[0..32]).unwrap()))
}
Ok(_) => Ok(FinderInfo::default()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(FinderInfo::default()),
Err(e) => Err(e),
}
}
})
.await
.map_err(std::io::Error::other)?
}
pub async fn write_finder_info(path: &Path, info: &FinderInfo) -> std::io::Result<()> {
let raw: [u8; 32] = (*info).into();
let path = path.to_path_buf();
#[cfg(unix)]
tokio::task::spawn_blocking(move || xattr::set(&path, FINDER_INFO_XATTR, &raw))
.await
.map_err(std::io::Error::other)??;
#[cfg(windows)]
{
let stream_path = format!("{}:{}", path.display(), FINDER_INFO_STREAM);
tokio::fs::write(&stream_path, &raw).await?;
}
Ok(())
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct Node {
pub id: u32,
pub parent_id: u32,
pub name: String,
pub is_dir: bool,
pub path: PathBuf,
pub data_fork: Option<tokio::fs::File>,
pub resource_fork: Option<tokio::fs::File>,
}
impl Node {
pub async fn open_data_fork(&mut self, absolute_path: &PathBuf) -> std::io::Result<()> {
let file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.open(absolute_path)
.await?;
self.data_fork = Some(file);
Ok(())
}
pub async fn close_data_fork(&mut self) {
if let Some(file) = self.data_fork.take() {
let _ = file.sync_data().await;
}
}
pub async fn open_resource_fork(&mut self, path: &Path) -> std::io::Result<()> {
if !is_native_resource_fork_path(path)
&& let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.await?;
self.resource_fork = Some(file);
Ok(())
}
pub async fn close_resource_fork(&mut self) {
if let Some(file) = self.resource_fork.take() {
let _ = file.sync_data().await;
}
}
pub async fn get_finder_info(&self, volume_root: &Path) -> FinderInfo {
let absolute_path = volume_root.join(&self.path);
let stored = read_finder_info(&absolute_path).await.unwrap_or_default();
if stored == FinderInfo::default() && !self.is_dir {
let abs = absolute_path.clone();
let maybe_tc = tokio::task::spawn_blocking(move || {
infer_type_creator_from_content(&abs)
.or_else(|| infer_type_creator_from_extension(&abs))
})
.await
.unwrap_or(None);
if let Some(tc) = maybe_tc {
let inferred = FinderInfo {
file_type: tc.file_type,
creator: tc.creator,
..Default::default()
};
if let Err(e) = write_finder_info(&absolute_path, &inferred).await {
error!("Failed to persist inferred Finder Info for {:?}: {:?}", self.path, e);
}
return inferred;
}
}
stored
}
pub async fn set_finder_info(&self, volume_root: &Path, info: &FinderInfo) -> Result<(), AfpError> {
write_finder_info(&volume_root.join(&self.path), info).await.map_err(|e| {
error!("Failed to set Finder Info for {:?}: {:?}", self.path, e);
AfpError::AccessDenied
})
}
pub async fn get_attributes(&self, volume_root: &Path) -> u16 {
let finder_info = self.get_finder_info(volume_root).await;
if finder_info.flags.contains(FinderFlags::IS_INVISIBLE) { 0x0001 } else { 0 }
}
pub async fn set_attributes(&self, volume_root: &Path, attributes: u16) -> Result<(), AfpError> {
let mut finder_info = self.get_finder_info(volume_root).await;
if (attributes & 0x0001) != 0 {
finder_info.flags |= FinderFlags::IS_INVISIBLE;
} else {
finder_info.flags &= !FinderFlags::IS_INVISIBLE;
}
self.set_finder_info(volume_root, &finder_info).await
}
pub async fn get_file_parms_resp(
&self,
volume_root: &Path,
bitmap: FPFileBitmap,
output: &mut [u8],
) -> Result<usize, AfpError> {
let mut offset = 0;
let mut variable_len_offset = 0;
let full_path = volume_root.join(&self.path);
let metadata = tokio::fs::metadata(&full_path)
.await
.map_err(|_| AfpError::ObjectNotFound)?;
if bitmap.contains(FPFileBitmap::ATTRIBUTES) {
let attributes = self.get_attributes(volume_root).await;
output[offset..offset + 2].copy_from_slice(&attributes.to_be_bytes());
offset += 2;
}
if bitmap.contains(FPFileBitmap::PARENT_DIR_ID) {
output[offset..offset + 4].copy_from_slice(&self.parent_id.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPFileBitmap::CREATE_DATE) {
let created_at_bytes = time_to_afp_v1(metadata.created().unwrap()).to_be_bytes();
output[offset..offset + 4].copy_from_slice(&created_at_bytes);
offset += 4;
}
if bitmap.contains(FPFileBitmap::MODIFICATION_DATE) {
let modified_at_bytes = time_to_afp_v1(metadata.modified().unwrap()).to_be_bytes();
output[offset..offset + 4].copy_from_slice(&modified_at_bytes);
offset += 4;
}
if bitmap.contains(FPFileBitmap::BACKUP_DATE) {
output[offset..offset + 4].copy_from_slice(&0u32.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPFileBitmap::FINDER_INFO) {
let raw: [u8; 32] = self.get_finder_info(volume_root).await.into();
output[offset..offset + 32].copy_from_slice(&raw);
offset += 32;
}
if bitmap.contains(FPFileBitmap::LONG_NAME) {
let mut long_name_offset = bitmap.long_name_offset();
output[offset..offset + 2].copy_from_slice(&(long_name_offset as u16).to_be_bytes());
offset += 2;
let encoded_name = mangle_name(&self.name);
let name_len = encoded_name.len();
output[long_name_offset] = name_len as u8;
long_name_offset += 1;
output[long_name_offset..long_name_offset + name_len]
.copy_from_slice(&encoded_name);
variable_len_offset += name_len + 1;
}
if bitmap.contains(FPFileBitmap::FILE_NUMBER) {
output[offset..offset + 4].copy_from_slice(&self.id.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPFileBitmap::DATA_FORK_LENGTH) {
output[offset..offset + 4].copy_from_slice(&(metadata.len() as u32).to_be_bytes());
offset += 4;
}
if bitmap.contains(FPFileBitmap::RESOURCE_FORK_LENGTH) {
let (_, rsrc_len) = resolve_resource_fork_path(volume_root, &self.path).await;
output[offset..offset + 4].copy_from_slice(&rsrc_len.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPFileBitmap::PRODOS_INFO) {
output[offset..offset + 6].fill(0);
offset += 6;
}
Ok(offset + variable_len_offset)
}
}
pub struct Volume {
name: String,
path: PathBuf,
created_at: u32,
volume_id: u16,
nodes: HashMap<u32, Node>,
path_to_id: HashMap<PathBuf, u32>,
next_id: u32,
fork_ref_to_node_id: HashMap<u16, (u32, ForkType)>,
next_fork_ref_num: u16,
fork_locks: HashMap<u16, Vec<(u64, u64)>>,
desktop_database: crate::afp::DesktopDatabase,
mangle_tree: sled::Tree,
}
pub fn rsrc_path(volume_root: &Path, relative_path: &Path) -> PathBuf {
volume_root
.join(".tailtalk")
.join("rsrc")
.join(relative_path)
}
fn is_native_resource_fork_path(path: &Path) -> bool {
path.components()
.any(|c| c.as_os_str() == std::ffi::OsStr::new("..namedfork"))
}
async fn resolve_resource_fork_path(volume_root: &Path, relative_path: &Path) -> (PathBuf, u32) {
#[cfg(target_os = "macos")]
{
let native = volume_root
.join(relative_path)
.join("..namedfork")
.join("rsrc");
if let Ok(meta) = tokio::fs::metadata(&native).await
&& meta.len() > 0
{
return (native, meta.len() as u32);
}
let sidecar = rsrc_path(volume_root, relative_path);
if let Ok(meta) = tokio::fs::metadata(&sidecar).await
&& meta.len() > 0
{
return (sidecar, meta.len() as u32);
}
(native, 0)
}
#[cfg(not(target_os = "macos"))]
{
let sidecar = rsrc_path(volume_root, relative_path);
if let Ok(meta) = tokio::fs::metadata(&sidecar).await
&& meta.len() > 0
{
return (sidecar, meta.len() as u32);
}
(sidecar, 0)
}
}
pub(super) fn afp_path_to_posix(afp_path: &str) -> PathBuf {
let mut result = PathBuf::new();
for component in afp_path.split(':') {
if !component.is_empty() {
result.push(component.replace('/', ":"));
}
}
result
}
fn posix_name_to_afp(name: &str) -> String {
name.replace(':', "/")
}
fn register_mangle_if_needed(tree: &sled::Tree, afp_name: &str, posix_name: &str) {
let (encoded, _, _) = MACINTOSH.encode(afp_name);
if encoded.len() > AFP2_MAX_NAME_LEN {
let mangled = mangle_name(afp_name);
let mangled_str = String::from_utf8_lossy(&mangled);
tracing::debug!(
"mangle: registering {:?} -> {:?} (key={:?})",
afp_name, posix_name, mangled_str
);
if let Err(e) = tree.insert(mangled, posix_name.as_bytes()) {
tracing::error!("Failed to register mangle entry for {:?}: {}", posix_name, e);
}
}
}
fn afp_path_is_empty(path: &Path) -> bool {
path.as_os_str().is_empty()
|| path
.as_os_str()
.to_str()
.is_some_and(|s| s.chars().all(|c| c == '\0'))
}
impl Volume {
pub async fn new(name: String, path: PathBuf, volume_id: u16, db: sled::Db) -> Self {
let created_at = time_to_afp_v1(SystemTime::now());
let desktop_database = crate::afp::DesktopDatabase::from_db(db, 1)
.expect("failed to open AFP desktop database");
let mangle_tree = desktop_database.mangle_names.clone();
let mut new_self = Self {
name,
path,
created_at,
volume_id,
nodes: HashMap::new(),
path_to_id: HashMap::new(),
next_id: 3, fork_ref_to_node_id: HashMap::new(),
next_fork_ref_num: 1,
fork_locks: HashMap::new(),
desktop_database,
mangle_tree,
};
let vol_node = Node {
id: 1,
parent_id: 1, name: new_self.name.clone(),
is_dir: true,
path: PathBuf::new(),
data_fork: None,
resource_fork: None,
};
new_self.nodes.insert(1, vol_node);
let root_node = Node {
id: 2,
parent_id: 1,
name: new_self.name.clone(),
is_dir: true,
path: PathBuf::new(),
data_fork: None,
resource_fork: None,
};
new_self.nodes.insert(2, root_node);
new_self.path_to_id.insert(PathBuf::new(), 2);
let _ = tokio::fs::create_dir_all(new_self.path.join(".tailtalk")).await;
new_self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn get_node_path(&self, id: u32) -> Option<PathBuf> {
self.nodes.get(&id).map(|node| node.path.clone())
}
pub fn path_to_id(&self) -> &HashMap<PathBuf, u32> {
&self.path_to_id
}
pub fn nodes_mut(&mut self) -> &mut HashMap<u32, Node> {
&mut self.nodes
}
pub fn resolve_node(&self, directory_id: u32, path_name: &Path) -> Result<u32, AfpError> {
let directory_id = if directory_id == 0 { 2 } else { directory_id };
let is_empty = afp_path_is_empty(path_name);
if directory_id == 1 {
if is_empty {
return Ok(1);
}
if path_name == Path::new(&self.name) {
return Ok(2);
}
info!(
"resolve_node failed (dir_id=1): path={:?} != volume_name={:?}",
path_name, self.name
);
return Err(AfpError::ObjectNotFound);
}
if is_empty {
return if self.nodes.contains_key(&directory_id) {
Ok(directory_id)
} else {
info!(
"resolve_node failed (empty path): dir_id={} not found",
directory_id
);
Err(AfpError::ObjectNotFound)
};
}
let base_path = self.get_node_path(directory_id).ok_or_else(|| {
info!(
"resolve_node failed: base dir_id={} not found",
directory_id
);
AfpError::ObjectNotFound
})?;
let full_path = base_path.join(path_name);
if let Some(&id) = self.path_to_id.get(&full_path) {
return Ok(id);
}
{
let mut resolved = PathBuf::new();
let mut had_match = false;
for comp in path_name.components() {
let s = comp.as_os_str().to_string_lossy();
let (mac_bytes, _, _) = MACINTOSH.encode(s.as_ref());
tracing::debug!(
"mangle: looking up component {:?} ({} MacRoman bytes)",
s, mac_bytes.len()
);
if let Ok(Some(original)) = self.mangle_tree.get(mac_bytes.as_ref())
&& let Ok(original_str) = std::str::from_utf8(&original)
{
tracing::debug!("mangle: hit — {:?} -> {:?}", s, original_str);
resolved.push(original_str);
had_match = true;
continue;
}
tracing::debug!("mangle: miss for {:?}", s);
resolved.push(s.as_ref());
}
if had_match {
let unmangled_path = base_path.join(&resolved);
if let Some(&id) = self.path_to_id.get(&unmangled_path) {
return Ok(id);
}
info!(
"mangle: unmangled to {:?} but path not in path_to_id (dir populated?)",
unmangled_path
);
}
}
if tracing::enabled!(tracing::Level::DEBUG) {
tracing::debug!("mangle: table contents at lookup failure:");
for entry in self.mangle_tree.iter().filter_map(|r| r.ok()) {
let key = String::from_utf8_lossy(&entry.0);
let val = String::from_utf8_lossy(&entry.1);
tracing::debug!(" {:?} -> {:?}", key, val);
}
}
info!(
"resolve_node failed: dir_id={}, path={:?} (resolved to {:?}) not found",
directory_id, path_name, full_path
);
Err(AfpError::ObjectNotFound)
}
pub async fn get_node_parms(
&self,
node_id: u32,
file_bitmap: FPFileBitmap,
dir_bitmap: FPDirectoryBitmap,
output: &mut [u8],
) -> Result<(bool, usize), AfpError> {
let node = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?;
let mut offset = 0;
if node.is_dir {
offset += self
.get_directory_parms_resp(dir_bitmap, &node.path, output)
.await?;
Ok((true, offset))
} else {
offset += self
.get_file_parms_resp(file_bitmap, &node.path, output)
.await?;
Ok((false, offset))
}
}
pub async fn set_node_parms(
&mut self,
node_id: u32,
file_bitmap: FPFileBitmap,
dir_bitmap: FPDirectoryBitmap,
data: &[u8],
) -> Result<(), AfpError> {
if node_id == 1 {
return Err(AfpError::ObjectNotFound);
}
let volume_root = self.path.clone();
let node = self
.nodes
.get_mut(&node_id)
.ok_or(AfpError::ObjectNotFound)?;
let is_dir = node.is_dir;
let mut offset = 0;
let has_attributes = if is_dir {
dir_bitmap.contains(FPDirectoryBitmap::ATTRIBUTES)
} else {
file_bitmap.contains(FPFileBitmap::ATTRIBUTES)
};
if has_attributes {
let attributes = u16::from_be_bytes([data[offset], data[offset + 1]]);
node.set_attributes(&volume_root, attributes).await?;
offset += 2;
}
let has_create_date = if is_dir {
dir_bitmap.contains(FPDirectoryBitmap::CREATE_DATE)
} else {
file_bitmap.contains(FPFileBitmap::CREATE_DATE)
};
if has_create_date {
offset += 4;
}
let has_mod_date = if is_dir {
dir_bitmap.contains(FPDirectoryBitmap::MODIFICATION_DATE)
} else {
file_bitmap.contains(FPFileBitmap::MODIFICATION_DATE)
};
if has_mod_date {
offset += 4;
}
let has_backup_date = if is_dir {
dir_bitmap.contains(FPDirectoryBitmap::BACKUP_DATE)
} else {
file_bitmap.contains(FPFileBitmap::BACKUP_DATE)
};
if has_backup_date {
offset += 4;
}
let has_finder_info = if is_dir {
dir_bitmap.contains(FPDirectoryBitmap::FINDER_INFO)
} else {
file_bitmap.contains(FPFileBitmap::FINDER_INFO)
};
if has_finder_info {
let raw: [u8; 32] = data[offset..offset + 32].try_into().unwrap();
node.set_finder_info(&volume_root, &FinderInfo::from(raw)).await?;
}
Ok(())
}
pub async fn create_dir(
&mut self,
directory_id: u32,
path_name: PathBuf,
) -> Result<u32, AfpError> {
let parent_node = self
.nodes
.get(&directory_id)
.ok_or(AfpError::ObjectNotFound)?;
let full_relative_path = parent_node.path.join(&path_name);
let absolute_path = self.path.join(&full_relative_path);
tracing::info!("Creating directory: {:?}", absolute_path);
if !absolute_path.exists() {
tokio::fs::create_dir(&absolute_path).await.map_err(|e| {
error!("Failed to create directory: {:?}", e);
AfpError::AccessDenied
})?;
}
if let Some(&id) = self.path_to_id.get(&full_relative_path) {
return Ok(id);
}
let new_id = self.next_id;
self.next_id += 1;
let node = Node {
id: new_id,
parent_id: directory_id,
name: posix_name_to_afp(
&path_name
.file_name()
.ok_or(AfpError::ObjectNotFound)?
.to_string_lossy(),
),
is_dir: true,
path: full_relative_path.clone(),
data_fork: None,
resource_fork: None,
};
self.nodes.insert(new_id, node);
self.path_to_id.insert(full_relative_path, new_id);
Ok(new_id)
}
pub async fn create_file(
&mut self,
create_flag: CreateFlag,
directory_id: u32,
relative_path: PathBuf,
) -> Result<u32, AfpError> {
let parent_node = self
.nodes
.get(&directory_id)
.ok_or(AfpError::ObjectNotFound)?;
let full_relative_path = parent_node.path.join(relative_path);
let absolute_path = self.path.join(&full_relative_path);
let exists = absolute_path.exists();
match create_flag {
CreateFlag::Soft => {
if exists {
return Err(AfpError::ObjectExists);
}
}
CreateFlag::Hard => {
if exists {
tokio::fs::remove_file(&absolute_path).await.map_err(|e| {
error!("Failed to remove file: {:?}", e);
AfpError::AccessDenied
})?;
}
}
}
tokio::fs::File::create(&absolute_path).await.map_err(|e| {
error!("Failed to create file {:?}: {:?}", absolute_path, e);
AfpError::AccessDenied
})?;
let new_id = self.next_id;
self.next_id += 1;
let node = Node {
id: new_id,
parent_id: directory_id,
name: posix_name_to_afp(
&full_relative_path
.file_name()
.ok_or(AfpError::ObjectNotFound)?
.to_string_lossy(),
),
is_dir: false,
path: full_relative_path.clone(),
data_fork: None,
resource_fork: None,
};
self.nodes.insert(new_id, node);
self.path_to_id.insert(full_relative_path, new_id);
Ok(new_id)
}
pub async fn walk_dir(&mut self, relative_path: PathBuf) -> std::io::Result<()> {
let full_path = self.path.join(&relative_path);
let mut start_id = 2; if let Some(&id) = self.path_to_id.get(&relative_path) {
start_id = id;
}
let mut stack = vec![(full_path, start_id)];
while let Some((current_dir, parent_id)) = stack.pop() {
let mut read_dir = tokio::fs::read_dir(¤t_dir).await?;
while let Some(entry) = read_dir.next_entry().await? {
let name = entry.file_name().to_string_lossy().to_string();
if name == ".tailtalk" || name == ".AppleDesktop" {
continue;
}
let entry_path = entry.path();
if let Ok(rel_path) = entry_path.strip_prefix(&self.path) {
let rel_path_buf = rel_path.to_path_buf();
let new_id = self.next_id;
self.next_id += 1;
let is_dir = entry.file_type().await?.is_dir();
let node = Node {
id: new_id,
parent_id,
name: posix_name_to_afp(&entry.file_name().to_string_lossy()),
is_dir,
path: rel_path_buf.clone(),
data_fork: None,
resource_fork: None,
};
self.nodes.insert(new_id, node);
self.path_to_id.insert(rel_path_buf, new_id);
{
let afp_name = posix_name_to_afp(&entry.file_name().to_string_lossy());
register_mangle_if_needed(&self.mangle_tree, &afp_name, &entry.file_name().to_string_lossy());
}
if is_dir {
stack.push((entry_path, new_id));
}
}
}
}
Ok(())
}
pub async fn ensure_dir_populated(&mut self, dir_id: u32) -> std::io::Result<()> {
let dir_path = {
let node = self.nodes.get(&dir_id).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "dir not found")
})?;
node.path.clone()
};
let full_path = self.path.join(&dir_path);
let mut read_dir = tokio::fs::read_dir(&full_path).await?;
while let Some(entry) = read_dir.next_entry().await? {
let name = entry.file_name().to_string_lossy().to_string();
if name == ".tailtalk" || name == ".AppleDesktop" {
continue;
}
let rel_path = dir_path.join(&name);
if self.path_to_id.contains_key(&rel_path) {
continue;
}
let is_dir = entry.file_type().await?.is_dir();
let new_id = self.next_id;
self.next_id += 1;
self.nodes.insert(
new_id,
Node {
id: new_id,
parent_id: dir_id,
name: posix_name_to_afp(&name),
is_dir,
path: rel_path.clone(),
data_fork: None,
resource_fork: None,
},
);
self.path_to_id.insert(rel_path, new_id);
{
let afp_name = posix_name_to_afp(&name);
register_mangle_if_needed(&self.mangle_tree, &afp_name, &name);
}
}
Ok(())
}
pub async fn resolve_node_lazy(
&mut self,
directory_id: u32,
path_name: &Path,
) -> Result<u32, AfpError> {
if let Ok(id) = self.resolve_node(directory_id, path_name) {
return Ok(id);
}
let dir_id = if directory_id == 0 { 2 } else { directory_id };
if dir_id >= 2 && !afp_path_is_empty(path_name) {
let _ = self.ensure_dir_populated(dir_id).await;
}
self.resolve_node(directory_id, path_name)
}
pub fn get_attributes(&self) -> u16 {
0
}
pub fn get_created_at(&self) -> u32 {
self.created_at
}
pub async fn get_modified_at(&self) -> u32 {
tokio::fs::metadata(&self.path)
.await
.and_then(|m| m.modified())
.map(time_to_afp_v1)
.unwrap_or(self.created_at)
}
pub fn get_backup_at(&self) -> u32 {
0
}
pub fn get_volume_id(&self) -> u16 {
self.volume_id
}
pub fn get_bytes_free(&self) -> u32 {
(i32::MAX / 2) as u32
}
pub fn get_bytes_total(&self) -> u32 {
(i32::MAX / 2) as u32
}
pub fn get_fp_volume(&self) -> FPVolume {
FPVolume {
has_password: false,
has_config_info: false,
name: self.name.clone().into(),
}
}
pub async fn get_bitmap_resp(
&self,
bitmap: FPVolumeBitmap,
output: &mut [u8],
) -> Result<usize, AfpError> {
let mut offset = 0;
output[offset..offset + 2].copy_from_slice(&bitmap.bits().to_be_bytes());
offset += 2;
if bitmap.contains(FPVolumeBitmap::ATTRIBUTES) {
let attr_bytes = self.get_attributes().to_be_bytes();
output[offset..offset + 2].copy_from_slice(&attr_bytes);
offset += 2;
}
if bitmap.contains(FPVolumeBitmap::SIGNATURE) {
let signature_bytes = (VolumeSignature::FixedDirectoryID as u16).to_be_bytes();
output[offset..offset + 2].copy_from_slice(&signature_bytes);
offset += 2;
}
if bitmap.contains(FPVolumeBitmap::CREATION_DATE) {
let created_at_bytes = self.get_created_at().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&created_at_bytes);
offset += 4;
}
if bitmap.contains(FPVolumeBitmap::MODIFICATION_DATE) {
let modified_at_bytes = self.get_modified_at().await.to_be_bytes();
output[offset..offset + 4].copy_from_slice(&modified_at_bytes);
offset += 4;
}
if bitmap.contains(FPVolumeBitmap::BACKUP_DATE) {
let backup_at_bytes = self.get_backup_at().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&backup_at_bytes);
offset += 4;
}
if bitmap.contains(FPVolumeBitmap::VOLUME_ID) {
let volume_id_bytes = self.get_volume_id().to_be_bytes();
output[offset..offset + 2].copy_from_slice(&volume_id_bytes);
offset += 2;
}
if bitmap.contains(FPVolumeBitmap::BYTES_FREE) {
let bytes_free_bytes = self.get_bytes_free().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&bytes_free_bytes);
offset += 4;
}
if bitmap.contains(FPVolumeBitmap::BYTES_TOTAL) {
let bytes_total_bytes = self.get_bytes_total().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&bytes_total_bytes);
offset += 4;
}
if bitmap.contains(FPVolumeBitmap::VOLUME_NAME) {
let params_relative_offset = offset as u16;
output[offset..offset + 2].copy_from_slice(¶ms_relative_offset.to_be_bytes());
offset += 2;
output[offset] = self.name.len() as u8;
offset += 1;
output[offset..(offset + self.name.len())].copy_from_slice(self.name.as_bytes());
offset += self.name.len();
}
Ok(offset)
}
pub async fn count_directory_entries(path: &PathBuf) -> std::io::Result<u16> {
let mut entries = tokio::fs::read_dir(path).await?;
let mut count: u16 = 0;
while let Some(entry) = entries.next_entry().await? {
let name = entry.file_name();
let name = name.to_string_lossy();
if name == ".tailtalk" || name == ".AppleDesktop" {
continue;
}
count = count.saturating_add(1);
}
Ok(count)
}
pub async fn get_directory_parms_resp(
&self,
bitmap: FPDirectoryBitmap,
relative_path: &PathBuf,
output: &mut [u8],
) -> Result<usize, AfpError> {
let mut offset = 0;
let mut variable_len_offset = 0;
let id = *self
.path_to_id
.get(relative_path)
.ok_or(AfpError::ObjectNotFound)?;
let node = self.nodes.get(&id).ok_or(AfpError::ObjectNotFound)?;
let full_path = self.path.join(relative_path);
if bitmap.contains(FPDirectoryBitmap::ATTRIBUTES) {
let attributes = node.get_attributes(&self.path).await;
output[offset..offset + 2].copy_from_slice(&attributes.to_be_bytes());
offset += 2;
}
if bitmap.contains(FPDirectoryBitmap::PARENT_DIR_ID) {
output[offset..offset + 4].copy_from_slice(&node.parent_id.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::CREATE_DATE) {
let created_at_bytes = self.get_created_at().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&created_at_bytes);
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::MODIFICATION_DATE) {
let modified_at_bytes = self.get_modified_at().await.to_be_bytes();
output[offset..offset + 4].copy_from_slice(&modified_at_bytes);
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::BACKUP_DATE) {
let backup_at_bytes = self.get_backup_at().to_be_bytes();
output[offset..offset + 4].copy_from_slice(&backup_at_bytes);
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::FINDER_INFO) {
let raw: [u8; 32] = node.get_finder_info(&self.path).await.into();
output[offset..offset + 32].copy_from_slice(&raw);
offset += 32;
}
if bitmap.contains(FPDirectoryBitmap::LONG_NAME) {
let mut long_name_offset = bitmap.long_name_offset();
output[offset..offset + 2].copy_from_slice(&(long_name_offset as u16).to_be_bytes());
offset += 2;
let encoded_name = mangle_name(&node.name);
let name_len = encoded_name.len();
output[long_name_offset] = name_len as u8;
long_name_offset += 1;
output[long_name_offset..long_name_offset + name_len]
.copy_from_slice(&encoded_name);
variable_len_offset += name_len + 1;
}
if bitmap.contains(FPDirectoryBitmap::DIR_ID) {
output[offset..offset + 4].copy_from_slice(&node.id.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::OFFSPRING_COUNT) {
let count = Volume::count_directory_entries(&full_path)
.await
.map_err(|_| AfpError::ObjectNotFound)?;
output[offset..offset + 2].copy_from_slice(&count.to_be_bytes());
offset += 2;
}
if bitmap.contains(FPDirectoryBitmap::OWNER_ID) {
output[offset..offset + 4].copy_from_slice(&0u32.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::GROUP_ID) {
output[offset..offset + 4].copy_from_slice(&0u32.to_be_bytes());
offset += 4;
}
if bitmap.contains(FPDirectoryBitmap::ACCESS_RIGHTS) {
output[offset..offset + 4].copy_from_slice(&0x87070707u32.to_be_bytes());
offset += 4;
}
Ok(offset + variable_len_offset)
}
pub async fn get_file_parms_resp(
&self,
bitmap: FPFileBitmap,
relative_path: &PathBuf,
output: &mut [u8],
) -> Result<usize, AfpError> {
let id = *self
.path_to_id
.get(relative_path)
.ok_or(AfpError::ObjectNotFound)?;
let node = self.nodes.get(&id).ok_or(AfpError::ObjectNotFound)?;
node.get_file_parms_resp(&self.path, bitmap, output).await
}
pub async fn get_fork_parms(
&self,
bitmap: FPFileBitmap,
fork_id: u16,
output: &mut [u8],
) -> Result<usize, AfpError> {
let (node_id, _fork_type) = self
.fork_ref_to_node_id
.get(&fork_id)
.ok_or(AfpError::ObjectNotFound)?;
let node = self.nodes.get(node_id).ok_or(AfpError::ObjectNotFound)?;
node.get_file_parms_resp(&self.path, bitmap, output).await
}
pub async fn open_fork(
&mut self,
fork_type: ForkType,
bitmap: FPFileBitmap,
dir_id: u32,
relative_path: &Path,
output: &mut [u8],
) -> Result<usize, AfpError> {
let mut offset = 0;
let id = self.resolve_node_lazy(dir_id, relative_path).await?;
let full_relative_path = self
.nodes
.get(&id)
.ok_or(AfpError::ObjectNotFound)?
.path
.clone();
match fork_type {
ForkType::Data => {
let node = self.nodes.get_mut(&id).ok_or(AfpError::ObjectNotFound)?;
if node.data_fork.is_some() {
return Err(AfpError::FileBusy);
}
let absolute_path = self.path.join(&full_relative_path);
node.open_data_fork(&absolute_path).await.map_err(|e| {
eprintln!("Error opening data fork: {:?}", e);
AfpError::AccessDenied
})?;
let fork_ref_num = self.next_fork_ref_num;
self.next_fork_ref_num = self.next_fork_ref_num.wrapping_add(1);
if self.next_fork_ref_num == 0 {
self.next_fork_ref_num = 1;
}
self.fork_ref_to_node_id.insert(fork_ref_num, (id, fork_type));
output[offset..offset + 2].copy_from_slice(&bitmap.bits().to_be_bytes());
offset += 2;
output[offset..offset + 2].copy_from_slice(&fork_ref_num.to_be_bytes());
offset += 2;
match self
.get_file_parms_resp(bitmap, &full_relative_path, &mut output[offset..])
.await
{
Ok(len) => {
offset += len;
Ok(offset)
}
Err(e) => Err(e),
}
}
ForkType::Resource => {
let node = self.nodes.get_mut(&id).ok_or(AfpError::ObjectNotFound)?;
if node.resource_fork.is_some() {
return Err(AfpError::FileBusy);
}
let (rsrc_target, _) =
resolve_resource_fork_path(&self.path, &node.path.clone()).await;
node.open_resource_fork(&rsrc_target).await.map_err(|e| {
eprintln!("Error opening resource fork: {:?}", e);
AfpError::AccessDenied
})?;
let fork_ref_num = self.next_fork_ref_num;
self.next_fork_ref_num = self.next_fork_ref_num.wrapping_add(1);
if self.next_fork_ref_num == 0 {
self.next_fork_ref_num = 1;
}
self.fork_ref_to_node_id.insert(fork_ref_num, (id, fork_type));
output[offset..offset + 2].copy_from_slice(&bitmap.bits().to_be_bytes());
offset += 2;
output[offset..offset + 2].copy_from_slice(&fork_ref_num.to_be_bytes());
offset += 2;
match self
.get_file_parms_resp(bitmap, &full_relative_path, &mut output[offset..])
.await
{
Ok(len) => {
offset += len;
Ok(offset)
}
Err(e) => Err(e),
}
}
}
}
pub async fn open_dt(&mut self) -> Result<u16, AfpError> {
let apple_desktop = self.path.join(".AppleDesktop");
if !apple_desktop.exists() {
let _ = tokio::fs::create_dir(&apple_desktop).await;
}
Ok(self.desktop_database.dt_ref_num)
}
pub fn add_icon(
&self,
dt_ref_num: u16,
req: &tailtalk_packets::afp::FPAddIcon,
data: &[u8],
) -> Result<(), AfpError> {
if self.desktop_database.dt_ref_num == dt_ref_num {
return self.desktop_database.add_icon(req.file_creator, req.file_type, req.icon_type, data);
}
Err(AfpError::ItemNotFound)
}
pub fn get_icon(
&self,
dt_ref_num: u16,
req: &tailtalk_packets::afp::FPGetIcon,
) -> Result<Vec<u8>, AfpError> {
if self.desktop_database.dt_ref_num == dt_ref_num {
return self.desktop_database.get_icon(req.file_creator, req.file_type, req.icon_type, req.size);
}
Err(AfpError::ItemNotFound)
}
pub fn get_icon_info(
&self,
dt_ref_num: u16,
req: &tailtalk_packets::afp::FPGetIconInfo,
) -> Result<(u32, u32, u16), AfpError> {
if self.desktop_database.dt_ref_num == dt_ref_num {
return self.desktop_database.get_icon_info(req.file_creator, req.icon_type);
}
Err(AfpError::ItemNotFound)
}
pub async fn close_fork(&mut self, fork_id: u16) -> Result<(), AfpError> {
let (node_id, fork_type) = *self
.fork_ref_to_node_id
.get(&fork_id)
.ok_or(AfpError::ObjectNotFound)?;
let node = self
.nodes
.get_mut(&node_id)
.ok_or(AfpError::ObjectNotFound)?;
match fork_type {
ForkType::Data => node.close_data_fork().await,
ForkType::Resource => node.close_resource_fork().await,
}
self.fork_ref_to_node_id.remove(&fork_id);
self.fork_locks.remove(&fork_id);
Ok(())
}
pub async fn read(
&mut self,
read_req: &FPRead,
output: &mut [u8],
) -> Result<(usize, bool), AfpError> {
use tokio::io::{AsyncReadExt, AsyncSeekExt};
let &(node_id, fork_type) = self
.fork_ref_to_node_id
.get(&read_req.fork_id)
.ok_or(AfpError::ObjectNotFound)?;
let node = self
.nodes
.get_mut(&node_id)
.ok_or(AfpError::ObjectNotFound)?;
let file = match fork_type {
ForkType::Data => node.data_fork.as_mut(),
ForkType::Resource => node.resource_fork.as_mut(),
}
.ok_or(AfpError::ObjectNotFound)?;
file.seek(std::io::SeekFrom::Start(read_req.offset as u64))
.await
.map_err(|e| {
error!("Failed to seek to offset {}: {:?}", read_req.offset, e);
AfpError::AccessDenied
})?;
let max_bytes = std::cmp::min(read_req.req_count as usize, output.len());
let (bytes_read, is_eof) = if read_req.newline_mask != 0 {
let mut total_read = 0;
let mut hit_eof = false;
for i in 0..max_bytes {
match file.read_exact(&mut output[i..i + 1]).await {
Ok(_) => {
total_read += 1;
if read_req.byte_matches_newline(output[i]) {
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
hit_eof = true;
break;
}
Err(e) => {
error!("Failed to read from fork: {:?}", e);
return Err(AfpError::AccessDenied);
}
}
}
(total_read, hit_eof)
} else {
let mut total_read = 0;
let mut hit_eof = false;
while total_read < max_bytes {
match file.read(&mut output[total_read..max_bytes]).await {
Ok(0) => {
hit_eof = true;
break;
}
Ok(n) => {
total_read += n;
}
Err(e) => {
error!("Failed to read from fork: {:?}", e);
return Err(AfpError::AccessDenied);
}
}
}
(total_read, hit_eof)
};
Ok((bytes_read, is_eof))
}
pub async fn set_fork_parms(&mut self, cmd: FPSetForkParms) -> Result<(), AfpError> {
let (node_id, fork_type) = *self
.fork_ref_to_node_id
.get(&cmd.fork_ref_num)
.ok_or(AfpError::ObjectNotFound)?;
let node = self
.nodes
.get_mut(&node_id)
.ok_or(AfpError::ObjectNotFound)?;
match fork_type {
ForkType::Data => {
if cmd.resource_fork_length.is_some() {
return Err(AfpError::BitmapErr);
}
if let Some(len) = cmd.data_fork_length {
let file = node.data_fork.as_mut().ok_or(AfpError::ObjectNotFound)?;
file.set_len(len as u64).await.map_err(|e| {
error!("Failed to set fork length: {:?}", e);
AfpError::AccessDenied
})?;
}
}
ForkType::Resource => {
if cmd.data_fork_length.is_some() {
return Err(AfpError::BitmapErr);
}
if let Some(len) = cmd.resource_fork_length {
let file = node
.resource_fork
.as_mut()
.ok_or(AfpError::ObjectNotFound)?;
file.set_len(len as u64).await.map_err(|e| {
error!("Failed to set resource fork length: {:?}", e);
AfpError::AccessDenied
})?;
}
}
}
Ok(())
}
pub async fn enumerate(
&mut self,
enumerate_cmd: FPEnumerate,
output: &mut [u8],
) -> Result<usize, AfpError> {
let node_id = self.resolve_node(
enumerate_cmd.directory_id,
&afp_path_to_posix(enumerate_cmd.path.as_str()),
)?;
let (node_is_dir, node_path) = {
let node = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?;
(node.is_dir, node.path.clone())
};
if !node_is_dir {
return Err(AfpError::ObjectTypeErr);
}
let full_path = self.path.join(&node_path);
let mut entries = Vec::new();
let mut read_dir = tokio::fs::read_dir(&full_path)
.await
.map_err(|_| AfpError::ObjectNotFound)?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|_| AfpError::ObjectNotFound)?
{
let name = entry.file_name().to_string_lossy().to_string();
if name == ".tailtalk" || name == ".AppleDesktop" {
continue;
}
let file_type = entry
.file_type()
.await
.map_err(|_| AfpError::ObjectNotFound)?;
let is_dir = file_type.is_dir();
entries.push((entry, is_dir, name));
}
entries.sort_by(|a, b| match (a.1, b.1) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.2.cmp(&b.2),
});
if entries.is_empty() {
return Err(AfpError::ObjectNotFound);
}
let start_index = enumerate_cmd.start_index as usize;
if start_index == 0 {
return Err(AfpError::ObjectNotFound);
}
let start_idx = start_index - 1;
let end_idx = std::cmp::min(start_idx + enumerate_cmd.req_count as usize, entries.len());
let entries_to_return: &[(tokio::fs::DirEntry, bool, String)] =
if start_idx < entries.len() {
&entries[start_idx..end_idx]
} else {
&[]
};
let mut offset = 0;
let count_offset = offset;
offset += 2;
let mut actual_count: u16 = 0;
for (_entry, is_dir, name) in entries_to_return {
let entry_relative_path = node_path.join(name);
if !self.path_to_id.contains_key(&entry_relative_path) {
let new_id = self.next_id;
self.next_id += 1;
let afp_name_for_node = posix_name_to_afp(name);
let new_node = Node {
id: new_id,
parent_id: node_id,
name: afp_name_for_node.clone(),
is_dir: *is_dir,
path: entry_relative_path.clone(),
data_fork: None,
resource_fork: None,
};
self.nodes.insert(new_id, new_node);
self.path_to_id.insert(entry_relative_path.clone(), new_id);
register_mangle_if_needed(&self.mangle_tree, &afp_name_for_node, name);
}
let mut entry_offset = offset;
let afp_name = posix_name_to_afp(name);
if *is_dir {
let mut pad_byte = false;
let mut directory_bitmap_len =
enumerate_cmd.directory_bitmap.response_len(&afp_name);
if !directory_bitmap_len.is_multiple_of(2) {
directory_bitmap_len += 1;
pad_byte = true;
}
if offset + 2 + directory_bitmap_len > enumerate_cmd.max_reply_size as usize {
break;
}
output[entry_offset] = (directory_bitmap_len + 2) as u8;
entry_offset += 1;
output[entry_offset] = FileType::Directory.into();
entry_offset += 1;
match self
.get_directory_parms_resp(
enumerate_cmd.directory_bitmap,
&entry_relative_path,
&mut output[entry_offset..],
)
.await
{
Ok(len) => {
entry_offset += len;
if pad_byte {
output[entry_offset] = 0;
entry_offset += 1;
}
offset = entry_offset;
actual_count += 1;
}
Err(e) => {
tracing::error!(
"BUG: failed to get parms for {:?}: {:?}",
entry_relative_path,
e
);
continue;
}
}
} else {
let mut pad_byte = false;
let mut file_bitmap_len = enumerate_cmd.file_bitmap.response_len(&afp_name);
if !file_bitmap_len.is_multiple_of(2) {
file_bitmap_len += 1;
pad_byte = true;
}
if offset + 2 + file_bitmap_len > enumerate_cmd.max_reply_size as usize {
break;
}
output[entry_offset] = (file_bitmap_len + 2) as u8;
entry_offset += 1;
output[entry_offset] = FileType::File.into();
entry_offset += 1;
match self
.get_file_parms_resp(
enumerate_cmd.file_bitmap,
&entry_relative_path,
&mut output[entry_offset..],
)
.await
{
Ok(len) => {
entry_offset += len;
if pad_byte {
output[entry_offset] = 0;
entry_offset += 1;
}
offset = entry_offset;
actual_count += 1;
}
Err(e) => {
tracing::error!(
"BUG: failed to get parms for {:?}: {:?}",
entry_relative_path,
e
);
continue;
}
}
}
}
output[count_offset..count_offset + 2].copy_from_slice(&actual_count.to_be_bytes());
Ok(offset)
}
pub async fn byte_range_lock(
&mut self,
lock_req: &tailtalk_packets::afp::FPByteRangeLock,
) -> Result<u32, AfpError> {
let (node_id, _fork_type) = self
.fork_ref_to_node_id
.get(&lock_req.fork_id)
.ok_or(AfpError::ObjectNotFound)?;
let node = self.nodes.get(node_id).ok_or(AfpError::ObjectNotFound)?;
let absolute_path = self.path.join(&node.path);
let metadata = tokio::fs::metadata(&absolute_path)
.await
.map_err(|_| AfpError::ObjectNotFound)?;
let fork_size = metadata.len();
let absolute_offset: u64 = match lock_req.flags.contains(FPByteRangeLockFlags::END) {
false => {
if lock_req.offset < 0 {
0
} else {
lock_req.offset as u64
}
}
true => {
if lock_req.offset < 0 {
fork_size.saturating_sub((-lock_req.offset) as u64)
} else {
fork_size.saturating_add(lock_req.offset as u64)
}
}
};
let lock_end = absolute_offset.saturating_add(lock_req.length as u64);
let locks = self.fork_locks.entry(lock_req.fork_id).or_default();
match lock_req.flags.contains(FPByteRangeLockFlags::UNLOCK) {
false => {
for (existing_offset, existing_length) in locks.iter() {
let existing_end = existing_offset.saturating_add(*existing_length);
if absolute_offset < existing_end && lock_end > *existing_offset {
return Err(AfpError::RangeOverlap);
}
}
locks.push((absolute_offset, lock_req.length as u64));
Ok(absolute_offset as u32)
}
true => {
if let Some(pos) = locks.iter().position(|(off, len)| {
*off == absolute_offset && *len == lock_req.length as u64
}) {
locks.remove(pos);
Ok(0)
} else {
Err(AfpError::RangeNotLocked)
}
}
}
}
pub async fn move_and_rename(
&mut self,
src_dir_id: u32,
dst_dir_id: u32,
src_path: &std::path::Path,
dst_path: &std::path::Path,
new_name: &str,
) -> Result<(), AfpError> {
let src_node_id = self.resolve_node_lazy(src_dir_id, src_path).await?;
if src_node_id <= 2 {
return Err(AfpError::AccessDenied);
}
let dst_node_id = self.resolve_node_lazy(dst_dir_id, dst_path).await?;
let (src_old_name, src_old_relative, src_is_dir) = {
let n = self
.nodes
.get(&src_node_id)
.ok_or(AfpError::ObjectNotFound)?;
(n.name.clone(), n.path.clone(), n.is_dir)
};
let dst_relative = {
let n = self
.nodes
.get(&dst_node_id)
.ok_or(AfpError::ObjectNotFound)?;
if !n.is_dir {
return Err(AfpError::ObjectTypeErr);
}
n.path.clone()
};
let effective_afp_name = if new_name.is_empty() {
src_old_name.clone()
} else {
new_name.to_string()
};
let effective_posix_name = effective_afp_name.replace('/', ":");
let new_relative = dst_relative.join(&effective_posix_name);
if new_relative != src_old_relative && self.path_to_id.contains_key(&new_relative) {
return Err(AfpError::ObjectExists);
}
let old_absolute = self.path.join(&src_old_relative);
let new_absolute = self.path.join(&new_relative);
tokio::fs::rename(&old_absolute, &new_absolute)
.await
.map_err(|e| {
error!("move {:?} → {:?}: {:?}", old_absolute, new_absolute, e);
AfpError::AccessDenied
})?;
if !src_is_dir {
let old_sidecar = rsrc_path(&self.path, &src_old_relative);
if old_sidecar.exists() {
let new_sidecar = rsrc_path(&self.path, &new_relative);
let _ = tokio::fs::rename(&old_sidecar, &new_sidecar).await;
}
}
self.path_to_id.remove(&src_old_relative);
self.path_to_id.insert(new_relative.clone(), src_node_id);
{
let node = self.nodes.get_mut(&src_node_id).unwrap();
node.parent_id = dst_node_id;
node.name = effective_afp_name;
node.path = new_relative.clone();
}
if src_is_dir {
let child_updates: Vec<(u32, PathBuf, PathBuf)> = self
.nodes
.iter()
.filter(|(id, node)| {
**id != src_node_id && node.path.starts_with(&src_old_relative)
})
.map(|(id, node)| {
let suffix = node.path.strip_prefix(&src_old_relative).unwrap();
let new_child_path = new_relative.join(suffix);
(*id, node.path.clone(), new_child_path)
})
.collect();
for (id, old_path, new_child_path) in child_updates {
self.path_to_id.remove(&old_path);
self.path_to_id.insert(new_child_path.clone(), id);
let _ = self.desktop_database.move_comment(&old_path, &new_child_path);
self.nodes.get_mut(&id).unwrap().path = new_child_path;
}
}
let _ = self.desktop_database.move_comment(&src_old_relative, &new_relative);
if !src_is_dir {
let old_path_str = src_old_relative.to_string_lossy();
let new_path_str = new_relative.to_string_lossy();
let _ = self.desktop_database.move_appl_path(&old_path_str, &new_path_str, src_dir_id, dst_node_id);
}
Ok(())
}
pub async fn rename(
&mut self,
dir_id: u32,
src_path: &std::path::Path,
new_name: &str,
) -> Result<(), AfpError> {
self.move_and_rename(dir_id, dir_id, src_path, std::path::Path::new(""), new_name)
.await
}
pub async fn delete(&mut self, delete_req: &FPDelete) -> Result<(), AfpError> {
let node_id = self
.resolve_node_lazy(
delete_req.directory_id,
&afp_path_to_posix(delete_req.path.as_str()),
)
.await?;
if node_id == 2 {
return Err(AfpError::AccessDenied);
}
let (is_dir, full_path, relative_path, is_open) = {
let node = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?;
(
node.is_dir,
self.path.join(&node.path),
node.path.clone(),
node.data_fork.is_some(),
)
};
if !is_dir {
if is_open {
return Err(AfpError::FileBusy);
}
tokio::fs::remove_file(&full_path).await.map_err(|e| {
error!("Failed to remove file {:?}: {:?}", full_path, e);
AfpError::AccessDenied
})?;
} else {
let mut read_dir = tokio::fs::read_dir(&full_path).await.map_err(|e| {
error!("Failed to read directory {:?}: {:?}", full_path, e);
AfpError::ObjectNotFound
})?;
if let Ok(Some(_)) = read_dir.next_entry().await {
return Err(AfpError::DirNotEmpty);
}
tokio::fs::remove_dir(&full_path).await.map_err(|e| {
error!("Failed to remove directory {:?}: {:?}", full_path, e);
AfpError::AccessDenied
})?;
}
if !is_dir {
let sidecar = rsrc_path(&self.path, &relative_path);
let _ = tokio::fs::remove_file(&sidecar).await;
}
let _ = self.desktop_database.remove_comment(&relative_path);
if !is_dir {
let path_str = relative_path.to_string_lossy();
let parent_id = self.nodes.get(&node_id).map(|n| n.parent_id).unwrap_or(0);
let _ = self.desktop_database.delete_appls_for_path(parent_id, &path_str);
}
self.nodes.remove(&node_id);
self.path_to_id.remove(&relative_path);
Ok(())
}
pub async fn copy_file(
&mut self,
src_dir_id: u32,
src_path: &Path,
dst_dir_id: u32,
dst_path: &Path,
new_name: &str,
) -> Result<u32, AfpError> {
let src_node_id = self.resolve_node_lazy(src_dir_id, src_path).await?;
let (src_is_dir, src_relative, src_name) = {
let n = self.nodes.get(&src_node_id).ok_or(AfpError::ObjectNotFound)?;
if n.is_dir {
return Err(AfpError::ObjectTypeErr);
}
(n.is_dir, n.path.clone(), n.name.clone())
};
let _ = src_is_dir;
let dst_node_id = self.resolve_node_lazy(dst_dir_id, dst_path).await?;
let dst_relative = {
let n = self.nodes.get(&dst_node_id).ok_or(AfpError::ObjectNotFound)?;
if !n.is_dir {
return Err(AfpError::ObjectTypeErr);
}
n.path.clone()
};
let effective_afp_name = if new_name.is_empty() { src_name.as_str() } else { new_name };
let effective_posix_name = effective_afp_name.replace('/', ":");
let new_relative = dst_relative.join(&effective_posix_name);
if self.path_to_id.contains_key(&new_relative) {
return Err(AfpError::ObjectExists);
}
let src_absolute = self.path.join(&src_relative);
let dst_absolute = self.path.join(&new_relative);
tokio::fs::copy(&src_absolute, &dst_absolute).await.map_err(|e| {
error!("copy_file {:?} → {:?}: {:?}", src_absolute, dst_absolute, e);
AfpError::AccessDenied
})?;
#[cfg(all(unix, not(target_os = "macos")))]
for attr in xattr::list(&src_absolute).into_iter().flatten() {
if let Ok(Some(val)) = xattr::get(&src_absolute, &attr) {
let _ = xattr::set(&dst_absolute, &attr, &val);
}
}
#[cfg(windows)]
{
let src_stream = format!("{}:{}", src_absolute.display(), FINDER_INFO_STREAM);
let dst_stream = format!("{}:{}", dst_absolute.display(), FINDER_INFO_STREAM);
if let Ok(data) = std::fs::read(&src_stream) {
let _ = std::fs::write(&dst_stream, &data);
}
}
let src_sidecar = rsrc_path(&self.path, &src_relative);
if src_sidecar.exists() {
let dst_sidecar = rsrc_path(&self.path, &new_relative);
if let Some(parent) = dst_sidecar.parent() {
let _ = tokio::fs::create_dir_all(parent).await;
}
let _ = tokio::fs::copy(&src_sidecar, &dst_sidecar).await;
}
let _ = self.desktop_database.copy_comment(&src_relative, &new_relative);
let src_path_str = src_relative.to_string_lossy();
let new_path_str = new_relative.to_string_lossy();
let _ = self.desktop_database.copy_appl(&src_path_str, &new_path_str, src_dir_id, dst_node_id);
let new_id = self.next_id;
self.next_id += 1;
let node = Node {
id: new_id,
parent_id: dst_node_id,
name: effective_afp_name.to_string(),
is_dir: false,
path: new_relative.clone(),
data_fork: None,
resource_fork: None,
};
self.nodes.insert(new_id, node);
self.path_to_id.insert(new_relative, new_id);
Ok(new_id)
}
pub async fn sync(&mut self) -> Result<(), AfpError> {
for node in self.nodes.values_mut() {
if let Some(file) = &mut node.data_fork {
file.sync_all().await.map_err(|e| {
error!("Failed to sync data fork {:?}: {:?}", node.path, e);
AfpError::AccessDenied
})?;
}
if let Some(file) = &mut node.resource_fork {
file.sync_all().await.map_err(|e| {
error!("Failed to sync resource fork {:?}: {:?}", node.path, e);
AfpError::AccessDenied
})?;
}
}
Ok(())
}
pub async fn write_fork(
&mut self,
fork_id: u16,
offset: u64,
data: &[u8],
) -> Result<usize, AfpError> {
let (node_id, fork_type) = self
.fork_ref_to_node_id
.get(&fork_id)
.ok_or(AfpError::AccessDenied)?;
let node = self
.nodes
.get_mut(node_id)
.ok_or(AfpError::ObjectNotFound)?;
tracing::info!(
"Writing {} bytes to fork {} at offset {}",
data.len(),
fork_id,
offset
);
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
let file = match fork_type {
ForkType::Data => node.data_fork.as_mut(),
ForkType::Resource => node.resource_fork.as_mut(),
}
.ok_or(AfpError::AccessDenied)?;
file.seek(tokio::io::SeekFrom::Start(offset))
.await
.map_err(|_| AfpError::MiscErr)?;
file.write_all(data).await.map_err(|_| AfpError::MiscErr)?;
Ok(data.len())
}
pub fn add_appl(
&self,
req: &tailtalk_packets::afp::FPAddAPPL,
) -> Result<(), AfpError> {
if self.desktop_database.dt_ref_num == req.dt_ref_num {
return self.desktop_database.add_appl(req.file_creator, req.tag, req.directory_id, req.path.as_str());
}
Err(AfpError::ItemNotFound)
}
pub fn remove_appl(
&self,
req: &tailtalk_packets::afp::FPRemoveAPPL,
) -> Result<(), AfpError> {
if self.desktop_database.dt_ref_num == req.dt_ref_num {
return self.desktop_database.remove_appl(req.file_creator, req.directory_id, req.path.as_str());
}
Err(AfpError::ItemNotFound)
}
pub fn get_appl(
&self,
req: &tailtalk_packets::afp::FPGetAPPL,
) -> Result<(u32, u32, String), AfpError> {
if self.desktop_database.dt_ref_num == req.dt_ref_num {
return self.desktop_database.get_appl(req.file_creator, req.appl_index);
}
Err(AfpError::ItemNotFound)
}
pub fn set_comment(
&self,
directory_id: u32,
path: &Path,
comment: &[u8],
) -> Result<(), AfpError> {
let node_id = self.resolve_node(directory_id, path)?;
let rel_path = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?.path.clone();
self.desktop_database.set_comment(&rel_path, comment)
}
pub fn get_comment(&self, directory_id: u32, path: &Path) -> Result<Vec<u8>, AfpError> {
let node_id = self.resolve_node(directory_id, path)?;
let rel_path = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?.path.clone();
self.desktop_database.get_comment(&rel_path)
}
pub fn remove_comment(&self, directory_id: u32, path: &Path) -> Result<(), AfpError> {
let node_id = self.resolve_node(directory_id, path)?;
let rel_path = self.nodes.get(&node_id).ok_or(AfpError::ObjectNotFound)?.path.clone();
self.desktop_database.remove_comment(&rel_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tailtalk_packets::afp::{FPDelete, FPDirectoryBitmap, FPEnumerate, FPFileBitmap};
use tempfile::tempdir;
use tokio::fs::File;
async fn make_test_volume(name: String, path: PathBuf) -> Volume {
let db = crate::afp::DesktopDatabase::open_or_create(&path).unwrap();
Volume::new(name, path, 1, db).await
}
#[tokio::test]
async fn test_enumerate_volume_root() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let volume_name = "TestVol".to_string();
let file1_path = root_path.join("file1.txt");
let file2_path = root_path.join("file2.txt");
File::create(&file1_path).await.unwrap();
File::create(&file2_path).await.unwrap();
let mut volume = make_test_volume(volume_name, root_path.clone()).await;
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: 2,
file_bitmap: FPFileBitmap::LONG_NAME | FPFileBitmap::FILE_NUMBER, directory_bitmap: FPDirectoryBitmap::LONG_NAME | FPDirectoryBitmap::DIR_ID,
req_count: 69,
start_index: 1,
max_reply_size: 1024,
path: "".into(), };
let mut output = [0u8; 1024];
let result = volume.enumerate(enumerate_cmd, &mut output).await;
assert!(result.is_ok(), "Enumerate failed: {:?}", result.err());
let count = u16::from_be_bytes(output[0..2].try_into().unwrap());
println!("Enumerated {} items", count);
assert_eq!(count, 2, "Should have found 2 files");
}
#[tokio::test]
async fn test_enumerate_respects_max_reply_size() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
for i in 0..10u32 {
File::create(root_path.join(format!("file_{}.txt", i)))
.await
.unwrap();
}
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let bitmap = FPFileBitmap::LONG_NAME | FPFileBitmap::FILE_NUMBER;
let max_reply_size: u16 = 55;
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: 2,
file_bitmap: bitmap,
directory_bitmap: FPDirectoryBitmap::empty(),
req_count: 100,
start_index: 1,
max_reply_size,
path: "".into(),
};
let mut output = [0u8; 512];
let offset = volume
.enumerate(enumerate_cmd, &mut output)
.await
.unwrap();
assert!(
offset <= max_reply_size as usize,
"enumerate wrote {} bytes but max_reply_size is {}",
offset,
max_reply_size
);
let count = u16::from_be_bytes(output[0..2].try_into().unwrap());
assert_eq!(count, 2, "expected exactly 2 entries to fit in {} bytes", max_reply_size);
}
#[tokio::test]
async fn test_open_fork_and_write() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("root_file.txt")).await.unwrap();
tokio::fs::create_dir(root_path.join("subdir"))
.await
.unwrap();
File::create(root_path.join("subdir").join("sub_file.txt"))
.await
.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let mut output = [0u8; 256];
let result = volume
.open_fork(
ForkType::Data,
FPFileBitmap::FILE_NUMBER,
2,
&PathBuf::from("root_file.txt"),
&mut output,
)
.await;
assert!(
result.is_ok(),
"open_fork at root failed: {:?}",
result.err()
);
let fork_ref = u16::from_be_bytes(output[2..4].try_into().unwrap());
assert!(fork_ref > 0);
volume.write_fork(fork_ref, 0, b"hello root").await.unwrap();
let double_open = volume
.open_fork(
ForkType::Data,
FPFileBitmap::FILE_NUMBER,
2,
&PathBuf::from("root_file.txt"),
&mut output,
)
.await;
assert_eq!(
double_open,
Err(AfpError::FileBusy),
"double open should return FileBusy"
);
volume.close_fork(fork_ref).await.unwrap();
let subdir_id = volume.resolve_node_lazy(2, Path::new("subdir")).await.unwrap();
let result = volume
.open_fork(
ForkType::Data,
FPFileBitmap::FILE_NUMBER,
subdir_id,
&PathBuf::from("sub_file.txt"),
&mut output,
)
.await;
assert!(
result.is_ok(),
"open_fork in subdir failed: {:?}",
result.err()
);
let fork_ref = u16::from_be_bytes(output[2..4].try_into().unwrap());
assert!(fork_ref > 0);
volume
.write_fork(fork_ref, 0, b"hello subdir")
.await
.unwrap();
volume.close_fork(fork_ref).await.unwrap();
}
#[tokio::test]
async fn test_resource_fork_roundtrip() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("rsrc_test.txt")).await.unwrap();
tokio::fs::create_dir(root_path.join("subdir"))
.await
.unwrap();
File::create(root_path.join("subdir").join("rsrc_sub.txt"))
.await
.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let mut output = [0u8; 256];
let result = volume
.open_fork(
ForkType::Resource,
FPFileBitmap::RESOURCE_FORK_LENGTH,
2,
&PathBuf::from("rsrc_test.txt"),
&mut output,
)
.await;
assert!(
result.is_ok(),
"open resource fork failed: {:?}",
result.err()
);
let fork_ref = u16::from_be_bytes(output[2..4].try_into().unwrap());
let payload = b"resource fork data";
volume.write_fork(fork_ref, 0, payload).await.unwrap();
volume.close_fork(fork_ref).await.unwrap();
#[cfg(target_os = "macos")]
{
let native = root_path.join("rsrc_test.txt").join("..namedfork").join("rsrc");
let contents = tokio::fs::read(&native).await.unwrap();
assert_eq!(contents, payload);
}
#[cfg(not(target_os = "macos"))]
{
let sidecar = rsrc_path(&root_path, Path::new("rsrc_test.txt"));
assert!(sidecar.exists(), "sidecar file should exist at {:?}", sidecar);
let contents = tokio::fs::read(&sidecar).await.unwrap();
assert_eq!(contents, payload);
}
let node_id = volume.resolve_node_lazy(2, Path::new("rsrc_test.txt")).await.unwrap();
let mut parms_output = [0u8; 64];
let (_, bytes_written) = volume
.get_node_parms(
node_id,
FPFileBitmap::RESOURCE_FORK_LENGTH,
FPDirectoryBitmap::empty(),
&mut parms_output,
)
.await
.unwrap();
assert_eq!(bytes_written, 4);
let reported_len = u32::from_be_bytes(parms_output[0..4].try_into().unwrap());
assert_eq!(reported_len as usize, payload.len());
let subdir_id = volume.resolve_node_lazy(2, Path::new("subdir")).await.unwrap();
let result = volume
.open_fork(
ForkType::Resource,
FPFileBitmap::RESOURCE_FORK_LENGTH,
subdir_id,
&PathBuf::from("rsrc_sub.txt"),
&mut output,
)
.await;
assert!(
result.is_ok(),
"open resource fork in subdir failed: {:?}",
result.err()
);
let fork_ref = u16::from_be_bytes(output[2..4].try_into().unwrap());
volume
.write_fork(fork_ref, 0, b"sub resource")
.await
.unwrap();
volume.close_fork(fork_ref).await.unwrap();
#[cfg(target_os = "macos")]
{
let native = root_path.join("subdir").join("rsrc_sub.txt").join("..namedfork").join("rsrc");
let contents = tokio::fs::read(&native).await.unwrap();
assert_eq!(contents, b"sub resource");
}
#[cfg(not(target_os = "macos"))]
{
let sub_sidecar = rsrc_path(&root_path, Path::new("subdir/rsrc_sub.txt"));
assert!(sub_sidecar.exists(), "subdir sidecar should exist at {:?}", sub_sidecar);
}
let delete_req = FPDelete {
volume_id: 1,
directory_id: 2,
path: "rsrc_test.txt".into(),
};
volume.delete(&delete_req).await.unwrap();
assert!(
!root_path.join("rsrc_test.txt").exists(),
"file should be removed after delete"
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn test_resource_fork_falls_back_to_macos_named_fork() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let file_path = root_path.join("MacApp");
File::create(&file_path).await.unwrap();
let rsrc_payload = b"native resource fork bytes from xattr";
xattr::set(&file_path, "com.apple.ResourceFork", rsrc_payload).unwrap();
let sidecar = rsrc_path(&root_path, Path::new("MacApp"));
tokio::fs::create_dir_all(sidecar.parent().unwrap()).await.unwrap();
tokio::fs::File::create(&sidecar).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let node_id = volume.resolve_node_lazy(2, Path::new("MacApp")).await.unwrap();
let mut parms_output = [0u8; 64];
let (_, bytes_written) = volume
.get_node_parms(
node_id,
FPFileBitmap::RESOURCE_FORK_LENGTH,
FPDirectoryBitmap::empty(),
&mut parms_output,
)
.await
.unwrap();
assert_eq!(bytes_written, 4);
let reported_len = u32::from_be_bytes(parms_output[0..4].try_into().unwrap());
assert_eq!(
reported_len as usize,
rsrc_payload.len(),
"RESOURCE_FORK_LENGTH must reflect the native named fork"
);
let mut output = [0u8; 256];
volume
.open_fork(
ForkType::Resource,
FPFileBitmap::RESOURCE_FORK_LENGTH,
2,
&PathBuf::from("MacApp"),
&mut output,
)
.await
.unwrap();
let fork_ref = u16::from_be_bytes(output[2..4].try_into().unwrap());
let mut buf = vec![0u8; 256];
let (n, _eof) = volume
.read(
&FPRead {
fork_id: fork_ref,
offset: 0,
req_count: 256,
newline_mask: 0,
newline_char: 0,
},
&mut buf,
)
.await
.unwrap();
assert_eq!(&buf[..n], rsrc_payload);
}
#[tokio::test]
async fn test_resolve_node_identity() {
let dir = tempdir().unwrap();
let volume = make_test_volume("TestVol".to_string(), dir.path().to_path_buf()).await;
assert_eq!(volume.resolve_node(1, Path::new("")).unwrap(), 1);
assert_eq!(volume.resolve_node(1, Path::new("\0")).unwrap(), 1);
assert_eq!(volume.resolve_node(1, Path::new("TestVol")).unwrap(), 2);
assert_eq!(volume.resolve_node(2, Path::new("")).unwrap(), 2);
}
#[tokio::test]
async fn test_resolve_subdir() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
tokio::fs::create_dir(root_path.join("subdir"))
.await
.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let subdir_id = volume.resolve_node_lazy(2, Path::new("subdir")).await.unwrap();
assert!(subdir_id >= 3, "subdir should have a real node ID");
assert_eq!(
volume.resolve_node(subdir_id, Path::new("")).unwrap(),
subdir_id
);
assert_eq!(
volume.resolve_node(subdir_id, Path::new("\0")).unwrap(),
subdir_id
);
}
#[tokio::test]
async fn test_enumerate_subdirectory() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let subdir = root_path.join("subdir");
tokio::fs::create_dir(&subdir).await.unwrap();
File::create(subdir.join("a.txt")).await.unwrap();
File::create(subdir.join("b.txt")).await.unwrap();
File::create(subdir.join("c.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let subdir_id = volume.resolve_node_lazy(2, Path::new("subdir")).await.unwrap();
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: subdir_id,
file_bitmap: FPFileBitmap::LONG_NAME | FPFileBitmap::FILE_NUMBER,
directory_bitmap: FPDirectoryBitmap::LONG_NAME | FPDirectoryBitmap::DIR_ID,
req_count: 100,
start_index: 1,
max_reply_size: 2048,
path: "".into(),
};
let mut output = [0u8; 2048];
let result = volume.enumerate(enumerate_cmd, &mut output).await;
assert!(result.is_ok(), "enumerate failed: {:?}", result.err());
let count = u16::from_be_bytes(output[0..2].try_into().unwrap());
assert_eq!(count, 3, "should enumerate 3 files inside subdir");
let zero_index_cmd = FPEnumerate {
volume_id: 1,
directory_id: subdir_id,
file_bitmap: FPFileBitmap::LONG_NAME,
directory_bitmap: FPDirectoryBitmap::empty(),
req_count: 10,
start_index: 0,
max_reply_size: 2048,
path: "".into(),
};
let result = volume.enumerate(zero_index_cmd, &mut output).await;
assert_eq!(
result,
Err(AfpError::ObjectNotFound),
"start_index=0 should return ObjectNotFound"
);
}
#[tokio::test]
async fn test_finder_info_roundtrip() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
tokio::fs::create_dir(root_path.join("mydir"))
.await
.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let node_id = volume.resolve_node_lazy(2, Path::new("mydir")).await.unwrap();
let mut finder_info = [0u8; 32];
finder_info[0..4].copy_from_slice(b"TEST");
finder_info[28..32].copy_from_slice(b"TAIL");
volume
.set_node_parms(
node_id,
FPFileBitmap::empty(),
FPDirectoryBitmap::FINDER_INFO,
&finder_info,
)
.await
.unwrap();
let mut output = [0u8; 256];
let (is_dir, bytes_written) = volume
.get_node_parms(
node_id,
FPFileBitmap::empty(),
FPDirectoryBitmap::FINDER_INFO,
&mut output,
)
.await
.unwrap();
assert!(is_dir);
assert_eq!(bytes_written, 32);
assert_eq!(
&output[0..32],
&finder_info,
"finder info roundtrip mismatch"
);
}
#[tokio::test]
async fn test_delete_file() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("todelete.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let delete_req = FPDelete {
volume_id: 1,
directory_id: 2,
path: "todelete.txt".into(),
};
volume.delete(&delete_req).await.unwrap();
assert!(
!root_path.join("todelete.txt").exists(),
"file should be gone from disk"
);
let result = volume.resolve_node(2, Path::new("todelete.txt"));
assert!(result.is_err(), "node should be removed from volume index");
}
#[tokio::test]
async fn test_delete_nonempty_dir_fails() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let subdir = root_path.join("notempty");
tokio::fs::create_dir(&subdir).await.unwrap();
File::create(subdir.join("occupant.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let delete_req = FPDelete {
volume_id: 1,
directory_id: 2,
path: "notempty".into(),
};
let result = volume.delete(&delete_req).await;
assert_eq!(result, Err(AfpError::DirNotEmpty));
}
#[tokio::test]
async fn test_file_backup_date_is_sentinel() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("test.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let node_id = volume.resolve_node_lazy(2, Path::new("test.txt")).await.unwrap();
let mut output = [0u8; 64];
let (is_dir, bytes_written) = volume
.get_node_parms(
node_id,
FPFileBitmap::BACKUP_DATE,
FPDirectoryBitmap::empty(),
&mut output,
)
.await
.unwrap();
assert!(!is_dir);
assert_eq!(bytes_written, 4);
let backup_date = u32::from_be_bytes(output[0..4].try_into().unwrap());
assert_eq!(backup_date, 0, "backup date should be zero (never backed up)");
}
#[tokio::test]
async fn test_finder_info_roundtrip_file() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("test.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path).await;
let node_id = volume.resolve_node_lazy(2, Path::new("test.txt")).await.unwrap();
let mut finder_info = [0u8; 32];
finder_info[0..4].copy_from_slice(b"TEXT");
finder_info[4..8].copy_from_slice(b"ttxt");
volume
.set_node_parms(
node_id,
FPFileBitmap::FINDER_INFO,
FPDirectoryBitmap::empty(),
&finder_info,
)
.await
.unwrap();
let mut output = [0u8; 256];
let (is_dir, bytes_written) = volume
.get_node_parms(
node_id,
FPFileBitmap::FINDER_INFO,
FPDirectoryBitmap::empty(),
&mut output,
)
.await
.unwrap();
assert!(!is_dir);
assert_eq!(bytes_written, 32);
assert_eq!(
&output[0..32],
&finder_info,
"finder info roundtrip failed for file"
);
}
#[tokio::test]
async fn test_delete_empty_dir() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
tokio::fs::create_dir(root_path.join("emptydir"))
.await
.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let delete_req = FPDelete {
volume_id: 1,
directory_id: 2,
path: "emptydir".into(),
};
volume.delete(&delete_req).await.unwrap();
assert!(
!root_path.join("emptydir").exists(),
"dir should be gone from disk"
);
let result = volume.resolve_node(2, Path::new("emptydir"));
assert!(result.is_err(), "node should be removed from volume index");
}
#[tokio::test]
async fn test_trash_folder_lifecycle() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let network_trash_id = volume
.create_dir(2, PathBuf::from("Network Trash Folder"))
.await
.unwrap();
assert!(network_trash_id >= 3, "should receive a real DID");
let trash_can_id = volume
.create_dir(network_trash_id, PathBuf::from("Trash Can #2"))
.await
.unwrap();
assert!(
trash_can_id > network_trash_id,
"child DID must be greater than parent DID"
);
let mut output = [0u8; 256];
let (is_dir, bytes_written) = volume
.get_node_parms(
network_trash_id,
FPFileBitmap::empty(),
FPDirectoryBitmap::PARENT_DIR_ID | FPDirectoryBitmap::DIR_ID,
&mut output,
)
.await
.unwrap();
assert!(
is_dir,
"Network Trash Folder must be reported as a directory"
);
assert_eq!(bytes_written, 8);
let parent_dir_id = u32::from_be_bytes(output[0..4].try_into().unwrap());
let dir_id = u32::from_be_bytes(output[4..8].try_into().unwrap());
assert_eq!(
parent_dir_id, 2,
"Network Trash Folder's parent should be volume root (DID=2)"
);
assert_eq!(
dir_id, network_trash_id,
"DIR_ID must match the ID returned by create_dir"
);
output.fill(0);
let (is_dir, bytes_written) = volume
.get_node_parms(
trash_can_id,
FPFileBitmap::empty(),
FPDirectoryBitmap::PARENT_DIR_ID | FPDirectoryBitmap::DIR_ID,
&mut output,
)
.await
.unwrap();
assert!(is_dir, "Trash Can must be reported as a directory");
assert_eq!(bytes_written, 8);
let parent_dir_id = u32::from_be_bytes(output[0..4].try_into().unwrap());
let dir_id = u32::from_be_bytes(output[4..8].try_into().unwrap());
assert_eq!(
parent_dir_id, network_trash_id,
"Trash Can's parent should be Network Trash Folder"
);
assert_eq!(
dir_id, trash_can_id,
"DIR_ID must match the ID returned by create_dir"
);
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: network_trash_id,
file_bitmap: FPFileBitmap::LONG_NAME,
directory_bitmap: FPDirectoryBitmap::LONG_NAME
| FPDirectoryBitmap::DIR_ID
| FPDirectoryBitmap::PARENT_DIR_ID,
req_count: 100,
start_index: 1,
max_reply_size: 2048,
path: "".into(),
};
let mut enum_buf = [0u8; 2048];
let result = volume.enumerate(enumerate_cmd, &mut enum_buf).await;
assert!(
result.is_ok(),
"enumerate should succeed: {:?}",
result.err()
);
let count = u16::from_be_bytes(enum_buf[0..2].try_into().unwrap());
assert_eq!(
count, 1,
"Network Trash Folder should contain exactly Trash Can #2"
);
let delete_req = FPDelete {
volume_id: 1,
directory_id: network_trash_id,
path: "Trash Can #2".into(),
};
volume.delete(&delete_req).await.unwrap();
assert!(
!root_path
.join("Network Trash Folder")
.join("Trash Can #2")
.exists(),
"Trash Can #2 must be gone from disk"
);
let enumerate_empty = FPEnumerate {
volume_id: 1,
directory_id: network_trash_id,
file_bitmap: FPFileBitmap::LONG_NAME,
directory_bitmap: FPDirectoryBitmap::LONG_NAME,
req_count: 100,
start_index: 1,
max_reply_size: 2048,
path: "".into(),
};
let result = volume.enumerate(enumerate_empty, &mut enum_buf).await;
assert_eq!(
result,
Err(AfpError::ObjectNotFound),
"empty directory enumeration should return ObjectNotFound"
);
output.fill(0);
let result = volume
.get_node_parms(
trash_can_id,
FPFileBitmap::empty(),
FPDirectoryBitmap::DIR_ID,
&mut output,
)
.await;
assert_eq!(
result,
Err(AfpError::ObjectNotFound),
"deleted node must not be found by its old DID"
);
let result = volume.resolve_node(network_trash_id, Path::new("Trash Can #2"));
assert!(result.is_err(), "deleted dir must not resolve by name");
}
#[tokio::test]
async fn test_comment_set_get_remove() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("readme.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
volume
.set_comment(2, Path::new("readme.txt"), b"hello comment")
.unwrap();
let got = volume.get_comment(2, Path::new("readme.txt")).unwrap();
assert_eq!(got, b"hello comment");
volume
.remove_comment(2, Path::new("readme.txt"))
.unwrap();
assert_eq!(
volume.get_comment(2, Path::new("readme.txt")),
Err(AfpError::ItemNotFound)
);
}
#[tokio::test]
async fn test_comment_survives_volume_restart() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("persist.txt")).await.unwrap();
{
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
volume
.set_comment(2, Path::new("persist.txt"), b"survives restart")
.unwrap();
}
{
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
let got = volume
.get_comment(2, Path::new("persist.txt"))
.unwrap();
assert_eq!(got, b"survives restart");
}
}
#[tokio::test]
async fn test_comment_follows_rename() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("before.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
volume.set_comment(2, Path::new("before.txt"), b"my comment").unwrap();
volume.rename(2, Path::new("before.txt"), "after.txt").await.unwrap();
let got = volume.get_comment(2, Path::new("after.txt")).unwrap();
assert_eq!(got, b"my comment");
assert_eq!(
volume.get_comment(2, Path::new("before.txt")),
Err(AfpError::ObjectNotFound)
);
}
#[tokio::test]
async fn test_comment_follows_move() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
tokio::fs::create_dir(root_path.join("src_dir")).await.unwrap();
tokio::fs::create_dir(root_path.join("dst_dir")).await.unwrap();
File::create(root_path.join("src_dir").join("file.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
let src_dir_id = volume.resolve_node(2, Path::new("src_dir")).unwrap();
let dst_dir_id = volume.resolve_node(2, Path::new("dst_dir")).unwrap();
volume.set_comment(src_dir_id, Path::new("file.txt"), b"moved comment").unwrap();
volume
.move_and_rename(src_dir_id, dst_dir_id, Path::new("file.txt"), Path::new(""), "")
.await
.unwrap();
let got = volume.get_comment(dst_dir_id, Path::new("file.txt")).unwrap();
assert_eq!(got, b"moved comment");
assert_eq!(
volume.get_comment(src_dir_id, Path::new("file.txt")),
Err(AfpError::ObjectNotFound)
);
}
#[tokio::test]
async fn test_comment_copied_with_file() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
tokio::fs::create_dir(root_path.join("dst_dir")).await.unwrap();
File::create(root_path.join("original.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
let dst_dir_id = volume.resolve_node(2, Path::new("dst_dir")).unwrap();
volume.set_comment(2, Path::new("original.txt"), b"copied comment").unwrap();
volume
.copy_file(2, Path::new("original.txt"), dst_dir_id, Path::new(""), "copy.txt")
.await
.unwrap();
let src_got = volume.get_comment(2, Path::new("original.txt")).unwrap();
assert_eq!(src_got, b"copied comment");
let dst_got = volume.get_comment(dst_dir_id, Path::new("copy.txt")).unwrap();
assert_eq!(dst_got, b"copied comment");
}
#[tokio::test]
async fn test_comment_removed_on_delete() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("doomed.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.walk_dir(PathBuf::new()).await.unwrap();
volume.set_comment(2, Path::new("doomed.txt"), b"goodbye").unwrap();
let delete_req = tailtalk_packets::afp::FPDelete {
volume_id: 1,
directory_id: 2,
path: "doomed.txt".into(),
};
volume.delete(&delete_req).await.unwrap();
drop(volume);
let verify_db = crate::afp::DesktopDatabase::open_or_create(&root_path).unwrap();
assert_eq!(
crate::afp::DesktopDatabase::from_db(verify_db, 1).unwrap()
.get_comment(std::path::Path::new("doomed.txt")),
Err(AfpError::ItemNotFound),
"comment should have been deleted with the file"
);
}
#[tokio::test]
async fn test_set_comment_works_without_open_dt() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("note.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.ensure_dir_populated(2).await.unwrap();
assert!(
volume.set_comment(2, Path::new("note.txt"), b"hello").is_ok(),
"set_comment should succeed without a prior FPOpenDT"
);
assert_eq!(
volume.get_comment(2, Path::new("note.txt")).unwrap(),
b"hello"
);
}
#[tokio::test]
async fn test_open_fork_succeeds_with_mangled_name() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let long_name = "This file has a very long name that exceeds the 31 byte AFP limit.txt";
File::create(root_path.join(long_name)).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: 2,
file_bitmap: FPFileBitmap::LONG_NAME | FPFileBitmap::FILE_NUMBER,
directory_bitmap: FPDirectoryBitmap::LONG_NAME | FPDirectoryBitmap::DIR_ID,
req_count: 100,
start_index: 1,
max_reply_size: 1024,
path: "".into(),
};
let mut output = [0u8; 1024];
volume.enumerate(enumerate_cmd, &mut output).await.unwrap();
let mangled_str = client_visible_name(long_name);
let result = volume
.open_fork(
ForkType::Data,
FPFileBitmap::FILE_NUMBER,
2,
&PathBuf::from(&mangled_str),
&mut output,
)
.await;
assert!(
result.is_ok(),
"open_fork must succeed when given a mangled name: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_mangle_registered_via_enumerate_path() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let long_name = "This file has a very long name that exceeds the 31 byte AFP limit.txt";
File::create(root_path.join(long_name)).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
let enumerate_cmd = FPEnumerate {
volume_id: 1,
directory_id: 2,
file_bitmap: FPFileBitmap::LONG_NAME | FPFileBitmap::FILE_NUMBER,
directory_bitmap: FPDirectoryBitmap::LONG_NAME | FPDirectoryBitmap::DIR_ID,
req_count: 100,
start_index: 1,
max_reply_size: 1024,
path: "".into(),
};
let mut output = [0u8; 1024];
volume.enumerate(enumerate_cmd, &mut output).await.unwrap();
let mangled_str = client_visible_name(long_name);
let result = volume.resolve_node(2, Path::new(&mangled_str));
assert!(
result.is_ok(),
"mangle entry must be registered during enumerate so subsequent \
file operations by mangled name succeed: {:?}",
result.err()
);
}
fn client_visible_name(posix_name: &str) -> String {
let mangled = mangle_name(posix_name);
let (s, _, _) = MACINTOSH.decode(&mangled);
s.into_owned()
}
#[tokio::test]
async fn test_short_name_resolves_directly_without_mangling() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
File::create(root_path.join("short.txt")).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
let id = volume.resolve_node_lazy(2, Path::new("short.txt")).await;
assert!(id.is_ok(), "short name should resolve without mangling");
}
#[tokio::test]
async fn test_resolve_node_by_mangled_long_name() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let long_name = "This file has a very long name that exceeds the 31 byte AFP limit.txt";
File::create(root_path.join(long_name)).await.unwrap();
let mut volume = make_test_volume("TestVol".to_string(), root_path.clone()).await;
volume.open_dt().await.unwrap();
volume.ensure_dir_populated(2).await.unwrap();
let mangled_str = client_visible_name(long_name);
assert_ne!(mangled_str, long_name, "sanity: name should have been mangled");
let result = volume.resolve_node(2, Path::new(&mangled_str));
assert!(result.is_ok(), "resolve_node should find the file by its mangled name: {:?}", result.err());
}
#[tokio::test]
async fn test_resolve_mangled_name_after_simulated_restart() {
let dir = tempdir().unwrap();
let root_path = dir.path().to_path_buf();
let long_name = "This file has a very long name that exceeds the 31 byte AFP limit.txt";
File::create(root_path.join(long_name)).await.unwrap();
{
let mut vol = make_test_volume("TestVol".to_string(), root_path.clone()).await;
vol.open_dt().await.unwrap();
vol.ensure_dir_populated(2).await.unwrap();
}
let mut vol2 = make_test_volume("TestVol".to_string(), root_path.clone()).await;
vol2.open_dt().await.unwrap();
vol2.ensure_dir_populated(2).await.unwrap();
let mangled_str = client_visible_name(long_name);
let result = vol2.resolve_node(2, Path::new(&mangled_str));
assert!(
result.is_ok(),
"resolve_node should find the file by mangled name after a simulated server restart: {:?}",
result.err()
);
}
}