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