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

require 'contrast/agent/assess/policy/propagator/select'

module Contrast
  module Agent
    module Assess
      module Policy
        module Propagator
          # Propagation that results in all the tags of the source being
          # applied to the target at the point of insertion. The target's
          # preexisting tags are shifted to account for this insertion.
          class Buffer < Contrast::Agent::Assess::Policy::Propagator::Base
            class << self
              # For the source, append its tags to the target.
              # Once the tag is applied, shift it to the location of the insert
              # Unlike additive propagation, this currently only supports one source
              # We assume that insert changes the preshift target
              def propagate_insert propagation_node, preshift, target
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))

                source = find_source(propagation_node.sources[0], preshift)

                patcher_target = propagation_node.targets[0]
                preshift_target = case patcher_target
                                  when Contrast::Utils::ObjectShare::OBJECT_KEY
                                    preshift.object
                                  else
                                    # this is hardly reached b/c insert supports only one source
                                    # changed to second argument: string.insert[idx, string_to_insert]
                                    # previously was args[int] => produces exception
                                    preshift.args[1]
                                  end

                # in case of situations, where buffer is initialized with X size and it's not fully
                preshift_target = preshift_target.get_string
                target = target.get_string
                # Find the first difference between the source to which
                # we inserted and the result. That is the insertion
                # point on which all tags need to be adjusted
                # If the insertion point is the end of the string, preshift length is returned
                # https://stackoverflow.com/questions/31714522/find-the-first-differing-character-between-two-strings-in-ruby
                insert_point = (0...preshift_target.length).find do |i|
                  preshift_target[i] != target[i]
                end || preshift_target.length
                # Depending what's inserted, we might be wrong. For instance, inserting 'foo'
                # into 'asdfasdf' could result in 'asdfoofasdf'. we'd be off by one b/c of the 'f'
                insert_point = target.rindex(source, insert_point)
                overflow = insert_point...(insert_point + source.length)

                # handle shifting the inserted range
                properties.shift_tags([overflow])

                properties.copy_from(source, target, insert_point, propagation_node.untags)
                properties.cleanup_tags
              end

              # This method will implement both KEEP and SPLAT actions.
              #
              # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
              # @param preshift [Contrast::Agent::Assess::Preshift]
              # @param ret [Object] Return target from method invocation.
              def buffer_keep_splat propagation_node, preshift, ret
                # If IO::Buffer gets duplicated the dup buffer is not allocated,
                # it's empty, So a way of doing it is to use the original object
                # itself. The class is included in the preshift's UNDUPLICABLE_MODULES.
                return unless preshift.object
                # We need the return in both propagation cases. So this propagation is
                # applied in the Contrast::Utils::Patching::PatchUtils.apply_post_patch.
                # we use this for the IO::Buffer#get_value and it requires 2 args always.
                return unless ret || Contrast::Agent::Assess::Tracker.tracked?(preshift.object)

                if preshift.args[1] == 0 # rubocop:disable Style/NumericPredicate
                  # KEEP
                  Contrast::Agent::Assess::Policy::Propagator::Keep.propagate(propagation_node, preshift, ret)
                else
                  # SPLAT
                  Contrast::Agent::Assess::Policy::Propagator::Splat.propagate(propagation_node, preshift, ret)
                end
              end

              # This method will implement both SELECT and KEEP actions for
              # IO::Buffer
              #
              # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
              # @param preshift [Contrast::Agent::Assess::Preshift]
              # @param ret [Object] Return targer from method invocation.
              # @param _block [nil, {}] block passed.
              def propagate_keep_select propagation_node, preshift, ret, _block
                # We need the return in both propagation cases. So this propagation is
                # applied in the Contrast::Utils::Patching::PatchUtils.apply_post_patch.
                # If IO::Buffer gets duplicated the dup buffer is not allocated,
                # it's empty, So a way of doing it is to use the original object
                # itself. The class is handled with no dup in preshift.
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret)) &&
                    (source = preshift.object)

                # KEEP means the tags just pass from the source to the target as is.
                # There is a case with IO::Buffer use, when we don't pass any args,
                # and the target (ret) in this case is the whole strings. We should
                # use Keep action. Otherwise the SELECT action covers all other cases.
                if preshift.args.empty?
                  # KEEP
                  properties.copy_from(source, ret, 0, propagation_node.untags)
                  ret
                else
                  # SELECT
                  Contrast::Agent::Assess::Policy::Propagator::Select.select_tagger(propagation_node, preshift, ret,
                                                                                    nil)
                end
              end
            end
          end
        end
      end
    end
  end
end