Added SharedContentMap to content module to handle content.map files

This commit is contained in:
Campbell 2024-07-27 19:22:21 -04:00
parent 102da808e6
commit 817a2c9ac5
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344

View File

@ -2,10 +2,11 @@
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
# #
# See https://wiibrew.org/wiki/Title for details about how titles are formatted # See https://wiibrew.org/wiki/Title for details about how titles are formatted
import binascii
import io import io
import hashlib import hashlib
from typing import List from typing import List
from dataclasses import dataclass as _dataclass
from ..types import _ContentRecord from ..types import _ContentRecord
from ..shared import _pad_bytes, _align_value from ..shared import _pad_bytes, _align_value
from .crypto import decrypt_content, encrypt_content from .crypto import decrypt_content, encrypt_content
@ -483,3 +484,109 @@ class ContentRegion:
# needs to accommodate that. Seems to only apply to custom WADs ? (Like cIOS WADs?) # needs to accommodate that. Seems to only apply to custom WADs ? (Like cIOS WADs?)
enc_content = encrypt_content(dec_content, title_key, index) enc_content = encrypt_content(dec_content, title_key, index)
self.content_list[target_index] = enc_content self.content_list[target_index] = enc_content
@_dataclass
class _SharedContentRecord:
"""
A _SharedContentRecord object used to store the data of a specific content stored in /shared1/. Private class used
by the content module.
Attributes
----------
shared_id : str
The incremental ID used to store the shared content.
content_hash : bytes
The SHA-1 hash of the shared content.
"""
shared_id: str
content_hash: bytes
class SharedContentMap:
"""
A SharedContentMap object to parse and edit the content.map file stored in /shared1/ on the Wii's NAND. This file is
used to keep track of all shared contents installed on the console.
Attributes
----------
shared_records : List[_SharedContentRecord]
The shared content records stored in content.map.
"""
def __init__(self):
self.shared_records: List[_SharedContentRecord] = []
def load(self, content_map: bytes) -> None:
"""
Loads the raw content map and parses the records in it.
Parameters
----------
content_map : bytes
The data of a content.map file.
"""
# Sanity check to ensure the length is divisible by 28 bytes. If it isn't, then it is malformed.
if (len(content_map) % 28) != 0:
raise ValueError("The provided content map appears to be corrupted!")
entry_count = len(content_map) // 28
with io.BytesIO(content_map) as map_data:
for i in range(entry_count):
shared_id = str(map_data.read(8).decode())
content_hash = binascii.hexlify(map_data.read(20))
self.shared_records.append(_SharedContentRecord(shared_id, content_hash))
def dump(self) -> bytes:
"""
Dumps the SharedContentMap object back into a content.map file.
Returns
-------
bytes
The raw data of the content.map file.
"""
map_data = b''
for record in self.shared_records:
map_data += record.shared_id.encode()
map_data += binascii.unhexlify(record.content_hash)
return map_data
def add_content(self, content_hash: str | bytes) -> str:
"""
Adds a new shared content SHA-1 hash to the content map and returns the file name assigned to that hash.
Parameters
----------
content_hash : str, bytes
The SHA-1 hash of the new shared content.
Returns
-------
str
The filename assigned to the provided content hash.
"""
content_hash_converted = b''
if type(content_hash) is bytes:
# This catches the format b'GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG'
if len(content_hash) == 40:
pass
# This catches the format
# b'\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG\xGG'
elif len(content_hash) == 20:
content_hash_converted = binascii.hexlify(content_hash)
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
else:
raise ValueError("SHA-1 hash is not valid!")
# Allow for a string like "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"
elif type(content_hash) is str:
content_hash_converted = binascii.unhexlify(content_hash)
# If the hash isn't bytes or a string, it isn't valid and is rejected.
else:
raise TypeError("SHA-1 hash type is not valid! It must be either type str or bytes.")
# Generate the file name for the new shared content by incrementing the highest name by 1. Thank you, Nintendo,
# for not just storing these as integers like you did EVERYWHERE else.
maximum_index = int(self.shared_records[-1].shared_id, 16)
new_index = f"{maximum_index + 1:08X}"
self.shared_records.append(_SharedContentRecord(new_index, content_hash_converted))
return new_index