|
from io import BytesIO |
|
import struct |
|
from fontTools.misc import sstruct |
|
from fontTools.misc.textTools import bytesjoin, tostr |
|
from collections import OrderedDict |
|
from collections.abc import MutableMapping |
|
|
|
|
|
class ResourceError(Exception): |
|
pass |
|
|
|
|
|
class ResourceReader(MutableMapping): |
|
"""Reader for Mac OS resource forks. |
|
|
|
Parses a resource fork and returns resources according to their type. |
|
If run on OS X, this will open the resource fork in the filesystem. |
|
Otherwise, it will open the file itself and attempt to read it as |
|
though it were a resource fork. |
|
|
|
The returned object can be indexed by type and iterated over, |
|
returning in each case a list of py:class:`Resource` objects |
|
representing all the resources of a certain type. |
|
|
|
""" |
|
|
|
def __init__(self, fileOrPath): |
|
"""Open a file |
|
|
|
Args: |
|
fileOrPath: Either an object supporting a ``read`` method, an |
|
``os.PathLike`` object, or a string. |
|
""" |
|
self._resources = OrderedDict() |
|
if hasattr(fileOrPath, "read"): |
|
self.file = fileOrPath |
|
else: |
|
try: |
|
|
|
self.file = self.openResourceFork(fileOrPath) |
|
self._readFile() |
|
return |
|
except (ResourceError, IOError): |
|
|
|
self.file = self.openDataFork(fileOrPath) |
|
self._readFile() |
|
|
|
@staticmethod |
|
def openResourceFork(path): |
|
if hasattr(path, "__fspath__"): |
|
path = path.__fspath__() |
|
with open(path + "/..namedfork/rsrc", "rb") as resfork: |
|
data = resfork.read() |
|
infile = BytesIO(data) |
|
infile.name = path |
|
return infile |
|
|
|
@staticmethod |
|
def openDataFork(path): |
|
with open(path, "rb") as datafork: |
|
data = datafork.read() |
|
infile = BytesIO(data) |
|
infile.name = path |
|
return infile |
|
|
|
def _readFile(self): |
|
self._readHeaderAndMap() |
|
self._readTypeList() |
|
|
|
def _read(self, numBytes, offset=None): |
|
if offset is not None: |
|
try: |
|
self.file.seek(offset) |
|
except OverflowError: |
|
raise ResourceError("Failed to seek offset ('offset' is too large)") |
|
if self.file.tell() != offset: |
|
raise ResourceError("Failed to seek offset (reached EOF)") |
|
try: |
|
data = self.file.read(numBytes) |
|
except OverflowError: |
|
raise ResourceError("Cannot read resource ('numBytes' is too large)") |
|
if len(data) != numBytes: |
|
raise ResourceError("Cannot read resource (not enough data)") |
|
return data |
|
|
|
def _readHeaderAndMap(self): |
|
self.file.seek(0) |
|
headerData = self._read(ResourceForkHeaderSize) |
|
sstruct.unpack(ResourceForkHeader, headerData, self) |
|
|
|
mapOffset = self.mapOffset + 22 |
|
resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) |
|
sstruct.unpack(ResourceMapHeader, resourceMapData, self) |
|
self.absTypeListOffset = self.mapOffset + self.typeListOffset |
|
self.absNameListOffset = self.mapOffset + self.nameListOffset |
|
|
|
def _readTypeList(self): |
|
absTypeListOffset = self.absTypeListOffset |
|
numTypesData = self._read(2, absTypeListOffset) |
|
(self.numTypes,) = struct.unpack(">H", numTypesData) |
|
absTypeListOffset2 = absTypeListOffset + 2 |
|
for i in range(self.numTypes + 1): |
|
resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i |
|
resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) |
|
item = sstruct.unpack(ResourceTypeItem, resTypeItemData) |
|
resType = tostr(item["type"], encoding="mac-roman") |
|
refListOffset = absTypeListOffset + item["refListOffset"] |
|
numRes = item["numRes"] + 1 |
|
resources = self._readReferenceList(resType, refListOffset, numRes) |
|
self._resources[resType] = resources |
|
|
|
def _readReferenceList(self, resType, refListOffset, numRes): |
|
resources = [] |
|
for i in range(numRes): |
|
refOffset = refListOffset + ResourceRefItemSize * i |
|
refData = self._read(ResourceRefItemSize, refOffset) |
|
res = Resource(resType) |
|
res.decompile(refData, self) |
|
resources.append(res) |
|
return resources |
|
|
|
def __getitem__(self, resType): |
|
return self._resources[resType] |
|
|
|
def __delitem__(self, resType): |
|
del self._resources[resType] |
|
|
|
def __setitem__(self, resType, resources): |
|
self._resources[resType] = resources |
|
|
|
def __len__(self): |
|
return len(self._resources) |
|
|
|
def __iter__(self): |
|
return iter(self._resources) |
|
|
|
def keys(self): |
|
return self._resources.keys() |
|
|
|
@property |
|
def types(self): |
|
"""A list of the types of resources in the resource fork.""" |
|
return list(self._resources.keys()) |
|
|
|
def countResources(self, resType): |
|
"""Return the number of resources of a given type.""" |
|
try: |
|
return len(self[resType]) |
|
except KeyError: |
|
return 0 |
|
|
|
def getIndices(self, resType): |
|
"""Returns a list of indices of resources of a given type.""" |
|
numRes = self.countResources(resType) |
|
if numRes: |
|
return list(range(1, numRes + 1)) |
|
else: |
|
return [] |
|
|
|
def getNames(self, resType): |
|
"""Return list of names of all resources of a given type.""" |
|
return [res.name for res in self.get(resType, []) if res.name is not None] |
|
|
|
def getIndResource(self, resType, index): |
|
"""Return resource of given type located at an index ranging from 1 |
|
to the number of resources for that type, or None if not found. |
|
""" |
|
if index < 1: |
|
return None |
|
try: |
|
res = self[resType][index - 1] |
|
except (KeyError, IndexError): |
|
return None |
|
return res |
|
|
|
def getNamedResource(self, resType, name): |
|
"""Return the named resource of given type, else return None.""" |
|
name = tostr(name, encoding="mac-roman") |
|
for res in self.get(resType, []): |
|
if res.name == name: |
|
return res |
|
return None |
|
|
|
def close(self): |
|
if not self.file.closed: |
|
self.file.close() |
|
|
|
|
|
class Resource(object): |
|
"""Represents a resource stored within a resource fork. |
|
|
|
Attributes: |
|
type: resource type. |
|
data: resource data. |
|
id: ID. |
|
name: resource name. |
|
attr: attributes. |
|
""" |
|
|
|
def __init__( |
|
self, resType=None, resData=None, resID=None, resName=None, resAttr=None |
|
): |
|
self.type = resType |
|
self.data = resData |
|
self.id = resID |
|
self.name = resName |
|
self.attr = resAttr |
|
|
|
def decompile(self, refData, reader): |
|
sstruct.unpack(ResourceRefItem, refData, self) |
|
|
|
(self.dataOffset,) = struct.unpack(">L", bytesjoin([b"\0", self.dataOffset])) |
|
absDataOffset = reader.dataOffset + self.dataOffset |
|
(dataLength,) = struct.unpack(">L", reader._read(4, absDataOffset)) |
|
self.data = reader._read(dataLength) |
|
if self.nameOffset == -1: |
|
return |
|
absNameOffset = reader.absNameListOffset + self.nameOffset |
|
(nameLength,) = struct.unpack("B", reader._read(1, absNameOffset)) |
|
(name,) = struct.unpack(">%ss" % nameLength, reader._read(nameLength)) |
|
self.name = tostr(name, encoding="mac-roman") |
|
|
|
|
|
ResourceForkHeader = """ |
|
> # big endian |
|
dataOffset: L |
|
mapOffset: L |
|
dataLen: L |
|
mapLen: L |
|
""" |
|
|
|
ResourceForkHeaderSize = sstruct.calcsize(ResourceForkHeader) |
|
|
|
ResourceMapHeader = """ |
|
> # big endian |
|
attr: H |
|
typeListOffset: H |
|
nameListOffset: H |
|
""" |
|
|
|
ResourceMapHeaderSize = sstruct.calcsize(ResourceMapHeader) |
|
|
|
ResourceTypeItem = """ |
|
> # big endian |
|
type: 4s |
|
numRes: H |
|
refListOffset: H |
|
""" |
|
|
|
ResourceTypeItemSize = sstruct.calcsize(ResourceTypeItem) |
|
|
|
ResourceRefItem = """ |
|
> # big endian |
|
id: h |
|
nameOffset: h |
|
attr: B |
|
dataOffset: 3s |
|
reserved: L |
|
""" |
|
|
|
ResourceRefItemSize = sstruct.calcsize(ResourceRefItem) |
|
|