dep11-basic-validate.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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. import os
  18. import sys
  19. import yaml
  20. import gzip
  21. import lzma
  22. from voluptuous import Schema, Required, All, Length, Match, Url
  23. from optparse import OptionParser
  24. import multiprocessing as mp
  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),
  31. 'Priority': All(int),
  32. })
  33. schema_translated = Schema({
  34. Required('C'): All(str, Length(min=1), msg='Must have an unlocalized \'C\' key'),
  35. dict: All(str, Length(min=1)),
  36. }, extra=True)
  37. schema_component = Schema({
  38. Required('Type'): All(str, Length(min=1)),
  39. Required('ID'): All(str, Length(min=1)),
  40. Required('Name'): All(dict, Length(min=1), schema_translated),
  41. Required('Summary'): All(dict, Length(min=1)),
  42. }, extra=True)
  43. def add_issue(msg):
  44. print(msg)
  45. def test_custom_objects(lines):
  46. ret = True
  47. for i in range(0, len(lines)):
  48. if '!!python/' in lines[i]:
  49. add_issue('Python object encoded in line %i.' % (i))
  50. ret = False
  51. return ret
  52. def test_localized_dict(doc, ldict, id_string):
  53. ret = True
  54. for lang, value in ldict.items():
  55. if lang == 'x-test':
  56. add_issue('[%s][%s]: %s' % (doc['ID'], id_string, 'Found cruft locale: x-test'))
  57. if lang == 'xx':
  58. add_issue('[%s][%s]: %s' % (doc['ID'], id_string, 'Found cruft locale: xx'))
  59. if lang.endswith('.UTF-8'):
  60. add_issue('[%s][%s]: %s' % (doc['ID'], id_string, 'AppStream locale names should not specify encoding (ends with .UTF-8)'))
  61. if ' ' in lang:
  62. add_issue('[%s][%s]: %s' % (doc['ID'], id_string, 'Locale name contains space: "%s"' % (lang)))
  63. # this - as opposed to the other issues - is an error
  64. ret = False
  65. return ret
  66. def test_localized(doc, key):
  67. ldict = doc.get(key, None)
  68. if not ldict:
  69. return True
  70. return test_localized_dict(doc, ldict, key)
  71. def validate_data(data):
  72. ret = True
  73. lines = data.split('\n')
  74. # see if there are any Python-specific objects encoded
  75. ret = test_custom_objects(lines)
  76. try:
  77. docs = yaml.safe_load_all(data)
  78. header = next(docs)
  79. except Exception as e:
  80. add_issue('Could not parse file: %s' % (str(e)))
  81. return False
  82. try:
  83. schema_header(header)
  84. except Exception as e:
  85. add_issue('Invalid DEP-11 header: %s' % (str(e)))
  86. ret = False
  87. for doc in docs:
  88. cptid = doc.get('ID')
  89. pkgname = doc.get('Package')
  90. cpttype = doc.get('Type')
  91. if not doc:
  92. add_issue('FATAL: Empty document found.')
  93. ret = False
  94. continue
  95. if not cptid:
  96. add_issue('FATAL: Component without ID found.')
  97. ret = False
  98. continue
  99. if not pkgname:
  100. if cpttype not in ['web-application', 'operating-system', 'repository']:
  101. add_issue('[%s]: %s' % (cptid, 'Component is missing a \'Package\' key.'))
  102. ret = False
  103. continue
  104. try:
  105. schema_component(doc)
  106. except Exception as e:
  107. add_issue('[%s]: %s' % (cptid, str(e)))
  108. ret = False
  109. continue
  110. # more tests for the icon key
  111. icon = doc.get('Icon')
  112. if cpttype in ['desktop-application', 'web-application']:
  113. if not doc.get('Icon'):
  114. add_issue('[%s]: %s' % (cptid, 'Components containing an application must have an \'Icon\' key.'))
  115. ret = False
  116. if icon:
  117. if (not icon.get('stock')) and (not icon.get('cached')) and (not icon.get('local')):
  118. add_issue('[%s]: %s' % (cptid, 'A \'stock\', \'cached\' or \'local\' icon must at least be provided. @ data[\'Icon\']'))
  119. ret = False
  120. if not test_localized(doc, 'Name'):
  121. ret = False
  122. if not test_localized(doc, 'Summary'):
  123. ret = False
  124. if not test_localized(doc, 'Description'):
  125. ret = False
  126. if not test_localized(doc, 'DeveloperName'):
  127. ret = False
  128. for shot in doc.get('Screenshots', list()):
  129. caption = shot.get('caption')
  130. if caption:
  131. if not test_localized_dict(doc, caption, 'Screenshots.x.caption'):
  132. ret = False
  133. return ret
  134. def validate_file(fname):
  135. f = None
  136. if fname.endswith('.gz'):
  137. f = gzip.open(fname, 'r')
  138. elif fname.endswith('.xz'):
  139. f = lzma.open(fname, 'r')
  140. else:
  141. f = open(fname, 'r')
  142. data = str(f.read(), 'utf-8')
  143. f.close()
  144. return validate_data(data)
  145. def validate_dir(dirname):
  146. ret = True
  147. asfiles = []
  148. # find interesting files
  149. for root, subfolders, files in os.walk(dirname):
  150. for fname in files:
  151. fpath = os.path.join(root, fname)
  152. if os.path.islink(fpath):
  153. add_issue('FATAL: Symlinks are not allowed')
  154. return False
  155. if fname.endswith('.yml.gz') or fname.endswith('.yml.xz'):
  156. asfiles.append(fpath)
  157. # validate the files, use multiprocessing to speed up the validation
  158. with mp.Pool() as pool:
  159. results = [pool.apply_async(validate_file, (fname,)) for fname in asfiles]
  160. for res in results:
  161. if not res.get():
  162. ret = False
  163. return ret
  164. def main():
  165. parser = OptionParser()
  166. (options, args) = parser.parse_args()
  167. if len(args) < 1:
  168. print('You need to specify a file to validate!')
  169. sys.exit(4)
  170. fname = args[0]
  171. if os.path.isdir(fname):
  172. ret = validate_dir(fname)
  173. elif os.path.islink(fname):
  174. add_issue('FATAL: Symlinks are not allowed')
  175. ret = False
  176. else:
  177. ret = validate_file(fname)
  178. if ret:
  179. msg = 'DEP-11 basic validation successful.'
  180. else:
  181. msg = 'DEP-11 validation failed!'
  182. print(msg)
  183. if not ret:
  184. sys.exit(1)
  185. if __name__ == '__main__':
  186. main()