mirror of
https://github.com/dgelessus/python-rsrcfork.git
synced 2025-07-03 00:23:56 +00:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
158ca4884b | |||
8568f355c4 | |||
97d2dbe1b3 | |||
a4b6328782 | |||
393160b5da | |||
476eaecd17 | |||
546edbc31a | |||
cf6ce3c2a6 | |||
af2ac70676 | |||
5af455992b | |||
2193c81518 | |||
7dc0d980a3 | |||
2ce1d6b63a | |||
ec5eb3bcc1 | |||
25bec2f93a | |||
6fbb919285 | |||
3be4d9c969 | |||
f537fb3d37 |
83
README.rst
83
README.rst
@ -108,72 +108,31 @@ Writing resource data is not supported at all.
|
||||
Further info on resource files
|
||||
------------------------------
|
||||
|
||||
Sources of information about the resource fork data format, and the structure of common resource types:
|
||||
|
||||
* The Inside Macintosh book series, specifically the chapter "Resource Manager". These books are Apple's official reference material for the classic Macintosh platform. Over time, they have gone through many revisions and updates, and their structure has been changed multiple times. This is a (likely incomplete) list of the major revisions of Inside Macintosh and where they can be obtained online.
|
||||
|
||||
* The earliest revisions consisted of two volumes, each a three-ring binder containing photocopied pages. The chapters were referred to as individual "manuals" and were essentially standalone - each one had its own title page, TOC, glossary, and page numbers. Various parts were still missing or not yet finalized, and updated pages were distributed regularly as part of the `Macintosh Software Supplement <https://macgui.com/news/article.php?t=447>`_.
|
||||
|
||||
* bitsavers.org has scanned and OCRed PDFs of a late (November 1984) revision: `Volume I <http://bitsavers.org/pdf/apple/mac/Inside_Macintosh_Vol_1_1984.pdf>`_, `Volume II <http://bitsavers.org/pdf/apple/mac/Inside_Macintosh_Vol_2_1984.pdf>`_.
|
||||
|
||||
* The Promotional Edition, released in early 1985, consisted of a single book (it was nicknamed the "phonebook" edition because of its paper quality). Although it was physically a single book, the contents were still structured into standalone "manuals" like in the ring binder version, and some parts were still missing or not finalized.
|
||||
|
||||
* bitsavers.org has `a scanned and OCRed PDF <http://bitsavers.org/pdf/apple/mac/Inside_Macintosh_Promotional_Edition_1985.pdf>`_.
|
||||
|
||||
* The published 1985 revision consisted of three volumes, available as three paperback books or a single hardcover book. They contained the finalized contents of the previous revisions, which documented the Macintosh 128k, Macintosh 512k, and Macintosh XL. Unlike the previous revisions, each volume had continuous page numbers and a full TOC and index, and volume III contained a complete glossary.
|
||||
|
||||
* pagetable.com has a `blog post <http://www.pagetable.com/?p=50>`_ with `a scanned and OCRed PDF of the three paperback volumes <http://www.weihenstephan.org/~michaste/pagetable/mac/Inside_Macintosh.pdf>`_.
|
||||
|
||||
* Additional volumes were published later to document newer Macintosh models. These served as incremental additions and did not fully supersede or replace any of the previous volumes.
|
||||
|
||||
* Volume IV was published in 1986 and documented the Macintosh Plus and Macintosh 512k Enhanced.
|
||||
* Volume V was published in 1986 and documented the Macintosh II and Macintosh SE.
|
||||
* Volume VI was published in 1991 and documented System 7.0.
|
||||
* VintageApple.org has `scanned and OCRed PDFs of Volumes I through VI <https://vintageapple.org/inside_o/>`_.
|
||||
|
||||
* After 1991, Inside Macintosh was restructured into over 20 volumes organized by topic, rather than chronologically by Macintosh model. These were published as books starting in 1992, and later also on CDs and online.
|
||||
|
||||
* VintageApple.org has `rendered (not scanned) PDFs of 26 volumes and 7 X-Ref volumes <https://vintageapple.org/inside_r/>`_.
|
||||
|
||||
* The Communications Toolbox and QuickDraw GX Programmers' Overview volumes appear to be missing.
|
||||
|
||||
* Many volumes are still available in Apple's legacy developer documentation archive, in HTML and rendered (not scanned) PDF formats:
|
||||
|
||||
* Two volumes appear on the website under Inside Macintosh, even though other sources don't consider them part of the Inside Macintosh series:
|
||||
|
||||
* `Advanced Color Imaging on the Mac OS (HTML) <https://developer.apple.com/library/archive/documentation/mac/ACI/ACI-2.html>`_ (November 1996)
|
||||
* `Advanced Color Imaging Reference (HTML) <https://developer.apple.com/library/archive/documentation/mac/ACIReference/ACIReference-2.html>`_ (November 1996)
|
||||
|
||||
* `Devices (HTML) <https://developer.apple.com/library/archive/documentation/mac/Devices/Devices-2.html>`_ (July 1996), `Devices (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Devices/pdf.html>`_ (1994)
|
||||
* `Files (HTML) <https://developer.apple.com/library/archive/documentation/mac/Files/Files-2.html>`_ (July 1996), `Files (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Files/pdf.html>`_ (1992)
|
||||
* `Imaging with QuickDraw (HTML) <https://developer.apple.com/library/archive/documentation/mac/QuickDraw/QuickDraw-2.html>`_ (July 1996), `Imaging with QuickDraw (single PDF) <https://developer.apple.com/library/archive/documentation/mac/pdf/ImagingWithQuickDraw.pdf>`_ (1994)
|
||||
* `Interapplication Communication (HTML) <https://developer.apple.com/library/archive/documentation/mac/IAC/IAC-2.html>`_ (July 1996), `Interapplication Communication (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Interapplication_Communication/pdf.html>`_ (1993)
|
||||
* `Macintosh Toolbox Essentials (HTML) <https://developer.apple.com/library/archive/documentation/mac/Toolbox/Toolbox-2.html>`_ (July 1996), `Macintosh Toolbox Essentials (single PDF) <https://developer.apple.com/library/archive/documentation/mac/pdf/MacintoshToolboxEssentials.pdf>`_ (1992)
|
||||
* `Memory (HTML) <https://developer.apple.com/library/archive/documentation/mac/Memory/Memory-2.html>`_ (July 1996), `Memory (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Memory/pdf.html>`_ (1992)
|
||||
* `More Macintosh Toolbox (HTML) <https://developer.apple.com/library/archive/documentation/mac/MoreToolbox/MoreToolbox-2.html>`_ (July 1996), `More Macintosh Toolbox (single PDF) <https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf>`_ (1993)
|
||||
* `Networking (HTML) <https://developer.apple.com/library/archive/documentation/mac/Networking/Networking-2.html>`_ (July 1996), `Networking (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Networking/pdf.html>`_ (1994)
|
||||
* `Operating System Utilities (HTML) <https://developer.apple.com/library/archive/documentation/mac/OSUtilities/OSUtilities-2.html>`_ (July 1996), `Operating System Utilities (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Operating_System_Utilities/pdf.html>`_ (1994)
|
||||
* `PowerPC Numerics (HTML) <https://developer.apple.com/library/archive/documentation/mac/PPCNumerics/PPCNumerics-2.html>`_ (July 1996), `PowerPC Numerics (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/PPC_Numerics.sit.hqx>`_ (1994)
|
||||
* `PowerPC System Software (HTML) <https://developer.apple.com/library/archive/documentation/mac/PPCSoftware/PPCSoftware-2.html>`_ (July 1996), `PowerPC System Software (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/PPC_System_Software.sit.hqx>`_ (1994)
|
||||
* `Processes (HTML) <https://developer.apple.com/library/archive/documentation/mac/Processes/Processes-2.html>`_ (June 1996), `Processes (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Processes/pdf.html>`_ (1992)
|
||||
* `Sound (HTML) <https://developer.apple.com/library/archive/documentation/mac/Sound/Sound-2.html>`_ (July 1996), `Sound (chapter PDFs) <https://developer.apple.com/library/archive/documentation/mac/pdf/Sound/pdf.html>`_ (1994)
|
||||
* `Text (HTML) <https://developer.apple.com/library/archive/documentation/mac/Text/Text-2.html>`_ (July 1996), `Text (single PDF) <https://developer.apple.com/library/archive/documentation/mac/pdf/Text.pdf>`_ (1993)
|
||||
* The two AOCE volumes, Communications Toolbox, Human Interface Guidelines, Overview, seven QuickDraw GX volumes, two QuickTime volumes, and X-Ref are missing.
|
||||
|
||||
* The Gryphel project (best known for the Mini vMac emulator) has `a list of physical book releases <https://www.gryphel.com/c/books/appledev.html>`_ of Inside Macintosh (and other Apple developer documentation), including ISBNs, publishers, dates, and Amazon links.
|
||||
|
||||
* `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
|
||||
* Alysis Software Corporation's article on resource compression (found on `the company's website <http://www.alysis.us/arctechnology.htm>`_ and in `MacTech Magazine's online archive <http://preserve.mactech.com/articles/mactech/Vol.09/09.01/ResCompression/index.html>`_) has some information on the structure of certain kinds of compressed resources.
|
||||
* 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 <https://archive.org/web/>`_ or `archive.is <http://archive.is/>`_ aka `archive.fo <https://archive.fo/>`_.
|
||||
For technical info and documentation about resource files and resources, see the `"resource forks" section of the mac_file_format_docs repo <https://github.com/dgelessus/mac_file_format_docs/blob/master/README.md#resource-forks>`_.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Version 1.7
|
||||
^^^^^^^^^^^
|
||||
|
||||
* Added a ``raw-decompress`` subcommand to decompress compressed resource data stored in a standalone file rather than as a resource.
|
||||
* Optimized lazy loading of ``Resource`` objects. Previously, resource data would be read from disk whenever a ``Resource`` object was looked up, even if the data itself is never used. Now the resource data is only loaded once the ``data`` (or ``data_raw``) attribute is accessed.
|
||||
|
||||
* The same optimization applies to the ``name`` attribute, although this is unlikely to make a difference in practice.
|
||||
* As a result, it is no longer possible to construct ``Resource`` objects without a resource file. This was previously possible, but had no practical use.
|
||||
* Fixed a small error in the ``'dcmp' (0)`` decompression implementation.
|
||||
|
||||
Version 1.6.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Added a new subcommand-based command-line syntax to the ``rsrcfork`` tool, similar to other CLI tools such as ``git`` or ``diskutil``.
|
||||
|
||||
* This subcommand-based syntax is meant to replace the old CLI options, as the subcommand structure is easier to understand and more extensible in the future.
|
||||
* Currently there are three subcommands: ``list`` to list resources in a file, ``read`` to read/display resource data, and ``read-header`` to read a resource file's header data. These subcommands can be used to perform all operations that were also available with the old CLI syntax.
|
||||
* The old CLI syntax is still supported for now, but it will be removed soon.
|
||||
* The new syntax no longer supports reading CLI arguments from a file (using ``@args_file.txt``), abbreviating long options (e. g. ``--no-d`` instead of ``--no-decompress``), or the short option ``-f`` instead of ``--fork``. If you have a need for any of these features, please open an issue.
|
||||
|
||||
Version 1.5.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
# * Add a new empty section for the next version to the README.rst changelog.
|
||||
# * Commit and push the changes to master.
|
||||
|
||||
__version__ = "1.5.0"
|
||||
__version__ = "1.7.0"
|
||||
|
||||
__all__ = [
|
||||
"Resource",
|
||||
|
@ -2,6 +2,7 @@ import argparse
|
||||
import collections
|
||||
import enum
|
||||
import itertools
|
||||
import pathlib
|
||||
import sys
|
||||
import textwrap
|
||||
import typing
|
||||
@ -220,7 +221,7 @@ def describe_resource(res: api.Resource, *, include_type: bool, decompress: bool
|
||||
desc = f"'{restype}' {desc}"
|
||||
return desc
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
def parse_args_old(args: typing.List[str]) -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
fromfile_prefix_chars="@",
|
||||
@ -256,7 +257,7 @@ def parse_args() -> argparse.Namespace:
|
||||
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 = ap.parse_args(args)
|
||||
return ns
|
||||
|
||||
def show_header_data(data: bytes, *, format: str) -> None:
|
||||
@ -363,18 +364,6 @@ def show_filtered_resources(resources: typing.Sequence[api.Resource], format: st
|
||||
raise ValueError(f"Unhandled output format: {format}")
|
||||
|
||||
def list_resource_file(rf: api.ResourceFile, *, sort: bool, group: str, decompress: bool) -> None:
|
||||
if rf.header_system_data != bytes(len(rf.header_system_data)):
|
||||
print("Header system data:")
|
||||
hexdump(rf.header_system_data)
|
||||
|
||||
if rf.header_application_data != bytes(len(rf.header_application_data)):
|
||||
print("Header application data:")
|
||||
hexdump(rf.header_application_data)
|
||||
|
||||
attrs = decompose_flags(rf.file_attributes)
|
||||
if attrs:
|
||||
print("File attributes: " + " | ".join(attr.name for attr in attrs))
|
||||
|
||||
if len(rf) == 0:
|
||||
print("No resources (empty resource file)")
|
||||
return
|
||||
@ -419,8 +408,8 @@ def list_resource_file(rf: api.ResourceFile, *, sort: bool, group: str, decompre
|
||||
else:
|
||||
raise AssertionError(f"Unhandled group mode: {group!r}")
|
||||
|
||||
def main() -> typing.NoReturn:
|
||||
ns = parse_args()
|
||||
def main_old(args: typing.List[str]) -> typing.NoReturn:
|
||||
ns = parse_args_old(args)
|
||||
|
||||
if ns.file == "-":
|
||||
if ns.fork != "auto":
|
||||
@ -432,14 +421,255 @@ def main() -> typing.NoReturn:
|
||||
rf = api.ResourceFile.open(ns.file, fork=ns.fork)
|
||||
|
||||
with rf:
|
||||
print("Warning: The syntax of the rsrcfork command has changed.", file=sys.stderr)
|
||||
|
||||
if ns.header_system or ns.header_application:
|
||||
if ns.header_system:
|
||||
print('Please use "rsrcfork read-header --part=system <file>" instead of "rsrcfork --header-system <file>".', file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
data = rf.header_system_data
|
||||
else:
|
||||
print('Please use "rsrcfork read-header --part=application <file>" instead of "rsrcfork --header-application <file>".', file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
data = rf.header_application_data
|
||||
|
||||
show_header_data(data, format=ns.format)
|
||||
elif ns.filter or ns.all:
|
||||
if ns.filter:
|
||||
print('Please use "rsrcfork read <file> <filters...>" instead of "rsrcfork <file> <filters...>".', file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
resources = filter_resources(rf, ns.filter)
|
||||
else:
|
||||
print('Please use "rsrcfork read <file>" instead of "rsrcfork <file> --all".', file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
resources = []
|
||||
for reses in rf.values():
|
||||
resources.extend(reses.values())
|
||||
|
||||
if ns.sort:
|
||||
resources.sort(key=lambda res: (res.type, res.id))
|
||||
|
||||
show_filtered_resources(resources, format=ns.format, decompress=ns.decompress)
|
||||
else:
|
||||
print('Please use "rsrcfork list <file>" instead of "rsrcfork <file>".', file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
if rf.header_system_data != bytes(len(rf.header_system_data)):
|
||||
print("Header system data:")
|
||||
hexdump(rf.header_system_data)
|
||||
|
||||
if rf.header_application_data != bytes(len(rf.header_application_data)):
|
||||
print("Header application data:")
|
||||
hexdump(rf.header_application_data)
|
||||
|
||||
attrs = decompose_flags(rf.file_attributes)
|
||||
if attrs:
|
||||
print("File attributes: " + " | ".join(attr.name for attr in attrs))
|
||||
|
||||
list_resource_file(rf, sort=ns.sort, group=ns.group, decompress=ns.decompress)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def make_argument_parser(*, description: str, **kwargs: typing.Any) -> argparse.ArgumentParser:
|
||||
"""Create an argparse.ArgumentParser with some slightly modified defaults.
|
||||
|
||||
This function is used to ensure that all subcommands use the same base configuration for their ArgumentParser.
|
||||
"""
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=description,
|
||||
allow_abbrev=False,
|
||||
add_help=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
ap.add_argument("--help", action="help", help="Display this help message and exit")
|
||||
|
||||
return ap
|
||||
|
||||
def add_resource_file_args(ap: argparse.ArgumentParser) -> None:
|
||||
"""Define common options/arguments for specifying an input resource file.
|
||||
|
||||
This includes a positional argument for the resource file's path, and the ``--fork`` option to select which fork of the file to use.
|
||||
"""
|
||||
|
||||
ap.add_argument("--fork", choices=["auto", "data", "rsrc"], default="auto", help="The fork from which to read the resource file data, or auto to guess. Default: %(default)s")
|
||||
ap.add_argument("file", help="The file from which to read resources, or - for stdin.")
|
||||
|
||||
def open_resource_file(file: str, *, fork: str = None) -> api.ResourceFile:
|
||||
"""Open a resource file at the given path, using the specified fork."""
|
||||
|
||||
if file == "-":
|
||||
if fork != "auto":
|
||||
print("Cannot specify an explicit fork when reading from stdin", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return api.ResourceFile(sys.stdin.buffer)
|
||||
else:
|
||||
return api.ResourceFile.open(file, fork=fork)
|
||||
|
||||
|
||||
def do_read_header(prog: str, args: typing.List[str]) -> typing.NoReturn:
|
||||
"""Read the header data from a resource file."""
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
description="""
|
||||
Read and output a resource file's header data.
|
||||
|
||||
The header data consists of two parts:
|
||||
|
||||
The system-reserved data is 112 bytes long and used by the Classic Mac OS
|
||||
Finder as temporary storage space. It usually contains parts of the
|
||||
file metadata (name, type/creator code, etc.).
|
||||
|
||||
The application-specific data is 128 bytes long and is available for use by
|
||||
applications. In practice it usually contains junk data that happened to be in
|
||||
memory when the resource file was written.
|
||||
|
||||
Mac OS X does not use the header data fields anymore. Resource files written
|
||||
on Mac OS X normally have both parts of the header data set to all zero bytes.
|
||||
""",
|
||||
)
|
||||
|
||||
ap.add_argument("--format", choices=["dump", "dump-text", "hex", "raw"], default="dump", help="How to output the header data: human-readable info with hex dump (dump) (default), human-readable info with newline-translated data (dump-text), data only as hex (hex), or data only as raw bytes (raw). Default: %(default)s")
|
||||
ap.add_argument("--part", choices=["system", "application", "all"], default="all", help="Which part of the header to read. Default: %(default)s")
|
||||
add_resource_file_args(ap)
|
||||
|
||||
ns = ap.parse_args(args)
|
||||
|
||||
with open_resource_file(ns.file, fork=ns.fork) as rf:
|
||||
if ns.format in {"dump", "dump-text"}:
|
||||
if ns.format == "dump":
|
||||
dump_func = hexdump
|
||||
elif ns.format == "dump-text":
|
||||
def dump_func(d):
|
||||
print(translate_text(d))
|
||||
else:
|
||||
raise AssertionError(f"Unhandled --format: {ns.format!r}")
|
||||
|
||||
if ns.part in {"system", "all"}:
|
||||
print("System-reserved header data:")
|
||||
dump_func(rf.header_system_data)
|
||||
|
||||
if ns.part in {"application", "all"}:
|
||||
print("Application-specific header data:")
|
||||
dump_func(rf.header_application_data)
|
||||
elif ns.format in {"hex", "raw"}:
|
||||
if ns.part == "system":
|
||||
data = rf.header_system_data
|
||||
elif ns.part == "application":
|
||||
data = rf.header_application_data
|
||||
elif ns.part == "all":
|
||||
data = rf.header_system_data + rf.header_application_data
|
||||
else:
|
||||
raise AssertionError(f"Unhandled --part: {ns.part!r}")
|
||||
|
||||
if ns.format == "hex":
|
||||
raw_hexdump(data)
|
||||
elif ns.format == "raw":
|
||||
sys.stdout.buffer.write(data)
|
||||
else:
|
||||
raise AssertionError(f"Unhandled --format: {ns.format!r}")
|
||||
else:
|
||||
raise AssertionError(f"Unhandled --format: {ns.format!r}")
|
||||
|
||||
def do_info(prog: str, args: typing.List[str]) -> typing.NoReturn:
|
||||
"""Display technical information about the resource file."""
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
description="""
|
||||
Display technical information and stats about the resource file.
|
||||
""",
|
||||
)
|
||||
add_resource_file_args(ap)
|
||||
|
||||
ns = ap.parse_args(args)
|
||||
|
||||
with open_resource_file(ns.file, fork=ns.fork) as rf:
|
||||
print("System-reserved header data:")
|
||||
hexdump(rf.header_system_data)
|
||||
print()
|
||||
print("Application-specific header data:")
|
||||
hexdump(rf.header_application_data)
|
||||
print()
|
||||
|
||||
print(f"Resource data starts at {rf.data_offset:#x} and is {rf.data_length:#x} bytes long")
|
||||
print(f"Resource map starts at {rf.map_offset:#x} and is {rf.map_length:#x} bytes long")
|
||||
attrs = decompose_flags(rf.file_attributes)
|
||||
if attrs:
|
||||
attrs_desc = " | ".join(attr.name for attr in attrs)
|
||||
else:
|
||||
attrs_desc = "(none)"
|
||||
print(f"Resource map attributes: {attrs_desc}")
|
||||
print(f"Resource map type list starts at {rf.map_type_list_offset:#x} (relative to map start) and contains {len(rf)} types")
|
||||
print(f"Resource map name list starts at {rf.map_name_list_offset:#x} (relative to map start)")
|
||||
|
||||
def do_list(prog: str, args: typing.List[str]) -> typing.NoReturn:
|
||||
"""List the resources in a file."""
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
description="""
|
||||
List the resources stored in a resource file.
|
||||
|
||||
Each resource's type, ID, name (if any), attributes (if any), and data length
|
||||
are displayed. For compressed resources, the compressed and decompressed data
|
||||
length are displayed, as well as the ID of the 'dcmp' resource used to
|
||||
decompress the resource data.
|
||||
""",
|
||||
)
|
||||
|
||||
ap.add_argument("--no-decompress", action="store_false", dest="decompress", help="Do not parse the data header of compressed resources and only output their compressed length.")
|
||||
ap.add_argument("--group", action="store", choices=["none", "type", "id"], default="type", help="Group resources by type or ID, or disable grouping. Default: %(default)s")
|
||||
ap.add_argument("--no-sort", action="store_false", dest="sort", help="Output resources in the order in which they are stored in the file, instead of sorting them by type and ID.")
|
||||
add_resource_file_args(ap)
|
||||
|
||||
ns = ap.parse_args(args)
|
||||
|
||||
with open_resource_file(ns.file, fork=ns.fork) as rf:
|
||||
list_resource_file(rf, sort=ns.sort, group=ns.group, decompress=ns.decompress)
|
||||
|
||||
def do_read(prog: str, args: typing.List[str]) -> typing.NoReturn:
|
||||
"""Read data from resources."""
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
description="""
|
||||
Read the data of one or more resources.
|
||||
|
||||
The resource filters use syntax similar to Rez (resource definition) files.
|
||||
Each filter can have 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")
|
||||
|
||||
Note that the resource filter syntax uses quotes, parentheses and spaces,
|
||||
which have special meanings in most shells. It is recommended to quote each
|
||||
resource filter (using double quotes) to ensure that it is not interpreted
|
||||
or rewritten by the shell.
|
||||
""",
|
||||
)
|
||||
|
||||
ap.add_argument("--no-decompress", action="store_false", dest="decompress", help="Do not decompress compressed resources, output the raw compressed resource data.")
|
||||
ap.add_argument("--format", choices=["dump", "dump-text", "hex", "raw", "derez"], default="dump", help="How to output the resources: human-readable info with hex dump (dump), human-readable info with newline-translated data (dump-text), data only as hex (hex), data only as raw bytes (raw), or like DeRez with no resource definitions (derez). Default: %(default)s")
|
||||
ap.add_argument("--no-sort", action="store_false", dest="sort", help="Output resources in the order in which they are stored in the file, instead of sorting them by type and ID.")
|
||||
add_resource_file_args(ap)
|
||||
ap.add_argument("filter", nargs="*", help="One or more filters to select which resources to read. If no filters are specified, all resources are read.")
|
||||
|
||||
ns = ap.parse_args(args)
|
||||
|
||||
with open_resource_file(ns.file, fork=ns.fork) as rf:
|
||||
if ns.filter:
|
||||
resources = filter_resources(rf, ns.filter)
|
||||
else:
|
||||
@ -451,10 +681,174 @@ def main() -> typing.NoReturn:
|
||||
resources.sort(key=lambda res: (res.type, res.id))
|
||||
|
||||
show_filtered_resources(resources, format=ns.format, decompress=ns.decompress)
|
||||
else:
|
||||
list_resource_file(rf, sort=ns.sort, group=ns.group, decompress=ns.decompress)
|
||||
|
||||
sys.exit(0)
|
||||
def do_raw_decompress(prog: str, args: typing.List[str]) -> typing.NoReturn:
|
||||
"""Decompress raw compressed resource data."""
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
description="""
|
||||
Decompress raw compressed resource data that is stored in a standalone file
|
||||
and not as a resource in a resource file.
|
||||
|
||||
This subcommand can be used in a shell pipeline by passing - as the input and
|
||||
output file name, i. e. "%(prog)s - -".
|
||||
|
||||
Note: All other rsrcfork subcommands natively support compressed resources and
|
||||
will automatically decompress them as needed. This subcommand is only needed
|
||||
to decompress resource data that has been read from a resource file in
|
||||
compressed form (e. g. using --no-decompress or another tool that does not
|
||||
handle resource compression).
|
||||
""",
|
||||
)
|
||||
|
||||
ap.add_argument("--debug", action="store_true", help="Display debugging output from the decompressor on stdout. Cannot be used if the output file is - (stdout).")
|
||||
|
||||
ap.add_argument("input_file", help="The file from which to read the compressed resource data, or - for stdin.")
|
||||
ap.add_argument("output_file", help="The file to which to write the decompressed resource data, or - for stdout.")
|
||||
|
||||
ns = ap.parse_args(args)
|
||||
|
||||
if ns.input_file == "-":
|
||||
in_stream = sys.stdin.buffer
|
||||
close_in_stream = False
|
||||
else:
|
||||
in_stream = open(ns.input_file, "rb")
|
||||
close_in_stream = True
|
||||
|
||||
try:
|
||||
header_info = compress.CompressedHeaderInfo.parse_stream(in_stream)
|
||||
|
||||
# Open the output file only after parsing the header, so that the file is only created (or its existing contents deleted) if the input file is valid.
|
||||
if ns.output_file == "-":
|
||||
if ns.debug:
|
||||
print("Cannot use --debug if the decompression output file is - (stdout).", file=sys.stderr)
|
||||
print("The debug output goes to stdout and would conflict with the decompressed data.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
out_stream = sys.stdout.buffer
|
||||
close_out_stream = False
|
||||
else:
|
||||
out_stream = open(ns.output_file, "wb")
|
||||
close_out_stream = True
|
||||
|
||||
try:
|
||||
for chunk in compress.decompress_stream_parsed(header_info, in_stream, debug=ns.debug):
|
||||
out_stream.write(chunk)
|
||||
finally:
|
||||
if close_out_stream:
|
||||
out_stream.close()
|
||||
finally:
|
||||
if close_in_stream:
|
||||
in_stream.close()
|
||||
|
||||
|
||||
SUBCOMMANDS = {
|
||||
"read-header": do_read_header,
|
||||
"info": do_info,
|
||||
"list": do_list,
|
||||
"read": do_read,
|
||||
"raw-decompress": do_raw_decompress,
|
||||
}
|
||||
|
||||
|
||||
def format_subcommands_help() -> str:
|
||||
"""Return a formatted help text describing the availble subcommands.
|
||||
|
||||
Because we do not use argparse's native support for subcommands (see comments in main function), the main ArgumentParser's help does not include any information about the subcommands by default, so we have to format and add it ourselves.
|
||||
"""
|
||||
|
||||
# The list of subcommands is formatted using a "fake" ArgumentParser, which is never actually used to parse any arguments.
|
||||
# The options are chosen so that the help text will only include the subcommands list and epilog, but no usage or any other arguments.
|
||||
fake_ap = argparse.ArgumentParser(
|
||||
usage=argparse.SUPPRESS,
|
||||
epilog=textwrap.dedent("""
|
||||
Most of the above subcommands take additional arguments. Run a subcommand with
|
||||
the option --help for help about the options understood by that subcommand.
|
||||
"""),
|
||||
add_help=False,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
# The subcommands are added as positional arguments to a custom group with the title "subcommands".
|
||||
# Semantically this makes no sense, but it looks right in the formatted help text:
|
||||
# the result is a section "subcommands" with an aligned list of command names and short descriptions.
|
||||
fake_group = fake_ap.add_argument_group(title="subcommands")
|
||||
|
||||
for name, func in SUBCOMMANDS.items():
|
||||
# Each command's short description is taken from the implementation function's docstring.
|
||||
fake_group.add_argument(name, help=func.__doc__)
|
||||
|
||||
return fake_ap.format_help()
|
||||
|
||||
|
||||
def main() -> typing.NoReturn:
|
||||
"""Main function of the CLI.
|
||||
|
||||
This function is a valid setuptools entry point. Arguments are passed in sys.argv, and every execution path ends with a sys.exit call. (setuptools entry points are also permitted to return an integer, which will be treated as an exit code. We do not use this feature and instead always call sys.exit ourselves.)
|
||||
"""
|
||||
|
||||
prog = pathlib.PurePath(sys.argv[0]).name
|
||||
args = sys.argv[1:]
|
||||
|
||||
# The rsrcfork CLI is structured into subcommands, each implemented in a separate function.
|
||||
# The main function parses the command-line arguments enough to determine which subcommand to call, but leaves parsing of the rest of the arguments to the subcommand itself.
|
||||
# In addition, it detects use of the old, non-subcommand-based CLI syntax, and delegates to the old main function in that case.
|
||||
# This backwards compatibility handling is one of the reasons why we cannot use the subcommand support of argparse or other CLI parsing libraries, so we have to implement most of the subcommand handling ourselves.
|
||||
|
||||
ap = make_argument_parser(
|
||||
prog=prog,
|
||||
# Custom usage string to make "subcommand ..." show up in the usage, but not as "positional arguments" in the main help text.
|
||||
usage=f"{prog} (--help | --version | subcommand ...)",
|
||||
description="""
|
||||
%(prog)s is a tool for working with Classic Mac OS resource files.
|
||||
Currently this tool can only read resource files; modifying/writing resource
|
||||
files is not supported yet.
|
||||
|
||||
Note: This tool is intended for human users. The output format is not
|
||||
machine-readable and may change at any time. The command-line syntax usually
|
||||
does not change much across versions, but this should not be relied on.
|
||||
Automated scripts and programs should use the Python API provided by the
|
||||
rsrcfork library, which this tool is a part of.
|
||||
""",
|
||||
# The list of subcommands is shown in the epilog so that it appears under the list of optional arguments.
|
||||
epilog=format_subcommands_help(),
|
||||
)
|
||||
|
||||
ap.add_argument("--version", action="version", version=__version__, help="Display version information and exit.")
|
||||
|
||||
# The help of these arguments is set to argparse.SUPPRESS so that they do not cause a mostly useless "positional arguments" list to appear.
|
||||
# If the old, non-subcommand syntax is used, the subcommand argument can actually be a file name.
|
||||
ap.add_argument("subcommand", help=argparse.SUPPRESS)
|
||||
ap.add_argument("args", nargs=argparse.REMAINDER, help=argparse.SUPPRESS)
|
||||
|
||||
if not args:
|
||||
print(f"{prog}: Missing subcommand.", file=sys.stderr)
|
||||
ap.print_help()
|
||||
sys.exit(2)
|
||||
|
||||
# First, parse only known arguments from the CLI.
|
||||
# This is so that we can extract the subcommand/file to check if the old CLI syntax was used, without causing CLI syntax errors because of unknown options before the subcommand/file.
|
||||
ns, _ = ap.parse_known_args(args)
|
||||
|
||||
try:
|
||||
# Check if the subcommand is valid.
|
||||
subcommand_func = SUBCOMMANDS[ns.subcommand]
|
||||
except KeyError:
|
||||
if ns.subcommand == "-" or pathlib.Path(ns.subcommand).exists():
|
||||
# Subcommand is actually a file path.
|
||||
# Call the old main function with the entire unparsed argument list, so that it can be reparsed and handled like in previous versions.
|
||||
main_old(args)
|
||||
else:
|
||||
# Subcommand is invalid and also not a path to an existing file. Display an error.
|
||||
print(f"{prog}: Unknown subcommand: {ns.subcommand}", file=sys.stderr)
|
||||
print(f"Run {prog} --help for a list of available subcommands.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
else:
|
||||
# Subcommand is valid. Parse the arguments again, this time without allowing unknown arguments before the subcommand.
|
||||
ns = ap.parse_args(args)
|
||||
# Call the looked up subcommand and pass on further arguments.
|
||||
subcommand_func(f"{prog} {ns.subcommand}", ns.args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
102
rsrcfork/api.py
102
rsrcfork/api.py
@ -97,24 +97,31 @@ class ResourceAttrs(enum.Flag):
|
||||
class Resource(object):
|
||||
"""A single resource from a resource file."""
|
||||
|
||||
_resfile: "ResourceFile"
|
||||
type: bytes
|
||||
id: int
|
||||
name: typing.Optional[bytes]
|
||||
name_offset: int
|
||||
_name: typing.Optional[bytes]
|
||||
attributes: ResourceAttrs
|
||||
data_raw: bytes
|
||||
data_raw_offset: int
|
||||
_data_raw: bytes
|
||||
_compressed_info: compress.common.CompressedHeaderInfo
|
||||
_data_decompressed: bytes
|
||||
|
||||
def __init__(self, resource_type: bytes, resource_id: int, name: typing.Optional[bytes], attributes: ResourceAttrs, data_raw: bytes) -> None:
|
||||
"""Create a new resource with the given type code, ID, name, attributes, and data."""
|
||||
def __init__(self, resfile: "ResourceFile", resource_type: bytes, resource_id: int, name_offset: int, attributes: ResourceAttrs, data_raw_offset: int) -> None:
|
||||
"""Create a resource object representing a resource stored in a resource file.
|
||||
|
||||
External code should not call this constructor manually. Resources should be looked up through a ResourceFile object instead.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._resfile = resfile
|
||||
self.type = resource_type
|
||||
self.id = resource_id
|
||||
self.name = name
|
||||
self.name_offset = name_offset
|
||||
self.attributes = attributes
|
||||
self.data_raw = data_raw
|
||||
self.data_raw_offset = data_raw_offset
|
||||
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
@ -133,7 +140,7 @@ class Resource(object):
|
||||
if not decompress_ok:
|
||||
data_repr = f"<decompression failed - compressed data: {data_repr}>"
|
||||
|
||||
return f"{type(self).__module__}.{type(self).__qualname__}(type={self.type}, id={self.id}, name={self.name}, attributes={self.attributes}, data={data_repr})"
|
||||
return f"<{type(self).__qualname__} type {self.type}, id {self.id}, name {self.name}, attributes {self.attributes}, data {data_repr}>"
|
||||
|
||||
@property
|
||||
def resource_type(self) -> bytes:
|
||||
@ -145,6 +152,30 @@ class Resource(object):
|
||||
warnings.warn(DeprecationWarning("The resource_id attribute has been deprecated and will be removed in a future version. Please use the id attribute instead."))
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def name(self) -> typing.Optional[bytes]:
|
||||
try:
|
||||
return self._name
|
||||
except AttributeError:
|
||||
if self.name_offset == 0xffff:
|
||||
self._name = None
|
||||
else:
|
||||
self._resfile._stream.seek(self._resfile.map_offset + self._resfile.map_name_list_offset + self.name_offset)
|
||||
(name_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_NAME_HEADER)
|
||||
self._name = self._resfile._read_exact(name_length)
|
||||
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def data_raw(self) -> bytes:
|
||||
try:
|
||||
return self._data_raw
|
||||
except AttributeError:
|
||||
self._resfile._stream.seek(self._resfile.data_offset + self.data_raw_offset)
|
||||
(data_raw_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_DATA_HEADER)
|
||||
self._data_raw = self._resfile._read_exact(data_raw_length)
|
||||
return self._data_raw
|
||||
|
||||
@property
|
||||
def compressed_info(self) -> typing.Optional[compress.common.CompressedHeaderInfo]:
|
||||
"""The compressed resource header information, or None if this resource is not compressed.
|
||||
@ -198,25 +229,22 @@ class Resource(object):
|
||||
else:
|
||||
return self.data_raw
|
||||
|
||||
class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.ContextManager["ResourceFile"]):
|
||||
"""A resource file reader operating on a byte stream."""
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class _LazyResourceMap(typing.Mapping[int, Resource]):
|
||||
"""Internal class: Lazy mapping of resource IDs to resource objects, returned when subscripting a ResourceFile."""
|
||||
"""Internal class: Read-only wrapper for a mapping of resource IDs to resource objects.
|
||||
|
||||
_resfile: "ResourceFile"
|
||||
_restype: bytes
|
||||
_submap: typing.Mapping[int, typing.Tuple[int, ResourceAttrs, int]]
|
||||
This class behaves like a normal read-only mapping. The main difference to a plain dict (or similar mapping) is that this mapping has a specialized repr to avoid excessive output when working in the REPL.
|
||||
"""
|
||||
|
||||
def __init__(self, resfile: "ResourceFile", restype: bytes) -> None:
|
||||
"""Create a new _LazyResourceMap "containing" all resources in resfile that have the type code restype."""
|
||||
type: bytes
|
||||
_submap: typing.Mapping[int, Resource]
|
||||
|
||||
def __init__(self, resource_type: bytes, submap: typing.Mapping[int, Resource]) -> None:
|
||||
"""Create a new _LazyResourceMap that wraps the given mapping."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._resfile = resfile
|
||||
self._restype = restype
|
||||
self._submap = self._resfile._references[self._restype]
|
||||
self.type = resource_type
|
||||
self._submap = submap
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the number of resources with this type code."""
|
||||
@ -236,26 +264,18 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.
|
||||
def __getitem__(self, key: int) -> Resource:
|
||||
"""Get a resource with the given ID for this type code."""
|
||||
|
||||
name_offset, attributes, data_offset = self._submap[key]
|
||||
|
||||
if name_offset == 0xffff:
|
||||
name = None
|
||||
else:
|
||||
self._resfile._stream.seek(self._resfile.map_offset + self._resfile.map_name_list_offset + name_offset)
|
||||
(name_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_NAME_HEADER)
|
||||
name = self._resfile._read_exact(name_length)
|
||||
|
||||
self._resfile._stream.seek(self._resfile.data_offset + data_offset)
|
||||
(data_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_DATA_HEADER)
|
||||
data = self._resfile._read_exact(data_length)
|
||||
|
||||
return Resource(self._restype, key, name, attributes, data)
|
||||
return self._submap[key]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if len(self) == 1:
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x} containing one resource: {next(iter(self.values()))}>"
|
||||
contents = f"one resource: {next(iter(self.values()))}"
|
||||
else:
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x} containing {len(self)} resources with IDs: {list(self)}>"
|
||||
contents = f"{len(self)} resources with IDs {list(self)}"
|
||||
|
||||
return f"<Resource map for type {self.type}, containing {contents}>"
|
||||
|
||||
class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.ContextManager["ResourceFile"]):
|
||||
"""A resource file reader operating on a byte stream."""
|
||||
|
||||
_close_stream: bool
|
||||
_stream: typing.BinaryIO
|
||||
@ -272,7 +292,7 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.
|
||||
file_attributes: ResourceFileAttrs
|
||||
|
||||
_reference_counts: typing.MutableMapping[bytes, int]
|
||||
_references: typing.MutableMapping[bytes, typing.MutableMapping[int, typing.Tuple[int, ResourceAttrs, int]]]
|
||||
_references: typing.MutableMapping[bytes, typing.MutableMapping[int, Resource]]
|
||||
|
||||
@classmethod
|
||||
def open(cls, filename: typing.Union[str, os.PathLike], *, fork: str="auto", **kwargs: typing.Any) -> "ResourceFile":
|
||||
@ -433,7 +453,7 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.
|
||||
self._references = collections.OrderedDict()
|
||||
|
||||
for resource_type, count in self._reference_counts.items():
|
||||
resmap: typing.MutableMapping[int, typing.Tuple[int, ResourceAttrs, int]] = collections.OrderedDict()
|
||||
resmap: typing.MutableMapping[int, Resource] = collections.OrderedDict()
|
||||
self._references[resource_type] = resmap
|
||||
for _ in range(count):
|
||||
(
|
||||
@ -445,7 +465,7 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.
|
||||
attributes = attributes_and_data_offset >> 24
|
||||
data_offset = attributes_and_data_offset & ((1 << 24) - 1)
|
||||
|
||||
resmap[resource_id] = (name_offset, ResourceAttrs(attributes), data_offset)
|
||||
resmap[resource_id] = Resource(self, resource_type, resource_id, name_offset, ResourceAttrs(attributes), data_offset)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close this ResourceFile.
|
||||
@ -483,10 +503,10 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing.
|
||||
|
||||
return key in self._references
|
||||
|
||||
def __getitem__(self, key: bytes) -> "ResourceFile._LazyResourceMap":
|
||||
def __getitem__(self, key: bytes) -> "_LazyResourceMap":
|
||||
"""Get a lazy mapping of all resources with the given type in this ResourceFile."""
|
||||
|
||||
return ResourceFile._LazyResourceMap(self, key)
|
||||
return _LazyResourceMap(key, self._references[key])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x}, attributes {self.file_attributes}, containing {len(self)} resource types: {list(self)}>"
|
||||
|
@ -119,7 +119,7 @@ def decompress_stream_inner(header_info: common.CompressedHeaderInfo, stream: ty
|
||||
print(f"\t-> segment number: {segment_number_int:#x}")
|
||||
|
||||
# The tail part of all jump table entries (i. e. everything except for the address).
|
||||
entry_tail = b"?<" + segment_number_int.to_bytes(2, "big", signed=True) + b"\xa9\xf0"
|
||||
entry_tail = b"?<" + segment_number_int.to_bytes(2, "big", signed=False) + b"\xa9\xf0"
|
||||
# The tail is output once *without* an address in front, i. e. the first entry's address must be generated manually by a previous code.
|
||||
yield entry_tail
|
||||
|
||||
|
Reference in New Issue
Block a user