woff 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +24 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +13 -0
- data/README.md +25 -4
- data/Rakefile +5 -0
- data/bin/rake +17 -0
- data/lib/woff.rb +8 -1
- data/lib/woff/builder.rb +66 -15
- data/lib/woff/file.rb +152 -0
- data/lib/woff/version.rb +1 -1
- data/requirements.txt +1 -0
- data/spec/builder_spec.rb +49 -0
- data/spec/data/font-with-no-metadata.woff +0 -0
- data/spec/spec_helper.rb +7 -0
- data/woff.gemspec +9 -2
- data/woffTools/Lib/woffTools/__init__.py +1176 -0
- data/woffTools/Lib/woffTools/test/__init__.py +0 -0
- data/woffTools/Lib/woffTools/test/test_validate.py +2657 -0
- data/woffTools/Lib/woffTools/tools/__init__.py +0 -0
- data/woffTools/Lib/woffTools/tools/css.py +292 -0
- data/woffTools/Lib/woffTools/tools/info.py +296 -0
- data/woffTools/Lib/woffTools/tools/proof.py +210 -0
- data/woffTools/Lib/woffTools/tools/support.py +417 -0
- data/woffTools/Lib/woffTools/tools/validate.py +2504 -0
- data/woffTools/License.txt +21 -0
- data/woffTools/README.txt +31 -0
- data/woffTools/setup.py +35 -0
- data/woffTools/woff-all +28 -0
- data/woffTools/woff-css +5 -0
- data/woffTools/woff-info +5 -0
- data/woffTools/woff-proof +5 -0
- data/woffTools/woff-validate +5 -0
- metadata +94 -9
- data/lib/woff/data.rb +0 -44
@@ -0,0 +1,2504 @@
|
|
1
|
+
#! /usr/bin/env python
|
2
|
+
|
3
|
+
"""
|
4
|
+
A module for validating the the file structure of WOFF Files.
|
5
|
+
*validateFont* is the only public function.
|
6
|
+
|
7
|
+
This can also be used as a command line tool for validating WOFF files.
|
8
|
+
"""
|
9
|
+
|
10
|
+
# import
|
11
|
+
|
12
|
+
import os
|
13
|
+
import re
|
14
|
+
import time
|
15
|
+
import sys
|
16
|
+
import struct
|
17
|
+
import zlib
|
18
|
+
import optparse
|
19
|
+
import codecs
|
20
|
+
from cStringIO import StringIO
|
21
|
+
from xml.etree import ElementTree
|
22
|
+
from xml.parsers.expat import ExpatError
|
23
|
+
|
24
|
+
# ----------------------
|
25
|
+
# Support: Metadata Spec
|
26
|
+
# ----------------------
|
27
|
+
|
28
|
+
"""
|
29
|
+
The Extended Metadata specifications are defined as a set of
|
30
|
+
nested Python objects. This allows for a very simple XML
|
31
|
+
validation procedure. The common element structure is as follows:
|
32
|
+
|
33
|
+
{
|
34
|
+
# ----------
|
35
|
+
# Attributes
|
36
|
+
# ----------
|
37
|
+
|
38
|
+
# In all cases, the dictionary has the attribute name at the top
|
39
|
+
# with the possible value(s) as the value. If an attribute has
|
40
|
+
# more than one representation (for exmaple xml:lang and lang)
|
41
|
+
# the two are specified as a space separated string for example
|
42
|
+
# "xml:lang lang".
|
43
|
+
|
44
|
+
# Required
|
45
|
+
"requiredAttributes" : {
|
46
|
+
# empty or one or more of the following
|
47
|
+
"name" : "default as string, list of options or None"
|
48
|
+
},
|
49
|
+
|
50
|
+
# Recommended
|
51
|
+
"recommendedAttributes" : {
|
52
|
+
# empty or one or more of the following
|
53
|
+
"name" : "default as string, list of options or None"
|
54
|
+
},
|
55
|
+
|
56
|
+
# Optional
|
57
|
+
"optionalAttributes" : {
|
58
|
+
# empty or one or more of the following
|
59
|
+
"name" : "default as string, list of options or None"
|
60
|
+
},
|
61
|
+
|
62
|
+
# -------
|
63
|
+
# Content
|
64
|
+
# -------
|
65
|
+
|
66
|
+
"contentLevel" : "not allowed", "recommended" or "required",
|
67
|
+
|
68
|
+
# --------------
|
69
|
+
# Child Elements
|
70
|
+
# --------------
|
71
|
+
|
72
|
+
# In all cases, the dictionary has the element name at the top
|
73
|
+
# with a dictionary as the value. The value dictionary defines
|
74
|
+
# the number of times the shild-element may occur along with
|
75
|
+
# the specification for the child-element.
|
76
|
+
|
77
|
+
# Required
|
78
|
+
"requiredChildElements" : {
|
79
|
+
# empty or one or more of the following
|
80
|
+
"name" : {
|
81
|
+
"minimumOccurrences" : int or None,
|
82
|
+
"maximumOccurrences" : int or None,
|
83
|
+
"spec" : {}
|
84
|
+
}
|
85
|
+
},
|
86
|
+
|
87
|
+
# Recommended
|
88
|
+
"recommendedChildElements" : {
|
89
|
+
# empty or one or more of the following
|
90
|
+
"name" : {
|
91
|
+
# minimumOccurrences is implicitly 0
|
92
|
+
"maximumOccurrences" : int or None,
|
93
|
+
"spec" : {}
|
94
|
+
}
|
95
|
+
},
|
96
|
+
|
97
|
+
# Optional
|
98
|
+
"optionalChildElements" : {
|
99
|
+
# empty or one or more of the following
|
100
|
+
"name" : {
|
101
|
+
# minimumOccurrences is implicitly 0
|
102
|
+
"maximumOccurrences" : int or None,
|
103
|
+
"spec" : {}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
The recommendedAttributes and recommendedChildElements are optional
|
109
|
+
but they are separated from the optionalAttributes and optionalChildElements
|
110
|
+
to allow for more detailed reporting.
|
111
|
+
"""
|
112
|
+
|
113
|
+
# Metadata 1.0
|
114
|
+
# ------------
|
115
|
+
|
116
|
+
# Common Options
|
117
|
+
|
118
|
+
dirOptions_1_0 = ["ltr", "rtl"]
|
119
|
+
|
120
|
+
# Fourth-Level Elements
|
121
|
+
|
122
|
+
divSpec_1_0 = {
|
123
|
+
"requiredAttributes" : {},
|
124
|
+
"recommendedAttributes" : {},
|
125
|
+
"optionalAttributes" : {
|
126
|
+
"dir" : dirOptions_1_0,
|
127
|
+
"class" : None
|
128
|
+
},
|
129
|
+
"content" : "recommended",
|
130
|
+
"requiredChildElements" : {},
|
131
|
+
"recommendedChildElements" : {},
|
132
|
+
"optionalChildElements" : {
|
133
|
+
"div" : {
|
134
|
+
"maximumOccurrences" : None,
|
135
|
+
"spec" : "recursive divSpec_1_0" # special override for recursion.
|
136
|
+
},
|
137
|
+
"span" : {
|
138
|
+
"maximumOccurrences" : None,
|
139
|
+
"spec" : "recursive spanSpec_1_0" # special override for recursion.
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
spanSpec_1_0 = {
|
145
|
+
"requiredAttributes" : {},
|
146
|
+
"recommendedAttributes" : {},
|
147
|
+
"optionalAttributes" : {
|
148
|
+
"dir" : dirOptions_1_0,
|
149
|
+
"class" : None
|
150
|
+
},
|
151
|
+
"content" : "recommended",
|
152
|
+
"requiredChildElements" : {},
|
153
|
+
"recommendedChildElements" : {},
|
154
|
+
"optionalChildElements" : {
|
155
|
+
"div" : {
|
156
|
+
"maximumOccurrences" : None,
|
157
|
+
"spec" : "recursive divSpec_1_0" # special override for recursion.
|
158
|
+
},
|
159
|
+
"span" : {
|
160
|
+
"maximumOccurrences" : None,
|
161
|
+
"spec" : "recursive spanSpec_1_0" # special override for recursion.
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
# Third-Level Elements
|
167
|
+
|
168
|
+
creditSpec_1_0 = {
|
169
|
+
"requiredAttributes" : {
|
170
|
+
"name" : None
|
171
|
+
},
|
172
|
+
"recommendedAttributes" : {},
|
173
|
+
"optionalAttributes" : {
|
174
|
+
"url" : None,
|
175
|
+
"role" : None,
|
176
|
+
"dir" : dirOptions_1_0,
|
177
|
+
"class" : None
|
178
|
+
},
|
179
|
+
"content" : "not allowed",
|
180
|
+
"requiredChildElements" : {},
|
181
|
+
"recommendedChildElements" : {},
|
182
|
+
"optionalChildElements" : {}
|
183
|
+
}
|
184
|
+
|
185
|
+
textSpec_1_0 = {
|
186
|
+
"requiredAttributes" : {},
|
187
|
+
"recommendedAttributes" : {},
|
188
|
+
"optionalAttributes" : {
|
189
|
+
"url" : None,
|
190
|
+
"role" : None,
|
191
|
+
"dir" : dirOptions_1_0,
|
192
|
+
"class" : None,
|
193
|
+
"xml:lang lang" : None
|
194
|
+
},
|
195
|
+
"content" : "recommended",
|
196
|
+
"requiredChildElements" : {},
|
197
|
+
"recommendedChildElements" : {},
|
198
|
+
"optionalChildElements" : {
|
199
|
+
"div" : {
|
200
|
+
"maximumOccurrences" : None,
|
201
|
+
"spec" : divSpec_1_0
|
202
|
+
},
|
203
|
+
"span" : {
|
204
|
+
"maximumOccurrences" : None,
|
205
|
+
"spec" : spanSpec_1_0
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
extensionNameSpec_1_0 = {
|
211
|
+
"requiredAttributes" : {},
|
212
|
+
"recommendedAttributes" : {},
|
213
|
+
"optionalAttributes" : {
|
214
|
+
"dir" : dirOptions_1_0,
|
215
|
+
"class" : None,
|
216
|
+
"xml:lang lang" : None
|
217
|
+
},
|
218
|
+
"content" : "recommended",
|
219
|
+
"requiredChildElements" : {},
|
220
|
+
"recommendedChildElements" : {},
|
221
|
+
"optionalChildElements" : {}
|
222
|
+
}
|
223
|
+
|
224
|
+
extensionValueSpec_1_0 = {
|
225
|
+
"requiredAttributes" : {},
|
226
|
+
"recommendedAttributes" : {},
|
227
|
+
"optionalAttributes" : {
|
228
|
+
"dir" : dirOptions_1_0,
|
229
|
+
"class" : None,
|
230
|
+
"xml:lang lang" : None
|
231
|
+
},
|
232
|
+
"content" : "recommended",
|
233
|
+
"requiredChildElements" : {},
|
234
|
+
"recommendedChildElements" : {},
|
235
|
+
"optionalChildElements" : {}
|
236
|
+
}
|
237
|
+
|
238
|
+
extensionItemSpec_1_0 = {
|
239
|
+
"requiredAttributes" : {},
|
240
|
+
"recommendedAttributes" : {},
|
241
|
+
"optionalAttributes" : {
|
242
|
+
"id" : None
|
243
|
+
},
|
244
|
+
"content" : "not allowed",
|
245
|
+
"requiredChildElements" : {
|
246
|
+
"name" : {
|
247
|
+
"minimumOccurrences" : 1,
|
248
|
+
"maximumOccurrences" : None,
|
249
|
+
"spec" : extensionNameSpec_1_0
|
250
|
+
},
|
251
|
+
"value" : {
|
252
|
+
"minimumOccurrences" : 1,
|
253
|
+
"maximumOccurrences" : None,
|
254
|
+
"spec" : extensionValueSpec_1_0
|
255
|
+
}
|
256
|
+
},
|
257
|
+
"recommendedChildElements" : {
|
258
|
+
},
|
259
|
+
"optionalChildElements" : {}
|
260
|
+
}
|
261
|
+
|
262
|
+
# Second Level Elements
|
263
|
+
|
264
|
+
uniqueidSpec_1_0 = {
|
265
|
+
"requiredAttributes" : {
|
266
|
+
"id" : None
|
267
|
+
},
|
268
|
+
"recommendedAttributes" : {},
|
269
|
+
"optionalAttributes" : {},
|
270
|
+
"content" : "not allowed",
|
271
|
+
"requiredChildElements" : {},
|
272
|
+
"recommendedChildElements" : {},
|
273
|
+
"optionalChildElements" : {}
|
274
|
+
}
|
275
|
+
|
276
|
+
vendorSpec_1_0 = {
|
277
|
+
"requiredAttributes" : {
|
278
|
+
"name" : None
|
279
|
+
},
|
280
|
+
"recommendedAttributes" : {},
|
281
|
+
"optionalAttributes" : {
|
282
|
+
"url" : None,
|
283
|
+
"dir" : dirOptions_1_0,
|
284
|
+
"class" : None
|
285
|
+
},
|
286
|
+
"content" : "not allowed",
|
287
|
+
"requiredChildElements" : {},
|
288
|
+
"recommendedChildElements" : {},
|
289
|
+
"optionalChildElements" : {}
|
290
|
+
}
|
291
|
+
|
292
|
+
creditsSpec_1_0 = {
|
293
|
+
"requiredAttributes" : {},
|
294
|
+
"recommendedAttributes" : {},
|
295
|
+
"optionalAttributes" : {},
|
296
|
+
"content" : "not allowed",
|
297
|
+
"requiredChildElements" : {
|
298
|
+
"credit" : {
|
299
|
+
"minimumOccurrences" : 1,
|
300
|
+
"maximumOccurrences" : None,
|
301
|
+
"spec" : creditSpec_1_0
|
302
|
+
}
|
303
|
+
},
|
304
|
+
"recommendedChildElements" : {},
|
305
|
+
"optionalChildElements" : {}
|
306
|
+
}
|
307
|
+
|
308
|
+
descriptionSpec_1_0 = {
|
309
|
+
"requiredAttributes" : {},
|
310
|
+
"recommendedAttributes" : {},
|
311
|
+
"optionalAttributes" : {
|
312
|
+
"url" : None,
|
313
|
+
},
|
314
|
+
"content" : "not allowed",
|
315
|
+
"requiredChildElements" : {
|
316
|
+
"text" : {
|
317
|
+
"minimumOccurrences" : 1,
|
318
|
+
"maximumOccurrences" : None,
|
319
|
+
"spec" : textSpec_1_0
|
320
|
+
}
|
321
|
+
},
|
322
|
+
"recommendedChildElements" : {},
|
323
|
+
"optionalChildElements" : {}
|
324
|
+
}
|
325
|
+
|
326
|
+
licenseSpec_1_0 = {
|
327
|
+
"requiredAttributes" : {},
|
328
|
+
"recommendedAttributes" : {},
|
329
|
+
"optionalAttributes" : {
|
330
|
+
"url" : None,
|
331
|
+
"id" : None
|
332
|
+
},
|
333
|
+
"content" : "not allowed",
|
334
|
+
"requiredChildElements" : {},
|
335
|
+
"recommendedChildElements" : {},
|
336
|
+
"optionalChildElements" : {
|
337
|
+
"text" : {
|
338
|
+
"maximumOccurrences" : None,
|
339
|
+
"spec" : textSpec_1_0
|
340
|
+
}
|
341
|
+
}
|
342
|
+
}
|
343
|
+
|
344
|
+
copyrightSpec_1_0 = {
|
345
|
+
"requiredAttributes" : {},
|
346
|
+
"recommendedAttributes" : {},
|
347
|
+
"optionalAttributes" : {},
|
348
|
+
"content" : "not allowed",
|
349
|
+
"requiredChildElements" : {
|
350
|
+
"text" : {
|
351
|
+
"minimumOccurrences" : 1,
|
352
|
+
"maximumOccurrences" : None,
|
353
|
+
"spec" : textSpec_1_0
|
354
|
+
}
|
355
|
+
},
|
356
|
+
"recommendedChildElements" : {},
|
357
|
+
"optionalChildElements" : {}
|
358
|
+
}
|
359
|
+
|
360
|
+
trademarkSpec_1_0 = {
|
361
|
+
"requiredAttributes" : {},
|
362
|
+
"recommendedAttributes" : {},
|
363
|
+
"optionalAttributes" : {},
|
364
|
+
"content" : "not allowed",
|
365
|
+
"requiredChildElements" : {
|
366
|
+
"text" : {
|
367
|
+
"minimumOccurrences" : 1,
|
368
|
+
"maximumOccurrences" : None,
|
369
|
+
"spec" : textSpec_1_0
|
370
|
+
}
|
371
|
+
},
|
372
|
+
"recommendedChildElements" : {},
|
373
|
+
"optionalChildElements" : {}
|
374
|
+
}
|
375
|
+
|
376
|
+
licenseeSpec_1_0 = {
|
377
|
+
"requiredAttributes" : {
|
378
|
+
"name" : None,
|
379
|
+
},
|
380
|
+
"recommendedAttributes" : {},
|
381
|
+
"optionalAttributes" : {
|
382
|
+
"dir" : dirOptions_1_0,
|
383
|
+
"class" : None
|
384
|
+
},
|
385
|
+
"content" : "not allowed",
|
386
|
+
"requiredChildElements" : {},
|
387
|
+
"recommendedChildElements" : {},
|
388
|
+
"optionalChildElements" : {}
|
389
|
+
}
|
390
|
+
|
391
|
+
extensionSpec_1_0 = {
|
392
|
+
"requiredAttributes" : {},
|
393
|
+
"recommendedAttributes" : {},
|
394
|
+
"optionalAttributes" : {
|
395
|
+
"id" : None
|
396
|
+
},
|
397
|
+
"content" : "not allowed",
|
398
|
+
"requiredChildElements" : {
|
399
|
+
"item" : {
|
400
|
+
"minimumOccurrences" : 1,
|
401
|
+
"maximumOccurrences" : None,
|
402
|
+
"spec" : extensionItemSpec_1_0
|
403
|
+
}
|
404
|
+
},
|
405
|
+
"recommendedChildElements" : {},
|
406
|
+
"optionalChildElements" : {
|
407
|
+
"name" : {
|
408
|
+
"maximumOccurrences" : None,
|
409
|
+
"spec" : extensionNameSpec_1_0
|
410
|
+
}
|
411
|
+
}
|
412
|
+
}
|
413
|
+
|
414
|
+
# First Level Elements
|
415
|
+
|
416
|
+
metadataSpec_1_0 = {
|
417
|
+
"requiredAttributes" : {
|
418
|
+
"version" : "1.0"
|
419
|
+
},
|
420
|
+
"recommendedAttributes" : {},
|
421
|
+
"optionalAttributes" : {},
|
422
|
+
"content" : "not allowed",
|
423
|
+
"requiredChildElements" : {},
|
424
|
+
"recommendedChildElements" : {
|
425
|
+
"uniqueid" : {
|
426
|
+
"maximumOccurrences" : 1,
|
427
|
+
"spec" : uniqueidSpec_1_0
|
428
|
+
}
|
429
|
+
},
|
430
|
+
"optionalChildElements" : {
|
431
|
+
"vendor" : {
|
432
|
+
"maximumOccurrences" : 1,
|
433
|
+
"spec" : vendorSpec_1_0
|
434
|
+
},
|
435
|
+
"credits" : {
|
436
|
+
"maximumOccurrences" : 1,
|
437
|
+
"spec" : creditsSpec_1_0
|
438
|
+
},
|
439
|
+
"description" : {
|
440
|
+
"maximumOccurrences" : 1,
|
441
|
+
"spec" : descriptionSpec_1_0
|
442
|
+
},
|
443
|
+
"license" : {
|
444
|
+
"maximumOccurrences" : 1,
|
445
|
+
"spec" : licenseSpec_1_0
|
446
|
+
},
|
447
|
+
"copyright" : {
|
448
|
+
"maximumOccurrences" : 1,
|
449
|
+
"spec" : copyrightSpec_1_0
|
450
|
+
},
|
451
|
+
"trademark" : {
|
452
|
+
"maximumOccurrences" : 1,
|
453
|
+
"spec" : trademarkSpec_1_0
|
454
|
+
},
|
455
|
+
"licensee" : {
|
456
|
+
"maximumOccurrences" : 1,
|
457
|
+
"spec" : licenseeSpec_1_0
|
458
|
+
},
|
459
|
+
"licensee" : {
|
460
|
+
"maximumOccurrences" : 1,
|
461
|
+
"spec" : licenseeSpec_1_0
|
462
|
+
},
|
463
|
+
"extension" : {
|
464
|
+
"maximumOccurrences" : None,
|
465
|
+
"spec" : extensionSpec_1_0
|
466
|
+
}
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
# ----------------------
|
471
|
+
# Support: struct Helper
|
472
|
+
# ----------------------
|
473
|
+
|
474
|
+
# This was inspired by Just van Rossum's sstruct module.
|
475
|
+
# http://fonttools.svn.sourceforge.net/svnroot/fonttools/trunk/Lib/sstruct.py
|
476
|
+
|
477
|
+
def structPack(format, obj):
|
478
|
+
keys, formatString = _structGetFormat(format)
|
479
|
+
values = []
|
480
|
+
for key in keys:
|
481
|
+
values.append(obj[key])
|
482
|
+
data = struct.pack(formatString, *values)
|
483
|
+
return data
|
484
|
+
|
485
|
+
def structUnpack(format, data):
|
486
|
+
keys, formatString = _structGetFormat(format)
|
487
|
+
size = struct.calcsize(formatString)
|
488
|
+
values = struct.unpack(formatString, data[:size])
|
489
|
+
unpacked = {}
|
490
|
+
for index, key in enumerate(keys):
|
491
|
+
value = values[index]
|
492
|
+
unpacked[key] = value
|
493
|
+
return unpacked, data[size:]
|
494
|
+
|
495
|
+
def structCalcSize(format):
|
496
|
+
keys, formatString = _structGetFormat(format)
|
497
|
+
return struct.calcsize(formatString)
|
498
|
+
|
499
|
+
_structFormatCache = {}
|
500
|
+
|
501
|
+
def _structGetFormat(format):
|
502
|
+
if format not in _structFormatCache:
|
503
|
+
keys = []
|
504
|
+
formatString = [">"] # always big endian
|
505
|
+
for line in format.strip().splitlines():
|
506
|
+
line = line.split("#", 1)[0].strip()
|
507
|
+
if not line:
|
508
|
+
continue
|
509
|
+
key, formatCharacter = line.split(":")
|
510
|
+
key = key.strip()
|
511
|
+
formatCharacter = formatCharacter.strip()
|
512
|
+
keys.append(key)
|
513
|
+
formatString.append(formatCharacter)
|
514
|
+
_structFormatCache[format] = (keys, "".join(formatString))
|
515
|
+
return _structFormatCache[format]
|
516
|
+
|
517
|
+
# -------------
|
518
|
+
# Tests: Header
|
519
|
+
# -------------
|
520
|
+
|
521
|
+
def testHeader(data, reporter):
|
522
|
+
"""
|
523
|
+
Test the WOFF header.
|
524
|
+
"""
|
525
|
+
functions = [
|
526
|
+
_testHeaderSignature,
|
527
|
+
_testHeaderFlavor,
|
528
|
+
_testHeaderLength,
|
529
|
+
_testHeaderReserved,
|
530
|
+
_testHeaderTotalSFNTSize,
|
531
|
+
_testHeaderNumTables
|
532
|
+
]
|
533
|
+
for function in functions:
|
534
|
+
shouldStop = function(data, reporter)
|
535
|
+
if shouldStop:
|
536
|
+
return True
|
537
|
+
return False
|
538
|
+
|
539
|
+
|
540
|
+
headerFormat = """
|
541
|
+
signature: 4s
|
542
|
+
flavor: 4s
|
543
|
+
length: L
|
544
|
+
numTables: H
|
545
|
+
reserved: H
|
546
|
+
totalSfntSize: L
|
547
|
+
majorVersion: H
|
548
|
+
minorVersion: H
|
549
|
+
metaOffset: L
|
550
|
+
metaLength: L
|
551
|
+
metaOrigLength: L
|
552
|
+
privOffset: L
|
553
|
+
privLength: L
|
554
|
+
"""
|
555
|
+
headerSize = structCalcSize(headerFormat)
|
556
|
+
|
557
|
+
def _testHeaderStructure(data, reporter):
|
558
|
+
"""
|
559
|
+
Tests:
|
560
|
+
- Header must be the proper structure.
|
561
|
+
"""
|
562
|
+
try:
|
563
|
+
structUnpack(headerFormat, data)
|
564
|
+
reporter.logPass(message="The header structure is correct.")
|
565
|
+
except:
|
566
|
+
reporter.logError(message="The header is not properly structured.")
|
567
|
+
return True
|
568
|
+
|
569
|
+
def _testHeaderSignature(data, reporter):
|
570
|
+
"""
|
571
|
+
Tests:
|
572
|
+
- The signature must be "wOFF".
|
573
|
+
"""
|
574
|
+
header = unpackHeader(data)
|
575
|
+
signature = header["signature"]
|
576
|
+
if signature != "wOFF":
|
577
|
+
reporter.logError(message="Invalid signature: %s." % signature)
|
578
|
+
return True
|
579
|
+
else:
|
580
|
+
reporter.logPass(message="The signature is correct.")
|
581
|
+
|
582
|
+
def _testHeaderFlavor(data, reporter):
|
583
|
+
"""
|
584
|
+
Tests:
|
585
|
+
- The flavor should be OTTO, 0x00010000 or true. Warn if another value is found.
|
586
|
+
- If the flavor is OTTO, the CFF table must be present.
|
587
|
+
- If the flavor is not OTTO, the CFF must not be present.
|
588
|
+
- If the directory cannot be unpacked, the flavor can not be validated. Issue a warning.
|
589
|
+
"""
|
590
|
+
header = unpackHeader(data)
|
591
|
+
flavor = header["flavor"]
|
592
|
+
if flavor not in ("OTTO", "\000\001\000\000", "true"):
|
593
|
+
reporter.logWarning(message="Unknown flavor: %s." % flavor)
|
594
|
+
else:
|
595
|
+
try:
|
596
|
+
tags = [table["tag"] for table in unpackDirectory(data)]
|
597
|
+
if "CFF " in tags and flavor != "OTTO":
|
598
|
+
reporter.logError(message="A \"CFF\" table is defined in the font and the flavor is not set to \"OTTO\".")
|
599
|
+
elif "CFF " not in tags and flavor == "OTTO":
|
600
|
+
reporter.logError(message="The flavor is set to \"OTTO\" but no \"CFF\" table is defined.")
|
601
|
+
else:
|
602
|
+
reporter.logPass(message="The flavor is a correct value.")
|
603
|
+
except:
|
604
|
+
reporter.logWarning(message="Could not validate the flavor.")
|
605
|
+
|
606
|
+
def _testHeaderLength(data, reporter):
|
607
|
+
"""
|
608
|
+
Tests:
|
609
|
+
- The length of the data must match the defined length.
|
610
|
+
- The length of the data must be long enough for header and directory for defined number of tables.
|
611
|
+
- The length of the data must be long enough to contain the table lengths defined in the directory,
|
612
|
+
the metaLength and the privLength.
|
613
|
+
"""
|
614
|
+
header = unpackHeader(data)
|
615
|
+
length = header["length"]
|
616
|
+
numTables = header["numTables"]
|
617
|
+
minLength = headerSize + (directorySize * numTables)
|
618
|
+
if length != len(data):
|
619
|
+
reporter.logError(message="Defined length (%d) does not match actual length of the data (%d)." % (length, len(data)))
|
620
|
+
return
|
621
|
+
if length < minLength:
|
622
|
+
reporter.logError(message="Invalid length defined (%d) for number of tables defined." % length)
|
623
|
+
return
|
624
|
+
directory = unpackDirectory(data)
|
625
|
+
for entry in directory:
|
626
|
+
compLength = entry["compLength"]
|
627
|
+
if compLength % 4:
|
628
|
+
compLength += 4 - (compLength % 4)
|
629
|
+
minLength += compLength
|
630
|
+
metaLength = privLength = 0
|
631
|
+
if header["metaOffset"]:
|
632
|
+
metaLength = header["metaLength"]
|
633
|
+
if header["privOffset"]:
|
634
|
+
privLength = header["privLength"]
|
635
|
+
if privLength and metaLength % 4:
|
636
|
+
metaLength += 4 - (metaLength % 4)
|
637
|
+
minLength += metaLength + privLength
|
638
|
+
if length < minLength:
|
639
|
+
reporter.logError(message="Defined length (%d) does not match the required length of the data (%d)." % (length, minLength))
|
640
|
+
return
|
641
|
+
reporter.logPass(message="The length defined in the header is correct.")
|
642
|
+
|
643
|
+
def _testHeaderReserved(data, reporter):
|
644
|
+
"""
|
645
|
+
Tests:
|
646
|
+
- The reserved bit must be set to 0.
|
647
|
+
"""
|
648
|
+
header = unpackHeader(data)
|
649
|
+
reserved = header["reserved"]
|
650
|
+
if reserved != 0:
|
651
|
+
reporter.logError(message="Invalid value in reserved field (%d)." % reserved)
|
652
|
+
else:
|
653
|
+
reporter.logPass(message="The value in the reserved field is correct.")
|
654
|
+
|
655
|
+
def _testHeaderTotalSFNTSize(data, reporter):
|
656
|
+
"""
|
657
|
+
Tests:
|
658
|
+
- The size of the unpacked SFNT data must be a multiple of 4.
|
659
|
+
- The origLength values in the directory, with proper padding, must sum
|
660
|
+
to the totalSfntSize in the header.
|
661
|
+
"""
|
662
|
+
header = unpackHeader(data)
|
663
|
+
directory = unpackDirectory(data)
|
664
|
+
totalSfntSize = header["totalSfntSize"]
|
665
|
+
isValid = True
|
666
|
+
if totalSfntSize % 4:
|
667
|
+
reporter.logError(message="The total sfnt size (%d) is not a multiple of four." % totalSfntSize)
|
668
|
+
isValid = False
|
669
|
+
else:
|
670
|
+
numTables = header["numTables"]
|
671
|
+
requiredSize = sfntHeaderSize + (numTables * sfntDirectoryEntrySize)
|
672
|
+
for table in directory:
|
673
|
+
origLength = table["origLength"]
|
674
|
+
if origLength % 4:
|
675
|
+
origLength += 4 - (origLength % 4)
|
676
|
+
requiredSize += origLength
|
677
|
+
if totalSfntSize != requiredSize:
|
678
|
+
reporter.logError(message="The total sfnt size (%d) does not match the required sfnt size (%d)." % (totalSfntSize, requiredSize))
|
679
|
+
isValid = False
|
680
|
+
if isValid:
|
681
|
+
reporter.logPass(message="The total sfnt size is valid.")
|
682
|
+
|
683
|
+
def _testHeaderNumTables(data, reporter):
|
684
|
+
"""
|
685
|
+
Tests:
|
686
|
+
- The number of tables must be at least 1.
|
687
|
+
- The directory entries for the specified number of tables must be properly formatted.
|
688
|
+
"""
|
689
|
+
header = unpackHeader(data)
|
690
|
+
numTables = header["numTables"]
|
691
|
+
if numTables < 1:
|
692
|
+
reporter.logError(message="Invalid number of tables defined in header structure (%d)." % numTables)
|
693
|
+
return
|
694
|
+
data = data[headerSize:]
|
695
|
+
for index in range(numTables):
|
696
|
+
try:
|
697
|
+
d, data = structUnpack(directoryFormat, data)
|
698
|
+
except:
|
699
|
+
reporter.logError(message="The defined number of tables in the header (%d) does not match the actual number of tables (%d)." % (numTables, index))
|
700
|
+
return
|
701
|
+
reporter.logPass(message="The number of tables defined in the header is valid.")
|
702
|
+
|
703
|
+
# -------------
|
704
|
+
# Tests: Tables
|
705
|
+
# -------------
|
706
|
+
|
707
|
+
def testDataBlocks(data, reporter):
|
708
|
+
"""
|
709
|
+
Test the WOFF data blocks.
|
710
|
+
"""
|
711
|
+
functions = [
|
712
|
+
_testBlocksOffsetLengthZero,
|
713
|
+
_testBlocksPositioning
|
714
|
+
]
|
715
|
+
for function in functions:
|
716
|
+
shouldStop = function(data, reporter)
|
717
|
+
if shouldStop:
|
718
|
+
return True
|
719
|
+
|
720
|
+
def _testBlocksOffsetLengthZero(data, reporter):
|
721
|
+
"""
|
722
|
+
- The metadata must have the offset and length set to zero consistently.
|
723
|
+
- The private data must have the offset and length set to zero consistently.
|
724
|
+
"""
|
725
|
+
header = unpackHeader(data)
|
726
|
+
# metadata
|
727
|
+
metaOffset = header["metaOffset"]
|
728
|
+
metaLength = header["metaLength"]
|
729
|
+
if metaOffset == 0 or metaLength == 0:
|
730
|
+
if metaOffset == 0 and metaLength == 0:
|
731
|
+
reporter.logPass(message="The length and offset are appropriately set for empty metadata.")
|
732
|
+
else:
|
733
|
+
reporter.logError(message="The metadata offset (%d) and metadata length (%d) are not properly set. If one is 0, they both must be 0." % (metaOffset, metaLength))
|
734
|
+
# private data
|
735
|
+
privOffset = header["privOffset"]
|
736
|
+
privLength = header["privLength"]
|
737
|
+
if privOffset == 0 or privLength == 0:
|
738
|
+
if privOffset == 0 and privLength == 0:
|
739
|
+
reporter.logPass(message="The length and offset are appropriately set for empty private data.")
|
740
|
+
else:
|
741
|
+
reporter.logError(message="The private data offset (%d) and private data length (%d) are not properly set. If one is 0, they both must be 0." % (privOffset, privLength))
|
742
|
+
|
743
|
+
def _testBlocksPositioning(data, reporter):
|
744
|
+
"""
|
745
|
+
Tests:
|
746
|
+
- The table data must start immediately after the directory.
|
747
|
+
- The table data must end at the beginning of the metadata, the beginning of the private data or the end of the file.
|
748
|
+
- The metadata must start immediately after the table data.
|
749
|
+
- the metadata must end at the beginning of he private data (padded as needed) or the end of the file.
|
750
|
+
- The private data must start immediately after the table data or metadata.
|
751
|
+
- The private data must end at the edge of the file.
|
752
|
+
"""
|
753
|
+
header = unpackHeader(data)
|
754
|
+
# table data start
|
755
|
+
directory = unpackDirectory(data)
|
756
|
+
if not directory:
|
757
|
+
return
|
758
|
+
expectedTableDataStart = headerSize + (directorySize * header["numTables"])
|
759
|
+
offsets = [entry["offset"] for entry in directory]
|
760
|
+
tableDataStart = min(offsets)
|
761
|
+
if expectedTableDataStart != tableDataStart:
|
762
|
+
reporter.logError(message="The table data does not start (%d) in the required position (%d)." % (tableDataStart, expectedTableDataStart))
|
763
|
+
else:
|
764
|
+
reporter.logPass(message="The table data begins in the required position.")
|
765
|
+
# table data end
|
766
|
+
if header["metaOffset"]:
|
767
|
+
definedTableDataEnd = header["metaOffset"]
|
768
|
+
elif header["privOffset"]:
|
769
|
+
definedTableDataEnd = header["privOffset"]
|
770
|
+
else:
|
771
|
+
definedTableDataEnd = header["length"]
|
772
|
+
directory = unpackDirectory(data)
|
773
|
+
ends = [table["offset"] + table["compLength"] + calcPaddingLength(table["compLength"]) for table in directory]
|
774
|
+
expectedTableDataEnd = max(ends)
|
775
|
+
if expectedTableDataEnd != definedTableDataEnd:
|
776
|
+
reporter.logError(message="The table data end (%d) is not in the required position (%d)." % (definedTableDataEnd, expectedTableDataEnd))
|
777
|
+
else:
|
778
|
+
reporter.logPass(message="The table data ends in the required position.")
|
779
|
+
# metadata
|
780
|
+
if header["metaOffset"]:
|
781
|
+
# start
|
782
|
+
expectedMetaStart = expectedTableDataEnd
|
783
|
+
definedMetaStart = header["metaOffset"]
|
784
|
+
if expectedMetaStart != definedMetaStart:
|
785
|
+
reporter.logError(message="The metadata does not start (%d) in the required position (%d)." % (definedMetaStart, expectedMetaStart))
|
786
|
+
else:
|
787
|
+
reporter.logPass(message="The metadata begins in the required position.")
|
788
|
+
# end
|
789
|
+
if header["privOffset"]:
|
790
|
+
definedMetaEnd = header["privOffset"]
|
791
|
+
needMetaPadding = True
|
792
|
+
else:
|
793
|
+
definedMetaEnd = header["length"]
|
794
|
+
needMetaPadding = False
|
795
|
+
expectedMetaEnd = header["metaOffset"] + header["metaLength"]
|
796
|
+
if needMetaPadding:
|
797
|
+
expectedMetaEnd += calcPaddingLength(header["metaLength"])
|
798
|
+
if expectedMetaEnd != definedMetaEnd:
|
799
|
+
reporter.logError(message="The metadata end (%d) is not in the required position (%d)." % (definedMetaEnd, expectedMetaEnd))
|
800
|
+
else:
|
801
|
+
reporter.logPass(message="The metadata ends in the required position.")
|
802
|
+
# private data
|
803
|
+
if header["privOffset"]:
|
804
|
+
# start
|
805
|
+
if header["metaOffset"]:
|
806
|
+
expectedPrivateStart = expectedMetaEnd
|
807
|
+
else:
|
808
|
+
expectedPrivateStart = expectedTableDataEnd
|
809
|
+
definedPrivateStart = header["privOffset"]
|
810
|
+
if expectedPrivateStart != definedPrivateStart:
|
811
|
+
reporter.logError(message="The private data does not start (%d) in the required position (%d)." % (definedPrivateStart, expectedPrivateStart))
|
812
|
+
else:
|
813
|
+
reporter.logPass(message="The private data begins in the required position.")
|
814
|
+
# end
|
815
|
+
expectedPrivateEnd = header["length"]
|
816
|
+
definedPrivateEnd = header["privOffset"] + header["privLength"]
|
817
|
+
if expectedPrivateEnd != definedPrivateEnd:
|
818
|
+
reporter.logError(message="The private data end (%d) is not in the required position (%d)." % (definedPrivateEnd, expectedPrivateEnd))
|
819
|
+
else:
|
820
|
+
reporter.logPass(message="The private data ends in the required position.")
|
821
|
+
|
822
|
+
# ----------------------
|
823
|
+
# Tests: Table Directory
|
824
|
+
# ----------------------
|
825
|
+
|
826
|
+
def testTableDirectory(data, reporter):
|
827
|
+
"""
|
828
|
+
Test the WOFF table directory.
|
829
|
+
"""
|
830
|
+
functions = [
|
831
|
+
_testTableDirectoryStructure,
|
832
|
+
_testTableDirectory4ByteOffsets,
|
833
|
+
_testTableDirectoryPadding,
|
834
|
+
_testTableDirectoryPositions,
|
835
|
+
_testTableDirectoryCompressedLength,
|
836
|
+
_testTableDirectoryDecompressedLength,
|
837
|
+
_testTableDirectoryChecksums,
|
838
|
+
_testTableDirectoryTableOrder
|
839
|
+
]
|
840
|
+
for function in functions:
|
841
|
+
shouldStop = function(data, reporter)
|
842
|
+
if shouldStop:
|
843
|
+
return True
|
844
|
+
|
845
|
+
directoryFormat = """
|
846
|
+
tag: 4s
|
847
|
+
offset: L
|
848
|
+
compLength: L
|
849
|
+
origLength: L
|
850
|
+
origChecksum: L
|
851
|
+
"""
|
852
|
+
directorySize = structCalcSize(directoryFormat)
|
853
|
+
|
854
|
+
def _testTableDirectoryStructure(data, reporter):
|
855
|
+
"""
|
856
|
+
Tests:
|
857
|
+
- The entries in the table directory can be unpacked.
|
858
|
+
"""
|
859
|
+
header = unpackHeader(data)
|
860
|
+
numTables = header["numTables"]
|
861
|
+
data = data[headerSize:]
|
862
|
+
try:
|
863
|
+
for index in range(numTables):
|
864
|
+
table, data = structUnpack(directoryFormat, data)
|
865
|
+
reporter.logPass(message="The table directory structure is correct.")
|
866
|
+
except:
|
867
|
+
reporter.logError(message="The table directory is not properly structured.")
|
868
|
+
return True
|
869
|
+
|
870
|
+
def _testTableDirectory4ByteOffsets(data, reporter):
|
871
|
+
"""
|
872
|
+
Tests:
|
873
|
+
- The font tables must each begin on a 4-byte boundary.
|
874
|
+
"""
|
875
|
+
directory = unpackDirectory(data)
|
876
|
+
for table in directory:
|
877
|
+
tag = table["tag"]
|
878
|
+
offset = table["offset"]
|
879
|
+
if offset % 4:
|
880
|
+
reporter.logError(message="The \"%s\" table does not begin on a 4-byte boundary (%d)." % (tag, offset))
|
881
|
+
else:
|
882
|
+
reporter.logPass(message="The \"%s\" table begins on a 4-byte boundary." % tag)
|
883
|
+
|
884
|
+
def _testTableDirectoryPadding(data, reporter):
|
885
|
+
"""
|
886
|
+
Tests:
|
887
|
+
- All tables, including the final table, must be padded to a
|
888
|
+
four byte boundary using null bytes as needed.
|
889
|
+
"""
|
890
|
+
header = unpackHeader(data)
|
891
|
+
directory = unpackDirectory(data)
|
892
|
+
# test final table
|
893
|
+
endError = False
|
894
|
+
sfntEnd = None
|
895
|
+
if header["metaOffset"] != 0:
|
896
|
+
sfntEnd = header["metaOffset"]
|
897
|
+
elif header["privOffset"] != 0:
|
898
|
+
sfntEnd = header["privOffset"]
|
899
|
+
else:
|
900
|
+
sfntEnd = header["length"]
|
901
|
+
if sfntEnd % 4:
|
902
|
+
reporter.logError(message="The sfnt data does not end with proper padding.")
|
903
|
+
else:
|
904
|
+
reporter.logPass(message="The sfnt data ends with proper padding.")
|
905
|
+
# test the bytes used for padding
|
906
|
+
for table in directory:
|
907
|
+
tag = table["tag"]
|
908
|
+
offset = table["offset"]
|
909
|
+
length = table["compLength"]
|
910
|
+
paddingLength = calcPaddingLength(length)
|
911
|
+
if paddingLength:
|
912
|
+
paddingOffset = offset + length
|
913
|
+
padding = data[paddingOffset:paddingOffset+paddingLength]
|
914
|
+
expectedPadding = "\0" * paddingLength
|
915
|
+
if padding != expectedPadding:
|
916
|
+
reporter.logError(message="The \"%s\" table is not padded with null bytes." % tag)
|
917
|
+
else:
|
918
|
+
reporter.logPass(message="The \"%s\" table is padded with null bytes." % tag)
|
919
|
+
|
920
|
+
def _testTableDirectoryPositions(data, reporter):
|
921
|
+
"""
|
922
|
+
Tests:
|
923
|
+
- The table offsets must not be before the end of the header/directory.
|
924
|
+
- The table offset + length must not be greater than the edge of the available space.
|
925
|
+
- The table offsets must not be after the edge of the available space.
|
926
|
+
- Table blocks must not overlap.
|
927
|
+
- There must be no gaps between the tables.
|
928
|
+
"""
|
929
|
+
directory = unpackDirectory(data)
|
930
|
+
tablesWithProblems = set()
|
931
|
+
# test for overlapping tables
|
932
|
+
locations = []
|
933
|
+
for table in directory:
|
934
|
+
offset = table["offset"]
|
935
|
+
length = table["compLength"]
|
936
|
+
length = length + calcPaddingLength(length)
|
937
|
+
locations.append((offset, offset + length, table["tag"]))
|
938
|
+
for start, end, tag in locations:
|
939
|
+
for otherStart, otherEnd, otherTag in locations:
|
940
|
+
if tag == otherTag:
|
941
|
+
continue
|
942
|
+
if start >= otherStart and start < otherEnd:
|
943
|
+
reporter.logError(message="The \"%s\" table overlaps the \"%s\" table." % (tag, otherTag))
|
944
|
+
tablesWithProblems.add(tag)
|
945
|
+
tablesWithProblems.add(otherTag)
|
946
|
+
# test for invalid offset, length and combo
|
947
|
+
header = unpackHeader(data)
|
948
|
+
if header["metaOffset"] != 0:
|
949
|
+
tableDataEnd = header["metaOffset"]
|
950
|
+
elif header["privOffset"] != 0:
|
951
|
+
tableDataEnd = header["privOffset"]
|
952
|
+
else:
|
953
|
+
tableDataEnd = header["length"]
|
954
|
+
numTables = header["numTables"]
|
955
|
+
minOffset = headerSize + (directorySize * numTables)
|
956
|
+
maxLength = tableDataEnd - minOffset
|
957
|
+
for table in directory:
|
958
|
+
tag = table["tag"]
|
959
|
+
offset = table["offset"]
|
960
|
+
length = table["compLength"]
|
961
|
+
# offset is before the beginning of the table data block
|
962
|
+
if offset < minOffset:
|
963
|
+
tablesWithProblems.add(tag)
|
964
|
+
message = "The \"%s\" table directory entry offset (%d) is before the start of the table data block (%d)." % (tag, offset, minOffset)
|
965
|
+
reporter.logError(message=message)
|
966
|
+
# offset is after the end of the table data block
|
967
|
+
elif offset > tableDataEnd:
|
968
|
+
tablesWithProblems.add(tag)
|
969
|
+
message = "The \"%s\" table directory entry offset (%d) is past the end of the table data block (%d)." % (tag, offset, tableDataEnd)
|
970
|
+
reporter.logError(message=message)
|
971
|
+
# offset + length is after the end of the table tada block
|
972
|
+
elif (offset + length) > tableDataEnd:
|
973
|
+
tablesWithProblems.add(tag)
|
974
|
+
message = "The \"%s\" table directory entry offset (%d) + length (%d) is past the end of the table data block (%d)." % (tag, offset, length, tableDataEnd)
|
975
|
+
reporter.logError(message=message)
|
976
|
+
# test for gaps
|
977
|
+
tables = []
|
978
|
+
for table in directory:
|
979
|
+
tag = table["tag"]
|
980
|
+
offset = table["offset"]
|
981
|
+
length = table["compLength"]
|
982
|
+
length += calcPaddingLength(length)
|
983
|
+
tables.append((offset, offset + length, tag))
|
984
|
+
tables.sort()
|
985
|
+
for index, (start, end, tag) in enumerate(tables):
|
986
|
+
if index == 0:
|
987
|
+
continue
|
988
|
+
prevStart, prevEnd, prevTag = tables[index - 1]
|
989
|
+
if prevEnd < start:
|
990
|
+
tablesWithProblems.add(prevTag)
|
991
|
+
tablesWithProblems.add(tag)
|
992
|
+
reporter.logError(message="Extraneous data between the \"%s\" and \"%s\" tables." % (prevTag, tag))
|
993
|
+
# log passes
|
994
|
+
for entry in directory:
|
995
|
+
tag = entry["tag"]
|
996
|
+
if tag in tablesWithProblems:
|
997
|
+
continue
|
998
|
+
reporter.logPass(message="The \"%s\" table directory entry has a valid offset and length." % tag)
|
999
|
+
|
1000
|
+
def _testTableDirectoryCompressedLength(data, reporter):
|
1001
|
+
"""
|
1002
|
+
Tests:
|
1003
|
+
- The compressed length must be less than or equal to the original length.
|
1004
|
+
"""
|
1005
|
+
directory = unpackDirectory(data)
|
1006
|
+
for table in directory:
|
1007
|
+
tag = table["tag"]
|
1008
|
+
compLength = table["compLength"]
|
1009
|
+
origLength = table["origLength"]
|
1010
|
+
if compLength > origLength:
|
1011
|
+
reporter.logError(message="The \"%s\" table directory entry has a compressed length (%d) larger than the original length (%d)." % (tag, compLength, origLength))
|
1012
|
+
else:
|
1013
|
+
reporter.logPass(message="The \"%s\" table directory entry has proper compLength and origLength values." % tag)
|
1014
|
+
|
1015
|
+
def _testTableDirectoryDecompressedLength(data, reporter):
|
1016
|
+
"""
|
1017
|
+
Tests:
|
1018
|
+
- The decompressed length of the data must match the defined original length.
|
1019
|
+
"""
|
1020
|
+
directory = unpackDirectory(data)
|
1021
|
+
tableData = unpackTableData(data)
|
1022
|
+
for table in directory:
|
1023
|
+
tag = table["tag"]
|
1024
|
+
offset = table["offset"]
|
1025
|
+
compLength = table["compLength"]
|
1026
|
+
origLength = table["origLength"]
|
1027
|
+
if compLength >= origLength:
|
1028
|
+
continue
|
1029
|
+
decompressedData = tableData[tag]
|
1030
|
+
# couldn't be decompressed. handled elsewhere.
|
1031
|
+
if decompressedData is None:
|
1032
|
+
continue
|
1033
|
+
decompressedLength = len(decompressedData)
|
1034
|
+
if origLength != decompressedLength:
|
1035
|
+
reporter.logError(message="The \"%s\" table directory entry has an original length (%d) that does not match the actual length of the decompressed data (%d)." % (tag, origLength, decompressedLength))
|
1036
|
+
else:
|
1037
|
+
reporter.logPass(message="The \"%s\" table directory entry has a proper original length compared to the actual decompressed data." % tag)
|
1038
|
+
|
1039
|
+
def _testTableDirectoryChecksums(data, reporter):
|
1040
|
+
"""
|
1041
|
+
Tests:
|
1042
|
+
- The checksums for the tables must match the checksums in the directory.
|
1043
|
+
- The head checksum adjustment must be correct.
|
1044
|
+
"""
|
1045
|
+
# check the table directory checksums
|
1046
|
+
directory = unpackDirectory(data)
|
1047
|
+
tables = unpackTableData(data)
|
1048
|
+
for entry in directory:
|
1049
|
+
tag = entry["tag"]
|
1050
|
+
origChecksum = entry["origChecksum"]
|
1051
|
+
decompressedData = tables[tag]
|
1052
|
+
# couldn't be decompressed.
|
1053
|
+
if decompressedData is None:
|
1054
|
+
continue
|
1055
|
+
newChecksum = calcChecksum(tag, decompressedData)
|
1056
|
+
if newChecksum != origChecksum:
|
1057
|
+
reporter.logError(message="The \"%s\" table directory entry original checksum (%s) does not match the checksum (%s) calculated from the data." % (tag, hex(origChecksum), hex(newChecksum)))
|
1058
|
+
else:
|
1059
|
+
reporter.logPass(message="The \"%s\" table directory entry original checksum is correct." % tag)
|
1060
|
+
# check the head checksum adjustment
|
1061
|
+
if "head" not in tables:
|
1062
|
+
reporter.logWarning(message="The font does not contain a \"head\" table.")
|
1063
|
+
else:
|
1064
|
+
newChecksum = calcHeadChecksum(data)
|
1065
|
+
data = tables["head"]
|
1066
|
+
try:
|
1067
|
+
checksum = struct.unpack(">L", data[8:12])[0]
|
1068
|
+
if checksum != newChecksum:
|
1069
|
+
reporter.logError(message="The \"head\" table checkSumAdjustment (%s) does not match the calculated checkSumAdjustment (%s)." % (hex(checksum), hex(newChecksum)))
|
1070
|
+
else:
|
1071
|
+
reporter.logPass(message="The \"head\" table checkSumAdjustment is valid.")
|
1072
|
+
except:
|
1073
|
+
reporter.logError(message="The \"head\" table is not properly structured.")
|
1074
|
+
|
1075
|
+
|
1076
|
+
def _testTableDirectoryTableOrder(data, reporter):
|
1077
|
+
"""
|
1078
|
+
Tests:
|
1079
|
+
- The directory entries must be stored in ascending order based on their tag.
|
1080
|
+
"""
|
1081
|
+
storedOrder = [table["tag"] for table in unpackDirectory(data)]
|
1082
|
+
if storedOrder != sorted(storedOrder):
|
1083
|
+
reporter.logError(message="The table directory entries are not stored in alphabetical order.")
|
1084
|
+
else:
|
1085
|
+
reporter.logPass(message="The table directory entries are stored in the proper order.")
|
1086
|
+
|
1087
|
+
# -----------------
|
1088
|
+
# Tests: Table Data
|
1089
|
+
# -----------------
|
1090
|
+
|
1091
|
+
def testTableData(data, reporter):
|
1092
|
+
"""
|
1093
|
+
Test the table data.
|
1094
|
+
"""
|
1095
|
+
functions = [
|
1096
|
+
_testTableDataDecompression
|
1097
|
+
]
|
1098
|
+
for function in functions:
|
1099
|
+
shouldStop = function(data, reporter)
|
1100
|
+
if shouldStop:
|
1101
|
+
return True
|
1102
|
+
return False
|
1103
|
+
|
1104
|
+
def _testTableDataDecompression(data, reporter):
|
1105
|
+
"""
|
1106
|
+
Tests:
|
1107
|
+
- The table data, when the defined compressed length is less
|
1108
|
+
than the original length, must be properly compressed.
|
1109
|
+
"""
|
1110
|
+
for table in unpackDirectory(data):
|
1111
|
+
tag = table["tag"]
|
1112
|
+
offset = table["offset"]
|
1113
|
+
compLength = table["compLength"]
|
1114
|
+
origLength = table["origLength"]
|
1115
|
+
if origLength <= compLength:
|
1116
|
+
continue
|
1117
|
+
entryData = data[offset:offset+compLength]
|
1118
|
+
try:
|
1119
|
+
decompressed = zlib.decompress(entryData)
|
1120
|
+
reporter.logPass(message="The \"%s\" table data can be decompressed with zlib." % tag)
|
1121
|
+
except zlib.error:
|
1122
|
+
reporter.logError(message="The \"%s\" table data can not be decompressed with zlib." % tag)
|
1123
|
+
|
1124
|
+
# ----------------
|
1125
|
+
# Tests: Metadata
|
1126
|
+
# ----------------
|
1127
|
+
|
1128
|
+
def testMetadata(data, reporter):
|
1129
|
+
"""
|
1130
|
+
Test the WOFF metadata.
|
1131
|
+
"""
|
1132
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1133
|
+
return False
|
1134
|
+
functions = [
|
1135
|
+
_testMetadataPadding,
|
1136
|
+
_testMetadataDecompression,
|
1137
|
+
_testMetadataDecompressedLength,
|
1138
|
+
_testMetadataParse,
|
1139
|
+
_testMetadataEncoding,
|
1140
|
+
_testMetadataStructure
|
1141
|
+
]
|
1142
|
+
for function in functions:
|
1143
|
+
shouldStop = function(data, reporter)
|
1144
|
+
if shouldStop:
|
1145
|
+
return True
|
1146
|
+
return False
|
1147
|
+
|
1148
|
+
def _shouldSkipMetadataTest(data, reporter):
|
1149
|
+
"""
|
1150
|
+
This is used at the start of metadata test functions.
|
1151
|
+
It writes a note and returns True if not metadata exists.
|
1152
|
+
"""
|
1153
|
+
header = unpackHeader(data)
|
1154
|
+
metaOffset = header["metaOffset"]
|
1155
|
+
metaLength = header["metaLength"]
|
1156
|
+
if metaOffset == 0 or metaLength == 0:
|
1157
|
+
reporter.logNote(message="No metadata to test.")
|
1158
|
+
return True
|
1159
|
+
|
1160
|
+
def _testMetadataPadding(data, reporter):
|
1161
|
+
"""
|
1162
|
+
- The padding must be null.
|
1163
|
+
"""
|
1164
|
+
header = unpackHeader(data)
|
1165
|
+
if not header["metaOffset"] or not header["privOffset"]:
|
1166
|
+
return
|
1167
|
+
paddingLength = calcPaddingLength(header["metaLength"])
|
1168
|
+
if not paddingLength:
|
1169
|
+
return
|
1170
|
+
paddingOffset = header["metaOffset"] + header["metaLength"]
|
1171
|
+
padding = data[paddingOffset:paddingOffset + paddingLength]
|
1172
|
+
expectedPadding = "\0" * paddingLength
|
1173
|
+
if padding != expectedPadding:
|
1174
|
+
reporter.logError(message="The metadata is not padded with null bytes.")
|
1175
|
+
else:
|
1176
|
+
reporter.logPass(message="The metadata is padded with null bytes,")
|
1177
|
+
|
1178
|
+
# does this need to be tested?
|
1179
|
+
#
|
1180
|
+
# def testMetadataIsCompressed(data, reporter):
|
1181
|
+
# """
|
1182
|
+
# Tests:
|
1183
|
+
# - The metadata must be compressed.
|
1184
|
+
# """
|
1185
|
+
# if _shouldSkipMetadataTest(data, reporter):
|
1186
|
+
# return
|
1187
|
+
# header = unpackHeader(data)
|
1188
|
+
# length = header["metaLength"]
|
1189
|
+
# origLength = header["metaOrigLength"]
|
1190
|
+
# if length >= origLength:
|
1191
|
+
# reporter.logError(message="The compressed metdata length (%d) is higher than or equal to the original, uncompressed length (%d)." % (length, origLength))
|
1192
|
+
# return True
|
1193
|
+
# reporter.logPass(message="The compressed metdata length is smaller than the original, uncompressed length.")
|
1194
|
+
|
1195
|
+
def _testMetadataDecompression(data, reporter):
|
1196
|
+
"""
|
1197
|
+
Tests:
|
1198
|
+
- Metadata must be compressed with zlib.
|
1199
|
+
"""
|
1200
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1201
|
+
return
|
1202
|
+
compData = unpackMetadata(data, decompress=False, parse=False)
|
1203
|
+
try:
|
1204
|
+
zlib.decompress(compData)
|
1205
|
+
except zlib.error:
|
1206
|
+
reporter.logError(message="The metadata can not be decompressed with zlib.")
|
1207
|
+
return True
|
1208
|
+
reporter.logPass(message="The metadata can be decompressed with zlib.")
|
1209
|
+
|
1210
|
+
def _testMetadataDecompressedLength(data, reporter):
|
1211
|
+
"""
|
1212
|
+
Tests:
|
1213
|
+
- The length of the decompressed metadata must match the defined original length.
|
1214
|
+
"""
|
1215
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1216
|
+
return
|
1217
|
+
header = unpackHeader(data)
|
1218
|
+
metadata = unpackMetadata(data, parse=False)
|
1219
|
+
metaOrigLength = header["metaOrigLength"]
|
1220
|
+
decompressedLength = len(metadata)
|
1221
|
+
if metaOrigLength != decompressedLength:
|
1222
|
+
reporter.logError(message="The decompressed metadata length (%d) does not match the original metadata length (%d) in the header." % (decompressedLength, metaOrigLength))
|
1223
|
+
else:
|
1224
|
+
reporter.logPass(message="The decompressed metadata length matches the original metadata length in the header.")
|
1225
|
+
|
1226
|
+
def _testMetadataParse(data, reporter):
|
1227
|
+
"""
|
1228
|
+
Tests:
|
1229
|
+
- The metadata must be well-formed.
|
1230
|
+
"""
|
1231
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1232
|
+
return
|
1233
|
+
metadata = unpackMetadata(data, parse=False)
|
1234
|
+
try:
|
1235
|
+
tree = ElementTree.fromstring(metadata)
|
1236
|
+
except (ExpatError, LookupError):
|
1237
|
+
reporter.logError(message="The metadata can not be parsed.")
|
1238
|
+
return True
|
1239
|
+
reporter.logPass(message="The metadata can be parsed.")
|
1240
|
+
|
1241
|
+
def _testMetadataEncoding(data, reporter):
|
1242
|
+
"""
|
1243
|
+
Tests:
|
1244
|
+
- The metadata must be UTF-8 encoded.
|
1245
|
+
"""
|
1246
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1247
|
+
return
|
1248
|
+
metadata = unpackMetadata(data, parse=False)
|
1249
|
+
errorMessage = "The metadata encoding is not valid."
|
1250
|
+
encoding = None
|
1251
|
+
# check the BOM
|
1252
|
+
if not metadata.startswith("<"):
|
1253
|
+
if not metadata.startswith(codecs.BOM_UTF8):
|
1254
|
+
reporter.logError(message=errorMessage)
|
1255
|
+
return
|
1256
|
+
else:
|
1257
|
+
encoding = "UTF-8"
|
1258
|
+
# sniff the encoding
|
1259
|
+
else:
|
1260
|
+
# quick test to ensure that the regular expression will work.
|
1261
|
+
# the string must start with <?xml. this will catch
|
1262
|
+
# other encodings such as: <\x00?\x00x\x00m\x00l
|
1263
|
+
if not metadata.startswith("<?xml"):
|
1264
|
+
reporter.logError(message=errorMessage)
|
1265
|
+
return
|
1266
|
+
# go to the first occurance of >
|
1267
|
+
line = metadata.split(">", 1)[0]
|
1268
|
+
# find an encoding string
|
1269
|
+
pattern = re.compile(
|
1270
|
+
"\s+"
|
1271
|
+
"encoding"
|
1272
|
+
"\s*"
|
1273
|
+
"="
|
1274
|
+
"\s*"
|
1275
|
+
"[\"']+"
|
1276
|
+
"([^\"']+)"
|
1277
|
+
)
|
1278
|
+
m = pattern.search(line)
|
1279
|
+
if m:
|
1280
|
+
encoding = m.group(1)
|
1281
|
+
else:
|
1282
|
+
encoding = "UTF-8"
|
1283
|
+
# report
|
1284
|
+
if encoding != "UTF-8":
|
1285
|
+
reporter.logError(message=errorMessage)
|
1286
|
+
else:
|
1287
|
+
reporter.logPass(message="The metadata is properly encoded.")
|
1288
|
+
|
1289
|
+
def _testMetadataStructure(data, reporter):
|
1290
|
+
"""
|
1291
|
+
Test the metadata structure.
|
1292
|
+
"""
|
1293
|
+
if _shouldSkipMetadataTest(data, reporter):
|
1294
|
+
return
|
1295
|
+
tree = unpackMetadata(data)
|
1296
|
+
# make sure the top element is metadata
|
1297
|
+
if tree.tag != "metadata":
|
1298
|
+
reporter.logError("The top element is not \"metadata\".")
|
1299
|
+
return
|
1300
|
+
# sniff the version
|
1301
|
+
version = tree.attrib.get("version")
|
1302
|
+
if not version:
|
1303
|
+
reporter.logError("The \"version\" attribute is not defined.")
|
1304
|
+
return
|
1305
|
+
# grab the appropriate specification
|
1306
|
+
versionSpecs = {
|
1307
|
+
"1.0" : metadataSpec_1_0
|
1308
|
+
}
|
1309
|
+
spec = versionSpecs.get(version)
|
1310
|
+
if spec is None:
|
1311
|
+
reporter.logError("Unknown version (\"%s\")." % version)
|
1312
|
+
return
|
1313
|
+
haveError = _validateMetadataElement(tree, spec, reporter)
|
1314
|
+
if not haveError:
|
1315
|
+
reporter.logPass("The \"metadata\" element is properly formatted.")
|
1316
|
+
|
1317
|
+
def _validateMetadataElement(element, spec, reporter, parentTree=[]):
|
1318
|
+
haveError = False
|
1319
|
+
# unknown attributes
|
1320
|
+
knownAttributes = []
|
1321
|
+
for attrib in spec["requiredAttributes"].keys() + spec["recommendedAttributes"].keys() + spec["optionalAttributes"].keys():
|
1322
|
+
attrib = _parseAttribute(attrib)
|
1323
|
+
knownAttributes.append(attrib)
|
1324
|
+
for attrib in sorted(element.attrib.keys()):
|
1325
|
+
# the search is a bit complicated because there are
|
1326
|
+
# attributes that have more than one name.
|
1327
|
+
found = False
|
1328
|
+
for knownAttrib in knownAttributes:
|
1329
|
+
if knownAttrib == attrib:
|
1330
|
+
found = True
|
1331
|
+
break
|
1332
|
+
elif isinstance(knownAttrib, list) and attrib in knownAttrib:
|
1333
|
+
found = True
|
1334
|
+
break
|
1335
|
+
if not found:
|
1336
|
+
_logMetadataResult(
|
1337
|
+
reporter,
|
1338
|
+
"error",
|
1339
|
+
"Unknown attribute (\"%s\")" % attrib,
|
1340
|
+
element.tag,
|
1341
|
+
parentTree
|
1342
|
+
)
|
1343
|
+
haveError = True
|
1344
|
+
# attributes
|
1345
|
+
s = [
|
1346
|
+
("requiredAttributes", "required"),
|
1347
|
+
("recommendedAttributes", "recommended"),
|
1348
|
+
("optionalAttributes", "optional")
|
1349
|
+
]
|
1350
|
+
for key, requirementLevel in s:
|
1351
|
+
if spec[key]:
|
1352
|
+
e = _validateAttributes(element, spec[key], reporter, parentTree, requirementLevel)
|
1353
|
+
if e:
|
1354
|
+
haveError = True
|
1355
|
+
# unknown child-elements
|
1356
|
+
knownChildElements = spec["requiredChildElements"].keys() + spec["recommendedChildElements"].keys() + spec["optionalChildElements"].keys()
|
1357
|
+
for childElement in element:
|
1358
|
+
if childElement.tag not in knownChildElements:
|
1359
|
+
_logMetadataResult(
|
1360
|
+
reporter,
|
1361
|
+
"error",
|
1362
|
+
"Unknown child-element (\"%s\")" % childElement.tag,
|
1363
|
+
element.tag,
|
1364
|
+
parentTree
|
1365
|
+
)
|
1366
|
+
haveError = True
|
1367
|
+
# child elements
|
1368
|
+
s = [
|
1369
|
+
("requiredChildElements", "required"),
|
1370
|
+
("recommendedChildElements", "recommended"),
|
1371
|
+
("optionalChildElements", "optional")
|
1372
|
+
]
|
1373
|
+
for key, requirementLevel in s:
|
1374
|
+
if spec[key]:
|
1375
|
+
for childElementTag, childElementData in sorted(spec[key].items()):
|
1376
|
+
e = _validateChildElements(element, childElementTag, childElementData, reporter, parentTree, requirementLevel)
|
1377
|
+
if e:
|
1378
|
+
haveError = True
|
1379
|
+
# content
|
1380
|
+
content = element.text
|
1381
|
+
if content is not None:
|
1382
|
+
content = content.strip()
|
1383
|
+
if content and spec["content"] == "not allowed":
|
1384
|
+
_logMetadataResult(
|
1385
|
+
reporter,
|
1386
|
+
"error",
|
1387
|
+
"Content defined",
|
1388
|
+
element.tag,
|
1389
|
+
parentTree
|
1390
|
+
)
|
1391
|
+
haveError = True
|
1392
|
+
elif not content and content and spec["content"] == "required":
|
1393
|
+
_logMetadataResult(
|
1394
|
+
reporter,
|
1395
|
+
"error",
|
1396
|
+
"Content not defined",
|
1397
|
+
element.tag,
|
1398
|
+
parentTree
|
1399
|
+
)
|
1400
|
+
elif not content and spec["content"] == "recommended":
|
1401
|
+
_logMetadataResult(
|
1402
|
+
reporter,
|
1403
|
+
"warn",
|
1404
|
+
"Content not defined",
|
1405
|
+
element.tag,
|
1406
|
+
parentTree
|
1407
|
+
)
|
1408
|
+
# log the result
|
1409
|
+
if not haveError and parentTree == ["metadata"]:
|
1410
|
+
reporter.logPass("The \"%s\" element is properly formatted." % element.tag)
|
1411
|
+
# done
|
1412
|
+
return haveError
|
1413
|
+
|
1414
|
+
def _parseAttribute(attrib):
|
1415
|
+
if " " in attrib:
|
1416
|
+
final = []
|
1417
|
+
for a in attrib.split(" "):
|
1418
|
+
if a.startswith("xml:"):
|
1419
|
+
a = "{http://www.w3.org/XML/1998/namespace}" + a[4:]
|
1420
|
+
final.append(a)
|
1421
|
+
return final
|
1422
|
+
return attrib
|
1423
|
+
|
1424
|
+
def _unEtreeAttribute(attrib):
|
1425
|
+
ns = "{http://www.w3.org/XML/1998/namespace}"
|
1426
|
+
if attrib.startswith(ns):
|
1427
|
+
attrib = "xml:" + attrib[len(ns):]
|
1428
|
+
return attrib
|
1429
|
+
|
1430
|
+
def _validateAttributes(element, spec, reporter, parentTree, requirementLevel):
|
1431
|
+
haveError = False
|
1432
|
+
for attrib, valueOptions in sorted(spec.items()):
|
1433
|
+
attribs = _parseAttribute(attrib)
|
1434
|
+
if isinstance(attribs, basestring):
|
1435
|
+
attribs = [attribs]
|
1436
|
+
found = []
|
1437
|
+
for attrib in attribs:
|
1438
|
+
if attrib in element.attrib:
|
1439
|
+
found.append(attrib)
|
1440
|
+
# make strings for reporting
|
1441
|
+
if len(attribs) > 1:
|
1442
|
+
attribString = ", ".join(["\"%s\"" % _unEtreeAttribute(i) for i in attribs])
|
1443
|
+
else:
|
1444
|
+
attribString = "\"%s\"" % attribs[0]
|
1445
|
+
if len(found) == 0:
|
1446
|
+
pass
|
1447
|
+
elif len(found) > 1:
|
1448
|
+
foundString = ", ".join(["\"%s\"" % _unEtreeAttribute(i) for i in found])
|
1449
|
+
else:
|
1450
|
+
foundString = "\"%s\"" % found[0]
|
1451
|
+
# more than one of the mutually exclusive attributes found
|
1452
|
+
if len(found) > 1:
|
1453
|
+
_logMetadataResult(
|
1454
|
+
reporter,
|
1455
|
+
"error",
|
1456
|
+
"More than one mutually exclusive attribute (%s) defined" % foundString,
|
1457
|
+
element.tag,
|
1458
|
+
parentTree
|
1459
|
+
)
|
1460
|
+
haveError = True
|
1461
|
+
# missing
|
1462
|
+
elif len(found) == 0:
|
1463
|
+
if requirementLevel == "optional":
|
1464
|
+
continue
|
1465
|
+
elif requirementLevel == "required":
|
1466
|
+
errorLevel = "error"
|
1467
|
+
else:
|
1468
|
+
errorLevel = "warn"
|
1469
|
+
_logMetadataResult(
|
1470
|
+
reporter,
|
1471
|
+
errorLevel,
|
1472
|
+
"%s \"%s\" attribute not defined" % (requirementLevel.title(), attrib),
|
1473
|
+
element.tag,
|
1474
|
+
parentTree
|
1475
|
+
)
|
1476
|
+
if requirementLevel == "required":
|
1477
|
+
haveError = True
|
1478
|
+
# incorrect value
|
1479
|
+
else:
|
1480
|
+
e = _validateAttributeValue(element, found[0], valueOptions, reporter, parentTree)
|
1481
|
+
if e:
|
1482
|
+
haveError = True
|
1483
|
+
# done
|
1484
|
+
return haveError
|
1485
|
+
|
1486
|
+
def _validateAttributeValue(element, attrib, valueOptions, reporter, parentTree):
|
1487
|
+
haveError = False
|
1488
|
+
value = element.attrib[attrib]
|
1489
|
+
if isinstance(valueOptions, basestring):
|
1490
|
+
valueOptions = [valueOptions]
|
1491
|
+
# no defined value options
|
1492
|
+
if valueOptions is None:
|
1493
|
+
# the string is empty
|
1494
|
+
if not value:
|
1495
|
+
_logMetadataResult(
|
1496
|
+
reporter,
|
1497
|
+
"warn",
|
1498
|
+
"Value for the \"%s\" attribute is an empty string" % attrib,
|
1499
|
+
element.tag,
|
1500
|
+
parentTree
|
1501
|
+
)
|
1502
|
+
# illegal value
|
1503
|
+
elif value not in valueOptions:
|
1504
|
+
_logMetadataResult(
|
1505
|
+
reporter,
|
1506
|
+
"error",
|
1507
|
+
"Invalid value (\"%s\") for the \"%s\" attribute" % (value, attrib),
|
1508
|
+
element.tag,
|
1509
|
+
parentTree
|
1510
|
+
)
|
1511
|
+
haveError = True
|
1512
|
+
# return the error state
|
1513
|
+
return haveError
|
1514
|
+
|
1515
|
+
def _validateChildElements(element, childElementTag, childElementData, reporter, parentTree, requirementLevel):
|
1516
|
+
haveError = False
|
1517
|
+
# get the valid counts
|
1518
|
+
minimumOccurrences = childElementData.get("minimumOccurrences", 0)
|
1519
|
+
maximumOccurrences = childElementData.get("maximumOccurrences", None)
|
1520
|
+
# find the appropriate elements
|
1521
|
+
found = element.findall(childElementTag)
|
1522
|
+
# not defined enough times
|
1523
|
+
if minimumOccurrences == 1 and len(found) == 0:
|
1524
|
+
_logMetadataResult(
|
1525
|
+
reporter,
|
1526
|
+
"error",
|
1527
|
+
"%s \"%s\" child-element not defined" % (requirementLevel.title(), childElementTag),
|
1528
|
+
element.tag,
|
1529
|
+
parentTree
|
1530
|
+
)
|
1531
|
+
haveError = True
|
1532
|
+
elif len(found) < minimumOccurrences:
|
1533
|
+
_logMetadataResult(
|
1534
|
+
reporter,
|
1535
|
+
"error",
|
1536
|
+
"%s \"%s\" child-element is defined %d times instead of the minimum %d times" % (requirementLevel.title(), childElementTag, len(found), minimumOccurrences),
|
1537
|
+
element.tag,
|
1538
|
+
parentTree
|
1539
|
+
)
|
1540
|
+
haveError = True
|
1541
|
+
# not defined, but not recommended
|
1542
|
+
elif len(found) == 0 and requirementLevel == "recommended":
|
1543
|
+
_logMetadataResult(
|
1544
|
+
reporter,
|
1545
|
+
"warn",
|
1546
|
+
"%s \"%s\" child-element is not defined" % (requirementLevel.title(), childElementTag),
|
1547
|
+
element.tag,
|
1548
|
+
parentTree
|
1549
|
+
)
|
1550
|
+
# defined too many times
|
1551
|
+
if maximumOccurrences is not None:
|
1552
|
+
if maximumOccurrences == 1 and len(found) > 1:
|
1553
|
+
_logMetadataResult(
|
1554
|
+
reporter,
|
1555
|
+
"error",
|
1556
|
+
"%s \"%s\" child-element defined more than once" % (requirementLevel.title(), childElementTag),
|
1557
|
+
element.tag,
|
1558
|
+
parentTree
|
1559
|
+
)
|
1560
|
+
haveError = True
|
1561
|
+
elif len(found) > maximumOccurrences:
|
1562
|
+
_logMetadataResult(
|
1563
|
+
reporter,
|
1564
|
+
"error",
|
1565
|
+
"%s \"%s\" child-element defined %d times instead of the maximum %d times" % (requirementLevel.title(), childElementTag, len(found), minimumOccurrences),
|
1566
|
+
element.tag,
|
1567
|
+
parentTree
|
1568
|
+
)
|
1569
|
+
haveError = True
|
1570
|
+
# validate the found elements
|
1571
|
+
if not haveError:
|
1572
|
+
for childElement in found:
|
1573
|
+
# handle recursive child-elements
|
1574
|
+
childElementSpec = childElementData["spec"]
|
1575
|
+
if childElementSpec == "recursive divSpec_1_0":
|
1576
|
+
childElementSpec = divSpec_1_0
|
1577
|
+
elif childElementSpec == "recursive spanSpec_1_0":
|
1578
|
+
childElementSpec = spanSpec_1_0
|
1579
|
+
# dive
|
1580
|
+
e = _validateMetadataElement(childElement, childElementSpec, reporter, parentTree + [element.tag])
|
1581
|
+
if e:
|
1582
|
+
haveError = True
|
1583
|
+
# return the error state
|
1584
|
+
return haveError
|
1585
|
+
|
1586
|
+
# logging support
|
1587
|
+
|
1588
|
+
def _logMetadataResult(reporter, result, message, elementTag, parentTree):
|
1589
|
+
message = _formatMetadataResultMessage(message, elementTag, parentTree)
|
1590
|
+
methods = {
|
1591
|
+
"error" : reporter.logError,
|
1592
|
+
"warn" : reporter.logWarning,
|
1593
|
+
"note" : reporter.logNote,
|
1594
|
+
"pass" : reporter.logPass
|
1595
|
+
}
|
1596
|
+
methods[result](message)
|
1597
|
+
|
1598
|
+
def _formatMetadataResultMessage(message, elementTag, parentTree):
|
1599
|
+
parentTree = parentTree + [elementTag]
|
1600
|
+
if parentTree[0] == "metadata":
|
1601
|
+
parentTree = parentTree[1:]
|
1602
|
+
if parentTree:
|
1603
|
+
parentTree = ["\"%s\"" % t for t in reversed(parentTree) if t is not None]
|
1604
|
+
message += " in " + " in ".join(parentTree)
|
1605
|
+
message += "."
|
1606
|
+
return message
|
1607
|
+
|
1608
|
+
# -------------------------
|
1609
|
+
# Support: Misc. SFNT Stuff
|
1610
|
+
# -------------------------
|
1611
|
+
|
1612
|
+
# Some of this was adapted from fontTools.ttLib.sfnt
|
1613
|
+
|
1614
|
+
sfntHeaderFormat = """
|
1615
|
+
sfntVersion: 4s
|
1616
|
+
numTables: H
|
1617
|
+
searchRange: H
|
1618
|
+
entrySelector: H
|
1619
|
+
rangeShift: H
|
1620
|
+
"""
|
1621
|
+
sfntHeaderSize = structCalcSize(sfntHeaderFormat)
|
1622
|
+
|
1623
|
+
sfntDirectoryEntryFormat = """
|
1624
|
+
tag: 4s
|
1625
|
+
checkSum: L
|
1626
|
+
offset: L
|
1627
|
+
length: L
|
1628
|
+
"""
|
1629
|
+
sfntDirectoryEntrySize = structCalcSize(sfntDirectoryEntryFormat)
|
1630
|
+
|
1631
|
+
def maxPowerOfTwo(value):
|
1632
|
+
exponent = 0
|
1633
|
+
while value:
|
1634
|
+
value = value >> 1
|
1635
|
+
exponent += 1
|
1636
|
+
return max(exponent - 1, 0)
|
1637
|
+
|
1638
|
+
def getSearchRange(numTables):
|
1639
|
+
exponent = maxPowerOfTwo(numTables)
|
1640
|
+
searchRange = (2 ** exponent) * 16
|
1641
|
+
entrySelector = exponent
|
1642
|
+
rangeShift = numTables * 16 - searchRange
|
1643
|
+
return searchRange, entrySelector, rangeShift
|
1644
|
+
|
1645
|
+
def calcPaddingLength(length):
|
1646
|
+
if not length % 4:
|
1647
|
+
return 0
|
1648
|
+
return 4 - (length % 4)
|
1649
|
+
|
1650
|
+
def padData(data):
|
1651
|
+
data += "\0" * calcPaddingLength(len(data))
|
1652
|
+
return data
|
1653
|
+
|
1654
|
+
def sumDataULongs(data):
|
1655
|
+
longs = struct.unpack(">%dL" % (len(data) / 4), data)
|
1656
|
+
value = sum(longs) % (2 ** 32)
|
1657
|
+
return value
|
1658
|
+
|
1659
|
+
def calcChecksum(tag, data):
|
1660
|
+
if tag == "head":
|
1661
|
+
data = data[:8] + "\0\0\0\0" + data[12:]
|
1662
|
+
data = padData(data)
|
1663
|
+
value = sumDataULongs(data)
|
1664
|
+
return value
|
1665
|
+
|
1666
|
+
def calcHeadChecksum(data):
|
1667
|
+
header = unpackHeader(data)
|
1668
|
+
directory = unpackDirectory(data)
|
1669
|
+
numTables = header["numTables"]
|
1670
|
+
# build the sfnt directory
|
1671
|
+
searchRange, entrySelector, rangeShift = getSearchRange(numTables)
|
1672
|
+
sfntHeaderData = dict(
|
1673
|
+
sfntVersion=header["flavor"],
|
1674
|
+
numTables=numTables,
|
1675
|
+
searchRange=searchRange,
|
1676
|
+
entrySelector=entrySelector,
|
1677
|
+
rangeShift=rangeShift
|
1678
|
+
)
|
1679
|
+
sfntData = structPack(sfntHeaderFormat, sfntHeaderData)
|
1680
|
+
sfntEntries = {}
|
1681
|
+
offset = sfntHeaderSize + (sfntDirectoryEntrySize * numTables)
|
1682
|
+
directory = [(entry["offset"], entry) for entry in directory]
|
1683
|
+
for o, entry in sorted(directory):
|
1684
|
+
checksum = entry["origChecksum"]
|
1685
|
+
tag = entry["tag"]
|
1686
|
+
length = entry["origLength"]
|
1687
|
+
sfntEntries[tag] = dict(
|
1688
|
+
tag=tag,
|
1689
|
+
checkSum=checksum,
|
1690
|
+
offset=offset,
|
1691
|
+
length=length
|
1692
|
+
)
|
1693
|
+
offset += length + calcPaddingLength(length)
|
1694
|
+
for tag, sfntEntry in sorted(sfntEntries.items()):
|
1695
|
+
sfntData += structPack(sfntDirectoryEntryFormat, sfntEntry)
|
1696
|
+
# calculate
|
1697
|
+
checkSums = [entry["checkSum"] for entry in sfntEntries.values()]
|
1698
|
+
checkSums.append(sumDataULongs(sfntData))
|
1699
|
+
checkSum = sum(checkSums)
|
1700
|
+
checkSum = (0xB1B0AFBA - checkSum) & 0xffffffff
|
1701
|
+
return checkSum
|
1702
|
+
|
1703
|
+
# ------------------
|
1704
|
+
# Support XML Writer
|
1705
|
+
# ------------------
|
1706
|
+
|
1707
|
+
class XMLWriter(object):
|
1708
|
+
|
1709
|
+
def __init__(self):
|
1710
|
+
self._root = None
|
1711
|
+
self._elements = []
|
1712
|
+
|
1713
|
+
def simpletag(self, tag, **kwargs):
|
1714
|
+
ElementTree.SubElement(self._elements[-1], tag, **kwargs)
|
1715
|
+
|
1716
|
+
def begintag(self, tag, **kwargs):
|
1717
|
+
if self._elements:
|
1718
|
+
s = ElementTree.SubElement(self._elements[-1], tag, **kwargs)
|
1719
|
+
else:
|
1720
|
+
s = ElementTree.Element(tag, **kwargs)
|
1721
|
+
if self._root is None:
|
1722
|
+
self._root = s
|
1723
|
+
self._elements.append(s)
|
1724
|
+
|
1725
|
+
def endtag(self, tag):
|
1726
|
+
assert self._elements[-1].tag == tag
|
1727
|
+
del self._elements[-1]
|
1728
|
+
|
1729
|
+
def write(self, text):
|
1730
|
+
if self._elements[-1].text is None:
|
1731
|
+
self._elements[-1].text = text
|
1732
|
+
else:
|
1733
|
+
self._elements[-1].text += text
|
1734
|
+
|
1735
|
+
def compile(self, encoding="utf-8"):
|
1736
|
+
f = StringIO()
|
1737
|
+
tree = ElementTree.ElementTree(self._root)
|
1738
|
+
indent(tree.getroot())
|
1739
|
+
tree.write(f, encoding=encoding)
|
1740
|
+
text = f.getvalue()
|
1741
|
+
del f
|
1742
|
+
return text
|
1743
|
+
|
1744
|
+
def indent(elem, level=0):
|
1745
|
+
# this is from http://effbot.python-hosting.com/file/effbotlib/ElementTree.py
|
1746
|
+
i = "\n" + level * "\t"
|
1747
|
+
if len(elem):
|
1748
|
+
if not elem.text or not elem.text.strip():
|
1749
|
+
elem.text = i + "\t"
|
1750
|
+
for e in elem:
|
1751
|
+
indent(e, level + 1)
|
1752
|
+
if not e.tail or not e.tail.strip():
|
1753
|
+
e.tail = i
|
1754
|
+
if level and (not elem.tail or not elem.tail.strip()):
|
1755
|
+
elem.tail = i
|
1756
|
+
|
1757
|
+
# ---------------------------------
|
1758
|
+
# Support: Reporters and HTML Stuff
|
1759
|
+
# ---------------------------------
|
1760
|
+
|
1761
|
+
class TestResultGroup(list):
|
1762
|
+
|
1763
|
+
def __init__(self, title):
|
1764
|
+
super(TestResultGroup, self).__init__()
|
1765
|
+
self.title = title
|
1766
|
+
|
1767
|
+
def _haveType(self, tp):
|
1768
|
+
for data in self:
|
1769
|
+
if data["type"] == tp:
|
1770
|
+
return True
|
1771
|
+
return False
|
1772
|
+
|
1773
|
+
def haveNote(self):
|
1774
|
+
return self._haveType("NOTE")
|
1775
|
+
|
1776
|
+
def haveWarning(self):
|
1777
|
+
return self._haveType("WARNING")
|
1778
|
+
|
1779
|
+
def haveError(self):
|
1780
|
+
return self._haveType("ERROR")
|
1781
|
+
|
1782
|
+
def havePass(self):
|
1783
|
+
return self._haveType("PASS")
|
1784
|
+
|
1785
|
+
def haveTraceback(self):
|
1786
|
+
return self._haveType("TRACEBACK")
|
1787
|
+
|
1788
|
+
|
1789
|
+
class BaseReporter(object):
|
1790
|
+
|
1791
|
+
"""
|
1792
|
+
Base reporter. This establishes the required API for reporters.
|
1793
|
+
"""
|
1794
|
+
|
1795
|
+
def __init__(self):
|
1796
|
+
self.title = ""
|
1797
|
+
self.fileInfo = []
|
1798
|
+
self.testResults = []
|
1799
|
+
self.haveReadError = False
|
1800
|
+
|
1801
|
+
def logTitle(self, title):
|
1802
|
+
self.title = title
|
1803
|
+
|
1804
|
+
def logFileInfo(self, title, value):
|
1805
|
+
self.fileInfo.append((title, value))
|
1806
|
+
|
1807
|
+
def logTableInfo(self, tag=None, offset=None, compLength=None, origLength=None, origChecksum=None):
|
1808
|
+
self.tableInfo.append((tag, offset, compLength, origLength, origChecksum))
|
1809
|
+
|
1810
|
+
def logTestTitle(self, title):
|
1811
|
+
self.testResults.append(TestResultGroup(title))
|
1812
|
+
|
1813
|
+
def logNote(self, message, information=""):
|
1814
|
+
d = dict(type="NOTE", message=message, information=information)
|
1815
|
+
self.testResults[-1].append(d)
|
1816
|
+
|
1817
|
+
def logWarning(self, message, information=""):
|
1818
|
+
d = dict(type="WARNING", message=message, information=information)
|
1819
|
+
self.testResults[-1].append(d)
|
1820
|
+
|
1821
|
+
def logError(self, message, information=""):
|
1822
|
+
d = dict(type="ERROR", message=message, information=information)
|
1823
|
+
self.testResults[-1].append(d)
|
1824
|
+
|
1825
|
+
def logPass(self, message, information=""):
|
1826
|
+
d = dict(type="PASS", message=message, information=information)
|
1827
|
+
self.testResults[-1].append(d)
|
1828
|
+
|
1829
|
+
def logTraceback(self, text):
|
1830
|
+
d = dict(type="TRACEBACK", message=text, information="")
|
1831
|
+
self.testResults[-1].append(d)
|
1832
|
+
|
1833
|
+
def getReport(self, *args, **kwargs):
|
1834
|
+
raise NotImplementedError
|
1835
|
+
|
1836
|
+
def numErrors(self):
|
1837
|
+
numErrors = 0
|
1838
|
+
for group in self.testResults:
|
1839
|
+
for result in group:
|
1840
|
+
if result["type"] == "ERROR":
|
1841
|
+
numErrors = numErrors + 1
|
1842
|
+
return numErrors
|
1843
|
+
|
1844
|
+
class TextReporter(BaseReporter):
|
1845
|
+
|
1846
|
+
"""
|
1847
|
+
Plain text reporter.
|
1848
|
+
"""
|
1849
|
+
|
1850
|
+
def getReport(self, reportNote=True, reportWarning=True, reportError=True, reportPass=True):
|
1851
|
+
report = []
|
1852
|
+
for group in self.testResults:
|
1853
|
+
for result in group:
|
1854
|
+
typ = result["type"]
|
1855
|
+
if typ == "NOTE" and not reportNote:
|
1856
|
+
continue
|
1857
|
+
elif typ == "WARNING" and not reportWarning:
|
1858
|
+
continue
|
1859
|
+
elif typ == "ERROR" and not reportError:
|
1860
|
+
continue
|
1861
|
+
elif typ == "PASS" and not reportPass:
|
1862
|
+
continue
|
1863
|
+
t = "%s - %s: %s" % (result["type"], group.title, result["message"])
|
1864
|
+
report.append(t)
|
1865
|
+
return "\n".join(report)
|
1866
|
+
|
1867
|
+
|
1868
|
+
class HTMLReporter(BaseReporter):
|
1869
|
+
|
1870
|
+
def getReport(self):
|
1871
|
+
writer = startHTML(title=self.title)
|
1872
|
+
# write the file info
|
1873
|
+
self._writeFileInfo(writer)
|
1874
|
+
# write major error alert
|
1875
|
+
if self.haveReadError:
|
1876
|
+
self._writeMajorError(writer)
|
1877
|
+
# write the test overview
|
1878
|
+
self._writeTestResultsOverview(writer)
|
1879
|
+
# write the test groups
|
1880
|
+
self._writeTestResults(writer)
|
1881
|
+
# close the html
|
1882
|
+
text = finishHTML(writer)
|
1883
|
+
# done
|
1884
|
+
return text
|
1885
|
+
|
1886
|
+
def _writeFileInfo(self, writer):
|
1887
|
+
# write the font info
|
1888
|
+
writer.begintag("div", c_l_a_s_s="infoBlock")
|
1889
|
+
## title
|
1890
|
+
writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
|
1891
|
+
writer.write("File Information")
|
1892
|
+
writer.endtag("h3")
|
1893
|
+
## table
|
1894
|
+
writer.begintag("table", c_l_a_s_s="report")
|
1895
|
+
## items
|
1896
|
+
for title, value in self.fileInfo:
|
1897
|
+
# row
|
1898
|
+
writer.begintag("tr")
|
1899
|
+
# title
|
1900
|
+
writer.begintag("td", c_l_a_s_s="title")
|
1901
|
+
writer.write(title)
|
1902
|
+
writer.endtag("td")
|
1903
|
+
# message
|
1904
|
+
writer.begintag("td")
|
1905
|
+
writer.write(value)
|
1906
|
+
writer.endtag("td")
|
1907
|
+
# close row
|
1908
|
+
writer.endtag("tr")
|
1909
|
+
writer.endtag("table")
|
1910
|
+
## close the container
|
1911
|
+
writer.endtag("div")
|
1912
|
+
|
1913
|
+
def _writeMajorError(self, writer):
|
1914
|
+
writer.begintag("h2", c_l_a_s_s="readError")
|
1915
|
+
writer.write("The file contains major structural errors!")
|
1916
|
+
writer.endtag("h2")
|
1917
|
+
|
1918
|
+
def _writeTestResultsOverview(self, writer):
|
1919
|
+
## tabulate
|
1920
|
+
notes = 0
|
1921
|
+
passes = 0
|
1922
|
+
errors = 0
|
1923
|
+
warnings = 0
|
1924
|
+
for group in self.testResults:
|
1925
|
+
for data in group:
|
1926
|
+
tp = data["type"]
|
1927
|
+
if tp == "NOTE":
|
1928
|
+
notes += 1
|
1929
|
+
elif tp == "PASS":
|
1930
|
+
passes += 1
|
1931
|
+
elif tp == "ERROR":
|
1932
|
+
errors += 1
|
1933
|
+
else:
|
1934
|
+
warnings += 1
|
1935
|
+
total = sum((notes, passes, errors, warnings))
|
1936
|
+
## container
|
1937
|
+
writer.begintag("div", c_l_a_s_s="infoBlock")
|
1938
|
+
## header
|
1939
|
+
writer.begintag("h3", c_l_a_s_s="infoBlockTitle")
|
1940
|
+
writer.write("Results for %d Tests" % total)
|
1941
|
+
writer.endtag("h3")
|
1942
|
+
## results
|
1943
|
+
results = [
|
1944
|
+
("PASS", passes),
|
1945
|
+
("WARNING", warnings),
|
1946
|
+
("ERROR", errors),
|
1947
|
+
("NOTE", notes),
|
1948
|
+
]
|
1949
|
+
writer.begintag("table", c_l_a_s_s="report")
|
1950
|
+
for tp, value in results:
|
1951
|
+
# title
|
1952
|
+
writer.begintag("tr", c_l_a_s_s="testReport%s" % tp.title())
|
1953
|
+
writer.begintag("td", c_l_a_s_s="title")
|
1954
|
+
writer.write(tp)
|
1955
|
+
writer.endtag("td")
|
1956
|
+
# count
|
1957
|
+
writer.begintag("td", c_l_a_s_s="testReportResultCount")
|
1958
|
+
writer.write(str(value))
|
1959
|
+
writer.endtag("td")
|
1960
|
+
# empty
|
1961
|
+
writer.begintag("td")
|
1962
|
+
writer.endtag("td")
|
1963
|
+
# toggle button
|
1964
|
+
buttonID = "testResult%sToggleButton" % tp
|
1965
|
+
writer.begintag("td",
|
1966
|
+
id=buttonID, c_l_a_s_s="toggleButton",
|
1967
|
+
onclick="testResultToggleButtonHit(a_p_o_s_t_r_o_p_h_e%sa_p_o_s_t_r_o_p_h_e, a_p_o_s_t_r_o_p_h_e%sa_p_o_s_t_r_o_p_h_e);" % (buttonID, "test%s" % tp.title()))
|
1968
|
+
writer.write("Hide")
|
1969
|
+
writer.endtag("td")
|
1970
|
+
# close the row
|
1971
|
+
writer.endtag("tr")
|
1972
|
+
writer.endtag("table")
|
1973
|
+
## close the container
|
1974
|
+
writer.endtag("div")
|
1975
|
+
|
1976
|
+
def _writeTestResults(self, writer):
|
1977
|
+
for infoBlock in self.testResults:
|
1978
|
+
# container
|
1979
|
+
writer.begintag("div", c_l_a_s_s="infoBlock")
|
1980
|
+
# header
|
1981
|
+
writer.begintag("h4", c_l_a_s_s="infoBlockTitle")
|
1982
|
+
writer.write(infoBlock.title)
|
1983
|
+
writer.endtag("h4")
|
1984
|
+
# individual reports
|
1985
|
+
writer.begintag("table", c_l_a_s_s="report")
|
1986
|
+
for data in infoBlock:
|
1987
|
+
tp = data["type"]
|
1988
|
+
message = data["message"]
|
1989
|
+
information = data["information"]
|
1990
|
+
# row
|
1991
|
+
writer.begintag("tr", c_l_a_s_s="test%s" % tp.title())
|
1992
|
+
# title
|
1993
|
+
writer.begintag("td", c_l_a_s_s="title")
|
1994
|
+
writer.write(tp)
|
1995
|
+
writer.endtag("td")
|
1996
|
+
# message
|
1997
|
+
writer.begintag("td")
|
1998
|
+
writer.write(message)
|
1999
|
+
## info
|
2000
|
+
if information:
|
2001
|
+
writer.begintag("p", c_l_a_s_s="info")
|
2002
|
+
writer.write(information)
|
2003
|
+
writer.endtag("p")
|
2004
|
+
writer.endtag("td")
|
2005
|
+
# close row
|
2006
|
+
writer.endtag("tr")
|
2007
|
+
writer.endtag("table")
|
2008
|
+
# close container
|
2009
|
+
writer.endtag("div")
|
2010
|
+
|
2011
|
+
|
2012
|
+
defaultCSS = """
|
2013
|
+
body {
|
2014
|
+
background-color: #e5e5e5;
|
2015
|
+
padding: 15px 15px 0px 15px;
|
2016
|
+
margin: 0px;
|
2017
|
+
font-family: Helvetica, Verdana, Arial, sans-serif;
|
2018
|
+
}
|
2019
|
+
|
2020
|
+
h2.readError {
|
2021
|
+
background-color: red;
|
2022
|
+
color: white;
|
2023
|
+
margin: 20px 15px 20px 15px;
|
2024
|
+
padding: 10px;
|
2025
|
+
border-radius: 5px;
|
2026
|
+
font-size: 25px;
|
2027
|
+
}
|
2028
|
+
|
2029
|
+
/* info blocks */
|
2030
|
+
|
2031
|
+
.infoBlock {
|
2032
|
+
background-color: white;
|
2033
|
+
margin: 0px 0px 15px 0px;
|
2034
|
+
padding: 15px;
|
2035
|
+
border-radius: 5px;
|
2036
|
+
}
|
2037
|
+
|
2038
|
+
h3.infoBlockTitle {
|
2039
|
+
font-size: 20px;
|
2040
|
+
margin: 0px 0px 15px 0px;
|
2041
|
+
padding: 0px 0px 10px 0px;
|
2042
|
+
border-bottom: 1px solid #e5e5e5;
|
2043
|
+
}
|
2044
|
+
|
2045
|
+
h4.infoBlockTitle {
|
2046
|
+
font-size: 17px;
|
2047
|
+
margin: 0px 0px 15px 0px;
|
2048
|
+
padding: 0px 0px 10px 0px;
|
2049
|
+
border-bottom: 1px solid #e5e5e5;
|
2050
|
+
}
|
2051
|
+
|
2052
|
+
table.report {
|
2053
|
+
border-collapse: collapse;
|
2054
|
+
width: 100%;
|
2055
|
+
font-size: 14px;
|
2056
|
+
}
|
2057
|
+
|
2058
|
+
table.report tr {
|
2059
|
+
border-top: 1px solid white;
|
2060
|
+
}
|
2061
|
+
|
2062
|
+
table.report tr.testPass, table.report tr.testReportPass {
|
2063
|
+
background-color: #c8ffaf;
|
2064
|
+
}
|
2065
|
+
|
2066
|
+
table.report tr.testError, table.report tr.testReportError {
|
2067
|
+
background-color: #ffc3af;
|
2068
|
+
}
|
2069
|
+
|
2070
|
+
table.report tr.testWarning, table.report tr.testReportWarning {
|
2071
|
+
background-color: #ffe1af;
|
2072
|
+
}
|
2073
|
+
|
2074
|
+
table.report tr.testNote, table.report tr.testReportNote {
|
2075
|
+
background-color: #96e1ff;
|
2076
|
+
}
|
2077
|
+
|
2078
|
+
table.report tr.testTraceback, table.report tr.testReportTraceback {
|
2079
|
+
background-color: red;
|
2080
|
+
color: white;
|
2081
|
+
}
|
2082
|
+
|
2083
|
+
table.report td {
|
2084
|
+
padding: 7px 5px 7px 5px;
|
2085
|
+
vertical-align: top;
|
2086
|
+
}
|
2087
|
+
|
2088
|
+
table.report td.title {
|
2089
|
+
width: 80px;
|
2090
|
+
text-align: right;
|
2091
|
+
font-weight: bold;
|
2092
|
+
text-transform: uppercase;
|
2093
|
+
}
|
2094
|
+
|
2095
|
+
table.report td.testReportResultCount {
|
2096
|
+
width: 100px;
|
2097
|
+
}
|
2098
|
+
|
2099
|
+
table.report td.toggleButton {
|
2100
|
+
text-align: center;
|
2101
|
+
width: 50px;
|
2102
|
+
border-left: 1px solid white;
|
2103
|
+
cursor: pointer;
|
2104
|
+
}
|
2105
|
+
|
2106
|
+
.infoBlock td p.info {
|
2107
|
+
font-size: 12px;
|
2108
|
+
font-style: italic;
|
2109
|
+
margin: 5px 0px 0px 0px;
|
2110
|
+
}
|
2111
|
+
|
2112
|
+
/* SFNT table */
|
2113
|
+
|
2114
|
+
table.sfntTableData {
|
2115
|
+
font-size: 14px;
|
2116
|
+
width: 100%;
|
2117
|
+
border-collapse: collapse;
|
2118
|
+
padding: 0px;
|
2119
|
+
}
|
2120
|
+
|
2121
|
+
table.sfntTableData th {
|
2122
|
+
padding: 5px 0px 5px 0px;
|
2123
|
+
text-align: left
|
2124
|
+
}
|
2125
|
+
|
2126
|
+
table.sfntTableData tr.uncompressed {
|
2127
|
+
background-color: #ffc3af;
|
2128
|
+
}
|
2129
|
+
|
2130
|
+
table.sfntTableData td {
|
2131
|
+
width: 20%;
|
2132
|
+
padding: 5px 0px 5px 0px;
|
2133
|
+
border: 1px solid #e5e5e5;
|
2134
|
+
border-left: none;
|
2135
|
+
border-right: none;
|
2136
|
+
font-family: Consolas, Menlo, "Vera Mono", Monaco, monospace;
|
2137
|
+
}
|
2138
|
+
|
2139
|
+
pre {
|
2140
|
+
font-size: 12px;
|
2141
|
+
font-family: Consolas, Menlo, "Vera Mono", Monaco, monospace;
|
2142
|
+
margin: 0px;
|
2143
|
+
padding: 0px;
|
2144
|
+
}
|
2145
|
+
|
2146
|
+
/* Metadata */
|
2147
|
+
|
2148
|
+
.metadataElement {
|
2149
|
+
background: rgba(0, 0, 0, 0.03);
|
2150
|
+
margin: 10px 0px 10px 0px;
|
2151
|
+
border: 2px solid #d8d8d8;
|
2152
|
+
padding: 10px;
|
2153
|
+
}
|
2154
|
+
|
2155
|
+
h5.metadata {
|
2156
|
+
font-size: 14px;
|
2157
|
+
margin: 5px 0px 10px 0px;
|
2158
|
+
padding: 0px 0px 5px 0px;
|
2159
|
+
border-bottom: 1px solid #d8d8d8;
|
2160
|
+
}
|
2161
|
+
|
2162
|
+
h6.metadata {
|
2163
|
+
font-size: 12px;
|
2164
|
+
font-weight: normal;
|
2165
|
+
margin: 10px 0px 10px 0px;
|
2166
|
+
padding: 0px 0px 5px 0px;
|
2167
|
+
border-bottom: 1px solid #d8d8d8;
|
2168
|
+
}
|
2169
|
+
|
2170
|
+
table.metadata {
|
2171
|
+
font-size: 12px;
|
2172
|
+
width: 100%;
|
2173
|
+
border-collapse: collapse;
|
2174
|
+
padding: 0px;
|
2175
|
+
}
|
2176
|
+
|
2177
|
+
table.metadata td.key {
|
2178
|
+
width: 5em;
|
2179
|
+
padding: 5px 5px 5px 0px;
|
2180
|
+
border-right: 1px solid #d8d8d8;
|
2181
|
+
text-align: right;
|
2182
|
+
vertical-align: top;
|
2183
|
+
}
|
2184
|
+
|
2185
|
+
table.metadata td.value {
|
2186
|
+
padding: 5px 0px 5px 5px;
|
2187
|
+
border-left: 1px solid #d8d8d8;
|
2188
|
+
text-align: left;
|
2189
|
+
vertical-align: top;
|
2190
|
+
}
|
2191
|
+
|
2192
|
+
p.metadata {
|
2193
|
+
font-size: 12px;
|
2194
|
+
font-style: italic;
|
2195
|
+
}
|
2196
|
+
}
|
2197
|
+
"""
|
2198
|
+
|
2199
|
+
defaultJavascript = """
|
2200
|
+
|
2201
|
+
//<![CDATA[
|
2202
|
+
function testResultToggleButtonHit(buttonID, className) {
|
2203
|
+
// change the button title
|
2204
|
+
var element = document.getElementById(buttonID);
|
2205
|
+
if (element.innerHTML == "Show" ) {
|
2206
|
+
element.innerHTML = "Hide";
|
2207
|
+
}
|
2208
|
+
else {
|
2209
|
+
element.innerHTML = "Show";
|
2210
|
+
}
|
2211
|
+
// toggle the elements
|
2212
|
+
var elements = getTestResults(className);
|
2213
|
+
for (var e = 0; e < elements.length; ++e) {
|
2214
|
+
toggleElement(elements[e]);
|
2215
|
+
}
|
2216
|
+
// toggle the info blocks
|
2217
|
+
toggleInfoBlocks();
|
2218
|
+
}
|
2219
|
+
|
2220
|
+
function getTestResults(className) {
|
2221
|
+
var rows = document.getElementsByTagName("tr");
|
2222
|
+
var found = Array();
|
2223
|
+
for (var r = 0; r < rows.length; ++r) {
|
2224
|
+
var row = rows[r];
|
2225
|
+
if (row.className == className) {
|
2226
|
+
found[found.length] = row;
|
2227
|
+
}
|
2228
|
+
}
|
2229
|
+
return found;
|
2230
|
+
}
|
2231
|
+
|
2232
|
+
function toggleElement(element) {
|
2233
|
+
if (element.style.display != "none" ) {
|
2234
|
+
element.style.display = "none";
|
2235
|
+
}
|
2236
|
+
else {
|
2237
|
+
element.style.display = "";
|
2238
|
+
}
|
2239
|
+
}
|
2240
|
+
|
2241
|
+
function toggleInfoBlocks() {
|
2242
|
+
var tables = document.getElementsByTagName("table")
|
2243
|
+
for (var t = 0; t < tables.length; ++t) {
|
2244
|
+
var table = tables[t];
|
2245
|
+
if (table.className == "report") {
|
2246
|
+
var haveVisibleRow = false;
|
2247
|
+
var rows = table.rows;
|
2248
|
+
for (var r = 0; r < rows.length; ++r) {
|
2249
|
+
var row = rows[r];
|
2250
|
+
if (row.style.display == "none") {
|
2251
|
+
var i = 0;
|
2252
|
+
}
|
2253
|
+
else {
|
2254
|
+
haveVisibleRow = true;
|
2255
|
+
}
|
2256
|
+
}
|
2257
|
+
var div = table.parentNode;
|
2258
|
+
if (haveVisibleRow == true) {
|
2259
|
+
div.style.display = "";
|
2260
|
+
}
|
2261
|
+
else {
|
2262
|
+
div.style.display = "none";
|
2263
|
+
}
|
2264
|
+
}
|
2265
|
+
}
|
2266
|
+
}
|
2267
|
+
//]]>
|
2268
|
+
"""
|
2269
|
+
|
2270
|
+
def startHTML(title=None, cssReplacements={}):
|
2271
|
+
writer = XMLWriter()
|
2272
|
+
# start the html
|
2273
|
+
writer.begintag("html", xmlns="http://www.w3.org/1999/xhtml", lang="en")
|
2274
|
+
# start the head
|
2275
|
+
writer.begintag("head")
|
2276
|
+
writer.simpletag("meta", http_equiv="Content-Type", content="text/html; charset=utf-8")
|
2277
|
+
# title
|
2278
|
+
if title is not None:
|
2279
|
+
writer.begintag("title")
|
2280
|
+
writer.write(title)
|
2281
|
+
writer.endtag("title")
|
2282
|
+
# write the css
|
2283
|
+
writer.begintag("style", type="text/css")
|
2284
|
+
css = defaultCSS
|
2285
|
+
for before, after in cssReplacements.items():
|
2286
|
+
css = css.replace(before, after)
|
2287
|
+
writer.write(css)
|
2288
|
+
writer.endtag("style")
|
2289
|
+
# write the javascript
|
2290
|
+
writer.begintag("script", type="text/javascript")
|
2291
|
+
javascript = defaultJavascript
|
2292
|
+
## hack around some ElementTree escaping
|
2293
|
+
javascript = javascript.replace("<", "l_e_s_s")
|
2294
|
+
javascript = javascript.replace(">", "g_r_e_a_t_e_r")
|
2295
|
+
writer.write(javascript)
|
2296
|
+
writer.endtag("script")
|
2297
|
+
# close the head
|
2298
|
+
writer.endtag("head")
|
2299
|
+
# start the body
|
2300
|
+
writer.begintag("body")
|
2301
|
+
# return the writer
|
2302
|
+
return writer
|
2303
|
+
|
2304
|
+
def finishHTML(writer):
|
2305
|
+
# close the body
|
2306
|
+
writer.endtag("body")
|
2307
|
+
# close the html
|
2308
|
+
writer.endtag("html")
|
2309
|
+
# get the text
|
2310
|
+
text = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
|
2311
|
+
text += writer.compile()
|
2312
|
+
text = text.replace("c_l_a_s_s", "class")
|
2313
|
+
text = text.replace("a_p_o_s_t_r_o_p_h_e", "'")
|
2314
|
+
text = text.replace("l_e_s_s", "<")
|
2315
|
+
text = text.replace("g_r_e_a_t_e_r", ">")
|
2316
|
+
text = text.replace("http_equiv", "http-equiv")
|
2317
|
+
# return
|
2318
|
+
return text
|
2319
|
+
|
2320
|
+
# ------------------
|
2321
|
+
# Support: Unpackers
|
2322
|
+
# ------------------
|
2323
|
+
|
2324
|
+
def unpackHeader(data):
|
2325
|
+
return structUnpack(headerFormat, data)[0]
|
2326
|
+
|
2327
|
+
def unpackDirectory(data):
|
2328
|
+
header = unpackHeader(data)
|
2329
|
+
numTables = header["numTables"]
|
2330
|
+
data = data[headerSize:]
|
2331
|
+
directory = []
|
2332
|
+
for index in range(numTables):
|
2333
|
+
table, data = structUnpack(directoryFormat, data)
|
2334
|
+
directory.append(table)
|
2335
|
+
return directory
|
2336
|
+
|
2337
|
+
def unpackTableData(data):
|
2338
|
+
directory = unpackDirectory(data)
|
2339
|
+
tables = {}
|
2340
|
+
for entry in directory:
|
2341
|
+
tag = entry["tag"]
|
2342
|
+
offset = entry["offset"]
|
2343
|
+
origLength = entry["origLength"]
|
2344
|
+
compLength = entry["compLength"]
|
2345
|
+
if offset > len(data) or offset < 0 or (offset + compLength) < 0:
|
2346
|
+
tableData = ""
|
2347
|
+
elif offset + compLength > len(data):
|
2348
|
+
tableData = data[offset:]
|
2349
|
+
else:
|
2350
|
+
tableData = data[offset:offset+compLength]
|
2351
|
+
if compLength < origLength:
|
2352
|
+
try:
|
2353
|
+
td = zlib.decompress(tableData)
|
2354
|
+
tableData = td
|
2355
|
+
except zlib.error:
|
2356
|
+
tableData = None
|
2357
|
+
tables[tag] = tableData
|
2358
|
+
return tables
|
2359
|
+
|
2360
|
+
def unpackMetadata(data, decompress=True, parse=True):
|
2361
|
+
header = unpackHeader(data)
|
2362
|
+
data = data[header["metaOffset"]:header["metaOffset"]+header["metaLength"]]
|
2363
|
+
if decompress and data:
|
2364
|
+
data = zlib.decompress(data)
|
2365
|
+
if parse and data:
|
2366
|
+
data = ElementTree.fromstring(data)
|
2367
|
+
return data
|
2368
|
+
|
2369
|
+
def unpackPrivateData(data):
|
2370
|
+
header = unpackHeader(data)
|
2371
|
+
data = data[header["privOffset"]:header["privOffset"]+header["privLength"]]
|
2372
|
+
return data
|
2373
|
+
|
2374
|
+
# -----------------------
|
2375
|
+
# Support: Report Helpers
|
2376
|
+
# -----------------------
|
2377
|
+
|
2378
|
+
def findUniqueFileName(path):
|
2379
|
+
if not os.path.exists(path):
|
2380
|
+
return path
|
2381
|
+
folder = os.path.dirname(path)
|
2382
|
+
fileName = os.path.basename(path)
|
2383
|
+
fileName, extension = os.path.splitext(fileName)
|
2384
|
+
stamp = time.strftime("%Y-%m-%d %H-%M-%S %Z")
|
2385
|
+
newFileName = "%s (%s)%s" % (fileName, stamp, extension)
|
2386
|
+
newPath = os.path.join(folder, newFileName)
|
2387
|
+
# intentionally break to prevent a file overwrite.
|
2388
|
+
# this could happen if the user has a directory full
|
2389
|
+
# of files with future time stamped file names.
|
2390
|
+
# not likely, but avoid it all the same.
|
2391
|
+
assert not os.path.exists(newPath)
|
2392
|
+
return newPath
|
2393
|
+
|
2394
|
+
|
2395
|
+
# ---------------
|
2396
|
+
# Public Function
|
2397
|
+
# ---------------
|
2398
|
+
|
2399
|
+
tests = [
|
2400
|
+
("Header", testHeader),
|
2401
|
+
("Data Blocks", testDataBlocks),
|
2402
|
+
("Table Directory", testTableDirectory),
|
2403
|
+
("Table Data", testTableData),
|
2404
|
+
("Metadata", testMetadata)
|
2405
|
+
]
|
2406
|
+
|
2407
|
+
def validateFont(path, options, writeFile=True):
|
2408
|
+
# start the reporter
|
2409
|
+
if options.outputFormat == "html":
|
2410
|
+
reporter = HTMLReporter()
|
2411
|
+
elif options.outputFormat == "text":
|
2412
|
+
reporter = TextReporter()
|
2413
|
+
else:
|
2414
|
+
raise NotImplementedError
|
2415
|
+
# log the title
|
2416
|
+
reporter.logTitle("Report: %s" % os.path.basename(path))
|
2417
|
+
# log fileinfo
|
2418
|
+
reporter.logFileInfo("FILE", os.path.basename(path))
|
2419
|
+
reporter.logFileInfo("DIRECTORY", os.path.dirname(path))
|
2420
|
+
# run tests and log results
|
2421
|
+
f = open(path, "rb")
|
2422
|
+
data = f.read()
|
2423
|
+
f.close()
|
2424
|
+
shouldStop = False
|
2425
|
+
for title, func in tests:
|
2426
|
+
# skip groups that are not specified in the options
|
2427
|
+
if options.testGroups and title not in options.testGroups:
|
2428
|
+
continue
|
2429
|
+
reporter.logTestTitle(title)
|
2430
|
+
shouldStop = func(data, reporter)
|
2431
|
+
if shouldStop:
|
2432
|
+
break
|
2433
|
+
reporter.haveReadError = shouldStop
|
2434
|
+
# get the report
|
2435
|
+
report = reporter.getReport()
|
2436
|
+
# write
|
2437
|
+
reportPath = None
|
2438
|
+
if writeFile:
|
2439
|
+
# make the output file name
|
2440
|
+
if options.outputFileName is not None:
|
2441
|
+
fileName = options.outputFileName
|
2442
|
+
else:
|
2443
|
+
fileName = os.path.splitext(os.path.basename(path))[0]
|
2444
|
+
fileName += "_validate"
|
2445
|
+
if options.outputFormat == "html":
|
2446
|
+
fileName += ".html"
|
2447
|
+
else:
|
2448
|
+
fileName += ".txt"
|
2449
|
+
# make the output directory
|
2450
|
+
if options.outputDirectory is not None:
|
2451
|
+
directory = options.outputDirectory
|
2452
|
+
else:
|
2453
|
+
directory = os.path.dirname(path)
|
2454
|
+
# write the file
|
2455
|
+
reportPath = os.path.join(directory, fileName)
|
2456
|
+
reportPath = findUniqueFileName(reportPath)
|
2457
|
+
f = open(reportPath, "wb")
|
2458
|
+
f.write(report)
|
2459
|
+
f.close()
|
2460
|
+
return reportPath, report, reporter.numErrors()
|
2461
|
+
|
2462
|
+
# --------------------
|
2463
|
+
# Command Line Behvior
|
2464
|
+
# --------------------
|
2465
|
+
|
2466
|
+
usage = "%prog [options] fontpath1 fontpath2"
|
2467
|
+
|
2468
|
+
description = """This tool examines the structure of one
|
2469
|
+
or more WOFF files and issues a detailed report about
|
2470
|
+
the validity of the file structure. It does not validate
|
2471
|
+
the wrapped font data.
|
2472
|
+
"""
|
2473
|
+
|
2474
|
+
def main():
|
2475
|
+
parser = optparse.OptionParser(usage=usage, description=description, version="%prog 0.1beta")
|
2476
|
+
parser.add_option("-d", dest="outputDirectory", help="Output directory. The default is to output the report into the same directory as the font file.")
|
2477
|
+
parser.add_option("-o", dest="outputFileName", help="Output file name. The default is \"fontfilename_validate.html\".")
|
2478
|
+
parser.add_option("-f", dest="outputFormat", help="Output format, text|html. The default is html.", default="html")
|
2479
|
+
parser.add_option("-q", dest="quiet", action="store_true", help="No report written", default=False)
|
2480
|
+
|
2481
|
+
|
2482
|
+
parser.set_defaults(excludeTests=[])
|
2483
|
+
(options, args) = parser.parse_args()
|
2484
|
+
outputDirectory = options.outputDirectory
|
2485
|
+
options.testGroups = None # don't expose this to the commandline. it's for testing only.
|
2486
|
+
if outputDirectory is not None and not os.path.exists(outputDirectory):
|
2487
|
+
print "Directory does not exist:", outputDirectory
|
2488
|
+
sys.exit()
|
2489
|
+
for fontPath in args:
|
2490
|
+
if not os.path.exists(fontPath):
|
2491
|
+
print "File does not exist:", fontPath
|
2492
|
+
sys.exit()
|
2493
|
+
else:
|
2494
|
+
if not options.quiet: print "Testing: %s..." % fontPath
|
2495
|
+
writeFile = True if not options.quiet else False
|
2496
|
+
fontPath = fontPath.decode("utf-8")
|
2497
|
+
outputPath, report, numErrors = validateFont(fontPath, options, writeFile)
|
2498
|
+
if not options.quiet: print "Wrote report to: %s" % outputPath
|
2499
|
+
# exit code
|
2500
|
+
if numErrors > 0: sys.exit(1)
|
2501
|
+
|
2502
|
+
|
2503
|
+
if __name__ == "__main__":
|
2504
|
+
main()
|