cat_dev/fsemul/
host_filesystem.rs

1//! A representation of the filesystem folder we end up serving a cat-dev
2//! client.
3
4use crate::{
5	TitleID,
6	errors::{CatBridgeError, FSError},
7	fsemul::{
8		bsf::BootSystemFile,
9		dlf::DiskLayoutFile,
10		errors::{FSEmulAPIError, FSEmulFSError},
11		pcfs::errors::PcfsApiError,
12	},
13};
14use bytes::{Bytes, BytesMut};
15use scc::{
16	HashMap as ConcurrentMap, HashSet as ConcurrentSet, hash_map::OccupiedEntry as CMOccupiedEntry,
17};
18use std::{
19	collections::HashMap,
20	ffi::OsString,
21	fs::{
22		DirEntry, copy as copy_file_sync, create_dir_all as create_dir_all_sync,
23		read_dir as read_dir_sync, read_link as read_link_sync,
24		remove_dir_all as remove_dir_all_sync, remove_file as remove_file_sync,
25		rename as rename_sync,
26	},
27	hash::RandomState,
28	io::{Error as IOError, SeekFrom},
29	path::{Path, PathBuf},
30	sync::{
31		Arc,
32		atomic::{AtomicBool, AtomicI32, Ordering as AtomicOrdering},
33	},
34};
35use tokio::{
36	fs::{File, OpenOptions, read as fs_read, write as fs_write},
37	io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
38	sync::Mutex,
39};
40use tracing::{info, warn};
41use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
42use walkdir::WalkDir;
43use whoami::username;
44
45/// A way to create truly unique file fd's. Just a counter going up.
46static UNIQUE_FILE_FD: AtomicI32 = AtomicI32::new(1);
47/// Current "FD" for directories. Just a counter going up.
48static FOLDER_FD: AtomicI32 = AtomicI32::new(1);
49
50/// A wrapper around interacting with the 'host' or PC filesystem for the
51/// various times a cat-dev will reach out to the host.
52///
53/// This is little more than a wrapper around a [`PathBuf`], and targeted
54/// methods to make getting files/generating default files/etc. easy. Most of
55/// the actual logic for turning a request from `SDIO`, `ATAPI`, etc. all come
56/// from those client/server implementations rather than the logic living here.
57#[allow(
58	// Clippy the type is _not_ that complex.
59	clippy::type_complexity,
60)]
61#[derive(Clone, Debug)]
62pub struct HostFilesystem {
63	/// The path to the base data directory to serve a filesystem out of.
64	cafe_sdk_path: PathBuf,
65	/// The actively mounted "disc".
66	///
67	/// This is a tuple of (isSLC, isSystem, [`TitleID`]).
68	///
69	/// When a disc is mounted we will copy the title from SLC/MLC
70	/// directory, into `disc/` recursively.
71	disc_mounted: Arc<Mutex<Option<(bool, bool, TitleID)>>>,
72	/// List of open file handles.
73	///
74	/// This contains a value of (file, file size, path, stream owner).
75	open_file_handles: Arc<ConcurrentMap<i32, (File, u64, PathBuf, Option<u64>)>>,
76	/// List of open folder "handles".
77	///
78	/// This contains a value of (directory items, index, is end, path, stream owner)
79	open_folder_handles:
80		Arc<ConcurrentMap<i32, (Vec<DirEntry>, usize, bool, PathBuf, Option<u64>)>>,
81	/// A set of folders that we've "marked" as read-only.
82	///
83	/// We don't actually synchronize this to the filesystem because the original
84	/// cafe-sdk was written for Windows 7 which silently ignores "Read-Only"
85	/// attributes on directories. Still allowing you to create files within
86	/// directories, modify them, etc.
87	///
88	/// This is not the case on older windows distributions, unix based distros,
89	/// or similar.
90	folders_marked_read_only: Arc<ConcurrentSet<PathBuf>>,
91	/// If we are forcing unique file fd's. This should only be changed
92	/// if we have not opened a file yet.
93	is_using_unique_fds: bool,
94	/// If we've opened a file, used to safely ensure we don't switch from
95	/// unique fdf's to not.
96	has_opened_file: Arc<AtomicBool>,
97}
98
99impl HostFilesystem {
100	/// Create a filesystem from a root cafe dir.
101	///
102	/// If no cafe dir is provided, we will attempt to locate the default
103	/// installation path for cafe sdk which is:
104	///
105	/// - `C:\cafe_sdk` on windows.
106	/// - `/opt/cafe_sdk` on any unix/bsd like OS.
107	///
108	/// NOTE: This will validate that all title id paths are lowercase, as
109	/// files are always expected to be lowercase when dealing with CAFE. Other
110	/// files are usually kept in the correct naming format. HOWEVER, users may
111	/// notice spurious errors with case-insensitivity on linux specifically. If
112	/// transferring an SDK from a Windows/Mac Case Insensitive to a Mac/Linux
113	/// case sensitive file system. It is recommended users
114	/// create their own folder using our recovery tools, rather than
115	/// rsync'ing a path over from case-insensitive, to case-sensitive.
116	///
117	/// ## Errors
118	///
119	/// If the Cafe SDK folder is corrupt, or can't be found. A Cafe SDK
120	/// folder is considered corrupt if it is missing core files that we
121	/// _need_ to be able to serve a Cafe-OS distribution. These file
122	/// requirements may change from version to version of this crate, but should
123	/// always be compatible with a clean cafe sdk folder.
124	pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
125		let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_folder) else {
126			return Err(FSEmulFSError::CantFindCafeSdkPath.into());
127		};
128
129		Self::patch_case_sensitivity(&cafe_sdk_path).await?;
130
131		for path in [
132			&[
133				"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
134			] as &[&str],
135			&[
136				"data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
137			],
138			&[
139				"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
140			],
141			&[
142				"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
143			],
144			&[
145				"data", "mlc", "sys", "title", "00050010", "1f700500", "content",
146			],
147			&[
148				"data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
149			],
150			// Can't generate a `fw.img` for now.... :(
151			&[
152				"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
153			],
154		] {
155			if !Self::join_many(&cafe_sdk_path, path).exists() {
156				return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
157			}
158		}
159
160		Self::prepare_for_serving(&cafe_sdk_path).await?;
161		let ro_folders = Self::get_default_read_only_folders(&cafe_sdk_path);
162
163		Ok(Self {
164			cafe_sdk_path,
165			disc_mounted: Arc::new(Mutex::new(None)),
166			folders_marked_read_only: Arc::new(ro_folders),
167			open_file_handles: Arc::new(ConcurrentMap::new()),
168			open_folder_handles: Arc::new(ConcurrentMap::new()),
169			is_using_unique_fds: false,
170			has_opened_file: Arc::new(AtomicBool::new(false)),
171		})
172	}
173
174	/// The root path to the Cafe SDK.
175	///
176	/// *note: although we do expose this for logging, and other info... we do
177	/// not recommend manually interacting with the SDK path. There are much
178	/// better alternatives.*
179	#[must_use]
180	pub const fn cafe_sdk_path(&self) -> &PathBuf {
181		&self.cafe_sdk_path
182	}
183
184	/// The root path to the Cafe SDK.
185	///
186	/// *note: although we do expose this for logging, and other info... we do
187	/// not recommend manually interacting with the SDK path. There are much
188	/// better alternatives.*
189	#[must_use]
190	pub fn disc_emu_path(&self) -> PathBuf {
191		Self::join_many(&self.cafe_sdk_path, ["data", "disc"])
192	}
193
194	/// Force unique file descriptors for open files.
195	///
196	/// Certain OS's _can_ return duplicate fd's especially when opening,
197	/// and closing files. This can make deciphering logs harder because the
198	/// same FD may appear multiple times, when you're trying to just find
199	/// the logs related to one file descriptor.
200	///
201	/// When unique fd's is turned on, similar to folders we just use a global
202	/// wrapping counter so that way every file descriptor is guaranteed to be
203	/// unique.
204	///
205	/// ## Errors
206	///
207	/// This will error if any file has ever been opened. This is because once
208	/// a client has already connected, and done some stuff with file stuff it
209	/// expects one set of behaviors, we cannot change another one.
210	pub fn force_unique_fds(&mut self) -> Result<(), FSEmulAPIError> {
211		if self.has_opened_file.load(AtomicOrdering::Relaxed) {
212			Err(FSEmulAPIError::CannotSwapFdStrategy)
213		} else {
214			self.is_using_unique_fds = true;
215			Ok(())
216		}
217	}
218
219	/// Open a file, and return it's file descriptor number.
220	///
221	/// ## Errors
222	///
223	/// If we cannot open our file with the open options provided.
224	pub async fn open_file(
225		&self,
226		open_options: OpenOptions,
227		path: &PathBuf,
228		stream_owner: Option<u64>,
229	) -> Result<i32, FSError> {
230		self.has_opened_file.store(true, AtomicOrdering::Relaxed);
231		let fd = open_options.open(path).await?;
232		let raw_fd;
233		#[cfg(unix)]
234		{
235			use std::os::fd::AsRawFd;
236			raw_fd = fd.as_raw_fd();
237		}
238		#[cfg(target_os = "windows")]
239		{
240			use std::os::windows::io::AsRawHandle;
241			raw_fd = fd.as_raw_handle() as i32;
242		}
243
244		let md = fd.metadata().await?;
245		let final_fd = if self.is_using_unique_fds {
246			UNIQUE_FILE_FD.fetch_add(1, AtomicOrdering::SeqCst)
247		} else {
248			raw_fd
249		};
250
251		self.open_file_handles
252			.insert_async(final_fd, (fd, md.len(), path.clone(), stream_owner))
253			.await
254			.map_err(|_| IOError::other("somehow got duplicate fd?"))?;
255		Ok(final_fd)
256	}
257
258	/// Get a file from a file descriptor number.
259	///
260	/// This file must already be opened (in order to get the file descriptor).
261	pub async fn get_file(
262		&self,
263		fd: i32,
264		for_stream: Option<u64>,
265	) -> Option<CMOccupiedEntry<'_, i32, (File, u64, PathBuf, Option<u64>), RandomState>> {
266		self.open_file_handles
267			.get_async(&fd)
268			.await
269			.and_then(|entry| {
270				if Self::allow_file_access(&entry, for_stream) {
271					Some(entry)
272				} else {
273					None
274				}
275			})
276	}
277
278	/// Get the file length from a file descriptor number.
279	///
280	/// This file must already be opened (in order to get the file descriptor).
281	pub async fn file_length(&self, fd: i32, for_stream: Option<u64>) -> Option<u64> {
282		self.open_file_handles
283			.get_async(&fd)
284			.await
285			.and_then(|entry| {
286				if Self::allow_file_access(&entry, for_stream) {
287					Some(entry.1)
288				} else {
289					None
290				}
291			})
292	}
293
294	/// Read from a file descriptor that is actively open.
295	///
296	/// This will read from a currently open file descriptor, in it's current
297	/// location. You might want to set your file location for this FD before
298	/// if you aren't already in the same location.
299	///
300	/// ## Errors
301	///
302	/// If the file descriptor is open, but we could not read from the open file
303	/// descriptor.
304	pub async fn read_file(
305		&self,
306		fd: i32,
307		mut total_data_to_read: usize,
308		for_stream: Option<u64>,
309	) -> Result<Option<Bytes>, FSError> {
310		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
311			return Ok(None);
312		};
313		if !Self::allow_file_access(&real_entry, for_stream) {
314			return Ok(None);
315		}
316		let file_reader = &mut real_entry.0;
317
318		let mut file_buff = BytesMut::zeroed(total_data_to_read);
319		let mut total_bytes_read = 0_usize;
320		while total_data_to_read > 0 {
321			let bytes_read = file_reader.read(&mut file_buff[total_bytes_read..]).await?;
322			if bytes_read == 0 {
323				break;
324			}
325			total_data_to_read -= bytes_read;
326			total_bytes_read += bytes_read;
327		}
328		if file_buff.len() > total_bytes_read {
329			file_buff.truncate(total_bytes_read);
330		}
331
332		Ok(Some(file_buff.freeze()))
333	}
334
335	/// Write to a file descriptor that is actively open.
336	///
337	/// This will write from a currently open file descriptor, in it's current
338	/// location. You might want to set your file location for this FD before
339	/// if you aren't already in the same location.
340	///
341	/// ## Errors
342	///
343	/// If the file descriptor is open, but we could not write to the open file
344	/// descriptor.
345	pub async fn write_file(
346		&self,
347		fd: i32,
348		data_to_write: Bytes,
349		for_stream: Option<u64>,
350	) -> Result<(), FSError> {
351		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
352			return Err(FSError::IO(IOError::other("file not open")));
353		};
354		if !Self::allow_file_access(&real_entry, for_stream) {
355			return Err(FSError::IO(IOError::other("file not open")));
356		}
357		let file_writer = &mut real_entry.0;
358		file_writer.write_all(&data_to_write).await?;
359
360		Ok(())
361	}
362
363	/// Seek to the beginning or end of a file.
364	///
365	/// If `begin` is true then we will seek to the beginning of the file
366	/// otherwise we will sync to the end of the file. Precise seeking is _not_
367	/// supported at this time.
368	///
369	/// ## Errors
370	///
371	/// If we cannot seek to the beginning or end of the file.
372	pub async fn seek_file(
373		&self,
374		fd: i32,
375		begin: bool,
376		for_stream: Option<u64>,
377	) -> Result<(), FSError> {
378		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
379			return Ok(());
380		};
381		if !Self::allow_file_access(&real_entry, for_stream) {
382			return Ok(());
383		}
384		let file_reader = &mut real_entry.0;
385
386		if begin {
387			file_reader.seek(SeekFrom::Start(0)).await?;
388		} else {
389			file_reader.seek(SeekFrom::End(0)).await?;
390		}
391
392		Ok(())
393	}
394
395	/// Decrement the ref count of handles to a file.
396	///
397	/// If ref count reaches 0 close the underlying file handle.
398	///
399	/// ## Errors
400	///
401	/// If we cannot close our file handle when our ref count reaches 0, or if
402	/// the file isn't open at all.
403	pub async fn close_file(&self, fd: i32, for_stream: Option<u64>) {
404		if let Some(entry) = self.open_file_handles.get_async(&fd).await
405			&& !Self::allow_file_access(&entry, for_stream)
406		{
407			// Don't allow streams to close other streams files.
408			return;
409		}
410
411		self.open_file_handles.remove_async(&fd).await;
412	}
413
414	/// "Open" a folder, or an iterator over a directory.
415	///
416	/// There's no real "open file handle", or reversible directory iterator,
417	/// so we just create an id from scratch.
418	///
419	/// ## Errors
420	///
421	/// If the path doesn't exist, then we can't open the folder.
422	pub fn open_folder(&self, path: &PathBuf, for_stream: Option<u64>) -> Result<i32, FSError> {
423		let mut dhandle = read_dir_sync(path)?
424			.filter_map(Result::ok)
425			.collect::<Vec<_>>();
426		dhandle.sort_by_key(DirEntry::path);
427
428		let fake_fd = FOLDER_FD.fetch_add(1, AtomicOrdering::SeqCst);
429
430		self.open_folder_handles
431			.insert_sync(fake_fd, (dhandle, 0, false, path.clone(), for_stream))
432			.map_err(|_| IOError::other("OS returned duplicate fd?"))?;
433		Ok(fake_fd)
434	}
435
436	/// Mark a folder as being 'read-only' for this session.
437	///
438	/// ## Errors
439	///
440	/// If we could not actually insert the folder into the read only map.
441	pub async fn mark_folder_read_only(&self, path: PathBuf) {
442		_ = self.folders_marked_read_only.insert_async(path).await;
443	}
444
445	/// Mark a folder as being 'read-write' for this session.
446	pub async fn ensure_folder_not_read_only(&self, path: &PathBuf) {
447		self.folders_marked_read_only.remove_async(path).await;
448	}
449
450	/// Check if a folder is marked as being read only.
451	pub async fn folder_is_read_only(&self, path: &PathBuf) -> bool {
452		self.folders_marked_read_only.contains_async(path).await
453	}
454
455	/// Get the next filename/foldername available in a particular folder, and
456	/// how many pieces to remove to get just the filename.
457	///
458	/// This will always return none even if it's already at the end, unlike a
459	/// particular iterator.
460	///
461	/// ## Errors
462	///
463	/// If we get an IO error from the underlying filesystem.
464	pub async fn next_in_folder(
465		&self,
466		fd: i32,
467		for_stream: Option<u64>,
468	) -> Result<Option<(PathBuf, usize)>, FSError> {
469		let Some(mut entry) = self.open_folder_handles.get_async(&fd).await else {
470			return Ok(None);
471		};
472		if !Self::allow_folder_access(&entry, for_stream) {
473			return Ok(None);
474		}
475
476		let component_count = entry.3.components().count();
477		let mut value: Option<PathBuf> = None;
478		if !entry.2 {
479			loop {
480				if entry.1 < entry.0.len() {
481					let ref_value = entry.0[entry.1].path();
482					entry.1 += 1;
483
484					if (!ref_value.is_file() && !ref_value.is_dir()) || ref_value.is_symlink() {
485						continue;
486					}
487
488					value = Some(ref_value);
489				}
490
491				break;
492			}
493			if value.is_none() {
494				entry.2 = true;
495			}
496		}
497
498		Ok(value.map(|val| (val, component_count)))
499	}
500
501	/// Reverse a particular iterator over a folder by one.
502	///
503	/// Note: This will recreate the directory iterator, and will temporarily
504	/// hold _two_ references to [`ReadDir`] at a time because the underlying
505	/// iterator from read directory is not a reversible iterator.
506	///
507	/// ## Errors
508	///
509	/// If opening another read dir call does not work.
510	pub async fn reverse_folder(&self, fd: i32, for_stream: Option<u64>) -> Result<(), FSError> {
511		let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
512			return Ok(());
513		};
514		if !Self::allow_folder_access(&real_entry, for_stream) {
515			return Ok(());
516		}
517		if real_entry.1 == 0 {
518			return Ok(());
519		}
520
521		real_entry.1 -= 1;
522		real_entry.2 = false;
523		Ok(())
524	}
525
526	/// Decrement the ref count of handles to a folder.
527	///
528	/// If ref count reaches 0 close the underlying folder handle.
529	///
530	/// ## Errors
531	///
532	/// If we cannot close our folder handle when our ref count reaches 0, or if
533	/// the folder isn't open at all.
534	pub async fn close_folder(&self, fd: i32, for_stream: Option<u64>) {
535		if let Some(real_entry) = self.open_folder_handles.get_async(&fd).await
536			&& !Self::allow_folder_access(&real_entry, for_stream)
537		{
538			return;
539		}
540
541		self.open_folder_handles.remove_async(&fd).await;
542	}
543
544	/// Get the path to the current boot1 `.bsf` file.
545	///
546	/// This function will create the boot1 system file, if it does not yet
547	/// exist. As a result it may error, if we can't create, and place the
548	/// boot system file.
549	///
550	/// ## Errors
551	///
552	/// - If the temp directory does not exist, and we can't create it.
553	/// - If the boot system file does not exist, and we can't write it to disk.
554	pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, FSError> {
555		let mut path = self.temp_path()?;
556		path.push("caferun");
557		if !path.exists() {
558			create_dir_all_sync(&path)?;
559		}
560		path.push("ppc.bsf");
561
562		if !path.exists() {
563			fs_write(&path, Bytes::from(BootSystemFile::default())).await?;
564		}
565
566		Ok(path)
567	}
568
569	/// Get the path to the current `diskid.bin`.
570	///
571	/// If the current Disk ID does not exist, we will write a blank diskid to
572	/// this path.
573	///
574	/// ## Errors
575	///
576	/// - If the temporary directory does not exist, and we can't create it.
577	/// - If the disk ID path does not exist, and we can't write it to disk.
578	pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
579		let mut path = self.temp_path()?;
580		path.push("caferun");
581		if !path.exists() {
582			create_dir_all_sync(&path)?;
583		}
584		path.push("diskid.bin");
585
586		if !path.exists() {
587			fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
588		}
589
590		Ok(path)
591	}
592
593	#[doc(
594		// This is not yet finished and the signature may change....
595		hidden,
596	)]
597	/// Mount a particular title as if it were a disc.
598	///
599	/// ## Errors
600	///
601	/// - If we cannot remove any existing disc that may be present.
602	/// - If we cannot copy the title to the disc id path.
603	pub async fn mount_disk_title(
604		&mut self,
605		is_slc: bool,
606		is_sys: bool,
607		title_id: TitleID,
608	) -> Result<(), FSError> {
609		let source_path = Self::join_many(
610			&self.cafe_sdk_path,
611			[
612				"data".to_owned(),
613				if is_slc { "slc" } else { "mlc" }.to_owned(),
614				if is_sys { "sys" } else { "usr" }.to_owned(),
615				"title".to_owned(),
616				format!("{:08x}", title_id.0),
617				format!("{:08x}", title_id.1),
618			],
619		);
620		let dest_path = Self::join_many(&self.cafe_sdk_path, ["data", "disc"]);
621		if dest_path.exists() {
622			remove_dir_all_sync(&dest_path).map_err(FSError::IO)?;
623		}
624
625		Self::copy_dir(&source_path, &dest_path)?;
626		// Mount was successful!
627		{
628			let mut guard = self.disc_mounted.lock().await;
629			guard.replace((is_slc, is_sys, title_id));
630		}
631		todo!("figure out how to mount diskid.bin")
632	}
633
634	/// Get the path to the current firmware file to boot on the MION.
635	///
636	/// This is guaranteed to always exist, as it's part of our check for a
637	/// corrupt SDK.
638	#[must_use]
639	pub fn firmware_file_path(&self) -> PathBuf {
640		Self::join_many(
641			&self.slc_path_for((0x0005_0010, 0x1000_400A)),
642			["code", "fw.img"],
643		)
644	}
645
646	/// Get the path to the disk layout file for the PPC booting process.
647	///
648	/// This function will create a disk layout file, as well as a Boot System
649	/// File, and a disk id file if they do not yet exist.
650	///
651	/// ## Errors
652	///
653	/// - If the temp directory does not exist, and we can't create it.
654	/// - If the boot system file does not exist, and we can't write it to disk.
655	/// - If the diskid file does not exist, and we can't write it to disk.
656	/// - If the firmware image file does not exist.
657	/// - If the dlf file does not exist, and we can't create it.
658	pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
659		let mut path = self.temp_path()?;
660		path.push("caferun");
661		if !path.exists() {
662			create_dir_all_sync(&path).map_err(FSError::from)?;
663		}
664		path.push("ppc_boot.dlf");
665
666		if !path.exists() {
667			// This probably isn't the right set of defaults for everyone, but i'm
668			// not yet smart enough to figure all this out.
669			let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
670			root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
671			root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
672			root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
673			fs_write(&path, Bytes::from(root_dlf))
674				.await
675				.map_err(FSError::from)?;
676		}
677
678		Ok(path)
679	}
680
681	/// Check if a path is allowed to be writable.
682	#[must_use]
683	pub fn path_allows_writes(&self, path: &Path) -> bool {
684		// TODO(mythra): check FSEmulAttributeRules
685		let lossy_path = path.to_string_lossy();
686		let trimmed_lossy_path = lossy_path
687			.trim_start_matches("/vol/pc")
688			.trim_start_matches('/');
689		if trimmed_lossy_path.starts_with("%DISC_EMU_DIR") {
690			return trimmed_lossy_path.starts_with("%DISC_EMU_DIR/save");
691		}
692		if path.starts_with(Self::join_many(&self.cafe_sdk_path, ["data", "disc"])) {
693			return path.starts_with(Self::join_many(
694				&self.cafe_sdk_path,
695				["data", "disc", "save"],
696			));
697		}
698
699		true
700	}
701
702	/// Given a UTF-8 string path, get a pathbuf reference.
703	///
704	/// This understands the current following implementations:
705	///
706	/// - `/%MLC_EMU_DIR`
707	/// - `/%SLC_EMU_DIR`
708	/// - `/%DISC_EMU_DIR`
709	/// - `/%SAVE_EMU_DIR`
710	/// - `/%NETWORK`
711	///
712	/// Most of these are just quick ways of referncing the current set of
713	/// directories, within cafe sdk. `%NETWORK` is the special one which
714	/// references a currently mounted network share.
715	///
716	/// ## Errors
717	///
718	/// If the path requested is not in a mounted path.
719	pub fn resolve_path(
720		&self,
721		potentially_prefixed_path: &str,
722	) -> Result<ResolvedLocation, CatBridgeError> {
723		// Requests coming may optionally have `/vol/pc` prefixed if they're built
724		// wrong.
725		//
726		// Or if a user is trying to get cat-dev style paths working with this api
727		// directly. CLean it up.
728		let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
729		if path.starts_with("/%NETWORK") {
730			todo!("NETWORK shares not yet implemented :( sorry!")
731		}
732
733		let non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
734			self.replace_emu_dir(path, "mlc")
735		} else if path.starts_with("/%SLC_EMU_DIR") {
736			self.replace_emu_dir(path, "slc")
737		} else if path.starts_with("/%DISC_EMU_DIR") {
738			self.replace_emu_dir(path, "disc")
739		} else if path.starts_with("/%SAVE_EMU_DIR") {
740			self.replace_emu_dir(path, "save")
741		} else {
742			PathBuf::from(path)
743		};
744
745		// We can't actually just call `canonicalize`, as that will fail if the
746		// file doesn't exist, and we could be requesting to resolve a path we want
747		// to turn around and create.
748		//
749		// So instead we try to canonicalize to the closest possible directory, and
750		// check if it is underneath our directory.
751		let mut closest_canonical_directory = non_canonical_path.clone();
752		let mut changed_at_all = false;
753		while !closest_canonical_directory.as_os_str().is_empty() {
754			if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
755				closest_canonical_directory = canonicalized;
756				break;
757			}
758
759			changed_at_all = true;
760			closest_canonical_directory.pop();
761		}
762		// We failed to find any directory, which means we're nowhere close to
763		// where we want to be.
764		if closest_canonical_directory.as_os_str().is_empty() {
765			return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
766		}
767		// Check for mapped directories...
768		let canonicalized_cafe = self
769			.cafe_sdk_path()
770			.canonicalize()
771			.unwrap_or_else(|_| self.cafe_sdk_path().clone());
772		if !closest_canonical_directory.starts_with(canonicalized_cafe) {
773			return Err(PcfsApiError::PathNotMapped(path.to_owned()).into());
774		}
775
776		Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
777			non_canonical_path,
778			closest_canonical_directory,
779			!changed_at_all,
780		)))
781	}
782
783	/// Create a directory within a particular path.
784	///
785	/// ## Errors
786	///
787	/// If we cannot end up creating this directory due to a filesystem error.
788	pub fn create_directory(&self, at: &Path) -> Result<(), FSError> {
789		create_dir_all_sync(at).map_err(FSError::IO)
790	}
791
792	/// Copy a file, symlink, or directory.
793	///
794	/// ## Errors
795	///
796	/// If we run into any filesystem error renaming a source, or directory.
797	pub fn copy(&self, from: &Path, to: &Path) -> Result<(), FSError> {
798		if from.is_dir() {
799			Self::copy_dir(from, to)
800		} else {
801			copy_file_sync(from, to).map_err(FSError::IO).map(|_| ())
802		}
803	}
804
805	/// Rename a file, symlink, or directory.
806	///
807	/// This is implemented so we can rename directories, and files without
808	/// having to worry about the logic. Especially given the fact the built in
809	/// rename doesn't support directories.
810	///
811	/// ## Errors
812	///
813	/// - If we run into any filesystem error renaming a source, or directory.
814	pub fn rename(&self, from: &Path, to: &Path) -> Result<(), FSError> {
815		if from.is_dir() {
816			Self::rename_dir(from, to)
817		} else {
818			rename_sync(from, to).map_err(FSError::IO)
819		}
820	}
821
822	/// Get a file from the SLC.
823	///
824	/// The SLC always serves "sys" files, and are relative to a title id, almost
825	/// always a system title id such as (`00050010`).
826	///
827	/// *note: the file is not guaranteed to exist! It's just a path!*
828	#[must_use]
829	pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
830		Self::join_many(
831			&self.cafe_sdk_path,
832			[
833				"data".to_owned(),
834				"slc".to_owned(),
835				"sys".to_owned(),
836				"title".to_owned(),
837				format!("{:08x}", title_id.0),
838				format!("{:08x}", title_id.1),
839			],
840		)
841	}
842
843	/// Get the current OS's default directory path.
844	///
845	/// For Windows this is: `C:\cafe_sdk`.
846	/// For Unix/BSD likes this is: `/opt/cafe_sdk`
847	#[allow(
848    // Not actually unreachable unless on unsupported OS.
849    unreachable_code,
850  )]
851	#[must_use]
852	pub fn default_cafe_folder() -> Option<PathBuf> {
853		#[cfg(target_os = "windows")]
854		{
855			return Some(PathBuf::from(r"C:\cafe_sdk"));
856		}
857
858		#[cfg(any(
859			target_os = "linux",
860			target_os = "freebsd",
861			target_os = "openbsd",
862			target_os = "netbsd",
863			target_os = "macos"
864		))]
865		{
866			return Some(PathBuf::from("/opt/cafe_sdk"));
867		}
868
869		None
870	}
871
872	/// Get the current path to the temporary directory for this Cafe SDK
873	/// install.
874	///
875	/// ## Errors
876	///
877	/// - If the temporary path does not exist and could not be created.
878	fn temp_path(&self) -> Result<PathBuf, FSError> {
879		let temp_path = Self::join_many(
880			&self.cafe_sdk_path,
881			["temp".to_owned(), username().to_lowercase()],
882		);
883		if !temp_path.exists() {
884			create_dir_all_sync(&temp_path)?;
885		}
886		Ok(temp_path)
887	}
888
889	/// A small utility function to join many paths into a single path effeciently.
890	#[must_use]
891	fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
892	where
893		PathTy: AsRef<Path>,
894		IterTy: IntoIterator<Item = PathTy>,
895	{
896		let mut as_owned = PathBuf::from(base);
897		for part in parts {
898			as_owned = as_owned.join(part.as_ref());
899		}
900		as_owned
901	}
902
903	/// Replace a particular emu directory string in a path.
904	fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
905		let path_minus = path
906			.trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
907			.trim_start_matches('/')
908			.trim_start_matches('\\')
909			.replace('\\', "/");
910
911		Self::join_many(
912			&Self::join_many(self.cafe_sdk_path(), ["data", dir]),
913			path_minus.split('/'),
914		)
915	}
916
917	async fn patch_case_sensitivity(cafe_sdk_path: &Path) -> Result<(), FSError> {
918		// First we need to check if we're even on a temporary filesystem/path.
919		if !cafe_sdk_path.exists() {
920			return Ok(());
921		}
922		let capital_path = Self::join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
923		let _ = File::create(&capital_path).await?;
924		let is_insensitive = File::open(Self::join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
925			.await
926			.is_ok();
927		remove_file_sync(capital_path)?;
928		if is_insensitive {
929			return Ok(());
930		}
931
932		info!(
933			"Your Host OS is not case-insensitive for file-paths... ensuring CafeSDK is all lowercase, this may take awhile..."
934		);
935		let cafe_sdk_components = cafe_sdk_path.components().count();
936		let mut had_rename = true;
937		while had_rename {
938			had_rename = false;
939			for directory in [
940				Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
941				Self::join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
942				Self::join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
943				Self::join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
944			] {
945				if !directory.exists() {
946					// Don't need to patch directories that don't exist.
947					continue;
948				}
949
950				let mut iter = WalkDir::new(&directory)
951					.contents_first(false)
952					.follow_links(false)
953					.follow_root_links(false)
954					.into_iter();
955				while let Some(Ok(entry)) = iter.next() {
956					let p = entry.path();
957					if !p.exists() {
958						continue;
959					}
960
961					let path_minus_cafe = p
962						.components()
963						.skip(cafe_sdk_components)
964						.collect::<PathBuf>();
965					let Some(path_as_utf8) = path_minus_cafe.as_os_str().to_str() else {
966						warn!(problematic_path = %p.display(), "Path in Cafe SDK directory is not UTF-8! This may cause errors fetching!");
967						continue;
968					};
969					let new_path = path_as_utf8.to_ascii_lowercase();
970					if path_as_utf8 != new_path {
971						let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
972						final_new_path.push("/");
973						final_new_path.push(&new_path);
974						let new = PathBuf::from(final_new_path);
975
976						if p.is_dir() {
977							Self::rename_dir(p, &new)?;
978							had_rename = true;
979						} else {
980							rename_sync(p, new)?;
981							had_rename = true;
982						}
983					}
984				}
985			}
986		}
987		info!("ensure CafeSDK path is now case-insensitive by renaming to all lowercase...");
988
989		Ok(())
990	}
991
992	fn allow_file_access(
993		entry: &CMOccupiedEntry<i32, (File, u64, PathBuf, Option<u64>), RandomState>,
994		requester: Option<u64>,
995	) -> bool {
996		let Some(requesting_stream_id) = requester else {
997			return true;
998		};
999		let Some(owned_stream_id) = entry.3 else {
1000			return true;
1001		};
1002
1003		requesting_stream_id == owned_stream_id
1004	}
1005
1006	#[allow(
1007		// TODO(mythra): fix
1008		clippy::type_complexity
1009	)]
1010	fn allow_folder_access(
1011		entry: &CMOccupiedEntry<
1012			i32,
1013			(Vec<DirEntry>, usize, bool, PathBuf, Option<u64>),
1014			RandomState,
1015		>,
1016		requester: Option<u64>,
1017	) -> bool {
1018		let Some(requesting_stream_id) = requester else {
1019			return true;
1020		};
1021		let Some(owned_stream_id) = entry.4 else {
1022			return true;
1023		};
1024
1025		requesting_stream_id == owned_stream_id
1026	}
1027
1028	/// Enusre an SDK path is ready for serving this means:
1029	///
1030	/// - Create some configuration files that SDKs don't come with, but will
1031	///   help the OS boot up.
1032	/// - Mount the `DISC` directory if one is not present.
1033	async fn prepare_for_serving(cafe_sdk_path: &Path) -> Result<(), FSError> {
1034		if !Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "config", "eco.xml"]).exists() {
1035			Self::generate_eco_xml(cafe_sdk_path).await?;
1036		}
1037		if !Self::join_many(
1038			cafe_sdk_path,
1039			["data", "slc", "sys", "proc", "prefs", "wii_acct.xml"],
1040		)
1041		.exists()
1042		{
1043			Self::generate_wii_acct_xml(cafe_sdk_path).await?;
1044		}
1045
1046		// Unmount any leftover discs....
1047		if Self::join_many(cafe_sdk_path, ["data", "disc"]).exists() {
1048			remove_dir_all_sync(Self::join_many(cafe_sdk_path, ["data", "disc"]))
1049				.map_err(FSError::IO)?;
1050		}
1051		// Manually mount in SysConfigTool.....
1052		//
1053		// This doesn't actually create a discid.bin, but the files do exist.
1054		let disc_dir = Self::join_many(cafe_sdk_path, ["data", "disc"]);
1055		let sctt_dir = Self::join_many(
1056			cafe_sdk_path,
1057			["data", "mlc", "sys", "title", "00050010", "1f700500"],
1058		);
1059		for subpath in ["code", "content", "meta"] {
1060			Self::copy_dir(
1061				&Self::join_many(&sctt_dir, [subpath]),
1062				&Self::join_many(&disc_dir, [subpath]),
1063			)?;
1064		}
1065		// Manually capitilize the title id in app.xml, the normal PCFS
1066		// tooling does this, even though it is case-insensitive, but for matching.
1067		let app_xml_path = Self::join_many(cafe_sdk_path, ["data", "disc", "code", "app.xml"]);
1068		// app.xml must be utf-8 to be read by the OS completely, so if we end up
1069		// writing a corrupt app.xml, would be the exact same as the OS
1070		// interpreting that.
1071		let base_app_xml = String::from_utf8_lossy(&fs_read(&app_xml_path).await?).to_string();
1072		fs_write(&app_xml_path, Self::capitilize_title_id(base_app_xml)).await?;
1073
1074		Ok(())
1075	}
1076
1077	fn copy_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1078		if !dest_path.exists() {
1079			create_dir_all_sync(dest_path)?;
1080		}
1081		let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1082		let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1083
1084		for result in WalkDir::new(source_path)
1085			.follow_links(false)
1086			.follow_root_links(false)
1087		{
1088			let rpb = result?.into_path();
1089			let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1090			let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1091			new_bytes.extend_from_slice(new_path_as_str_bytes);
1092			new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1093			let as_new_path =
1094				PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1095
1096			if rpb.is_symlink() {
1097				let mut resolved_path = read_link_sync(&rpb)?;
1098				{
1099					// If this symlink is a symlink to another path within the same
1100					// directory, then rewrite it as well to start under our new directory.
1101					let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1102					if os_str_for_resolved.starts_with(old_path_bytes) {
1103						let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1104						new_bytes.extend_from_slice(new_path_as_str_bytes);
1105						new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1106						resolved_path = PathBuf::from(unsafe {
1107							OsString::from_encoded_bytes_unchecked(new_bytes)
1108						});
1109					}
1110				}
1111
1112				#[cfg(unix)]
1113				{
1114					use std::os::unix::fs::symlink;
1115					symlink(resolved_path, &as_new_path)?;
1116				}
1117
1118				#[cfg(target_os = "windows")]
1119				{
1120					use std::os::windows::fs::{symlink_dir, symlink_file};
1121
1122					if resolved_path.is_dir() {
1123						symlink_dir(resolved_path, &as_new_path)?;
1124					} else {
1125						symlink_file(resolved_path, &as_new_path)?;
1126					}
1127				}
1128			} else if rpb.is_file() {
1129				copy_file_sync(&rpb, &as_new_path)?;
1130			} else if rpb.is_dir() {
1131				create_dir_all_sync(&as_new_path)?;
1132			}
1133		}
1134
1135		Ok(())
1136	}
1137
1138	/// Rename an entire directory.
1139	///
1140	/// We have to implement this ourselves, because [`tokio::fs::rename`], and
1141	/// [`std::fs::rename`] don't support renaming a directory at all on windows,
1142	/// which is one of the critical OS's that we need to support.
1143	///
1144	/// This 'rename' works by actually creating a new directory. Then
1145	/// moving all the files over with rename. This is slow, but
1146	/// works.
1147	fn rename_dir(source_path: &Path, dest_path: &Path) -> Result<(), FSError> {
1148		if !dest_path.exists() {
1149			create_dir_all_sync(dest_path)?;
1150		}
1151		let new_path_as_str_bytes = dest_path.as_os_str().as_encoded_bytes();
1152		let old_path_bytes = source_path.as_os_str().as_encoded_bytes();
1153
1154		for result in WalkDir::new(source_path)
1155			.follow_links(false)
1156			.follow_root_links(false)
1157		{
1158			let rpb = result?.into_path();
1159			let os_str_for_entry = rpb.as_os_str().as_encoded_bytes();
1160			let mut new_bytes = Vec::with_capacity(os_str_for_entry.len() + 3);
1161			new_bytes.extend_from_slice(new_path_as_str_bytes);
1162			new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1163			let as_new_path =
1164				PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(new_bytes) });
1165
1166			if rpb.is_symlink() {
1167				let mut resolved_path = read_link_sync(&rpb)?;
1168				{
1169					// If this symlink is a symlink to another path within the same
1170					// directory, then rewrite it as well to start under our new directory.
1171					let os_str_for_resolved = resolved_path.as_os_str().as_encoded_bytes();
1172					if os_str_for_resolved.starts_with(old_path_bytes) {
1173						let mut new_bytes = Vec::with_capacity(os_str_for_resolved.len() + 3);
1174						new_bytes.extend_from_slice(new_path_as_str_bytes);
1175						new_bytes.extend_from_slice(&os_str_for_entry[old_path_bytes.len()..]);
1176						resolved_path = PathBuf::from(unsafe {
1177							OsString::from_encoded_bytes_unchecked(new_bytes)
1178						});
1179					}
1180				}
1181
1182				// Symlinks to directories on Windows run into
1183				// edge cases, and will frequently get permission denied when
1184				// attempting to remove them.
1185				//
1186				// They will instead be cleaned up by the final folder cleanup which
1187				// will not run into any such errors.
1188				let should_remove: bool;
1189				#[cfg(unix)]
1190				{
1191					use std::os::unix::fs::symlink;
1192					symlink(resolved_path, &as_new_path)?;
1193					should_remove = true;
1194				}
1195
1196				#[cfg(target_os = "windows")]
1197				{
1198					use std::os::windows::fs::{symlink_dir, symlink_file};
1199
1200					if resolved_path.is_dir() {
1201						symlink_dir(resolved_path, &as_new_path)?;
1202						should_remove = false;
1203					} else {
1204						symlink_file(resolved_path, &as_new_path)?;
1205						should_remove = true;
1206					}
1207				}
1208
1209				// Remove the original link, we renamed this....
1210				if should_remove {
1211					remove_file_sync(&rpb)?;
1212				}
1213			} else if rpb.is_file() {
1214				rename_sync(&rpb, &as_new_path)?;
1215			} else if rpb.is_dir() {
1216				create_dir_all_sync(&as_new_path)?;
1217			}
1218		}
1219		// Clean up after ourselves...
1220		remove_dir_all_sync(source_path)?;
1221
1222		Ok(())
1223	}
1224
1225	/// Generate an `eco.xml` if one is not present.
1226	///
1227	/// This is _required_ in order to provide an actual functional PCFS install,
1228	/// and not actually normally created on the host filesystem with the
1229	/// official tools. It just generates it in memory.
1230	///
1231	/// ## Errors
1232	///
1233	/// If we cannot create the config directory, or write the eco config
1234	/// file to disk.
1235	async fn generate_eco_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1236		let mut eco_path = Self::join_many(cafe_os_path, ["data", "slc", "sys", "config"]);
1237		if !eco_path.exists() {
1238			create_dir_all_sync(&eco_path).map_err(FSError::IO)?;
1239		}
1240		eco_path.push("eco.xml");
1241
1242		let mut eco_file = File::create(eco_path).await.map_err(FSError::IO)?;
1243		eco_file
1244			.write_all(
1245				br#"<?xml version="1.0" encoding="utf-8"?>
1246<eco type="complex" access="777">
1247  <enable type="unsignedInt" length="4">0</enable>
1248  <max_on_time type="unsignedInt" length="4">3601</max_on_time>
1249  <default_off_time type="unsignedInt" length="4">15</default_off_time>
1250  <wd_disable type="unsignedInt" length="4">1</wd_disable>
1251</eco>"#,
1252			)
1253			.await
1254			.map_err(FSError::IO)?;
1255
1256		#[cfg(unix)]
1257		{
1258			use std::{fs::Permissions, os::unix::prelude::*};
1259			eco_file
1260				.set_permissions(Permissions::from_mode(0o770))
1261				.await?;
1262		}
1263
1264		Ok(())
1265	}
1266
1267	/// Generate a `wii_acct.xml` if one is not present.
1268	///
1269	/// This is _required_ in order to provide an actual functional PCFS install,
1270	/// and not actually normally created on the host filesystem with the
1271	/// official tools. It just generates it in memory.
1272	///
1273	/// ## Errors
1274	///
1275	/// If we cannot create the config directory, or write the wii acct config
1276	/// file to disk.
1277	async fn generate_wii_acct_xml(cafe_os_path: &Path) -> Result<(), FSError> {
1278		let mut wii_path = Self::join_many(cafe_os_path, ["data", "slc", "sys", "proc", "prefs"]);
1279		if !wii_path.exists() {
1280			create_dir_all_sync(&wii_path).map_err(FSError::IO)?;
1281		}
1282		wii_path.push("wii_acct.xml");
1283
1284		let mut wii_file = File::create(wii_path).await.map_err(FSError::IO)?;
1285		wii_file
1286			.write_all(
1287				br#"<?xml version="1.0" encoding="utf-8"?>
1288<wii_acct type="complex">
1289  <profile type="complex">
1290    <nickname type="hexBinary" length="22">00570069006900000000000000000000000000000000</nickname>
1291
1292    <language type="unsignedInt" length="4">0</language>
1293    <country type="unsignedInt" length="4">1</country>
1294  </profile>
1295  <pc type="complex">
1296    <rating type="unsignedInt" length="4">18</rating>
1297    <organization type="unsignedInt" length="4">0</organization>
1298    <rst_internet_ch type="unsignedByte" length="1">0</rst_internet_ch>
1299    <rst_nw_access type="unsignedByte" length="1">0</rst_nw_access>
1300    <rst_pt_order type="unsignedByte" length="1">0</rst_pt_order>
1301  </pc>
1302</wii_acct>"#,
1303			)
1304			.await
1305			.map_err(FSError::IO)?;
1306
1307		#[cfg(unix)]
1308		{
1309			use std::{fs::Permissions, os::unix::prelude::*};
1310			wii_file
1311				.set_permissions(Permissions::from_mode(0o770))
1312				.await?;
1313		}
1314
1315		Ok(())
1316	}
1317
1318	/// Take an app.xml, and capitilize the title id. Used for byte-matching
1319	/// perfectly with the official SDK.
1320	#[must_use]
1321	fn capitilize_title_id(app_xml: String) -> String {
1322		let Some(title_id_xml_tag_start) = app_xml.find("<title_id") else {
1323			return app_xml;
1324		};
1325		let Some(title_id_tag_end) = app_xml[title_id_xml_tag_start..].find('>') else {
1326			return app_xml;
1327		};
1328
1329		let tid_start = title_id_xml_tag_start + title_id_tag_end;
1330		let Some(title_slash_location) = app_xml[tid_start..].find("</title_id>") else {
1331			return app_xml;
1332		};
1333		let tid_end = tid_start + title_slash_location;
1334		let title_id = &app_xml[tid_start..tid_end];
1335		let mut final_xml = String::with_capacity(app_xml.len());
1336		final_xml += &app_xml[..tid_start];
1337		final_xml += &title_id.to_uppercase();
1338		final_xml += &app_xml[tid_end..];
1339
1340		final_xml
1341	}
1342
1343	fn get_default_read_only_folders(cafe_dir: &Path) -> ConcurrentSet<PathBuf> {
1344		let set = ConcurrentSet::new();
1345
1346		for cafe_sub_paths in [
1347			&["data", "slc", "sys", "config"] as &[&str],
1348			&["data", "slc", "sys", "proc"],
1349			&["data", "slc", "sys", "logs"],
1350			&["data", "mlc", "usr"],
1351			&["data", "mlc", "usr", "import"],
1352			&["data", "mlc", "usr", "title"],
1353		] {
1354			_ = set.insert_sync(Self::join_many(cafe_dir, cafe_sub_paths));
1355		}
1356
1357		set
1358	}
1359}
1360
1361const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
1362	NamedField::new("cafe_sdk_path"),
1363	NamedField::new("open_file_handles"),
1364	NamedField::new("open_folder_handles"),
1365];
1366
1367impl Structable for HostFilesystem {
1368	fn definition(&self) -> StructDef<'_> {
1369		StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
1370	}
1371}
1372
1373impl Valuable for HostFilesystem {
1374	fn as_value(&self) -> Value<'_> {
1375		Value::Structable(self)
1376	}
1377
1378	fn visit(&self, visitor: &mut dyn Visit) {
1379		let mut values = HashMap::with_capacity(self.open_file_handles.len());
1380		self.open_file_handles.iter_sync(|k, v| {
1381			values.insert(*k, format!("{}", v.2.display()));
1382			true
1383		});
1384		let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
1385		self.open_folder_handles.iter_sync(|k, v| {
1386			folder_values.insert(*k, format!("{}", v.3.display()));
1387			true
1388		});
1389
1390		visitor.visit_named_fields(&NamedValues::new(
1391			HOST_FILESYSTEM_FIELDS,
1392			&[
1393				Valuable::as_value(&self.cafe_sdk_path),
1394				Valuable::as_value(&values),
1395				Valuable::as_value(&folder_values),
1396			],
1397		));
1398	}
1399}
1400
1401/// A resolved location given an arbitrary path.
1402#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
1403pub enum ResolvedLocation {
1404	/// A location on a particular filesystem.
1405	///
1406	/// This location _may not exist_. There is a boolean in this struct that
1407	/// tells you the final resolved location, the closest resolved location for
1408	/// permission checks, and if the path exists and is the same between
1409	/// resolution, and what actually exists.
1410	Filesystem(FilesystemLocation),
1411	/// A network location to fetch.
1412	///
1413	/// TODO(mythra): figure out type.
1414	Network(()),
1415}
1416
1417/// A location that's been resolved, and is guaranteed to be in one of our
1418/// mounted paths.
1419#[derive(Clone, Debug, PartialEq, Eq)]
1420pub struct FilesystemLocation {
1421	/// The final resolved path (may not exist).
1422	resolved_path: PathBuf,
1423	/// The resolved path that may not be the same as the final path, but is
1424	/// enough to confirm we're in the same directory.
1425	closest_resolved_path: PathBuf,
1426	/// If the canonicalized path is the same as the resolved path.
1427	canonicalized_is_exact: bool,
1428}
1429impl FilesystemLocation {
1430	#[must_use]
1431	pub const fn new(
1432		resolved_path: PathBuf,
1433		closest_resolved_path: PathBuf,
1434		canonicalized_is_exact: bool,
1435	) -> Self {
1436		Self {
1437			resolved_path,
1438			closest_resolved_path,
1439			canonicalized_is_exact,
1440		}
1441	}
1442
1443	#[must_use]
1444	pub const fn resolved_path(&self) -> &PathBuf {
1445		&self.resolved_path
1446	}
1447	#[must_use]
1448	pub const fn closest_resolved_path(&self) -> &PathBuf {
1449		&self.closest_resolved_path
1450	}
1451	#[must_use]
1452	pub const fn canonicalized_is_exact(&self) -> bool {
1453		self.canonicalized_is_exact
1454	}
1455}
1456
1457const FILESYSTEM_LOCATION_FIELDS: &[NamedField<'static>] = &[
1458	NamedField::new("resolved_path"),
1459	NamedField::new("closest_resolved_path"),
1460	NamedField::new("canonicalized_is_exact"),
1461];
1462
1463impl Structable for FilesystemLocation {
1464	fn definition(&self) -> StructDef<'_> {
1465		StructDef::new_static(
1466			"FilesystemLocation",
1467			Fields::Named(FILESYSTEM_LOCATION_FIELDS),
1468		)
1469	}
1470}
1471
1472impl Valuable for FilesystemLocation {
1473	fn as_value(&self) -> Value<'_> {
1474		Value::Structable(self)
1475	}
1476
1477	fn visit(&self, visitor: &mut dyn Visit) {
1478		visitor.visit_named_fields(&NamedValues::new(
1479			FILESYSTEM_LOCATION_FIELDS,
1480			&[
1481				Valuable::as_value(&self.resolved_path),
1482				Valuable::as_value(&self.closest_resolved_path),
1483				Valuable::as_value(&self.canonicalized_is_exact),
1484			],
1485		));
1486	}
1487}
1488
1489#[cfg_attr(docsrs, doc(cfg(test)))]
1490#[cfg(test)]
1491pub mod test_helpers {
1492	use super::*;
1493	use std::fs::{File, create_dir_all};
1494	use tempfile::{TempDir, tempdir};
1495
1496	/// Test helper that creates a simple host filesystem.
1497	#[allow(
1498		// Allow anyone to write a test for this internally on any feature set.
1499		dead_code,
1500	)]
1501	pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
1502		let dir = tempdir().expect("Failed to create temporary directory!");
1503
1504		for directory_to_create in vec![
1505			// Create data directories
1506			vec!["data", "slc"],
1507			vec!["data", "mlc"],
1508			vec!["data", "disc"],
1509			vec!["data", "save"],
1510			// Create necessary to pass checks.
1511			vec![
1512				"data", "mlc", "sys", "title", "00050030", "1001000a", "code",
1513			],
1514			vec![
1515				"data", "mlc", "sys", "title", "00050010", "1f700500", "code",
1516			],
1517			vec![
1518				"data", "mlc", "sys", "title", "00050010", "1f700500", "content",
1519			],
1520			vec![
1521				"data", "mlc", "sys", "title", "00050010", "1f700500", "meta",
1522			],
1523			// Purposefully create capital so we can validate renaming works!
1524			vec![
1525				"data", "mlc", "sys", "title", "00050030", "1001010A", "code",
1526			],
1527			vec![
1528				"data", "mlc", "sys", "title", "00050030", "1001020a", "code",
1529			],
1530			vec![
1531				"data", "slc", "sys", "title", "00050010", "1000400a", "code",
1532			],
1533			vec!["data", "mlc", "sys", "update", "nand", "os_v10_ndebug"],
1534			vec!["data", "mlc", "sys", "update", "nand", "os_v10_debug"],
1535			vec!["data", "slc", "sys", "proc", "prefs"],
1536			vec![
1537				"data", "slc", "sys", "title", "00050010", "1000800a", "code",
1538			],
1539			vec![
1540				"data", "slc", "sys", "title", "00050010", "1000400a", "code",
1541			],
1542		] {
1543			create_dir_all(HostFilesystem::join_many(dir.path(), directory_to_create))
1544				.expect("Failed to create directories necessary for host filesystem to work.");
1545		}
1546
1547		// Place files that need to exist, they are not real, but enough to "fool"
1548		// our basic check.
1549		File::create(HostFilesystem::join_many(
1550			dir.path(),
1551			[
1552				"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
1553			],
1554		))
1555		.expect("Failed to create needed app.xml!");
1556		File::create(HostFilesystem::join_many(
1557			dir.path(),
1558			[
1559				"data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
1560			],
1561		))
1562		.expect("Failed to create needed app.xml!");
1563		File::create(HostFilesystem::join_many(
1564			dir.path(),
1565			[
1566				"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
1567			],
1568		))
1569		.expect("Failed to create needed app.xml!");
1570
1571		File::create(HostFilesystem::join_many(
1572			dir.path(),
1573			[
1574				"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
1575			],
1576		))
1577		.expect("Failed to create needed fw.img!");
1578		File::create(HostFilesystem::join_many(
1579			dir.path(),
1580			[
1581				"data", "mlc", "sys", "title", "00050010", "1f700500", "code", "app.xml",
1582			],
1583		))
1584		.expect("Failed to create needed app.xml for disc!");
1585
1586		let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
1587			.await
1588			.expect("Failed to load empty host filesystem!");
1589
1590		(dir, fs)
1591	}
1592
1593	/// Re-export host file system join many for tests.
1594	#[allow(
1595		// Allow anyone to write a test for this internally on any feature set.
1596		dead_code,
1597	)]
1598	#[must_use]
1599	pub fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
1600	where
1601		PathTy: AsRef<Path>,
1602		IterTy: IntoIterator<Item = PathTy>,
1603	{
1604		HostFilesystem::join_many(base, parts)
1605	}
1606}
1607
1608#[cfg(test)]
1609mod unit_tests {
1610	use super::test_helpers::*;
1611	use super::*;
1612	use std::fs::read;
1613
1614	fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
1615
1616	#[test]
1617	pub fn is_send_sync() {
1618		only_accepts_send_sync::<HostFilesystem>(None);
1619	}
1620
1621	#[test]
1622	pub fn can_find_default_cafe_directory() {
1623		assert!(
1624			HostFilesystem::default_cafe_folder().is_some(),
1625			"Failed to find default cafe directory for your OS",
1626		);
1627	}
1628
1629	#[tokio::test]
1630	pub async fn creatable_files() {
1631		// Validate that our functions that create files can actually, well, create
1632		// those files.
1633		let (tempdir, fs) = create_temporary_host_filesystem().await;
1634
1635		let expected_bsf_path = HostFilesystem::join_many(
1636			tempdir.path(),
1637			[
1638				"temp".to_owned(),
1639				username().to_lowercase(),
1640				"caferun".to_owned(),
1641				"ppc.bsf".to_owned(),
1642			],
1643		);
1644		assert!(
1645			!expected_bsf_path.exists(),
1646			"ppc.bsf existed before we asked for it?"
1647		);
1648		let bsf_path = fs
1649			.boot1_sytstem_path()
1650			.await
1651			.expect("Failed to create bsf!");
1652		assert_eq!(expected_bsf_path, bsf_path);
1653		assert!(
1654			BootSystemFile::try_from(Bytes::from(
1655				read(bsf_path).expect("Failed to read written boot system file!")
1656			))
1657			.is_ok(),
1658			"Failed to read generated boot system file!"
1659		);
1660
1661		let expected_diskid_path = HostFilesystem::join_many(
1662			tempdir.path(),
1663			[
1664				"temp".to_owned(),
1665				username().to_lowercase(),
1666				"caferun".to_owned(),
1667				"diskid.bin".to_owned(),
1668			],
1669		);
1670		assert!(
1671			!expected_diskid_path.exists(),
1672			"diskid.bin existed before we asked for it?"
1673		);
1674		let diskid_path = fs
1675			.disk_id_path()
1676			.await
1677			.expect("Failed to create diskid.bin!");
1678		assert_eq!(expected_diskid_path, diskid_path);
1679		assert_eq!(
1680			read(diskid_path).expect("Failed to read written diskid.bin!"),
1681			vec![0; 32],
1682			"Failed to read generated diskid.bin!"
1683		);
1684
1685		// Can't generate firmware files for now.
1686		assert_eq!(
1687			fs.firmware_file_path(),
1688			HostFilesystem::join_many(
1689				tempdir.path(),
1690				[
1691					"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"
1692				],
1693			),
1694		);
1695
1696		let expected_ppc_boot_dlf_path = HostFilesystem::join_many(
1697			tempdir.path(),
1698			[
1699				"temp".to_owned(),
1700				username().to_lowercase(),
1701				"caferun".to_owned(),
1702				"ppc_boot.dlf".to_owned(),
1703			],
1704		);
1705		assert!(
1706			!expected_ppc_boot_dlf_path.exists(),
1707			"ppc_boot.dlf existed before we asked for it?"
1708		);
1709		let ppc_boot_dlf_path = fs
1710			.ppc_boot_dlf_path()
1711			.await
1712			.expect("Failed to create ppc_boot.dlf!");
1713		assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
1714		assert!(
1715			DiskLayoutFile::try_from(Bytes::from(
1716				read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
1717			))
1718			.is_ok(),
1719			"Failed to read generated ppc_boot.dlf!"
1720		);
1721	}
1722
1723	#[tokio::test]
1724	pub async fn path_allows_writes() {
1725		let (_tempdir, fs) = create_temporary_host_filesystem().await;
1726
1727		// DIRECTORIES BESIDES DISC should allow writes.
1728		// unless excluded by fsemul attrs.
1729		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
1730		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
1731		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
1732		assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1733		assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1734	}
1735
1736	#[tokio::test]
1737	pub async fn resolve_path() {
1738		// Validate that our functions that create files can actually, well, create
1739		// those files.
1740		let (tempdir, fs) = create_temporary_host_filesystem().await;
1741
1742		// Validate each of the regular directories work.
1743		for (dir, name) in [
1744			("/%MLC_EMU_DIR", "mlc"),
1745			("/%SLC_EMU_DIR", "slc"),
1746			("/%DISC_EMU_DIR", "disc"),
1747			("/%SAVE_EMU_DIR", "save"),
1748		] {
1749			assert!(
1750				fs.resolve_path(&format!("{dir}")).is_ok(),
1751				"Failed to resolve: `{}`: {:?}",
1752				dir,
1753				fs.resolve_path(&format!("{dir}"))
1754			);
1755			assert!(
1756				fs.resolve_path(&format!("{dir}/")).is_ok(),
1757				"Failed to resolve: `{}/`",
1758				dir,
1759			);
1760			assert!(
1761				fs.resolve_path(&format!("{dir}/./")).is_ok(),
1762				"Failed to resolve: `{}/./`",
1763				dir,
1764			);
1765			assert!(
1766				fs.resolve_path(&format!("{dir}/../{name}")).is_ok(),
1767				"Failed to resolve: `{}/../{}`",
1768				dir,
1769				name,
1770			);
1771		}
1772
1773		// Validate that paths outside of our root directory don't work.
1774		let mut out_of_path = PathBuf::from(tempdir.path());
1775		// We now left tempdir, and this path isn't mounted, so we should error out
1776		// on this.
1777		out_of_path.pop();
1778
1779		// We shouldn't be able to resolve paths outside of our directory.
1780		assert!(
1781			fs.resolve_path(
1782				&out_of_path
1783					.clone()
1784					.into_os_string()
1785					.into_string()
1786					.expect("Failed to convert pathbuf to string!")
1787			)
1788			.is_err()
1789		);
1790		assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").is_err());
1791
1792		#[cfg(unix)]
1793		{
1794			use std::os::unix::fs::symlink;
1795
1796			let mut tempdir_symlink = PathBuf::from(tempdir.path());
1797			tempdir_symlink.push("symlink");
1798			symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1799			assert!(
1800				fs.resolve_path(&format!(
1801					"{}/symlink",
1802					tempdir_symlink
1803						.into_os_string()
1804						.into_string()
1805						.expect("tempdir symlink wasn't utf8?"),
1806				))
1807				.is_err()
1808			);
1809		}
1810
1811		#[cfg(target_os = "windows")]
1812		{
1813			use std::os::windows::fs::symlink_dir;
1814
1815			let mut tempdir_symlink = PathBuf::from(tempdir.path());
1816			tempdir_symlink.push("symlink");
1817			symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1818			assert!(
1819				fs.resolve_path(&format!(
1820					"{}/symlink",
1821					tempdir_symlink
1822						.into_os_string()
1823						.into_string()
1824						.expect("tempdir symlink wasn't utf8?"),
1825				))
1826				.is_err()
1827			);
1828		}
1829	}
1830
1831	#[tokio::test]
1832	pub async fn opening_files() {
1833		let (tempdir, fs) = create_temporary_host_filesystem().await;
1834		let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1835		tokio::fs::write(path.clone(), vec![0; 1307])
1836			.await
1837			.expect("Failed to write test file!");
1838		let create_path = HostFilesystem::join_many(tempdir.path(), ["new-file.txt"]);
1839
1840		let mut oo = OpenOptions::new();
1841		oo.create(false).write(true).read(true);
1842		assert!(
1843			fs.open_file(oo, &create_path, None).await.is_err(),
1844			"Somehow succeeding opening a file that doesn't exist with no create flag?",
1845		);
1846		oo = OpenOptions::new();
1847		oo.create(true).write(true).truncate(true);
1848		let fd = fs
1849			.open_file(oo, &create_path, None)
1850			.await
1851			.expect("Failed opening a file that doesn't exist with a create flag?");
1852		assert!(
1853			fs.open_file_handles.len() == 1 && fs.open_file_handles.get_sync(&fd).is_some(),
1854			"Open file wasn't in open files list!",
1855		);
1856		fs.close_file(fd, None).await;
1857		assert!(
1858			fs.open_file_handles.is_empty(),
1859			"Somehow after opening/closing, open file handles was not empty?",
1860		);
1861	}
1862
1863	#[tokio::test]
1864	pub async fn seek_and_read() {
1865		let (tempdir, fs) = create_temporary_host_filesystem().await;
1866		let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1867		tokio::fs::write(path.clone(), vec![0; 1307])
1868			.await
1869			.expect("Failed to write test file!");
1870
1871		let mut oo = OpenOptions::new();
1872		oo.read(true).create(false).write(false);
1873		let fd = fs
1874			.open_file(oo, &path, None)
1875			.await
1876			.expect("Failed to open existing file!");
1877
1878		// Should be possible to read all bytes.
1879		assert_eq!(
1880			Some(BytesMut::zeroed(1307).freeze()),
1881			fs.read_file(fd, 1307, None)
1882				.await
1883				.expect("Failed to read from FD!"),
1884		);
1885		fs.seek_file(fd, true, None)
1886			.await
1887			.expect("Failed to sync to beginning of file!");
1888		// Can read all bytes again!
1889		assert_eq!(
1890			Some(BytesMut::zeroed(1307).freeze()),
1891			fs.read_file(fd, 1307, None)
1892				.await
1893				.expect("Failed to read from FD!"),
1894		);
1895		fs.close_file(fd, None).await;
1896		assert!(
1897			fs.open_file_handles.is_empty(),
1898			"Somehow after opening/closing, open file handles was not empty?",
1899		);
1900	}
1901
1902	#[tokio::test]
1903	pub async fn open_and_close_folder() {
1904		let (tempdir, fs) = create_temporary_host_filesystem().await;
1905		let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1906		tokio::fs::create_dir_all(path.clone())
1907			.await
1908			.expect("Failed to create test directory!");
1909
1910		let fd = fs
1911			.open_folder(&path, None)
1912			.expect("Failed to open existing folder!");
1913		assert!(
1914			fs.open_folder_handles.len() == 1,
1915			"Expected one open folder handle",
1916		);
1917		fs.close_folder(fd, None).await;
1918
1919		assert!(
1920			fs.open_folder_handles.is_empty(),
1921			"Somehow after opening/closing, open folder handles was not empty?",
1922		);
1923	}
1924
1925	#[tokio::test]
1926	pub async fn seek_within_folder() {
1927		let (tempdir, fs) = create_temporary_host_filesystem().await;
1928		let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1929		tokio::fs::create_dir_all(path.clone())
1930			.await
1931			.expect("Failed to create test directory!");
1932
1933		// Only `c`, `d`, and `f` should be returned.
1934		//
1935		// `e` is a symlink   (ignored)
1936		// `d/a` is an item in a subdirectory (ignored)
1937		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["c"]))
1938			.await
1939			.expect("Failed to create file to use!");
1940		tokio::fs::create_dir(HostFilesystem::join_many(&path, ["d"]))
1941			.await
1942			.expect("Failed to create directory to use!");
1943		#[cfg(unix)]
1944		{
1945			use std::os::unix::fs::symlink;
1946
1947			let mut tempdir_symlink = path.clone();
1948			tempdir_symlink.push("e");
1949			symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1950		}
1951		#[cfg(target_os = "windows")]
1952		{
1953			use std::os::windows::fs::symlink_dir;
1954
1955			let mut tempdir_symlink = path.clone();
1956			tempdir_symlink.push("e");
1957			symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1958		}
1959		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["f"]))
1960			.await
1961			.expect("Failed to create file to use!");
1962		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["d", "a"]))
1963			.await
1964			.expect("Failed to create file to use!");
1965
1966		let dfd = fs.open_folder(&path, None).expect("Failed to open folder!");
1967		assert!(
1968			fs.next_in_folder(dfd, None)
1969				.await
1970				.expect("Failed to query for next in folder! 1.1!")
1971				.is_some()
1972		);
1973		assert!(
1974			fs.next_in_folder(dfd, None)
1975				.await
1976				.expect("Failed to query for next in folder! 1.2!")
1977				.is_some()
1978		);
1979		assert!(
1980			fs.next_in_folder(dfd, None)
1981				.await
1982				.expect("Failed to query for next in folder! 1.3!")
1983				.is_some()
1984		);
1985		// We should have hit the end...
1986		assert!(
1987			fs.next_in_folder(dfd, None)
1988				.await
1989				.expect("Failed to query for next in folder! 1.4!")
1990				.is_none()
1991		);
1992		// We can call as many times as we want.
1993		assert!(
1994			fs.next_in_folder(dfd, None)
1995				.await
1996				.expect("Failed to query for next in folder! 1.5!")
1997				.is_none()
1998		);
1999		// Rewind to get to reads again!
2000		fs.reverse_folder(dfd, None)
2001			.await
2002			.expect("Failed to reverse directory search!");
2003		assert!(
2004			fs.next_in_folder(dfd, None)
2005				.await
2006				.expect("Failed to query for next in folder! 2.1!")
2007				.is_some()
2008		);
2009		assert!(
2010			fs.next_in_folder(dfd, None)
2011				.await
2012				.expect("Failed to query for next in folder! 2.2!")
2013				.is_none()
2014		);
2015	}
2016
2017	#[test]
2018	pub fn can_capitilize_ids() {
2019		assert_eq!(
2020			HostFilesystem::capitilize_title_id(
2021				r#"<?xml version="1.0" encoding = "utf-8"?>
2022<app type="complex" access="777">
2023  <version type="unsignedInt" length="4">16</version>
2024  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2025  <title_id type="hexBinary" length="8">000500101f700500</title_id>
2026  <title_version type="hexBinary" length="2">090D</title_version>
2027  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2028  <app_type type="hexBinary" length="4">90000001</app_type>
2029  <group_id type="hexBinary" length="4">00000400</group_id>
2030  <os_mask  type="hexBinary" length="32">0</os_mask>
2031  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2032</app>"#
2033					.to_owned()
2034			),
2035			r#"<?xml version="1.0" encoding = "utf-8"?>
2036<app type="complex" access="777">
2037  <version type="unsignedInt" length="4">16</version>
2038  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2039  <title_id type="hexBinary" length="8">000500101F700500</title_id>
2040  <title_version type="hexBinary" length="2">090D</title_version>
2041  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2042  <app_type type="hexBinary" length="4">90000001</app_type>
2043  <group_id type="hexBinary" length="4">00000400</group_id>
2044  <os_mask  type="hexBinary" length="32">0</os_mask>
2045  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2046</app>"#
2047				.to_owned(),
2048		);
2049
2050		assert_eq!(
2051			HostFilesystem::capitilize_title_id(
2052				r#"<?xml version="1.0" encoding = "utf-8"?>
2053<app type="complex" access="777">
2054  <version type="unsignedInt" length="4">16</version>
2055  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2056  <title_id type="hexBinary" length="8">000500101F700500</title_id>
2057  <title_version type="hexBinary" length="2">090D</title_version>
2058  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2059  <app_type type="hexBinary" length="4">90000001</app_type>
2060  <group_id type="hexBinary" length="4">00000400</group_id>
2061  <os_mask  type="hexBinary" length="32">0</os_mask>
2062  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2063</app>"#
2064					.to_owned()
2065			),
2066			r#"<?xml version="1.0" encoding = "utf-8"?>
2067<app type="complex" access="777">
2068  <version type="unsignedInt" length="4">16</version>
2069  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2070  <title_id type="hexBinary" length="8">000500101F700500</title_id>
2071  <title_version type="hexBinary" length="2">090D</title_version>
2072  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2073  <app_type type="hexBinary" length="4">90000001</app_type>
2074  <group_id type="hexBinary" length="4">00000400</group_id>
2075  <os_mask  type="hexBinary" length="32">0</os_mask>
2076  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2077</app>"#
2078				.to_owned(),
2079		);
2080
2081		assert_eq!(
2082			HostFilesystem::capitilize_title_id(
2083				r#"<?xml version="1.0" encoding = "utf-8"?>
2084<app type="complex" access="777">
2085  <version type="unsignedInt" length="4">16</version>
2086  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2087  <title_version type="hexBinary" length="2">090D</title_version>
2088  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2089  <app_type type="hexBinary" length="4">90000001</app_type>
2090  <group_id type="hexBinary" length="4">00000400</group_id>
2091  <os_mask  type="hexBinary" length="32">0</os_mask>
2092  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2093</app>"#
2094					.to_owned()
2095			),
2096			r#"<?xml version="1.0" encoding = "utf-8"?>
2097<app type="complex" access="777">
2098  <version type="unsignedInt" length="4">16</version>
2099  <os_version type="hexBinary" length="8">000500101000400A</os_version>
2100  <title_version type="hexBinary" length="2">090D</title_version>
2101  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2102  <app_type type="hexBinary" length="4">90000001</app_type>
2103  <group_id type="hexBinary" length="4">00000400</group_id>
2104  <os_mask  type="hexBinary" length="32">0</os_mask>
2105  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2106</app>"#
2107				.to_owned(),
2108		);
2109
2110		assert_eq!(
2111			HostFilesystem::capitilize_title_id(r#"<?xml version="1.0" encoding = "utf-8"?>
2112<app type="complex" access="777">
2113  <version type="unsignedInt" length="4">16</version>
2114  <os_version type="hexBinary" length="8">000500101000400A</os_version><title_id type="hexBinary" length="8">000500101f700500</title_id><title_version type="hexBinary" length="2">090D</title_version>
2115  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2116  <app_type type="hexBinary" length="4">90000001</app_type>
2117  <group_id type="hexBinary" length="4">00000400</group_id>
2118  <os_mask  type="hexBinary" length="32">0</os_mask>
2119  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2120</app>"#.to_owned()),
2121			r#"<?xml version="1.0" encoding = "utf-8"?>
2122<app type="complex" access="777">
2123  <version type="unsignedInt" length="4">16</version>
2124  <os_version type="hexBinary" length="8">000500101000400A</os_version><title_id type="hexBinary" length="8">000500101F700500</title_id><title_version type="hexBinary" length="2">090D</title_version>
2125  <sdk_version type="unsignedInt" length="4">21213</sdk_version>
2126  <app_type type="hexBinary" length="4">90000001</app_type>
2127  <group_id type="hexBinary" length="4">00000400</group_id>
2128  <os_mask  type="hexBinary" length="32">0</os_mask>
2129  <common_id type="hexBinary" length="8">0000000000000000</common_id>
2130</app>"#.to_owned(),
2131		);
2132	}
2133}