/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */

#include "cmBinUtilsLinuxELFLinker.h"

#include <queue>
#include <sstream>
#include <unordered_set>
#include <utility>

#include <cm/memory>
#include <cm/string_view>

#include <cmsys/RegularExpression.hxx>

#include "cmBinUtilsLinuxELFObjdumpGetRuntimeDependenciesTool.h"
#include "cmELF.h"
#include "cmLDConfigLDConfigTool.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmRuntimeDependencyArchive.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

static std::string ReplaceOrigin(std::string const& rpath,
                                 std::string const& origin)
{
  static cmsys::RegularExpression const originRegex(
    "(\\$ORIGIN)([^a-zA-Z0-9_]|$)");
  static cmsys::RegularExpression const originCurlyRegex("\\${ORIGIN}");

  cmsys::RegularExpressionMatch match;
  if (originRegex.find(rpath.c_str(), match)) {
    cm::string_view pathv(rpath);
    auto begin = pathv.substr(0, match.start(1));
    auto end = pathv.substr(match.end(1));
    return cmStrCat(begin, origin, end);
  }
  if (originCurlyRegex.find(rpath.c_str(), match)) {
    cm::string_view pathv(rpath);
    auto begin = pathv.substr(0, match.start());
    auto end = pathv.substr(match.end());
    return cmStrCat(begin, origin, end);
  }
  return rpath;
}

cmBinUtilsLinuxELFLinker::cmBinUtilsLinuxELFLinker(
  cmRuntimeDependencyArchive* archive)
  : cmBinUtilsLinker(archive)
{
}

bool cmBinUtilsLinuxELFLinker::Prepare()
{
  std::string tool = this->Archive->GetGetRuntimeDependenciesTool();
  if (tool.empty()) {
    tool = "objdump";
  }
  if (tool == "objdump") {
    this->Tool =
      cm::make_unique<cmBinUtilsLinuxELFObjdumpGetRuntimeDependenciesTool>(
        this->Archive);
  } else {
    std::ostringstream e;
    e << "Invalid value for CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL: " << tool;
    this->SetError(e.str());
    return false;
  }

  std::string ldConfigTool =
    this->Archive->GetMakefile()->GetSafeDefinition("CMAKE_LDCONFIG_TOOL");
  if (ldConfigTool.empty()) {
    ldConfigTool = "ldconfig";
  }
  if (ldConfigTool == "ldconfig") {
    this->LDConfigTool =
      cm::make_unique<cmLDConfigLDConfigTool>(this->Archive);
    if (!this->LDConfigTool->GetLDConfigPaths(this->LDConfigPaths)) {
      return false;
    }
  } else {
    std::ostringstream e;
    e << "Invalid value for CMAKE_LDCONFIG_TOOL: " << ldConfigTool;
    this->SetError(e.str());
    return false;
  }

  return true;
}

bool cmBinUtilsLinuxELFLinker::ScanDependencies(
  std::string const& file, cmStateEnums::TargetType /* unused */)
{
  cmELF elf(file.c_str());
  if (!elf) {
    return false;
  }
  if (elf.GetMachine() != 0) {
    if (this->Machine != 0) {
      if (elf.GetMachine() != this->Machine) {
        this->SetError("All files must have the same architecture.");
        return false;
      }
    } else {
      this->Machine = elf.GetMachine();
    }
  }

  return this->ScanDependencies(file);
}

bool cmBinUtilsLinuxELFLinker::ScanDependencies(std::string const& mainFile)
{
  std::unordered_set<std::string> resolvedDependencies;
  std::queue<std::pair<std::string, std::vector<std::string>>> queueToResolve;
  queueToResolve.push(std::make_pair(mainFile, std::vector<std::string>{}));

  while (!queueToResolve.empty()) {
    std::string file = std::move(queueToResolve.front().first);
    std::vector<std::string> parentRpaths =
      std::move(queueToResolve.front().second);
    queueToResolve.pop();

    std::string origin = cmSystemTools::GetFilenamePath(file);
    std::vector<std::string> needed;
    std::vector<std::string> rpaths;
    std::vector<std::string> runpaths;
    if (!this->Tool->GetFileInfo(file, needed, rpaths, runpaths)) {
      return false;
    }
    for (auto& runpath : runpaths) {
      runpath = ReplaceOrigin(runpath, origin);
    }
    for (auto& rpath : rpaths) {
      rpath = ReplaceOrigin(rpath, origin);
    }

    std::vector<std::string> searchPaths;
    if (!runpaths.empty()) {
      searchPaths = runpaths;
    } else {
      searchPaths = rpaths;
      searchPaths.insert(searchPaths.end(), parentRpaths.begin(),
                         parentRpaths.end());
    }

    searchPaths.insert(searchPaths.end(), this->LDConfigPaths.begin(),
                       this->LDConfigPaths.end());

    for (auto const& dep : needed) {
      if (resolvedDependencies.count(dep) != 0 ||
          this->Archive->IsPreExcluded(dep)) {
        continue;
      }

      std::string path;
      bool resolved = false;
      if (dep.find('/') != std::string::npos) {
        this->SetError("Paths to dependencies are not supported");
        return false;
      }
      if (!this->ResolveDependency(dep, searchPaths, path, resolved)) {
        return false;
      }
      if (resolved) {
        resolvedDependencies.emplace(dep);
        if (!this->Archive->IsPostExcluded(path)) {
          bool unique;
          this->Archive->AddResolvedPath(dep, path, unique);
          if (unique) {
            std::vector<std::string> combinedParentRpaths = parentRpaths;
            combinedParentRpaths.insert(combinedParentRpaths.end(),
                                        rpaths.begin(), rpaths.end());

            queueToResolve.push(std::make_pair(path, combinedParentRpaths));
          }
        }
      } else {
        this->Archive->AddUnresolvedPath(dep);
      }
    }
  }

  return true;
}

namespace {
bool FileHasArchitecture(char const* filename, std::uint16_t machine)
{
  cmELF elf(filename);
  if (!elf) {
    return false;
  }
  return machine == 0 || machine == elf.GetMachine();
}
}

bool cmBinUtilsLinuxELFLinker::ResolveDependency(
  std::string const& name, std::vector<std::string> const& searchPaths,
  std::string& path, bool& resolved)
{
  for (auto const& searchPath : searchPaths) {
    path = cmStrCat(searchPath, '/', name);
    if (cmSystemTools::PathExists(path) &&
        FileHasArchitecture(path.c_str(), this->Machine)) {
      resolved = true;
      return true;
    }
  }

  for (auto const& searchPath : this->Archive->GetSearchDirectories()) {
    path = cmStrCat(searchPath, '/', name);
    if (cmSystemTools::PathExists(path) &&
        FileHasArchitecture(path.c_str(), this->Machine)) {
      std::ostringstream warning;
      warning << "Dependency " << name << " found in search directory:\n  "
              << searchPath
              << "\nSee file(GET_RUNTIME_DEPENDENCIES) documentation for "
              << "more information.";
      this->Archive->GetMakefile()->IssueMessage(MessageType::WARNING,
                                                 warning.str());
      resolved = true;
      return true;
    }
  }

  resolved = false;
  return true;
}
