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

module Contrast
  module Utils
    module Patching
      # This module will include all methods for different patch applies from Patch module and some other module
      # methods from the same place, so we can ease the main module
      module PatchUtils
        # Given a module and method, construct an expected name for the alias by which Contrast will reference the
        # original.
        #
        # @param patched_class [Module] the module being patched
        # @param patched_method [Symbol] the method being patched
        # @return [Symbol]
        def build_method_name patched_class, patched_method
          (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
            patched_class.cs__name.gsub('::', '_').downcase +
            Contrast::Utils::ObjectShare::UNDERSCORE +
            patched_method.to_s).to_sym
        end

        # Given a method, return a symbol in the format
        # <method_start>_unbound_<method_name>
        def build_unbound_method_name patcher_method
          "#{ Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START }unbound"\
          "#{ Contrast::Utils::ObjectShare::UNDERSCORE }"\
          "#{ patcher_method }".to_sym
        end

        # ===== PATCH APPLIERS =====
        # THIS IS CALLED FROM C. Do not change the signature lightly.
        #
        # This method functions to call the infilter methods from our patches, allowing for analysis and reporting at
        # the point just before the patched code is invoked.
        #
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given
        #   method.
        # @param method [Symbol] The method into which we're patching
        # @param exception [StandardError] Any exception raised during the call of the patched method.
        # @param object [Object] The object on which the method is invoked, typically what would be returned by self.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        def apply_pre_patch method_policy, method, exception, object, args
          apply_protect(method_policy, method, exception, object, args)
          apply_inventory(method_policy, method, exception, object, args)
        rescue Contrast::SecurityException => e
          # We were told to block something, so we gotta. Don't catch this one, let it get back to our Middleware or
          # even all the way out to the framework
          raise(e)
        rescue StandardError => e
          # Anything else was our bad and we gotta catch that to allow for normal application flow
          logger.error('Unable to apply pre patch to method.', e)
        end

        # THIS IS CALLED FROM C. Do not change the signature lightly.
        #
        # This method functions to call the infilter methods from our patches, allowing for analysis and reporting at
        # the point just after the patched code is invoked
        #
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given
        #   method.
        # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to the
        #   invocation of the patched method.
        # @param object [Object] The object on which the method was invoked, typically what would be returned by self.
        # @param ret [Object] The return of the method that was invoked.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        # @param block [Proc] The block passed to the method that was invoked.
        def apply_post_patch method_policy, preshift, object, ret, args, block
          apply_assess(method_policy, preshift, object, ret, args, block)
        rescue StandardError => e
          logger.error('Unable to apply post patch to method.', e)
        end

        # Apply the Protect patch which applies to the given method.
        #
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given
        #   method.
        # @param method [Symbol] The method into which we're patching
        # @param exception [StandardError] Any exception raised during the call of the patched method.
        # @param object [Object] The object on which the method is invoked, typically what would be returned by self.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        def apply_protect method_policy, method, exception, object, args
          return unless ::Contrast::AGENT.enabled?
          return unless ::Contrast::PROTECT.enabled?

          apply_trigger_only(method_policy&.protect_node, method, exception, object, args)
        end

        # Apply the Inventory patch which applies to the given method.
        #
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given
        #   method.
        # @param method [Symbol] The method into which we're patching
        # @param exception [StandardError] Any exception raised during the call of the patched method.
        # @param object [Object] The object on which the method is invoked, typically what would be returned by self.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        def apply_inventory method_policy, method, exception, object, args
          return unless ::Contrast::INVENTORY.enable

          apply_trigger_only(method_policy&.inventory_node, method, exception, object, args)
        end

        # Apply the Assess patches which apply to the given method.
        #
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] Mapping of the triggers on the given
        #   method.
        # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to the
        #   invocation of the patched method.
        # @param object [Object] The object on which the method was invoked, typically what would be returned by self.
        # @param ret [Object] The return of the method that was invoked.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        # @param block [Proc] The block passed to the method that was invoked.
        def apply_assess method_policy, preshift, object, ret, args, block
          return ret unless method_policy && ::Contrast::ASSESS.enabled?

          current_context = Contrast::Agent::REQUEST_TRACKER.current
          return ret if current_context && !current_context.analyze_request?

          trigger_node = method_policy.trigger_node
          if trigger_node
            Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args)
          end
          if method_policy.source_node
            Contrast::Agent::Assess::Policy::SourceMethod.apply_source(method_policy, object, ret, args)
          end
          if method_policy.propagation_node
            Contrast::Agent::Assess::Policy::PropagationMethod.apply_propagation(
                method_policy,
                preshift,
                object,
                ret,
                args,
                block)
          end
        rescue StandardError => e
          logger.error('Unable to assess method call.', e)
        rescue Exception => e # rubocop:disable Lint/RescueException
          logger.error('Unable to assess method call.', e)
          raise(e)
        ensure
          ret.rewind if Contrast::Utils::IOUtil.should_rewind?(ret)
        end

        # Generic invocation of the Inventory or Protect patch which apply to the given method.
        #
        # @param trigger_node [Contrast::Agent::Inventory::Policy::TriggerNode] Mapping of the specific trigger on the
        #   given method.
        # @param method [Symbol] The method into which we're patching
        # @param exception [StandardError] Any exception raised during the call of the patched method.
        # @param object [Object] The object on which the method is invoked, typically what would be returned by self.
        # @param args [Array<Object>] The arguments passed to the method being invoked.
        def apply_trigger_only trigger_node, method, exception, object, args
          return unless trigger_node

          # If that rule only applies in the case of an exception being thrown and there's no exception here, move
          # along, or vice versa
          return if trigger_node.on_exception && !exception
          return if !trigger_node.on_exception && exception

          # Each patch has an applicator that handles logic for it. Think of this as being similar to propagator
          # actions, most closely resembling CUSTOM - they all have a common interface but their own logic based on
          # what's in the method(s) they've been patched into.
          # Each patch also knows the method of its applicator. Some things, like AppliesXxeRule, have different
          # methods depending on the library patched. This lets us handle the boilerplate of patching while still
          # allowing for custom handling of the methods.
          applicator_method = trigger_node.applicator_method
          # By calling send like this, we can reuse all the patching. We `send` to the given method of the given class
          # (applicator) since they all accept the same inputs
          trigger_node.applicator.send(applicator_method, method, exception, trigger_node.properties, object, args)
        end
      end
    end
  end
end