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 <[email protected]>" 56 57 58class Error(Exception): 59 pass 60 61 62def pack(fmt, obj): 63 formatstring, names, fixes = getformat(fmt, keep_pad_byte=True) 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, str): 73 value = tobytes(value) 74 elements.append(value) 75 data = struct.pack(*(formatstring,) + tuple(elements)) 76 return data 77 78 79def unpack(fmt, data, obj=None): 80 if obj is None: 81 obj = {} 82 data = tobytes(data) 83 formatstring, names, fixes = getformat(fmt) 84 if isinstance(obj, dict): 85 d = obj 86 else: 87 d = obj.__dict__ 88 elements = struct.unpack(formatstring, data) 89 for i in range(len(names)): 90 name = names[i] 91 value = elements[i] 92 if name in fixes: 93 # fixed point conversion 94 value = fi2fl(value, fixes[name]) 95 elif isinstance(value, bytes): 96 try: 97 value = tostr(value) 98 except UnicodeDecodeError: 99 pass 100 d[name] = value 101 return obj 102 103 104def unpack2(fmt, data, obj=None): 105 length = calcsize(fmt) 106 return unpack(fmt, data[:length], obj), data[length:] 107 108 109def calcsize(fmt): 110 formatstring, names, fixes = getformat(fmt) 111 return struct.calcsize(formatstring) 112 113 114# matches "name:formatchar" (whitespace is allowed) 115_elementRE = re.compile( 116 r"\s*" # whitespace 117 r"([A-Za-z_][A-Za-z_0-9]*)" # name (python identifier) 118 r"\s*:\s*" # whitespace : whitespace 119 r"([xcbB?hHiIlLqQfd]|" # formatchar... 120 r"[0-9]+[ps]|" # ...formatchar... 121 r"([0-9]+)\.([0-9]+)(F))" # ...formatchar 122 r"\s*" # whitespace 123 r"(#.*)?$" # [comment] + end of string 124) 125 126# matches the special struct fmt chars and 'x' (pad byte) 127_extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$") 128 129# matches an "empty" string, possibly containing whitespace and/or a comment 130_emptyRE = re.compile(r"\s*(#.*)?$") 131 132_fixedpointmappings = {8: "b", 16: "h", 32: "l"} 133 134_formatcache = {} 135 136 137def getformat(fmt, keep_pad_byte=False): 138 fmt = tostr(fmt, encoding="ascii") 139 try: 140 formatstring, names, fixes = _formatcache[fmt] 141 except KeyError: 142 lines = re.split("[\n;]", fmt) 143 formatstring = "" 144 names = [] 145 fixes = {} 146 for line in lines: 147 if _emptyRE.match(line): 148 continue 149 m = _extraRE.match(line) 150 if m: 151 formatchar = m.group(1) 152 if formatchar != "x" and formatstring: 153 raise Error("a special fmt char must be first") 154 else: 155 m = _elementRE.match(line) 156 if not m: 157 raise Error("syntax error in fmt: '%s'" % line) 158 name = m.group(1) 159 formatchar = m.group(2) 160 if keep_pad_byte or formatchar != "x": 161 names.append(name) 162 if m.group(3): 163 # fixed point 164 before = int(m.group(3)) 165 after = int(m.group(4)) 166 bits = before + after 167 if bits not in [8, 16, 32]: 168 raise Error("fixed point must be 8, 16 or 32 bits long") 169 formatchar = _fixedpointmappings[bits] 170 assert m.group(5) == "F" 171 fixes[name] = after 172 formatstring = formatstring + formatchar 173 _formatcache[fmt] = formatstring, names, fixes 174 return formatstring, names, fixes 175 176 177def _test(): 178 fmt = """ 179 # comments are allowed 180 > # big endian (see documentation for struct) 181 # empty lines are allowed: 182 183 ashort: h 184 along: l 185 abyte: b # a byte 186 achar: c 187 astr: 5s 188 afloat: f; adouble: d # multiple "statements" are allowed 189 afixed: 16.16F 190 abool: ? 191 apad: x 192 """ 193 194 print("size:", calcsize(fmt)) 195 196 class foo(object): 197 pass 198 199 i = foo() 200 201 i.ashort = 0x7FFF 202 i.along = 0x7FFFFFFF 203 i.abyte = 0x7F 204 i.achar = "a" 205 i.astr = "12345" 206 i.afloat = 0.5 207 i.adouble = 0.5 208 i.afixed = 1.5 209 i.abool = True 210 211 data = pack(fmt, i) 212 print("data:", repr(data)) 213 print(unpack(fmt, data)) 214 i2 = foo() 215 unpack(fmt, data, i2) 216 print(vars(i2)) 217 218 219if __name__ == "__main__": 220 _test() 221