mirror of
https://github.com/dgelessus/python-rsrcfork.git
synced 2025-07-01 17:23:51 +00:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
2f2472cfe9 | |||
e0f73d3220 | |||
b77c85c295 | |||
e5875ffe67 | |||
449bf4dd71 | |||
0ac6e8a3c4 | |||
29ddd21740 | |||
add22b704a | |||
fdd04c944b | |||
97c459bca7 | |||
9ef084de58 | |||
6d03954784 | |||
343259049c | |||
e75e88018e | |||
0f72e8eb1f | |||
84f09d0b83 | |||
c108af60ca | |||
0c942e26ec | |||
868a322b8e | |||
a23cd0fcb2 | |||
53e73be980 | |||
9dbdf5b827 | |||
87d4ae43d4 | |||
716ac30a53 | |||
20991154d3 | |||
7207b1d32b | |||
1de940d597 | |||
d7255bc977 | |||
c6337bdfbd | |||
f4c2717720 | |||
8ad0234633 | |||
7612322c43 | |||
51ae7c6a09 | |||
194c886472 | |||
b2fa5f8b0f | |||
752ec9e828 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,3 +6,6 @@ __pycache__/
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
92
README.rst
92
README.rst
@ -56,7 +56,7 @@ Simple example
|
||||
>>> rf
|
||||
<rsrcfork.ResourceFile at 0x1046e6048, attributes ResourceFileAttrs.0, containing 4 resource types: [b'utxt', b'utf8', b'TEXT', b'drag']>
|
||||
>>> rf[b"TEXT"]
|
||||
<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')>
|
||||
<rsrcfork.ResourceFile._LazyResourceMap at 0x10470ed30 containing one resource: rsrcfork.Resource(type=b'TEXT', id=256, name=None, attributes=ResourceAttrs.0, data=b'Here is some text')>
|
||||
|
||||
Automatic selection of data/resource fork
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -110,10 +110,57 @@ 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:
|
||||
* 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.
|
||||
|
||||
* `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>`_
|
||||
* 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)
|
||||
@ -127,6 +174,43 @@ If these links are no longer functional, some are archived in the `Internet Arch
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Version 1.4.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Added ``length`` and ``length_raw`` attributes to ``Resource``. These attributes are equivalent to the ``len`` of ``data`` and ``data_raw`` respectively, but may be faster to access.
|
||||
|
||||
* Currently, the only optimized case is ``length`` for compressed resources, but more optimizations may be added in the future.
|
||||
|
||||
* Added a ``compressed_info`` attribute to ``Resource`` that provides access to the header information of compressed resources.
|
||||
* Improved handling of compressed resources when listing resource files with the command line tool.
|
||||
|
||||
* Metadata of compressed resources is now displayed even if no decompressor implementation is available (as long as the compressed data header can be parsed).
|
||||
* Performance has been improved - the data no longer needs to be fully decompressed to get its length, this information is now read from the header.
|
||||
* The ``'dcmp'`` ID used to decompress each resource is displayed.
|
||||
|
||||
* Fixed an incorrect ``options.packages`` in ``setup.cfg``, which made the library unusable except when installing from source using ``--editable``.
|
||||
* Fixed ``ResourceFile.__enter__`` returning ``None``, which made it impossible to use ``ResourceFile`` properly in a ``with`` statement.
|
||||
* Fixed various minor errors reported by type checking with ``mypy``.
|
||||
|
||||
Version 1.3.0.post1
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Fixed an incorrect ``options.packages`` in ``setup.cfg``, which made the library unusable except when installing from source using ``--editable``.
|
||||
|
||||
Version 1.2.0.post1
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Fixed an incorrect ``options.packages`` in ``setup.cfg``, which made the library unusable except when installing from source using ``--editable``.
|
||||
|
||||
Version 1.3.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Added a ``--group`` command line option to group resources in list format by type (the default), ID, or with no grouping.
|
||||
* Added a ``dump-text`` output format to the command line tool. This format is identical to ``dump``, but instead of a hex dump, it outputs the resource data as text. The data is decoded as MacRoman and classic Mac newlines (``\r``) are translated. This is useful for examining resources that contain mostly plain text.
|
||||
* Changed the command line tool to sort resources by type and ID, and added a ``--no-sort`` option to disable sorting and output resources in file order (which was the previous behavior).
|
||||
* Renamed the ``rsrcfork.Resource`` attributes ``resource_type`` and ``resource_id`` to ``type`` and ``id``, respectively. The old names have been deprecated and will be removed in the future, but are still supported for now.
|
||||
* Changed ``--format=dump`` output to match ``hexdump -C``'s format - spacing has been adjusted, and multiple subsequent identical lines are collapsed into a single ``*``.
|
||||
|
||||
Version 1.2.0
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
@ -1,6 +1,26 @@
|
||||
"""A pure Python, cross-platform library/tool for reading Macintosh resource data, as stored in resource forks and ``.rsrc`` files."""
|
||||
|
||||
__version__ = "1.2.0"
|
||||
# To release a new version:
|
||||
# * Remove the .dev suffix from the version number in this file.
|
||||
# * Update the changelog in the README.rst (rename the "next version" section to the correct version number).
|
||||
# * Remove the ``dist`` directory (if it exists) to clean up any old release files.
|
||||
# * Run ``python3 setup.py sdist bdist_wheel`` to build the release files.
|
||||
# * Run ``python3 -m twine check dist/*`` to check the release files.
|
||||
# * Fix any errors reported by the build and/or check steps.
|
||||
# * Commit the changes to master.
|
||||
# * Tag the release commit with the version number, prefixed with a "v" (e. g. version 1.2.3 is tagged as v1.2.3).
|
||||
# * Fast-forward the release branch to the new release commit.
|
||||
# * Push the master and release branches.
|
||||
# * Upload the release files to PyPI using ``python3 -m twine upload dist/*``.
|
||||
# * On the GitHub repo's Releases page, edit the new release tag and add the relevant changelog section from the README.rst. (Note: The README is in reStructuredText format, but GitHub's release notes use Markdown, so it may be necessary to adjust the markup syntax.)
|
||||
|
||||
# After releasing:
|
||||
# * (optional) Remove the build and dist directories from the previous release as they are no longer needed.
|
||||
# * Bump the version number in this file to the next version and add a .dev suffix.
|
||||
# * Add a new empty section for the next version to the README.rst changelog.
|
||||
# * Commit and push the changes to master.
|
||||
|
||||
__version__ = "1.4.0"
|
||||
|
||||
__all__ = [
|
||||
"Resource",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import collections
|
||||
import enum
|
||||
import itertools
|
||||
import sys
|
||||
import textwrap
|
||||
import typing
|
||||
@ -24,7 +25,7 @@ _REZ_ATTR_NAMES = {
|
||||
api.ResourceAttrs.resCompressed: None, # "Extended Header resource attribute"
|
||||
}
|
||||
|
||||
F = typing.TypeVar("F", bound=enum.Flag, covariant=True)
|
||||
F = typing.TypeVar("F", bound=enum.Flag)
|
||||
def _decompose_flags(value: F) -> typing.Sequence[F]:
|
||||
"""Decompose an enum.Flags instance into separate enum constants."""
|
||||
|
||||
@ -44,14 +45,14 @@ def _bytes_unescape(string: str) -> bytes:
|
||||
(We implement our own unescaping mechanism here to not depend on any of Python's string/bytes escape syntax.)
|
||||
"""
|
||||
|
||||
out = []
|
||||
out: typing.List[int] = []
|
||||
it = iter(string)
|
||||
for char in it:
|
||||
if char == "\\":
|
||||
try:
|
||||
esc = next(it)
|
||||
if esc in "\\\'\"":
|
||||
out.append(esc)
|
||||
out.extend(esc.encode(_TEXT_ENCODING))
|
||||
elif esc == "x":
|
||||
x1, x2 = next(it), next(it)
|
||||
out.append(int(x1+x2, 16))
|
||||
@ -64,7 +65,7 @@ def _bytes_unescape(string: str) -> bytes:
|
||||
|
||||
return bytes(out)
|
||||
|
||||
def _bytes_escape(bs: bytes, *, quote: str=None) -> str:
|
||||
def _bytes_escape(bs: bytes, *, quote: typing.Optional[str]=None) -> str:
|
||||
"""Convert a bytestring to a string (using _TEXT_ENCODING), with non-printable characters hex-escaped.
|
||||
|
||||
(We implement our own escaping mechanism here to not depend on Python's str or bytes repr.)
|
||||
@ -81,8 +82,8 @@ def _bytes_escape(bs: bytes, *, quote: str=None) -> str:
|
||||
|
||||
return "".join(out)
|
||||
|
||||
def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typing.Sequence[api.Resource]:
|
||||
matching = collections.OrderedDict()
|
||||
def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> typing.List[api.Resource]:
|
||||
matching: typing.MutableMapping[typing.Tuple[bytes, int], api.Resource] = collections.OrderedDict()
|
||||
|
||||
for filter in filters:
|
||||
if len(filter) == 4:
|
||||
@ -92,7 +93,7 @@ def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> ty
|
||||
continue
|
||||
|
||||
for res in resources.values():
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
matching[res.type, res.id] = res
|
||||
elif filter[0] == filter[-1] == "'":
|
||||
try:
|
||||
resources = rf[_bytes_unescape(filter[1:-1])]
|
||||
@ -100,7 +101,7 @@ def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> ty
|
||||
continue
|
||||
|
||||
for res in resources.values():
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
matching[res.type, res.id] = res
|
||||
else:
|
||||
pos = filter.find("'", 1)
|
||||
if pos == -1:
|
||||
@ -108,68 +109,83 @@ def _filter_resources(rf: api.ResourceFile, filters: typing.Sequence[str]) -> ty
|
||||
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:]
|
||||
restype_str, resid_str = filter[:pos + 1], filter[pos + 2:]
|
||||
|
||||
if not restype[0] == restype[-1] == "'":
|
||||
if not restype_str[0] == restype_str[-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])
|
||||
f"Invalid filter {filter!r}: Resource type is not a single-quoted type identifier: {restype_str!r}")
|
||||
restype = _bytes_unescape(restype_str[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] != ")":
|
||||
if resid_str[0] != "(" or resid_str[-1] != ")":
|
||||
raise ValueError(f"Invalid filter {filter!r}: Resource ID must be parenthesized")
|
||||
resid = resid[1:-1]
|
||||
resid_str = resid_str[1:-1]
|
||||
|
||||
try:
|
||||
resources = rf[restype]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if resid[0] == resid[-1] == '"':
|
||||
name = _bytes_unescape(resid[1:-1])
|
||||
if resid_str[0] == resid_str[-1] == '"':
|
||||
name = _bytes_unescape(resid_str[1:-1])
|
||||
|
||||
for res in resources.values():
|
||||
if res.name == name:
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
matching[res.type, res.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)
|
||||
elif ":" in resid_str:
|
||||
if resid_str.count(":") > 1:
|
||||
raise ValueError(f"Invalid filter {filter!r}: Too many colons in ID range expression: {resid_str!r}")
|
||||
start_str, end_str = resid_str.split(":")
|
||||
start, end = int(start_str), int(end_str)
|
||||
|
||||
for res in resources.values():
|
||||
if start <= res.resource_id <= end:
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
if start <= res.id <= end:
|
||||
matching[res.type, res.id] = res
|
||||
else:
|
||||
resid = int(resid)
|
||||
resid = int(resid_str)
|
||||
try:
|
||||
res = resources[resid]
|
||||
except KeyError:
|
||||
continue
|
||||
matching[res.resource_type, res.resource_id] = res
|
||||
matching[res.type, res.id] = res
|
||||
|
||||
return list(matching.values())
|
||||
|
||||
def _hexdump(data: bytes):
|
||||
def _hexdump(data: bytes) -> None:
|
||||
last_line = None
|
||||
asterisk_shown = False
|
||||
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(_TEXT_ENCODING).translate(_TRANSLATE_NONPRINTABLES)
|
||||
print(f"{i:08x} {line_hex:<{16*2+15}} |{line_char}|")
|
||||
# If the same 16-byte lines appear multiple times, print only the first one, and replace all further lines with a single line with an asterisk.
|
||||
# This is unambiguous - to find out how many lines were collapsed this way, the user can compare the addresses of the lines before and after the asterisk.
|
||||
if line == last_line:
|
||||
if not asterisk_shown:
|
||||
print("*")
|
||||
asterisk_shown = True
|
||||
else:
|
||||
line_hex_left = " ".join(f"{byte:02x}" for byte in line[:8])
|
||||
line_hex_right = " ".join(f"{byte:02x}" for byte in line[8:])
|
||||
line_char = line.decode(_TEXT_ENCODING).translate(_TRANSLATE_NONPRINTABLES)
|
||||
print(f"{i:08x} {line_hex_left:<{8*2+7}} {line_hex_right:<{8*2+7}} |{line_char}|")
|
||||
asterisk_shown = False
|
||||
last_line = line
|
||||
|
||||
if data:
|
||||
print(f"{len(data):08x}")
|
||||
|
||||
def _raw_hexdump(data: bytes):
|
||||
def _raw_hexdump(data: bytes) -> None:
|
||||
for i in range(0, len(data), 16):
|
||||
print(" ".join(f"{byte:02x}" for byte in data[i:i + 16]))
|
||||
|
||||
def _translate_text(data: bytes) -> str:
|
||||
return data.decode(_TEXT_ENCODING).replace("\r", "\n")
|
||||
|
||||
def _describe_resource(res: api.Resource, *, include_type: bool, decompress: bool) -> str:
|
||||
id_desc_parts = [f"{res.resource_id}"]
|
||||
id_desc_parts = [f"{res.id}"]
|
||||
|
||||
if res.name is not None:
|
||||
name = _bytes_escape(res.name, quote='"')
|
||||
@ -181,13 +197,15 @@ def _describe_resource(res: api.Resource, *, include_type: bool, decompress: boo
|
||||
|
||||
if decompress and api.ResourceAttrs.resCompressed in res.attributes:
|
||||
try:
|
||||
res.data
|
||||
res.compressed_info
|
||||
except compress.DecompressError:
|
||||
length_desc = f"decompression failed ({len(res.data_raw)} bytes compressed)"
|
||||
length_desc = f"unparseable compressed data header ({res.length_raw} bytes compressed)"
|
||||
else:
|
||||
length_desc = f"{len(res.data)} bytes ({len(res.data_raw)} bytes compressed)"
|
||||
assert res.compressed_info is not None
|
||||
length_desc = f"{res.length} bytes ({res.length_raw} bytes compressed, 'dcmp' ({res.compressed_info.dcmp_id}) format)"
|
||||
else:
|
||||
length_desc = f"{len(res.data_raw)} bytes"
|
||||
assert res.compressed_info is None
|
||||
length_desc = f"{res.length_raw} bytes"
|
||||
content_desc_parts.append(length_desc)
|
||||
|
||||
attrs = _decompose_flags(res.attributes)
|
||||
@ -198,11 +216,11 @@ def _describe_resource(res: api.Resource, *, include_type: bool, decompress: boo
|
||||
|
||||
desc = f"({id_desc}): {content_desc}"
|
||||
if include_type:
|
||||
restype = _bytes_escape(res.resource_type, quote="'")
|
||||
restype = _bytes_escape(res.type, quote="'")
|
||||
desc = f"'{restype}' {desc}"
|
||||
return desc
|
||||
|
||||
def main():
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
fromfile_prefix_chars="@",
|
||||
@ -229,7 +247,9 @@ def main():
|
||||
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("--no-decompress", action="store_false", dest="decompress", help="Do not decompress compressed resources, output compressed resource data as-is")
|
||||
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("--format", choices=["dump", "dump-text", "hex", "raw", "derez"], default="dump", help="How to output the resources - human-readable info with hex dump (dump) (default), 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)")
|
||||
ap.add_argument("--group", action="store", choices=["none", "type", "id"], default="type", help="Group resources in list view by type or ID, or disable grouping (default: type)")
|
||||
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")
|
||||
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")
|
||||
|
||||
@ -237,6 +257,170 @@ def main():
|
||||
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()
|
||||
return ns
|
||||
|
||||
def _show_header_data(data: bytes, *, format: str) -> None:
|
||||
if format == "dump":
|
||||
_hexdump(data)
|
||||
elif format == "dump-text":
|
||||
print(_translate_text(data))
|
||||
elif format == "hex":
|
||||
_raw_hexdump(data)
|
||||
elif format == "raw":
|
||||
sys.stdout.buffer.write(data)
|
||||
elif format == "derez":
|
||||
print("Cannot output file header data in derez format", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise ValueError(f"Unhandled output format: {format}")
|
||||
|
||||
def _show_filtered_resources(resources: typing.Sequence[api.Resource], format: str, decompress: bool) -> None:
|
||||
if not resources:
|
||||
if format in ("dump", "dump-text"):
|
||||
print("No resources matched the filter")
|
||||
elif format in ("hex", "raw"):
|
||||
print("No resources matched the filter", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif format == "derez":
|
||||
print("/* No resources matched the filter */")
|
||||
else:
|
||||
raise AssertionError(f"Unhandled output format: {format}")
|
||||
elif format in ("hex", "raw") and len(resources) != 1:
|
||||
print(f"Format {format} can only output a single resource, but the filter matched {len(resources)} resources", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for res in resources:
|
||||
if decompress:
|
||||
data = res.data
|
||||
else:
|
||||
data = res.data_raw
|
||||
|
||||
if format in ("dump", "dump-text"):
|
||||
# Human-readable info and hex or text dump
|
||||
desc = _describe_resource(res, include_type=True, decompress=decompress)
|
||||
print(f"Resource {desc}:")
|
||||
if format == "dump":
|
||||
_hexdump(data)
|
||||
elif format == "dump-text":
|
||||
print(_translate_text(data))
|
||||
else:
|
||||
raise AssertionError(f"Unhandled format: {format!r}")
|
||||
print()
|
||||
elif format == "hex":
|
||||
# Data only as hex
|
||||
|
||||
_raw_hexdump(data)
|
||||
elif format == "raw":
|
||||
# Data only as raw bytes
|
||||
|
||||
sys.stdout.buffer.write(data)
|
||||
elif format == "derez":
|
||||
# Like DeRez with no resource definitions
|
||||
|
||||
attrs = list(_decompose_flags(res.attributes))
|
||||
|
||||
if decompress and api.ResourceAttrs.resCompressed in attrs:
|
||||
attrs.remove(api.ResourceAttrs.resCompressed)
|
||||
attrs_comment = " /* was compressed */"
|
||||
else:
|
||||
attrs_comment = ""
|
||||
|
||||
attr_descs_with_none = [_REZ_ATTR_NAMES[attr] for attr in attrs]
|
||||
if None in attr_descs_with_none:
|
||||
attr_descs = [f"${res.attributes.value:02X}"]
|
||||
else:
|
||||
attr_descs = typing.cast(typing.List[str], attr_descs_with_none)
|
||||
|
||||
parts = [str(res.id)]
|
||||
|
||||
if res.name is not None:
|
||||
name = _bytes_escape(res.name, quote='"')
|
||||
parts.append(f'"{name}"')
|
||||
|
||||
parts += attr_descs
|
||||
|
||||
restype = _bytes_escape(res.type, quote="'")
|
||||
print(f"data '{restype}' ({', '.join(parts)}{attrs_comment}) {{")
|
||||
|
||||
for i in range(0, len(data), 16):
|
||||
# Two-byte grouping is really annoying to implement.
|
||||
groups = []
|
||||
for j in range(0, 16, 2):
|
||||
if i+j >= len(data):
|
||||
break
|
||||
elif i+j+1 >= len(data):
|
||||
groups.append(f"{data[i+j]:02X}")
|
||||
else:
|
||||
groups.append(f"{data[i+j]:02X}{data[i+j+1]:02X}")
|
||||
|
||||
s = f'$"{" ".join(groups)}"'
|
||||
comment = "/* " + data[i:i + 16].decode(_TEXT_ENCODING).translate(_TRANSLATE_NONPRINTABLES) + " */"
|
||||
print(f"\t{s:<54s}{comment}")
|
||||
|
||||
print("};")
|
||||
print()
|
||||
else:
|
||||
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
|
||||
|
||||
if group == "none":
|
||||
all_resources: typing.List[api.Resource] = []
|
||||
for reses in rf.values():
|
||||
all_resources.extend(reses.values())
|
||||
if sort:
|
||||
all_resources.sort(key=lambda res: (res.type, res.id))
|
||||
print(f"{len(all_resources)} resources:")
|
||||
for res in all_resources:
|
||||
print(_describe_resource(res, include_type=True, decompress=decompress))
|
||||
elif group == "type":
|
||||
print(f"{len(rf)} resource types:")
|
||||
restype_items: typing.Collection[typing.Tuple[bytes, typing.Mapping[int, api.Resource]]] = rf.items()
|
||||
if sort:
|
||||
restype_items = sorted(restype_items, key=lambda item: item[0])
|
||||
for typecode, resources_map in restype_items:
|
||||
restype = _bytes_escape(typecode, quote="'")
|
||||
print(f"'{restype}': {len(resources_map)} resources:")
|
||||
resources_items: typing.Collection[typing.Tuple[int, api.Resource]] = resources_map.items()
|
||||
if sort:
|
||||
resources_items = sorted(resources_items, key=lambda item: item[0])
|
||||
for resid, res in resources_items:
|
||||
print(_describe_resource(res, include_type=False, decompress=decompress))
|
||||
print()
|
||||
elif group == "id":
|
||||
all_resources = []
|
||||
for reses in rf.values():
|
||||
all_resources.extend(reses.values())
|
||||
all_resources.sort(key=lambda res: res.id)
|
||||
resources_by_id = {resid: list(reses) for resid, reses in itertools.groupby(all_resources, key=lambda res: res.id)}
|
||||
print(f"{len(resources_by_id)} resource IDs:")
|
||||
for resid, resources in resources_by_id.items():
|
||||
print(f"({resid}): {len(resources)} resources:")
|
||||
if sort:
|
||||
resources.sort(key=lambda res: res.type)
|
||||
for res in resources:
|
||||
print(_describe_resource(res, include_type=True, decompress=decompress))
|
||||
print()
|
||||
else:
|
||||
raise AssertionError(f"Unhandled group mode: {group!r}")
|
||||
|
||||
def main() -> typing.NoReturn:
|
||||
ns = _parse_args()
|
||||
|
||||
if ns.file == "-":
|
||||
if ns.fork is not None:
|
||||
@ -254,17 +438,7 @@ def main():
|
||||
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}")
|
||||
_show_header_data(data, format=ns.format)
|
||||
elif ns.filter or ns.all:
|
||||
if ns.filter:
|
||||
resources = _filter_resources(rf, ns.filter)
|
||||
@ -273,108 +447,12 @@ def main():
|
||||
for reses in rf.values():
|
||||
resources.extend(reses.values())
|
||||
|
||||
if not resources:
|
||||
if ns.format == "dump":
|
||||
print("No resources matched the filter")
|
||||
elif ns.format in ("hex", "raw"):
|
||||
print("No resources matched the filter", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif ns.format == "derez":
|
||||
print("/* No resources matched the filter */")
|
||||
else:
|
||||
raise AssertionError(f"Unhandled output format: {ns.format}")
|
||||
elif ns.format in ("hex", "raw") and len(resources) != 1:
|
||||
print(f"Format {ns.format} can only output a single resource, but the filter matched {len(resources)} resources", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if ns.sort:
|
||||
resources.sort(key=lambda res: (res.type, res.id))
|
||||
|
||||
for res in resources:
|
||||
if ns.decompress:
|
||||
data = res.data
|
||||
else:
|
||||
data = res.data_raw
|
||||
|
||||
if ns.format == "dump":
|
||||
# Human-readable info and hex dump
|
||||
desc = _describe_resource(res, include_type=True, decompress=ns.decompress)
|
||||
print(f"Resource {desc}:")
|
||||
_hexdump(data)
|
||||
print()
|
||||
elif ns.format == "hex":
|
||||
# Data only as hex
|
||||
|
||||
_raw_hexdump(data)
|
||||
elif ns.format == "raw":
|
||||
# Data only as raw bytes
|
||||
|
||||
sys.stdout.buffer.write(data)
|
||||
elif ns.format == "derez":
|
||||
# Like DeRez with no resource definitions
|
||||
|
||||
attrs = list(_decompose_flags(res.attributes))
|
||||
|
||||
if ns.decompress and api.ResourceAttrs.resCompressed in attrs:
|
||||
attrs.remove(api.ResourceAttrs.resCompressed)
|
||||
attrs_comment = " /* was compressed */"
|
||||
else:
|
||||
attrs_comment = ""
|
||||
|
||||
attr_descs = [_REZ_ATTR_NAMES[attr] for attr in attrs]
|
||||
if None in attr_descs:
|
||||
attr_descs[:] = [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 += attr_descs
|
||||
|
||||
restype = _bytes_escape(res.resource_type, quote="'")
|
||||
print(f"data '{restype}' ({', '.join(parts)}{attrs_comment}) {{")
|
||||
|
||||
for i in range(0, len(data), 16):
|
||||
# Two-byte grouping is really annoying to implement.
|
||||
groups = []
|
||||
for j in range(0, 16, 2):
|
||||
if i+j >= len(data):
|
||||
break
|
||||
elif i+j+1 >= len(data):
|
||||
groups.append(f"{data[i+j]:02X}")
|
||||
else:
|
||||
groups.append(f"{data[i+j]:02X}{data[i+j+1]:02X}")
|
||||
|
||||
s = f'$"{" ".join(groups)}"'
|
||||
comment = "/* " + data[i:i + 16].decode(_TEXT_ENCODING).translate(_TRANSLATE_NONPRINTABLES) + " */"
|
||||
print(f"\t{s:<54s}{comment}")
|
||||
|
||||
print("};")
|
||||
print()
|
||||
else:
|
||||
raise ValueError(f"Unhandled output format: {ns.format}")
|
||||
_show_filtered_resources(resources, format=ns.format, decompress=ns.decompress)
|
||||
else:
|
||||
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(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():
|
||||
print(_describe_resource(res, include_type=False, decompress=ns.decompress))
|
||||
print()
|
||||
else:
|
||||
print("No resource types (empty resource file)")
|
||||
_list_resource_file(rf, sort=ns.sort, group=ns.group, decompress=ns.decompress)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
170
rsrcfork/api.py
170
rsrcfork/api.py
@ -4,6 +4,7 @@ import enum
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
@ -96,20 +97,26 @@ class ResourceAttrs(enum.Flag):
|
||||
class Resource(object):
|
||||
"""A single resource from a resource file."""
|
||||
|
||||
__slots__ = ("resource_type", "resource_id", "name", "attributes", "data_raw", "_data_decompressed")
|
||||
type: bytes
|
||||
id: int
|
||||
name: typing.Optional[bytes]
|
||||
attributes: ResourceAttrs
|
||||
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):
|
||||
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."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.resource_type: bytes = resource_type
|
||||
self.resource_id: int = resource_id
|
||||
self.name: typing.Optional[bytes] = name
|
||||
self.attributes: ResourceAttrs = attributes
|
||||
self.data_raw: bytes = data_raw
|
||||
self.type = resource_type
|
||||
self.id = resource_id
|
||||
self.name = name
|
||||
self.attributes = attributes
|
||||
self.data_raw = data_raw
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
data = self.data
|
||||
except compress.DecompressError:
|
||||
@ -126,7 +133,54 @@ class Resource(object):
|
||||
if not decompress_ok:
|
||||
data_repr = f"<decompression failed - compressed data: {data_repr}>"
|
||||
|
||||
return f"{type(self).__module__}.{type(self).__qualname__}(resource_type={self.resource_type}, resource_id={self.resource_id}, name={self.name}, attributes={self.attributes}, 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})"
|
||||
|
||||
@property
|
||||
def resource_type(self) -> bytes:
|
||||
warnings.warn(DeprecationWarning("The resource_type attribute has been deprecated and will be removed in a future version. Please use the type attribute instead."))
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def resource_id(self) -> int:
|
||||
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 compressed_info(self) -> typing.Optional[compress.common.CompressedHeaderInfo]:
|
||||
"""The compressed resource header information, or None if this resource is not compressed.
|
||||
|
||||
Accessing this attribute may raise a DecompressError if the resource data is compressed and the header could not be parsed. To access the unparsed header data, use the data_raw attribute.
|
||||
"""
|
||||
|
||||
if ResourceAttrs.resCompressed in self.attributes:
|
||||
try:
|
||||
return self._compressed_info
|
||||
except AttributeError:
|
||||
self._compressed_info = compress.common.CompressedHeaderInfo.parse(self.data_raw)
|
||||
return self._compressed_info
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def length_raw(self) -> int:
|
||||
"""The length of the raw resource data, which may be compressed.
|
||||
|
||||
Accessing this attribute may be faster than computing len(self.data_raw) manually.
|
||||
"""
|
||||
|
||||
return len(self.data_raw)
|
||||
|
||||
@property
|
||||
def length(self) -> int:
|
||||
"""The length of the resource data. If the resource data is compressed, this is the length of the data after decompression.
|
||||
|
||||
Accessing this attribute may be faster than computing len(self.data) manually.
|
||||
"""
|
||||
|
||||
if self.compressed_info is not None:
|
||||
return self.compressed_info.decompressed_length
|
||||
else:
|
||||
return self.length_raw
|
||||
|
||||
@property
|
||||
def data(self) -> bytes:
|
||||
@ -135,42 +189,46 @@ class Resource(object):
|
||||
Accessing this attribute may raise a DecompressError if the resource data is compressed and could not be decompressed. To access the compressed resource data, use the data_raw attribute.
|
||||
"""
|
||||
|
||||
if ResourceAttrs.resCompressed in self.attributes:
|
||||
if self.compressed_info is not None:
|
||||
try:
|
||||
return self._data_decompressed
|
||||
except AttributeError:
|
||||
self._data_decompressed = compress.decompress(self.data_raw)
|
||||
self._data_decompressed = compress.decompress_parsed(self.compressed_info, self.data_raw[self.compressed_info.header_length:])
|
||||
return self._data_decompressed
|
||||
else:
|
||||
return self.data_raw
|
||||
|
||||
class ResourceFile(collections.abc.Mapping):
|
||||
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(collections.abc.Mapping):
|
||||
class _LazyResourceMap(typing.Mapping[int, Resource]):
|
||||
"""Internal class: Lazy mapping of resource IDs to resource objects, returned when subscripting a ResourceFile."""
|
||||
|
||||
def __init__(self, resfile: "ResourceFile", restype: bytes):
|
||||
_resfile: "ResourceFile"
|
||||
_restype: bytes
|
||||
_submap: typing.Mapping[int, typing.Tuple[int, ResourceAttrs, int]]
|
||||
|
||||
def __init__(self, resfile: "ResourceFile", restype: bytes) -> None:
|
||||
"""Create a new _LazyResourceMap "containing" all resources in resfile that have the type code restype."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._resfile: "ResourceFile" = resfile
|
||||
self._restype: bytes = restype
|
||||
self._submap: typing.Mapping[int, typing.Tuple[int, ResourceAttrs, int]] = self._resfile._references[self._restype]
|
||||
self._resfile = resfile
|
||||
self._restype = restype
|
||||
self._submap = self._resfile._references[self._restype]
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Get the number of resources with this type code."""
|
||||
|
||||
return len(self._submap)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[int]:
|
||||
"""Iterate over the IDs of all resources with this type code."""
|
||||
|
||||
return iter(self._submap)
|
||||
|
||||
def __contains__(self, key: int):
|
||||
def __contains__(self, key: object) -> bool:
|
||||
"""Check if a resource with the given ID exists for this type code."""
|
||||
|
||||
return key in self._submap
|
||||
@ -193,14 +251,31 @@ class ResourceFile(collections.abc.Mapping):
|
||||
|
||||
return Resource(self._restype, key, name, attributes, data)
|
||||
|
||||
def __repr__(self):
|
||||
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()))}>"
|
||||
else:
|
||||
return f"<{type(self).__module__}.{type(self).__qualname__} at {id(self):#x} containing {len(self)} resources with IDs: {list(self)}>"
|
||||
|
||||
_close_stream: bool
|
||||
_stream: typing.BinaryIO
|
||||
|
||||
data_offset: int
|
||||
map_offset: int
|
||||
data_length: int
|
||||
map_length: int
|
||||
header_system_data: bytes
|
||||
header_application_data: bytes
|
||||
|
||||
map_type_list_offset: int
|
||||
map_name_list_offset: int
|
||||
file_attributes: ResourceFileAttrs
|
||||
|
||||
_reference_counts: typing.MutableMapping[bytes, int]
|
||||
_references: typing.MutableMapping[bytes, typing.MutableMapping[int, typing.Tuple[int, ResourceAttrs, int]]]
|
||||
|
||||
@classmethod
|
||||
def open(cls, filename: typing.Union[str, bytes, os.PathLike], *, fork: str="auto", **kwargs) -> "ResourceFile":
|
||||
def open(cls, filename: typing.Union[str, os.PathLike], *, fork: str="auto", **kwargs: typing.Any) -> "ResourceFile":
|
||||
"""Open the file at the given path as a ResourceFile.
|
||||
|
||||
The fork parameter controls which fork of the file the resource data will be read from. It accepts the following values:
|
||||
@ -259,7 +334,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
else:
|
||||
raise ValueError(f"Unsupported value for the fork parameter: {fork!r}")
|
||||
|
||||
def __init__(self, stream: typing.io.BinaryIO, *, close: bool=False):
|
||||
def __init__(self, stream: typing.BinaryIO, *, close: bool=False) -> None:
|
||||
"""Create a ResourceFile wrapping the given byte stream.
|
||||
|
||||
To read resource file data from a bytes object, wrap it in an io.BytesIO.
|
||||
@ -273,8 +348,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._close_stream: bool = close
|
||||
self._stream: typing.io.BinaryIO
|
||||
self._close_stream = close
|
||||
if stream.seekable():
|
||||
self._stream = stream
|
||||
else:
|
||||
@ -298,7 +372,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
raise InvalidResourceFileError(f"Attempted to read {byte_count} bytes of data, but only got {len(data)} bytes")
|
||||
return data
|
||||
|
||||
def _stream_unpack(self, st: struct.Struct) -> typing.Tuple:
|
||||
def _stream_unpack(self, st: struct.Struct) -> tuple:
|
||||
"""Unpack data from the stream according to the struct st. The number of bytes to read is determined using st.size, so variable-sized structs cannot be used with this method."""
|
||||
|
||||
try:
|
||||
@ -306,17 +380,11 @@ class ResourceFile(collections.abc.Mapping):
|
||||
except struct.error as e:
|
||||
raise InvalidResourceFileError(str(e))
|
||||
|
||||
def _read_header(self):
|
||||
def _read_header(self) -> None:
|
||||
"""Read the resource file header, starting at the current stream position."""
|
||||
|
||||
assert self._stream.tell() == 0
|
||||
|
||||
self.data_offset: int
|
||||
self.map_offset: int
|
||||
self.data_length: int
|
||||
self.map_length: int
|
||||
self.header_system_data: bytes
|
||||
self.header_application_data: bytes
|
||||
(
|
||||
self.data_offset,
|
||||
self.map_offset,
|
||||
@ -329,25 +397,23 @@ class ResourceFile(collections.abc.Mapping):
|
||||
if self._stream.tell() != self.data_offset:
|
||||
raise InvalidResourceFileError(f"The data offset ({self.data_offset}) should point exactly to the end of the file header ({self._stream.tell()})")
|
||||
|
||||
def _read_map_header(self):
|
||||
def _read_map_header(self) -> None:
|
||||
"""Read the map header, starting at the current stream position."""
|
||||
|
||||
assert self._stream.tell() == self.map_offset
|
||||
|
||||
self.map_type_list_offset: int
|
||||
self.map_name_list_offset: int
|
||||
(
|
||||
_file_attributes,
|
||||
self.map_type_list_offset,
|
||||
self.map_name_list_offset,
|
||||
) = self._stream_unpack(STRUCT_RESOURCE_MAP_HEADER)
|
||||
|
||||
self.file_attributes: ResourceFileAttrs = ResourceFileAttrs(_file_attributes)
|
||||
self.file_attributes = ResourceFileAttrs(_file_attributes)
|
||||
|
||||
def _read_all_resource_types(self):
|
||||
def _read_all_resource_types(self) -> None:
|
||||
"""Read all resource types, starting at the current stream position."""
|
||||
|
||||
self._reference_counts: typing.MutableMapping[bytes, int] = collections.OrderedDict()
|
||||
self._reference_counts = collections.OrderedDict()
|
||||
|
||||
(type_list_length_m1,) = self._stream_unpack(STRUCT_RESOURCE_TYPE_LIST_HEADER)
|
||||
type_list_length = (type_list_length_m1 + 1) % 0x10000
|
||||
@ -361,10 +427,10 @@ class ResourceFile(collections.abc.Mapping):
|
||||
count = (count_m1 + 1) % 0x10000
|
||||
self._reference_counts[resource_type] = count
|
||||
|
||||
def _read_all_references(self):
|
||||
def _read_all_references(self) -> None:
|
||||
"""Read all resource references, starting at the current stream position."""
|
||||
|
||||
self._references: typing.MutableMapping[bytes, typing.MutableMapping[int, typing.Tuple[int, ResourceAttrs, int]]] = collections.OrderedDict()
|
||||
self._references = collections.OrderedDict()
|
||||
|
||||
for resource_type, count in self._reference_counts.items():
|
||||
resmap: typing.MutableMapping[int, typing.Tuple[int, ResourceAttrs, int]] = collections.OrderedDict()
|
||||
@ -381,7 +447,7 @@ class ResourceFile(collections.abc.Mapping):
|
||||
|
||||
resmap[resource_id] = (name_offset, ResourceAttrs(attributes), data_offset)
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Close this ResourceFile.
|
||||
|
||||
If close=True was passed when this ResourceFile was created, the underlying stream's close method is called as well.
|
||||
@ -390,23 +456,29 @@ class ResourceFile(collections.abc.Mapping):
|
||||
if self._close_stream:
|
||||
self._stream.close()
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
def __enter__(self) -> "ResourceFile":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: typing.Optional[typing.Type[BaseException]],
|
||||
exc_val: typing.Optional[BaseException],
|
||||
exc_tb: typing.Optional[types.TracebackType]
|
||||
) -> typing.Optional[bool]:
|
||||
self.close()
|
||||
return None
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
"""Get the number of resource types in this ResourceFile."""
|
||||
|
||||
return len(self._references)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[bytes]:
|
||||
"""Iterate over all resource types in this ResourceFile."""
|
||||
|
||||
return iter(self._references)
|
||||
|
||||
def __contains__(self, key: bytes):
|
||||
def __contains__(self, key: object) -> bool:
|
||||
"""Check whether this ResourceFile contains any resources of the given type."""
|
||||
|
||||
return key in self._references
|
||||
@ -416,5 +488,5 @@ class ResourceFile(collections.abc.Mapping):
|
||||
|
||||
return ResourceFile._LazyResourceMap(self, key)
|
||||
|
||||
def __repr__(self):
|
||||
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)}>"
|
||||
|
@ -1,97 +1,44 @@
|
||||
import struct
|
||||
|
||||
from . import dcmp0
|
||||
from . import dcmp1
|
||||
from . import dcmp2
|
||||
|
||||
from .common import DecompressError
|
||||
from .common import DecompressError, CompressedApplicationHeaderInfo, CompressedHeaderInfo, CompressedSystemHeaderInfo
|
||||
|
||||
__all__ = [
|
||||
"DecompressError",
|
||||
"decompress",
|
||||
]
|
||||
|
||||
# The signature of all compressed resource data, 0xa89f6572 in hex, or "®üer" in MacRoman.
|
||||
COMPRESSED_SIGNATURE = b"\xa8\x9fer"
|
||||
# The compression type commonly used for application resources.
|
||||
COMPRESSED_TYPE_APPLICATION = 0x0801
|
||||
# The compression type commonly used for System file resources.
|
||||
COMPRESSED_TYPE_SYSTEM = 0x0901
|
||||
|
||||
# Common header for compressed resources of all types.
|
||||
# 4 bytes: Signature (see above).
|
||||
# 2 bytes: Length of the complete header (this common part and the type-specific part that follows it). (This meaning is just a guess - the field's value is always 0x0012, so there's no way to know for certain what it means.)
|
||||
# 2 bytes: Compression type. Known so far: 0x0901 is used in the System file's resources. 0x0801 is used in other files' resources.
|
||||
# 4 bytes: Length of the data after decompression.
|
||||
STRUCT_COMPRESSED_HEADER = struct.Struct(">4sHHI")
|
||||
|
||||
# Header continuation part for an "application" compressed resource.
|
||||
# 1 byte: "Working buffer fractional size" - the ratio of the compressed data size to the uncompressed data size, times 256.
|
||||
# 1 byte: "Expansion buffer size" - the maximum number of bytes that the data might grow during decompression.
|
||||
# 2 bytes: The ID of the 'dcmp' resource that can decompress this resource. Currently only ID 0 is supported.
|
||||
# 2 bytes: Reserved (always zero).
|
||||
STRUCT_COMPRESSED_APPLICATION_HEADER = struct.Struct(">BBhH")
|
||||
|
||||
# Header continuation part for a "system" compressed resource.
|
||||
# 2 bytes: The ID of the 'dcmp' resource that can decompress this resource. Currently only ID 2 is supported.
|
||||
# 4 bytes: Decompressor-specific parameters.
|
||||
STRUCT_COMPRESSED_SYSTEM_HEADER = struct.Struct(">h4s")
|
||||
# Maps 'dcmp' IDs to their corresponding Python implementations.
|
||||
# Each decompressor has the signature (header_info: CompressedHeaderInfo, data: bytes, *, debug: bool=False) -> bytes.
|
||||
DECOMPRESSORS = {
|
||||
0: dcmp0.decompress,
|
||||
1: dcmp1.decompress,
|
||||
2: dcmp2.decompress,
|
||||
}
|
||||
|
||||
|
||||
def _decompress_application(data: bytes, decompressed_length: int, *, debug: bool=False) -> bytes:
|
||||
working_buffer_fractional_size, expansion_buffer_size, dcmp_id, reserved = STRUCT_COMPRESSED_APPLICATION_HEADER.unpack_from(data)
|
||||
def decompress_parsed(header_info: CompressedHeaderInfo, data: bytes, *, debug: bool=False) -> bytes:
|
||||
"""Decompress the given compressed resource data, whose header has already been removed and parsed into a CompressedHeaderInfo object."""
|
||||
|
||||
if debug:
|
||||
print(f"Working buffer fractional size: {working_buffer_fractional_size} (=> {len(data) * 256 / working_buffer_fractional_size})")
|
||||
print(f"Expansion buffer size: {expansion_buffer_size}")
|
||||
try:
|
||||
decompress_func = DECOMPRESSORS[header_info.dcmp_id]
|
||||
except KeyError:
|
||||
raise DecompressError(f"Unsupported 'dcmp' ID: {header_info.dcmp_id}")
|
||||
|
||||
if dcmp_id == 0:
|
||||
decompress_func = dcmp0.decompress
|
||||
elif dcmp_id == 1:
|
||||
decompress_func = dcmp1.decompress
|
||||
else:
|
||||
raise DecompressError(f"Unsupported 'dcmp' ID: {dcmp_id}, expected 0 or 1")
|
||||
|
||||
if reserved != 0:
|
||||
raise DecompressError(f"Reserved field should be 0, not 0x{reserved:>04x}")
|
||||
|
||||
return decompress_func(data[STRUCT_COMPRESSED_APPLICATION_HEADER.size:], decompressed_length, debug=debug)
|
||||
|
||||
|
||||
def _decompress_system(data: bytes, decompressed_length: int, *, debug: bool=False) -> bytes:
|
||||
dcmp_id, params = STRUCT_COMPRESSED_SYSTEM_HEADER.unpack_from(data)
|
||||
|
||||
if dcmp_id == 2:
|
||||
decompress_func = dcmp2.decompress
|
||||
else:
|
||||
raise DecompressError(f"Unsupported 'dcmp' ID: {dcmp_id}, expected 2")
|
||||
|
||||
return decompress_func(data[STRUCT_COMPRESSED_SYSTEM_HEADER.size:], decompressed_length, params, debug=debug)
|
||||
decompressed = decompress_func(header_info, data, debug=debug)
|
||||
if len(decompressed) != header_info.decompressed_length:
|
||||
raise DecompressError(f"Actual length of decompressed data ({len(decompressed)}) does not match length stored in resource ({header_info.decompressed_length})")
|
||||
return decompressed
|
||||
|
||||
|
||||
def decompress(data: bytes, *, debug: bool=False) -> bytes:
|
||||
"""Decompress the given compressed resource data."""
|
||||
|
||||
try:
|
||||
signature, header_length, compression_type, decompressed_length = STRUCT_COMPRESSED_HEADER.unpack_from(data)
|
||||
except struct.error:
|
||||
raise DecompressError(f"Invalid header")
|
||||
if signature != COMPRESSED_SIGNATURE:
|
||||
raise DecompressError(f"Invalid signature: {signature!r}, expected {COMPRESSED_SIGNATURE}")
|
||||
if header_length != 0x12:
|
||||
raise DecompressError(f"Unsupported header length: 0x{header_length:>04x}, expected 0x12")
|
||||
|
||||
if compression_type == COMPRESSED_TYPE_APPLICATION:
|
||||
decompress_func = _decompress_application
|
||||
elif compression_type == COMPRESSED_TYPE_SYSTEM:
|
||||
decompress_func = _decompress_system
|
||||
else:
|
||||
raise DecompressError(f"Unsupported compression type: 0x{compression_type:>04x}")
|
||||
header_info = CompressedHeaderInfo.parse(data)
|
||||
|
||||
if debug:
|
||||
print(f"Decompressed length: {decompressed_length}")
|
||||
print(f"Compressed resource data header: {header_info}")
|
||||
|
||||
decompressed = decompress_func(data[STRUCT_COMPRESSED_HEADER.size:], decompressed_length, debug=debug)
|
||||
if len(decompressed) != decompressed_length:
|
||||
raise DecompressError(f"Actual length of decompressed data ({len(decompressed)}) does not match length stored in resource ({decompressed_length})")
|
||||
return decompressed
|
||||
return decompress_parsed(header_info, data[header_info.header_length:], debug=debug)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import struct
|
||||
import typing
|
||||
|
||||
|
||||
@ -5,6 +6,100 @@ class DecompressError(Exception):
|
||||
"""Raised when resource data decompression fails, because the data is invalid or the compression type is not supported."""
|
||||
|
||||
|
||||
# The signature of all compressed resource data, 0xa89f6572 in hex, or "®üer" in MacRoman.
|
||||
COMPRESSED_SIGNATURE = b"\xa8\x9fer"
|
||||
# The compression type commonly used for application resources.
|
||||
COMPRESSED_TYPE_APPLICATION = 0x0801
|
||||
# The compression type commonly used for System file resources.
|
||||
COMPRESSED_TYPE_SYSTEM = 0x0901
|
||||
|
||||
# Common header for compressed resources of all types.
|
||||
# 4 bytes: Signature (see above).
|
||||
# 2 bytes: Length of the complete header (this common part and the type-specific part that follows it). (This meaning is just a guess - the field's value is always 0x0012, so there's no way to know for certain what it means.)
|
||||
# 2 bytes: Compression type. Known so far: 0x0901 is used in the System file's resources. 0x0801 is used in other files' resources.
|
||||
# 4 bytes: Length of the data after decompression.
|
||||
# 6 bytes: Remainder of the header. The exact format varies depending on the compression type.
|
||||
STRUCT_COMPRESSED_HEADER = struct.Struct(">4sHHI6s")
|
||||
|
||||
# Remainder of header for an "application" compressed resource.
|
||||
# 1 byte: "Working buffer fractional size" - the ratio of the compressed data size to the uncompressed data size, times 256.
|
||||
# 1 byte: "Expansion buffer size" - the maximum number of bytes that the data might grow during decompression.
|
||||
# 2 bytes: The ID of the 'dcmp' resource that can decompress this resource. Currently only ID 0 is supported.
|
||||
# 2 bytes: Reserved (always zero).
|
||||
STRUCT_COMPRESSED_APPLICATION_HEADER = struct.Struct(">BBhH")
|
||||
|
||||
# Remainder of header for a "system" compressed resource.
|
||||
# 2 bytes: The ID of the 'dcmp' resource that can decompress this resource. Currently only ID 2 is supported.
|
||||
# 4 bytes: Decompressor-specific parameters.
|
||||
STRUCT_COMPRESSED_SYSTEM_HEADER = struct.Struct(">h4s")
|
||||
|
||||
|
||||
class CompressedHeaderInfo(object):
|
||||
@classmethod
|
||||
def parse(cls, data: bytes) -> "CompressedHeaderInfo":
|
||||
try:
|
||||
signature, header_length, compression_type, decompressed_length, remainder = STRUCT_COMPRESSED_HEADER.unpack_from(data)
|
||||
except struct.error:
|
||||
raise DecompressError(f"Invalid header")
|
||||
if signature != COMPRESSED_SIGNATURE:
|
||||
raise DecompressError(f"Invalid signature: {signature!r}, expected {COMPRESSED_SIGNATURE}")
|
||||
if header_length != 0x12:
|
||||
raise DecompressError(f"Unsupported header length: 0x{header_length:>04x}, expected 0x12")
|
||||
|
||||
if compression_type == COMPRESSED_TYPE_APPLICATION:
|
||||
working_buffer_fractional_size, expansion_buffer_size, dcmp_id, reserved = STRUCT_COMPRESSED_APPLICATION_HEADER.unpack(remainder)
|
||||
|
||||
if reserved != 0:
|
||||
raise DecompressError(f"Reserved field should be 0, not 0x{reserved:>04x}")
|
||||
|
||||
return CompressedApplicationHeaderInfo(header_length, compression_type, decompressed_length, dcmp_id, working_buffer_fractional_size, expansion_buffer_size)
|
||||
elif compression_type == COMPRESSED_TYPE_SYSTEM:
|
||||
dcmp_id, parameters = STRUCT_COMPRESSED_SYSTEM_HEADER.unpack(remainder)
|
||||
|
||||
return CompressedSystemHeaderInfo(header_length, compression_type, decompressed_length, dcmp_id, parameters)
|
||||
else:
|
||||
raise DecompressError(f"Unsupported compression type: 0x{compression_type:>04x}")
|
||||
|
||||
header_length: int
|
||||
compression_type: int
|
||||
decompressed_length: int
|
||||
dcmp_id: int
|
||||
|
||||
def __init__(self, header_length: int, compression_type: int, decompressed_length: int, dcmp_id: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.header_length = header_length
|
||||
self.compression_type = compression_type
|
||||
self.decompressed_length = decompressed_length
|
||||
self.dcmp_id = dcmp_id
|
||||
|
||||
|
||||
class CompressedApplicationHeaderInfo(CompressedHeaderInfo):
|
||||
working_buffer_fractional_size: int
|
||||
expansion_buffer_size: int
|
||||
|
||||
def __init__(self, header_length: int, compression_type: int, decompressed_length: int, dcmp_id: int, working_buffer_fractional_size: int, expansion_buffer_size: int) -> None:
|
||||
super().__init__(header_length, compression_type, decompressed_length, dcmp_id)
|
||||
|
||||
self.working_buffer_fractional_size = working_buffer_fractional_size
|
||||
self.expansion_buffer_size = expansion_buffer_size
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__qualname__}(header_length={self.header_length}, compression_type=0x{self.compression_type:>04x}, decompressed_length={self.decompressed_length}, dcmp_id={self.dcmp_id}, working_buffer_fractional_size={self.working_buffer_fractional_size}, expansion_buffer_size={self.expansion_buffer_size})"
|
||||
|
||||
|
||||
class CompressedSystemHeaderInfo(CompressedHeaderInfo):
|
||||
parameters: bytes
|
||||
|
||||
def __init__(self, header_length: int, compression_type: int, decompressed_length: int, dcmp_id: int, parameters: bytes) -> None:
|
||||
super().__init__(header_length, compression_type, decompressed_length, dcmp_id)
|
||||
|
||||
self.parameters = parameters
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__qualname__}(header_length={self.header_length}, compression_type=0x{self.compression_type:>04x}, decompressed_length={self.decompressed_length}, dcmp_id={self.dcmp_id}, parameters={self.parameters!r})"
|
||||
|
||||
|
||||
def _read_variable_length_integer(data: bytes, position: int) -> typing.Tuple[int, int]:
|
||||
"""Read a variable-length integer starting at the given position in the data, and return the integer as well as the number of bytes consumed.
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import typing
|
||||
|
||||
from . import common
|
||||
|
||||
# Lookup table for codes in range(0x4b, 0xfe).
|
||||
@ -36,10 +38,13 @@ TABLE = [TABLE_DATA[i:i + 2] for i in range(0, len(TABLE_DATA), 2)]
|
||||
assert len(TABLE) == len(range(0x4b, 0xfe))
|
||||
|
||||
|
||||
def decompress(data: bytes, decompressed_length: int, *, debug: bool=False) -> bytes:
|
||||
def decompress(header_info: common.CompressedHeaderInfo, data: bytes, *, debug: bool=False) -> bytes:
|
||||
"""Decompress compressed data in the format used by 'dcmp' (0)."""
|
||||
|
||||
prev_literals = []
|
||||
if not isinstance(header_info, common.CompressedApplicationHeaderInfo):
|
||||
raise common.DecompressError(f"Incorrect header type: {type(header_info).__qualname__}")
|
||||
|
||||
prev_literals: typing.List[bytes] = []
|
||||
decompressed = b""
|
||||
|
||||
i = 0
|
||||
@ -287,7 +292,7 @@ def decompress(data: bytes, decompressed_length: int, *, debug: bool=False) -> b
|
||||
else:
|
||||
raise common.DecompressError(f"Unknown tag byte: 0x{data[i]:>02x}")
|
||||
|
||||
if decompressed_length % 2 != 0 and len(decompressed) == decompressed_length + 1:
|
||||
if header_info.decompressed_length % 2 != 0 and len(decompressed) == header_info.decompressed_length + 1:
|
||||
# Special case: if the decompressed data length stored in the header is odd and one less than the length of the actual decompressed data, drop the last byte.
|
||||
# This is necessary because nearly all codes generate data in groups of 2 or 4 bytes, so it is basically impossible to represent data with an odd length using this compression format.
|
||||
decompressed = decompressed[:-1]
|
||||
|
@ -1,3 +1,5 @@
|
||||
import typing
|
||||
|
||||
from . import common
|
||||
|
||||
# Lookup table for codes in range(0xd5, 0xfe).
|
||||
@ -19,10 +21,13 @@ TABLE = [TABLE_DATA[i:i + 2] for i in range(0, len(TABLE_DATA), 2)]
|
||||
assert len(TABLE) == len(range(0xd5, 0xfe))
|
||||
|
||||
|
||||
def decompress(data: bytes, decompressed_length: int, *, debug: bool=False) -> bytes:
|
||||
def decompress(header_info: common.CompressedHeaderInfo, data: bytes, *, debug: bool=False) -> bytes:
|
||||
"""Decompress compressed data in the format used by 'dcmp' (1)."""
|
||||
|
||||
prev_literals = []
|
||||
if not isinstance(header_info, common.CompressedApplicationHeaderInfo):
|
||||
raise common.DecompressError(f"Incorrect header type: {type(header_info).__qualname__}")
|
||||
|
||||
prev_literals: typing.List[bytes] = []
|
||||
decompressed = b""
|
||||
|
||||
i = 0
|
||||
|
@ -131,10 +131,13 @@ def _decompress_system_tagged(data: bytes, decompressed_length: int, table: typi
|
||||
return b"".join(parts)
|
||||
|
||||
|
||||
def decompress(data: bytes, decompressed_length: int, parameters: bytes, *, debug: bool=False) -> bytes:
|
||||
def decompress(header_info: common.CompressedHeaderInfo, data: bytes, *, debug: bool=False) -> bytes:
|
||||
"""Decompress compressed data in the format used by 'dcmp' (2)."""
|
||||
|
||||
unknown, table_count_m1, flags_raw = STRUCT_PARAMETERS.unpack(parameters)
|
||||
if not isinstance(header_info, common.CompressedSystemHeaderInfo):
|
||||
raise common.DecompressError(f"Incorrect header type: {type(header_info).__qualname__}")
|
||||
|
||||
unknown, table_count_m1, flags_raw = STRUCT_PARAMETERS.unpack(header_info.parameters)
|
||||
|
||||
if debug:
|
||||
print(f"Value of unknown parameter field: 0x{unknown:>04x}")
|
||||
@ -172,4 +175,4 @@ def decompress(data: bytes, decompressed_length: int, parameters: bytes, *, debu
|
||||
else:
|
||||
decompress_func = _decompress_system_untagged
|
||||
|
||||
return decompress_func(data[data_start:], decompressed_length, table, debug=debug)
|
||||
return decompress_func(data[data_start:], header_info.decompressed_length, table, debug=debug)
|
||||
|
21
setup.cfg
21
setup.cfg
@ -36,9 +36,28 @@ keywords =
|
||||
setup_requires =
|
||||
setuptools>=39.2.0
|
||||
python_requires = >=3.6
|
||||
packages =
|
||||
packages = find:
|
||||
|
||||
[options.packages.find]
|
||||
include =
|
||||
rsrcfork
|
||||
rsrcfork.*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
rsrcfork = rsrcfork.__main__:main
|
||||
|
||||
[mypy]
|
||||
files=rsrcfork/**/*.py
|
||||
python_version = 3.6
|
||||
|
||||
disallow_untyped_calls = True
|
||||
disallow_untyped_defs = True
|
||||
disallow_untyped_decorators = True
|
||||
|
||||
no_implicit_optional = True
|
||||
|
||||
warn_unused_ignores = True
|
||||
warn_unreachable = True
|
||||
|
||||
warn_redundant_casts = True
|
||||
|
Reference in New Issue
Block a user