]> git.decadent.org.uk Git - dak.git/blobdiff - scripts/debian/dep11-basic-validate.py
Include AppStream metadata from the generator server
[dak.git] / scripts / debian / dep11-basic-validate.py
diff --git a/scripts/debian/dep11-basic-validate.py b/scripts/debian/dep11-basic-validate.py
new file mode 100755 (executable)
index 0000000..6f46e14
--- /dev/null
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2015 Matthias Klumpp <mak@debian.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3.0 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program.
+
+import os
+import sys
+import yaml
+import gzip
+from voluptuous import Schema, Required, All, Any, Length, Range, Match, Url
+from optparse import OptionParser
+
+schema_header = Schema({
+    Required('File'): All(str, 'DEP-11', msg="Must be \"DEP-11\""),
+    Required('Origin'): All(str, Length(min=1)),
+    Required('Version'): All(str, Match(r'(\d+\.?)+$'), msg="Must be a valid version number"),
+    Required('MediaBaseUrl'): All(str, Url()),
+    'Time': All(str, str),
+    'Priority': All(str, int),
+})
+
+schema_translated = Schema({
+    Required('C'): All(str, Length(min=1), msg="Must have an unlocalized 'C' key"),
+    dict: All(str, Length(min=1)),
+}, extra = True)
+
+schema_component = Schema({
+    Required('Type'): All(str, Length(min=1)),
+    Required('ID'): All(str, Length(min=1)),
+    Required('Name'): All(dict, Length(min=1), schema_translated),
+    Required('Package'): All(str, Length(min=1)),
+}, extra = True)
+
+def add_issue(msg):
+    print(msg)
+
+def test_custom_objects(lines):
+    ret = True
+    for i in range(0, len(lines)):
+        if "!!python/" in lines[i]:
+            add_issue("Python object encoded in line %i." % (i))
+            ret = False
+    return ret
+
+def is_quoted(s):
+        return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'"))
+
+def test_localized_dict(doc, ldict, id_string):
+    ret = True
+    for lang, value in ldict.items():
+        if lang == 'x-test':
+            add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: x-test"))
+        if lang == 'xx':
+            add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: xx"))
+        if lang.endswith('.UTF-8'):
+            add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "AppStream locale names should not specify encoding (ends with .UTF-8)"))
+        if is_quoted(value):
+            add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "String is quoted: '%s' @ %s" % (value, lang)))
+        if " " in lang:
+            add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Locale name contains space: '%s'" % (lang)))
+            # this - as opposed to the other issues - is an error
+            ret = False
+    return ret
+
+def test_localized(doc, key):
+    ldict = doc.get(key, None)
+    if not ldict:
+        return True
+
+    return test_localized_dict(doc, ldict, key)
+
+def validate_data(data):
+        ret = True
+        lines = data.split("\n")
+
+        # see if there are any Python-specific objects encoded
+        ret = test_custom_objects(lines)
+
+        try:
+            docs = yaml.safe_load_all(data)
+            header = next(docs)
+        except Exception as e:
+            add_issue("Could not parse file: %s" % (str(e)))
+            return False
+
+        try:
+            schema_header(header)
+        except Exception as e:
+            add_issue("Invalid DEP-11 header: %s" % (str(e)))
+            ret = False
+
+        for doc in docs:
+            docid = doc.get('ID')
+            pkgname = doc.get('Package')
+            if not pkgname:
+                pkgname = "?unknown?"
+            if not doc:
+                add_issue("FATAL: Empty document found.")
+                ret = False
+                continue
+            if not docid:
+                add_issue("FATAL: Component without ID found.")
+                ret = False
+                continue
+
+            try:
+                schema_component(doc)
+            except Exception as e:
+                add_issue("[%s]: %s" % (docid, str(e)))
+                ret = False
+                continue
+
+            # more tests for the icon key
+            icon = doc.get('Icon')
+            if (doc['Type'] == "desktop-app") or (doc['Type'] == "web-app"):
+                if not doc.get('Icon'):
+                    add_issue("[%s]: %s" % (docid, "Components containing an application must have an 'Icon' key."))
+                    ret = False
+            if icon:
+                if (not icon.get('stock')) and (not icon.get('cached')) and (not icon.get('local')):
+                    add_issue("[%s]: %s" % (docid, "A 'stock', 'cached' or 'local' icon must at least be provided. @ data['Icon']"))
+                    ret = False
+
+            if not test_localized(doc, 'Name'):
+                ret = False
+            if not test_localized(doc, 'Summary'):
+                ret = False
+            if not test_localized(doc, 'Description'):
+                ret = False
+            if not test_localized(doc, 'DeveloperName'):
+                ret = False
+
+            for shot in doc.get('Screenshots', list()):
+                caption = shot.get('caption')
+                if caption:
+                    if not test_localized_dict(doc, caption, "Screenshots.x.caption"):
+                        ret = False
+
+        return ret
+
+def validate_file(fname):
+    f = None
+    if fname.endswith(".gz"):
+        f = gzip.open(fname, 'r')
+    else:
+        f = open(fname, 'r')
+
+    data = str(f.read(), 'utf-8')
+    f.close()
+
+    return validate_data(data)
+
+def validate_dir(dirname):
+    ret = True
+    for root, subfolders, files in os.walk(dirname):
+        for fname in files:
+             if fname.endswith(".yml.gz"):
+                 if not validate_file(os.path.join(root, fname)):
+                     ret = False
+
+    return ret
+
+def main():
+    parser = OptionParser()
+
+    (options, args) = parser.parse_args()
+
+    if len(args) < 1:
+        print("You need to specify a file to validate!")
+        sys.exit(4)
+    fname = args[0]
+
+    if os.path.isdir(fname):
+        ret = validate_dir(fname)
+    else:
+        ret = validate_file(fname)
+    if ret:
+        msg = "DEP-11 basic validation successful."
+    else:
+        msg = "DEP-11 validation failed!"
+    print(msg)
+
+    if not ret:
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()