xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/macRes.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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