#!/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 Optional

_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.field_types as field_types
import metadata.fields.util as util
import metadata.validation_result as vr

# Pattern for CPE 2.3 URI binding (also compatible with CPE 2.2).
_PATTERN_CPE_URI = re.compile(
    r"^c[pP][eE]:/[AHOaho]?(:[A-Za-z0-9._\-~%]*){0,6}$")

# Pattern that will match CPE 2.3 formatted string binding.
_PATTERN_CPE_FORMATTED_STRING = re.compile(r"^cpe:2\.3:[aho\-\*](:[^:]+){10}$")


def is_uri_cpe(value: str) -> bool:
    """Returns whether the value conforms to the CPE 2.3 URI binding
    (which is compatible with CPE 2.2), with the additional constraint
    that at least one component other than "part" has been specified.

    For reference, see section 6.1 of the CPE Naming Specification
    Version 2.3 at
    https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf.
    """
    if not util.matches(_PATTERN_CPE_URI, value):
        return False

    components = value.split(":")
    if len(components) < 3:
        # At most, only part was provided.
        return False

    # Check at least one component other than "part" has been specified.
    for component in components[2:]:
        if component:
            return True

    return False


def is_formatted_string_cpe(value: str) -> bool:
    """Returns whether the value conforms to the CPE 2.3 formatted
    string binding.

    For reference, see section 6.2 of the CPE Naming Specification
    Version 2.3 at
    https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf.
    """
    return util.matches(_PATTERN_CPE_FORMATTED_STRING, value)


class CPEPrefixField(field_types.SingleLineTextField):
    """Custom field for the package's CPE."""
    def __init__(self):
        super().__init__(name="CPEPrefix")

    def _is_valid(self, value: str) -> bool:
        return (util.is_unknown(value) or is_formatted_string_cpe(value)
                or is_uri_cpe(value))

    def validate(self, value: str) -> Optional[vr.ValidationResult]:
        """Checks the given value is either 'unknown', or conforms to
        either the CPE 2.3 or 2.2 format.
        """
        if self._is_valid(value):
            return None

        return vr.ValidationError(
            reason=f"{self._name} is invalid.",
            additional=[
                "This field should be a CPE (version 2.3 or 2.2), "
                "or 'unknown'.",
                "Search for a CPE tag for the package at "
                "https://nvd.nist.gov/products/cpe/search.",
                f"Current value: '{value}'.",
            ])

    def narrow_type(self, value: str) -> Optional[str]:
        if not self._is_valid(value):
            return None

        if util.is_unknown(value):
            return None

        # CPE names are case-insensitive, we normalize to lowercase.
        # See https://cpe.mitre.org/specification/.
        value = value.lower()

        return value
