mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
11 Commits
unfinished
...
6d38df9133
| Author | SHA1 | Date | |
|---|---|---|---|
|
6d38df9133
|
|||
|
2ca2ff1f44
|
|||
|
79ab33c18a
|
|||
|
e06bb39f4c
|
|||
|
8269a0db98
|
|||
|
8adbef26b1
|
|||
|
5dde9f7835
|
|||
|
93abad1f31
|
|||
|
9eabf2caee
|
|||
|
5ae867197b
|
|||
| 6552dc5fa8 |
11
README.md
11
README.md
@@ -2,7 +2,7 @@
|
|||||||
# libWiiPy
|
# libWiiPy
|
||||||
libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
||||||
|
|
||||||
libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), which was originally created by `Leathl` and is now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee). If you're looking for a Wii library that isn't in Python, then go check it out!
|
libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), which was originally created by `Leathl` and is now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee).
|
||||||
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
@@ -10,14 +10,18 @@ This list will expand as libWiiPy is developed, but these features are currently
|
|||||||
- TMD and Ticket parsing/editing (`.tmd`, `.tik`)
|
- TMD and Ticket parsing/editing (`.tmd`, `.tik`)
|
||||||
- Title parsing/editing, including content encryption/decryption (both retail and development)
|
- Title parsing/editing, including content encryption/decryption (both retail and development)
|
||||||
- WAD file parsing/editing (`.wad`)
|
- WAD file parsing/editing (`.wad`)
|
||||||
- Downloading titles from the NUS
|
- Downloading titles and their components from the NUS
|
||||||
|
- Certificate, TMD, and Ticket signature verification
|
||||||
- Packing and unpacking U8 archives (`.app`, `.arc`)
|
- Packing and unpacking U8 archives (`.app`, `.arc`)
|
||||||
- Decompressing ASH files (`.ash`, both the standard variants and the variants found in My Pokémon Ranch)
|
- Decompressing ASH files (`.ash`, both the standard variants and the variants found in My Pokémon Ranch)
|
||||||
|
- Compressing/Decompressing LZ77-compressed files
|
||||||
- IOS patching
|
- IOS patching
|
||||||
- NAND-related functionality:
|
- NAND-related functionality:
|
||||||
- EmuNAND title management (currently requires an existing EmuNAND)
|
- EmuNAND title management (currently requires an existing EmuNAND)
|
||||||
- `content.map` parsing/editing
|
- `content.map` parsing/editing
|
||||||
|
- `setting.txt` parsing/editing
|
||||||
- `uid.sys` parsing/editing
|
- `uid.sys` parsing/editing
|
||||||
|
- Limited channel banner parsing/editing
|
||||||
- Assorted miscellaneous features used to make the other core features possible
|
- Assorted miscellaneous features used to make the other core features possible
|
||||||
|
|
||||||
For a more detailed look at what's available in libWiiPy, check out our [API docs](https://ninjacheetah.github.io/libWiiPy).
|
For a more detailed look at what's available in libWiiPy, check out our [API docs](https://ninjacheetah.github.io/libWiiPy).
|
||||||
@@ -68,6 +72,3 @@ Thank you to all of the contributors to the documentation on the WiiBrew pages t
|
|||||||
|
|
||||||
### One additional special thanks to [@DamiDoop](https://github.com/DamiDoop)!
|
### One additional special thanks to [@DamiDoop](https://github.com/DamiDoop)!
|
||||||
She made the very cool banner you can see at the top of this README, and has also helped greatly with my sanity throughout debugging this library.
|
She made the very cool banner you can see at the top of this README, and has also helped greatly with my sanity throughout debugging this library.
|
||||||
|
|
||||||
**Note:** While libWiiPy is directly inspired by libWiiSharp and aims to have feature parity with it, no code from either libWiiSharp or Wii.py was used in the making of this library. All code is original and is written by [@NinjaCheetah](https://github.com/NinjaCheetah), [@rvtr](https://github.com/rvtr), and any other GitHub contributors.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# libWiiPy.archive Package
|
# libWiiPy.archive Package
|
||||||
|
|
||||||
## Modules
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.archive` package contains modules for packing and extracting archive formats used by the Wii. This currently includes packing and unpacking support for U8 archives and decompression support for ASH archives.
|
The `libWiiPy.archive` package contains modules for packing and extracting archive formats used by the Wii. This currently includes packing and unpacking support for U8 archives and decompression support for ASH archives.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
| Module | Description |
|
| Module | Description |
|
||||||
|----------------------------------------|---------------------------------------------------------|
|
|----------------------------------------|---------------------------------------------------------|
|
||||||
| [libWiiPy.archive.ash](/archive/ash) | Provides support for decompressing ASH archives |
|
| [libWiiPy.archive.ash](/archive/ash) | Provides support for decompressing ASH archives |
|
||||||
| [libWiiPy.archive.lz77](/archive/lz77) | Provides support for the LZ77 compression scheme |
|
| [libWiiPy.archive.lz77](/archive/lz77) | Provides support for the LZ77 compression scheme |
|
||||||
| [libWiiPy.archive.u8](/archive/u8) | Provides support for packing and extracting U8 archives |
|
| [libWiiPy.archive.u8](/archive/u8) | Provides support for packing and extracting U8 archives |
|
||||||
|
|
||||||
### libWiiPy.archive Package Contents
|
## Full Package Contents
|
||||||
|
|
||||||
```{toctree}
|
```{toctree}
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.archive.ash Module
|
# libWiiPy.archive.ash Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.archive.ash` module provides support for handling ASH files, which are a compressed format primarily used in the Wii Menu, but also in some other titles such as My Pokémon Ranch.
|
The `libWiiPy.archive.ash` module provides support for handling ASH files, which are a compressed format primarily used in the Wii Menu, but also in some other titles such as My Pokémon Ranch.
|
||||||
|
|
||||||
At present, libWiiPy only has support for decompressing ASH files, with compression as a planned feature for the future.
|
At present, libWiiPy only has support for decompressing ASH files, with compression as a planned feature for the future.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.archive.lz77 Module
|
# libWiiPy.archive.lz77 Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.archive.lz77` module provides support for handling LZ77 compression, which is a compression format used across the Wii and other Nintendo consoles.
|
The `libWiiPy.archive.lz77` module provides support for handling LZ77 compression, which is a compression format used across the Wii and other Nintendo consoles.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.archive.u8 Module
|
# libWiiPy.archive.u8 Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.archive.u8` module provides support for handling U8 archives, which are a non-compressed archive format used extensively on the Wii to join multiple files into one.
|
The `libWiiPy.archive.u8` module provides support for handling U8 archives, which are a non-compressed archive format used extensively on the Wii to join multiple files into one.
|
||||||
|
|
||||||
This module exposes functions for both packing and unpacking U8 archives, as well as code to parse IMET headers. IMET headers are a header format used specifically for U8 archives containing the banner of a channel, as they store the localized name of the channel along with other banner metadata.
|
This module exposes functions for both packing and unpacking U8 archives, as well as code to parse IMET headers. IMET headers are a header format used specifically for U8 archives containing the banner of a channel, as they store the localized name of the channel along with other banner metadata.
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ release = 'main'
|
|||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
extensions = ['myst_parser', 'sphinx.ext.napoleon', 'sphinx_copybutton', 'sphinx_tippy', 'sphinx_design']
|
extensions = [
|
||||||
|
'myst_parser',
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
|
'sphinx_copybutton',
|
||||||
|
'sphinx_tippy',
|
||||||
|
'sphinx_design'
|
||||||
|
]
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
exclude_patterns = ["Thumbs.db", ".DS_Store"]
|
exclude_patterns = ["Thumbs.db", ".DS_Store"]
|
||||||
@@ -31,7 +37,8 @@ html_logo = "banner.png"
|
|||||||
html_title = "libWiiPy API Docs"
|
html_title = "libWiiPy API Docs"
|
||||||
html_theme_options = {
|
html_theme_options = {
|
||||||
"repository_url": "https://github.com/NinjaCheetah/libWiiPy",
|
"repository_url": "https://github.com/NinjaCheetah/libWiiPy",
|
||||||
"use_repository_button": True
|
"use_repository_button": True,
|
||||||
|
"show_toc_level": 3
|
||||||
}
|
}
|
||||||
|
|
||||||
# MyST Configuration
|
# MyST Configuration
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.media.banner Module
|
# libWiiPy.media.banner Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.media.banner` module is essentially a stub at this point in time. It only provides one dataclass that is likely to become a traditional class when fully implemented. It is not recommended to use this module for anything yet.
|
The `libWiiPy.media.banner` module is essentially a stub at this point in time. It only provides one dataclass that is likely to become a traditional class when fully implemented. It is not recommended to use this module for anything yet.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# libWiiPy.media Package
|
# libWiiPy.media Package
|
||||||
|
|
||||||
## Modules
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.media` package contains modules used for parsing and editing media formats used by the Wii. This currently only includes limited support for parsing channel banners.
|
The `libWiiPy.media` package contains modules used for parsing and editing media formats used by the Wii. This currently only includes limited support for parsing channel banners.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
| Module | Description |
|
| Module | Description |
|
||||||
|----------------------------------------|---------------------------------------------------|
|
|----------------------------------------|---------------------------------------------------|
|
||||||
| [libWiiPy.media.banner](/media/banner) | Provides support for basic channel banner parsing |
|
| [libWiiPy.media.banner](/media/banner) | Provides support for basic channel banner parsing |
|
||||||
|
|
||||||
### libWiiPy.media Package Contents
|
## Full Package Contents
|
||||||
|
|
||||||
```{toctree}
|
```{toctree}
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.nand.emunand Module
|
# libWiiPy.nand.emunand Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.nand.emunand` module provides support for creating and managing Wii EmuNANDs. At present, you cannot create an EmuNAND compatible with something like NEEK on a real Wii with the features provided by this library, but you can create an EmuNAND compatible with Dolphin.
|
The `libWiiPy.nand.emunand` module provides support for creating and managing Wii EmuNANDs. At present, you cannot create an EmuNAND compatible with something like NEEK on a real Wii with the features provided by this library, but you can create an EmuNAND compatible with Dolphin.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# libWiiPy.nand Package
|
# libWiiPy.nand Package
|
||||||
|
|
||||||
## Modules
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.nand` package contains modules for parsing and manipulating EmuNANDs as well as modules for parsing and editing core system files found on the Wii's NAND.
|
The `libWiiPy.nand` package contains modules for parsing and manipulating EmuNANDs as well as modules for parsing and editing core system files found on the Wii's NAND.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
| Module | Description |
|
| Module | Description |
|
||||||
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [libWiiPy.nand.emunand](/nand/emunand) | Provides support for parsing, creating, and editing EmuNANDs |
|
| [libWiiPy.nand.emunand](/nand/emunand) | Provides support for parsing, creating, and editing EmuNANDs |
|
||||||
| [libWiiPy.nand.setting](/nand/setting) | Provides support for parsing, creating, and editing `setting.txt`, which is used to store the console's region and serial number |
|
| [libWiiPy.nand.setting](/nand/setting) | Provides support for parsing, creating, and editing `setting.txt`, which is used to store the console's region and serial number |
|
||||||
| [libWiiPy.nand.sys](/nand/sys) | Provides support for parsing, creating, and editing `uid.sys`, which is used to store a log of all titles run on a console |
|
| [libWiiPy.nand.sys](/nand/sys) | Provides support for parsing, creating, and editing `uid.sys`, which is used to store a log of all titles run on a console |
|
||||||
|
|
||||||
### libWiiPy.nand Package Contents
|
## Full Package Contents
|
||||||
|
|
||||||
```{toctree}
|
```{toctree}
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.nand.setting Module
|
# libWiiPy.nand.setting Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.nand.setting` module provides support for handling the Wii's `setting.txt` file. This file is stored as part of the Wii Menu's save data (stored in `/title/00000001/00000002/data/`) and is an encrypted text file that's primarily used to store your console's serial number and region information.
|
The `libWiiPy.nand.setting` module provides support for handling the Wii's `setting.txt` file. This file is stored as part of the Wii Menu's save data (stored in `/title/00000001/00000002/data/`) and is an encrypted text file that's primarily used to store your console's serial number and region information.
|
||||||
|
|
||||||
This module allows you to encrypt or decrypt this file, and exposes the keys stored in it for editing.
|
This module allows you to encrypt or decrypt this file, and exposes the keys stored in it for editing.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.nand.sys Module
|
# libWiiPy.nand.sys Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.nand.sys` module provides support for editing system files used on the Wii. Currently, it only offers support for `uid.sys`, which keeps a record of the Title IDs of every title launched on the console, assigning each one a unique ID.
|
The `libWiiPy.nand.sys` module provides support for editing system files used on the Wii. Currently, it only offers support for `uid.sys`, which keeps a record of the Title IDs of every title launched on the console, assigning each one a unique ID.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.cert Module
|
# libWiiPy.title.cert Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.cert` module provides support for parsing the various signing certificates used by the Wii for content validation.
|
The `libWiiPy.title.cert` module provides support for parsing the various signing certificates used by the Wii for content validation.
|
||||||
|
|
||||||
This module allows you to write your own code for validating the authenticity of a TMD or Ticket by providing the certificates from the Wii's certificate chain. Both retail and development certificate chains are supported.
|
This module allows you to write your own code for validating the authenticity of a TMD or Ticket by providing the certificates from the Wii's certificate chain. Both retail and development certificate chains are supported.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.commonkeys Module
|
# libWiiPy.title.commonkeys Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.commonkeys` module simply provides easy access to the Wii's common encryption keys.
|
The `libWiiPy.title.commonkeys` module simply provides easy access to the Wii's common encryption keys.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.content Module
|
# libWiiPy.title.content Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.content` module provides support for parsing, adding, removing, and editing content files from a digital Wii title.
|
The `libWiiPy.title.content` module provides support for parsing, adding, removing, and editing content files from a digital Wii title.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.crypto Module
|
# libWiiPy.title.crypto Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.crypto` module provides low-level cryptography functions required for handling digital Wii titles. It does not expose many functions that are likely to be required during typical use, and instead acts more as a dependency for other modules.
|
The `libWiiPy.title.crypto` module provides low-level cryptography functions required for handling digital Wii titles. It does not expose many functions that are likely to be required during typical use, and instead acts more as a dependency for other modules.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.iospatcher Module
|
# libWiiPy.title.iospatcher Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.iospatcher` module provides support for applying various binary patches to IOS' ES module. These patches and what they do can be found attached to the methods used to apply them.
|
The `libWiiPy.title.iospatcher` module provides support for applying various binary patches to IOS' ES module. These patches and what they do can be found attached to the methods used to apply them.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.nus Module
|
# libWiiPy.title.nus Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.nus` module provides support for downloading digital Wii titles from the Nintendo Update Servers. This module provides easy methods for downloading TMDs, common Tickets (when present), encrypted content, and the certificate chain.
|
The `libWiiPy.title.nus` module provides support for downloading digital Wii titles from the Nintendo Update Servers. This module provides easy methods for downloading TMDs, common Tickets (when present), encrypted content, and the certificate chain.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
@@ -9,4 +11,5 @@ The `libWiiPy.title.nus` module provides support for downloading digital Wii tit
|
|||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
:special-members: __call__
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.ticket Module
|
# libWiiPy.title.ticket Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.ticket` module provides support for handling Tickets, which are the license files used to decrypt the content of digital titles during installation. This module allows for easy parsing and editing of Tickets.
|
The `libWiiPy.title.ticket` module provides support for handling Tickets, which are the license files used to decrypt the content of digital titles during installation. This module allows for easy parsing and editing of Tickets.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# libWiiPy.title Package
|
# libWiiPy.title Package
|
||||||
|
|
||||||
|
## Description
|
||||||
|
The `libWiiPy.title` package contains modules for interacting with Wii titles. This is the most complete package in libWiiPy, as it offers the functionality one would be most likely to need. As a result, it gets the most attention during development and should be the most reliable.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
The `libWiiPy.title` package contains modules for interacting with Wii titles. This is the most complete package in libWiiPy, and therefore offers the most functionality.
|
|
||||||
|
|
||||||
| Module | Description |
|
| Module | Description |
|
||||||
|------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||||
@@ -17,7 +19,7 @@ The `libWiiPy.title` package contains modules for interacting with Wii titles. T
|
|||||||
| [libWiiPy.title.util](/title/util) | Provides some simple utility functions relating to titles |
|
| [libWiiPy.title.util](/title/util) | Provides some simple utility functions relating to titles |
|
||||||
| [libWiiPy.title.wad](/title/wad) | Provides support for parsing and editing WAD files, allowing you to load each component into the other available classes |
|
| [libWiiPy.title.wad](/title/wad) | Provides support for parsing and editing WAD files, allowing you to load each component into the other available classes |
|
||||||
|
|
||||||
### libWiiPy.title Package Contents
|
## Full Package Contents
|
||||||
|
|
||||||
```{toctree}
|
```{toctree}
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.title Module
|
# libWiiPy.title.title Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.title` module provides a high-level interface for handling all the components of a digital Wii title through one class. It allows for directly importing a WAD, and will automatically extract the various components and load them into their appropriate classes. Additionally, it provides duplicates of some methods found in those classes that require fewer arguments, as it has the context of the other components and is able to retrieve additional data automatically.
|
The `libWiiPy.title.title` module provides a high-level interface for handling all the components of a digital Wii title through one class. It allows for directly importing a WAD, and will automatically extract the various components and load them into their appropriate classes. Additionally, it provides duplicates of some methods found in those classes that require fewer arguments, as it has the context of the other components and is able to retrieve additional data automatically.
|
||||||
|
|
||||||
An example of that idea can be seen with the method `get_content_by_index()`. In its original definition, which can be seen at <project:#libWiiPy.title.content.ContentRegion.get_content_by_index>, you are required to supply the Title Key for the title that the content is sourced from. In contrast, when using <project:#libWiiPy.title.title.Title.get_content_by_index>, you do not need to supply a Title Key, as the Title object already has the context of the Ticket and can retrieve the Title Key from it automatically. In a similar vein, this module provides the easiest route for verifying that a title is legitimately signed by Nintendo. The method <project:#libWiiPy.title.title.Title.get_is_signed> is able to access the entire certificate chain, the TMD, and the Ticket, and is therefore able to verify all components of the title by itself.
|
An example of that idea can be seen with the method `get_content_by_index()`. In its original definition, which can be seen at <project:#libWiiPy.title.content.ContentRegion.get_content_by_index>, you are required to supply the Title Key for the title that the content is sourced from. In contrast, when using <project:#libWiiPy.title.title.Title.get_content_by_index>, you do not need to supply a Title Key, as the Title object already has the context of the Ticket and can retrieve the Title Key from it automatically. In a similar vein, this module provides the easiest route for verifying that a title is legitimately signed by Nintendo. The method <project:#libWiiPy.title.title.Title.get_is_signed> is able to access the entire certificate chain, the TMD, and the Ticket, and is therefore able to verify all components of the title by itself.
|
||||||
@@ -12,5 +14,4 @@ Because using <project:#libWiiPy.title.title.Title> allows many operations to be
|
|||||||
.. automodule:: libWiiPy.title.title
|
.. automodule:: libWiiPy.title.title
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.tmd Module
|
# libWiiPy.title.tmd Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.tmd` module provides support for handling TMD (Title Metadata) files, which contain the metadata of both digital and physical Wii titles. This module allows for easy parsing and editing of TMDs.
|
The `libWiiPy.title.tmd` module provides support for handling TMD (Title Metadata) files, which contain the metadata of both digital and physical Wii titles. This module allows for easy parsing and editing of TMDs.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.util Module
|
# libWiiPy.title.util Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.util` module provides common utility functions internally. It is not designed to be used directly.
|
The `libWiiPy.title.util` module provides common utility functions internally. It is not designed to be used directly.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# libWiiPy.title.wad Module
|
# libWiiPy.title.wad Module
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
The `libWiiPy.title.wad` module provides support for handling WAD (Wii Archive Data) files, which is the format used to deliver digital Wii titles. This module allows for extracting the various components for a WAD, as well as properly padding and writing out that data when it has been edited using other modules.
|
The `libWiiPy.title.wad` module provides support for handling WAD (Wii Archive Data) files, which is the format used to deliver digital Wii titles. This module allows for extracting the various components for a WAD, as well as properly padding and writing out that data when it has been edited using other modules.
|
||||||
|
|
||||||
## Module Contents
|
## Module Contents
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "libWiiPy"
|
name = "libWiiPy"
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
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" }
|
||||||
@@ -13,7 +13,7 @@ classifiers = [
|
|||||||
# 3 - Alpha
|
# 3 - Alpha
|
||||||
# 4 - Beta
|
# 4 - Beta
|
||||||
# 5 - Production/Stable
|
# 5 - Production/Stable
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
@@ -23,7 +23,8 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pycryptodome",
|
"pycryptodome",
|
||||||
"requests"
|
"requests",
|
||||||
|
"types-requests"
|
||||||
]
|
]
|
||||||
keywords = ["Wii", "wii"]
|
keywords = ["Wii", "wii"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
build
|
build
|
||||||
pycryptodome
|
pycryptodome
|
||||||
requests
|
requests
|
||||||
|
types-requests
|
||||||
sphinx
|
sphinx
|
||||||
sphinx-book-theme
|
sphinx-book-theme
|
||||||
myst-parser
|
myst-parser
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
# See <link pending> for details about the ASH compression format.
|
# See <link pending> for details about the ASH compression format.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from dataclasses import dataclass as _dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@_dataclass
|
@dataclass
|
||||||
class _ASHBitReader:
|
class _ASHBitReader:
|
||||||
"""
|
"""
|
||||||
An _ASHBitReader class used to parse individual words in an ASH file. Private class used by the ASH module.
|
An _ASHBitReader class used to parse individual words in an ASH file. Private class used by the ASH module.
|
||||||
@@ -93,7 +94,7 @@ def _ash_bit_reader_read_bits(bit_reader: _ASHBitReader, num_bits: int):
|
|||||||
return bits
|
return bits
|
||||||
|
|
||||||
|
|
||||||
def _ash_read_tree(bit_reader: _ASHBitReader, width: int, left_tree: [int], right_tree: [int]):
|
def _ash_read_tree(bit_reader: _ASHBitReader, width: int, left_tree: List[int], right_tree: List[int]):
|
||||||
# Read either the symbol or distance tree from the ASH file, and return the root of that tree.
|
# Read either the symbol or distance tree from the ASH file, and return the root of that tree.
|
||||||
work = [0] * (2 * (1 << width))
|
work = [0] * (2 * (1 << width))
|
||||||
work_pos = 0
|
work_pos = 0
|
||||||
|
|||||||
@@ -4,6 +4,235 @@
|
|||||||
# See https://wiibrew.org/wiki/LZ77 for details about the LZ77 compression format.
|
# See https://wiibrew.org/wiki/LZ77 for details about the LZ77 compression format.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from dataclasses import dataclass as _dataclass
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
_LZ_MIN_DISTANCE = 0x01 # Minimum distance for each reference.
|
||||||
|
_LZ_MAX_DISTANCE = 0x1000 # Maximum distance for each reference.
|
||||||
|
_LZ_MIN_LENGTH = 0x03 # Minimum length for each reference.
|
||||||
|
_LZ_MAX_LENGTH = 0x12 # Maximum length for each reference.
|
||||||
|
|
||||||
|
|
||||||
|
@_dataclass
|
||||||
|
class _LZNode:
|
||||||
|
dist: int = 0
|
||||||
|
len: int = 0
|
||||||
|
weight: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_compare_bytes(buffer: List[int], offset1: int, offset2: int, abs_len_max: int) -> int:
|
||||||
|
# Compare bytes up to the maximum length we can match. Start by comparing the first 3 bytes, since that's the
|
||||||
|
# minimum match length and this allows for a more optimized early exit.
|
||||||
|
num_matched = 0
|
||||||
|
while num_matched < abs_len_max:
|
||||||
|
if buffer[offset1 + num_matched] != buffer[offset2 + num_matched]:
|
||||||
|
break
|
||||||
|
num_matched += 1
|
||||||
|
return num_matched
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_search_matches_optimized(buffer: List[int], pos: int) -> Tuple[int, int]:
|
||||||
|
bytes_left = len(buffer) - pos
|
||||||
|
global _LZ_MAX_DISTANCE, _LZ_MIN_LENGTH, _LZ_MAX_LENGTH, _LZ_MIN_DISTANCE
|
||||||
|
# Default to only looking back 4096 bytes, unless we've moved fewer than 4096 bytes, in which case we should
|
||||||
|
# only look as far back as we've gone.
|
||||||
|
max_dist = min(_LZ_MAX_DISTANCE, pos)
|
||||||
|
# Default to only matching up to 18 bytes, unless fewer than 18 bytes remain, in which case we can only match
|
||||||
|
# up to that many bytes.
|
||||||
|
max_len = min(_LZ_MAX_LENGTH, bytes_left)
|
||||||
|
# Log the longest match we found and its offset.
|
||||||
|
biggest_match, biggest_match_pos = 0, 0
|
||||||
|
# Search for matches.
|
||||||
|
for i in range(_LZ_MIN_DISTANCE, max_dist + 1):
|
||||||
|
num_matched = _compress_compare_bytes(buffer, pos - i, pos, max_len)
|
||||||
|
if num_matched > biggest_match:
|
||||||
|
biggest_match = num_matched
|
||||||
|
biggest_match_pos = i
|
||||||
|
if biggest_match == max_len:
|
||||||
|
break
|
||||||
|
return biggest_match, biggest_match_pos
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_search_matches_greedy(buffer: List[int], pos: int) -> Tuple[int, int]:
|
||||||
|
# Finds and returns the first valid match, rather that finding the best one.
|
||||||
|
bytes_left = len(buffer) - pos
|
||||||
|
global _LZ_MAX_DISTANCE, _LZ_MAX_LENGTH, _LZ_MIN_DISTANCE
|
||||||
|
# Default to only looking back 4096 bytes, unless we've moved fewer than 4096 bytes, in which case we should
|
||||||
|
# only look as far back as we've gone.
|
||||||
|
max_dist = min(_LZ_MAX_DISTANCE, pos)
|
||||||
|
# Default to only matching up to 18 bytes, unless fewer than 18 bytes remain, in which case we can only match
|
||||||
|
# up to that many bytes.
|
||||||
|
max_len = min(_LZ_MAX_LENGTH, bytes_left)
|
||||||
|
match, match_pos = 0, 0
|
||||||
|
for i in range(_LZ_MIN_DISTANCE, max_dist + 1):
|
||||||
|
match = _compress_compare_bytes(buffer, pos - i, pos, max_len)
|
||||||
|
match_pos = i
|
||||||
|
if match >= _LZ_MIN_LENGTH or match == max_len:
|
||||||
|
break
|
||||||
|
return match, match_pos
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_node_is_ref(node: _LZNode) -> bool:
|
||||||
|
return node.len >= _LZ_MIN_LENGTH
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_get_node_cost(length: int) -> int:
|
||||||
|
if length >= _LZ_MIN_LENGTH:
|
||||||
|
num_bytes = 2
|
||||||
|
else:
|
||||||
|
num_bytes = 1
|
||||||
|
return 1 + (num_bytes * 8)
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_lz77_optimized(data: bytes) -> bytes:
|
||||||
|
# Optimized compressor based around a node graph that finds optimal string matches. Originally the default
|
||||||
|
# implementation, but unfortunately it's very slow.
|
||||||
|
nodes = [_LZNode() for _ in range(len(data))]
|
||||||
|
# Iterate over the uncompressed data, starting from the end.
|
||||||
|
pos = len(data)
|
||||||
|
global _LZ_MAX_LENGTH, _LZ_MIN_LENGTH, _LZ_MIN_DISTANCE
|
||||||
|
data_list = list(data)
|
||||||
|
while pos:
|
||||||
|
pos -= 1
|
||||||
|
node = nodes[pos]
|
||||||
|
# Limit the maximum search length when we're near the end of the file.
|
||||||
|
max_search_len = min(_LZ_MAX_LENGTH, len(data_list) - pos)
|
||||||
|
if max_search_len < _LZ_MIN_DISTANCE:
|
||||||
|
max_search_len = 1
|
||||||
|
# Initialize as 1 for each, since that's all we could use if we weren't compressing.
|
||||||
|
length, dist = 1, 1
|
||||||
|
if max_search_len >= _LZ_MIN_LENGTH:
|
||||||
|
length, dist = _compress_search_matches_optimized(data_list, pos)
|
||||||
|
# Treat as direct bytes if it's too short to copy.
|
||||||
|
if length == 0 or length < _LZ_MIN_LENGTH:
|
||||||
|
length = 1
|
||||||
|
# If the node goes to the end of the file, the weight is the cost of the node.
|
||||||
|
if (pos + length) == len(data_list):
|
||||||
|
node.len = length
|
||||||
|
node.dist = dist
|
||||||
|
node.weight = _compress_get_node_cost(length)
|
||||||
|
# Otherwise, search for possible matches and determine the one with the best cost.
|
||||||
|
else:
|
||||||
|
weight_best = 0xFFFFFFFF # This was originally UINT_MAX, but that isn't a thing here so 32-bit it is!
|
||||||
|
len_best = 1
|
||||||
|
while length:
|
||||||
|
weight_next = nodes[pos + length].weight
|
||||||
|
weight = _compress_get_node_cost(length) + weight_next
|
||||||
|
if weight < weight_best:
|
||||||
|
len_best = length
|
||||||
|
weight_best = weight
|
||||||
|
length -= 1
|
||||||
|
if length != 0 and length < _LZ_MIN_LENGTH:
|
||||||
|
length = 1
|
||||||
|
node.len = len_best
|
||||||
|
node.dist = dist
|
||||||
|
node.weight = weight_best
|
||||||
|
# Write the compressed data.
|
||||||
|
with io.BytesIO() as buffer:
|
||||||
|
# Write the header data.
|
||||||
|
buffer.write(b'LZ77\x10') # The LZ type on the Wii is *always* 0x10.
|
||||||
|
buffer.write(len(data).to_bytes(3, 'little'))
|
||||||
|
|
||||||
|
src_pos = 0
|
||||||
|
while src_pos < len(data):
|
||||||
|
head = 0
|
||||||
|
head_pos = buffer.tell()
|
||||||
|
buffer.write(b'\x00') # Reserve a byte for the chunk head.
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < 8 and src_pos < len(data):
|
||||||
|
current_node = nodes[src_pos]
|
||||||
|
length = current_node.len
|
||||||
|
dist = current_node.dist
|
||||||
|
# This is a reference node.
|
||||||
|
if _compress_node_is_ref(current_node):
|
||||||
|
encoded = (((length - _LZ_MIN_LENGTH) & 0xF) << 12) | ((dist - _LZ_MIN_DISTANCE) & 0xFFF)
|
||||||
|
buffer.write(encoded.to_bytes(2))
|
||||||
|
head = (head | (1 << (7 - i))) & 0xFF
|
||||||
|
# This is a direct copy node.
|
||||||
|
else:
|
||||||
|
buffer.write(data[src_pos:src_pos + 1])
|
||||||
|
src_pos += length
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
pos = buffer.tell()
|
||||||
|
buffer.seek(head_pos)
|
||||||
|
buffer.write(head.to_bytes(1))
|
||||||
|
buffer.seek(pos)
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
out_data = buffer.read()
|
||||||
|
return out_data
|
||||||
|
|
||||||
|
|
||||||
|
def _compress_lz77_greedy(data: bytes) -> bytes:
|
||||||
|
# Greedy compressor that processes the file start to end and saves the first matches found. Faster than the
|
||||||
|
# optimized implementation, but creates larger files.
|
||||||
|
global _LZ_MAX_LENGTH, _LZ_MIN_LENGTH, _LZ_MIN_DISTANCE
|
||||||
|
with io.BytesIO() as buffer:
|
||||||
|
# Write the header data.
|
||||||
|
buffer.write(b'LZ77\x10') # The LZ type on the Wii is *always* 0x10.
|
||||||
|
buffer.write(len(data).to_bytes(3, 'little'))
|
||||||
|
|
||||||
|
src_pos = 0
|
||||||
|
data_list = list(data)
|
||||||
|
while src_pos < len(data):
|
||||||
|
head = 0
|
||||||
|
head_pos = buffer.tell()
|
||||||
|
buffer.write(b'\x00') # Reserve a byte for the chunk head.
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < 8 and src_pos < len(data):
|
||||||
|
length, dist = _compress_search_matches_greedy(data_list, src_pos)
|
||||||
|
# This is a reference node.
|
||||||
|
if length >= _LZ_MIN_LENGTH:
|
||||||
|
encoded = (((length - _LZ_MIN_LENGTH) & 0xF) << 12) | ((dist - _LZ_MIN_DISTANCE) & 0xFFF)
|
||||||
|
buffer.write(encoded.to_bytes(2))
|
||||||
|
head = (head | (1 << (7 - i))) & 0xFF
|
||||||
|
src_pos += length
|
||||||
|
# This is a direct copy node.
|
||||||
|
else:
|
||||||
|
buffer.write(data[src_pos:src_pos + 1])
|
||||||
|
src_pos += 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
pos = buffer.tell()
|
||||||
|
buffer.seek(head_pos)
|
||||||
|
buffer.write(head.to_bytes(1))
|
||||||
|
buffer.seek(pos)
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
out_data = buffer.read()
|
||||||
|
return out_data
|
||||||
|
|
||||||
|
|
||||||
|
def compress_lz77(data: bytes, compression_level: int = 1) -> bytes:
|
||||||
|
"""
|
||||||
|
Compresses data using the Wii's LZ77 compression algorithm and returns the compressed result. Supports two
|
||||||
|
different levels of compression, one based around a "greedy" LZ compression algorithm and the other based around
|
||||||
|
an optimized LZ compression algorithm. The greedy compressor, level 1, will produce a larger compressed file but
|
||||||
|
will run noticeably faster than the optimized compressor, which is level 2, especially for larger data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: bytes
|
||||||
|
The data to compress.
|
||||||
|
compression_level: int
|
||||||
|
The compression level to use, either 1 and 2. Default value is 1.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The LZ77-compressed data.
|
||||||
|
"""
|
||||||
|
if compression_level == 1:
|
||||||
|
out_data = _compress_lz77_greedy(data)
|
||||||
|
elif compression_level == 2:
|
||||||
|
out_data = _compress_lz77_optimized(data)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid compression level \"{compression_level}\"!\"")
|
||||||
|
return out_data
|
||||||
|
|
||||||
|
|
||||||
def decompress_lz77(lz77_data: bytes) -> bytes:
|
def decompress_lz77(lz77_data: bytes) -> bytes:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class U8Archive:
|
|||||||
imet_header: IMETHeader
|
imet_header: IMETHeader
|
||||||
The IMET header of the U8 archive, if one exists. Otherwise, an empty IMETHeader object.
|
The IMET header of the U8 archive, if one exists. Otherwise, an empty IMETHeader object.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.u8_magic = b''
|
self.u8_magic = b''
|
||||||
self.u8_node_list: List[_U8Node] = [] # All the nodes in the header of a U8 file.
|
self.u8_node_list: List[_U8Node] = [] # All the nodes in the header of a U8 file.
|
||||||
self.file_name_list: List[str] = []
|
self.file_name_list: List[str] = []
|
||||||
@@ -68,16 +68,16 @@ class U8Archive:
|
|||||||
self.root_node: _U8Node = _U8Node(0, 0, 0, 0)
|
self.root_node: _U8Node = _U8Node(0, 0, 0, 0)
|
||||||
self.imet_header: IMETHeader = IMETHeader()
|
self.imet_header: IMETHeader = IMETHeader()
|
||||||
|
|
||||||
def load(self, u8_data: bytes) -> None:
|
def load(self, u8: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Loads raw U8 data into a new U8 object. This allows for extracting the file and updating its contents.
|
Loads raw U8 data into a new U8 object. This allows for extracting the file and updating its contents.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
u8_data : bytes
|
u8 : bytes
|
||||||
The data for the U8 file to load.
|
The data for the U8 file to load.
|
||||||
"""
|
"""
|
||||||
with io.BytesIO(u8_data) as u8_data:
|
with io.BytesIO(u8) as u8_data:
|
||||||
# Read the first 4 bytes of the file to ensure that it's a U8 archive.
|
# Read the first 4 bytes of the file to ensure that it's a U8 archive.
|
||||||
u8_data.seek(0x0)
|
u8_data.seek(0x0)
|
||||||
self.u8_magic = u8_data.read(4)
|
self.u8_magic = u8_data.read(4)
|
||||||
@@ -126,7 +126,7 @@ class U8Archive:
|
|||||||
# Seek back before the root node so that it gets read with all the rest.
|
# Seek back before the root node so that it gets read with all the rest.
|
||||||
u8_data.seek(u8_data.tell() - 12)
|
u8_data.seek(u8_data.tell() - 12)
|
||||||
# Iterate over the number of nodes that the root node lists.
|
# Iterate over the number of nodes that the root node lists.
|
||||||
for node in range(root_node_size):
|
for _ in range(root_node_size):
|
||||||
node_type = int.from_bytes(u8_data.read(1))
|
node_type = int.from_bytes(u8_data.read(1))
|
||||||
node_name_offset = int.from_bytes(u8_data.read(3))
|
node_name_offset = int.from_bytes(u8_data.read(3))
|
||||||
node_data_offset = int.from_bytes(u8_data.read(4))
|
node_data_offset = int.from_bytes(u8_data.read(4))
|
||||||
@@ -160,7 +160,7 @@ class U8Archive:
|
|||||||
# This is 0 because the header size DOES NOT include the initial 32 bytes describing the file.
|
# This is 0 because the header size DOES NOT include the initial 32 bytes describing the file.
|
||||||
header_size = 0
|
header_size = 0
|
||||||
# Add 12 bytes for each node, since that's how many bytes each one is made up of.
|
# Add 12 bytes for each node, since that's how many bytes each one is made up of.
|
||||||
for node in range(len(self.u8_node_list)):
|
for _ in range(len(self.u8_node_list)):
|
||||||
header_size += 12
|
header_size += 12
|
||||||
# Add the number of bytes used for each file/folder name in the string table.
|
# Add the number of bytes used for each file/folder name in the string table.
|
||||||
for file_name in self.file_name_list:
|
for file_name in self.file_name_list:
|
||||||
@@ -170,13 +170,13 @@ class U8Archive:
|
|||||||
# Adjust all nodes to place file data in the same order as the nodes. Why isn't it already like this?
|
# Adjust all nodes to place file data in the same order as the nodes. Why isn't it already like this?
|
||||||
current_data_offset = data_offset
|
current_data_offset = data_offset
|
||||||
current_name_offset = 0
|
current_name_offset = 0
|
||||||
for node in range(len(self.u8_node_list)):
|
for idx in range(len(self.u8_node_list)):
|
||||||
if self.u8_node_list[node].type == 0:
|
if self.u8_node_list[idx].type == 0:
|
||||||
self.u8_node_list[node].data_offset = _align_value(current_data_offset, 32)
|
self.u8_node_list[idx].data_offset = _align_value(current_data_offset, 32)
|
||||||
current_data_offset += _align_value(self.u8_node_list[node].size, 32)
|
current_data_offset += _align_value(self.u8_node_list[idx].size, 32)
|
||||||
# Calculate the name offsets, including the extra 1 for the NULL byte at the end of each name.
|
# Calculate the name offsets, including the extra 1 for the NULL byte at the end of each name.
|
||||||
self.u8_node_list[node].name_offset = current_name_offset
|
self.u8_node_list[idx].name_offset = current_name_offset
|
||||||
current_name_offset += len(self.file_name_list[node]) + 1
|
current_name_offset += len(self.file_name_list[idx]) + 1
|
||||||
# Begin joining all the U8 archive data into bytes.
|
# Begin joining all the U8 archive data into bytes.
|
||||||
u8_data = b''
|
u8_data = b''
|
||||||
# Magic number.
|
# Magic number.
|
||||||
@@ -300,7 +300,7 @@ def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, parent_node):
|
|||||||
return u8_archive, node_count
|
return u8_archive, node_count
|
||||||
|
|
||||||
|
|
||||||
def pack_u8(input_path, generate_imet=False, imet_titles:List[str]=None) -> bytes:
|
def pack_u8(input_path, generate_imet=False, imet_titles:List[str] | None = None) -> bytes:
|
||||||
"""
|
"""
|
||||||
Packs the provided file or folder into a new U8 archive, and returns the raw file data for it.
|
Packs the provided file or folder into a new U8 archive, and returns the raw file data for it.
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ class IMETHeader:
|
|||||||
md5_hash : bytes
|
md5_hash : bytes
|
||||||
MD5 sum of the entire header, with this field being all zeros during the hashing.
|
MD5 sum of the entire header, with this field being all zeros during the hashing.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.magic: str = "" # Should always be "IMET"
|
self.magic: str = "" # Should always be "IMET"
|
||||||
self.header_size: int = 0 # Always 1536? I assumed this would mean something, but it's just the header length.
|
self.header_size: int = 0 # Always 1536? I assumed this would mean something, but it's just the header length.
|
||||||
self.imet_version: int = 0 # Always 3?
|
self.imet_version: int = 0 # Always 3?
|
||||||
@@ -513,13 +513,15 @@ class IMETHeader:
|
|||||||
raise ValueError(f"The specified language is not valid!")
|
raise ValueError(f"The specified language is not valid!")
|
||||||
return self.channel_names[target_languages]
|
return self.channel_names[target_languages]
|
||||||
# If multiple channel names were requested.
|
# If multiple channel names were requested.
|
||||||
else:
|
elif type(target_languages) == List:
|
||||||
channel_names = []
|
channel_names = []
|
||||||
for lang in target_languages:
|
for lang in target_languages:
|
||||||
if lang not in self.LocalizedTitles:
|
if lang not in self.LocalizedTitles:
|
||||||
raise ValueError(f"The specified language at index {target_languages.index(lang)} is not valid!")
|
raise ValueError(f"The specified language at index {target_languages.index(lang)} is not valid!")
|
||||||
channel_names.append(self.channel_names[lang])
|
channel_names.append(self.channel_names[lang])
|
||||||
return channel_names
|
return channel_names
|
||||||
|
else:
|
||||||
|
raise TypeError("Target languages must be type int or List[int]!")
|
||||||
|
|
||||||
def set_channel_names(self, channel_names: Tuple[int, str] | List[Tuple[int, str]]) -> None:
|
def set_channel_names(self, channel_names: Tuple[int, str] | List[Tuple[int, str]]) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -544,7 +546,7 @@ class IMETHeader:
|
|||||||
f"42 characters!")
|
f"42 characters!")
|
||||||
self.channel_names[channel_names[0]] = channel_names[1]
|
self.channel_names[channel_names[0]] = channel_names[1]
|
||||||
# If a list of channel names was provided.
|
# If a list of channel names was provided.
|
||||||
else:
|
elif type(channel_names) == list:
|
||||||
for name in channel_names:
|
for name in channel_names:
|
||||||
if name[0] not in self.LocalizedTitles:
|
if name[0] not in self.LocalizedTitles:
|
||||||
raise ValueError(f"The target language \"{name[0]}\" for the name at index {channel_names.index(name)} "
|
raise ValueError(f"The target language \"{name[0]}\" for the name at index {channel_names.index(name)} "
|
||||||
@@ -553,3 +555,5 @@ class IMETHeader:
|
|||||||
raise ValueError(f"The channel name \"{name[1]}\" at index {channel_names.index(name)} is too long! "
|
raise ValueError(f"The channel name \"{name[1]}\" at index {channel_names.index(name)} is too long! "
|
||||||
f"Channel names cannot exceed 42 characters!")
|
f"Channel names cannot exceed 42 characters!")
|
||||||
self.channel_names[name[0]] = name[1]
|
self.channel_names[name[0]] = name[1]
|
||||||
|
else:
|
||||||
|
raise TypeError("Channel names must be type Tuple[int, str] or List[Tuple[int, str]]!")
|
||||||
|
|||||||
@@ -14,16 +14,10 @@ class IMD5Header:
|
|||||||
|
|
||||||
An IMD5 header is always 32 bytes long.
|
An IMD5 header is always 32 bytes long.
|
||||||
|
|
||||||
Attributes
|
:ivar magic: Magic number for the header, should be "IMD5".
|
||||||
----------
|
:ivar file_size: The size of the file this header precedes.
|
||||||
magic : str
|
:ivar zeros: 8 bytes of zero padding.
|
||||||
Magic number for the header, should be "IMD5".
|
:ivar md5_hash: The MD5 hash of the file this header precedes.
|
||||||
file_size : int
|
|
||||||
The size of the file this header precedes.
|
|
||||||
zeros : int
|
|
||||||
8 bytes of zero padding.
|
|
||||||
md5_hash : bytes
|
|
||||||
The MD5 hash of the file this header precedes.
|
|
||||||
"""
|
"""
|
||||||
magic: str # Should always be "IMD5"
|
magic: str # Should always be "IMD5"
|
||||||
file_size: int
|
file_size: int
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
from dataclasses import dataclass as _dataclass
|
from dataclasses import dataclass as _dataclass
|
||||||
from typing import List
|
from typing import Callable, List
|
||||||
from ..title.ticket import Ticket
|
from ..title.ticket import Ticket
|
||||||
from ..title.title import Title
|
from ..title.title import Title
|
||||||
from ..title.tmd import TMD
|
from ..title.tmd import TMD
|
||||||
@@ -32,7 +32,7 @@ class EmuNAND:
|
|||||||
emunand_root : pathlib.Path
|
emunand_root : pathlib.Path
|
||||||
The path to the EmuNAND root directory.
|
The path to the EmuNAND root directory.
|
||||||
"""
|
"""
|
||||||
def __init__(self, emunand_root: str | pathlib.Path, callback: callable = None):
|
def __init__(self, emunand_root: str | pathlib.Path, callback: Callable | None = None):
|
||||||
self.emunand_root = pathlib.Path(emunand_root)
|
self.emunand_root = pathlib.Path(emunand_root)
|
||||||
self.log = callback if callback is not None else None
|
self.log = callback if callback is not None else None
|
||||||
|
|
||||||
@@ -128,6 +128,10 @@ class EmuNAND:
|
|||||||
uid_sys = _UidSys()
|
uid_sys = _UidSys()
|
||||||
if not uid_sys_path.exists():
|
if not uid_sys_path.exists():
|
||||||
uid_sys.create()
|
uid_sys.create()
|
||||||
|
else:
|
||||||
|
uid_sys.load(uid_sys_path.read_bytes())
|
||||||
|
uid_sys.add(title.tmd.title_id)
|
||||||
|
uid_sys_path.write_bytes(uid_sys.dump())
|
||||||
|
|
||||||
def uninstall_title(self, tid: str) -> None:
|
def uninstall_title(self, tid: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -170,12 +174,8 @@ class EmuNAND:
|
|||||||
An InstalledTitles object that is used to track a title type and any titles that belong to that type that are
|
An InstalledTitles object that is used to track a title type and any titles that belong to that type that are
|
||||||
installed to an EmuNAND.
|
installed to an EmuNAND.
|
||||||
|
|
||||||
Attributes
|
:ivar type: The type (Title ID high) of the installed titles.
|
||||||
----------
|
:ivar titles: The Title ID low of each installed title.
|
||||||
type : str
|
|
||||||
The type (Title ID high) of the installed titles.
|
|
||||||
titles : List[str]
|
|
||||||
The Title ID low of each installed title.
|
|
||||||
"""
|
"""
|
||||||
type: str
|
type: str
|
||||||
titles: List[str]
|
titles: List[str]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# See https://wiibrew.org/wiki//title/00000001/00000002/data/setting.txt for information about setting.txt.
|
# See https://wiibrew.org/wiki//title/00000001/00000002/data/setting.txt for information about setting.txt.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from typing import List
|
||||||
from ..shared import _pad_bytes
|
from ..shared import _pad_bytes
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ class SettingTxt:
|
|||||||
game : str
|
game : str
|
||||||
Another region code, possibly set by the hidden region select channel.
|
Another region code, possibly set by the hidden region select channel.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.area: str = ""
|
self.area: str = ""
|
||||||
self.model: str = ""
|
self.model: str = ""
|
||||||
self.dvd: int = 0
|
self.dvd: int = 0
|
||||||
@@ -53,16 +54,16 @@ class SettingTxt:
|
|||||||
"""
|
"""
|
||||||
with io.BytesIO(setting_txt) as setting_data:
|
with io.BytesIO(setting_txt) as setting_data:
|
||||||
global _key # I still don't actually know what *kind* of encryption this is.
|
global _key # I still don't actually know what *kind* of encryption this is.
|
||||||
setting_txt_dec: [int] = []
|
setting_txt_dec: List[int] = []
|
||||||
for i in range(0, 256):
|
for i in range(0, 256):
|
||||||
setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
||||||
_key = (_key << 1) | (_key >> 31)
|
_key = (_key << 1) | (_key >> 31)
|
||||||
setting_txt_dec = bytes(setting_txt_dec)
|
setting_txt_bytes = bytes(setting_txt_dec)
|
||||||
try:
|
try:
|
||||||
setting_str = setting_txt_dec.decode('utf-8')
|
setting_str = setting_txt_bytes.decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
last_newline_pos = setting_txt_dec.rfind(b'\n') # This makes sure we don't try to decode any garbage data.
|
last_newline_pos = setting_txt_bytes.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')
|
setting_str = setting_txt_bytes[:last_newline_pos + 1].decode('utf-8')
|
||||||
self.load_decrypted(setting_str)
|
self.load_decrypted(setting_str)
|
||||||
|
|
||||||
def load_decrypted(self, setting_txt: str) -> None:
|
def load_decrypted(self, setting_txt: str) -> None:
|
||||||
@@ -104,13 +105,13 @@ class SettingTxt:
|
|||||||
setting_txt_dec = setting_str.encode()
|
setting_txt_dec = setting_str.encode()
|
||||||
global _key
|
global _key
|
||||||
# This could probably be made more efficient somehow.
|
# This could probably be made more efficient somehow.
|
||||||
setting_txt_enc: [int] = []
|
setting_txt_enc: List[int] = []
|
||||||
with io.BytesIO(setting_txt_dec) as setting_data:
|
with io.BytesIO(setting_txt_dec) as setting_data:
|
||||||
for i in range(0, len(setting_txt_dec)):
|
for i in range(0, len(setting_txt_dec)):
|
||||||
setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
|
||||||
_key = (_key << 1) | (_key >> 31)
|
_key = (_key << 1) | (_key >> 31)
|
||||||
setting_txt_enc = _pad_bytes(bytes(setting_txt_enc), 256)
|
setting_txt_bytes = _pad_bytes(bytes(setting_txt_enc), 256)
|
||||||
return setting_txt_enc
|
return setting_txt_bytes
|
||||||
|
|
||||||
def dump_decrypted(self) -> str:
|
def dump_decrypted(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class UidSys:
|
|||||||
The entries stored in the uid.sys file.
|
The entries stored in the uid.sys file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.uid_entries: List[_UidSysEntry] = []
|
self.uid_entries: List[_UidSysEntry] = []
|
||||||
|
|
||||||
def load(self, uid_sys: bytes) -> None:
|
def load(self, uid_sys: bytes) -> None:
|
||||||
@@ -77,7 +77,8 @@ class UidSys:
|
|||||||
|
|
||||||
def add(self, title_id: str | bytes) -> int:
|
def add(self, title_id: str | bytes) -> int:
|
||||||
"""
|
"""
|
||||||
Adds a new Title ID to the uid.sys file and returns the UID assigned to that title.
|
Adds a new Title ID to the uid.sys file and returns the UID assigned to that title. The new entry will only
|
||||||
|
be added if the provided Title ID doesn't already have an assigned UID.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -90,11 +91,8 @@ class UidSys:
|
|||||||
The UID assigned to the new Title ID.
|
The UID assigned to the new Title ID.
|
||||||
"""
|
"""
|
||||||
if type(title_id) is bytes:
|
if type(title_id) is bytes:
|
||||||
# This catches the format b'0000000100000002'
|
|
||||||
if len(title_id) == 16:
|
|
||||||
title_id_converted = title_id.encode()
|
|
||||||
# This catches the format b'\x00\x00\x00\x01\x00\x00\x00\x02'
|
# This catches the format b'\x00\x00\x00\x01\x00\x00\x00\x02'
|
||||||
elif len(title_id) == 8:
|
if len(title_id) == 8:
|
||||||
title_id_converted = binascii.hexlify(title_id).decode()
|
title_id_converted = binascii.hexlify(title_id).decode()
|
||||||
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
|
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
|
||||||
else:
|
else:
|
||||||
@@ -106,6 +104,11 @@ class UidSys:
|
|||||||
title_id_converted = title_id
|
title_id_converted = title_id
|
||||||
else:
|
else:
|
||||||
raise TypeError("Title ID type is not valid! It must be either type str or bytes.")
|
raise TypeError("Title ID type is not valid! It must be either type str or bytes.")
|
||||||
|
# Ensure this TID hasn't already been assigned a UID. If it has, just exit early and return the UID.
|
||||||
|
if self.uid_entries.count != 0:
|
||||||
|
for entry in self.uid_entries:
|
||||||
|
if entry.title_id == title_id_converted:
|
||||||
|
return entry.uid
|
||||||
# Generate the new UID by incrementing the current highest UID by 1.
|
# Generate the new UID by incrementing the current highest UID by 1.
|
||||||
try:
|
try:
|
||||||
new_uid = self.uid_entries[-1].uid + 1
|
new_uid = self.uid_entries[-1].uid + 1
|
||||||
|
|||||||
0
src/libWiiPy/py.typed
Normal file
0
src/libWiiPy/py.typed
Normal file
@@ -60,11 +60,11 @@ class Certificate:
|
|||||||
pub_key_exponent: int
|
pub_key_exponent: int
|
||||||
The exponent of this certificate's public key. Combined with the modulus to get the full key.
|
The exponent of this certificate's public key. Combined with the modulus to get the full key.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.type: CertificateType | None = None
|
self.type: CertificateType = CertificateType.RSA_4096
|
||||||
self.signature: bytes = b''
|
self.signature: bytes = b''
|
||||||
self.issuer: str = ""
|
self.issuer: str = ""
|
||||||
self.pub_key_type: CertificateKeyType | None = None
|
self.pub_key_type: CertificateKeyType = CertificateKeyType.RSA_4096
|
||||||
self.child_name: str = ""
|
self.child_name: str = ""
|
||||||
self.pub_key_id: int = 0
|
self.pub_key_id: int = 0
|
||||||
self.pub_key_modulus: int = 0
|
self.pub_key_modulus: int = 0
|
||||||
@@ -151,7 +151,7 @@ class CertificateChain:
|
|||||||
ticket_cert: Certificate
|
ticket_cert: Certificate
|
||||||
The XS (Ticket) certificate from the chain.
|
The XS (Ticket) certificate from the chain.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.ca_cert: Certificate = Certificate()
|
self.ca_cert: Certificate = Certificate()
|
||||||
self.tmd_cert: Certificate = Certificate()
|
self.tmd_cert: Certificate = Certificate()
|
||||||
self.ticket_cert: Certificate = Certificate()
|
self.ticket_cert: Certificate = Certificate()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ContentRegion:
|
|||||||
The total number of contents stored in the region.
|
The total number of contents stored in the region.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.content_records: List[_ContentRecord] = []
|
self.content_records: List[_ContentRecord] = []
|
||||||
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.
|
||||||
@@ -66,16 +66,16 @@ class ContentRegion:
|
|||||||
start_offset += 64 - (content.content_size % 64)
|
start_offset += 64 - (content.content_size % 64)
|
||||||
self.content_start_offsets.append(start_offset)
|
self.content_start_offsets.append(start_offset)
|
||||||
# Build a list of all the encrypted content data.
|
# Build a list of all the encrypted content data.
|
||||||
for content in range(self.num_contents):
|
for idx in range(self.num_contents):
|
||||||
# Seek to the start of the content based on the list of offsets.
|
# Seek to the start of the content based on the list of offsets.
|
||||||
content_region_data.seek(self.content_start_offsets[content])
|
content_region_data.seek(self.content_start_offsets[idx])
|
||||||
# Calculate the number of bytes we need to read by adding bytes up the nearest multiple of 16 if needed.
|
# Calculate the number of bytes we need to read by adding bytes up the nearest multiple of 16 if needed.
|
||||||
bytes_to_read = self.content_records[content].content_size
|
content_size = self.content_records[idx].content_size
|
||||||
if (bytes_to_read % 16) != 0:
|
if (content_size % 16) != 0:
|
||||||
bytes_to_read += 16 - (bytes_to_read % 16)
|
content_size += 16 - (content_size % 16)
|
||||||
# Read the file based on the size of the content in the associated record, then append that data to
|
# Read the file based on the size of the content in the associated record, then append that data to
|
||||||
# the list of content.
|
# the list of content.
|
||||||
content_enc = content_region_data.read(bytes_to_read)
|
content_enc = content_region_data.read(content_size)
|
||||||
self.content_list.append(content_enc)
|
self.content_list.append(content_enc)
|
||||||
|
|
||||||
def dump(self) -> tuple[bytes, int]:
|
def dump(self) -> tuple[bytes, int]:
|
||||||
@@ -336,8 +336,8 @@ class ContentRegion:
|
|||||||
enc_content = encrypt_content(dec_content, title_key, index)
|
enc_content = encrypt_content(dec_content, title_key, index)
|
||||||
self.add_enc_content(enc_content, cid, index, content_type, content_size, content_hash)
|
self.add_enc_content(enc_content, cid, index, content_type, content_size, content_hash)
|
||||||
|
|
||||||
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,
|
||||||
content_type: int = None) -> None:
|
cid: int | None = None, content_type: int | None = 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
|
||||||
@@ -373,8 +373,8 @@ class ContentRegion:
|
|||||||
self.content_list.append(b'')
|
self.content_list.append(b'')
|
||||||
self.content_list[index] = enc_content
|
self.content_list[index] = enc_content
|
||||||
|
|
||||||
def set_content(self, dec_content: bytes, index: int, title_key: bytes, cid: int = None,
|
def set_content(self, dec_content: bytes, index: int, title_key: bytes, cid: int | None = None,
|
||||||
content_type: int = None) -> None:
|
content_type: int | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -525,7 +525,7 @@ class SharedContentMap:
|
|||||||
The shared content records stored in content.map.
|
The shared content records stored in content.map.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.shared_records: List[_SharedContentRecord] = []
|
self.shared_records: List[_SharedContentRecord] = []
|
||||||
|
|
||||||
def load(self, content_map: bytes) -> None:
|
def load(self, content_map: bytes) -> None:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class IOSPatcher:
|
|||||||
dip_module_index : int
|
dip_module_index : int
|
||||||
The content index that DIP resides in and where DIP patches are applied. -1 if DIP patches are not applied.
|
The content index that DIP resides in and where DIP patches are applied. -1 if DIP patches are not applied.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.title: Title = Title()
|
self.title: Title = Title()
|
||||||
self.es_module_index: int = -1
|
self.es_module_index: int = -1
|
||||||
self.dip_module_index: int = -1
|
self.dip_module_index: int = -1
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
# See https://wiibrew.org/wiki/NUS for details about the NUS
|
# See https://wiibrew.org/wiki/NUS for details about the NUS
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
#import hashlib
|
||||||
from typing import List
|
from typing import Any, List, Protocol
|
||||||
from urllib.parse import urlparse as _urlparse
|
#from urllib.parse import urlparse as _urlparse
|
||||||
from .title import Title
|
from .title import Title
|
||||||
from .tmd import TMD
|
from .tmd import TMD
|
||||||
from .ticket import Ticket
|
from .ticket import Ticket
|
||||||
@@ -14,13 +14,36 @@ from .ticket import Ticket
|
|||||||
_nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"]
|
_nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"]
|
||||||
|
|
||||||
|
|
||||||
def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False,
|
class DownloadCallback(Protocol):
|
||||||
endpoint_override: str = None) -> Title:
|
"""
|
||||||
|
The format of a callable passed to a NUS download function.
|
||||||
|
"""
|
||||||
|
def __call__(self, done: int, total: int) -> Any:
|
||||||
|
"""
|
||||||
|
This function will be called with the current number of bytes downloaded and the total size of the file being
|
||||||
|
downloaded.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
done : int
|
||||||
|
The number of bytes already downloaded.
|
||||||
|
total : int
|
||||||
|
The total size of the file being downloaded.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def download_title(title_id: str, title_version: int | None = None, wiiu_endpoint: bool = False,
|
||||||
|
endpoint_override: str | None = None, progress: DownloadCallback = lambda done, total: None) -> Title:
|
||||||
"""
|
"""
|
||||||
Download an entire title and all of its contents, then load the downloaded components into a Title object for
|
Download an entire title and all of its contents, then load the downloaded components into a Title object for
|
||||||
further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
|
further use. This method is NOT recommended for general use, as it has extremely limited verbosity. It is instead
|
||||||
recommended to call the individual download methods instead to provide more flexibility and output.
|
recommended to call the individual download methods instead to provide more flexibility and output.
|
||||||
|
|
||||||
|
Be aware that you will receive fairly vague feedback from this function if you attach a progress callback. The
|
||||||
|
callback will be connected to each of the individual functions called by this function, but there will be no
|
||||||
|
indication of which function is currently running, just the progress of its download.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
title_id : str
|
title_id : str
|
||||||
@@ -32,27 +55,34 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
|
|||||||
endpoint_override: str, optional
|
endpoint_override: str, optional
|
||||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||||
set entirely overrides the "wiiu_endpoint" parameter.
|
set entirely overrides the "wiiu_endpoint" parameter.
|
||||||
|
progress: DownloadCallback, optional
|
||||||
|
A callback function used to return the progress of the downloads. The provided callable must match the signature
|
||||||
|
defined in DownloadCallback.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Title
|
Title
|
||||||
A Title object containing all the data from the downloaded title.
|
A Title object containing all the data from the downloaded title.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
libWiiPy.title.nus.DownloadCallback
|
||||||
"""
|
"""
|
||||||
# First, create the new title.
|
# First, create the new title.
|
||||||
title = Title()
|
title = Title()
|
||||||
# Download and load the certificate chain, TMD, and Ticket.
|
# Download and load the certificate chain, TMD, and Ticket.
|
||||||
title.load_cert_chain(download_cert_chain(wiiu_endpoint, endpoint_override))
|
title.load_cert_chain(download_cert_chain(wiiu_endpoint, endpoint_override))
|
||||||
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint, endpoint_override))
|
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint, endpoint_override, progress))
|
||||||
title.load_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override))
|
title.load_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override, progress))
|
||||||
# Download all contents
|
# Download all contents
|
||||||
title.load_content_records()
|
title.load_content_records()
|
||||||
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override)
|
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override, progress)
|
||||||
# Return the completed title.
|
# Return the completed title.
|
||||||
return title
|
return title
|
||||||
|
|
||||||
|
|
||||||
def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool = False,
|
def download_tmd(title_id: str, title_version: int | None = None, wiiu_endpoint: bool = False,
|
||||||
endpoint_override: str = None) -> bytes:
|
endpoint_override: str | None = None, progress: DownloadCallback = lambda done, total: None) -> bytes:
|
||||||
"""
|
"""
|
||||||
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
|
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
|
||||||
version if it was manually specified in the object.
|
version if it was manually specified in the object.
|
||||||
@@ -68,11 +98,18 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
|||||||
endpoint_override: str, optional
|
endpoint_override: str, optional
|
||||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||||
set entirely overrides the "wiiu_endpoint" parameter.
|
set entirely overrides the "wiiu_endpoint" parameter.
|
||||||
|
progress: DownloadCallback, optional
|
||||||
|
A callback function used to return the progress of the download. The provided callable must match the signature
|
||||||
|
defined in DownloadCallback.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bytes
|
bytes
|
||||||
The TMD file from the NUS.
|
The TMD file from the NUS.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
libWiiPy.title.nus.DownloadCallback
|
||||||
"""
|
"""
|
||||||
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
|
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
|
||||||
# when a specific version is requested.
|
# when a specific version is requested.
|
||||||
@@ -89,7 +126,7 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
|||||||
tmd_url += "." + str(title_version)
|
tmd_url += "." + str(title_version)
|
||||||
# Make the request.
|
# Make the request.
|
||||||
try:
|
try:
|
||||||
tmd_request = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
response = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
if endpoint_override:
|
if endpoint_override:
|
||||||
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
||||||
@@ -97,11 +134,16 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
|||||||
else:
|
else:
|
||||||
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
||||||
# Handle a 404 if the TID/version doesn't exist.
|
# Handle a 404 if the TID/version doesn't exist.
|
||||||
if tmd_request.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
|
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
|
||||||
" version and then try again.")
|
" version and then try again.")
|
||||||
# Save the raw TMD.
|
total_size = int(response.headers["Content-Length"])
|
||||||
raw_tmd = tmd_request.content
|
progress(0, total_size)
|
||||||
|
# Stream the TMD's data in chunks so that we can post updates to the callback function (assuming one was supplied).
|
||||||
|
raw_tmd = b""
|
||||||
|
for chunk in response.iter_content(512):
|
||||||
|
raw_tmd += chunk
|
||||||
|
progress(len(raw_tmd), total_size)
|
||||||
# Use a TMD object to load the data and then return only the actual TMD.
|
# Use a TMD object to load the data and then return only the actual TMD.
|
||||||
tmd_temp = TMD()
|
tmd_temp = TMD()
|
||||||
tmd_temp.load(raw_tmd)
|
tmd_temp.load(raw_tmd)
|
||||||
@@ -109,7 +151,8 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
|||||||
return tmd
|
return tmd
|
||||||
|
|
||||||
|
|
||||||
def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes:
|
def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_override: str | None = None,
|
||||||
|
progress: DownloadCallback = lambda done, total: None) -> bytes:
|
||||||
"""
|
"""
|
||||||
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
|
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
|
||||||
a free title.
|
a free title.
|
||||||
@@ -123,11 +166,18 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
|
|||||||
endpoint_override: str, optional
|
endpoint_override: str, optional
|
||||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||||
set entirely overrides the "wiiu_endpoint" parameter.
|
set entirely overrides the "wiiu_endpoint" parameter.
|
||||||
|
progress: DownloadCallback, optional
|
||||||
|
A callback function used to return the progress of the download. The provided callable must match the signature
|
||||||
|
defined in DownloadCallback.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bytes
|
bytes
|
||||||
The Ticket file from the NUS.
|
The Ticket file from the NUS.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
libWiiPy.title.nus.DownloadCallback
|
||||||
"""
|
"""
|
||||||
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
|
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
|
||||||
# title.
|
# title.
|
||||||
@@ -141,18 +191,23 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
|
|||||||
ticket_url = endpoint_url + title_id + "/cetk"
|
ticket_url = endpoint_url + title_id + "/cetk"
|
||||||
# Make the request.
|
# Make the request.
|
||||||
try:
|
try:
|
||||||
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
response = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
if endpoint_override:
|
if endpoint_override:
|
||||||
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
||||||
"override is valid.")
|
"override is valid.")
|
||||||
else:
|
else:
|
||||||
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
||||||
if ticket_request.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
|
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
|
||||||
" be downloaded for titles that are free on the NUS.")
|
" be downloaded for titles that are free on the NUS.")
|
||||||
# Save the raw cetk file.
|
total_size = int(response.headers["Content-Length"])
|
||||||
cetk = ticket_request.content
|
progress(0, total_size)
|
||||||
|
# Stream the Ticket's data just like with the TMD.
|
||||||
|
cetk = b""
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
cetk += chunk
|
||||||
|
progress(len(cetk), total_size)
|
||||||
# Use a Ticket object to load only the Ticket data from cetk and return it.
|
# Use a Ticket object to load only the Ticket data from cetk and return it.
|
||||||
ticket_temp = Ticket()
|
ticket_temp = Ticket()
|
||||||
ticket_temp.load(cetk)
|
ticket_temp.load(cetk)
|
||||||
@@ -160,7 +215,7 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
|
|||||||
return ticket
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
def download_cert_chain(wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes:
|
def download_cert_chain(wiiu_endpoint: bool = False, endpoint_override: str | None = None) -> bytes:
|
||||||
"""
|
"""
|
||||||
Downloads the signing certificate chain used by all WADs. This uses System Menu 4.3U as the source.
|
Downloads the signing certificate chain used by all WADs. This uses System Menu 4.3U as the source.
|
||||||
|
|
||||||
@@ -211,8 +266,8 @@ def download_cert_chain(wiiu_endpoint: bool = False, endpoint_override: str = No
|
|||||||
return cert_chain
|
return cert_chain
|
||||||
|
|
||||||
|
|
||||||
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False,
|
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False, endpoint_override: str | None = None,
|
||||||
endpoint_override: str = None) -> bytes:
|
progress: DownloadCallback = lambda done, total: None) -> bytes:
|
||||||
"""
|
"""
|
||||||
Downloads a specified content for the title specified in the object.
|
Downloads a specified content for the title specified in the object.
|
||||||
|
|
||||||
@@ -227,11 +282,18 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
|
|||||||
endpoint_override: str, optional
|
endpoint_override: str, optional
|
||||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||||
set entirely overrides the "wiiu_endpoint" parameter.
|
set entirely overrides the "wiiu_endpoint" parameter.
|
||||||
|
progress: DownloadCallback, optional
|
||||||
|
A callback function used to return the progress of the download. The provided callable must match the signature
|
||||||
|
defined in DownloadCallback.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bytes
|
bytes
|
||||||
The downloaded content.
|
The downloaded content.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
libWiiPy.title.nus.DownloadCallback
|
||||||
"""
|
"""
|
||||||
# Build the download URL. The structure is download/<TID>/<Content ID>.
|
# Build the download URL. The structure is download/<TID>/<Content ID>.
|
||||||
content_id_hex = hex(content_id)[2:]
|
content_id_hex = hex(content_id)[2:]
|
||||||
@@ -247,23 +309,29 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
|
|||||||
content_url = endpoint_url + title_id + "/000000" + content_id_hex
|
content_url = endpoint_url + title_id + "/000000" + content_id_hex
|
||||||
# Make the request.
|
# Make the request.
|
||||||
try:
|
try:
|
||||||
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
response = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
if endpoint_override:
|
if endpoint_override:
|
||||||
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
|
||||||
"override is valid.")
|
"override is valid.")
|
||||||
else:
|
else:
|
||||||
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
|
||||||
if content_request.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
|
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
|
||||||
" content records provided.\n Failed while downloading Content ID: 000000" +
|
" content records provided.\n Failed while downloading Content ID: 000000" +
|
||||||
content_id_hex)
|
content_id_hex)
|
||||||
content_data = content_request.content
|
total_size = int(response.headers["Content-Length"])
|
||||||
return content_data
|
progress(0, total_size)
|
||||||
|
# Stream the content just like the TMD/Ticket.
|
||||||
|
content = b""
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
content += chunk
|
||||||
|
progress(len(content), total_size)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
|
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False, endpoint_override: str | None = None,
|
||||||
endpoint_override: str = None) -> List[bytes]:
|
progress: DownloadCallback = lambda done, total: None) -> List[bytes]:
|
||||||
"""
|
"""
|
||||||
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
|
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
|
||||||
so that the content records can be accessed.
|
so that the content records can be accessed.
|
||||||
@@ -279,11 +347,18 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
|
|||||||
endpoint_override: str, optional
|
endpoint_override: str, optional
|
||||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||||
set entirely overrides the "wiiu_endpoint" parameter.
|
set entirely overrides the "wiiu_endpoint" parameter.
|
||||||
|
progress: DownloadCallback, optional
|
||||||
|
A callback function used to return the progress of the downloads. The provided callable must match the signature
|
||||||
|
defined in DownloadCallback.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
List[bytes]
|
List[bytes]
|
||||||
A list of all the downloaded contents.
|
A list of all the downloaded contents.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
libWiiPy.title.nus.DownloadCallback
|
||||||
"""
|
"""
|
||||||
# Retrieve the content records from the TMD.
|
# Retrieve the content records from the TMD.
|
||||||
content_records = tmd.content_records
|
content_records = tmd.content_records
|
||||||
@@ -295,7 +370,7 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
|
|||||||
content_list = []
|
content_list = []
|
||||||
for content_id in content_ids:
|
for content_id in content_ids:
|
||||||
# Call self.download_content() for each Content ID.
|
# Call self.download_content() for each Content ID.
|
||||||
content = download_content(title_id, content_id, wiiu_endpoint, endpoint_override)
|
content = download_content(title_id, content_id, wiiu_endpoint, endpoint_override, progress)
|
||||||
content_list.append(content)
|
content_list.append(content)
|
||||||
return content_list
|
return content_list
|
||||||
|
|
||||||
@@ -315,9 +390,10 @@ def _validate_endpoint(endpoint: str) -> str:
|
|||||||
The validated NUS endpoint with the proper path.
|
The validated NUS endpoint with the proper path.
|
||||||
"""
|
"""
|
||||||
# Find the root of the URL and then assemble the correct URL based on that.
|
# Find the root of the URL and then assemble the correct URL based on that.
|
||||||
new_url = _urlparse(endpoint)
|
# TODO: Rewrite in a way that makes more sense and un-stub
|
||||||
if new_url.netloc == "":
|
#new_url = _urlparse(endpoint)
|
||||||
endpoint_url = "http://" + new_url.path + "/ccs/download/"
|
#if new_url.netloc == "":
|
||||||
else:
|
# endpoint_url = "http://" + new_url.path + "/ccs/download/"
|
||||||
endpoint_url = "http://" + new_url.netloc + "/ccs/download/"
|
#else:
|
||||||
return endpoint_url
|
# endpoint_url = "http://" + new_url.netloc + "/ccs/download/"
|
||||||
|
return endpoint
|
||||||
|
|||||||
@@ -57,7 +57,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):
|
def __init__(self) -> None:
|
||||||
# If this is a dev ticket
|
# If this is a dev ticket
|
||||||
self.is_dev: bool = False # Defaults to false, set to true during load if this ticket is using dev certs.
|
self.is_dev: bool = False # Defaults to false, set to true during load if this ticket is using dev certs.
|
||||||
# Signature blob header
|
# Signature blob header
|
||||||
@@ -75,7 +75,7 @@ class Ticket:
|
|||||||
self.title_version: int = 0 # Version of the ticket's associated title.
|
self.title_version: int = 0 # Version of the ticket's associated title.
|
||||||
self.permitted_titles: bytes = b'' # Permitted titles mask
|
self.permitted_titles: bytes = b'' # Permitted titles mask
|
||||||
# "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the
|
# "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the
|
||||||
# Permitted Titles Mask."
|
# Permitted Titles Mask." -WiiBrew
|
||||||
self.permit_mask: bytes = b''
|
self.permit_mask: bytes = b''
|
||||||
self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not.
|
self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not.
|
||||||
self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key
|
self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key
|
||||||
@@ -128,7 +128,7 @@ class Ticket:
|
|||||||
self.console_id = int.from_bytes(ticket_data.read(4))
|
self.console_id = int.from_bytes(ticket_data.read(4))
|
||||||
# Title ID.
|
# Title ID.
|
||||||
ticket_data.seek(0x1DC)
|
ticket_data.seek(0x1DC)
|
||||||
self.title_id = binascii.hexlify(ticket_data.read(8))
|
self.title_id = ticket_data.read(8)
|
||||||
# Unknown data 1.
|
# Unknown data 1.
|
||||||
ticket_data.seek(0x1E4)
|
ticket_data.seek(0x1E4)
|
||||||
self.unknown1 = ticket_data.read(2)
|
self.unknown1 = ticket_data.read(2)
|
||||||
@@ -202,7 +202,7 @@ class Ticket:
|
|||||||
# Console ID.
|
# Console ID.
|
||||||
ticket_data += int.to_bytes(self.console_id, 4)
|
ticket_data += int.to_bytes(self.console_id, 4)
|
||||||
# Title ID.
|
# Title ID.
|
||||||
ticket_data += binascii.unhexlify(self.title_id)
|
ticket_data += self.title_id
|
||||||
# Unknown data 1.
|
# Unknown data 1.
|
||||||
ticket_data += self.unknown1
|
ticket_data += self.unknown1
|
||||||
# Title version.
|
# Title version.
|
||||||
@@ -318,6 +318,8 @@ class Ticket:
|
|||||||
return "Korean"
|
return "Korean"
|
||||||
case 2:
|
case 2:
|
||||||
return "vWii"
|
return "vWii"
|
||||||
|
case _:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
def get_title_key(self) -> bytes:
|
def get_title_key(self) -> bytes:
|
||||||
"""
|
"""
|
||||||
@@ -343,7 +345,7 @@ class Ticket:
|
|||||||
"""
|
"""
|
||||||
if len(title_id) != 16:
|
if len(title_id) != 16:
|
||||||
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
||||||
self.title_id = title_id.encode()
|
self.title_id = binascii.unhexlify(title_id.encode())
|
||||||
|
|
||||||
def set_title_version(self, new_version: str | int) -> None:
|
def set_title_version(self, new_version: str | int) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ 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):
|
def __init__(self) -> None:
|
||||||
self.wad: _WAD = _WAD()
|
self.wad: _WAD = _WAD()
|
||||||
self.cert_chain: _CertificateChain = _CertificateChain()
|
self.cert_chain: _CertificateChain = _CertificateChain()
|
||||||
self.tmd: _TMD = _TMD()
|
self.tmd: _TMD = _TMD()
|
||||||
@@ -178,7 +178,7 @@ class Title:
|
|||||||
self.tmd.set_title_version(title_version)
|
self.tmd.set_title_version(title_version)
|
||||||
self.ticket.set_title_version(title_version)
|
self.ticket.set_title_version(title_version)
|
||||||
|
|
||||||
def get_content_by_index(self, index: id, skip_hash=False) -> bytes:
|
def get_content_by_index(self, index: int, skip_hash=False) -> bytes:
|
||||||
"""
|
"""
|
||||||
Gets an individual content from the content region based on the provided index, in decrypted form.
|
Gets an individual content from the content region based on the provided index, in decrypted form.
|
||||||
|
|
||||||
@@ -194,6 +194,8 @@ class Title:
|
|||||||
bytes
|
bytes
|
||||||
The decrypted content listed in the content record.
|
The decrypted content listed in the content record.
|
||||||
"""
|
"""
|
||||||
|
if self.ticket.title_id == "":
|
||||||
|
raise ValueError("A Ticket must be loaded to get decrypted content.")
|
||||||
dec_content = self.content.get_content_by_index(index, self.ticket.get_title_key(), skip_hash)
|
dec_content = self.content.get_content_by_index(index, self.ticket.get_title_key(), skip_hash)
|
||||||
return dec_content
|
return dec_content
|
||||||
|
|
||||||
@@ -213,6 +215,8 @@ class Title:
|
|||||||
bytes
|
bytes
|
||||||
The decrypted content listed in the content record.
|
The decrypted content listed in the content record.
|
||||||
"""
|
"""
|
||||||
|
if self.ticket.title_id == "":
|
||||||
|
raise ValueError("A Ticket must be loaded to get decrypted content.")
|
||||||
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
|
||||||
|
|
||||||
@@ -317,8 +321,8 @@ class Title:
|
|||||||
# Update the TMD to match.
|
# Update the TMD to match.
|
||||||
self.tmd.content_records = self.content.content_records
|
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,
|
||||||
content_type: int = None) -> None:
|
cid: int | None = None, content_type: int | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the content at the provided index to the provided new encrypted content. The provided hash and content size
|
Sets the content at the provided 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 specified, but if it
|
are set in the corresponding content record. A new Content ID or content type can also be specified, but if it
|
||||||
@@ -346,7 +350,8 @@ class Title:
|
|||||||
# Update the TMD to match.
|
# Update the TMD to match.
|
||||||
self.tmd.content_records = self.content.content_records
|
self.tmd.content_records = self.content.content_records
|
||||||
|
|
||||||
def set_content(self, dec_content: bytes, index: int, cid: int = None, content_type: int = None) -> None:
|
def set_content(self, dec_content: bytes, index: int, cid: int | None = None,
|
||||||
|
content_type: int | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the content at the provided index to the provided new decrypted content. The hash and content size of this
|
Sets the content at the provided 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 type can
|
content will be generated and then set in the corresponding content record. A new Content ID or content type can
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from typing import List
|
|||||||
from enum import IntEnum as _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_standard_to_dec
|
||||||
|
|
||||||
|
|
||||||
class TMD:
|
class TMD:
|
||||||
@@ -34,9 +34,9 @@ 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):
|
def __init__(self) -> None:
|
||||||
self.blob_header: bytes = b''
|
self.blob_header: bytes = b''
|
||||||
self.signature_type: int = 0
|
self.signature_type: bytes = b''
|
||||||
self.signature: bytes = b''
|
self.signature: bytes = b''
|
||||||
self.signature_issuer: str = "" # 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?
|
||||||
@@ -55,7 +55,6 @@ class TMD:
|
|||||||
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
|
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
|
||||||
self.access_rights: int = 0
|
self.access_rights: int = 0
|
||||||
self.title_version: int = 0 # The version of the associated title.
|
self.title_version: int = 0 # The version of the associated title.
|
||||||
self.title_version_converted: int = 0 # The title version in vX.X format.
|
|
||||||
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 # The content index that contains the bootable executable.
|
self.boot_index: int = 0 # The content index that contains the bootable executable.
|
||||||
self.minor_version: int = 0 # Minor version (unused typically).
|
self.minor_version: int = 0 # Minor version (unused typically).
|
||||||
@@ -137,8 +136,6 @@ class TMD:
|
|||||||
# Version number straight from the TMD.
|
# Version number straight from the TMD.
|
||||||
tmd_data.seek(0x1DC)
|
tmd_data.seek(0x1DC)
|
||||||
self.title_version = int.from_bytes(tmd_data.read(2))
|
self.title_version = int.from_bytes(tmd_data.read(2))
|
||||||
# Calculate the converted version number via util module.
|
|
||||||
self.title_version_converted = title_ver_dec_to_standard(self.title_version, self.title_id, bool(self.vwii))
|
|
||||||
# The number of contents listed in the TMD.
|
# The number of contents listed in the TMD.
|
||||||
tmd_data.seek(0x1DE)
|
tmd_data.seek(0x1DE)
|
||||||
self.num_contents = int.from_bytes(tmd_data.read(2))
|
self.num_contents = int.from_bytes(tmd_data.read(2))
|
||||||
@@ -305,6 +302,8 @@ class TMD:
|
|||||||
return "None"
|
return "None"
|
||||||
case 4:
|
case 4:
|
||||||
return "KOR"
|
return "KOR"
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Title contains unknown region \"{self.region}\".")
|
||||||
|
|
||||||
def get_title_type(self) -> str:
|
def get_title_type(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -500,7 +499,7 @@ class TMD:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
new_version : str, int
|
new_version : str, int
|
||||||
The new version of the title. See description for valid formats.
|
The new version of the title.
|
||||||
"""
|
"""
|
||||||
if type(new_version) is str:
|
if type(new_version) is str:
|
||||||
# Validate string input is in the correct format, then validate that the version isn't higher than v255.0.
|
# Validate string input is in the correct format, then validate that the version isn't higher than v255.0.
|
||||||
@@ -510,8 +509,7 @@ class TMD:
|
|||||||
raise ValueError("Title version is not valid! String version must be entered in format \"X.X\".")
|
raise ValueError("Title version is not valid! String version must be entered in format \"X.X\".")
|
||||||
if int(version_str_split[0]) > 255 or int(version_str_split[1]) > 255:
|
if int(version_str_split[0]) > 255 or int(version_str_split[1]) > 255:
|
||||||
raise ValueError("Title version is not valid! String version number cannot exceed v255.255.")
|
raise ValueError("Title version is not valid! String version number cannot exceed v255.255.")
|
||||||
self.title_version_converted = new_version
|
version_converted: int = title_ver_standard_to_dec(new_version, self.title_id)
|
||||||
version_converted = title_ver_standard_to_dec(new_version, self.title_id)
|
|
||||||
self.title_version = version_converted
|
self.title_version = version_converted
|
||||||
elif type(new_version) is int:
|
elif type(new_version) is int:
|
||||||
# Validate that the version isn't higher than v65280. If the check passes, set that as the title version,
|
# Validate that the version isn't higher than v65280. If the check passes, set that as the title version,
|
||||||
@@ -519,7 +517,5 @@ class TMD:
|
|||||||
if new_version > 65535:
|
if new_version > 65535:
|
||||||
raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.")
|
raise ValueError("Title version is not valid! Integer version number cannot exceed v65535.")
|
||||||
self.title_version = new_version
|
self.title_version = new_version
|
||||||
version_converted = title_ver_dec_to_standard(new_version, self.title_id, bool(self.vwii))
|
|
||||||
self.title_version_converted = version_converted
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("Title version type is not valid! Type must be either integer or string.")
|
raise TypeError("Title version type is not valid! Type must be either integer or string.")
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class WAD:
|
|||||||
wad_meta_size : int
|
wad_meta_size : int
|
||||||
The size of the WAD's meta/footer.
|
The size of the WAD's meta/footer.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.wad_hdr_size: int = 64
|
self.wad_hdr_size: int = 64
|
||||||
self.wad_type: str = "Is"
|
self.wad_type: str = "Is"
|
||||||
self.wad_version: bytes = b'\x00\x00'
|
self.wad_version: bytes = b'\x00\x00'
|
||||||
@@ -49,17 +49,17 @@ class WAD:
|
|||||||
self.wad_content_data: bytes = b''
|
self.wad_content_data: bytes = b''
|
||||||
self.wad_meta_data: bytes = b''
|
self.wad_meta_data: bytes = b''
|
||||||
|
|
||||||
def load(self, wad_data: bytes) -> None:
|
def load(self, wad: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
|
Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
|
||||||
existing WAD file.
|
existing WAD file.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
wad_data : bytes
|
wad : bytes
|
||||||
The data for the WAD file to load.
|
The data for the WAD file to load.
|
||||||
"""
|
"""
|
||||||
with io.BytesIO(wad_data) as wad_data:
|
with io.BytesIO(wad) as wad_data:
|
||||||
# Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two
|
# Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two
|
||||||
# different types of WADs that might be encountered.
|
# different types of WADs that might be encountered.
|
||||||
wad_data.seek(0x0)
|
wad_data.seek(0x0)
|
||||||
@@ -311,7 +311,7 @@ class WAD:
|
|||||||
# Calculate the size of the new Ticket data.
|
# Calculate the size of the new Ticket data.
|
||||||
self.wad_tik_size = len(tik_data)
|
self.wad_tik_size = len(tik_data)
|
||||||
|
|
||||||
def set_content_data(self, content_data, size: int = None) -> None:
|
def set_content_data(self, content_data, size: int | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the content data of the WAD. Also calculates the new size.
|
Sets the content data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user