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

module Contrast
  module Agent
    module Assess
      module Policy
        module Propagator
          # This class is specifically for String#tr(_s) 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.
          module Trim
            class << self
              # @param policy_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 ret [nil, String] the target to which to propagate.
              # @return [nil, String] ret
              def tr_tagger policy_node, preshift, ret, _block
                return ret unless ret && !ret.empty?
                return ret unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

                properties.copy_from(preshift.object, ret)
                handle_tr(policy_node, preshift, ret, properties)

                properties.build_event(policy_node, ret, preshift.object, ret, preshift.args, 1)
                ret
              end

              def tr_s_tagger patcher, preshift, ret, _block
                return unless ret && !ret.empty?
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))

                source = preshift.object
                args = preshift.args
                properties.splat_from(source, ret)
                properties.build_event(patcher, ret, source, ret, args)
                ret
              end

              private

              # @param policy_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 ret [String] the target to which to propagate.
              # @param properties [Contrast::Agent::Assess::Properties] the properties of the ret
              def handle_tr policy_node, preshift, ret, properties
                source = preshift.object
                replace_string = preshift.args[1]

                # if the replace string is empty, then there's a bunch of deletes. this functions the same as the
                # Removal propagation.
                if replace_string == Contrast::Utils::ObjectShare::EMPTY_STRING
                  Contrast::Agent::Assess::Policy::Propagator::Remove.handle_removal(policy_node, source, ret)
                  return
                end

                # Otherwise, we need to target each insertion point. Based on the spec for #tr & #tr_s, the find is
                # treated as a regexp range, excepting the `\` character, which we'll need to escape. This converts to
                # that form, wrapping the input in `[]`.
                find_string = preshift.args[0]
                find_string += '\\' if find_string.end_with?('\\')
                find_regexp = Regexp.new("[#{ find_string }]")

                # Find the first instance to be replaced. If there isn't one, than nothing changed here.
                idx = source.index(find_regexp)
                return unless idx

                # Iterate over each change and record where it happened. B/c this is a one to one replace, the index of
                # the replacement is always one; however, there may be adjacent replacements which become a single
                # range.
                start = idx
                stop = idx + 1
                remove_ranges = []
                while (idx = source.index(find_regexp, idx + 1))
                  # If the previous range ends at this index, we can expand that range to include this index.
                  # Otherwise, we need to record the held range and start a new one.
                  if stop != idx
                    remove_ranges << (start...stop)
                    start = idx
                  end
                  stop = idx + 1
                end
                # Be sure to capture the last range in the holder.
                remove_ranges << (start...stop)
                properties.delete_tags_at_ranges(remove_ranges, false)
              end
            end
          end
        end
      end
    end
  end
end