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	fmt = tostr(fmt, encoding="ascii")
137	try:
138		formatstring, names, fixes = _formatcache[fmt]
139	except KeyError:
140		lines = re.split("[\n;]", fmt)
141		formatstring = ""
142		names = []
143		fixes = {}
144		for line in lines:
145			if _emptyRE.match(line):
146				continue
147			m = _extraRE.match(line)
148			if m:
149				formatchar = m.group(1)
150				if formatchar != 'x' and formatstring:
151					raise Error("a special fmt char must be first")
152			else:
153				m = _elementRE.match(line)
154				if not m:
155					raise Error("syntax error in fmt: '%s'" % line)
156				name = m.group(1)
157				names.append(name)
158				formatchar = m.group(2)
159				if m.group(3):
160					# fixed point
161					before = int(m.group(3))
162					after = int(m.group(4))
163					bits = before + after
164					if bits not in [8, 16, 32]:
165						raise Error("fixed point must be 8, 16 or 32 bits long")
166					formatchar = _fixedpointmappings[bits]
167					assert m.group(5) == "F"
168					fixes[name] = after
169			formatstring = formatstring + formatchar
170		_formatcache[fmt] = formatstring, names, fixes
171	return formatstring, names, fixes
172
173def _test():
174	fmt = """
175		# comments are allowed
176		>  # big endian (see documentation for struct)
177		# empty lines are allowed:
178
179		ashort: h
180		along: l
181		abyte: b	# a byte
182		achar: c
183		astr: 5s
184		afloat: f; adouble: d	# multiple "statements" are allowed
185		afixed: 16.16F
186	"""
187
188	print('size:', calcsize(fmt))
189
190	class foo(object):
191		pass
192
193	i = foo()
194
195	i.ashort = 0x7fff
196	i.along = 0x7fffffff
197	i.abyte = 0x7f
198	i.achar = "a"
199	i.astr = "12345"
200	i.afloat = 0.5
201	i.adouble = 0.5
202	i.afixed = 1.5
203
204	data = pack(fmt, i)
205	print('data:', repr(data))
206	print(unpack(fmt, data))
207	i2 = foo()
208	unpack(fmt, data, i2)
209	print(vars(i2))
210
211if __name__ == "__main__":
212	_test()
213