stfs 0.1.0

Parser for Xbox 360 STFS (Secure Transacted File System) packages
Documentation
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
use serde::Serialize;
use std::io::Cursor;
use std::io::Read;

use crate::error::StfsError;
use crate::header::StfsVolumeDescriptor;
use crate::header::XContentHeader;
use crate::io::ReadAt;
use crate::types::*;

#[derive(Default, Debug, Serialize, Clone)]
pub struct HashEntry {
	pub block_hash: Sha1Digest,
	pub status: u8,
	pub next_block: BlockNumber,
}

#[derive(Debug, Serialize)]
pub struct HashTable {
	pub level: HashTableLevel,
	pub true_block_number: usize,
	pub entry_count: usize,
	pub address_in_file: usize,
	pub entries: Vec<HashEntry>,
}

impl Default for HashTable {
	fn default() -> Self {
		Self {
			level: HashTableLevel::First,
			true_block_number: 0,
			entry_count: 0,
			address_in_file: 0,
			entries: Vec::new(),
		}
	}
}

#[derive(Default, Debug, Serialize)]
pub struct HashTableMeta {
	pub block_step: [usize; 2],
	pub tables_per_level: [usize; 3],
	pub top_table: HashTable,
	pub first_table_address: usize,
}

impl HashTableMeta {
	pub fn from_header(header: &XContentHeader, sex: StfsPackageSex) -> Result<Self, StfsError> {
		let block_step = sex.block_step();
		let first_table_address = ((header.header_size as usize) + 0x0FFF) & 0xFFFF_F000;

		let stfs_vol = header.volume_descriptor.stfs_ref();
		let allocated_block_count = stfs_vol.allocated_block_count as usize;

		let tables_per_level = [
			(allocated_block_count / HASHES_PER_HASH_TABLE)
				+ if !allocated_block_count.is_multiple_of(HASHES_PER_HASH_TABLE) { 1 } else { 0 },
			0,
			0,
		];

		let top_level = header.root_hash_table_level()?;

		let mut meta = HashTableMeta {
			block_step,
			tables_per_level,
			first_table_address,
			top_table: HashTable { level: top_level, ..Default::default() },
		};

		meta.top_table.true_block_number = meta.compute_backing_hash_block_number_for_level(0, top_level, sex);

		let base_address = (meta.top_table.true_block_number * BLOCK_SIZE) + first_table_address;
		meta.top_table.address_in_file = base_address + (((stfs_vol.block_separation as usize) & 2) << 0xB);

		meta.top_table.entry_count = allocated_block_count / DATA_BLOCKS_PER_HASH_TREE_LEVEL[top_level as usize];

		if (allocated_block_count > DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]
			&& !allocated_block_count.is_multiple_of(DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]))
			|| (allocated_block_count > HASHES_PER_HASH_TABLE
				&& !allocated_block_count.is_multiple_of(HASHES_PER_HASH_TABLE))
		{
			meta.top_table.entry_count += 1;
		}

		Ok(meta)
	}

	pub fn read_top_table<R: ReadAt>(&mut self, source: &R) -> Result<(), StfsError> {
		let entry_size = 0x18;
		let range_start = self.top_table.address_in_file;
		let range_end = range_start + self.top_table.entry_count * entry_size;
		let data = source.read_at(range_start..range_end)?;
		let data = data.as_ref();

		let mut cursor = Cursor::new(data);
		self.top_table.entries.reserve(self.top_table.entry_count);

		for _ in 0..self.top_table.entry_count {
			let mut hash_bytes = [0u8; 0x14];
			cursor.read_exact(&mut hash_bytes)?;
			let status = cursor.read_u8()?;
			let next_block = BlockNumber(cursor.read_u24::<BigEndian>()? as usize);
			self.top_table.entries.push(HashEntry { block_hash: Sha1Digest(hash_bytes), status, next_block });
		}

		Ok(())
	}

	pub(crate) fn compute_backing_hash_block_number_for_level(
		&self,
		block: usize,
		level: HashTableLevel,
		sex: StfsPackageSex,
	) -> usize {
		match level {
			HashTableLevel::First => self.compute_first_level_backing_hash_block_number(block, sex),
			HashTableLevel::Second => self.compute_second_level_backing_hash_block_number(block, sex),
			HashTableLevel::Third => self.compute_third_level_backing_hash_block_number(),
		}
	}

	pub(crate) fn compute_first_level_backing_hash_block_number(&self, block: usize, sex: StfsPackageSex) -> usize {
		if block < HASHES_PER_HASH_TABLE {
			return 0;
		}

		let mut block_number = (block / HASHES_PER_HASH_TABLE) * self.block_step[0];
		block_number += ((block / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]) + 1) << (sex as u8);

		if block / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2] == 0 {
			block_number
		} else {
			block_number + (1 << (sex as u8))
		}
	}

	pub(crate) fn compute_second_level_backing_hash_block_number(&self, block: usize, sex: StfsPackageSex) -> usize {
		if block < DATA_BLOCKS_PER_HASH_TREE_LEVEL[2] {
			self.block_step[0]
		} else {
			(1 << (sex as u8)) + (block / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]) * self.block_step[1]
		}
	}

	pub(crate) fn compute_third_level_backing_hash_block_number(&self) -> usize {
		self.block_step[1]
	}

	pub fn block_to_addr(&self, block: BlockNumber, sex: StfsPackageSex) -> usize {
		let block = block.0;
		if block > 2usize.pow(24) - 1 {
			panic!("invalid block: {:#x}", block);
		}

		(self.compute_data_block_num(block, sex) * BLOCK_SIZE) + self.first_table_address
	}

	fn compute_data_block_num(&self, block: usize, sex: StfsPackageSex) -> usize {
		let addr = (((block + HASHES_PER_HASH_TABLE) / HASHES_PER_HASH_TABLE) << (sex as usize)) + block;
		if block < HASHES_PER_HASH_TABLE {
			addr
		} else if block < DATA_BLOCKS_PER_HASH_TREE_LEVEL[2] {
			(addr + ((addr + DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]) / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2])) << sex as usize
		} else {
			(1 << sex as usize)
				+ ((addr + ((block + DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]) / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]))
					<< sex as usize)
		}
	}

	pub(crate) fn hash_table_skip_for_address(&self, table_address: usize, sex: StfsPackageSex) -> usize {
		let mut block_number = (table_address - self.first_table_address) / BLOCK_SIZE;

		if block_number == 0 {
			return BLOCK_SIZE << sex as usize;
		}

		if block_number == self.block_step[1] {
			return 0x3000 << sex as usize;
		} else if block_number > self.block_step[1] {
			block_number -= self.block_step[1] + (1 << sex as usize);
		}

		if block_number == self.block_step[0] || block_number.is_multiple_of(self.block_step[1]) {
			return 0x2000 << sex as usize;
		}

		BLOCK_SIZE << sex as usize
	}

	pub fn read_block_hash_entry<R: ReadAt>(
		&self,
		source: &R,
		block: BlockNumber,
		sex: StfsPackageSex,
		stfs_vol: &StfsVolumeDescriptor,
	) -> Result<HashEntry, StfsError> {
		if block.0 > stfs_vol.allocated_block_count as usize {
			return Err(StfsError::InvalidBlock { block: block.0, allocated: stfs_vol.allocated_block_count as usize });
		}

		let addr = self.block_hash_address(block.0, sex, stfs_vol, source)?;
		let data = source.read_at(addr..addr + 0x18)?;
		let data = data.as_ref();

		let mut cursor = Cursor::new(data);
		let mut hash_bytes = [0u8; 0x14];
		cursor.read_exact(&mut hash_bytes)?;
		let status = cursor.read_u8()?;
		let next_block = BlockNumber(cursor.read_u24::<BigEndian>()? as usize);

		Ok(HashEntry { block_hash: Sha1Digest(hash_bytes), status, next_block })
	}

	pub(crate) fn block_hash_address<R: ReadAt>(
		&self,
		block: usize,
		sex: StfsPackageSex,
		stfs_vol: &StfsVolumeDescriptor,
		source: &R,
	) -> Result<usize, StfsError> {
		if block > stfs_vol.allocated_block_count as usize {
			return Err(StfsError::InvalidBlock { block, allocated: stfs_vol.allocated_block_count as usize });
		}

		let mut hash_addr =
			(self.compute_first_level_backing_hash_block_number(block, sex) * BLOCK_SIZE) + self.first_table_address;
		hash_addr += (block % HASHES_PER_HASH_TABLE) * 0x18;

		match self.top_table.level {
			HashTableLevel::First => Ok(hash_addr + (((stfs_vol.block_separation as usize) & 2) << 0xB)),
			HashTableLevel::Second => {
				let entry = &self.top_table.entries[block / DATA_BLOCKS_PER_HASH_TREE_LEVEL[1]];
				Ok(hash_addr + (((entry.status as usize) & 0x40) << 6))
			}
			HashTableLevel::Third => {
				let first_level_offset =
					((self.top_table.entries[block / DATA_BLOCKS_PER_HASH_TREE_LEVEL[2]].status as usize) & 0x40) << 6;

				let position = (self.compute_second_level_backing_hash_block_number(block, sex) * BLOCK_SIZE)
					+ self.first_table_address
					+ first_level_offset
					+ ((block % DATA_BLOCKS_PER_HASH_TREE_LEVEL[1]) * 0x18);

				let data = source.read_at(position + 0x14..position + 0x15)?;
				let status_byte = data.as_ref()[0];

				Ok(hash_addr + (((status_byte as usize) & 0x40) << 0x6))
			}
		}
	}
}