-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathu8archive.py
executable file
·168 lines (153 loc) · 5.27 KB
/
u8archive.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python
# Author: Bryan Cain (Plombo)
# Date: December 27, 2010
# Description: Reads Wii U8 archives.
import os, struct, posixpath
from cStringIO import StringIO
import lz77, huf8, lzh8
class U8Archive(object):
# archive can be a string (filesystem path) or file-like object
def __init__(self, archive):
if type(archive) == str:
#print archive
self.file = open(archive, 'rb')
else:
self.file = archive
assert self.file
self.files = []
self.readheader()
def readheader(self):
magic, rootnode_offset, header_size, data_offset = tuple(struct.unpack('>IIII', self.file.read(16)))
assert magic == 0x55aa382d
assert rootnode_offset == 0x20
assert self.file.read(16) == 16 * '\0'
root = Node(self.file, rootnode_offset)
root.path = '<root>'
path = ''
curdirs = [root.size]
dirnames = ['<root>']
filenum = 1
while curdirs:
node = Node(self.file, rootnode_offset + 12 * root.size)
node.path = posixpath.join(path, node.name)
filenum += 1
if node.type == 0x100:
# change current path if this is a directory
path = node.path
#print node.name, node.path
curdirs.append(node.size)
dirnames.append(node.name)
else: self.files.append(node)
indices = range(len(curdirs))
indices.reverse()
while curdirs and filenum >= curdirs[len(curdirs)-1]:
#print 'done with ' + dirnames.pop() + ' at %d' % filenum
path = posixpath.dirname(path)
curdirs.pop()
# closes the physical file associated with this archive
def close(self):
self.file.close()
# returns True if this archive has a file with the given path
def hasfile(self, path):
f = self.getfile(path)
if f: f.close(); return True
else: return False
# returns a file-like object (actually a cStringIO object) for the specified
# file; detects and decompresses compressed files (LZ77/Huf8/LZH8) automatically!
# path: file name (string) or actual file node, but NOT node path :D
def getfile(self, path):
for node in self.files:
if node == path or (type(path) == str and node.name.endswith(path)):
if node == path: path = node.name
self.file.seek(node.data_offset)
file = StringIO(self.file.read(node.size))
if path.startswith("LZ77"):
try:
decompressed_file = lz77.decompress(file)
file.close()
return decompressed_file
except ValueError, IndexError:
print "LZ77 decompression of '%s' failed" % path
print 'Dumping compressed file to %s' % path
f2 = open(path, "wb")
f2.write(file.read())
f2.close()
file.close()
return None
elif path.startswith("Huf8"):
try:
decompressed_file = StringIO()
huf8.decompress(file, decompressed_file)
file.close()
decompressed_file.seek(0)
return decompressed_file
except Exception:
print "Huf8 decompression of '%s' failed" % path
print "Dumping compressed file to %s" % path
f2 = open(path, "wb")
f2.write(file.read())
f2.close()
file.close()
return decompressed_file
elif path.startswith("LZH8"):
try:
decompressed_file = StringIO()
decompressed_file.write(lzh8.decompress(file))
decompressed_file.seek(0)
file.close()
return decompressed_file
except Exception:
print "LZH8 decompression of '%s' failed" % path
print "Dumping compressed file to %s" % path
f2 = open(path, "wb")
f2.write(file.read())
f2.close()
file.close()
else:
return file
return None
# finds a file with the given name, accounting for compression prefixes like "LZ77", "Huf8", etc.
def findfile(self, name):
for f in self.files:
names = (name, "LZ77"+name, "LZ77_"+name, "Huf8"+name, "Huf8_"+name, "LZH8"+name, "LZH8_"+name)
if f.name in names: return f.name
return None
def extract(self, dest):
if not os.path.lexists(dest): os.makedirs(dest)
for node in self.files:
if node.name in ('<root>', '.'): continue
if node.type == 0x100:
os.makedirs(os.path.join(dest, node.path))
#print 'created dir %s' % os.path.join(dest, node.path)
else:
#print node.path
path = os.path.join(dest, node.path)
if not os.path.lexists(os.path.dirname(path)): os.makedirs(os.path.dirname(path))
f = open(path, 'wb')
contents = self.getfile(node)
contents.seek(0)
f.write(contents.read())
f.close()
#print 'extracted file %s' % os.path.join(dest, node.path)
# file node object
class Node(object):
def __init__(self, arcfile, stringoffset):
self.rawdata = arcfile.read(12)
arcpos = arcfile.tell()
chunk1, self.data_offset, self.size = tuple(struct.unpack('>III', self.rawdata))
self.type = chunk1 >> 16
self.name_offset = chunk1 & 0xffffff
# the root node has a name_offset of 0
if not self.name_offset: return
# no sane file name should be more than 64 bytes; if one is, string.index() will throw an exception
arcfile.seek(stringoffset + self.name_offset)
self.name = arcfile.read(64)
self.name = self.name[0:self.name.index('\0')]
#print self.name
arcfile.seek(arcpos)
if __name__ == '__main__':
# Quick functionality test and sanity check; will only work on my (Plombo's) computer without a path change
import os, os.path
arc = U8Archive(os.path.join(os.getenv('HOME'), 'wii/ssb/00000005.app'))
print arc.hasfile('romc')
print len(arc.getfile('romc').read())