mirror of
https://github.com/grp/Wii.py.git
synced 2025-06-19 07:15:49 -04:00
Merge branch 'master' of git@github.com:icefire/Wii.py
This commit is contained in:
commit
faf47a0f89
377
U8.py
377
U8.py
@ -1,377 +0,0 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
|
|
||||||
class U8():
|
|
||||||
"""This class can unpack and pack U8 archives, which are used all over the Wii. They are often used in Banners and contents in Downloadable Titles. Please remove all headers and compression first, kthx.
|
|
||||||
|
|
||||||
The f parameter is either the source folder to pack, or the source file to unpack."""
|
|
||||||
class U8Header(Struct):
|
|
||||||
__endian__ = Struct.BE
|
|
||||||
def __format__(self):
|
|
||||||
self.tag = Struct.string(4)
|
|
||||||
self.rootnode_offset = Struct.uint32
|
|
||||||
self.header_size = Struct.uint32
|
|
||||||
self.data_offset = Struct.uint32
|
|
||||||
self.zeroes = Struct.string(16)
|
|
||||||
class U8Node(Struct):
|
|
||||||
__endian__ = Struct.BE
|
|
||||||
def __format__(self):
|
|
||||||
self.type = Struct.uint16
|
|
||||||
self.name_offset = Struct.uint16
|
|
||||||
self.data_offset = Struct.uint32
|
|
||||||
self.size = Struct.uint32
|
|
||||||
def __init__(self, f):
|
|
||||||
self.f = f
|
|
||||||
def _pack(self, file, recursion, is_root = 0): #internal
|
|
||||||
node = self.U8Node()
|
|
||||||
node.name_offset = len(self.strings)
|
|
||||||
if(is_root != 1):
|
|
||||||
self.strings += (file)
|
|
||||||
self.strings += ("\x00")
|
|
||||||
|
|
||||||
if(os.path.isdir(file)):
|
|
||||||
node.type = 0x0100
|
|
||||||
node.data_offset = recursion - 1
|
|
||||||
recursion += 1
|
|
||||||
files = sorted(os.listdir(file))
|
|
||||||
#if(sorted(files) == ["banner.bin", "icon.bin", "sound.bin"]):
|
|
||||||
# files = ["icon.bin", "banner.bin", "sound.bin"]
|
|
||||||
|
|
||||||
oldsz = len(self.nodes)
|
|
||||||
if(is_root != 1):
|
|
||||||
self.nodes.append(node)
|
|
||||||
|
|
||||||
os.chdir(file)
|
|
||||||
for entry in files:
|
|
||||||
if(entry != ".DS_Store" and entry[len(entry) - 4:] != "_out"):
|
|
||||||
self._pack(entry, recursion)
|
|
||||||
os.chdir("..")
|
|
||||||
|
|
||||||
self.nodes[oldsz].size = len(self.nodes) + 1
|
|
||||||
else:
|
|
||||||
f = open(file, "rb")
|
|
||||||
data = f.read()
|
|
||||||
f.close()
|
|
||||||
sz = len(data)
|
|
||||||
data += "\x00" * (align(sz, 32) - sz) #32 seems to work best for fuzzyness? I'm still really not sure
|
|
||||||
node.data_offset = len(self.data)
|
|
||||||
self.data += data
|
|
||||||
node.size = sz
|
|
||||||
node.type = 0x0000
|
|
||||||
if(is_root != 1):
|
|
||||||
self.nodes.append(node)
|
|
||||||
|
|
||||||
def pack(self, fn = ""):
|
|
||||||
"""This function will pack a folder into a U8 archive. The output file name is specified in the parameter fn. If fn is an empty string, the filename is deduced from the input folder name. Returns the output filename.
|
|
||||||
|
|
||||||
This creates valid U8 archives for all purposes."""
|
|
||||||
header = self.U8Header()
|
|
||||||
rootnode = self.U8Node()
|
|
||||||
|
|
||||||
header.tag = "U\xAA8-"
|
|
||||||
header.rootnode_offset = 0x20
|
|
||||||
header.zeroes = "\x00" * 16
|
|
||||||
|
|
||||||
self.nodes = []
|
|
||||||
self.strings = "\x00"
|
|
||||||
self.data = ""
|
|
||||||
origdir = os.getcwd()
|
|
||||||
os.chdir(self.f)
|
|
||||||
self._pack(".", 0, 1)
|
|
||||||
os.chdir(origdir)
|
|
||||||
|
|
||||||
header.header_size = (len(self.nodes) + 1) * len(rootnode) + len(self.strings)
|
|
||||||
header.data_offset = align(header.header_size + header.rootnode_offset, 64)
|
|
||||||
rootnode.size = len(self.nodes) + 1
|
|
||||||
rootnode.type = 0x0100
|
|
||||||
|
|
||||||
for i in range(len(self.nodes)):
|
|
||||||
if(self.nodes[i].type == 0x0000):
|
|
||||||
self.nodes[i].data_offset += header.data_offset
|
|
||||||
|
|
||||||
if(fn == ""):
|
|
||||||
if(self.f[len(self.f) - 4:] == "_out"):
|
|
||||||
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:-4].replace("_", ".")
|
|
||||||
else:
|
|
||||||
fn = self.f
|
|
||||||
|
|
||||||
fd = open(fn, "wb")
|
|
||||||
fd.write(header.pack())
|
|
||||||
fd.write(rootnode.pack())
|
|
||||||
for node in self.nodes:
|
|
||||||
fd.write(node.pack())
|
|
||||||
fd.write(self.strings)
|
|
||||||
fd.write("\x00" * (header.data_offset - header.rootnode_offset - header.header_size))
|
|
||||||
fd.write(self.data)
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
return fn
|
|
||||||
|
|
||||||
def unpack(self, fn = ""):
|
|
||||||
"""This will unpack the U8 archive specified by the initialization parameter into either the folder specified by the parameter fn, or into a folder created with this formula:
|
|
||||||
``filename_extension_out``
|
|
||||||
|
|
||||||
This will recreate the directory structure, including the initial root folder in the U8 archive (such as "arc" or "meta"). Returns the output directory name."""
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
|
|
||||||
header = self.U8Header()
|
|
||||||
header.unpack(data[offset:offset + len(header)])
|
|
||||||
offset += len(header)
|
|
||||||
|
|
||||||
if(header.tag != "U\xAA8-"):
|
|
||||||
raise NameError("Bad U8 Tag")
|
|
||||||
offset = header.rootnode_offset
|
|
||||||
|
|
||||||
rootnode = self.U8Node()
|
|
||||||
rootnode.unpack(data[offset:offset + len(rootnode)])
|
|
||||||
offset += len(rootnode)
|
|
||||||
|
|
||||||
nodes = []
|
|
||||||
for i in range(rootnode.size - 1):
|
|
||||||
node = self.U8Node()
|
|
||||||
node.unpack(data[offset:offset + len(node)])
|
|
||||||
offset += len(node)
|
|
||||||
nodes.append(node)
|
|
||||||
|
|
||||||
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
|
|
||||||
offset += len(strings)
|
|
||||||
|
|
||||||
if(fn == ""):
|
|
||||||
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f).replace(".", "_") + "_out"
|
|
||||||
try:
|
|
||||||
origdir = os.getcwd()
|
|
||||||
os.mkdir(fn)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
os.chdir(fn)
|
|
||||||
|
|
||||||
recursion = [rootnode.size]
|
|
||||||
counter = 0
|
|
||||||
for node in nodes:
|
|
||||||
counter += 1
|
|
||||||
name = strings[node.name_offset:].split('\0', 1)[0]
|
|
||||||
|
|
||||||
if(node.type == 0x0100): #folder
|
|
||||||
recursion.append(node.size)
|
|
||||||
try:
|
|
||||||
os.mkdir(name)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
os.chdir(name)
|
|
||||||
continue
|
|
||||||
elif(node.type == 0): #file
|
|
||||||
file = open(name, "wb")
|
|
||||||
file.write(data[node.data_offset:node.data_offset + node.size])
|
|
||||||
offset += node.size
|
|
||||||
else: #unknown
|
|
||||||
pass #ignore
|
|
||||||
|
|
||||||
sz = recursion.pop()
|
|
||||||
if(sz == counter + 1):
|
|
||||||
os.chdir("..")
|
|
||||||
else:
|
|
||||||
recursion.append(sz)
|
|
||||||
os.chdir("..")
|
|
||||||
|
|
||||||
os.chdir(origdir)
|
|
||||||
return fn
|
|
||||||
def __str__(self):
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
|
|
||||||
header = self.U8Header()
|
|
||||||
header.unpack(data[offset:offset + len(header)])
|
|
||||||
offset += len(header)
|
|
||||||
|
|
||||||
if(header.tag != "U\xAA8-"):
|
|
||||||
raise NameError("Bad U8 Tag")
|
|
||||||
offset = header.rootnode_offset
|
|
||||||
|
|
||||||
rootnode = self.U8Node()
|
|
||||||
rootnode.unpack(data[offset:offset + len(rootnode)])
|
|
||||||
offset += len(rootnode)
|
|
||||||
|
|
||||||
nodes = []
|
|
||||||
for i in xrange(rootnode.size - 1):
|
|
||||||
node = self.U8Node()
|
|
||||||
node.unpack(data[offset:offset + len(node)])
|
|
||||||
offset += len(node)
|
|
||||||
nodes.append(node)
|
|
||||||
|
|
||||||
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
|
|
||||||
offset += len(strings)
|
|
||||||
|
|
||||||
out = "[root]\n"
|
|
||||||
recursion = [rootnode.size]
|
|
||||||
counter = 0
|
|
||||||
for node in nodes:
|
|
||||||
counter += 1
|
|
||||||
name = strings[node.name_offset:].split('\0', 1)[0]
|
|
||||||
out += " " * len(recursion)
|
|
||||||
if(node.type == 0x0100): #folder
|
|
||||||
recursion.append(node.size)
|
|
||||||
out += "[%s]\n" % name
|
|
||||||
continue
|
|
||||||
elif(node.type == 0): #file
|
|
||||||
out += "%s\n" % name
|
|
||||||
offset += node.size
|
|
||||||
else: #unknown, ignore
|
|
||||||
pass
|
|
||||||
|
|
||||||
sz = recursion.pop()
|
|
||||||
if(sz == counter + 1):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
recursion.append(sz)
|
|
||||||
return out
|
|
||||||
|
|
||||||
class IMD5():
|
|
||||||
"""This class can add and remove IMD5 headers to files. The parameter f is the file to use for the addition or removal of the header. IMD5 headers are found in banner.bin, icon.bin, and sound.bin."""
|
|
||||||
class IMD5Header(Struct):
|
|
||||||
__endian__ = Struct.BE
|
|
||||||
def __format__(self):
|
|
||||||
self.tag = Struct.string(4)
|
|
||||||
self.size = Struct.uint32
|
|
||||||
self.zeroes = Struct.uint8[8]
|
|
||||||
self.crypto = Struct.string(16)
|
|
||||||
def __init__(self, f):
|
|
||||||
self.f = f
|
|
||||||
def add(self, fn = ""):
|
|
||||||
"""This function adds an IMD5 header to the file specified by f in the initializer. The output file is specified with fn, if it is empty, it will overwrite the input file. If the file already has an IMD5 header, it will now have two. Returns the output filename."""
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
|
|
||||||
imd5 = self.IMD5Header()
|
|
||||||
for i in range(8):
|
|
||||||
imd5.zeroes[i] = 0x00
|
|
||||||
imd5.tag = "IMD5"
|
|
||||||
imd5.size = len(data)
|
|
||||||
imd5.crypto = str(Crypto().createMD5Hash(data))
|
|
||||||
data = imd5.pack() + data
|
|
||||||
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
open(self.f, "wb").write(data)
|
|
||||||
return self.f
|
|
||||||
def remove(self, fn = ""):
|
|
||||||
"""This will remove an IMD5 header from the file specified in f, if one exists. If there is no IMD5 header, it will output the file as it is. It will output in the parameter fn if available, otherwise it will overwrite the source. Returns the output filename."""
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
imd5 = self.IMD5Header()
|
|
||||||
if(data[:4] != "IMD5"):
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
return self.f
|
|
||||||
data = data[len(imd5):]
|
|
||||||
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
open(self.f, "wb").write(data)
|
|
||||||
return self.f
|
|
||||||
|
|
||||||
class IMET():
|
|
||||||
"""IMET headers are found in Opening.bnr and 0000000.app files. They contain the channel titles and more metadata about channels. They are in two different formats with different amounts of padding before the start of the IMET header. This class suports both.
|
|
||||||
|
|
||||||
The parameter f is used to specify the input file name."""
|
|
||||||
class IMETHeader(Struct):
|
|
||||||
__endian__ = Struct.BE
|
|
||||||
def __format__(self):
|
|
||||||
self.zeroes = Struct.uint8[128]
|
|
||||||
self.tag = Struct.string(4)
|
|
||||||
self.unk = Struct.uint64
|
|
||||||
self.sizes = Struct.uint32[3] #icon, banner, sound
|
|
||||||
self.unk2 = Struct.uint32
|
|
||||||
self.names = Struct.string(84, encoding = "utf-16-be", stripNulls = True)[7]
|
|
||||||
self.zeroes2 = Struct.uint8[840]
|
|
||||||
self.hash = Struct.string(16)
|
|
||||||
def __init__(self, f):
|
|
||||||
self.f = f
|
|
||||||
def add(self, iconsz, bannersz, soundsz, name = "", langs = [], fn = ""):
|
|
||||||
"""This function adds an IMET header to the file specified with f in the initializer. The file will be output to fn if it is not empty, otherwise it will overwrite the input file. You must specify the size of banner.bin in bannersz, and respectivly for iconsz and soundsz. langs is an optional arguement that is a list of different langauge channel titles. name is the english name that is copied everywhere in langs that there is an empty string. Returns the output filename."""
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
imet = self.IMETHeader()
|
|
||||||
|
|
||||||
for i in imet.zeroes:
|
|
||||||
imet.zeroes[i] = 0x00
|
|
||||||
imet.tag = "IMET"
|
|
||||||
imet.unk = 0x0000060000000003
|
|
||||||
imet.sizes[0] = iconsz
|
|
||||||
imet.sizes[1] = bannersz
|
|
||||||
imet.sizes[2] = soundsz
|
|
||||||
imet.unk2 = 0
|
|
||||||
for i in range(len(imet.names)):
|
|
||||||
if(len(langs) > 0 and langs[i] != ""):
|
|
||||||
imet.names[i] = langs[i]
|
|
||||||
else:
|
|
||||||
imet.names[i] = name
|
|
||||||
for i in imet.zeroes2:
|
|
||||||
imet.zeroes2[i] = 0x00
|
|
||||||
imet.hash = "\x00" * 16
|
|
||||||
|
|
||||||
tmp = imet.pack()
|
|
||||||
imet.hash = Crypto().createMD5Hash(tmp[0x40:0x640]) #0x00 or 0x40?
|
|
||||||
|
|
||||||
data = imet.pack() + data
|
|
||||||
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
open(self.f, "wb").write(data)
|
|
||||||
return self.f
|
|
||||||
|
|
||||||
def remove(self, fn = ""):
|
|
||||||
"""This method removes an IMET header from a file specified with f in the initializer. fn is the output file name if it isn't an empty string, if it is, it will overwrite the input. If the input has no IMD5 header, it is output as is. Returns the output filename."""
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
if(data[0x80:0x84] == "IMET"):
|
|
||||||
data = data[0x640:]
|
|
||||||
elif(data[0x40:0x44] == "IMET"):
|
|
||||||
data = data[0x640:]
|
|
||||||
else:
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
return self.f
|
|
||||||
if(fn != ""):
|
|
||||||
open(fn, "wb").write(data)
|
|
||||||
return fn
|
|
||||||
else:
|
|
||||||
open(self.f, "wb").write(data)
|
|
||||||
return self.f
|
|
||||||
def getTitle(self):
|
|
||||||
imet = self.IMETHeader()
|
|
||||||
data = open(self.f, "rb").read()
|
|
||||||
|
|
||||||
if(data[0x40:0x44] == "IMET"):
|
|
||||||
pass
|
|
||||||
elif(data[0x80:0x84] == "IMET"):
|
|
||||||
data = data[0x40:]
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
imet.unpack(data[:len(imet)])
|
|
||||||
name = imet.names[1]
|
|
||||||
topop = []
|
|
||||||
for i in range(len(name)):
|
|
||||||
if(name[i] == "\x00"):
|
|
||||||
topop.append(i)
|
|
||||||
name = list(name)
|
|
||||||
popped = 0 #don't ask me why I did this
|
|
||||||
for pop in topop:
|
|
||||||
name.pop(pop - popped)
|
|
||||||
popped += 1
|
|
||||||
|
|
||||||
name = ''.join(name)
|
|
||||||
return name
|
|
23
Wii.py
23
Wii.py
@ -1,14 +1,21 @@
|
|||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
from compression import *
|
from common import *
|
||||||
from savedata import *
|
|
||||||
from banner import *
|
|
||||||
from disc import *
|
|
||||||
from nand import *
|
|
||||||
from formats import *
|
from formats import *
|
||||||
|
from banner import *
|
||||||
from title import *
|
from title import *
|
||||||
from TPL import *
|
from disc import *
|
||||||
from U8 import *
|
from image import *
|
||||||
|
from archive import *
|
||||||
|
from export import *
|
||||||
|
from compression import *
|
||||||
|
from nand import *
|
||||||
|
|
||||||
if (__name__ == "__main__"):
|
if (__name__ == "__main__"):
|
||||||
print ("\nAll components loaded sucessfully!\n\n")
|
Crypto()
|
||||||
|
TMD()
|
||||||
|
Ticket()
|
||||||
|
|
||||||
|
#insert non-dependant check code here
|
||||||
|
|
||||||
|
print ("\nAll Wii.py components loaded sucessfully!\n")
|
||||||
|
501
archive.py
Normal file
501
archive.py
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
from common import *
|
||||||
|
from title import *
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
class U8():
|
||||||
|
"""This class can unpack and pack U8 archives, which are used all over the Wii. They are often used in Banners and contents in Downloadable Titles. Please remove all headers and compression first, kthx.
|
||||||
|
|
||||||
|
The f parameter is either the source folder to pack, or the source file to unpack."""
|
||||||
|
class U8Header(Struct):
|
||||||
|
__endian__ = Struct.BE
|
||||||
|
def __format__(self):
|
||||||
|
self.tag = Struct.string(4)
|
||||||
|
self.rootnode_offset = Struct.uint32
|
||||||
|
self.header_size = Struct.uint32
|
||||||
|
self.data_offset = Struct.uint32
|
||||||
|
self.zeroes = Struct.string(16)
|
||||||
|
class U8Node(Struct):
|
||||||
|
__endian__ = Struct.BE
|
||||||
|
def __format__(self):
|
||||||
|
self.type = Struct.uint16
|
||||||
|
self.name_offset = Struct.uint16
|
||||||
|
self.data_offset = Struct.uint32
|
||||||
|
self.size = Struct.uint32
|
||||||
|
def __init__(self, f):
|
||||||
|
self.f = f
|
||||||
|
def _pack(self, file, recursion, is_root = 0): #internal
|
||||||
|
node = self.U8Node()
|
||||||
|
node.name_offset = len(self.strings)
|
||||||
|
if(is_root != 1):
|
||||||
|
self.strings += (file)
|
||||||
|
self.strings += ("\x00")
|
||||||
|
|
||||||
|
if(os.path.isdir(file)):
|
||||||
|
node.type = 0x0100
|
||||||
|
node.data_offset = recursion - 1
|
||||||
|
recursion += 1
|
||||||
|
files = sorted(os.listdir(file))
|
||||||
|
#if(sorted(files) == ["banner.bin", "icon.bin", "sound.bin"]):
|
||||||
|
# files = ["icon.bin", "banner.bin", "sound.bin"]
|
||||||
|
|
||||||
|
oldsz = len(self.nodes)
|
||||||
|
if(is_root != 1):
|
||||||
|
self.nodes.append(node)
|
||||||
|
|
||||||
|
os.chdir(file)
|
||||||
|
for entry in files:
|
||||||
|
if(entry != ".DS_Store" and entry[len(entry) - 4:] != "_out"):
|
||||||
|
self._pack(entry, recursion)
|
||||||
|
os.chdir("..")
|
||||||
|
|
||||||
|
self.nodes[oldsz].size = len(self.nodes) + 1
|
||||||
|
else:
|
||||||
|
f = open(file, "rb")
|
||||||
|
data = f.read()
|
||||||
|
f.close()
|
||||||
|
sz = len(data)
|
||||||
|
data += "\x00" * (align(sz, 32) - sz) #32 seems to work best for fuzzyness? I'm still really not sure
|
||||||
|
node.data_offset = len(self.data)
|
||||||
|
self.data += data
|
||||||
|
node.size = sz
|
||||||
|
node.type = 0x0000
|
||||||
|
if(is_root != 1):
|
||||||
|
self.nodes.append(node)
|
||||||
|
|
||||||
|
def pack(self, fn = ""):
|
||||||
|
"""This function will pack a folder into a U8 archive. The output file name is specified in the parameter fn. If fn is an empty string, the filename is deduced from the input folder name. Returns the output filename.
|
||||||
|
|
||||||
|
This creates valid U8 archives for all purposes."""
|
||||||
|
header = self.U8Header()
|
||||||
|
rootnode = self.U8Node()
|
||||||
|
|
||||||
|
header.tag = "U\xAA8-"
|
||||||
|
header.rootnode_offset = 0x20
|
||||||
|
header.zeroes = "\x00" * 16
|
||||||
|
|
||||||
|
self.nodes = []
|
||||||
|
self.strings = "\x00"
|
||||||
|
self.data = ""
|
||||||
|
origdir = os.getcwd()
|
||||||
|
os.chdir(self.f)
|
||||||
|
self._pack(".", 0, 1)
|
||||||
|
os.chdir(origdir)
|
||||||
|
|
||||||
|
header.header_size = (len(self.nodes) + 1) * len(rootnode) + len(self.strings)
|
||||||
|
header.data_offset = align(header.header_size + header.rootnode_offset, 64)
|
||||||
|
rootnode.size = len(self.nodes) + 1
|
||||||
|
rootnode.type = 0x0100
|
||||||
|
|
||||||
|
for i in range(len(self.nodes)):
|
||||||
|
if(self.nodes[i].type == 0x0000):
|
||||||
|
self.nodes[i].data_offset += header.data_offset
|
||||||
|
|
||||||
|
if(fn == ""):
|
||||||
|
if(self.f[len(self.f) - 4:] == "_out"):
|
||||||
|
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:-4].replace("_", ".")
|
||||||
|
else:
|
||||||
|
fn = self.f
|
||||||
|
|
||||||
|
fd = open(fn, "wb")
|
||||||
|
fd.write(header.pack())
|
||||||
|
fd.write(rootnode.pack())
|
||||||
|
for node in self.nodes:
|
||||||
|
fd.write(node.pack())
|
||||||
|
fd.write(self.strings)
|
||||||
|
fd.write("\x00" * (header.data_offset - header.rootnode_offset - header.header_size))
|
||||||
|
fd.write(self.data)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
return fn
|
||||||
|
|
||||||
|
def unpack(self, fn = ""):
|
||||||
|
"""This will unpack the U8 archive specified by the initialization parameter into either the folder specified by the parameter fn, or into a folder created with this formula:
|
||||||
|
``filename_extension_out``
|
||||||
|
|
||||||
|
This will recreate the directory structure, including the initial root folder in the U8 archive (such as "arc" or "meta"). Returns the output directory name."""
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
header = self.U8Header()
|
||||||
|
header.unpack(data[offset:offset + len(header)])
|
||||||
|
offset += len(header)
|
||||||
|
|
||||||
|
if(header.tag != "U\xAA8-"):
|
||||||
|
raise NameError("Bad U8 Tag")
|
||||||
|
offset = header.rootnode_offset
|
||||||
|
|
||||||
|
rootnode = self.U8Node()
|
||||||
|
rootnode.unpack(data[offset:offset + len(rootnode)])
|
||||||
|
offset += len(rootnode)
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for i in range(rootnode.size - 1):
|
||||||
|
node = self.U8Node()
|
||||||
|
node.unpack(data[offset:offset + len(node)])
|
||||||
|
offset += len(node)
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
|
||||||
|
offset += len(strings)
|
||||||
|
|
||||||
|
if(fn == ""):
|
||||||
|
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f).replace(".", "_") + "_out"
|
||||||
|
try:
|
||||||
|
origdir = os.getcwd()
|
||||||
|
os.mkdir(fn)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
os.chdir(fn)
|
||||||
|
|
||||||
|
recursion = [rootnode.size]
|
||||||
|
counter = 0
|
||||||
|
for node in nodes:
|
||||||
|
counter += 1
|
||||||
|
name = strings[node.name_offset:].split('\0', 1)[0]
|
||||||
|
|
||||||
|
if(node.type == 0x0100): #folder
|
||||||
|
recursion.append(node.size)
|
||||||
|
try:
|
||||||
|
os.mkdir(name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
os.chdir(name)
|
||||||
|
continue
|
||||||
|
elif(node.type == 0): #file
|
||||||
|
file = open(name, "wb")
|
||||||
|
file.write(data[node.data_offset:node.data_offset + node.size])
|
||||||
|
offset += node.size
|
||||||
|
else: #unknown
|
||||||
|
pass #ignore
|
||||||
|
|
||||||
|
sz = recursion.pop()
|
||||||
|
if(sz == counter + 1):
|
||||||
|
os.chdir("..")
|
||||||
|
else:
|
||||||
|
recursion.append(sz)
|
||||||
|
os.chdir("..")
|
||||||
|
|
||||||
|
os.chdir(origdir)
|
||||||
|
return fn
|
||||||
|
def __str__(self):
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
header = self.U8Header()
|
||||||
|
header.unpack(data[offset:offset + len(header)])
|
||||||
|
offset += len(header)
|
||||||
|
|
||||||
|
if(header.tag != "U\xAA8-"):
|
||||||
|
raise NameError("Bad U8 Tag")
|
||||||
|
offset = header.rootnode_offset
|
||||||
|
|
||||||
|
rootnode = self.U8Node()
|
||||||
|
rootnode.unpack(data[offset:offset + len(rootnode)])
|
||||||
|
offset += len(rootnode)
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for i in xrange(rootnode.size - 1):
|
||||||
|
node = self.U8Node()
|
||||||
|
node.unpack(data[offset:offset + len(node)])
|
||||||
|
offset += len(node)
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
|
||||||
|
offset += len(strings)
|
||||||
|
|
||||||
|
out = "[root]\n"
|
||||||
|
recursion = [rootnode.size]
|
||||||
|
counter = 0
|
||||||
|
for node in nodes:
|
||||||
|
counter += 1
|
||||||
|
name = strings[node.name_offset:].split('\0', 1)[0]
|
||||||
|
out += " " * len(recursion)
|
||||||
|
if(node.type == 0x0100): #folder
|
||||||
|
recursion.append(node.size)
|
||||||
|
out += "[%s]\n" % name
|
||||||
|
continue
|
||||||
|
elif(node.type == 0): #file
|
||||||
|
out += "%s\n" % name
|
||||||
|
offset += node.size
|
||||||
|
else: #unknown, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
sz = recursion.pop()
|
||||||
|
if(sz == counter + 1):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
recursion.append(sz)
|
||||||
|
return out
|
||||||
|
|
||||||
|
class WAD:
|
||||||
|
"""This class is to pack and unpack WAD files, which store a single title. You pass the input filename or input directory name to the parameter f.
|
||||||
|
|
||||||
|
WAD packing support currently creates WAD files that return -4100 on install."""
|
||||||
|
def __init__(self, f, boot2 = False):
|
||||||
|
self.f = f
|
||||||
|
self.boot2 = boot2
|
||||||
|
def pack(self, fn = "", fakesign = True, decrypted = True):
|
||||||
|
"""Packs a WAD into the filename specified by fn, if it is not empty. If it is empty, it packs into a filename generated from the folder's name. If fakesign is True, it will fakesign the Ticket and TMD, and update them as needed. If decrypted is true, it will assume the contents are already decrypted. For now, fakesign can not be True if decrypted is False, however fakesign can be False if decrypted is True. Title ID is a long integer of the destination title id."""
|
||||||
|
os.chdir(self.f)
|
||||||
|
|
||||||
|
tik = Ticket("tik")
|
||||||
|
tmd = TMD("tmd")
|
||||||
|
titlekey = tik.getTitleKey()
|
||||||
|
contents = tmd.getContents()
|
||||||
|
|
||||||
|
apppack = ""
|
||||||
|
for content in contents:
|
||||||
|
tmpdata = open("%08x.app" % content.index, "rb").read()
|
||||||
|
|
||||||
|
if(decrypted):
|
||||||
|
if(fakesign):
|
||||||
|
content.hash = str(Crypto().createSHAHash(tmpdata))
|
||||||
|
content.size = len(tmpdata)
|
||||||
|
|
||||||
|
encdata = Crypto().encryptContent(titlekey, content.index, tmpdata)
|
||||||
|
else:
|
||||||
|
encdata = tmpdata
|
||||||
|
|
||||||
|
apppack += encdata
|
||||||
|
if(len(encdata) % 64 != 0):
|
||||||
|
apppack += "\x00" * (64 - (len(encdata) % 64))
|
||||||
|
|
||||||
|
if(fakesign):
|
||||||
|
tmd.setContents(contents)
|
||||||
|
tmd.dump()
|
||||||
|
tik.dump()
|
||||||
|
|
||||||
|
rawtmd = open("tmd", "rb").read()
|
||||||
|
rawcert = open("cert", "rb").read()
|
||||||
|
rawtik = open("tik", "rb").read()
|
||||||
|
|
||||||
|
sz = 0
|
||||||
|
for i in range(len(contents)):
|
||||||
|
sz += contents[i].size
|
||||||
|
if(sz % 64 != 0):
|
||||||
|
sz += 64 - (contents[i].size % 64)
|
||||||
|
|
||||||
|
if(self.boot2 != True):
|
||||||
|
pack = struct.pack('>I4s6I', 32, "Is\x00\x00", len(rawcert), 0, len(rawtik), len(rawtmd), sz, 0)
|
||||||
|
pack += "\x00" * 32
|
||||||
|
else:
|
||||||
|
pack = struct.pack('>IIIII12s', 32, align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40), len(rawcert), len(rawtik), len(rawtmd), "\x00" * 12)
|
||||||
|
|
||||||
|
pack += rawcert
|
||||||
|
if(len(rawcert) % 64 != 0 and self.boot2 != True):
|
||||||
|
pack += "\x00" * (64 - (len(rawcert) % 64))
|
||||||
|
pack += rawtik
|
||||||
|
if(len(rawtik) % 64 != 0 and self.boot2 != True):
|
||||||
|
pack += "\x00" * (64 - (len(rawtik) % 64))
|
||||||
|
pack += rawtmd
|
||||||
|
if(len(rawtmd) % 64 != 0 and self.boot2 != True):
|
||||||
|
pack += "\x00" * (64 - (len(rawtmd) % 64))
|
||||||
|
|
||||||
|
if(self.boot2 == True):
|
||||||
|
pack += "\x00" * (align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40) - (len(rawcert) + len(rawtik) + len(rawtmd)))
|
||||||
|
|
||||||
|
pack += apppack
|
||||||
|
|
||||||
|
os.chdir('..')
|
||||||
|
if(fn == ""):
|
||||||
|
if(self.f[len(self.f) - 4:] == "_out"):
|
||||||
|
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:len(os.path.basename(self.f)) - 4].replace("_", ".")
|
||||||
|
else:
|
||||||
|
fn = self.f
|
||||||
|
open(fn, "wb").write(pack)
|
||||||
|
return fn
|
||||||
|
def unpack(self, fn = ""):
|
||||||
|
"""Unpacks the WAD from the parameter f in the initializer to either the value of fn, if there is one, or a folder created with this formula: `filename_extension_out`. Certs are put in the file "cert", TMD in the file "tmd", ticket in the file "tik", and contents are put in the files based on index and with ".app" added to the end."""
|
||||||
|
fd = open(self.f, 'rb')
|
||||||
|
if(self.boot2 != True):
|
||||||
|
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64))
|
||||||
|
else:
|
||||||
|
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if(fn == ""):
|
||||||
|
fn = self.f.replace(".", "_") + "_out"
|
||||||
|
os.mkdir(fn)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
os.chdir(fn)
|
||||||
|
|
||||||
|
rawcert = fd.read(certsize)
|
||||||
|
if(self.boot2 != True):
|
||||||
|
if(certsize % 64 != 0):
|
||||||
|
fd.seek(64 - (certsize % 64), 1)
|
||||||
|
open('cert', 'wb').write(rawcert)
|
||||||
|
|
||||||
|
rawtik = fd.read(tiksize)
|
||||||
|
if(self.boot2 != True):
|
||||||
|
if(tiksize % 64 != 0):
|
||||||
|
fd.seek(64 - (tiksize % 64), 1)
|
||||||
|
open('tik', 'wb').write(rawtik)
|
||||||
|
|
||||||
|
rawtmd = fd.read(tmdsize)
|
||||||
|
if(self.boot2 == True):
|
||||||
|
fd.seek(data_offset)
|
||||||
|
else:
|
||||||
|
fd.seek(64 - (tmdsize % 64), 1)
|
||||||
|
open('tmd', 'wb').write(rawtmd)
|
||||||
|
|
||||||
|
titlekey = Ticket("tik").getTitleKey()
|
||||||
|
contents = TMD("tmd").getContents()
|
||||||
|
for i in range(0, len(contents)):
|
||||||
|
tmpsize = contents[i].size
|
||||||
|
if(tmpsize % 16 != 0):
|
||||||
|
tmpsize += 16 - (tmpsize % 16)
|
||||||
|
tmptmpdata = fd.read(tmpsize)
|
||||||
|
tmpdata = Crypto().decryptContent(titlekey, contents[i].index, tmptmpdata)
|
||||||
|
open("%08x.app" % contents[i].index, "wb").write(tmpdata)
|
||||||
|
if(tmpsize % 64 != 0):
|
||||||
|
fd.seek(64 - (tmpsize % 64), 1)
|
||||||
|
fd.close()
|
||||||
|
os.chdir('..')
|
||||||
|
|
||||||
|
return fn
|
||||||
|
def __str__(self):
|
||||||
|
out = ""
|
||||||
|
out += "Wii WAD:\n"
|
||||||
|
fd = open(self.f, 'rb')
|
||||||
|
|
||||||
|
if(self.boot2 != True):
|
||||||
|
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64))
|
||||||
|
else:
|
||||||
|
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32))
|
||||||
|
|
||||||
|
rawcert = fd.read(certsize)
|
||||||
|
if(certsize % 64 != 0):
|
||||||
|
fd.seek(64 - (certsize % 64), 1)
|
||||||
|
rawtik = fd.read(tiksize)
|
||||||
|
if(self.boot2 != True):
|
||||||
|
if(tiksize % 64 != 0):
|
||||||
|
fd.seek(64 - (tiksize % 64), 1)
|
||||||
|
rawtmd = fd.read(tmdsize)
|
||||||
|
|
||||||
|
if(self.boot2 != True):
|
||||||
|
out += " Header %02x Type '%s' Certs %x Tiket %x TMD %x Data %x Footer %x\n" % (headersize, wadtype, certsize, tiksize, tmdsize, datasize, footersize)
|
||||||
|
else:
|
||||||
|
out += " Header %02x Type 'boot2' Certs %x Tiket %x TMD %x Data @ %x\n" % (headersize, certsize, tiksize, tmdsize, data_offset)
|
||||||
|
|
||||||
|
out += str(Ticket(rawtik))
|
||||||
|
out += str(TMD(rawtmd))
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class CCF():
|
||||||
|
class CCFHeader(Struct):
|
||||||
|
__endian__ = Struct.LE
|
||||||
|
def __format__(self):
|
||||||
|
self.magic = Struct.string(4)
|
||||||
|
self.zeroes12 = Struct.string(12)
|
||||||
|
self.rootOffset = Struct.uint32
|
||||||
|
self.filesCount = Struct.uint32
|
||||||
|
self.zeroes8 = Struct.string(8)
|
||||||
|
|
||||||
|
class CCFFile(Struct):
|
||||||
|
__endian__ = Struct.LE
|
||||||
|
def __format__(self):
|
||||||
|
self.fileName = Struct.string(20)
|
||||||
|
self.fileOffset = Struct.uint32
|
||||||
|
self.fileSize = Struct.uint32
|
||||||
|
self.fileSizeDecompressed = Struct.uint32
|
||||||
|
|
||||||
|
def __init__(self, fileName):
|
||||||
|
self.fileName = fileName
|
||||||
|
self.fd = open(fileName, 'r+b')
|
||||||
|
|
||||||
|
def compress(self, folder):
|
||||||
|
fileList = []
|
||||||
|
|
||||||
|
fileHdr = self.CCFHeader()
|
||||||
|
|
||||||
|
files = os.listdir(folder)
|
||||||
|
|
||||||
|
fileHdr.magic = "\x43\x43\x46\x00"
|
||||||
|
fileHdr.zeroes12 = '\x00' * 12
|
||||||
|
fileHdr.rootOffset = 0x20
|
||||||
|
fileHdr.zeroes8 = '\x00' * 8
|
||||||
|
|
||||||
|
currentOffset = len(fileHdr)
|
||||||
|
packedFiles = 0
|
||||||
|
previousFileEndOffset = 0
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if os.path.isdir(folder + '/' + file) or file == '.DS_Store':
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
fileList.append(file)
|
||||||
|
|
||||||
|
fileHdr.filesCount = len(fileList)
|
||||||
|
self.fd.write(fileHdr.pack())
|
||||||
|
self.fd.write('\x00' * (fileHdr.filesCount * len(self.CCFFile())))
|
||||||
|
|
||||||
|
for fileNumber in range(len(fileList)):
|
||||||
|
|
||||||
|
fileEntry = self.CCFFile()
|
||||||
|
|
||||||
|
compressedBuffer = zlib.compress(open(folder + '/' + fileList[fileNumber]).read())
|
||||||
|
|
||||||
|
fileEntry.fileName = fileList[fileNumber]
|
||||||
|
fileEntry.fileSize = len(compressedBuffer)
|
||||||
|
fileEntry.fileSizeDecompressed = os.stat(folder + '/' + fileList[fileNumber]).st_size
|
||||||
|
fileEntry.fileOffset = align(self.fd.tell(), 32) / 32
|
||||||
|
|
||||||
|
print 'File {0} ({1}Kb) placed at offset 0x{2:X}'.format(fileEntry.fileName, fileEntry.fileSize / 1024, fileEntry.fileOffset * 32)
|
||||||
|
|
||||||
|
self.fd.seek(len(fileHdr) + fileNumber * len(self.CCFFile()))
|
||||||
|
self.fd.write(fileEntry.pack())
|
||||||
|
self.fd.seek(fileEntry.fileOffset * 32)
|
||||||
|
self.fd.write(compressedBuffer)
|
||||||
|
|
||||||
|
self.fd.close()
|
||||||
|
|
||||||
|
def decompress(self):
|
||||||
|
fileHdr = self.CCFHeader()
|
||||||
|
hdrData = self.fd.read(len(fileHdr))
|
||||||
|
fileHdr.unpack(hdrData)
|
||||||
|
|
||||||
|
print 'Found {0} file/s and root node at 0x{1:X}'.format(fileHdr.filesCount, fileHdr.rootOffset)
|
||||||
|
|
||||||
|
if fileHdr.magic != "\x43\x43\x46\x00":
|
||||||
|
raise ValueError("Wrong magic, 0x{0}".format(fileHdr.magic))
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(os.path.dirname(self.fileName) + '/' + self.fd.name.replace(".", "_") + "_out")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
os.chdir(os.path.dirname(self.fileName) + '/' + self.fd.name.replace(".", "_") + "_out")
|
||||||
|
|
||||||
|
currentOffset = len(fileHdr)
|
||||||
|
|
||||||
|
for x in range(fileHdr.filesCount):
|
||||||
|
self.fd.seek(currentOffset)
|
||||||
|
|
||||||
|
fileEntry = self.CCFFile()
|
||||||
|
fileData = self.fd.read(len(fileEntry))
|
||||||
|
fileEntry.unpack(fileData)
|
||||||
|
|
||||||
|
fileEntry.fileOffset = fileEntry.fileOffset * 32
|
||||||
|
|
||||||
|
print 'File {0} at offset 0x{1:X}'.format(fileEntry.fileName, fileEntry.fileOffset)
|
||||||
|
print 'File size {0}Kb ({1}Kb decompressed)'.format(fileEntry.fileSize / 1024, fileEntry.fileSizeDecompressed / 1024)
|
||||||
|
|
||||||
|
output = open(fileEntry.fileName.rstrip('\0'), 'w+b')
|
||||||
|
|
||||||
|
self.fd.seek(fileEntry.fileOffset)
|
||||||
|
if fileEntry.fileSize == fileEntry.fileSizeDecompressed:
|
||||||
|
print 'The file is stored uncompressed'
|
||||||
|
output.write(self.fd.read(fileEntry.fileSize))
|
||||||
|
else:
|
||||||
|
print 'The file is stored compressed..decompressing'
|
||||||
|
decompressedBuffer = zlib.decompress(self.fd.read(fileEntry.fileSize))
|
||||||
|
output.write(decompressedBuffer)
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
currentOffset += len(fileEntry)
|
||||||
|
|
12
common.py
12
common.py
@ -1,14 +1,14 @@
|
|||||||
import os, hashlib, struct, subprocess, fnmatch, shutil, urllib, array
|
import os, hashlib, struct, subprocess, fnmatch, shutil, urllib, array, time, sys, tempfile
|
||||||
|
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from Struct import Struct
|
from Struct import Struct
|
||||||
|
|
||||||
|
|
||||||
def align(x, boundary):
|
def align(x, boundary):
|
||||||
if(x % boundary == 0):
|
if(x % boundary):
|
||||||
return x
|
x += (x + boundary) - (x % boundary)
|
||||||
else:
|
return x
|
||||||
return (x + boundary) - (x % boundary)
|
|
||||||
|
|
||||||
def hexdump(s, sep=" "):
|
def hexdump(s, sep=" "):
|
||||||
return sep.join(map(lambda x: "%02x" % ord(x), s))
|
return sep.join(map(lambda x: "%02x" % ord(x), s))
|
||||||
|
119
compression.py
119
compression.py
@ -1,124 +1,5 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
|
|
||||||
import zlib
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
|
|
||||||
class CCF():
|
|
||||||
class CCFHeader(Struct):
|
|
||||||
__endian__ = Struct.LE
|
|
||||||
def __format__(self):
|
|
||||||
self.magic = Struct.string(4)
|
|
||||||
self.zeroes12 = Struct.string(12)
|
|
||||||
self.rootOffset = Struct.uint32
|
|
||||||
self.filesCount = Struct.uint32
|
|
||||||
self.zeroes8 = Struct.string(8)
|
|
||||||
|
|
||||||
class CCFFile(Struct):
|
|
||||||
__endian__ = Struct.LE
|
|
||||||
def __format__(self):
|
|
||||||
self.fileName = Struct.string(20)
|
|
||||||
self.fileOffset = Struct.uint32
|
|
||||||
self.fileSize = Struct.uint32
|
|
||||||
self.fileSizeDecompressed = Struct.uint32
|
|
||||||
|
|
||||||
def __init__(self, fileName):
|
|
||||||
self.fileName = fileName
|
|
||||||
self.fd = open(fileName, 'r+b')
|
|
||||||
|
|
||||||
def compress(self, folder):
|
|
||||||
fileList = []
|
|
||||||
|
|
||||||
fileHdr = self.CCFHeader()
|
|
||||||
|
|
||||||
files = os.listdir(folder)
|
|
||||||
|
|
||||||
fileHdr.magic = "\x43\x43\x46\x00"
|
|
||||||
fileHdr.zeroes12 = '\x00' * 12
|
|
||||||
fileHdr.rootOffset = 0x20
|
|
||||||
fileHdr.zeroes8 = '\x00' * 8
|
|
||||||
|
|
||||||
currentOffset = len(fileHdr)
|
|
||||||
packedFiles = 0
|
|
||||||
previousFileEndOffset = 0
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
if os.path.isdir(folder + '/' + file) or file == '.DS_Store':
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
fileList.append(file)
|
|
||||||
|
|
||||||
fileHdr.filesCount = len(fileList)
|
|
||||||
self.fd.write(fileHdr.pack())
|
|
||||||
self.fd.write('\x00' * (fileHdr.filesCount * len(self.CCFFile())))
|
|
||||||
|
|
||||||
for fileNumber in range(len(fileList)):
|
|
||||||
|
|
||||||
fileEntry = self.CCFFile()
|
|
||||||
|
|
||||||
compressedBuffer = zlib.compress(open(folder + '/' + fileList[fileNumber]).read())
|
|
||||||
|
|
||||||
fileEntry.fileName = fileList[fileNumber]
|
|
||||||
fileEntry.fileSize = len(compressedBuffer)
|
|
||||||
fileEntry.fileSizeDecompressed = os.stat(folder + '/' + fileList[fileNumber]).st_size
|
|
||||||
fileEntry.fileOffset = align(self.fd.tell(), 32) / 32
|
|
||||||
|
|
||||||
print 'File {0} ({1}Kb) placed at offset 0x{2:X}'.format(fileEntry.fileName, fileEntry.fileSize / 1024, fileEntry.fileOffset * 32)
|
|
||||||
|
|
||||||
self.fd.seek(len(fileHdr) + fileNumber * len(self.CCFFile()))
|
|
||||||
self.fd.write(fileEntry.pack())
|
|
||||||
self.fd.seek(fileEntry.fileOffset * 32)
|
|
||||||
self.fd.write(compressedBuffer)
|
|
||||||
|
|
||||||
self.fd.close()
|
|
||||||
|
|
||||||
def decompress(self):
|
|
||||||
fileHdr = self.CCFHeader()
|
|
||||||
hdrData = self.fd.read(len(fileHdr))
|
|
||||||
fileHdr.unpack(hdrData)
|
|
||||||
|
|
||||||
print 'Found {0} file/s and root node at 0x{1:X}'.format(fileHdr.filesCount, fileHdr.rootOffset)
|
|
||||||
|
|
||||||
if fileHdr.magic != "\x43\x43\x46\x00":
|
|
||||||
raise ValueError("Wrong magic, 0x{0}".format(fileHdr.magic))
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.mkdir(os.path.dirname(self.fileName) + '/' + self.fd.name.replace(".", "_") + "_out")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
os.chdir(os.path.dirname(self.fileName) + '/' + self.fd.name.replace(".", "_") + "_out")
|
|
||||||
|
|
||||||
currentOffset = len(fileHdr)
|
|
||||||
|
|
||||||
for x in range(fileHdr.filesCount):
|
|
||||||
self.fd.seek(currentOffset)
|
|
||||||
|
|
||||||
fileEntry = self.CCFFile()
|
|
||||||
fileData = self.fd.read(len(fileEntry))
|
|
||||||
fileEntry.unpack(fileData)
|
|
||||||
|
|
||||||
fileEntry.fileOffset = fileEntry.fileOffset * 32
|
|
||||||
|
|
||||||
print 'File {0} at offset 0x{1:X}'.format(fileEntry.fileName, fileEntry.fileOffset)
|
|
||||||
print 'File size {0}Kb ({1}Kb decompressed)'.format(fileEntry.fileSize / 1024, fileEntry.fileSizeDecompressed / 1024)
|
|
||||||
|
|
||||||
output = open(fileEntry.fileName.rstrip('\0'), 'w+b')
|
|
||||||
|
|
||||||
self.fd.seek(fileEntry.fileOffset)
|
|
||||||
if fileEntry.fileSize == fileEntry.fileSizeDecompressed:
|
|
||||||
print 'The file is stored uncompressed'
|
|
||||||
output.write(self.fd.read(fileEntry.fileSize))
|
|
||||||
else:
|
|
||||||
print 'The file is stored compressed..decompressing'
|
|
||||||
decompressedBuffer = zlib.decompress(self.fd.read(fileEntry.fileSize))
|
|
||||||
output.write(decompressedBuffer)
|
|
||||||
output.close()
|
|
||||||
|
|
||||||
currentOffset += len(fileEntry)
|
|
||||||
|
|
||||||
|
|
||||||
class LZ77():
|
class LZ77():
|
||||||
class WiiLZ77: #class by marcan
|
class WiiLZ77: #class by marcan
|
||||||
TYPE_LZ77 = 1
|
TYPE_LZ77 = 1
|
||||||
|
8
disc.py
8
disc.py
@ -1,11 +1,5 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
|
|
||||||
import time
|
|
||||||
from title import *
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
|
from title import *
|
||||||
|
|
||||||
class WOD: #WiiOpticalDisc
|
class WOD: #WiiOpticalDisc
|
||||||
class discHeader(Struct):
|
class discHeader(Struct):
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
from title import *
|
from title import *
|
||||||
from TPL import *
|
from image import *
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
class Savegame():
|
class Savegame():
|
||||||
class savegameHeader(Struct):
|
class savegameHeader(Struct):
|
||||||
@ -256,5 +251,3 @@ class Savegame():
|
|||||||
|
|
||||||
def getFilesCount(self):
|
def getFilesCount(self):
|
||||||
return self.bkHdr.filesCount
|
return self.bkHdr.filesCount
|
||||||
|
|
||||||
|
|
51
formats.py
51
formats.py
@ -1,8 +1,5 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
from binascii import *
|
from binascii import *
|
||||||
|
import struct
|
||||||
from Struct import Struct
|
|
||||||
from struct import *
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
from title import *
|
from title import *
|
||||||
@ -335,7 +332,7 @@ class netConfig:
|
|||||||
def getNotBlank(self, config):
|
def getNotBlank(self, config):
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
fp.close()
|
fp.close()
|
||||||
if(sel & 0x20):
|
if(sel & 0x20):
|
||||||
return 1
|
return 1
|
||||||
@ -347,7 +344,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(sel & 0x04):
|
if(sel & 0x04):
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
@ -360,7 +357,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(sel & 0x02):
|
if(sel & 0x02):
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
@ -372,7 +369,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config) + 2021)
|
fp.seek(8 + (0x91C * config) + 2021)
|
||||||
len = unpack(">B", fp.read(1))[0]
|
len = struct.unpack(">B", fp.read(1))[0]
|
||||||
fp.seek(8 + (0x91C * config) + 1988)
|
fp.seek(8 + (0x91C * config) + 1988)
|
||||||
ssid = fp.read(len)
|
ssid = fp.read(len)
|
||||||
fp.close()
|
fp.close()
|
||||||
@ -383,7 +380,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config) + 2025)
|
fp.seek(8 + (0x91C * config) + 2025)
|
||||||
crypt = unpack(">B", fp.read(1))[0]
|
crypt = struct.unpack(">B", fp.read(1))[0]
|
||||||
type = ""
|
type = ""
|
||||||
if(crypt == 0):
|
if(crypt == 0):
|
||||||
type = "OPEN"
|
type = "OPEN"
|
||||||
@ -407,7 +404,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
fp = open(self.f, "rb")
|
fp = open(self.f, "rb")
|
||||||
fp.seek(8 + (0x91C * config) + 2025)
|
fp.seek(8 + (0x91C * config) + 2025)
|
||||||
crypt = unpack(">B", fp.read(1))[0]
|
crypt = struct.unpack(">B", fp.read(1))[0]
|
||||||
type = ""
|
type = ""
|
||||||
if(crypt == 0):
|
if(crypt == 0):
|
||||||
type = "OPEN"
|
type = "OPEN"
|
||||||
@ -425,7 +422,7 @@ class netConfig:
|
|||||||
return None
|
return None
|
||||||
if(crypt != "\x00"):
|
if(crypt != "\x00"):
|
||||||
fp.seek(8 + (0x91C * config) + 2029)
|
fp.seek(8 + (0x91C * config) + 2029)
|
||||||
keylen = unpack(">B", fp.read(1))[0]
|
keylen = struct.unpack(">B", fp.read(1))[0]
|
||||||
fp.seek(8 + (0x91C * config) + 2032)
|
fp.seek(8 + (0x91C * config) + 2032)
|
||||||
key = fp.read(keylen)
|
key = fp.read(keylen)
|
||||||
fp.close()
|
fp.close()
|
||||||
@ -436,44 +433,44 @@ class netConfig:
|
|||||||
def clearConfig(self, config):
|
def clearConfig(self, config):
|
||||||
fp = open(self.f, "rb+")
|
fp = open(self.f, "rb+")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
sel &= 0xDF
|
sel &= 0xDF
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
def setNotBlank(self, config):
|
def setNotBlank(self, config):
|
||||||
fp = open(self.f, "rb+")
|
fp = open(self.f, "rb+")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
sel |= 0x20
|
sel |= 0x20
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
def setIPType(self, config, static):
|
def setIPType(self, config, static):
|
||||||
fp = open(self.f, "rb+")
|
fp = open(self.f, "rb+")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(not static):
|
if(not static):
|
||||||
sel |= 0x04
|
sel |= 0x04
|
||||||
else:
|
else:
|
||||||
sel &= 0xFB
|
sel &= 0xFB
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.close()
|
fp.close()
|
||||||
self.setNotBlank(config)
|
self.setNotBlank(config)
|
||||||
|
|
||||||
def setWireType(self, config, wired):
|
def setWireType(self, config, wired):
|
||||||
fp = open(self.f, "rb+")
|
fp = open(self.f, "rb+")
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(not wired):
|
if(not wired):
|
||||||
sel |= 0x02
|
sel |= 0x02
|
||||||
else:
|
else:
|
||||||
sel &= 0xFD
|
sel &= 0xFD
|
||||||
fp.seek(8 + (0x91C * config))
|
fp.seek(8 + (0x91C * config))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.close()
|
fp.close()
|
||||||
self.setNotBlank(config)
|
self.setNotBlank(config)
|
||||||
|
|
||||||
@ -514,29 +511,29 @@ class netConfig:
|
|||||||
def selectConfig(self, config):
|
def selectConfig(self, config):
|
||||||
fp = open(self.f, "rb+")
|
fp = open(self.f, "rb+")
|
||||||
fp.seek(8 + (0x91C * 0))
|
fp.seek(8 + (0x91C * 0))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(config == 0):
|
if(config == 0):
|
||||||
sel |= 0x80
|
sel |= 0x80
|
||||||
else:
|
else:
|
||||||
sel &= 0x7F
|
sel &= 0x7F
|
||||||
fp.seek(8 + (0x91C * 0))
|
fp.seek(8 + (0x91C * 0))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.seek(8 + (0x91C * 1))
|
fp.seek(8 + (0x91C * 1))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(config == 1):
|
if(config == 1):
|
||||||
sel |= 0x80
|
sel |= 0x80
|
||||||
else:
|
else:
|
||||||
sel &= 0x7F
|
sel &= 0x7F
|
||||||
fp.seek(8 + (0x91C * 1))
|
fp.seek(8 + (0x91C * 1))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
fp.seek(8 + (0x91C * 2))
|
fp.seek(8 + (0x91C * 2))
|
||||||
sel = unpack(">B", fp.read(1))[0]
|
sel = struct.unpack(">B", fp.read(1))[0]
|
||||||
if(config == 2):
|
if(config == 2):
|
||||||
sel |= 0x80
|
sel |= 0x80
|
||||||
else:
|
else:
|
||||||
sel &= 0x7F
|
sel &= 0x7F
|
||||||
fp.seek(8 + (0x91C * 2))
|
fp.seek(8 + (0x91C * 2))
|
||||||
fp.write(pack(">B", sel))
|
fp.write(struct.pack(">B", sel))
|
||||||
self.setNotBlank(config)
|
self.setNotBlank(config)
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
@ -710,8 +707,8 @@ class uidsys:
|
|||||||
if(hexdump(uidstr.titleid, "") == ("%016X" % title)):
|
if(hexdump(uidstr.titleid, "") == ("%016X" % title)):
|
||||||
uidfp.close()
|
uidfp.close()
|
||||||
return uidstr.uid
|
return uidstr.uid
|
||||||
if(unpack(">H", uidstr.uid) >= unpack(">H", enduid)):
|
if(struct.unpack(">H", uidstr.uid) >= struct.unpack(">H", enduid)):
|
||||||
enduid = a2b_hex("%04X" % (unpack(">H", uidstr.uid)[0] + 1))
|
enduid = a2b_hex("%04X" % (struct.unpack(">H", uidstr.uid)[0] + 1))
|
||||||
uidict[uidstr.titleid] = uidstr.uid
|
uidict[uidstr.titleid] = uidstr.uid
|
||||||
uidict[a2b_hex("%016X" % title)] = enduid
|
uidict[a2b_hex("%016X" % title)] = enduid
|
||||||
uidfp.close()
|
uidfp.close()
|
||||||
|
145
headers.py
Normal file
145
headers.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
from common import *
|
||||||
|
|
||||||
|
class IMD5():
|
||||||
|
"""This class can add and remove IMD5 headers to files. The parameter f is the file to use for the addition or removal of the header. IMD5 headers are found in banner.bin, icon.bin, and sound.bin."""
|
||||||
|
class IMD5Header(Struct):
|
||||||
|
__endian__ = Struct.BE
|
||||||
|
def __format__(self):
|
||||||
|
self.tag = Struct.string(4)
|
||||||
|
self.size = Struct.uint32
|
||||||
|
self.zeroes = Struct.uint8[8]
|
||||||
|
self.crypto = Struct.string(16)
|
||||||
|
def __init__(self, f):
|
||||||
|
self.f = f
|
||||||
|
def add(self, fn = ""):
|
||||||
|
"""This function adds an IMD5 header to the file specified by f in the initializer. The output file is specified with fn, if it is empty, it will overwrite the input file. If the file already has an IMD5 header, it will now have two. Returns the output filename."""
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
|
||||||
|
imd5 = self.IMD5Header()
|
||||||
|
for i in range(8):
|
||||||
|
imd5.zeroes[i] = 0x00
|
||||||
|
imd5.tag = "IMD5"
|
||||||
|
imd5.size = len(data)
|
||||||
|
imd5.crypto = str(Crypto().createMD5Hash(data))
|
||||||
|
data = imd5.pack() + data
|
||||||
|
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
open(self.f, "wb").write(data)
|
||||||
|
return self.f
|
||||||
|
def remove(self, fn = ""):
|
||||||
|
"""This will remove an IMD5 header from the file specified in f, if one exists. If there is no IMD5 header, it will output the file as it is. It will output in the parameter fn if available, otherwise it will overwrite the source. Returns the output filename."""
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
imd5 = self.IMD5Header()
|
||||||
|
if(data[:4] != "IMD5"):
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
return self.f
|
||||||
|
data = data[len(imd5):]
|
||||||
|
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
open(self.f, "wb").write(data)
|
||||||
|
return self.f
|
||||||
|
|
||||||
|
class IMET():
|
||||||
|
"""IMET headers are found in Opening.bnr and 0000000.app files. They contain the channel titles and more metadata about channels. They are in two different formats with different amounts of padding before the start of the IMET header. This class suports both.
|
||||||
|
|
||||||
|
The parameter f is used to specify the input file name."""
|
||||||
|
class IMETHeader(Struct):
|
||||||
|
__endian__ = Struct.BE
|
||||||
|
def __format__(self):
|
||||||
|
self.zeroes = Struct.uint8[128]
|
||||||
|
self.tag = Struct.string(4)
|
||||||
|
self.unk = Struct.uint64
|
||||||
|
self.sizes = Struct.uint32[3] #icon, banner, sound
|
||||||
|
self.unk2 = Struct.uint32
|
||||||
|
self.names = Struct.string(84, encoding = "utf-16-be", stripNulls = True)[7]
|
||||||
|
self.zeroes2 = Struct.uint8[840]
|
||||||
|
self.hash = Struct.string(16)
|
||||||
|
def __init__(self, f):
|
||||||
|
self.f = f
|
||||||
|
def add(self, iconsz, bannersz, soundsz, name = "", langs = [], fn = ""):
|
||||||
|
"""This function adds an IMET header to the file specified with f in the initializer. The file will be output to fn if it is not empty, otherwise it will overwrite the input file. You must specify the size of banner.bin in bannersz, and respectivly for iconsz and soundsz. langs is an optional arguement that is a list of different langauge channel titles. name is the english name that is copied everywhere in langs that there is an empty string. Returns the output filename."""
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
imet = self.IMETHeader()
|
||||||
|
|
||||||
|
for i in imet.zeroes:
|
||||||
|
imet.zeroes[i] = 0x00
|
||||||
|
imet.tag = "IMET"
|
||||||
|
imet.unk = 0x0000060000000003
|
||||||
|
imet.sizes[0] = iconsz
|
||||||
|
imet.sizes[1] = bannersz
|
||||||
|
imet.sizes[2] = soundsz
|
||||||
|
imet.unk2 = 0
|
||||||
|
for i in range(len(imet.names)):
|
||||||
|
if(len(langs) > 0 and langs[i] != ""):
|
||||||
|
imet.names[i] = langs[i]
|
||||||
|
else:
|
||||||
|
imet.names[i] = name
|
||||||
|
for i in imet.zeroes2:
|
||||||
|
imet.zeroes2[i] = 0x00
|
||||||
|
imet.hash = "\x00" * 16
|
||||||
|
|
||||||
|
tmp = imet.pack()
|
||||||
|
imet.hash = Crypto().createMD5Hash(tmp[0x40:0x640]) #0x00 or 0x40?
|
||||||
|
|
||||||
|
data = imet.pack() + data
|
||||||
|
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
open(self.f, "wb").write(data)
|
||||||
|
return self.f
|
||||||
|
|
||||||
|
def remove(self, fn = ""):
|
||||||
|
"""This method removes an IMET header from a file specified with f in the initializer. fn is the output file name if it isn't an empty string, if it is, it will overwrite the input. If the input has no IMD5 header, it is output as is. Returns the output filename."""
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
if(data[0x80:0x84] == "IMET"):
|
||||||
|
data = data[0x640:]
|
||||||
|
elif(data[0x40:0x44] == "IMET"):
|
||||||
|
data = data[0x640:]
|
||||||
|
else:
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
return self.f
|
||||||
|
if(fn != ""):
|
||||||
|
open(fn, "wb").write(data)
|
||||||
|
return fn
|
||||||
|
else:
|
||||||
|
open(self.f, "wb").write(data)
|
||||||
|
return self.f
|
||||||
|
def getTitle(self):
|
||||||
|
imet = self.IMETHeader()
|
||||||
|
data = open(self.f, "rb").read()
|
||||||
|
|
||||||
|
if(data[0x40:0x44] == "IMET"):
|
||||||
|
pass
|
||||||
|
elif(data[0x80:0x84] == "IMET"):
|
||||||
|
data = data[0x40:]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
imet.unpack(data[:len(imet)])
|
||||||
|
name = imet.names[1]
|
||||||
|
topop = []
|
||||||
|
for i in range(len(name)):
|
||||||
|
if(name[i] == "\x00"):
|
||||||
|
topop.append(i)
|
||||||
|
name = list(name)
|
||||||
|
popped = 0 #don't ask me why I did this
|
||||||
|
for pop in topop:
|
||||||
|
name.pop(pop - popped)
|
||||||
|
popped += 1
|
||||||
|
|
||||||
|
name = ''.join(name)
|
||||||
|
return name
|
@ -1,8 +1,3 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
|
|
||||||
def flatten(myTuple):
|
def flatten(myTuple):
|
||||||
@ -37,9 +32,9 @@ def avg(w0, w1, c0, c1):
|
|||||||
class TPL():
|
class TPL():
|
||||||
"""This is the class to generate TPL texutres from PNG images, and to convert TPL textures to PNG images. The parameter file specifies the filename of the source, either a PNG image or a TPL image.
|
"""This is the class to generate TPL texutres from PNG images, and to convert TPL textures to PNG images. The parameter file specifies the filename of the source, either a PNG image or a TPL image.
|
||||||
|
|
||||||
Currently supported are the following formats to convert from TPL: RGBA8, RGB565, I4, IA4, I8, IA8, CI4, CI8, CMP, CI14X2. Currently not working is RBG5A3. RBG5A3 is having alpha issues, i'm working on it.
|
Currently supported are the following formats to convert from TPL (all formats): RGBA8, RGB565, RGB5A3, I4, IA4, I8, IA8, CI4, CI8, CMP, CI14X2.
|
||||||
|
|
||||||
Currently support to convert to TPL: I4, I8, IA4, IA8, RBG565, RBGA8. Currently not working/done are CI4, CI8, CMP, CI14X2, and RBG5A3. RBG5A3 is having alpha issues."""
|
Currently supported to convert to TPL: I4, I8, IA4, IA8, RBG565, RBGA8, RGB5A3. Currently not supported are CI4, CI8, CMP, CI14X2."""
|
||||||
|
|
||||||
|
|
||||||
class TPLHeader(Struct):
|
class TPLHeader(Struct):
|
||||||
@ -513,22 +508,21 @@ class TPL():
|
|||||||
|
|
||||||
Again, only a single texture is supported."""
|
Again, only a single texture is supported."""
|
||||||
import wx
|
import wx
|
||||||
class imp(wx.Panel):
|
class imp(wx.Dialog):
|
||||||
def __init__(self, parent, id, im):
|
def __init__(self, title, im):
|
||||||
wx.Panel.__init__(self, parent, id)
|
|
||||||
w = img.GetWidth()
|
w = img.GetWidth()
|
||||||
h = img.GetHeight()
|
h = img.GetHeight()
|
||||||
wx.StaticBitmap(self, -1, im, ( ((max(w, 300) - w) / 2), ((max(h, 200) - h) / 2) ), (w, h))
|
|
||||||
|
|
||||||
|
wx.Dialog.__init__(self, None, -1, title, size = (max(w, 300), max(h, 200)))
|
||||||
|
|
||||||
|
wx.StaticBitmap(self, -1, im, ( ((max(w, 300) - w) / 2), ((max(h, 200) - h) / 2) ), (w, h))
|
||||||
self.toImage("tmp.png")
|
self.toImage("tmp.png")
|
||||||
img = wx.Image("tmp.png", wx.BITMAP_TYPE_ANY).ConvertToBitmap()
|
img = wx.Image("tmp.png", wx.BITMAP_TYPE_ANY).ConvertToBitmap()
|
||||||
w = img.GetWidth()
|
w = img.GetWidth()
|
||||||
h = img.GetHeight()
|
h = img.GetHeight()
|
||||||
app = wx.App(redirect = False)
|
dialog = imp("TPL (" + str(w) + ", " + str(h) + ")", img)
|
||||||
frame = wx.Frame(None, -1, "TPL (" + str(w) + ", " + str(h) + ")", size = (max(w, 300), max(h, 200)))
|
dialog.ShowModal()
|
||||||
image = imp(frame, -1, img)
|
dialog.Destroy()
|
||||||
frame.Show(True)
|
|
||||||
app.MainLoop()
|
|
||||||
os.unlink("tmp.png")
|
os.unlink("tmp.png")
|
||||||
def RGBA8(self, (x, y), data):
|
def RGBA8(self, (x, y), data):
|
||||||
out = [0 for i in range(x * y)]
|
out = [0 for i in range(x * y)]
|
4
nand.py
4
nand.py
@ -1,14 +1,10 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
from binascii import *
|
from binascii import *
|
||||||
|
|
||||||
from Struct import Struct
|
|
||||||
from struct import *
|
from struct import *
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
from title import *
|
from title import *
|
||||||
from formats import *
|
from formats import *
|
||||||
|
|
||||||
|
|
||||||
class NAND:
|
class NAND:
|
||||||
"""This class performs all NAND related things. It includes functions to copy a title (given the TMD) into the correct structure as the Wii does, and has an entire ES-like system. Parameter f to the initializer is the folder that will be used as the NAND root."""
|
"""This class performs all NAND related things. It includes functions to copy a title (given the TMD) into the correct structure as the Wii does, and has an entire ES-like system. Parameter f to the initializer is the folder that will be used as the NAND root."""
|
||||||
def __init__(self, f):
|
def __init__(self, f):
|
||||||
|
5
tests/imd5sign.py
Normal file
5
tests/imd5sign.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Wii
|
||||||
|
import sys
|
||||||
|
|
||||||
|
for elem in sys.argv:
|
||||||
|
Wii.IMD5(elem).add(elem)
|
229
title.py
229
title.py
@ -1,7 +1,3 @@
|
|||||||
import os, struct, subprocess, fnmatch, shutil, urllib, array
|
|
||||||
|
|
||||||
from Struct import Struct
|
|
||||||
|
|
||||||
from common import *
|
from common import *
|
||||||
|
|
||||||
class TicketView:
|
class TicketView:
|
||||||
@ -41,7 +37,6 @@ class TicketView:
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
class Ticket:
|
class Ticket:
|
||||||
"""Creates a ticket from the filename defined in f. This may take a longer amount of time than expected, as it also decrypts the title key. Now supports Korean tickets (but their title keys stay Korean on dump)."""
|
"""Creates a ticket from the filename defined in f. This may take a longer amount of time than expected, as it also decrypts the title key. Now supports Korean tickets (but their title keys stay Korean on dump)."""
|
||||||
class TicketStruct(Struct):
|
class TicketStruct(Struct):
|
||||||
@ -65,21 +60,26 @@ class Ticket:
|
|||||||
self.unk3 = Struct.uint16
|
self.unk3 = Struct.uint16
|
||||||
self.limits = Struct.string(96)
|
self.limits = Struct.string(96)
|
||||||
self.unk4 = Struct.uint8
|
self.unk4 = Struct.uint8
|
||||||
def __init__(self, f):
|
def __init__(self):
|
||||||
isf = False
|
|
||||||
try:
|
|
||||||
isf = os.path.isfile(f)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if(isf):
|
|
||||||
self.f = f
|
|
||||||
data = open(f, "rb").read()
|
|
||||||
else:
|
|
||||||
self.f = "tik"
|
|
||||||
data = f
|
|
||||||
|
|
||||||
self.size = len(data)
|
|
||||||
self.tik = self.TicketStruct()
|
self.tik = self.TicketStruct()
|
||||||
|
|
||||||
|
self.tik.rsaexp = 0x10001
|
||||||
|
self.tik.rsamod = "\x00" * 256
|
||||||
|
self.tik.padding1 = "\x00" * 60
|
||||||
|
self.tik.rsaid = "\x00" * 64
|
||||||
|
self.tik.padding2 = "\x00" * 63
|
||||||
|
self.tik.enctitlekey = "\x00" * 16
|
||||||
|
self.tik.titleid = 0x0000000100000000
|
||||||
|
self.tik.reserved = "\x00" * 80
|
||||||
|
self.tik.limits = "\x00" * 96
|
||||||
|
|
||||||
|
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
|
||||||
|
koreankey = "\x63\xB8\x2B\xB4\xF4\x61\x4E\x2E\x13\xF2\xFE\xFB\xBA\x4C\x9B\x7E"
|
||||||
|
|
||||||
|
if(self.tik.commonkey_index == 1): #korean, kekekekek!
|
||||||
|
commonkey = koreankey
|
||||||
|
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
|
||||||
|
def load(self, data):
|
||||||
self.tik.unpack(data[:len(self.tik)])
|
self.tik.unpack(data[:len(self.tik)])
|
||||||
|
|
||||||
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
|
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
|
||||||
@ -89,6 +89,9 @@ class Ticket:
|
|||||||
commonkey = koreankey
|
commonkey = koreankey
|
||||||
|
|
||||||
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
|
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
|
||||||
|
return self
|
||||||
|
def loadFile(self, filename):
|
||||||
|
return self.load(open(filename, "rb").read())
|
||||||
def getTitleKey(self):
|
def getTitleKey(self):
|
||||||
"""Returns a string containing the title key."""
|
"""Returns a string containing the title key."""
|
||||||
return self.titlekey
|
return self.titlekey
|
||||||
@ -98,6 +101,12 @@ class Ticket:
|
|||||||
def setTitleID(self, titleid):
|
def setTitleID(self, titleid):
|
||||||
"""Sets the title id of the ticket from the long integer passed in titleid."""
|
"""Sets the title id of the ticket from the long integer passed in titleid."""
|
||||||
self.tik.titleid = titleid
|
self.tik.titleid = titleid
|
||||||
|
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
|
||||||
|
koreankey = "\x63\xB8\x2B\xB4\xF4\x61\x4E\x2E\x13\xF2\xFE\xFB\xBA\x4C\x9B\x7E"
|
||||||
|
|
||||||
|
if(self.tik.commonkey_index == 1): #korean, kekekekek!
|
||||||
|
commonkey = koreankey
|
||||||
|
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey) #This changes the decrypted title key!
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
out = ""
|
out = ""
|
||||||
out += " Ticket:\n"
|
out += " Ticket:\n"
|
||||||
@ -116,7 +125,6 @@ class Ticket:
|
|||||||
out += "\n"
|
out += "\n"
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def dump(self, fn = ""):
|
def dump(self, fn = ""):
|
||||||
"""Fakesigns (or Trucha signs) and dumps the ticket to either fn, if not empty, or overwriting the source if empty. Returns the output filename."""
|
"""Fakesigns (or Trucha signs) and dumps the ticket to either fn, if not empty, or overwriting the source if empty. Returns the output filename."""
|
||||||
self.rsamod = self.rsamod = "\x00" * 256
|
self.rsamod = self.rsamod = "\x00" * 256
|
||||||
@ -173,30 +181,21 @@ class TMD:
|
|||||||
self.boot_index = Struct.uint16
|
self.boot_index = Struct.uint16
|
||||||
self.padding2 = Struct.uint16
|
self.padding2 = Struct.uint16
|
||||||
#contents follow this
|
#contents follow this
|
||||||
|
def load(self, data):
|
||||||
def __init__(self, f):
|
|
||||||
isf = False
|
|
||||||
try:
|
|
||||||
isf = os.path.isfile(f)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if(isf):
|
|
||||||
self.f = f
|
|
||||||
data = open(f, "rb").read()
|
|
||||||
else:
|
|
||||||
self.f = "tmd"
|
|
||||||
data = f
|
|
||||||
|
|
||||||
self.tmd = self.TMDStruct()
|
|
||||||
self.tmd.unpack(data[:len(self.tmd)])
|
self.tmd.unpack(data[:len(self.tmd)])
|
||||||
|
|
||||||
self.contents = []
|
|
||||||
pos = len(self.tmd)
|
pos = len(self.tmd)
|
||||||
for i in range(self.tmd.numcontents):
|
for i in range(self.tmd.numcontents):
|
||||||
cont = self.TMDContent()
|
cont = self.TMDContent()
|
||||||
cont.unpack(data[pos:pos + len(cont)])
|
cont.unpack(data[pos:pos + len(cont)])
|
||||||
pos += len(cont)
|
pos += len(cont)
|
||||||
self.contents.append(cont)
|
self.contents.append(cont)
|
||||||
|
return self
|
||||||
|
def loadFile(self, filename):
|
||||||
|
return self.load(open(filename, "rb").read())
|
||||||
|
def __init__(self):
|
||||||
|
self.tmd = self.TMDStruct()
|
||||||
|
self.tmd.titleid = 0x0000000100000000
|
||||||
|
self.contents = []
|
||||||
def getContents(self):
|
def getContents(self):
|
||||||
"""Returns a list of contents. Each content is an object with the members "size", the size of the content's decrypted data; "cid", the content id; "type", the type of the content (0x8001 for shared, 0x0001 for standard, more possible), and a 20 byte string called "hash"."""
|
"""Returns a list of contents. Each content is an object with the members "size", the size of the content's decrypted data; "cid", the content id; "type", the type of the content (0x8001 for shared, 0x0001 for standard, more possible), and a 20 byte string called "hash"."""
|
||||||
return self.contents
|
return self.contents
|
||||||
@ -342,159 +341,3 @@ class NUS:
|
|||||||
open("%08x.app" % output, "wb").write(tmpdata)
|
open("%08x.app" % output, "wb").write(tmpdata)
|
||||||
|
|
||||||
os.chdir("..")
|
os.chdir("..")
|
||||||
|
|
||||||
class WAD:
|
|
||||||
"""This class is to pack and unpack WAD files, which store a single title. You pass the input filename or input directory name to the parameter f.
|
|
||||||
|
|
||||||
WAD packing support currently creates WAD files that return -4100 on install."""
|
|
||||||
def __init__(self, f, boot2 = False):
|
|
||||||
self.f = f
|
|
||||||
self.boot2 = boot2
|
|
||||||
def pack(self, fn = "", fakesign = True, decrypted = True):
|
|
||||||
"""Packs a WAD into the filename specified by fn, if it is not empty. If it is empty, it packs into a filename generated from the folder's name. If fakesign is True, it will fakesign the Ticket and TMD, and update them as needed. If decrypted is true, it will assume the contents are already decrypted. For now, fakesign can not be True if decrypted is False, however fakesign can be False if decrypted is True. Title ID is a long integer of the destination title id."""
|
|
||||||
os.chdir(self.f)
|
|
||||||
|
|
||||||
tik = Ticket("tik")
|
|
||||||
tmd = TMD("tmd")
|
|
||||||
titlekey = tik.getTitleKey()
|
|
||||||
contents = tmd.getContents()
|
|
||||||
|
|
||||||
apppack = ""
|
|
||||||
for content in contents:
|
|
||||||
tmpdata = open("%08x.app" % content.index, "rb").read()
|
|
||||||
|
|
||||||
if(decrypted):
|
|
||||||
if(fakesign):
|
|
||||||
content.hash = str(Crypto().createSHAHash(tmpdata))
|
|
||||||
content.size = len(tmpdata)
|
|
||||||
|
|
||||||
encdata = Crypto().encryptContent(titlekey, content.index, tmpdata)
|
|
||||||
else:
|
|
||||||
encdata = tmpdata
|
|
||||||
|
|
||||||
apppack += encdata
|
|
||||||
if(len(encdata) % 64 != 0):
|
|
||||||
apppack += "\x00" * (64 - (len(encdata) % 64))
|
|
||||||
|
|
||||||
if(fakesign):
|
|
||||||
tmd.setContents(contents)
|
|
||||||
tmd.dump()
|
|
||||||
tik.dump()
|
|
||||||
|
|
||||||
rawtmd = open("tmd", "rb").read()
|
|
||||||
rawcert = open("cert", "rb").read()
|
|
||||||
rawtik = open("tik", "rb").read()
|
|
||||||
|
|
||||||
sz = 0
|
|
||||||
for i in range(len(contents)):
|
|
||||||
sz += contents[i].size
|
|
||||||
if(sz % 64 != 0):
|
|
||||||
sz += 64 - (contents[i].size % 64)
|
|
||||||
|
|
||||||
if(self.boot2 != True):
|
|
||||||
pack = struct.pack('>I4s6I', 32, "Is\x00\x00", len(rawcert), 0, len(rawtik), len(rawtmd), sz, 0)
|
|
||||||
pack += "\x00" * 32
|
|
||||||
else:
|
|
||||||
pack = struct.pack('>IIIII12s', 32, align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40), len(rawcert), len(rawtik), len(rawtmd), "\x00" * 12)
|
|
||||||
|
|
||||||
pack += rawcert
|
|
||||||
if(len(rawcert) % 64 != 0 and self.boot2 != True):
|
|
||||||
pack += "\x00" * (64 - (len(rawcert) % 64))
|
|
||||||
pack += rawtik
|
|
||||||
if(len(rawtik) % 64 != 0 and self.boot2 != True):
|
|
||||||
pack += "\x00" * (64 - (len(rawtik) % 64))
|
|
||||||
pack += rawtmd
|
|
||||||
if(len(rawtmd) % 64 != 0 and self.boot2 != True):
|
|
||||||
pack += "\x00" * (64 - (len(rawtmd) % 64))
|
|
||||||
|
|
||||||
if(self.boot2 == True):
|
|
||||||
pack += "\x00" * (align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40) - (len(rawcert) + len(rawtik) + len(rawtmd)))
|
|
||||||
|
|
||||||
pack += apppack
|
|
||||||
|
|
||||||
os.chdir('..')
|
|
||||||
if(fn == ""):
|
|
||||||
if(self.f[len(self.f) - 4:] == "_out"):
|
|
||||||
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:len(os.path.basename(self.f)) - 4].replace("_", ".")
|
|
||||||
else:
|
|
||||||
fn = self.f
|
|
||||||
open(fn, "wb").write(pack)
|
|
||||||
return fn
|
|
||||||
def unpack(self, fn = ""):
|
|
||||||
"""Unpacks the WAD from the parameter f in the initializer to either the value of fn, if there is one, or a folder created with this formula: `filename_extension_out`. Certs are put in the file "cert", TMD in the file "tmd", ticket in the file "tik", and contents are put in the files based on index and with ".app" added to the end."""
|
|
||||||
fd = open(self.f, 'rb')
|
|
||||||
if(self.boot2 != True):
|
|
||||||
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64))
|
|
||||||
else:
|
|
||||||
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32))
|
|
||||||
|
|
||||||
try:
|
|
||||||
if(fn == ""):
|
|
||||||
fn = self.f.replace(".", "_") + "_out"
|
|
||||||
os.mkdir(fn)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
os.chdir(fn)
|
|
||||||
|
|
||||||
rawcert = fd.read(certsize)
|
|
||||||
if(self.boot2 != True):
|
|
||||||
if(certsize % 64 != 0):
|
|
||||||
fd.seek(64 - (certsize % 64), 1)
|
|
||||||
open('cert', 'wb').write(rawcert)
|
|
||||||
|
|
||||||
rawtik = fd.read(tiksize)
|
|
||||||
if(self.boot2 != True):
|
|
||||||
if(tiksize % 64 != 0):
|
|
||||||
fd.seek(64 - (tiksize % 64), 1)
|
|
||||||
open('tik', 'wb').write(rawtik)
|
|
||||||
|
|
||||||
rawtmd = fd.read(tmdsize)
|
|
||||||
if(self.boot2 == True):
|
|
||||||
fd.seek(data_offset)
|
|
||||||
else:
|
|
||||||
fd.seek(64 - (tmdsize % 64), 1)
|
|
||||||
open('tmd', 'wb').write(rawtmd)
|
|
||||||
|
|
||||||
titlekey = Ticket("tik").getTitleKey()
|
|
||||||
contents = TMD("tmd").getContents()
|
|
||||||
for i in range(0, len(contents)):
|
|
||||||
tmpsize = contents[i].size
|
|
||||||
if(tmpsize % 16 != 0):
|
|
||||||
tmpsize += 16 - (tmpsize % 16)
|
|
||||||
tmptmpdata = fd.read(tmpsize)
|
|
||||||
tmpdata = Crypto().decryptContent(titlekey, contents[i].index, tmptmpdata)
|
|
||||||
open("%08x.app" % contents[i].index, "wb").write(tmpdata)
|
|
||||||
if(tmpsize % 64 != 0):
|
|
||||||
fd.seek(64 - (tmpsize % 64), 1)
|
|
||||||
fd.close()
|
|
||||||
os.chdir('..')
|
|
||||||
|
|
||||||
return fn
|
|
||||||
def __str__(self):
|
|
||||||
out = ""
|
|
||||||
out += "Wii WAD:\n"
|
|
||||||
fd = open(self.f, 'rb')
|
|
||||||
|
|
||||||
if(self.boot2 != True):
|
|
||||||
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64))
|
|
||||||
else:
|
|
||||||
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32))
|
|
||||||
|
|
||||||
rawcert = fd.read(certsize)
|
|
||||||
if(certsize % 64 != 0):
|
|
||||||
fd.seek(64 - (certsize % 64), 1)
|
|
||||||
rawtik = fd.read(tiksize)
|
|
||||||
if(self.boot2 != True):
|
|
||||||
if(tiksize % 64 != 0):
|
|
||||||
fd.seek(64 - (tiksize % 64), 1)
|
|
||||||
rawtmd = fd.read(tmdsize)
|
|
||||||
|
|
||||||
if(self.boot2 != True):
|
|
||||||
out += " Header %02x Type '%s' Certs %x Tiket %x TMD %x Data %x Footer %x\n" % (headersize, wadtype, certsize, tiksize, tmdsize, datasize, footersize)
|
|
||||||
else:
|
|
||||||
out += " Header %02x Type 'boot2' Certs %x Tiket %x TMD %x Data @ %x\n" % (headersize, certsize, tiksize, tmdsize, data_offset)
|
|
||||||
|
|
||||||
out += str(Ticket(rawtik))
|
|
||||||
out += str(TMD(rawtmd))
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
Loading…
Reference in New Issue
Block a user