# typed: strict
# frozen_string_literal: true

require "os/linux/ld"

# {Pathname} extension for dealing with ELF files.
# @see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
module ELFShim
  extend T::Helpers

  MAGIC_NUMBER_OFFSET = 0
  private_constant :MAGIC_NUMBER_OFFSET
  MAGIC_NUMBER_ASCII = "\x7fELF"
  private_constant :MAGIC_NUMBER_ASCII

  OS_ABI_OFFSET = 0x07
  private_constant :OS_ABI_OFFSET
  OS_ABI_SYSTEM_V = 0
  private_constant :OS_ABI_SYSTEM_V
  OS_ABI_LINUX = 3
  private_constant :OS_ABI_LINUX

  TYPE_OFFSET = 0x10
  private_constant :TYPE_OFFSET
  TYPE_EXECUTABLE = 2
  private_constant :TYPE_EXECUTABLE
  TYPE_SHARED = 3
  private_constant :TYPE_SHARED

  ARCHITECTURE_OFFSET = 0x12
  private_constant :ARCHITECTURE_OFFSET
  ARCHITECTURE_I386 = 0x3
  private_constant :ARCHITECTURE_I386
  ARCHITECTURE_POWERPC = 0x14
  private_constant :ARCHITECTURE_POWERPC
  ARCHITECTURE_POWERPC64 = 0x15
  private_constant :ARCHITECTURE_POWERPC64
  ARCHITECTURE_ARM = 0x28
  private_constant :ARCHITECTURE_ARM
  ARCHITECTURE_X86_64 = 0x3E
  private_constant :ARCHITECTURE_X86_64
  ARCHITECTURE_AARCH64 = 0xB7
  private_constant :ARCHITECTURE_AARCH64

  requires_ancestor { Pathname }

  sig { params(path: T.anything).void }
  def initialize(path)
    @elf = T.let(nil, T.nilable(T::Boolean))
    @arch = T.let(nil, T.nilable(Symbol))
    @elf_type = T.let(nil, T.nilable(Symbol))
    @rpath = T.let(nil, T.nilable(String))
    @interpreter = T.let(nil, T.nilable(String))
    @dynamic_elf = T.let(nil, T.nilable(T::Boolean))
    @metadata = T.let(nil, T.nilable(Metadata))

    super
  end

  sig { params(offset: Integer).returns(Integer) }
  def read_uint8(offset)
    read(1, offset).unpack1("C")
  end

  sig { params(offset: Integer).returns(Integer) }
  def read_uint16(offset)
    read(2, offset).unpack1("v")
  end

  sig { returns(T::Boolean) }
  def elf?
    return @elf unless @elf.nil?

    return @elf = false if read(MAGIC_NUMBER_ASCII.size, MAGIC_NUMBER_OFFSET) != MAGIC_NUMBER_ASCII

    # Check that this ELF file is for Linux or System V.
    # OS_ABI is often set to 0 (System V), regardless of the target platform.
    @elf = [OS_ABI_LINUX, OS_ABI_SYSTEM_V].include? read_uint8(OS_ABI_OFFSET)
  end

  sig { returns(Symbol) }
  def arch
    return :dunno unless elf?

    @arch ||= case read_uint16(ARCHITECTURE_OFFSET)
    when ARCHITECTURE_I386 then :i386
    when ARCHITECTURE_X86_64 then :x86_64
    when ARCHITECTURE_POWERPC then :ppc32
    when ARCHITECTURE_POWERPC64 then :ppc64
    when ARCHITECTURE_ARM then :arm
    when ARCHITECTURE_AARCH64 then :arm64
    else :dunno
    end
  end

  sig { params(wanted_arch: Symbol).returns(T::Boolean) }
  def arch_compatible?(wanted_arch)
    return true unless elf?

    # Treat ppc64le and ppc64 the same
    wanted_arch = :ppc64 if wanted_arch == :ppc64le

    wanted_arch == arch
  end

  sig { returns(Symbol) }
  def elf_type
    return :dunno unless elf?

    @elf_type ||= case read_uint16(TYPE_OFFSET)
    when TYPE_EXECUTABLE then :executable
    when TYPE_SHARED then :dylib
    else :dunno
    end
  end

  sig { returns(T::Boolean) }
  def dylib?
    elf_type == :dylib
  end

  sig { returns(T::Boolean) }
  def binary_executable?
    elf_type == :executable
  end

  # The runtime search path, such as:
  # "/lib:/usr/lib:/usr/local/lib"
  sig { returns(T.nilable(String)) }
  def rpath
    metadata.rpath
  end

  # An array of runtime search path entries, such as:
  # ["/lib", "/usr/lib", "/usr/local/lib"]
  sig { returns(T::Array[String]) }
  def rpaths
    Array(rpath&.split(":"))
  end

  sig { returns(T.nilable(String)) }
  def interpreter
    metadata.interpreter
  end

  sig { params(interpreter: T.nilable(String), rpath: T.nilable(String)).void }
  def patch!(interpreter: nil, rpath: nil)
    return if interpreter.blank? && rpath.blank?

    save_using_patchelf_rb interpreter, rpath
  end

  sig { returns(T::Boolean) }
  def dynamic_elf?
    metadata.dynamic_elf?
  end

  sig { returns(T::Array[String]) }
  def section_names
    metadata.section_names
  end

  # Helper class for reading metadata from an ELF file.
  class Metadata
    sig { returns(ELFShim) }
    attr_reader :path

    sig { returns(T.nilable(String)) }
    attr_reader :dylib_id

    sig { returns(T::Boolean) }
    def dynamic_elf?
      @dynamic_elf
    end

    sig { returns(T.nilable(String)) }
    attr_reader :interpreter

    sig { returns(T.nilable(String)) }
    attr_reader :rpath

    sig { returns(T::Array[String]) }
    attr_reader :section_names

    sig { params(path: ELFShim).void }
    def initialize(path)
      require "patchelf"
      patcher = path.patchelf_patcher

      @path = path
      @dylibs = T.let(nil, T.nilable(T::Array[String]))
      @dylib_id = T.let(nil, T.nilable(String))
      @needed = T.let([], T::Array[String])

      dynamic_segment = patcher.elf.segment_by_type(:dynamic)
      @dynamic_elf = T.let(dynamic_segment.present?, T::Boolean)
      @dylib_id, @needed = if @dynamic_elf
        [patcher.soname, patcher.needed]
      else
        [nil, []]
      end

      @interpreter = T.let(patcher.interpreter, T.nilable(String))
      @rpath = T.let(patcher.runpath || patcher.rpath, T.nilable(String))
      @section_names = T.let(patcher.elf.sections.map(&:name).compact_blank, T::Array[String])

      @dt_flags_1 = T.let(dynamic_segment&.tag_by_type(:flags_1)&.value, T.nilable(Integer))
    end

    sig { returns(T::Array[String]) }
    def dylibs
      @dylibs ||= @needed.map { |lib| find_full_lib_path(lib).to_s }
    end

    private

    sig { params(basename: String).returns(::Pathname) }
    def find_full_lib_path(basename)
      basename = ::Pathname.new(basename)
      local_paths = rpath&.split(":")

      # Search for dependencies in the runpath/rpath first
      local_paths&.each do |local_path|
        local_path = OS::Linux::Elf.expand_elf_dst(local_path, "ORIGIN", path.parent)
        candidate = ::Pathname.new(local_path)/basename
        elf_candidate = ELFPathname.wrap(candidate)
        return candidate if candidate.exist? && elf_candidate.elf?
      end

      # Check if DF_1_NODEFLIB is set
      nodeflib_flag = if @dt_flags_1.nil?
        false
      else
        @dt_flags_1 & ELFTools::Constants::DF::DF_1_NODEFLIB != 0
      end

      linker_library_paths = OS::Linux::Ld.library_paths
      linker_system_dirs = OS::Linux::Ld.system_dirs

      # If DF_1_NODEFLIB is set, exclude any library paths that are subdirectories
      # of the system dirs
      if nodeflib_flag
        linker_library_paths = linker_library_paths.reject do |lib_path|
          linker_system_dirs.any? { |system_dir| Utils::Path.child_of? system_dir, lib_path }
        end
      end

      # If not found, search recursively in the paths listed in ld.so.conf (skipping
      # paths that are subdirectories of the system dirs if DF_1_NODEFLIB is set)
      linker_library_paths.each do |linker_library_path|
        candidate = Pathname(linker_library_path)/basename
        elf_candidate = ELFPathname.wrap(candidate)
        return candidate if candidate.exist? && elf_candidate.elf?
      end

      # If not found, search in the system dirs, unless DF_1_NODEFLIB is set
      unless nodeflib_flag
        linker_system_dirs.each do |linker_system_dir|
          candidate = Pathname(linker_system_dir)/basename
          elf_candidate = ELFPathname.wrap(candidate)
          return candidate if candidate.exist? && elf_candidate.elf?
        end
      end

      basename
    end
  end
  private_constant :Metadata

  sig { params(new_interpreter: T.nilable(String), new_rpath: T.nilable(String)).void }
  def save_using_patchelf_rb(new_interpreter, new_rpath)
    patcher = patchelf_patcher
    patcher.interpreter = new_interpreter if new_interpreter.present?
    patcher.rpath = new_rpath if new_rpath.present?
    patcher.save(patchelf_compatible: true)
  end

  # Don't cache the patcher; it keeps the ELF file open so long as it is alive.
  # Instead, for read-only access to the ELF file's metadata, fetch it and cache
  # it with {Metadata}.
  sig { returns(::PatchELF::Patcher) }
  def patchelf_patcher
    require "patchelf"
    ::PatchELF::Patcher.new to_s, on_error: :silent
  end

  sig { returns(Metadata) }
  def metadata
    @metadata ||= Metadata.new(self)
  end
  private :metadata

  sig { returns(T.nilable(String)) }
  def dylib_id
    metadata.dylib_id
  end

  sig { params(except: Symbol, resolve_variable_references: T::Boolean).returns(T::Array[String]) }
  def dynamically_linked_libraries(except: :none, resolve_variable_references: true)
    metadata.dylibs
  end
end
require "os/linux/elf/os"
