1mod open_file;
5mod open_folder;
6mod utilities;
7
8use crate::{
9 errors::{CatBridgeError, FSError},
10 fsemul::{
11 bsf::BootSystemFile,
12 dlf::DiskLayoutFile,
13 errors::{FSEmulAPIError, FSEmulFSError},
14 filesystem::{
15 FilesystemLocation, ItemInFolder, ResolvedLocation,
16 host::{
17 open_folder::DirectoryListing,
18 utilities::{get_new_unique_folder_fd, join_many},
19 },
20 },
21 pcfs::errors::PcfsApiError,
22 },
23};
24use bytes::{Bytes, BytesMut};
25use sachet::title::TitleID;
26use scc::{
27 HashMap as ConcurrentMap, HashSet as ConcurrentSet, hash_map::OccupiedEntry as CMOccupiedEntry,
28};
29use std::{
30 collections::HashMap,
31 ffi::{OsStr, OsString},
32 fs::{
33 copy as copy_file_sync, create_dir_all as create_dir_all_sync, read_link as read_link_sync,
34 remove_dir_all as remove_dir_all_sync, remove_file as remove_file_sync,
35 rename as rename_sync,
36 },
37 hash::RandomState,
38 io::{Error as IOError, SeekFrom},
39 path::{Component, Path, PathBuf},
40 sync::{
41 Arc,
42 atomic::{AtomicBool, Ordering as AtomicOrdering},
43 },
44};
45use tokio::{
46 fs::{File, OpenOptions, create_dir_all, remove_dir_all, write as fs_write},
47 io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
48 sync::Mutex,
49};
50use tracing::{info, warn};
51use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
52use walkdir::WalkDir;
53use whoami::username;
54
55pub use self::open_file::OpenFileHandle;
56
57#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
58#[cfg(feature = "nus")]
59use crate::fsemul::filesystem::nus_fuse::NUSFuse;
60#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
61#[cfg(feature = "nus")]
62use std::str::FromStr;
63
64#[derive(Clone, Debug)]
72pub struct HostFilesystem {
73 cafe_sdk_path: PathBuf,
75 create_save_directories: bool,
78 disc_mounted: Arc<Mutex<Option<(bool, bool, TitleID)>>>,
85 #[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
86 #[cfg(feature = "nus")]
87 nus: Option<NUSFuse>,
89 open_file_handles: Arc<ConcurrentMap<i32, OpenFileHandle>>,
91 open_folder_handles: Arc<ConcurrentMap<i32, DirectoryListing>>,
93 folders_marked_read_only: Arc<ConcurrentSet<PathBuf>>,
103 is_using_unique_fds: bool,
106 has_opened_file: Arc<AtomicBool>,
109}
110
111impl HostFilesystem {
112 pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
137 let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
138 return Err(FSEmulFSError::CantFindCafeSdkPath.into());
139 };
140
141 Self::patch_case_sensitivity(&cafe_sdk_path).await?;
142
143 for path in [
144 &[
145 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
146 ] as &[&str],
147 &[
148 "data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
149 ],
150 &[
151 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
152 ],
153 &[
154 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
155 ],
156 &[
157 "data", "mlc", "sys", "title", "00050010", "1f700500", "content",
158 ],
159 &[
160 "data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
161 ],
162 &[
164 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
165 ],
166 ] {
167 if !join_many(&cafe_sdk_path, path).exists() {
168 return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
169 }
170 }
171
172 Self::prepare_for_serving(&cafe_sdk_path).await?;
173 let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
174
175 Ok(Self {
176 cafe_sdk_path,
177 create_save_directories: false,
178 disc_mounted: Arc::new(Mutex::new(None)),
179 #[cfg(feature = "nus")]
180 nus: None,
181 folders_marked_read_only: Arc::new(ro_folders),
182 open_file_handles: Arc::new(ConcurrentMap::new()),
183 open_folder_handles: Arc::new(ConcurrentMap::new()),
184 is_using_unique_fds: false,
185 has_opened_file: Arc::new(AtomicBool::new(false)),
186 })
187 }
188
189 #[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
190 #[cfg(feature = "nus")]
191 pub async fn from_cafe_dir_and_nus(
217 cafe_dir: Option<PathBuf>,
218 nus: Option<NUSFuse>,
219 ) -> Result<Self, CatBridgeError> {
220 let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
221 return Err(FSEmulFSError::CantFindCafeSdkPath.into());
222 };
223
224 Self::patch_case_sensitivity(&cafe_sdk_path).await?;
225
226 for path in [
227 &[
228 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
229 ] as &[&str],
230 &[
231 "data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
232 ],
233 &[
234 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
235 ],
236 &[
237 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
238 ],
239 &[
240 "data", "mlc", "sys", "title", "00050010", "1f700500", "content",
241 ],
242 &[
243 "data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
244 ],
245 &[
247 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
248 ],
249 ] {
250 if !join_many(&cafe_sdk_path, path).exists() {
251 return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
252 }
253 }
254
255 Self::prepare_for_serving(&cafe_sdk_path).await?;
256 let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
257
258 Ok(Self {
259 cafe_sdk_path,
260 create_save_directories: false,
261 disc_mounted: Arc::new(Mutex::new(None)),
262 #[cfg(feature = "nus")]
263 nus,
264 folders_marked_read_only: Arc::new(ro_folders),
265 open_file_handles: Arc::new(ConcurrentMap::new()),
266 open_folder_handles: Arc::new(ConcurrentMap::new()),
267 is_using_unique_fds: false,
268 has_opened_file: Arc::new(AtomicBool::new(false)),
269 })
270 }
271
272 pub const fn allow_save_directory_creation(&mut self, allow: bool) {
279 self.create_save_directories = allow;
280 }
281
282 #[must_use]
288 pub const fn cafe_sdk_path(&self) -> &PathBuf {
289 &self.cafe_sdk_path
290 }
291
292 #[must_use]
298 pub fn disc_emu_path(&self) -> PathBuf {
299 join_many(&self.cafe_sdk_path, ["data", "disc"])
300 }
301
302 pub fn force_unique_fds(&mut self) -> Result<(), FSEmulAPIError> {
321 if self.has_opened_file.load(AtomicOrdering::Relaxed) {
322 return Err(FSEmulAPIError::CannotSwapFdStrategy);
323 }
324 #[cfg(feature = "nus")]
325 if self.nus.is_some() {
326 return Err(FSEmulAPIError::CannotSwapFdStrategy);
327 }
328
329 self.is_using_unique_fds = true;
330 Ok(())
331 }
332
333 #[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
334 #[cfg(feature = "nus")]
335 pub fn set_nus_provider(&mut self, nus: Option<NUSFuse>) -> Result<(), FSEmulAPIError> {
342 if self.has_opened_file.load(AtomicOrdering::Relaxed)
343 && !self.is_using_unique_fds
344 && nus.is_some()
345 {
346 return Err(FSEmulAPIError::CannotSetNUS);
347 }
348
349 if nus.is_some() {
350 self.is_using_unique_fds = true;
351 }
352 self.nus = nus;
353 Ok(())
354 }
355
356 #[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
357 #[cfg(feature = "nus")]
358 #[must_use]
359 pub fn get_nus_provider(&self) -> Option<&NUSFuse> {
361 self.nus.as_ref()
362 }
363
364 pub async fn open_file(
370 &self,
371 open_options: OpenOptions,
372 path: &Path,
373 stream_owner: Option<u64>,
374 ) -> Result<i32, FSError> {
375 self.has_opened_file.store(true, AtomicOrdering::Relaxed);
376 let handle = OpenFileHandle::open_file(
377 self.is_using_unique_fds,
378 open_options,
379 path,
380 stream_owner,
381 #[cfg(feature = "nus")]
382 &join_many(self.cafe_sdk_path(), ["data", "mlc", "usr", "title"]),
383 #[cfg(feature = "nus")]
384 self.nus.as_ref(),
385 )
386 .await?;
387
388 let fd = handle.fd();
389 self.open_file_handles
390 .insert_async(fd, handle)
391 .await
392 .map_err(|_| IOError::other("somehow got duplicate fd?"))?;
393 Ok(fd)
394 }
395
396 pub async fn get_file(
400 &self,
401 fd: i32,
402 for_stream: Option<u64>,
403 ) -> Option<CMOccupiedEntry<'_, i32, OpenFileHandle, RandomState>> {
404 self.open_file_handles
405 .get_async(&fd)
406 .await
407 .and_then(|entry| {
408 if Self::allow_access(entry.stream_owner(), for_stream) {
409 Some(entry)
410 } else {
411 None
412 }
413 })
414 }
415
416 pub async fn file_length(&self, fd: i32, for_stream: Option<u64>) -> Option<u64> {
420 self.open_file_handles
421 .get_async(&fd)
422 .await
423 .and_then(|entry| {
424 if Self::allow_access(entry.stream_owner(), for_stream) {
425 Some(entry.file_size())
426 } else {
427 None
428 }
429 })
430 }
431
432 pub async fn read_file(
443 &self,
444 fd: i32,
445 mut total_data_to_read: usize,
446 for_stream: Option<u64>,
447 ) -> Result<Option<Bytes>, CatBridgeError> {
448 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
449 return Ok(None);
450 };
451 if !Self::allow_access(real_entry.stream_owner(), for_stream) {
452 return Ok(None);
453 }
454 let file_reader = real_entry
455 .force_file_handle(
456 #[cfg(feature = "nus")]
457 self.nus.as_ref(),
458 )
459 .await?;
460
461 let mut file_buff = BytesMut::zeroed(total_data_to_read);
462 let mut total_bytes_read = 0_usize;
463 while total_data_to_read > 0 {
464 let bytes_read = file_reader
465 .read(&mut file_buff[total_bytes_read..])
466 .await
467 .map_err(FSError::IO)?;
468 if bytes_read == 0 {
469 break;
470 }
471 total_data_to_read -= bytes_read;
472 total_bytes_read += bytes_read;
473 }
474 if file_buff.len() > total_bytes_read {
475 file_buff.truncate(total_bytes_read);
476 }
477
478 Ok(Some(file_buff.freeze()))
479 }
480
481 pub async fn write_file(
492 &self,
493 fd: i32,
494 data_to_write: Bytes,
495 for_stream: Option<u64>,
496 ) -> Result<(), CatBridgeError> {
497 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
498 return Err(FSError::IO(IOError::other("file not open")).into());
499 };
500 if !Self::allow_access(real_entry.stream_owner(), for_stream) {
501 return Err(FSError::IO(IOError::other("file not open")).into());
502 }
503 let file_writer = real_entry
504 .force_file_handle(
505 #[cfg(feature = "nus")]
506 self.nus.as_ref(),
507 )
508 .await?;
509 file_writer
510 .write_all(&data_to_write)
511 .await
512 .map_err(FSError::IO)?;
513
514 Ok(())
515 }
516
517 pub async fn get_file_position(
527 &self,
528 fd: i32,
529 for_stream: Option<u64>,
530 ) -> Result<u64, CatBridgeError> {
531 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
532 return Ok(0);
533 };
534 if !Self::allow_access(real_entry.stream_owner(), for_stream) {
535 return Ok(0);
536 }
537 let file_reader = real_entry
538 .force_file_handle(
539 #[cfg(feature = "nus")]
540 self.nus.as_ref(),
541 )
542 .await?;
543
544 Ok(file_reader.stream_position().await.map_err(FSError::IO)?)
545 }
546
547 pub async fn seek_file(
557 &self,
558 fd: i32,
559 from_begin: bool,
560 offset: u64,
561 for_stream: Option<u64>,
562 ) -> Result<(), CatBridgeError> {
563 let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
564 return Ok(());
565 };
566 if !Self::allow_access(real_entry.stream_owner(), for_stream) {
567 return Ok(());
568 }
569 let file_reader = real_entry
570 .force_file_handle(
571 #[cfg(feature = "nus")]
572 self.nus.as_ref(),
573 )
574 .await?;
575
576 if from_begin {
577 file_reader
578 .seek(SeekFrom::Start(offset))
579 .await
580 .map_err(FSError::IO)?;
581 } else {
582 file_reader
583 .seek(SeekFrom::End(i64::try_from(offset).unwrap_or(i64::MAX)))
584 .await
585 .map_err(FSError::IO)?;
586 }
587
588 Ok(())
589 }
590
591 pub async fn close_file(&self, fd: i32, for_stream: Option<u64>) {
600 if let Some(entry) = self.open_file_handles.get_async(&fd).await
601 && !Self::allow_access(entry.stream_owner(), for_stream)
602 {
603 return;
605 }
606
607 self.open_file_handles.remove_async(&fd).await;
608 }
609
610 pub async fn open_folder(&self, path: &Path, for_stream: Option<u64>) -> Result<i32, FSError> {
619 let listing = DirectoryListing::new(
620 path,
621 self.mlc_path(),
622 for_stream,
623 #[cfg(feature = "nus")]
624 self.nus.as_ref(),
625 )
626 .await?;
627 let fake_fd = get_new_unique_folder_fd();
628
629 self.open_folder_handles
630 .insert_sync(fake_fd, listing)
631 .map_err(|_| IOError::other("OS returned duplicate fd?"))?;
632 Ok(fake_fd)
633 }
634
635 pub async fn mark_folder_read_only(&self, path: PathBuf) {
641 _ = self.folders_marked_read_only.insert_async(path).await;
642 }
643
644 pub async fn ensure_folder_not_read_only(&self, path: &PathBuf) {
646 self.folders_marked_read_only.remove_async(path).await;
647 }
648
649 pub async fn folder_is_read_only(&self, path: &PathBuf) -> bool {
651 self.folders_marked_read_only.contains_async(path).await
652 }
653
654 #[must_use]
660 pub async fn next_in_folder(&self, fd: i32, for_stream: Option<u64>) -> Option<ItemInFolder> {
661 let mut entry = self.open_folder_handles.get_async(&fd).await?;
662 if !Self::allow_access(entry.stream_owner(), for_stream) {
663 return None;
664 }
665
666 entry.next_item()
667 }
668
669 pub async fn reverse_folder(&self, fd: i32, for_stream: Option<u64>) {
679 let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
680 return;
681 };
682 if !Self::allow_access(real_entry.stream_owner(), for_stream) {
683 return;
684 }
685 real_entry.reverse_folder();
686 }
687
688 pub async fn close_folder(&self, fd: i32, for_stream: Option<u64>) {
697 if let Some(real_entry) = self.open_folder_handles.get_async(&fd).await
698 && !Self::allow_access(real_entry.stream_owner(), for_stream)
699 {
700 return;
701 }
702
703 self.open_folder_handles.remove_async(&fd).await;
704 }
705
706 pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, CatBridgeError> {
717 let mut path = self.temp_path()?;
718 path.push("caferun");
719 if !path.exists() {
720 create_dir_all(&path).await.map_err(FSError::IO)?;
721 }
722 path.push("ppc.bsf");
723
724 if !path.exists() {
725 fs_write(&path, Bytes::try_from(BootSystemFile::default())?)
726 .await
727 .map_err(FSError::IO)?;
728 }
729
730 Ok(path)
731 }
732
733 pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
743 let mut path = self.temp_path()?;
744 path.push("caferun");
745 if !path.exists() {
746 create_dir_all(&path).await?;
747 }
748 path.push("diskid.bin");
749
750 if !path.exists() {
751 fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
752 }
753
754 Ok(path)
755 }
756
757 #[doc(
758 hidden,
760 )]
761 pub async fn mount_disk_title(
768 &mut self,
769 is_slc: bool,
770 is_sys: bool,
771 title_id: TitleID,
772 ) -> Result<(), FSError> {
773 let dest_path = join_many(&self.cafe_sdk_path, ["data", "disc"]);
775 if dest_path.exists() {
776 remove_dir_all(&dest_path).await.map_err(FSError::IO)?;
777 }
778 _ = self.disk_id_path().await?;
780
781 {
783 let mut guard = self.disc_mounted.lock().await;
784 guard.replace((is_slc, is_sys, title_id));
785 }
786
787 Ok(())
788 }
789
790 #[must_use]
795 pub fn firmware_file_path(&self) -> PathBuf {
796 join_many(
797 &self.slc_path_for(TitleID::new(0x0005_0010_1000_400A)),
798 ["code", "fw.img"],
799 )
800 }
801
802 pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
815 let mut path = self.temp_path()?;
816 path.push("caferun");
817 if !path.exists() {
818 create_dir_all(&path).await.map_err(FSError::from)?;
819 }
820 path.push("ppc_boot.dlf");
821
822 if !path.exists() {
823 let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
826 root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
827 root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
828 root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
829 fs_write(&path, Bytes::from(root_dlf))
830 .await
831 .map_err(FSError::from)?;
832 }
833
834 Ok(path)
835 }
836
837 #[must_use]
839 pub fn path_allows_writes(&self, path: &Path) -> bool {
840 let lossy_path = path.to_string_lossy();
842 let trimmed_lossy_path = lossy_path
843 .trim_start_matches("/vol/pc")
844 .trim_start_matches('/');
845 if trimmed_lossy_path.starts_with("%DISC_EMU_DIR") {
846 return trimmed_lossy_path.starts_with("%DISC_EMU_DIR/save");
847 }
848 if path.starts_with(join_many(&self.cafe_sdk_path, ["data", "disc"])) {
849 return path.starts_with(join_many(&self.cafe_sdk_path, ["data", "disc", "save"]));
850 }
851
852 true
853 }
854
855 pub async fn resolve_path(
873 &self,
874 potentially_prefixed_path: &str,
875 ) -> Result<ResolvedLocation, CatBridgeError> {
876 let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
882 if path.starts_with("/%NETWORK") {
883 todo!("NETWORK shares not yet implemented :( sorry!")
884 }
885
886 let mut non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
887 self.replace_emu_dir(path, "mlc")
888 } else if path.starts_with("/%SLC_EMU_DIR") {
889 self.replace_emu_dir(path, "slc")
890 } else if path.starts_with("/%DISC_EMU_DIR") {
891 self.replace_emu_dir(path, "disc")
892 } else if path.starts_with("/%SAVE_EMU_DIR") {
893 self.replace_emu_dir(path, "save")
894 } else {
895 PathBuf::from(path)
896 };
897
898 if let Some(new_dir) = self.do_disc_mapping(&non_canonical_path).await {
899 non_canonical_path = new_dir;
900 }
901 if self.create_save_directories {
902 self.ensure_save_dir_exists(&non_canonical_path).await;
903 }
904
905 let mut closest_canonical_directory = non_canonical_path.clone();
912 let mut changed_at_all = false;
913 while !closest_canonical_directory.as_os_str().is_empty() {
914 if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
915 closest_canonical_directory = canonicalized;
916 break;
917 }
918
919 changed_at_all = true;
920 closest_canonical_directory.pop();
921 }
922 if closest_canonical_directory.as_os_str().is_empty() {
925 return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
926 }
927 let canonicalized_cafe = self
929 .cafe_sdk_path()
930 .canonicalize()
931 .unwrap_or_else(|_| self.cafe_sdk_path().clone());
932 if !closest_canonical_directory.starts_with(canonicalized_cafe) {
933 return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
934 }
935
936 #[cfg(feature = "nus")]
937 {
938 let mut base_mlc_title_dir = self.mlc_path();
940 base_mlc_title_dir.push("usr");
941 base_mlc_title_dir.push("title");
942 if let Some(nus_ref) = self.nus.as_ref()
944 && !non_canonical_path.exists()
945 && let Ok(leftover) = non_canonical_path.strip_prefix(&base_mlc_title_dir)
946 && leftover.components().count() > 2
947 {
948 let mut path_components = leftover.components();
949 let mut tid_str = String::new();
950 tid_str += path_components
952 .next()
953 .unwrap_or_else(|| unreachable!())
954 .as_os_str()
955 .to_string_lossy()
956 .as_ref();
957 tid_str += path_components
958 .next()
959 .unwrap_or_else(|| unreachable!())
960 .as_os_str()
961 .to_string_lossy()
962 .as_ref();
963 if let Ok(title_id) = TitleID::from_str(&tid_str) {
964 let p = path_components.as_path().to_path_buf();
965 if nus_ref.exists(title_id, &p).await.is_some() {
966 return Ok(ResolvedLocation::NUSLocation(
967 title_id,
968 p,
969 non_canonical_path,
970 ));
971 }
972 }
973 }
974 }
975
976 Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
977 non_canonical_path,
978 closest_canonical_directory,
979 !changed_at_all,
980 )))
981 }
982
983 pub fn create_directory(&self, at: &Path) -> Result<(), FSError> {
989 create_dir_all_sync(at).map_err(FSError::IO)
990 }
991
992 pub fn copy(&self, from: &Path, to: &Path) -> Result<(), FSError> {
998 if from.is_dir() {
999 Self::copy_dir(from, to)
1000 } else {
1001 copy_file_sync(from, to).map_err(FSError::IO).map(|_| ())
1002 }
1003 }
1004
1005 pub fn rename(&self, from: &Path, to: &Path) -> Result<(), FSError> {
1015 if from.is_dir() {
1016 Self::rename_dir(from, to)
1017 } else {
1018 rename_sync(from, to).map_err(FSError::IO)
1019 }
1020 }
1021
1022 #[must_use]
1029 pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
1030 join_many(
1031 &self.cafe_sdk_path,
1032 [
1033 "data".to_owned(),
1034 "slc".to_owned(),
1035 "sys".to_owned(),
1036 "title".to_owned(),
1037 format!("{:08x}", title_id.group_id()),
1038 format!("{:08x}", title_id.title_id()),
1039 ],
1040 )
1041 }
1042
1043 #[must_use]
1045 pub fn mlc_path(&self) -> PathBuf {
1046 join_many(&self.cafe_sdk_path, ["data".to_owned(), "mlc".to_owned()])
1047 }
1048
1049 #[allow(
1054 unreachable_code,
1056 )]
1057 #[must_use]
1058 pub fn default_cafe_folder() -> Option<PathBuf> {
1059 #[cfg(target_os = "windows")]
1060 {
1061 return Some(PathBuf::from(r"C:\cafe_sdk"));
1062 }
1063
1064 #[cfg(any(
1065 target_os = "linux",
1066 target_os = "freebsd",
1067 target_os = "openbsd",
1068 target_os = "netbsd",
1069 target_os = "macos"
1070 ))]
1071 {
1072 return Some(PathBuf::from("/opt/cafe_sdk"));
1073 }
1074
1075 None
1076 }
1077
1078 fn temp_path(&self) -> Result<PathBuf, FSError> {
1085 let temp_path = join_many(
1086 &self.cafe_sdk_path,
1087 [
1088 "temp".to_owned(),
1089 username()
1090 .unwrap_or_else(|_| "unknown-user".to_owned())
1091 .to_lowercase(),
1092 ],
1093 );
1094 if !temp_path.exists() {
1095 create_dir_all_sync(&temp_path)?;
1096 }
1097 Ok(temp_path)
1098 }
1099
1100 fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
1102 let path_minus = path
1103 .trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
1104 .trim_start_matches('/')
1105 .trim_start_matches('\\')
1106 .replace('\\', "/");
1107
1108 join_many(
1109 &join_many(self.cafe_sdk_path(), ["data", dir]),
1110 path_minus.split('/'),
1111 )
1112 }
1113
1114 async fn do_disc_mapping(&self, previous_path: &Path) -> Option<PathBuf> {
1116 let Ok(leftover) = previous_path.strip_prefix(self.disc_emu_path()) else {
1117 return None;
1118 };
1119 let lock = self.disc_mounted.lock().await;
1120 let (is_slc, is_sys, tid) = match lock.as_ref() {
1121 Some((is_slc, is_sys, tid)) => (*is_slc, *is_sys, *tid),
1122 None => {
1123 (false, true, TitleID::new(0x0005_0010_1F70_0500))
1125 }
1126 };
1127
1128 let mut new_path = self.cafe_sdk_path().clone();
1129 new_path.push("data");
1130 if is_slc {
1131 new_path.push("slc");
1132 } else {
1133 new_path.push("mlc");
1134 }
1135 if is_sys {
1136 new_path.push("sys");
1137 } else {
1138 new_path.push("usr");
1139 }
1140 new_path.push("title");
1141 new_path.push(format!("{:08x}", tid.group_id()));
1142 new_path.push(format!("{:08x}", tid.title_id()));
1143 for comp in leftover.components() {
1144 new_path.push(comp);
1145 }
1146
1147 Some(new_path)
1148 }
1149
1150 async fn ensure_save_dir_exists(&self, path: &Path) {
1152 if !path.starts_with(&self.cafe_sdk_path) || path.exists() {
1155 return;
1156 }
1157 let mut components_left = path
1158 .components()
1159 .skip(self.cafe_sdk_path.components().count());
1160
1161 if components_left.next().map(Component::as_os_str) != Some(OsStr::new("data")) {
1163 return;
1164 }
1165
1166 let mlc_slc_component = components_left.next().map(Component::as_os_str);
1167 if mlc_slc_component != Some(OsStr::new("mlc"))
1168 && mlc_slc_component != Some(OsStr::new("slc"))
1169 {
1170 return;
1171 }
1172 let is_mlc = mlc_slc_component == Some(OsStr::new("mlc"));
1173 let sys_usr_component = components_left.next().map(Component::as_os_str);
1174 if sys_usr_component != Some(OsStr::new("sys"))
1175 && sys_usr_component != Some(OsStr::new("usr"))
1176 {
1177 return;
1178 }
1179 let is_sys = sys_usr_component == Some(OsStr::new("sys"));
1180 if components_left.next().map(Component::as_os_str) != Some(OsStr::new("save")) {
1181 return;
1182 }
1183
1184 let Some(group_id) = components_left
1186 .next()
1187 .map(|p| p.as_os_str().to_string_lossy().to_string())
1188 else {
1189 return;
1190 };
1191 let Some(title_id) = components_left
1192 .next()
1193 .map(|p| p.as_os_str().to_string_lossy().to_string())
1194 else {
1195 return;
1196 };
1197
1198 if join_many(
1202 self.cafe_sdk_path(),
1203 [
1204 "data",
1205 if is_mlc { "mlc" } else { "slc" },
1206 if is_sys { "sys" } else { "usr" },
1207 "title",
1208 &group_id,
1209 &title_id,
1210 ],
1211 )
1212 .exists()
1213 {
1214 let mut save_path = join_many(
1215 self.cafe_sdk_path(),
1216 [
1217 "data",
1218 if is_mlc { "mlc" } else { "slc" },
1219 if is_sys { "sys" } else { "usr" },
1220 "save",
1221 &group_id,
1222 &title_id,
1223 "meta",
1224 ],
1225 );
1226 _ = create_dir_all(&save_path).await;
1227 save_path.pop();
1228 save_path.push("user");
1229 _ = create_dir_all(&save_path).await;
1230 }
1231 }
1232
1233 async fn patch_case_sensitivity(cafe_sdk_path: &Path) -> Result<(), FSError> {
1234 if !cafe_sdk_path.exists() {
1236 return Ok(());
1237 }
1238 let capital_path = join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
1239 let _ = File::create(&capital_path).await?;
1240 let is_insensitive = File::open(join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
1241 .await
1242 .is_ok();
1243 remove_file_sync(capital_path)?;
1244 if is_insensitive {
1245 return Ok(());
1246 }
1247
1248 info!(
1249 "Your Host OS is not case-insensitive for file-paths... ensuring CafeSDK is all lowercase, this may take awhile..."
1250 );
1251 let cafe_sdk_components = cafe_sdk_path.components().count();
1252 let mut had_rename = true;
1253 while had_rename {
1254 had_rename = false;
1255 for directory in [
1256 join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
1257 join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
1258 join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
1259 join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
1260 ] {
1261 if !directory.exists() {
1262 continue;
1264 }
1265
1266 let mut iter = WalkDir::new(&directory)
1267 .contents_first(false)
1268 .follow_links(false)
1269 .follow_root_links(false)
1270 .into_iter();
1271 while let Some(Ok(entry)) = iter.next() {
1272 let p = entry.path();
1273 if !p.exists() {
1274 continue;
1275 }
1276
1277 let path_minus_cafe = p
1278 .components()
1279 .skip(cafe_sdk_components)
1280 .collect::<PathBuf>();
1281 let Some(path_as_utf8) = path_minus_cafe.as_os_str().to_str() else {
1282 warn!(problematic_path = %p.display(), "Path in Cafe SDK directory is not UTF-8! This may cause errors fetching!");
1283 continue;
1284 };
1285 let new_path = Self::to_insensitive(path_as_utf8);
1286 if path_as_utf8 != new_path {
1287 let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
1288 final_new_path.push("/");
1289 final_new_path.push(&new_path);
1290 let new = PathBuf::from(final_new_path);
1291
1292 if p.is_dir() {
1293 Self::rename_dir(p, &new)?;
1294 had_rename = true;
1295 } else {
1296 rename_sync(p, new)?;
1297 had_rename = true;
1298 }
1299 }
1300 }
1301 }
1302 }
1303 info!("ensure CafeSDK path is now case-insensitive by renaming to all lowercase...");
1304
1305 Ok(())
1306 }
1307
1308 fn to_insensitive(path: &str) -> String {
1309 if path.contains("/content/") {
1313 return path.to_owned();
1314 }
1315
1316 let mut as_lowercase = path.to_ascii_lowercase();
1317
1318 if as_lowercase.ends_with("meta/bootdrctex.tga") {
1319 as_lowercase = as_lowercase
1320 .trim_end_matches("meta/bootdrctex.tga")
1321 .to_owned();
1322 as_lowercase += "meta/bootDrcTex.tga";
1323 } else if as_lowercase.ends_with("meta/bootlogotex.tga") {
1324 as_lowercase = as_lowercase
1325 .trim_end_matches("meta/bootlogotex.tga")
1326 .to_owned();
1327 as_lowercase += "meta/bootLogoTex.tga";
1328 } else if as_lowercase.ends_with("meta/bootmovie.h264") {
1329 as_lowercase = as_lowercase
1330 .trim_end_matches("meta/bootmovie.h264")
1331 .to_owned();
1332 as_lowercase += "meta/bootMovie.h264";
1333 } else if as_lowercase.ends_with("meta/boottvtex.tga") {
1334 as_lowercase = as_lowercase
1335 .trim_end_matches("meta/boottvtex.tga")
1336 .to_owned();
1337 as_lowercase += "meta/bootTvTex.tga";
1338 } else if as_lowercase.ends_with("meta/icontex.tga") {
1339 as_lowercase = as_lowercase.trim_end_matches("meta/icontex.tga").to_owned();
1340 as_lowercase += "meta/iconTex.tga";
1341 } else if as_lowercase.ends_with("content/beep.snd") {
1342 as_lowercase = as_lowercase.trim_end_matches("content/beep.snd").to_owned();
1343 as_lowercase += "content/BEEP.snd";
1344 }
1345
1346 as_lowercase
1347 }
1348
1349 fn allow_access(entry: Option<u64>, requester: Option<u64>) -> bool {
1350 let Some(requesting_stream_id) = requester else {
1351 return true;
1352 };
1353 let Some(owned_stream_id) = entry else {
1354 return true;
1355 };
1356
1357 requesting_stream_id == owned_stream_id
1358 }
1359
1360 async fn prepare_for_serving(cafe_sdk_path: &Path) -> Result<(), FSError> {
1367 if !join_many(cafe_sdk_path, ["data", "slc", "sys", "config", "eco.xml"]).exists() {
1368 Self::generate_eco_xml(cafe_sdk_path).await?;
1369 }
1370 if !join_many(
1371 cafe_sdk_path,
1372 ["data", "slc", "sys", "proc", "prefs", "wii_acct.xml"],
1373 )
1374 .exists()
1375 {
1376 Self::generate_wii_acct_xml(cafe_sdk_path).await?;
1377 }
1378 if !join_many(
1379 cafe_sdk_path,
1380 ["data", "slc", "sys", "proc", "prefs", "rmtCfg.xml"],
1381 )
1382 .exists()
1383 {
1384 Self::generate_rmt_cfg(cafe_sdk_path).await?;
1385 }
1386
1387 if join_many(cafe_sdk_path, ["data", "disc"]).exists() {
1393 remove_dir_all_sync(join_many(cafe_sdk_path, ["data", "disc"])).map_err(FSError::IO)?;
1394 }
1395
1396 let sct_code_path = join_many(
1397 cafe_sdk_path,
1398 [
1399 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
1400 ],
1401 );
1402 Self::copy_rpls(
1403 &sct_code_path,
1404 &join_many(
1405 cafe_sdk_path,
1406 [
1407 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
1408 ],
1409 ),
1410 )?;
1411 Self::copy_rpls(
1412 &sct_code_path,
1413 &join_many(
1414 cafe_sdk_path,
1415 [
1416 "data", "slc", "sys", "title", "00050010", "1000800a", "code",
1417 ],
1418 ),
1419 )?;
1420
1421 Ok(())
1422 }
1423
1424 fn copy_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1425 if !dest_path.exists() {
1426 create_dir_all_sync(dest_path)?;
1427 }
1428 let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1429 let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1430
1431 for result in WalkDir::new(source_path)
1432 .follow_links(false)
1433 .follow_root_links(false)
1434 {
1435 let rpb = result?.into_path();
1436 let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1437 let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1438 new_bytes.extend_from_slice(new_path_as_str_bytes);
1439 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1440 let as_new_path =
1441 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1442
1443 if rpb.is_symlink() {
1444 let mut resolved_path = read_link_sync(&rpb)?;
1445 {
1446 let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1449 if os_str_for_resolved.starts_with(old_path_bytes) {
1450 let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1451 new_bytes.extend_from_slice(new_path_as_str_bytes);
1452 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1453 resolved_path = PathBuf::from(unsafe {
1454 OsString::from_encoded_bytes_unchecked(new_bytes)
1455 });
1456 }
1457 }
1458
1459 #[cfg(unix)]
1460 {
1461 use std::os::unix::fs::symlink;
1462 symlink(resolved_path, &as_new_path)?;
1463 }
1464
1465 #[cfg(target_os = "windows")]
1466 {
1467 use std::os::windows::fs::{symlink_dir, symlink_file};
1468
1469 if resolved_path.is_dir() {
1470 symlink_dir(resolved_path, &as_new_path)?;
1471 } else {
1472 symlink_file(resolved_path, &as_new_path)?;
1473 }
1474 }
1475 } else if rpb.is_file() {
1476 copy_file_sync(&rpb, &as_new_path)?;
1477 } else if rpb.is_dir() {
1478 create_dir_all_sync(&as_new_path)?;
1479 }
1480 }
1481
1482 Ok(())
1483 }
1484
1485 fn copy_rpls(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1491 if !dest_path.exists() {
1492 create_dir_all_sync(dest_path)?;
1493 }
1494 let destination_path_bytes = dest_path.as_os_str().as_encoded_bytes();
1495 let source_path_bytes = source_path.as_os_str().as_encoded_bytes();
1496
1497 for result in WalkDir::new(source_path)
1498 .follow_links(false)
1499 .follow_root_links(false)
1500 {
1501 let rpb = result?.into_path();
1502 if rpb.extension().unwrap_or_default().to_string_lossy() != "rpl" || rpb.is_symlink() {
1504 continue;
1505 }
1506 let mut new_bytes = Vec::from(destination_path_bytes);
1508 new_bytes.extend_from_slice(
1509 &rpb.as_path().as_os_str().as_encoded_bytes()[source_path_bytes.len()..],
1510 );
1511 let as_new_path =
1512 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1513
1514 if !as_new_path.exists() {
1516 copy_file_sync(rpb, as_new_path)?;
1517 }
1518 }
1519
1520 Ok(())
1521 }
1522
1523 fn rename_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1533 if !dest_path.exists() {
1534 create_dir_all_sync(dest_path)?;
1535 }
1536 let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1537 let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1538
1539 for result in WalkDir::new(source_path)
1540 .follow_links(false)
1541 .follow_root_links(false)
1542 {
1543 let rpb = result?.into_path();
1544 let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1545 let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1546 new_bytes.extend_from_slice(new_path_as_str_bytes);
1547 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1548 let as_new_path =
1549 PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1550
1551 if rpb.is_symlink() {
1552 let mut resolved_path = read_link_sync(&rpb)?;
1553 {
1554 let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1557 if os_str_for_resolved.starts_with(old_path_bytes) {
1558 let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1559 new_bytes.extend_from_slice(new_path_as_str_bytes);
1560 new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1561 resolved_path = PathBuf::from(unsafe {
1562 OsString::from_encoded_bytes_unchecked(new_bytes)
1563 });
1564 }
1565 }
1566
1567 let should_remove: bool;
1574 #[cfg(unix)]
1575 {
1576 use std::os::unix::fs::symlink;
1577 symlink(resolved_path, &as_new_path)?;
1578 should_remove = true;
1579 }
1580
1581 #[cfg(target_os = "windows")]
1582 {
1583 use std::os::windows::fs::{symlink_dir, symlink_file};
1584
1585 if resolved_path.is_dir() {
1586 symlink_dir(resolved_path, &as_new_path)?;
1587 should_remove = false;
1588 } else {
1589 symlink_file(resolved_path, &as_new_path)?;
1590 should_remove = true;
1591 }
1592 }
1593
1594 if should_remove {
1596 remove_file_sync(&rpb)?;
1597 }
1598 } else if rpb.is_file() {
1599 rename_sync(&rpb, &as_new_path)?;
1600 } else if rpb.is_dir() {
1601 create_dir_all_sync(&as_new_path)?;
1602 }
1603 }
1604 remove_dir_all_sync(source_path)?;
1606
1607 Ok(())
1608 }
1609
1610 async fn generate_eco_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1621 let mut eco_path = join_many(cafe_os_path, ["data", "slc", "sys", "config"]);
1622 if !eco_path.exists() {
1623 create_dir_all_sync(&eco_path).map_err(FSError::IO)?;
1624 }
1625 eco_path.push("eco.xml");
1626
1627 let mut eco_file = File::create(eco_path).await.map_err(FSError::IO)?;
1628 eco_file
1629 .write_all(
1630 br#"<?xml version="1.0" encoding="utf-8"?>
1631<eco type="complex" access="777">
1632 <enable type="unsignedInt" length="4">0</enable>
1633 <max_on_time type="unsignedInt" length="4">3601</max_on_time>
1634 <default_off_time type="unsignedInt" length="4">15</default_off_time>
1635 <wd_disable type="unsignedInt" length="4">1</wd_disable>
1636</eco>"#,
1637 )
1638 .await
1639 .map_err(FSError::IO)?;
1640
1641 #[cfg(unix)]
1642 {
1643 use std::{fs::Permissions, os::unix::prelude::*};
1644 eco_file
1645 .set_permissions(Permissions::from_mode(0o770))
1646 .await?;
1647 }
1648
1649 Ok(())
1650 }
1651
1652 async fn generate_wii_acct_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1663 let mut wii_path = join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
1664 if !wii_path.exists() {
1665 create_dir_all_sync(&wii_path).map_err(FSError::IO)?;
1666 }
1667 wii_path.push("wii_acct.xml");
1668
1669 let mut wii_file = File::create(wii_path).await.map_err(FSError::IO)?;
1670 wii_file
1671 .write_all(
1672 br#"<?xml version="1.0" encoding="utf-8"?>
1673<wii_acct type="complex">
1674 <profile type="complex">
1675 <nickname type="hexBinary" length="22">00570069006900000000000000000000000000000000</nickname>
1676
1677 <language type="unsignedInt" length="4">0</language>
1678 <country type="unsignedInt" length="4">1</country>
1679 </profile>
1680 <pc type="complex">
1681 <rating type="unsignedInt" length="4">18</rating>
1682 <organization type="unsignedInt" length="4">0</organization>
1683 <rst_internet_ch type="unsignedByte" length="1">0</rst_internet_ch>
1684 <rst_nw_access type="unsignedByte" length="1">0</rst_nw_access>
1685 <rst_pt_order type="unsignedByte" length="1">0</rst_pt_order>
1686 </pc>
1687</wii_acct>"#,
1688 )
1689 .await
1690 .map_err(FSError::IO)?;
1691
1692 #[cfg(unix)]
1693 {
1694 use std::{fs::Permissions, os::unix::prelude::*};
1695 wii_file
1696 .set_permissions(Permissions::from_mode(0o770))
1697 .await?;
1698 }
1699
1700 Ok(())
1701 }
1702
1703 async fn generate_rmt_cfg(cafe_os_path: &Path) -> Result<(), FSError> {
1713 let mut rmt_path = join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
1714 if !rmt_path.exists() {
1715 create_dir_all(&rmt_path).await.map_err(FSError::IO)?;
1716 }
1717 rmt_path.push("rmtCfg.xml");
1718
1719 let mut rmt_file = File::create(rmt_path).await.map_err(FSError::IO)?;
1720 rmt_file
1721 .write_all(
1722 br#"<?xml version="1.0" encoding="utf-8"?>
1723<rmtCfg type="complex" access="7777">
1724 <sensitivity type="unsignedByte" length="1">3</sensitivity>
1725 <sbPos type="unsignedByte" length="1">1</sbPos>
1726 <volume type="unsignedByte" length="1">63</volume>
1727 <vibrator type="unsignedByte" length="1">1</vibrator>
1728</rmtCfg>"#,
1729 )
1730 .await
1731 .map_err(FSError::IO)?;
1732
1733 #[cfg(unix)]
1734 {
1735 use std::{fs::Permissions, os::unix::prelude::*};
1736 rmt_file
1737 .set_permissions(Permissions::from_mode(0o770))
1738 .await?;
1739 }
1740
1741 Ok(())
1742 }
1743
1744 fn get_default_read_only_folders(cafe_dir: &Path) -> ConcurrentSet<PathBuf> {
1745 let set = ConcurrentSet::new();
1746
1747 for cafe_sub_paths in [
1748 &["data", "slc", "sys", "config"] as &[&str],
1749 &["data", "slc", "sys", "proc"],
1750 &["data", "slc", "sys", "logs"],
1751 &["data", "mlc", "usr"],
1752 &["data", "mlc", "usr", "import"],
1753 &["data", "mlc", "usr", "title"],
1754 ] {
1755 _ = set.insert_sync(join_many(cafe_dir, cafe_sub_paths));
1756 }
1757
1758 set
1759 }
1760}
1761
1762const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
1763 NamedField::new("cafe_sdk_path"),
1764 NamedField::new("open_file_handles"),
1765 NamedField::new("open_folder_handles"),
1766];
1767
1768impl Structable for HostFilesystem {
1769 fn definition(&self) -> StructDef<'_> {
1770 StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
1771 }
1772}
1773
1774impl Valuable for HostFilesystem {
1775 fn as_value(&self) -> Value<'_> {
1776 Value::Structable(self)
1777 }
1778
1779 fn visit(&self, visitor: &mut dyn Visit) {
1780 let mut values = HashMap::with_capacity(self.open_file_handles.len());
1781 self.open_file_handles.iter_sync(|k, v| {
1782 values.insert(*k, format!("{v:?}"));
1783 true
1784 });
1785 let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
1786 self.open_folder_handles.iter_sync(|k, v| {
1787 folder_values.insert(*k, format!("{v:?}"));
1788 true
1789 });
1790
1791 visitor.visit_named_fields(&NamedValues::new(
1792 HOST_FILESYSTEM_FIELDS,
1793 &[
1794 Valuable::as_value(&self.cafe_sdk_path),
1795 Valuable::as_value(&values),
1796 Valuable::as_value(&folder_values),
1797 ],
1798 ));
1799 }
1800}
1801
1802#[cfg_attr(docsrs, doc(cfg(test)))]
1803#[cfg(test)]
1804pub mod test_helpers {
1805 pub use super::utilities::join_many;
1806 use super::*;
1807 use std::fs::{File, create_dir_all};
1808 use tempfile::{TempDir, tempdir};
1809
1810 #[allow(
1812 dead_code,
1814 )]
1815 pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
1816 let dir = tempdir().expect("Failed to create temporary directory!");
1817
1818 for directory_to_create in vec![
1819 vec!["data", "slc"],
1821 vec!["data", "mlc"],
1822 vec!["data", "disc"],
1823 vec!["data", "save"],
1824 vec![
1826 "data", "mlc", "sys", "title", "00050030", "1001000a", "code",
1827 ],
1828 vec![
1829 "data", "mlc", "sys", "title", "00050010", "1f700500", "code",
1830 ],
1831 vec![
1832 "data", "mlc", "sys", "title", "00050010", "1f700500", "content",
1833 ],
1834 vec![
1835 "data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
1836 ],
1837 vec![
1839 "data", "mlc", "sys", "title", "00050030", "1001010A", "code",
1840 ],
1841 vec![
1842 "data", "mlc", "sys", "title", "00050030", "1001020a", "code",
1843 ],
1844 vec![
1845 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
1846 ],
1847 vec!["data", "mlc", "sys", "update", "nand", "os_v10_ndebug"],
1848 vec!["data", "mlc", "sys", "update", "nand", "os_v10_debug"],
1849 vec!["data", "slc", "sys", "proc", "prefs"],
1850 vec![
1851 "data", "slc", "sys", "title", "00050010", "1000800a", "code",
1852 ],
1853 vec![
1854 "data", "slc", "sys", "title", "00050010", "1000400a", "code",
1855 ],
1856 ] {
1857 create_dir_all(join_many(dir.path(), directory_to_create))
1858 .expect("Failed to create directories necessary for host filesystem to work.");
1859 }
1860
1861 File::create(join_many(
1864 dir.path(),
1865 [
1866 "data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
1867 ],
1868 ))
1869 .expect("Failed to create needed app.xml!");
1870 File::create(join_many(
1871 dir.path(),
1872 [
1873 "data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
1874 ],
1875 ))
1876 .expect("Failed to create needed app.xml!");
1877 File::create(join_many(
1878 dir.path(),
1879 [
1880 "data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
1881 ],
1882 ))
1883 .expect("Failed to create needed app.xml!");
1884
1885 File::create(join_many(
1886 dir.path(),
1887 [
1888 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
1889 ],
1890 ))
1891 .expect("Failed to create needed fw.img!");
1892 File::create(join_many(
1893 dir.path(),
1894 [
1895 "data", "mlc", "sys", "title", "00050010", "1f700500", "code", "app.xml",
1896 ],
1897 ))
1898 .expect("Failed to create needed app.xml for disc!");
1899
1900 let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
1901 .await
1902 .expect("Failed to load empty host filesystem!");
1903
1904 (dir, fs)
1905 }
1906}
1907
1908#[cfg(test)]
1909mod unit_tests {
1910 use super::test_helpers::*;
1911 use super::*;
1912 use std::fs::read;
1913
1914 fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
1915
1916 #[test]
1917 pub fn is_send_sync() {
1918 only_accepts_send_sync::<HostFilesystem>(None);
1919 }
1920
1921 #[test]
1922 pub fn can_find_default_cafe_directory() {
1923 assert!(
1924 HostFilesystem::default_cafe_folder().is_some(),
1925 "Failed to find default cafe directory for your OS",
1926 );
1927 }
1928
1929 #[tokio::test]
1930 pub async fn creatable_files() {
1931 let (tempdir, fs) = create_temporary_host_filesystem().await;
1934
1935 let expected_bsf_path = join_many(
1936 tempdir.path(),
1937 [
1938 "temp".to_owned(),
1939 username()
1940 .expect("Failed to get system username!")
1941 .to_lowercase(),
1942 "caferun".to_owned(),
1943 "ppc.bsf".to_owned(),
1944 ],
1945 );
1946 assert!(
1947 !expected_bsf_path.exists(),
1948 "ppc.bsf existed before we asked for it?"
1949 );
1950 let bsf_path = fs
1951 .boot1_sytstem_path()
1952 .await
1953 .expect("Failed to create bsf!");
1954 assert_eq!(expected_bsf_path, bsf_path);
1955 assert!(
1956 BootSystemFile::try_from(Bytes::from(
1957 read(bsf_path).expect("Failed to read written boot system file!")
1958 ))
1959 .is_ok(),
1960 "Failed to read generated boot system file!"
1961 );
1962
1963 let expected_diskid_path = join_many(
1964 tempdir.path(),
1965 [
1966 "temp".to_owned(),
1967 username()
1968 .expect("Failed to get system username!")
1969 .to_lowercase(),
1970 "caferun".to_owned(),
1971 "diskid.bin".to_owned(),
1972 ],
1973 );
1974 assert!(
1975 !expected_diskid_path.exists(),
1976 "diskid.bin existed before we asked for it?"
1977 );
1978 let diskid_path = fs
1979 .disk_id_path()
1980 .await
1981 .expect("Failed to create diskid.bin!");
1982 assert_eq!(expected_diskid_path, diskid_path);
1983 assert_eq!(
1984 read(diskid_path).expect("Failed to read written diskid.bin!"),
1985 vec![0; 32],
1986 "Failed to read generated diskid.bin!"
1987 );
1988
1989 assert_eq!(
1991 fs.firmware_file_path(),
1992 join_many(
1993 tempdir.path(),
1994 [
1995 "data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"
1996 ],
1997 ),
1998 );
1999
2000 let expected_ppc_boot_dlf_path = join_many(
2001 tempdir.path(),
2002 [
2003 "temp".to_owned(),
2004 username()
2005 .expect("Failed to get system username!")
2006 .to_lowercase(),
2007 "caferun".to_owned(),
2008 "ppc_boot.dlf".to_owned(),
2009 ],
2010 );
2011 assert!(
2012 !expected_ppc_boot_dlf_path.exists(),
2013 "ppc_boot.dlf existed before we asked for it?"
2014 );
2015 let ppc_boot_dlf_path = fs
2016 .ppc_boot_dlf_path()
2017 .await
2018 .expect("Failed to create ppc_boot.dlf!");
2019 assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
2020 assert!(
2021 DiskLayoutFile::try_from(Bytes::from(
2022 read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
2023 ))
2024 .is_ok(),
2025 "Failed to read generated ppc_boot.dlf!"
2026 );
2027 }
2028
2029 #[tokio::test]
2030 pub async fn path_allows_writes() {
2031 let (_tempdir, fs) = create_temporary_host_filesystem().await;
2032
2033 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
2036 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
2037 assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
2038 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
2039 assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
2040 }
2041
2042 #[tokio::test]
2043 pub async fn resolve_path() {
2044 let (tempdir, fs) = create_temporary_host_filesystem().await;
2047
2048 for (dir, name) in [
2050 ("/%MLC_EMU_DIR", "mlc"),
2051 ("/%SLC_EMU_DIR", "slc"),
2052 ("/%DISC_EMU_DIR", "disc"),
2053 ("/%SAVE_EMU_DIR", "save"),
2054 ] {
2055 assert!(
2056 fs.resolve_path(&format!("{dir}")).await.is_ok(),
2057 "Failed to resolve: `{}`: {:?}",
2058 dir,
2059 fs.resolve_path(&format!("{dir}")).await
2060 );
2061 assert!(
2062 fs.resolve_path(&format!("{dir}/")).await.is_ok(),
2063 "Failed to resolve: `{}/`",
2064 dir,
2065 );
2066 assert!(
2067 fs.resolve_path(&format!("{dir}/./")).await.is_ok(),
2068 "Failed to resolve: `{}/./`",
2069 dir,
2070 );
2071 assert!(
2072 fs.resolve_path(&format!("{dir}/../{name}")).await.is_ok(),
2073 "Failed to resolve: `{}/../{}`",
2074 dir,
2075 name,
2076 );
2077 }
2078
2079 let mut out_of_path = PathBuf::from(tempdir.path());
2081 out_of_path.pop();
2084
2085 assert!(
2087 fs.resolve_path(
2088 &out_of_path
2089 .clone()
2090 .into_os_string()
2091 .into_string()
2092 .expect("Failed to convert pathbuf to string!")
2093 )
2094 .await
2095 .is_err()
2096 );
2097 assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").await.is_err());
2098
2099 #[cfg(unix)]
2100 {
2101 use std::os::unix::fs::symlink;
2102
2103 let mut tempdir_symlink = PathBuf::from(tempdir.path());
2104 tempdir_symlink.push("symlink");
2105 symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
2106 assert!(
2107 fs.resolve_path(&format!(
2108 "{}/symlink",
2109 tempdir_symlink
2110 .into_os_string()
2111 .into_string()
2112 .expect("tempdir symlink wasn't utf8?"),
2113 ))
2114 .await
2115 .is_err()
2116 );
2117 }
2118
2119 #[cfg(target_os = "windows")]
2120 {
2121 use std::os::windows::fs::symlink_dir;
2122
2123 let mut tempdir_symlink = PathBuf::from(tempdir.path());
2124 tempdir_symlink.push("symlink");
2125 symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
2126 assert!(
2127 fs.resolve_path(&format!(
2128 "{}/symlink",
2129 tempdir_symlink
2130 .into_os_string()
2131 .into_string()
2132 .expect("tempdir symlink wasn't utf8?"),
2133 ))
2134 .await
2135 .is_err()
2136 );
2137 }
2138 }
2139
2140 #[tokio::test]
2141 pub async fn opening_files() {
2142 let (tempdir, fs) = create_temporary_host_filesystem().await;
2143 let path = join_many(tempdir.path(), ["file.txt"]);
2144 tokio::fs::write(path.clone(), vec![0; 1307])
2145 .await
2146 .expect("Failed to write test file!");
2147 let create_path = join_many(tempdir.path(), ["new-file.txt"]);
2148
2149 let mut oo = OpenOptions::new();
2150 oo.create(false).write(true).read(true);
2151 assert!(
2152 fs.open_file(oo, &create_path, None).await.is_err(),
2153 "Somehow succeeding opening a file that doesn't exist with no create flag?",
2154 );
2155 oo = OpenOptions::new();
2156 oo.create(true).write(true).truncate(true);
2157 let fd = fs
2158 .open_file(oo, &create_path, None)
2159 .await
2160 .expect("Failed opening a file that doesn't exist with a create flag?");
2161 assert!(
2162 fs.open_file_handles.len() == 1 && fs.open_file_handles.get_sync(&fd).is_some(),
2163 "Open file wasn't in open files list!",
2164 );
2165 fs.close_file(fd, None).await;
2166 assert!(
2167 fs.open_file_handles.is_empty(),
2168 "Somehow after opening/closing, open file handles was not empty?",
2169 );
2170 }
2171
2172 #[tokio::test]
2173 pub async fn seek_and_read() {
2174 let (tempdir, fs) = create_temporary_host_filesystem().await;
2175 let path = join_many(tempdir.path(), ["file.txt"]);
2176 tokio::fs::write(path.clone(), vec![0; 1307])
2177 .await
2178 .expect("Failed to write test file!");
2179
2180 let mut oo = OpenOptions::new();
2181 oo.read(true).create(false).write(false);
2182 let fd = fs
2183 .open_file(oo, &path, None)
2184 .await
2185 .expect("Failed to open existing file!");
2186
2187 assert_eq!(
2189 Some(BytesMut::zeroed(1307).freeze()),
2190 fs.read_file(fd, 1307, None)
2191 .await
2192 .expect("Failed to read from FD!"),
2193 );
2194 fs.seek_file(fd, true, 0, None)
2195 .await
2196 .expect("Failed to sync to beginning of file!");
2197 assert_eq!(
2199 Some(BytesMut::zeroed(1307).freeze()),
2200 fs.read_file(fd, 1307, None)
2201 .await
2202 .expect("Failed to read from FD!"),
2203 );
2204 fs.close_file(fd, None).await;
2205 assert!(
2206 fs.open_file_handles.is_empty(),
2207 "Somehow after opening/closing, open file handles was not empty?",
2208 );
2209 }
2210
2211 #[tokio::test]
2212 pub async fn open_and_close_folder() {
2213 let (tempdir, fs) = create_temporary_host_filesystem().await;
2214 let path = join_many(tempdir.path(), ["a", "b"]);
2215 tokio::fs::create_dir_all(path.clone())
2216 .await
2217 .expect("Failed to create test directory!");
2218
2219 let fd = fs
2220 .open_folder(&path, None)
2221 .await
2222 .expect("Failed to open folder!");
2223 assert!(
2224 fs.open_folder_handles.len() == 1,
2225 "Expected one open folder handle",
2226 );
2227 fs.close_folder(fd, None).await;
2228
2229 assert!(
2230 fs.open_folder_handles.is_empty(),
2231 "Somehow after opening/closing, open folder handles was not empty?",
2232 );
2233 }
2234
2235 #[tokio::test]
2236 pub async fn seek_within_folder() {
2237 let (tempdir, fs) = create_temporary_host_filesystem().await;
2238 let path = join_many(tempdir.path(), ["a", "b"]);
2239 tokio::fs::create_dir_all(path.clone())
2240 .await
2241 .expect("Failed to create test directory!");
2242
2243 _ = tokio::fs::File::create(join_many(&path, ["c"]))
2248 .await
2249 .expect("Failed to create file to use!");
2250 tokio::fs::create_dir(join_many(&path, ["d"]))
2251 .await
2252 .expect("Failed to create directory to use!");
2253 #[cfg(unix)]
2254 {
2255 use std::os::unix::fs::symlink;
2256
2257 let mut tempdir_symlink = path.clone();
2258 tempdir_symlink.push("e");
2259 symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
2260 }
2261 #[cfg(target_os = "windows")]
2262 {
2263 use std::os::windows::fs::symlink_dir;
2264
2265 let mut tempdir_symlink = path.clone();
2266 tempdir_symlink.push("e");
2267 symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
2268 }
2269 _ = tokio::fs::File::create(join_many(&path, ["f"]))
2270 .await
2271 .expect("Failed to create file to use!");
2272 _ = tokio::fs::File::create(join_many(&path, ["d", "a"]))
2273 .await
2274 .expect("Failed to create file to use!");
2275
2276 let dfd = fs
2277 .open_folder(&path, None)
2278 .await
2279 .expect("Failed to open folder!");
2280 assert!(fs.next_in_folder(dfd, None).await.is_some());
2281 assert!(fs.next_in_folder(dfd, None).await.is_some());
2282 assert!(fs.next_in_folder(dfd, None).await.is_some());
2283 assert!(fs.next_in_folder(dfd, None).await.is_none());
2285 assert!(fs.next_in_folder(dfd, None).await.is_none());
2287 fs.reverse_folder(dfd, None).await;
2289 assert!(fs.next_in_folder(dfd, None).await.is_some());
2290 assert!(fs.next_in_folder(dfd, None).await.is_none());
2291 }
2292}