use crate::{
errors::{CatBridgeError, FSError},
fsemul::errors::{FSEmulAPIError, FSEmulFSError},
};
use bytes::{Bytes, BytesMut};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use tokio::fs::metadata as get_path_metadata;
use tracing::warn;
const MAX_ADDRESS: u128 = 0x000F_FFFF_FFFF_FFFF_FFFF_u128;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiskLayoutFile {
address_to_path_map: BTreeMap<u128, PathBuf>,
current_ending_address: u128,
major_version: u8,
minor_version: u8,
}
impl DiskLayoutFile {
#[must_use]
pub fn new(max_address: u128) -> Self {
let mut map = BTreeMap::new();
map.insert(max_address, PathBuf::new());
Self {
address_to_path_map: map,
current_ending_address: max_address,
major_version: 1,
minor_version: 0,
}
}
#[must_use]
pub fn version(&self) -> String {
format!("v{}.{:02}", self.major_version, self.minor_version)
}
#[must_use]
pub const fn major_version(&self) -> u8 {
self.major_version
}
#[must_use]
pub const fn minor_version(&self) -> u8 {
self.minor_version
}
#[must_use]
pub const fn max_address(&self) -> u128 {
self.current_ending_address
}
#[must_use]
pub fn address_to_path_map(&self) -> &BTreeMap<u128, PathBuf> {
&self.address_to_path_map
}
#[must_use]
pub async fn get_path_and_offset_for_file(
&self,
requested_address: u128,
) -> Option<(&PathBuf, u64)> {
if let Some(path) = self.address_to_path_map.get(&requested_address) {
return Some((path, 0));
}
let mut last_addr = 0_u128;
let mut last_path = self
.address_to_path_map
.get(&self.max_address())
.unwrap_or_else(|| unreachable!());
for (addr, path) in &self.address_to_path_map {
if *addr < requested_address {
last_addr = *addr;
last_path = path;
continue;
}
let metadata = match get_path_metadata(last_path).await {
Ok(md) => md,
Err(cause) => {
warn!(
?cause,
path = %last_path.display(),
"Failed to get metadata for path, not sure if matching over SDIO, treating as non-match.",
);
break;
}
};
let offset = u64::try_from(requested_address - last_addr).unwrap_or(u64::MAX);
if metadata.len() > offset {
return Some((last_path, offset));
}
break;
}
None
}
pub fn upsert_addressed_path(
&mut self,
address: u128,
path: &Path,
) -> Result<(), CatBridgeError> {
if address >= MAX_ADDRESS {
return Err(FSEmulAPIError::DlfAddressTooLarge(address, MAX_ADDRESS).into());
}
let mut update_ending_address = false;
if address > self.current_ending_address {
if !path.as_os_str().is_empty() {
return Err(FSEmulAPIError::DlfUpsertEndingFirst.into());
}
update_ending_address = true;
}
let canonicalized_path = path.canonicalize().map_err(FSError::from)?;
let Ok(_) = canonicalized_path.as_os_str().to_owned().into_string() else {
return Err(FSEmulAPIError::DlfPathMustBeUtf8(Bytes::from(Vec::from(
canonicalized_path.as_os_str().to_owned().as_encoded_bytes(),
)))
.into());
};
self.address_to_path_map.insert(address, canonicalized_path);
if update_ending_address {
self.current_ending_address = address;
}
Ok(())
}
pub fn remove_path_at_address(&mut self, address: u128) -> Result<(), FSEmulAPIError> {
if address == self.current_ending_address {
return Err(FSEmulAPIError::DlfMustHaveEnding);
}
self.address_to_path_map.remove(&address);
Ok(())
}
}
impl From<&DiskLayoutFile> for Bytes {
fn from(value: &DiskLayoutFile) -> Self {
let mut bytes = BytesMut::new();
bytes.extend_from_slice(value.version().as_bytes());
bytes.extend_from_slice(b"\r\n");
for (address, path) in &value.address_to_path_map {
bytes.extend_from_slice(
format!(
"0x{address:016X},\"{}\"\r\n",
path.to_string_lossy(),
)
.as_bytes(),
);
}
bytes.freeze()
}
}
impl From<DiskLayoutFile> for Bytes {
fn from(value: DiskLayoutFile) -> Self {
Self::from(&value)
}
}
impl TryFrom<Bytes> for DiskLayoutFile {
type Error = FSError;
fn try_from(value: Bytes) -> Result<Self, Self::Error> {
let as_utf8 = String::from_utf8(value.to_vec())?;
let lines = as_utf8.split("\r\n").collect::<Vec<_>>();
if lines.len() < 3 {
return Err(FSError::TooFewLines(lines.len(), 3_usize));
}
let mut address_map = BTreeMap::new();
let mut last_read_address: u128 = 0;
for line in &lines[1..lines.len() - 2] {
let mut iterator = line.splitn(2, ',');
let Some(address_str) = iterator.next() else {
return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
};
let Some(path_string) = iterator.next() else {
return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
};
let address = u128::from_str_radix(address_str.trim_start_matches("0x"), 16)
.map_err(|_| FSEmulFSError::DlfCorruptLine((*line).to_owned()))?;
if last_read_address != 0 && address <= last_read_address {
return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
}
last_read_address = address;
let path = PathBuf::from(path_string.trim_matches('"'));
if path.as_os_str().is_empty() {
return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
}
if !path.is_absolute() {
return Err(FSEmulFSError::DlfCorruptLine((*line).to_owned()).into());
}
address_map.insert(address, path);
}
let should_be_ending_line = lines[lines.len() - 2];
let mut ending_iter = should_be_ending_line.splitn(2, ',');
let Some(final_address_str) = ending_iter.next() else {
return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
};
let Some(final_path_str) = ending_iter.next() else {
return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
};
let final_address = u128::from_str_radix(final_address_str.trim_start_matches("0x"), 16)
.map_err(|_| FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()))?;
if last_read_address != 0 && final_address <= last_read_address {
return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
}
if final_path_str != r#""""# {
return Err(FSEmulFSError::DlfCorruptLine(should_be_ending_line.to_owned()).into());
}
address_map.insert(final_address, PathBuf::new());
if !lines[lines.len() - 1].is_empty() {
return Err(
FSEmulFSError::DlfCorruptFinalLine(lines[lines.len() - 1].to_owned()).into(),
);
}
let version_str = lines[0];
if !version_str.starts_with('v') {
return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
}
let mut version_iter = version_str.trim_start_matches('v').splitn(2, '.');
let Some(major_version_str) = version_iter.next() else {
return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
};
let Some(minor_version_str) = version_iter.next() else {
return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
};
let Ok(major_version) = major_version_str.parse::<u8>() else {
return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
};
let Ok(minor_version) = minor_version_str.parse::<u8>() else {
return Err(FSEmulFSError::DlfCorruptVersionLine(version_str.to_owned()).into());
};
Ok(Self {
address_to_path_map: address_map,
current_ending_address: final_address,
major_version,
minor_version,
})
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[must_use]
pub fn get_test_data_path(relative_to_test_data: &str) -> PathBuf {
let mut final_path = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
);
final_path.push("src");
final_path.push("fsemul");
final_path.push("test-data");
for file_part in relative_to_test_data.split('/') {
if file_part.is_empty() {
continue;
}
final_path.push(file_part);
}
final_path
}
#[tokio::test]
pub async fn can_parse_real_files() {
let real_life_dlf;
#[cfg(target_os = "windows")]
{
real_life_dlf = Bytes::from(
std::fs::read(get_test_data_path("ppc_boot_win.dlf"))
.expect("Failed to read `ppc_boot.dlf` test data file!"),
);
}
#[cfg(not(target_os = "windows"))]
{
real_life_dlf = Bytes::from(
std::fs::read(get_test_data_path("ppc_boot.dlf"))
.expect("Failed to read `ppc_boot.dlf` test data file!"),
);
}
let empty_dlf = Bytes::from(
std::fs::read(get_test_data_path("minimal.dlf"))
.expect("Failed to read `minimal.dlf` test data file!"),
);
let dlf = DiskLayoutFile::try_from(real_life_dlf.clone())
.expect("Failed to parse real life dlf file!");
let edlf = DiskLayoutFile::try_from(empty_dlf.clone())
.expect("Failed to parse the most minimal of disk layout files!");
assert_eq!(
dlf.major_version(),
1,
"Real-DLF didnt parse correct major version!"
);
assert_eq!(
dlf.minor_version(),
0,
"Real-DLF didn't parse correct minor version!"
);
#[cfg(target_os = "windows")]
assert_eq!(
dlf.get_path_and_offset_for_file(0x80000_u128).await,
Some((
&PathBuf::from(r#"C:\cafe_sdk\temp\mythra\caferun\ppc.bsf"#),
0
)),
"Real-DLF did not match correct path for address.",
);
#[cfg(not(target_os = "windows"))]
assert_eq!(
dlf.get_path_and_offset_for_file(0x80000_u128).await,
Some((
&PathBuf::from(r#"/opt/cafe_sdk/temp/mythra/caferun/ppc.bsf"#),
0
)),
"Real-DLF did not match correct path for address.",
);
assert_eq!(
Bytes::from(dlf),
real_life_dlf,
"Failed to serialize real life DLF into the exact same contents as real life DLF!"
);
assert_eq!(
edlf.major_version(),
1,
"Empty DLF didn't parse correct major version!"
);
assert_eq!(
edlf.minor_version(),
0,
"Empty DLF didn't parse correct minor version!"
);
assert_eq!(
edlf.get_path_and_offset_for_file(0x0_u128).await,
Some((&PathBuf::new(), 0)),
"Empty dlf did not match correct path for address.",
);
assert_eq!(
Bytes::from(edlf),
empty_dlf,
"Failed to serialize empty DLF into the exact same contents as empty DLF!"
);
}
}