Allow creating blank objects for WADs, TMDs, Tickets, ContentRegions, and Titles to make WAD packing possible

This commit is contained in:
Campbell 2024-03-31 23:38:52 -04:00
parent 142a121fa9
commit 640ca91716
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
5 changed files with 89 additions and 102 deletions

View File

@ -13,33 +13,27 @@ from .crypto import decrypt_content, encrypt_content
class ContentRegion: class ContentRegion:
"""Creates a ContentRegion object to parse the continuous content region of a WAD. """Creates a ContentRegion object to parse the continuous content region of a WAD.
Parameters
----------
content_region : bytes
A bytes object containing the content region of a WAD file.
content_records : list[ContentRecord]
A list of ContentRecord objects detailing all contents contained in the region.
""" """
def __init__(self, content_region, content_records: List[ContentRecord]): def __init__(self):
self.content_region = content_region self.content_records: List[ContentRecord] = []
self.content_records = content_records
self.content_region_size: int = 0 # Size of the content region. self.content_region_size: int = 0 # Size of the content region.
self.num_contents: int = 0 # Number of contents in the content region. self.num_contents: int = 0 # Number of contents in the content region.
self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region. self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region.
self.content_list: List[bytes] = [] self.content_list: List[bytes] = []
# Call load() to set all of the attributes from the raw content region provided.
self.load()
def load(self): def load(self, content_region: bytes, content_records: List[ContentRecord]) -> None:
"""Loads the raw content region and builds a list of all the contents. """Loads the raw content region and builds a list of all the contents.
Returns Parameters
------- ----------
none content_region : bytes
The raw data for the content region being loaded.
content_records : list[ContentRecord]
A list of ContentRecord objects detailing all contents contained in the region.
""" """
with io.BytesIO(self.content_region) as content_region_data: self.content_records = content_records
with io.BytesIO(content_region) as content_region_data:
# Get the total size of the content region. # Get the total size of the content region.
self.content_region_size = sys.getsizeof(content_region_data) self.content_region_size = sys.getsizeof(content_region_data)
self.num_contents = len(self.content_records) self.num_contents = len(self.content_records)
@ -86,13 +80,9 @@ class ContentRegion:
if padding_bytes > 0: if padding_bytes > 0:
content_region_data.write(b'\x00' * padding_bytes) content_region_data.write(b'\x00' * padding_bytes)
content_region_data.seek(0x0) content_region_data.seek(0x0)
self.content_region = content_region_data.read() content_region_raw = content_region_data.read()
# Clear existing lists. # Return the raw ContentRegion for the data contained in the object.
self.content_start_offsets = [0] return content_region_raw
self.content_list = []
# Reload object's attributes to ensure the raw data and object match.
self.load()
return self.content_region
def get_enc_content_by_index(self, index: int) -> bytes: def get_enc_content_by_index(self, index: int) -> bytes:
"""Gets an individual content from the content region based on the provided index, in encrypted form. """Gets an individual content from the content region based on the provided index, in encrypted form.
@ -291,3 +281,11 @@ class ContentRegion:
enc_content = encrypt_content(dec_content, title_key, index) enc_content = encrypt_content(dec_content, title_key, index)
# Pass values to set_enc_content() # Pass values to set_enc_content()
self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash) self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash)
def load_enc_content(self, enc_content: bytes, index: int) -> bytes:
"""Loads the provided encrypted content into the content region at the specified index, with the assumption that
it matches the record at that index.
:param index:
:return:
"""

View File

@ -11,12 +11,9 @@ from typing import List
class Ticket: class Ticket:
"""Creates a Ticket object to parse a Ticket file to retrieve the Title Key needed to decrypt it. """
Creates a Ticket object that allows for either loading and editing an existing Ticket or creating one manually if
Parameters desired.
----------
ticket : bytes
A bytes object containing the contents of a ticket file.
Attributes Attributes
---------- ----------
@ -35,8 +32,7 @@ class Ticket:
common_key_index : int common_key_index : int
The index of the common key required to decrypt this ticket's Title Key. The index of the common key required to decrypt this ticket's Title Key.
""" """
def __init__(self, ticket): def __init__(self):
self.ticket = ticket
# Signature blob header # Signature blob header
self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048 self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048
self.signature: bytes = b'' # Actual signature data self.signature: bytes = b'' # Actual signature data
@ -60,17 +56,17 @@ class Ticket:
self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title. self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title.
# v1 ticket data # v1 ticket data
# TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1. # TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1.
# Call load() to set all of the attributes from the raw Ticket data provided.
self.load()
def load(self): def load(self, ticket: bytes) -> None:
"""Loads the raw Ticket data and sets all attributes of the Ticket object. """Loads raw Ticket data and sets all attributes of the WAD object. This allows for manipulating an already
existing Ticket.
Returns Parameters
------- ----------
none ticket : bytes
The data for the Ticket you wish to load.
""" """
with io.BytesIO(self.ticket) as ticket_data: with io.BytesIO(ticket) as ticket_data:
# ==================================================================================== # ====================================================================================
# Parses each of the keys contained in the Ticket. # Parses each of the keys contained in the Ticket.
# ==================================================================================== # ====================================================================================
@ -209,10 +205,9 @@ class Ticket:
title_limit_data.close() title_limit_data.close()
# Set the Ticket attribute of the object to the new raw Ticket. # Set the Ticket attribute of the object to the new raw Ticket.
ticket_data.seek(0x0) ticket_data.seek(0x0)
self.ticket = ticket_data.read() ticket_data_raw = ticket_data.read()
# Reload object's attributes to ensure the raw data and object match. # Return the raw TMD for the data contained in the object.
self.load() return ticket_data_raw
return self.ticket
def get_title_id(self): def get_title_id(self):
"""Gets the Title ID of the ticket's associated title. """Gets the Title ID of the ticket's associated title.

View File

@ -12,11 +12,6 @@ from .wad import WAD
class Title: class Title:
"""Creates a Title object that contains all components of a title, and allows altering them. """Creates a Title object that contains all components of a title, and allows altering them.
Parameters
----------
wad : WAD
A WAD object to load data from.
Attributes Attributes
---------- ----------
tmd : TMD tmd : TMD
@ -26,18 +21,33 @@ class Title:
content: ContentRegion content: ContentRegion
A ContentRegion object containing the title's contents. A ContentRegion object containing the title's contents.
""" """
def __init__(self, wad: WAD): def __init__(self):
self.wad = wad self.wad: WAD = WAD()
self.tmd: TMD self.tmd: TMD = TMD()
self.ticket: Ticket self.ticket: Ticket = Ticket()
self.content: ContentRegion self.content: ContentRegion = ContentRegion()
# Load data from the WAD object, and generate all other objects from the data in it.
def set_wad(self, wad: bytes) -> None:
"""Load existing WAD data into the title and create WAD, TMD, Ticket, and ContentRegion objects based off of it
to allow you to modify that data. Note that this will overwrite any existing data for this title.
Parameters
----------
wad : bytes
The data for the WAD you wish to load.
"""
# Create a new WAD object based on the WAD data provided.
self.wad = WAD()
self.wad.load(wad)
# Load the TMD. # Load the TMD.
self.tmd = TMD(self.wad.get_tmd_data()) self.tmd = TMD()
self.tmd.load(self.wad.get_tmd_data())
# Load the ticket. # Load the ticket.
self.ticket = Ticket(self.wad.get_ticket_data()) self.ticket = Ticket()
self.ticket.load(self.wad.get_ticket_data())
# Load the content. # Load the content.
self.content = ContentRegion(self.wad.get_content_data(), self.tmd.content_records) self.content = ContentRegion()
self.content.load(self.wad.get_content_data(), self.tmd.content_records)
# Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an # Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an
# error because clearly something strange has gone on with the WAD and editing it probably won't work. # error because clearly something strange has gone on with the WAD and editing it probably won't work.
if self.tmd.title_id != self.ticket.title_id_str: if self.tmd.title_id != self.ticket.title_id_str:

View File

@ -12,12 +12,7 @@ from .types import ContentRecord
class TMD: class TMD:
""" """
Creates a TMD object to parse a TMD file to retrieve information about a title. Creates a TMD object that allows for either loading and editing an existing TMD or creating one manually if desired.
Parameters
----------
tmd : bytes
A bytes object containing the contents of a TMD file.
Attributes Attributes
---------- ----------
@ -34,8 +29,7 @@ class TMD:
num_contents : int num_contents : int
The number of contents listed in the TMD. The number of contents listed in the TMD.
""" """
def __init__(self, tmd): def __init__(self):
self.tmd = tmd
self.blob_header: bytes = b'' self.blob_header: bytes = b''
self.sig_type: int = 0 self.sig_type: int = 0
self.sig: bytes = b'' self.sig: bytes = b''
@ -57,17 +51,17 @@ class TMD:
self.num_contents: int = 0 # The number of contents contained in the associated title. self.num_contents: int = 0 # The number of contents contained in the associated title.
self.boot_index: int = 0 self.boot_index: int = 0
self.content_records: List[ContentRecord] = [] self.content_records: List[ContentRecord] = []
# Call load() to set all of the attributes from the raw TMD data provided.
self.load()
def load(self): def load(self, tmd: bytes) -> None:
"""Loads the raw TMD data and sets all attributes of the TMD object. """Loads raw TMD data and sets all attributes of the WAD object. This allows for manipulating an already
existing TMD.
Returns Parameters
------- ----------
none tmd : bytes
The data for the TMD you wish to load.
""" """
with io.BytesIO(self.tmd) as tmd_data: with io.BytesIO(tmd) as tmd_data:
# ==================================================================================== # ====================================================================================
# Parses each of the keys contained in the TMD. # Parses each of the keys contained in the TMD.
# ==================================================================================== # ====================================================================================
@ -214,12 +208,9 @@ class TMD:
content_data.close() content_data.close()
# Set the TMD attribute of the object to the new raw TMD. # Set the TMD attribute of the object to the new raw TMD.
tmd_data.seek(0x0) tmd_data.seek(0x0)
self.tmd = tmd_data.read() tmd_data_raw = tmd_data.read()
# Clear existing lists. # Return the raw TMD for the data contained in the object.
self.content_records = [] return tmd_data_raw
# Reload object's attributes to ensure the raw data and object match.
self.load()
return self.tmd
def get_title_region(self): def get_title_region(self):
"""Gets the region of the TMD's associated title. """Gets the region of the TMD's associated title.

View File

@ -10,15 +10,9 @@ from .shared import align_value, pad_bytes_stream
class WAD: class WAD:
""" """
Creates a WAD object to parse the header of a WAD file and retrieve the data contained in it. Creates a WAD object that allows for either loading and editing an existing WAD or creating a new WAD from raw data.
Parameters
----------
wad : bytes
A bytes object containing the contents of a WAD file.
""" """
def __init__(self, wad): def __init__(self):
self.wad = wad
self.wad_hdr_size: int = 64 self.wad_hdr_size: int = 64
self.wad_type: str = "" self.wad_type: str = ""
self.wad_version: bytes = b'' self.wad_version: bytes = b''
@ -37,17 +31,17 @@ class WAD:
self.wad_tmd_data: bytes = b'' self.wad_tmd_data: bytes = b''
self.wad_content_data: bytes = b'' self.wad_content_data: bytes = b''
self.wad_meta_data: bytes = b'' self.wad_meta_data: bytes = b''
# Call load() to set all of the attributes from the raw WAD data provided.
self.load()
def load(self): def load(self, wad_data) -> None:
"""Loads the raw WAD data and sets all attributes of the WAD object. """Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
existing WAD file.
Returns Parameters
------- ----------
none wad_data : bytes
The data for the WAD you wish to load.
""" """
with io.BytesIO(self.wad) as wad_data: with io.BytesIO(wad_data) as wad_data:
# Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but # Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but
# this tool cannot handle them correctly right now anyway. # this tool cannot handle them correctly right now anyway.
wad_data.seek(0x0) wad_data.seek(0x0)
@ -120,8 +114,8 @@ class WAD:
self.wad_meta_data = wad_data.read(self.wad_meta_size) self.wad_meta_data = wad_data.read(self.wad_meta_size)
def dump(self) -> bytes: def dump(self) -> bytes:
"""Dumps the WAD object back into bytes. This also sets the raw WAD attribute of WAD object to the dumped data, """Dumps the WAD object into the raw WAD file. This allows for creating a WAD file from the data contained in
and triggers load() again to ensure that the raw data and object match. the WAD object.
Returns Returns
------- -------
@ -169,10 +163,9 @@ class WAD:
wad_data = pad_bytes_stream(wad_data) wad_data = pad_bytes_stream(wad_data)
# Seek to the beginning and save this as the WAD data for the object. # Seek to the beginning and save this as the WAD data for the object.
wad_data.seek(0x0) wad_data.seek(0x0)
self.wad = wad_data.read() wad_data_raw = wad_data.read()
# Reload object's attributes to ensure the raw data and object match. # Return the raw WAD file for the data contained in the object.
self.load() return wad_data_raw
return self.wad
def get_wad_type(self): def get_wad_type(self):
"""Gets the type of the WAD. """Gets the type of the WAD.