""" Tools for reading Mac resource forks. """ from fontTools.misc.py23 import * import struct from fontTools.misc import sstruct from collections import OrderedDict try: from collections.abc import MutableMapping except ImportError: from UserDict import DictMixin as MutableMapping class ResourceError(Exception): pass class ResourceReader(MutableMapping): def __init__(self, fileOrPath): 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): 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): 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): 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)