Skip to main content

cat_dev/fsemul/
mod.rs

1//! Code related to filesystem emulation.
2//!
3//! It should be noted there are two common terms when talking about file
4//! system emulation.
5//!
6//! - `FSEmul` is the core process that talks with the MION, and talks with
7//!   the MION to implement effectively all of the actual protocols.
8//! - `PCFS`/`PCFSServer` are tools built _on-top_ of `FSEmul`, and contain
9//!   their own protocols to implement a filesystem on your PC.
10//!
11//! This is meant to be an all encompassing set of utilities related to
12//! file-system emulation, and as a result covers _both_ `FSEmul` & `PCFS`.
13
14pub mod atapi;
15pub mod bsf;
16pub mod dlf;
17pub mod errors;
18pub mod filesystem;
19pub mod pcfs;
20pub mod sdio;
21
22use crate::{errors::FSError, fsemul::errors::FSEmulFSError};
23use configparser::ini::Ini;
24use std::path::PathBuf;
25use tracing::warn;
26
27/// Active configuration for file-system emulation.
28///
29/// This is generally soter along with the host-bridge configuration in a
30/// simple ini file without much configuration. All official nintendo tools
31/// will read from this file as opposed to querying the actual device itself.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct FSEmulConfig {
34	/// The fully existing configuration as we know it.
35	configuration: Ini,
36	/// The path we originally loaded ourselves from.
37	loaded_from_path: PathBuf,
38}
39
40impl FSEmulConfig {
41	/// Attempt to load the fsemul configuration from the filesystem.
42	///
43	/// This is commonly referred to as `fsemul.ini`, stored normally in
44	/// Windows under the `C:\Program Files\Nintendo\HostBridge\fsemul.ini`
45	/// file. This is where tools like `fsemul` store which ports are being used,
46	/// and where things like Session Manager ports happen.
47	///
48	/// ## Errors
49	///
50	/// - If we cannot get the default host path for your OS.
51	/// - Any error case from [`FSEmulConfig::load_explicit_path`].
52	pub async fn load() -> Result<Self, FSError> {
53		let default_host_path = Self::get_default_host_path().ok_or(FSEmulFSError::CantFindPath)?;
54		Self::load_explicit_path(default_host_path).await
55	}
56
57	/// Attempt to load the fsemul configuration file from the filesystem.
58	///
59	/// This is commonly referred to as `fsemul.ini`, and is a small
60	/// Windows (so UTF8) ini file, separated by newlines being `\r\n`.
61	///
62	/// ## Errors
63	///
64	/// - If we cannot read from the file on the file system.
65	/// - If we cannot parse the data in the file as UTF8.
66	/// - If we cannot parse the data as an INI file.
67	pub async fn load_explicit_path(path: PathBuf) -> Result<Self, FSError> {
68		if path.exists() {
69			let as_bytes = tokio::fs::read(&path).await?;
70			let as_string = String::from_utf8(as_bytes)?;
71
72			let mut ini_contents = Ini::new_cs();
73			ini_contents
74				.read(as_string)
75				.map_err(|ini_error| FSError::InvalidDataNeedsToBeINI(format!("{ini_error:?}")))?;
76
77			Ok(Self {
78				configuration: ini_contents,
79				loaded_from_path: path,
80			})
81		} else {
82			Ok(Self {
83				configuration: Ini::new_cs(),
84				loaded_from_path: path,
85			})
86		}
87	}
88
89	/// Get the configured ATAPI Emulation port if one has been configured.
90	#[must_use]
91	pub fn get_atapi_emulation_port(&self) -> Option<u16> {
92		self.configuration
93			.get("DEBUG_PORTS", "ATAPI_EMUL")
94			.and_then(|data| match data.parse::<u16>() {
95				Ok(value) => Some(value),
96				Err(cause) => {
97					warn!(
98						?cause,
99						fsemul.path = %self.loaded_from_path.display(),
100						fsemul.section_name = "DEBUG_PORTS",
101						fsemul.value_name = "ATAPI_EMUL",
102						fsemul.value_raw = data,
103						"Failed to parse ATAPI Emulation port as number, ignoring!",
104					);
105					None
106				}
107			})
108	}
109
110	/// Set the current ATAPI Emulation port.
111	pub fn set_atapi_emulation_port(&mut self, port: u16) {
112		self.configuration
113			.set("DEBUG_PORTS", "ATAPI_EMUL", Some(format!("{port}")));
114	}
115
116	/// Get the configured debug out port if one has been configured.
117	#[must_use]
118	pub fn get_debug_out_port(&self) -> Option<u16> {
119		self.configuration
120			.get("DEBUG_PORTS", "DEBUG_OUT")
121			.and_then(|data| match data.parse::<u16>() {
122				Ok(value) => Some(value),
123				Err(cause) => {
124					warn!(
125						?cause,
126						fsemul.path = %self.loaded_from_path.display(),
127						fsemul.section_name = "DEBUG_PORTS",
128						fsemul.value_name = "DEBUG_OUT",
129						fsemul.value_raw = data,
130						"Failed to parse Debug OUT port as number, ignoring!",
131					);
132					None
133				}
134			})
135	}
136
137	/// Set the current Debug Out port.
138	pub fn set_debug_out_port(&mut self, port: u16) {
139		self.configuration
140			.set("DEBUG_PORTS", "DEBUG_OUT", Some(format!("{port}")));
141	}
142
143	/// Get the configured debug control port if one has been configured.
144	#[must_use]
145	pub fn get_debug_control_port(&self) -> Option<u16> {
146		self.configuration
147			.get("DEBUG_PORTS", "DEBUG_CONTROL")
148			.and_then(|data| match data.parse::<u16>() {
149				Ok(value) => Some(value),
150				Err(cause) => {
151					warn!(
152						?cause,
153						fsemul.path = %self.loaded_from_path.display(),
154						fsemul.section_name = "DEBUG_PORTS",
155						fsemul.value_name = "DEBUG_CONTROL",
156						fsemul.value_raw = data,
157						"Failed to parse Debug CTRL port as number, ignoring!",
158					);
159					None
160				}
161			})
162	}
163
164	/// Set the current Debug Control port.
165	pub fn set_debug_ctrl_port(&mut self, port: u16) {
166		self.configuration
167			.set("DEBUG_PORTS", "DEBUG_CONTROL", Some(format!("{port}")));
168	}
169
170	/// Get the configured HIO out port if one has been configured.
171	#[must_use]
172	pub fn get_hio_out_port(&self) -> Option<u16> {
173		self.configuration
174			.get("DEBUG_PORTS", "HIO_OUT")
175			.and_then(|data| match data.parse::<u16>() {
176				Ok(value) => Some(value),
177				Err(cause) => {
178					warn!(
179						?cause,
180						fsemul.path = %self.loaded_from_path.display(),
181						fsemul.section_name = "DEBUG_PORTS",
182						fsemul.value_name = "HIO_OUT",
183						fsemul.value_raw = data,
184						"Failed to parse HIO OUT port as number, ignoring!",
185					);
186					None
187				}
188			})
189	}
190
191	/// Set the current HIO OUT port.
192	pub fn set_hio_out_port(&mut self, port: u16) {
193		self.configuration
194			.set("DEBUG_PORTS", "HIO_OUT", Some(format!("{port}")));
195	}
196
197	/// Get the configured PCFS Character port if one has been configured.
198	#[must_use]
199	pub fn get_pcfs_character_port(&self) -> Option<u16> {
200		self.configuration
201			.get("DEBUG_PORTS", "CHAR_PCFS")
202			.and_then(|data| match data.parse::<u16>() {
203				Ok(value) => Some(value),
204				Err(cause) => {
205					warn!(
206						?cause,
207						fsemul.path = %self.loaded_from_path.display(),
208						fsemul.section_name = "DEBUG_PORTS",
209						fsemul.value_name = "CHAR_PCFS",
210						fsemul.value_raw = data,
211						"Failed to parse PCFS Character port as number, ignoring!",
212					);
213					None
214				}
215			})
216	}
217
218	/// Set the current PCFS Character port.
219	pub fn set_pcfs_character_port(&mut self, port: u16) {
220		self.configuration
221			.set("DEBUG_PORTS", "CHAR_PCFS", Some(format!("{port}")));
222	}
223
224	/// Get the configured PCFS Block port if one has been configured.
225	#[must_use]
226	pub fn get_pcfs_block_port(&self) -> Option<u16> {
227		self.configuration
228			.get("DEBUG_PORTS", "PCFS_INOUT")
229			.and_then(|data| match data.parse::<u16>() {
230				Ok(value) => Some(value),
231				Err(cause) => {
232					warn!(
233						?cause,
234						fsemul.path = %self.loaded_from_path.display(),
235						fsemul.section_name = "DEBUG_PORTS",
236						fsemul.value_name = "PCFS_INOUT",
237						fsemul.value_raw = data,
238						"Failed to parse PCFS Block port as number, ignoring!",
239					);
240					None
241				}
242			})
243	}
244
245	/// Set the current PCFS Block port.
246	pub fn set_pcfs_block_port(&mut self, port: u16) {
247		self.configuration
248			.set("DEBUG_PORTS", "PCFS_INOUT", Some(format!("{port}")));
249	}
250
251	/// Get the configured Launch Control port if one has been configured.
252	#[must_use]
253	pub fn get_launch_control_port(&self) -> Option<u16> {
254		self.configuration
255			.get("DEBUG_PORTS", "LAUNCH_CTRL")
256			.and_then(|data| match data.parse::<u16>() {
257				Ok(value) => Some(value),
258				Err(cause) => {
259					warn!(
260						?cause,
261						fsemul.path = %self.loaded_from_path.display(),
262						fsemul.section_name = "DEBUG_PORTS",
263						fsemul.value_name = "LAUNCH_CTRL",
264						fsemul.value_raw = data,
265						"Failed to parse Launch Control port as number, ignoring!",
266					);
267					None
268				}
269			})
270	}
271
272	/// Set the current Launch Control port.
273	pub fn set_launch_control_port(&mut self, port: u16) {
274		self.configuration
275			.set("DEBUG_PORTS", "LAUNCH_CTRL", Some(format!("{port}")));
276	}
277
278	/// Get the configured Net Manage port if one has been configured.
279	#[must_use]
280	pub fn get_net_manage_port(&self) -> Option<u16> {
281		self.configuration
282			.get("DEBUG_PORTS", "NET_MANAGE")
283			.and_then(|data| match data.parse::<u16>() {
284				Ok(value) => Some(value),
285				Err(cause) => {
286					warn!(
287						?cause,
288						fsemul.path = %self.loaded_from_path.display(),
289						fsemul.section_name = "DEBUG_PORTS",
290						fsemul.value_name = "NET_MANAGE",
291						fsemul.value_raw = data,
292						"Failed to parse Net Manage port as number, ignoring!",
293					);
294					None
295				}
296			})
297	}
298
299	/// Set the current Net Manage port.
300	pub fn set_net_manage_port(&mut self, port: u16) {
301		self.configuration
302			.set("DEBUG_PORTS", "NET_MANAGE", Some(format!("{port}")));
303	}
304
305	/// Get the configured PCFS Sata port if one has been configured.
306	#[must_use]
307	pub fn get_pcfs_sata_port(&self) -> Option<u16> {
308		self.configuration
309			.get("DEBUG_PORTS", "PCFS_SATA")
310			.and_then(|data| match data.parse::<u16>() {
311				Ok(value) => Some(value),
312				Err(cause) => {
313					warn!(
314						?cause,
315						fsemul.path = %self.loaded_from_path.display(),
316						fsemul.section_name = "DEBUG_PORTS",
317						fsemul.value_name = "PCFS_SATA",
318						fsemul.value_raw = data,
319						"Failed to parse PCFS Sata port as number, ignoring!",
320					);
321					None
322				}
323			})
324	}
325
326	/// Set the current PCFS Sata port.
327	pub fn set_pcfs_sata_port(&mut self, port: u16) {
328		self.configuration
329			.set("DEBUG_PORTS", "PCFS_SATA", Some(format!("{port}")));
330	}
331
332	/// Write the current configuration to disk as a Windows INI file.
333	///
334	/// We always write the file with carriage returns `\r\n` (windows line
335	/// endings), and in UTF-8. So we can always copy-paste the file onto
336	/// a windows host and have it be read by the official tools without issue.
337	///
338	/// ## Errors
339	///
340	/// If we run into a system error when writing the file to the disk.
341	pub async fn write_to_disk(&self) -> Result<(), FSError> {
342		let mut serialized_configuration = self.configuration.writes();
343		// Multiline is disabled -- so this is safe to check if we have actual carriage returns.
344		if !serialized_configuration.contains("\r\n") {
345			serialized_configuration = serialized_configuration.replace('\n', "\r\n");
346		}
347
348		let parent_dir = {
349			let mut path = self.loaded_from_path.clone();
350			path.pop();
351			path
352		};
353		tokio::fs::create_dir_all(&parent_dir).await?;
354
355		tokio::fs::write(
356			&self.loaded_from_path,
357			serialized_configuration.into_bytes(),
358		)
359		.await?;
360
361		Ok(())
362	}
363
364	/// Get the default path that the bridge host state is supposed to be stored
365	/// in.
366	///
367	/// NOTE: this directory is not necissarily guaranteed to exist.
368	///
369	/// Returns none when we can't find an appropriate path to store bridge host
370	/// state in.
371	#[allow(
372		// We explicitly use cfg blocks to block all escape.
373		//
374		// However, if you're on a non explicitly mentioned OS, we still want the
375		// fallback.
376		unreachable_code,
377	)]
378	#[must_use]
379	pub fn get_default_host_path() -> Option<PathBuf> {
380		#[cfg(target_os = "windows")]
381		{
382			return Some(PathBuf::from(
383				r"C:\Program Files\Nintendo\HostBridge\fsemul.ini",
384			));
385		}
386
387		#[cfg(target_os = "macos")]
388		{
389			use std::env::var as env_var;
390			if let Ok(home_dir) = env_var("HOME") {
391				let mut path = PathBuf::from(home_dir);
392				path.push("Library");
393				path.push("Application Support");
394				path.push("Nintendo");
395				path.push("HostBridge");
396				path.push("fsemul.ini");
397				return Some(path);
398			}
399
400			return None;
401		}
402
403		#[cfg(any(
404			target_os = "linux",
405			target_os = "freebsd",
406			target_os = "openbsd",
407			target_os = "netbsd"
408		))]
409		{
410			use std::env::var as env_var;
411			if let Ok(xdg_config_dir) = env_var("XDG_CONFIG_HOME") {
412				let mut path = PathBuf::from(xdg_config_dir);
413				path.push("Nintendo");
414				path.push("HostBridge");
415				path.push("fsemul.ini");
416				return Some(path);
417			} else if let Ok(home_dir) = env_var("HOME") {
418				let mut path = PathBuf::from(home_dir);
419				path.push(".config");
420				path.push("Nintendo");
421				path.push("HostBridge");
422				path.push("fsemul.ini");
423				return Some(path);
424			}
425
426			return None;
427		}
428
429		None
430	}
431}
432
433#[cfg(test)]
434mod unit_tests {
435	use super::*;
436
437	#[tokio::test]
438	pub async fn can_load_ini_files() {
439		let mut test_data_dir = PathBuf::from(
440			std::env::var("CARGO_MANIFEST_DIR")
441				.expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
442		);
443		test_data_dir.push("src");
444		test_data_dir.push("fsemul");
445		test_data_dir.push("test-data");
446
447		// Real actual fsemul configuration file I had.
448		{
449			let mut base_path = test_data_dir.clone();
450			base_path.push("orig-fsemul.ini");
451			let loaded = FSEmulConfig::load_explicit_path(base_path).await;
452
453			assert!(
454				loaded.is_ok(),
455				"Failed to load a real original `fsemul.ini`: {:?}",
456				loaded,
457			);
458			let fsemul = loaded.unwrap();
459
460			assert_eq!(fsemul.get_atapi_emulation_port(), None);
461			assert_eq!(fsemul.get_debug_out_port(), Some(6001));
462			assert_eq!(fsemul.get_debug_control_port(), Some(6002));
463			assert_eq!(fsemul.get_hio_out_port(), None);
464			assert_eq!(fsemul.get_pcfs_character_port(), None);
465			assert_eq!(fsemul.get_pcfs_block_port(), None);
466			assert_eq!(fsemul.get_launch_control_port(), None);
467			assert_eq!(fsemul.get_net_manage_port(), None);
468			assert_eq!(fsemul.get_pcfs_sata_port(), None);
469		}
470	}
471
472	#[test]
473	pub fn can_get_default_path_for_os() {
474		assert!(
475			FSEmulConfig::get_default_host_path().is_some(),
476			"Failed to get default FSEMul.ini path for your os!",
477		);
478	}
479
480	#[tokio::test]
481	pub async fn can_set_and_write_to_file() {
482		use tempfile::tempdir;
483		use tokio::fs::File;
484
485		let temporary_directory =
486			tempdir().expect("Failed to create temporary directory for tests!");
487		let mut path = PathBuf::from(temporary_directory.path());
488		path.push("fsemul_custom_made.ini");
489		{
490			File::create(&path)
491				.await
492				.expect("Failed to create test file to write too!");
493		}
494		let mut conf = FSEmulConfig::load_explicit_path(path.clone())
495			.await
496			.expect("Failed to load empty file to write too!");
497
498		conf.set_atapi_emulation_port(8000);
499		assert!(conf.write_to_disk().await.is_ok());
500
501		let read_data = String::from_utf8(
502			tokio::fs::read(path)
503				.await
504				.expect("Failed to read written data!"),
505		)
506		.expect("Written INI file wasn't UTF8?");
507		assert_eq!(read_data, "[DEBUG_PORTS]\r\nATAPI_EMUL=8000\r\n");
508	}
509}