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