diff --git a/Wii.py b/Wii.py index dc008b8..2f0b9cf 100644 --- a/Wii.py +++ b/Wii.py @@ -2,7 +2,6 @@ __all__ = [] from common import * from formats import * -from banner import * from title import * from disc import * from image import * diff --git a/archive.py b/archive.py index 0f8dfd3..233e044 100644 --- a/archive.py +++ b/archive.py @@ -2,7 +2,7 @@ from common import * from title import * import zlib -class U8(): +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.""" @@ -21,48 +21,9 @@ class U8(): 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 = ""): + def __init__(self): + self.files = [] + def _dump(self): """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.""" @@ -73,56 +34,94 @@ class U8(): 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) + nodes = [] + strings = "\x00" + data = '' - header.header_size = (len(self.nodes) + 1) * len(rootnode) + len(self.strings) + for item, value in self.files: + node = self.U8Node() + node.name_offset = len(strings) + + recursion = item.count('/') + if(recursion < 0): + recursion = 0 + name = item[item.rfind('/') + 1:] + strings += name + '\x00' + + if(value == None): + node.type = 0x0100 + node.data_offset = recursion + + this_length = 0 + for one, two in self.files: + subdirs = one + if(subdirs.find(item) != -1): + this_length += 1 + node.size = len(nodes) + this_length + 1 + else: + sz = len(value) + value += "\x00" * (align(sz, 32) - sz) #32 seems to work best for fuzzyness? I'm still really not sure + node.data_offset = len(data) + data += value + 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(self.nodes) + 1 + rootnode.size = len(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("_", ".") + 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) + os.chdir(dir) + for item, data in self.files: + if(data == None): + if(not os.path.isdir(item)): + os.mkdir(item) 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() - + open(item, "wb").write(data) + os.chdir("..") + def _loadDir(self, dir): + try: + self._tmpPath += '' + except: + self._tmpPath = '' + 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("..") + 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) - if(header.tag != "U\xAA8-"): - raise NameError("Bad U8 Tag") + assert header.tag == "U\xAA8-" offset = header.rootnode_offset rootnode = self.U8Node() @@ -139,95 +138,52 @@ class U8(): 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] + recursiondir = [] counter = 0 for node in nodes: counter += 1 name = strings[node.name_offset:].split('\0', 1)[0] - if(node.type == 0x0100): #folder + 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]) + recursiondir.append(name) + assert len(recursion) == node.data_offset + 2 #bad idea? + 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 - pass #ignore - + else: # unknown + pass + sz = recursion.pop() - if(sz == counter + 1): - os.chdir("..") - else: + if(sz != counter + 1): recursion.append(sz) - os.chdir("..") - - os.chdir(origdir) - return fn + else: + recursiondir.pop() 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 + ret = '' + for key, value in self.files: + name = key[key.rfind('/') + 1:] + recursion = key.count('/') + ret += ' ' * recursion + if(value == None): + ret += '[' + name + ']' else: - recursion.append(sz) - return out + ret += name + ret += '\n' + return ret + def __getitem__(self, key): + for item, val in self.files: + if(item == key): + return val + raise KeyError + def __setitem__(self, key, val): + for i in self.files: + if(self.files[i][0] == key): + self.files[i][1] = val + raise KeyError + 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. @@ -240,8 +196,8 @@ class WAD: """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().loadFile("tik") - tmd = TMD().loadFile("tmd") + tik = Ticket.loadFile("tik") + tmd = TMD.loadFile("tmd") titlekey = tik.getTitleKey() contents = tmd.getContents() @@ -264,8 +220,10 @@ class WAD: if(fakesign): tmd.setContents(contents) - tmd.dump() - tik.dump() + tmd.fakesign() + tik.fakesign() + tmd.dumpFile("tmd") + tik.dumpFile("tik") rawtmd = open("tmd", "rb").read() rawcert = open("cert", "rb").read() @@ -341,8 +299,8 @@ class WAD: fd.seek(64 - (tmdsize % 64), 1) open('tmd', 'wb').write(rawtmd) - titlekey = Ticket().loadFile("tik").getTitleKey() - contents = TMD().loadFile("tmd").getContents() + titlekey = Ticket.loadFile("tik").getTitleKey() + contents = TMD.loadFile("tmd").getContents() for i in range(0, len(contents)): tmpsize = contents[i].size if(tmpsize % 16 != 0): @@ -380,8 +338,8 @@ class WAD: else: out += " Header %02x Type 'boot2' Certs %x Tiket %x TMD %x Data @ %x\n" % (headersize, certsize, tiksize, tmdsize, data_offset) - out += str(Ticket().load(rawtik)) - out += str(TMD().load(rawtmd)) + out += str(Ticket.load(rawtik)) + out += str(TMD.load(rawtmd)) return out diff --git a/common.py b/common.py index 94fcac8..10e3b97 100644 --- a/common.py +++ b/common.py @@ -62,3 +62,31 @@ class Crypto: # else: # return 0 +class WiiObject(object): + @classmethod + def load(cls, data): + self = cls() + self._load(data) + return self + @classmethod + def loadFile(cls, filename): + return cls.load(open(filename, "rb").read()) + + def dump(self): + return self._dump() + def dumpFile(self, filename): + open(filename, "wb").write(self.dump()) + return filename + +class WiiArchive(WiiObject): + @classmethod + def loadDir(cls, dirname): + self = cls() + self._loadDir(dirname) + return self + + def dumpDir(self, dirname): + if(not os.path.isdir(dirname)): + os.mkdir(dirname) + self._dumpDir(dirname) + return dirname diff --git a/nand.py b/nand.py index ac7ae18..dbca5f5 100644 --- a/nand.py +++ b/nand.py @@ -385,12 +385,14 @@ class NAND: tmdpth = self.f + "/title/%08x/%08x/content/title.tmd" % (title >> 32, title & 0xFFFFFFFF) if(version != 0): tmdpth += ".%d" % version - tmd = TMD().loadFile(tmdpth) + tmd = TMD.loadFile(tmdpth) if(not os.path.isdir("export")): os.mkdir("export") - tmd.dump("export/tmd") - tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (title >> 32, title & 0xFFFFFFFF)) - tik.dump("export/tik") + tmd.fakesign() + tmd.dumpFile("export/tmd") + tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (title >> 32, title & 0xFFFFFFFF)) + tik.fakesign() + tik.dumpFile("export/tik") contents = tmd.getContents() for i in range(tmd.tmd.numcontents): path = "" @@ -645,14 +647,14 @@ class ESClass: def Identify(self, id, version=0): if(not os.path.isfile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF))): return None - tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF)) + tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF)) titleid = tik.titleid path = "/title/%08x/%08x/content/title.tmd" % (titleid >> 32, titleid & 0xFFFFFFFF) if(version): path += ".%d" % version if(not os.path.isfile(self.f + path)): return None - tmd = TMD().loadFile(self.f + path) + tmd = TMD.loadFile(self.f + path) self.title = titleid self.group = tmd.tmd.group_id return self.title @@ -670,7 +672,7 @@ class ESClass: path += ".%d" % version if(not os.path.isfile(self.f + path)): return None - return TMD().loadFile(self.f + path) + return TMD.loadFile(self.f + path) def GetTitleContentsCount(self, titleid, version=0): """Gets the number of contents the title with the specified titleid and version has.""" tmd = self.GetStoredTMD(titleid, version) @@ -708,7 +710,7 @@ class ESClass: return def AddTicket(self, tik): """Adds ticket to the title being added.""" - tik.rawdump(self.f + "/tmp/title.tik") + tik.dumpFile(self.f + "/tmp/title.tik") self.ticketadded = 1 def DeleteTicket(self, tikview): """Deletes the ticket relating to tikview @@ -716,7 +718,7 @@ class ESClass: return def AddTitleTMD(self, tmd): """Adds TMD to the title being added.""" - tmd.rawdump(self.f + "/tmp/title.tmd") + tmd.dumpFile(self.f + "/tmp/title.tmd") self.tmdadded = 1 def AddContentStart(self, titleid, cid): """Starts adding a content with content id cid to the title being added with ID titleid.""" @@ -724,9 +726,9 @@ class ESClass: "Trying to start an already existing process" return -41 if(self.tmdadded): - a = TMD().loadFile(self.f + "/tmp/title.tmd") + a = TMD.loadFile(self.f + "/tmp/title.tmd") else: - a = TMD().loadFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version)) + a = TMD.loadFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version)) x = self.getContentIndexFromCID(a, cid) if(x == None): "Not a valid Content ID" @@ -766,11 +768,11 @@ class ESClass: def AddTitleFinish(self): """Finishes the adding of a title.""" if(self.ticketadded): - tik = Ticket().loadFile(self.f + "/tmp/title.tik") + tik = Ticket.loadFile(self.f + "/tmp/title.tik") else: - tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) + tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) if(self.tmdadded): - tmd = TMD().loadFile(self.f + "/tmp/title.tmd") + tmd = TMD.loadFile(self.f + "/tmp/title.tmd") contents = tmd.getContents() for i in range(self.workingcidcnt): idx = self.getContentIndexFromCID(tmd, self.workingcids[i]) @@ -805,12 +807,12 @@ class ESClass: outfp.close() if(self.tmdadded and self.use_version): self.nand.newFile("/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version), "rwrw--", 0x0000) - tmd.rawdump(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version)) + tmd.dumpFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version)) elif(self.tmdadded): self.nand.newFile("/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF), "rwrw--", 0x0000) - tmd.rawdump(self.f + "/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) + tmd.dumpFile(self.f + "/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) if(self.ticketadded): self.nand.newFile("/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF), "rwrw--", 0x0000) - tik.rawdump(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) + tik.dumpFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF)) self.AddTitleCancel() return 0 diff --git a/title.py b/title.py index a5c2223..165ed04 100644 --- a/title.py +++ b/title.py @@ -37,7 +37,7 @@ class TicketView: return out -class Ticket: +class Ticket(WiiObject): """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): __endian__ = Struct.BE @@ -79,9 +79,7 @@ class Ticket: if(self.tik.commonkey_index == 1): #korean, kekekekek! commonkey = koreankey self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey) - @classmethod - def load(cls, data): - self = cls() + def _load(self, data): self.tik.unpack(data[:len(self.tik)]) commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7" @@ -92,9 +90,6 @@ class Ticket: self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey) return self - @classmethod - def loadFile(cls, filename): - return cls.load(open(filename, "rb").read()) def getTitleKey(self): """Returns a string containing the title key.""" return self.titlekey @@ -128,7 +123,7 @@ class Ticket: out += "\n" return out - def dump(self, fn = ""): + def fakesign(self): """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 for i in range(65536): @@ -137,25 +132,13 @@ class Ticket: break if(i == 65535): raise ValueError("Failed to fakesign. Aborting...") - - if(fn == ""): - open(self.f, "wb").write(self.tik.pack()) - return self.f - else: - open(fn, "wb").write(self.tik.pack()) - return fn - def rawdump(self, fn = ""): + def _dump(self): """Dumps the ticket to either fn, if not empty, or overwriting the source if empty. **Does not fakesign.** Returns the output filename.""" - if(fn == ""): - open(self.f, "wb").write(self.tik.pack()) - return self.f - else: - open(fn, "wb").write(self.tik.pack()) - return fn + return self.tik.pack() def __len__(self): return len(self.tik) -class TMD: +class TMD(WiiObject): """This class allows you to edit TMDs. TMD (Title Metadata) files are used in many places to hold information about titles. The parameter f to the initialization is the filename to open and create a TMD from.""" class TMDContent(Struct): __endian__ = Struct.BE @@ -184,7 +167,7 @@ class TMD: self.boot_index = Struct.uint16 self.padding2 = Struct.uint16 #contents follow this - def load(self, data): + def _load(self, data): self.tmd.unpack(data[:len(self.tmd)]) pos = len(self.tmd) for i in range(self.tmd.numcontents): @@ -192,9 +175,6 @@ class TMD: cont.unpack(data[pos:pos + len(cont)]) pos += len(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 @@ -232,7 +212,7 @@ class TMD: for i in range(len(contents)): sz += len(contents[i]) return sz - def dump(self, fn = ""): + def fakesign(self): """Dumps the TMD to the filename specified in fn, if not empty. If that is empty, it overwrites the original. This fakesigns the TMD, but does not update the hashes and the sizes, that is left as a job for you. Returns output filename.""" for i in range(65536): self.tmd.padding2 = i @@ -245,26 +225,14 @@ class TMD: break if(i == 65535): raise ValueError("Failed to fakesign! Aborting...") - - if(fn == ""): - open(self.f, "wb").write(data) - return self.f - else: - open(fn, "wb").write(data) - return fn - def rawdump(self, fn = ""): + def _dump(self): """Same as the :dump: function, but does not fakesign the TMD. Also returns output filename.""" data = "" data += self.tmd.pack() for i in range(self.tmd.numcontents): data += self.contents[i].pack() - if(fn == ""): - open(self.f, "wb").write(data) - return self.f - else: - open(fn, "wb").write(data) - return fn + return data def getTitleID(self): """Returns the long integer title id.""" return self.tmd.titleid @@ -320,11 +288,11 @@ class NUS: urllib.urlretrieve(self.baseurl + "tmd" + versionstring, "tmd") tmd = TMD.loadFile("tmd") - tmd.rawdump("tmd") # strip certs + tmd.dumpFile("tmd") # strip certs urllib.urlretrieve(self.baseurl + "cetk", "tik") tik = Ticket.loadFile("tik") - tik.rawdump("tik") # strip certs + tik.dumpFile("tik") # strip certs if(decrypt): titlekey = tik.getTitleKey()