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