1pub 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#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct FSEmulConfig {
36 configuration: Ini,
38 loaded_from_path: PathBuf,
40}
41
42impl FSEmulConfig {
43 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 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 #[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 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 #[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 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 #[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 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 #[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 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 #[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 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 #[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 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 #[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 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 #[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 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 #[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 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 pub async fn write_to_disk(&self) -> Result<(), FSError> {
344 let mut serialized_configuration = self.configuration.writes();
345 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 #[allow(
374 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 {
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}