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