mirror of
https://github.com/dgelessus/python-rsrcfork.git
synced 2025-07-01 17:23:51 +00:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
43a4073432 | |||
6247013592 | |||
9dfb33f436 | |||
a9a3168345 | |||
6b067bd762 | |||
e8df959894 | |||
2883354ef2 | |||
0b8699c2f1 | |||
2dbf0f7047 | |||
cbc55fcbc2 | |||
bc4bad678a | |||
ee796d0eb1 | |||
3d802c570f | |||
f6e424674d | |||
5dbec5d905 | |||
c207703c9f | |||
d6de63e17b | |||
e741fb063f |
96
README.rst
96
README.rst
@ -8,6 +8,23 @@ Requirements
|
||||
|
||||
Python 3.6 or later. No other libraries are required.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
``rsrcfork`` is available `on PyPI`__ and can be installed using ``pip``:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 -m pip install rsrcfork
|
||||
|
||||
Alternatively you can run the ``setup.py`` script manually:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 setup.py install
|
||||
|
||||
__ https://pypi.python.org/pypi/rsrcfork
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
@ -20,7 +37,7 @@ Examples
|
||||
--------
|
||||
|
||||
Simple example
|
||||
``````````````
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@ -32,7 +49,7 @@ Simple example
|
||||
<rsrcfork.ResourceFile._LazyResourceMap at 0x10470ed30 containing one resource: rsrcfork.Resource(resource_type=b'TEXT', resource_id=256, name=None, attributes=ResourceAttrs.0, data=b'Here is some text')>
|
||||
|
||||
Automatic selection of data/resource fork
|
||||
`````````````````````````````````````````
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@ -44,6 +61,34 @@ Automatic selection of data/resource fork
|
||||
>>> resourcerf._stream
|
||||
<_io.BufferedReader name='/Users/Shared/Test.textClipping/..namedfork/rsrc'>
|
||||
|
||||
Command-line interface
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: sh
|
||||
$ python3 -m rsrcfork /Users/Shared/Test.textClipping
|
||||
No header system data
|
||||
No header application data
|
||||
No file attributes
|
||||
4 resource types:
|
||||
'utxt': 1 resources:
|
||||
(256), unnamed, no attributes, 34 bytes
|
||||
|
||||
'utf8': 1 resources:
|
||||
(256), unnamed, no attributes, 17 bytes
|
||||
|
||||
'TEXT': 1 resources:
|
||||
(256), unnamed, no attributes, 17 bytes
|
||||
|
||||
'drag': 1 resources:
|
||||
(128), unnamed, no attributes, 64 bytes
|
||||
|
||||
$ python3 -m rsrcfork /Users/Shared/Test.textClipping "'TEXT' (256)"
|
||||
Resource 'TEXT' (256), unnamed, no attributes, 17 bytes:
|
||||
00000000 48 65 72 65 20 69 73 20 73 6f 6d 65 20 74 65 78 |Here is some tex|
|
||||
00000010 74 |t|
|
||||
00000011
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
@ -59,34 +104,45 @@ Further info on resource files
|
||||
Sources of information about the resource fork data format, and the structure of common resource types:
|
||||
|
||||
* Inside Macintosh, Volume I, Chapter 5 "The Resource Manager". This book can probably be obtained in physical form somewhere, but the relevant chapter/book is also available in a few places online:
|
||||
* `Apple's legacy documentation`__
|
||||
* pagetable.com, a site that happened to have a copy of the book: `info blog post`__, `direct download`__
|
||||
* `Wikipedia`__, of course
|
||||
* The `Resource Fork`__ article on "Just Solve the File Format Problem" (despite the title, this is a decent site and not clickbait)
|
||||
* The `KSFL`__ library (and `its wiki`__), written in Java, which supports reading and writing resource files
|
||||
* Apple's macOS SDK, which is distributed with Xcode. The latest version of Xcode is available for free from the Mac App Store. Current and previous versions can be downloaded from `the Apple Developer download page`__. Accessing these downloads requires an Apple ID with (at least) a free developer program membership.
|
||||
- `Apple's legacy documentation <https://developer.apple.com/legacy/library/documentation/mac/pdf/MoreMacintoshToolbox.pdf>`_
|
||||
- pagetable.com, a site that happened to have a copy of the book: `info blog post <http://www.pagetable.com/?p=50>`_, `direct download <http://www.weihenstephan.org/~michaste/pagetable/mac/Inside_Macintosh.pdf>`_
|
||||
* `Wikipedia <https://en.wikipedia.org/wiki/Resource_fork>`_, of course
|
||||
* The `Resource Fork <http://fileformats.archiveteam.org/wiki/Resource_Fork>`_ article on "Just Solve the File Format Problem" (despite the title, this is a decent site and not clickbait)
|
||||
* The `KSFL <https://github.com/kreativekorp/ksfl>`_ library (and `its wiki <https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format>`_), written in Java, which supports reading and writing resource files
|
||||
* Apple's macOS SDK, which is distributed with Xcode. The latest version of Xcode is available for free from the Mac App Store. Current and previous versions can be downloaded from `the Apple Developer download page <https://developer.apple.com/download/more/>`_. Accessing these downloads requires an Apple ID with (at least) a free developer program membership.
|
||||
* Apple's MPW (Macintosh Programmer's Workshop) and related developer tools. These were previously available from Apple's FTP server at ftp://ftp.apple.com/, which is no longer functional. Because of this, these downloads are only available on mirror sites, such as http://staticky.com/mirrors/ftp.apple.com/.
|
||||
|
||||
If these links are no longer functional, some are archived in the `Internet Archive Wayback Machine`__ or `archive.is`__ aka `archive.fo`__.
|
||||
If these links are no longer functional, some are archived in the `Internet Archive Wayback Machine <https://archive.org/web/>`_ or `archive.is <http://archive.is/>`_ aka `archive.fo <https://archive.fo/>`_.
|
||||
|
||||
__ https://developer.apple.com/legacy/library/documentation/mac/pdf/MoreMacintoshToolbox.pdf
|
||||
Changelog
|
||||
---------
|
||||
|
||||
__ http://www.pagetable.com/?p=50
|
||||
Version 1.1.3
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
__ http://www.weihenstephan.org/~michaste/pagetable/mac/Inside_Macintosh.pdf
|
||||
* Added a setuptools entry point for the command-line interface. This allows calling it using just ``rsrcfork`` instead of ``python3 -m rsrcfork``.
|
||||
* Changed the default value of ``ResourceFork.__init__``'s ``close`` keyword argument from ``True`` to ``False``. This matches the behavior of classes like ``zipfile.ZipFile`` and ``tarfile.TarFile``.
|
||||
* Fixed ``ResourceFork.open`` and ``ResourceFork.__init__`` not closing their streams in some cases.
|
||||
* Refactored the single ``rsrcfork.py`` file into a package. This is an internal change and should have no effect on how the ``rsrcfork`` module is used.
|
||||
|
||||
__ https://en.wikipedia.org/wiki/Resource_fork
|
||||
Version 1.1.2
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
__ http://fileformats.archiveteam.org/wiki/Resource_Fork
|
||||
* Added support for the resource file attributes "Resources Locked" and "Printer Driver MultiFinder Compatible" from ResEdit.
|
||||
* Added more dummy constants for resource attributes with unknown meaning, so that resource files containing such attributes can be loaded without errors.
|
||||
|
||||
__ https://github.com/kreativekorp/ksfl
|
||||
Version 1.1.1
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
__ https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
|
||||
* Fixed overflow issue with empty resource files or empty resource type entries
|
||||
* Changed ``_hexdump`` to behave more like ``hexdump -C``
|
||||
|
||||
__ https://developer.apple.com/download/more/
|
||||
Version 1.1.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
__ https://archive.org/web/
|
||||
* Added a command-line interface - run ``python3 -m rsrcfork --help`` for more info
|
||||
|
||||
__ http://archive.is/
|
||||
Version 1.0.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
__ https://archive.fo/
|
||||
* Initial version
|
||||
|
22
rsrcfork/__init__.py
Normal file
22
rsrcfork/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""A library for reading old Macintosh resource manager data, as found in resource forks or .rsrc files even on current Mac OS X/macOS systems.
|
||||
|
||||
This library only understands the resource file's general structure, i. e. the type codes, IDs, attributes, and data of the resources stored in the file. The data of individual resources is provided in raw bytes form and is not processed further - the format of this data is specific to each resource type.
|
||||
|
||||
Writing resource data is not supported at all.
|
||||
"""
|
||||
|
||||
__version__ = "1.1.3"
|
||||
|
||||
__all__ = [
|
||||
"Resource",
|
||||
"ResourceAttrs",
|
||||
"ResourceFile",
|
||||
"ResourceFileAttrs",
|
||||
"open",
|
||||
]
|
||||
|
||||
from . import api
|
||||
from .api import Resource, ResourceAttrs, ResourceFile, ResourceFileAttrs
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
open = ResourceFile.open
|
351
rsrcfork/__main__.py
Normal file
351
rsrcfork/__main__.py
Normal file
@ -0,0 +1,351 @@
|
||||
import argparse
|
||||
import collections
|
||||
import enum
|
||||
import sys
|
||||
import textwrap
|
||||
import typing
|
||||
|
||||
from . import __version__, api
|
||||
|
||||
# Translation table to replace ASCII non-printable characters with periods.
|
||||
_TRANSLATE_NONPRINTABLES = {k: "." for k in [*range(0x20), 0x7f]}
|
||||
|
||||
_REZ_ATTR_NAMES = {
|
||||
api.ResourceAttrs.resSysRef: None, # "Illegal or reserved attribute"
|
||||
api.ResourceAttrs.resSysHeap: "sysheap",
|
||||
api.ResourceAttrs.resPurgeable: "purgeable",
|
||||
api.ResourceAttrs.resLocked: "locked",
|
||||
api.ResourceAttrs.resProtected: "protected",
|
||||
api.ResourceAttrs.resPreload: "preload",
|
||||
api.ResourceAttrs.resChanged: None, # "Illegal or reserved attribute"
|
||||
api.ResourceAttrs.resCompressed: None, # "Extended Header resource attribute"
|
||||
}
|
||||
|
||||
F = typing.TypeVar("F", bound=enum.Flag, covariant=True)
|
||||
def _decompose_flags(value: F) -> typing.Sequence[F]:
|
||||
"""Decompose an enum.Flags instance into separate enum constants."""
|
||||
|
||||
return [bit for bit in type(value) if bit in value]
|
||||
|
||||
def _bytes_unescape(string: str) -> bytes:
|
||||
"""Convert a string containing ASCII characters and hex escapes to a bytestring.
|
||||
|
||||
(We implement our own unescaping mechanism here to not depend on any of Python's string/bytes escape syntax.)
|
||||
"""
|
||||
|
||||
out = []
|
||||
it = iter(string)
|
||||
n = 0
|
||||
for char in it:
|
||||
if char == "\\":
|
||||
try:
|
||||
esc = next(it)
|
||||
if esc in "\\\'\"":
|
||||
out.append(esc)
|
||||
elif esc == "x":
|
||||
x1, x2 = next(it), next(it)
|
||||
out.append(int(x1+x2, 16))
|
||||
else:
|
||||
raise ValueError(f"Unknown escape character: {esc}")
|
||||
except StopIteration:
|
||||
raise ValueError("End of string in escape sequence")
|
||||
else:
|
||||
out.append(ord(char))
|
||||
n += 1
|
||||
|
||||
return bytes(out)
|
||||
|
||||
def _bytes_escape(bs: bytes, *, quote: str=None) -> str:
|
||||
"""Convert a bytestring to a string, with non-ASCII bytes hex-escaped.
|
||||
|
||||
(We implement our own escaping mechanism here to not depend on Python's str or bytes repr.)
|
||||
"""
|
||||
|
||||
out = []
|
||||
for byte in bs:
|
||||
c = chr(byte)
|
||||
if c in {quote, "\\"}:
|
||||
out.append(f"\\{c}")
|
||||
elif 0x20 <= byte < 0x7f:
|
||||
out.append(c)
|
||||
else:
|
||||
out.append(f"\\x{byte:02x}")
|
||||
|
||||
return "".join(out)
|
||||
|
||||
def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typing.Sequence[api.Resource]:
|
||||
matching = collections.OrderedDict()
|
||||
|
||||
for filter in filters:
|
||||
if len(filter) == 4:
|
||||
try:
|
||||
resources = rf[filter.encode("ascii")]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
for res in resources.values():
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
elif filter[0] == filter[-1] == "'":
|
||||
try:
|
||||
resources = rf[_bytes_unescape(filter[1:-1])]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
for res in resources.values():
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
else:
|
||||
pos = filter.find("'", 1)
|
||||
if pos == -1:
|
||||
raise ValueError(f"Invalid filter {filter!r}: Resource type must be single-quoted")
|
||||
elif filter[pos + 1] != " ":
|
||||
raise ValueError(f"Invalid filter {filter!r}: Resource type and ID must be separated by a space")
|
||||
|
||||
restype, resid = filter[:pos + 1], filter[pos + 2:]
|
||||
|
||||
if not restype[0] == restype[-1] == "'":
|
||||
raise ValueError(
|
||||
f"Invalid filter {filter!r}: Resource type is not a single-quoted type identifier: {restype!r}")
|
||||
restype = _bytes_unescape(restype[1:-1])
|
||||
|
||||
if len(restype) != 4:
|
||||
raise ValueError(
|
||||
f"Invalid filter {filter!r}: Type identifier must be 4 bytes after replacing escapes, got {len(restype)} bytes: {restype!r}")
|
||||
|
||||
if resid[0] != "(" or resid[-1] != ")":
|
||||
raise ValueError(f"Invalid filter {filter!r}: Resource ID must be parenthesized")
|
||||
resid = resid[1:-1]
|
||||
|
||||
try:
|
||||
resources = rf[restype]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if resid[0] == resid[-1] == '"':
|
||||
name = _bytes_unescape(resid[1:-1])
|
||||
|
||||
for res in resources.values():
|
||||
if res.name == name:
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
break
|
||||
elif ":" in resid:
|
||||
if resid.count(":") > 1:
|
||||
raise ValueError(f"Invalid filter {filter!r}: Too many colons in ID range expression: {resid!r}")
|
||||
start, end = resid.split(":")
|
||||
start, end = int(start), int(end)
|
||||
|
||||
for res in resources.values():
|
||||
if start <= res.resource_id <= end:
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
else:
|
||||
resid = int(resid)
|
||||
try:
|
||||
res = resources[resid]
|
||||
except KeyError:
|
||||
continue
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
|
||||
return list(matching.values())
|
||||
|
||||
def _hexdump(data: bytes):
|
||||
for i in range(0, len(data), 16):
|
||||
line = data[i:i + 16]
|
||||
line_hex = " ".join(f"{byte:02x}" for byte in line)
|
||||
line_char = line.decode("MacRoman").translate(_TRANSLATE_NONPRINTABLES)
|
||||
print(f"{i:08x} {line_hex:<{16*2+15}} |{line_char}|")
|
||||
|
||||
if data:
|
||||
print(f"{len(data):08x}")
|
||||
|
||||
def _raw_hexdump(data: bytes):
|
||||
for i in range(0, len(data), 16):
|
||||
print(" ".join(f"{byte:02x}" for byte in data[i:i + 16]))
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
fromfile_prefix_chars="@",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=textwrap.dedent("""
|
||||
Read and display resources from a file's resource or data fork.
|
||||
|
||||
When specifying resource filters, each one may be of one of the
|
||||
following forms:
|
||||
|
||||
An unquoted type name (without escapes): TYPE
|
||||
A quoted type name: 'TYPE'
|
||||
A quoted type name and an ID: 'TYPE' (42)
|
||||
A quoted type name and an ID range: 'TYPE' (24:42)
|
||||
A quoted type name and a resource name: 'TYPE' ("foobar")
|
||||
|
||||
When multiple filters are specified, all resources matching any of them
|
||||
are displayed.
|
||||
"""),
|
||||
)
|
||||
|
||||
ap.add_argument("--help", action="help", help="Display this help message and exit")
|
||||
ap.add_argument("--version", action="version", version=__version__, help="Display version information and exit")
|
||||
ap.add_argument("-a", "--all", action="store_true", help="When no filters are given, show all resources in full, instead of an overview")
|
||||
ap.add_argument("-f", "--fork", choices=["auto", "data", "rsrc"], default="auto", help="The fork from which to read the resource data, or auto to guess (default: %(default)s)")
|
||||
ap.add_argument("--format", choices=["dump", "hex", "raw", "derez"], default="dump", help="How to output the resources - human-readable info with hex dump (dump), data only as hex (hex), data only as raw bytes (raw), or like DeRez with no resource definitions (derez)")
|
||||
ap.add_argument("--header-system", action="store_true", help="Output system-reserved header data and nothing else")
|
||||
ap.add_argument("--header-application", action="store_true", help="Output application-specific header data and nothing else")
|
||||
ap.add_argument("--read-mode", choices=["auto", "stream", "seek"], default="auto", help="Whether to read the data sequentially (stream) or on-demand (seek), or auto to use seeking when possible (default: %(default)s)")
|
||||
|
||||
ap.add_argument("file", help="The file to read, or - for stdin")
|
||||
ap.add_argument("filter", nargs="*", help="One or more filters to select which resources to display, or omit to show an overview of all resources")
|
||||
|
||||
ns = ap.parse_args()
|
||||
|
||||
ns.fork = {"auto": None, "data": False, "rsrc": True}[ns.fork]
|
||||
ns.read_mode = {"auto": None, "stream": False, "seek": True}[ns.read_mode]
|
||||
|
||||
if ns.file == "-":
|
||||
if ns.fork is not None:
|
||||
print("Cannot specify an explicit fork when reading from stdin", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
rf = api.ResourceFile(sys.stdin.buffer, allow_seek=ns.read_mode)
|
||||
else:
|
||||
rf = api.ResourceFile.open(ns.file, rsrcfork=ns.fork, allow_seek=ns.read_mode)
|
||||
|
||||
with rf:
|
||||
if ns.header_system or ns.header_application:
|
||||
if ns.header_system:
|
||||
data = rf.header_system_data
|
||||
else:
|
||||
data = rf.header_application_data
|
||||
|
||||
if ns.format == "dump":
|
||||
_hexdump(data)
|
||||
elif ns.format == "hex":
|
||||
_raw_hexdump(data)
|
||||
elif ns.format == "raw":
|
||||
sys.stdout.buffer.write(data)
|
||||
elif ns.format == "derez":
|
||||
print("Cannot output file header data in derez format", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise ValueError(f"Unhandled output format: {ns.format}")
|
||||
elif ns.filter or ns.all:
|
||||
if ns.filter:
|
||||
resources = _filter_resources(rf, ns.filter)
|
||||
else:
|
||||
resources = []
|
||||
for reses in rf.values():
|
||||
resources.extend(reses.values())
|
||||
|
||||
if ns.format in ("hex", "raw") and len(resources) != 1:
|
||||
print(f"Format {ns.format} only supports exactly one resource, but found {len(resources)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for res in resources:
|
||||
if ns.format == "dump":
|
||||
# Human-readable info and hex dump
|
||||
|
||||
if res.name is None:
|
||||
name = "unnamed"
|
||||
else:
|
||||
name = _bytes_escape(res.name, quote='"')
|
||||
name = f'name "{name}"'
|
||||
|
||||
attrs = _decompose_flags(res.attributes)
|
||||
if attrs:
|
||||
attrdesc = "attributes: " + " | ".join(attr.name for attr in attrs)
|
||||
else:
|
||||
attrdesc = "no attributes"
|
||||
|
||||
restype = _bytes_escape(res.resource_type, quote="'")
|
||||
print(f"Resource '{restype}' ({res.resource_id}), {name}, {attrdesc}, {len(res.data)} bytes:")
|
||||
_hexdump(res.data)
|
||||
print()
|
||||
elif ns.format == "hex":
|
||||
# Data only as hex
|
||||
|
||||
_raw_hexdump(res.data)
|
||||
elif ns.format == "raw":
|
||||
# Data only as raw bytes
|
||||
|
||||
sys.stdout.buffer.write(res.data)
|
||||
elif ns.format == "derez":
|
||||
# Like DeRez with no resource definitions
|
||||
|
||||
attrs = [_REZ_ATTR_NAMES[attr] for attr in _decompose_flags(res.attributes)]
|
||||
if None in attrs:
|
||||
attrs[:] = [f"${res.attributes.value:02X}"]
|
||||
|
||||
parts = [str(res.resource_id)]
|
||||
|
||||
if res.name is not None:
|
||||
name = _bytes_escape(res.name, quote='"')
|
||||
parts.append(f'"{name}"')
|
||||
|
||||
parts += attrs
|
||||
|
||||
restype = _bytes_escape(res.resource_type, quote="'")
|
||||
print(f"data '{restype}' ({', '.join(parts)}) {{")
|
||||
|
||||
for i in range(0, len(res.data), 16):
|
||||
# Two-byte grouping is really annoying to implement.
|
||||
groups = []
|
||||
for j in range(0, 16, 2):
|
||||
if i+j >= len(res.data):
|
||||
break
|
||||
elif i+j+1 >= len(res.data):
|
||||
groups.append(f"{res.data[i+j]:02X}")
|
||||
else:
|
||||
groups.append(f"{res.data[i+j]:02X}{res.data[i+j+1]:02X}")
|
||||
|
||||
s = f'$"{" ".join(groups)}"'
|
||||
comment = "/* " + res.data[i:i + 16].decode("MacRoman").translate(_TRANSLATE_NONPRINTABLES) + " */"
|
||||
print(f"\t{s:<54s}{comment}")
|
||||
|
||||
print("};")
|
||||
print()
|
||||
else:
|
||||
raise ValueError(f"Unhandled output format: {ns.format}")
|
||||
else:
|
||||
if rf.header_system_data != bytes(len(rf.header_system_data)):
|
||||
print("Header system data:")
|
||||
_hexdump(rf.header_system_data)
|
||||
else:
|
||||
print("No header system data")
|
||||
|
||||
if rf.header_application_data != bytes(len(rf.header_application_data)):
|
||||
print("Header application data:")
|
||||
_hexdump(rf.header_application_data)
|
||||
else:
|
||||
print("No header application data")
|
||||
|
||||
attrs = _decompose_flags(rf.file_attributes)
|
||||
if attrs:
|
||||
print("File attributes: " + " | ".join(attr.name for attr in attrs))
|
||||
else:
|
||||
print("No file attributes")
|
||||
|
||||
if len(rf) > 0:
|
||||
print(f"{len(rf)} resource types:")
|
||||
for typecode, resources in rf.items():
|
||||
restype = _bytes_escape(typecode, quote="'")
|
||||
print(f"'{restype}': {len(resources)} resources:")
|
||||
for resid, res in rf[typecode].items():
|
||||
if res.name is None:
|
||||
name = "unnamed"
|
||||
else:
|
||||
name = _bytes_escape(res.name, quote='"')
|
||||
name = f'name "{name}"'
|
||||
|
||||
attrs = _decompose_flags(res.attributes)
|
||||
if attrs:
|
||||
attrdesc = " | ".join(attr.name for attr in attrs)
|
||||
else:
|
||||
attrdesc = "no attributes"
|
||||
|
||||
print(f"({resid}), {name}, {attrdesc}, {len(res.data)} bytes")
|
||||
print()
|
||||
else:
|
||||
print("No resource types (empty resource file)")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
@ -1,26 +1,10 @@
|
||||
"""A library for reading old Macintosh resource manager data, as found in resource forks or .rsrc files even on current Mac OS X/macOS systems.
|
||||
|
||||
This library only understands the resource file's general structure, i. e. the type codes, IDs, attributes, and data of the resources stored in the file. The data of individual resources is provided in raw bytes form and is not processed further - the format of this data is specific to each resource type.
|
||||
|
||||
Writing resource data is not supported at all.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import collections.abc
|
||||
import enum
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
import typing
|
||||
|
||||
__all__ = [
|
||||
"Resource",
|
||||
"ResourceAttrs",
|
||||
"ResourceFile",
|
||||
"ResourceFileAttrs",
|
||||
"open",
|
||||
]
|
||||
|
||||
# The formats of all following structures is as described in the Inside Macintosh book (see module docstring).
|
||||
# Signedness and byte order of the integers is never stated explicitly in IM.
|
||||
# All integers are big-endian, as this is the native byte order of the 68k and PowerPC processors used in old Macs.
|
||||
@ -73,26 +57,34 @@ STRUCT_RESOURCE_NAME_HEADER = struct.Struct(">B")
|
||||
class ResourceFileAttrs(enum.Flag):
|
||||
"""Resource file attribute flags. The descriptions for these flags are taken from comments on the map*Bit and map* enum constants in <CarbonCore/Resources.h>."""
|
||||
|
||||
mapReadOnly = 128 # "is this file read-only?", "Resource file read-only"
|
||||
mapCompact = 64 # "Is a compact necessary?", "Compact resource file"
|
||||
mapChanged = 32 # "Is it necessary to write map?", "Write map out at update"
|
||||
_UNKNOWN_16 = 16
|
||||
_UNKNOWN_8 = 8
|
||||
_UNKNOWN_4 = 4
|
||||
_UNKNOWN_2 = 2
|
||||
_UNKNWON_1 = 1
|
||||
mapResourcesLocked = 1 << 15 # "Resources Locked" (undocumented, but available as a checkbox in ResEdit)
|
||||
_BIT_14 = 1 << 14
|
||||
_BIT_13 = 1 << 13
|
||||
_BIT_12 = 1 << 12
|
||||
_BIT_11 = 1 << 11
|
||||
_BIT_10 = 1 << 10
|
||||
_BIT_9 = 1 << 9
|
||||
mapPrinterDriverMultiFinderCompatible = 1 << 8 # "Printer Driver MultiFinder Compatible" (undocumented, but available as a checkbox in ResEdit)
|
||||
mapReadOnly = 1 << 7 # "is this file read-only?", "Resource file read-only"
|
||||
mapCompact = 1 << 6 # "Is a compact necessary?", "Compact resource file"
|
||||
mapChanged = 1 << 5 # "Is it necessary to write map?", "Write map out at update"
|
||||
_BIT_4 = 1 << 4
|
||||
_BIT_3 = 1 << 3
|
||||
_BIT_2 = 1 << 2
|
||||
_BIT_1 = 1 << 1
|
||||
_BIT_0 = 1 << 0
|
||||
|
||||
class ResourceAttrs(enum.Flag):
|
||||
"""Resource attribute flags. The descriptions for these flags are taken from comments on the res*Bit and res* enum constants in <CarbonCore/Resources.h>."""
|
||||
|
||||
resSysRef = 128 # "reference to system/local reference" (only documented as resSysRefBit = 7 in <CarbonCore/Resources.h>
|
||||
resSysHeap = 64 # "In system/in application heap", "System or application heap?"
|
||||
resPurgeable = 32 # "Purgeable/not purgeable", "Purgeable resource?"
|
||||
resLocked = 16 # "Locked/not locked", "Load it in locked?"
|
||||
resProtected = 8 # "Protected/not protected", "Protected?"
|
||||
resPreload = 4 # "Read in at OpenResource?", "Load in on OpenResFile?"
|
||||
resChanged = 2 # "Existing resource changed since last update", "Resource changed?"
|
||||
_UNKNWON_1 = 1
|
||||
resSysRef = 1 << 7 # "reference to system/local reference" (only documented as resSysRefBit = 7 in <CarbonCore/Resources.h>
|
||||
resSysHeap = 1 << 6 # "In system/in application heap", "System or application heap?"
|
||||
resPurgeable = 1 << 5 # "Purgeable/not purgeable", "Purgeable resource?"
|
||||
resLocked = 1 << 4 # "Locked/not locked", "Load it in locked?"
|
||||
resProtected = 1 << 3 # "Protected/not protected", "Protected?"
|
||||
resPreload = 1 << 2 # "Read in at OpenResource?", "Load in on OpenResFile?"
|
||||
resChanged = 1 << 1 # "Existing resource changed since last update", "Resource changed?"
|
||||
resCompressed = 1 << 0 # "indicates that the resource data is compressed" (only documented in https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format)
|
||||
|
||||
class Resource(object):
|
||||
"""A single resource from a resource file."""
|
||||
@ -179,7 +171,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x} containing {len(self)} resources with IDs: {list(self)}>"
|
||||
|
||||
@classmethod
|
||||
def open(cls, filename: typing.Union[str, bytes, os.PathLike], *, rsrcfork: typing.Optional[bool]=None) -> "ResourceFile":
|
||||
def open(cls, filename: typing.Union[str, bytes, os.PathLike], *, rsrcfork: typing.Optional[bool]=None, **kwargs) -> "ResourceFile":
|
||||
"""Open the file at the given path as a ResourceFile.
|
||||
|
||||
If rsrcfork is not None, it is treated as boolean and controls whether the data or resource fork of the file should be opened. (On systems other than macOS, opening resource forks will not work of course, since they don't exist.)
|
||||
@ -191,29 +183,34 @@ class ResourceFile(collections.abc.Mapping):
|
||||
# Determine whether the file has a usable resource fork.
|
||||
try:
|
||||
# Try to open the resource fork.
|
||||
f = io.open(os.path.join(filename, "..namedfork", "rsrc"), "rb")
|
||||
f = open(os.path.join(filename, "..namedfork", "rsrc"), "rb")
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
# If the resource fork doesn't exist, fall back to the data fork.
|
||||
f = io.open(filename, "rb")
|
||||
f = open(filename, "rb")
|
||||
else:
|
||||
# Resource fork exists, check if it actually contains anything.
|
||||
if f.read(1):
|
||||
# Resource fork contains data, seek back to start before using it.
|
||||
f.seek(0)
|
||||
else:
|
||||
# Resource fork contains no data, fall back to the data fork.
|
||||
f = io.open(filename, "rb")
|
||||
try:
|
||||
# Resource fork exists, check if it actually contains anything.
|
||||
if f.read(1):
|
||||
# Resource fork contains data, seek back to start before using it.
|
||||
f.seek(0)
|
||||
else:
|
||||
# Resource fork contains no data, fall back to the data fork.
|
||||
f.close()
|
||||
f = open(filename, "rb")
|
||||
except BaseException:
|
||||
f.close()
|
||||
raise
|
||||
elif rsrcfork:
|
||||
# Force use of the resource fork.
|
||||
f = io.open(os.path.join(filename, "..namedfork", "rsrc"), "rb")
|
||||
f = open(os.path.join(filename, "..namedfork", "rsrc"), "rb")
|
||||
else:
|
||||
# Force use of the data fork.
|
||||
f = io.open(filename, "rb")
|
||||
f = open(filename, "rb")
|
||||
|
||||
# Use the selected fork to build a ResourceFile.
|
||||
return cls(f)
|
||||
return cls(f, close=True, **kwargs)
|
||||
|
||||
def __init__(self, stream: typing.io.BinaryIO, *, allow_seek: typing.Optional[bool]=None, close: bool=True):
|
||||
def __init__(self, stream: typing.io.BinaryIO, *, allow_seek: typing.Optional[bool]=None, close: bool=False):
|
||||
"""Create a ResourceFile wrapping the given byte stream.
|
||||
|
||||
To read resource file data from a bytes object, wrap it in an io.BytesIO.
|
||||
@ -222,7 +219,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
If seeking is used, only the file header, map header, resource types, and resource references are read into memory. Resource data and names are loaded on-demand when the respective resource is accessed.
|
||||
If seeking is not used, the entire stream is processed sequentially and read into memory, including all resource data and names. This may be necessary when the stream does not support seeking at all. Memory is usually not a concern, most resource files are not even a megabyte in size.
|
||||
|
||||
close controls whether the stream should be closed when the ResourceFile's close method is called.
|
||||
close controls whether the stream should be closed when the ResourceFile's close method is called. By default this is False.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
@ -230,18 +227,22 @@ class ResourceFile(collections.abc.Mapping):
|
||||
self._close_stream: bool = close
|
||||
self._stream: typing.io.BinaryIO = stream
|
||||
|
||||
self._allow_seek: bool
|
||||
if allow_seek is None:
|
||||
self._allow_seek = self._stream.seekable()
|
||||
else:
|
||||
self._allow_seek = allow_seek
|
||||
|
||||
if self._allow_seek:
|
||||
self._pos = None
|
||||
self._init_seeking()
|
||||
else:
|
||||
self._pos: int = 0
|
||||
self._init_streaming()
|
||||
try:
|
||||
self._allow_seek: bool
|
||||
if allow_seek is None:
|
||||
self._allow_seek = self._stream.seekable()
|
||||
else:
|
||||
self._allow_seek = allow_seek
|
||||
|
||||
if self._allow_seek:
|
||||
self._pos = None
|
||||
self._init_seeking()
|
||||
else:
|
||||
self._pos: int = 0
|
||||
self._init_streaming()
|
||||
except BaseException:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def _tell(self) -> int:
|
||||
"""Get the current position in the stream. This uses the stream's tell method if seeking is enabled, and an internal counter otherwise."""
|
||||
@ -284,13 +285,6 @@ class ResourceFile(collections.abc.Mapping):
|
||||
self.header_application_data,
|
||||
) = self._stream_unpack(STRUCT_RESOURCE_HEADER)
|
||||
|
||||
if __debug__:
|
||||
if self.header_system_data != bytes(len(self.header_system_data)):
|
||||
print("Header system data is not all null bytes. This may be of interest.")
|
||||
|
||||
if self.header_application_data != bytes(len(self.header_application_data)):
|
||||
print("Header application data is not all null bytes. This may be of interest.")
|
||||
|
||||
assert self._tell() == self.data_offset
|
||||
|
||||
def _read_all_resource_data(self):
|
||||
@ -329,14 +323,16 @@ class ResourceFile(collections.abc.Mapping):
|
||||
self._reference_counts: typing.MutableMapping[bytes, int] = collections.OrderedDict()
|
||||
|
||||
(type_list_length_m1,) = self._stream_unpack(STRUCT_RESOURCE_TYPE_LIST_HEADER)
|
||||
type_list_length = (type_list_length_m1 + 1) % 0x10000
|
||||
|
||||
for _ in range(type_list_length_m1 + 1):
|
||||
for _ in range(type_list_length):
|
||||
(
|
||||
resource_type,
|
||||
count_m1,
|
||||
reflist_offset,
|
||||
) = self._stream_unpack(STRUCT_RESOURCE_TYPE)
|
||||
self._reference_counts[resource_type] = count_m1 + 1
|
||||
count = (count_m1 + 1) % 0x10000
|
||||
self._reference_counts[resource_type] = count
|
||||
|
||||
def _read_all_references(self):
|
||||
"""Read all resource references, starting at the current stream position."""
|
||||
@ -397,7 +393,10 @@ class ResourceFile(collections.abc.Mapping):
|
||||
self._read_all_resource_names()
|
||||
|
||||
def close(self):
|
||||
"""Close the underlying stream, unless this behavior was suppressed by passing close=False to the constructor. If seeking is enabled for this ResourceFile, resources can no longer be read after closing the stream. On the other hand, if seeking is disabled, closing the stream does not affect the ResourceFile."""
|
||||
"""Close this ResourceFile.
|
||||
|
||||
If close=True was passed when this ResourceFile was created, the underlying stream's close method is called as well.
|
||||
"""
|
||||
|
||||
if self._close_stream:
|
||||
self._stream.close()
|
||||
@ -430,5 +429,3 @@ class ResourceFile(collections.abc.Mapping):
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x}, attributes {self.file_attributes}, containing {len(self)} resource types: {list(self)}>"
|
||||
|
||||
open = ResourceFile.open
|
40
setup.cfg
Normal file
40
setup.cfg
Normal file
@ -0,0 +1,40 @@
|
||||
[metadata]
|
||||
name = rsrcfork
|
||||
version = attr: rsrcfork.__version__
|
||||
url = https://github.com/dgelessus/python-rsrcfork
|
||||
author = dgelessus
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Intended Audience :: Developers
|
||||
Topic :: Software Development :: Libraries :: Python Modules
|
||||
Topic :: Utilities
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
description = A pure Python library for reading old Macintosh resource manager data
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
keywords =
|
||||
rsrc
|
||||
fork
|
||||
resource
|
||||
manager
|
||||
macintosh
|
||||
mac
|
||||
macos
|
||||
|
||||
[options]
|
||||
setup_requires =
|
||||
setuptools>=39.2.0
|
||||
python_requires = >=3.6
|
||||
packages =
|
||||
rsrcfork
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
rsrcfork = rsrcfork.__main__:main
|
26
setup.py
26
setup.py
@ -2,28 +2,4 @@
|
||||
|
||||
import setuptools
|
||||
|
||||
with open("README.rst", "r", encoding="utf-8") as f:
|
||||
long_description = f.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="rsrcfork",
|
||||
version="1.0.0",
|
||||
description="A pure Python library for reading old Macintosh resource manager data",
|
||||
long_description=long_description,
|
||||
url="https://github.com/dgelessus/python-rsrcfork",
|
||||
author="dgelessus",
|
||||
license="MIT",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
],
|
||||
keywords="rsrc fork resource manager macintosh mac macos",
|
||||
py_modules=["rsrcfork"],
|
||||
)
|
||||
setuptools.setup()
|
||||
|
Reference in New Issue
Block a user