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