mod open_file;
mod open_folder;
mod utilities;
use crate::{
errors::{CatBridgeError, FSError},
fsemul::{
bsf::BootSystemFile,
dlf::DiskLayoutFile,
errors::{FSEmulAPIError, FSEmulFSError},
filesystem::{
FilesystemLocation, ItemInFolder, ResolvedLocation,
host::{
open_folder::DirectoryListing,
utilities::{get_new_unique_folder_fd, join_many},
},
},
pcfs::errors::PcfsApiError,
},
};
use bytes::{Bytes, BytesMut};
use sachet::title::TitleID;
use scc::{
HashMap as ConcurrentMap, HashSet as ConcurrentSet, hash_map::OccupiedEntry as CMOccupiedEntry,
};
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
fs::{
copy as copy_file_sync, create_dir_all as create_dir_all_sync, read_link as read_link_sync,
remove_dir_all as remove_dir_all_sync, remove_file as remove_file_sync,
rename as rename_sync,
},
hash::RandomState,
io::{Error as IOError, SeekFrom},
path::{Component, Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering as AtomicOrdering},
},
};
use tokio::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all, write as fs_write},
io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
sync::Mutex,
};
use tracing::{info, warn};
use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
use walkdir::WalkDir;
use whoami::username;
pub use self::open_file::OpenFileHandle;
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
use crate::fsemul::filesystem::nus_fuse::NUSFuse;
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
use std::str::FromStr;
#[derive(Clone, Debug)]
pub struct HostFilesystem {
cafe_sdk_path: PathBuf,
create_save_directories: bool,
disc_mounted: Arc<Mutex<Option<(bool, bool, TitleID)>>>,
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
nus: Option<NUSFuse>,
open_file_handles: Arc<ConcurrentMap<i32, OpenFileHandle>>,
open_folder_handles: Arc<ConcurrentMap<i32, DirectoryListing>>,
folders_marked_read_only: Arc<ConcurrentSet<PathBuf>>,
is_using_unique_fds: bool,
has_opened_file: Arc<AtomicBool>,
}
impl HostFilesystem {
pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
return Err(FSEmulFSError::CantFindCafeSdkPath.into());
};
Self::patch_case_sensitivity(&cafe_sdk_path).await?;
for path in [
&[
"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
] as &[&str],
&[
"data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
],
&[
"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "content",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
],
&[
"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
],
] {
if !join_many(&cafe_sdk_path, path).exists() {
return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
}
}
Self::prepare_for_serving(&cafe_sdk_path).await?;
let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
Ok(Self {
cafe_sdk_path,
create_save_directories: false,
disc_mounted: Arc::new(Mutex::new(None)),
#[cfg(feature = "nus")]
nus: None,
folders_marked_read_only: Arc::new(ro_folders),
open_file_handles: Arc::new(ConcurrentMap::new()),
open_folder_handles: Arc::new(ConcurrentMap::new()),
is_using_unique_fds: false,
has_opened_file: Arc::new(AtomicBool::new(false)),
})
}
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
pub async fn from_cafe_dir_and_nus(
cafe_dir: Option<PathBuf>,
nus: Option<NUSFuse>,
) -> Result<Self, CatBridgeError> {
let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
return Err(FSEmulFSError::CantFindCafeSdkPath.into());
};
Self::patch_case_sensitivity(&cafe_sdk_path).await?;
for path in [
&[
"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
] as &[&str],
&[
"data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
],
&[
"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "content",
],
&[
"data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
],
&[
"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
],
] {
if !join_many(&cafe_sdk_path, path).exists() {
return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
}
}
Self::prepare_for_serving(&cafe_sdk_path).await?;
let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
Ok(Self {
cafe_sdk_path,
create_save_directories: false,
disc_mounted: Arc::new(Mutex::new(None)),
#[cfg(feature = "nus")]
nus,
folders_marked_read_only: Arc::new(ro_folders),
open_file_handles: Arc::new(ConcurrentMap::new()),
open_folder_handles: Arc::new(ConcurrentMap::new()),
is_using_unique_fds: false,
has_opened_file: Arc::new(AtomicBool::new(false)),
})
}
pub const fn allow_save_directory_creation(&mut self, allow: bool) {
self.create_save_directories = allow;
}
#[must_use]
pub const fn cafe_sdk_path(&self) -> &PathBuf {
&self.cafe_sdk_path
}
#[must_use]
pub fn disc_emu_path(&self) -> PathBuf {
join_many(&self.cafe_sdk_path, ["data", "disc"])
}
pub fn force_unique_fds(&mut self) -> Result<(), FSEmulAPIError> {
if self.has_opened_file.load(AtomicOrdering::Relaxed) {
return Err(FSEmulAPIError::CannotSwapFdStrategy);
}
#[cfg(feature = "nus")]
if self.nus.is_some() {
return Err(FSEmulAPIError::CannotSwapFdStrategy);
}
self.is_using_unique_fds = true;
Ok(())
}
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
pub fn set_nus_provider(&mut self, nus: Option<NUSFuse>) -> Result<(), FSEmulAPIError> {
if self.has_opened_file.load(AtomicOrdering::Relaxed)
&& !self.is_using_unique_fds
&& nus.is_some()
{
return Err(FSEmulAPIError::CannotSetNUS);
}
if nus.is_some() {
self.is_using_unique_fds = true;
}
self.nus = nus;
Ok(())
}
#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
#[cfg(feature = "nus")]
#[must_use]
pub fn get_nus_provider(&self) -> Option<&NUSFuse> {
self.nus.as_ref()
}
pub async fn open_file(
&self,
open_options: OpenOptions,
path: &Path,
stream_owner: Option<u64>,
) -> Result<i32, FSError> {
self.has_opened_file.store(true, AtomicOrdering::Relaxed);
let handle = OpenFileHandle::open_file(
self.is_using_unique_fds,
open_options,
path,
stream_owner,
#[cfg(feature = "nus")]
&join_many(self.cafe_sdk_path(), ["data", "mlc", "usr", "title"]),
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
let fd = handle.fd();
self.open_file_handles
.insert_async(fd, handle)
.await
.map_err(|_| IOError::other("somehow got duplicate fd?"))?;
Ok(fd)
}
pub async fn get_file(
&self,
fd: i32,
for_stream: Option<u64>,
) -> Option<CMOccupiedEntry<'_, i32, OpenFileHandle, RandomState>> {
self.open_file_handles
.get_async(&fd)
.await
.and_then(|entry| {
if Self::allow_access(entry.stream_owner(), for_stream) {
Some(entry)
} else {
None
}
})
}
pub async fn file_length(&self, fd: i32, for_stream: Option<u64>) -> Option<u64> {
self.open_file_handles
.get_async(&fd)
.await
.and_then(|entry| {
if Self::allow_access(entry.stream_owner(), for_stream) {
Some(entry.file_size())
} else {
None
}
})
}
pub async fn read_file(
&self,
fd: i32,
mut total_data_to_read: usize,
for_stream: Option<u64>,
) -> Result<Option<Bytes>, CatBridgeError> {
let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
return Ok(None);
};
if !Self::allow_access(real_entry.stream_owner(), for_stream) {
return Ok(None);
}
let file_reader = real_entry
.force_file_handle(
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
let mut file_buff = BytesMut::zeroed(total_data_to_read);
let mut total_bytes_read = 0_usize;
while total_data_to_read > 0 {
let bytes_read = file_reader
.read(&mut file_buff[total_bytes_read..])
.await
.map_err(FSError::IO)?;
if bytes_read == 0 {
break;
}
total_data_to_read -= bytes_read;
total_bytes_read += bytes_read;
}
if file_buff.len() > total_bytes_read {
file_buff.truncate(total_bytes_read);
}
Ok(Some(file_buff.freeze()))
}
pub async fn write_file(
&self,
fd: i32,
data_to_write: Bytes,
for_stream: Option<u64>,
) -> Result<(), CatBridgeError> {
let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
return Err(FSError::IO(IOError::other("file not open")).into());
};
if !Self::allow_access(real_entry.stream_owner(), for_stream) {
return Err(FSError::IO(IOError::other("file not open")).into());
}
let file_writer = real_entry
.force_file_handle(
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
file_writer
.write_all(&data_to_write)
.await
.map_err(FSError::IO)?;
Ok(())
}
pub async fn get_file_position(
&self,
fd: i32,
for_stream: Option<u64>,
) -> Result<u64, CatBridgeError> {
let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
return Ok(0);
};
if !Self::allow_access(real_entry.stream_owner(), for_stream) {
return Ok(0);
}
let file_reader = real_entry
.force_file_handle(
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
Ok(file_reader.stream_position().await.map_err(FSError::IO)?)
}
pub async fn seek_file(
&self,
fd: i32,
from_begin: bool,
offset: u64,
for_stream: Option<u64>,
) -> Result<(), CatBridgeError> {
let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
return Ok(());
};
if !Self::allow_access(real_entry.stream_owner(), for_stream) {
return Ok(());
}
let file_reader = real_entry
.force_file_handle(
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
if from_begin {
file_reader
.seek(SeekFrom::Start(offset))
.await
.map_err(FSError::IO)?;
} else {
file_reader
.seek(SeekFrom::End(i64::try_from(offset).unwrap_or(i64::MAX)))
.await
.map_err(FSError::IO)?;
}
Ok(())
}
pub async fn close_file(&self, fd: i32, for_stream: Option<u64>) {
if let Some(entry) = self.open_file_handles.get_async(&fd).await
&& !Self::allow_access(entry.stream_owner(), for_stream)
{
return;
}
self.open_file_handles.remove_async(&fd).await;
}
pub async fn open_folder(&self, path: &Path, for_stream: Option<u64>) -> Result<i32, FSError> {
let listing = DirectoryListing::new(
path,
self.mlc_path(),
for_stream,
#[cfg(feature = "nus")]
self.nus.as_ref(),
)
.await?;
let fake_fd = get_new_unique_folder_fd();
self.open_folder_handles
.insert_sync(fake_fd, listing)
.map_err(|_| IOError::other("OS returned duplicate fd?"))?;
Ok(fake_fd)
}
pub async fn mark_folder_read_only(&self, path: PathBuf) {
_ = self.folders_marked_read_only.insert_async(path).await;
}
pub async fn ensure_folder_not_read_only(&self, path: &PathBuf) {
self.folders_marked_read_only.remove_async(path).await;
}
pub async fn folder_is_read_only(&self, path: &PathBuf) -> bool {
self.folders_marked_read_only.contains_async(path).await
}
#[must_use]
pub async fn next_in_folder(&self, fd: i32, for_stream: Option<u64>) -> Option<ItemInFolder> {
let mut entry = self.open_folder_handles.get_async(&fd).await?;
if !Self::allow_access(entry.stream_owner(), for_stream) {
return None;
}
entry.next_item()
}
pub async fn reverse_folder(&self, fd: i32, for_stream: Option<u64>) {
let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
return;
};
if !Self::allow_access(real_entry.stream_owner(), for_stream) {
return;
}
real_entry.reverse_folder();
}
pub async fn close_folder(&self, fd: i32, for_stream: Option<u64>) {
if let Some(real_entry) = self.open_folder_handles.get_async(&fd).await
&& !Self::allow_access(real_entry.stream_owner(), for_stream)
{
return;
}
self.open_folder_handles.remove_async(&fd).await;
}
pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, CatBridgeError> {
let mut path = self.temp_path()?;
path.push("caferun");
if !path.exists() {
create_dir_all(&path).await.map_err(FSError::IO)?;
}
path.push("ppc.bsf");
if !path.exists() {
fs_write(&path, Bytes::try_from(BootSystemFile::default())?)
.await
.map_err(FSError::IO)?;
}
Ok(path)
}
pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
let mut path = self.temp_path()?;
path.push("caferun");
if !path.exists() {
create_dir_all(&path).await?;
}
path.push("diskid.bin");
if !path.exists() {
fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
}
Ok(path)
}
#[doc(
// This is not yet finished and the signature may change....
hidden,
)]
pub async fn mount_disk_title(
&mut self,
is_slc: bool,
is_sys: bool,
title_id: TitleID,
) -> Result<(), FSError> {
let dest_path = join_many(&self.cafe_sdk_path, ["data", "disc"]);
if dest_path.exists() {
remove_dir_all(&dest_path).await.map_err(FSError::IO)?;
}
_ = self.disk_id_path().await?;
{
let mut guard = self.disc_mounted.lock().await;
guard.replace((is_slc, is_sys, title_id));
}
Ok(())
}
#[must_use]
pub fn firmware_file_path(&self) -> PathBuf {
join_many(
&self.slc_path_for(TitleID::new(0x0005_0010_1000_400A)),
["code", "fw.img"],
)
}
pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
let mut path = self.temp_path()?;
path.push("caferun");
if !path.exists() {
create_dir_all(&path).await.map_err(FSError::from)?;
}
path.push("ppc_boot.dlf");
if !path.exists() {
let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
fs_write(&path, Bytes::from(root_dlf))
.await
.map_err(FSError::from)?;
}
Ok(path)
}
#[must_use]
pub fn path_allows_writes(&self, path: &Path) -> bool {
let lossy_path = path.to_string_lossy();
let trimmed_lossy_path = lossy_path
.trim_start_matches("/vol/pc")
.trim_start_matches('/');
if trimmed_lossy_path.starts_with("%DISC_EMU_DIR") {
return trimmed_lossy_path.starts_with("%DISC_EMU_DIR/save");
}
if path.starts_with(join_many(&self.cafe_sdk_path, ["data", "disc"])) {
return path.starts_with(join_many(&self.cafe_sdk_path, ["data", "disc", "save"]));
}
true
}
pub async fn resolve_path(
&self,
potentially_prefixed_path: &str,
) -> Result<ResolvedLocation, CatBridgeError> {
let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
if path.starts_with("/%NETWORK") {
todo!("NETWORK shares not yet implemented :( sorry!")
}
let mut non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
self.replace_emu_dir(path, "mlc")
} else if path.starts_with("/%SLC_EMU_DIR") {
self.replace_emu_dir(path, "slc")
} else if path.starts_with("/%DISC_EMU_DIR") {
self.replace_emu_dir(path, "disc")
} else if path.starts_with("/%SAVE_EMU_DIR") {
self.replace_emu_dir(path, "save")
} else {
PathBuf::from(path)
};
if let Some(new_dir) = self.do_disc_mapping(&non_canonical_path).await {
non_canonical_path = new_dir;
}
if self.create_save_directories {
self.ensure_save_dir_exists(&non_canonical_path).await;
}
let mut closest_canonical_directory = non_canonical_path.clone();
let mut changed_at_all = false;
while !closest_canonical_directory.as_os_str().is_empty() {
if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
closest_canonical_directory = canonicalized;
break;
}
changed_at_all = true;
closest_canonical_directory.pop();
}
if closest_canonical_directory.as_os_str().is_empty() {
return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
}
let canonicalized_cafe = self
.cafe_sdk_path()
.canonicalize()
.unwrap_or_else(|_| self.cafe_sdk_path().clone());
if !closest_canonical_directory.starts_with(canonicalized_cafe) {
return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
}
#[cfg(feature = "nus")]
{
let mut base_mlc_title_dir = self.mlc_path();
base_mlc_title_dir.push("usr");
base_mlc_title_dir.push("title");
if let Some(nus_ref) = self.nus.as_ref()
&& !non_canonical_path.exists()
&& let Ok(leftover) = non_canonical_path.strip_prefix(&base_mlc_title_dir)
&& leftover.components().count() > 2
{
let mut path_components = leftover.components();
let mut tid_str = String::new();
tid_str += path_components
.next()
.unwrap_or_else(|| unreachable!())
.as_os_str()
.to_string_lossy()
.as_ref();
tid_str += path_components
.next()
.unwrap_or_else(|| unreachable!())
.as_os_str()
.to_string_lossy()
.as_ref();
if let Ok(title_id) = TitleID::from_str(&tid_str) {
let p = path_components.as_path().to_path_buf();
if nus_ref.exists(title_id, &p).await.is_some() {
return Ok(ResolvedLocation::NUSLocation(
title_id,
p,
non_canonical_path,
));
}
}
}
}
Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
non_canonical_path,
closest_canonical_directory,
!changed_at_all,
)))
}
pub fn create_directory(&self, at: &Path) -> Result<(), FSError> {
create_dir_all_sync(at).map_err(FSError::IO)
}
pub fn copy(&self, from: &Path, to: &Path) -> Result<(), FSError> {
if from.is_dir() {
Self::copy_dir(from, to)
} else {
copy_file_sync(from, to).map_err(FSError::IO).map(|_| ())
}
}
pub fn rename(&self, from: &Path, to: &Path) -> Result<(), FSError> {
if from.is_dir() {
Self::rename_dir(from, to)
} else {
rename_sync(from, to).map_err(FSError::IO)
}
}
#[must_use]
pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
join_many(
&self.cafe_sdk_path,
[
"data".to_owned(),
"slc".to_owned(),
"sys".to_owned(),
"title".to_owned(),
format!("{:08x}", title_id.group_id()),
format!("{:08x}", title_id.title_id()),
],
)
}
#[must_use]
pub fn mlc_path(&self) -> PathBuf {
join_many(&self.cafe_sdk_path, ["data".to_owned(), "mlc".to_owned()])
}
#[allow(
// Not actually unreachable unless on unsupported OS.
unreachable_code,
)]
#[must_use]
pub fn default_cafe_folder() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
return Some(PathBuf::from(r"C:\cafe_sdk"));
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos"
))]
{
return Some(PathBuf::from("/opt/cafe_sdk"));
}
None
}
fn temp_path(&self) -> Result<PathBuf, FSError> {
let temp_path = join_many(
&self.cafe_sdk_path,
[
"temp".to_owned(),
username()
.unwrap_or_else(|_| "unknown-user".to_owned())
.to_lowercase(),
],
);
if !temp_path.exists() {
create_dir_all_sync(&temp_path)?;
}
Ok(temp_path)
}
fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
let path_minus = path
.trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
.trim_start_matches('/')
.trim_start_matches('\\')
.replace('\\', "/");
join_many(
&join_many(self.cafe_sdk_path(), ["data", dir]),
path_minus.split('/'),
)
}
async fn do_disc_mapping(&self, previous_path: &Path) -> Option<PathBuf> {
let Ok(leftover) = previous_path.strip_prefix(self.disc_emu_path()) else {
return None;
};
let lock = self.disc_mounted.lock().await;
let (is_slc, is_sys, tid) = match lock.as_ref() {
Some((is_slc, is_sys, tid)) => (*is_slc, *is_sys, *tid),
None => {
(false, true, TitleID::new(0x0005_0010_1F70_0500))
}
};
let mut new_path = self.cafe_sdk_path().clone();
new_path.push("data");
if is_slc {
new_path.push("slc");
} else {
new_path.push("mlc");
}
if is_sys {
new_path.push("sys");
} else {
new_path.push("usr");
}
new_path.push("title");
new_path.push(format!("{:08x}", tid.group_id()));
new_path.push(format!("{:08x}", tid.title_id()));
for comp in leftover.components() {
new_path.push(comp);
}
Some(new_path)
}
async fn ensure_save_dir_exists(&self, path: &Path) {
if !path.starts_with(&self.cafe_sdk_path) || path.exists() {
return;
}
let mut components_left = path
.components()
.skip(self.cafe_sdk_path.components().count());
if components_left.next().map(Component::as_os_str) != Some(OsStr::new("data")) {
return;
}
let mlc_slc_component = components_left.next().map(Component::as_os_str);
if mlc_slc_component != Some(OsStr::new("mlc"))
&& mlc_slc_component != Some(OsStr::new("slc"))
{
return;
}
let is_mlc = mlc_slc_component == Some(OsStr::new("mlc"));
let sys_usr_component = components_left.next().map(Component::as_os_str);
if sys_usr_component != Some(OsStr::new("sys"))
&& sys_usr_component != Some(OsStr::new("usr"))
{
return;
}
let is_sys = sys_usr_component == Some(OsStr::new("sys"));
if components_left.next().map(Component::as_os_str) != Some(OsStr::new("save")) {
return;
}
let Some(group_id) = components_left
.next()
.map(|p| p.as_os_str().to_string_lossy().to_string())
else {
return;
};
let Some(title_id) = components_left
.next()
.map(|p| p.as_os_str().to_string_lossy().to_string())
else {
return;
};
if join_many(
self.cafe_sdk_path(),
[
"data",
if is_mlc { "mlc" } else { "slc" },
if is_sys { "sys" } else { "usr" },
"title",
&group_id,
&title_id,
],
)
.exists()
{
let mut save_path = join_many(
self.cafe_sdk_path(),
[
"data",
if is_mlc { "mlc" } else { "slc" },
if is_sys { "sys" } else { "usr" },
"save",
&group_id,
&title_id,
"meta",
],
);
_ = create_dir_all(&save_path).await;
save_path.pop();
save_path.push("user");
_ = create_dir_all(&save_path).await;
}
}
async fn patch_case_sensitivity(cafe_sdk_path: &Path) -> Result<(), FSError> {
if !cafe_sdk_path.exists() {
return Ok(());
}
let capital_path = join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
let _ = File::create(&capital_path).await?;
let is_insensitive = File::open(join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
.await
.is_ok();
remove_file_sync(capital_path)?;
if is_insensitive {
return Ok(());
}
info!(
"Your Host OS is not case-insensitive for file-paths... ensuring CafeSDK is all lowercase, this may take awhile..."
);
let cafe_sdk_components = cafe_sdk_path.components().count();
let mut had_rename = true;
while had_rename {
had_rename = false;
for directory in [
join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
] {
if !directory.exists() {
continue;
}
let mut iter = WalkDir::new(&directory)
.contents_first(false)
.follow_links(false)
.follow_root_links(false)
.into_iter();
while let Some(Ok(entry)) = iter.next() {
let p = entry.path();
if !p.exists() {
continue;
}
let path_minus_cafe = p
.components()
.skip(cafe_sdk_components)
.collect::<PathBuf>();
let Some(path_as_utf8) = path_minus_cafe.as_os_str().to_str() else {
warn!(problematic_path = %p.display(), "Path in Cafe SDK directory is not UTF-8! This may cause errors fetching!");
continue;
};
let new_path = Self::to_insensitive(path_as_utf8);
if path_as_utf8 != new_path {
let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
final_new_path.push("/");
final_new_path.push(&new_path);
let new = PathBuf::from(final_new_path);
if p.is_dir() {
Self::rename_dir(p, &new)?;
had_rename = true;
} else {
rename_sync(p, new)?;
had_rename = true;
}
}
}
}
}
info!("ensure CafeSDK path is now case-insensitive by renaming to all lowercase...");
Ok(())
}
fn to_insensitive(path: &str) -> String {
if path.contains("/content/") {
return path.to_owned();
}
let mut as_lowercase = path.to_ascii_lowercase();
if as_lowercase.ends_with("meta/bootdrctex.tga") {
as_lowercase = as_lowercase
.trim_end_matches("meta/bootdrctex.tga")
.to_owned();
as_lowercase += "meta/bootDrcTex.tga";
} else if as_lowercase.ends_with("meta/bootlogotex.tga") {
as_lowercase = as_lowercase
.trim_end_matches("meta/bootlogotex.tga")
.to_owned();
as_lowercase += "meta/bootLogoTex.tga";
} else if as_lowercase.ends_with("meta/bootmovie.h264") {
as_lowercase = as_lowercase
.trim_end_matches("meta/bootmovie.h264")
.to_owned();
as_lowercase += "meta/bootMovie.h264";
} else if as_lowercase.ends_with("meta/boottvtex.tga") {
as_lowercase = as_lowercase
.trim_end_matches("meta/boottvtex.tga")
.to_owned();
as_lowercase += "meta/bootTvTex.tga";
} else if as_lowercase.ends_with("meta/icontex.tga") {
as_lowercase = as_lowercase.trim_end_matches("meta/icontex.tga").to_owned();
as_lowercase += "meta/iconTex.tga";
} else if as_lowercase.ends_with("content/beep.snd") {
as_lowercase = as_lowercase.trim_end_matches("content/beep.snd").to_owned();
as_lowercase += "content/BEEP.snd";
}
as_lowercase
}
fn allow_access(entry: Option<u64>, requester: Option<u64>) -> bool {
let Some(requesting_stream_id) = requester else {
return true;
};
let Some(owned_stream_id) = entry else {
return true;
};
requesting_stream_id == owned_stream_id
}
async fn prepare_for_serving(cafe_sdk_path: &Path) -> Result<(), FSError> {
if !join_many(cafe_sdk_path, ["data", "slc", "sys", "config", "eco.xml"]).exists() {
Self::generate_eco_xml(cafe_sdk_path).await?;
}
if !join_many(
cafe_sdk_path,
["data", "slc", "sys", "proc", "prefs", "wii_acct.xml"],
)
.exists()
{
Self::generate_wii_acct_xml(cafe_sdk_path).await?;
}
if !join_many(
cafe_sdk_path,
["data", "slc", "sys", "proc", "prefs", "rmtCfg.xml"],
)
.exists()
{
Self::generate_rmt_cfg(cafe_sdk_path).await?;
}
if join_many(cafe_sdk_path, ["data", "disc"]).exists() {
remove_dir_all_sync(join_many(cafe_sdk_path, ["data", "disc"])).map_err(FSError::IO)?;
}
let sct_code_path = join_many(
cafe_sdk_path,
[
"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
],
);
Self::copy_rpls(
&sct_code_path,
&join_many(
cafe_sdk_path,
[
"data", "slc", "sys", "title", "00050010", "1000400a", "code",
],
),
)?;
Self::copy_rpls(
&sct_code_path,
&join_many(
cafe_sdk_path,
[
"data", "slc", "sys", "title", "00050010", "1000800a", "code",
],
),
)?;
Ok(())
}
fn copy_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
if !dest_path.exists() {
create_dir_all_sync(dest_path)?;
}
let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
for result in WalkDir::new(source_path)
.follow_links(false)
.follow_root_links(false)
{
let rpb = result?.into_path();
let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
new_bytes.extend_from_slice(new_path_as_str_bytes);
new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
let as_new_path =
PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
if rpb.is_symlink() {
let mut resolved_path = read_link_sync(&rpb)?;
{
let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
if os_str_for_resolved.starts_with(old_path_bytes) {
let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
new_bytes.extend_from_slice(new_path_as_str_bytes);
new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
resolved_path = PathBuf::from(unsafe {
OsString::from_encoded_bytes_unchecked(new_bytes)
});
}
}
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink(resolved_path, &as_new_path)?;
}
#[cfg(target_os = "windows")]
{
use std::os::windows::fs::{symlink_dir, symlink_file};
if resolved_path.is_dir() {
symlink_dir(resolved_path, &as_new_path)?;
} else {
symlink_file(resolved_path, &as_new_path)?;
}
}
} else if rpb.is_file() {
copy_file_sync(&rpb, &as_new_path)?;
} else if rpb.is_dir() {
create_dir_all_sync(&as_new_path)?;
}
}
Ok(())
}
fn copy_rpls(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
if !dest_path.exists() {
create_dir_all_sync(dest_path)?;
}
let destination_path_bytes = dest_path.as_os_str().as_encoded_bytes();
let source_path_bytes = source_path.as_os_str().as_encoded_bytes();
for result in WalkDir::new(source_path)
.follow_links(false)
.follow_root_links(false)
{
let rpb = result?.into_path();
if rpb.extension().unwrap_or_default().to_string_lossy() != "rpl" || rpb.is_symlink() {
continue;
}
let mut new_bytes = Vec::from(destination_path_bytes);
new_bytes.extend_from_slice(
&rpb.as_path().as_os_str().as_encoded_bytes()[source_path_bytes.len()..],
);
let as_new_path =
PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
if !as_new_path.exists() {
copy_file_sync(rpb, as_new_path)?;
}
}
Ok(())
}
fn rename_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
if !dest_path.exists() {
create_dir_all_sync(dest_path)?;
}
let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
for result in WalkDir::new(source_path)
.follow_links(false)
.follow_root_links(false)
{
let rpb = result?.into_path();
let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
new_bytes.extend_from_slice(new_path_as_str_bytes);
new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
let as_new_path =
PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
if rpb.is_symlink() {
let mut resolved_path = read_link_sync(&rpb)?;
{
let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
if os_str_for_resolved.starts_with(old_path_bytes) {
let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
new_bytes.extend_from_slice(new_path_as_str_bytes);
new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
resolved_path = PathBuf::from(unsafe {
OsString::from_encoded_bytes_unchecked(new_bytes)
});
}
}
let should_remove: bool;
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink(resolved_path, &as_new_path)?;
should_remove = true;
}
#[cfg(target_os = "windows")]
{
use std::os::windows::fs::{symlink_dir, symlink_file};
if resolved_path.is_dir() {
symlink_dir(resolved_path, &as_new_path)?;
should_remove = false;
} else {
symlink_file(resolved_path, &as_new_path)?;
should_remove = true;
}
}
if should_remove {
remove_file_sync(&rpb)?;
}
} else if rpb.is_file() {
rename_sync(&rpb, &as_new_path)?;
} else if rpb.is_dir() {
create_dir_all_sync(&as_new_path)?;
}
}
remove_dir_all_sync(source_path)?;
Ok(())
}
async fn generate_eco_xml(cafe_os_path: &Path) -> Result<(), FSError> {
let mut eco_path = join_many(cafe_os_path, ["data", "slc", "sys", "config"]);
if !eco_path.exists() {
create_dir_all_sync(&eco_path).map_err(FSError::IO)?;
}
eco_path.push("eco.xml");
let mut eco_file = File::create(eco_path).await.map_err(FSError::IO)?;
eco_file
.write_all(
br#"<?xml version="1.0" encoding="utf-8"?>
<eco type="complex" access="777">
<enable type="unsignedInt" length="4">0</enable>
<max_on_time type="unsignedInt" length="4">3601</max_on_time>
<default_off_time type="unsignedInt" length="4">15</default_off_time>
<wd_disable type="unsignedInt" length="4">1</wd_disable>
</eco>"#,
)
.await
.map_err(FSError::IO)?;
#[cfg(unix)]
{
use std::{fs::Permissions, os::unix::prelude::*};
eco_file
.set_permissions(Permissions::from_mode(0o770))
.await?;
}
Ok(())
}
async fn generate_wii_acct_xml(cafe_os_path: &Path) -> Result<(), FSError> {
let mut wii_path = join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
if !wii_path.exists() {
create_dir_all_sync(&wii_path).map_err(FSError::IO)?;
}
wii_path.push("wii_acct.xml");
let mut wii_file = File::create(wii_path).await.map_err(FSError::IO)?;
wii_file
.write_all(
br#"<?xml version="1.0" encoding="utf-8"?>
<wii_acct type="complex">
<profile type="complex">
<nickname type="hexBinary" length="22">00570069006900000000000000000000000000000000</nickname>
<language type="unsignedInt" length="4">0</language>
<country type="unsignedInt" length="4">1</country>
</profile>
<pc type="complex">
<rating type="unsignedInt" length="4">18</rating>
<organization type="unsignedInt" length="4">0</organization>
<rst_internet_ch type="unsignedByte" length="1">0</rst_internet_ch>
<rst_nw_access type="unsignedByte" length="1">0</rst_nw_access>
<rst_pt_order type="unsignedByte" length="1">0</rst_pt_order>
</pc>
</wii_acct>"#,
)
.await
.map_err(FSError::IO)?;
#[cfg(unix)]
{
use std::{fs::Permissions, os::unix::prelude::*};
wii_file
.set_permissions(Permissions::from_mode(0o770))
.await?;
}
Ok(())
}
async fn generate_rmt_cfg(cafe_os_path: &Path) -> Result<(), FSError> {
let mut rmt_path = join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
if !rmt_path.exists() {
create_dir_all(&rmt_path).await.map_err(FSError::IO)?;
}
rmt_path.push("rmtCfg.xml");
let mut rmt_file = File::create(rmt_path).await.map_err(FSError::IO)?;
rmt_file
.write_all(
br#"<?xml version="1.0" encoding="utf-8"?>
<rmtCfg type="complex" access="7777">
<sensitivity type="unsignedByte" length="1">3</sensitivity>
<sbPos type="unsignedByte" length="1">1</sbPos>
<volume type="unsignedByte" length="1">63</volume>
<vibrator type="unsignedByte" length="1">1</vibrator>
</rmtCfg>"#,
)
.await
.map_err(FSError::IO)?;
#[cfg(unix)]
{
use std::{fs::Permissions, os::unix::prelude::*};
rmt_file
.set_permissions(Permissions::from_mode(0o770))
.await?;
}
Ok(())
}
fn get_default_read_only_folders(cafe_dir: &Path) -> ConcurrentSet<PathBuf> {
let set = ConcurrentSet::new();
for cafe_sub_paths in [
&["data", "slc", "sys", "config"] as &[&str],
&["data", "slc", "sys", "proc"],
&["data", "slc", "sys", "logs"],
&["data", "mlc", "usr"],
&["data", "mlc", "usr", "import"],
&["data", "mlc", "usr", "title"],
] {
_ = set.insert_sync(join_many(cafe_dir, cafe_sub_paths));
}
set
}
}
const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
NamedField::new("cafe_sdk_path"),
NamedField::new("open_file_handles"),
NamedField::new("open_folder_handles"),
];
impl Structable for HostFilesystem {
fn definition(&self) -> StructDef<'_> {
StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
}
}
impl Valuable for HostFilesystem {
fn as_value(&self) -> Value<'_> {
Value::Structable(self)
}
fn visit(&self, visitor: &mut dyn Visit) {
let mut values = HashMap::with_capacity(self.open_file_handles.len());
self.open_file_handles.iter_sync(|k, v| {
values.insert(*k, format!("{v:?}"));
true
});
let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
self.open_folder_handles.iter_sync(|k, v| {
folder_values.insert(*k, format!("{v:?}"));
true
});
visitor.visit_named_fields(&NamedValues::new(
HOST_FILESYSTEM_FIELDS,
&[
Valuable::as_value(&self.cafe_sdk_path),
Valuable::as_value(&values),
Valuable::as_value(&folder_values),
],
));
}
}
#[cfg_attr(docsrs, doc(cfg(test)))]
#[cfg(test)]
pub mod test_helpers {
pub use super::utilities::join_many;
use super::*;
use std::fs::{File, create_dir_all};
use tempfile::{TempDir, tempdir};
#[allow(
// Allow anyone to write a test for this internally on any feature set.
dead_code,
)]
pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
let dir = tempdir().expect("Failed to create temporary directory!");
for directory_to_create in vec![
vec!["data", "slc"],
vec!["data", "mlc"],
vec!["data", "disc"],
vec!["data", "save"],
vec![
"data", "mlc", "sys", "title", "00050030", "1001000a", "code",
],
vec![
"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
],
vec![
"data", "mlc", "sys", "title", "00050010", "1f700500", "content",
],
vec![
"data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
],
vec![
"data", "mlc", "sys", "title", "00050030", "1001010A", "code",
],
vec![
"data", "mlc", "sys", "title", "00050030", "1001020a", "code",
],
vec![
"data", "slc", "sys", "title", "00050010", "1000400a", "code",
],
vec!["data", "mlc", "sys", "update", "nand", "os_v10_ndebug"],
vec!["data", "mlc", "sys", "update", "nand", "os_v10_debug"],
vec!["data", "slc", "sys", "proc", "prefs"],
vec![
"data", "slc", "sys", "title", "00050010", "1000800a", "code",
],
vec![
"data", "slc", "sys", "title", "00050010", "1000400a", "code",
],
] {
create_dir_all(join_many(dir.path(), directory_to_create))
.expect("Failed to create directories necessary for host filesystem to work.");
}
File::create(join_many(
dir.path(),
[
"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
],
))
.expect("Failed to create needed app.xml!");
File::create(join_many(
dir.path(),
[
"data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
],
))
.expect("Failed to create needed app.xml!");
File::create(join_many(
dir.path(),
[
"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
],
))
.expect("Failed to create needed app.xml!");
File::create(join_many(
dir.path(),
[
"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
],
))
.expect("Failed to create needed fw.img!");
File::create(join_many(
dir.path(),
[
"data", "mlc", "sys", "title", "00050010", "1f700500", "code", "app.xml",
],
))
.expect("Failed to create needed app.xml for disc!");
let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
.await
.expect("Failed to load empty host filesystem!");
(dir, fs)
}
}
#[cfg(test)]
mod unit_tests {
use super::test_helpers::*;
use super::*;
use std::fs::read;
fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
#[test]
pub fn is_send_sync() {
only_accepts_send_sync::<HostFilesystem>(None);
}
#[test]
pub fn can_find_default_cafe_directory() {
assert!(
HostFilesystem::default_cafe_folder().is_some(),
"Failed to find default cafe directory for your OS",
);
}
#[tokio::test]
pub async fn creatable_files() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
let expected_bsf_path = join_many(
tempdir.path(),
[
"temp".to_owned(),
username()
.expect("Failed to get system username!")
.to_lowercase(),
"caferun".to_owned(),
"ppc.bsf".to_owned(),
],
);
assert!(
!expected_bsf_path.exists(),
"ppc.bsf existed before we asked for it?"
);
let bsf_path = fs
.boot1_sytstem_path()
.await
.expect("Failed to create bsf!");
assert_eq!(expected_bsf_path, bsf_path);
assert!(
BootSystemFile::try_from(Bytes::from(
read(bsf_path).expect("Failed to read written boot system file!")
))
.is_ok(),
"Failed to read generated boot system file!"
);
let expected_diskid_path = join_many(
tempdir.path(),
[
"temp".to_owned(),
username()
.expect("Failed to get system username!")
.to_lowercase(),
"caferun".to_owned(),
"diskid.bin".to_owned(),
],
);
assert!(
!expected_diskid_path.exists(),
"diskid.bin existed before we asked for it?"
);
let diskid_path = fs
.disk_id_path()
.await
.expect("Failed to create diskid.bin!");
assert_eq!(expected_diskid_path, diskid_path);
assert_eq!(
read(diskid_path).expect("Failed to read written diskid.bin!"),
vec![0; 32],
"Failed to read generated diskid.bin!"
);
assert_eq!(
fs.firmware_file_path(),
join_many(
tempdir.path(),
[
"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"
],
),
);
let expected_ppc_boot_dlf_path = join_many(
tempdir.path(),
[
"temp".to_owned(),
username()
.expect("Failed to get system username!")
.to_lowercase(),
"caferun".to_owned(),
"ppc_boot.dlf".to_owned(),
],
);
assert!(
!expected_ppc_boot_dlf_path.exists(),
"ppc_boot.dlf existed before we asked for it?"
);
let ppc_boot_dlf_path = fs
.ppc_boot_dlf_path()
.await
.expect("Failed to create ppc_boot.dlf!");
assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
assert!(
DiskLayoutFile::try_from(Bytes::from(
read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
))
.is_ok(),
"Failed to read generated ppc_boot.dlf!"
);
}
#[tokio::test]
pub async fn path_allows_writes() {
let (_tempdir, fs) = create_temporary_host_filesystem().await;
assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
}
#[tokio::test]
pub async fn resolve_path() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
for (dir, name) in [
("/%MLC_EMU_DIR", "mlc"),
("/%SLC_EMU_DIR", "slc"),
("/%DISC_EMU_DIR", "disc"),
("/%SAVE_EMU_DIR", "save"),
] {
assert!(
fs.resolve_path(&format!("{dir}")).await.is_ok(),
"Failed to resolve: `{}`: {:?}",
dir,
fs.resolve_path(&format!("{dir}")).await
);
assert!(
fs.resolve_path(&format!("{dir}/")).await.is_ok(),
"Failed to resolve: `{}/`",
dir,
);
assert!(
fs.resolve_path(&format!("{dir}/./")).await.is_ok(),
"Failed to resolve: `{}/./`",
dir,
);
assert!(
fs.resolve_path(&format!("{dir}/../{name}")).await.is_ok(),
"Failed to resolve: `{}/../{}`",
dir,
name,
);
}
let mut out_of_path = PathBuf::from(tempdir.path());
out_of_path.pop();
assert!(
fs.resolve_path(
&out_of_path
.clone()
.into_os_string()
.into_string()
.expect("Failed to convert pathbuf to string!")
)
.await
.is_err()
);
assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").await.is_err());
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let mut tempdir_symlink = PathBuf::from(tempdir.path());
tempdir_symlink.push("symlink");
symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
assert!(
fs.resolve_path(&format!(
"{}/symlink",
tempdir_symlink
.into_os_string()
.into_string()
.expect("tempdir symlink wasn't utf8?"),
))
.await
.is_err()
);
}
#[cfg(target_os = "windows")]
{
use std::os::windows::fs::symlink_dir;
let mut tempdir_symlink = PathBuf::from(tempdir.path());
tempdir_symlink.push("symlink");
symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
assert!(
fs.resolve_path(&format!(
"{}/symlink",
tempdir_symlink
.into_os_string()
.into_string()
.expect("tempdir symlink wasn't utf8?"),
))
.await
.is_err()
);
}
}
#[tokio::test]
pub async fn opening_files() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
let path = join_many(tempdir.path(), ["file.txt"]);
tokio::fs::write(path.clone(), vec![0; 1307])
.await
.expect("Failed to write test file!");
let create_path = join_many(tempdir.path(), ["new-file.txt"]);
let mut oo = OpenOptions::new();
oo.create(false).write(true).read(true);
assert!(
fs.open_file(oo, &create_path, None).await.is_err(),
"Somehow succeeding opening a file that doesn't exist with no create flag?",
);
oo = OpenOptions::new();
oo.create(true).write(true).truncate(true);
let fd = fs
.open_file(oo, &create_path, None)
.await
.expect("Failed opening a file that doesn't exist with a create flag?");
assert!(
fs.open_file_handles.len() == 1 && fs.open_file_handles.get_sync(&fd).is_some(),
"Open file wasn't in open files list!",
);
fs.close_file(fd, None).await;
assert!(
fs.open_file_handles.is_empty(),
"Somehow after opening/closing, open file handles was not empty?",
);
}
#[tokio::test]
pub async fn seek_and_read() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
let path = join_many(tempdir.path(), ["file.txt"]);
tokio::fs::write(path.clone(), vec![0; 1307])
.await
.expect("Failed to write test file!");
let mut oo = OpenOptions::new();
oo.read(true).create(false).write(false);
let fd = fs
.open_file(oo, &path, None)
.await
.expect("Failed to open existing file!");
assert_eq!(
Some(BytesMut::zeroed(1307).freeze()),
fs.read_file(fd, 1307, None)
.await
.expect("Failed to read from FD!"),
);
fs.seek_file(fd, true, 0, None)
.await
.expect("Failed to sync to beginning of file!");
assert_eq!(
Some(BytesMut::zeroed(1307).freeze()),
fs.read_file(fd, 1307, None)
.await
.expect("Failed to read from FD!"),
);
fs.close_file(fd, None).await;
assert!(
fs.open_file_handles.is_empty(),
"Somehow after opening/closing, open file handles was not empty?",
);
}
#[tokio::test]
pub async fn open_and_close_folder() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
let path = join_many(tempdir.path(), ["a", "b"]);
tokio::fs::create_dir_all(path.clone())
.await
.expect("Failed to create test directory!");
let fd = fs
.open_folder(&path, None)
.await
.expect("Failed to open folder!");
assert!(
fs.open_folder_handles.len() == 1,
"Expected one open folder handle",
);
fs.close_folder(fd, None).await;
assert!(
fs.open_folder_handles.is_empty(),
"Somehow after opening/closing, open folder handles was not empty?",
);
}
#[tokio::test]
pub async fn seek_within_folder() {
let (tempdir, fs) = create_temporary_host_filesystem().await;
let path = join_many(tempdir.path(), ["a", "b"]);
tokio::fs::create_dir_all(path.clone())
.await
.expect("Failed to create test directory!");
_ = tokio::fs::File::create(join_many(&path, ["c"]))
.await
.expect("Failed to create file to use!");
tokio::fs::create_dir(join_many(&path, ["d"]))
.await
.expect("Failed to create directory to use!");
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let mut tempdir_symlink = path.clone();
tempdir_symlink.push("e");
symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
}
#[cfg(target_os = "windows")]
{
use std::os::windows::fs::symlink_dir;
let mut tempdir_symlink = path.clone();
tempdir_symlink.push("e");
symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
}
_ = tokio::fs::File::create(join_many(&path, ["f"]))
.await
.expect("Failed to create file to use!");
_ = tokio::fs::File::create(join_many(&path, ["d", "a"]))
.await
.expect("Failed to create file to use!");
let dfd = fs
.open_folder(&path, None)
.await
.expect("Failed to open folder!");
assert!(fs.next_in_folder(dfd, None).await.is_some());
assert!(fs.next_in_folder(dfd, None).await.is_some());
assert!(fs.next_in_folder(dfd, None).await.is_some());
assert!(fs.next_in_folder(dfd, None).await.is_none());
assert!(fs.next_in_folder(dfd, None).await.is_none());
fs.reverse_folder(dfd, None).await;
assert!(fs.next_in_folder(dfd, None).await.is_some());
assert!(fs.next_in_folder(dfd, None).await.is_none());
}
}