//===----------------- MachO.cpp - MachO format utilities -----------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "llvm/ExecutionEngine/Orc/MachO.h"

#include "llvm/ADT/ScopeExit.h"
#include "llvm/BinaryFormat/MachO.h"
#include "llvm/BinaryFormat/Magic.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/Layer.h"
#include "llvm/Object/MachOUniversal.h"
#include "llvm/Object/TapiUniversal.h"
#include "llvm/Support/FileSystem.h"

#define DEBUG_TYPE "orc"

namespace llvm {
namespace orc {

static std::string objDesc(const MemoryBufferRef &Obj, const Triple &TT,
                           bool ObjIsSlice) {
  std::string Desc;
  if (ObjIsSlice)
    Desc += (TT.getArchName() + " slice of universal binary").str();
  Desc += Obj.getBufferIdentifier();
  return Desc;
}

template <typename HeaderType>
static Error checkMachORelocatableObject(MemoryBufferRef Obj,
                                         bool SwapEndianness, const Triple &TT,
                                         bool ObjIsSlice) {
  StringRef Data = Obj.getBuffer();

  HeaderType Hdr;
  memcpy(&Hdr, Data.data(), sizeof(HeaderType));

  if (SwapEndianness)
    swapStruct(Hdr);

  if (Hdr.filetype != MachO::MH_OBJECT)
    return make_error<StringError>(objDesc(Obj, TT, ObjIsSlice) +
                                       " is not a MachO relocatable object",
                                   inconvertibleErrorCode());

  auto ObjArch = object::MachOObjectFile::getArch(Hdr.cputype, Hdr.cpusubtype);
  if (ObjArch != TT.getArch())
    return make_error<StringError>(
        objDesc(Obj, TT, ObjIsSlice) + Triple::getArchTypeName(ObjArch) +
            ", cannot be loaded into " + TT.str() + " process",
        inconvertibleErrorCode());

  return Error::success();
}

Error checkMachORelocatableObject(MemoryBufferRef Obj, const Triple &TT,
                                  bool ObjIsSlice) {
  StringRef Data = Obj.getBuffer();

  if (Data.size() < 4)
    return make_error<StringError>(
        objDesc(Obj, TT, ObjIsSlice) +
            " is not a valid MachO relocatable object file (truncated header)",
        inconvertibleErrorCode());

  uint32_t Magic;
  memcpy(&Magic, Data.data(), sizeof(uint32_t));

  switch (Magic) {
  case MachO::MH_MAGIC:
  case MachO::MH_CIGAM:
    return checkMachORelocatableObject<MachO::mach_header>(
        std::move(Obj), Magic == MachO::MH_CIGAM, TT, ObjIsSlice);
  case MachO::MH_MAGIC_64:
  case MachO::MH_CIGAM_64:
    return checkMachORelocatableObject<MachO::mach_header_64>(
        std::move(Obj), Magic == MachO::MH_CIGAM_64, TT, ObjIsSlice);
  default:
    return make_error<StringError>(
        objDesc(Obj, TT, ObjIsSlice) +
            " is not a valid MachO relocatable object (bad magic value)",
        inconvertibleErrorCode());
  }
}

Expected<std::unique_ptr<MemoryBuffer>>
checkMachORelocatableObject(std::unique_ptr<MemoryBuffer> Obj, const Triple &TT,
                            bool ObjIsSlice) {
  if (auto Err =
          checkMachORelocatableObject(Obj->getMemBufferRef(), TT, ObjIsSlice))
    return std::move(Err);
  return std::move(Obj);
}

Expected<std::pair<std::unique_ptr<MemoryBuffer>, LinkableFileKind>>
loadMachORelocatableObject(StringRef Path, const Triple &TT, LoadArchives LA,
                           std::optional<StringRef> IdentifierOverride) {
  assert((TT.getObjectFormat() == Triple::UnknownObjectFormat ||
          TT.getObjectFormat() == Triple::MachO) &&
         "TT must specify MachO or Unknown object format");

  if (!IdentifierOverride)
    IdentifierOverride = Path;

  Expected<sys::fs::file_t> FDOrErr =
      sys::fs::openNativeFileForRead(Path, sys::fs::OF_None);
  if (!FDOrErr)
    return createFileError(Path, FDOrErr.takeError());
  sys::fs::file_t FD = *FDOrErr;
  llvm::scope_exit CloseFile([&]() { sys::fs::closeFile(FD); });

  auto Buf =
      MemoryBuffer::getOpenFile(FD, *IdentifierOverride, /*FileSize=*/-1);
  if (!Buf)
    return make_error<StringError>(
        StringRef("Could not load MachO object at path ") + Path,
        Buf.getError());

  switch (identify_magic((*Buf)->getBuffer())) {
  case file_magic::macho_object: {
    auto CheckedObj = checkMachORelocatableObject(std::move(*Buf), TT, false);
    if (!CheckedObj)
      return CheckedObj.takeError();
    return std::make_pair(std::move(*CheckedObj),
                          LinkableFileKind::RelocatableObject);
  }
  case file_magic::macho_universal_binary:
    return loadLinkableSliceFromMachOUniversalBinary(FD, std::move(*Buf), TT,
                                                     LoadArchives::Never, Path,
                                                     *IdentifierOverride);
  default:
    return make_error<StringError>(
        Path + " does not contain a relocatable object file compatible with " +
            TT.str(),
        inconvertibleErrorCode());
  }
}

Expected<std::pair<std::unique_ptr<MemoryBuffer>, LinkableFileKind>>
loadLinkableSliceFromMachOUniversalBinary(sys::fs::file_t FD,
                                          std::unique_ptr<MemoryBuffer> UBBuf,
                                          const Triple &TT, LoadArchives LA,
                                          StringRef UBPath,
                                          StringRef Identifier) {

  auto UniversalBin =
      object::MachOUniversalBinary::create(UBBuf->getMemBufferRef());
  if (!UniversalBin)
    return UniversalBin.takeError();

  auto SliceRange = getMachOSliceRangeForTriple(**UniversalBin, TT);
  if (!SliceRange)
    return SliceRange.takeError();

  auto Buf = MemoryBuffer::getOpenFileSlice(FD, Identifier, SliceRange->second,
                                            SliceRange->first);
  if (!Buf)
    return make_error<StringError>(
        "Could not load " + TT.getArchName() +
            " slice of MachO universal binary at path " + UBPath,
        Buf.getError());

  switch (identify_magic((*Buf)->getBuffer())) {
  case file_magic::archive:
    if (LA != LoadArchives::Never)
      return std::make_pair(std::move(*Buf), LinkableFileKind::Archive);
    break;
  case file_magic::macho_object: {
    if (LA != LoadArchives::Required) {
      auto CheckedObj = checkMachORelocatableObject(std::move(*Buf), TT, true);
      if (!CheckedObj)
        return CheckedObj.takeError();
      return std::make_pair(std::move(*CheckedObj),
                            LinkableFileKind::RelocatableObject);
    }
    break;
  }
  default:
    break;
  }

  auto FT = [&] {
    switch (LA) {
    case LoadArchives::Never:
      return "a mach-o relocatable object file";
    case LoadArchives::Allowed:
      return "a mach-o relocatable object file or archive";
    case LoadArchives::Required:
      return "an archive";
    }
    llvm_unreachable("Unknown LoadArchives enum");
  };

  return make_error<StringError>(TT.getArchName() + " slice of " + UBPath +
                                     " does not contain " + FT(),
                                 inconvertibleErrorCode());
}

Expected<std::pair<size_t, size_t>>
getMachOSliceRangeForTriple(object::MachOUniversalBinary &UB,
                            const Triple &TT) {

  for (const auto &Obj : UB.objects()) {
    auto ObjTT = Obj.getTriple();
    if (ObjTT.getArch() == TT.getArch() &&
        ObjTT.getSubArch() == TT.getSubArch() &&
        (TT.getVendor() == Triple::UnknownVendor ||
         ObjTT.getVendor() == TT.getVendor())) {
      // We found a match. Return the range for the slice.
      return std::make_pair(Obj.getOffset(), Obj.getSize());
    }
  }

  return make_error<StringError>(Twine("Universal binary ") + UB.getFileName() +
                                     " does not contain a slice for " +
                                     TT.str(),
                                 inconvertibleErrorCode());
}

Expected<std::pair<size_t, size_t>>
getMachOSliceRangeForTriple(MemoryBufferRef UBBuf, const Triple &TT) {

  auto UB = object::MachOUniversalBinary::create(UBBuf);
  if (!UB)
    return UB.takeError();

  return getMachOSliceRangeForTriple(**UB, TT);
}

Expected<bool> ForceLoadMachOArchiveMembers::operator()(
    object::Archive &A, MemoryBufferRef MemberBuf, size_t Index) {

  auto LoadMember = [&]() {
    return StaticLibraryDefinitionGenerator::createMemberBuffer(A, MemberBuf,
                                                                Index);
  };

  if (!ObjCOnly) {
    // If we're loading all files then just load the buffer immediately. Return
    // false to indicate that there's no further loading to do here.
    if (auto Err = L.add(JD, LoadMember()))
      return Err;
    return false;
  }

  // We need to check whether this archive member contains any Objective-C
  // or Swift metadata.
  auto Obj = object::ObjectFile::createObjectFile(MemberBuf);
  if (!Obj) {
    // Invalid files are not loadable, but don't invalidate the archive.
    consumeError(Obj.takeError());
    return false;
  }

  if (auto *MachOObj = dyn_cast<object::MachOObjectFile>(&**Obj)) {
    // Load the object if any recognized special section is present.
    for (auto Sec : MachOObj->sections()) {
      auto SegName =
          MachOObj->getSectionFinalSegmentName(Sec.getRawDataRefImpl());
      if (auto SecName = Sec.getName()) {
        if (*SecName == "__objc_classlist" || *SecName == "__objc_protolist" ||
            *SecName == "__objc_clsrolist" || *SecName == "__objc_catlist" ||
            *SecName == "__objc_catlist2" || *SecName == "__objc_nlcatlist" ||
            (SegName == "__TEXT" && (*SecName).starts_with("__swift") &&
             *SecName != "__swift_modhash")) {
          if (auto Err = L.add(JD, LoadMember()))
            return Err;
          return false;
        }
      } else
        return SecName.takeError();
    }
  }

  // This is an object file but we didn't load it, so return true to indicate
  // that it's still loadable.
  return true;
}

SmallVector<std::pair<uint32_t, uint32_t>>
noFallbackArchs(uint32_t CPUType, uint32_t CPUSubType) {
  SmallVector<std::pair<uint32_t, uint32_t>> Result;
  Result.push_back({CPUType, CPUSubType});
  return Result;
}

SmallVector<std::pair<uint32_t, uint32_t>>
standardMachOFallbackArchs(uint32_t CPUType, uint32_t CPUSubType) {
  SmallVector<std::pair<uint32_t, uint32_t>> Archs;

  // Match given CPU type/subtype first.
  Archs.push_back({CPUType, CPUSubType});

  switch (CPUType) {
  case MachO::CPU_TYPE_ARM64:
    // Handle arm64 variants.
    switch (CPUSubType) {
    case MachO::CPU_SUBTYPE_ARM64_ALL:
      Archs.push_back({CPUType, MachO::CPU_SUBTYPE_ARM64E});
      break;
    default:
      break;
    }
    break;
  default:
    break;
  }

  return Archs;
}

Expected<SymbolNameSet>
getDylibInterfaceFromDylib(ExecutionSession &ES, Twine Path,
                           GetFallbackArchsFn GetFallbackArchs) {
  auto InitCPUType = MachO::getCPUType(ES.getTargetTriple());
  if (!InitCPUType)
    return InitCPUType.takeError();

  auto InitCPUSubType = MachO::getCPUSubType(ES.getTargetTriple());
  if (!InitCPUSubType)
    return InitCPUSubType.takeError();

  auto Buf = MemoryBuffer::getFile(Path);
  if (!Buf)
    return createFileError(Path, Buf.getError());

  auto BinFile = object::createBinary((*Buf)->getMemBufferRef());
  if (!BinFile)
    return BinFile.takeError();

  std::unique_ptr<object::MachOObjectFile> MachOFile;
  if (isa<object::MachOObjectFile>(**BinFile)) {
    MachOFile.reset(dyn_cast<object::MachOObjectFile>(BinFile->release()));

    // TODO: Check that dylib arch is compatible.
  } else if (auto *MachOUni =
                 dyn_cast<object::MachOUniversalBinary>(BinFile->get())) {
    SmallVector<std::pair<uint32_t, uint32_t>> ArchsToTry;
    if (GetFallbackArchs)
      ArchsToTry = GetFallbackArchs(*InitCPUType, *InitCPUSubType);
    else
      ArchsToTry.push_back({*InitCPUType, *InitCPUSubType});

    for (auto &[CPUType, CPUSubType] : ArchsToTry) {
      for (auto &O : MachOUni->objects()) {
        if (O.getCPUType() == CPUType &&
            (O.getCPUSubType() & ~MachO::CPU_SUBTYPE_MASK) == CPUSubType) {
          if (auto Obj = O.getAsObjectFile())
            MachOFile = std::move(*Obj);
          else
            return Obj.takeError();
          break;
        }
      }
      if (MachOFile) // If found, break out.
        break;
    }
    if (!MachOFile)
      return make_error<StringError>(
          "MachO universal binary at " + Path +
              " does not contain a compatible slice for " +
              ES.getTargetTriple().str(),
          inconvertibleErrorCode());
  } else
    return make_error<StringError>("File at " + Path + " is not a MachO",
                                   inconvertibleErrorCode());

  if (MachOFile->getHeader().filetype != MachO::MH_DYLIB)
    return make_error<StringError>("MachO at " + Path + " is not a dylib",
                                   inconvertibleErrorCode());

  SymbolNameSet Symbols;
  for (auto &Sym : MachOFile->symbols()) {
    if (auto Name = Sym.getName())
      Symbols.insert(ES.intern(*Name));
    else
      return Name.takeError();
  }

  return std::move(Symbols);
}

Expected<SymbolNameSet>
getDylibInterfaceFromTapiFile(ExecutionSession &ES, Twine Path,
                              GetFallbackArchsFn GetFallbackArchs) {
  SymbolNameSet Symbols;

  auto TapiFileBuffer = MemoryBuffer::getFile(Path);
  if (!TapiFileBuffer)
    return createFileError(Path, TapiFileBuffer.getError());

  auto Tapi =
      object::TapiUniversal::create((*TapiFileBuffer)->getMemBufferRef());
  if (!Tapi)
    return Tapi.takeError();

  auto InitCPUType = MachO::getCPUType(ES.getTargetTriple());
  if (!InitCPUType)
    return InitCPUType.takeError();

  auto InitCPUSubType = MachO::getCPUSubType(ES.getTargetTriple());
  if (!InitCPUSubType)
    return InitCPUSubType.takeError();

  SmallVector<std::pair<uint32_t, uint32_t>> ArchsToTry;
  if (GetFallbackArchs)
    ArchsToTry = GetFallbackArchs(*InitCPUType, *InitCPUSubType);
  else
    ArchsToTry.push_back({*InitCPUType, *InitCPUSubType});

  auto &IF = (*Tapi)->getInterfaceFile();

  auto ArchSet = IF.getArchitectures();
  for (auto [CPUType, CPUSubType] : ArchsToTry) {
    auto A = MachO::getArchitectureFromCpuType(CPUType, CPUSubType);
    if (ArchSet.has(A)) {
      if (auto Interface = IF.extract(A)) {
        for (auto *Sym : (*Interface)->exports())
          Symbols.insert(ES.intern(Sym->getName()));
        return Symbols;
      } else
        return Interface.takeError();
    }
  }

  return make_error<StringError>(
      "MachO interface file at " + Path +
          " does not contain a compatible slice for " +
          ES.getTargetTriple().str(),
      inconvertibleErrorCode());
}

Expected<SymbolNameSet> getDylibInterface(ExecutionSession &ES, Twine Path,
                                          GetFallbackArchsFn GetFallbackArchs) {
  file_magic Magic;
  if (auto EC = identify_magic(Path, Magic))
    return createFileError(Path, EC);

  switch (Magic) {
  case file_magic::macho_universal_binary:
  case file_magic::macho_dynamically_linked_shared_lib:
    return getDylibInterfaceFromDylib(ES, Path, std::move(GetFallbackArchs));
  case file_magic::tapi_file:
    return getDylibInterfaceFromTapiFile(ES, Path, std::move(GetFallbackArchs));
  default:
    return make_error<StringError>("Cannot get interface for " + Path +
                                       " unrecognized file type",
                                   inconvertibleErrorCode());
  }
}

} // End namespace orc.
} // End namespace llvm.
