Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions fatxlib/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ fn main() {

let xbox360 = read_xbox360("data/xbox360_titles.json");
let xbox_og = read_xbox_originals("data/xbox_originals.tsv");
assert!(
xbox360.len() > 4000,
"xbox360 title catalog unexpectedly small"
);
assert!(
xbox_og.len() > 800,
"xbox original title catalog unexpectedly small"
);
let (merged, conflicts) = merge(&xbox360, &xbox_og);

write_merged(&out_dir.join("titles.rs"), &merged);
Expand Down Expand Up @@ -197,11 +205,14 @@ fn strip_ws(s: &str) -> String {

fn write_merged(dst: &PathBuf, map: &BTreeMap<u32, (String, &str)>) {
let mut builder = phf_codegen::Map::<u32>::new();
let literals: Vec<String> = map
.values()
.map(|(name, src)| format!("TitleInfo {{ name: {name:?}, source: {src} }}"))
.collect();
for ((id, _), lit) in map.iter().zip(literals.iter()) {
let mut entries = Vec::with_capacity(map.len());
for (id, (name, src)) in map {
entries.push((
*id,
format!("TitleInfo {{ name: {name:?}, source: {src} }}"),
));
}
for (id, lit) in &entries {
builder.entry(*id, lit);
}

Expand Down
27 changes: 26 additions & 1 deletion fatxlib/src/executable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl TitleExecutionInfo {
reader.seek(SeekFrom::Current(8)).map_err(FatxError::Io)?;
let title_id = reader.read_u32::<LE>().map_err(FatxError::Io)?;

reader.seek(SeekFrom::Current(164)).map_err(FatxError::Io)?;
reader.seek(SeekFrom::Current(160)).map_err(FatxError::Io)?;
let version = reader.read_u32::<LE>().map_err(FatxError::Io)?;

Ok(TitleExecutionInfo {
Expand Down Expand Up @@ -106,3 +106,28 @@ impl TitleInfo {
}
}
}

#[cfg(test)]
mod tests {
use super::xbe::XbeHeader;

#[test]
fn xbe_certificate_version_is_read_from_correct_offset() {
let mut data = vec![0u8; 0x200];
data[0..4].copy_from_slice(b"XBEH");
data[0x104..0x108].copy_from_slice(&0x0001_0000u32.to_le_bytes());
data[0x118..0x11c].copy_from_slice(&0x0001_0100u32.to_le_bytes());

let title_id = 0x4D53_07E6u32;
let version = 0x1122_3344u32;
let wrong_version = 0x5566_7788u32;
data[0x108..0x10c].copy_from_slice(&title_id.to_le_bytes());
data[0x1ac..0x1b0].copy_from_slice(&version.to_le_bytes());
data[0x1b0..0x1b4].copy_from_slice(&wrong_version.to_le_bytes());

let header = XbeHeader::read(std::io::Cursor::new(data)).expect("parse synthetic xbe");
let info = header.fields.execution_info.expect("execution info");
assert_eq!(info.title_id, title_id);
assert_eq!(info.version, version);
}
}
26 changes: 22 additions & 4 deletions fatxlib/src/iso/god/con_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ impl ConHeaderBuilder {
self.buffer[offset..offset + buf.len()].copy_from_slice(buf);
}

fn write_utf16_be(&mut self, offset: usize, s: &str) {
for (i, c) in s.encode_utf16().chain([0]).enumerate() {
fn write_utf16_be(&mut self, offset: usize, s: &str, max_units: usize) {
for (i, c) in s
.encode_utf16()
.take(max_units.saturating_sub(1))
.chain([0])
.enumerate()
{
self.write_u16_be(offset + i * 2, c);
}
}
Expand Down Expand Up @@ -99,8 +104,8 @@ impl ConHeaderBuilder {
}

pub fn with_game_title(mut self, game_title: &str) -> Self {
self.write_utf16_be(0x0411, game_title);
self.write_utf16_be(0x1691, game_title);
self.write_utf16_be(0x0411, game_title, 0x40);
self.write_utf16_be(0x1691, game_title, 0x40);
self
}

Expand All @@ -120,3 +125,16 @@ impl ConHeaderBuilder {
self.buffer
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn game_title_is_truncated_to_field_size() {
let title = "X".repeat(80);
let bytes = ConHeaderBuilder::new().with_game_title(&title).finalize();
assert_eq!(&bytes[0x0411 + 126..0x0411 + 128], &[0, 0]);
assert_eq!(&bytes[0x1691 + 126..0x1691 + 128], &[0, 0]);
}
}
136 changes: 136 additions & 0 deletions fatxlib/src/iso/god/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,139 @@ pub(crate) fn part_payload_bytes(data_size: u64, part_index: u64) -> u64 {
.saturating_sub(part_start)
.min(BLOCKS_PER_PART * BLOCK_SIZE)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::executable::TitleExecutionInfo;
use crate::iso::god::prepare::test_prepared_source;
use std::io::Cursor;

struct TestSink {
masters: Vec<HashList>,
writes: Vec<(u64, [u8; 20])>,
con_header: Option<Vec<u8>>,
last_part_size: u64,
}

impl TestSink {
fn new(masters: Vec<HashList>, last_part_size: u64) -> Self {
Self {
masters,
writes: Vec::new(),
con_header: None,
last_part_size,
}
}
}

impl GodSink for TestSink {
fn begin(&mut self, _source: &PreparedSource) -> Result<()> {
Ok(())
}

fn write_part<'a>(
&mut self,
_source: &PreparedSource,
_part_index: u64,
_opts: &mut ConvertOptions<'a>,
) -> Result<()> {
Ok(())
}

fn read_master_hash(
&mut self,
_source: &PreparedSource,
part_index: u64,
) -> Result<HashList> {
self.masters
.get(part_index as usize)
.cloned()
.ok_or_else(|| FatxError::Other("missing master".to_string()))
}

fn write_master_hash(
&mut self,
_source: &PreparedSource,
part_index: u64,
mht: &HashList,
) -> Result<()> {
self.writes.push((part_index, mht.digest()));
if let Some(slot) = self.masters.get_mut(part_index as usize) {
*slot = mht.clone();
}
Ok(())
}

fn last_part_size(&self, _source: &PreparedSource) -> Result<u64> {
Ok(self.last_part_size)
}

fn write_con_header(&mut self, _source: &PreparedSource, con_bytes: Vec<u8>) -> Result<()> {
self.con_header = Some(con_bytes);
Ok(())
}
}

fn hash_list_from_seed(seed: u8) -> HashList {
let mut bytes = [0u8; 4096];
bytes[..20].fill(seed);
HashList::read(Cursor::new(bytes)).expect("seeded hash list")
}

#[test]
fn mht_back_chain_updates_previous_part() {
let report = ConvertReport {
title_id: 0x4D53_07E6,
media_id: 0x1234_5678,
content_type: super::super::ContentType::GamesOnDemand,
part_count: 3,
block_count: 3,
data_size: BLOCK_SIZE * BLOCKS_PER_PART * 2,
};
let source = test_prepared_source(
report,
TitleExecutionInfo {
media_id: 0x1234_5678,
version: 1,
base_version: 0,
title_id: 0x4D53_07E6,
platform: 0xFF,
executable_type: 0,
disc_number: 1,
disc_count: 1,
},
super::super::ContentType::GamesOnDemand,
);
let part0 = hash_list_from_seed(0x11);
let part1 = hash_list_from_seed(0x22);
let part2 = hash_list_from_seed(0x33);
let mut sink = TestSink::new(
vec![part0.clone(), part1.clone(), part2.clone()],
BLOCK_SIZE,
);
let mut opts = ConvertOptions::default();

let result = run_conversion(&source, &mut sink, &mut opts, "test").expect("conversion");
assert_eq!(result.part_count, 3);
assert_eq!(sink.writes.len(), 2);

assert_eq!(sink.writes[0].0, 1);
assert_eq!(
sink.writes[0].1,
[
0x6a, 0xb0, 0xca, 0x46, 0x1a, 0x72, 0xcf, 0x70, 0x51, 0x79, 0xa1, 0xf3, 0x08, 0x8b,
0x54, 0xfa, 0x4d, 0xd4, 0xfa, 0x24,
]
);
assert_eq!(sink.writes[1].0, 0);
assert_eq!(
sink.writes[1].1,
[
0xe9, 0x63, 0x9e, 0x2e, 0x25, 0xf0, 0x3c, 0x58, 0xa2, 0x07, 0xb7, 0xad, 0x9b, 0x27,
0x4e, 0xa5, 0x5b, 0x61, 0x15, 0x0d,
]
);
assert!(sink.con_header.is_some());
}
}
46 changes: 46 additions & 0 deletions fatxlib/src/iso/god/hash_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,49 @@ impl HashList {
Ok(())
}
}

#[cfg(test)]
mod tests {
use std::io::Cursor;

use super::*;

#[test]
fn read_preserves_fixed_bytes_and_len() {
let mut bytes = [0u8; 4096];
bytes[..20].fill(0x11);
bytes[40..60].fill(0x22);

let list = HashList::read(Cursor::new(bytes)).expect("read hash list");
assert_eq!(&list.bytes()[..20], &[0x11; 20]);
assert_eq!(&list.bytes()[20..40], &[0x00; 20]);
assert_eq!(&list.bytes()[40..60], &[0x22; 20]);
}

#[test]
fn write_emits_exact_fixed_buffer() {
let mut list = HashList::new();
list.add_hash(&[0x11; 20]);
list.add_hash(&[0x22; 20]);

let mut out = Vec::new();
list.write(&mut out).expect("write hash list");

assert_eq!(out.len(), 4096);
assert_eq!(&out[..20], &[0x11; 20]);
assert_eq!(&out[20..40], &[0x22; 20]);
assert!(out[40..].iter().all(|b| *b == 0));
}

#[test]
fn digest_matches_known_zero_page() {
let list = HashList::new();
assert_eq!(
list.digest(),
[
0x1c, 0xea, 0xf7, 0x3d, 0xf4, 0x0e, 0x53, 0x1d, 0xf3, 0xbf, 0xb2, 0x6b, 0x4f, 0xb7,
0xcd, 0x95, 0xfb, 0x7b, 0xff, 0x1d,
]
);
}
}
17 changes: 17 additions & 0 deletions fatxlib/src/iso/god/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ pub(crate) fn prepare_source(
})
}

#[cfg(test)]
pub(crate) fn test_prepared_source(
report: ConvertReport,
exe_info: crate::executable::TitleExecutionInfo,
content_type: ContentType,
) -> PreparedSource {
PreparedSource {
report,
exe_info,
content_type,
reader: ReaderSource::Raw {
source_iso: PathBuf::new(),
root_offset: 0,
},
}
}

fn build_report(
title_id: u32,
media_id: u32,
Expand Down
Loading