]> git.decadent.org.uk Git - dak.git/blob - scripts/debian/dep11-basic-validate.py
Correct indention.
[dak.git] / scripts / debian / dep11-basic-validate.py
1 #!/usr/bin/env python3
2 #
3 # Copyright (C) 2015 Matthias Klumpp <mak@debian.org>
4 #
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.
9 #
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.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program.
17
18 import os
19 import sys
20 import yaml
21 import gzip
22 import lzma
23 from voluptuous import Schema, Required, All, Any, Length, Range, Match, Url
24 from optparse import OptionParser
25
26 schema_header = Schema({
27     Required('File'): All(str, 'DEP-11', msg="Must be \"DEP-11\""),
28     Required('Origin'): All(str, Length(min=1)),
29     Required('Version'): All(str, Match(r'(\d+\.?)+$'), msg="Must be a valid version number"),
30     Required('MediaBaseUrl'): All(str, Url()),
31     'Time': All(str),
32     'Priority': All(int),
33 })
34
35 schema_translated = Schema({
36     Required('C'): All(str, Length(min=1), msg="Must have an unlocalized 'C' key"),
37     dict: All(str, Length(min=1)),
38 }, extra = True)
39
40 schema_component = Schema({
41     Required('Type'): All(str, Length(min=1)),
42     Required('ID'): All(str, Length(min=1)),
43     Required('Name'): All(dict, Length(min=1), schema_translated),
44     Required('Package'): All(str, Length(min=1)),
45 }, extra = True)
46
47 def add_issue(msg):
48     print(msg)
49
50 def test_custom_objects(lines):
51     ret = True
52     for i in range(0, len(lines)):
53         if "!!python/" in lines[i]:
54             add_issue("Python object encoded in line %i." % (i))
55             ret = False
56     return ret
57
58 def is_quoted(s):
59         return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'"))
60
61 def test_localized_dict(doc, ldict, id_string):
62     ret = True
63     for lang, value in ldict.items():
64         if lang == 'x-test':
65             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: x-test"))
66         if lang == 'xx':
67             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: xx"))
68         if lang.endswith('.UTF-8'):
69             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "AppStream locale names should not specify encoding (ends with .UTF-8)"))
70         if is_quoted(value):
71             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "String is quoted: '%s' @ %s" % (value, lang)))
72         if " " in lang:
73             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Locale name contains space: '%s'" % (lang)))
74             # this - as opposed to the other issues - is an error
75             ret = False
76     return ret
77
78 def test_localized(doc, key):
79     ldict = doc.get(key, None)
80     if not ldict:
81         return True
82
83     return test_localized_dict(doc, ldict, key)
84
85 def validate_data(data):
86         ret = True
87         lines = data.split("\n")
88
89         # see if there are any Python-specific objects encoded
90         ret = test_custom_objects(lines)
91
92         try:
93             docs = yaml.safe_load_all(data)
94             header = next(docs)
95         except Exception as e:
96             add_issue("Could not parse file: %s" % (str(e)))
97             return False
98
99         try:
100             schema_header(header)
101         except Exception as e:
102             add_issue("Invalid DEP-11 header: %s" % (str(e)))
103             ret = False
104
105         for doc in docs:
106             docid = doc.get('ID')
107             pkgname = doc.get('Package')
108             if not pkgname:
109                 pkgname = "?unknown?"
110             if not doc:
111                 add_issue("FATAL: Empty document found.")
112                 ret = False
113                 continue
114             if not docid:
115                 add_issue("FATAL: Component without ID found.")
116                 ret = False
117                 continue
118
119             try:
120                 schema_component(doc)
121             except Exception as e:
122                 add_issue("[%s]: %s" % (docid, str(e)))
123                 ret = False
124                 continue
125
126             # more tests for the icon key
127             icon = doc.get('Icon')
128             if (doc['Type'] == "desktop-app") or (doc['Type'] == "web-app"):
129                 if not doc.get('Icon'):
130                     add_issue("[%s]: %s" % (docid, "Components containing an application must have an 'Icon' key."))
131                     ret = False
132             if icon:
133                 if (not icon.get('stock')) and (not icon.get('cached')) and (not icon.get('local')):
134                     add_issue("[%s]: %s" % (docid, "A 'stock', 'cached' or 'local' icon must at least be provided. @ data['Icon']"))
135                     ret = False
136
137             if not test_localized(doc, 'Name'):
138                 ret = False
139             if not test_localized(doc, 'Summary'):
140                 ret = False
141             if not test_localized(doc, 'Description'):
142                 ret = False
143             if not test_localized(doc, 'DeveloperName'):
144                 ret = False
145
146             for shot in doc.get('Screenshots', list()):
147                 caption = shot.get('caption')
148                 if caption:
149                     if not test_localized_dict(doc, caption, "Screenshots.x.caption"):
150                         ret = False
151
152         return ret
153
154 def validate_file(fname):
155     f = None
156     if fname.endswith(".gz"):
157         f = gzip.open(fname, 'r')
158     elif fname.endswith(".xz"):
159         f = lzma.open(fname, 'r')
160     else:
161         f = open(fname, 'r')
162
163     data = str(f.read(), 'utf-8')
164     f.close()
165
166     return validate_data(data)
167
168 def validate_dir(dirname):
169     ret = True
170     for root, subfolders, files in os.walk(dirname):
171         for fname in files:
172             if fname.endswith(".yml.gz") or fname.endswith(".yml.xz"):
173                 if not validate_file(os.path.join(root, fname)):
174                     ret = False
175
176     return ret
177
178 def main():
179     parser = OptionParser()
180
181     (options, args) = parser.parse_args()
182
183     if len(args) < 1:
184         print("You need to specify a file to validate!")
185         sys.exit(4)
186     fname = args[0]
187
188     if os.path.isdir(fname):
189         ret = validate_dir(fname)
190     else:
191         ret = validate_file(fname)
192     if ret:
193         msg = "DEP-11 basic validation successful."
194     else:
195         msg = "DEP-11 validation failed!"
196     print(msg)
197
198     if not ret:
199         sys.exit(1)
200
201 if __name__ == "__main__":
202     main()