Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fontTools/misc/sstruct.py: 56%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

118 statements  

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()