[ssci] Defined basic metadata fields

Bug: b:277147404
Change-Id: Iabba43add61ce5090be50ac8f0245f624028c44b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4776728
Reviewed-by: Rachael Newitt <renewitt@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Commit-Queue: Anne Redulla <aredulla@google.com>
changes/28/4776728/4
Anne Redulla 3 years ago committed by LUCI CQ
parent 4e60071d3d
commit 51690612da

@ -0,0 +1,6 @@
# Software Supply Chain Integrity/SBOM
aredulla@google.com
dlf@google.com
jsca@google.com
renewitt@google.com
sumakasa@google.com

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
from typing import Union
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)
import metadata.fields.types as field_types
# Freeform text fields.
NAME = field_types.FreeformTextField("Name")
SHORT_NAME = field_types.FreeformTextField("Short Name")
REVISION = field_types.FreeformTextField("Revision")
DESCRIPTION = field_types.FreeformTextField("Description", one_liner=False)
LOCAL_MODIFICATIONS = field_types.FreeformTextField("Local Modifications",
one_liner=False)
# Yes/no fields.
SECURITY_CRITICAL = field_types.YesNoField("Security Critical")
SHIPPED = field_types.YesNoField("Shipped")
LICENSE_ANDROID_COMPATIBLE = field_types.YesNoField(
"License Android Compatible")
ALL_FIELDS = (
NAME,
SHORT_NAME,
REVISION,
SECURITY_CRITICAL,
SHIPPED,
LICENSE_ANDROID_COMPATIBLE,
DESCRIPTION,
LOCAL_MODIFICATIONS,
)
ALL_FIELD_NAMES = {field.get_name() for field in ALL_FIELDS}
FIELD_MAPPING = {field.get_name().lower(): field for field in ALL_FIELDS}
def get_field(label: str) -> Union[field_types.MetadataField, None]:
return FIELD_MAPPING.get(label.lower())

@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import re
import sys
from typing import Union
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)
import metadata.fields.util as util
import metadata.validation_result as vr
# Pattern used to check if the entire string is either "yes" or "no",
# case-insensitive.
_PATTERN_YES_OR_NO = re.compile(r"^(yes|no)$", re.IGNORECASE)
# Pattern used to check if the string starts with "yes" or "no",
# case-insensitive. e.g. "No (test only)", "Yes?"
_PATTERN_STARTS_WITH_YES_OR_NO = re.compile(r"^(yes|no)", re.IGNORECASE)
class MetadataField:
"""Base class for all metadata fields."""
def __init__(self, name: str, one_liner: bool = True):
self._name = name
self._one_liner = one_liner
def get_name(self):
return self._name
def validate(self, value: str) -> Union[vr.ValidationResult, None]:
"""Checks the given value is acceptable for the field.
Raises: NotImplementedError if called. This method must be overridden with
the actual validation of the field.
"""
raise NotImplementedError(f"{self._name} field validation not defined.")
class FreeformTextField(MetadataField):
"""Field where the value is freeform text."""
def validate(self, value: str) -> Union[vr.ValidationResult, None]:
"""Checks the given value has at least one non-whitespace character."""
if util.is_empty(value):
return vr.ValidationError(f"{self._name} is empty.")
return None
class YesNoField(MetadataField):
"""Field where the value must be yes or no."""
def __init__(self, name: str):
super().__init__(name=name, one_liner=True)
def validate(self, value: str) -> Union[vr.ValidationResult, None]:
"""Checks the given value is either yes or no."""
if util.matches(_PATTERN_YES_OR_NO, value):
return None
if util.matches(_PATTERN_STARTS_WITH_YES_OR_NO, value):
return vr.ValidationWarning(
f"{self._name} is '{value}' - should be only {util.YES} or {util.NO}."
)
return vr.ValidationError(
f"{self._name} is '{value}' - must be {util.YES} or {util.NO}.")

@ -0,0 +1,38 @@
#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
from typing import List
# Preferred values for yes/no fields (i.e. all lowercase).
YES = "yes"
NO = "no"
# Pattern used to check if the entire string is "unknown", case-insensitive.
_PATTERN_UNKNOWN = re.compile(r"^unknown$", re.IGNORECASE)
# Pattern used to check if the entire string is functionally empty, i.e.
# empty string, or all characters are only whitespace.
_PATTERN_ONLY_WHITESPACE = re.compile(r"^\s*$")
def matches(pattern: re.Pattern, value: str) -> bool:
"""Returns whether the value matches the pattern."""
return pattern.match(value) is not None
def is_empty(value: str) -> bool:
"""Returns whether the value is functionally empty."""
return matches(_PATTERN_ONLY_WHITESPACE, value)
def is_unknown(value: str) -> bool:
"""Returns whether the value is 'unknown' (case insensitive)."""
return matches(_PATTERN_UNKNOWN, value)
def quoted(values: List[str]) -> str:
"""Returns a string of the given values, each being individually quoted."""
return ", ".join([f"'{entry}'" for entry in values])

@ -0,0 +1,70 @@
#!/usr/bin/env vpython3
# Copyright (c) 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
from typing import List
import unittest
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)
import metadata.fields.types as field_types
import metadata.validation_result as vr
class FieldValidationTest(unittest.TestCase):
def _run_field_validation(self,
field: field_types.MetadataField,
valid_values: List[str],
error_values: List[str],
warning_values: List[str] = []):
"""Helper to run a field's validation for different values."""
for value in valid_values:
self.assertIsNone(field.validate(value))
for value in error_values:
self.assertIsInstance(field.validate(value), vr.ValidationError)
for value in warning_values:
self.assertIsInstance(field.validate(value), vr.ValidationWarning)
def test_freeform_text_validation(self):
# Check validation of a freeform text field that should be on one line.
self._run_field_validation(
field=field_types.FreeformTextField("Freeform single", one_liner=True),
valid_values=["Text on single line", "a", "1"],
error_values=["", "\n", " "],
)
# Check validation of a freeform text field that can span multiple lines.
self._run_field_validation(
field=field_types.FreeformTextField("Freeform multi", one_liner=False),
valid_values=[
"This is text spanning multiple lines:\n"
" * with this point\n"
" * and this other point",
"Text on single line",
"a",
"1",
],
error_values=["", "\n", " "],
)
def test_yes_no_field_validation(self):
self._run_field_validation(
field=field_types.YesNoField("Yes/No test"),
valid_values=["yes", "no", "No", "YES"],
error_values=["", "\n", "Probably yes"],
warning_values=["Yes?", "not"],
)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import textwrap
from typing import Dict, Union
class ValidationResult:
"""Base class for validation issues."""
def __init__(self, message: str, fatal: bool, **tags: Dict[str, str]):
self._fatal = fatal
self._message = message
self._tags = tags
def __str__(self) -> str:
prefix = "ERROR" if self._fatal else "[non-fatal]"
return f"{prefix} - {self._message}"
def __repr__(self) -> str:
return str(self)
def is_fatal(self) -> bool:
return self._fatal
def set_tag(self, tag: str, value: str) -> bool:
self._tags[tag] = value
def get_tag(self, tag: str) -> Union[str, None]:
return self._tags.get(tag)
def get_all_tags(self) -> Dict[str, str]:
return dict(self._tags)
def get_message(self, width: int = 0) -> str:
if width > 0:
return textwrap.fill(text=self._message, width=width)
return self._message
class ValidationError(ValidationResult):
"""Fatal validation issue. Presubmit should fail."""
def __init__(self, message: str, **tags: Dict[str, str]):
super().__init__(message=message, fatal=True, **tags)
class ValidationWarning(ValidationResult):
"""Non-fatal validation issue. Presubmit should pass."""
def __init__(self, message: str, **tags: Dict[str, str]):
super().__init__(message=message, fatal=False, **tags)
Loading…
Cancel
Save