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

require 'contrast/components/interface'
require 'contrast/utils/duck_utils'

module Contrast
  module Agent
    module Assess
      module Policy
        module Propagator
          # This class is specifically for String#(g)sub propagation
          #
          # Disclaimer: there may be a better way, but we're
          # in a 'get it work' state. hopefully, we'll be in
          # a 'get it right' state soon.
          class Substitution
            include Contrast::Components::Interface
            access_component :logging

            CAPTURE_GROUP_REGEXP = /\\[[:digit:]]/.cs__freeze
            CAPTURE_NAME_REGEXP = /\\k<[[:alpha:]]/.cs__freeze

            class << self
              # gsub is hard. there are four versions of this method
              # 1) pattern, replacement (my fav)
              # 2) pattern, hash        (not bad)
              # 3) pattern, block       (are you kidding me?)
              # 4) pattern              (plz no)
              #
              # In addition, it requires things from $~ & $1-9, which
              # are method scoped. Rather than fight that, we'll
              # call gsub from C land using a CUSTOM patch.
              def gsub_tagger patcher, preshift, ret, block
                substitution_tagger(patcher, preshift, ret, !block.nil?)
              end

              def sub_tagger patcher, preshift, ret, block
                substitution_tagger(patcher, preshift, ret, !block.nil?, false)
              end

              private

              def substitution_tagger patcher, preshift, ret, block, global = true
                return ret unless ret

                begin
                  source = preshift.object
                  self_tracked = Contrast::Agent::Assess::Tracker.tracked?(source)
                  args = preshift.args[1]
                  incoming_tracked = args && determine_tracked(args)
                  return ret unless self_tracked || incoming_tracked

                  parent_events = []
                  if block
                    block_sub(self_tracked, source, ret)
                  elsif args.is_a?(String)
                    string_sub(parent_events, self_tracked, preshift, ret, args, incoming_tracked, global)
                  elsif args.is_a?(Hash)
                    hash_sub(self_tracked, source, ret)
                  else # Enumerator, only for gsub
                    pattern_gsub(parent_events, preshift, ret)
                  end

                  if self_tracked
                    source_properties = Contrast::Agent::Assess::Tracker.properties(source)
                    parent_event = source_properties&.event
                    parent_events.prepend(parent_event) if parent_event
                  end
                  string_build_event(parent_events, patcher, preshift, ret)
                rescue StandardError => e
                  logger.error('Unable to apply gsub propagator', e)
                end
                ret
              end

              def determine_tracked args
                # if there's no arg, it can't be tracked
                return false unless args

                # if it's a string, just ask if it's tracked
                case args
                when String
                  Contrast::Agent::Assess::Tracker.tracked?(args)
                  # if it's a hash, ask if it has a tracked string
                when Hash
                  args.values.any? { |value| value.is_a?(String) && Contrast::Agent::Assess::Tracker.tracked?(value) }
                  # this should never happen
                else
                  false
                end
              end

              def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

                incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
                parent_event = incoming_properties&.event
                parent_events << parent_event if parent_event

                pattern = preshift.args[0]
                source = preshift.object

                # We can't efficiently find the places that things were
                # copied from regexp / captures. Trading accuracy for
                # performance
                if incoming.match?(CAPTURE_GROUP_REGEXP) || incoming.match?(CAPTURE_NAME_REGEXP)
                  properties.splat_from(source, ret) if self_tracked
                  return
                end

                # if it's just a straight insert, that we can do
                # Copy the tags from us to the return
                properties.copy_from(source, ret)
                # Figure out where inserts occurred
                last_idx = 0
                ranges = []
                # For each insert, move the tag ranges
                while last_idx
                  idx = source.index(pattern, last_idx)
                  break unless idx

                  last_idx = idx ? idx + 1 : nil
                  start_index = idx
                  end_index = idx + incoming.length
                  ranges << (start_index...end_index)
                  break unless global
                end
                properties.delete_tags_at_ranges(ranges)
                properties.shift_tags(ranges)
                return unless incoming_tracked
                return unless incoming_properties

                tags = incoming_properties.tag_keys
                ranges.each do |range|
                  tags.each do |tag|
                    properties.add_tag(tag, range)
                  end
                end
              end

              def block_sub self_tracked, source, ret
                return unless self_tracked

                properties = Contrast::Agent::Assess::Tracker.properties!(ret)
                properties&.splat_from(source, ret)
              end

              def hash_sub self_tracked, source, ret
                return unless self_tracked

                properties = Contrast::Agent::Assess::Tracker.properties!(ret)
                properties&.splat_from(source, ret)
              end

              def pattern_gsub parent_events, preshift, ret
                source = preshift.object
                return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

                source_properties.tag_keys.each do |key|
                  properties.add_tag(key, 0...1)
                end
                parent_event = source_properties.event
                parent_events << parent_event if parent_event
              end

              def string_build_event parent_events, patcher, preshift, ret
                return unless Contrast::Agent::Assess::Tracker.tracked?(ret)

                properties = Contrast::Agent::Assess::Tracker.properties(ret)
                args = preshift.args
                properties.build_event(
                    patcher,
                    ret,
                    preshift.object,
                    ret,
                    args,
                    2)
                properties.event.instance_variable_set(:@_parent_events, parent_events)
              end
            end
          end
        end
      end
    end
  end
end