262 lines
8.4 KiB
Python
262 lines
8.4 KiB
Python
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()
|
|
|
|
@staticmethod
|
|
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
|
|
|
|
@staticmethod
|
|
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()
|
|
|
|
@property
|
|
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)
|