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

require 'contrast/extension/module'
require 'contrast/utils/object_share'
require 'contrast/utils/lru_cache'

module Contrast
  module Utils
    # Utility methods for exploring the complete space of Objects
    class ClassUtil
      @lru_cache = LRUCache.new(300)
      @string_cache = LRUCache.new(300)
      class << self
        # some classes have had things prepended to them, like Marshal in Rails 5 and higher. Their
        # ActiveSupport::MarshalWithAutoloading will break our alias patching approach, as will any other prepend on
        # something that we touch. Prepend and Alias are inherently incompatible monkey patching approaches. As such,
        # we need to know if something has been prepended to.
        #
        # @param mod [Module] the Module to check to see if it has had something prepended
        # @param ancestors [Array<Module>] the array of ancestors for the mod
        # @return [Boolean] if the mod has been prepended or not
        def prepended? mod, ancestors = nil
          ancestors ||= mod.ancestors
          ancestors[0] != mod
        end

        # return true if the given method is overwritten by one of the ancestors in the ancestor change that comes
        # before the given module
        #
        # @param mod [Module] the Module to check to see if it has had something prepended
        # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy] the policy that holds the method we
        #   need to check
        # @return [Boolean] if this method specifically was prepended
        def prepended_method? mod, method_policy
          target_module = determine_target_class(mod, method_policy.instance_method)
          ancestors = target_module.ancestors
          return false unless prepended?(target_module, ancestors)

          ancestors.each do |ancestor|
            break if ancestor == target_module

            methods = ancestor.instance_methods(false)
            return true if methods.include?(method_policy.method_name)
          end
          false
        end

        # Return a String representing the object invoking this method in the form expected by our dataflow events.
        # After implementing the LRU Cache, we firstly need to check if already had that object cached and if we have
        # it - we can return it directly; otherwise we'll calculate and store the result before returning.
        #
        # Combining of the caches have close performance, but keeping the two with current implementation has
        # a slight advantage in performance. For now we can keep the things the way they are.
        #
        # @param object [Object, nil] the entity to convert to a String
        # @return [String, Object] the human readable form of the String, as defined by
        #   https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
        def to_contrast_string object
          # Only treat object like a string if it actually is a string+ some subclasses of String override string
          # methods we depend on
          if object.cs__class == String
            return @string_cache[object] if @string_cache.key?(object)

            @string_cache[object] = to_cached_string(object) || object.dup
          else
            return @lru_cache[object.__id__] if @lru_cache.key?(object.__id__)

            @lru_cache[object.__id__] = convert_object(object)
          end
        end

        def convert_object object
          if object.nil?
            Contrast::Utils::ObjectShare::NIL_STRING
          elsif object.cs__is_a?(Symbol)
            ":#{ object }"
          elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
            "#{ object.cs__name }@#{ object.__id__ }"
          elsif object.cs__is_a?(Regexp)
            object.source
          elsif use_to_s?(object)
            object.to_s
          else
            "#{ object.cs__class.cs__name }@#{ object.__id__ }"
          end
        end

        # The method Module.const_defined? can raise an exception if the constant is poorly named. As such, we need to
        # handle the case where that exception is raised.
        #
        # @param name [String] the name of the constant to look up
        # @return [Boolean]
        def truly_defined? name
          return false unless name

          Module.cs__const_defined?(name)
        rescue NameError # account for nonsense / poorly formatted constants
          false
        end

        private

        # Some objects have nice to_s that we can use to make them human readable. If they do, we should leverage them.
        # We used to do this by default, but this opened us up to danger, so we're instead using an allow list
        # approach.
        #
        # @param object [Object] something that may have a safe to_s method
        # @return [Boolean] if we should invoke to_s to represent the object
        def use_to_s? object
          return true if object.cs__is_a?(Numeric)
          return true if defined?(Arel::Nodes::SqlLiteral) && object.cs__is_a?(Arel::Nodes::SqlLiteral)

          false
        end

        # Find the target class based on the instance, or module, provided. If a module, return it.
        #
        # @param mod [Module] the Module, or instance of a Module, that we need to check
        # @param is_instance [Boolean] is the object provided an instance of a class, requiring lookup by class
        # @return [Module]
        def determine_target_class mod, is_instance
          return mod if mod.singleton_class?

          return mod.cs__singleton_class unless is_instance

          mod
        end

        # If the String matches a common String in our ObjectShare, return that rather that for use as the
        # representation of the String rather than forcing a duplication of the String.
        #
        # @param string [String] some string of which we want a Contrast representation.
        # @return [String, nil] the ObjectShare version of the String or nil
        def to_cached_string string
          return Contrast::Utils::ObjectShare::EMPTY_STRING if string.empty?
          return Contrast::Utils::ObjectShare::SLASH if string == Contrast::Utils::ObjectShare::SLASH
          return Contrast::Utils::ObjectShare::EQUALS if string == Contrast::Utils::ObjectShare::EQUALS

          nil
        end
      end
    end
  end
end