# encoding: ascii-8bit
# frozen_string_literal: true

require 'heapinfo/helper'
require 'heapinfo/nil'

module HeapInfo
  # @api private
  #
  # Class for memory dump relation works
  class Dumper
    # Default dump length
    DUMP_BYTES = 8

    # Instantiate a {HeapInfo::Dumper} object
    #
    # @param [String] mem_filename The filename that can be access for dump. Should be +/proc/[pid]/mem+.
    # @param [Proc] block
    #   Use for get segment info.
    #   See {Dumper#base_len_of} for more information.
    def initialize(mem_filename, &block)
      @filename = mem_filename
      @info = block || ->(*) { HeapInfo::Nil.instance }
      need_permission unless dumpable?
    end

    # A helper for {HeapInfo::Process} to dump memory.
    # @param [Mixed] args The use input commands, see examples of {HeapInfo::Process#dump}.
    # @return [String, nil] Dump results. If error happend, +nil+ is returned.
    # @example
    #   p dump(:elf, 4)
    #   #=> "\x7fELF"
    def dump(*args)
      return need_permission unless dumpable?

      base, len = base_len_of(*args)
      file = mem_f
      file.pos = base
      mem = file.readpartial(len)
      file.close
      mem
    rescue => e # rubocop:disable Style/RescueStandardError
      raise e if e.is_a? ArgumentError

      nil
    end

    # Return the dump result as chunks.
    # see {HeapInfo::Chunks} and {HeapInfo::Chunk} for more information.
    #
    # Note: Same as {#dump}, need permission of attaching another process.
    # @return [HeapInfo::Chunks] An array of chunk(s).
    # @param [Mixed] args Same as arguments of {#dump}.
    def dump_chunks(*args)
      base = base_of(args.first)
      dump(*args).to_chunks(bits: @info[:bits], base: base)
    end

    # Show dump results like in gdb's command +x+.
    #
    # Details are in {HeapInfo::Process#x}.
    # @param [Integer] count The number of result need to dump.
    # @param [Symbol, String, Integer] address The base address to be dumped.
    # @return [void]
    # @example
    #   x 3, 0x400000
    #   # 0x400000:       0x00010102464c457f      0x0000000000000000
    #   # 0x400010:       0x00000001003e0002
    def x(count, address)
      base = base_of(address)
      commands = [address, count * size_t]
      res = dump(*commands).unpack(size_t == 4 ? 'L*' : 'Q*')
      str = res.group_by.with_index { |_, i| i / (16 / size_t) }.map do |round, values|
        Helper.hex(base + round * 16) + ":\t" +
          values.map { |v| Helper.color(format("0x%0#{size_t * 2}x", v)) }.join("\t")
      end.join("\n")
      puts str
    end

    # Dump data from +address+ until reach null-byte.
    #
    # @return [String]
    def cstring(address)
      base = base_of(address)
      len = 1
      cur = +''
      loop do
        cur << (dump(base + len - 1, len) || '')
        break if cur.index("\x00")

        len <<= 1
        return cur if cur.size != len - 1 # reached undumpable memory
      end
      cur[0, cur.index("\x00")]
    end

    # Search a specific value/string/regexp in memory.
    # +#find+ only return the first matched address.
    # @param [Integer, String, Regexp] pattern
    #   The desired search pattern, can be value(+Integer+), string, or regular expression.
    # @param [Integer, String, Symbol] from
    #   Start address for searching, can be segment(+Symbol+)
    #   or segments with offset. See examples for more information.
    # @param [Integer] length The length limit for searching.
    # @param [Boolean] rel Return relative or absolute.
    # @return [Integer, nil] The first matched address, +nil+ is returned when no such pattern found.
    # @example
    #   find(/E.F/, :elf, false)
    #   #=> 4194305
    #   find(0x4141414141414141, 'heap+0x10', 0x1000, false)
    #   #=> 6291472
    #   find('/bin/sh', :libc, true)
    #   #=> 1622391 # 0x18c177
    def find(pattern, from, length, rel)
      from = base_of(from)
      length = 1 << 40 if length.is_a? Symbol
      ret = case pattern
            when Integer then find_integer(pattern, from, length)
            when String then find_string(pattern, from, length)
            when Regexp then find_regexp(pattern, from, length)
            end
      ret -= from if ret && rel
      ret
    end

    # @return [Array<Integer>]
    def scan(pattern, from, length)
      from = base_of(from)
      cur = from
      result = []
      loop do
        addr = find(pattern, cur, length, false)
        break if addr.nil?

        result << addr - from
        cur = addr + @match_length
      end
      result
    end

    # Get the base address given the address calculation annotation.
    #
    # @param [Integer, Symbol, String] arg The base address, see examples.
    # @example
    #   base_of(123) #=> 123
    #   base_of(:heap) #=> 0x603000 # assume heap base @ 0x603000
    #   base_of('heap+0x30') #=> 0x603030
    #   base_of('elf+0x3*2-1') #=> 0x400005
    def base_of(arg)
      base_len_of(arg)[0]
    end

    private

    def need_permission
      msg = 'Could not attach to process. ' \
            'Check the setting of /proc/sys/kernel/yama/ptrace_scope, ' \
            'or try again as the root user. ' \
            'For more details, see /etc/sysctl.d/10-ptrace.conf'
      puts Helper.color(msg, sev: :fatal)
    end

    # use /proc/[pid]/mem for memory dump, must sure have permission
    def dumpable?
      mem_f.close
      true
    rescue Errno::EACCES, Errno::ENOENT
      false
    end

    def mem_f
      File.open(@filename)
    end

    # Get the base address and length.
    #
    # +@info+ will be used for getting the segment base,
    # so we can support use symbol as base address.
    # @param [Integer, Symbol, String] arg The base address, see examples.
    # @param [Integer] len An integer.
    # @example
    #   base_len_of(123, 321) #=> [123, 321]
    #   base_len_of(123) #=> [123, DUMP_BYTES]
    #   base_len_of(:heap, 10) #=> [0x603000, 10] # assume heap base @ 0x603000
    #   base_len_of('heap+0x30', 10) #=> [0x603030, 10]
    #   base_len_of('elf+0x3*2-1') #=> [0x400005, DUMP_BYTES]
    def base_len_of(arg, len = DUMP_BYTES)
      segments = @info.call(:segments) || {}
      segments = segments.each_with_object({}) do |(k, seg), memo|
        memo[k] = seg.base
      end
      base = case arg
             when Integer then arg
             when Symbol then segments[arg]
             when String then Helper.evaluate(arg, store: segments)
             end
      raise ArgumentError, "Invalid base: #{arg.inspect}" unless base.is_a?(Integer) # invalid usage

      [base, len]
    end

    def find_integer(value, from, length)
      @match_length = size_t
      find_string([value].pack(size_t == 4 ? 'L*' : 'Q*'), from, length)
    end

    def find_string(string, from, length)
      @match_length = string.size
      batch_dumper(from, length) { |str| str.index(string) }
    end

    def find_regexp(pattern, from, length)
      batch_dumper(from, length) do |str|
        str.match(pattern).tap { |m| @match_length = m[0].size if m }
        str =~ pattern
      end
    end

    def batch_dumper(from, remain_size)
      page_size = 0x1000
      while remain_size.positive?
        dump_size = [remain_size, page_size].min
        str = dump(from, dump_size)
        break if str.nil? # unreadable
        break unless (idx = yield(str)).nil?
        break if str.length < dump_size # remain is unreadable

        remain_size -= str.length
        from += str.length
      end
      return if idx.nil?

      from + idx
    end

    def size_t
      @info[:bits] / 8
    end
  end
end
