mirror of
https://gitlab.com/Nanolx/qwad.git
synced 2024-11-01 08:35:07 +01:00
e573cd1f2f
support relative paths --tmdinfo no longer fails when an IOS-tmd is check don't enforce decrypt=false on unpacking CLI
455 lines
12 KiB
Python
455 lines
12 KiB
Python
from common import *
|
|
from title import *
|
|
import zlib
|
|
|
|
class U8(WiiArchive):
|
|
"""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):
|
|
self.files = []
|
|
def _dump(self):
|
|
header = self.U8Header()
|
|
rootnode = self.U8Node()
|
|
|
|
# constants
|
|
header.tag = "U\xAA8-"
|
|
header.rootnode_offset = 0x20
|
|
header.zeroes = "\x00" * 16
|
|
rootnode.type = 0x0100
|
|
|
|
nodes = []
|
|
strings = "\x00"
|
|
data = ''
|
|
|
|
for item, value in self.files:
|
|
node = self.U8Node()
|
|
|
|
recursion = item.count('/')
|
|
if(recursion < 0):
|
|
recursion = 0
|
|
name = item[item.rfind('/') + 1:]
|
|
|
|
node.name_offset = len(strings)
|
|
strings += name + '\x00'
|
|
|
|
if(value == None):
|
|
node.type = 0x0100
|
|
node.data_offset = recursion
|
|
|
|
node.size = len(nodes)
|
|
for one, two in self.files:
|
|
if(one[:len(item)] == item): # find nodes in the folder
|
|
node.size += 1
|
|
node.size += 1
|
|
else:
|
|
sz = len(value)
|
|
node.data_offset = len(data)
|
|
data += value + "\x00" * (align(sz, 32) - sz) # 32 seems to work best for fuzzyness? I'm still really not sure
|
|
node.size = sz
|
|
node.type = 0x0000
|
|
nodes.append(node)
|
|
|
|
header.header_size = ((len(nodes) + 1) * len(rootnode)) + len(strings)
|
|
header.data_offset = align(header.header_size + header.rootnode_offset, 64)
|
|
rootnode.size = len(nodes) + 1
|
|
|
|
for i in range(len(nodes)):
|
|
if(nodes[i].type == 0x0000):
|
|
nodes[i].data_offset += header.data_offset
|
|
|
|
fd = ''
|
|
fd += header.pack()
|
|
fd += rootnode.pack()
|
|
for nodeobj in nodes:
|
|
fd += nodeobj.pack()
|
|
fd += strings
|
|
fd += "\x00" * (header.data_offset - header.rootnode_offset - header.header_size)
|
|
fd += data
|
|
|
|
return fd
|
|
def _dumpDir(self, dir):
|
|
if(not os.path.isdir(dir)):
|
|
os.mkdir(dir)
|
|
old = os.getcwd()
|
|
os.chdir(dir)
|
|
for item, data in self.files:
|
|
if(data == None):
|
|
if(not os.path.isdir(item)):
|
|
os.mkdir(item)
|
|
else:
|
|
open(item, "wb").write(data)
|
|
os.chdir(old)
|
|
def _loadDir(self, dir):
|
|
try:
|
|
self._tmpPath += ''
|
|
except:
|
|
self._tmpPath = ''
|
|
old = os.getcwd()
|
|
os.chdir(dir)
|
|
entries = os.listdir(".")
|
|
for entry in entries:
|
|
if(os.path.isdir(entry)):
|
|
self.files.append((self._tmpPath + entry, None))
|
|
self._tmpPath += entry + '/'
|
|
self._loadDir(entry)
|
|
elif(os.path.isfile(entry)):
|
|
data = open(entry, "rb").read()
|
|
self.files.append((self._tmpPath + entry, data))
|
|
os.chdir(old)
|
|
self._tmpPath = self._tmpPath[:self._tmpPath.find('/') + 1]
|
|
def _load(self, data):
|
|
offset = 0
|
|
|
|
header = self.U8Header()
|
|
header.unpack(data[offset:offset + len(header)])
|
|
offset += len(header)
|
|
|
|
assert header.tag == "U\xAA8-"
|
|
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)
|
|
|
|
recursion = [rootnode.size]
|
|
recursiondir = []
|
|
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)
|
|
recursiondir.append(name)
|
|
assert len(recursion) == node.data_offset + 2 # haxx
|
|
self.files.append(('/'.join(recursiondir), None))
|
|
elif(node.type == 0): # file
|
|
self.files.append(('/'.join(recursiondir) + '/' + name, data[node.data_offset:node.data_offset + node.size]))
|
|
offset += node.size
|
|
else: # unknown type -- wtf?
|
|
pass
|
|
|
|
sz = recursion.pop()
|
|
if(sz != counter + 1):
|
|
recursion.append(sz)
|
|
else:
|
|
recursiondir.pop()
|
|
def __str__(self):
|
|
ret = ''
|
|
for key, value in self.files:
|
|
name = key[key.rfind('/') + 1:]
|
|
recursion = key.count('/')
|
|
ret += ' ' * recursion
|
|
if(value == None):
|
|
ret += '[' + name + ']'
|
|
else:
|
|
ret += name
|
|
ret += '\n'
|
|
return ret
|
|
def __getitem__(self, key):
|
|
for item, val in self.files:
|
|
if(item == key):
|
|
if(val != None):
|
|
return val
|
|
else:
|
|
ret = []
|
|
for item2, val2 in self.files:
|
|
if(item2.find(item) == 0):
|
|
ret.append(item2[len(item) + 1:])
|
|
return ret[1:]
|
|
raise KeyError
|
|
def __setitem__(self, key, val):
|
|
for i in range(len(self.files)):
|
|
if(self.files[i][0] == key):
|
|
self.files[i] = (self.files[i][0], val)
|
|
return
|
|
self.files.append((key, val))
|
|
|
|
|
|
class WAD(WiiArchive):
|
|
def __init__(self, boot2 = False):
|
|
self.tmd = TMD()
|
|
self.tik = Ticket()
|
|
self.contents = []
|
|
self.boot2 = False
|
|
self.cert = ""
|
|
def _load(self, data):
|
|
if(self.boot2 != True):
|
|
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding = struct.unpack('>I4s6I32s', data[:64])
|
|
pos = 64
|
|
else:
|
|
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', data[:32])
|
|
pos = 32
|
|
|
|
rawcert = data[pos:pos + certsize]
|
|
pos += certsize
|
|
if(self.boot2 != True):
|
|
if(certsize % 64 != 0):
|
|
pos += 64 - (certsize % 64)
|
|
self.cert = rawcert
|
|
|
|
rawtik = data[pos:pos + tiksize]
|
|
pos += tiksize
|
|
if(self.boot2 != True):
|
|
if(tiksize % 64 != 0):
|
|
pos += 64 - (tiksize % 64)
|
|
self.tik = Ticket.load(rawtik)
|
|
|
|
rawtmd = data[pos:pos + tmdsize]
|
|
pos += tmdsize
|
|
if(self.boot2 == True):
|
|
pos = data_offset
|
|
else:
|
|
pos += 64 - (tmdsize % 64)
|
|
self.tmd = TMD.load(rawtmd)
|
|
|
|
titlekey = self.tik.getTitleKey()
|
|
contents = self.tmd.getContents()
|
|
for i in range(0, len(contents)):
|
|
tmpsize = contents[i].size
|
|
if(tmpsize % 16 != 0):
|
|
tmpsize += 16 - (tmpsize % 16)
|
|
encdata = data[pos:pos + tmpsize]
|
|
pos += tmpsize
|
|
decdata = Crypto().decryptContent(titlekey, contents[i].index, encdata)
|
|
self.contents.append(decdata)
|
|
if(tmpsize % 64 != 0):
|
|
pos += 64 - (tmpsize % 64)
|
|
def _loadDir(self, dir):
|
|
origdir = os.getcwd()
|
|
os.chdir(dir)
|
|
|
|
self.tmd = TMD.loadFile("tmd")
|
|
self.tik = Ticket.loadFile("tik")
|
|
self.cert = open("cert", "rb").read()
|
|
|
|
contents = self.tmd.getContents()
|
|
for i in range(len(contents)):
|
|
self.contents.append(open("%08x.app" % i, "rb").read())
|
|
os.chdir(origdir)
|
|
def _dumpDir(self, dir):
|
|
origdir = os.getcwd()
|
|
os.chdir(dir)
|
|
|
|
contents = self.tmd.getContents()
|
|
for i in range(len(contents)):
|
|
open("%08x.app" % i, "wb").write(self.contents[i])
|
|
self.tmd.dumpFile("tmd")
|
|
self.tik.dumpFile("tik")
|
|
open("cert", "wb").write(self.cert)
|
|
|
|
os.chdir(origdir)
|
|
def _dump(self, fakesign = True):
|
|
titlekey = self.tik.getTitleKey()
|
|
contents = self.tmd.getContents()
|
|
|
|
apppack = ""
|
|
for i, content in enumerate(contents):
|
|
if(fakesign):
|
|
content.hash = str(Crypto().createSHAHash(self.contents[content.index]))
|
|
content.size = len(self.contents[content.index])
|
|
|
|
encdata = Crypto().encryptContent(titlekey, content.index, self.contents[content.index])
|
|
|
|
apppack += encdata
|
|
if(len(encdata) % 64 != 0):
|
|
apppack += "\x00" * (64 - (len(encdata) % 64))
|
|
|
|
if(fakesign):
|
|
self.tmd.setContents(contents)
|
|
self.tmd.fakesign()
|
|
self.tik.fakesign()
|
|
|
|
rawtmd = self.tmd.dump()
|
|
rawcert = self.cert
|
|
rawtik = self.tik.dump()
|
|
|
|
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
|
|
return pack
|
|
def __getitem__(self, idx):
|
|
return self.contents[idx]
|
|
def __setitem__(self, idx, value):
|
|
self.contents[idx] = value
|
|
def __str__(self):
|
|
out = ""
|
|
out += "Wii WAD:\n"
|
|
out += str(self.tmd)
|
|
out += str(self.tik)
|
|
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)
|
|
|
|
if(__name__ == '__main__'):
|
|
wad = WAD.loadFile("testing.wad")
|
|
print wad
|
|
wad.dumpDir("outdir")
|
|
wad.dumpFile("interesting.wad", fakesign = False) #keyword arguements work as expected when calling _dump(). awesome.
|
|
wad2 = WAD.loadDir("outdir")
|
|
print wad2
|
|
wad3 = WAD.loadFile("interesting.wad")
|
|
print wad3
|
|
wad3.dumpDir("outdir2")
|