3 # Copyright (C) 2015 Matthias Klumpp <mak@debian.org>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation; either
8 # version 3.0 of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program.
22 from voluptuous import Schema, Required, All, Any, Length, Range, Match, Url
23 from optparse import OptionParser
25 schema_header = Schema({
26 Required('File'): All(str, 'DEP-11', msg="Must be \"DEP-11\""),
27 Required('Origin'): All(str, Length(min=1)),
28 Required('Version'): All(str, Match(r'(\d+\.?)+$'), msg="Must be a valid version number"),
29 Required('MediaBaseUrl'): All(str, Url()),
30 'Time': All(str, str),
31 'Priority': All(str, int),
34 schema_translated = Schema({
35 Required('C'): All(str, Length(min=1), msg="Must have an unlocalized 'C' key"),
36 dict: All(str, Length(min=1)),
39 schema_component = Schema({
40 Required('Type'): All(str, Length(min=1)),
41 Required('ID'): All(str, Length(min=1)),
42 Required('Name'): All(dict, Length(min=1), schema_translated),
43 Required('Package'): All(str, Length(min=1)),
49 def test_custom_objects(lines):
51 for i in range(0, len(lines)):
52 if "!!python/" in lines[i]:
53 add_issue("Python object encoded in line %i." % (i))
58 return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'"))
60 def test_localized_dict(doc, ldict, id_string):
62 for lang, value in ldict.items():
64 add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: x-test"))
66 add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: xx"))
67 if lang.endswith('.UTF-8'):
68 add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "AppStream locale names should not specify encoding (ends with .UTF-8)"))
70 add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "String is quoted: '%s' @ %s" % (value, lang)))
72 add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Locale name contains space: '%s'" % (lang)))
73 # this - as opposed to the other issues - is an error
77 def test_localized(doc, key):
78 ldict = doc.get(key, None)
82 return test_localized_dict(doc, ldict, key)
84 def validate_data(data):
86 lines = data.split("\n")
88 # see if there are any Python-specific objects encoded
89 ret = test_custom_objects(lines)
92 docs = yaml.safe_load_all(data)
94 except Exception as e:
95 add_issue("Could not parse file: %s" % (str(e)))
100 except Exception as e:
101 add_issue("Invalid DEP-11 header: %s" % (str(e)))
105 docid = doc.get('ID')
106 pkgname = doc.get('Package')
108 pkgname = "?unknown?"
110 add_issue("FATAL: Empty document found.")
114 add_issue("FATAL: Component without ID found.")
119 schema_component(doc)
120 except Exception as e:
121 add_issue("[%s]: %s" % (docid, str(e)))
125 # more tests for the icon key
126 icon = doc.get('Icon')
127 if (doc['Type'] == "desktop-app") or (doc['Type'] == "web-app"):
128 if not doc.get('Icon'):
129 add_issue("[%s]: %s" % (docid, "Components containing an application must have an 'Icon' key."))
132 if (not icon.get('stock')) and (not icon.get('cached')) and (not icon.get('local')):
133 add_issue("[%s]: %s" % (docid, "A 'stock', 'cached' or 'local' icon must at least be provided. @ data['Icon']"))
136 if not test_localized(doc, 'Name'):
138 if not test_localized(doc, 'Summary'):
140 if not test_localized(doc, 'Description'):
142 if not test_localized(doc, 'DeveloperName'):
145 for shot in doc.get('Screenshots', list()):
146 caption = shot.get('caption')
148 if not test_localized_dict(doc, caption, "Screenshots.x.caption"):
153 def validate_file(fname):
155 if fname.endswith(".gz"):
156 f = gzip.open(fname, 'r')
160 data = str(f.read(), 'utf-8')
163 return validate_data(data)
165 def validate_dir(dirname):
167 for root, subfolders, files in os.walk(dirname):
169 if fname.endswith(".yml.gz"):
170 if not validate_file(os.path.join(root, fname)):
176 parser = OptionParser()
178 (options, args) = parser.parse_args()
181 print("You need to specify a file to validate!")
185 if os.path.isdir(fname):
186 ret = validate_dir(fname)
188 ret = validate_file(fname)
190 msg = "DEP-11 basic validation successful."
192 msg = "DEP-11 validation failed!"
198 if __name__ == "__main__":