diff --git a/rsrcfork/api.py b/rsrcfork/api.py index d0a5a33..9ea6abe 100644 --- a/rsrcfork/api.py +++ b/rsrcfork/api.py @@ -96,7 +96,7 @@ class ResourceAttrs(enum.Flag): class Resource(object): """A single resource from a resource file.""" - __slots__ = ("type", "id", "name", "attributes", "data_raw", "_data_decompressed") + __slots__ = ("type", "id", "name", "attributes", "data_raw", "_compressed_info", "_data_decompressed") def __init__(self, resource_type: bytes, resource_id: int, name: typing.Optional[bytes], attributes: ResourceAttrs, data_raw: bytes): """Create a new resource with the given type code, ID, name, attributes, and data.""" @@ -138,6 +138,22 @@ 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 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 data(self) -> bytes: """The resource data, decompressed if necessary. @@ -149,7 +165,7 @@ class Resource(object): 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 diff --git a/rsrcfork/compress/__init__.py b/rsrcfork/compress/__init__.py index 5a9b610..3f971ef 100644 --- a/rsrcfork/compress/__init__.py +++ b/rsrcfork/compress/__init__.py @@ -19,6 +19,20 @@ DECOMPRESSORS = { } +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.""" + + try: + decompress_func = DECOMPRESSORS[header_info.dcmp_id] + except KeyError: + raise DecompressError(f"Unsupported 'dcmp' ID: {header_info.dcmp_id}") + + 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.""" @@ -27,12 +41,4 @@ def decompress(data: bytes, *, debug: bool=False) -> bytes: if debug: print(f"Compressed resource data header: {header_info}") - try: - decompress_func = DECOMPRESSORS[header_info.dcmp_id] - except KeyError: - raise DecompressError(f"Unsupported 'dcmp' ID: {header_info.dcmp_id}") - - decompressed = decompress_func(header_info, data[header_info.header_length:], 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 + return decompress_parsed(header_info, data[header_info.header_length:], debug=debug)