• 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.py23 import tobytes, tostr
50from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
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)
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"([cbBhHiIlLqQfd]|[0-9]+[ps]|"	# formatchar...
116			r"([0-9]+)\.([0-9]+)(F))"	# ...formatchar
117		r"\s*"							# whitespace
118		r"(#.*)?$"						# [comment] + end of string
119	)
120
121# matches the special struct fmt chars and 'x' (pad byte)
122_extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$")
123
124# matches an "empty" string, possibly containing whitespace and/or a comment
125_emptyRE = re.compile(r"\s*(#.*)?$")
126
127_fixedpointmappings = {
128		8: "b",
129		16: "h",
130		32: "l"}
131
132_formatcache = {}
133
134def getformat(fmt):
135	fmt = tostr(fmt, encoding="ascii")
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