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.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi 50from fontTools.misc.textTools import tobytes, tostr 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, keep_pad_byte=True) 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"([xcbB?hHiIlLqQfd]|" # formatchar... 116 r"[0-9]+[ps]|" # ...formatchar... 117 r"([0-9]+)\.([0-9]+)(F))" # ...formatchar 118 r"\s*" # whitespace 119 r"(#.*)?$" # [comment] + end of string 120 ) 121 122# matches the special struct fmt chars and 'x' (pad byte) 123_extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$") 124 125# matches an "empty" string, possibly containing whitespace and/or a comment 126_emptyRE = re.compile(r"\s*(#.*)?$") 127 128_fixedpointmappings = { 129 8: "b", 130 16: "h", 131 32: "l"} 132 133_formatcache = {} 134 135def getformat(fmt, keep_pad_byte=False): 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 formatchar = m.group(2) 158 if keep_pad_byte or formatchar != "x": 159 names.append(name) 160 if m.group(3): 161 # fixed point 162 before = int(m.group(3)) 163 after = int(m.group(4)) 164 bits = before + after 165 if bits not in [8, 16, 32]: 166 raise Error("fixed point must be 8, 16 or 32 bits long") 167 formatchar = _fixedpointmappings[bits] 168 assert m.group(5) == "F" 169 fixes[name] = after 170 formatstring = formatstring + formatchar 171 _formatcache[fmt] = formatstring, names, fixes 172 return formatstring, names, fixes 173 174def _test(): 175 fmt = """ 176 # comments are allowed 177 > # big endian (see documentation for struct) 178 # empty lines are allowed: 179 180 ashort: h 181 along: l 182 abyte: b # a byte 183 achar: c 184 astr: 5s 185 afloat: f; adouble: d # multiple "statements" are allowed 186 afixed: 16.16F 187 abool: ? 188 apad: x 189 """ 190 191 print('size:', calcsize(fmt)) 192 193 class foo(object): 194 pass 195 196 i = foo() 197 198 i.ashort = 0x7fff 199 i.along = 0x7fffffff 200 i.abyte = 0x7f 201 i.achar = "a" 202 i.astr = "12345" 203 i.afloat = 0.5 204 i.adouble = 0.5 205 i.afixed = 1.5 206 i.abool = True 207 208 data = pack(fmt, i) 209 print('data:', repr(data)) 210 print(unpack(fmt, data)) 211 i2 = foo() 212 unpack(fmt, data, i2) 213 print(vars(i2)) 214 215if __name__ == "__main__": 216 _test() 217