Skip to main content

cat_dev/fsemul/filesystem/host/
mod.rs

1//! A representation of the filesystem folder we end up serving a cat-dev
2//! client.
3
4mod 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/// A wrapper around interacting with the 'host' or PC filesystem for the
65/// various times a cat-dev will reach out to the host.
66///
67/// This is little more than a wrapper around a [`PathBuf`], and targeted
68/// methods to make getting files/generating default files/etc. easy. Most of
69/// the actual logic for turning a request from `SDIO`, `ATAPI`, etc. all come
70/// from those client/server implementations rather than the logic living here.
71#[derive(Clone, Debug)]
72pub struct HostFilesystem {
73	/// The path to the base data directory to serve a filesystem out of.
74	cafe_sdk_path: PathBuf,
75	/// If we should create save file directories for titles as they request
76	/// access to their save files.
77	create_save_directories: bool,
78	/// The actively mounted "disc".
79	///
80	/// This is a tuple of (isSLC, isSystem, [`TitleID`]).
81	///
82	/// When a disc is mounted we will copy the title from SLC/MLC
83	/// directory, into `disc/` recursively.
84	disc_mounted: Arc<Mutex<Option<(bool, bool, TitleID)>>>,
85	#[cfg_attr(docsrs, doc(cfg(feature = "nus")))]
86	#[cfg(feature = "nus")]
87	/// The currently mounted NUS System.
88	nus: Option<NUSFuse>,
89	/// List of open file handles.
90	open_file_handles: Arc<ConcurrentMap<i32, OpenFileHandle>>,
91	/// List of open folder "handles".
92	open_folder_handles: Arc<ConcurrentMap<i32, DirectoryListing>>,
93	/// A set of folders that we've "marked" as read-only.
94	///
95	/// We don't actually synchronize this to the filesystem because the original
96	/// cafe-sdk was written for Windows 7 which silently ignores "Read-Only"
97	/// attributes on directories. Still allowing you to create files within
98	/// directories, modify them, etc.
99	///
100	/// This is not the case on older windows distributions, unix based distros,
101	/// or similar.
102	folders_marked_read_only: Arc<ConcurrentSet<PathBuf>>,
103	/// If we are forcing unique file fd's. This should only be changed
104	/// if we have not opened a file yet.
105	is_using_unique_fds: bool,
106	/// If we've opened a file, used to safely ensure we don't switch from
107	/// unique fdf's to not.
108	has_opened_file: Arc<AtomicBool>,
109}
110
111impl HostFilesystem {
112	/// Create a filesystem from a root cafe dir.
113	///
114	/// If no cafe dir is provided, we will attempt to locate the default
115	/// installation path for cafe sdk which is:
116	///
117	/// - `C:\cafe_sdk` on windows.
118	/// - `/opt/cafe_sdk` on any unix/bsd like OS.
119	///
120	/// NOTE: This will validate that all title id paths are lowercase, as
121	/// files are always expected to be lowercase when dealing with CAFE. Other
122	/// files are usually kept in the correct naming format. HOWEVER, users may
123	/// notice spurious errors with case-insensitivity on linux specifically. If
124	/// transferring an SDK from a Windows/Mac Case Insensitive to a Mac/Linux
125	/// case sensitive file system. It is recommended users
126	/// create their own folder using our recovery tools, rather than
127	/// rsync'ing a path over from case-insensitive, to case-sensitive.
128	///
129	/// ## Errors
130	///
131	/// If the Cafe SDK folder is corrupt, or can't be found. A Cafe SDK
132	/// folder is considered corrupt if it is missing core files that we
133	/// _need_ to be able to serve a Cafe-OS distribution. These file
134	/// requirements may change from version to version of this crate, but should
135	/// always be compatible with a clean cafe sdk folder.
136	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			// Can't generate a `fw.img` for now.... :(
163			&[
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	/// Create a filesystem from a root cafe dir, and mount in a virtual
192	/// NUS filesystem.
193	///
194	/// If no cafe dir is provided, we will attempt to locate the default
195	/// installation path for cafe sdk which is:
196	///
197	/// - `C:\cafe_sdk` on windows.
198	/// - `/opt/cafe_sdk` on any unix/bsd like OS.
199	///
200	/// NOTE: This will validate that all title id paths are lowercase, as
201	/// files are always expected to be lowercase when dealing with CAFE. Other
202	/// files are usually kept in the correct naming format. HOWEVER, users may
203	/// notice spurious errors with case-insensitivity on linux specifically. If
204	/// transferring an SDK from a Windows/Mac Case Insensitive to a Mac/Linux
205	/// case sensitive file system. It is recommended users
206	/// create their own folder using our recovery tools, rather than
207	/// rsync'ing a path over from case-insensitive, to case-sensitive.
208	///
209	/// ## Errors
210	///
211	/// If the Cafe SDK folder is corrupt, or can't be found. A Cafe SDK
212	/// folder is considered corrupt if it is missing core files that we
213	/// _need_ to be able to serve a Cafe-OS distribution. These file
214	/// requirements may change from version to version of this crate, but should
215	/// always be compatible with a clean cafe sdk folder.
216	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			// Can't generate a `fw.img` for now.... :(
246			&[
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	/// Whether or not we should create save directories as requested.
273	///
274	/// This may be necessary in order to launch several games from NUS that
275	/// wouldn't normally launch. The only reason you _wouldn't_ want to enable
276	/// this is if you can't trust your clients (they can create lots of folders,
277	/// and in theory eat a bunch of inodes, etc.).
278	pub const fn allow_save_directory_creation(&mut self, allow: bool) {
279		self.create_save_directories = allow;
280	}
281
282	/// The root path to the Cafe SDK.
283	///
284	/// *note: although we do expose this for logging, and other info... we do
285	/// not recommend manually interacting with the SDK path. There are much
286	/// better alternatives.*
287	#[must_use]
288	pub const fn cafe_sdk_path(&self) -> &PathBuf {
289		&self.cafe_sdk_path
290	}
291
292	/// The root path to the Cafe SDK.
293	///
294	/// *note: although we do expose this for logging, and other info... we do
295	/// not recommend manually interacting with the SDK path. There are much
296	/// better alternatives.*
297	#[must_use]
298	pub fn disc_emu_path(&self) -> PathBuf {
299		join_many(&self.cafe_sdk_path, ["data", "disc"])
300	}
301
302	/// Force unique file descriptors for open files.
303	///
304	/// Certain OS's _can_ return duplicate fd's especially when opening,
305	/// and closing files. This can make deciphering logs harder because the
306	/// same FD may appear multiple times, when you're trying to just find
307	/// the logs related to one file descriptor.
308	///
309	/// When unique fd's is turned on, similar to folders we just use a global
310	/// wrapping counter so that way every file descriptor is guaranteed to be
311	/// unique.
312	///
313	/// ## Errors
314	///
315	/// This will error if any file has ever been opened. This is because once
316	/// a client has already connected, and done some stuff with file stuff it
317	/// expects one set of behaviors, we cannot change another one.
318	///
319	/// It can also not be set if nus is set to a value which forces unique fds.
320	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	/// Update the current NUS provider to a particular value.
336	///
337	/// ## Errors
338	///
339	/// Cannot set a new nus provider which forces unique fds if a file has been
340	/// opened, we aren't already using unique fds, and our nus client is some.
341	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	/// Attempt to get the current NUS provider.
360	pub fn get_nus_provider(&self) -> Option<&NUSFuse> {
361		self.nus.as_ref()
362	}
363
364	/// Open a file, and return it's file descriptor number.
365	///
366	/// ## Errors
367	///
368	/// If we cannot open our file with the open options provided.
369	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	/// Get a file from a file descriptor number.
397	///
398	/// This file must already be opened (in order to get the file descriptor).
399	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	/// Get the file length from a file descriptor number.
417	///
418	/// This file must already be opened (in order to get the file descriptor).
419	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	/// Read from a file descriptor that is actively open.
433	///
434	/// This will read from a currently open file descriptor, in it's current
435	/// location. You might want to set your file location for this FD before
436	/// if you aren't already in the same location.
437	///
438	/// ## Errors
439	///
440	/// If the file descriptor is open, but we could not read from the open file
441	/// descriptor.
442	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	/// Write to a file descriptor that is actively open.
482	///
483	/// This will write from a currently open file descriptor, in it's current
484	/// location. You might want to set your file location for this FD before
485	/// if you aren't already in the same location.
486	///
487	/// ## Errors
488	///
489	/// If the file descriptor is open, but we could not write to the open file
490	/// descriptor.
491	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	/// Seek to the beginning or end of a file.
518	///
519	/// If `begin` is true then we will seek to the beginning of the file
520	/// otherwise we will sync to the end of the file. Precise seeking is _not_
521	/// supported at this time.
522	///
523	/// ## Errors
524	///
525	/// If we cannot seek to the beginning or end of the file.
526	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	/// Seek to the beginning or end of a file.
548	///
549	/// If `begin` is true then we will seek to the beginning of the file
550	/// otherwise we will sync to the end of the file. Precise seeking is _not_
551	/// supported at this time.
552	///
553	/// ## Errors
554	///
555	/// If we cannot seek to the beginning or end of the file.
556	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	/// Decrement the ref count of handles to a file.
592	///
593	/// If ref count reaches 0 close the underlying file handle.
594	///
595	/// ## Errors
596	///
597	/// If we cannot close our file handle when our ref count reaches 0, or if
598	/// the file isn't open at all.
599	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			// Don't allow streams to close other streams files.
604			return;
605		}
606
607		self.open_file_handles.remove_async(&fd).await;
608	}
609
610	/// "Open" a folder, or an iterator over a directory.
611	///
612	/// There's no real "open file handle", or reversible directory iterator,
613	/// so we just create an id from scratch.
614	///
615	/// ## Errors
616	///
617	/// If the path doesn't exist, then we can't open the folder.
618	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	/// Mark a folder as being 'read-only' for this session.
636	///
637	/// ## Errors
638	///
639	/// If we could not actually insert the folder into the read only map.
640	pub async fn mark_folder_read_only(&self, path: PathBuf) {
641		_ = self.folders_marked_read_only.insert_async(path).await;
642	}
643
644	/// Mark a folder as being 'read-write' for this session.
645	pub async fn ensure_folder_not_read_only(&self, path: &PathBuf) {
646		self.folders_marked_read_only.remove_async(path).await;
647	}
648
649	/// Check if a folder is marked as being read only.
650	pub async fn folder_is_read_only(&self, path: &PathBuf) -> bool {
651		self.folders_marked_read_only.contains_async(path).await
652	}
653
654	/// Get the next filename/foldername available in a particular folder, and
655	/// how many pieces to remove to get just the filename.
656	///
657	/// This will always return none even if it's already at the end, unlike a
658	/// particular iterator.
659	#[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	/// Reverse a particular iterator over a folder by one.
670	///
671	/// Note: This will recreate the directory iterator, and will temporarily
672	/// hold _two_ references to [`ReadDir`] at a time because the underlying
673	/// iterator from read directory is not a reversible iterator.
674	///
675	/// ## Errors
676	///
677	/// If opening another read dir call does not work.
678	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	/// Decrement the ref count of handles to a folder.
689	///
690	/// If ref count reaches 0 close the underlying folder handle.
691	///
692	/// ## Errors
693	///
694	/// If we cannot close our folder handle when our ref count reaches 0, or if
695	/// the folder isn't open at all.
696	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	/// Get the path to the current boot1 `.bsf` file.
707	///
708	/// This function will create the boot1 system file, if it does not yet
709	/// exist. As a result it may error, if we can't create, and place the
710	/// boot system file.
711	///
712	/// ## Errors
713	///
714	/// - If the temp directory does not exist, and we can't create it.
715	/// - If the boot system file does not exist, and we can't write it to disk.
716	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	/// Get the path to the current `diskid.bin`.
734	///
735	/// If the current Disk ID does not exist, we will write a blank diskid to
736	/// this path.
737	///
738	/// ## Errors
739	///
740	/// - If the temporary directory does not exist, and we can't create it.
741	/// - If the disk ID path does not exist, and we can't write it to disk.
742	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		// This is not yet finished and the signature may change....
759		hidden,
760	)]
761	/// Mount a particular title as if it were a disc.
762	///
763	/// ## Errors
764	///
765	/// - If we cannot remove any existing disc that may be present.
766	/// - If we cannot copy the title to the disc id path.
767	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		// Once again clear any disc contents so users aren't confused.
774		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		// Create the only file we do actually serve... `diskid.bin`.
779		_ = self.disk_id_path().await?;
780
781		// Perform our 'virtual' mount.
782		{
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	/// Get the path to the current firmware file to boot on the MION.
791	///
792	/// This is guaranteed to always exist, as it's part of our check for a
793	/// corrupt SDK.
794	#[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	/// Get the path to the disk layout file for the PPC booting process.
803	///
804	/// This function will create a disk layout file, as well as a Boot System
805	/// File, and a disk id file if they do not yet exist.
806	///
807	/// ## Errors
808	///
809	/// - If the temp directory does not exist, and we can't create it.
810	/// - If the boot system file does not exist, and we can't write it to disk.
811	/// - If the diskid file does not exist, and we can't write it to disk.
812	/// - If the firmware image file does not exist.
813	/// - If the dlf file does not exist, and we can't create it.
814	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			// This probably isn't the right set of defaults for everyone, but i'm
824			// not yet smart enough to figure all this out.
825			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	/// Check if a path is allowed to be writable.
838	#[must_use]
839	pub fn path_allows_writes(&self, path: &Path) -> bool {
840		// TODO(mythra): check FSEmulAttributeRules
841		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	/// Given a UTF-8 string path, get a pathbuf reference.
856	///
857	/// This understands the current following implementations:
858	///
859	/// - `/%MLC_EMU_DIR`
860	/// - `/%SLC_EMU_DIR`
861	/// - `/%DISC_EMU_DIR`
862	/// - `/%SAVE_EMU_DIR`
863	/// - `/%NETWORK`
864	///
865	/// Most of these are just quick ways of referncing the current set of
866	/// directories, within cafe sdk. `%NETWORK` is the special one which
867	/// references a currently mounted network share.
868	///
869	/// ## Errors
870	///
871	/// If the path requested is not in a mounted path.
872	pub async fn resolve_path(
873		&self,
874		potentially_prefixed_path: &str,
875	) -> Result<ResolvedLocation, CatBridgeError> {
876		// Requests coming may optionally have `/vol/pc` prefixed if they're built
877		// wrong.
878		//
879		// Or if a user is trying to get cat-dev style paths working with this api
880		// directly. CLean it up.
881		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		// We can't actually just call `canonicalize`, as that will fail if the
906		// file doesn't exist, and we could be requesting to resolve a path we want
907		// to turn around and create.
908		//
909		// So instead we try to canonicalize to the closest possible directory, and
910		// check if it is underneath our directory.
911		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		// We failed to find any directory, which means we're nowhere close to
923		// where we want to be.
924		if closest_canonical_directory.as_os_str().is_empty() {
925			return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
926		}
927		// Check for mapped directories...
928		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			// Check if this path needs to be handled by NUS.
939			let mut base_mlc_title_dir = self.mlc_path();
940			base_mlc_title_dir.push("usr");
941			base_mlc_title_dir.push("title");
942			// Only check for things that aren't locally existing first.
943			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				// Get <group id>/<title id>
951				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	/// Create a directory within a particular path.
984	///
985	/// ## Errors
986	///
987	/// If we cannot end up creating this directory due to a filesystem error.
988	pub fn create_directory(&self, at: &Path) -> Result<(), FSError> {
989		create_dir_all_sync(at).map_err(FSError::IO)
990	}
991
992	/// Copy a file, symlink, or directory.
993	///
994	/// ## Errors
995	///
996	/// If we run into any filesystem error renaming a source, or directory.
997	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	/// Rename a file, symlink, or directory.
1006	///
1007	/// This is implemented so we can rename directories, and files without
1008	/// having to worry about the logic. Especially given the fact the built in
1009	/// rename doesn't support directories.
1010	///
1011	/// ## Errors
1012	///
1013	/// - If we run into any filesystem error renaming a source, or directory.
1014	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	/// Get a file from the SLC.
1023	///
1024	/// The SLC always serves "sys" files, and are relative to a title id, almost
1025	/// always a system title id such as (`00050010`).
1026	///
1027	/// *note: the file is not guaranteed to exist! It's just a path!*
1028	#[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	/// Get the path to the current MLC directory.
1044	#[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	/// Get the current OS's default directory path.
1050	///
1051	/// For Windows this is: `C:\cafe_sdk`.
1052	/// For Unix/BSD likes this is: `/opt/cafe_sdk`
1053	#[allow(
1054    // Not actually unreachable unless on unsupported OS.
1055    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	/// Get the current path to the temporary directory for this Cafe SDK
1079	/// install.
1080	///
1081	/// ## Errors
1082	///
1083	/// - If the temporary path does not exist and could not be created.
1084	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	/// Replace a particular emu directory string in a path.
1101	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	/// "Map" a disc directory to the title that is actively mounted.
1115	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				// System-Config Tool.
1124				(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	/// Attempt to create all save directories if we're creating them.
1151	async fn ensure_save_dir_exists(&self, path: &Path) {
1152		// First off let's make sure we're dealing with something in the cafe SDK
1153		// path, and not trying to recreate a directory that already exists.
1154		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		// Validate we're in data/[slc|mlc]/[sys|usr]/save/[gid]/[tid]/
1162		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		// Get the group id/title id.
1185		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		// Only create when the actual title exists on disk.
1199		//
1200		// Don't claim we have save file directories for titles we don't have installed.
1201		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		// First we need to check if we're even on a temporary filesystem/path.
1235		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					// Don't need to patch directories that don't exist.
1263					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		// We don't lowercase anything underneath `/content/` as many games expect
1310		// specific capitilization, and we don't have anything in base system that
1311		// do require specific validation.
1312		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	/// Enusre an SDK path is ready for serving this means:
1361	///
1362	/// - Create some configuration files that SDKs don't come with, but will
1363	///   help the OS boot up.
1364	/// - Mount the `DISC` directory if one is not present.
1365	/// - Copy RPLs from SCT (System Config Tool), to OS title IDs.
1366	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		// Unmount any leftover discs....
1388		//
1389		// Our disc mounting is 'virtual'/'in-memory', and thus we don't want a user
1390		// to get confused when they wonder why the files on their local hard drive
1391		// aren't actually getting mounted.
1392		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					// If this symlink is a symlink to another path within the same
1447					// directory, then rewrite it as well to start under our new directory.
1448					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	/// Copy the RPLs from one directory recursively to another.
1486	///
1487	/// This is a very specialized function that we need to use to move system
1488	/// libraries into the correct place for the system to be able to find them.
1489	/// Sometimes system libraries need to be in multiple places.
1490	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			// Skip over anything that isn't a concrete RPL.
1503			if rpb.extension().unwrap_or_default().to_string_lossy() != "rpl" || rpb.is_symlink() {
1504				continue;
1505			}
1506			// Okay we've got an RPL! Let's rename it...
1507			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			// Don't overwrite potential user data.
1515			if !as_new_path.exists() {
1516				copy_file_sync(rpb, as_new_path)?;
1517			}
1518		}
1519
1520		Ok(())
1521	}
1522
1523	/// Rename an entire directory.
1524	///
1525	/// We have to implement this ourselves, because [`tokio::fs::rename`], and
1526	/// [`std::fs::rename`] don't support renaming a directory at all on windows,
1527	/// which is one of the critical OS's that we need to support.
1528	///
1529	/// This 'rename' works by actually creating a new directory. Then
1530	/// moving all the files over with rename. This is slow, but
1531	/// works.
1532	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					// If this symlink is a symlink to another path within the same
1555					// directory, then rewrite it as well to start under our new directory.
1556					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				// Symlinks to directories on Windows run into
1568				// edge cases, and will frequently get permission denied when
1569				// attempting to remove them.
1570				//
1571				// They will instead be cleaned up by the final folder cleanup which
1572				// will not run into any such errors.
1573				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				// Remove the original link, we renamed this....
1595				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		// Clean up after ourselves...
1605		remove_dir_all_sync(source_path)?;
1606
1607		Ok(())
1608	}
1609
1610	/// Generate an `eco.xml` if one is not present.
1611	///
1612	/// This is _required_ in order to provide an actual functional PCFS install,
1613	/// and not actually normally created on the host filesystem with the
1614	/// official tools. It just generates it in memory.
1615	///
1616	/// ## Errors
1617	///
1618	/// If we cannot create the config directory, or write the eco config
1619	/// file to disk.
1620	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	/// Generate a `wii_acct.xml` if one is not present.
1653	///
1654	/// This is _required_ in order to provide an actual functional PCFS install,
1655	/// and not actually normally created on the host filesystem with the
1656	/// official tools. It just generates it in memory.
1657	///
1658	/// ## Errors
1659	///
1660	/// If we cannot create the config directory, or write the wii acct config
1661	/// file to disk.
1662	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	/// Generate an `rmtCfg.xml` if one is not present.
1704	///
1705	/// This is not _required_ to boot a cat-dev, but certain games & titles users
1706	/// boot may require access to this file in order to start.
1707	///
1708	/// ## Errors
1709	///
1710	/// If we cannot create the config directory, or write the rmt config
1711	/// file to disk.
1712	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	/// Test helper that creates a simple host filesystem.
1811	#[allow(
1812		// Allow anyone to write a test for this internally on any feature set.
1813		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			// Create data directories
1820			vec!["data", "slc"],
1821			vec!["data", "mlc"],
1822			vec!["data", "disc"],
1823			vec!["data", "save"],
1824			// Create necessary to pass checks.
1825			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			// Purposefully create capital so we can validate renaming works!
1838			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		// Place files that need to exist, they are not real, but enough to "fool"
1862		// our basic check.
1863		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		// Validate that our functions that create files can actually, well, create
1932		// those files.
1933		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		// Can't generate firmware files for now.
1990		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		// DIRECTORIES BESIDES DISC should allow writes.
2034		// unless excluded by fsemul attrs.
2035		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		// Validate that our functions that create files can actually, well, create
2045		// those files.
2046		let (tempdir, fs) = create_temporary_host_filesystem().await;
2047
2048		// Validate each of the regular directories work.
2049		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		// Validate that paths outside of our root directory don't work.
2080		let mut out_of_path = PathBuf::from(tempdir.path());
2081		// We now left tempdir, and this path isn't mounted, so we should error out
2082		// on this.
2083		out_of_path.pop();
2084
2085		// We shouldn't be able to resolve paths outside of our directory.
2086		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		// Should be possible to read all bytes.
2188		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		// Can read all bytes again!
2198		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		// Only `c`, `d`, and `f` should be returned.
2244		//
2245		// `e` is a symlink   (ignored)
2246		// `d/a` is an item in a subdirectory (ignored)
2247		_ = 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		// We should have hit the end...
2284		assert!(fs.next_in_folder(dfd, None).await.is_none());
2285		// We can call as many times as we want.
2286		assert!(fs.next_in_folder(dfd, None).await.is_none());
2287		// Rewind to get to reads again!
2288		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}