Home | History | Annotate | Download | only in misc
      1 """sstruct.py -- SuperStruct
      2 
      3 Higher level layer on top of the struct module, enabling to 
      4 bind names to struct elements. The interface is similar to 
      5 struct, except the objects passed and returned are not tuples 
      6 (or argument lists), but dictionaries or instances. 
      7 
      8 Just like struct, we use fmt strings to describe a data 
      9 structure, except we use one line per element. Lines are 
     10 separated by newlines or semi-colons. Each line contains 
     11 either one of the special struct characters ('@', '=', '<', 
     12 '>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f'). 
     13 Repetitions, like the struct module offers them are not useful 
     14 in this context, except for fixed length strings  (eg. 'myInt:5h' 
     15 is not allowed but 'myString:5s' is). The 'x' fmt character 
     16 (pad byte) is treated as 'special', since it is by definition 
     17 anonymous. Extra whitespace is allowed everywhere.
     18 
     19 The sstruct module offers one feature that the "normal" struct
     20 module doesn't: support for fixed point numbers. These are spelled
     21 as "n.mF", where n is the number of bits before the point, and m
     22 the number of bits after the point. Fixed point numbers get 
     23 converted to floats.
     24 
     25 pack(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 
     32 unpack(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 
     40 unpack2(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 
     44 calcsize(fmt)
     45 	like struct.calcsize(), but uses our own fmt strings:
     46 	it returns the size of the data in bytes.
     47 """
     48 
     49 from __future__ import print_function, division, absolute_import
     50 from fontTools.misc.py23 import *
     51 from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
     52 import struct
     53 import re
     54 
     55 __version__ = "1.2"
     56 __copyright__ = "Copyright 1998, Just van Rossum <just (at] letterror.com>"
     57 
     58 
     59 class Error(Exception):
     60 	pass
     61 
     62 def 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 
     78 def 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 
    102 def unpack2(fmt, data, obj=None):
    103 	length = calcsize(fmt)
    104 	return unpack(fmt, data[:length], obj), data[length:]
    105 
    106 def 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 
    135 def 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 
    172 def _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 
    210 if __name__ == "__main__":
    211 	_test()
    212