• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
50from fontTools.misc.textTools import tobytes, tostr
51import struct
52import re
53
54__version__ = "1.2"
55__copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>"
56
57
58class Error(Exception):
59	pass
60
61def pack(fmt, obj):
62	formatstring, names, fixes = getformat(fmt, keep_pad_byte=True)
63	elements = []
64	if not isinstance(obj, dict):
65		obj = obj.__dict__
66	for name in names:
67		value = obj[name]
68		if name in fixes:
69			# fixed point conversion
70			value = fl2fi(value, fixes[name])
71		elif isinstance(value, str):
72			value = tobytes(value)
73		elements.append(value)
74	data = struct.pack(*(formatstring,) + tuple(elements))
75	return data
76
77def unpack(fmt, data, obj=None):
78	if obj is None:
79		obj = {}
80	data = tobytes(data)
81	formatstring, names, fixes = getformat(fmt)
82	if isinstance(obj, dict):
83		d = obj
84	else:
85		d = obj.__dict__
86	elements = struct.unpack(formatstring, data)
87	for i in range(len(names)):
88		name = names[i]
89		value = elements[i]
90		if name in fixes:
91			# fixed point conversion
92			value = fi2fl(value, fixes[name])
93		elif isinstance(value, bytes):
94			try:
95				value = tostr(value)
96			except UnicodeDecodeError:
97				pass
98		d[name] = value
99	return obj
100
101def unpack2(fmt, data, obj=None):
102	length = calcsize(fmt)
103	return unpack(fmt, data[:length], obj), data[length:]
104
105def calcsize(fmt):
106	formatstring, names, fixes = getformat(fmt)
107	return struct.calcsize(formatstring)
108
109
110# matches "name:formatchar" (whitespace is allowed)
111_elementRE = re.compile(
112		r"\s*"							# whitespace
113		r"([A-Za-z_][A-Za-z_0-9]*)"		# name (python identifier)
114		r"\s*:\s*"						# whitespace : whitespace
115		r"([xcbB?hHiIlLqQfd]|"			# formatchar...
116			r"[0-9]+[ps]|"				# ...formatchar...
117			r"([0-9]+)\.([0-9]+)(F))"	# ...formatchar
118		r"\s*"							# whitespace
119		r"(#.*)?$"						# [comment] + end of string
120	)
121
122# matches the special struct fmt chars and 'x' (pad byte)
123_extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$")
124
125# matches an "empty" string, possibly containing whitespace and/or a comment
126_emptyRE = re.compile(r"\s*(#.*)?$")
127
128_fixedpointmappings = {
129		8: "b",
130		16: "h",
131		32: "l"}
132
133_formatcache = {}
134
135def getformat(fmt, keep_pad_byte=False):
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				formatchar = m.group(2)
158				if keep_pad_byte or formatchar != "x":
159					names.append(name)
160				if m.group(3):
161					# fixed point
162					before = int(m.group(3))
163					after = int(m.group(4))
164					bits = before + after
165					if bits not in [8, 16, 32]:
166						raise Error("fixed point must be 8, 16 or 32 bits long")
167					formatchar = _fixedpointmappings[bits]
168					assert m.group(5) == "F"
169					fixes[name] = after
170			formatstring = formatstring + formatchar
171		_formatcache[fmt] = formatstring, names, fixes
172	return formatstring, names, fixes
173
174def _test():
175	fmt = """
176		# comments are allowed
177		>  # big endian (see documentation for struct)
178		# empty lines are allowed:
179
180		ashort: h
181		along: l
182		abyte: b	# a byte
183		achar: c
184		astr: 5s
185		afloat: f; adouble: d	# multiple "statements" are allowed
186		afixed: 16.16F
187		abool: ?
188		apad: x
189	"""
190
191	print('size:', calcsize(fmt))
192
193	class foo(object):
194		pass
195
196	i = foo()
197
198	i.ashort = 0x7fff
199	i.along = 0x7fffffff
200	i.abyte = 0x7f
201	i.achar = "a"
202	i.astr = "12345"
203	i.afloat = 0.5
204	i.adouble = 0.5
205	i.afixed = 1.5
206	i.abool = True
207
208	data = pack(fmt, i)
209	print('data:', repr(data))
210	print(unpack(fmt, data))
211	i2 = foo()
212	unpack(fmt, data, i2)
213	print(vars(i2))
214
215if __name__ == "__main__":
216	_test()
217