200 lines
5.5 KiB
Python
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)
|