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