tbxi/tbxi/prclc.py

200 lines
5.5 KiB
Python

#!/usr/bin/env python3
from shlex import split
from os import path
import struct
from binascii import crc32
from .lowlevel import PrclNodeStruct, PrclChildStruct, MAGIC
try:
from .fast_lzss import compress
except ImportError:
from .slow_lzss import compress
class CodeLine(dict):
def __getattr__(self, attrname):
return self[attrname]
def __setattr__(self, attrname, attrval):
self[attrname] = attrval
def get_indent_level(from_str):
if from_str.startswith('\t\t'):
return 2
elif from_str.startswith('\t'):
return 1
else:
return 0
def get_keys(from_list, **available):
ret = CodeLine()
for k, v in available.items():
ret[k] = v('')
for i in from_list:
k, _, v = i.partition('=')
fmt = available[k]
ret[k] = fmt(v)
return ret
def gethex(from_str):
if not from_str: return 0
if from_str.lower().startswith('0x'):
return int(from_str[2:], base=16)
else:
return int(from_str)
def getbool(from_str):
from_str = from_str.lower()
if from_str.strip() in ('', 'no', 'n', 'false', 'f', '0'):
return False
else:
return True
class PdslParseError(Exception):
pass
def load_and_cache_path(from_path):
# No compression, easy
if not from_path.lower().endswith('.lzss'):
with open(from_path, 'rb') as f:
return f.read()
# Compression, first try to read cached file
try:
f = open(from_path, 'rb')
except FileNotFoundError:
pass
else:
try:
orig_t = path.getmtime(from_path[:-5])
except FileNotFoundError:
orig_t = None
if orig_t is None or orig_t < path.getmtime(from_path):
data = f.read()
f.close()
return data
# Compression, no valid cached file available
with open(from_path[:-5], 'rb') as f:
data = compress(f.read())
with open(from_path, 'wb') as f:
f.write(data)
return data
def compile(src):
parent = path.dirname(path.abspath(src))
node_list = []
with open(src) as f:
try:
for line_num, line in enumerate(f, start=1):
level = get_indent_level(line)
pieces = split(line, comments=True, posix=True)
if not pieces: continue
if level == 0:
# parcel node
new = get_keys(pieces[1:], flags=gethex, a=str, b=str)
new.ostype = pieces[0]
new.children = []
node_list.append(new)
elif level == 1:
# parcel child
new = get_keys(pieces[1:], flags=gethex, name=str, src=str, deduplicate=getbool)
new.ostype = pieces[0]
new.data = bytearray()
new.compress = ''
if new.src:
if not path.isabs(new.src): # look rel to Parcelfile
new.src = path.join(path.dirname(src), new.src)
if new.src.lower().endswith('.lzss'):
new.compress = 'lzss'
new.data = load_and_cache_path(new.src)
node_list[-1].children.append(new)
elif level == 2:
# some C strings to add to the data
assert not node_list[-1].children[-1].src
for x in pieces:
node_list[-1].children[-1].data.extend(x.encode('mac_roman') + b'\0')
except:
raise PdslParseError('Line %d' % line_num)
# Great! Now that we have this cool data structure, turn it into parcels...
accum = bytearray()
accum.extend(MAGIC)
accum.extend(b'\x00\x00\x00\x14')
hdr_ptr = len(accum)
accum.extend(bytes(4))
accum.extend(bytes(4))
dedup_dict = {}
for node in node_list:
# Link previous member to this one
struct.pack_into('>I', accum, hdr_ptr, len(accum))
hdr_ptr = len(accum)
hdr_size = PrclNodeStruct.size + len(node.children)*PrclChildStruct.size
accum.extend(b'!' * hdr_size)
# okay, now start blatting data!
for child in node.children:
child.data = bytes(child.data) # no more mutability
dedup_tpl = (child.compress, child.data)
child.unpackedlen = len(child.data)
if child.deduplicate and dedup_tpl in dedup_dict:
child.ptr, child.packedlen = dedup_dict[dedup_tpl]
continue
child.ptr = len(accum)
accum.extend(child.data)
child.packedlen = len(accum) - child.ptr
while len(accum) % 4 != 0:
accum.append(0x99) # this is the only place we pad
if child.deduplicate:
dedup_dict[dedup_tpl] = (child.ptr, child.packedlen)
PrclNodeStruct.pack_into(accum, hdr_ptr,
link=0, ostype=node.ostype, hdr_size=hdr_size, flags=node.flags,
n_children=len(node.children), child_size=PrclChildStruct.size,
a=node.a, b=node.b,
)
pack_ptr = hdr_ptr + PrclNodeStruct.size
for child in node.children:
if child.flags & 4:
data = accum[child.ptr:child.ptr+child.packedlen]
child.cksum = crc32(data)
else:
child.cksum = 0
PrclChildStruct.pack_into(accum, pack_ptr, **child)
pack_ptr += PrclChildStruct.size
return bytes(accum)