123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- 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:
- # try reading from the resource fork (only works on OS X)
- self.file = self.openResourceFork(fileOrPath)
- self._readFile()
- return
- except (ResourceError, IOError):
- # if it fails, use the data fork
- self.file = self.openDataFork(fileOrPath)
- self._readFile()
- @staticmethod
- def openResourceFork(path):
- if hasattr(path, "__fspath__"): # support os.PathLike objects
- 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)
- # seek to resource map, skip reserved
- 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)
- # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct
- (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)
|