//===-- SymbolLocatorDebugSymbols.cpp -------------------------------------===//
//
// 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 "SymbolLocatorDebugSymbols.h"

#include "Plugins/ObjectFile/wasm/ObjectFileWasm.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/ModuleList.h"
#include "lldb/Core/ModuleSpec.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/Progress.h"
#include "lldb/Core/Section.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/HostInfo.h"
#include "lldb/Symbol/ObjectFile.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/DataBuffer.h"
#include "lldb/Utility/DataExtractor.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/Timer.h"
#include "lldb/Utility/UUID.h"

#include "llvm/ADT/SmallSet.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/ThreadPool.h"

#include "Host/macosx/cfcpp/CFCBundle.h"
#include "Host/macosx/cfcpp/CFCData.h"
#include "Host/macosx/cfcpp/CFCReleaser.h"
#include "Host/macosx/cfcpp/CFCString.h"

#include "mach/machine.h"

#include <CoreFoundation/CoreFoundation.h>

#include <cstring>
#include <dirent.h>
#include <dlfcn.h>
#include <optional>
#include <pwd.h>

using namespace lldb;
using namespace lldb_private;

static CFURLRef (*g_dlsym_DBGCopyFullDSYMURLForUUID)(
    CFUUIDRef uuid, CFURLRef exec_url) = nullptr;
static CFDictionaryRef (*g_dlsym_DBGCopyDSYMPropertyLists)(CFURLRef dsym_url) =
    nullptr;

LLDB_PLUGIN_DEFINE(SymbolLocatorDebugSymbols)

SymbolLocatorDebugSymbols::SymbolLocatorDebugSymbols() : SymbolLocator() {}

void SymbolLocatorDebugSymbols::Initialize() {
  PluginManager::RegisterPlugin(
      GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
      LocateExecutableObjectFile, LocateExecutableSymbolFile,
      DownloadObjectAndSymbolFile, FindSymbolFileInBundle);
}

void SymbolLocatorDebugSymbols::Terminate() {
  PluginManager::UnregisterPlugin(CreateInstance);
}

llvm::StringRef SymbolLocatorDebugSymbols::GetPluginDescriptionStatic() {
  return "DebugSymbols symbol locator.";
}

SymbolLocator *SymbolLocatorDebugSymbols::CreateInstance() {
  return new SymbolLocatorDebugSymbols();
}

std::optional<ModuleSpec> SymbolLocatorDebugSymbols::LocateExecutableObjectFile(
    const ModuleSpec &module_spec) {
  Log *log = GetLog(LLDBLog::Host);
  if (!ModuleList::GetGlobalModuleListProperties().GetEnableExternalLookup()) {
    LLDB_LOGF(log, "Spotlight lookup for .dSYM bundles is disabled.");
    return {};
  }
  ModuleSpec return_module_spec;
  return_module_spec = module_spec;
  return_module_spec.GetFileSpec().Clear();
  return_module_spec.GetSymbolFileSpec().Clear();

  const UUID *uuid = module_spec.GetUUIDPtr();
  const ArchSpec *arch = module_spec.GetArchitecturePtr();

  int items_found = 0;

  if (g_dlsym_DBGCopyFullDSYMURLForUUID == nullptr ||
      g_dlsym_DBGCopyDSYMPropertyLists == nullptr) {
    void *handle = dlopen(
        "/System/Library/PrivateFrameworks/DebugSymbols.framework/DebugSymbols",
        RTLD_LAZY | RTLD_LOCAL);
    if (handle) {
      g_dlsym_DBGCopyFullDSYMURLForUUID =
          (CFURLRef(*)(CFUUIDRef, CFURLRef))dlsym(handle,
                                                  "DBGCopyFullDSYMURLForUUID");
      g_dlsym_DBGCopyDSYMPropertyLists = (CFDictionaryRef(*)(CFURLRef))dlsym(
          handle, "DBGCopyDSYMPropertyLists");
    }
  }

  if (g_dlsym_DBGCopyFullDSYMURLForUUID == nullptr ||
      g_dlsym_DBGCopyDSYMPropertyLists == nullptr) {
    return {};
  }

  if (uuid && uuid->IsValid()) {
    // Try and locate the dSYM file using DebugSymbols first
    llvm::ArrayRef<uint8_t> module_uuid = uuid->GetBytes();
    if (module_uuid.size() == 16) {
      CFCReleaser<CFUUIDRef> module_uuid_ref(::CFUUIDCreateWithBytes(
          NULL, module_uuid[0], module_uuid[1], module_uuid[2], module_uuid[3],
          module_uuid[4], module_uuid[5], module_uuid[6], module_uuid[7],
          module_uuid[8], module_uuid[9], module_uuid[10], module_uuid[11],
          module_uuid[12], module_uuid[13], module_uuid[14], module_uuid[15]));

      if (module_uuid_ref.get()) {
        CFCReleaser<CFURLRef> exec_url;
        const FileSpec *exec_fspec = module_spec.GetFileSpecPtr();
        if (exec_fspec) {
          char exec_cf_path[PATH_MAX];
          if (exec_fspec->GetPath(exec_cf_path, sizeof(exec_cf_path)))
            exec_url.reset(::CFURLCreateFromFileSystemRepresentation(
                NULL, (const UInt8 *)exec_cf_path, strlen(exec_cf_path),
                FALSE));
        }

        CFCReleaser<CFURLRef> dsym_url(g_dlsym_DBGCopyFullDSYMURLForUUID(
            module_uuid_ref.get(), exec_url.get()));
        char path[PATH_MAX];

        if (dsym_url.get()) {
          if (::CFURLGetFileSystemRepresentation(
                  dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
            LLDB_LOGF(log,
                      "DebugSymbols framework returned dSYM path of %s for "
                      "UUID %s -- looking for the dSYM",
                      path, uuid->GetAsString().c_str());
            FileSpec dsym_filespec(path);
            if (path[0] == '~')
              FileSystem::Instance().Resolve(dsym_filespec);

            if (FileSystem::Instance().IsDirectory(dsym_filespec)) {
              dsym_filespec = PluginManager::FindSymbolFileInBundle(
                  dsym_filespec, uuid, arch);
              ++items_found;
            } else {
              ++items_found;
            }
            return_module_spec.GetSymbolFileSpec() = dsym_filespec;
          }

          bool success = false;
          if (log) {
            if (::CFURLGetFileSystemRepresentation(
                    dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
              LLDB_LOGF(log,
                        "DebugSymbols framework returned dSYM path of %s for "
                        "UUID %s -- looking for an exec file",
                        path, uuid->GetAsString().c_str());
            }
          }

          CFCReleaser<CFDictionaryRef> dict(
              g_dlsym_DBGCopyDSYMPropertyLists(dsym_url.get()));
          CFDictionaryRef uuid_dict = NULL;
          if (dict.get()) {
            CFCString uuid_cfstr(uuid->GetAsString().c_str());
            uuid_dict = static_cast<CFDictionaryRef>(
                ::CFDictionaryGetValue(dict.get(), uuid_cfstr.get()));
          }

          // Check to see if we have the file on the local filesystem.
          if (FileSystem::Instance().Exists(module_spec.GetFileSpec())) {
            ModuleSpec exe_spec;
            exe_spec.GetFileSpec() = module_spec.GetFileSpec();
            exe_spec.GetUUID() = module_spec.GetUUID();
            ModuleSP module_sp;
            module_sp.reset(new Module(exe_spec));
            if (module_sp && module_sp->GetObjectFile() &&
                module_sp->MatchesModuleSpec(exe_spec)) {
              success = true;
              return_module_spec.GetFileSpec() = module_spec.GetFileSpec();
              LLDB_LOGF(log, "using original binary filepath %s for UUID %s",
                        module_spec.GetFileSpec().GetPath().c_str(),
                        uuid->GetAsString().c_str());
              ++items_found;
            }
          }

          // Check if the requested image is in our shared cache.
          if (!success) {
            SharedCacheImageInfo image_info = HostInfo::GetSharedCacheImageInfo(
                module_spec.GetFileSpec().GetPath());

            // If we found it and it has the correct UUID, let's proceed with
            // creating a module from the memory contents.
            if (image_info.uuid && (!module_spec.GetUUID() ||
                                    module_spec.GetUUID() == image_info.uuid)) {
              success = true;
              return_module_spec.GetFileSpec() = module_spec.GetFileSpec();
              LLDB_LOGF(log,
                        "using binary from shared cache for filepath %s for "
                        "UUID %s",
                        module_spec.GetFileSpec().GetPath().c_str(),
                        uuid->GetAsString().c_str());
              ++items_found;
            }
          }

          // Use the DBGSymbolRichExecutable filepath if present
          if (!success && uuid_dict) {
            CFStringRef exec_cf_path =
                static_cast<CFStringRef>(::CFDictionaryGetValue(
                    uuid_dict, CFSTR("DBGSymbolRichExecutable")));
            if (exec_cf_path && ::CFStringGetFileSystemRepresentation(
                                    exec_cf_path, path, sizeof(path))) {
              LLDB_LOGF(log, "plist bundle has exec path of %s for UUID %s",
                        path, uuid->GetAsString().c_str());
              ++items_found;
              FileSpec exec_filespec(path);
              if (path[0] == '~')
                FileSystem::Instance().Resolve(exec_filespec);
              if (FileSystem::Instance().Exists(exec_filespec)) {
                success = true;
                return_module_spec.GetFileSpec() = exec_filespec;
              }
            }
          }

          // Look next to the dSYM for the binary file.
          if (!success) {
            if (::CFURLGetFileSystemRepresentation(
                    dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
              char *dsym_extension_pos = ::strstr(path, ".dSYM");
              if (dsym_extension_pos) {
                *dsym_extension_pos = '\0';
                LLDB_LOGF(log,
                          "Looking for executable binary next to dSYM "
                          "bundle with name with name %s",
                          path);
                FileSpec file_spec(path);
                FileSystem::Instance().Resolve(file_spec);
                ModuleSpecList module_specs;
                ModuleSpec matched_module_spec;
                using namespace llvm::sys::fs;
                switch (get_file_type(file_spec.GetPath())) {

                case file_type::directory_file: // Bundle directory?
                {
                  CFCBundle bundle(path);
                  CFCReleaser<CFURLRef> bundle_exe_url(
                      bundle.CopyExecutableURL());
                  if (bundle_exe_url.get()) {
                    if (::CFURLGetFileSystemRepresentation(bundle_exe_url.get(),
                                                           true, (UInt8 *)path,
                                                           sizeof(path) - 1)) {
                      FileSpec bundle_exe_file_spec(path);
                      FileSystem::Instance().Resolve(bundle_exe_file_spec);
                      if (ObjectFile::GetModuleSpecifications(
                              bundle_exe_file_spec, 0, 0, module_specs) &&
                          module_specs.FindMatchingModuleSpec(
                              module_spec, matched_module_spec))

                      {
                        ++items_found;
                        return_module_spec.GetFileSpec() = bundle_exe_file_spec;
                        LLDB_LOGF(log,
                                  "Executable binary %s next to dSYM is "
                                  "compatible; using",
                                  path);
                      }
                    }
                  }
                } break;

                case file_type::fifo_file:      // Forget pipes
                case file_type::socket_file:    // We can't process socket files
                case file_type::file_not_found: // File doesn't exist...
                case file_type::status_error:
                  break;

                case file_type::type_unknown:
                case file_type::regular_file:
                case file_type::symlink_file:
                case file_type::block_file:
                case file_type::character_file:
                  if (ObjectFile::GetModuleSpecifications(file_spec, 0, 0,
                                                          module_specs) &&
                      module_specs.FindMatchingModuleSpec(module_spec,
                                                          matched_module_spec))

                  {
                    ++items_found;
                    return_module_spec.GetFileSpec() = file_spec;
                    LLDB_LOGF(log,
                              "Executable binary %s next to dSYM is "
                              "compatible; using",
                              path);
                  }
                  break;
                }
              }
            }
          }
        }
      }
    }
  }

  if (items_found)
    return return_module_spec;

  return {};
}

std::optional<FileSpec> SymbolLocatorDebugSymbols::FindSymbolFileInBundle(
    const FileSpec &dsym_bundle_fspec, const UUID *uuid, const ArchSpec *arch) {
  std::string dsym_bundle_path = dsym_bundle_fspec.GetPath();
  llvm::SmallString<128> buffer(dsym_bundle_path);
  llvm::sys::path::append(buffer, "Contents", "Resources", "DWARF");

  std::error_code EC;
  llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs =
      FileSystem::Instance().GetVirtualFileSystem();
  llvm::vfs::recursive_directory_iterator Iter(*vfs, buffer.str(), EC);
  llvm::vfs::recursive_directory_iterator End;
  for (; Iter != End && !EC; Iter.increment(EC)) {
    llvm::ErrorOr<llvm::vfs::Status> Status = vfs->status(Iter->path());
    if (Status->isDirectory())
      continue;

    FileSpec dsym_fspec(Iter->path());
    ModuleSpecList module_specs;
    if (ObjectFile::GetModuleSpecifications(dsym_fspec, 0, 0, module_specs)) {
      ModuleSpec spec;
      for (size_t i = 0; i < module_specs.GetSize(); ++i) {
        bool got_spec = module_specs.GetModuleSpecAtIndex(i, spec);
        assert(got_spec); // The call has side-effects so can't be inlined.
        UNUSED_IF_ASSERT_DISABLED(got_spec);
        if ((uuid == nullptr ||
             (spec.GetUUIDPtr() && spec.GetUUID() == *uuid)) &&
            (arch == nullptr ||
             (spec.GetArchitecturePtr() &&
              spec.GetArchitecture().IsCompatibleMatch(*arch)))) {
          return dsym_fspec;
        }
      }
    }
  }

  return {};
}

static bool FileAtPathContainsArchAndUUID(const FileSpec &file_fspec,
                                          const ArchSpec *arch,
                                          const lldb_private::UUID *uuid) {
  ModuleSpecList module_specs;
  if (ObjectFile::GetModuleSpecifications(file_fspec, 0, 0, module_specs)) {
    ModuleSpec spec;
    for (size_t i = 0; i < module_specs.GetSize(); ++i) {
      bool got_spec = module_specs.GetModuleSpecAtIndex(i, spec);
      UNUSED_IF_ASSERT_DISABLED(got_spec);
      assert(got_spec);
      if ((uuid == nullptr || (spec.GetUUIDPtr() && spec.GetUUID() == *uuid)) &&
          (arch == nullptr ||
           (spec.GetArchitecturePtr() &&
            spec.GetArchitecture().IsCompatibleMatch(*arch)))) {
        return true;
      }
    }
  }
  return false;
}

// Given a binary exec_fspec, and a ModuleSpec with an architecture/uuid,
// return true if there is a matching dSYM bundle next to the exec_fspec,
// and return that value in dsym_fspec.
// If there is a .dSYM.yaa compressed archive next to the exec_fspec,
// call through PluginManager::DownloadObjectAndSymbolFile to download the
// expanded/uncompressed dSYM and return that filepath in dsym_fspec.
static bool LookForDsymNextToExecutablePath(const ModuleSpec &mod_spec,
                                            const FileSpec &exec_fspec,
                                            FileSpec &dsym_fspec) {
  ConstString filename = exec_fspec.GetFilename();
  FileSpec dsym_directory = exec_fspec;
  dsym_directory.RemoveLastPathComponent();

  std::string dsym_filename = filename.AsCString();
  dsym_filename += ".dSYM";
  dsym_directory.AppendPathComponent(dsym_filename);
  dsym_directory.AppendPathComponent("Contents");
  dsym_directory.AppendPathComponent("Resources");
  dsym_directory.AppendPathComponent("DWARF");

  if (FileSystem::Instance().Exists(dsym_directory)) {

    // See if the binary name exists in the dSYM DWARF
    // subdir.
    dsym_fspec = dsym_directory;
    dsym_fspec.AppendPathComponent(filename.AsCString());
    if (FileSystem::Instance().Exists(dsym_fspec) &&
        FileAtPathContainsArchAndUUID(dsym_fspec, mod_spec.GetArchitecturePtr(),
                                      mod_spec.GetUUIDPtr())) {
      return true;
    }

    // See if we have "../CF.framework" - so we'll look for
    // CF.framework.dSYM/Contents/Resources/DWARF/CF
    // We need to drop the last suffix after '.' to match
    // 'CF' in the DWARF subdir.
    std::string binary_name(filename.AsCString());
    auto last_dot = binary_name.find_last_of('.');
    if (last_dot != std::string::npos) {
      binary_name.erase(last_dot);
      dsym_fspec = dsym_directory;
      dsym_fspec.AppendPathComponent(binary_name);
      if (FileSystem::Instance().Exists(dsym_fspec) &&
          FileAtPathContainsArchAndUUID(dsym_fspec,
                                        mod_spec.GetArchitecturePtr(),
                                        mod_spec.GetUUIDPtr())) {
        return true;
      }
    }
  }

  // See if we have a .dSYM.yaa next to this executable path.
  FileSpec dsym_yaa_fspec = exec_fspec;
  dsym_yaa_fspec.RemoveLastPathComponent();
  std::string dsym_yaa_filename = filename.AsCString();
  dsym_yaa_filename += ".dSYM.yaa";
  dsym_yaa_fspec.AppendPathComponent(dsym_yaa_filename);

  if (FileSystem::Instance().Exists(dsym_yaa_fspec)) {
    ModuleSpec mutable_mod_spec = mod_spec;
    Status error;
    if (PluginManager::DownloadObjectAndSymbolFile(mutable_mod_spec, error,
                                                   true) &&
        FileSystem::Instance().Exists(mutable_mod_spec.GetSymbolFileSpec())) {
      dsym_fspec = mutable_mod_spec.GetSymbolFileSpec();
      return true;
    }
  }

  return false;
}

// Given a ModuleSpec with a FileSpec and optionally uuid/architecture
// filled in, look for a .dSYM bundle next to that binary.  Returns true
// if a .dSYM bundle is found, and that path is returned in the dsym_fspec
// FileSpec.
//
// This routine looks a few directory layers above the given exec_path -
// exec_path might be /System/Library/Frameworks/CF.framework/CF and the
// dSYM might be /System/Library/Frameworks/CF.framework.dSYM.
//
// If there is a .dSYM.yaa compressed archive found next to the binary,
// we'll call DownloadObjectAndSymbolFile to expand it into a plain .dSYM
static bool LocateDSYMInVincinityOfExecutable(const ModuleSpec &module_spec,
                                              FileSpec &dsym_fspec) {
  Log *log = GetLog(LLDBLog::Host);
  const FileSpec &exec_fspec = module_spec.GetFileSpec();
  if (exec_fspec) {
    if (::LookForDsymNextToExecutablePath(module_spec, exec_fspec,
                                          dsym_fspec)) {
      if (log) {
        LLDB_LOGF(log, "dSYM with matching UUID & arch found at %s",
                  dsym_fspec.GetPath().c_str());
      }
      return true;
    } else {
      FileSpec parent_dirs = exec_fspec;

      // Remove the binary name from the FileSpec
      parent_dirs.RemoveLastPathComponent();

      // Add a ".dSYM" name to each directory component of the path,
      // stripping off components.  e.g. we may have a binary like
      // /S/L/F/Foundation.framework/Versions/A/Foundation and
      // /S/L/F/Foundation.framework.dSYM
      //
      // so we'll need to start with
      // /S/L/F/Foundation.framework/Versions/A, add the .dSYM part to the
      // "A", and if that doesn't exist, strip off the "A" and try it again
      // with "Versions", etc., until we find a dSYM bundle or we've
      // stripped off enough path components that there's no need to
      // continue.

      for (int i = 0; i < 4; i++) {
        // Does this part of the path have a "." character - could it be a
        // bundle's top level directory?
        const char *fn = parent_dirs.GetFilename().AsCString();
        if (fn == nullptr)
          break;
        if (::strchr(fn, '.') != nullptr) {
          if (::LookForDsymNextToExecutablePath(module_spec, parent_dirs,
                                                dsym_fspec)) {
            if (log) {
              LLDB_LOGF(log, "dSYM with matching UUID & arch found at %s",
                        dsym_fspec.GetPath().c_str());
            }
            return true;
          }
        }
        parent_dirs.RemoveLastPathComponent();
      }
    }
  }
  dsym_fspec.Clear();
  return false;
}

static int LocateMacOSXFilesUsingDebugSymbols(const ModuleSpec &module_spec,
                                              ModuleSpec &return_module_spec) {
  Log *log = GetLog(LLDBLog::Host);
  if (!ModuleList::GetGlobalModuleListProperties().GetEnableExternalLookup()) {
    LLDB_LOGF(log, "Spotlight lookup for .dSYM bundles is disabled.");
    return 0;
  }

  return_module_spec = module_spec;
  return_module_spec.GetFileSpec().Clear();
  return_module_spec.GetSymbolFileSpec().Clear();

  const UUID *uuid = module_spec.GetUUIDPtr();
  const ArchSpec *arch = module_spec.GetArchitecturePtr();

  int items_found = 0;

  if (g_dlsym_DBGCopyFullDSYMURLForUUID == nullptr ||
      g_dlsym_DBGCopyDSYMPropertyLists == nullptr) {
    void *handle = dlopen(
        "/System/Library/PrivateFrameworks/DebugSymbols.framework/DebugSymbols",
        RTLD_LAZY | RTLD_LOCAL);
    if (handle) {
      g_dlsym_DBGCopyFullDSYMURLForUUID =
          (CFURLRef(*)(CFUUIDRef, CFURLRef))dlsym(handle,
                                                  "DBGCopyFullDSYMURLForUUID");
      g_dlsym_DBGCopyDSYMPropertyLists = (CFDictionaryRef(*)(CFURLRef))dlsym(
          handle, "DBGCopyDSYMPropertyLists");
    }
  }

  if (g_dlsym_DBGCopyFullDSYMURLForUUID == nullptr ||
      g_dlsym_DBGCopyDSYMPropertyLists == nullptr) {
    return items_found;
  }

  if (uuid && uuid->IsValid()) {
    // Try and locate the dSYM file using DebugSymbols first
    llvm::ArrayRef<uint8_t> module_uuid = uuid->GetBytes();
    if (module_uuid.size() == 16) {
      CFCReleaser<CFUUIDRef> module_uuid_ref(::CFUUIDCreateWithBytes(
          NULL, module_uuid[0], module_uuid[1], module_uuid[2], module_uuid[3],
          module_uuid[4], module_uuid[5], module_uuid[6], module_uuid[7],
          module_uuid[8], module_uuid[9], module_uuid[10], module_uuid[11],
          module_uuid[12], module_uuid[13], module_uuid[14], module_uuid[15]));

      if (module_uuid_ref.get()) {
        CFCReleaser<CFURLRef> exec_url;
        const FileSpec *exec_fspec = module_spec.GetFileSpecPtr();
        if (exec_fspec) {
          char exec_cf_path[PATH_MAX];
          if (exec_fspec->GetPath(exec_cf_path, sizeof(exec_cf_path)))
            exec_url.reset(::CFURLCreateFromFileSystemRepresentation(
                NULL, (const UInt8 *)exec_cf_path, strlen(exec_cf_path),
                FALSE));
        }

        CFCReleaser<CFURLRef> dsym_url(g_dlsym_DBGCopyFullDSYMURLForUUID(
            module_uuid_ref.get(), exec_url.get()));
        char path[PATH_MAX];

        if (dsym_url.get()) {
          if (::CFURLGetFileSystemRepresentation(
                  dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
            LLDB_LOGF(log,
                      "DebugSymbols framework returned dSYM path of %s for "
                      "UUID %s -- looking for the dSYM",
                      path, uuid->GetAsString().c_str());
            FileSpec dsym_filespec(path);
            if (path[0] == '~')
              FileSystem::Instance().Resolve(dsym_filespec);

            if (FileSystem::Instance().IsDirectory(dsym_filespec)) {
              dsym_filespec = PluginManager::FindSymbolFileInBundle(
                  dsym_filespec, uuid, arch);
              ++items_found;
            } else {
              ++items_found;
            }
            return_module_spec.GetSymbolFileSpec() = dsym_filespec;
          }

          bool success = false;
          if (log) {
            if (::CFURLGetFileSystemRepresentation(
                    dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
              LLDB_LOGF(log,
                        "DebugSymbols framework returned dSYM path of %s for "
                        "UUID %s -- looking for an exec file",
                        path, uuid->GetAsString().c_str());
            }
          }

          CFCReleaser<CFDictionaryRef> dict(
              g_dlsym_DBGCopyDSYMPropertyLists(dsym_url.get()));
          CFDictionaryRef uuid_dict = NULL;
          if (dict.get()) {
            CFCString uuid_cfstr(uuid->GetAsString().c_str());
            uuid_dict = static_cast<CFDictionaryRef>(
                ::CFDictionaryGetValue(dict.get(), uuid_cfstr.get()));
          }

          // Check to see if we have the file on the local filesystem.
          if (FileSystem::Instance().Exists(module_spec.GetFileSpec())) {
            ModuleSpec exe_spec;
            exe_spec.GetFileSpec() = module_spec.GetFileSpec();
            exe_spec.GetUUID() = module_spec.GetUUID();
            ModuleSP module_sp;
            module_sp.reset(new Module(exe_spec));
            if (module_sp && module_sp->GetObjectFile() &&
                module_sp->MatchesModuleSpec(exe_spec)) {
              success = true;
              return_module_spec.GetFileSpec() = module_spec.GetFileSpec();
              LLDB_LOGF(log, "using original binary filepath %s for UUID %s",
                        module_spec.GetFileSpec().GetPath().c_str(),
                        uuid->GetAsString().c_str());
              ++items_found;
            }
          }

          // Check if the requested image is in our shared cache.
          if (!success) {
            SharedCacheImageInfo image_info = HostInfo::GetSharedCacheImageInfo(
                module_spec.GetFileSpec().GetPath());

            // If we found it and it has the correct UUID, let's proceed with
            // creating a module from the memory contents.
            if (image_info.uuid && (!module_spec.GetUUID() ||
                                    module_spec.GetUUID() == image_info.uuid)) {
              success = true;
              return_module_spec.GetFileSpec() = module_spec.GetFileSpec();
              LLDB_LOGF(log,
                        "using binary from shared cache for filepath %s for "
                        "UUID %s",
                        module_spec.GetFileSpec().GetPath().c_str(),
                        uuid->GetAsString().c_str());
              ++items_found;
            }
          }

          // Use the DBGSymbolRichExecutable filepath if present
          if (!success && uuid_dict) {
            CFStringRef exec_cf_path =
                static_cast<CFStringRef>(::CFDictionaryGetValue(
                    uuid_dict, CFSTR("DBGSymbolRichExecutable")));
            if (exec_cf_path && ::CFStringGetFileSystemRepresentation(
                                    exec_cf_path, path, sizeof(path))) {
              LLDB_LOGF(log, "plist bundle has exec path of %s for UUID %s",
                        path, uuid->GetAsString().c_str());
              ++items_found;
              FileSpec exec_filespec(path);
              if (path[0] == '~')
                FileSystem::Instance().Resolve(exec_filespec);
              if (FileSystem::Instance().Exists(exec_filespec)) {
                success = true;
                return_module_spec.GetFileSpec() = exec_filespec;
              }
            }
          }

          // Look next to the dSYM for the binary file.
          if (!success) {
            if (::CFURLGetFileSystemRepresentation(
                    dsym_url.get(), true, (UInt8 *)path, sizeof(path) - 1)) {
              char *dsym_extension_pos = ::strstr(path, ".dSYM");
              if (dsym_extension_pos) {
                *dsym_extension_pos = '\0';
                LLDB_LOGF(log,
                          "Looking for executable binary next to dSYM "
                          "bundle with name with name %s",
                          path);
                FileSpec file_spec(path);
                FileSystem::Instance().Resolve(file_spec);
                ModuleSpecList module_specs;
                ModuleSpec matched_module_spec;
                using namespace llvm::sys::fs;
                switch (get_file_type(file_spec.GetPath())) {

                case file_type::directory_file: // Bundle directory?
                {
                  CFCBundle bundle(path);
                  CFCReleaser<CFURLRef> bundle_exe_url(
                      bundle.CopyExecutableURL());
                  if (bundle_exe_url.get()) {
                    if (::CFURLGetFileSystemRepresentation(bundle_exe_url.get(),
                                                           true, (UInt8 *)path,
                                                           sizeof(path) - 1)) {
                      FileSpec bundle_exe_file_spec(path);
                      FileSystem::Instance().Resolve(bundle_exe_file_spec);
                      if (ObjectFile::GetModuleSpecifications(
                              bundle_exe_file_spec, 0, 0, module_specs) &&
                          module_specs.FindMatchingModuleSpec(
                              module_spec, matched_module_spec))

                      {
                        ++items_found;
                        return_module_spec.GetFileSpec() = bundle_exe_file_spec;
                        LLDB_LOGF(log,
                                  "Executable binary %s next to dSYM is "
                                  "compatible; using",
                                  path);
                      }
                    }
                  }
                } break;

                case file_type::fifo_file:      // Forget pipes
                case file_type::socket_file:    // We can't process socket files
                case file_type::file_not_found: // File doesn't exist...
                case file_type::status_error:
                  break;

                case file_type::type_unknown:
                case file_type::regular_file:
                case file_type::symlink_file:
                case file_type::block_file:
                case file_type::character_file:
                  if (ObjectFile::GetModuleSpecifications(file_spec, 0, 0,
                                                          module_specs) &&
                      module_specs.FindMatchingModuleSpec(module_spec,
                                                          matched_module_spec))

                  {
                    ++items_found;
                    return_module_spec.GetFileSpec() = file_spec;
                    LLDB_LOGF(log,
                              "Executable binary %s next to dSYM is "
                              "compatible; using",
                              path);
                  }
                  break;
                }
              }
            }
          }
        }
      }
    }
  }

  return items_found;
}

std::optional<FileSpec> SymbolLocatorDebugSymbols::LocateExecutableSymbolFile(
    const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
  const FileSpec *exec_fspec = module_spec.GetFileSpecPtr();
  const ArchSpec *arch = module_spec.GetArchitecturePtr();
  const UUID *uuid = module_spec.GetUUIDPtr();

  LLDB_SCOPED_TIMERF(
      "LocateExecutableSymbolFileDsym (file = %s, arch = %s, uuid = %p)",
      exec_fspec ? exec_fspec->GetFilename().AsCString("<NULL>") : "<NULL>",
      arch ? arch->GetArchitectureName() : "<NULL>", (const void *)uuid);

  Progress progress(
      "Locating external symbol file",
      module_spec.GetFileSpec().GetFilename().AsCString("<Unknown>"));

  FileSpec symbol_fspec;
  ModuleSpec dsym_module_spec;
  // First try and find the dSYM in the same directory as the executable or in
  // an appropriate parent directory
  if (!LocateDSYMInVincinityOfExecutable(module_spec, symbol_fspec)) {
    // We failed to easily find the dSYM above, so use DebugSymbols
    LocateMacOSXFilesUsingDebugSymbols(module_spec, dsym_module_spec);
  } else {
    dsym_module_spec.GetSymbolFileSpec() = symbol_fspec;
  }

  return dsym_module_spec.GetSymbolFileSpec();
}

static bool GetModuleSpecInfoFromUUIDDictionary(CFDictionaryRef uuid_dict,
                                                ModuleSpec &module_spec,
                                                Status &error,
                                                const std::string &command) {
  Log *log = GetLog(LLDBLog::Host);
  bool success = false;
  if (uuid_dict != NULL && CFGetTypeID(uuid_dict) == CFDictionaryGetTypeID()) {
    std::string str;
    CFStringRef cf_str;
    CFDictionaryRef cf_dict;

    cf_str = (CFStringRef)CFDictionaryGetValue((CFDictionaryRef)uuid_dict,
                                               CFSTR("DBGError"));
    if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
      if (CFCString::FileSystemRepresentation(cf_str, str)) {
        std::string errorstr = command;
        errorstr += ":\n";
        errorstr += str;
        error = Status(errorstr);
      }
    }

    cf_str = (CFStringRef)CFDictionaryGetValue(
        (CFDictionaryRef)uuid_dict, CFSTR("DBGSymbolRichExecutable"));
    if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
      if (CFCString::FileSystemRepresentation(cf_str, str)) {
        module_spec.GetFileSpec().SetFile(str.c_str(), FileSpec::Style::native);
        FileSystem::Instance().Resolve(module_spec.GetFileSpec());
        LLDB_LOGF(log,
                  "From dsymForUUID plist: Symbol rich executable is at '%s'",
                  str.c_str());
      }
    }

    cf_str = (CFStringRef)CFDictionaryGetValue((CFDictionaryRef)uuid_dict,
                                               CFSTR("DBGDSYMPath"));
    if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
      if (CFCString::FileSystemRepresentation(cf_str, str)) {
        module_spec.GetSymbolFileSpec().SetFile(str.c_str(),
                                                FileSpec::Style::native);
        FileSystem::Instance().Resolve(module_spec.GetFileSpec());
        success = true;
        LLDB_LOGF(log, "From dsymForUUID plist: dSYM is at '%s'", str.c_str());
      }
    }

    std::string DBGBuildSourcePath;
    std::string DBGSourcePath;

    // If DBGVersion 1 or DBGVersion missing, ignore DBGSourcePathRemapping.
    // If DBGVersion 2, strip last two components of path remappings from
    //                  entries to fix an issue with a specific set of
    //                  DBGSourcePathRemapping entries that lldb worked
    //                  with.
    // If DBGVersion 3, trust & use the source path remappings as-is.
    //
    cf_dict = (CFDictionaryRef)CFDictionaryGetValue(
        (CFDictionaryRef)uuid_dict, CFSTR("DBGSourcePathRemapping"));
    if (cf_dict && CFGetTypeID(cf_dict) == CFDictionaryGetTypeID()) {
      // If we see DBGVersion with a value of 2 or higher, this is a new style
      // DBGSourcePathRemapping dictionary
      bool new_style_source_remapping_dictionary = false;
      bool do_truncate_remapping_names = false;
      std::string original_DBGSourcePath_value = DBGSourcePath;
      cf_str = (CFStringRef)CFDictionaryGetValue((CFDictionaryRef)uuid_dict,
                                                 CFSTR("DBGVersion"));
      if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
        std::string version;
        CFCString::FileSystemRepresentation(cf_str, version);
        if (!version.empty() && isdigit(version[0])) {
          int version_number = atoi(version.c_str());
          if (version_number > 1) {
            new_style_source_remapping_dictionary = true;
          }
          if (version_number == 2) {
            do_truncate_remapping_names = true;
          }
        }
      }

      CFIndex kv_pair_count = CFDictionaryGetCount((CFDictionaryRef)uuid_dict);
      if (kv_pair_count > 0) {
        CFStringRef *keys =
            (CFStringRef *)malloc(kv_pair_count * sizeof(CFStringRef));
        CFStringRef *values =
            (CFStringRef *)malloc(kv_pair_count * sizeof(CFStringRef));
        if (keys != nullptr && values != nullptr) {
          CFDictionaryGetKeysAndValues((CFDictionaryRef)uuid_dict,
                                       (const void **)keys,
                                       (const void **)values);
        }
        for (CFIndex i = 0; i < kv_pair_count; i++) {
          DBGBuildSourcePath.clear();
          DBGSourcePath.clear();
          if (keys[i] && CFGetTypeID(keys[i]) == CFStringGetTypeID()) {
            CFCString::FileSystemRepresentation(keys[i], DBGBuildSourcePath);
          }
          if (values[i] && CFGetTypeID(values[i]) == CFStringGetTypeID()) {
            CFCString::FileSystemRepresentation(values[i], DBGSourcePath);
          }
          if (!DBGBuildSourcePath.empty() && !DBGSourcePath.empty()) {
            // In the "old style" DBGSourcePathRemapping dictionary, the
            // DBGSourcePath values (the "values" half of key-value path pairs)
            // were wrong.  Ignore them and use the universal DBGSourcePath
            // string from earlier.
            if (new_style_source_remapping_dictionary &&
                !original_DBGSourcePath_value.empty()) {
              DBGSourcePath = original_DBGSourcePath_value;
            }
            if (DBGSourcePath[0] == '~') {
              FileSpec resolved_source_path(DBGSourcePath.c_str());
              FileSystem::Instance().Resolve(resolved_source_path);
              DBGSourcePath = resolved_source_path.GetPath();
            }
            // With version 2 of DBGSourcePathRemapping, we can chop off the
            // last two filename parts from the source remapping and get a more
            // general source remapping that still works. Add this as another
            // option in addition to the full source path remap.
            module_spec.GetSourceMappingList().Append(DBGBuildSourcePath,
                                                      DBGSourcePath, true);
            if (do_truncate_remapping_names) {
              FileSpec build_path(DBGBuildSourcePath.c_str());
              FileSpec source_path(DBGSourcePath.c_str());
              build_path.RemoveLastPathComponent();
              build_path.RemoveLastPathComponent();
              source_path.RemoveLastPathComponent();
              source_path.RemoveLastPathComponent();
              module_spec.GetSourceMappingList().Append(
                  build_path.GetPath(), source_path.GetPath(), true);
            }
          }
        }
        if (keys)
          free(keys);
        if (values)
          free(values);
      }
    }

    // If we have a DBGBuildSourcePath + DBGSourcePath pair, append them to the
    // source remappings list.

    cf_str = (CFStringRef)CFDictionaryGetValue((CFDictionaryRef)uuid_dict,
                                               CFSTR("DBGBuildSourcePath"));
    if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
      CFCString::FileSystemRepresentation(cf_str, DBGBuildSourcePath);
    }

    cf_str = (CFStringRef)CFDictionaryGetValue((CFDictionaryRef)uuid_dict,
                                               CFSTR("DBGSourcePath"));
    if (cf_str && CFGetTypeID(cf_str) == CFStringGetTypeID()) {
      CFCString::FileSystemRepresentation(cf_str, DBGSourcePath);
    }

    if (!DBGBuildSourcePath.empty() && !DBGSourcePath.empty()) {
      if (DBGSourcePath[0] == '~') {
        FileSpec resolved_source_path(DBGSourcePath.c_str());
        FileSystem::Instance().Resolve(resolved_source_path);
        DBGSourcePath = resolved_source_path.GetPath();
      }
      module_spec.GetSourceMappingList().Append(DBGBuildSourcePath,
                                                DBGSourcePath, true);
    }
  }
  return success;
}

/// It's expensive to check for the DBGShellCommands defaults setting. Only do
/// it once per lldb run and cache the result.
static llvm::StringRef GetDbgShellCommand() {
  static std::once_flag g_once_flag;
  static std::string g_dbgshell_command;
  std::call_once(g_once_flag, [&]() {
    CFTypeRef defaults_setting = CFPreferencesCopyAppValue(
        CFSTR("DBGShellCommands"), CFSTR("com.apple.DebugSymbols"));
    if (defaults_setting &&
        CFGetTypeID(defaults_setting) == CFStringGetTypeID()) {
      char buffer[PATH_MAX];
      if (CFStringGetCString((CFStringRef)defaults_setting, buffer,
                             sizeof(buffer), kCFStringEncodingUTF8)) {
        g_dbgshell_command = buffer;
      }
    }
    if (defaults_setting) {
      CFRelease(defaults_setting);
    }
  });
  return g_dbgshell_command;
}

/// Get the dsymForUUID executable and cache the result so we don't end up
/// stat'ing the binary over and over.
static FileSpec GetDsymForUUIDExecutable() {
  // The LLDB_APPLE_DSYMFORUUID_EXECUTABLE environment variable is used by the
  // test suite to override the dsymForUUID location. Because we must be able
  // to change the value within a single test, don't bother caching it.
  if (const char *dsymForUUID_env =
          getenv("LLDB_APPLE_DSYMFORUUID_EXECUTABLE")) {
    FileSpec dsymForUUID_executable(dsymForUUID_env);
    FileSystem::Instance().Resolve(dsymForUUID_executable);
    if (FileSystem::Instance().Exists(dsymForUUID_executable))
      return dsymForUUID_executable;
  }

  static std::once_flag g_once_flag;
  static FileSpec g_dsymForUUID_executable;
  std::call_once(g_once_flag, [&]() {
    // Try the DBGShellCommand.
    llvm::StringRef dbgshell_command = GetDbgShellCommand();
    if (!dbgshell_command.empty()) {
      g_dsymForUUID_executable = FileSpec(dbgshell_command);
      FileSystem::Instance().Resolve(g_dsymForUUID_executable);
      if (FileSystem::Instance().Exists(g_dsymForUUID_executable))
        return;
    }

    // Try dsymForUUID in /usr/local/bin
    {
      g_dsymForUUID_executable = FileSpec("/usr/local/bin/dsymForUUID");
      if (FileSystem::Instance().Exists(g_dsymForUUID_executable))
        return;
    }

    // We couldn't find the dsymForUUID binary.
    g_dsymForUUID_executable = {};
  });
  return g_dsymForUUID_executable;
}

bool SymbolLocatorDebugSymbols::DownloadObjectAndSymbolFile(
    ModuleSpec &module_spec, Status &error, bool force_lookup,
    bool copy_executable) {
  const UUID *uuid_ptr = module_spec.GetUUIDPtr();
  const FileSpec *file_spec_ptr = module_spec.GetFileSpecPtr();

  // If \a dbgshell_command is set, the user has specified
  // forced symbol lookup via that command.  We'll get the
  // path back from GetDsymForUUIDExecutable() later.
  llvm::StringRef dbgshell_command = GetDbgShellCommand();

  // If forced lookup isn't set, by the user's \a dbgshell_command or
  // by the \a force_lookup argument, exit this method.
  if (!force_lookup && dbgshell_command.empty())
    return false;

  // We need a UUID or valid existing FileSpec.
  if (!uuid_ptr &&
      (!file_spec_ptr || !FileSystem::Instance().Exists(*file_spec_ptr)))
    return false;

  // We need a dsymForUUID binary or an equivalent executable/script.
  FileSpec dsymForUUID_exe_spec = GetDsymForUUIDExecutable();
  if (!dsymForUUID_exe_spec)
    return false;

  // Create the dsymForUUID command.
  const std::string dsymForUUID_exe_path = dsymForUUID_exe_spec.GetPath();
  const std::string uuid_str = uuid_ptr ? uuid_ptr->GetAsString() : "";

  std::string lookup_arg = uuid_str;
  if (lookup_arg.empty())
    lookup_arg = file_spec_ptr ? file_spec_ptr->GetPath() : "";
  if (lookup_arg.empty())
    return false;

  StreamString command;
  command << dsymForUUID_exe_path << " --ignoreNegativeCache ";
  if (copy_executable)
    command << "--copyExecutable ";
  command << lookup_arg;

  // Log and report progress.
  std::string lookup_desc;
  if (uuid_ptr && file_spec_ptr)
    lookup_desc =
        llvm::formatv("{0} ({1})", file_spec_ptr->GetFilename().GetString(),
                      uuid_ptr->GetAsString());
  else if (uuid_ptr)
    lookup_desc = uuid_ptr->GetAsString();
  else if (file_spec_ptr)
    lookup_desc = file_spec_ptr->GetFilename().GetString();

  Log *log = GetLog(LLDBLog::Host);
  LLDB_LOG(log, "Calling {0} for {1} to find dSYM: {2}", dsymForUUID_exe_path,
           lookup_desc, command.GetString());

  Progress progress("Downloading symbol file for", lookup_desc);

  // Invoke dsymForUUID.
  int exit_status = -1;
  int signo = -1;
  std::string command_output;
  error = Host::RunShellCommand(
      command.GetData(),
      FileSpec(),      // current working directory
      &exit_status,    // Exit status
      &signo,          // Signal int *
      &command_output, // Command output
      std::chrono::seconds(
          640), // Large timeout to allow for long dsym download times
      false);   // Don't run in a shell (we don't need shell expansion)

  if (error.Fail() || exit_status != 0 || command_output.empty()) {
    LLDB_LOGF(log, "'%s' failed (exit status: %d, error: '%s', output: '%s')",
              command.GetData(), exit_status, error.AsCString(),
              command_output.c_str());
    return false;
  }

  CFCData data(
      CFDataCreateWithBytesNoCopy(NULL, (const UInt8 *)command_output.data(),
                                  command_output.size(), kCFAllocatorNull));

  CFCReleaser<CFDictionaryRef> plist(
      (CFDictionaryRef)::CFPropertyListCreateWithData(
          NULL, data.get(), kCFPropertyListImmutable, NULL, NULL));

  if (!plist.get()) {
    LLDB_LOGF(log, "'%s' failed: output is not a valid plist",
              command.GetData());
    return false;
  }

  if (CFGetTypeID(plist.get()) != CFDictionaryGetTypeID()) {
    LLDB_LOGF(log, "'%s' failed: output plist is not a valid CFDictionary",
              command.GetData());
    return false;
  }

  if (!uuid_str.empty()) {
    CFCString uuid_cfstr(uuid_str.c_str());
    CFDictionaryRef uuid_dict =
        (CFDictionaryRef)CFDictionaryGetValue(plist.get(), uuid_cfstr.get());
    return GetModuleSpecInfoFromUUIDDictionary(uuid_dict, module_spec, error,
                                               command.GetData());
  }

  if (const CFIndex num_values = ::CFDictionaryGetCount(plist.get())) {
    std::vector<CFStringRef> keys(num_values, NULL);
    std::vector<CFDictionaryRef> values(num_values, NULL);
    ::CFDictionaryGetKeysAndValues(plist.get(), NULL,
                                   (const void **)&values[0]);
    if (num_values == 1) {
      return GetModuleSpecInfoFromUUIDDictionary(values[0], module_spec, error,
                                                 command.GetData());
    }

    for (CFIndex i = 0; i < num_values; ++i) {
      ModuleSpec curr_module_spec;
      if (GetModuleSpecInfoFromUUIDDictionary(values[i], curr_module_spec,
                                              error, command.GetData())) {
        if (module_spec.GetArchitecture().IsCompatibleMatch(
                curr_module_spec.GetArchitecture())) {
          module_spec = curr_module_spec;
          return true;
        }
      }
    }
  }

  return false;
}
