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