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

require 'ffi'
# require the gem
require 'contrast-agent-lib'
require 'contrast/utils/object_share'

module Contrast
  module AgentLib
    # This module is defined in Rust as external, we used it here.
    # Initializes the AgentLib. Here will be all methods from
    # the C bindings contrast_c::panic_error module.
    module Panic
      extend FFI::Library
      ffi_lib ContrastAgentLib::CONTRAST_C

      attach_function :last_error_message, %i[pointer int32 pointer int32], :int32
      attach_function :last_error_message_length, [], :int32
      attach_function :last_error_stack_length, [], :int32

      private

      # Returns the last Error message saved for the thread.
      # If there is no message it will return empty string.
      #
      # @return [String] Empty string if no errors, or a
      # message + stack trace concat String.
      def dl__get_error
        message_length = last_error_message_length
        stack_length = last_error_stack_length
        return Contrast::Utils::ObjectShare::EMPTY_STRING if message_length.zero?

        # If the buffer size is retrieved as 0 we need to add 1 byte for it
        # else the Agent-Lib check for allocated memory would fail.
        stack_length += 1 if stack_length.zero?
        message_buffer = FFI::MemoryPointer.new(:char, message_length)
        stack_buffer = FFI::MemoryPointer.new(:char, stack_length)
        return dl__parse_error(message_buffer, stack_buffer) if last_error_message(message_buffer,
                                                                                   message_length,
                                                                                   stack_buffer,
                                                                                   stack_length).positive?

        Contrast::Utils::ObjectShare::EMPTY_STRING
      ensure
        # The free methods are auto called form the FFI::MemoryPointer
        # when the variable is out of scope. Making sure it's all free,
        # and call with safe navigation if is been already cleaned.
        message_buffer&.free
        stack_buffer&.free
      end

      # Retrieves the buffer bytecode and transforms it into String.
      #
      # @param buffer [FFI::MemoryPointer, nil] of type char (String)
      # @return [String] the received message.
      def dl__read_buffer buffer
        return Contrast::Utils::ObjectShare::EMPTY_STRING unless buffer

        string = buffer.read_string
        return string unless string.empty?

        # If there is a incorrect buffer size or the message can't be read
        # from the FFI::MemoryPointer#read_string method we can still try
        # and get the partial message:
        bytes = buffer.get_array_of_int8(0, buffer.size)&.map { |byte| byte.zero? ? nil : byte }
        bytes.compact.pack('C*').force_encoding('UTF-8') if bytes&.cs__is_a?(Array)
      end

      # The stack_message is still not integrated in the AgentLib.
      # It always returns "". For now we are returning the message,
      # but keeping the functionality for feature use.
      #
      # @param message_buffer [FFI::MemoryPointer, nil] of type char (String)
      # @param stack_buffer [FFI::MemoryPointer, nil] of type char (String)
      # @return [String] the received error message with stack.
      def dl__parse_error message_buffer, stack_buffer
        message = dl__read_buffer(message_buffer)
        stack = dl__read_buffer(stack_buffer)

        error_message = "Message: #{ message }"
        error_message + " Stack Trace: #{ stack }" unless stack.empty?
        error_message
      end
    end
  end
end