xref: /aosp_15_r20/external/fonttools/Lib/fontTools/misc/sstruct.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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