// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#include "bin/macho_loader.h"

#include <memory>
#include <utility>

#include "platform/globals.h"

#if defined(DART_HOST_OS_FUCHSIA)
#include <sys/mman.h>
#endif

#include "platform/mach_o.h"
#include "platform/unwinding_records.h"

#include "bin/file.h"
#include "bin/mappable.h"
#include "bin/virtual_memory.h"

namespace dart {
namespace bin {

namespace mach_o {

class LoadCommandIterator : public ValueObject {
 public:
  LoadCommandIterator(const void* start, size_t size)
      : end_(reinterpret_cast<const void*>(reinterpret_cast<uword>(start) +
                                           size)),
        current_(start) {}

  void Advance() {
    ASSERT(!Done());
    const uint32_t size = current()->cmdsize;
    current_ =
        reinterpret_cast<const void*>(reinterpret_cast<uword>(current_) + size);
  }

  bool Done() { return current_ >= end_; }

  const dart::mach_o::load_command* current() const {
    return reinterpret_cast<const dart::mach_o::load_command*>(current_);
  }

 private:
  const void* end_;
  const void* current_;

  DISALLOW_COPY_AND_ASSIGN(LoadCommandIterator);
};

// For MachO structs of constant size that are contiguous in memory.
template <typename T>
class MachOStructIterator : public ValueObject {
 public:
  MachOStructIterator(const T* start, size_t size_in_bytes)
      : start_(start),
        end_(reinterpret_cast<const T*>(reinterpret_cast<uword>(start) +
                                        size_in_bytes)) {
    ASSERT_EQUAL(0, size_in_bytes % sizeof(T));
  }

  const T* begin() const { return start_; }
  const T* end() const { return end_; }

 private:
  const T* start_;
  const T* end_;

  DISALLOW_COPY_AND_ASSIGN(MachOStructIterator);
};

/// A loader for a subset of Mach-O which may be used to load objects produced
/// by Dart_CreateAppAOTSnapshotAsMachO.
class LoadedMachODylib {
 public:
  LoadedMachODylib(std::unique_ptr<Mappable> mappable,
                   uint64_t macho_data_offset)
      : mappable_(std::move(mappable)), macho_data_offset_(macho_data_offset) {}

  ~LoadedMachODylib();

  /// Loads the Mach-O dynamic library object into memory. Returns whether the
  /// load was successful. On failure, the error may be retrieved by 'error()'.
  bool Load();

  /// Reads Dart-specific symbols from the loaded Mach-O dynamic library.
  ///
  /// Stores the address of the corresponding symbol in each non-null output
  /// parameter.
  ///
  /// Fails if any output parameter is non-null but points to null and the
  /// corresponding symbol was not found, or if the dynamic symbol table could
  /// not be decoded.
  ///
  /// Has the side effect of initializing the relocated addresses for the text
  /// sections corresponding to non-null output parameters in the BSS segment.
  ///
  /// On failure, the error may be retrieved by 'error()'.
  bool ResolveSymbols(const uint8_t** snapshot_data,
                      const uint8_t** snapshot_text);

  const char* error() { return error_; }

 private:
  bool ReadHeader();
  bool LoadSegments();
  bool ReadDynamicSymbolTable();

  static uword PageSize() { return VirtualMemory::PageSize(); }

  // Unlike File::Map, allows non-aligned 'start' and 'length'.
  MappedMemory* MapFilePiece(uword start,
                             uword length,
                             const void** mapping_start);

  // Initialized on a successful Load().
  std::unique_ptr<Mappable> mappable_;
  const uint64_t macho_data_offset_;

  // Initialized on error.
  const char* error_ = nullptr;

  // Initialized by ReadHeader().
  dart::mach_o::mach_header header_;
  std::unique_ptr<MappedMemory> load_commands_mapping_;
  const void* load_commands_ = nullptr;

  // Initialized by LoadSegments().
  std::unique_ptr<VirtualMemory> base_;

  // Initialized by ReadDynamicSymbolTable().
  const char* string_table_ = nullptr;
  std::unique_ptr<MappedMemory> string_table_mapping_;
  const dart::mach_o::nlist* external_symbols_ = nullptr;
  uword external_symbol_count_ = 0;
  std::unique_ptr<MappedMemory> external_symbols_mapping_;

#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
  // Dynamic table for looking up unwinding exceptions info.
  // Initialized by LoadSegments as we load executable segment.
  MallocGrowableArray<void*> dynamic_runtime_function_tables_;
#endif

  DISALLOW_COPY_AND_ASSIGN(LoadedMachODylib);
};

#define CHECK(value)                                                           \
  if (!(value)) {                                                              \
    ASSERT(error_ != nullptr);                                                 \
    return false;                                                              \
  }

#define ERROR(message)                                                         \
  {                                                                            \
    error_ = (message);                                                        \
    return false;                                                              \
  }

#define CHECK_ERROR(value, message)                                            \
  if (!(value)) {                                                              \
    error_ = (message);                                                        \
    return false;                                                              \
  }

bool LoadedMachODylib::Load() {
  VirtualMemory::Init();

  if (error_ != nullptr) {
    return false;
  }

  CHECK_ERROR(Utils::IsAligned(macho_data_offset_, PageSize()),
              "File offset must be page-aligned.");

  ASSERT(mappable_ != nullptr);
  CHECK_ERROR(mappable_->SetPosition(macho_data_offset_),
              "Invalid file offset.");

  CHECK(ReadHeader());
  CHECK(LoadSegments());
  CHECK(ReadDynamicSymbolTable());

  mappable_.reset();

  return true;
}

LoadedMachODylib::~LoadedMachODylib() {
#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
  for (intptr_t i = 0; i < dynamic_runtime_function_tables_.length(); i++) {
    UnwindingRecordsPlatform::UnregisterDynamicTable(
        dynamic_runtime_function_tables_[i]);
  }
#endif

  // Unmap the image.
  base_.reset();

  // Explicitly destroy all the mappings before closing the file.
  load_commands_mapping_.reset();
  string_table_mapping_.reset();
  external_symbols_mapping_.reset();
}

bool LoadedMachODylib::ReadHeader() {
  CHECK_ERROR(mappable_->ReadFully(&header_, sizeof(dart::mach_o::mach_header)),
              "Could not read Mach-O file.");

  CHECK_ERROR(header_.magic == dart::mach_o::MH_MAGIC ||
                  header_.magic == dart::mach_o::MH_MAGIC_64,
              "Expected a host-endian Mach-O object.");

  CHECK_ERROR(header_.filetype == dart::mach_o::MH_DYLIB,
              "Can only load Mach-O dynamic libraries.");

#if defined(TARGET_ARCH_IA32)
  CHECK_ERROR(header_.cputype == dart::mach_o::CPU_TYPE_I386,
              "Architecture mismatch.");
  CHECK_ERROR(header_.cpusubtype == dart::mach_o::CPU_SUBTYPE_I386_ALL,
              "Unexpected subtype of X86 specified");
#elif defined(TARGET_ARCH_X64)
  CHECK_ERROR(header_.cputype == dart::mach_o::CPU_TYPE_X86_64,
              "Architecture mismatch.");
  CHECK_ERROR(header_.cpusubtype == dart::mach_o::CPU_SUBTYPE_X86_64_ALL,
              "Unexpected subtype of X86_64 specified");
#elif defined(TARGET_ARCH_ARM)
  CHECK_ERROR(header_.cputype == dart::mach_o::CPU_TYPE_ARM,
              "Architecture mismatch.");
  CHECK_ERROR(header_.cpusubtype == dart::mach_o::CPU_SUBTYPE_ARM_ALL,
              "Unexpected subtype of ARM specified");
#elif defined(TARGET_ARCH_ARM64)
  CHECK_ERROR(header_.cputype == dart::mach_o::CPU_TYPE_ARM64,
              "Architecture mismatch.");
  CHECK_ERROR(header_.cpusubtype == dart::mach_o::CPU_SUBTYPE_ARM64_ALL,
              "Unexpected subtype of ARM64 specified");
#else
  // Not an architecture with appropriate constants defined in <mach/machine.h>,
  // which means we set the cpu type and subtype to ANY as the snapshot header
  // check after loading also catches any architecture mismatches.
  CHECK_ERROR(header_.cputype == dart::mach_o::CPU_TYPE_ANY,
              "Architecture mismatch.");
  CHECK_ERROR(header_.cpusubtype == dart::mach_o::CPU_SUBTYPE_ANY,
              "Unexpected subtype specified");
#endif

  const uword file_start = header_.magic == dart::mach_o::MH_MAGIC_64
                               ? sizeof(dart::mach_o::mach_header_64)
                               : sizeof(dart::mach_o::mach_header);
  const uword file_length = header_.sizeofcmds;
  load_commands_mapping_.reset(
      MapFilePiece(file_start, file_length,
                   reinterpret_cast<const void**>(&load_commands_)));
  CHECK_ERROR(load_commands_mapping_ != nullptr,
              "Could not mmap the load commands.");
  return true;
}

bool LoadedMachODylib::LoadSegments() {
  // Calculate the total amount of virtual memory needed.
  uint64_t total_memory = 0;
  {
    LoadCommandIterator it(load_commands_, header_.sizeofcmds);
    while (!it.Done()) {
      auto* const current = it.current();
      if (current->cmd == dart::mach_o::LC_SEGMENT) {
        auto* const segment =
            reinterpret_cast<const dart::mach_o::segment_command*>(current);
        total_memory = Utils::Maximum<uint64_t>(
            segment->vmaddr + segment->vmsize, total_memory);
      } else if (current->cmd == dart::mach_o::LC_SEGMENT_64) {
        auto* const segment_64 =
            reinterpret_cast<const dart::mach_o::segment_command_64*>(current);
        total_memory = Utils::Maximum<uint64_t>(
            segment_64->vmaddr + segment_64->vmsize, total_memory);
      }
      it.Advance();
    }
  }
  total_memory = Utils::RoundUp(total_memory, PageSize());

  base_.reset(VirtualMemory::Allocate(total_memory,
                                      /*is_executable=*/false,
                                      "dart-compiled-image"));
  CHECK_ERROR(base_ != nullptr, "Could not reserve virtual memory.");

  {
    LoadCommandIterator it(load_commands_, header_.sizeofcmds);
    while (!it.Done()) {
      auto* const current = it.current();
      uint64_t memory_offset, memory_size, file_offset, file_size;
      dart::mach_o::vm_prot_t initprot;
#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
      uint64_t records_address = 0, records_size = 0;
#endif
      if (current->cmd == dart::mach_o::LC_SEGMENT) {
        auto* const segment =
            reinterpret_cast<const dart::mach_o::segment_command*>(current);
        memory_offset = segment->vmaddr;
        memory_size = segment->vmsize;
        file_offset = segment->fileoff;
        file_size = segment->filesize;
        initprot = segment->initprot;
#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
        if ((initprot & dart::mach_o::VM_PROT_EXECUTE) != 0) {
          auto* const start =
              reinterpret_cast<const dart::mach_o::section*>(segment + 1);
          const size_t size_in_bytes = segment->cmdsize - sizeof(*segment);
          MachOStructIterator sections(start, size_in_bytes);
          for (const auto& section : sections) {
            if (strcmp(section.sectname, dart::mach_o::SECT_UNWIND_INFO) == 0) {
              records_address = section.addr;
              records_size = section.size;
              break;
            }
          }
        }
#endif
      } else if (current->cmd == dart::mach_o::LC_SEGMENT_64) {
        auto* const segment_64 =
            reinterpret_cast<const dart::mach_o::segment_command_64*>(current);
        memory_offset = segment_64->vmaddr;
        memory_size = segment_64->vmsize;
        file_offset = segment_64->fileoff;
        file_size = segment_64->filesize;
        initprot = segment_64->initprot;
#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
        if ((initprot & dart::mach_o::VM_PROT_EXECUTE) != 0) {
          auto* const start =
              reinterpret_cast<const dart::mach_o::section_64*>(segment_64 + 1);
          const size_t size_in_bytes =
              segment_64->cmdsize - sizeof(*segment_64);
          MachOStructIterator sections(start, size_in_bytes);
          for (const auto& section : sections) {
            if (strcmp(section.sectname, dart::mach_o::SECT_UNWIND_INFO) == 0) {
              records_address = section.addr;
              records_size = section.size;
              break;
            }
          }
        }
#endif
      } else {
        it.Advance();
        continue;
      }

      const uint64_t adjustment = memory_offset % PageSize();
      CHECK_ERROR(
          adjustment == (file_offset % PageSize()),
          "Difference between file and memory offset must be page-aligned.");

      void* const memory_start =
          static_cast<char*>(base_->address()) + memory_offset - adjustment;
      const uword file_start = macho_data_offset_ + file_offset - adjustment;
      const uword length = memory_size + adjustment;

      File::MapType map_type = File::kReadOnly;
      if (initprot ==
          (dart::mach_o::VM_PROT_READ | dart::mach_o::VM_PROT_WRITE)) {
        map_type = File::kReadWrite;
      } else if (initprot ==
                 (dart::mach_o::VM_PROT_READ | dart::mach_o::VM_PROT_EXECUTE)) {
        map_type = File::kReadExecute;
      } else if (initprot == dart::mach_o::VM_PROT_READ) {
        map_type = File::kReadOnly;
      } else {
        Syslog::PrintErr("VM protection flags were: 0x%x\n", initprot);
        ERROR("Unsupported VM protection flags set.");
      }

#if defined(DART_HOST_OS_FUCHSIA)
      // mmap is less flexible on Fuchsia than on Linux and Darwin, in
      // (at least) two important ways:
      //
      // 1. We cannot map a file opened as RX into an RW mapping, even if the
      //    mode is MAP_PRIVATE (which implies copy-on-write).
      // 2. We cannot atomically replace an existing anonymous mapping with a
      //    file mapping: we must first unmap the existing mapping.

      if (map_type == File::kReadWrite) {
        CHECK_ERROR(mappable_->SetPosition(file_start),
                    "Could not advance file position.");
        CHECK_ERROR(mappable_->ReadFully(memory_start, length),
                    "Could not read file.");
        it.Advance();
        continue;
      }

      CHECK_ERROR(munmap(memory_start, length) == 0,
                  "Could not unmap reservation.");
#endif

      std::unique_ptr<MappedMemory> memory(
          mappable_->Map(map_type, file_start, length, memory_start));
      CHECK_ERROR(memory != nullptr, "Could not map segment.");
      CHECK_ERROR(memory->address() == memory_start,
                  "Mapping not at requested address.");
#if defined(UNWINDING_RECORDS_WINDOWS_HOST)
      // For executable pages register unwinding information that should be
      // present on the page.
      if (map_type == File::kReadExecute) {
        CHECK_ERROR(records_address != 0,
                    "No __unwind_info section found in segment");
        CHECK_ERROR(records_size == UnwindingRecordsPlatform::SizeInBytes(),
                    "__unwind_info section does not contain expected "
                    "unwinding records");
        void* ptable = nullptr;
        void* start = memory->address();
        void* records_start = reinterpret_cast<void*>(
            reinterpret_cast<uword>(memory->address()) + adjustment +
            (records_address - memory_offset));
        UnwindingRecordsPlatform::RegisterExecutableMemory(
            start, length, records_start, &ptable);
        dynamic_runtime_function_tables_.Add(ptable);
      }
#else
      USE(file_size);
#endif
      it.Advance();
    }
  }

  return true;
}

bool LoadedMachODylib::ReadDynamicSymbolTable() {
  const dart::mach_o::symtab_command* symtab = nullptr;
  const dart::mach_o::dysymtab_command* dysymtab = nullptr;
  LoadCommandIterator it(load_commands_, header_.sizeofcmds);
  while (!it.Done()) {
    auto* const c = it.current();
    if (c->cmd == dart::mach_o::LC_SYMTAB) {
      symtab = reinterpret_cast<const dart::mach_o::symtab_command*>(c);
    } else if (c->cmd == dart::mach_o::LC_DYSYMTAB) {
      dysymtab = reinterpret_cast<const dart::mach_o::dysymtab_command*>(c);
    }
    it.Advance();
  }
  CHECK_ERROR(symtab != nullptr, "Could not locate symbol table.");
  CHECK_ERROR(dysymtab != nullptr, "Could not locate dynamic symbol table.");
  CHECK_ERROR(dysymtab->iextdefsym + dysymtab->nextdefsym <= symtab->nsyms,
              "Dynamic symbol table offsets are out of range.");

  {
    const uword file_start = symtab->stroff;
    const uword file_length = symtab->strsize;
    string_table_mapping_.reset(
        MapFilePiece(file_start, file_length,
                     reinterpret_cast<const void**>(&string_table_)));
    CHECK_ERROR(string_table_mapping_ != nullptr,
                "Could not mmap the string table.");
  }
  external_symbol_count_ = dysymtab->nextdefsym;
  {
    // Note that the offset iextdefsym is the offset into the symbol table in
    // terms of symbols, not in terms of raw bytes.
    const intptr_t symbol_size = sizeof(dart::mach_o::nlist);
    const uword file_start =
        symtab->symoff + dysymtab->iextdefsym * symbol_size;
    const uword file_length = dysymtab->nextdefsym * symbol_size;
    external_symbols_mapping_.reset(
        MapFilePiece(file_start, file_length,
                     reinterpret_cast<const void**>(&external_symbols_)));
    CHECK_ERROR(external_symbols_mapping_ != nullptr,
                "Could not mmap the external symbols.");
  }
  return true;
}

bool LoadedMachODylib::ResolveSymbols(const uint8_t** data,
                                      const uint8_t** text) {
  if (error_ != nullptr) {
    return false;
  }

  for (uword i = 0; i < external_symbol_count_; ++i) {
    const auto& sym = external_symbols_[i];
    const char* name = string_table_ + sym.n_idx;
    const uint8_t** output = nullptr;

    if (strcmp(name, kSnapshotDataAsmSymbol) == 0) {
      output = data;
    } else if (strcmp(name, kSnapshotTextAsmSymbol) == 0) {
      output = text;
    }

    if (output != nullptr) {
      *output = reinterpret_cast<const uint8_t*>(base_->start() + sym.n_value);
    }
  }

  CHECK_ERROR(data == nullptr || *data != nullptr,
              "Could not find snapshot data.");
  CHECK_ERROR(text == nullptr || *text != nullptr,
              "Could not find snapshot text.");
  return true;
}

MappedMemory* LoadedMachODylib::MapFilePiece(uword file_start,
                                             uword file_length,
                                             const void** mem_start) {
  const uword adjustment = (macho_data_offset_ + file_start) % PageSize();
  const uword mapping_offset = macho_data_offset_ + file_start - adjustment;
  const uword mapping_length =
      Utils::RoundUp(macho_data_offset_ + file_start + file_length,
                     PageSize()) -
      mapping_offset;
  MappedMemory* const mapping =
      mappable_->Map(bin::File::kReadOnly, mapping_offset, mapping_length);

  if (mapping != nullptr) {
    *mem_start = reinterpret_cast<uint8_t*>(mapping->start() +
                                            (file_start % PageSize()));
  }

  return mapping;
}

}  // namespace mach_o
}  // namespace bin
}  // namespace dart

using namespace dart::bin::mach_o;  // NOLINT
using Mappable = dart::bin::Mappable;

#if defined(DART_HOST_OS_FUCHSIA) || defined(DART_HOST_OS_LINUX)
DART_EXPORT Dart_LoadedMachODylib* Dart_LoadMachODylib_Fd(
    int fd,
    uint64_t file_offset,
    const char** error,
    const uint8_t** snapshot_data,
    const uint8_t** snapshot_text) {
  std::unique_ptr<Mappable> mappable(Mappable::FromFD(fd));
  std::unique_ptr<LoadedMachODylib> macho(
      new LoadedMachODylib(std::move(mappable), file_offset));

  if (!macho->Load() || !macho->ResolveSymbols(snapshot_data, snapshot_text)) {
    *error = macho->error();
    return nullptr;
  }

  return reinterpret_cast<Dart_LoadedMachODylib*>(macho.release());
}
#endif

DART_EXPORT Dart_LoadedMachODylib* Dart_LoadMachODylib(
    const char* filename,
    uint64_t file_offset,
    const char** error,
    const uint8_t** snapshot_data,
    const uint8_t** snapshot_text) {
  std::unique_ptr<Mappable> mappable(Mappable::FromPath(filename));
  if (mappable == nullptr) {
    *error = "Couldn't open file.";
    return nullptr;
  }
  std::unique_ptr<LoadedMachODylib> macho(
      new LoadedMachODylib(std::move(mappable), file_offset));

  if (!macho->Load() || !macho->ResolveSymbols(snapshot_data, snapshot_text)) {
    *error = macho->error();
    return nullptr;
  }

  return reinterpret_cast<Dart_LoadedMachODylib*>(macho.release());
}

DART_EXPORT Dart_LoadedMachODylib* Dart_LoadMachODylib_Memory(
    const uint8_t* snapshot,
    uint64_t snapshot_size,
    const char** error,
    const uint8_t** snapshot_data,
    const uint8_t** snapshot_text) {
  std::unique_ptr<Mappable> mappable(
      Mappable::FromMemory(snapshot, snapshot_size));
  if (mappable == nullptr) {
    *error = "Couldn't open file.";
    return nullptr;
  }
  std::unique_ptr<LoadedMachODylib> macho(
      new LoadedMachODylib(std::move(mappable), /*macho_data_offset=*/0));

  if (!macho->Load() || !macho->ResolveSymbols(snapshot_data, snapshot_text)) {
    *error = macho->error();
    return nullptr;
  }

  return reinterpret_cast<Dart_LoadedMachODylib*>(macho.release());
}

DART_EXPORT void Dart_UnloadMachODylib(Dart_LoadedMachODylib* loaded) {
  delete reinterpret_cast<LoadedMachODylib*>(loaded);
}
