# Copyright (c) 2023 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
          # Propagation that results in all the tags of the source being applied to the totality of the target and then
          # those sections which have been removed from the target are removed from the tags. The target's preexisting
          # tags are also updated by this removal.
          class Remove < Contrast::Agent::Assess::Policy::Propagator::Base
            class << self
              # For the source, append its tags to the target. Once the tag is applied, remove the section that was
              # removed by the delete. Unlike additive propagation, this currently only supports one source.
              #
              # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] Current node
              # governing the propagation.
              # @param preshift_or_object [Contrast::Agent::Assess::Preshift] current preshift state or
              # source object passed if track_original_object flag is active.
              # @param target [Object] To where the tainted data flows.
              def propagate propagation_node, preshift_or_object, target
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))

                source = if propagation_node.use_original_object?
                           # for now this is used with string based object and with methods not mutating
                           # original object (clearing empty spaces, \n..) so we will always return 'O' key
                           # which is the object itself. Because preshift is disabled for this methods
                           # we use it's parameter to pass in the original object( in this case just a unchained
                           # source), preshift = object. To see list of methods in which this is used:
                           # Contrast::Utils::MethodCheck::ORIGINAL_OBJECT_METHODS
                           preshift_or_object
                         else
                           find_source(propagation_node.sources[0], preshift_or_object)
                         end
                properties.copy_from(source, target, 0, propagation_node.untags)
                handle_removal(propagation_node, source, target)
              end

              # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node responsible for the
              # propagation action required by this method.
              # @param source [Object] the object to which the source is being appended
              # @param target [Object] the object to which the source is being appended
              def handle_removal propagation_node, source, target
                return unless source
                return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))

                source_string = source.is_a?(String) ? source : source.to_s
                # If the lengths are the same, we should just copy the tags because nothing was removed, but a new
                # instance could have been created. copy_from will handle the case where the source is the target.
                if source_string.length == target.length
                  properties.copy_from(source, target, 0, propagation_node.untags)
                  return
                end

                source_idx = 0
                target_idx = 0

                remove_ranges = []
                start = nil

                # loop over the target, the result of the delete every range of characters that it differs from the
                # source represents a section that was deleted. these sections need to have their tags updated
                while target_idx < target.length
                  target_char = target[target_idx]
                  source_char = source_string[source_idx]
                  if target_char == source_char
                    target_idx += 1
                    if start
                      remove_ranges << (start...source_idx)
                      start = nil
                    end
                  else
                    start ||= source_idx
                  end
                  source_idx += 1
                end

                # once we're done looping over the target, anything left over is extra from the source that was
                # deleted. tags applying to it need to be removed.
                remove_ranges << (source_idx...source_string.length) if source_idx != source_string.length

                # handle deleting the removed ranges
                properties.delete_tags_at_ranges(remove_ranges)
              end
            end
          end
        end
      end
    end
  end
end