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