# Copyright New York University and the in-toto contributors
# SPDX-License-Identifier: Apache-2.0

"""
<Module Name>
  in_toto/rulelib.py

<Author>
  Santiago Torres-Arias <santiago@nyu.edu>
  Lukas Puehringer <lukas.puehringer@nyu.edu>

<Started>
  Nov 18, 2016

<Copyright>
  See LICENSE for licensing information.

<Purpose>
  This module provides functions parse artifact rules and validate their
  syntax.

"""
import securesystemslib.exceptions
import securesystemslib.formats

from in_toto.formats import _check_str, _check_str_list

GENERIC_RULES = {
    "create",
    "modify",
    "delete",
    "allow",
    "disallow",
    "require",
}
COMPLEX_RULES = {
    "match",
}
ALL_RULES = GENERIC_RULES | COMPLEX_RULES


def unpack_rule(rule):
    """
    Parses the rule and extracts and returns the necessary data to apply the
    rule. Can also be used to verify if a rule complies with any of the formats

    <Arguments>
      rule:
          The list of rule elements, in one of the following formats:
          MATCH <pattern> [IN <source-path-prefix>] WITH (MATERIALS|PRODUCTS)
              [IN <destination-path-prefix>] FROM <step>,
          CREATE <pattern>,
          DELETE <pattern>,
          MODIFY <pattern>,
          ALLOW <pattern>,
          DISALLOW <pattern>,
          REQUIRE <file>

    Note that REQUIRE is somewhat of a weird animal that does not use patterns
    but rather single filenames (for now).

    <Exceptions>
      raises FormatError, if the rule does not comply with any of the formats.

    <Side Effects>
      None.

    <Returns>
      A dictionary of the artifact rule data,
      if it is a generic rule the dictionary is:
      {
        "rule_type": rule[0] ("CREATE"|"MODIFY"|"DELETE"|"ALLOW"|"DISALLOW")
        "pattern" : rule[1], a path pattern
      }

      if it is a match rule, the dictionary is:
      {
        "rule_type": rule[0],  ("MATCH"),
        "pattern": rule[1], a path pattern
        "source_prefix": path or empty string
        "dest_prefix": path or empty string
        "dest_type" : destination artifact type, ("MATERIAL"|"PRODUCT")
        "dest_name": destination step/inspection name
      }

    """
    _check_str_list(rule)

    # Create all lower rule copy to case insensitively parse out tokens whose
    # position we don't know yet
    # We keep the original rule to retain the non-token elements' case
    rule_lower = []
    for rule_elem in rule:
        rule_lower.append(rule_elem.lower())

    rule_len = len(rule)

    if rule_len < 2 or rule_lower[0] not in ALL_RULES:
        raise securesystemslib.exceptions.FormatError(
            "Wrong rule format,"
            " rules must start with one of '{0}' and specify a 'pattern' as"
            " second element.\n"
            "Got: \n\t'{1}'".format(", ".join(ALL_RULES), rule)
        )

    rule_type = rule_lower[0]
    pattern = rule[1]

    # Type is one of "CREATE", "MODIFY", "DELETE", "ALLOW", "DISALLOW"
    if rule_type in GENERIC_RULES:
        if rule_len != 2:
            raise securesystemslib.exceptions.FormatError(
                "Wrong rule format,"
                " generic rules must have one of the formats:\n\t"
                "CREATE <pattern>\n\t"
                "MODIFY <pattern>\n\t"
                "DELETE <pattern>\n\t"
                "ALLOW <pattern>\n\t"
                "DISALLOW <pattern>\n"
                "REQUIRE <file>\n"
                "Got:\n\t{}".format(rule)
            )
        return {
            "rule_type": rule_type,
            "pattern": pattern,
        }

    # Type is "MATCH"
    if rule_type in COMPLEX_RULES:
        # ... IN <source-path-prefix> WITH (MATERIALS|PRODUCTS)
        # IN <destination-path-prefix> FROM <step>
        if (
            rule_len == 10
            and rule_lower[2] == "in"
            and rule_lower[4] == "with"
            and rule_lower[6] == "in"
            and rule_lower[8] == "from"
        ):
            source_prefix = rule[3]
            dest_type = rule_lower[5]
            dest_prefix = rule[7]
            dest_name = rule[9]

        # ... IN <source-path-prefix> WITH (MATERIALS|PRODUCTS) FROM <step>
        elif (
            rule_len == 8
            and rule_lower[2] == "in"
            and rule_lower[4] == "with"
            and rule_lower[6] == "from"
        ):
            source_prefix = rule[3]
            dest_type = rule_lower[5]
            dest_prefix = ""
            dest_name = rule[7]

        # ... WITH (MATERIALS|PRODUCTS) IN <destination-path-prefix> FROM <step>
        elif (
            rule_len == 8
            and rule_lower[2] == "with"
            and rule_lower[4] == "in"
            and rule_lower[6] == "from"
        ):
            source_prefix = ""
            dest_type = rule_lower[3]
            dest_prefix = rule[5]
            dest_name = rule[7]

        # ... WITH (MATERIALS|PRODUCTS) FROM <step>
        elif (
            rule_len == 6
            and rule_lower[2] == "with"
            and rule_lower[4] == "from"
        ):
            source_prefix = ""
            dest_type = rule_lower[3]
            dest_prefix = ""
            dest_name = rule[5]

        else:
            raise securesystemslib.exceptions.FormatError(
                "Wrong rule format,"
                " match rules must have the format:\n\t"
                " MATCH <pattern> [IN <source-path-prefix>] WITH"
                " (MATERIALS|PRODUCTS) [IN <destination-path-prefix>] FROM <step>.\n"
                "Got: \n\t{}".format(rule)
            )

        if dest_type not in {"materials", "products"}:
            raise securesystemslib.exceptions.FormatError(
                "Wrong rule format,"
                " match rules must have either MATERIALS or PRODUCTS (case"
                " insensitive) as destination.\n"
                "Got: \n\t{}".format(rule)
            )

        return {
            "rule_type": rule_type,
            "pattern": pattern,
            "source_prefix": source_prefix,
            "dest_prefix": dest_prefix,
            "dest_type": dest_type,
            "dest_name": dest_name,
        }
    return None


def pack_rule(
    rule_type,
    pattern,
    source_prefix=None,
    dest_type=None,
    dest_prefix=None,
    dest_name=None,
):
    """
    <Purpose>
      Create an artifact rule in the passed arguments and return it as list
      as it is stored in a step's or inspection's expected_material or
      expected_product field.

    <Arguments>
      rule_type:
              One of "MATCH", "CREATE", "DELETE", MODIFY, ALLOW, DISALLOW,
              REQUIRE (case insensitive).

      pattern:
              A glob pattern to match artifact paths.

      source_prefix: (only for "MATCH" rules)
              A prefix for 'pattern' to match artifacts reported by the link
              corresponding to the step that contains the rule.

      dest_type: (only for "MATCH" rules)
              One of "MATERIALS" or "PRODUCTS" (case insensitive) to specify
              if materials or products of the link corresponding to the step
              identified by 'dest_name' should be matched.

      dest_prefix: (only for "MATCH" rules)
              A prefix for 'pattern' to match artifacts reported by the link
              corresponding to the step identified by 'dest_name'.

      dest_name: (only for "MATCH" rules)
              The name of the step whose corresponding link is used to match
              artifacts.


    <Exceptions>
      securesystemslib.exceptions.FormatError
              if any of the arguments is malformed.


    <Returns>
      One of the following rule formats in as a list
        MATCH <pattern> [IN <source-path-prefix>] WITH (MATERIALS|PRODUCTS)
            [IN <destination-path-prefix>] FROM <step>,
        CREATE <pattern>,
        DELETE <pattern>,
        MODIFY <pattern>,
        ALLOW <pattern>,
        DISALLOW <pattern>,
        REQUIRE <file>

      Note that REQUIRE is somewhat of a weird animal that does not use patterns
      but rather single filenames (for now).
    """
    _check_str(rule_type)
    _check_str(pattern)

    if rule_type.lower() not in ALL_RULES:
        raise securesystemslib.exceptions.FormatError(
            "'{0}' is not a valid "
            "'type'.  Rule type must be one of:  {1} (case insensitive).".format(
                rule_type, ", ".join(ALL_RULES)
            )
        )

    if rule_type.upper() == "MATCH":
        if not isinstance(dest_type, str) or not (
            dest_type.lower() == "materials" or dest_type.lower() == "products"
        ):
            raise securesystemslib.exceptions.FormatError(
                "'{}' is not a valid"
                " 'dest_type'. Rules of type 'MATCH' require a destination type of"
                " either 'MATERIALS' or 'PRODUCTS' (case insensitive).".format(
                    dest_type
                )
            )

        if not (isinstance(dest_name, str) and dest_name):
            raise securesystemslib.exceptions.FormatError(
                "'{}' is not a valid"
                " 'dest_name'. Rules of type 'MATCH' require a step name as a"
                " destination name.".format(dest_name)
            )

        # Construct rule
        rule = ["MATCH", pattern]

        if source_prefix:
            _check_str(source_prefix)

            rule += ["IN", source_prefix]

        rule += ["WITH", dest_type.upper()]

        if dest_prefix:
            _check_str(dest_prefix)
            rule += ["IN", dest_prefix]

        rule += ["FROM", dest_name]

    else:
        rule = [rule_type.upper(), pattern]

    return rule


def pack_rule_data(rule_data):
    """Shortcut for 'pack_rule' to pack a rule based on a rule_data dictionary
    as returned by 'unpack_rule'."""
    return pack_rule(**rule_data)


def pack_create_rule(pattern):
    """Shortcut for 'pack_rule' to pack a CREATE rule."""
    return pack_rule("CREATE", pattern)


def pack_delete_rule(pattern):
    """Shortcut for 'pack_rule' to pack a DELETE rule."""
    return pack_rule("DELETE", pattern)


def pack_modify_rule(pattern):
    """Shortcut for 'pack_rule' to pack a MODIFY rule."""
    return pack_rule("MODIFY", pattern)


def pack_allow_rule(pattern):
    """Shortcut for 'pack_rule' to pack an ALLOW rule."""
    return pack_rule("ALLOW", pattern)


def pack_disallow_rule(pattern):
    """Shortcut for 'pack_rule' to pack a DISALLOW rule."""
    return pack_rule("DISALLOW", pattern)


def pack_require_rule(filename):
    """
    Shortcut for 'pack_rule' to pack a REQUIRE rule:
    note that REQUIRE is somewhat of a weird animal that does not use patterns
    but rather single filenames (for now).
    """
    return pack_rule("REQUIRE", filename)
