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	errors::{CatBridgeError, FSError},
6	fsemul::{
7		bsf::BootSystemFile, dlf::DiskLayoutFile, errors::FSEmulFSError, pcfs::errors::PCFSApiError,
8	},
9	TitleID,
10};
11use bytes::{Bytes, BytesMut};
12use scc::{hash_map::OccupiedEntry as CMOccupiedEntry, HashMap as ConcurrentMap};
13use std::{
14	collections::HashMap,
15	hash::RandomState,
16	io::{Error as IOError, SeekFrom},
17	path::{Path, PathBuf},
18	sync::atomic::{AtomicI32, Ordering as AtomicOrdering},
19};
20use tokio::{
21	fs::{
22		create_dir_all, read_dir, remove_file, rename, write as fs_write, File, OpenOptions,
23		ReadDir,
24	},
25	io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
26};
27use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
28use whoami::username;
29
30/// Current "FD" for directories. Just a counter going up.
31static DIRECTORY_FD: AtomicI32 = AtomicI32::new(1);
32
33/// A wrapper around interacting with the 'host' or PC filesystem for the
34/// various times a cat-dev will reach out to the host.
35///
36/// This is little more than a wrapper around a [`PathBuf`], and targeted
37/// methods to make getting files/generating default files/etc. easy. Most of
38/// the actual logic for turning a request from `SDIO`, `ATAPI`, etc. all come
39/// from those client/server implementations rather than the logic living here.
40#[derive(Debug)]
41pub struct HostFilesystem {
42	/// The path to the base data directory to serve a filesystem out of.
43	cafe_sdk_path: PathBuf,
44	/// List of open file handles.
45	///
46	/// This contains a value of (file, file size, path).
47	open_file_handles: ConcurrentMap<i32, (File, u64, PathBuf)>,
48	/// List of open directory "handles".
49	///
50	/// This contains a value of (read directory, is end, path)
51	open_folder_handles: ConcurrentMap<i32, (ReadDir, bool, PathBuf)>,
52}
53
54impl HostFilesystem {
55	/// Create a filesystem from a root cafe dir.
56	///
57	/// If no cafe dir is provided, we will attempt to locate the default
58	/// installation path for cafe sdk which is:
59	///
60	/// - `C:\cafe_sdk` on windows.
61	/// - `/opt/cafe_sdk` on any unix/bsd like OS.
62	///
63	/// NOTE: This will validate that all title id paths are lowercase, as
64	/// files are always expected to be lowercase when dealing with CAFE. Other
65	/// files are usually kept in the correct naming format. HOWEVER, users may
66	/// notice spurious errors with case-insensitivity on linux specifically. If
67	/// transferring an SDK from a Windows/Mac Case Insensitive to a Mac/Linux
68	/// case sensitive file system. It is recommended users
69	/// create their own directory using our recovery tools, rather than
70	/// rsync'ing a path over from case-insensitive, to case-sensitive.
71	///
72	/// ## Errors
73	///
74	/// If the Cafe SDK directory is corrupt, or can't be found. A Cafe SDK
75	/// directory is considered corrupt if it is missing core files that we
76	/// _need_ to be able to serve a Cafe-OS distribution. These file
77	/// requirements may change from version to version of this crate, but should
78	/// always be compatible with a clean cafe sdk directory.
79	pub async fn from_cafe_dir(cafe_dir: Option<PathBuf>) -> Result<Self, FSError> {
80		let Some(cafe_sdk_path) = cafe_dir.or_else(Self::default_cafe_directory) else {
81			return Err(FSEmulFSError::CantFindCafeSdkPath.into());
82		};
83
84		Self::patch_case_sensitive_title_ids(&cafe_sdk_path).await?;
85
86		if !Self::join_many(
87			&cafe_sdk_path,
88			[
89				"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
90			],
91		)
92		.exists() || !Self::join_many(
93			&cafe_sdk_path,
94			[
95				"data", "mlc", "sys", "title", "00050030", "1001010a", "code", "app.xml",
96			],
97		)
98		.exists() || !Self::join_many(
99			&cafe_sdk_path,
100			[
101				"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
102			],
103		)
104		.exists()
105		{
106			return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
107		}
108
109		// Can't generate a `fw.img` file for now :(
110		if !Self::join_many(
111			&cafe_sdk_path,
112			[
113				"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
114			],
115		)
116		.exists()
117		{
118			return Err(FSEmulFSError::CafeSdkPathCorrupt.into());
119		}
120
121		Ok(Self {
122			cafe_sdk_path,
123			open_file_handles: ConcurrentMap::new(),
124			open_folder_handles: ConcurrentMap::new(),
125		})
126	}
127
128	/// The root path to the Cafe SDK.
129	///
130	/// *note: although we do expose this for logging, and other info... we do
131	/// not recommend manually interacting with the SDK path. There are much
132	/// better alternatives.*
133	#[must_use]
134	pub const fn cafe_sdk_path(&self) -> &PathBuf {
135		&self.cafe_sdk_path
136	}
137
138	/// Open a file, and return it's file descriptor number.
139	///
140	/// ## Errors
141	///
142	/// If we cannot open our file with the open options provided.
143	pub async fn open_file(
144		&self,
145		open_options: OpenOptions,
146		path: &PathBuf,
147	) -> Result<i32, FSError> {
148		let fd = open_options.open(path).await?;
149		let raw_fd;
150		#[cfg(unix)]
151		{
152			use std::os::fd::AsRawFd;
153			raw_fd = fd.as_raw_fd();
154		}
155		#[cfg(target_os = "windows")]
156		{
157			use std::os::windows::io::AsRawHandle;
158			raw_fd = fd.as_raw_handle() as i32;
159		}
160
161		let md = fd.metadata().await?;
162
163		self.open_file_handles
164			.insert(raw_fd, (fd, md.len(), path.clone()))
165			.map_err(|_| IOError::other("OS returned duplicate fd?"))?;
166		Ok(raw_fd)
167	}
168
169	/// Get a file from a file descriptor number.
170	///
171	/// This file must already be opened (in order to get the file descriptor).
172	pub async fn get_file(
173		&self,
174		fd: i32,
175	) -> Option<CMOccupiedEntry<i32, (File, u64, PathBuf), RandomState>> {
176		self.open_file_handles.get_async(&fd).await
177	}
178
179	/// Get the file length from a file descriptor number.
180	///
181	/// This file must already be opened (in order to get the file descriptor).
182	pub async fn file_length(&self, fd: i32) -> Option<u64> {
183		self.open_file_handles.get_async(&fd).await.map(|e| e.1)
184	}
185
186	/// Read from a file descriptor that is actively open.
187	///
188	/// This will read from a currently open file descriptor, in it's current
189	/// location. You might want to set your file location for this FD before
190	/// if you aren't already in the same location.
191	///
192	/// ## Errors
193	///
194	/// If the file descriptor is open, but we could not read from the open file
195	/// descriptor.
196	pub async fn read_file(
197		&self,
198		fd: i32,
199		total_data_to_read: usize,
200	) -> Result<Option<Bytes>, FSError> {
201		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
202			return Ok(None);
203		};
204		let file_reader = &mut real_entry.0;
205		let mut file_buff = BytesMut::zeroed(total_data_to_read);
206		let bytes_read = file_reader.read(&mut file_buff).await?;
207		if bytes_read < total_data_to_read {
208			file_buff[bytes_read..].fill(0xCD);
209		}
210
211		Ok(Some(file_buff.freeze()))
212	}
213
214	/// Write to a file descriptor that is actively open.
215	///
216	/// This will write from a currently open file descriptor, in it's current
217	/// location. You might want to set your file location for this FD before
218	/// if you aren't already in the same location.
219	///
220	/// ## Errors
221	///
222	/// If the file descriptor is open, but we could not write to the open file
223	/// descriptor.
224	pub async fn write_file(&self, fd: i32, data_to_write: Bytes) -> Result<(), FSError> {
225		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
226			return Err(FSError::IO(IOError::other("file not open")));
227		};
228		let file_writer = &mut real_entry.0;
229		file_writer.write_all(&data_to_write).await?;
230
231		Ok(())
232	}
233
234	/// Seek to the beginning or end of a file.
235	///
236	/// If `begin` is true then we will seek to the beginning of the file
237	/// otherwise we will sync to the end of the file. Precise seeking is _not_
238	/// supported at this time.
239	///
240	/// ## Errors
241	///
242	/// If we cannot seek to the beginning or end of the file.
243	pub async fn seek_file(&self, fd: i32, begin: bool) -> Result<(), FSError> {
244		let Some(mut real_entry) = self.open_file_handles.get_async(&fd).await else {
245			return Ok(());
246		};
247		let file_reader = &mut real_entry.0;
248
249		if begin {
250			file_reader.seek(SeekFrom::Start(0)).await?;
251		} else {
252			file_reader.seek(SeekFrom::End(0)).await?;
253		}
254
255		Ok(())
256	}
257
258	/// Decrement the ref count of handles to a file.
259	///
260	/// If ref count reaches 0 close the underlying file handle.
261	///
262	/// ## Errors
263	///
264	/// If we cannot close our file handle when our ref count reaches 0, or if
265	/// the file isn't open at all.
266	pub async fn close_file(&self, fd: i32) {
267		self.open_file_handles.remove_async(&fd).await;
268	}
269
270	/// "Open" a folder, or an iterator over a directory.
271	///
272	/// There's no real "open file handle", or reversible directory iterator,
273	/// so we just create an id from scratch.
274	///
275	/// ## Errors
276	///
277	/// If the path doesn't exist, then we can't open the directory.
278	pub async fn open_folder(&self, path: &PathBuf) -> Result<i32, FSError> {
279		let dhandle = read_dir(path).await?;
280		let fake_fd = DIRECTORY_FD.fetch_add(1, AtomicOrdering::SeqCst);
281		self.open_folder_handles
282			.insert(fake_fd, (dhandle, false, path.clone()))
283			.map_err(|_| IOError::other("OS returned duplicate fd?"))?;
284		Ok(fake_fd)
285	}
286
287	/// Get the next filename/foldername available in a particular folder, and
288	/// how many pieces to remove to get just the filename.
289	///
290	/// This will always return none even if it's already at the end, unlike a
291	/// particular iterator.
292	///
293	/// ## Errors
294	///
295	/// If we get an IO error from the underlying filesystem.
296	pub async fn next_in_folder(&self, fd: i32) -> Result<Option<(PathBuf, usize)>, FSError> {
297		let Some(mut entry) = self.open_folder_handles.get_async(&fd).await else {
298			return Ok(None);
299		};
300
301		let component_count = entry.2.components().count();
302		let mut value: Option<PathBuf> = None;
303		if !entry.1 {
304			let iter = &mut entry.0;
305			loop {
306				value = iter.next_entry().await?.map(|de| de.path());
307				if let Some(ref_value) = value.as_ref() {
308					if (!ref_value.is_file() && !ref_value.is_dir()) || ref_value.is_symlink() {
309						continue;
310					}
311				}
312				break;
313			}
314			if value.is_none() {
315				entry.1 = true;
316			}
317		}
318
319		Ok(value.map(|val| (val, component_count)))
320	}
321
322	/// Reverse a particular iterator over a folder by one.
323	///
324	/// Note: This will recreate the directory iterator, and will temporarily
325	/// hold _two_ references to [`ReadDir`] at a time because the underlying
326	/// iterator from read directory is not a reversible iterator.
327	///
328	/// ## Errors
329	///
330	/// If opening another read dir call does not work.
331	pub async fn reverse_directory(&self, fd: i32) -> Result<(), FSError> {
332		let Some(mut real_entry) = self.open_folder_handles.get_async(&fd).await else {
333			return Ok(());
334		};
335
336		real_entry.0 = read_dir(&real_entry.2).await?;
337		real_entry.1 = false;
338		Ok(())
339	}
340
341	/// Decrement the ref count of handles to a folder.
342	///
343	/// If ref count reaches 0 close the underlying folder handle.
344	///
345	/// ## Errors
346	///
347	/// If we cannot close our folder handle when our ref count reaches 0, or if
348	/// the folder isn't open at all.
349	pub async fn close_folder(&self, fd: i32) {
350		self.open_folder_handles.remove_async(&fd).await;
351	}
352
353	/// Get the path to the current boot1 `.bsf` file.
354	///
355	/// This function will create the boot1 system file, if it does not yet
356	/// exist. As a result it may error, if we can't create, and place the
357	/// boot system file.
358	///
359	/// ## Errors
360	///
361	/// - If the temp directory does not exist, and we can't create it.
362	/// - If the boot system file does not exist, and we can't write it to disk.
363	pub async fn boot1_sytstem_path(&self) -> Result<PathBuf, FSError> {
364		let mut path = self.temp_path().await?;
365		path.push("caferun");
366		if !path.exists() {
367			create_dir_all(&path).await?;
368		}
369		path.push("ppc.bsf");
370
371		if !path.exists() {
372			fs_write(&path, Bytes::from(BootSystemFile::default())).await?;
373		}
374
375		Ok(path)
376	}
377
378	/// Get the path to the current `diskid.bin`.
379	///
380	/// If the current Disk ID does not exist, we will write a blank diskid to
381	/// this path.
382	///
383	/// ## Errors
384	///
385	/// - If the temporary directory does not exist, and we can't create it.
386	/// - If the disk ID path does not exist, and we can't write it to disk.
387	pub async fn disk_id_path(&self) -> Result<PathBuf, FSError> {
388		let mut path = self.temp_path().await?;
389		path.push("caferun");
390		if !path.exists() {
391			create_dir_all(&path).await?;
392		}
393		path.push("diskid.bin");
394
395		if !path.exists() {
396			fs_write(&path, BytesMut::zeroed(32).freeze()).await?;
397		}
398
399		Ok(path)
400	}
401
402	/// Get the path to the current firmware file to boot on the MION.
403	///
404	/// This is guaranteed to always exist, as it's part of our check for a
405	/// corrupt SDK.
406	#[must_use]
407	pub fn firmware_file_path(&self) -> PathBuf {
408		Self::join_many(
409			&self.slc_path_for((0x0005_0010, 0x1000_400A)),
410			["code", "fw.img"],
411		)
412	}
413
414	/// Get the path to the disk layout file for the PPC booting process.
415	///
416	/// This function will create a disk layout file, as well as a Boot System
417	/// File, and a disk id file if they do not yet exist.
418	///
419	/// ## Errors
420	///
421	/// - If the temp directory does not exist, and we can't create it.
422	/// - If the boot system file does not exist, and we can't write it to disk.
423	/// - If the diskid file does not exist, and we can't write it to disk.
424	/// - If the firmware image file does not exist.
425	/// - If the dlf file does not exist, and we can't create it.
426	pub async fn ppc_boot_dlf_path(&self) -> Result<PathBuf, CatBridgeError> {
427		let mut path = self.temp_path().await?;
428		path.push("caferun");
429		if !path.exists() {
430			create_dir_all(&path).await.map_err(FSError::from)?;
431		}
432		path.push("ppc_boot.dlf");
433
434		if !path.exists() {
435			// This probably isn't the right set of defaults for everyone, but i'm
436			// not yet smart enough to figure all this out.
437			let mut root_dlf = DiskLayoutFile::new(0x00B8_8200_u128);
438			root_dlf.upsert_addressed_path(0_u128, &self.disk_id_path().await?)?;
439			root_dlf.upsert_addressed_path(0x80000_u128, &self.boot1_sytstem_path().await?)?;
440			root_dlf.upsert_addressed_path(0x90000_u128, &self.firmware_file_path())?;
441			fs_write(&path, Bytes::from(root_dlf))
442				.await
443				.map_err(FSError::from)?;
444		}
445
446		Ok(path)
447	}
448
449	/// Check if a path is allowed to be writable.
450	pub fn path_allows_writes(&self, path: &Path) -> bool {
451		// TODO(mythra): check FSEmulAttributeRules
452		!path.to_string_lossy().contains("%DISC_EMU_DIR")
453			&& !path.starts_with(Self::join_many(&self.cafe_sdk_path, ["data", "disc"]))
454	}
455
456	/// Given a UTF-8 string path, get a pathbuf reference.
457	///
458	/// This understands the current following implementations:
459	///
460	/// - `/%MLC_EMU_DIR`
461	/// - `/%SLC_EMU_DIR`
462	/// - `/%DISC_EMU_DIR`
463	/// - `/%SAVE_EMU_DIR`
464	/// - `/%NETWORK`
465	///
466	/// Most of these are just quick ways of referncing the current set of
467	/// directories, within cafe sdk. `%NETWORK` is the special one which
468	/// references a currently mounted network share.
469	///
470	/// ## Errors
471	///
472	/// If the path requested is not in a mounted path.
473	pub fn resolve_path(
474		&self,
475		potentially_prefixed_path: &str,
476	) -> Result<ResolvedLocation, CatBridgeError> {
477		// Requests coming may optionally have `/vol/pc` prefixed if they're built
478		// wrong.
479		//
480		// Or if a user is trying to get cat-dev style paths working with this api
481		// directly. CLean it up.
482		let path = potentially_prefixed_path.trim_start_matches("/vol/pc");
483		if path.starts_with("/%NETWORK") {
484			todo!("NETWORK shares not yet implemented :( sorry!")
485		}
486
487		let non_canonical_path = if path.starts_with("/%MLC_EMU_DIR") {
488			self.replace_emu_dir(path, "mlc")
489		} else if path.starts_with("/%SLC_EMU_DIR") {
490			self.replace_emu_dir(path, "slc")
491		} else if path.starts_with("/%DISC_EMU_DIR") {
492			self.replace_emu_dir(path, "disc")
493		} else if path.starts_with("/%SAVE_EMU_DIR") {
494			self.replace_emu_dir(path, "save")
495		} else {
496			PathBuf::from(path)
497		};
498
499		// We can't actually just call `canonicalize`, as that will fail if the
500		// file doesn't exist, and we could be requesting to resolve a path we want
501		// to turn around and create.
502		//
503		// So instead we try to canonicalize to the closest possible directory, and
504		// check if it is underneath our directory.
505		let mut closest_canonical_directory = non_canonical_path.clone();
506		let mut changed_at_all = false;
507		while !closest_canonical_directory.as_os_str().is_empty() {
508			if let Ok(canonicalized) = closest_canonical_directory.canonicalize() {
509				closest_canonical_directory = canonicalized;
510				break;
511			}
512
513			changed_at_all = true;
514			closest_canonical_directory.pop();
515		}
516		// We failed to find any directory, which means we're nowhere close to
517		// where we want to be.
518		if closest_canonical_directory.as_os_str().is_empty() {
519			return Err(PCFSApiError::PathNotMapped(path.to_owned()).into());
520		}
521		// Check for mapped directories...
522		let canonicalized_cafe = self
523			.cafe_sdk_path()
524			.canonicalize()
525			.unwrap_or_else(|_| self.cafe_sdk_path().clone());
526		if !closest_canonical_directory.starts_with(canonicalized_cafe) {
527			return Err(PCFSApiError::PathNotMapped(path.to_owned()).into());
528		}
529
530		Ok(ResolvedLocation::Filesystem(FilesystemLocation::new(
531			non_canonical_path,
532			closest_canonical_directory,
533			!changed_at_all,
534		)))
535	}
536
537	/// Get a file from the SLC.
538	///
539	/// The SLC always serves "sys" files, and are relative to a title id, almost
540	/// always a system title id such as (`00050010`).
541	///
542	/// *note: the file is not guaranteed to exist! It's just a path!*
543	#[must_use]
544	pub fn slc_path_for(&self, title_id: TitleID) -> PathBuf {
545		Self::join_many(
546			&self.cafe_sdk_path,
547			[
548				"data".to_owned(),
549				"slc".to_owned(),
550				"sys".to_owned(),
551				"title".to_owned(),
552				format!("{:08x}", title_id.0),
553				format!("{:08x}", title_id.1),
554			],
555		)
556	}
557
558	/// Get the current path to the temporary directory for this Cafe SDK
559	/// install.
560	///
561	/// ## Errors
562	///
563	/// - If the temporary path does not exist and could not be created.
564	async fn temp_path(&self) -> Result<PathBuf, FSError> {
565		let temp_path = Self::join_many(
566			&self.cafe_sdk_path,
567			["temp".to_owned(), username().to_lowercase()],
568		);
569		if !temp_path.exists() {
570			create_dir_all(&temp_path).await?;
571		}
572		Ok(temp_path)
573	}
574
575	/// A small utility function to join many paths into a single path effeciently.
576	#[must_use]
577	fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
578	where
579		PathTy: AsRef<Path>,
580		IterTy: IntoIterator<Item = PathTy>,
581	{
582		let mut as_owned = PathBuf::from(base);
583		for part in parts {
584			as_owned = as_owned.join(part.as_ref());
585		}
586		as_owned
587	}
588
589	/// Replace a particular emu directory string in a path.
590	fn replace_emu_dir(&self, path: &str, dir: &str) -> PathBuf {
591		let path_minus = path
592			.trim_start_matches(&format!("/%{}_EMU_DIR", dir.to_ascii_uppercase()))
593			.trim_start_matches('/')
594			.trim_start_matches('\\')
595			.replace('\\', "/");
596
597		Self::join_many(
598			&Self::join_many(self.cafe_sdk_path(), ["data", dir]),
599			path_minus.split('/'),
600		)
601	}
602
603	/// Get the current OS's default directory path.
604	///
605	/// For Windows this is: `C:\cafe_sdk`.
606	/// For Unix/BSD likes this is: `/opt/cafe_sdk`
607	#[allow(
608    // Not actually unreachable unless on unsupported OS.
609    unreachable_code,
610  )]
611	#[must_use]
612	pub fn default_cafe_directory() -> Option<PathBuf> {
613		#[cfg(target_os = "windows")]
614		{
615			return Some(PathBuf::from(r"C:\cafe_sdk"));
616		}
617
618		#[cfg(any(
619			target_os = "linux",
620			target_os = "freebsd",
621			target_os = "openbsd",
622			target_os = "netbsd",
623			target_os = "macos"
624		))]
625		{
626			return Some(PathBuf::from("/opt/cafe_sdk"));
627		}
628
629		None
630	}
631
632	async fn patch_case_sensitive_title_ids(cafe_sdk_path: &Path) -> Result<(), FSError> {
633		// First we need to check if we're even on a temporary filesystem/path.
634		if !cafe_sdk_path.exists() {
635			return Ok(());
636		}
637		let capital_path = Self::join_many(cafe_sdk_path, ["InsensitiveCheck.txt"]);
638		let _ = File::create(&capital_path).await?;
639		let is_insensitive = File::open(Self::join_many(cafe_sdk_path, ["insensitivecheck.txt"]))
640			.await
641			.is_ok();
642		remove_file(capital_path).await?;
643		if is_insensitive {
644			return Ok(());
645		}
646
647		for directory in [
648			Self::join_many(cafe_sdk_path, ["data", "slc", "sys", "title"]),
649			Self::join_many(cafe_sdk_path, ["data", "slc", "usr", "title"]),
650			Self::join_many(cafe_sdk_path, ["data", "mlc", "sys", "title"]),
651			Self::join_many(cafe_sdk_path, ["data", "mlc", "usr", "title"]),
652		] {
653			if !directory.exists() {
654				// Don't need to patch directories that don't exist.
655				continue;
656			}
657
658			// Now we need to scan, and lowercase all title ids. So those are the
659			// next two sub dirs as they're split into `title/{upper}/{lower}`.
660			let mut iter = read_dir(&directory).await?;
661			let lossy_cafe_dir = cafe_sdk_path.as_os_str().to_string_lossy().to_string();
662			while let Ok(Some(entry)) = iter.next_entry().await {
663				let p = entry.path();
664				if !p.is_dir() || !p.exists() {
665					continue;
666				}
667
668				let mut inner_iter = read_dir(&p).await?;
669				while let Ok(Some(inner_entry)) = inner_iter.next_entry().await {
670					let ip = inner_entry.path();
671					if !ip.is_dir() || !ip.exists() {
672						continue;
673					}
674
675					// Doing a lossy conversion is safe here cause we know all title ids are valid ascii + utf-8.
676					let new_path = ip
677						.as_os_str()
678						.to_string_lossy()
679						.trim_start_matches(&lossy_cafe_dir)
680						.to_ascii_lowercase();
681					if ip
682						.as_os_str()
683						.to_string_lossy()
684						.trim_start_matches(&lossy_cafe_dir)
685						!= new_path
686					{
687						let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
688						final_new_path.push(&new_path);
689						let new = PathBuf::from(final_new_path);
690						rename(ip, new).await?;
691					}
692				}
693
694				let new_path = p
695					.as_os_str()
696					.to_string_lossy()
697					.trim_start_matches(&lossy_cafe_dir)
698					.to_ascii_lowercase();
699				if p.as_os_str()
700					.to_string_lossy()
701					.trim_start_matches(&lossy_cafe_dir)
702					!= new_path
703				{
704					let mut final_new_path = cafe_sdk_path.as_os_str().to_owned();
705					final_new_path.push(&new_path);
706					rename(p, final_new_path).await?;
707				}
708			}
709		}
710
711		Ok(())
712	}
713}
714
715const HOST_FILESYSTEM_FIELDS: &[NamedField<'static>] = &[
716	NamedField::new("cafe_sdk_path"),
717	NamedField::new("open_file_handles"),
718	NamedField::new("open_folder_handles"),
719];
720
721impl Structable for HostFilesystem {
722	fn definition(&self) -> StructDef<'_> {
723		StructDef::new_static("HostFilesystem", Fields::Named(HOST_FILESYSTEM_FIELDS))
724	}
725}
726
727impl Valuable for HostFilesystem {
728	fn as_value(&self) -> Value<'_> {
729		Value::Structable(self)
730	}
731
732	fn visit(&self, visitor: &mut dyn Visit) {
733		let mut values = HashMap::with_capacity(self.open_file_handles.len());
734		self.open_file_handles.scan(|k, v| {
735			values.insert(*k, format!("{}", v.2.display()));
736		});
737		let mut folder_values = HashMap::with_capacity(self.open_folder_handles.len());
738		self.open_folder_handles.scan(|k, v| {
739			folder_values.insert(*k, format!("{}", v.2.display()));
740		});
741
742		visitor.visit_named_fields(&NamedValues::new(
743			HOST_FILESYSTEM_FIELDS,
744			&[
745				Valuable::as_value(&self.cafe_sdk_path),
746				Valuable::as_value(&values),
747				Valuable::as_value(&folder_values),
748			],
749		));
750	}
751}
752
753/// A resolved location given an arbitrary path.
754#[derive(Clone, Debug, PartialEq, Eq, Valuable)]
755pub enum ResolvedLocation {
756	/// A location on a particular filesystem.
757	///
758	/// This contains a tuple of:
759	///
760	/// `(ResolvedPath, ClosestExistingCanonicalDirectory)`
761	Filesystem(FilesystemLocation),
762	/// A network location to fetch.
763	///
764	/// TODO(mythra): figure out type.
765	Network(()),
766}
767
768/// A location that's been resolved, and is guaranteed to be in one of our
769/// mounted paths.
770#[derive(Clone, Debug, PartialEq, Eq)]
771pub struct FilesystemLocation {
772	/// The final resolved path (may not exist).
773	resolved_path: PathBuf,
774	/// The resolved path that may not be the same as the final path, but is
775	/// enough to confirm we're in the same directory.
776	closest_resolved_path: PathBuf,
777	/// If the canonicalized path is the same as the resolved path.
778	canonicalized_is_exact: bool,
779}
780impl FilesystemLocation {
781	#[must_use]
782	pub const fn new(
783		resolved_path: PathBuf,
784		closest_resolved_path: PathBuf,
785		canonicalized_is_exact: bool,
786	) -> Self {
787		Self {
788			resolved_path,
789			closest_resolved_path,
790			canonicalized_is_exact,
791		}
792	}
793
794	#[must_use]
795	pub const fn resolved_path(&self) -> &PathBuf {
796		&self.resolved_path
797	}
798	#[must_use]
799	pub const fn closest_resolved_path(&self) -> &PathBuf {
800		&self.closest_resolved_path
801	}
802	#[must_use]
803	pub const fn canonicalized_is_exact(&self) -> bool {
804		self.canonicalized_is_exact
805	}
806}
807
808const FILESYSTEM_LOCATION_FIELDS: &[NamedField<'static>] = &[
809	NamedField::new("resolved_path"),
810	NamedField::new("closest_resolved_path"),
811	NamedField::new("canonicalized_is_exact"),
812];
813
814impl Structable for FilesystemLocation {
815	fn definition(&self) -> StructDef<'_> {
816		StructDef::new_static(
817			"FilesystemLocation",
818			Fields::Named(FILESYSTEM_LOCATION_FIELDS),
819		)
820	}
821}
822
823impl Valuable for FilesystemLocation {
824	fn as_value(&self) -> Value<'_> {
825		Value::Structable(self)
826	}
827
828	fn visit(&self, visitor: &mut dyn Visit) {
829		visitor.visit_named_fields(&NamedValues::new(
830			FILESYSTEM_LOCATION_FIELDS,
831			&[
832				Valuable::as_value(&self.resolved_path),
833				Valuable::as_value(&self.closest_resolved_path),
834				Valuable::as_value(&self.canonicalized_is_exact),
835			],
836		));
837	}
838}
839
840#[cfg(test)]
841pub mod test_helpers {
842	use super::*;
843	use std::fs::{create_dir_all, File};
844	use tempfile::{tempdir, TempDir};
845
846	/// Test helper that creates a simple host filesystem.
847	pub async fn create_temporary_host_filesystem() -> (TempDir, HostFilesystem) {
848		let dir = tempdir().expect("Failed to create temporary directory!");
849
850		for directory_to_create in vec![
851			// Create data directories
852			vec!["data", "slc"],
853			vec!["data", "mlc"],
854			vec!["data", "disc"],
855			vec!["data", "save"],
856			// Create necessary to pass checks.
857			vec![
858				"data", "mlc", "sys", "title", "00050030", "1001000a", "code",
859			],
860			// Purposefully create capital so we can validate renaming works!
861			vec![
862				"data", "mlc", "sys", "title", "00050030", "1001010A", "code",
863			],
864			vec![
865				"data", "mlc", "sys", "title", "00050030", "1001020a", "code",
866			],
867			vec![
868				"data", "slc", "sys", "title", "00050010", "1000400a", "code",
869			],
870		] {
871			create_dir_all(HostFilesystem::join_many(dir.path(), directory_to_create))
872				.expect("Failed to create directories necessary for host filesystem to work.");
873		}
874
875		// Place files that need to exist, they are not real, but enough to "fool"
876		// our basic check.
877		File::create(HostFilesystem::join_many(
878			dir.path(),
879			[
880				"data", "mlc", "sys", "title", "00050030", "1001000a", "code", "app.xml",
881			],
882		))
883		.expect("Failed to create needed app.xml!");
884		File::create(HostFilesystem::join_many(
885			dir.path(),
886			[
887				"data", "mlc", "sys", "title", "00050030", "1001010A", "code", "app.xml",
888			],
889		))
890		.expect("Failed to create needed app.xml!");
891		File::create(HostFilesystem::join_many(
892			dir.path(),
893			[
894				"data", "mlc", "sys", "title", "00050030", "1001020a", "code", "app.xml",
895			],
896		))
897		.expect("Failed to create needed app.xml!");
898
899		File::create(HostFilesystem::join_many(
900			dir.path(),
901			[
902				"data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img",
903			],
904		))
905		.expect("Failed to create needed fw.img!");
906
907		let fs = HostFilesystem::from_cafe_dir(Some(PathBuf::from(dir.path())))
908			.await
909			.expect("Failed to load empty host filesystem!");
910
911		(dir, fs)
912	}
913
914	/// Re-export host file system join many for tests.
915	#[must_use]
916	pub fn join_many<PathTy, IterTy>(base: &Path, parts: IterTy) -> PathBuf
917	where
918		PathTy: AsRef<Path>,
919		IterTy: IntoIterator<Item = PathTy>,
920	{
921		HostFilesystem::join_many(base, parts)
922	}
923}
924
925#[cfg(test)]
926mod unit_tests {
927	use super::test_helpers::*;
928	use super::*;
929	use std::fs::read;
930
931	fn only_accepts_send_sync<T: Send + Sync>(_opt: Option<T>) {}
932
933	#[test]
934	pub fn is_send_sync() {
935		only_accepts_send_sync::<HostFilesystem>(None);
936	}
937
938	#[test]
939	pub fn can_find_default_cafe_directory() {
940		assert!(
941			HostFilesystem::default_cafe_directory().is_some(),
942			"Failed to find default cafe directory for your OS",
943		);
944	}
945
946	#[tokio::test]
947	pub async fn creatable_files() {
948		// Validate that our functions that create files can actually, well, create
949		// those files.
950		let (tempdir, fs) = create_temporary_host_filesystem().await;
951
952		let expected_bsf_path = HostFilesystem::join_many(
953			tempdir.path(),
954			[
955				"temp".to_owned(),
956				username(),
957				"caferun".to_owned(),
958				"ppc.bsf".to_owned(),
959			],
960		);
961		assert!(
962			!expected_bsf_path.exists(),
963			"ppc.bsf existed before we asked for it?"
964		);
965		let bsf_path = fs
966			.boot1_sytstem_path()
967			.await
968			.expect("Failed to create bsf!");
969		assert_eq!(expected_bsf_path, bsf_path);
970		assert!(
971			BootSystemFile::try_from(Bytes::from(
972				read(bsf_path).expect("Failed to read written boot system file!")
973			))
974			.is_ok(),
975			"Failed to read generated boot system file!"
976		);
977
978		let expected_diskid_path = HostFilesystem::join_many(
979			tempdir.path(),
980			[
981				"temp".to_owned(),
982				username(),
983				"caferun".to_owned(),
984				"diskid.bin".to_owned(),
985			],
986		);
987		assert!(
988			!expected_diskid_path.exists(),
989			"diskid.bin existed before we asked for it?"
990		);
991		let diskid_path = fs
992			.disk_id_path()
993			.await
994			.expect("Failed to create diskid.bin!");
995		assert_eq!(expected_diskid_path, diskid_path);
996		assert_eq!(
997			read(diskid_path).expect("Failed to read written diskid.bin!"),
998			vec![0; 32],
999			"Failed to read generated diskid.bin!"
1000		);
1001
1002		// Can't generate firmware files for now.
1003		assert_eq!(
1004			fs.firmware_file_path(),
1005			HostFilesystem::join_many(
1006				tempdir.path(),
1007				["data", "slc", "sys", "title", "00050010", "1000400a", "code", "fw.img"],
1008			),
1009		);
1010
1011		let expected_ppc_boot_dlf_path = HostFilesystem::join_many(
1012			tempdir.path(),
1013			[
1014				"temp".to_owned(),
1015				username(),
1016				"caferun".to_owned(),
1017				"ppc_boot.dlf".to_owned(),
1018			],
1019		);
1020		assert!(
1021			!expected_ppc_boot_dlf_path.exists(),
1022			"ppc_boot.dlf existed before we asked for it?"
1023		);
1024		let ppc_boot_dlf_path = fs
1025			.ppc_boot_dlf_path()
1026			.await
1027			.expect("Failed to create ppc_boot.dlf!");
1028		assert_eq!(expected_ppc_boot_dlf_path, ppc_boot_dlf_path);
1029		assert!(
1030			DiskLayoutFile::try_from(Bytes::from(
1031				read(ppc_boot_dlf_path).expect("Failed to read written ppc_boot.dlf!")
1032			))
1033			.is_ok(),
1034			"Failed to read generated ppc_boot.dlf!"
1035		);
1036	}
1037
1038	#[tokio::test]
1039	pub async fn path_allows_writes() {
1040		let (_tempdir, fs) = create_temporary_host_filesystem().await;
1041
1042		// DIRECTORIES BESIDES DISC should allow writes.
1043		// unless excluded by fsemul attrs.
1044		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%MLC_EMU_DIR/")));
1045		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SLC_EMU_DIR/")));
1046		assert!(fs.path_allows_writes(&PathBuf::from("/vol/pc/%SAVE_EMU_DIR/")));
1047		assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1048		assert!(!fs.path_allows_writes(&PathBuf::from("/vol/pc/%DISC_EMU_DIR/")));
1049	}
1050
1051	#[tokio::test]
1052	pub async fn resolve_path() {
1053		// Validate that our functions that create files can actually, well, create
1054		// those files.
1055		let (tempdir, fs) = create_temporary_host_filesystem().await;
1056
1057		// Validate each of the regular directories work.
1058		for (dir, name) in [
1059			("/%MLC_EMU_DIR", "mlc"),
1060			("/%SLC_EMU_DIR", "slc"),
1061			("/%DISC_EMU_DIR", "disc"),
1062			("/%SAVE_EMU_DIR", "save"),
1063		] {
1064			assert!(
1065				fs.resolve_path(&format!("{dir}")).is_ok(),
1066				"Failed to resolve: `{}`: {:?}",
1067				dir,
1068				fs.resolve_path(&format!("{dir}"))
1069			);
1070			assert!(
1071				fs.resolve_path(&format!("{dir}/")).is_ok(),
1072				"Failed to resolve: `{}/`",
1073				dir,
1074			);
1075			assert!(
1076				fs.resolve_path(&format!("{dir}/./")).is_ok(),
1077				"Failed to resolve: `{}/./`",
1078				dir,
1079			);
1080			assert!(
1081				fs.resolve_path(&format!("{dir}/../{name}")).is_ok(),
1082				"Failed to resolve: `{}/../{}`",
1083				dir,
1084				name,
1085			);
1086		}
1087
1088		// Validate that paths outside of our root directory don't work.
1089		let mut out_of_path = PathBuf::from(tempdir.path());
1090		// We now left tempdir, and this path isn't mounted, so we should error out
1091		// on this.
1092		out_of_path.pop();
1093
1094		// We shouldn't be able to resolve paths outside of our directory.
1095		assert!(fs
1096			.resolve_path(
1097				&out_of_path
1098					.clone()
1099					.into_os_string()
1100					.into_string()
1101					.expect("Failed to convert pathbuf to string!")
1102			)
1103			.is_err());
1104		assert!(fs.resolve_path("/%MLC_EMU_DIR/../../../").is_err());
1105
1106		#[cfg(unix)]
1107		{
1108			use std::os::unix::fs::symlink;
1109
1110			let mut tempdir_symlink = PathBuf::from(tempdir.path());
1111			tempdir_symlink.push("symlink");
1112			symlink(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1113			assert!(fs
1114				.resolve_path(&format!(
1115					"{}/symlink",
1116					tempdir_symlink
1117						.into_os_string()
1118						.into_string()
1119						.expect("tempdir symlink wasn't utf8?"),
1120				))
1121				.is_err());
1122		}
1123
1124		#[cfg(target_os = "windows")]
1125		{
1126			use std::os::windows::fs::symlink_dir;
1127
1128			let mut tempdir_symlink = PathBuf::from(tempdir.path());
1129			tempdir_symlink.push("symlink");
1130			symlink_dir(out_of_path, tempdir_symlink.clone()).expect("Failed to do symlink!");
1131			assert!(fs
1132				.resolve_path(&format!(
1133					"{}/symlink",
1134					tempdir_symlink
1135						.into_os_string()
1136						.into_string()
1137						.expect("tempdir symlink wasn't utf8?"),
1138				))
1139				.is_err());
1140		}
1141	}
1142
1143	#[tokio::test]
1144	pub async fn opening_files() {
1145		let (tempdir, fs) = create_temporary_host_filesystem().await;
1146		let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1147		tokio::fs::write(path.clone(), vec![0; 1307])
1148			.await
1149			.expect("Failed to write test file!");
1150		let create_path = HostFilesystem::join_many(tempdir.path(), ["new-file.txt"]);
1151
1152		let mut oo = OpenOptions::new();
1153		oo.create(false).write(true).read(true);
1154		assert!(
1155			fs.open_file(oo, &create_path).await.is_err(),
1156			"Somehow succeeding opening a file that doesn't exist with no create flag?",
1157		);
1158		oo = OpenOptions::new();
1159		oo.create(true).write(true).truncate(true);
1160		let fd = fs
1161			.open_file(oo, &create_path)
1162			.await
1163			.expect("Failed opening a file that doesn't exist with a create flag?");
1164		assert!(
1165			fs.open_file_handles.len() == 1 && fs.open_file_handles.get(&fd).is_some(),
1166			"Open file wasn't in open files list!",
1167		);
1168		fs.close_file(fd).await;
1169		assert!(
1170			fs.open_file_handles.is_empty(),
1171			"Somehow after opening/closing, open file handles was not empty?",
1172		);
1173	}
1174
1175	#[tokio::test]
1176	pub async fn seek_and_read() {
1177		let (tempdir, fs) = create_temporary_host_filesystem().await;
1178		let path = HostFilesystem::join_many(tempdir.path(), ["file.txt"]);
1179		tokio::fs::write(path.clone(), vec![0; 1307])
1180			.await
1181			.expect("Failed to write test file!");
1182
1183		let mut oo = OpenOptions::new();
1184		oo.read(true).create(false).write(false);
1185		let fd = fs
1186			.open_file(oo, &path)
1187			.await
1188			.expect("Failed to open existing file!");
1189
1190		// Should be possible to read all bytes.
1191		assert_eq!(
1192			Some(BytesMut::zeroed(1307).freeze()),
1193			fs.read_file(fd, 1307)
1194				.await
1195				.expect("Failed to read from FD!"),
1196		);
1197		fs.seek_file(fd, true)
1198			.await
1199			.expect("Failed to sync to beginning of file!");
1200		// Can read all bytes again!
1201		assert_eq!(
1202			Some(BytesMut::zeroed(1307).freeze()),
1203			fs.read_file(fd, 1307)
1204				.await
1205				.expect("Failed to read from FD!"),
1206		);
1207		fs.close_file(fd).await;
1208		assert!(
1209			fs.open_file_handles.is_empty(),
1210			"Somehow after opening/closing, open file handles was not empty?",
1211		);
1212	}
1213
1214	#[tokio::test]
1215	pub async fn open_and_close_folder() {
1216		let (tempdir, fs) = create_temporary_host_filesystem().await;
1217		let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1218		tokio::fs::create_dir_all(path.clone())
1219			.await
1220			.expect("Failed to create test directory!");
1221
1222		let fd = fs
1223			.open_folder(&path)
1224			.await
1225			.expect("Failed to open existing folder!");
1226		assert!(
1227			fs.open_folder_handles.len() == 1,
1228			"Expected one open folder handle",
1229		);
1230		fs.close_folder(fd).await;
1231
1232		assert!(
1233			fs.open_folder_handles.is_empty(),
1234			"Somehow after opening/closing, open folder handles was not empty?",
1235		);
1236	}
1237
1238	#[tokio::test]
1239	pub async fn seek_within_folder() {
1240		let (tempdir, fs) = create_temporary_host_filesystem().await;
1241		let path = HostFilesystem::join_many(tempdir.path(), ["a", "b"]);
1242		tokio::fs::create_dir_all(path.clone())
1243			.await
1244			.expect("Failed to create test directory!");
1245
1246		// Only `c`, `d`, and `f` should be returned.
1247		//
1248		// `e` is a symlink   (ignored)
1249		// `d/a` is an item in a subdirectory (ignored)
1250		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["c"]))
1251			.await
1252			.expect("Failed to create file to use!");
1253		tokio::fs::create_dir(HostFilesystem::join_many(&path, ["d"]))
1254			.await
1255			.expect("Failed to create directory to use!");
1256		#[cfg(unix)]
1257		{
1258			use std::os::unix::fs::symlink;
1259
1260			let mut tempdir_symlink = path.clone();
1261			tempdir_symlink.push("e");
1262			symlink(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1263		}
1264		#[cfg(target_os = "windows")]
1265		{
1266			use std::os::windows::fs::symlink_dir;
1267
1268			let mut tempdir_symlink = path.clone();
1269			tempdir_symlink.push("e");
1270			symlink_dir(tempdir.path(), tempdir_symlink).expect("Failed to do symlink!");
1271		}
1272		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["f"]))
1273			.await
1274			.expect("Failed to create file to use!");
1275		_ = tokio::fs::File::create(HostFilesystem::join_many(&path, ["d", "a"]))
1276			.await
1277			.expect("Failed to create file to use!");
1278
1279		let dfd = fs.open_folder(&path).await.expect("Failed to open file!");
1280		assert!(fs
1281			.next_in_folder(dfd)
1282			.await
1283			.expect("Failed to query for next in folder! 1.1!")
1284			.is_some());
1285		assert!(fs
1286			.next_in_folder(dfd)
1287			.await
1288			.expect("Failed to query for next in folder! 1.2!")
1289			.is_some());
1290		assert!(fs
1291			.next_in_folder(dfd)
1292			.await
1293			.expect("Failed to query for next in folder! 1.3!")
1294			.is_some());
1295		// We should have hit the end...
1296		assert!(fs
1297			.next_in_folder(dfd)
1298			.await
1299			.expect("Failed to query for next in folder! 1.4!")
1300			.is_none());
1301		// We can call as many times as we want.
1302		assert!(fs
1303			.next_in_folder(dfd)
1304			.await
1305			.expect("Failed to query for next in folder! 1.5!")
1306			.is_none());
1307		// Rewind to get to reads again!
1308		fs.reverse_directory(dfd)
1309			.await
1310			.expect("Failed to reverse directory search!");
1311		assert!(fs
1312			.next_in_folder(dfd)
1313			.await
1314			.expect("Failed to query for next in folder! 2.1!")
1315			.is_some());
1316		assert!(fs
1317			.next_in_folder(dfd)
1318			.await
1319			.expect("Failed to query for next in folder! 2.2!")
1320			.is_some());
1321		assert!(fs
1322			.next_in_folder(dfd)
1323			.await
1324			.expect("Failed to query for next in folder! 2.3!")
1325			.is_some());
1326	}
1327}