| """ Tools for reading Mac resource forks. """ |
| from __future__ import print_function, division, absolute_import |
| 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) |