1"""sstruct.py -- SuperStruct
2
3Higher level layer on top of the struct module, enabling to
4bind names to struct elements. The interface is similar to
5struct, except the objects passed and returned are not tuples
6(or argument lists), but dictionaries or instances.
7
8Just like struct, we use fmt strings to describe a data
9structure, except we use one line per element. Lines are
10separated by newlines or semi-colons. Each line contains
11either one of the special struct characters ('@', '=', '<',
12'>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f').
13Repetitions, like the struct module offers them are not useful
14in this context, except for fixed length strings  (eg. 'myInt:5h'
15is not allowed but 'myString:5s' is). The 'x' fmt character
16(pad byte) is treated as 'special', since it is by definition
17anonymous. Extra whitespace is allowed everywhere.
18
19The sstruct module offers one feature that the "normal" struct
20module doesn't: support for fixed point numbers. These are spelled
21as "n.mF", where n is the number of bits before the point, and m
22the number of bits after the point. Fixed point numbers get
23converted to floats.
24
25pack(fmt, object):
26	'object' is either a dictionary or an instance (or actually
27	anything that has a __dict__ attribute). If it is a dictionary,
28	its keys are used for names. If it is an instance, it's
29	attributes are used to grab struct elements from. Returns
30	a string containing the data.
31
32unpack(fmt, data, object=None)
33	If 'object' is omitted (or None), a new dictionary will be
34	returned. If 'object' is a dictionary, it will be used to add
35	struct elements to. If it is an instance (or in fact anything
36	that has a __dict__ attribute), an attribute will be added for
37	each struct element. In the latter two cases, 'object' itself
38	is returned.
39
40unpack2(fmt, data, object=None)
41	Convenience function. Same as unpack, except data may be longer
42	than needed. The returned value is a tuple: (object, leftoverdata).
43
44calcsize(fmt)
45	like struct.calcsize(), but uses our own fmt strings:
46	it returns the size of the data in bytes.
47"""
48
49from __future__ import print_function, division, absolute_import
50from fontTools.misc.py23 import *
51from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
52import struct
53import re
54
55__version__ = "1.2"
56__copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>"
57
58
59class Error(Exception):
60	pass
61
62def pack(fmt, obj):
63	formatstring, names, fixes = getformat(fmt)
64	elements = []
65	if not isinstance(obj, dict):
66		obj = obj.__dict__
67	for name in names:
68		value = obj[name]
69		if name in fixes:
70			# fixed point conversion
71			value = fl2fi(value, fixes[name])
72		elif isinstance(value, basestring):
73			value = tobytes(value)
74		elements.append(value)
75	data = struct.pack(*(formatstring,) + tuple(elements))
76	return data
77
78def unpack(fmt, data, obj=None):
79	if obj is None:
80		obj = {}
81	data = tobytes(data)
82	formatstring, names, fixes = getformat(fmt)
83	if isinstance(obj, dict):
84		d = obj
85	else:
86		d = obj.__dict__
87	elements = struct.unpack(formatstring, data)
88	for i in range(len(names)):
89		name = names[i]
90		value = elements[i]
91		if name in fixes:
92			# fixed point conversion
93			value = fi2fl(value, fixes[name])
94		elif isinstance(value, bytes):
95			try:
96				value = tostr(value)
97			except UnicodeDecodeError:
98				pass
99		d[name] = value
100	return obj
101
102def unpack2(fmt, data, obj=None):
103	length = calcsize(fmt)
104	return unpack(fmt, data[:length], obj), data[length:]
105
106def calcsize(fmt):
107	formatstring, names, fixes = getformat(fmt)
108	return struct.calcsize(formatstring)
109
110
111# matches "name:formatchar" (whitespace is allowed)
112_elementRE = re.compile(
113		"\s*"							# whitespace
114		"([A-Za-z_][A-Za-z_0-9]*)"		# name (python identifier)
115		"\s*:\s*"						# whitespace : whitespace
116		"([cbBhHiIlLqQfd]|[0-9]+[ps]|"	# formatchar...
117			"([0-9]+)\.([0-9]+)(F))"	# ...formatchar
118		"\s*"							# whitespace
119		"(#.*)?$"						# [comment] + end of string
120	)
121
122# matches the special struct fmt chars and 'x' (pad byte)
123_extraRE = re.compile("\s*([x@=<>!])\s*(#.*)?$")
124
125# matches an "empty" string, possibly containing whitespace and/or a comment
126_emptyRE = re.compile("\s*(#.*)?$")
127
128_fixedpointmappings = {
129		8: "b",
130		16: "h",
131		32: "l"}
132
133_formatcache = {}
134
135def getformat(fmt):
136	try:
137		formatstring, names, fixes = _formatcache[fmt]
138	except KeyError:
139		lines = re.split("[\n;]", fmt)
140		formatstring = ""
141		names = []
142		fixes = {}
143		for line in lines:
144			if _emptyRE.match(line):
145				continue
146			m = _extraRE.match(line)
147			if m:
148				formatchar = m.group(1)
149				if formatchar != 'x' and formatstring:
150					raise Error("a special fmt char must be first")
151			else:
152				m = _elementRE.match(line)
153				if not m:
154					raise Error("syntax error in fmt: '%s'" % line)
155				name = m.group(1)
156				names.append(name)
157				formatchar = m.group(2)
158				if m.group(3):
159					# fixed point
160					before = int(m.group(3))
161					after = int(m.group(4))
162					bits = before + after
163					if bits not in [8, 16, 32]:
164						raise Error("fixed point must be 8, 16 or 32 bits long")
165					formatchar = _fixedpointmappings[bits]
166					assert m.group(5) == "F"
167					fixes[name] = after
168			formatstring = formatstring + formatchar
169		_formatcache[fmt] = formatstring, names, fixes
170	return formatstring, names, fixes
171
172def _test():
173	fmt = """
174		# comments are allowed
175		>  # big endian (see documentation for struct)
176		# empty lines are allowed:
177
178		ashort: h
179		along: l
180		abyte: b	# a byte
181		achar: c
182		astr: 5s
183		afloat: f; adouble: d	# multiple "statements" are allowed
184		afixed: 16.16F
185	"""
186
187	print('size:', calcsize(fmt))
188
189	class foo(object):
190		pass
191
192	i = foo()
193
194	i.ashort = 0x7fff
195	i.along = 0x7fffffff
196	i.abyte = 0x7f
197	i.achar = "a"
198	i.astr = "12345"
199	i.afloat = 0.5
200	i.adouble = 0.5
201	i.afixed = 1.5
202
203	data = pack(fmt, i)
204	print('data:', repr(data))
205	print(unpack(fmt, data))
206	i2 = foo()
207	unpack(fmt, data, i2)
208	print(vars(i2))
209
210if __name__ == "__main__":
211	_test()
212