mirror of
https://github.com/encounter/nod-rs.git
synced 2025-07-07 13:45:51 +00:00
825 lines
29 KiB
Rust
825 lines
29 KiB
Rust
#![allow(missing_docs)] // TODO
|
|
use std::{
|
|
io,
|
|
io::{Read, Seek, Write},
|
|
sync::Arc,
|
|
};
|
|
|
|
use tracing::debug;
|
|
use zerocopy::{FromZeros, IntoBytes};
|
|
|
|
use crate::{
|
|
disc::{
|
|
fst::{Fst, FstBuilder},
|
|
BootHeader, DiscHeader, BI2_SIZE, BOOT_SIZE, GCN_MAGIC, MINI_DVD_SIZE, SECTOR_SIZE,
|
|
WII_MAGIC,
|
|
},
|
|
read::DiscStream,
|
|
util::{array_ref, array_ref_mut, lfg::LaggedFibonacci, Align},
|
|
Error, Result, ResultContext,
|
|
};
|
|
|
|
pub trait FileCallback: Clone + Send + Sync {
|
|
fn read_file(&mut self, out: &mut [u8], name: &str, offset: u64) -> io::Result<()>;
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct FileInfo {
|
|
pub name: String,
|
|
pub size: u64,
|
|
pub offset: Option<u64>,
|
|
pub alignment: Option<u32>,
|
|
}
|
|
|
|
pub struct GCPartitionBuilder {
|
|
disc_header: Box<DiscHeader>,
|
|
boot_header: Box<BootHeader>,
|
|
user_files: Vec<FileInfo>,
|
|
overrides: PartitionOverrides,
|
|
junk_files: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum WriteKind {
|
|
File(String),
|
|
Static(Arc<[u8]>, &'static str),
|
|
Junk,
|
|
}
|
|
|
|
impl WriteKind {
|
|
fn name(&self) -> &str {
|
|
match self {
|
|
WriteKind::File(name) => name,
|
|
WriteKind::Static(_, name) => name,
|
|
WriteKind::Junk => "[junk data]",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WriteInfo {
|
|
pub kind: WriteKind,
|
|
pub size: u64,
|
|
pub offset: u64,
|
|
}
|
|
|
|
pub struct GCPartitionWriter {
|
|
write_info: Vec<WriteInfo>,
|
|
disc_size: u64,
|
|
disc_id: [u8; 4],
|
|
disc_num: u8,
|
|
}
|
|
|
|
const BI2_OFFSET: u64 = BOOT_SIZE as u64;
|
|
const APPLOADER_OFFSET: u64 = BI2_OFFSET + BI2_SIZE as u64;
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct PartitionOverrides {
|
|
pub game_id: Option<[u8; 6]>,
|
|
pub game_title: Option<String>,
|
|
pub disc_num: Option<u8>,
|
|
pub disc_version: Option<u8>,
|
|
pub audio_streaming: Option<bool>,
|
|
pub audio_stream_buf_size: Option<u8>,
|
|
pub junk_id: Option<[u8; 4]>,
|
|
pub region: Option<u8>,
|
|
}
|
|
|
|
impl GCPartitionBuilder {
|
|
pub fn new(is_wii: bool, overrides: PartitionOverrides) -> Self {
|
|
let mut disc_header = DiscHeader::new_box_zeroed().unwrap();
|
|
if is_wii {
|
|
disc_header.gcn_magic = [0u8; 4];
|
|
disc_header.wii_magic = WII_MAGIC;
|
|
} else {
|
|
disc_header.gcn_magic = GCN_MAGIC;
|
|
disc_header.wii_magic = [0u8; 4];
|
|
}
|
|
Self {
|
|
disc_header,
|
|
boot_header: BootHeader::new_box_zeroed().unwrap(),
|
|
user_files: Vec::new(),
|
|
overrides,
|
|
junk_files: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_disc_header(&mut self, disc_header: Box<DiscHeader>) {
|
|
self.disc_header = disc_header;
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_boot_header(&mut self, boot_header: Box<BootHeader>) {
|
|
self.boot_header = boot_header;
|
|
}
|
|
|
|
pub fn add_file(&mut self, info: FileInfo) -> Result<()> {
|
|
if let (Some(offset), Some(alignment)) = (info.offset, info.alignment) {
|
|
if offset % alignment as u64 != 0 {
|
|
return Err(Error::Other(format!(
|
|
"File {} offset {:#X} is not aligned to {}",
|
|
info.name, offset, alignment
|
|
)));
|
|
}
|
|
}
|
|
self.user_files.push(info);
|
|
Ok(())
|
|
}
|
|
|
|
/// A junk file exists in the FST, but is excluded from the disc layout, so junk data will be
|
|
/// written in its place.
|
|
pub fn add_junk_file(&mut self, name: String) { self.junk_files.push(name); }
|
|
|
|
pub fn build(
|
|
&self,
|
|
sys_file_callback: impl FnMut(&mut dyn Write, &str) -> io::Result<()>,
|
|
) -> Result<GCPartitionWriter> {
|
|
let mut layout = GCPartitionLayout::new(self);
|
|
layout.locate_sys_files(sys_file_callback)?;
|
|
layout.apply_overrides(&self.overrides)?;
|
|
let write_info = layout.layout_files()?;
|
|
let disc_size =
|
|
layout.boot_header.user_offset.get() as u64 + layout.boot_header.user_size.get() as u64;
|
|
let junk_id = layout.junk_id();
|
|
Ok(GCPartitionWriter::new(write_info, disc_size, junk_id, self.disc_header.disc_num))
|
|
}
|
|
}
|
|
|
|
struct GCPartitionLayout {
|
|
disc_header: Box<DiscHeader>,
|
|
boot_header: Box<BootHeader>,
|
|
user_files: Vec<FileInfo>,
|
|
apploader_file: Option<FileInfo>,
|
|
dol_file: Option<FileInfo>,
|
|
raw_fst: Option<Box<[u8]>>,
|
|
raw_bi2: Option<Box<[u8]>>,
|
|
junk_id: Option<[u8; 4]>,
|
|
junk_files: Vec<String>,
|
|
}
|
|
|
|
impl GCPartitionLayout {
|
|
fn new(builder: &GCPartitionBuilder) -> Self {
|
|
GCPartitionLayout {
|
|
disc_header: builder.disc_header.clone(),
|
|
boot_header: builder.boot_header.clone(),
|
|
user_files: builder.user_files.clone(),
|
|
apploader_file: None,
|
|
dol_file: None,
|
|
raw_fst: None,
|
|
raw_bi2: None,
|
|
junk_id: builder.overrides.junk_id,
|
|
junk_files: builder.junk_files.clone(),
|
|
}
|
|
}
|
|
|
|
fn locate_sys_files(
|
|
&mut self,
|
|
mut file_callback: impl FnMut(&mut dyn Write, &str) -> io::Result<()>,
|
|
) -> Result<()> {
|
|
let mut handled = vec![false; self.user_files.len()];
|
|
|
|
// Locate fixed offset system files
|
|
for (info, handled) in self.user_files.iter().zip(handled.iter_mut()) {
|
|
if info.offset == Some(0) || info.name == "sys/boot.bin" {
|
|
let mut data = Vec::with_capacity(BOOT_SIZE);
|
|
file_callback(&mut data, &info.name)
|
|
.with_context(|| format!("Failed to read file {}", info.name))?;
|
|
if data.len() != BOOT_SIZE {
|
|
return Err(Error::Other(format!(
|
|
"Boot file {} is {} bytes, expected {}",
|
|
info.name,
|
|
data.len(),
|
|
BOOT_SIZE
|
|
)));
|
|
}
|
|
self.disc_header.as_mut_bytes().copy_from_slice(&data[..size_of::<DiscHeader>()]);
|
|
self.boot_header.as_mut_bytes().copy_from_slice(&data[size_of::<DiscHeader>()..]);
|
|
*handled = true;
|
|
continue;
|
|
}
|
|
|
|
if info.offset == Some(BI2_OFFSET) || info.name == "sys/bi2.bin" {
|
|
let mut data = Vec::with_capacity(BI2_SIZE);
|
|
file_callback(&mut data, &info.name)
|
|
.with_context(|| format!("Failed to read file {}", info.name))?;
|
|
if data.len() != BI2_SIZE {
|
|
return Err(Error::Other(format!(
|
|
"BI2 file {} is {} bytes, expected {}",
|
|
info.name,
|
|
data.len(),
|
|
BI2_SIZE
|
|
)));
|
|
}
|
|
self.raw_bi2 = Some(data.into_boxed_slice());
|
|
*handled = true;
|
|
continue;
|
|
}
|
|
|
|
if info.offset == Some(APPLOADER_OFFSET) || info.name == "sys/apploader.img" {
|
|
self.apploader_file = Some(info.clone());
|
|
*handled = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Locate other system files
|
|
let is_wii = self.disc_header.is_wii();
|
|
for (info, handled) in self.user_files.iter().zip(handled.iter_mut()) {
|
|
let dol_offset = self.boot_header.dol_offset(is_wii);
|
|
if (dol_offset != 0 && info.offset == Some(dol_offset)) || info.name == "sys/main.dol" {
|
|
let mut info = info.clone();
|
|
if info.alignment.is_none() {
|
|
info.alignment = Some(128);
|
|
}
|
|
self.dol_file = Some(info);
|
|
*handled = true; // TODO DOL in user data
|
|
continue;
|
|
}
|
|
|
|
let fst_offset = self.boot_header.fst_offset(is_wii);
|
|
if (fst_offset != 0 && info.offset == Some(fst_offset)) || info.name == "sys/fst.bin" {
|
|
let mut data = Vec::with_capacity(info.size as usize);
|
|
file_callback(&mut data, &info.name)
|
|
.with_context(|| format!("Failed to read file {}", info.name))?;
|
|
if data.len() != info.size as usize {
|
|
return Err(Error::Other(format!(
|
|
"FST file {} is {} bytes, expected {}",
|
|
info.name,
|
|
data.len(),
|
|
info.size
|
|
)));
|
|
}
|
|
self.raw_fst = Some(data.into_boxed_slice());
|
|
*handled = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Remove handled files
|
|
let mut iter = handled.iter();
|
|
self.user_files.retain(|_| !iter.next().unwrap());
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_overrides(&mut self, overrides: &PartitionOverrides) -> Result<()> {
|
|
if let Some(game_id) = overrides.game_id {
|
|
self.disc_header.game_id.copy_from_slice(&game_id);
|
|
}
|
|
if let Some(game_title) = overrides.game_title.as_ref() {
|
|
let max_size = self.disc_header.game_title.len() - 1; // nul terminator
|
|
if game_title.len() > max_size {
|
|
return Err(Error::Other(format!(
|
|
"Game title \"{}\" is too long ({} > {})",
|
|
game_title,
|
|
game_title.len(),
|
|
max_size
|
|
)));
|
|
}
|
|
let len = game_title.len().min(max_size);
|
|
self.disc_header.game_title[..len].copy_from_slice(&game_title.as_bytes()[..len]);
|
|
}
|
|
if let Some(disc_num) = overrides.disc_num {
|
|
self.disc_header.disc_num = disc_num;
|
|
}
|
|
if let Some(disc_version) = overrides.disc_version {
|
|
self.disc_header.disc_version = disc_version;
|
|
}
|
|
if let Some(audio_streaming) = overrides.audio_streaming {
|
|
self.disc_header.audio_streaming = audio_streaming as u8;
|
|
}
|
|
if let Some(audio_stream_buf_size) = overrides.audio_stream_buf_size {
|
|
self.disc_header.audio_stream_buf_size = audio_stream_buf_size;
|
|
}
|
|
let set_bi2 = self.raw_bi2.is_none() || overrides.region.is_some();
|
|
let raw_bi2 = self.raw_bi2.get_or_insert_with(|| {
|
|
<[u8]>::new_box_zeroed_with_elems(BI2_SIZE).expect("Failed to allocate BI2")
|
|
});
|
|
if set_bi2 {
|
|
let region = overrides.region.unwrap_or(0xFF) as u32;
|
|
*array_ref_mut![raw_bi2, 0x18, 4] = region.to_be_bytes();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn can_use_orig_fst(&self) -> bool {
|
|
if let Some(existing) = self.raw_fst.as_deref() {
|
|
let Ok(existing_fst) = Fst::new(existing) else {
|
|
return false;
|
|
};
|
|
for (_, node, path) in existing_fst.iter() {
|
|
if node.is_dir() {
|
|
continue;
|
|
}
|
|
if !self.user_files.iter().any(|info| info.name == path)
|
|
&& !self.junk_files.contains(&path)
|
|
{
|
|
println!("FST file {} not found", path);
|
|
return false;
|
|
}
|
|
}
|
|
println!("Using existing FST");
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn calculate_fst_size(&self) -> Result<u64> {
|
|
if self.can_use_orig_fst() {
|
|
return Ok(self.raw_fst.as_deref().unwrap().len() as u64);
|
|
}
|
|
|
|
let mut file_names = Vec::with_capacity(self.user_files.len());
|
|
for info in &self.user_files {
|
|
file_names.push(info.name.as_str());
|
|
}
|
|
// file_names.sort_unstable();
|
|
let is_wii = self.disc_header.is_wii();
|
|
let mut builder = if let Some(existing) = self.raw_fst.as_deref() {
|
|
let existing_fst = Fst::new(existing)?;
|
|
FstBuilder::new_with_string_table(is_wii, Vec::from(existing_fst.string_table))?
|
|
} else {
|
|
FstBuilder::new(is_wii)
|
|
};
|
|
for name in file_names {
|
|
builder.add_file(name, 0, 0);
|
|
}
|
|
let size = builder.byte_size() as u64;
|
|
// if size != self.partition_header.fst_size(is_wii) {
|
|
// return Err(Error::Other(format!(
|
|
// "FST size {} != {}",
|
|
// size,
|
|
// self.partition_header.fst_size(is_wii)
|
|
// )));
|
|
// }
|
|
Ok(size)
|
|
}
|
|
|
|
fn generate_fst(&mut self, write_info: &[WriteInfo]) -> Result<Arc<[u8]>> {
|
|
if self.can_use_orig_fst() {
|
|
let fst_data = self.raw_fst.as_ref().unwrap().clone();
|
|
// TODO update offsets and sizes
|
|
// let node_count = Fst::new(fst_data.as_ref())?.nodes.len();
|
|
// let string_base = node_count * size_of::<Node>();
|
|
// let (node_buf, string_table) = fst_data.split_at_mut(string_base);
|
|
// let nodes = <[Node]>::mut_from_bytes(node_buf).unwrap();
|
|
return Ok(Arc::from(fst_data));
|
|
}
|
|
|
|
let files = write_info.to_vec();
|
|
// files.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
|
let is_wii = self.disc_header.is_wii();
|
|
let mut builder = if let Some(existing) = self.raw_fst.as_deref() {
|
|
let existing_fst = Fst::new(existing)?;
|
|
FstBuilder::new_with_string_table(is_wii, Vec::from(existing_fst.string_table))?
|
|
} else {
|
|
FstBuilder::new(is_wii)
|
|
};
|
|
for info in files {
|
|
if let WriteKind::File(name) = info.kind {
|
|
builder.add_file(&name, info.offset, info.size as u32);
|
|
}
|
|
}
|
|
let raw_fst = builder.finalize();
|
|
if raw_fst.len() != self.boot_header.fst_size(is_wii) as usize {
|
|
return Err(Error::Other(format!(
|
|
"FST size mismatch: {} != {}",
|
|
raw_fst.len(),
|
|
self.boot_header.fst_size(is_wii)
|
|
)));
|
|
}
|
|
Ok(Arc::from(raw_fst))
|
|
}
|
|
|
|
fn layout_system_data(&mut self, write_info: &mut Vec<WriteInfo>) -> Result<u64> {
|
|
let mut last_offset = 0;
|
|
|
|
let Some(apploader_file) = self.apploader_file.as_ref() else {
|
|
return Err(Error::Other("Apploader not set".to_string()));
|
|
};
|
|
let Some(dol_file) = self.dol_file.as_ref() else {
|
|
return Err(Error::Other("DOL not set".to_string()));
|
|
};
|
|
let Some(raw_bi2) = self.raw_bi2.as_ref() else {
|
|
return Err(Error::Other("BI2 not set".to_string()));
|
|
};
|
|
// let Some(raw_fst) = self.raw_fst.as_ref() else {
|
|
// return Err(Error::Other("FST not set".to_string()));
|
|
// };
|
|
|
|
let mut boot = <[u8]>::new_box_zeroed_with_elems(BOOT_SIZE)?;
|
|
boot[..size_of::<DiscHeader>()].copy_from_slice(self.disc_header.as_bytes());
|
|
boot[size_of::<DiscHeader>()..].copy_from_slice(self.boot_header.as_bytes());
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::Static(Arc::from(boot), "[BOOT]"),
|
|
size: BOOT_SIZE as u64,
|
|
offset: last_offset,
|
|
});
|
|
last_offset += BOOT_SIZE as u64;
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::Static(Arc::from(raw_bi2.as_ref()), "[BI2]"),
|
|
size: BI2_SIZE as u64,
|
|
offset: last_offset,
|
|
});
|
|
last_offset += BI2_SIZE as u64;
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::File(apploader_file.name.clone()),
|
|
size: apploader_file.size,
|
|
offset: last_offset,
|
|
});
|
|
last_offset += apploader_file.size;
|
|
|
|
// Update DOL and FST offsets if not set
|
|
let is_wii = self.disc_header.is_wii();
|
|
let mut dol_offset = self.boot_header.dol_offset(is_wii);
|
|
if dol_offset == 0 {
|
|
dol_offset = last_offset.align_up(dol_file.alignment.unwrap() as u64);
|
|
self.boot_header.set_dol_offset(dol_offset, is_wii);
|
|
}
|
|
let mut fst_offset = self.boot_header.fst_offset(is_wii);
|
|
if fst_offset == 0 {
|
|
// TODO handle DOL in user data
|
|
fst_offset = (dol_offset + dol_file.size).align_up(128);
|
|
self.boot_header.set_fst_offset(fst_offset, is_wii);
|
|
}
|
|
let fst_size = self.calculate_fst_size()?;
|
|
self.boot_header.set_fst_size(fst_size, is_wii);
|
|
if self.boot_header.fst_max_size(is_wii) < fst_size {
|
|
self.boot_header.set_fst_max_size(fst_size, is_wii);
|
|
}
|
|
|
|
if dol_offset < fst_offset {
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::File(dol_file.name.clone()),
|
|
size: dol_file.size,
|
|
offset: dol_offset,
|
|
});
|
|
} else {
|
|
// DOL in user data
|
|
}
|
|
// write_info.push(WriteInfo {
|
|
// kind: WriteKind::Static(Arc::from(raw_fst.as_ref()), "[FST]"),
|
|
// size: fst_size,
|
|
// offset: fst_offset,
|
|
// });
|
|
|
|
Ok(fst_offset + fst_size)
|
|
}
|
|
|
|
fn layout_files(&mut self) -> Result<Vec<WriteInfo>> {
|
|
let mut system_write_info = Vec::new();
|
|
let mut write_info = Vec::with_capacity(self.user_files.len());
|
|
let mut last_offset = self.layout_system_data(&mut system_write_info)?;
|
|
|
|
// Layout user data
|
|
let mut user_offset = self.boot_header.user_offset.get() as u64;
|
|
if user_offset == 0 {
|
|
user_offset = last_offset.align_up(SECTOR_SIZE as u64);
|
|
self.boot_header.user_offset.set(user_offset as u32);
|
|
} else if user_offset < last_offset {
|
|
return Err(Error::Other(format!(
|
|
"User offset {:#X} is before FST {:#X}",
|
|
user_offset, last_offset
|
|
)));
|
|
}
|
|
last_offset = user_offset;
|
|
for info in &self.user_files {
|
|
let offset = info
|
|
.offset
|
|
.unwrap_or_else(|| last_offset.align_up(info.alignment.unwrap_or(32) as u64));
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::File(info.name.clone()),
|
|
offset,
|
|
size: info.size,
|
|
});
|
|
last_offset = offset + info.size;
|
|
}
|
|
|
|
// Generate FST from only user files
|
|
let is_wii = self.disc_header.is_wii();
|
|
let fst_data = self.generate_fst(&write_info)?;
|
|
let fst_size = fst_data.len() as u64;
|
|
write_info.push(WriteInfo {
|
|
kind: WriteKind::Static(fst_data, "[FST]"),
|
|
size: fst_size,
|
|
offset: self.boot_header.fst_offset(is_wii),
|
|
});
|
|
// Add system files to write info
|
|
write_info.extend(system_write_info);
|
|
// Sort files by offset
|
|
sort_files(&mut write_info)?;
|
|
|
|
// Update user size if not set
|
|
if self.boot_header.user_size.get() == 0 {
|
|
let user_end = if self.disc_header.is_wii() {
|
|
last_offset.align_up(SECTOR_SIZE as u64)
|
|
} else {
|
|
MINI_DVD_SIZE
|
|
};
|
|
self.boot_header.user_size.set((user_end - user_offset) as u32);
|
|
}
|
|
|
|
// Insert junk data
|
|
let write_info = insert_junk_data(write_info, &self.boot_header);
|
|
|
|
Ok(write_info)
|
|
}
|
|
|
|
fn junk_id(&self) -> [u8; 4] {
|
|
self.junk_id.unwrap_or_else(|| *array_ref![self.disc_header.game_id, 0, 4])
|
|
}
|
|
}
|
|
|
|
pub(crate) fn insert_junk_data(
|
|
write_info: Vec<WriteInfo>,
|
|
boot_header: &BootHeader,
|
|
) -> Vec<WriteInfo> {
|
|
let mut new_write_info = Vec::with_capacity(write_info.len());
|
|
|
|
let fst_end = boot_header.fst_offset(false) + boot_header.fst_size(false);
|
|
let file_gap = find_file_gap(&write_info, fst_end);
|
|
let mut last_file_end = 0;
|
|
for info in write_info {
|
|
if let WriteKind::File(..) | WriteKind::Static(..) = &info.kind {
|
|
let aligned_end = gcm_align(last_file_end);
|
|
if info.offset > aligned_end && last_file_end >= fst_end {
|
|
// Junk data is aligned to 4 bytes with a 28 byte padding (aka `(n + 31) & !3`)
|
|
// but a few cases don't have the 28 byte padding. Namely, the junk data after the
|
|
// FST, and the junk data in between the inner and outer rim files. This attempts to
|
|
// determine the correct alignment, but is not 100% accurate.
|
|
let junk_start = if file_gap == Some(last_file_end) {
|
|
last_file_end.align_up(4)
|
|
} else {
|
|
aligned_end
|
|
};
|
|
new_write_info.push(WriteInfo {
|
|
kind: WriteKind::Junk,
|
|
size: info.offset - junk_start,
|
|
offset: junk_start,
|
|
});
|
|
}
|
|
last_file_end = info.offset + info.size;
|
|
}
|
|
new_write_info.push(info);
|
|
}
|
|
let aligned_end = gcm_align(last_file_end);
|
|
let user_end = boot_header.user_offset.get() as u64 + boot_header.user_size.get() as u64;
|
|
if aligned_end < user_end && aligned_end >= fst_end {
|
|
new_write_info.push(WriteInfo {
|
|
kind: WriteKind::Junk,
|
|
size: user_end - aligned_end,
|
|
offset: aligned_end,
|
|
});
|
|
}
|
|
|
|
new_write_info
|
|
}
|
|
|
|
impl GCPartitionWriter {
|
|
fn new(write_info: Vec<WriteInfo>, disc_size: u64, disc_id: [u8; 4], disc_num: u8) -> Self {
|
|
Self { write_info, disc_size, disc_id, disc_num }
|
|
}
|
|
|
|
pub fn write_to<W>(
|
|
&self,
|
|
out: &mut W,
|
|
mut file_callback: impl FnMut(&mut dyn Write, &str) -> io::Result<()>,
|
|
) -> Result<()>
|
|
where
|
|
W: Write + ?Sized,
|
|
{
|
|
let mut out = WriteCursor { inner: out, position: 0 };
|
|
let mut lfg = LaggedFibonacci::default();
|
|
for info in &self.write_info {
|
|
out.write_zeroes_until(info.offset).context("Writing padding")?;
|
|
match &info.kind {
|
|
WriteKind::File(name) => file_callback(&mut out, name)
|
|
.with_context(|| format!("Writing file {}", name))?,
|
|
WriteKind::Static(data, name) => out.write_all(data).with_context(|| {
|
|
format!("Writing static data {} ({} bytes)", name, data.len())
|
|
})?,
|
|
WriteKind::Junk => {
|
|
lfg.write_sector_chunked(
|
|
&mut out,
|
|
info.size,
|
|
self.disc_id,
|
|
self.disc_num,
|
|
info.offset,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"Writing junk data at {:X} -> {:X}",
|
|
info.offset,
|
|
info.offset + info.size
|
|
)
|
|
})?;
|
|
}
|
|
};
|
|
if out.position != info.offset + info.size {
|
|
return Err(Error::Other(format!(
|
|
"File {}: Wrote {} bytes, expected {}",
|
|
info.kind.name(),
|
|
out.position - info.offset,
|
|
info.size
|
|
)));
|
|
}
|
|
}
|
|
out.write_zeroes_until(self.disc_size).context("Writing end of file")?;
|
|
out.flush().context("Flushing output")?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn into_stream<Cb>(self, file_callback: Cb) -> Result<Box<dyn DiscStream>>
|
|
where Cb: FileCallback + 'static {
|
|
Ok(Box::new(GCPartitionStream::new(
|
|
file_callback,
|
|
Arc::from(self.write_info),
|
|
self.disc_size,
|
|
self.disc_id,
|
|
self.disc_num,
|
|
)))
|
|
}
|
|
}
|
|
|
|
struct WriteCursor<W> {
|
|
inner: W,
|
|
position: u64,
|
|
}
|
|
|
|
impl<W> WriteCursor<W>
|
|
where W: Write
|
|
{
|
|
fn write_zeroes_until(&mut self, until: u64) -> io::Result<()> {
|
|
static ZEROES: [u8; 0x1000] = [0u8; 0x1000];
|
|
let mut remaining = until.saturating_sub(self.position);
|
|
while remaining > 0 {
|
|
let write_len = remaining.min(ZEROES.len() as u64) as usize;
|
|
let written = self.write(&ZEROES[..write_len])?;
|
|
remaining -= written as u64;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<W> Write for WriteCursor<W>
|
|
where W: Write
|
|
{
|
|
#[inline]
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
let len = self.inner.write(buf)?;
|
|
self.position += len as u64;
|
|
Ok(len)
|
|
}
|
|
|
|
#[inline]
|
|
fn flush(&mut self) -> io::Result<()> { self.inner.flush() }
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct GCPartitionStream<Cb> {
|
|
file_callback: Cb,
|
|
pos: u64,
|
|
write_info: Arc<[WriteInfo]>,
|
|
size: u64,
|
|
disc_id: [u8; 4],
|
|
disc_num: u8,
|
|
}
|
|
|
|
impl<Cb> GCPartitionStream<Cb> {
|
|
pub fn new(
|
|
file_callback: Cb,
|
|
write_info: Arc<[WriteInfo]>,
|
|
size: u64,
|
|
disc_id: [u8; 4],
|
|
disc_num: u8,
|
|
) -> Self {
|
|
Self { file_callback, pos: 0, write_info, size, disc_id, disc_num }
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_position(&mut self, pos: u64) { self.pos = pos; }
|
|
|
|
#[inline]
|
|
pub fn len(&self) -> u64 { self.size }
|
|
}
|
|
|
|
impl<Cb> Read for GCPartitionStream<Cb>
|
|
where Cb: FileCallback
|
|
{
|
|
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
|
|
if self.pos >= self.size {
|
|
// Out of bounds
|
|
return Ok(0);
|
|
}
|
|
|
|
let end = (self.size - self.pos).min(out.len() as u64) as usize;
|
|
let mut buf = &mut out[..end];
|
|
let mut curr = self
|
|
.write_info
|
|
.binary_search_by_key(&self.pos, |i| i.offset)
|
|
.unwrap_or_else(|idx| idx.saturating_sub(1));
|
|
let mut pos = self.pos;
|
|
let mut total = 0;
|
|
while !buf.is_empty() {
|
|
let Some(info) = self.write_info.get(curr) else {
|
|
buf.fill(0);
|
|
total += buf.len();
|
|
break;
|
|
};
|
|
if pos > info.offset + info.size {
|
|
curr += 1;
|
|
continue;
|
|
}
|
|
let read = if pos < info.offset {
|
|
let read = buf.len().min((info.offset - pos) as usize);
|
|
buf[..read].fill(0);
|
|
read
|
|
} else {
|
|
let read = buf.len().min((info.offset + info.size - pos) as usize);
|
|
match &info.kind {
|
|
WriteKind::File(name) => {
|
|
self.file_callback.read_file(&mut buf[..read], name, pos - info.offset)?;
|
|
}
|
|
WriteKind::Static(data, _) => {
|
|
let offset = (pos - info.offset) as usize;
|
|
buf[..read].copy_from_slice(&data[offset..offset + read]);
|
|
}
|
|
WriteKind::Junk => {
|
|
let mut lfg = LaggedFibonacci::default();
|
|
lfg.fill_sector_chunked(&mut buf[..read], self.disc_id, self.disc_num, pos);
|
|
}
|
|
}
|
|
curr += 1;
|
|
read
|
|
};
|
|
buf = &mut buf[read..];
|
|
pos += read as u64;
|
|
total += read;
|
|
}
|
|
|
|
Ok(total)
|
|
}
|
|
}
|
|
|
|
impl<Cb> Seek for GCPartitionStream<Cb> {
|
|
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
|
self.pos = match pos {
|
|
io::SeekFrom::Start(pos) => pos,
|
|
io::SeekFrom::End(v) => self.size.saturating_add_signed(v),
|
|
io::SeekFrom::Current(v) => self.pos.saturating_add_signed(v),
|
|
};
|
|
Ok(self.pos)
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn gcm_align(n: u64) -> u64 { (n + 31) & !3 }
|
|
|
|
fn sort_files(files: &mut [WriteInfo]) -> Result<()> {
|
|
files.sort_unstable_by_key(|info| (info.offset, info.size));
|
|
for i in 1..files.len() {
|
|
let prev = &files[i - 1];
|
|
let cur = &files[i];
|
|
if cur.offset < prev.offset + prev.size {
|
|
let name = match &cur.kind {
|
|
WriteKind::File(name) => name.as_str(),
|
|
WriteKind::Static(_, name) => name,
|
|
WriteKind::Junk => "[junk data]",
|
|
};
|
|
let prev_name = match &prev.kind {
|
|
WriteKind::File(name) => name.as_str(),
|
|
WriteKind::Static(_, name) => name,
|
|
WriteKind::Junk => "[junk data]",
|
|
};
|
|
return Err(Error::Other(format!(
|
|
"File {} ({:#X}-{:#X}) overlaps with {} ({:#X}-{:#X})",
|
|
name,
|
|
cur.offset,
|
|
cur.offset + cur.size,
|
|
prev_name,
|
|
prev.offset,
|
|
prev.offset + prev.size
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Files can be located on the inner rim of the disc (closer to the center) or the outer rim
|
|
/// (closer to the edge). The inner rim is slower to read, so developers often configured certain
|
|
/// files to be located on the outer rim. This function attempts to find a gap in the file offsets
|
|
/// between the inner and outer rim, which we need to recreate junk data properly.
|
|
fn find_file_gap(file_infos: &[WriteInfo], fst_end: u64) -> Option<u64> {
|
|
let mut last_offset = 0;
|
|
for info in file_infos {
|
|
if let WriteKind::File(..) | WriteKind::Static(..) = &info.kind {
|
|
if last_offset > fst_end && info.offset > last_offset + SECTOR_SIZE as u64 {
|
|
debug!("Found file gap at {:X} -> {:X}", last_offset, info.offset);
|
|
return Some(last_offset);
|
|
}
|
|
last_offset = info.offset + info.size;
|
|
}
|
|
}
|
|
None
|
|
}
|