1use crate::{
5 errors::{CatBridgeError, FSError},
6 fsemul::{
7 bsf::BootSystemFile, dlf::DiskLayoutFile, errors::FSEmulFSError, pcfs::errors::PCFSApiError,
8 },
9 TitleID,
10};
11use bytes::{Bytes, BytesMut};
12use scc::{hash_map::OccupiedEntry as CMOccupiedEntry, HashMap as ConcurrentMap};
13use std::{
14 collections::HashMap,
15 hash::RandomState,
16 io::{Error as IOError, SeekFrom},
17 path::{Path, PathBuf},
18 sync::atomic::{AtomicI32, Ordering as AtomicOrdering},
19};
20use tokio::{
21 fs::{
22 create_dir_all, read_dir, remove_file, rename, write as fs_write, File, OpenOptions,
23 ReadDir,
24 },
25 io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
26};
27use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
28use whoami::username;
29
30static DIRECTORY_FD: AtomicI32 = AtomicI32::new(1);
32
33#[derive(Debug)]
41pub struct HostFilesystem {
42 cafe_sdk_path: PathBuf,
44 open_file_handles: ConcurrentMap<i32, (File, u64, PathBuf)>,
48 open_folder_handles: ConcurrentMap<i32, (ReadDir, bool, PathBuf)>,
52}
53
54impl HostFilesystem {
55 pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
80 let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_directory) else {
81 return Err(FSEmulFSError::CantFindCafeSdkPath.into());
82 };
83
84 Self::patch_case_sensitive_title_ids(&cafe_sdk_path).await?;
85
86 if !Self::join_many(
87 &cafe_sdk_path,
88 [
89 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
90 ],
91 )
92 .exists() || !Self::join_many(
93 &cafe_sdk_path,
94 [
95 "data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
96 ],
97 )
98 .exists() || !Self::join_many(
99 &cafe_sdk_path,
100 [
101 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
102 ],
103 )
104 .exists()
105 {
106 return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
107 }
108
109 if !Self::join_many(
111 &cafe_sdk_path,
112 [
113 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
114 ],
115 )
116 .exists()
117 {
118 return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
119 }
120
121 Ok(Self {
122 cafe_sdk_path,
123 open_file_handles: ConcurrentMap::new(),
124 open_folder_handles: ConcurrentMap::new(),
125 })
126 }
127
128 #[must_use]
134 pub const fn cafe_sdk_path(&self) -> &PathBuf {
135 &self.cafe_sdk_path
136 }
137
138 pub async fn open_file(
144 &self,
145 open_options: OpenOptions,
146 path: &PathBuf,
147 ) -> Result<i32, FSError> {
148 let fd = open_options.open(path).await?;
149 let raw_fd;
150 #[cfg(unix)]
151 {
152 use std::os::fd::AsRawFd;
153 raw_fd = fd.as_raw_fd();
154 }
155 #[cfg(target_os = "windows")]
156 {
157 use std::os::windows::io::AsRawHandle;
158 raw_fd = fd.as_raw_handle() as i32;
159 }
160
161 let md = fd.metadata().await?;
162
163 self.open_file_handles
164 .insert(raw_fd, (fd, md.len(), path.clone()))
165 .map_err(|_| IOError::other("OS returned duplicate fd?"))?;
166 Ok(raw_fd)
167 }
168
169 pub async fn get_file(
173 &self,
174 fd: i32,
175 ) -> Option<CMOccupiedEntry<i32, (File, u64, PathBuf), RandomState>> {
176 self.open_file_handles.get_async(&fd).await
177 }
178
179 pub async fn file_length(&self, fd: i32) -> Option<u64> {
183 self.open_file_handles.get_async(&fd).await.map(|e| e.1)
184 }
185
186 pub async fn read_file(
197 &self,
198 fd: i32,
199 total_data_to_read: usize,
200 ) -> Result<Option<Bytes>, FSError> {
201 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
202 return Ok(None);
203 };
204 let file_reader = &mut real_entry.0;
205 let mut file_buff = BytesMut::zeroed(total_data_to_read);
206 let bytes_read = file_reader.read(&mut file_buff).await?;
207 if bytes_read < total_data_to_read {
208 file_buff[bytes_read..].fill(0xCD);
209 }
210
211 Ok(Some(file_buff.freeze()))
212 }
213
214 pub async fn write_file(&self, fd: i32, data_to_write: Bytes) -> Result<(), FSError> {
225 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
226 return Err(FSError::IO(IOError::other("file not open")));
227 };
228 let file_writer = &mut real_entry.0;
229 file_writer.write_all(&data_to_write).await?;
230
231 Ok(())
232 }
233
234 pub async fn seek_file(&self, fd: i32, begin: bool) -> Result<(), FSError> {
244 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
245 return Ok(());
246 };
247 let file_reader = &mut real_entry.0;
248
249 if begin {
250 file_reader.seek(SeekFrom::Start(0)).await?;
251 } else {
252 file_reader.seek(SeekFrom::End(0)).await?;
253 }
254
255 Ok(())
256 }
257
258 pub async fn close_file(&self, fd: i32) {
267 self.open_file_handles.remove_async(&fd).await;
268 }
269
270 pub async fn open_folder(&self, path: &PathBuf) -> Result<i32, FSError> {
279 let dhandle = read_dir(path).await?;
280 let fake_fd = DIRECTORY_FD.fetch_add(1, AtomicOrdering::SeqCst);
281 self.open_folder_handles
282 .insert(fake_fd, (dhandle, false, path.clone()))
283 .map_err(|_| IOError::other("OS returned duplicate fd?"))?;
284 Ok(fake_fd)
285 }
286
287 pub async fn next_in_folder(&self, fd: i32) -> Result<Option<(PathBuf, usize)>, FSError> {
297 let Some(mut entry) = self.open_folder_handles.get_async(&fd).await else {
298 return Ok(None);
299 };
300
301 let component_count = entry.2.components().count();
302 let mut value: Option<PathBuf> = None;
303 if !entry.1 {
304 let iter = &mut entry.0;
305 loop {
306 value = iter.next_entry().await?.map(|de| de.path());
307 if let Some(ref_value) = value.as_ref() {
308 if (!ref_value.is_file() && !ref_value.is_dir()) || ref_value.is_symlink() {
309 continue;
310 }
311 }
312 break;
313 }
314 if value.is_none() {
315 entry.1 = true;
316 }
317 }
318
319 Ok(value.map(|val| (val, component_count)))
320 }
321
322 pub async fn reverse_directory(&self, fd: i32) -> Result<(), FSError> {
332 let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
333 return Ok(());
334 };
335
336 real_entry.0 = read_dir(&real_entry.2).await?;
337 real_entry.1 = false;
338 Ok(())
339 }
340
341 pub async fn close_folder(&self, fd: i32) {
350 self.open_folder_handles.remove_async(&fd).await;
351 }
352
353 pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, FSError> {
364 let mut path = self.temp_path().await?;
365 path.push("caferun");
366 if !path.exists() {
367 create_dir_all(&path).await?;
368 }
369 path.push("ppc.bsf");
370
371 if !path.exists() {
372 fs_write(&path, Bytes::from(BootSystemFile::default())).await?;
373 }
374
375 Ok(path)
376 }
377
378 pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
388 let mut path = self.temp_path().await?;
389 path.push("caferun");
390 if !path.exists() {
391 create_dir_all(&path).await?;
392 }
393 path.push("diskid.bin");
394
395 if !path.exists() {
396 fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
397 }
398
399 Ok(path)
400 }
401
402 #[must_use]
407 pub fn firmware_file_path(&self) -> PathBuf {
408 Self::join_many(
409 &self.slc_path_for((0x0005_0010, 0x1000_400A)),
410 ["code", "fw.img"],
411 )
412 }
413
414 pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
427 let mut path = self.temp_path().await?;
428 path.push("caferun");
429 if !path.exists() {
430 create_dir_all(&path).await.map_err(FSError::from)?;
431 }
432 path.push("ppc_boot.dlf");
433
434 if !path.exists() {
435 let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
438 root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
439 root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
440 root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
441 fs_write(&path, Bytes::from(root_dlf))
442 .await
443 .map_err(FSError::from)?;
444 }
445
446 Ok(path)
447 }
448
449 pub fn path_allows_writes(&self, path: &Path) -> bool {
451 !path.to_string_lossy().contains("%DISC_EMU_DIR")
453 && !path.starts_with(Self::join_many(&self.cafe_sdk_path, ["data", "disc"]))
454 }
455
456 pub fn resolve_path(
474 &self,
475 potentially_prefixed_path: &str,
476 ) -> Result<ResolvedLocation, CatBridgeError> {
477 let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
483 if path.starts_with("/%NETWORK") {
484 todo!("NETWORK shares not yet implemented :( sorry!")
485 }
486
487 let non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
488 self.replace_emu_dir(path, "mlc")
489 } else if path.starts_with("/%SLC_EMU_DIR") {
490 self.replace_emu_dir(path, "slc")
491 } else if path.starts_with("/%DISC_EMU_DIR") {
492 self.replace_emu_dir(path, "disc")
493 } else if path.starts_with("/%SAVE_EMU_DIR") {
494 self.replace_emu_dir(path, "save")
495 } else {
496 PathBuf::from(path)
497 };
498
499 let mut closest_canonical_directory = non_canonical_path.clone();
506 let mut changed_at_all = false;
507 while !closest_canonical_directory.as_os_str().is_empty() {
508 if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
509 closest_canonical_directory = canonicalized;
510 break;
511 }
512
513 changed_at_all = true;
514 closest_canonical_directory.pop();
515 }
516 if closest_canonical_directory.as_os_str().is_empty() {
519 return Err(PCFSApiError::PathNotMapped(path.to_owned()).into());
520 }
521 let canonicalized_cafe = self
523 .cafe_sdk_path()
524 .canonicalize()
525 .unwrap_or_else(|_| self.cafe_sdk_path().clone());
526 if !closest_canonical_directory.starts_with(canonicalized_cafe) {
527 return Err(PCFSApiError::PathNotMapped(path.to_owned()).into());
528 }
529
530 Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
531 non_canonical_path,
532 closest_canonical_directory,
533 !changed_at_all,
534 )))
535 }
536
537 #[must_use]
544 pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
545 Self::join_many(
546 &self.cafe_sdk_path,
547 [
548 "data".to_owned(),
549 "slc".to_owned(),
550 "sys".to_owned(),
551 "title".to_owned(),
552 format!("{:08x}", title_id.0),
553 format!("{:08x}", title_id.1),
554 ],
555 )
556 }
557
558 async fn temp_path(&self) -> Result<PathBuf, FSError> {
565 let temp_path = Self::join_many(
566 &self.cafe_sdk_path,
567 ["temp".to_owned(), username().to_lowercase()],
568 );
569 if !temp_path.exists() {
570 create_dir_all(&temp_path).await?;
571 }
572 Ok(temp_path)
573 }
574
575 #[must_use]
577 fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
578 where
579 PathTy: AsRef<Path>,
580 IterTy: IntoIterator<Item = PathTy>,
581 {
582 let mut as_owned = PathBuf::from(base);
583 for part in parts {
584 as_owned = as_owned.join(part.as_ref());
585 }
586 as_owned
587 }
588
589 fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
591 let path_minus = path
592 .trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
593 .trim_start_matches('/')
594 .trim_start_matches('\\')
595 .replace('\\', "/");
596
597 Self::join_many(
598 &Self::join_many(self.cafe_sdk_path(), ["data", dir]),
599 path_minus.split('/'),
600 )
601 }
602
603 #[allow(
608 unreachable_code,
610 )]
611 #[must_use]
612 pub fn default_cafe_directory() -> Option<PathBuf> {
613 #[cfg(target_os = "windows")]
614 {
615 return Some(PathBuf::from(r"C:\cafe_sdk"));
616 }
617
618 #[cfg(any(
619 target_os = "linux",
620 target_os = "freebsd",
621 target_os = "openbsd",
622 target_os = "netbsd",
623 target_os = "macos"
624 ))]
625 {
626 return Some(PathBuf::from("/opt/cafe_sdk"));
627 }
628
629 None
630 }
631
632 async fn patch_case_sensitive_title_ids(cafe_sdk_path: &Path) -> Result<(), FSError> {
633 if !cafe_sdk_path.exists() {
635 return Ok(());
636 }
637 let capital_path = Self::join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
638 let _ = File::create(&capital_path).await?;
639 let is_insensitive = File::open(Self::join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
640 .await
641 .is_ok();
642 remove_file(capital_path).await?;
643 if is_insensitive {
644 return Ok(());
645 }
646
647 for directory in [
648 Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
649 Self::join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
650 Self::join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
651 Self::join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
652 ] {
653 if !directory.exists() {
654 continue;
656 }
657
658 let mut iter = read_dir(&directory).await?;
661 let lossy_cafe_dir = cafe_sdk_path.as_os_str().to_string_lossy().to_string();
662 while let Ok(Some(entry)) = iter.next_entry().await {
663 let p = entry.path();
664 if !p.is_dir() || !p.exists() {
665 continue;
666 }
667
668 let mut inner_iter = read_dir(&p).await?;
669 while let Ok(Some(inner_entry)) = inner_iter.next_entry().await {
670 let ip = inner_entry.path();
671 if !ip.is_dir() || !ip.exists() {
672 continue;
673 }
674
675 let new_path = ip
677 .as_os_str()
678 .to_string_lossy()
679 .trim_start_matches(&lossy_cafe_dir)
680 .to_ascii_lowercase();
681 if ip
682 .as_os_str()
683 .to_string_lossy()
684 .trim_start_matches(&lossy_cafe_dir)
685 != new_path
686 {
687 let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
688 final_new_path.push(&new_path);
689 let new = PathBuf::from(final_new_path);
690 rename(ip, new).await?;
691 }
692 }
693
694 let new_path = p
695 .as_os_str()
696 .to_string_lossy()
697 .trim_start_matches(&lossy_cafe_dir)
698 .to_ascii_lowercase();
699 if p.as_os_str()
700 .to_string_lossy()
701 .trim_start_matches(&lossy_cafe_dir)
702 != new_path
703 {
704 let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
705 final_new_path.push(&new_path);
706 rename(p, final_new_path).await?;
707 }
708 }
709 }
710
711 Ok(())
712 }
713}
714
715const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
716 NamedField::new("cafe_sdk_path"),
717 NamedField::new("open_file_handles"),
718 NamedField::new("open_folder_handles"),
719];
720
721impl Structable for HostFilesystem {
722 fn definition(&self) -> StructDef<'_> {
723 StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
724 }
725}
726
727impl Valuable for HostFilesystem {
728 fn as_value(&self) -> Value<'_> {
729 Value::Structable(self)
730 }
731
732 fn visit(&self, visitor: &mut dyn Visit) {
733 let mut values = HashMap::with_capacity(self.open_file_handles.len());
734 self.open_file_handles.scan(|k, v| {
735 values.insert(*k, format!("{}", v.2.display()));
736 });
737 let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
738 self.open_folder_handles.scan(|k, v| {
739 folder_values.insert(*k, format!("{}", v.2.display()));
740 });
741
742 visitor.visit_named_fields(&NamedValues::new(
743 HOST_FILESYSTEM_FIELDS,
744 &[
745 Valuable::as_value(&self.cafe_sdk_path),
746 Valuable::as_value(&values),
747 Valuable::as_value(&folder_values),
748 ],
749 ));
750 }
751}
752
753#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
755pub enum ResolvedLocation {
756 Filesystem(FilesystemLocation),
762 Network(()),
766}
767
768#[derive(Clone, Debug, PartialEq, Eq)]
771pub struct FilesystemLocation {
772 resolved_path: PathBuf,
774 closest_resolved_path: PathBuf,
777 canonicalized_is_exact: bool,
779}
780impl FilesystemLocation {
781 #[must_use]
782 pub const fn new(
783 resolved_path: PathBuf,
784 closest_resolved_path: PathBuf,
785 canonicalized_is_exact: bool,
786 ) -> Self {
787 Self {
788 resolved_path,
789 closest_resolved_path,
790 canonicalized_is_exact,
791 }
792 }
793
794 #[must_use]
795 pub const fn resolved_path(&self) -> &PathBuf {
796 &self.resolved_path
797 }
798 #[must_use]
799 pub const fn closest_resolved_path(&self) -> &PathBuf {
800 &self.closest_resolved_path
801 }
802 #[must_use]
803 pub const fn canonicalized_is_exact(&self) -> bool {
804 self.canonicalized_is_exact
805 }
806}
807
808const FILESYSTEM_LOCATION_FIELDS: &[NamedField<'static>] = &[
809 NamedField::new("resolved_path"),
810 NamedField::new("closest_resolved_path"),
811 NamedField::new("canonicalized_is_exact"),
812];
813
814impl Structable for FilesystemLocation {
815 fn definition(&self) -> StructDef<'_> {
816 StructDef::new_static(
817 "FilesystemLocation",
818 Fields::Named(FILESYSTEM_LOCATION_FIELDS),
819 )
820 }
821}
822
823impl Valuable for FilesystemLocation {
824 fn as_value(&self) -> Value<'_> {
825 Value::Structable(self)
826 }
827
828 fn visit(&self, visitor: &mut dyn Visit) {
829 visitor.visit_named_fields(&NamedValues::new(
830 FILESYSTEM_LOCATION_FIELDS,
831 &[
832 Valuable::as_value(&self.resolved_path),
833 Valuable::as_value(&self.closest_resolved_path),
834 Valuable::as_value(&self.canonicalized_is_exact),
835 ],
836 ));
837 }
838}
839
840#[cfg(test)]
841pub mod test_helpers {
842 use super::*;
843 use std::fs::{create_dir_all, File};
844 use tempfile::{tempdir, TempDir};
845
846 pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
848 let dir = tempdir().expect("Failed to create temporary directory!");
849
850 for directory_to_create in vec![
851 vec!["data", "slc"],
853 vec!["data", "mlc"],
854 vec!["data", "disc"],
855 vec!["data", "save"],
856 vec![
858 "data", "mlc", "sys", "title", "00050030", "1001000a", "code",
859 ],
860 vec![
862 "data", "mlc", "sys", "title", "00050030", "1001010A", "code",
863 ],
864 vec![
865 "data", "mlc", "sys", "title", "00050030", "1001020a", "code",
866 ],
867 vec![
868 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
869 ],
870 ] {
871 create_dir_all(HostFilesystem::join_many(dir.path(), directory_to_create))
872 .expect("Failed to create directories necessary for host filesystem to work.");
873 }
874
875 File::create(HostFilesystem::join_many(
878 dir.path(),
879 [
880 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
881 ],
882 ))
883 .expect("Failed to create needed app.xml!");
884 File::create(HostFilesystem::join_many(
885 dir.path(),
886 [
887 "data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
888 ],
889 ))
890 .expect("Failed to create needed app.xml!");
891 File::create(HostFilesystem::join_many(
892 dir.path(),
893 [
894 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
895 ],
896 ))
897 .expect("Failed to create needed app.xml!");
898
899 File::create(HostFilesystem::join_many(
900 dir.path(),
901 [
902 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
903 ],
904 ))
905 .expect("Failed to create needed fw.img!");
906
907 let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
908 .await
909 .expect("Failed to load empty host filesystem!");
910
911 (dir, fs)
912 }
913
914 #[must_use]
916 pub fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
917 where
918 PathTy: AsRef<Path>,
919 IterTy: IntoIterator<Item = PathTy>,
920 {
921 HostFilesystem::join_many(base, parts)
922 }
923}
924
925#[cfg(test)]
926mod unit_tests {
927 use super::test_helpers::*;
928 use super::*;
929 use std::fs::read;
930
931 fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
932
933 #[test]
934 pub fn is_send_sync() {
935 only_accepts_send_sync::<HostFilesystem>(None);
936 }
937
938 #[test]
939 pub fn can_find_default_cafe_directory() {
940 assert!(
941 HostFilesystem::default_cafe_directory().is_some(),
942 "Failed to find default cafe directory for your OS",
943 );
944 }
945
946 #[tokio::test]
947 pub async fn creatable_files() {
948 let (tempdir, fs) = create_temporary_host_filesystem().await;
951
952 let expected_bsf_path = HostFilesystem::join_many(
953 tempdir.path(),
954 [
955 "temp".to_owned(),
956 username(),
957 "caferun".to_owned(),
958 "ppc.bsf".to_owned(),
959 ],
960 );
961 assert!(
962 !expected_bsf_path.exists(),
963 "ppc.bsf existed before we asked for it?"
964 );
965 let bsf_path = fs
966 .boot1_sytstem_path()
967 .await
968 .expect("Failed to create bsf!");
969 assert_eq!(expected_bsf_path, bsf_path);
970 assert!(
971 BootSystemFile::try_from(Bytes::from(
972 read(bsf_path).expect("Failed to read written boot system file!")
973 ))
974 .is_ok(),
975 "Failed to read generated boot system file!"
976 );
977
978 let expected_diskid_path = HostFilesystem::join_many(
979 tempdir.path(),
980 [
981 "temp".to_owned(),
982 username(),
983 "caferun".to_owned(),
984 "diskid.bin".to_owned(),
985 ],
986 );
987 assert!(
988 !expected_diskid_path.exists(),
989 "diskid.bin existed before we asked for it?"
990 );
991 let diskid_path = fs
992 .disk_id_path()
993 .await
994 .expect("Failed to create diskid.bin!");
995 assert_eq!(expected_diskid_path, diskid_path);
996 assert_eq!(
997 read(diskid_path).expect("Failed to read written diskid.bin!"),
998 vec![0; 32],
999 "Failed to read generated diskid.bin!"
1000 );
1001
1002 assert_eq!(
1004 fs.firmware_file_path(),
1005 HostFilesystem::join_many(
1006 tempdir.path(),
1007 ["data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"],
1008 ),
1009 );
1010
1011 let expected_ppc_boot_dlf_path = HostFilesystem::join_many(
1012 tempdir.path(),
1013 [
1014 "temp".to_owned(),
1015 username(),
1016 "caferun".to_owned(),
1017 "ppc_boot.dlf".to_owned(),
1018 ],
1019 );
1020 assert!(
1021 !expected_ppc_boot_dlf_path.exists(),
1022 "ppc_boot.dlf existed before we asked for it?"
1023 );
1024 let ppc_boot_dlf_path = fs
1025 .ppc_boot_dlf_path()
1026 .await
1027 .expect("Failed to create ppc_boot.dlf!");
1028 assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
1029 assert!(
1030 DiskLayoutFile::try_from(Bytes::from(
1031 read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
1032 ))
1033 .is_ok(),
1034 "Failed to read generated ppc_boot.dlf!"
1035 );
1036 }
1037
1038 #[tokio::test]
1039 pub async fn path_allows_writes() {
1040 let (_tempdir, fs) = create_temporary_host_filesystem().await;
1041
1042 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
1045 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
1046 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
1047 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1048 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1049 }
1050
1051 #[tokio::test]
1052 pub async fn resolve_path() {
1053 let (tempdir, fs) = create_temporary_host_filesystem().await;
1056
1057 for (dir, name) in [
1059 ("/%MLC_EMU_DIR", "mlc"),
1060 ("/%SLC_EMU_DIR", "slc"),
1061 ("/%DISC_EMU_DIR", "disc"),
1062 ("/%SAVE_EMU_DIR", "save"),
1063 ] {
1064 assert!(
1065 fs.resolve_path(&format!("{dir}")).is_ok(),
1066 "Failed to resolve: `{}`: {:?}",
1067 dir,
1068 fs.resolve_path(&format!("{dir}"))
1069 );
1070 assert!(
1071 fs.resolve_path(&format!("{dir}/")).is_ok(),
1072 "Failed to resolve: `{}/`",
1073 dir,
1074 );
1075 assert!(
1076 fs.resolve_path(&format!("{dir}/./")).is_ok(),
1077 "Failed to resolve: `{}/./`",
1078 dir,
1079 );
1080 assert!(
1081 fs.resolve_path(&format!("{dir}/../{name}")).is_ok(),
1082 "Failed to resolve: `{}/../{}`",
1083 dir,
1084 name,
1085 );
1086 }
1087
1088 let mut out_of_path = PathBuf::from(tempdir.path());
1090 out_of_path.pop();
1093
1094 assert!(fs
1096 .resolve_path(
1097 &out_of_path
1098 .clone()
1099 .into_os_string()
1100 .into_string()
1101 .expect("Failed to convert pathbuf to string!")
1102 )
1103 .is_err());
1104 assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").is_err());
1105
1106 #[cfg(unix)]
1107 {
1108 use std::os::unix::fs::symlink;
1109
1110 let mut tempdir_symlink = PathBuf::from(tempdir.path());
1111 tempdir_symlink.push("symlink");
1112 symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1113 assert!(fs
1114 .resolve_path(&format!(
1115 "{}/symlink",
1116 tempdir_symlink
1117 .into_os_string()
1118 .into_string()
1119 .expect("tempdir symlink wasn't utf8?"),
1120 ))
1121 .is_err());
1122 }
1123
1124 #[cfg(target_os = "windows")]
1125 {
1126 use std::os::windows::fs::symlink_dir;
1127
1128 let mut tempdir_symlink = PathBuf::from(tempdir.path());
1129 tempdir_symlink.push("symlink");
1130 symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1131 assert!(fs
1132 .resolve_path(&format!(
1133 "{}/symlink",
1134 tempdir_symlink
1135 .into_os_string()
1136 .into_string()
1137 .expect("tempdir symlink wasn't utf8?"),
1138 ))
1139 .is_err());
1140 }
1141 }
1142
1143 #[tokio::test]
1144 pub async fn opening_files() {
1145 let (tempdir, fs) = create_temporary_host_filesystem().await;
1146 let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1147 tokio::fs::write(path.clone(), vec![0; 1307])
1148 .await
1149 .expect("Failed to write test file!");
1150 let create_path = HostFilesystem::join_many(tempdir.path(), ["new-file.txt"]);
1151
1152 let mut oo = OpenOptions::new();
1153 oo.create(false).write(true).read(true);
1154 assert!(
1155 fs.open_file(oo, &create_path).await.is_err(),
1156 "Somehow succeeding opening a file that doesn't exist with no create flag?",
1157 );
1158 oo = OpenOptions::new();
1159 oo.create(true).write(true).truncate(true);
1160 let fd = fs
1161 .open_file(oo, &create_path)
1162 .await
1163 .expect("Failed opening a file that doesn't exist with a create flag?");
1164 assert!(
1165 fs.open_file_handles.len() == 1 && fs.open_file_handles.get(&fd).is_some(),
1166 "Open file wasn't in open files list!",
1167 );
1168 fs.close_file(fd).await;
1169 assert!(
1170 fs.open_file_handles.is_empty(),
1171 "Somehow after opening/closing, open file handles was not empty?",
1172 );
1173 }
1174
1175 #[tokio::test]
1176 pub async fn seek_and_read() {
1177 let (tempdir, fs) = create_temporary_host_filesystem().await;
1178 let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1179 tokio::fs::write(path.clone(), vec![0; 1307])
1180 .await
1181 .expect("Failed to write test file!");
1182
1183 let mut oo = OpenOptions::new();
1184 oo.read(true).create(false).write(false);
1185 let fd = fs
1186 .open_file(oo, &path)
1187 .await
1188 .expect("Failed to open existing file!");
1189
1190 assert_eq!(
1192 Some(BytesMut::zeroed(1307).freeze()),
1193 fs.read_file(fd, 1307)
1194 .await
1195 .expect("Failed to read from FD!"),
1196 );
1197 fs.seek_file(fd, true)
1198 .await
1199 .expect("Failed to sync to beginning of file!");
1200 assert_eq!(
1202 Some(BytesMut::zeroed(1307).freeze()),
1203 fs.read_file(fd, 1307)
1204 .await
1205 .expect("Failed to read from FD!"),
1206 );
1207 fs.close_file(fd).await;
1208 assert!(
1209 fs.open_file_handles.is_empty(),
1210 "Somehow after opening/closing, open file handles was not empty?",
1211 );
1212 }
1213
1214 #[tokio::test]
1215 pub async fn open_and_close_folder() {
1216 let (tempdir, fs) = create_temporary_host_filesystem().await;
1217 let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1218 tokio::fs::create_dir_all(path.clone())
1219 .await
1220 .expect("Failed to create test directory!");
1221
1222 let fd = fs
1223 .open_folder(&path)
1224 .await
1225 .expect("Failed to open existing folder!");
1226 assert!(
1227 fs.open_folder_handles.len() == 1,
1228 "Expected one open folder handle",
1229 );
1230 fs.close_folder(fd).await;
1231
1232 assert!(
1233 fs.open_folder_handles.is_empty(),
1234 "Somehow after opening/closing, open folder handles was not empty?",
1235 );
1236 }
1237
1238 #[tokio::test]
1239 pub async fn seek_within_folder() {
1240 let (tempdir, fs) = create_temporary_host_filesystem().await;
1241 let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1242 tokio::fs::create_dir_all(path.clone())
1243 .await
1244 .expect("Failed to create test directory!");
1245
1246 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["c"]))
1251 .await
1252 .expect("Failed to create file to use!");
1253 tokio::fs::create_dir(HostFilesystem::join_many(&path, ["d"]))
1254 .await
1255 .expect("Failed to create directory to use!");
1256 #[cfg(unix)]
1257 {
1258 use std::os::unix::fs::symlink;
1259
1260 let mut tempdir_symlink = path.clone();
1261 tempdir_symlink.push("e");
1262 symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1263 }
1264 #[cfg(target_os = "windows")]
1265 {
1266 use std::os::windows::fs::symlink_dir;
1267
1268 let mut tempdir_symlink = path.clone();
1269 tempdir_symlink.push("e");
1270 symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1271 }
1272 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["f"]))
1273 .await
1274 .expect("Failed to create file to use!");
1275 _ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["d", "a"]))
1276 .await
1277 .expect("Failed to create file to use!");
1278
1279 let dfd = fs.open_folder(&path).await.expect("Failed to open file!");
1280 assert!(fs
1281 .next_in_folder(dfd)
1282 .await
1283 .expect("Failed to query for next in folder! 1.1!")
1284 .is_some());
1285 assert!(fs
1286 .next_in_folder(dfd)
1287 .await
1288 .expect("Failed to query for next in folder! 1.2!")
1289 .is_some());
1290 assert!(fs
1291 .next_in_folder(dfd)
1292 .await
1293 .expect("Failed to query for next in folder! 1.3!")
1294 .is_some());
1295 assert!(fs
1297 .next_in_folder(dfd)
1298 .await
1299 .expect("Failed to query for next in folder! 1.4!")
1300 .is_none());
1301 assert!(fs
1303 .next_in_folder(dfd)
1304 .await
1305 .expect("Failed to query for next in folder! 1.5!")
1306 .is_none());
1307 fs.reverse_directory(dfd)
1309 .await
1310 .expect("Failed to reverse directory search!");
1311 assert!(fs
1312 .next_in_folder(dfd)
1313 .await
1314 .expect("Failed to query for next in folder! 2.1!")
1315 .is_some());
1316 assert!(fs
1317 .next_in_folder(dfd)
1318 .await
1319 .expect("Failed to query for next in folder! 2.2!")
1320 .is_some());
1321 assert!(fs
1322 .next_in_folder(dfd)
1323 .await
1324 .expect("Failed to query for next in folder! 2.3!")
1325 .is_some());
1326 }
1327}