summaryrefslogtreecommitdiffstats
path: root/scripts/build/complay.py
diff options
context:
space:
mode:
author Vas Crabb <vas@vastheman.com>2017-07-08 18:30:43 +1000
committer Vas Crabb <vas@vastheman.com>2017-07-08 18:30:43 +1000
commitd97c4f9d9f3fe7eac01645497132467f533e0873 (patch)
tree28f3ce0d971a8e418c96fd45de1616c8ecdd9d75 /scripts/build/complay.py
parent81269d8fbfd646be1e595ecd11909818616fc49d (diff)
Do substantial validation of internal layouts at build time
Diffstat (limited to 'scripts/build/complay.py')
-rw-r--r--scripts/build/complay.py204
1 files changed, 202 insertions, 2 deletions
diff --git a/scripts/build/complay.py b/scripts/build/complay.py
index 4eecaa97353..cc9869c590c 100644
--- a/scripts/build/complay.py
+++ b/scripts/build/complay.py
@@ -4,6 +4,7 @@
## copyright-holders:Vas Crabb
import os
+import re
import sys
import xml.sax
import xml.sax.saxutils
@@ -85,6 +86,205 @@ class XmlError(Exception):
pass
+class LayoutError(Exception):
+ def __init__(self, msg, locator):
+ super(LayoutError, self).__init__(
+ '%s:%d:%d: %s' % (locator.getPublicId(), locator.getLineNumber(), locator.getColumnNumber(), msg));
+
+
+class LayoutChecker(Minifyer):
+ VARPATTERN = re.compile('^~scr(0|[1-9][0-9]*)(native[xy]aspect|width|height)~$')
+ SHAPES = frozenset(('disk', 'led16seg', 'led7seg', 'rect'))
+ OBJECTS = frozenset(('backdrop', 'bezel', 'cpanel', 'marquee', 'overlay'))
+
+ def __init__(self, output, **kwargs):
+ super(LayoutChecker, self).__init__(output=output, **kwargs)
+ self.locator = None
+ self.errors = 0
+ self.elements = { }
+ self.views = { }
+ self.referenced = { }
+
+ def formatLocation(self):
+ return '%s:%d:%d' % (self.locator.getSystemId(), self.locator.getLineNumber(), self.locator.getColumnNumber())
+
+ def handleError(self, msg):
+ self.errors += 1
+ sys.stderr.write('error: %s: %s\n' % (self.formatLocation(), msg))
+
+ def checkBoundsDimension(self, attrs, name):
+ if name in attrs:
+ try:
+ return float(attrs[name])
+ except:
+ if not self.VARPATTERN.match(attrs[name]):
+ self.handleError('Element bounds attribute %s "%s" is not numeric' % (name, attrs[name]))
+ return None
+
+ def checkBounds(self, attrs):
+ left = self.checkBoundsDimension(attrs, 'left')
+ top = self.checkBoundsDimension(attrs, 'top')
+ right = self.checkBoundsDimension(attrs, 'right')
+ bottom = self.checkBoundsDimension(attrs, 'bottom')
+ x = self.checkBoundsDimension(attrs, 'bottom')
+ y = self.checkBoundsDimension(attrs, 'bottom')
+ width = self.checkBoundsDimension(attrs, 'width')
+ height = self.checkBoundsDimension(attrs, 'height')
+ if (left is not None) and (right is not None) and (left > right):
+ self.handleError('Element bounds attribute left "%s" is greater than attribute right "%s"' % (
+ attrs['left'],
+ attrs['right']))
+ if (top is not None) and (bottom is not None) and (top > bottom):
+ self.handleError('Element bounds attribute top "%s" is greater than attribute bottom "%s"' % (
+ attrs['top'],
+ attrs['bottom']))
+ if (width is not None) and (0.0 > width):
+ self.handleError('Element bounds attribute width "%s" is negative' % (attrs['width'], ))
+ if (height is not None) and (0.0 > height):
+ self.handleError('Element bounds attribute height "%s" is negative' % (attrs['height'], ))
+ if ('left' not in attrs) and ('x' not in attrs):
+ self.handleError('Element bounds has neither attribute left nor attribute x')
+ has_ltrb = ('left' in attrs) or ('top' in attrs) or ('right' in attrs) or ('bottom' in attrs)
+ has_origin_size = ('x' in attrs) or ('y' in attrs) or ('width' in attrs) or ('height' in attrs)
+ if has_ltrb and has_origin_size:
+ self.handleError('Element bounds has both left/top/right/bottom and origin/size')
+
+ def checkColorChannel(self, attrs, name):
+ if name in attrs:
+ try:
+ channel = float(attrs[name])
+ if (0.0 > channel) or (1.0 < channel):
+ self.handleError('Element color attribute %s "%s" outside valid range 0.0-1.0' % (name, attrs[name]))
+ except:
+ self.handleError('Element color attribute %s "%s" is not numeric' % (name, attrs[name]))
+
+ def setDocumentLocator(self, locator):
+ self.locator = locator
+ super(LayoutChecker, self).setDocumentLocator(locator)
+
+ def startDocument(self):
+ self.in_layout = False
+ self.in_element = False
+ self.in_view = False
+ self.in_shape = False
+ self.in_object = False
+ self.ignored_depth = 0
+ super(LayoutChecker, self).startDocument()
+
+ def endDocument(self):
+ self.locator = None
+ self.elements.clear()
+ self.views.clear()
+ self.referenced.clear()
+ super(LayoutChecker, self).endDocument()
+
+ def startElement(self, name, attrs):
+ if 0 < self.ignored_depth:
+ self.ignored_depth += 1
+ elif not self.in_layout:
+ if 'mamelayout' != name:
+ self.ignored_depth = 1
+ self.handleError('Expected root element mamelayout but found %s' % (name, ))
+ else:
+ if 'version' not in attrs:
+ self.handleError('Element mamelayout missing attribute version')
+ else:
+ try:
+ long(attrs['version'])
+ except:
+ self.handleError('Element mamelayout attribute version "%s" is not an integer' % (attrs['version'], ))
+ self.in_layout = True
+ elif self.in_object:
+ if 'bounds' == name:
+ self.checkBounds(attrs)
+ self.ignored_depth = 1
+ elif self.in_shape:
+ if 'bounds' == name:
+ self.checkBounds(attrs)
+ elif 'color' == name:
+ self.checkColorChannel(attrs, 'red')
+ self.checkColorChannel(attrs, 'green')
+ self.checkColorChannel(attrs, 'blue')
+ self.checkColorChannel(attrs, 'alpha')
+ self.ignored_depth = 1
+ elif self.in_element:
+ if name in self.SHAPES:
+ self.in_shape = True
+ elif 'text' == name:
+ if 'string' not in attrs:
+ self.handleError('Element bounds missing attribute string')
+ if 'align' in attrs:
+ try:
+ align = long(attrs['align'])
+ if (0 > align) or (2 < align):
+ self.handleError('Element text attribute align "%s" not in valid range 0-2' % (attrs['align'], ))
+ except:
+ self.handleError('Element text attribute align "%s" is not an integer' % (attrs['align'], ))
+ self.in_shape = True
+ else:
+ self.ignored_depth = 1
+ elif self.in_view:
+ if name in self.OBJECTS:
+ if 'element' not in attrs:
+ self.handleError('Element %s missing attribute element', (name, ))
+ elif attrs['element'] not in self.referenced:
+ self.referenced[attrs['element']] = self.formatLocation()
+ self.in_object = True
+ elif 'screen' == name:
+ if 'index' in attrs:
+ try:
+ index = long(attrs['index'])
+ if 0 > index:
+ self.handleError('Element screen attribute index "%s" is negative' % (attrs['index'], ))
+ except:
+ self.handleError('Element screen attribute index "%s" is not an integer' % (attrs['index'], ))
+ self.in_object = True
+ elif 'bounds' == name:
+ self.checkBounds(attrs)
+ self.ignored_depth = 1
+ else:
+ self.ignored_depth = 1
+ elif 'element' == name:
+ if 'name' not in attrs:
+ self.handleError('Element element missing attribute name')
+ else:
+ if attrs['name'] in self.elements:
+ self.handleError('Element element has duplicate name (previous %s)' % (self.elements[attrs['name']], ))
+ else:
+ self.elements[attrs['name']] = self.formatLocation()
+ self.in_element = True
+ elif 'view' == name:
+ if 'name' not in attrs:
+ self.handleError('Element view missing attribute name')
+ else:
+ if attrs['name'] in self.views:
+ self.handleError('Element view has duplicate name (previous %s)' % (self.views[attrs['name']], ))
+ else:
+ self.views[attrs['name']] = self.formatLocation()
+ self.in_view = True
+ else:
+ self.ignored_depth = 1
+ super(LayoutChecker, self).startElement(name, attrs)
+
+ def endElement(self, name):
+ if 0 < self.ignored_depth:
+ self.ignored_depth -= 1
+ elif self.in_object:
+ self.in_object = False
+ elif self.in_shape:
+ self.in_shape = False
+ elif self.in_element:
+ self.in_element = False
+ elif self.in_view:
+ self.in_view = False
+ elif self.in_layout:
+ for element in self.referenced:
+ if element not in self.elements:
+ self.handleError('Element "%s" not found (first referenced at %s)' % (element, self.referenced[element]))
+ self.in_layout = False
+ super(LayoutChecker, self).endElement(name)
+
+
def compressLayout(src, dst, comp):
state = [0, 0]
def write(block):
@@ -104,7 +304,7 @@ def compressLayout(src, dst, comp):
write(comp.compress(block))
error_handler = ErrorHandler()
- content_handler = Minifyer(output)
+ content_handler = LayoutChecker(output)
parser = xml.sax.make_parser()
parser.setErrorHandler(error_handler)
parser.setContentHandler(content_handler)
@@ -115,7 +315,7 @@ def compressLayout(src, dst, comp):
except xml.sax.SAXException as exception:
print('fatal error: %s' % (exception))
raise XmlError('Fatal error parsing XML')
- if (error_handler.errors > 0) or (error_handler.warnings > 0):
+ if (content_handler.errors > 0) or (error_handler.errors > 0) or (error_handler.warnings > 0):
raise XmlError('Error(s) and/or warning(s) parsing XML')
return state[1], state[0]