mirror of https://github.com/encounter/nod-rs.git
375 lines
12 KiB
Rust
375 lines
12 KiB
Rust
use std::{
|
|
fs::File,
|
|
io,
|
|
io::{BufReader, Read, Seek, SeekFrom},
|
|
path::{Component, Path, PathBuf},
|
|
};
|
|
|
|
use aes::{
|
|
cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit},
|
|
Aes128,
|
|
};
|
|
|
|
use crate::{
|
|
disc::SECTOR_SIZE,
|
|
io::DiscIO,
|
|
streams::ReadStream,
|
|
util::reader::{read_vec, struct_size, FromReader},
|
|
Error, Result, ResultContext,
|
|
};
|
|
|
|
type Aes128Cbc = cbc::Decryptor<Aes128>;
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub(crate) struct LBARange {
|
|
pub(crate) start_block: u32,
|
|
pub(crate) num_blocks: u32,
|
|
}
|
|
|
|
impl FromReader for LBARange {
|
|
type Args<'a> = ();
|
|
|
|
const STATIC_SIZE: usize = struct_size([
|
|
u32::STATIC_SIZE, // start_block
|
|
u32::STATIC_SIZE, // num_blocks
|
|
]);
|
|
|
|
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
|
|
where R: Read + ?Sized {
|
|
Ok(LBARange {
|
|
start_block: u32::from_reader(reader)?,
|
|
num_blocks: u32::from_reader(reader)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
type MagicBytes = [u8; 4];
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub(crate) struct NFSHeader {
|
|
pub(crate) version: u32,
|
|
pub(crate) unk1: u32,
|
|
pub(crate) unk2: u32,
|
|
pub(crate) lba_ranges: Vec<LBARange>,
|
|
}
|
|
|
|
impl FromReader for NFSHeader {
|
|
type Args<'a> = ();
|
|
|
|
const STATIC_SIZE: usize = struct_size([
|
|
MagicBytes::STATIC_SIZE, // magic
|
|
u32::STATIC_SIZE, // version
|
|
u32::STATIC_SIZE, // unk1
|
|
u32::STATIC_SIZE, // unk2
|
|
u32::STATIC_SIZE, // lba_range_count
|
|
LBARange::STATIC_SIZE * 61, // lba_ranges
|
|
MagicBytes::STATIC_SIZE, // end_magic
|
|
]);
|
|
|
|
fn from_reader_args<R>(reader: &mut R, _args: Self::Args<'_>) -> io::Result<Self>
|
|
where R: Read + ?Sized {
|
|
if MagicBytes::from_reader(reader)? != *b"EGGS" {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid NFS magic"));
|
|
}
|
|
let version = u32::from_reader(reader)?;
|
|
let unk1 = u32::from_reader(reader)?;
|
|
let unk2 = u32::from_reader(reader)?;
|
|
let lba_range_count = u32::from_reader(reader)?;
|
|
let mut lba_ranges = read_vec(reader, 61)?;
|
|
lba_ranges.truncate(lba_range_count as usize);
|
|
if MagicBytes::from_reader(reader)? != *b"SGGE" {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid NFS end magic"));
|
|
}
|
|
Ok(NFSHeader { version, unk1, unk2, lba_ranges })
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub(crate) struct Fbo {
|
|
pub(crate) file: u32,
|
|
pub(crate) block: u32,
|
|
pub(crate) l_block: u32,
|
|
pub(crate) offset: u32,
|
|
}
|
|
|
|
impl Default for Fbo {
|
|
fn default() -> Self {
|
|
Fbo { file: u32::MAX, block: u32::MAX, l_block: u32::MAX, offset: u32::MAX }
|
|
}
|
|
}
|
|
|
|
impl NFSHeader {
|
|
pub(crate) fn calculate_num_files(&self) -> u32 {
|
|
let total_block_count =
|
|
self.lba_ranges.iter().fold(0u32, |acc, range| acc + range.num_blocks);
|
|
(((total_block_count as u64) * 0x8000u64 + (0x200u64 + 0xF9FFFFFu64)) / 0xFA00000u64) as u32
|
|
}
|
|
|
|
pub(crate) fn logical_to_fbo(&self, offset: u64) -> Fbo {
|
|
let block_div = (offset / 0x8000) as u32;
|
|
let block_off = (offset % 0x8000) as u32;
|
|
let mut block = u32::MAX;
|
|
let mut physical_block = 0u32;
|
|
for range in self.lba_ranges.iter() {
|
|
if block_div >= range.start_block && block_div - range.start_block < range.num_blocks {
|
|
block = physical_block + (block_div - range.start_block);
|
|
break;
|
|
}
|
|
physical_block += range.num_blocks;
|
|
}
|
|
if block == u32::MAX {
|
|
Fbo::default()
|
|
} else {
|
|
Fbo { file: block / 8000, block: block % 8000, l_block: block_div, offset: block_off }
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct DiscIONFS {
|
|
pub(crate) directory: PathBuf,
|
|
pub(crate) key: [u8; 16],
|
|
pub(crate) header: Option<NFSHeader>,
|
|
}
|
|
|
|
impl DiscIONFS {
|
|
pub(crate) fn new(directory: &Path) -> Result<DiscIONFS> {
|
|
let mut disc_io = DiscIONFS { directory: directory.to_owned(), key: [0; 16], header: None };
|
|
disc_io.validate_files()?;
|
|
Ok(disc_io)
|
|
}
|
|
}
|
|
|
|
pub(crate) struct NFSReadStream<'a> {
|
|
disc_io: &'a DiscIONFS,
|
|
file: Option<File>,
|
|
crypto: [u8; 16],
|
|
// Physical address - all UINT32_MAX indicates logical zero block
|
|
phys_addr: Fbo,
|
|
// Logical address
|
|
offset: u64,
|
|
// Active file stream and its offset as set in the system.
|
|
// Block is typically one ahead of the presently decrypted block.
|
|
cur_file: u32,
|
|
cur_block: u32,
|
|
buf: [u8; SECTOR_SIZE],
|
|
}
|
|
|
|
impl<'a> NFSReadStream<'a> {
|
|
fn set_cur_file(&mut self, cur_file: u32) -> Result<()> {
|
|
if cur_file >= self.disc_io.header.as_ref().unwrap().calculate_num_files() {
|
|
return Err(Error::DiscFormat(format!("Out of bounds NFS file access: {}", cur_file)));
|
|
}
|
|
self.cur_file = cur_file;
|
|
self.cur_block = u32::MAX;
|
|
let path = self.disc_io.get_nfs(cur_file)?;
|
|
self.file = Option::from(
|
|
File::open(&path).with_context(|| format!("Opening file {}", path.display()))?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn set_cur_block(&mut self, cur_block: u32) -> io::Result<()> {
|
|
self.cur_block = cur_block;
|
|
self.file
|
|
.as_ref()
|
|
.unwrap()
|
|
.seek(SeekFrom::Start(self.cur_block as u64 * SECTOR_SIZE as u64 + 0x200u64))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn set_phys_addr(&mut self, phys_addr: Fbo) -> Result<()> {
|
|
// If we're just changing the offset, nothing else needs to be done
|
|
if self.phys_addr.file == phys_addr.file && self.phys_addr.block == phys_addr.block {
|
|
self.phys_addr.offset = phys_addr.offset;
|
|
return Ok(());
|
|
}
|
|
self.phys_addr = phys_addr;
|
|
|
|
// Set logical zero block
|
|
if phys_addr.file == u32::MAX {
|
|
self.buf.fill(0u8);
|
|
return Ok(());
|
|
}
|
|
|
|
// Make necessary file and block current with system
|
|
if phys_addr.file != self.cur_file {
|
|
self.set_cur_file(phys_addr.file)?;
|
|
}
|
|
if phys_addr.block != self.cur_block {
|
|
self.set_cur_block(phys_addr.block)
|
|
.with_context(|| format!("Seeking to NFS block {}", phys_addr.block))?;
|
|
}
|
|
|
|
// Read block, handling 0x200 overlap case
|
|
if phys_addr.block == 7999 {
|
|
self.file
|
|
.as_ref()
|
|
.unwrap()
|
|
.read_exact(&mut self.buf[..SECTOR_SIZE - 0x200])
|
|
.context("Reading NFS block 7999 part 1")?;
|
|
self.set_cur_file(self.cur_file + 1)?;
|
|
self.file
|
|
.as_ref()
|
|
.unwrap()
|
|
.read_exact(&mut self.buf[SECTOR_SIZE - 0x200..])
|
|
.context("Reading NFS block 7999 part 2")?;
|
|
self.cur_block = 0;
|
|
} else {
|
|
self.file
|
|
.as_ref()
|
|
.unwrap()
|
|
.read_exact(&mut self.buf)
|
|
.with_context(|| format!("Reading NFS block {}", phys_addr.block))?;
|
|
self.cur_block += 1;
|
|
}
|
|
|
|
// Decrypt
|
|
#[rustfmt::skip]
|
|
let iv: [u8; 16] = [
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
(phys_addr.l_block & 0xFF) as u8,
|
|
((phys_addr.l_block >> 8) & 0xFF) as u8,
|
|
((phys_addr.l_block >> 16) & 0xFF) as u8,
|
|
((phys_addr.l_block >> 24) & 0xFF) as u8,
|
|
];
|
|
Aes128Cbc::new(self.crypto.as_ref().into(), &iv.into())
|
|
.decrypt_padded_mut::<NoPadding>(&mut self.buf)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_logical_addr(&mut self, addr: u64) -> Result<()> {
|
|
self.set_phys_addr(self.disc_io.header.as_ref().unwrap().logical_to_fbo(addr))
|
|
}
|
|
}
|
|
|
|
impl<'a> Read for NFSReadStream<'a> {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
let mut rem = buf.len();
|
|
let mut read: usize = 0;
|
|
while rem > 0 {
|
|
let mut read_size = rem;
|
|
let block_offset: usize =
|
|
if self.phys_addr.offset == u32::MAX { 0 } else { self.phys_addr.offset as usize };
|
|
if read_size + block_offset > SECTOR_SIZE {
|
|
read_size = SECTOR_SIZE - block_offset
|
|
}
|
|
buf[read..read + read_size]
|
|
.copy_from_slice(&self.buf[block_offset..block_offset + read_size]);
|
|
read += read_size;
|
|
rem -= read_size;
|
|
self.offset += read_size as u64;
|
|
self.set_logical_addr(self.offset).map_err(|e| match e {
|
|
Error::Io(s, e) => io::Error::new(e.kind(), s),
|
|
_ => io::Error::from(io::ErrorKind::Other),
|
|
})?;
|
|
}
|
|
Ok(read)
|
|
}
|
|
}
|
|
|
|
impl<'a> Seek for NFSReadStream<'a> {
|
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
self.offset = match pos {
|
|
SeekFrom::Start(v) => v,
|
|
SeekFrom::End(v) => (self.stable_stream_len()? as i64 + v) as u64,
|
|
SeekFrom::Current(v) => (self.offset as i64 + v) as u64,
|
|
};
|
|
self.set_logical_addr(self.offset).map_err(|v| match v {
|
|
Error::Io(_, v) => v,
|
|
_ => io::Error::from(io::ErrorKind::Other),
|
|
})?;
|
|
Ok(self.offset)
|
|
}
|
|
|
|
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.offset) }
|
|
}
|
|
|
|
impl<'a> ReadStream for NFSReadStream<'a> {
|
|
fn stable_stream_len(&mut self) -> io::Result<u64> { todo!() }
|
|
|
|
fn as_dyn(&mut self) -> &mut dyn ReadStream { self }
|
|
}
|
|
|
|
impl DiscIO for DiscIONFS {
|
|
fn begin_read_stream(&mut self, offset: u64) -> io::Result<Box<dyn ReadStream + '_>> {
|
|
Ok(Box::from(NFSReadStream {
|
|
disc_io: self,
|
|
file: None,
|
|
crypto: self.key,
|
|
phys_addr: Fbo::default(),
|
|
offset,
|
|
cur_file: u32::MAX,
|
|
cur_block: u32::MAX,
|
|
buf: [0; SECTOR_SIZE],
|
|
}))
|
|
}
|
|
|
|
fn has_wii_crypto(&self) -> bool { false }
|
|
}
|
|
|
|
impl DiscIONFS {
|
|
fn get_path<P>(&self, path: P) -> PathBuf
|
|
where P: AsRef<Path> {
|
|
let mut buf = self.directory.clone();
|
|
for component in path.as_ref().components() {
|
|
match component {
|
|
Component::ParentDir => {
|
|
buf.pop();
|
|
}
|
|
_ => buf.push(component),
|
|
}
|
|
}
|
|
buf
|
|
}
|
|
|
|
fn get_nfs(&self, num: u32) -> Result<PathBuf> {
|
|
let path = self.get_path(format!("hif_{:06}.nfs", num));
|
|
if path.exists() {
|
|
Ok(path)
|
|
} else {
|
|
Err(Error::DiscFormat(format!("Failed to locate {}", path.display())))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_files(&mut self) -> Result<()> {
|
|
{
|
|
// Load key file
|
|
let primary_key_path =
|
|
self.get_path(["..", "code", "htk.bin"].iter().collect::<PathBuf>());
|
|
let secondary_key_path = self.get_path("htk.bin");
|
|
let mut key_path = primary_key_path.canonicalize();
|
|
if key_path.is_err() {
|
|
key_path = secondary_key_path.canonicalize();
|
|
}
|
|
if key_path.is_err() {
|
|
return Err(Error::DiscFormat(format!(
|
|
"Failed to locate {} or {}",
|
|
primary_key_path.display(),
|
|
secondary_key_path.display()
|
|
)));
|
|
}
|
|
let resolved_path = key_path.unwrap();
|
|
File::open(resolved_path.as_path())
|
|
.map_err(|v| Error::Io(format!("Failed to open {}", resolved_path.display()), v))?
|
|
.read(&mut self.key)
|
|
.map_err(|v| Error::Io(format!("Failed to read {}", resolved_path.display()), v))?;
|
|
}
|
|
{
|
|
// Load header from first file
|
|
let path = self.get_nfs(0)?;
|
|
let mut file = BufReader::new(
|
|
File::open(&path).with_context(|| format!("Opening file {}", path.display()))?,
|
|
);
|
|
let header = NFSHeader::from_reader(&mut file)
|
|
.with_context(|| format!("Reading NFS header from file {}", path.display()))?;
|
|
// Ensure remaining files exist
|
|
for i in 1..header.calculate_num_files() {
|
|
self.get_nfs(i)?;
|
|
}
|
|
self.header = Option::from(header);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|