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

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

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