1use crate::{
11 errors::{CatBridgeError, FSError},
12 fsemul::errors::{FSEmulAPIError, FSEmulFSError},
13};
14use bytes::{Bytes, BytesMut};
15use std::{
16 collections::BTreeMap,
17 path::{Path, PathBuf},
18};
19use tokio::fs::metadata as get_path_metadata;
20use tracing::warn;
21
22const MAX_ADDRESS: u128 = 0x000F_FFFF_FFFF_FFFF_FFFF_u128;
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct DiskLayoutFile {
28 address_to_path_map: BTreeMap<u128, PathBuf>,
30 current_ending_address: u128,
32 major_version: u8,
38 minor_version: u8,
44}
45
46impl DiskLayoutFile {
47 #[must_use]
49 pub fn new(max_address: u128) -> Self {
50 let mut map = BTreeMap::new();
51 map.insert(max_address, PathBuf::new());
52
53 Self {
54 address_to_path_map: map,
55 current_ending_address: max_address,
56 major_version: 1,
57 minor_version: 0,
58 }
59 }
60
61 #[must_use]
63 pub fn version(&self) -> String {
64 format!("v{}.{:02}", self.major_version, self.minor_version)
65 }
66
67 #[must_use]
69 pub const fn major_version(&self) -> u8 {
70 self.major_version
71 }
72
73 #[must_use]
75 pub const fn minor_version(&self) -> u8 {
76 self.minor_version
77 }
78
79 #[must_use]
81 pub const fn max_address(&self) -> u128 {
82 self.current_ending_address
83 }
84
85 #[must_use]
87 pub fn address_to_path_map(&self) -> &BTreeMap<u128, PathBuf> {
88 &self.address_to_path_map
89 }
90
91 #[must_use]
93 pub async fn get_path_and_offset_for_file(
94 &self,
95 requested_address: u128,
96 ) -> Option<(&PathBuf, u64)> {
97 if let Some(path) = self.address_to_path_map.get(&requested_address) {
99 return Some((path, 0));
100 }
101
102 let mut last_addr = 0_u128;
104 let mut last_path = self
105 .address_to_path_map
106 .get(&self.max_address())
107 .unwrap_or_else(|| unreachable!());
108 for (addr, path) in &self.address_to_path_map {
109 if *addr < requested_address {
110 last_addr = *addr;
111 last_path = path;
112 continue;
113 }
114 let metadata = match get_path_metadata(last_path).await {
115 Ok(md) => md,
116 Err(cause) => {
117 warn!(
118 ?cause,
119 path = %last_path.display(),
120 "Failed to get metadata for path, not sure if matching over SDIO, treating as non-match.",
121 );
122 break;
123 }
124 };
125 let offset = u64::try_from(requested_address - last_addr).unwrap_or(u64::MAX);
126 if metadata.len() > offset {
129 return Some((last_path, offset));
130 }
131
132 break;
134 }
135
136 None
138 }
139
140 pub fn upsert_addressed_path(
148 &mut self,
149 address: u128,
150 path: &Path,
151 ) -> Result<(), CatBridgeError> {
152 if address >= MAX_ADDRESS {
153 return Err(FSEmulAPIError::DlfAddressTooLarge(address, MAX_ADDRESS).into());
154 }
155
156 let mut update_ending_address = false;
157 if address > self.current_ending_address {
159 if !path.as_os_str().is_empty() {
160 return Err(FSEmulAPIError::DlfUpsertEndingFirst.into());
161 }
162
163 update_ending_address = true;
164 }
165
166 let canonicalized_path = path.canonicalize().map_err(FSError::from)?;
167 let Ok(_) = canonicalized_path.as_os_str().to_owned().into_string() else {
169 return Err(FSEmulAPIError::DlfPathMustBeUtf8(Bytes::from(Vec::from(
170 canonicalized_path.as_os_str().to_owned().as_encoded_bytes(),
171 )))
172 .into());
173 };
174 self.address_to_path_map.insert(address, canonicalized_path);
175
176 if update_ending_address {
177 self.current_ending_address = address;
178 }
179
180 Ok(())
181 }
182
183 pub fn remove_path_at_address(&mut self, address: u128) -> Result<(), FSEmulAPIError> {
190 if address == self.current_ending_address {
191 return Err(FSEmulAPIError::DlfMustHaveEnding);
192 }
193 self.address_to_path_map.remove(&address);
194
195 Ok(())
196 }
197}
198
199impl From<&DiskLayoutFile> for Bytes {
200 fn from(value: &DiskLayoutFile) -> Self {
201 let mut bytes = BytesMut::new();
202 bytes.extend_from_slice(value.version().as_bytes());
203 bytes.extend_from_slice(b"\r\n");
204 for (address, path) in &value.address_to_path_map {
205 bytes.extend_from_slice(
206 format!(
207 "0x{address:016X},\"{}\"\r\n",
208 path.to_string_lossy(),
210 )
211 .as_bytes(),
212 );
213 }
214
215 bytes.freeze()
216 }
217}
218
219impl From<DiskLayoutFile> for Bytes {
220 fn from(value: DiskLayoutFile) -> Self {
221 Self::from(&value)
222 }
223}
224
225impl TryFrom<Bytes> for DiskLayoutFile {
226 type Error = FSError;
227
228 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
229 let as_utf8 = String::from_utf8(value.to_vec())?;
230 let lines = as_utf8.split("\r\n").collect::<Vec<_>>();
231 if lines.len() < 3 {
242 return Err(FSError::TooFewLines(lines.len(), 3_usize));
243 }
244
245 let mut address_map = BTreeMap::new();
246 let mut last_read_address: u128 = 0;
250 for line in &lines[1..lines.len() - 2] {
251 let mut iterator = line.splitn(2, ',');
252 let Some(address_str) = iterator.next() else {
253 return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
254 };
255 let Some(path_string) = iterator.next() else {
256 return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
257 };
258
259 let address = u128::from_str_radix(address_str.trim_start_matches("0x"), 16)
261 .map_err(|_| FSEmulFSError::DlfCorruptLine((*line).to_owned()))?;
262 if last_read_address != 0 && address <= last_read_address {
265 return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
266 }
267 last_read_address = address;
268
269 let path = PathBuf::from(path_string.trim_matches('"'));
272 if path.as_os_str().is_empty() {
275 return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
276 }
277 if !path.is_absolute() {
278 return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
279 }
280 address_map.insert(address, path);
281 }
282
283 let should_be_ending_line = lines[lines.len() - 2];
284 let mut ending_iter = should_be_ending_line.splitn(2, ',');
285 let Some(final_address_str) = ending_iter.next() else {
286 return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
287 };
288 let Some(final_path_str) = ending_iter.next() else {
289 return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
290 };
291 let final_address = u128::from_str_radix(final_address_str.trim_start_matches("0x"), 16)
292 .map_err(|_| FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()))?;
293
294 if last_read_address != 0 && final_address <= last_read_address {
295 return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
296 }
297 if final_path_str != r#""""# {
298 return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
299 }
300 address_map.insert(final_address, PathBuf::new());
301 if !lines[lines.len() - 1].is_empty() {
302 return Err(
303 FSEmulFSError::DlfCorruptFinalLine(lines[lines.len() - 1].to_owned()).into(),
304 );
305 }
306
307 let version_str = lines[0];
308 if !version_str.starts_with('v') {
309 return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
310 }
311 let mut version_iter = version_str.trim_start_matches('v').splitn(2, '.');
312 let Some(major_version_str) = version_iter.next() else {
313 return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
314 };
315 let Some(minor_version_str) = version_iter.next() else {
316 return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
317 };
318 let Ok(major_version) = major_version_str.parse::<u8>() else {
319 return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
320 };
321 let Ok(minor_version) = minor_version_str.parse::<u8>() else {
322 return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
323 };
324
325 Ok(Self {
326 address_to_path_map: address_map,
327 current_ending_address: final_address,
328 major_version,
329 minor_version,
330 })
331 }
332}
333
334#[cfg(test)]
335mod unit_tests {
336 use super::*;
337
338 #[must_use]
339 pub fn get_test_data_path(relative_to_test_data: &str) -> PathBuf {
340 let mut final_path = PathBuf::from(
341 std::env::var("CARGO_MANIFEST_DIR")
342 .expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
343 );
344 final_path.push("src");
345 final_path.push("fsemul");
346 final_path.push("test-data");
347 for file_part in relative_to_test_data.split('/') {
348 if file_part.is_empty() {
349 continue;
350 }
351 final_path.push(file_part);
352 }
353 final_path
354 }
355
356 #[tokio::test]
357 pub async fn can_parse_real_files() {
358 let real_life_dlf;
360
361 #[cfg(target_os = "windows")]
362 {
363 real_life_dlf = Bytes::from(
364 std::fs::read(get_test_data_path("ppc_boot_win.dlf"))
365 .expect("Failed to read `ppc_boot.dlf` test data file!"),
366 );
367 }
368 #[cfg(not(target_os = "windows"))]
369 {
370 real_life_dlf = Bytes::from(
371 std::fs::read(get_test_data_path("ppc_boot.dlf"))
372 .expect("Failed to read `ppc_boot.dlf` test data file!"),
373 );
374 }
375
376 let empty_dlf = Bytes::from(
377 std::fs::read(get_test_data_path("minimal.dlf"))
378 .expect("Failed to read `minimal.dlf` test data file!"),
379 );
380
381 let dlf = DiskLayoutFile::try_from(real_life_dlf.clone())
382 .expect("Failed to parse real life dlf file!");
383 let edlf = DiskLayoutFile::try_from(empty_dlf.clone())
384 .expect("Failed to parse the most minimal of disk layout files!");
385
386 assert_eq!(
387 dlf.major_version(),
388 1,
389 "Real-DLF didnt parse correct major version!"
390 );
391 assert_eq!(
392 dlf.minor_version(),
393 0,
394 "Real-DLF didn't parse correct minor version!"
395 );
396 #[cfg(target_os = "windows")]
397 assert_eq!(
398 dlf.get_path_and_offset_for_file(0x80000_u128).await,
399 Some((
400 &PathBuf::from(r#"C:\cafe_sdk\temp\mythra\caferun\ppc.bsf"#),
401 0
402 )),
403 "Real-DLF did not match correct path for address.",
404 );
405 #[cfg(not(target_os = "windows"))]
406 assert_eq!(
407 dlf.get_path_and_offset_for_file(0x80000_u128).await,
408 Some((
409 &PathBuf::from(r#"/opt/cafe_sdk/temp/mythra/caferun/ppc.bsf"#),
410 0
411 )),
412 "Real-DLF did not match correct path for address.",
413 );
414 assert_eq!(
415 Bytes::from(dlf),
416 real_life_dlf,
417 "Failed to serialize real life DLF into the exact same contents as real life DLF!"
418 );
419
420 assert_eq!(
421 edlf.major_version(),
422 1,
423 "Empty DLF didn't parse correct major version!"
424 );
425 assert_eq!(
426 edlf.minor_version(),
427 0,
428 "Empty DLF didn't parse correct minor version!"
429 );
430 assert_eq!(
431 edlf.get_path_and_offset_for_file(0x0_u128).await,
432 Some((&PathBuf::new(), 0)),
433 "Empty dlf did not match correct path for address.",
434 );
435 assert_eq!(
436 Bytes::from(edlf),
437 empty_dlf,
438 "Failed to serialize empty DLF into the exact same contents as empty DLF!"
439 );
440 }
441}