mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2025-04-25 21:01:01 -04:00
Improve WAD handling, fixes IOS WADs made with other tools not extracting
The way content sizes are handled has been adjusted to allow IOS WADs (which have their content structured a bit differently) made via other tools to be extracted. Writing out WADs has also been changed so that the content size in the header now matches the output of older tools.
This commit is contained in:
parent
9bfb44771e
commit
1f731bbc81
@ -18,14 +18,14 @@ In libWiiPy, a TMD is represented by a `TMD()` object, which is part of the `tmd
|
||||
## Ticket
|
||||
<project:#libWiiPy.title.ticket>
|
||||
|
||||
A **Ticket** primarily contains the encrypted Title Key for a title, as well as the information required to decrypt that key. They come in two forms: common tickets, which are freely available from the Nintendo Update Servers, and personalized tickets, which are issued to your console specifically by the Wii Shop Channel (or at least they were before it closed, excluding the free titles still available).
|
||||
A **Ticket** primarily contains the encrypted Title Key for a title, as well as the information required to decrypt that key. They come in two forms: common tickets, which are freely available from the Nintendo Update Servers (NUS), and personalized tickets, which are issued to your console specifically by the Wii Shop Channel (or at least they were before it closed, excluding the free titles still available).
|
||||
|
||||
In libWiiPy, a Ticket is represented by a `Ticket()` object, which is part of the `ticket` module in the `title` subpackage, and is imported automatically.
|
||||
|
||||
## Content
|
||||
<project:#libWiiPy.title.content>
|
||||
|
||||
**Contents** are the files in a title that contain the actual data, whether that be the main executable or resources required by it. They're usually stored encrypted in a WAD file or on the Nintendo Update Servers, until they are decrypted during installation to a console. The Title Key stored in the Ticket is required to decrypt the contents of a title. Each content has a matching record with its index and Content ID, as well as the SHA-1 hash of its decrypted data. These records are stored in the TMD.
|
||||
**Contents** are the files in a title that contain the actual data, whether that be the main executable or resources required by it. They're usually stored encrypted in a WAD file or on the NUS, until they are decrypted during installation to a console. The Title Key stored in the Ticket is required to decrypt the contents of a title. Each content has a matching record with its index and Content ID, as well as the SHA-1 hash of its decrypted data. These records are stored in the TMD.
|
||||
|
||||
In libWiiPy, contents are represented by a `ContentRegion()` object, which is part of the `content` module in the `title` subpackge, and is imported automatically. A content record is represented by its own `ContentRecord()` object, which is a private class designed to only be used by other modules.
|
||||
|
||||
|
@ -4,10 +4,10 @@
|
||||
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
||||
|
||||
import io
|
||||
import sys
|
||||
import hashlib
|
||||
from typing import List
|
||||
from ..types import _ContentRecord
|
||||
from ..shared import _pad_bytes
|
||||
from .crypto import decrypt_content, encrypt_content
|
||||
|
||||
|
||||
@ -43,9 +43,9 @@ class ContentRegion:
|
||||
A list of ContentRecord objects detailing all contents contained in the region.
|
||||
"""
|
||||
self.content_records = content_records
|
||||
# Get the total size of the content region.
|
||||
self.content_region_size = len(content_region)
|
||||
with io.BytesIO(content_region) as content_region_data:
|
||||
# Get the total size of the content region.
|
||||
self.content_region_size = sys.getsizeof(content_region_data)
|
||||
self.num_contents = len(self.content_records)
|
||||
# Calculate the offsets of each content in the content region.
|
||||
# Content is aligned to 16 bytes, however a new content won't start until the next multiple of 64 bytes.
|
||||
@ -56,7 +56,7 @@ class ContentRegion:
|
||||
start_offset += 64 - (content.content_size % 64)
|
||||
self.content_start_offsets.append(start_offset)
|
||||
# Build a list of all the encrypted content data.
|
||||
for content in range(len(self.content_start_offsets)):
|
||||
for content in range(self.num_contents):
|
||||
# Seek to the start of the content based on the list of offsets.
|
||||
content_region_data.seek(self.content_start_offsets[content])
|
||||
# Calculate the number of bytes we need to read by adding bytes up the nearest multiple of 16 if needed.
|
||||
@ -68,7 +68,7 @@ class ContentRegion:
|
||||
content_enc = content_region_data.read(bytes_to_read)
|
||||
self.content_list.append(content_enc)
|
||||
|
||||
def dump(self) -> bytes:
|
||||
def dump(self) -> tuple[bytes, int]:
|
||||
"""
|
||||
Takes the list of contents and assembles them back into one content region. Returns this content region as a
|
||||
bytes object and sets the raw content region variable to this result, then calls load() again to make sure the
|
||||
@ -77,19 +77,28 @@ class ContentRegion:
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The full WAD file as bytes.
|
||||
The full ContentRegion as bytes, including padding between content.
|
||||
int
|
||||
The size of the ContentRegion, including padding.
|
||||
"""
|
||||
content_region_data = b''
|
||||
for content in self.content_list:
|
||||
# If this isn't the first content, pad the whole region to 64 bytes before the next one.
|
||||
if content_region_data is not b'':
|
||||
content_region_data = _pad_bytes(content_region_data, 64)
|
||||
# Calculate padding after this content before the next one.
|
||||
padding_bytes = 0
|
||||
if (len(content) % 64) != 0:
|
||||
padding_bytes = 64 - (len(content) % 64)
|
||||
if (len(content) % 16) != 0:
|
||||
padding_bytes = 16 - (len(content) % 16)
|
||||
# Write content data, then the padding afterward if necessary.
|
||||
content_region_data += content
|
||||
if padding_bytes > 0:
|
||||
content_region_data += b'\x00' * padding_bytes
|
||||
return content_region_data
|
||||
# Calculate the size of the whole content region.
|
||||
content_region_size = 0
|
||||
for record in range(len(self.content_records)):
|
||||
content_region_size += self.content_records[record].content_size
|
||||
return content_region_data, content_region_size
|
||||
|
||||
def get_enc_content_by_index(self, index: int) -> bytes:
|
||||
"""
|
||||
@ -173,7 +182,7 @@ class ContentRegion:
|
||||
# Compare the hash and throw a ValueError if the hash doesn't match.
|
||||
if content_dec_hash != content_record_hash:
|
||||
raise ValueError("Content hash did not match the expected hash in its record! The incorrect Title Key may "
|
||||
"have been used!.\n"
|
||||
"have been used!\n"
|
||||
"Expected hash is: {}\n".format(content_record_hash) +
|
||||
"Actual hash is: {}".format(content_dec_hash))
|
||||
return content_dec
|
||||
|
@ -78,7 +78,8 @@ class Title:
|
||||
# Dump the Ticket and set it in the WAD.
|
||||
self.wad.set_ticket_data(self.ticket.dump())
|
||||
# Dump the ContentRegion and set it in the WAD.
|
||||
self.wad.set_content_data(self.content.dump())
|
||||
content_data, content_size = self.content.dump()
|
||||
self.wad.set_content_data(content_data, content_size)
|
||||
return self.wad.dump()
|
||||
|
||||
def load_tmd(self, tmd: bytes) -> None:
|
||||
|
@ -91,9 +91,11 @@ class WAD:
|
||||
# WAD TMD size.
|
||||
wad_data.seek(0x14)
|
||||
self.wad_tmd_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||
# WAD content size.
|
||||
# WAD content size. This needs to be rounded now, because with some titles (primarily IOS?), there can be
|
||||
# extra bytes past the listed end of the content that is needed for decryption.
|
||||
wad_data.seek(0x18)
|
||||
self.wad_content_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||
self.wad_content_size = _align_value(self.wad_content_size, 16)
|
||||
# Time/build stamp for the title contained in the WAD.
|
||||
wad_data.seek(0x1c)
|
||||
self.wad_meta_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||
@ -309,7 +311,7 @@ class WAD:
|
||||
# Calculate the size of the new Ticket data.
|
||||
self.wad_tik_size = len(tik_data)
|
||||
|
||||
def set_content_data(self, content_data) -> None:
|
||||
def set_content_data(self, content_data, size: int = None) -> None:
|
||||
"""
|
||||
Sets the content data of the WAD. Also calculates the new size.
|
||||
|
||||
@ -317,10 +319,15 @@ class WAD:
|
||||
----------
|
||||
content_data : bytes
|
||||
The new content data.
|
||||
size : int, option
|
||||
The size of the new content data.
|
||||
"""
|
||||
self.wad_content_data = content_data
|
||||
# Calculate the size of the new content data.
|
||||
self.wad_content_size = len(content_data)
|
||||
# Calculate the size of the new content data, if one wasn't supplied.
|
||||
if size is None:
|
||||
self.wad_content_size = len(content_data)
|
||||
else:
|
||||
self.wad_content_size = size
|
||||
|
||||
def set_meta_data(self, meta_data) -> None:
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user