mirror of
https://github.com/encounter/nod-rs.git
synced 2025-07-31 01:15:34 +00:00
365 lines
12 KiB
Rust
365 lines
12 KiB
Rust
use std::{
|
|
io,
|
|
io::{Seek, SeekFrom},
|
|
mem::size_of,
|
|
sync::Arc,
|
|
};
|
|
|
|
use adler::adler32_slice;
|
|
use bytes::{BufMut, Bytes, BytesMut};
|
|
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout, little_endian::*};
|
|
|
|
use crate::{
|
|
Error, Result, ResultContext,
|
|
common::{Compression, Format, MagicBytes},
|
|
disc::{
|
|
SECTOR_SIZE,
|
|
reader::DiscReader,
|
|
writer::{BlockProcessor, BlockResult, DiscWriter, par_process, read_block},
|
|
},
|
|
io::block::{Block, BlockKind, BlockReader, GCZ_MAGIC},
|
|
read::{DiscMeta, DiscStream},
|
|
util::{
|
|
compress::{Compressor, DecompressionKind, Decompressor},
|
|
digest::DigestManager,
|
|
read::{read_arc_slice_at, read_at},
|
|
static_assert,
|
|
},
|
|
write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
|
|
};
|
|
|
|
/// GCZ header (little endian)
|
|
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
|
#[repr(C, align(4))]
|
|
struct GCZHeader {
|
|
magic: MagicBytes,
|
|
disc_type: U32,
|
|
compressed_size: U64,
|
|
disc_size: U64,
|
|
block_size: U32,
|
|
block_count: U32,
|
|
}
|
|
|
|
static_assert!(size_of::<GCZHeader>() == 32);
|
|
|
|
pub struct BlockReaderGCZ {
|
|
inner: Box<dyn DiscStream>,
|
|
header: GCZHeader,
|
|
block_map: Arc<[U64]>,
|
|
block_hashes: Arc<[U32]>,
|
|
block_buf: Box<[u8]>,
|
|
data_offset: u64,
|
|
decompressor: Decompressor,
|
|
}
|
|
|
|
impl Clone for BlockReaderGCZ {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
inner: self.inner.clone(),
|
|
header: self.header.clone(),
|
|
block_map: self.block_map.clone(),
|
|
block_hashes: self.block_hashes.clone(),
|
|
block_buf: <[u8]>::new_box_zeroed_with_elems(self.block_buf.len()).unwrap(),
|
|
data_offset: self.data_offset,
|
|
decompressor: self.decompressor.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BlockReaderGCZ {
|
|
pub fn new(mut inner: Box<dyn DiscStream>) -> Result<Box<Self>> {
|
|
// Read header
|
|
let header: GCZHeader = read_at(inner.as_mut(), 0).context("Reading GCZ header")?;
|
|
if header.magic != GCZ_MAGIC {
|
|
return Err(Error::DiscFormat("Invalid GCZ magic".to_string()));
|
|
}
|
|
|
|
// Read block map and hashes
|
|
let block_count = header.block_count.get();
|
|
let block_map =
|
|
read_arc_slice_at(inner.as_mut(), block_count as usize, size_of::<GCZHeader>() as u64)
|
|
.context("Reading GCZ block map")?;
|
|
let block_hashes = read_arc_slice_at(
|
|
inner.as_mut(),
|
|
block_count as usize,
|
|
size_of::<GCZHeader>() as u64 + block_count as u64 * 8,
|
|
)
|
|
.context("Reading GCZ block hashes")?;
|
|
|
|
// header + block_count * (u64 + u32)
|
|
let data_offset = size_of::<GCZHeader>() as u64 + block_count as u64 * 12;
|
|
let block_buf = <[u8]>::new_box_zeroed_with_elems(header.block_size.get() as usize)?;
|
|
let decompressor = Decompressor::new(DecompressionKind::Deflate);
|
|
Ok(Box::new(Self {
|
|
inner,
|
|
header,
|
|
block_map,
|
|
block_hashes,
|
|
block_buf,
|
|
data_offset,
|
|
decompressor,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl BlockReader for BlockReaderGCZ {
|
|
fn read_block(&mut self, out: &mut [u8], sector: u32) -> io::Result<Block> {
|
|
let block_size = self.header.block_size.get();
|
|
let block_idx = ((sector as u64 * SECTOR_SIZE as u64) / block_size as u64) as u32;
|
|
if block_idx >= self.header.block_count.get() {
|
|
// Out of bounds
|
|
return Ok(Block::new(block_idx, block_size, BlockKind::None));
|
|
}
|
|
|
|
// Find block offset and size
|
|
let mut file_offset = self.block_map[block_idx as usize].get();
|
|
let mut compressed = true;
|
|
if file_offset & (1 << 63) != 0 {
|
|
file_offset &= !(1 << 63);
|
|
compressed = false;
|
|
}
|
|
let compressed_size = ((self
|
|
.block_map
|
|
.get(block_idx as usize + 1)
|
|
.unwrap_or(&self.header.compressed_size)
|
|
.get()
|
|
& !(1 << 63))
|
|
- file_offset) as usize;
|
|
if compressed_size > block_size as usize {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!(
|
|
"Compressed block size exceeds block size: {} > {}",
|
|
compressed_size, block_size
|
|
),
|
|
));
|
|
} else if !compressed && compressed_size != block_size as usize {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!(
|
|
"Uncompressed block size does not match block size: {} != {}",
|
|
compressed_size, block_size
|
|
),
|
|
));
|
|
}
|
|
|
|
// Read block
|
|
self.inner.read_exact_at(
|
|
&mut self.block_buf[..compressed_size],
|
|
self.data_offset + file_offset,
|
|
)?;
|
|
|
|
// Verify block checksum
|
|
let checksum = adler32_slice(&self.block_buf[..compressed_size]);
|
|
let expected_checksum = self.block_hashes[block_idx as usize].get();
|
|
if checksum != expected_checksum {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!(
|
|
"Block {} checksum mismatch: {:#010x} != {:#010x}",
|
|
block_idx, checksum, expected_checksum
|
|
),
|
|
));
|
|
}
|
|
|
|
if compressed {
|
|
// Decompress block
|
|
let out_len = self.decompressor.decompress(&self.block_buf[..compressed_size], out)?;
|
|
if out_len != block_size as usize {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!(
|
|
"Block {} decompression failed: in: {}, out: {}",
|
|
block_idx, compressed_size, out_len
|
|
),
|
|
));
|
|
}
|
|
} else {
|
|
// Copy uncompressed block
|
|
out.copy_from_slice(self.block_buf.as_ref());
|
|
}
|
|
|
|
Ok(Block::new(block_idx, block_size, BlockKind::Raw))
|
|
}
|
|
|
|
fn block_size(&self) -> u32 { self.header.block_size.get() }
|
|
|
|
fn meta(&self) -> DiscMeta {
|
|
DiscMeta {
|
|
format: Format::Gcz,
|
|
compression: Compression::Deflate(0),
|
|
block_size: Some(self.header.block_size.get()),
|
|
lossless: true,
|
|
disc_size: Some(self.header.disc_size.get()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct BlockProcessorGCZ {
|
|
inner: DiscReader,
|
|
header: GCZHeader,
|
|
compressor: Compressor,
|
|
}
|
|
|
|
impl Clone for BlockProcessorGCZ {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
inner: self.inner.clone(),
|
|
header: self.header.clone(),
|
|
compressor: self.compressor.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct BlockMetaGCZ {
|
|
is_compressed: bool,
|
|
block_hash: u32,
|
|
}
|
|
|
|
impl BlockProcessor for BlockProcessorGCZ {
|
|
type BlockMeta = BlockMetaGCZ;
|
|
|
|
fn process_block(&mut self, block_idx: u32) -> io::Result<BlockResult<Self::BlockMeta>> {
|
|
let block_size = self.header.block_size.get();
|
|
self.inner.seek(SeekFrom::Start(block_idx as u64 * block_size as u64))?;
|
|
let (mut block_data, disc_data) = read_block(&mut self.inner, block_size as usize)?;
|
|
|
|
// Try to compress block
|
|
let is_compressed = if self.compressor.compress(&block_data)? {
|
|
println!("Compressed block {} to {}", block_idx, self.compressor.buffer.len());
|
|
block_data = Bytes::copy_from_slice(self.compressor.buffer.as_slice());
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
|
|
let block_hash = adler32_slice(block_data.as_ref());
|
|
Ok(BlockResult {
|
|
block_idx,
|
|
disc_data,
|
|
block_data,
|
|
meta: BlockMetaGCZ { is_compressed, block_hash },
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DiscWriterGCZ {
|
|
inner: DiscReader,
|
|
header: GCZHeader,
|
|
compression: Compression,
|
|
}
|
|
|
|
pub const DEFAULT_BLOCK_SIZE: u32 = 0x8000; // 32 KiB
|
|
|
|
// Level 0 will be converted to the default level in [`Compression::validate_level`]
|
|
pub const DEFAULT_COMPRESSION: Compression = Compression::Deflate(0);
|
|
|
|
impl DiscWriterGCZ {
|
|
pub fn new(inner: DiscReader, options: &FormatOptions) -> Result<Box<dyn DiscWriter>> {
|
|
if options.format != Format::Gcz {
|
|
return Err(Error::DiscFormat("Invalid format for GCZ writer".to_string()));
|
|
}
|
|
if !matches!(options.compression, Compression::Deflate(_)) {
|
|
return Err(Error::DiscFormat(format!(
|
|
"Unsupported compression for GCZ: {:?}",
|
|
options.compression
|
|
)));
|
|
}
|
|
|
|
let block_size = options.block_size;
|
|
if block_size < SECTOR_SIZE as u32 || block_size % SECTOR_SIZE as u32 != 0 {
|
|
return Err(Error::DiscFormat("Invalid block size for GCZ".to_string()));
|
|
}
|
|
|
|
let disc_header = inner.header();
|
|
let disc_size = inner.disc_size();
|
|
let block_count = disc_size.div_ceil(block_size as u64) as u32;
|
|
|
|
// Generate header
|
|
let header = GCZHeader {
|
|
magic: GCZ_MAGIC,
|
|
disc_type: if disc_header.is_wii() { 1 } else { 0 }.into(),
|
|
compressed_size: 0.into(), // Written when finalized
|
|
disc_size: disc_size.into(),
|
|
block_size: block_size.into(),
|
|
block_count: block_count.into(),
|
|
};
|
|
|
|
Ok(Box::new(Self { inner, header, compression: options.compression }))
|
|
}
|
|
}
|
|
|
|
impl DiscWriter for DiscWriterGCZ {
|
|
fn process(
|
|
&self,
|
|
data_callback: &mut DataCallback,
|
|
options: &ProcessOptions,
|
|
) -> Result<DiscFinalization> {
|
|
let disc_size = self.header.disc_size.get();
|
|
let block_size = self.header.block_size.get();
|
|
let block_count = self.header.block_count.get();
|
|
|
|
// Create hashers
|
|
let digest = DigestManager::new(options);
|
|
|
|
// Generate block map and hashes
|
|
let mut block_map = <[U64]>::new_box_zeroed_with_elems(block_count as usize)?;
|
|
let mut block_hashes = <[U32]>::new_box_zeroed_with_elems(block_count as usize)?;
|
|
|
|
let header_data_size = size_of::<GCZHeader>()
|
|
+ size_of_val(block_map.as_ref())
|
|
+ size_of_val(block_hashes.as_ref());
|
|
let mut header_data = BytesMut::with_capacity(header_data_size);
|
|
header_data.put_slice(self.header.as_bytes());
|
|
header_data.resize(header_data_size, 0);
|
|
data_callback(header_data.freeze(), 0, disc_size).context("Failed to write GCZ header")?;
|
|
|
|
let mut input_position = 0;
|
|
let mut data_position = 0;
|
|
par_process(
|
|
BlockProcessorGCZ {
|
|
inner: self.inner.clone(),
|
|
header: self.header.clone(),
|
|
compressor: Compressor::new(self.compression, block_size as usize),
|
|
},
|
|
block_count,
|
|
options.processor_threads,
|
|
|block| {
|
|
// Update hashers
|
|
input_position += block.disc_data.len() as u64;
|
|
digest.send(block.disc_data);
|
|
|
|
// Update block map and hash
|
|
let uncompressed_bit = (!block.meta.is_compressed as u64) << 63;
|
|
block_map[block.block_idx as usize] = (data_position | uncompressed_bit).into();
|
|
block_hashes[block.block_idx as usize] = block.meta.block_hash.into();
|
|
|
|
// Write block data
|
|
data_position += block.block_data.len() as u64;
|
|
data_callback(block.block_data, input_position, disc_size)
|
|
.with_context(|| format!("Failed to write block {}", block.block_idx))?;
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
// Write updated header, block map and hashes
|
|
let mut header = self.header.clone();
|
|
header.compressed_size = data_position.into();
|
|
let mut header_data = BytesMut::with_capacity(header_data_size);
|
|
header_data.extend_from_slice(header.as_bytes());
|
|
header_data.extend_from_slice(block_map.as_bytes());
|
|
header_data.extend_from_slice(block_hashes.as_bytes());
|
|
|
|
let mut finalization =
|
|
DiscFinalization { header: header_data.freeze(), ..Default::default() };
|
|
finalization.apply_digests(&digest.finish());
|
|
Ok(finalization)
|
|
}
|
|
|
|
fn progress_bound(&self) -> u64 { self.header.disc_size.get() }
|
|
|
|
fn weight(&self) -> DiscWriterWeight { DiscWriterWeight::Heavy }
|
|
}
|