dep11-basic-validate.py 7.4 KB

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