/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "src/trace_processor/util/elf/binary_info.h"

#include <fcntl.h>

#include <cstdint>
#include <limits>
#include <optional>
#include <string>

#include "perfetto/base/logging.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_mmap.h"
#include "src/trace_processor/util/elf/elf.h"

namespace perfetto::trace_processor::elf {
namespace {

bool InRange(const void* base,
             size_t total_size,
             const void* ptr,
             size_t size) {
  return ptr >= base && static_cast<const char*>(ptr) + size <=
                            static_cast<const char*>(base) + total_size;
}
// TODO(rasikanavarange): ETM registers files with large sizes causing size in
// TraceBlobView to become truncated. This means we can not trust any of the
// below checks for large files. So a solution is needed that is not too
// expensive memory wise. b/438916722
template <typename E>
std::optional<uint64_t> GetElfLoadBias(const void* mem, size_t size) {
  const typename E::Ehdr* ehdr = static_cast<const typename E::Ehdr*>(mem);
  if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
    PERFETTO_ELOG("Corrupted ELF.");
    return std::nullopt;
  }
  for (size_t i = 0; i < ehdr->e_phnum; ++i) {
    const typename E::Phdr* phdr = GetPhdr<E>(mem, ehdr, i);
    if (!InRange(mem, size, phdr, sizeof(typename E::Phdr))) {
      PERFETTO_ELOG("Corrupted ELF.");
      return std::nullopt;
    }
    if (phdr->p_type == PT_LOAD && phdr->p_flags & PF_X) {
      return phdr->p_vaddr - phdr->p_offset;
    }
  }
  return 0u;
}

template <typename E>
std::optional<std::string> GetElfBuildId(const void* mem, size_t size) {
  const typename E::Ehdr* ehdr = static_cast<const typename E::Ehdr*>(mem);
  if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
    PERFETTO_ELOG("Corrupted ELF.");
    return std::nullopt;
  }
  for (size_t i = 0; i < ehdr->e_shnum; ++i) {
    const typename E::Shdr* shdr = GetShdr<E>(mem, ehdr, i);
    if (!InRange(mem, size, shdr, sizeof(typename E::Shdr))) {
      PERFETTO_ELOG("Corrupted ELF.");
      return std::nullopt;
    }

    if (shdr->sh_type != SHT_NOTE)
      continue;

    auto offset = shdr->sh_offset;
    while (offset < shdr->sh_offset + shdr->sh_size) {
      const typename E::Nhdr* nhdr = reinterpret_cast<const typename E::Nhdr*>(
          static_cast<const char*>(mem) + offset);

      if (!InRange(mem, size, nhdr, sizeof(typename E::Nhdr))) {
        PERFETTO_ELOG("Corrupted ELF.");
        return std::nullopt;
      }
      if (nhdr->n_type == NT_GNU_BUILD_ID && nhdr->n_namesz == 4) {
        const char* name = reinterpret_cast<const char*>(nhdr) + sizeof(*nhdr);
        if (!InRange(mem, size, name, 4)) {
          PERFETTO_ELOG("Corrupted ELF.");
          return std::nullopt;
        }
        if (memcmp(name, "GNU", 3) == 0) {
          const char* value = reinterpret_cast<const char*>(nhdr) +
                              sizeof(*nhdr) + base::AlignUp<4>(nhdr->n_namesz);

          if (!InRange(mem, size, value, nhdr->n_descsz)) {
            PERFETTO_ELOG("Corrupted ELF.");
            return std::nullopt;
          }
          return std::string(value, nhdr->n_descsz);
        }
      }
      offset += sizeof(*nhdr) + base::AlignUp<4>(nhdr->n_namesz) +
                base::AlignUp<4>(nhdr->n_descsz);
    }
  }
  return std::nullopt;
}

constexpr uint32_t kMachO64Magic = 0xfeedfacf;

bool IsMachO64(const uint8_t* mem, size_t size) {
  if (size < sizeof(kMachO64Magic))
    return false;
  return memcmp(mem, &kMachO64Magic, sizeof(kMachO64Magic)) == 0;
}

struct mach_header_64 {
  uint32_t magic;      /* mach magic number identifier */
  int32_t cputype;     /* cpu specifier */
  int32_t cpusubtype;  /* machine specifier */
  uint32_t filetype;   /* type of file */
  uint32_t ncmds;      /* number of load commands */
  uint32_t sizeofcmds; /* the size of all the load commands */
  uint32_t flags;      /* flags */
  uint32_t reserved;   /* reserved */
};

struct load_command {
  uint32_t cmd;     /* type of load command */
  uint32_t cmdsize; /* total size of command in bytes */
};

struct segment_64_command {
  uint32_t cmd;      /* LC_SEGMENT_64 */
  uint32_t cmdsize;  /* includes sizeof section_64 structs */
  char segname[16];  /* segment name */
  uint64_t vmaddr;   /* memory address of this segment */
  uint64_t vmsize;   /* memory size of this segment */
  uint64_t fileoff;  /* file offset of this segment */
  uint64_t filesize; /* amount to map from the file */
  uint32_t maxprot;  /* maximum VM protection */
  uint32_t initprot; /* initial VM protection */
  uint32_t nsects;   /* number of sections in segment */
  uint32_t flags;    /* flags */
};

std::optional<BinaryInfo> GetMachOBinaryInfo(const uint8_t* mem, size_t size) {
  if (size < sizeof(mach_header_64))
    return {};

  mach_header_64 header;
  memcpy(&header, mem, sizeof(mach_header_64));

  if (size < sizeof(mach_header_64) + header.sizeofcmds)
    return {};

  std::optional<std::string> build_id;
  uint64_t load_bias = 0;

  const uint8_t* pcmd = mem + sizeof(mach_header_64);
  const uint8_t* pcmds_end = pcmd + header.sizeofcmds;
  while (pcmd < pcmds_end) {
    load_command cmd_header;
    memcpy(&cmd_header, pcmd, sizeof(load_command));

    constexpr uint32_t LC_SEGMENT_64 = 0x19;
    constexpr uint32_t LC_UUID = 0x1b;

    switch (cmd_header.cmd) {
      case LC_UUID: {
        build_id = std::string(
            reinterpret_cast<const char*>(pcmd) + sizeof(load_command),
            cmd_header.cmdsize - sizeof(load_command));
        break;
      }
      case LC_SEGMENT_64: {
        segment_64_command seg_cmd;
        memcpy(&seg_cmd, pcmd, sizeof(segment_64_command));
        if (strcmp(seg_cmd.segname, "__TEXT") == 0) {
          load_bias = seg_cmd.vmaddr;
        }
        break;
      }
      default:
        break;
    }

    pcmd += cmd_header.cmdsize;
  }

  if (build_id) {
    constexpr uint32_t MH_DSYM = 0xa;
    BinaryType type = header.filetype == MH_DSYM ? BinaryType::kMachODsym
                                                 : BinaryType::kMachO;
    return BinaryInfo{*build_id, load_bias, type};
  }
  return {};
}

}  // namespace

bool IsElf(const uint8_t* mem, size_t size) {
  if (size <= EI_MAG3)
    return false;
  return (mem[EI_MAG0] == ELFMAG0 && mem[EI_MAG1] == ELFMAG1 &&
          mem[EI_MAG2] == ELFMAG2 && mem[EI_MAG3] == ELFMAG3);
}

std::optional<BinaryInfo> GetBinaryInfo(const uint8_t* mem, size_t size) {
  static_assert(EI_MAG3 + 1 == sizeof(kMachO64Magic));
  static_assert(EI_CLASS > EI_MAG3, "mem[EI_MAG?] accesses are in range.");
  if (size <= EI_CLASS) {
    return std::nullopt;
  }
  std::optional<std::string> build_id;
  std::optional<uint64_t> load_bias;
  if (IsElf(mem, size)) {
    switch (mem[EI_CLASS]) {
      case ELFCLASS32:
        build_id = GetElfBuildId<Elf32>(mem, size);
        load_bias = GetElfLoadBias<Elf32>(mem, size);
        break;
      case ELFCLASS64:
        build_id = GetElfBuildId<Elf64>(mem, size);
        load_bias = GetElfLoadBias<Elf64>(mem, size);
        break;
      default:
        return std::nullopt;
    }
    if (load_bias) {
      return BinaryInfo{build_id, *load_bias, BinaryType::kElf};
    }
  } else if (IsMachO64(mem, size)) {
    return GetMachOBinaryInfo(mem, size);
  }
  return std::nullopt;
}

}  // namespace perfetto::trace_processor::elf
