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