Spaces:
Sleeping
Sleeping
| from io import BytesIO | |
| import struct | |
| from fontTools.misc import sstruct | |
| from fontTools.misc.textTools import bytesjoin, tostr | |
| from collections import OrderedDict | |
| from collections.abc import MutableMapping | |
| class ResourceError(Exception): | |
| pass | |
| class ResourceReader(MutableMapping): | |
| """Reader for Mac OS resource forks. | |
| Parses a resource fork and returns resources according to their type. | |
| If run on OS X, this will open the resource fork in the filesystem. | |
| Otherwise, it will open the file itself and attempt to read it as | |
| though it were a resource fork. | |
| The returned object can be indexed by type and iterated over, | |
| returning in each case a list of py:class:`Resource` objects | |
| representing all the resources of a certain type. | |
| """ | |
| def __init__(self, fileOrPath): | |
| """Open a file | |
| Args: | |
| fileOrPath: Either an object supporting a ``read`` method, an | |
| ``os.PathLike`` object, or a string. | |
| """ | |
| 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() | |
| 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 | |
| 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() | |
| def types(self): | |
| """A list of the types of resources in the resource fork.""" | |
| 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): | |
| """Returns a list of indices of resources of a given type.""" | |
| 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): | |
| """Represents a resource stored within a resource fork. | |
| Attributes: | |
| type: resource type. | |
| data: resource data. | |
| id: ID. | |
| name: resource name. | |
| attr: attributes. | |
| """ | |
| 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) | |