use std::{
io::{self, BufRead, Read, Seek},
path::Path,
sync::{Arc, Mutex},
};
use dyn_clone::DynClone;
use zerocopy::FromBytes;
use crate::{
Result,
common::{Compression, Format, PartitionInfo, PartitionKind},
disc,
disc::{
ApploaderHeader, BB2_OFFSET, BI2_SIZE, BOOT_SIZE, BootHeader, DebugHeader, DiscHeader,
DolHeader,
fst::{Fst, Node},
wii::{ContentMetadata, H3_TABLE_SIZE, REGION_SIZE, Ticket, TmdHeader},
},
io::block,
util::{WindowedReader, array_ref},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum PartitionEncryption {
#[default]
Original,
ForceEncrypted,
ForceDecrypted,
ForceDecryptedNoHashes,
}
#[derive(Default, Debug, Clone)]
pub struct DiscOptions {
pub partition_encryption: PartitionEncryption,
#[cfg(feature = "threading")]
pub preloader_threads: usize,
}
#[derive(Default, Debug, Clone)]
pub struct PartitionOptions {
pub validate_hashes: bool,
}
pub trait DiscStream: DynClone + Send {
fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> io::Result<()>;
fn stream_len(&mut self) -> io::Result<u64>;
}
dyn_clone::clone_trait_object!(DiscStream);
impl<T> DiscStream for T
where T: AsRef<[u8]> + Send + Clone
{
fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> io::Result<()> {
let data = self.as_ref();
let len = data.len() as u64;
let end = offset + buf.len() as u64;
if offset >= len || end > len {
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
}
buf.copy_from_slice(&data[offset as usize..end as usize]);
Ok(())
}
fn stream_len(&mut self) -> io::Result<u64> { Ok(self.as_ref().len() as u64) }
}
#[derive(Debug, Clone)]
pub(crate) struct CloneableStream<T>(pub T)
where T: Read + Seek + Clone + Send;
impl<T> CloneableStream<T>
where T: Read + Seek + Clone + Send
{
pub fn new(stream: T) -> Self { Self(stream) }
}
impl<T> DiscStream for CloneableStream<T>
where T: Read + Seek + Clone + Send
{
fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> io::Result<()> {
self.0.seek(io::SeekFrom::Start(offset))?;
self.0.read_exact(buf)
}
fn stream_len(&mut self) -> io::Result<u64> { self.0.seek(io::SeekFrom::End(0)) }
}
#[derive(Debug)]
pub(crate) struct NonCloneableStream<T>(pub Arc<Mutex<T>>)
where T: Read + Seek + Send;
impl<T> Clone for NonCloneableStream<T>
where T: Read + Seek + Send
{
fn clone(&self) -> Self { Self(self.0.clone()) }
}
impl<T> NonCloneableStream<T>
where T: Read + Seek + Send
{
pub fn new(stream: T) -> Self { Self(Arc::new(Mutex::new(stream))) }
fn lock(&self) -> io::Result<std::sync::MutexGuard<'_, T>> {
self.0.lock().map_err(|_| io::Error::other("NonCloneableStream mutex poisoned"))
}
}
impl<T> DiscStream for NonCloneableStream<T>
where T: Read + Seek + Send
{
fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> io::Result<()> {
let mut stream = self.lock()?;
stream.seek(io::SeekFrom::Start(offset))?;
stream.read_exact(buf)
}
fn stream_len(&mut self) -> io::Result<u64> {
let mut stream = self.lock()?;
stream.seek(io::SeekFrom::End(0))
}
}
#[derive(Clone)]
#[repr(transparent)]
pub struct DiscReader(disc::reader::DiscReader);
impl DiscReader {
pub fn new<P: AsRef<Path>>(path: P, options: &DiscOptions) -> Result<DiscReader> {
let io = block::open(path.as_ref())?;
let inner = disc::reader::DiscReader::new(io, options)?;
Ok(DiscReader(inner))
}
pub fn new_stream(stream: Box<dyn DiscStream>, options: &DiscOptions) -> Result<DiscReader> {
let io = block::new(stream)?;
let inner = disc::reader::DiscReader::new(io, options)?;
Ok(DiscReader(inner))
}
pub fn new_from_cloneable_read<R>(stream: R, options: &DiscOptions) -> Result<DiscReader>
where R: Read + Seek + Clone + Send + 'static {
Self::new_stream(Box::new(CloneableStream::new(stream)), options)
}
pub fn new_from_non_cloneable_read<R>(stream: R, options: &DiscOptions) -> Result<DiscReader>
where R: Read + Seek + Send + 'static {
Self::new_stream(Box::new(NonCloneableStream::new(stream)), options)
}
#[inline]
pub fn detect<R>(stream: &mut R) -> io::Result<Option<Format>>
where R: Read + ?Sized {
block::detect(stream)
}
#[inline]
pub fn header(&self) -> &DiscHeader { self.0.header() }
#[inline]
pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.0.region() }
#[inline]
pub fn meta(&self) -> DiscMeta { self.0.meta() }
#[inline]
pub fn disc_size(&self) -> u64 { self.0.disc_size() }
#[inline]
pub fn partitions(&self) -> &[PartitionInfo] { self.0.partitions() }
#[inline]
pub fn open_partition(
&self,
index: usize,
options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> {
self.0.open_partition(index, options)
}
#[inline]
pub fn open_partition_kind(
&self,
kind: PartitionKind,
options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> {
self.0.open_partition_kind(kind, options)
}
pub(crate) fn into_inner(self) -> disc::reader::DiscReader { self.0 }
}
impl BufRead for DiscReader {
#[inline]
fn fill_buf(&mut self) -> io::Result<&[u8]> { self.0.fill_buf() }
#[inline]
fn consume(&mut self, amt: usize) { self.0.consume(amt) }
}
impl Read for DiscReader {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.0.read(buf) }
}
impl Seek for DiscReader {
#[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { self.0.seek(pos) }
}
#[derive(Debug, Clone, Default)]
pub struct DiscMeta {
pub format: Format,
pub compression: Compression,
pub block_size: Option<u32>,
pub decrypted: bool,
pub needs_hash_recovery: bool,
pub lossless: bool,
pub disc_size: Option<u64>,
pub crc32: Option<u32>,
pub md5: Option<[u8; 16]>,
pub sha1: Option<[u8; 20]>,
pub xxh64: Option<u64>,
}
pub trait PartitionReader: DynClone + BufRead + Seek + Send {
fn is_wii(&self) -> bool;
fn meta(&mut self) -> Result<PartitionMeta>;
}
pub type FileReader<'a> = WindowedReader<&'a mut dyn PartitionReader>;
pub type OwnedFileReader = WindowedReader<Box<dyn PartitionReader>>;
impl dyn PartitionReader + '_ {
pub fn open_file(&mut self, node: Node) -> io::Result<FileReader<'_>> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
let is_wii = self.is_wii();
FileReader::new(self, node.offset(is_wii), node.length() as u64)
}
}
impl dyn PartitionReader {
pub fn into_open_file(self: Box<Self>, node: Node) -> io::Result<OwnedFileReader> {
if !node.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Node is not a file".to_string(),
));
}
let is_wii = self.is_wii();
OwnedFileReader::new(self, node.offset(is_wii), node.length() as u64)
}
}
dyn_clone::clone_trait_object!(PartitionReader);
#[derive(Clone, Debug)]
pub struct PartitionMeta {
pub raw_boot: Arc<[u8; BOOT_SIZE]>,
pub raw_bi2: Arc<[u8; BI2_SIZE]>,
pub raw_apploader: Arc<[u8]>,
pub raw_dol: Arc<[u8]>,
pub raw_fst: Arc<[u8]>,
pub raw_ticket: Option<Arc<[u8]>>,
pub raw_tmd: Option<Arc<[u8]>>,
pub raw_cert_chain: Option<Arc<[u8]>>,
pub raw_h3_table: Option<Arc<[u8; H3_TABLE_SIZE]>>,
}
impl PartitionMeta {
#[inline]
pub fn disc_header(&self) -> &DiscHeader {
DiscHeader::ref_from_bytes(array_ref![self.raw_boot, 0, size_of::<DiscHeader>()])
.expect("Invalid disc header alignment")
}
#[inline]
pub fn debug_header(&self) -> &DebugHeader {
DebugHeader::ref_from_bytes(array_ref![
self.raw_boot,
size_of::<DiscHeader>(),
size_of::<DebugHeader>()
])
.expect("Invalid debug header alignment")
}
#[inline]
pub fn boot_header(&self) -> &BootHeader {
BootHeader::ref_from_bytes(array_ref![self.raw_boot, BB2_OFFSET, size_of::<BootHeader>()])
.expect("Invalid boot header alignment")
}
#[inline]
pub fn apploader_header(&self) -> &ApploaderHeader {
ApploaderHeader::ref_from_prefix(&self.raw_apploader)
.expect("Invalid apploader alignment")
.0
}
#[inline]
pub fn fst(&self) -> Result<Fst<'_>, &'static str> { Fst::new(&self.raw_fst) }
#[inline]
pub fn dol_header(&self) -> &DolHeader {
DolHeader::ref_from_prefix(&self.raw_dol).expect("Invalid DOL alignment").0
}
#[inline]
pub fn ticket(&self) -> Option<&Ticket> {
let raw_ticket = self.raw_ticket.as_deref()?;
Some(Ticket::ref_from_bytes(raw_ticket).expect("Invalid ticket alignment"))
}
#[inline]
pub fn tmd_header(&self) -> Option<&TmdHeader> {
let raw_tmd = self.raw_tmd.as_deref()?;
Some(TmdHeader::ref_from_prefix(raw_tmd).expect("Invalid TMD alignment").0)
}
#[inline]
pub fn content_metadata(&self) -> Option<&[ContentMetadata]> {
let raw_cmd = &self.raw_tmd.as_deref()?[size_of::<TmdHeader>()..];
Some(<[ContentMetadata]>::ref_from_bytes(raw_cmd).expect("Invalid CMD alignment"))
}
}