utils/lifextractor.pyw

133 lines
3.9 KiB
Python

import datetime
import enum
import io
import os
import struct
import tkinter.filedialog as filedialog
import extractor
from pyraknet.bitstream import ReadStream, UnsignedIntStruct
class be_ushort(UnsignedIntStruct):
_struct = struct.Struct(">H")
class be_uint(UnsignedIntStruct):
_struct = struct.Struct(">I")
class be_uint64(UnsignedIntStruct):
_struct = struct.Struct(">Q")
class Enum1(enum.Enum):
Root = 1
Unknown = 2
Directory = 3
File = 4
Metadata = 5
class LIFExtractor(extractor.Extractor):
def askopener(self):
return filedialog.askopenfilename(filetypes=[("LIF", "*.lif")])
def load(self, path: str) -> None:
super().load(path)
self.lif_path = path
self.current_file_data_offset = 84
with open(path, "rb") as file:
header = file.read(4)
if header != b"LIFF":
raise ValueError("Not a LIF file")
header = ReadStream(file.read(14))
lifsize = header.read(be_uint64)
assert header.read(be_ushort) == 1
assert header.read(be_uint) == 0
self._read_part(file, 0)
assert file.tell() == lifsize
self.set_headings("Size (Bytes)", "Creation time?", "Last modification time?", "Last access time?", treeheading="Filename")
for filename in sorted(self.records.keys()):
self.tree_insert_path(filename, self.records[filename][1:])
def _read_part(self, file, level):
start = file.tell()
stream = ReadStream(file.read(20))
assert stream.read(be_ushort) == 1
entry_type = Enum1(stream.read(be_ushort))
size = stream.read(be_uint64)
uint1 = stream.read(be_uint)
if entry_type in (Enum1.Unknown, Enum1.File, Enum1.Metadata):
assert uint1 == 1
else:
assert uint1 == 0
if entry_type != Enum1.File:
print(" "*level+entry_type.name)
assert stream.read(be_uint) == 0
if entry_type == Enum1.Unknown:
t2stream = ReadStream(file.read(6))
assert t2stream.read(be_ushort) == 1
assert t2stream.read(be_uint) == 0
elif entry_type == Enum1.File:
file.seek(size - 20, io.SEEK_CUR) # skip file content
elif entry_type == Enum1.Metadata:
self.lif = ReadStream(file.read(size - 20))
assert self.lif.read(be_ushort) == 1
self._read_dir()
assert self.lif.all_read()
if uint1 == 0:
while file.tell() - start < size:
self._read_part(file, level+1)
def _read_direntry(self):
something = self.lif.read(be_uint)
string = b""
while True:
char = self.lif.read(bytes, length=2)
if char == b"\0\0":
break
string += char
name = string.decode("utf-16-be")
size = self.lif.read(be_uint64)
return something, name, size
def _convert_time(self, wintime):
microseconds = wintime / 10
return str(datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=microseconds))
def _read_dir(self, dirname=""):
something, name, size = self._read_direntry()
dirname = os.path.join(dirname, name)
if dirname == "":
assert something == 0 # root
else:
assert something == 7 # directory
assert size == 20
for _ in range(self.lif.read(be_uint)):
entry_type = self.lif.read(be_ushort) # 1 = directory, 2 = file
self.current_file_data_offset += 20
if entry_type == 1:
self._read_dir(dirname)
elif entry_type == 2:
something, name, size = self._read_direntry()
assert something in (5 , 7) # 7 if .lif or directory, 5 if otherwise?
t1 = self._convert_time(self.lif.read(be_uint64))
t2 = self._convert_time(self.lif.read(be_uint64))
t3 = self._convert_time(self.lif.read(be_uint64))
self.records[os.path.join(dirname, name)] = self.current_file_data_offset, size - 20, t1, t2, t3
self.current_file_data_offset += size - 20
else:
raise ValueError(entry_type)
def extract_data(self, path: str) -> bytes:
file_offset, file_size = self.records[path][:2]
with open(self.lif_path, "rb") as file:
file.seek(file_offset)
return file.read(file_size)
if __name__ == "__main__":
app = LIFExtractor()
app.mainloop()