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

require 'contrast/agent/assess/policy/propagation_node'
require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/agent/assess/events/event_data'

# In order to instrument some difficult methods like String#gsub, as it
# returns an enumerator, we need to instrument methods on Fiber.
# Specifically, we instrument 'rb_fiber_yield' and 'rb_fiber_new' in
# order to track what happens within Enumerator#next.
module Contrast
  module Extension
    module Assess
      # This Class provides us with a way to invoke Regexp propagation for those
      # methods which are too complex to fit into one of the standard
      # Contrast::Agent::Assess::Policy::Propagator molds without cluttering up the
      # Fiber Class or exposing our methods there.
      class FiberPropagator
        extend Contrast::Components::Logger::InstanceMethods
        extend Contrast::Components::Scope::InstanceMethods

        # we use funchook to patch rb_fiber_new the initialize method is not exposed by Ruby core
        FIBER_NEW_NODE_HASH = {
            'class_name' => 'Fiber',
            'instance_method' => true,
            'method_visibility' => 'public',
            'method_name' => 'c_new',
            'action' => 'CUSTOM',
            'source' => 'O',
            'target' => 'R',
            'patch_class' => 'Contrast::Extension::Assess::FiberPropagator',
            'patch_method' => 'track_rb_fiber_new'
        }.cs__freeze
        FIBER_NEW_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(FIBER_NEW_NODE_HASH)
        private_constant :FIBER_NEW_NODE_HASH
        private_constant :FIBER_NEW_NODE

        FIBER_YIELD_NODE_HASH = {
            'class_name' => 'Fiber',
            'instance_method' => true,
            'method_visibility' => 'public',
            'method_name' => 'c_yield',
            'action' => 'CUSTOM',
            'source' => 'O',
            'target' => 'R',
            'patch_class' => 'Contrast::Extension::Assess::FiberPropagator',
            'patch_method' => 'track_rb_fiber_yield'
        }.cs__freeze
        FIBER_YIELD_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(FIBER_YIELD_NODE_HASH)
        private_constant :FIBER_YIELD_NODE_HASH
        private_constant :FIBER_YIELD_NODE

        class << self
          def track_rb_fiber_yield fiber, _method, results
            return unless ::Contrast::ASSESS.enabled?

            # results will be nil if StopIteration was raised,
            # otherwise an Array of the yielded arguments
            return unless results.cs__is_a?(Array)

            with_contrast_scope do
              results.each do |result|
                next unless (result_properties = Contrast::Agent::Assess::Tracker.properties!(result))

                result_properties.splat_from(fiber, result)
                event_data = Contrast::Agent::Assess::Events::EventData.new(FIBER_YIELD_NODE, result, fiber, result, [])
                result_properties.build_event(event_data)
              end
            end
          rescue Exception => e # rubocop:disable Lint/RescueException
            logger.error('Unable to propagate during Fiber#yield', e)
          end

          def track_rb_fiber_new fiber, _enum, _enum_method, underlying, _underlying_method
            return unless ::Contrast::ASSESS.enabled?
            return unless underlying.is_a?(String) && !underlying.empty?

            with_contrast_scope do
              properties = Contrast::Agent::Assess::Tracker.properties!(fiber)
              return unless properties

              properties.splat_from(underlying, fiber)
              event_data = Contrast::Agent::Assess::Events::EventData.new(FIBER_NEW_NODE, fiber, underlying, fiber, [])
              properties.build_event(event_data)
            end
          rescue Exception => e # rubocop:disable Lint/RescueException
            logger.error('Unable to propagate during Fiber.new', e)
          end
        end
      end
    end
  end
end