diff --git a/base.py b/base.py index 5253806..43aabc3 100644 --- a/base.py +++ b/base.py @@ -2,6 +2,9 @@ import struct from bisect import bisect_left from rect import * from utils import * +import sys + +from resource_writer import ResourceWriter from constants import * @@ -147,7 +150,7 @@ class rObject: # return self.id @staticmethod - def dump_exports(type="c"): + def dump_exports(type="c", io=None): type = type.lower() if type not in ("c", "equ", "gequ"): return @@ -160,7 +163,7 @@ class rObject: for rType,rList in rObject._resources.items(): for r in rList: if r._export: - print(fmt.format(r._export, r.get_id())) + print(fmt.format(r._export, r.get_id()), file=io) def _format_attr(self): attr = self._attr @@ -181,14 +184,14 @@ class rObject: return ", ".join(opts) @staticmethod - def dump(): + def dump(io=None): for rType,rList in rObject._resources.items(): for r in rList: - print("${:04x} {} - ${:08x}".format(rType, r.rName, r.get_id())) + print("${:04x} {} - ${:08x}".format(rType, r.rName, r.get_id()), file=io) @staticmethod - def dump_hex(): + def dump_hex(io=None): for rType,rList in rObject._resources.items(): for r in rList: bb = bytes(r) @@ -197,22 +200,44 @@ class rObject: print("{}(${:08x}{}) {{".format( r.rName, r.get_id(), r._format_attr() - )) + ), file=io) for x in data: - print("\t$\"" + x.hex() + "\"") - print("}\n") + print("\t$\"" + x.hex() + "\"", file=io) + print("}\n", file=io) @staticmethod - def dump_rez(): + def dump_rez(io=None): for rType,rList in rObject._resources.items(): for r in rList: content = r._rez_string() print("{}(${:08x}{}) {{".format( r.rName, r.get_id(), r._format_attr() - )) - print(content) - print("}\n") + ), file=io) + print(content, file=io) + print("}\n", file=io) + + + @staticmethod + def save_resource(io): + rw = ResourceWriter() + + for rType,rList in rObject._resources.items(): + if rType == 0x8014: continue # rResName handled via rw + + for r in rList: + + rw.add_resource( + rType, + r.get_id(), + bytes(r), + attr = r._attr, + name = r._name + ) + + rw.write(io) + + # container for a 0-terminated list of resource ids. # NOT EXPORTED BY DEFAULT diff --git a/open_rfork.py b/open_rfork.py new file mode 100644 index 0000000..b989efb --- /dev/null +++ b/open_rfork.py @@ -0,0 +1,117 @@ +import io +import sys +import os +import os.path + +def _validate_mode(mode): + # strips "t" format, adds "b" format, and checks if the + # base file needs to be created. + # full validation will be handled by os.open + r = False + w = False + plus = False + rmode = "b" + for x in mode: + if x == "r": r = True + elif x in ["a", "x", "w"]: w = True + elif x in ["+"]: plus = True + else: continue + rmode += x + + mode = ["r", "a"][w] # create if it does not exist. + return (mode, rmode) + +# There's a better way to handle this but I don't recall it offhand. +# - open() opener=argument! opener(file,flags) -> fd +# - ... but that can't force it to be opened in binary mode. +def _open2(file, rfile, mode): + + a = None + b = None + + (mode, rmode) = _validate_mode(mode) + try: + a = io.open(file, mode) + b = io.open(rfile, rmode) + except Exception as e: + raise + finally: + if a: a.close() + + a.close() + return b + +_finder_magic = { + (0x00, 0x0000): b"BINApdos", + (0x04, 0x0000): b"TEXTpdos", + (0xff, 0x0000): b"PSYSpdos", + (0xb3, 0x0000): b"PS16pdos", + (0xd7, 0x0000): b"MIDIpdos", + (0xd8, 0x0000): b"AIFFpdos", + (0xd7, 0x0001): b"AIFCpdos", + (0xe0, 0x0005): b"dImgdCpy", +} +_z24 = bytearray(24) +def _make_finder_data(filetype, auxtype): + + k = (filetype, auxtype) + x = _finder_magic.get((filetype, auxtype)) + if not x: + x = struct.pack(">cBH4s", b'p', filetype & 0xff, auxtype & 0xffff, b"pdos") + + return x + +if sys.platform == "win32": + + def open_rfork(file, mode="r"): + # protect against c -> c:stream + file = os.path.realpath(file) + rfile = file + ":AFP_Resource" + return _open2(file, rfile, mode) + + def set_file_type(path, filetype, auxtype): + path = os.path.realpath(path) + path += ":AFP_AfpInfo" + f = open(path, "wb") + + data = struct.pack("8s24x", _make_finder_data(filetype, auxtype)) + ok = _setxattr(path.encode("utf-8"), b"com.apple.FinderInfo", data, 32, 0, 0) + e = ctypes.get_errno() + if ok < 0: return False + return True + +if sys.platform == "linux": + def open_rfork(file, mode="r"): + raise NotImplementedError("open_rfork") + + def set_file_type(path, filetype, auxtype): + + data = struct.pack(">8s24x", _make_finder_data(filetype, auxtype)) + os.setxattr(path, "com.apple.FinderInfo", data, 0, 0) + return True diff --git a/resource_writer.py b/resource_writer.py new file mode 100644 index 0000000..976e7f2 --- /dev/null +++ b/resource_writer.py @@ -0,0 +1,284 @@ +from enum import Enum, Flag + + +class rTypes(Enum): + rIcon = 0x8001 # Icon type + rPicture = 0x8002 # Picture type + rControlList = 0x8003 # Control list type + rControlTemplate = 0x8004 # Control template type + rC1InputString = 0x8005 # GS/OS class 1 input string + rPString = 0x8006 # Pascal string type + rStringList = 0x8007 # String list type + rMenuBar = 0x8008 # MenuBar type + rMenu = 0x8009 # Menu template + rMenuItem = 0x800A # Menu item definition + rTextForLETextBox2 = 0x800B # Data for LineEdit LETextBox2 call + rCtlDefProc = 0x800C # Control definition procedure type + rCtlColorTbl = 0x800D # Color table for control + rWindParam1 = 0x800E # Parameters for NewWindow2 call + rWindParam2 = 0x800F # Parameters for NewWindow2 call + rWindColor = 0x8010 # Window Manager color table + rTextBlock = 0x8011 # Text block + rStyleBlock = 0x8012 # TextEdit style information + rToolStartup = 0x8013 # Tool set startup record + rResName = 0x8014 # Resource name + rAlertString = 0x8015 # AlertWindow input data + rText = 0x8016 # Unformatted text + rCodeResource = 0x8017 + rCDEVCode = 0x8018 + rCDEVFlags = 0x8019 + rTwoRects = 0x801A # Two rectangles + rFileType = 0x801B # Filetype descriptors--see File Type Note $42 + rListRef = 0x801C # List member + rCString = 0x801D # C string + rXCMD = 0x801E + rXFCN = 0x801F + rErrorString = 0x8020 # ErrorWindow input data + rKTransTable = 0x8021 # Keystroke translation table + rWString = 0x8022 # not useful--duplicates $8005 + rC1OutputString = 0x8023 # GS/OS class 1 output string + rSoundSample = 0x8024 + rTERuler = 0x8025 # TextEdit ruler information + rFSequence = 0x8026 + rCursor = 0x8027 # Cursor resource type + rItemStruct = 0x8028 # for 6.0 Menu Manager + rVersion = 0x8029 + rComment = 0x802A + rBundle = 0x802B + rFinderPath = 0x802C + rPaletteWindow = 0x802D # used by HyperCard IIgs 1.1 + rTaggedStrings = 0x802E + rPatternList = 0x802F + rRectList = 0xC001 + rPrintRecord = 0xC002 + rFont = 0xC003 + + +class rAttr(Flag): + + attrPage = 0x0004 + attrNoSpec = 0x0008 + attrNoCross = 0x0010 + resChanged = 0x0020 + resPreLoad = 0x0040 + resProtected = 0x0080 + attrPurge1 = 0x0100 + attrPurge2 = 0x0200 + attrPurge3 = 0x0300 + resAbsLoad = 0x0400 + resConverter = 0x0800 + attrFixed = 0x4000 + attrLocked = 0x8000 + + attrPurge = 0x0300 + +class ResourceWriter(object): + + def __init__(self): + self._resources = [] + self._resource_ids = set() + self._resource_names = {} + + + def unique_resource_id(self, rtype, range): + if type(rtype) == rTypes: rtype = rtype.value + if rtype < 0 or rtype > 0xffff: + raise ValueError("Invalid resource type ${:04x}".format(rtype)) + + if range > 0xffff: + raise ValueError("Invalid range ${:04x}".format(range)) + if range > 0x7ff and range < 0xffff: + raise ValueError("Invalid range ${:04x}".format(range)) + + min = range << 16 + max = min + 0xffff + if range == 0: + min = 1 + elif range == 0xffff: + min = 1 + max = 0x07feffff + + used = [x[1] for x in self._resource_ids if x[0] == rtype and x[1] >= min and x[1] <= max] + if len(used) == 0: return min + + used.sort() + # if used[0] > min: return min + + id = min + for x in used: + if x > id: return id + id = x + 1 + if id >= max: + raise OverflowError("No Resource ID available in range") + raise id + + def add_resource(self, rtype, rid, data, *, attr=0, reserved=0, name=None): + if type(rtype) == rTypes: rtype = rtype.value + if rtype < 0 or rtype > 0xffff: + raise ValueError("Invalid resource type ${:04x}".format(rtype)) + + if rid < 0 or rid > 0x07ffffff: + raise ValueError("Invalid resource id ${:08x}".format(rid)) + + if (rid, rtype) in self._resource_ids: + raise ValueError("Duplicate resource ${:04x}:${:08x}".format(rtype, rid)) + + # don't allow standard res names since they're handled elsewhere. + if rtype == rTypes.rResName.value and rid > 0x00010000 and rid < 0x00020000: + raise ValueError("Invalid resource ${:04x}:${:08x}".format(rtype, rid)) + + + if name: + if type(name) == str: name = name.encode('macroman') + if len(name) > 255: name = name[0:255] + self._resource_names[(rtype, name)]=rid + + self._resources.append((rtype, rid, attr, data, reserved)) + self._resource_ids.add((rtype, rid)) + + + + def set_resource_name(rtype, rid, name): + if type(rtype) == rTypes: rtype = rtype.value + if rtype < 0 or rtype > 0xffff: + raise ValueError("Invalid resource type ${:04x}".format(rtype)) + + if rid < 0 or rid > 0x07ffffff: + raise ValueError("Invalid resource id ${:08x}".format(rid)) + + key = (rtype, name) + if not name: + self._resource_names.pop(key, None) + else: + if type(name) == str: name = str.encode('macroman') + if len(name) > 255: name = name[0:255] + self._resource_names[key] = rid + + + + @staticmethod + def _merge_free_list(fl): + rv = [] + eof = None + for (offset, size) in fl: + if offset == eof: + tt = rv.pop() + tt[1] += size + rv.append(tt) + else: + rv.append((offset, size)) + eof = offset + size + return rv + + def _build_res_names(self): + # format: + # type $8014, id $0001xxxx (where xxxx = resource type) + # version:2 [1] + # name count:4 + # [id:4, name:pstring]+ + # + + rv = [] + tmp = [] + + if not len(self._resource_names): return rv + for (rtype, rname), rid in self._resource_names.items(): + tmp.append( (rtype, rid, rname) ) + + + keyfunc_type = lambda x: x[0] + keyfunc_name = lambda x: x[2] + tmp.sort(key = keyfunc_type) + for rtype, iter in groupby(tmp, keyfunc_type): + tmp = list(iter) + tmp.sort(key=keyfunc_name) + data = bytearray() + data += struct.pack("