validate.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python3
  2. # Copyright 2023 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. import os
  6. import sys
  7. from typing import Callable, List, Tuple, Union
  8. _THIS_DIR = os.path.abspath(os.path.dirname(__file__))
  9. # The repo's root directory.
  10. _ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, ".."))
  11. # Add the repo's root directory for clearer imports.
  12. sys.path.insert(0, _ROOT_DIR)
  13. import gclient_utils
  14. import metadata.parse
  15. import metadata.validation_result as vr
  16. _TRANSITION_PRESCRIPT = (
  17. "The following issue should be addressed now, as it will become a "
  18. "presubmit error (instead of warning) once third party metadata "
  19. "validation is enforced.\nThird party metadata issue:")
  20. def validate_content(content: str,
  21. source_file_dir: str,
  22. repo_root_dir: str,
  23. is_open_source_project: bool = False) -> List[vr.ValidationResult]:
  24. """Validate the content as a metadata file.
  25. Args:
  26. content: the entire content of a file to be validated as a
  27. metadata file.
  28. source_file_dir: the directory of the metadata file that the
  29. license file value is from; this is needed to
  30. construct file paths to license files.
  31. repo_root_dir: the repository's root directory; this is needed
  32. to construct file paths to license files.
  33. is_open_source_project: whether the project is open source.
  34. Returns: the validation results for the given content, sorted based
  35. severity then message.
  36. """
  37. results = []
  38. dependencies = metadata.parse.parse_content(content)
  39. if not dependencies:
  40. result = vr.ValidationError(reason="No dependency metadata found.")
  41. return [result]
  42. for dependency in dependencies:
  43. dependency_results = dependency.validate(
  44. source_file_dir=source_file_dir,
  45. repo_root_dir=repo_root_dir,
  46. is_open_source_project=is_open_source_project,
  47. )
  48. results.extend(dependency_results)
  49. return sorted(results)
  50. def _construct_file_read_error(filepath: str, cause: str) -> vr.ValidationError:
  51. """Helper function to create a validation error for a
  52. file reading issue.
  53. """
  54. result = vr.ValidationError(
  55. reason="Cannot read metadata file.",
  56. additional=[f"Attempted to read '{filepath}' but {cause}."])
  57. return result
  58. def validate_file(
  59. filepath: str,
  60. repo_root_dir: str,
  61. reader: Callable[[str], Union[str, bytes]] = None,
  62. is_open_source_project: bool = False,
  63. ) -> List[vr.ValidationResult]:
  64. """Validate the item located at the given filepath is a valid
  65. dependency metadata file.
  66. Args:
  67. filepath: the path to validate, e.g.
  68. "/chromium/src/third_party/libname/README.chromium"
  69. repo_root_dir: the repository's root directory; this is needed
  70. to construct file paths to license files.
  71. reader (optional): callable function/method to read the content
  72. of the file.
  73. is_open_source_project: whether to allow reciprocal licenses.
  74. This should only be True for open source projects.
  75. Returns: the validation results for the given filepath and its
  76. contents, if it exists.
  77. """
  78. if reader is None:
  79. reader = gclient_utils.FileRead
  80. try:
  81. content = reader(filepath)
  82. except FileNotFoundError:
  83. return [_construct_file_read_error(filepath, "file not found")]
  84. except PermissionError:
  85. return [_construct_file_read_error(filepath, "access denied")]
  86. except Exception as e:
  87. return [
  88. _construct_file_read_error(filepath, f"unexpected error: '{e}'")
  89. ]
  90. else:
  91. if not isinstance(content, str):
  92. return [_construct_file_read_error(filepath, "decoding failed")]
  93. # Get the directory the metadata file is in.
  94. source_file_dir = os.path.dirname(filepath)
  95. return validate_content(content=content,
  96. source_file_dir=source_file_dir,
  97. repo_root_dir=repo_root_dir,
  98. is_open_source_project=is_open_source_project)
  99. def check_file(
  100. filepath: str,
  101. repo_root_dir: str,
  102. reader: Callable[[str], Union[str, bytes]] = None,
  103. is_open_source_project: bool = False,
  104. ) -> Tuple[List[str], List[str]]:
  105. """Run metadata validation on the given filepath, and return all
  106. validation errors and validation warnings.
  107. Args:
  108. filepath: the path to a metadata file, e.g.
  109. "/chromium/src/third_party/libname/README.chromium"
  110. repo_root_dir: the repository's root directory; this is needed
  111. to construct file paths to license files.
  112. reader (optional): callable function/method to read the content
  113. of the file.
  114. is_open_source_project: whether to allow reciprocal licenses.
  115. This should only be True for open source projects.
  116. Returns:
  117. error_messages: the fatal validation issues present in the file;
  118. i.e. presubmit should fail.
  119. warning_messages: the non-fatal validation issues present in the
  120. file; i.e. presubmit should still pass.
  121. """
  122. results = validate_file(filepath=filepath,
  123. repo_root_dir=repo_root_dir,
  124. reader=reader,
  125. is_open_source_project=is_open_source_project)
  126. error_messages = []
  127. warning_messages = []
  128. for result in results:
  129. target = error_messages if result.is_fatal() else warning_messages
  130. target.append(result.get_message(width=60))
  131. return error_messages, warning_messages