]> git.decadent.org.uk Git - dak.git/blob - scripts/debian/dep11-basic-validate.py
6f46e14b08c526e4ac97ed42bb4fba9f64aec882
[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 from voluptuous import Schema, Required, All, Any, Length, Range, Match, Url
23 from optparse import OptionParser
24
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),
32 })
33
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)),
37 }, extra = True)
38
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)),
44 }, extra = True)
45
46 def add_issue(msg):
47     print(msg)
48
49 def test_custom_objects(lines):
50     ret = True
51     for i in range(0, len(lines)):
52         if "!!python/" in lines[i]:
53             add_issue("Python object encoded in line %i." % (i))
54             ret = False
55     return ret
56
57 def is_quoted(s):
58         return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'"))
59
60 def test_localized_dict(doc, ldict, id_string):
61     ret = True
62     for lang, value in ldict.items():
63         if lang == 'x-test':
64             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: x-test"))
65         if lang == 'xx':
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)"))
69         if is_quoted(value):
70             add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "String is quoted: '%s' @ %s" % (value, lang)))
71         if " " in 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
74             ret = False
75     return ret
76
77 def test_localized(doc, key):
78     ldict = doc.get(key, None)
79     if not ldict:
80         return True
81
82     return test_localized_dict(doc, ldict, key)
83
84 def validate_data(data):
85         ret = True
86         lines = data.split("\n")
87
88         # see if there are any Python-specific objects encoded
89         ret = test_custom_objects(lines)
90
91         try:
92             docs = yaml.safe_load_all(data)
93             header = next(docs)
94         except Exception as e:
95             add_issue("Could not parse file: %s" % (str(e)))
96             return False
97
98         try:
99             schema_header(header)
100         except Exception as e:
101             add_issue("Invalid DEP-11 header: %s" % (str(e)))
102             ret = False
103
104         for doc in docs:
105             docid = doc.get('ID')
106             pkgname = doc.get('Package')
107             if not pkgname:
108                 pkgname = "?unknown?"
109             if not doc:
110                 add_issue("FATAL: Empty document found.")
111                 ret = False
112                 continue
113             if not docid:
114                 add_issue("FATAL: Component without ID found.")
115                 ret = False
116                 continue
117
118             try:
119                 schema_component(doc)
120             except Exception as e:
121                 add_issue("[%s]: %s" % (docid, str(e)))
122                 ret = False
123                 continue
124
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."))
130                     ret = False
131             if icon:
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']"))
134                     ret = False
135
136             if not test_localized(doc, 'Name'):
137                 ret = False
138             if not test_localized(doc, 'Summary'):
139                 ret = False
140             if not test_localized(doc, 'Description'):
141                 ret = False
142             if not test_localized(doc, 'DeveloperName'):
143                 ret = False
144
145             for shot in doc.get('Screenshots', list()):
146                 caption = shot.get('caption')
147                 if caption:
148                     if not test_localized_dict(doc, caption, "Screenshots.x.caption"):
149                         ret = False
150
151         return ret
152
153 def validate_file(fname):
154     f = None
155     if fname.endswith(".gz"):
156         f = gzip.open(fname, 'r')
157     else:
158         f = open(fname, 'r')
159
160     data = str(f.read(), 'utf-8')
161     f.close()
162
163     return validate_data(data)
164
165 def validate_dir(dirname):
166     ret = True
167     for root, subfolders, files in os.walk(dirname):
168         for fname in files:
169              if fname.endswith(".yml.gz"):
170                  if not validate_file(os.path.join(root, fname)):
171                      ret = False
172
173     return ret
174
175 def main():
176     parser = OptionParser()
177
178     (options, args) = parser.parse_args()
179
180     if len(args) < 1:
181         print("You need to specify a file to validate!")
182         sys.exit(4)
183     fname = args[0]
184
185     if os.path.isdir(fname):
186         ret = validate_dir(fname)
187     else:
188         ret = validate_file(fname)
189     if ret:
190         msg = "DEP-11 basic validation successful."
191     else:
192         msg = "DEP-11 validation failed!"
193     print(msg)
194
195     if not ret:
196         sys.exit(1)
197
198 if __name__ == "__main__":
199     main()