macresources/bin/rfx
2020-01-10 20:20:17 +08:00

187 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
import macresources
import sys
import tempfile
import os
from os import path
import re
import subprocess
import textwrap
if len(sys.argv) < 2 or sys.argv[1].startswith('-'):
sys.exit(textwrap.dedent('''
usage: rfx command [arg | arg// | arg//type | arg//type/id ...]
Shell command wrapper for accessing resources inside a Rez textfile
Resources specified as filename.rdump//type/id are converted to tempfiles before
the command is run, and back to resources after the command returns. Truncated
// arguments are wildcards.
examples:
rfx mv Doc.rdump//STR/0 Doc.rdump//STR/1
rfx cp App.rdump//PICT allpictures/
rfx rm System.rdump//vers/2
''').strip())
bytearray_cache = {}
original_cache = {}
def get_cached_file(the_path):
# Different paths to the same file are unlikely, but just in case:
the_path = path.abspath(the_path)
try:
return bytearray_cache[the_path]
except KeyError:
try:
with open(the_path, 'rb') as f:
d = f.read()
except FileNotFoundError:
d = bytes()
original_cache[the_path] = d
bytearray_cache[the_path] = bytearray(d)
return bytearray_cache[the_path]
def flush_cache():
for the_path, the_data in bytearray_cache.items():
if original_cache[the_path] != the_data:
with open(the_path, 'wb') as f:
f.write(the_data)
def rez_resource_range(the_data, the_type, the_id):
if not the_data: return (0, 0)
# Hack... do a text search instead of Rezzing the whole file!
search = macresources.make_rez_code([macresources.Resource(the_type, the_id)], ascii_clean=True)
search = search.rpartition(b')')[0]
start = 0
while True:
start = the_data.find(search, start)
if start == -1: return (0, 0)
if (the_data[start-1:start] in b'\n') and (the_data[start+len(search):start+len(search)+1] in (b',', b')')):
break
start += len(search)
stop = the_data.index(b'\n};\n\n', start) + 5
return (start, stop)
def rez_shrink_range(the_data, start, stop):
start = the_data.index(b'\n', start) + 1
while the_data[stop:stop+1] != b'}': stop -= 1
return (start, stop)
def rez_get_resource(the_path, the_type, the_id):
the_file = get_cached_file(the_path)
start, stop = rez_resource_range(the_file, the_type, the_id)
if start == stop == 0: return None
return next(macresources.parse_rez_code(the_file[start:stop])).data
def rez_set_resource(the_path, the_type, the_id, the_data):
the_file = get_cached_file(the_path)
newdata = macresources.make_rez_code([macresources.Resource(the_type, the_id, data=the_data)], ascii_clean=True)
start, stop = rez_resource_range(the_file, the_type, the_id)
if start == stop == 0:
the_file.extend(newdata)
else:
start, stop = rez_shrink_range(the_file, start, stop)
istart, istop = rez_shrink_range(newdata, 0, len(newdata))
the_file[start:stop] = newdata[istart:istop]
def rez_delete_resource(the_path, the_type, the_id):
the_file = get_cached_file(the_path)
start, stop = rez_resource_range(the_file, the_type, the_id)
del the_file[start:stop]
def escape_ostype(ostype):
escaped = ''
for char in ostype:
if ord('A') <= char <= ord('Z') or ord('a') <= char <= ord('z'):
escaped += chr(char)
else:
escaped += '_%02X' % char
return escaped
with tempfile.TemporaryDirectory() as backup_tmp_dir:
new_argv = [sys.argv[1]]
to_retrieve = []
for i, arg in enumerate(sys.argv[2:], 1):
m = re.match(r'(.*[^/])//(?:([^/]{1,4})(?:/(-?\d+)?)?)?$'.replace('/', re.escape(path.sep)), arg)
if not m:
# Do not expand this argument
new_argv.append(arg)
else:
# Expand arg into 1+ fake-resource tempfiles. This is a (filename, type, id) list.
res_specs = []
res_file = m.group(1)
res_type = m.group(2).encode('mac_roman').ljust(4)[:4] if m.group(2) else None
res_id = int(m.group(3)) if m.group(3) else None
if res_type is None:
# File// = every resource
for found_res in macresources.parse_rez_code(get_cached_file(res_file)):
res_specs.append((res_file, found_res.type, found_res.id))
elif res_id is None:
# File//Type/ = resources of type (can omit trailing slash)
for found_res in macresources.parse_rez_code(get_cached_file(res_file)):
if found_res.type == res_type:
res_specs.append((res_file, res_type, found_res.id))
else:
# File//Type/ID = 1 resource
res_specs.append((res_file, res_type, res_id))
if not res_specs:
# Failed to expand so leave unchanged
new_argv.append(arg)
else:
# Expand!
tmp_subdir = path.join(backup_tmp_dir, str(i))
os.mkdir(tmp_subdir)
for res_spec in res_specs:
res_file, res_type, res_id = res_spec
tmp_file = path.join(tmp_subdir, '%s.%d' % (escape_ostype(res_type), res_id))
to_retrieve.append((tmp_file, res_spec))
res_data = rez_get_resource(*res_spec)
if res_data is not None:
with open(tmp_file, 'wb') as f:
f.write(res_data)
new_argv.append(tmp_file)
result = subprocess.run(new_argv)
for tmp_file, res_spec in to_retrieve:
try:
with open(tmp_file, 'rb') as f:
rez_set_resource(*res_spec, f.read())
except FileNotFoundError:
rez_delete_resource(*res_spec)
flush_cache()
sys.exit(result.returncode)