# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Utils
    module Assess
      # This module will include all methods for some internal validations in the PropagationMethod module
      # and some other module methods from the same place, so we can ease the main module
      module PropagationMethodUtils
        APPEND_ACTION = 'APPEND'
        CENTER_ACTION = 'CENTER'
        INSERT_ACTION = 'INSERT'
        BUFFER_ACTION = 'BUFFER'
        KEEP_ACTION = 'KEEP'
        NEXT_ACTION = 'NEXT'
        NOOP_ACTION = 'NOOP'
        PREPEND_ACTION = 'PREPEND'
        REPLACE_ACTION = 'REPLACE'
        REMOVE_ACTION = 'REMOVE'
        REVERSE_ACTION = 'REVERSE'
        RESPONSE_ACTION = 'RESPONSE'
        SPLAT_ACTION = 'SPLAT'
        SPLIT_ACTION = 'SPLIT'
        DB_WRITE_ACTION = 'DB_WRITE'
        CUSTOM_ACTION = 'CUSTOM'

        ZERO_LENGTH_ACTIONS = [DB_WRITE_ACTION, CUSTOM_ACTION, KEEP_ACTION, REPLACE_ACTION, SPLAT_ACTION].cs__freeze

        PROPAGATION_ACTIONS = {
            APPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Append,
            CENTER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Center,
            INSERT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Insert,
            KEEP_ACTION => Contrast::Agent::Assess::Policy::Propagator::Keep,
            NEXT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Next,
            BUFFER_ACTION => Contrast::Agent::Assess::Policy::Propagator::Buffer,
            NOOP_ACTION => nil,
            PREPEND_ACTION => Contrast::Agent::Assess::Policy::Propagator::Prepend,
            REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
            REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
            REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
            RESPONSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Response,
            SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
            SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
        }.cs__freeze

        def determine_target propagation_node, ret, object, args
          target = propagation_node.targets[0]
          case target
          when Contrast::Utils::ObjectShare::OBJECT_KEY
            object
          when Contrast::Utils::ObjectShare::RETURN_KEY
            ret
          else
            args[target]
          end
        end

        # Custom actions tend to be the more complex of our propagations. Often, the method has to make decisions
        # about the target based on the context with which the method was called. As such, defer determining if the
        # target is valid to that method.
        #
        # In all other cases, a target is valid for propagation if it is not nil
        #
        # @param target [Object] the thing to which to propagate
        # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
        #   propagation event.
        # @return [Boolean]
        def valid_target? target, propagation_node
          return true if propagation_node.action == CUSTOM_ACTION

          !!target
        end

        # If the action required needs a length and the target does not have one, the length is not valid
        #
        # @param target [Object] the thing to which to propagate
        # @param action [String] the name of the action taken during this propagation
        # @return [Boolean]
        def valid_length? target, action
          return true if ZERO_LENGTH_ACTIONS.include?(action)

          if Contrast::Utils::DuckUtils.quacks_to?(target, :length)
            target.length != 0 # rubocop:disable Style/ZeroLengthPredicate
          else
            !target.to_s.empty?
          end
        end

        # Before we do any work, we should check if we even need to. If the source and target of this patcher are
        # not tracked, there's no need to do anything. A copy of nothing is still nothing.
        #
        # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
        #   propagation event.
        # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
        #   the invocation of the patched method.
        # @param target [Object] the thing to which to propagate
        # @param propagation_data [Contrast::Agent::Assess::Events::EventData] this will hold the
        #                         object [Object] the Object on which the method was invoked
        #                         args [Array<Object>] the Arguments with which the method was invoked
        # @return [Boolean]
        def can_propagate? propagation_node, preshift, target, propagation_data
          return false unless appropriate_target?(propagation_node, target)
          return true if Contrast::Utils::Assess::TrackingUtil.tracked?(target)
          return false unless appropriate_source?(propagation_node, propagation_data, preshift)

          propagation_node.sources.each do |source|
            case source
            when Contrast::Utils::ObjectShare::OBJECT_KEY
              source_object = if propagation_node.use_original_object?
                                propagation_data.object
                              else
                                preshift.object
                              end
              return true if Contrast::Utils::Assess::TrackingUtil.tracked?(source_object)
            else
              # has to be P, there's no ret source type (yet? ever?)
              return true if preshift.args && Contrast::Utils::Assess::TrackingUtil.tracked?(preshift.args[source])
            end
          end
          false
        end

        # We cannot propagate to frozen things that have not been updated to work with our property tracking,
        # unless they're duplicable and the return. We probably shouldn't propagate to frozen things at all, as
        # they're supposed to be immutable, but third parties do jenky things, so allow it as long as it is safe to
        # do.
        #
        # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
        #   propagation event.
        # @param target [Object] the Target to which to propagate.
        # @return [Boolean] if the target can be propagated to
        def appropriate_target? propagation_node, target
          # special handle Returns b/c we can do unfreezing magic during propagation
          return true if propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY

          Contrast::Agent::Assess::Tracker.trackable?(target)
        end

        # A source is appropriate if it is available for propagation
        #
        # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
        #   propagation event.
        # @param propagation_data [Contrast::Agent::Assess::Events::EventData] this will hold the
        #                         object [Object] the Object on which the method was invoked
        #                         args [Array<Object>] the Arguments with which the method was invoked
        # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
        #   the invocation of the patched method.
        # @return [Boolean]
        def appropriate_source? propagation_node, propagation_data, preshift
          return true if preshift

          propagation_node.use_original_object? && propagation_data&.object
        end
      end
    end
  end
end