1use crate::{
5 TitleID,
6 errors::{CatBridgeError, FSError},
7 fsemul::{
8 bsf::BootSystemFile,
9 dlf::DiskLayoutFile,
10 errors::{FSEmulAPIError, FSEmulFSError},
11 pcfs::errors::PcfsApiError,
12 },
13};
14use bytes::{Bytes, BytesMut};
15use scc::{
16 HashMap as ConcurrentMap, HashSet as ConcurrentSet, hash_map::OccupiedEntry as CMOccupiedEntry,
17};
18use std::{
19 collections::HashMap,
20 ffi::OsString,
21 fs::{
22 DirEntry, copy as copy_file_sync, create_dir_all as create_dir_all_sync,
23 read_dir as read_dir_sync, read_link as read_link_sync,
24 remove_dir_all as remove_dir_all_sync, remove_file as remove_file_sync,
25 rename as rename_sync,
26 },
27 hash::RandomState,
28 io::{Error as IOError, SeekFrom},
29 path::{Path, PathBuf},
30 sync::{
31 Arc,
32 atomic::{AtomicBool, AtomicI32, Ordering as AtomicOrdering},
33 },
34};
35use tokio::{
36 fs::{File, OpenOptions, read as fs_read, write as fs_write},
37 io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
38 sync::Mutex,
39};
40use tracing::{info, warn};
41use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
42use walkdir::WalkDir;
43use whoami::username;
44
45static UNIQUE_FILE_FD: AtomicI32 = AtomicI32::new(1);
47static FOLDER_FD: AtomicI32 = AtomicI32::new(1);
49
50#[allow(
58 clippy::type_complexity,
60)]
61#[derive(Clone, Debug)]
62pub struct HostFilesystem {
63 cafe_sdk_path: PathBuf,
65 disc_mounted: Arc<Mutex<Option<(bool, bool, TitleID)>>>,
72 open_file_handles: Arc<ConcurrentMap<i32, (File, u64, PathBuf, Option<u64>)>>,
76 open_folder_handles:
80 Arc<ConcurrentMap<i32, (Vec<DirEntry>, usize, bool, PathBuf, Option<u64>)>>,
81 folders_marked_read_only: Arc<ConcurrentSet<PathBuf>>,
91 is_using_unique_fds: bool,
94 has_opened_file: Arc<AtomicBool>,
97}
98
99impl HostFilesystem {
100 pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
125 let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
126 return Err(FSEmulFSError::CantFindCafeSdkPath.into());
127 };
128
129 Self::patch_case_sensitivity(&cafe_sdk_path).await?;
130
131 for path in [
132 &[
133 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
134 ] as &[&str],
135 &[
136 "data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
137 ],
138 &[
139 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
140 ],
141 &[
142 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
143 ],
144 &[
145 "data", "mlc", "sys", "title", "00050010", "1f700500", "content",
146 ],
147 &[
148 "data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
149 ],
150 &[
152 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
153 ],
154 ] {
155 if !Self::join_many(&cafe_sdk_path, path).exists() {
156 return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
157 }
158 }
159
160 Self::prepare_for_serving(&cafe_sdk_path).await?;
161 let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
162
163 Ok(Self {
164 cafe_sdk_path,
165 disc_mounted: Arc::new(Mutex::new(None)),
166 folders_marked_read_only: Arc::new(ro_folders),
167 open_file_handles: Arc::new(ConcurrentMap::new()),
168 open_folder_handles: Arc::new(ConcurrentMap::new()),
169 is_using_unique_fds: false,
170 has_opened_file: Arc::new(AtomicBool::new(false)),
171 })
172 }
173
174 #[must_use]
180 pub const fn cafe_sdk_path(&self) -> &PathBuf {
181 &self.cafe_sdk_path
182 }
183
184 #[must_use]
190 pub fn disc_emu_path(&self) -> PathBuf {
191 Self::join_many(&self.cafe_sdk_path, ["data", "disc"])
192 }
193
194 pub fn force_unique_fds(&mut self) -> Result<(), FSEmulAPIError> {
211 if self.has_opened_file.load(AtomicOrdering::Relaxed) {
212 Err(FSEmulAPIError::CannotSwapFdStrategy)
213 } else {
214 self.is_using_unique_fds = true;
215 Ok(())
216 }
217 }
218
219 pub async fn open_file(
225 &self,
226 open_options: OpenOptions,
227 path: &PathBuf,
228 stream_owner: Option<u64>,
229 ) -> Result<i32, FSError> {
230 self.has_opened_file.store(true, AtomicOrdering::Relaxed);
231 let fd = open_options.open(path).await?;
232 let raw_fd;
233 #[cfg(unix)]
234 {
235 use std::os::fd::AsRawFd;
236 raw_fd = fd.as_raw_fd();
237 }
238 #[cfg(target_os = "windows")]
239 {
240 use std::os::windows::io::AsRawHandle;
241 raw_fd = fd.as_raw_handle() as i32;
242 }
243
244 let md = fd.metadata().await?;
245 let final_fd = if self.is_using_unique_fds {
246 UNIQUE_FILE_FD.fetch_add(1, AtomicOrdering::SeqCst)
247 } else {
248 raw_fd
249 };
250
251 self.open_file_handles
252 .insert_async(final_fd, (fd, md.len(), path.clone(), stream_owner))
253 .await
254 .map_err(|_| IOError::other("somehow got duplicate fd?"))?;
255 Ok(final_fd)
256 }
257
258 pub async fn get_file(
262 &self,
263 fd: i32,
264 for_stream: Option<u64>,
265 ) -> Option<CMOccupiedEntry<'_, i32, (File, u64, PathBuf, Option<u64>), RandomState>> {
266 self.open_file_handles
267 .get_async(&fd)
268 .await
269 .and_then(|entry| {
270 if Self::allow_file_access(&entry, for_stream) {
271 Some(entry)
272 } else {
273 None
274 }
275 })
276 }
277
278 pub async fn file_length(&self, fd: i32, for_stream: Option<u64>) -> Option<u64> {
282 self.open_file_handles
283 .get_async(&fd)
284 .await
285 .and_then(|entry| {
286 if Self::allow_file_access(&entry, for_stream) {
287 Some(entry.1)
288 } else {
289 None
290 }
291 })
292 }
293
294 pub async fn read_file(
305 &self,
306 fd: i32,
307 mut total_data_to_read: usize,
308 for_stream: Option<u64>,
309 ) -> Result<Option<Bytes>, FSError> {
310 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
311 return Ok(None);
312 };
313 if !Self::allow_file_access(&real_entry, for_stream) {
314 return Ok(None);
315 }
316 let file_reader = &mut real_entry.0;
317
318 let mut file_buff = BytesMut::zeroed(total_data_to_read);
319 let mut total_bytes_read = 0_usize;
320 while total_data_to_read > 0 {
321 let bytes_read = file_reader.read(&mut file_buff[total_bytes_read..]).await?;
322 if bytes_read == 0 {
323 break;
324 }
325 total_data_to_read -= bytes_read;
326 total_bytes_read += bytes_read;
327 }
328 if file_buff.len() > total_bytes_read {
329 file_buff.truncate(total_bytes_read);
330 }
331
332 Ok(Some(file_buff.freeze()))
333 }
334
335 pub async fn write_file(
346 &self,
347 fd: i32,
348 data_to_write: Bytes,
349 for_stream: Option<u64>,
350 ) -> Result<(), FSError> {
351 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
352 return Err(FSError::IO(IOError::other("file not open")));
353 };
354 if !Self::allow_file_access(&real_entry, for_stream) {
355 return Err(FSError::IO(IOError::other("file not open")));
356 }
357 let file_writer = &mut real_entry.0;
358 file_writer.write_all(&data_to_write).await?;
359
360 Ok(())
361 }
362
363 pub async fn seek_file(
373 &self,
374 fd: i32,
375 begin: bool,
376 for_stream: Option<u64>,
377 ) -> Result<(), FSError> {
378 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
379 return Ok(());
380 };
381 if !Self::allow_file_access(&real_entry, for_stream) {
382 return Ok(());
383 }
384 let file_reader = &mut real_entry.0;
385
386 if begin {
387 file_reader.seek(SeekFrom::Start(0)).await?;
388 } else {
389 file_reader.seek(SeekFrom::End(0)).await?;
390 }
391
392 Ok(())
393 }
394
395 pub async fn close_file(&self, fd: i32, for_stream: Option<u64>) {
404 if let Some(entry) = self.open_file_handles.get_async(&fd).await
405 && !Self::allow_file_access(&entry, for_stream)
406 {
407 return;
409 }
410
411 self.open_file_handles.remove_async(&fd).await;
412 }
413
414 pub fn open_folder(&self, path: &PathBuf, for_stream: Option<u64>) -> Result<i32, FSError> {
423 let mut dhandle = read_dir_sync(path)?
424 .filter_map(Result::ok)
425 .collect::<Vec<_>>();
426 dhandle.sort_by_key(DirEntry::path);
427
428 let fake_fd = FOLDER_FD.fetch_add(1, AtomicOrdering::SeqCst);
429
430 self.open_folder_handles
431 .insert_sync(fake_fd, (dhandle, 0, false, path.clone(), for_stream))
432 .map_err(|_| IOError::other("OS returned duplicate fd?"))?;
433 Ok(fake_fd)
434 }
435
436 pub async fn mark_folder_read_only(&self, path: PathBuf) {
442 _ = self.folders_marked_read_only.insert_async(path).await;
443 }
444
445 pub async fn ensure_folder_not_read_only(&self, path: &PathBuf) {
447 self.folders_marked_read_only.remove_async(path).await;
448 }
449
450 pub async fn folder_is_read_only(&self, path: &PathBuf) -> bool {
452 self.folders_marked_read_only.contains_async(path).await
453 }
454
455 pub async fn next_in_folder(
465 &self,
466 fd: i32,
467 for_stream: Option<u64>,
468 ) -> Result<Option<(PathBuf, usize)>, FSError> {
469 let Some(mut entry) = self.open_folder_handles.get_async(&fd).await else {
470 return Ok(None);
471 };
472 if !Self::allow_folder_access(&entry, for_stream) {
473 return Ok(None);
474 }
475
476 let component_count = entry.3.components().count();
477 let mut value: Option<PathBuf> = None;
478 if !entry.2 {
479 loop {
480 if entry.1 < entry.0.len() {
481 let ref_value = entry.0[entry.1].path();
482 entry.1 += 1;
483
484 if (!ref_value.is_file() && !ref_value.is_dir()) || ref_value.is_symlink() {
485 continue;
486 }
487
488 value = Some(ref_value);
489 }
490
491 break;
492 }
493 if value.is_none() {
494 entry.2 = true;
495 }
496 }
497
498 Ok(value.map(|val| (val, component_count)))
499 }
500
501 pub async fn reverse_folder(&self, fd: i32, for_stream: Option<u64>) -> Result<(), FSError> {
511 let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
512 return Ok(());
513 };
514 if !Self::allow_folder_access(&real_entry, for_stream) {
515 return Ok(());
516 }
517 if real_entry.1 == 0 {
518 return Ok(());
519 }
520
521 real_entry.1 -= 1;
522 real_entry.2 = false;
523 Ok(())
524 }
525
526 pub async fn close_folder(&self, fd: i32, for_stream: Option<u64>) {
535 if let Some(real_entry) = self.open_folder_handles.get_async(&fd).await
536 && !Self::allow_folder_access(&real_entry, for_stream)
537 {
538 return;
539 }
540
541 self.open_folder_handles.remove_async(&fd).await;
542 }
543
544 pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, FSError> {
555 let mut path = self.temp_path()?;
556 path.push("caferun");
557 if !path.exists() {
558 create_dir_all_sync(&path)?;
559 }
560 path.push("ppc.bsf");
561
562 if !path.exists() {
563 fs_write(&path, Bytes::from(BootSystemFile::default())).await?;
564 }
565
566 Ok(path)
567 }
568
569 pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
579 let mut path = self.temp_path()?;
580 path.push("caferun");
581 if !path.exists() {
582 create_dir_all_sync(&path)?;
583 }
584 path.push("diskid.bin");
585
586 if !path.exists() {
587 fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
588 }
589
590 Ok(path)
591 }
592
593 #[doc(
594 hidden,
596 )]
597 pub async fn mount_disk_title(
604 &mut self,
605 is_slc: bool,
606 is_sys: bool,
607 title_id: TitleID,
608 ) -> Result<(), FSError> {
609 let source_path = Self::join_many(
610 &self.cafe_sdk_path,
611 [
612 "data".to_owned(),
613 if is_slc { "slc" } else { "mlc" }.to_owned(),
614 if is_sys { "sys" } else { "usr" }.to_owned(),
615 "title".to_owned(),
616 format!("{:08x}", title_id.0),
617 format!("{:08x}", title_id.1),
618 ],
619 );
620 let dest_path = Self::join_many(&self.cafe_sdk_path, ["data", "disc"]);
621 if dest_path.exists() {
622 remove_dir_all_sync(&dest_path).map_err(FSError::IO)?;
623 }
624
625 Self::copy_dir(&source_path, &dest_path)?;
626 {
628 let mut guard = self.disc_mounted.lock().await;
629 guard.replace((is_slc, is_sys, title_id));
630 }
631 todo!("figure out how to mount diskid.bin")
632 }
633
634 #[must_use]
639 pub fn firmware_file_path(&self) -> PathBuf {
640 Self::join_many(
641 &self.slc_path_for((0x0005_0010, 0x1000_400A)),
642 ["code", "fw.img"],
643 )
644 }
645
646 pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
659 let mut path = self.temp_path()?;
660 path.push("caferun");
661 if !path.exists() {
662 create_dir_all_sync(&path).map_err(FSError::from)?;
663 }
664 path.push("ppc_boot.dlf");
665
666 if !path.exists() {
667 let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
670 root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
671 root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
672 root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
673 fs_write(&path, Bytes::from(root_dlf))
674 .await
675 .map_err(FSError::from)?;
676 }
677
678 Ok(path)
679 }
680
681 #[must_use]
683 pub fn path_allows_writes(&self, path: &Path) -> bool {
684 let lossy_path = path.to_string_lossy();
686 let trimmed_lossy_path = lossy_path
687 .trim_start_matches("/vol/pc")
688 .trim_start_matches('/');
689 if trimmed_lossy_path.starts_with("%DISC_EMU_DIR") {
690 return trimmed_lossy_path.starts_with("%DISC_EMU_DIR/save");
691 }
692 if path.starts_with(Self::join_many(&self.cafe_sdk_path, ["data", "disc"])) {
693 return path.starts_with(Self::join_many(
694 &self.cafe_sdk_path,
695 ["data", "disc", "save"],
696 ));
697 }
698
699 true
700 }
701
702 pub fn resolve_path(
720 &self,
721 potentially_prefixed_path: &str,
722 ) -> Result<ResolvedLocation, CatBridgeError> {
723 let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
729 if path.starts_with("/%NETWORK") {
730 todo!("NETWORK shares not yet implemented :( sorry!")
731 }
732
733 let non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
734 self.replace_emu_dir(path, "mlc")
735 } else if path.starts_with("/%SLC_EMU_DIR") {
736 self.replace_emu_dir(path, "slc")
737 } else if path.starts_with("/%DISC_EMU_DIR") {
738 self.replace_emu_dir(path, "disc")
739 } else if path.starts_with("/%SAVE_EMU_DIR") {
740 self.replace_emu_dir(path, "save")
741 } else {
742 PathBuf::from(path)
743 };
744
745 let mut closest_canonical_directory = non_canonical_path.clone();
752 let mut changed_at_all = false;
753 while !closest_canonical_directory.as_os_str().is_empty() {
754 if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
755 closest_canonical_directory = canonicalized;
756 break;
757 }
758
759 changed_at_all = true;
760 closest_canonical_directory.pop();
761 }
762 if closest_canonical_directory.as_os_str().is_empty() {
765 return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
766 }
767 let canonicalized_cafe = self
769 .cafe_sdk_path()
770 .canonicalize()
771 .unwrap_or_else(|_| self.cafe_sdk_path().clone());
772 if !closest_canonical_directory.starts_with(canonicalized_cafe) {
773 return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
774 }
775
776 Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
777 non_canonical_path,
778 closest_canonical_directory,
779 !changed_at_all,
780 )))
781 }
782
783 pub fn create_directory(&self, at: &Path) -> Result<(), FSError> {
789 create_dir_all_sync(at).map_err(FSError::IO)
790 }
791
792 pub fn copy(&self, from: &Path, to: &Path) -> Result<(), FSError> {
798 if from.is_dir() {
799 Self::copy_dir(from, to)
800 } else {
801 copy_file_sync(from, to).map_err(FSError::IO).map(|_| ())
802 }
803 }
804
805 pub fn rename(&self, from: &Path, to: &Path) -> Result<(), FSError> {
815 if from.is_dir() {
816 Self::rename_dir(from, to)
817 } else {
818 rename_sync(from, to).map_err(FSError::IO)
819 }
820 }
821
822 #[must_use]
829 pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
830 Self::join_many(
831 &self.cafe_sdk_path,
832 [
833 "data".to_owned(),
834 "slc".to_owned(),
835 "sys".to_owned(),
836 "title".to_owned(),
837 format!("{:08x}", title_id.0),
838 format!("{:08x}", title_id.1),
839 ],
840 )
841 }
842
843 #[allow(
848 unreachable_code,
850 )]
851 #[must_use]
852 pub fn default_cafe_folder() -> Option<PathBuf> {
853 #[cfg(target_os = "windows")]
854 {
855 return Some(PathBuf::from(r"C:\cafe_sdk"));
856 }
857
858 #[cfg(any(
859 target_os = "linux",
860 target_os = "freebsd",
861 target_os = "openbsd",
862 target_os = "netbsd",
863 target_os = "macos"
864 ))]
865 {
866 return Some(PathBuf::from("/opt/cafe_sdk"));
867 }
868
869 None
870 }
871
872 fn temp_path(&self) -> Result<PathBuf, FSError> {
879 let temp_path = Self::join_many(
880 &self.cafe_sdk_path,
881 ["temp".to_owned(), username().to_lowercase()],
882 );
883 if !temp_path.exists() {
884 create_dir_all_sync(&temp_path)?;
885 }
886 Ok(temp_path)
887 }
888
889 #[must_use]
891 fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
892 where
893 PathTy: AsRef<Path>,
894 IterTy: IntoIterator<Item = PathTy>,
895 {
896 let mut as_owned = PathBuf::from(base);
897 for part in parts {
898 as_owned = as_owned.join(part.as_ref());
899 }
900 as_owned
901 }
902
903 fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
905 let path_minus = path
906 .trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
907 .trim_start_matches('/')
908 .trim_start_matches('\\')
909 .replace('\\', "/");
910
911 Self::join_many(
912 &Self::join_many(self.cafe_sdk_path(), ["data", dir]),
913 path_minus.split('/'),
914 )
915 }
916
917 async fn patch_case_sensitivity(cafe_sdk_path: &Path) -> Result<(), FSError> {
918 if !cafe_sdk_path.exists() {
920 return Ok(());
921 }
922 let capital_path = Self::join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
923 let _ = File::create(&capital_path).await?;
924 let is_insensitive = File::open(Self::join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
925 .await
926 .is_ok();
927 remove_file_sync(capital_path)?;
928 if is_insensitive {
929 return Ok(());
930 }
931
932 info!(
933 "Your Host OS is not case-insensitive for file-paths... ensuring CafeSDK is all lowercase, this may take awhile..."
934 );
935 let cafe_sdk_components = cafe_sdk_path.components().count();
936 let mut had_rename = true;
937 while had_rename {
938 had_rename = false;
939 for directory in [
940 Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
941 Self::join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
942 Self::join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
943 Self::join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
944 ] {
945 if !directory.exists() {
946 continue;
948 }
949
950 let mut iter = WalkDir::new(&directory)
951 .contents_first(false)
952 .follow_links(false)
953 .follow_root_links(false)
954 .into_iter();
955 while let Some(Ok(entry)) = iter.next() {
956 let p = entry.path();
957 if !p.exists() {
958 continue;
959 }
960
961 let path_minus_cafe = p
962 .components()
963 .skip(cafe_sdk_components)
964 .collect::<PathBuf>();
965 let Some(path_as_utf8) = path_minus_cafe.as_os_str().to_str() else {
966 warn!(problematic_path = %p.display(), "Path in Cafe SDK directory is not UTF-8! This may cause errors fetching!");
967 continue;
968 };
969 let new_path = path_as_utf8.to_ascii_lowercase();
970 if path_as_utf8 != new_path {
971 let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
972 final_new_path.push("/");
973 final_new_path.push(&new_path);
974 let new = PathBuf::from(final_new_path);
975
976 if p.is_dir() {
977 Self::rename_dir(p, &new)?;
978 had_rename = true;
979 } else {
980 rename_sync(p, new)?;
981 had_rename = true;
982 }
983 }
984 }
985 }
986 }
987 info!("ensure CafeSDK path is now case-insensitive by renaming to all lowercase...");
988
989 Ok(())
990 }
991
992 fn allow_file_access(
993 entry: &CMOccupiedEntry<i32, (File, u64, PathBuf, Option<u64>), RandomState>,
994 requester: Option<u64>,
995 ) -> bool {
996 let Some(requesting_stream_id) = requester else {
997 return true;
998 };
999 let Some(owned_stream_id) = entry.3 else {
1000 return true;
1001 };
1002
1003 requesting_stream_id == owned_stream_id
1004 }
1005
1006 #[allow(
1007 clippy::type_complexity
1009 )]
1010 fn allow_folder_access(
1011 entry: &CMOccupiedEntry<
1012 i32,
1013 (Vec<DirEntry>, usize, bool, PathBuf, Option<u64>),
1014 RandomState,
1015 >,
1016 requester: Option<u64>,
1017 ) -> bool {
1018 let Some(requesting_stream_id) = requester else {
1019 return true;
1020 };
1021 let Some(owned_stream_id) = entry.4 else {
1022 return true;
1023 };
1024
1025 requesting_stream_id == owned_stream_id
1026 }
1027
1028 async fn prepare_for_serving(cafe_sdk_path: &Path) -> Result<(), FSError> {
1034 if !Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "config", "eco.xml"]).exists() {
1035 Self::generate_eco_xml(cafe_sdk_path).await?;
1036 }
1037 if !Self::join_many(
1038 cafe_sdk_path,
1039 ["data", "slc", "sys", "proc", "prefs", "wii_acct.xml"],
1040 )
1041 .exists()
1042 {
1043 Self::generate_wii_acct_xml(cafe_sdk_path).await?;
1044 }
1045
1046 if Self::join_many(cafe_sdk_path, ["data", "disc"]).exists() {
1048 remove_dir_all_sync(Self::join_many(cafe_sdk_path, ["data", "disc"]))
1049 .map_err(FSError::IO)?;
1050 }
1051 let disc_dir = Self::join_many(cafe_sdk_path, ["data", "disc"]);
1055 let sctt_dir = Self::join_many(
1056 cafe_sdk_path,
1057 ["data", "mlc", "sys", "title", "00050010", "1f700500"],
1058 );
1059 for subpath in ["code", "content", "meta"] {
1060 Self::copy_dir(
1061 &Self::join_many(&sctt_dir, [subpath]),
1062 &Self::join_many(&disc_dir, [subpath]),
1063 )?;
1064 }
1065 let app_xml_path = Self::join_many(cafe_sdk_path, ["data", "disc", "code", "app.xml"]);
1068 let base_app_xml = String::from_utf8_lossy(&fs_read(&app_xml_path).await?).to_string();
1072 fs_write(&app_xml_path, Self::capitilize_title_id(base_app_xml)).await?;
1073
1074 Ok(())
1075 }
1076
1077 fn copy_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1078 if !dest_path.exists() {
1079 create_dir_all_sync(dest_path)?;
1080 }
1081 let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1082 let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1083
1084 for result in WalkDir::new(source_path)
1085 .follow_links(false)
1086 .follow_root_links(false)
1087 {
1088 let rpb = result?.into_path();
1089 let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1090 let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1091 new_bytes.extend_from_slice(new_path_as_str_bytes);
1092 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1093 let as_new_path =
1094 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1095
1096 if rpb.is_symlink() {
1097 let mut resolved_path = read_link_sync(&rpb)?;
1098 {
1099 let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1102 if os_str_for_resolved.starts_with(old_path_bytes) {
1103 let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1104 new_bytes.extend_from_slice(new_path_as_str_bytes);
1105 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1106 resolved_path = PathBuf::from(unsafe {
1107 OsString::from_encoded_bytes_unchecked(new_bytes)
1108 });
1109 }
1110 }
1111
1112 #[cfg(unix)]
1113 {
1114 use std::os::unix::fs::symlink;
1115 symlink(resolved_path, &as_new_path)?;
1116 }
1117
1118 #[cfg(target_os = "windows")]
1119 {
1120 use std::os::windows::fs::{symlink_dir, symlink_file};
1121
1122 if resolved_path.is_dir() {
1123 symlink_dir(resolved_path, &as_new_path)?;
1124 } else {
1125 symlink_file(resolved_path, &as_new_path)?;
1126 }
1127 }
1128 } else if rpb.is_file() {
1129 copy_file_sync(&rpb, &as_new_path)?;
1130 } else if rpb.is_dir() {
1131 create_dir_all_sync(&as_new_path)?;
1132 }
1133 }
1134
1135 Ok(())
1136 }
1137
1138 fn rename_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1148 if !dest_path.exists() {
1149 create_dir_all_sync(dest_path)?;
1150 }
1151 let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1152 let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1153
1154 for result in WalkDir::new(source_path)
1155 .follow_links(false)
1156 .follow_root_links(false)
1157 {
1158 let rpb = result?.into_path();
1159 let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1160 let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1161 new_bytes.extend_from_slice(new_path_as_str_bytes);
1162 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1163 let as_new_path =
1164 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1165
1166 if rpb.is_symlink() {
1167 let mut resolved_path = read_link_sync(&rpb)?;
1168 {
1169 let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1172 if os_str_for_resolved.starts_with(old_path_bytes) {
1173 let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1174 new_bytes.extend_from_slice(new_path_as_str_bytes);
1175 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1176 resolved_path = PathBuf::from(unsafe {
1177 OsString::from_encoded_bytes_unchecked(new_bytes)
1178 });
1179 }
1180 }
1181
1182 let should_remove: bool;
1189 #[cfg(unix)]
1190 {
1191 use std::os::unix::fs::symlink;
1192 symlink(resolved_path, &as_new_path)?;
1193 should_remove = true;
1194 }
1195
1196 #[cfg(target_os = "windows")]
1197 {
1198 use std::os::windows::fs::{symlink_dir, symlink_file};
1199
1200 if resolved_path.is_dir() {
1201 symlink_dir(resolved_path, &as_new_path)?;
1202 should_remove = false;
1203 } else {
1204 symlink_file(resolved_path, &as_new_path)?;
1205 should_remove = true;
1206 }
1207 }
1208
1209 if should_remove {
1211 remove_file_sync(&rpb)?;
1212 }
1213 } else if rpb.is_file() {
1214 rename_sync(&rpb, &as_new_path)?;
1215 } else if rpb.is_dir() {
1216 create_dir_all_sync(&as_new_path)?;
1217 }
1218 }
1219 remove_dir_all_sync(source_path)?;
1221
1222 Ok(())
1223 }
1224
1225 async fn generate_eco_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1236 let mut eco_path = Self::join_many(cafe_os_path, ["data", "slc", "sys", "config"]);
1237 if !eco_path.exists() {
1238 create_dir_all_sync(&eco_path).map_err(FSError::IO)?;
1239 }
1240 eco_path.push("eco.xml");
1241
1242 let mut eco_file = File::create(eco_path).await.map_err(FSError::IO)?;
1243 eco_file
1244 .write_all(
1245 br#"<?xml version="1.0" encoding="utf-8"?>
1246<eco type="complex" access="777">
1247 <enable type="unsignedInt" length="4">0</enable>
1248 <max_on_time type="unsignedInt" length="4">3601</max_on_time>
1249 <default_off_time type="unsignedInt" length="4">15</default_off_time>
1250 <wd_disable type="unsignedInt" length="4">1</wd_disable>
1251</eco>"#,
1252 )
1253 .await
1254 .map_err(FSError::IO)?;
1255
1256 #[cfg(unix)]
1257 {
1258 use std::{fs::Permissions, os::unix::prelude::*};
1259 eco_file
1260 .set_permissions(Permissions::from_mode(0o770))
1261 .await?;
1262 }
1263
1264 Ok(())
1265 }
1266
1267 async fn generate_wii_acct_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1278 let mut wii_path = Self::join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
1279 if !wii_path.exists() {
1280 create_dir_all_sync(&wii_path).map_err(FSError::IO)?;
1281 }
1282 wii_path.push("wii_acct.xml");
1283
1284 let mut wii_file = File::create(wii_path).await.map_err(FSError::IO)?;
1285 wii_file
1286 .write_all(
1287 br#"<?xml version="1.0" encoding="utf-8"?>
1288<wii_acct type="complex">
1289 <profile type="complex">
1290 <nickname type="hexBinary" length="22">00570069006900000000000000000000000000000000</nickname>
1291
1292 <language type="unsignedInt" length="4">0</language>
1293 <country type="unsignedInt" length="4">1</country>
1294 </profile>
1295 <pc type="complex">
1296 <rating type="unsignedInt" length="4">18</rating>
1297 <organization type="unsignedInt" length="4">0</organization>
1298 <rst_internet_ch type="unsignedByte" length="1">0</rst_internet_ch>
1299 <rst_nw_access type="unsignedByte" length="1">0</rst_nw_access>
1300 <rst_pt_order type="unsignedByte" length="1">0</rst_pt_order>
1301 </pc>
1302</wii_acct>"#,
1303 )
1304 .await
1305 .map_err(FSError::IO)?;
1306
1307 #[cfg(unix)]
1308 {
1309 use std::{fs::Permissions, os::unix::prelude::*};
1310 wii_file
1311 .set_permissions(Permissions::from_mode(0o770))
1312 .await?;
1313 }
1314
1315 Ok(())
1316 }
1317
1318 #[must_use]
1321 fn capitilize_title_id(app_xml: String) -> String {
1322 let Some(title_id_xml_tag_start) = app_xml.find("<title_id") else {
1323 return app_xml;
1324 };
1325 let Some(title_id_tag_end) = app_xml[title_id_xml_tag_start..].find('>') else {
1326 return app_xml;
1327 };
1328
1329 let tid_start = title_id_xml_tag_start + title_id_tag_end;
1330 let Some(title_slash_location) = app_xml[tid_start..].find("</title_id>") else {
1331 return app_xml;
1332 };
1333 let tid_end = tid_start + title_slash_location;
1334 let title_id = &app_xml[tid_start..tid_end];
1335 let mut final_xml = String::with_capacity(app_xml.len());
1336 final_xml += &app_xml[..tid_start];
1337 final_xml += &title_id.to_uppercase();
1338 final_xml += &app_xml[tid_end..];
1339
1340 final_xml
1341 }
1342
1343 fn get_default_read_only_folders(cafe_dir: &Path) -> ConcurrentSet<PathBuf> {
1344 let set = ConcurrentSet::new();
1345
1346 for cafe_sub_paths in [
1347 &["data", "slc", "sys", "config"] as &[&str],
1348 &["data", "slc", "sys", "proc"],
1349 &["data", "slc", "sys", "logs"],
1350 &["data", "mlc", "usr"],
1351 &["data", "mlc", "usr", "import"],
1352 &["data", "mlc", "usr", "title"],
1353 ] {
1354 _ = set.insert_sync(Self::join_many(cafe_dir, cafe_sub_paths));
1355 }
1356
1357 set
1358 }
1359}
1360
1361const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
1362 NamedField::new("cafe_sdk_path"),
1363 NamedField::new("open_file_handles"),
1364 NamedField::new("open_folder_handles"),
1365];
1366
1367impl Structable for HostFilesystem {
1368 fn definition(&self) -> StructDef<'_> {
1369 StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
1370 }
1371}
1372
1373impl Valuable for HostFilesystem {
1374 fn as_value(&self) -> Value<'_> {
1375 Value::Structable(self)
1376 }
1377
1378 fn visit(&self, visitor: &mut dyn Visit) {
1379 let mut values = HashMap::with_capacity(self.open_file_handles.len());
1380 self.open_file_handles.iter_sync(|k, v| {
1381 values.insert(*k, format!("{}", v.2.display()));
1382 true
1383 });
1384 let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
1385 self.open_folder_handles.iter_sync(|k, v| {
1386 folder_values.insert(*k, format!("{}", v.3.display()));
1387 true
1388 });
1389
1390 visitor.visit_named_fields(&NamedValues::new(
1391 HOST_FILESYSTEM_FIELDS,
1392 &[
1393 Valuable::as_value(&self.cafe_sdk_path),
1394 Valuable::as_value(&values),
1395 Valuable::as_value(&folder_values),
1396 ],
1397 ));
1398 }
1399}
1400
1401#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
1403pub enum ResolvedLocation {
1404 Filesystem(FilesystemLocation),
1411 Network(()),
1415}
1416
1417#[derive(Clone, Debug, PartialEq, Eq)]
1420pub struct FilesystemLocation {
1421 resolved_path: PathBuf,
1423 closest_resolved_path: PathBuf,
1426 canonicalized_is_exact: bool,
1428}
1429impl FilesystemLocation {
1430 #[must_use]
1431 pub const fn new(
1432 resolved_path: PathBuf,
1433 closest_resolved_path: PathBuf,
1434 canonicalized_is_exact: bool,
1435 ) -> Self {
1436 Self {
1437 resolved_path,
1438 closest_resolved_path,
1439 canonicalized_is_exact,
1440 }
1441 }
1442
1443 #[must_use]
1444 pub const fn resolved_path(&self) -> &PathBuf {
1445 &self.resolved_path
1446 }
1447 #[must_use]
1448 pub const fn closest_resolved_path(&self) -> &PathBuf {
1449 &self.closest_resolved_path
1450 }
1451 #[must_use]
1452 pub const fn canonicalized_is_exact(&self) -> bool {
1453 self.canonicalized_is_exact
1454 }
1455}
1456
1457const FILESYSTEM_LOCATION_FIELDS: &[NamedField<'static>] = &[
1458 NamedField::new("resolved_path"),
1459 NamedField::new("closest_resolved_path"),
1460 NamedField::new("canonicalized_is_exact"),
1461];
1462
1463impl Structable for FilesystemLocation {
1464 fn definition(&self) -> StructDef<'_> {
1465 StructDef::new_static(
1466 "FilesystemLocation",
1467 Fields::Named(FILESYSTEM_LOCATION_FIELDS),
1468 )
1469 }
1470}
1471
1472impl Valuable for FilesystemLocation {
1473 fn as_value(&self) -> Value<'_> {
1474 Value::Structable(self)
1475 }
1476
1477 fn visit(&self, visitor: &mut dyn Visit) {
1478 visitor.visit_named_fields(&NamedValues::new(
1479 FILESYSTEM_LOCATION_FIELDS,
1480 &[
1481 Valuable::as_value(&self.resolved_path),
1482 Valuable::as_value(&self.closest_resolved_path),
1483 Valuable::as_value(&self.canonicalized_is_exact),
1484 ],
1485 ));
1486 }
1487}
1488
1489#[cfg_attr(docsrs, doc(cfg(test)))]
1490#[cfg(test)]
1491pub mod test_helpers {
1492 use super::*;
1493 use std::fs::{File, create_dir_all};
1494 use tempfile::{TempDir, tempdir};
1495
1496 #[allow(
1498 dead_code,
1500 )]
1501 pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
1502 let dir = tempdir().expect("Failed to create temporary directory!");
1503
1504 for directory_to_create in vec![
1505 vec!["data", "slc"],
1507 vec!["data", "mlc"],
1508 vec!["data", "disc"],
1509 vec!["data", "save"],
1510 vec![
1512 "data", "mlc", "sys", "title", "00050030", "1001000a", "code",
1513 ],
1514 vec![
1515 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
1516 ],
1517 vec![
1518 "data", "mlc", "sys", "title", "00050010", "1f700500", "content",
1519 ],
1520 vec![
1521 "data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
1522 ],
1523 vec![
1525 "data", "mlc", "sys", "title", "00050030", "1001010A", "code",
1526 ],
1527 vec![
1528 "data", "mlc", "sys", "title", "00050030", "1001020a", "code",
1529 ],
1530 vec![
1531 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
1532 ],
1533 vec!["data", "mlc", "sys", "update", "nand", "os_v10_ndebug"],
1534 vec!["data", "mlc", "sys", "update", "nand", "os_v10_debug"],
1535 vec!["data", "slc", "sys", "proc", "prefs"],
1536 vec![
1537 "data", "slc", "sys", "title", "00050010", "1000800a", "code",
1538 ],
1539 vec![
1540 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
1541 ],
1542 ] {
1543 create_dir_all(HostFilesystem::join_many(dir.path(), directory_to_create))
1544 .expect("Failed to create directories necessary for host filesystem to work.");
1545 }
1546
1547 File::create(HostFilesystem::join_many(
1550 dir.path(),
1551 [
1552 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
1553 ],
1554 ))
1555 .expect("Failed to create needed app.xml!");
1556 File::create(HostFilesystem::join_many(
1557 dir.path(),
1558 [
1559 "data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
1560 ],
1561 ))
1562 .expect("Failed to create needed app.xml!");
1563 File::create(HostFilesystem::join_many(
1564 dir.path(),
1565 [
1566 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
1567 ],
1568 ))
1569 .expect("Failed to create needed app.xml!");
1570
1571 File::create(HostFilesystem::join_many(
1572 dir.path(),
1573 [
1574 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
1575 ],
1576 ))
1577 .expect("Failed to create needed fw.img!");
1578 File::create(HostFilesystem::join_many(
1579 dir.path(),
1580 [
1581 "data", "mlc", "sys", "title", "00050010", "1f700500", "code", "app.xml",
1582 ],
1583 ))
1584 .expect("Failed to create needed app.xml for disc!");
1585
1586 let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
1587 .await
1588 .expect("Failed to load empty host filesystem!");
1589
1590 (dir, fs)
1591 }
1592
1593 #[allow(
1595 dead_code,
1597 )]
1598 #[must_use]
1599 pub fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
1600 where
1601 PathTy: AsRef<Path>,
1602 IterTy: IntoIterator<Item = PathTy>,
1603 {
1604 HostFilesystem::join_many(base, parts)
1605 }
1606}
1607
1608#[cfg(test)]
1609mod unit_tests {
1610 use super::test_helpers::*;
1611 use super::*;
1612 use std::fs::read;
1613
1614 fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
1615
1616 #[test]
1617 pub fn is_send_sync() {
1618 only_accepts_send_sync::<HostFilesystem>(None);
1619 }
1620
1621 #[test]
1622 pub fn can_find_default_cafe_directory() {
1623 assert!(
1624 HostFilesystem::default_cafe_folder().is_some(),
1625 "Failed to find default cafe directory for your OS",
1626 );
1627 }
1628
1629 #[tokio::test]
1630 pub async fn creatable_files() {
1631 let (tempdir, fs) = create_temporary_host_filesystem().await;
1634
1635 let expected_bsf_path = HostFilesystem::join_many(
1636 tempdir.path(),
1637 [
1638 "temp".to_owned(),
1639 username().to_lowercase(),
1640 "caferun".to_owned(),
1641 "ppc.bsf".to_owned(),
1642 ],
1643 );
1644 assert!(
1645 !expected_bsf_path.exists(),
1646 "ppc.bsf existed before we asked for it?"
1647 );
1648 let bsf_path = fs
1649 .boot1_sytstem_path()
1650 .await
1651 .expect("Failed to create bsf!");
1652 assert_eq!(expected_bsf_path, bsf_path);
1653 assert!(
1654 BootSystemFile::try_from(Bytes::from(
1655 read(bsf_path).expect("Failed to read written boot system file!")
1656 ))
1657 .is_ok(),
1658 "Failed to read generated boot system file!"
1659 );
1660
1661 let expected_diskid_path = HostFilesystem::join_many(
1662 tempdir.path(),
1663 [
1664 "temp".to_owned(),
1665 username().to_lowercase(),
1666 "caferun".to_owned(),
1667 "diskid.bin".to_owned(),
1668 ],
1669 );
1670 assert!(
1671 !expected_diskid_path.exists(),
1672 "diskid.bin existed before we asked for it?"
1673 );
1674 let diskid_path = fs
1675 .disk_id_path()
1676 .await
1677 .expect("Failed to create diskid.bin!");
1678 assert_eq!(expected_diskid_path, diskid_path);
1679 assert_eq!(
1680 read(diskid_path).expect("Failed to read written diskid.bin!"),
1681 vec![0; 32],
1682 "Failed to read generated diskid.bin!"
1683 );
1684
1685 assert_eq!(
1687 fs.firmware_file_path(),
1688 HostFilesystem::join_many(
1689 tempdir.path(),
1690 [
1691 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"
1692 ],
1693 ),
1694 );
1695
1696 let expected_ppc_boot_dlf_path = HostFilesystem::join_many(
1697 tempdir.path(),
1698 [
1699 "temp".to_owned(),
1700 username().to_lowercase(),
1701 "caferun".to_owned(),
1702 "ppc_boot.dlf".to_owned(),
1703 ],
1704 );
1705 assert!(
1706 !expected_ppc_boot_dlf_path.exists(),
1707 "ppc_boot.dlf existed before we asked for it?"
1708 );
1709 let ppc_boot_dlf_path = fs
1710 .ppc_boot_dlf_path()
1711 .await
1712 .expect("Failed to create ppc_boot.dlf!");
1713 assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
1714 assert!(
1715 DiskLayoutFile::try_from(Bytes::from(
1716 read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
1717 ))
1718 .is_ok(),
1719 "Failed to read generated ppc_boot.dlf!"
1720 );
1721 }
1722
1723 #[tokio::test]
1724 pub async fn path_allows_writes() {
1725 let (_tempdir, fs) = create_temporary_host_filesystem().await;
1726
1727 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
1730 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
1731 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
1732 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1733 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1734 }
1735
1736 #[tokio::test]
1737 pub async fn resolve_path() {
1738 let (tempdir, fs) = create_temporary_host_filesystem().await;
1741
1742 for (dir, name) in [
1744 ("/%MLC_EMU_DIR", "mlc"),
1745 ("/%SLC_EMU_DIR", "slc"),
1746 ("/%DISC_EMU_DIR", "disc"),
1747 ("/%SAVE_EMU_DIR", "save"),
1748 ] {
1749 assert!(
1750 fs.resolve_path(&format!("{dir}")).is_ok(),
1751 "Failed to resolve: `{}`: {:?}",
1752 dir,
1753 fs.resolve_path(&format!("{dir}"))
1754 );
1755 assert!(
1756 fs.resolve_path(&format!("{dir}/")).is_ok(),
1757 "Failed to resolve: `{}/`",
1758 dir,
1759 );
1760 assert!(
1761 fs.resolve_path(&format!("{dir}/./")).is_ok(),
1762 "Failed to resolve: `{}/./`",
1763 dir,
1764 );
1765 assert!(
1766 fs.resolve_path(&format!("{dir}/../{name}")).is_ok(),
1767 "Failed to resolve: `{}/../{}`",
1768 dir,
1769 name,
1770 );
1771 }
1772
1773 let mut out_of_path = PathBuf::from(tempdir.path());
1775 out_of_path.pop();
1778
1779 assert!(
1781 fs.resolve_path(
1782 &out_of_path
1783 .clone()
1784 .into_os_string()
1785 .into_string()
1786 .expect("Failed to convert pathbuf to string!")
1787 )
1788 .is_err()
1789 );
1790 assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").is_err());
1791
1792 #[cfg(unix)]
1793 {
1794 use std::os::unix::fs::symlink;
1795
1796 let mut tempdir_symlink = PathBuf::from(tempdir.path());
1797 tempdir_symlink.push("symlink");
1798 symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1799 assert!(
1800 fs.resolve_path(&format!(
1801 "{}/symlink",
1802 tempdir_symlink
1803 .into_os_string()
1804 .into_string()
1805 .expect("tempdir symlink wasn't utf8?"),
1806 ))
1807 .is_err()
1808 );
1809 }
1810
1811 #[cfg(target_os = "windows")]
1812 {
1813 use std::os::windows::fs::symlink_dir;
1814
1815 let mut tempdir_symlink = PathBuf::from(tempdir.path());
1816 tempdir_symlink.push("symlink");
1817 symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1818 assert!(
1819 fs.resolve_path(&format!(
1820 "{}/symlink",
1821 tempdir_symlink
1822 .into_os_string()
1823 .into_string()
1824 .expect("tempdir symlink wasn't utf8?"),
1825 ))
1826 .is_err()
1827 );
1828 }
1829 }
1830
1831 #[tokio::test]
1832 pub async fn opening_files() {
1833 let (tempdir, fs) = create_temporary_host_filesystem().await;
1834 let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1835 tokio::fs::write(path.clone(), vec![0; 1307])
1836 .await
1837 .expect("Failed to write test file!");
1838 let create_path = HostFilesystem::join_many(tempdir.path(), ["new-file.txt"]);
1839
1840 let mut oo = OpenOptions::new();
1841 oo.create(false).write(true).read(true);
1842 assert!(
1843 fs.open_file(oo, &create_path, None).await.is_err(),
1844 "Somehow succeeding opening a file that doesn't exist with no create flag?",
1845 );
1846 oo = OpenOptions::new();
1847 oo.create(true).write(true).truncate(true);
1848 let fd = fs
1849 .open_file(oo, &create_path, None)
1850 .await
1851 .expect("Failed opening a file that doesn't exist with a create flag?");
1852 assert!(
1853 fs.open_file_handles.len() == 1 && fs.open_file_handles.get_sync(&fd).is_some(),
1854 "Open file wasn't in open files list!",
1855 );
1856 fs.close_file(fd, None).await;
1857 assert!(
1858 fs.open_file_handles.is_empty(),
1859 "Somehow after opening/closing, open file handles was not empty?",
1860 );
1861 }
1862
1863 #[tokio::test]
1864 pub async fn seek_and_read() {
1865 let (tempdir, fs) = create_temporary_host_filesystem().await;
1866 let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1867 tokio::fs::write(path.clone(), vec![0; 1307])
1868 .await
1869 .expect("Failed to write test file!");
1870
1871 let mut oo = OpenOptions::new();
1872 oo.read(true).create(false).write(false);
1873 let fd = fs
1874 .open_file(oo, &path, None)
1875 .await
1876 .expect("Failed to open existing file!");
1877
1878 assert_eq!(
1880 Some(BytesMut::zeroed(1307).freeze()),
1881 fs.read_file(fd, 1307, None)
1882 .await
1883 .expect("Failed to read from FD!"),
1884 );
1885 fs.seek_file(fd, true, None)
1886 .await
1887 .expect("Failed to sync to beginning of file!");
1888 assert_eq!(
1890 Some(BytesMut::zeroed(1307).freeze()),
1891 fs.read_file(fd, 1307, None)
1892 .await
1893 .expect("Failed to read from FD!"),
1894 );
1895 fs.close_file(fd, None).await;
1896 assert!(
1897 fs.open_file_handles.is_empty(),
1898 "Somehow after opening/closing, open file handles was not empty?",
1899 );
1900 }
1901
1902 #[tokio::test]
1903 pub async fn open_and_close_folder() {
1904 let (tempdir, fs) = create_temporary_host_filesystem().await;
1905 let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1906 tokio::fs::create_dir_all(path.clone())
1907 .await
1908 .expect("Failed to create test directory!");
1909
1910 let fd = fs
1911 .open_folder(&path, None)
1912 .expect("Failed to open existing folder!");
1913 assert!(
1914 fs.open_folder_handles.len() == 1,
1915 "Expected one open folder handle",
1916 );
1917 fs.close_folder(fd, None).await;
1918
1919 assert!(
1920 fs.open_folder_handles.is_empty(),
1921 "Somehow after opening/closing, open folder handles was not empty?",
1922 );
1923 }
1924
1925 #[tokio::test]
1926 pub async fn seek_within_folder() {
1927 let (tempdir, fs) = create_temporary_host_filesystem().await;
1928 let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1929 tokio::fs::create_dir_all(path.clone())
1930 .await
1931 .expect("Failed to create test directory!");
1932
1933 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["c"]))
1938 .await
1939 .expect("Failed to create file to use!");
1940 tokio::fs::create_dir(HostFilesystem::join_many(&path, ["d"]))
1941 .await
1942 .expect("Failed to create directory to use!");
1943 #[cfg(unix)]
1944 {
1945 use std::os::unix::fs::symlink;
1946
1947 let mut tempdir_symlink = path.clone();
1948 tempdir_symlink.push("e");
1949 symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1950 }
1951 #[cfg(target_os = "windows")]
1952 {
1953 use std::os::windows::fs::symlink_dir;
1954
1955 let mut tempdir_symlink = path.clone();
1956 tempdir_symlink.push("e");
1957 symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1958 }
1959 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["f"]))
1960 .await
1961 .expect("Failed to create file to use!");
1962 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["d", "a"]))
1963 .await
1964 .expect("Failed to create file to use!");
1965
1966 let dfd = fs.open_folder(&path, None).expect("Failed to open folder!");
1967 assert!(
1968 fs.next_in_folder(dfd, None)
1969 .await
1970 .expect("Failed to query for next in folder! 1.1!")
1971 .is_some()
1972 );
1973 assert!(
1974 fs.next_in_folder(dfd, None)
1975 .await
1976 .expect("Failed to query for next in folder! 1.2!")
1977 .is_some()
1978 );
1979 assert!(
1980 fs.next_in_folder(dfd, None)
1981 .await
1982 .expect("Failed to query for next in folder! 1.3!")
1983 .is_some()
1984 );
1985 assert!(
1987 fs.next_in_folder(dfd, None)
1988 .await
1989 .expect("Failed to query for next in folder! 1.4!")
1990 .is_none()
1991 );
1992 assert!(
1994 fs.next_in_folder(dfd, None)
1995 .await
1996 .expect("Failed to query for next in folder! 1.5!")
1997 .is_none()
1998 );
1999 fs.reverse_folder(dfd, None)
2001 .await
2002 .expect("Failed to reverse directory search!");
2003 assert!(
2004 fs.next_in_folder(dfd, None)
2005 .await
2006 .expect("Failed to query for next in folder! 2.1!")
2007 .is_some()
2008 );
2009 assert!(
2010 fs.next_in_folder(dfd, None)
2011 .await
2012 .expect("Failed to query for next in folder! 2.2!")
2013 .is_none()
2014 );
2015 }
2016
2017 #[test]
2018 pub fn can_capitilize_ids() {
2019 assert_eq!(
2020 HostFilesystem::capitilize_title_id(
2021 r#"<?xml version="1.0" encoding = "utf-8"?>
2022<app type="complex" access="777">
2023 <version type="unsignedInt" length="4">16</version>
2024 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2025 <title_id type="hexBinary" length="8">000500101f700500</title_id>
2026 <title_version type="hexBinary" length="2">090D</title_version>
2027 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2028 <app_type type="hexBinary" length="4">90000001</app_type>
2029 <group_id type="hexBinary" length="4">00000400</group_id>
2030 <os_mask type="hexBinary" length="32">0</os_mask>
2031 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2032</app>"#
2033 .to_owned()
2034 ),
2035 r#"<?xml version="1.0" encoding = "utf-8"?>
2036<app type="complex" access="777">
2037 <version type="unsignedInt" length="4">16</version>
2038 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2039 <title_id type="hexBinary" length="8">000500101F700500</title_id>
2040 <title_version type="hexBinary" length="2">090D</title_version>
2041 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2042 <app_type type="hexBinary" length="4">90000001</app_type>
2043 <group_id type="hexBinary" length="4">00000400</group_id>
2044 <os_mask type="hexBinary" length="32">0</os_mask>
2045 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2046</app>"#
2047 .to_owned(),
2048 );
2049
2050 assert_eq!(
2051 HostFilesystem::capitilize_title_id(
2052 r#"<?xml version="1.0" encoding = "utf-8"?>
2053<app type="complex" access="777">
2054 <version type="unsignedInt" length="4">16</version>
2055 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2056 <title_id type="hexBinary" length="8">000500101F700500</title_id>
2057 <title_version type="hexBinary" length="2">090D</title_version>
2058 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2059 <app_type type="hexBinary" length="4">90000001</app_type>
2060 <group_id type="hexBinary" length="4">00000400</group_id>
2061 <os_mask type="hexBinary" length="32">0</os_mask>
2062 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2063</app>"#
2064 .to_owned()
2065 ),
2066 r#"<?xml version="1.0" encoding = "utf-8"?>
2067<app type="complex" access="777">
2068 <version type="unsignedInt" length="4">16</version>
2069 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2070 <title_id type="hexBinary" length="8">000500101F700500</title_id>
2071 <title_version type="hexBinary" length="2">090D</title_version>
2072 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2073 <app_type type="hexBinary" length="4">90000001</app_type>
2074 <group_id type="hexBinary" length="4">00000400</group_id>
2075 <os_mask type="hexBinary" length="32">0</os_mask>
2076 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2077</app>"#
2078 .to_owned(),
2079 );
2080
2081 assert_eq!(
2082 HostFilesystem::capitilize_title_id(
2083 r#"<?xml version="1.0" encoding = "utf-8"?>
2084<app type="complex" access="777">
2085 <version type="unsignedInt" length="4">16</version>
2086 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2087 <title_version type="hexBinary" length="2">090D</title_version>
2088 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2089 <app_type type="hexBinary" length="4">90000001</app_type>
2090 <group_id type="hexBinary" length="4">00000400</group_id>
2091 <os_mask type="hexBinary" length="32">0</os_mask>
2092 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2093</app>"#
2094 .to_owned()
2095 ),
2096 r#"<?xml version="1.0" encoding = "utf-8"?>
2097<app type="complex" access="777">
2098 <version type="unsignedInt" length="4">16</version>
2099 <os_version type="hexBinary" length="8">000500101000400A</os_version>
2100 <title_version type="hexBinary" length="2">090D</title_version>
2101 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2102 <app_type type="hexBinary" length="4">90000001</app_type>
2103 <group_id type="hexBinary" length="4">00000400</group_id>
2104 <os_mask type="hexBinary" length="32">0</os_mask>
2105 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2106</app>"#
2107 .to_owned(),
2108 );
2109
2110 assert_eq!(
2111 HostFilesystem::capitilize_title_id(r#"<?xml version="1.0" encoding = "utf-8"?>
2112<app type="complex" access="777">
2113 <version type="unsignedInt" length="4">16</version>
2114 <os_version type="hexBinary" length="8">000500101000400A</os_version><title_id type="hexBinary" length="8">000500101f700500</title_id><title_version type="hexBinary" length="2">090D</title_version>
2115 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2116 <app_type type="hexBinary" length="4">90000001</app_type>
2117 <group_id type="hexBinary" length="4">00000400</group_id>
2118 <os_mask type="hexBinary" length="32">0</os_mask>
2119 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2120</app>"#.to_owned()),
2121 r#"<?xml version="1.0" encoding = "utf-8"?>
2122<app type="complex" access="777">
2123 <version type="unsignedInt" length="4">16</version>
2124 <os_version type="hexBinary" length="8">000500101000400A</os_version><title_id type="hexBinary" length="8">000500101F700500</title_id><title_version type="hexBinary" length="2">090D</title_version>
2125 <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2126 <app_type type="hexBinary" length="4">90000001</app_type>
2127 <group_id type="hexBinary" length="4">00000400</group_id>
2128 <os_mask type="hexBinary" length="32">0</os_mask>
2129 <common_id type="hexBinary" length="8">0000000000000000</common_id>
2130</app>"#.to_owned(),
2131 );
2132 }
2133}