mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c604c195d2
|
|||
|
0c2e13f18a
|
|||
|
7fed039fdc
|
|||
|
0d306076a2
|
|||
|
a1773b9a02
|
|||
|
7c2f0fb21f
|
|||
|
0edd4fa6bb
|
|||
|
e163d34f0b
|
|||
|
9fb0fdbc17
|
@@ -6,6 +6,7 @@
|
|||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
libWiiPy.archive
|
libWiiPy.archive
|
||||||
|
libWiiPy.nand
|
||||||
libWiiPy.title
|
libWiiPy.title
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
27
docs/source/libWiiPy.nand.md
Normal file
27
docs/source/libWiiPy.nand.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# libWiiPy.nand package
|
||||||
|
|
||||||
|
## Submodules
|
||||||
|
|
||||||
|
### libWiiPy.nand.emunand module
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: libWiiPy.nand.emunand
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### libWiiPy.nand.setting module
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: libWiiPy.nand.setting
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
|
### libWiiPy.nand.sys module
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: libWiiPy.nand.sys
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
@@ -26,14 +26,6 @@
|
|||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
```
|
```
|
||||||
|
|
||||||
### libWiiPy.title.emunand module
|
|
||||||
```{eval-rst}
|
|
||||||
.. automodule:: libWiiPy.title.emunand
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
```
|
|
||||||
|
|
||||||
### libWiipy.title.iospatcher module
|
### libWiipy.title.iospatcher module
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: libWiiPy.title.iospatcher
|
.. automodule:: libWiiPy.title.iospatcher
|
||||||
@@ -50,14 +42,6 @@
|
|||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
```
|
```
|
||||||
|
|
||||||
### libWiiPy.title.sys module
|
|
||||||
```{eval-rst}
|
|
||||||
.. automodule:: libWiiPy.title.sys
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
```
|
|
||||||
|
|
||||||
### libWiiPy.title.ticket module
|
### libWiiPy.title.ticket module
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: libWiiPy.title.ticket
|
.. automodule:: libWiiPy.title.ticket
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "libWiiPy"
|
name = "libWiiPy"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
||||||
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#
|
#
|
||||||
# These are the essential submodules from libWiiPy that you'd probably want imported by default.
|
# These are the essential submodules from libWiiPy that you'd probably want imported by default.
|
||||||
|
|
||||||
__all__ = ["archive", "title"]
|
__all__ = ["archive", "nand", "title"]
|
||||||
|
|
||||||
from . import archive
|
from . import archive
|
||||||
|
from . import nand
|
||||||
from . import title
|
from . import title
|
||||||
|
|||||||
6
src/libWiiPy/nand/__init__.py
Normal file
6
src/libWiiPy/nand/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# "nand/__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
from .emunand import *
|
||||||
|
from .setting import *
|
||||||
|
from .sys import *
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "title/emunand.py" from libWiiPy by NinjaCheetah & Contributors
|
# "nand/emunand.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# Code for handling setting up and modifying a Wii EmuNAND.
|
# Code for handling setting up and modifying a Wii EmuNAND.
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
from .title import Title
|
from ..title.title import Title
|
||||||
from .content import SharedContentMap as _SharedContentMap
|
from ..title.content import SharedContentMap as _SharedContentMap
|
||||||
from .sys import UidSys as _UidSys
|
from .sys import UidSys as _UidSys
|
||||||
|
|
||||||
|
|
||||||
134
src/libWiiPy/nand/setting.py
Normal file
134
src/libWiiPy/nand/setting.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# "nand/setting.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki//title/00000001/00000002/data/setting.txt for information about setting.txt.
|
||||||
|
|
||||||
|
import io
|
||||||
|
from ..shared import _pad_bytes
|
||||||
|
|
||||||
|
|
||||||
|
_key = 0x73B5DBFA
|
||||||
|
|
||||||
|
class SettingTxt:
|
||||||
|
"""
|
||||||
|
A SettingTxt object that allows for decrypting and then parsing a setting.txt file from the Wii.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
area : str
|
||||||
|
The region of the System Menu this file matches with.
|
||||||
|
model : str
|
||||||
|
The model of the console, usually RVL-001 or RVL-101.
|
||||||
|
dvd : int
|
||||||
|
Unknown, might have to do with indicating support for scrapped DVD playback capabilities.
|
||||||
|
mpch : str
|
||||||
|
Unknown, generally accepted value is "0x7FFE".
|
||||||
|
code : str
|
||||||
|
Unknown code, may match with manufacturer code in serial number?
|
||||||
|
serial_number : str
|
||||||
|
Serial number of the console.
|
||||||
|
video : str
|
||||||
|
Video mode, either NTSC or PAL.
|
||||||
|
game : str
|
||||||
|
Another region code, possibly set by the hidden region select channel.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.area: str = ""
|
||||||
|
self.model: str = ""
|
||||||
|
self.dvd: int = 0
|
||||||
|
self.mpch: str = "" # What does this mean, Movie Player Channel? It's also a hex string, it seems.
|
||||||
|
self.code: str = ""
|
||||||
|
self.serial_number: str = ""
|
||||||
|
self.video: str = ""
|
||||||
|
self.game: str = ""
|
||||||
|
|
||||||
|
def load(self, setting_txt: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Loads the raw data of an encrypted setting.txt file and decrypts it to parse its arguments
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
setting_txt : bytes
|
||||||
|
The data of an encrypted setting.txt file.
|
||||||
|
"""
|
||||||
|
with io.BytesIO(setting_txt) as setting_data:
|
||||||
|
global _key # I still don't actually know what *kind* of encryption this is.
|
||||||
|
setting_txt_dec: [int] = []
|
||||||
|
for i in range(0, 256):
|
||||||
|
setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
||||||
|
_key = (_key << 1) | (_key >> 31)
|
||||||
|
setting_txt_dec = bytes(setting_txt_dec)
|
||||||
|
try:
|
||||||
|
setting_str = setting_txt_dec.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
last_newline_pos = setting_txt_dec.rfind(b'\n') # This makes sure we don't try to decode any garbage data.
|
||||||
|
setting_str = setting_txt_dec[:last_newline_pos + 1].decode('utf-8')
|
||||||
|
self.load_decrypted(setting_str)
|
||||||
|
|
||||||
|
def load_decrypted(self, setting_txt: str) -> None:
|
||||||
|
"""
|
||||||
|
Loads the raw data of a decrypted setting.txt file and parses its arguments
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
setting_txt : str
|
||||||
|
The data of a decrypted setting.txt file.
|
||||||
|
"""
|
||||||
|
setting_dict = {}
|
||||||
|
# Iterate over every key in the file to create a dictionary.
|
||||||
|
for line in setting_txt.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line is not None:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
setting_dict[key.strip()] = value.strip()
|
||||||
|
# Load the values from the dictionary into the object.
|
||||||
|
self.area = setting_dict["AREA"]
|
||||||
|
self.model = setting_dict["MODEL"]
|
||||||
|
self.dvd = int(setting_dict["DVD"])
|
||||||
|
self.mpch = setting_dict["MPCH"]
|
||||||
|
self.code = setting_dict["CODE"]
|
||||||
|
self.serial_number = setting_dict["SERNO"]
|
||||||
|
self.video = setting_dict["VIDEO"]
|
||||||
|
self.game = setting_dict["GAME"]
|
||||||
|
|
||||||
|
def dump(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Dumps the SettingTxt object back into an encrypted bytes that the Wii can load.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The setting.txt file as encrypted bytes.
|
||||||
|
"""
|
||||||
|
setting_str = self.dump_decrypted()
|
||||||
|
setting_txt_dec = setting_str.encode()
|
||||||
|
global _key
|
||||||
|
# This could probably be made more efficient somehow.
|
||||||
|
setting_txt_enc: [int] = []
|
||||||
|
with io.BytesIO(setting_txt_dec) as setting_data:
|
||||||
|
for i in range(0, len(setting_txt_dec)):
|
||||||
|
setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
||||||
|
_key = (_key << 1) | (_key >> 31)
|
||||||
|
setting_txt_enc = _pad_bytes(bytes(setting_txt_enc), 256)
|
||||||
|
return setting_txt_enc
|
||||||
|
|
||||||
|
def dump_decrypted(self) -> str:
|
||||||
|
"""
|
||||||
|
Dumps the SettingTxt object into a decrypted string.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The setting.txt file as decrypted text.
|
||||||
|
"""
|
||||||
|
# Write the keys back into a text file that can then be manually edited or re-encrypted.
|
||||||
|
setting_txt = ""
|
||||||
|
setting_txt += f"AREA={self.area}\r\n"
|
||||||
|
setting_txt += f"MODEL={self.model}\r\n"
|
||||||
|
setting_txt += f"DVD={self.dvd}\r\n"
|
||||||
|
setting_txt += f"MPCH={self.mpch}\r\n"
|
||||||
|
setting_txt += f"CODE={self.code}\r\n"
|
||||||
|
setting_txt += f"SERNO={self.serial_number}\r\n"
|
||||||
|
setting_txt += f"VIDEO={self.video}\r\n"
|
||||||
|
setting_txt += f"GAME={self.game}\r\n"
|
||||||
|
return setting_txt
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "title/sys.py" from libWiiPy by NinjaCheetah & Contributors
|
# "nand/sys.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
#
|
#
|
||||||
# See https://wiibrew.org/wiki//sys/uid.sys for information about uid.sys.
|
# See https://wiibrew.org/wiki//sys/uid.sys for information about uid.sys.
|
||||||
@@ -28,7 +28,7 @@ class _UidSysEntry:
|
|||||||
class UidSys:
|
class UidSys:
|
||||||
"""
|
"""
|
||||||
A UidSys object to parse and edit the uid.sys file stored in /sys/ on the Wii's NAND. This file is used to track all
|
A UidSys object to parse and edit the uid.sys file stored in /sys/ on the Wii's NAND. This file is used to track all
|
||||||
the titles installed on the console.
|
the titles that have been launched on a console.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
@@ -3,10 +3,8 @@
|
|||||||
|
|
||||||
from .content import *
|
from .content import *
|
||||||
from .crypto import *
|
from .crypto import *
|
||||||
from .emunand import *
|
|
||||||
from .iospatcher import *
|
from .iospatcher import *
|
||||||
from .nus import *
|
from .nus import *
|
||||||
from .sys import *
|
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
from .title import *
|
from .title import *
|
||||||
from .tmd import *
|
from .tmd import *
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ import io
|
|||||||
import hashlib
|
import hashlib
|
||||||
from typing import List
|
from typing import List
|
||||||
from dataclasses import dataclass as _dataclass
|
from dataclasses import dataclass as _dataclass
|
||||||
|
from enum import IntEnum as _IntEnum
|
||||||
from ..types import _ContentRecord
|
from ..types import _ContentRecord
|
||||||
from ..shared import _pad_bytes, _align_value
|
from ..shared import _pad_bytes, _align_value
|
||||||
from .crypto import decrypt_content, encrypt_content
|
from .crypto import decrypt_content, encrypt_content
|
||||||
|
|
||||||
|
|
||||||
|
class ContentType(_IntEnum):
|
||||||
|
NORMAL = 1
|
||||||
|
HASH_TREE = 3
|
||||||
|
DLC = 16385
|
||||||
|
SHARED = 32769
|
||||||
|
|
||||||
|
|
||||||
class ContentRegion:
|
class ContentRegion:
|
||||||
"""
|
"""
|
||||||
A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the
|
A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the
|
||||||
@@ -150,16 +158,11 @@ class ContentRegion:
|
|||||||
bytes
|
bytes
|
||||||
The encrypted content listed in the content record.
|
The encrypted content listed in the content record.
|
||||||
"""
|
"""
|
||||||
# Get a list of the current Content IDs, so we can make sure the target one exists.
|
try:
|
||||||
content_ids = []
|
content_index = self.get_index_from_cid(cid)
|
||||||
for record in self.content_records:
|
except ValueError:
|
||||||
content_ids.append(record.content_id)
|
raise ValueError(f"You are trying to get a content with Content ID {cid}, "
|
||||||
if cid not in content_ids:
|
f"but no content with that ID exists!")
|
||||||
raise ValueError("You are trying to get a content with Content ID " + str(cid) + ", but no content with "
|
|
||||||
"that ID exists!")
|
|
||||||
# Get the content index associated with the CID we now know exists.
|
|
||||||
target_index = content_ids.index(cid)
|
|
||||||
content_index = self.content_records[target_index].index
|
|
||||||
content_enc = self.get_enc_content_by_index(content_index)
|
content_enc = self.get_enc_content_by_index(content_index)
|
||||||
return content_enc
|
return content_enc
|
||||||
|
|
||||||
@@ -238,16 +241,11 @@ class ContentRegion:
|
|||||||
bytes
|
bytes
|
||||||
The decrypted content listed in the content record.
|
The decrypted content listed in the content record.
|
||||||
"""
|
"""
|
||||||
# Get a list of the current Content IDs, so we can make sure the target one exists.
|
try:
|
||||||
content_ids = []
|
content_index = self.get_index_from_cid(cid)
|
||||||
for record in self.content_records:
|
except ValueError:
|
||||||
content_ids.append(record.content_id)
|
raise ValueError(f"You are trying to get a content with Content ID {cid}, "
|
||||||
if cid not in content_ids:
|
f"but no content with that ID exists!")
|
||||||
raise ValueError("You are trying to get a content with Content ID " + str(cid) + ", but no content with "
|
|
||||||
"that ID exists!")
|
|
||||||
# Get the content index associated with the CID we now know exists.
|
|
||||||
target_index = content_ids.index(cid)
|
|
||||||
content_index = self.content_records[target_index].index
|
|
||||||
content_dec = self.get_content_by_index(content_index, title_key, skip_hash)
|
content_dec = self.get_content_by_index(content_index, title_key, skip_hash)
|
||||||
return content_dec
|
return content_dec
|
||||||
|
|
||||||
@@ -273,6 +271,32 @@ class ContentRegion:
|
|||||||
dec_contents.append(self.get_content_by_index(content, title_key, skip_hash))
|
dec_contents.append(self.get_content_by_index(content, title_key, skip_hash))
|
||||||
return dec_contents
|
return dec_contents
|
||||||
|
|
||||||
|
def get_index_from_cid(self, cid: int) -> int:
|
||||||
|
"""
|
||||||
|
Gets the content index of a content by its Content ID. The returned index is the value tied to each content and
|
||||||
|
used as the IV for encryption, rather than the literal index in the array of content, because sometimes the
|
||||||
|
contents end up out of order in a WAD while still retaining the original indices.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cid : int
|
||||||
|
The Content ID to get the index of.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The content index.
|
||||||
|
"""
|
||||||
|
# Get a list of the current Content IDs, so we can make sure the target one exists.
|
||||||
|
content_ids = []
|
||||||
|
for record in self.content_records:
|
||||||
|
content_ids.append(record.content_id)
|
||||||
|
if cid not in content_ids:
|
||||||
|
raise ValueError("The specified Content ID does not exist!")
|
||||||
|
literal_index = content_ids.index(cid)
|
||||||
|
target_index = self.content_records[literal_index].index
|
||||||
|
return target_index
|
||||||
|
|
||||||
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
||||||
content_hash: bytes) -> None:
|
content_hash: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -286,7 +310,7 @@ class ContentRegion:
|
|||||||
cid : int
|
cid : int
|
||||||
The Content ID to assign the new content in the content record.
|
The Content ID to assign the new content in the content record.
|
||||||
index : int
|
index : int
|
||||||
The index to place the new content at.
|
The index used when encrypting the new content.
|
||||||
content_type : int
|
content_type : int
|
||||||
The type of the new content.
|
The type of the new content.
|
||||||
content_size : int
|
content_size : int
|
||||||
@@ -304,10 +328,11 @@ class ContentRegion:
|
|||||||
self.content_list.append(enc_content)
|
self.content_list.append(enc_content)
|
||||||
self.content_records.append(_ContentRecord(cid, index, content_type, content_size, content_hash))
|
self.content_records.append(_ContentRecord(cid, index, content_type, content_size, content_hash))
|
||||||
|
|
||||||
def add_content(self, dec_content: bytes, cid: int, index: int, content_type: int, title_key: bytes) -> None:
|
def add_content(self, dec_content: bytes, cid: int, content_type: int, title_key: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a new decrypted content to the ContentRegion, and adds the provided Content ID, index, content type,
|
Adds a new decrypted content to the end of the ContentRegion, and adds the provided Content ID, content type,
|
||||||
content size, and content hash to a new record in the ContentRecord list.
|
content size, and content hash to a new record in the ContentRecord list. The index will be automatically
|
||||||
|
assigned by incrementing the current highest index in the records.
|
||||||
|
|
||||||
This first gets the content hash and size from the provided data, and then encrypts the content with the
|
This first gets the content hash and size from the provided data, and then encrypts the content with the
|
||||||
provided Title Key before adding it to the ContentRegion.
|
provided Title Key before adding it to the ContentRegion.
|
||||||
@@ -318,13 +343,16 @@ class ContentRegion:
|
|||||||
The new decrypted content to add.
|
The new decrypted content to add.
|
||||||
cid : int
|
cid : int
|
||||||
The Content ID to assign the new content in the content record.
|
The Content ID to assign the new content in the content record.
|
||||||
index : int
|
|
||||||
The index to place the new content at.
|
|
||||||
content_type : int
|
content_type : int
|
||||||
The type of the new content.
|
The type of the new content.
|
||||||
title_key : bytes
|
title_key : bytes
|
||||||
The Title Key that matches the other content in the ContentRegion.
|
The Title Key that matches the other content in the ContentRegion.
|
||||||
"""
|
"""
|
||||||
|
# Find the current highest content index and increment it for this content.
|
||||||
|
content_indices = []
|
||||||
|
for record in self.content_records:
|
||||||
|
content_indices.append(record.index)
|
||||||
|
index = max(content_indices) + 1
|
||||||
content_size = len(dec_content)
|
content_size = len(dec_content)
|
||||||
content_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
|
content_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
|
||||||
enc_content = encrypt_content(dec_content, title_key, index)
|
enc_content = encrypt_content(dec_content, title_key, index)
|
||||||
@@ -486,6 +514,49 @@ class ContentRegion:
|
|||||||
enc_content = encrypt_content(dec_content, title_key, index)
|
enc_content = encrypt_content(dec_content, title_key, index)
|
||||||
self.content_list[target_index] = enc_content
|
self.content_list[target_index] = enc_content
|
||||||
|
|
||||||
|
def remove_content_by_index(self, index: int) -> None:
|
||||||
|
"""
|
||||||
|
Removes the content at the specified index from the ContentRegion and content records.
|
||||||
|
|
||||||
|
This will allow gaps to be left in content indices, however this should not cause any issues.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : int
|
||||||
|
The index of the content you want to remove.
|
||||||
|
"""
|
||||||
|
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way
|
||||||
|
# ensures we can find the target, even if the highest content index is greater than the highest literal index.
|
||||||
|
current_indices = []
|
||||||
|
for record in self.content_records:
|
||||||
|
current_indices.append(record.index)
|
||||||
|
if index not in current_indices:
|
||||||
|
raise ValueError("You are trying to remove the content at index " + str(index) + ", but no content with "
|
||||||
|
"that index currently exists!")
|
||||||
|
# This is the literal index in the list of content/content records that we're going to change.
|
||||||
|
target_index = current_indices.index(index)
|
||||||
|
# Delete the target index from both the content list and content records.
|
||||||
|
self.content_list.pop(target_index)
|
||||||
|
self.content_records.pop(target_index)
|
||||||
|
|
||||||
|
def remove_content_by_cid(self, cid: int) -> None:
|
||||||
|
"""
|
||||||
|
Removes the content with the specified Content ID from the ContentRegion and content records.
|
||||||
|
|
||||||
|
This will allow gaps to be left in content indices, however this should not cause any issues.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cid : int
|
||||||
|
The Content ID of the content you want to remove.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content_index = self.get_index_from_cid(cid)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"You are trying to remove content with Content ID {cid}, "
|
||||||
|
f"but no content with that ID exists!")
|
||||||
|
self.remove_content_by_index(content_index)
|
||||||
|
|
||||||
|
|
||||||
@_dataclass
|
@_dataclass
|
||||||
class _SharedContentRecord:
|
class _SharedContentRecord:
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ class Title:
|
|||||||
if self.tmd.title_id == "0000000100000001":
|
if self.tmd.title_id == "0000000100000001":
|
||||||
self.wad.wad_type = "ib"
|
self.wad.wad_type = "ib"
|
||||||
# Dump the TMD and set it in the WAD.
|
# Dump the TMD and set it in the WAD.
|
||||||
|
# This requires updating the content records and number of contents in the TMD first.
|
||||||
self.tmd.content_records = self.content.content_records
|
self.tmd.content_records = self.content.content_records
|
||||||
|
self.tmd.num_contents = len(self.content.content_records)
|
||||||
self.wad.set_tmd_data(self.tmd.dump())
|
self.wad.set_tmd_data(self.tmd.dump())
|
||||||
# Dump the Ticket and set it in the WAD.
|
# Dump the Ticket and set it in the WAD.
|
||||||
self.wad.set_ticket_data(self.ticket.dump())
|
self.wad.set_ticket_data(self.ticket.dump())
|
||||||
@@ -190,9 +192,15 @@ class Title:
|
|||||||
dec_content = self.content.get_content_by_cid(cid, self.ticket.get_title_key(), skip_hash)
|
dec_content = self.content.get_content_by_cid(cid, self.ticket.get_title_key(), skip_hash)
|
||||||
return dec_content
|
return dec_content
|
||||||
|
|
||||||
def get_title_size(self) -> int:
|
def get_title_size(self, absolute=False) -> int:
|
||||||
"""
|
"""
|
||||||
Gets the installed size of the title, including the TMD and Ticket, in bytes.
|
Gets the installed size of the title, including the TMD and Ticket, in bytes. The "absolute" option determines
|
||||||
|
whether shared content sizes should be included in the total size or not. This option defaults to False.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
absolute : bool, optional
|
||||||
|
Whether shared contents should be included in the total size or not. Defaults to False.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -207,30 +215,90 @@ class Title:
|
|||||||
# For contents, get their sizes from the content records, because they store the intended sizes of the decrypted
|
# For contents, get their sizes from the content records, because they store the intended sizes of the decrypted
|
||||||
# contents, which are usually different from the encrypted sizes.
|
# contents, which are usually different from the encrypted sizes.
|
||||||
for record in self.content.content_records:
|
for record in self.content.content_records:
|
||||||
|
if record.content_type == 32769:
|
||||||
|
if absolute:
|
||||||
|
title_size += record.content_size
|
||||||
|
else:
|
||||||
title_size += record.content_size
|
title_size += record.content_size
|
||||||
return title_size
|
return title_size
|
||||||
|
|
||||||
def get_title_size_blocks(self) -> int:
|
def get_title_size_blocks(self, absolute=False) -> int:
|
||||||
"""
|
"""
|
||||||
Gets the installed size of the title, including the TMD and Ticket, in the Wii's displayed "blocks" format.
|
Gets the installed size of the title, including the TMD and Ticket, in the Wii's displayed "blocks" format. The
|
||||||
|
"absolute" option determines whether shared content sizes should be included in the total size or not. This
|
||||||
|
option defaults to False.
|
||||||
|
|
||||||
1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used.
|
1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
absolute : bool, optional
|
||||||
|
Whether shared contents should be included in the total size or not. Defaults to False.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
int
|
int
|
||||||
The installed size of the title, in blocks.
|
The installed size of the title, in blocks.
|
||||||
"""
|
"""
|
||||||
title_size_bytes = self.get_title_size()
|
title_size_bytes = self.get_title_size(absolute)
|
||||||
blocks = math.ceil(title_size_bytes / 131072)
|
blocks = math.ceil(title_size_bytes / 131072)
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
||||||
|
content_hash: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Adds a new encrypted content to the ContentRegion, and adds the provided Content ID, index, content type,
|
||||||
|
content size, and content hash to a new record in the ContentRecord list.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
enc_content : bytes
|
||||||
|
The new encrypted content to add.
|
||||||
|
cid : int
|
||||||
|
The Content ID to assign the new content in the content record.
|
||||||
|
index : int
|
||||||
|
The index used when encrypting the new content.
|
||||||
|
content_type : int
|
||||||
|
The type of the new content.
|
||||||
|
content_size : int
|
||||||
|
The size of the new encrypted content when decrypted.
|
||||||
|
content_hash : bytes
|
||||||
|
The hash of the new encrypted content when decrypted.
|
||||||
|
"""
|
||||||
|
# Add the encrypted content.
|
||||||
|
self.content.add_enc_content(enc_content, cid, index, content_type, content_size, content_hash)
|
||||||
|
# Update the TMD to match.
|
||||||
|
self.tmd.content_records = self.content.content_records
|
||||||
|
|
||||||
|
def add_content(self, dec_content: bytes, cid: int, content_type: int) -> None:
|
||||||
|
"""
|
||||||
|
Adds a new decrypted content to the end of the ContentRegion, and adds the provided Content ID, content type,
|
||||||
|
content size, and content hash to a new record in the ContentRecord list. The index will be automatically
|
||||||
|
assigned by incrementing the current highest index in the records.
|
||||||
|
|
||||||
|
This first gets the content hash and size from the provided data, and then encrypts the content with the
|
||||||
|
Title Key before adding it to the ContentRegion.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dec_content : bytes
|
||||||
|
The new decrypted content to add.
|
||||||
|
cid : int
|
||||||
|
The Content ID to assign the new content in the content record.
|
||||||
|
content_type : int
|
||||||
|
The type of the new content.
|
||||||
|
"""
|
||||||
|
# Add the decrypted content.
|
||||||
|
self.content.add_content(dec_content, cid, content_type, self.ticket.get_title_key())
|
||||||
|
# Update the TMD to match.
|
||||||
|
self.tmd.content_records = self.content.content_records
|
||||||
|
|
||||||
def set_enc_content(self, enc_content: bytes, index: int, content_size: int, content_hash: bytes, cid: int = None,
|
def set_enc_content(self, enc_content: bytes, index: int, content_size: int, content_hash: bytes, cid: int = None,
|
||||||
content_type: int = None) -> None:
|
content_type: int = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the content at the provided content index to the provided new encrypted content. The provided hash and
|
Sets the content at the provided content index to the provided new encrypted content. The provided hash and
|
||||||
content size are set in the corresponding content record. A new Content ID or content type can also be
|
content size are set in the corresponding content record. A new Content ID or content type can also be
|
||||||
specified, but if it isn't than the current values are preserved.
|
specified, but if it isn't then the current values are preserved.
|
||||||
|
|
||||||
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
|
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
|
||||||
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
|
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
|
||||||
@@ -262,7 +330,7 @@ class Title:
|
|||||||
"""
|
"""
|
||||||
Sets the content at the provided content index to the provided new decrypted content. The hash and content size
|
Sets the content at the provided content index to the provided new decrypted content. The hash and content size
|
||||||
of this content will be generated and then set in the corresponding content record. A new Content ID or content
|
of this content will be generated and then set in the corresponding content record. A new Content ID or content
|
||||||
type can also be specified, but if it isn't than the current values are preserved.
|
type can also be specified, but if it isn't then the current values are preserved.
|
||||||
|
|
||||||
This also updates the content records in the TMD after the content is set.
|
This also updates the content records in the TMD after the content is set.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import binascii
|
|||||||
import hashlib
|
import hashlib
|
||||||
import struct
|
import struct
|
||||||
from typing import List
|
from typing import List
|
||||||
from enum import IntEnum
|
from enum import IntEnum as _IntEnum
|
||||||
from ..types import _ContentRecord
|
from ..types import _ContentRecord
|
||||||
from ..shared import _bitmask
|
from ..shared import _bitmask
|
||||||
from .util import title_ver_dec_to_standard, title_ver_standard_to_dec
|
from .util import title_ver_dec_to_standard, title_ver_standard_to_dec
|
||||||
@@ -37,7 +37,7 @@ class TMD:
|
|||||||
self.blob_header: bytes = b''
|
self.blob_header: bytes = b''
|
||||||
self.signature_type: int = 0
|
self.signature_type: int = 0
|
||||||
self.signature: bytes = b''
|
self.signature: bytes = b''
|
||||||
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
|
self.signature_issuer: str = "" # Follows the format "Root-CA%08x-CP%08x"
|
||||||
self.tmd_version: int = 0 # This seems to always be 0 no matter what?
|
self.tmd_version: int = 0 # This seems to always be 0 no matter what?
|
||||||
self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
|
self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
|
||||||
self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
|
self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
|
||||||
@@ -45,7 +45,7 @@ class TMD:
|
|||||||
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
|
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
|
||||||
self.ios_version: int = 0 # The IOS version the associated title runs on.
|
self.ios_version: int = 0 # The IOS version the associated title runs on.
|
||||||
self.title_id: str = "" # The Title ID of the associated title.
|
self.title_id: str = "" # The Title ID of the associated title.
|
||||||
self.title_type: str = "" # The type of the associated title.
|
self.title_type: bytes = b'' # The type of the associated title. Should always be 00000001 in a Wii TMD.
|
||||||
self.group_id: int = 0 # The ID of the publisher of the associated title.
|
self.group_id: int = 0 # The ID of the publisher of the associated title.
|
||||||
self.region: int = 0 # The ID of the region of the associated title.
|
self.region: int = 0 # The ID of the region of the associated title.
|
||||||
self.ratings: bytes = b'' # The parental controls rating of the associated title.
|
self.ratings: bytes = b'' # The parental controls rating of the associated title.
|
||||||
@@ -82,7 +82,7 @@ class TMD:
|
|||||||
self.signature = tmd_data.read(256)
|
self.signature = tmd_data.read(256)
|
||||||
# Signing certificate issuer.
|
# Signing certificate issuer.
|
||||||
tmd_data.seek(0x140)
|
tmd_data.seek(0x140)
|
||||||
self.issuer = tmd_data.read(64)
|
self.signature_issuer = str(tmd_data.read(64).decode())
|
||||||
# TMD version, seems to usually be 0, but I've seen references to other numbers.
|
# TMD version, seems to usually be 0, but I've seen references to other numbers.
|
||||||
tmd_data.seek(0x180)
|
tmd_data.seek(0x180)
|
||||||
self.tmd_version = int.from_bytes(tmd_data.read(1))
|
self.tmd_version = int.from_bytes(tmd_data.read(1))
|
||||||
@@ -107,11 +107,10 @@ class TMD:
|
|||||||
title_id_bin = tmd_data.read(8)
|
title_id_bin = tmd_data.read(8)
|
||||||
title_id_hex = binascii.hexlify(title_id_bin)
|
title_id_hex = binascii.hexlify(title_id_bin)
|
||||||
self.title_id = str(title_id_hex.decode())
|
self.title_id = str(title_id_hex.decode())
|
||||||
# Type of content.
|
# Type of the title. This is an internal property used to show if this title is for the ill-fated
|
||||||
|
# NetCard (0), or the Wii (1), and is therefore always 1 for Wii TMDs.
|
||||||
tmd_data.seek(0x194)
|
tmd_data.seek(0x194)
|
||||||
content_type_bin = tmd_data.read(4)
|
self.title_type = tmd_data.read(4)
|
||||||
content_type_hex = binascii.hexlify(content_type_bin)
|
|
||||||
self.title_type = str(content_type_hex.decode())
|
|
||||||
# Publisher of the title.
|
# Publisher of the title.
|
||||||
tmd_data.seek(0x198)
|
tmd_data.seek(0x198)
|
||||||
self.group_id = int.from_bytes(tmd_data.read(2))
|
self.group_id = int.from_bytes(tmd_data.read(2))
|
||||||
@@ -175,7 +174,7 @@ class TMD:
|
|||||||
# Padding to 64 bytes.
|
# Padding to 64 bytes.
|
||||||
tmd_data += b'\x00' * 60
|
tmd_data += b'\x00' * 60
|
||||||
# Signing certificate issuer.
|
# Signing certificate issuer.
|
||||||
tmd_data += self.issuer
|
tmd_data += str.encode(self.signature_issuer)
|
||||||
# TMD version.
|
# TMD version.
|
||||||
tmd_data += int.to_bytes(self.tmd_version, 1)
|
tmd_data += int.to_bytes(self.tmd_version, 1)
|
||||||
# Certificate Authority CRL version.
|
# Certificate Authority CRL version.
|
||||||
@@ -188,8 +187,8 @@ class TMD:
|
|||||||
tmd_data += binascii.unhexlify(self.ios_tid)
|
tmd_data += binascii.unhexlify(self.ios_tid)
|
||||||
# Title's Title ID.
|
# Title's Title ID.
|
||||||
tmd_data += binascii.unhexlify(self.title_id)
|
tmd_data += binascii.unhexlify(self.title_id)
|
||||||
# Content type.
|
# Title type.
|
||||||
tmd_data += binascii.unhexlify(self.title_type)
|
tmd_data += self.title_type
|
||||||
# Group ID.
|
# Group ID.
|
||||||
tmd_data += int.to_bytes(self.group_id, 2)
|
tmd_data += int.to_bytes(self.group_id, 2)
|
||||||
# 2 bytes of zero for reasons.
|
# 2 bytes of zero for reasons.
|
||||||
@@ -280,10 +279,11 @@ class TMD:
|
|||||||
|
|
||||||
def get_title_region(self) -> str:
|
def get_title_region(self) -> str:
|
||||||
"""
|
"""
|
||||||
Gets the region of the TMD's associated title.
|
Gets the system region specified in the TMD. This is not necessarily the true region of the title, but is the
|
||||||
|
hardware region that this title is designed and allowed to be run on.
|
||||||
|
|
||||||
Can be one of several possible values:
|
Can be one of several possible values:
|
||||||
'Japan', 'North America', 'Europe', 'World', or 'Korea'.
|
'JPN', 'USA', 'EUR', 'None', or 'KOR'.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -292,19 +292,19 @@ class TMD:
|
|||||||
"""
|
"""
|
||||||
match self.region:
|
match self.region:
|
||||||
case 0:
|
case 0:
|
||||||
return "Japan"
|
return "JPN"
|
||||||
case 1:
|
case 1:
|
||||||
return "North America"
|
return "USA"
|
||||||
case 2:
|
case 2:
|
||||||
return "Europe"
|
return "EUR"
|
||||||
case 3:
|
case 3:
|
||||||
return "World"
|
return "None"
|
||||||
case 4:
|
case 4:
|
||||||
return "Korea"
|
return "KOR"
|
||||||
|
|
||||||
def get_title_type(self) -> str:
|
def get_title_type(self) -> str:
|
||||||
"""
|
"""
|
||||||
Gets the type of the TMD's associated title.
|
Gets the type of the title this TMD describes. The title_type field is not related to these types.
|
||||||
|
|
||||||
Can be one of several possible values:
|
Can be one of several possible values:
|
||||||
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
|
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
|
||||||
@@ -314,7 +314,7 @@ class TMD:
|
|||||||
str
|
str
|
||||||
The type of the title.
|
The type of the title.
|
||||||
"""
|
"""
|
||||||
match self.title_type:
|
match self.title_id[:8]:
|
||||||
case '00000001':
|
case '00000001':
|
||||||
return "System"
|
return "System"
|
||||||
case '00010000':
|
case '00010000':
|
||||||
@@ -390,7 +390,7 @@ class TMD:
|
|||||||
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
|
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
|
||||||
"' contents but index was '" + str(record) + "'!")
|
"' contents but index was '" + str(record) + "'!")
|
||||||
|
|
||||||
class AccessFlags(IntEnum):
|
class AccessFlags(_IntEnum):
|
||||||
AHB = 0
|
AHB = 0
|
||||||
DVD_VIDEO = 1
|
DVD_VIDEO = 1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user