1""" Tools for reading Mac resource forks. """ 2from __future__ import print_function, division, absolute_import 3from fontTools.misc.py23 import * 4import struct 5from fontTools.misc import sstruct 6from collections import OrderedDict 7try: 8 from collections.abc import MutableMapping 9except ImportError: 10 from UserDict import DictMixin as MutableMapping 11 12 13class ResourceError(Exception): 14 pass 15 16 17class ResourceReader(MutableMapping): 18 19 def __init__(self, fileOrPath): 20 self._resources = OrderedDict() 21 if hasattr(fileOrPath, 'read'): 22 self.file = fileOrPath 23 else: 24 try: 25 # try reading from the resource fork (only works on OS X) 26 self.file = self.openResourceFork(fileOrPath) 27 self._readFile() 28 return 29 except (ResourceError, IOError): 30 # if it fails, use the data fork 31 self.file = self.openDataFork(fileOrPath) 32 self._readFile() 33 34 @staticmethod 35 def openResourceFork(path): 36 if hasattr(path, "__fspath__"): # support os.PathLike objects 37 path = path.__fspath__() 38 with open(path + '/..namedfork/rsrc', 'rb') as resfork: 39 data = resfork.read() 40 infile = BytesIO(data) 41 infile.name = path 42 return infile 43 44 @staticmethod 45 def openDataFork(path): 46 with open(path, 'rb') as datafork: 47 data = datafork.read() 48 infile = BytesIO(data) 49 infile.name = path 50 return infile 51 52 def _readFile(self): 53 self._readHeaderAndMap() 54 self._readTypeList() 55 56 def _read(self, numBytes, offset=None): 57 if offset is not None: 58 try: 59 self.file.seek(offset) 60 except OverflowError: 61 raise ResourceError("Failed to seek offset ('offset' is too large)") 62 if self.file.tell() != offset: 63 raise ResourceError('Failed to seek offset (reached EOF)') 64 try: 65 data = self.file.read(numBytes) 66 except OverflowError: 67 raise ResourceError("Cannot read resource ('numBytes' is too large)") 68 if len(data) != numBytes: 69 raise ResourceError('Cannot read resource (not enough data)') 70 return data 71 72 def _readHeaderAndMap(self): 73 self.file.seek(0) 74 headerData = self._read(ResourceForkHeaderSize) 75 sstruct.unpack(ResourceForkHeader, headerData, self) 76 # seek to resource map, skip reserved 77 mapOffset = self.mapOffset + 22 78 resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) 79 sstruct.unpack(ResourceMapHeader, resourceMapData, self) 80 self.absTypeListOffset = self.mapOffset + self.typeListOffset 81 self.absNameListOffset = self.mapOffset + self.nameListOffset 82 83 def _readTypeList(self): 84 absTypeListOffset = self.absTypeListOffset 85 numTypesData = self._read(2, absTypeListOffset) 86 self.numTypes, = struct.unpack('>H', numTypesData) 87 absTypeListOffset2 = absTypeListOffset + 2 88 for i in range(self.numTypes + 1): 89 resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i 90 resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) 91 item = sstruct.unpack(ResourceTypeItem, resTypeItemData) 92 resType = tostr(item['type'], encoding='mac-roman') 93 refListOffset = absTypeListOffset + item['refListOffset'] 94 numRes = item['numRes'] + 1 95 resources = self._readReferenceList(resType, refListOffset, numRes) 96 self._resources[resType] = resources 97 98 def _readReferenceList(self, resType, refListOffset, numRes): 99 resources = [] 100 for i in range(numRes): 101 refOffset = refListOffset + ResourceRefItemSize * i 102 refData = self._read(ResourceRefItemSize, refOffset) 103 res = Resource(resType) 104 res.decompile(refData, self) 105 resources.append(res) 106 return resources 107 108 def __getitem__(self, resType): 109 return self._resources[resType] 110 111 def __delitem__(self, resType): 112 del self._resources[resType] 113 114 def __setitem__(self, resType, resources): 115 self._resources[resType] = resources 116 117 def __len__(self): 118 return len(self._resources) 119 120 def __iter__(self): 121 return iter(self._resources) 122 123 def keys(self): 124 return self._resources.keys() 125 126 @property 127 def types(self): 128 return list(self._resources.keys()) 129 130 def countResources(self, resType): 131 """Return the number of resources of a given type.""" 132 try: 133 return len(self[resType]) 134 except KeyError: 135 return 0 136 137 def getIndices(self, resType): 138 numRes = self.countResources(resType) 139 if numRes: 140 return list(range(1, numRes+1)) 141 else: 142 return [] 143 144 def getNames(self, resType): 145 """Return list of names of all resources of a given type.""" 146 return [res.name for res in self.get(resType, []) if res.name is not None] 147 148 def getIndResource(self, resType, index): 149 """Return resource of given type located at an index ranging from 1 150 to the number of resources for that type, or None if not found. 151 """ 152 if index < 1: 153 return None 154 try: 155 res = self[resType][index-1] 156 except (KeyError, IndexError): 157 return None 158 return res 159 160 def getNamedResource(self, resType, name): 161 """Return the named resource of given type, else return None.""" 162 name = tostr(name, encoding='mac-roman') 163 for res in self.get(resType, []): 164 if res.name == name: 165 return res 166 return None 167 168 def close(self): 169 if not self.file.closed: 170 self.file.close() 171 172 173class Resource(object): 174 175 def __init__(self, resType=None, resData=None, resID=None, resName=None, 176 resAttr=None): 177 self.type = resType 178 self.data = resData 179 self.id = resID 180 self.name = resName 181 self.attr = resAttr 182 183 def decompile(self, refData, reader): 184 sstruct.unpack(ResourceRefItem, refData, self) 185 # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct 186 self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset])) 187 absDataOffset = reader.dataOffset + self.dataOffset 188 dataLength, = struct.unpack(">L", reader._read(4, absDataOffset)) 189 self.data = reader._read(dataLength) 190 if self.nameOffset == -1: 191 return 192 absNameOffset = reader.absNameListOffset + self.nameOffset 193 nameLength, = struct.unpack('B', reader._read(1, absNameOffset)) 194 name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength)) 195 self.name = tostr(name, encoding='mac-roman') 196 197 198ResourceForkHeader = """ 199 > # big endian 200 dataOffset: L 201 mapOffset: L 202 dataLen: L 203 mapLen: L 204""" 205 206ResourceForkHeaderSize = sstruct.calcsize(ResourceForkHeader) 207 208ResourceMapHeader = """ 209 > # big endian 210 attr: H 211 typeListOffset: H 212 nameListOffset: H 213""" 214 215ResourceMapHeaderSize = sstruct.calcsize(ResourceMapHeader) 216 217ResourceTypeItem = """ 218 > # big endian 219 type: 4s 220 numRes: H 221 refListOffset: H 222""" 223 224ResourceTypeItemSize = sstruct.calcsize(ResourceTypeItem) 225 226ResourceRefItem = """ 227 > # big endian 228 id: h 229 nameOffset: h 230 attr: B 231 dataOffset: 3s 232 reserved: L 233""" 234 235ResourceRefItemSize = sstruct.calcsize(ResourceRefItem) 236