1
0
mirror of https://github.com/mon/ifstools.git synced 2024-12-02 20:47:16 +01:00
ifstools/ifstools.py

208 lines
6.3 KiB
Python
Raw Normal View History

2017-12-18 10:37:16 +01:00
from os.path import basename, dirname, splitext, join, isdir, isfile, getmtime
from os import mkdir, utime, walk
from io import BytesIO
2017-12-15 18:43:57 +01:00
import hashlib
import lxml.etree as etree
2017-12-18 10:37:16 +01:00
from time import time as unixtime
2017-12-15 18:43:57 +01:00
from kbinxml.kbinxml import KBinXML
from kbinxml.bytebuffer import ByteBuffer
from handlers import GenericFolder
2017-12-18 10:37:16 +01:00
SIGNATURE = 0x6CAD8F89
2017-12-15 18:43:57 +01:00
KBIN_OFFSET = 36
2017-12-18 10:37:16 +01:00
FILE_VERSION = 3
2017-12-15 18:43:57 +01:00
class IFS:
def __init__(self, path):
2017-12-18 10:37:16 +01:00
if isfile(path):
self._load_ifs(path)
self.is_file = True
elif isdir(path):
self._load_dir(path)
self.is_file = False
else:
raise IOError('Input path does not exist')
def _load_ifs(self, path):
2017-12-15 18:43:57 +01:00
out = splitext(basename(path))[0] + '_ifs'
self.default_out = join(dirname(path), out)
2017-12-18 10:37:16 +01:00
self.ifs_out = path
2017-12-15 18:43:57 +01:00
with open(path, 'rb') as f:
self.file = f.read()
b = ByteBuffer(self.file)
2017-12-18 10:37:16 +01:00
signature = b.get_u32()
if signature != SIGNATURE:
raise IOError('Given file was not an IFS file!')
self.file_version = b.get_u16()
# next u16 is just NOT(version)
assert b.get_u16() ^ self.file_version == 0xFFFF
self.time = b.get_u32()
self.tree_size = b.get_u32()
2017-12-15 18:43:57 +01:00
self.header_end = b.get_u32()
2017-12-18 10:37:16 +01:00
# 16 bytes for manifest md5, unchecked
2017-12-15 18:43:57 +01:00
self.manifest = KBinXML(self.file[KBIN_OFFSET:])
self._parse_manifest()
2017-12-18 10:37:16 +01:00
assert self.tree_size == self._tree_size()
def _load_dir(self, path):
self.default_out = path
2017-12-18 10:41:34 +01:00
self.ifs_out = dirname(path).replace('_ifs', '.ifs')
2017-12-18 10:37:16 +01:00
self.file_version = FILE_VERSION
self.time = int(getmtime(path))
self.tree_size = -1
self.header_end = -1
self.manifest = None
os_tree = self._create_dir_tree(path)
self.tree = GenericFolder.from_filesystem(self, os_tree)
def _create_dir_tree(self, path):
tree = self._create_dir_tree_recurse(walk(path))
if 'ifs_manifest.xml' in tree['files']:
tree['files'].remove('ifs_manifest.xml')
return tree
def _create_dir_tree_recurse(self, walker):
tree = {}
root, dirs, files = next(walker)
tree['path'] = root
tree['files'] = files
tree['folders'] = []
for dir in dirs:
tree['folders'].append(self._create_dir_tree_recurse(walker))
return tree
2017-12-15 18:43:57 +01:00
def _parse_manifest(self):
2017-12-18 10:37:16 +01:00
self.tree = GenericFolder.from_xml(self, self.manifest.xml_doc)
2017-12-15 18:43:57 +01:00
def tostring(self):
return self.tree.tostring()
def extract_all(self, progress = True, recurse = True, path = None):
self.out = path if path else self.default_out
self._mkdir(self.out)
2017-12-18 10:37:16 +01:00
if self.manifest:
with open(join(self.out, 'ifs_manifest.xml'), 'wb') as f:
f.write(self.manifest.to_text().encode('utf8'))
2017-12-15 18:43:57 +01:00
self._extract_tree(self.tree, progress, recurse)
2017-12-18 10:37:16 +01:00
def repack(self, progress = True, path = None):
if path is None:
path = self.ifs_out
data_blob = BytesIO()
self.manifest = KBinXML(etree.Element('imgfs'))
manifest_info = etree.SubElement(self.manifest.xml_doc, '_info_')
# the important bit
self.tree.repack(self.manifest.xml_doc, data_blob, progress)
data = data_blob.getvalue()
data_md5 = etree.SubElement(manifest_info, 'md5')
data_md5.attrib['__type'] = 'bin'
data_md5.attrib['__size'] = '16'
data_md5.text = hashlib.md5(data).hexdigest()
data_size = etree.SubElement(manifest_info, 'size')
data_size.attrib['__type'] = 'u32'
data_size.text = str(len(data))
manifest_bin = self.manifest.to_binary()
self.header_end = 36 + len(manifest_bin)
self.ifs_size = self.header_end + len(data)
self.tree_size = self._tree_size()
manifest_hash = hashlib.md5(manifest_bin).digest()
head = ByteBuffer()
head.append_u32(SIGNATURE)
head.append_u16(self.file_version)
head.append_u16(self.file_version ^ 0xFFFF)
head.append_u32(int(unixtime()))
head.append_u32(self.tree_size)
head.append_u32(self.header_end)
with open(path, 'wb') as ifs_file:
ifs_file.write(head.data)
ifs_file.write(manifest_hash)
ifs_file.write(manifest_bin)
ifs_file.write(data)
# suspected to be the in-memory representation
def _tree_size(self):
BASE_SIZE = 856
return BASE_SIZE + self._tree_size_recurse(self.tree)
def _tree_size_recurse(self, tree, depth = 0):
FILE = 64
FOLDER = 56
DEPTH_MULTIPLIER = 16
size = len(tree.files) * FILE
size += len(tree.folders) * (FOLDER - depth*DEPTH_MULTIPLIER)
for name, folder in tree.folders.items():
size += self._tree_size_recurse(folder, depth+1)
return size
2017-12-15 18:43:57 +01:00
def _extract_tree(self, tree, progress = True, recurse = True, dir = ''):
outdir = join(self.out, dir)
if progress:
print(outdir)
self._mkdir(outdir)
for name, f in tree.files.items():
out = join(outdir, f.name)
if progress:
print(out)
data = f.load()
self._save_with_time(out, data, f.time)
if recurse and f.name.endswith('.ifs'):
i = IFS(out)
i.extract_all()
for name, f in tree.folders.items():
self._extract_tree(f, progress, recurse, join(dir, f.name))
2017-12-18 10:37:16 +01:00
# fallback to file timestamp
timestamp = tree.time if tree.time else self.time
utime(outdir, (timestamp, timestamp))
2017-12-15 18:43:57 +01:00
def _mkdir(self, dir):
try:
mkdir(dir)
except FileExistsError:
pass
def load_file(self, start, size):
2017-12-18 10:37:16 +01:00
start = self.header_end+start
end = start + size
assert start <= len(self.file) and end <= len(self.file)
return self.file[start:end]
2017-12-15 18:43:57 +01:00
def _save_with_time(self, filename, data, time):
with open(filename, 'wb') as f:
f.write(data)
utime(filename, (time,time))
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
2017-12-18 10:37:16 +01:00
print('ifstools filename.ifs OR folder_ifs')
2017-12-15 18:43:57 +01:00
exit()
2017-12-16 02:00:56 +01:00
i = IFS(sys.argv[1])
2017-12-18 10:37:16 +01:00
if i.is_file:
i.extract_all()
else:
i.repack()