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