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