1#!/usr/bin/python
2##
3## license:BSD-3-Clause
4## copyright-holders:Vas Crabb
5
6import os
7import os.path
8import re
9import sys
10import xml.sax
11import xml.sax.saxutils
12import zlib
13
14
15# workaround for version incompatibility
16if sys.version_info > (3, ):
17    long = int
18
19
20class ErrorHandler(object):
21    def __init__(self, **kwargs):
22        super(ErrorHandler, self).__init__(**kwargs)
23        self.errors = 0
24        self.warnings = 0
25
26    def error(self, exception):
27        self.errors += 1
28        sys.stderr.write('error: %s' % (exception))
29
30    def fatalError(self, exception):
31        raise exception
32
33    def warning(self, exception):
34        self.warnings += 1
35        sys.stderr.write('warning: %s' % (exception))
36
37
38class Minifyer(object):
39    def __init__(self, output, **kwargs):
40        super(Minifyer, self).__init__(**kwargs)
41
42        self.output = output
43        self.incomplete_tag = False
44        self.element_content = ''
45
46    def setDocumentLocator(self, locator):
47        pass
48
49    def startDocument(self):
50        self.output('<?xml version="1.0"?>')
51
52    def endDocument(self):
53        self.output('\n')
54
55    def startElement(self, name, attrs):
56        self.flushElementContent()
57        if self.incomplete_tag:
58            self.output('>')
59        self.output('<%s' % (name))
60        for name in attrs.getNames():
61            self.output(' %s=%s' % (name, xml.sax.saxutils.quoteattr(attrs[name])))
62        self.incomplete_tag = True
63
64    def endElement(self, name):
65        self.flushElementContent()
66        if self.incomplete_tag:
67            self.output('/>')
68        else:
69            self.output('</%s>' % (name))
70        self.incomplete_tag = False
71
72    def characters(self, content):
73        self.element_content += content
74
75    def ignorableWhitespace(self, whitespace):
76        pass
77
78    def processingInstruction(self, target, data):
79        pass
80
81    def flushElementContent(self):
82        self.element_content = self.element_content.strip()
83        if self.element_content:
84            if self.incomplete_tag:
85                self.output('>')
86                self.incomplete_tag = False
87            self.output(xml.sax.saxutils.escape(self.element_content))
88            self.element_content = ''
89
90
91class XmlError(Exception):
92    pass
93
94
95class LayoutChecker(Minifyer):
96    BADTAGPATTERN = re.compile('[^abcdefghijklmnopqrstuvwxyz0123456789_.:^$]')
97    VARPATTERN = re.compile('^.*~[0-9A-Za-z_]+~.*$')
98    FLOATCHARS = re.compile('^.*[.eE].*$')
99    SHAPES = frozenset(('disk', 'led14seg', 'led14segsc', 'led16seg', 'led16segsc', 'led7seg', 'led8seg_gts1', 'rect'))
100    ORIENTATIONS = frozenset((0, 90, 180, 270))
101    YESNO = frozenset(('yes', 'no'))
102    BLENDMODES = frozenset(('none', 'alpha', 'multiply', 'add'))
103
104    def __init__(self, output, **kwargs):
105        super(LayoutChecker, self).__init__(output=output, **kwargs)
106        self.locator = None
107        self.errors = 0
108        self.elements = { }
109        self.groups = { }
110        self.views = { }
111        self.referenced_elements = { }
112        self.referenced_groups = { }
113        self.group_collections = { }
114        self.current_collections = None
115
116    def formatLocation(self):
117        return '%s:%d:%d' % (self.locator.getSystemId(), self.locator.getLineNumber(), self.locator.getColumnNumber())
118
119    def handleError(self, msg):
120        self.errors += 1
121        sys.stderr.write('error: %s: %s\n' % (self.formatLocation(), msg))
122
123    def checkIntAttribute(self, name, attrs, key, default):
124        if key not in attrs:
125            return default
126        val = attrs[key]
127        if self.VARPATTERN.match(val):
128            return None
129        base = 10
130        offs = 0
131        if (len(val) >= 1) and ('$' == val[0]):
132            base = 16
133            offs = 1
134        elif (len(val) >= 2) and ('0' == val[0]) and (('x' == val[1]) or ('X' == val[1])):
135            base = 16
136            offs = 2
137        elif (len(val) >= 1) and ('#' == val[0]):
138            offs = 1
139        try:
140            return int(val[offs:], base)
141        except:
142            self.handleError('Element %s attribute %s "%s" is not an integer' % (name, key, val))
143            return None
144
145    def checkFloatAttribute(self, name, attrs, key, default):
146        if key not in attrs:
147            return default
148        val = attrs[key]
149        if self.VARPATTERN.match(val):
150            return None
151        try:
152            return float(val)
153        except:
154            self.handleError('Element %s attribute %s "%s" is not a floating point number' % (name, key, val))
155            return None
156
157    def checkNumericAttribute(self, name, attrs, key, default):
158        if key not in attrs:
159            return default
160        val = attrs[key]
161        if self.VARPATTERN.match(val):
162            return None
163        base = 0
164        offs = 0
165        try:
166            if (len(val) >= 1) and ('$' == val[0]):
167                base = 16
168                offs = 1
169            elif (len(val) >= 2) and ('0' == val[0]) and (('x' == val[1]) or ('X' == val[1])):
170                base = 16
171                offs = 2
172            elif (len(val) >= 1) and ('#' == val[0]):
173                base = 10
174                offs = 1
175            elif self.FLOATCHARS.match(val):
176                return float(val)
177            return int(val[offs:], base)
178        except:
179            self.handleError('Element %s attribute %s "%s" is not a number' % (name, key, val))
180            return None
181
182    def checkParameter(self, attrs):
183        if 'name' not in attrs:
184            self.handleError('Element param missing attribute name')
185        else:
186            name = attrs['name']
187        self.checkNumericAttribute('param', attrs, 'increment', None)
188        lshift = self.checkIntAttribute('param', attrs, 'lshift', None)
189        if (lshift is not None) and (0 > lshift):
190            self.handleError('Element param attribute lshift "%s" is negative' % (attrs['lshift'], ))
191        rshift = self.checkIntAttribute('param', attrs, 'rshift', None)
192        if (rshift is not None) and (0 > rshift):
193            self.handleError('Element param attribute rshift "%s" is negative' % (attrs['rshift'], ))
194        if self.repeat_depth and self.repeat_depth[-1]:
195            if 'start' in attrs:
196                if 'value' in attrs:
197                    self.handleError('Element param has both start and value attributes')
198                if 'name' in attrs:
199                    if name not in self.variable_scopes[-1]:
200                        self.variable_scopes[-1][name] = True
201                    elif not self.VARPATTERN.match(name):
202                        self.handleError('Generator parameter "%s" redefined' % (name, ))
203            else:
204                if 'value' not in attrs:
205                    self.handleError('Element param missing attribute value')
206                if ('increment' in attrs) or ('lshift' in attrs) or ('rshift' in attrs):
207                    self.handleError('Element param has increment/lshift/rshift attribute(s) without start attribute')
208                if 'name' in attrs:
209                    if not self.variable_scopes[-1].get(name, False):
210                        self.variable_scopes[-1][name] = False
211                    elif not self.VARPATTERN.match(name):
212                        self.handleError('Generator parameter "%s" redefined' % (name, ))
213        else:
214            if ('start' in attrs) or ('increment' in attrs) or ('lshift' in attrs) or ('rshift' in attrs):
215                self.handleError('Element param with start/increment/lshift/rshift attribute(s) not in repeat scope')
216            if 'value' not in attrs:
217                self.handleError('Element param missing attribute value')
218            if 'name' in attrs:
219                self.variable_scopes[-1][attrs['name']] = False
220
221    def checkBounds(self, attrs):
222        left = self.checkFloatAttribute('bounds', attrs, 'left', 0.0)
223        top = self.checkFloatAttribute('bounds', attrs, 'top', 0.0)
224        right = self.checkFloatAttribute('bounds', attrs, 'right', 1.0)
225        bottom = self.checkFloatAttribute('bounds', attrs, 'bottom', 1.0)
226        x = self.checkFloatAttribute('bounds', attrs, 'x', 0.0)
227        y = self.checkFloatAttribute('bounds', attrs, 'y', 0.0)
228        xc = self.checkFloatAttribute('bounds', attrs, 'xc', 0.0)
229        yc = self.checkFloatAttribute('bounds', attrs, 'yc', 0.0)
230        width = self.checkFloatAttribute('bounds', attrs, 'width', 1.0)
231        height = self.checkFloatAttribute('bounds', attrs, 'height', 1.0)
232        if (left is not None) and (right is not None) and (left > right):
233            self.handleError('Element bounds attribute left "%s" is greater than attribute right "%s"' % (
234                    attrs.get('left', 0.0),
235                    attrs.get('right', 1.0)))
236        if (top is not None) and (bottom is not None) and (top > bottom):
237            self.handleError('Element bounds attribute top "%s" is greater than attribute bottom "%s"' % (
238                    attrs.get('top', 0.0),
239                    attrs.get('bottom', 1.0)))
240        if (width is not None) and (0.0 > width):
241            self.handleError('Element bounds attribute width "%s" is negative' % (attrs['width'], ))
242        if (height is not None) and (0.0 > height):
243            self.handleError('Element bounds attribute height "%s" is negative' % (attrs['height'], ))
244        if (('left' in attrs) and (('x' in attrs) or ('xc' in attrs))) or (('x' in attrs) and ('xc' in attrs)):
245            self.handleError('Element bounds has multiple horizontal origin attributes (left/x/xc)')
246        if (('left' in attrs) and ('width' in attrs)) or ((('x' in attrs) or ('xc' in attrs)) and ('right' in attrs)):
247            self.handleError('Element bounds has both left/right and x/xc/width attributes')
248        if (('top' in attrs) and (('y' in attrs) or ('yc' in attrs))) or (('y' in attrs) and ('yc' in attrs)):
249            self.handleError('Element bounds has multiple vertical origin attributes (top/y/yc)')
250        if (('top' in attrs) and ('height' in attrs)) or ((('y' in attrs) or ('yc' in attrs)) and ('bottom' in attrs)):
251            self.handleError('Element bounds has both top/bottom and y/yc/height attributes')
252
253    def checkOrientation(self, attrs):
254        if self.have_orientation[-1]:
255            self.handleError('Duplicate element orientation')
256        else:
257            self.have_orientation[-1] = True
258        if self.checkIntAttribute('orientation', attrs, 'rotate', 0) not in self.ORIENTATIONS:
259            self.handleError('Element orientation attribute rotate "%s" is unsupported' % (attrs['rotate'], ))
260        for name in ('swapxy', 'flipx', 'flipy'):
261            if (attrs.get(name, 'no') not in self.YESNO) and (not self.VARPATTERN.match(attrs['yesno'])):
262                self.handleError('Element orientation attribute %s "%s" is not "yes" or "no"' % (name, attrs[name]))
263
264    def checkColor(self, attrs):
265        self.checkColorChannel(attrs, 'red')
266        self.checkColorChannel(attrs, 'green')
267        self.checkColorChannel(attrs, 'blue')
268        self.checkColorChannel(attrs, 'alpha')
269
270    def checkColorChannel(self, attrs, name):
271        channel = self.checkFloatAttribute('color', attrs, name, None)
272        if (channel is not None) and ((0.0 > channel) or (1.0 < channel)):
273            self.handleError('Element color attribute %s "%s" outside valid range 0.0-1.0' % (name, attrs[name]))
274
275    def checkTag(self, tag, element, attr):
276        if '' == tag:
277            self.handleError('Element %s attribute %s is empty' % (element, attr))
278        else:
279            if tag.find('^') >= 0:
280                self.handleError('Element %s attribute %s "%s" contains parent device reference' % (element, attr, tag))
281            if ':' == tag[-1]:
282                self.handleError('Element %s attribute %s "%s" ends with separator' % (element, attr, tag))
283            if tag.find('::') >= 0:
284                self.handleError('Element %s attribute %s "%s" contains double separator' % (element, attr, tag))
285
286    def checkComponent(self, name, attrs):
287        statemask = self.checkIntAttribute(name, attrs, 'statemask', None)
288        stateval = self.checkIntAttribute(name, attrs, 'state', None)
289        if stateval is not None:
290            if 0 > stateval:
291                self.handleError('Element %s attribute state "%s" is negative' % (name, attrs['state']))
292            if (statemask is not None) and (stateval & ~statemask):
293                self.handleError('Element %s attribute state "%s" has bits set that are clear in attribute statemask "%s"' % (name, attrs['state'], attrs['statemask']))
294        if 'image' == name:
295            self.handlers.append((self.imageComponentStartHandler, self.imageComponentEndHandler))
296        else:
297            self.handlers.append((self.componentStartHandler, self.componentEndHandler))
298        self.have_bounds.append({ })
299        self.have_color.append({ })
300
301    def checkViewItem(self, name, attrs):
302        if ('blend' in attrs) and (attrs['blend'] not in self.BLENDMODES) and not self.VARPATTERN.match(attrs['blend']):
303            self.handleError('Element %s attribute blend "%s" is unsupported' % (name, attrs['blend']))
304        if 'inputtag' in attrs:
305            if 'inputmask' not in attrs:
306                self.handleError('Element %s has inputtag attribute without inputmask attribute' % (name, ))
307            self.checkTag(attrs['inputtag'], name, 'inputtag')
308        elif 'inputmask' in attrs:
309            self.handleError('Element %s has inputmask attribute without inputtag attribute' % (name, ))
310        inputraw = None
311        if 'inputraw' in attrs:
312            if (attrs['inputraw'] not in self.YESNO) and (not self.VARPATTERN.match(attrs['inputraw'])):
313                self.handleError('Element %s attribute inputraw "%s" is not "yes" or "no"' % (name, attrs['inputraw']))
314            else:
315                inputraw = 'yes' == attrs['inputraw']
316            if 'inputmask' not in attrs:
317                self.handleError('Element %s has inputraw attribute without inputmask attribute' % (name, ))
318            if 'inputtag' not in attrs:
319                self.handleError('Element %s has inputraw attribute without inputtag attribute' % (name, ))
320        inputmask = self.checkIntAttribute(name, attrs, 'inputmask', None)
321        if (inputmask is not None) and (not inputmask):
322            if (inputraw is None) or (not inputraw):
323                self.handleError('Element %s attribute inputmask "%s" is zero' % (name, attrs['inputmask']))
324
325    def startViewItem(self, name):
326        self.handlers.append((self.viewItemStartHandler, self.viewItemEndHandler))
327        self.have_bounds.append(None if 'group' == name else { })
328        self.have_orientation.append(False)
329        self.have_color.append(None if 'group' == name else { })
330
331    def rootStartHandler(self, name, attrs):
332        if 'mamelayout' != name:
333            self.ignored_depth = 1
334            self.handleError('Expected root element mamelayout but found %s' % (name, ))
335        else:
336            if 'version' not in attrs:
337                self.handleError('Element mamelayout missing attribute version')
338            else:
339                try:
340                    long(attrs['version'])
341                except:
342                    self.handleError('Element mamelayout attribute version "%s" is not an integer' % (attrs['version'], ))
343            self.variable_scopes.append({ })
344            self.repeat_depth.append(0)
345            self.handlers.append((self.layoutStartHandler, self.layoutEndHandler))
346
347    def rootEndHandler(self, name, attrs):
348        pass # should be unreachable
349
350    def layoutStartHandler(self, name, attrs):
351        if 'element' == name:
352            if 'name' not in attrs:
353                self.handleError('Element element missing attribute name')
354            else:
355                generated_name = self.VARPATTERN.match(attrs['name'])
356                if generated_name:
357                    self.generated_element_names = True
358                if attrs['name'] not in self.elements:
359                    self.elements[attrs['name']] = self.formatLocation()
360                elif not generated_name:
361                    self.handleError('Element element has duplicate name (previous %s)' % (self.elements[attrs['name']], ))
362            defstate = self.checkIntAttribute(name, attrs, 'defstate', None)
363            if (defstate is not None) and (0 > defstate):
364                self.handleError('Element element attribute defstate "%s" is negative' % (attrs['defstate'], ))
365            self.handlers.append((self.elementStartHandler, self.elementEndHandler))
366        elif 'group' == name:
367            self.current_collections = { }
368            if 'name' not in attrs:
369                self.handleError('Element group missing attribute name')
370            else:
371                generated_name = self.VARPATTERN.match(attrs['name'])
372                if generated_name:
373                    self.generated_group_names = True
374                if attrs['name'] not in self.groups:
375                    self.groups[attrs['name']] = self.formatLocation()
376                    if not generated_name:
377                        self.group_collections[attrs['name']] = self.current_collections
378                elif not generated_name:
379                    self.handleError('Element group has duplicate name (previous %s)' % (self.groups[attrs['name']], ))
380            self.handlers.append((self.groupViewStartHandler, self.groupViewEndHandler))
381            self.variable_scopes.append({ })
382            self.repeat_depth.append(0)
383            self.have_bounds.append(None)
384        elif ('view' == name) and (not self.repeat_depth[-1]):
385            self.current_collections = { }
386            if 'name' not in attrs:
387                self.handleError('Element view missing attribute name')
388            else:
389                if attrs['name'] not in self.views:
390                    self.views[attrs['name']] = self.formatLocation()
391                elif not self.VARPATTERN.match(attrs['name']):
392                    self.handleError('Element view has duplicate name (previous %s)' % (self.views[attrs['name']], ))
393            self.handlers.append((self.groupViewStartHandler, self.groupViewEndHandler))
394            self.variable_scopes.append({ })
395            self.repeat_depth.append(0)
396            self.have_bounds.append(None)
397        elif 'repeat' == name:
398            if 'count' not in attrs:
399                self.handleError('Element repeat missing attribute count')
400            else:
401                count = self.checkIntAttribute(name, attrs, 'count', None)
402                if (count is not None) and (0 >= count):
403                    self.handleError('Element repeat attribute count "%s" is not positive' % (attrs['count'], ))
404            self.variable_scopes.append({ })
405            self.repeat_depth[-1] += 1
406        elif 'param' == name:
407            self.checkParameter(attrs)
408            self.ignored_depth = 1
409        elif ('script' == name) and (not self.repeat_depth[-1]):
410            self.ignored_depth = 1
411        else:
412            self.handleError('Encountered unexpected element %s' % (name, ))
413            self.ignored_depth = 1
414
415    def layoutEndHandler(self, name):
416        self.variable_scopes.pop()
417        if self.repeat_depth[-1]:
418            self.repeat_depth[-1] -= 1
419        else:
420            if not self.generated_element_names:
421                for element in self.referenced_elements:
422                    if (element not in self.elements) and (not self.VARPATTERN.match(element)):
423                        self.handleError('Element "%s" not found (first referenced at %s)' % (element, self.referenced_elements[element]))
424            if not self.generated_group_names:
425                for group in self.referenced_groups:
426                    if (group not in self.groups) and (not self.VARPATTERN.match(group)):
427                        self.handleError('Group "%s" not found (first referenced at %s)' % (group, self.referenced_groups[group]))
428            if not self.views:
429                self.handleError('No view elements found')
430            self.handlers.pop()
431
432    def elementStartHandler(self, name, attrs):
433        if name in self.SHAPES:
434            self.checkComponent(name, attrs)
435        elif 'text' == name:
436            if 'string' not in attrs:
437                self.handleError('Element text missing attribute string')
438            align = self.checkIntAttribute(name, attrs, 'align', None)
439            if (align is not None) and ((0 > align) or (2 < align)):
440                self.handleError('Element text attribute align "%s" not in valid range 0-2' % (attrs['align'], ))
441            self.checkComponent(name, attrs)
442        elif 'simplecounter' == name:
443            maxstate = self.checkIntAttribute(name, attrs, 'maxstate', None)
444            if (maxstate is not None) and (0 > maxstate):
445                self.handleError('Element simplecounter attribute maxstate "%s" is negative' % (attrs['maxstate'], ))
446            digits = self.checkIntAttribute(name, attrs, 'digits', None)
447            if (digits is not None) and (0 >= digits):
448                self.handleError('Element simplecounter attribute digits "%s" is not positive' % (attrs['digits'], ))
449            align = self.checkIntAttribute(name, attrs, 'align', None)
450            if (align is not None) and ((0 > align) or (2 < align)):
451                self.handleError('Element simplecounter attribute align "%s" not in valid range 0-2' % (attrs['align'], ))
452            self.checkComponent(name, attrs)
453        elif 'image' == name:
454            self.have_file = 'file' in attrs
455            self.have_data = None
456            self.checkComponent(name, attrs)
457        elif 'reel' == name:
458            # TODO: validate symbollist and improve validation of other attributes
459            self.checkIntAttribute(name, attrs, 'stateoffset', None)
460            numsymbolsvisible = self.checkIntAttribute(name, attrs, 'numsymbolsvisible', None)
461            if (numsymbolsvisible is not None) and (0 >= numsymbolsvisible):
462                self.handleError('Element reel attribute numsymbolsvisible "%s" not positive' % (attrs['numsymbolsvisible'], ))
463            reelreversed = self.checkIntAttribute(name, attrs, 'reelreversed', None)
464            if (reelreversed is not None) and ((0 > reelreversed) or (1 < reelreversed)):
465                self.handleError('Element reel attribute reelreversed "%s" not in valid range 0-1' % (attrs['reelreversed'], ))
466            beltreel = self.checkIntAttribute(name, attrs, 'beltreel', None)
467            if (beltreel is not None) and ((0 > beltreel) or (1 < beltreel)):
468                self.handleError('Element reel attribute beltreel "%s" not in valid range 0-1' % (attrs['beltreel'], ))
469            self.checkComponent(name, attrs)
470        else:
471            self.handleError('Encountered unexpected element %s' % (name, ))
472            self.ignored_depth = 1
473
474    def elementEndHandler(self, name):
475        self.handlers.pop()
476
477    def componentStartHandler(self, name, attrs):
478        if 'bounds' == name:
479            state = self.checkIntAttribute(name, attrs, 'state', 0)
480            if state is not None:
481                if 0 > state:
482                    self.handleError('Element bounds attribute state "%s" is negative' % (attrs['state'], ))
483                if state in self.have_bounds[-1]:
484                    self.handleError('Duplicate bounds for state %d (previous %s)' % (state, self.have_bounds[-1][state]))
485                else:
486                    self.have_bounds[-1][state] = self.formatLocation()
487            self.checkBounds(attrs)
488        elif 'color' == name:
489            state = self.checkIntAttribute(name, attrs, 'state', 0)
490            if state is not None:
491                if 0 > state:
492                    self.handleError('Element color attribute state "%s" is negative' % (attrs['state'], ))
493                if state in self.have_color[-1]:
494                    self.handleError('Duplicate color for state %d (previous %s)' % (state, self.have_color[-1][state]))
495                else:
496                    self.have_color[-1][state] = self.formatLocation()
497            self.checkColor(attrs)
498        self.ignored_depth = 1
499
500    def componentEndHandler(self, name):
501        self.have_bounds.pop()
502        self.have_color.pop()
503        self.handlers.pop()
504
505    def imageComponentStartHandler(self, name, attrs):
506        if 'data' == name:
507            if self.have_data is not None:
508                self.handleError('Element image has multiple data child elements (previous %s)' % (self.have_data))
509            else:
510                self.have_data = self.formatLocation()
511                if self.have_file:
512                    self.handleError('Element image has attribute file and child element data')
513            self.ignored_depth = 1
514        else:
515            self.componentStartHandler(name, attrs)
516
517    def imageComponentEndHandler(self, name):
518        if (not self.have_file) and (self.have_data is None):
519            self.handleError('Element image missing attribute file or child element data')
520        del self.have_file
521        del self.have_data
522        self.componentEndHandler(name)
523
524    def groupViewStartHandler(self, name, attrs):
525        if 'element' == name:
526            if 'ref' not in attrs:
527                self.handleError('Element %s missing attribute ref' % (name, ))
528            elif attrs['ref'] not in self.referenced_elements:
529                self.referenced_elements[attrs['ref']] = self.formatLocation()
530            self.checkViewItem(name, attrs)
531            self.startViewItem(name)
532        elif 'screen' == name:
533            if 'index' in attrs:
534                index = self.checkIntAttribute(name, attrs, 'index', None)
535                if (index is not None) and (0 > index):
536                    self.handleError('Element screen attribute index "%s" is negative' % (attrs['index'], ))
537                if 'tag' in attrs:
538                    self.handleError('Element screen has both index and tag attributes')
539            if 'tag' in attrs:
540                tag = attrs['tag']
541                self.checkTag(tag, name, 'tag')
542                if self.BADTAGPATTERN.search(tag):
543                    self.handleError('Element screen attribute tag "%s" contains invalid characters' % (tag, ))
544            self.checkViewItem(name, attrs)
545            self.startViewItem(name)
546        elif 'group' == name:
547            if 'ref' not in attrs:
548                self.handleError('Element group missing attribute ref')
549            else:
550                if attrs['ref'] not in self.referenced_groups:
551                    self.referenced_groups[attrs['ref']] = self.formatLocation()
552                if (not self.VARPATTERN.match(attrs['ref'])) and (attrs['ref'] in self.group_collections):
553                    for n, l in self.group_collections[attrs['ref']].items():
554                        if n not in self.current_collections:
555                            self.current_collections[n] = l
556                        else:
557                            self.handleError('Element group instantiates collection with duplicate name "%s" from %s (previous %s)' % (n, l, self.current_collections[n]))
558            self.startViewItem(name)
559        elif 'repeat' == name:
560            if 'count' not in attrs:
561                self.handleError('Element repeat missing attribute count')
562            else:
563                count = self.checkIntAttribute(name, attrs, 'count', None)
564                if (count is not None) and (0 >= count):
565                    self.handleError('Element repeat attribute count "%s" is negative' % (attrs['count'], ))
566            self.variable_scopes.append({ })
567            self.repeat_depth[-1] += 1
568        elif 'collection' == name:
569            if 'name' not in attrs:
570                self.handleError('Element collection missing attribute name')
571            elif not self.VARPATTERN.match(attrs['name']):
572                if attrs['name'] not in self.current_collections:
573                    self.current_collections[attrs['name']] = self.formatLocation()
574                else:
575                    self.handleError('Element collection has duplicate name (previous %s)' % (self.current_collections[attrs['name']], ))
576            if attrs.get('visible', 'yes') not in self.YESNO:
577                self.handleError('Element collection attribute visible "%s" is not "yes" or "no"' % (attrs['visible'], ))
578            self.variable_scopes.append({ })
579            self.collection_depth += 1
580        elif 'param' == name:
581            self.checkParameter(attrs)
582            self.ignored_depth = 1
583        elif 'bounds' == name:
584            if self.have_bounds[-1] is not None:
585                self.handleError('Duplicate element bounds (previous %s)' % (self.have_bounds[-1], ))
586            else:
587                self.have_bounds[-1] = self.formatLocation()
588            self.checkBounds(attrs)
589            if self.repeat_depth[-1]:
590                self.handleError('Element bounds inside repeat')
591            elif self.collection_depth:
592                self.handleError('Element bounds inside collection')
593            self.ignored_depth = 1
594        else:
595            self.handleError('Encountered unexpected element %s' % (name, ))
596            self.ignored_depth = 1
597
598    def groupViewEndHandler(self, name):
599        self.variable_scopes.pop()
600        if 'collection' == name:
601            self.collection_depth -= 1
602        elif self.repeat_depth[-1]:
603            self.repeat_depth[-1] -= 1
604        else:
605            self.current_collections = None
606            self.repeat_depth.pop()
607            self.have_bounds.pop()
608            self.handlers.pop()
609
610    def viewItemStartHandler(self, name, attrs):
611        if 'animate' == name:
612            if isinstance(self.have_bounds[-1], dict):
613                if 'inputtag' in attrs:
614                    if 'name' in attrs:
615                        self.handleError('Element animate has both attribute inputtag and attribute name')
616                    self.checkTag(attrs['inputtag'], name, 'inputtag')
617                elif 'name' not in attrs:
618                    self.handleError('Element animate has neither attribute inputtag nor attribute name')
619                self.checkIntAttribute(name, attrs, 'mask', None)
620            else:
621                self.handleError('Encountered unexpected element %s' % (name, ))
622        elif 'bounds' == name:
623            if self.have_bounds[-1] is None:
624                self.have_bounds[-1] = self.formatLocation()
625            elif isinstance(self.have_bounds[-1], dict):
626                state = self.checkIntAttribute(name, attrs, 'state', 0)
627                if state is not None:
628                    if 0 > state:
629                        self.handleError('Element bounds attribute state "%s" is negative' % (attrs['state'], ))
630                    if state in self.have_bounds[-1]:
631                        self.handleError('Duplicate bounds for state %d (previous %s)' % (state, self.have_bounds[-1][state]))
632                    else:
633                        self.have_bounds[-1][state] = self.formatLocation()
634            else:
635                self.handleError('Duplicate element bounds (previous %s)' % (self.have_bounds[-1], ))
636            self.checkBounds(attrs)
637        elif 'orientation' == name:
638            self.checkOrientation(attrs)
639        elif 'color' == name:
640            if self.have_color[-1] is None:
641                self.have_color[-1] = self.formatLocation()
642            elif isinstance(self.have_color[-1], dict):
643                state = self.checkIntAttribute(name, attrs, 'state', 0)
644                if state is not None:
645                    if 0 > state:
646                        self.handleError('Element color attribute state "%s" is negative' % (attrs['state'], ))
647                    if state in self.have_color[-1]:
648                        self.handleError('Duplicate color for state %d (previous %s)' % (state, self.have_color[-1][state]))
649                    else:
650                        self.have_color[-1][state] = self.formatLocation()
651            else:
652                self.handleError('Duplicate element color (previous %s)' % (self.have_color[-1], ))
653            self.checkColor(attrs)
654        else:
655            self.handleError('Encountered unexpected element %s' % (name, ))
656        self.ignored_depth = 1
657
658    def viewItemEndHandler(self, name):
659        self.have_bounds.pop()
660        self.have_orientation.pop()
661        self.have_color.pop()
662        self.handlers.pop()
663
664    def setDocumentLocator(self, locator):
665        self.locator = locator
666        super(LayoutChecker, self).setDocumentLocator(locator)
667
668    def startDocument(self):
669        self.handlers = [(self.rootStartHandler, self.rootEndHandler)]
670        self.ignored_depth = 0
671        self.variable_scopes = [ ]
672        self.repeat_depth = [ ]
673        self.collection_depth = 0
674        self.have_bounds = [ ]
675        self.have_orientation = [ ]
676        self.have_color = [ ]
677        self.generated_element_names = False
678        self.generated_group_names = False
679        super(LayoutChecker, self).startDocument()
680
681    def endDocument(self):
682        self.locator = None
683        self.elements.clear()
684        self.groups.clear()
685        self.views.clear()
686        self.referenced_elements.clear()
687        self.referenced_groups.clear()
688        self.group_collections.clear()
689        self.current_collections = None
690        del self.handlers
691        del self.ignored_depth
692        del self.variable_scopes
693        del self.repeat_depth
694        del self.collection_depth
695        del self.have_bounds
696        del self.have_orientation
697        del self.have_color
698        del self.generated_element_names
699        del self.generated_group_names
700        super(LayoutChecker, self).endDocument()
701
702    def startElement(self, name, attrs):
703        if 0 < self.ignored_depth:
704            self.ignored_depth += 1
705        else:
706            self.handlers[-1][0](name, attrs)
707        super(LayoutChecker, self).startElement(name, attrs)
708
709    def endElement(self, name):
710        if 0 < self.ignored_depth:
711            self.ignored_depth -= 1
712        else:
713            self.handlers[-1][1](name)
714        super(LayoutChecker, self).endElement(name)
715
716
717def compressLayout(src, dst, comp):
718    state = [0, 0]
719    def write(block):
720        for ch in bytearray(block):
721            if 0 == state[0]:
722                dst('\t')
723            elif 0 == (state[0] % 32):
724                dst(',\n\t')
725            else:
726                dst(', ')
727            state[0] += 1
728            dst('%3u' % (ch))
729
730    def output(text):
731        block = text.encode('UTF-8')
732        state[1] += len(block)
733        write(comp.compress(block))
734
735    error_handler = ErrorHandler()
736    content_handler = LayoutChecker(output)
737    parser = xml.sax.make_parser()
738    parser.setErrorHandler(error_handler)
739    parser.setContentHandler(content_handler)
740    try:
741        parser.parse(src)
742        write(comp.flush())
743        dst('\n')
744    except xml.sax.SAXException as exception:
745        print('fatal error: %s' % (exception))
746        raise XmlError('Fatal error parsing XML')
747    if (content_handler.errors > 0) or (error_handler.errors > 0) or (error_handler.warnings > 0):
748        raise XmlError('Error(s) and/or warning(s) parsing XML')
749
750    return state[1], state[0]
751
752
753class BlackHole(object):
754    def write(self, *args):
755        pass
756    def close(self):
757        pass
758
759
760if __name__ == '__main__':
761    if (len(sys.argv) > 4) or (len(sys.argv) < 2):
762        print('Usage:')
763        print('  complay <source.lay> [<output.h> [<varname>]]')
764        sys.exit(0 if len(sys.argv) <= 1 else 1)
765
766    srcfile = sys.argv[1]
767    dstfile = sys.argv[2] if len(sys.argv) >= 3 else None
768    if len(sys.argv) >= 4:
769        varname = sys.argv[3]
770    else:
771        varname = os.path.basename(srcfile)
772        base, ext = os.path.splitext(varname)
773        if ext.lower() == '.lay':
774            varname = base
775        varname = 'layout_' + re.sub('[^0-9A-Za-z_]', '_', varname)
776
777    comp_type = 'internal_layout::compression::ZLIB'
778    try:
779        dst = open(dstfile,'w') if dstfile is not None else BlackHole()
780        dst.write('static const unsigned char %s_data[] = {\n' % (varname))
781        byte_count, comp_size = compressLayout(srcfile, lambda x: dst.write(x), zlib.compressobj())
782        dst.write('};\n\n')
783        dst.write('const internal_layout %s = {\n' % (varname))
784        dst.write('\t%d, sizeof(%s_data), %s, %s_data\n' % (byte_count, varname, comp_type, varname))
785        dst.write('};\n')
786        dst.close()
787    except XmlError:
788        dst.close()
789        if dstfile is not None:
790            os.remove(dstfile)
791        sys.exit(2)
792    except IOError:
793        sys.stderr.write("Unable to open output file '%s'\n" % dstfile)
794        dst.close()
795        if dstfile is not None:
796            os.remove(dstfile)
797        sys.exit(3)
798