diff --git a/README.md b/README.md index 7db8b72..0e96938 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Currently supported file formats: - CISO (+ NKit 2 lossless) - NFS (Wii U VC) - GCZ +- TGC ## CLI tool diff --git a/nod/src/fst.rs b/nod/src/fst.rs index b99a39e..421d6b6 100644 --- a/nod/src/fst.rs +++ b/nod/src/fst.rs @@ -25,7 +25,7 @@ pub struct Node { kind: u8, // u24 big-endian name_offset: [u8; 3], - offset: U32, + pub(crate) offset: U32, length: U32, } diff --git a/nod/src/io/block.rs b/nod/src/io/block.rs index 7201f1c..98bdbba 100644 --- a/nod/src/io/block.rs +++ b/nod/src/io/block.rs @@ -114,6 +114,7 @@ pub fn open(filename: &Path) -> Result> { crate::io::wia::WIA_MAGIC | crate::io::wia::RVZ_MAGIC => { crate::io::wia::DiscIOWIA::new(path)? } + crate::io::tgc::TGC_MAGIC => crate::io::tgc::DiscIOTGC::new(path)?, _ => crate::io::iso::DiscIOISO::new(path)?, }; if io.block_size_internal() < SECTOR_SIZE as u32 diff --git a/nod/src/io/mod.rs b/nod/src/io/mod.rs index 8f3d276..5c1f7aa 100644 --- a/nod/src/io/mod.rs +++ b/nod/src/io/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod iso; pub(crate) mod nfs; pub(crate) mod nkit; pub(crate) mod split; +pub(crate) mod tgc; pub(crate) mod wbfs; pub(crate) mod wia; @@ -40,6 +41,8 @@ pub enum Format { Wbfs, /// WIA Wia, + /// TGC + Tgc, } impl fmt::Display for Format { @@ -52,6 +55,7 @@ impl fmt::Display for Format { Format::Rvz => write!(f, "RVZ"), Format::Wbfs => write!(f, "WBFS"), Format::Wia => write!(f, "WIA"), + Format::Tgc => write!(f, "TGC"), } } } diff --git a/nod/src/io/tgc.rs b/nod/src/io/tgc.rs new file mode 100644 index 0000000..8167833 --- /dev/null +++ b/nod/src/io/tgc.rs @@ -0,0 +1,141 @@ +use std::{ + io, + io::{Read, Seek, SeekFrom}, + path::Path, +}; + +use zerocopy::{big_endian::U32, AsBytes, FromBytes, FromZeroes}; + +use crate::{ + disc::SECTOR_SIZE, + io::{ + block::{Block, BlockIO, PartitionInfo}, + split::SplitFileReader, + Format, MagicBytes, + }, + util::read::{read_box_slice, read_from}, + DiscHeader, DiscMeta, Error, Node, PartitionHeader, Result, ResultContext, +}; + +pub const TGC_MAGIC: MagicBytes = [0xae, 0x0f, 0x38, 0xa2]; + +/// TGC header (big endian) +#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)] +#[repr(C, align(4))] +struct TGCHeader { + magic: MagicBytes, + unk1: U32, + header_size: U32, + disc_area_header_size: U32, + fst_offset: U32, + fst_size: U32, + fst_max_size: U32, + dol_offset: U32, + dol_size: U32, + file_area: U32, + file_area_size: U32, + banner_offset: U32, + banner_size: U32, + file_offset_base: U32, +} + +#[derive(Clone)] +pub struct DiscIOTGC { + inner: SplitFileReader, + header: TGCHeader, + fst: Box<[u8]>, +} + +impl DiscIOTGC { + pub fn new(filename: &Path) -> Result> { + let mut inner = SplitFileReader::new(filename)?; + + // Read header + let header: TGCHeader = read_from(&mut inner).context("Reading TGC header")?; + if header.magic != TGC_MAGIC { + return Err(Error::DiscFormat("Invalid TGC magic".to_string())); + } + + // Read FST and adjust offsets + inner + .seek(SeekFrom::Start(header.fst_offset.get() as u64)) + .context("Seeking to TGC FST")?; + let mut fst = read_box_slice(&mut inner, header.fst_size.get() as usize) + .context("Reading TGC FST")?; + let root_node = Node::ref_from_prefix(&fst) + .ok_or_else(|| Error::DiscFormat("Invalid TGC FST".to_string()))?; + let node_count = root_node.length() as usize; + let (nodes, _) = Node::mut_slice_from_prefix(&mut fst, node_count) + .ok_or_else(|| Error::DiscFormat("Invalid TGC FST".to_string()))?; + for node in nodes { + if node.is_file() { + node.offset = + node.offset - header.file_offset_base + header.file_area - header.header_size; + } + } + + Ok(Box::new(Self { inner, header, fst })) + } +} + +impl BlockIO for DiscIOTGC { + fn read_block_internal( + &mut self, + out: &mut [u8], + block: u32, + _partition: Option<&PartitionInfo>, + ) -> io::Result { + let offset = self.header.header_size.get() as u64 + block as u64 * SECTOR_SIZE as u64; + let total_size = self.inner.len(); + if offset >= total_size { + // End of file + return Ok(Block::Zero); + } + + self.inner.seek(SeekFrom::Start(offset))?; + if offset + SECTOR_SIZE as u64 > total_size { + // If the last block is not a full sector, fill the rest with zeroes + let read = (total_size - offset) as usize; + self.inner.read_exact(&mut out[..read])?; + out[read..].fill(0); + } else { + self.inner.read_exact(out)?; + } + + // Adjust internal GCM header + if block == 0 { + let partition_header = PartitionHeader::mut_from( + &mut out[size_of::() + ..size_of::() + size_of::()], + ) + .unwrap(); + partition_header.dol_offset = self.header.dol_offset - self.header.header_size; + partition_header.fst_offset = self.header.fst_offset - self.header.header_size; + } + + // Copy modified FST to output + if offset + out.len() as u64 > self.header.fst_offset.get() as u64 + && offset < self.header.fst_offset.get() as u64 + self.header.fst_size.get() as u64 + { + let out_offset = (self.header.fst_offset.get() as u64).saturating_sub(offset) as usize; + let fst_offset = offset.saturating_sub(self.header.fst_offset.get() as u64) as usize; + let copy_len = + (out.len() - out_offset).min(self.header.fst_size.get() as usize - fst_offset); + out[out_offset..out_offset + copy_len] + .copy_from_slice(&self.fst[fst_offset..fst_offset + copy_len]); + } + + Ok(Block::Raw) + } + + fn block_size_internal(&self) -> u32 { SECTOR_SIZE as u32 } + + fn meta(&self) -> DiscMeta { + DiscMeta { + format: Format::Tgc, + lossless: true, + disc_size: Some(self.inner.len() - self.header.header_size.get() as u64), + ..Default::default() + } + } +}