use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use nod::{Disc, PartitionBase, PartitionKind, PartitionMeta};
use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::formats::FilesystemType;
use crate::gameid::{Console, GameDiscInfo, Region};
pub struct NodeFilesystem {
partition: Box<dyn PartitionBase>,
meta: Box<PartitionMeta>,
volume_name: String,
}
impl NodeFilesystem {
pub fn new(path: &Path) -> Result<Self, FilesystemError> {
let disc = Disc::new(path).map_err(nod_err)?;
let volume_name = ascii_z(&disc.header().game_title);
let mut partition = disc
.open_partition_kind(PartitionKind::Data)
.map_err(nod_err)?;
let meta = partition.meta().map_err(nod_err)?;
Ok(Self {
partition,
meta,
volume_name,
})
}
fn node_at(&self, idx: usize) -> Result<nod::Node, FilesystemError> {
let fst = self
.meta
.fst()
.map_err(|e| FilesystemError::Parse(e.into()))?;
fst.nodes
.get(idx)
.copied()
.ok_or_else(|| FilesystemError::NotFound(format!("FST node {idx}")))
}
}
impl Filesystem for NodeFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(FileEntry {
name: String::new(),
path: "/".into(),
entry_type: EntryType::Directory,
size: 0,
location: 0,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
})
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
let fst = self
.meta
.fst()
.map_err(|e| FilesystemError::Parse(e.into()))?;
let dir_idx = entry.location as usize;
let dir = fst
.nodes
.get(dir_idx)
.copied()
.ok_or_else(|| FilesystemError::NotFound(entry.path.clone()))?;
if !dir.is_dir() {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let end = (dir.length() as usize).min(fst.nodes.len());
let base = entry.path.trim_end_matches('/');
let mut out = Vec::new();
let mut i = dir_idx + 1;
while i < end {
let child = fst.nodes[i];
let name = fst
.get_name(child)
.map_err(FilesystemError::Parse)?
.into_owned();
let path = format!("{base}/{name}");
if child.is_dir() {
out.push(dir_entry(name, path, i));
let next = (child.length() as usize).max(i + 1);
i = next;
} else {
out.push(file_entry(name, path, child.length(), i));
i += 1;
}
}
Ok(out)
}
fn read_file(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
let node = self.node_at(entry.location as usize)?;
if !node.is_file() {
return Err(FilesystemError::NotFound(format!(
"{} is not a file",
entry.path
)));
}
let mut stream = self
.partition
.open_file(node)
.map_err(FilesystemError::Io)?;
let mut buf = Vec::with_capacity(node.length() as usize);
stream.read_to_end(&mut buf).map_err(FilesystemError::Io)?;
Ok(buf)
}
fn read_file_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Vec<u8>, FilesystemError> {
let node = self.node_at(entry.location as usize)?;
if !node.is_file() {
return Err(FilesystemError::NotFound(format!(
"{} is not a file",
entry.path
)));
}
let mut stream = self
.partition
.open_file(node)
.map_err(FilesystemError::Io)?;
stream
.seek(SeekFrom::Start(offset))
.map_err(FilesystemError::Io)?;
let mut buf = vec![0u8; length];
let n = read_up_to(&mut stream, &mut buf)?;
buf.truncate(n);
Ok(buf)
}
fn read_resource_fork(
&mut self,
_entry: &FileEntry,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn read_resource_fork_range(
&mut self,
_entry: &FileEntry,
_offset: u64,
_length: usize,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn volume_name(&self) -> Option<&str> {
if self.volume_name.is_empty() {
None
} else {
Some(&self.volume_name)
}
}
}
pub fn probe_nintendo(path: &Path) -> Option<(FilesystemType, GameDiscInfo)> {
let disc = Disc::new(path).ok()?;
let header = disc.header();
let (fs, console) = if header.is_wii() {
(FilesystemType::Wii, Console::Wii)
} else if header.is_gamecube() {
(FilesystemType::GameCube, Console::GameCube)
} else {
return None;
};
let serial = ascii_z(&header.game_id);
let title = ascii_z(&header.game_title);
let region = header.game_id.get(3).and_then(|&c| nintendo_region(c));
let maker = header
.game_id
.get(4..6)
.map(ascii_z)
.filter(|m| !m.is_empty());
Some((
fs,
GameDiscInfo {
console,
serial: (!serial.is_empty()).then_some(serial),
title: (!title.is_empty()).then_some(title),
region,
maker,
version: Some(format!("rev {}", header.disc_version)),
},
))
}
fn nintendo_region(c: u8) -> Option<Region> {
match c {
b'E' | b'N' => Some(Region::NtscU),
b'J' => Some(Region::NtscJ),
b'P' | b'D' | b'F' | b'I' | b'S' | b'H' | b'X' | b'Y' | b'Z' | b'U' => Some(Region::Pal),
b'K' | b'Q' | b'T' => Some(Region::Korea),
b'W' => Some(Region::Asia),
_ => None,
}
}
fn dir_entry(name: String, path: String, idx: usize) -> FileEntry {
FileEntry {
name,
path,
entry_type: EntryType::Directory,
size: 0,
location: idx as u64,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
fn file_entry(name: String, path: String, size: u64, idx: usize) -> FileEntry {
FileEntry {
name,
path,
entry_type: EntryType::File,
size,
location: idx as u64,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
fn ascii_z(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn read_up_to(stream: &mut impl Read, buf: &mut [u8]) -> Result<usize, FilesystemError> {
let mut total = 0;
while total < buf.len() {
match stream.read(&mut buf[total..]) {
Ok(0) => break,
Ok(n) => total += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(FilesystemError::Io(e)),
}
}
Ok(total)
}
fn nod_err(e: nod::Error) -> FilesystemError {
FilesystemError::InvalidData(format!("nod: {e}"))
}