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