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