// Copyright 2020 The Crashpad Authors
//
// 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 "snapshot/linux/test_modules.h"

#include <elf.h>

#include <limits>

#include "base/check_op.h"
#include "base/files/file_path.h"
#include "build/build_config.h"
#include "gtest/gtest.h"
#include "test/test_paths.h"
#include "util/file/filesystem.h"
#include "util/file/file_writer.h"

namespace crashpad {
namespace test {

bool WriteTestModule(const base::FilePath& module_path,
                     const std::string& soname) {
#if defined(ARCH_CPU_64_BITS)
  using Ehdr = Elf64_Ehdr;
  using Phdr = Elf64_Phdr;
  using Shdr = Elf64_Shdr;
  using Dyn = Elf64_Dyn;
  using Sym = Elf64_Sym;
  unsigned char elf_class = ELFCLASS64;
#else
  using Ehdr = Elf32_Ehdr;
  using Phdr = Elf32_Phdr;
  using Shdr = Elf32_Shdr;
  using Dyn = Elf32_Dyn;
  using Sym = Elf32_Sym;
  unsigned char elf_class = ELFCLASS32;
#endif

  struct {
    Ehdr ehdr;
    struct {
      Phdr load1;
      Phdr load2;
      Phdr dynamic;
    } phdr_table;
    struct {
      Dyn hash;
      Dyn strtab;
      Dyn symtab;
      Dyn strsz;
      Dyn syment;
      Dyn soname;
      Dyn null;
    } dynamic_array;
    struct {
      Elf32_Word nbucket;
      Elf32_Word nchain;
      Elf32_Word bucket;
      Elf32_Word chain;
    } hash_table;
    char string_table[32];
    struct {
    } section_header_string_table;
    struct {
      Sym und_symbol;
    } symbol_table;
    struct {
      Shdr null;
      Shdr dynamic;
      Shdr string_table;
      Shdr section_header_string_table;
    } shdr_table;
  } module = {};

  module.ehdr.e_ident[EI_MAG0] = ELFMAG0;
  module.ehdr.e_ident[EI_MAG1] = ELFMAG1;
  module.ehdr.e_ident[EI_MAG2] = ELFMAG2;
  module.ehdr.e_ident[EI_MAG3] = ELFMAG3;

  module.ehdr.e_ident[EI_CLASS] = elf_class;

#if defined(ARCH_CPU_LITTLE_ENDIAN)
  module.ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
#else
  module.ehdr.e_ident[EI_DATA] = ELFDATA2MSB;
#endif  // ARCH_CPU_LITTLE_ENDIAN

  module.ehdr.e_ident[EI_VERSION] = EV_CURRENT;

  module.ehdr.e_type = ET_DYN;

#if defined(ARCH_CPU_X86)
  module.ehdr.e_machine = EM_386;
#elif defined(ARCH_CPU_X86_64)
  module.ehdr.e_machine = EM_X86_64;
#elif defined(ARCH_CPU_ARMEL)
  module.ehdr.e_machine = EM_ARM;
#elif defined(ARCH_CPU_ARM64)
  module.ehdr.e_machine = EM_AARCH64;
#elif defined(ARCH_CPU_MIPSEL) || defined(ARCH_CPU_MIPS64EL)
  module.ehdr.e_machine = EM_MIPS;
#endif

  module.ehdr.e_version = EV_CURRENT;
  module.ehdr.e_ehsize = sizeof(module.ehdr);

  module.ehdr.e_phoff = offsetof(decltype(module), phdr_table);
  module.ehdr.e_phnum = sizeof(module.phdr_table) / sizeof(Phdr);
  module.ehdr.e_phentsize = sizeof(Phdr);

  module.ehdr.e_shoff = offsetof(decltype(module), shdr_table);
  module.ehdr.e_shentsize = sizeof(Shdr);
  module.ehdr.e_shnum = sizeof(module.shdr_table) / sizeof(Shdr);
  module.ehdr.e_shstrndx =
      offsetof(decltype(module.shdr_table), section_header_string_table) /
      sizeof(Shdr);

  const size_t page_size = getpagesize();
  auto align = [page_size](uintptr_t addr) {
    return (addr + page_size - 1) & ~(page_size - 1);
  };
  constexpr size_t segment_size = offsetof(decltype(module), shdr_table);

  // This test module covers cases where:
  // 1. Multiple segments are mapped from file offset 0.
  // 2. Load bias is negative.

  const uintptr_t load2_vaddr = align(std::numeric_limits<uintptr_t>::max() -
                                      align(segment_size) - page_size);
  const uintptr_t load1_vaddr = load2_vaddr - align(segment_size);

  module.phdr_table.load1.p_type = PT_LOAD;
  module.phdr_table.load1.p_offset = 0;
  module.phdr_table.load1.p_vaddr = load1_vaddr;
  module.phdr_table.load1.p_filesz = segment_size;
  module.phdr_table.load1.p_memsz = segment_size;
  module.phdr_table.load1.p_flags = PF_R;
  module.phdr_table.load1.p_align = page_size;

  module.phdr_table.load2.p_type = PT_LOAD;
  module.phdr_table.load2.p_offset = 0;
  module.phdr_table.load2.p_vaddr = load2_vaddr;
  module.phdr_table.load2.p_filesz = segment_size;
  module.phdr_table.load2.p_memsz = segment_size;
  module.phdr_table.load2.p_flags = PF_R | PF_W;
  module.phdr_table.load2.p_align = page_size;

  module.phdr_table.dynamic.p_type = PT_DYNAMIC;
  module.phdr_table.dynamic.p_offset =
      offsetof(decltype(module), dynamic_array);
  module.phdr_table.dynamic.p_vaddr =
      load2_vaddr + module.phdr_table.dynamic.p_offset;
  module.phdr_table.dynamic.p_filesz = sizeof(module.dynamic_array);
  module.phdr_table.dynamic.p_memsz = sizeof(module.dynamic_array);
  module.phdr_table.dynamic.p_flags = PF_R | PF_W;
  module.phdr_table.dynamic.p_align = 8;

  module.dynamic_array.hash.d_tag = DT_HASH;
  module.dynamic_array.hash.d_un.d_ptr =
      load1_vaddr + offsetof(decltype(module), hash_table);
  module.dynamic_array.strtab.d_tag = DT_STRTAB;
  module.dynamic_array.strtab.d_un.d_ptr =
      load1_vaddr + offsetof(decltype(module), string_table);
  module.dynamic_array.symtab.d_tag = DT_SYMTAB;
  module.dynamic_array.symtab.d_un.d_ptr =
      load1_vaddr + offsetof(decltype(module), symbol_table);
  module.dynamic_array.strsz.d_tag = DT_STRSZ;
  module.dynamic_array.strsz.d_un.d_val = sizeof(module.string_table);
  module.dynamic_array.syment.d_tag = DT_SYMENT;
  module.dynamic_array.syment.d_un.d_val = sizeof(Sym);
  constexpr size_t kSonameOffset = 1;
  module.dynamic_array.soname.d_tag = DT_SONAME;
  module.dynamic_array.soname.d_un.d_val = kSonameOffset;

  module.dynamic_array.null.d_tag = DT_NULL;

  module.hash_table.nbucket = 1;
  module.hash_table.nchain = 1;
  module.hash_table.bucket = 0;
  module.hash_table.chain = 0;

  if (sizeof(module.string_table) < soname.size() + 2) {
    ADD_FAILURE() << "string table too small";
    return false;
  }
  module.string_table[0] = '\0';
  memcpy(&module.string_table[kSonameOffset], soname.c_str(), soname.size());

  module.shdr_table.null.sh_type = SHT_NULL;

  module.shdr_table.dynamic.sh_name = 0;
  module.shdr_table.dynamic.sh_type = SHT_DYNAMIC;
  module.shdr_table.dynamic.sh_flags = SHF_WRITE | SHF_ALLOC;
  module.shdr_table.dynamic.sh_addr = module.phdr_table.dynamic.p_vaddr;
  module.shdr_table.dynamic.sh_offset = module.phdr_table.dynamic.p_offset;
  module.shdr_table.dynamic.sh_size = module.phdr_table.dynamic.p_filesz;
  module.shdr_table.dynamic.sh_link =
      offsetof(decltype(module.shdr_table), string_table) / sizeof(Shdr);

  module.shdr_table.string_table.sh_name = 0;
  module.shdr_table.string_table.sh_type = SHT_STRTAB;
  module.shdr_table.string_table.sh_offset =
      offsetof(decltype(module), string_table);
  module.shdr_table.string_table.sh_size = sizeof(module.string_table);

  module.shdr_table.section_header_string_table.sh_name = 0;
  module.shdr_table.section_header_string_table.sh_type = SHT_STRTAB;
  module.shdr_table.section_header_string_table.sh_offset =
      offsetof(decltype(module), section_header_string_table);
  module.shdr_table.section_header_string_table.sh_size =
      sizeof(module.section_header_string_table);

  FileWriter writer;
  if (!writer.Open(module_path,
                   FileWriteMode::kCreateOrFail,
                   FilePermissions::kWorldReadable)) {
    ADD_FAILURE();
    return false;
  }

  if (!writer.Write(&module, sizeof(module))) {
    ADD_FAILURE();
    LoggingRemoveFile(module_path);
    return false;
  }

  return true;
}

ScopedModuleHandle LoadTestModule(const std::string& module_name,
                                  const std::string& module_soname) {
  base::FilePath module_path(
      TestPaths::Executable().DirName().Append(module_name));

  if (!WriteTestModule(module_path, module_soname)) {
    return ScopedModuleHandle(nullptr);
  }
  EXPECT_TRUE(IsRegularFile(module_path));

  ScopedModuleHandle handle(
      dlopen(module_path.value().c_str(), RTLD_LAZY | RTLD_LOCAL));
  EXPECT_TRUE(handle.valid())
      << "dlopen: " << module_path.value() << " " << dlerror();

  EXPECT_TRUE(LoggingRemoveFile(module_path));

  return handle;
}

}  // namespace test
}  // namespace crashpad
