# frozen_string_literal: true

# @see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
module ELFShim
  MAGIC_NUMBER_OFFSET = 0
  MAGIC_NUMBER_ASCII = "\x7fELF"

  OS_ABI_OFFSET = 0x07
  OS_ABI_SYSTEM_V = 0
  OS_ABI_LINUX = 3

  TYPE_OFFSET = 0x10
  TYPE_EXECUTABLE = 2
  TYPE_SHARED = 3

  ARCHITECTURE_OFFSET = 0x12
  ARCHITECTURE_I386 = 0x3
  ARCHITECTURE_POWERPC = 0x14
  ARCHITECTURE_ARM = 0x28
  ARCHITECTURE_X86_64 = 0x62
  ARCHITECTURE_AARCH64 = 0xB7

  def read_uint8(offset)
    read(1, offset).unpack1("C")
  end

  def read_uint16(offset)
    read(2, offset).unpack1("v")
  end

  def elf?
    return @elf if defined? @elf
    return @elf = false unless 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

  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 :powerpc
    when ARCHITECTURE_ARM then :arm
    when ARCHITECTURE_AARCH64 then :arm64
    else :dunno
    end
  end

  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

  def dylib?
    elf_type == :dylib
  end

  def binary_executable?
    elf_type == :executable
  end

  def interpreter
    return @interpreter if defined? @interpreter

    @interpreter = if HOMEBREW_PATCHELF_RB
      begin
        patchelf_patcher.interpreter
      rescue PatchELF::PatchError => e
        opoo e unless e.to_s.start_with? "No interpreter found"
        nil
      end
    elsif (patchelf = DevelopmentTools.locate "patchelf")
      interp = Utils.popen_read(patchelf, "--print-interpreter", to_s, err: :out).strip
      $CHILD_STATUS.success? ? interp : nil
    elsif (file = DevelopmentTools.locate("file"))
      output = Utils.popen_read(file, "-L", "-b", to_s, err: :out).strip
      output[/^ELF.*, interpreter (.+?), /, 1]
    else
      raise "Please install either patchelf or file."
    end
  end

  def dynamic_elf?
    return @dynamic_elf if defined? @dynamic_elf

    @dynamic_elf = if HOMEBREW_PATCHELF_RB
      patchelf_patcher.instance_variable_get(:@elf).segment_by_type(:DYNAMIC).present?
    elsif which "readelf"
      Utils.popen_read("readelf", "-l", to_path).include?(" DYNAMIC ")
    elsif which "file"
      !Utils.popen_read("file", "-L", "-b", to_path)[/dynamic|shared/].nil?
    else
      raise "Please install either readelf (from binutils) or file."
    end
  end

  class Metadata
    attr_reader :path, :dylib_id, :dylibs

    def initialize(path)
      @path = path
      @dylibs = []
      @dylib_id, needed = needed_libraries path
      return if needed.empty?

      ldd = DevelopmentTools.locate "ldd"
      ldd_output = Utils.popen_read(ldd, path.expand_path.to_s).split("\n")
      return unless $CHILD_STATUS.success?

      ldd_paths = ldd_output.map do |line|
        match = line.match(/\t.+ => (.+) \(.+\)|\t(.+) => not found/)
        next unless match

        match.captures.compact.first
      end.compact
      @dylibs = ldd_paths.select do |ldd_path|
        next true unless ldd_path.start_with? "/"

        needed.include? File.basename(ldd_path)
      end
    end

    private

    def needed_libraries(path)
      if HOMEBREW_PATCHELF_RB
        needed_libraries_using_patchelf_rb path
      elsif DevelopmentTools.locate "readelf"
        needed_libraries_using_readelf path
      elsif DevelopmentTools.locate "patchelf"
        needed_libraries_using_patchelf path
      else
        return [nil, []] if path.basename.to_s == "patchelf"

        raise "patchelf must be installed: brew install patchelf"
      end
    end

    def needed_libraries_using_patchelf_rb(path)
      patcher = path.patchelf_patcher
      return [nil, []] unless patcher

      soname = begin
        patcher.soname
      rescue PatchELF::PatchError => e
        opoo e unless e.to_s.start_with? "Entry DT_SONAME not found, not a shared library?"
        nil
      end
      needed = begin
        patcher.needed
      rescue PatchELF::PatchError => e
        opoo e
        []
      end
      [soname, needed]
    end

    def needed_libraries_using_patchelf(path)
      return [nil, []] unless path.dynamic_elf?

      patchelf = DevelopmentTools.locate "patchelf"
      if path.dylib?
        command = [patchelf, "--print-soname", path.expand_path.to_s]
        soname = Utils.safe_popen_read(*command).chomp
      end
      command = [patchelf, "--print-needed", path.expand_path.to_s]
      needed = Utils.safe_popen_read(*command).split("\n")
      [soname, needed]
    end

    def needed_libraries_using_readelf(path)
      soname = nil
      needed = []
      command = ["readelf", "-d", path.expand_path.to_s]
      lines = Utils.popen_read(*command, err: :out).split("\n")
      lines.each do |s|
        next if s.start_with?("readelf: Warning: possibly corrupt ELF header")

        filename = s[/\[(.*)\]/, 1]
        next if filename.nil?

        if s.include? "(SONAME)"
          soname = filename
        elsif s.include? "(NEEDED)"
          needed << filename
        end
      end
      [soname, needed]
    end
  end

  def patchelf_patcher
    return unless HOMEBREW_PATCHELF_RB

    @patchelf_patcher ||= begin
      Homebrew.install_bundler_gems!
      require "patchelf"
      PatchELF::Patcher.new to_s, logging: false
    end
  end

  def metadata
    @metadata ||= Metadata.new(self)
  end

  def dylib_id
    metadata.dylib_id
  end

  def dynamically_linked_libraries(*)
    metadata.dylibs
  end
end
