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

#include "cmFileInstaller.h"

#include <map>
#include <sstream>
#include <utility>

#include <cm/string_view>
#include <cmext/string_view>

#include "cm_sys_stat.h"

#include "cmExecutionStatus.h"
#include "cmFSPermissions.h"
#include "cmMakefile.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"

using namespace cmFSPermissions;

cmFileInstaller::cmFileInstaller(cmExecutionStatus& status)
  : cmFileCopier(status, "INSTALL")
  , InstallType(cmInstallType_FILES)
  , InstallMode(cmInstallMode::COPY)
  , Optional(false)
  , MessageAlways(false)
  , MessageLazy(false)
  , MessageNever(false)
  , DestDirLength(0)
{
  // Installation does not use source permissions by default.
  this->UseSourcePermissions = false;
  // Check whether to copy files always or only if they have changed.
  std::string install_always;
  if (cmSystemTools::GetEnv("CMAKE_INSTALL_ALWAYS", install_always)) {
    this->Always = cmIsOn(install_always);
  }
  // Get the current manifest.
  this->Manifest =
    this->Makefile->GetSafeDefinition("CMAKE_INSTALL_MANIFEST_FILES");
}
cmFileInstaller::~cmFileInstaller()
{
  // Save the updated install manifest.
  this->Makefile->AddDefinition("CMAKE_INSTALL_MANIFEST_FILES",
                                this->Manifest);
}

void cmFileInstaller::ManifestAppend(std::string const& file)
{
  if (!this->Manifest.empty()) {
    this->Manifest += ";";
  }
  this->Manifest += file.substr(this->DestDirLength);
}

std::string const& cmFileInstaller::ToName(std::string const& fromName)
{
  return this->Rename.empty() ? fromName : this->Rename;
}

void cmFileInstaller::ReportCopy(const std::string& toFile, Type type,
                                 bool copy)
{
  if (!this->MessageNever && (copy || !this->MessageLazy)) {
    std::string message =
      cmStrCat((copy ? "Installing: " : "Up-to-date: "), toFile);
    this->Makefile->DisplayStatus(message, -1);
  }
  if (type != TypeDir) {
    // Add the file to the manifest.
    this->ManifestAppend(toFile);
  }
}
bool cmFileInstaller::ReportMissing(const std::string& fromFile)
{
  return (this->Optional || this->cmFileCopier::ReportMissing(fromFile));
}
bool cmFileInstaller::Install(const std::string& fromFile,
                              const std::string& toFile)
{
  // Support installing from empty source to make a directory.
  if (this->InstallType == cmInstallType_DIRECTORY && fromFile.empty()) {
    return this->InstallDirectory(fromFile, toFile, MatchProperties());
  }
  return this->cmFileCopier::Install(fromFile, toFile);
}

bool cmFileInstaller::InstallFile(const std::string& fromFile,
                                  const std::string& toFile,
                                  MatchProperties match_properties)
{
  if (this->InstallMode == cmInstallMode::COPY) {
    return this->cmFileCopier::InstallFile(fromFile, toFile, match_properties);
  }

  std::string newFromFile;

  if (this->InstallMode == cmInstallMode::REL_SYMLINK ||
      this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY ||
      this->InstallMode == cmInstallMode::SYMLINK ||
      this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
    // Try to get a relative path.
    std::string toDir = cmSystemTools::GetParentDirectory(toFile);
    newFromFile = cmSystemTools::ForceToRelativePath(toDir, fromFile);

    // Double check that we can restore the original path.
    std::string reassembled =
      cmSystemTools::CollapseFullPath(newFromFile, toDir);
    if (!cmSystemTools::ComparePath(reassembled, fromFile)) {
      if (this->InstallMode == cmInstallMode::SYMLINK ||
          this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
        // User does not mind, silently proceed with absolute path.
        newFromFile = fromFile;
      } else if (this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY) {
        // User expects a relative symbolic link or a copy.
        // Since an absolute symlink won't do, copy instead.
        return this->cmFileCopier::InstallFile(fromFile, toFile,
                                               match_properties);
      } else {
        // We cannot meet user's expectation (REL_SYMLINK)
        auto e = cmStrCat(this->Name,
                          " cannot determine relative path for symlink to \"",
                          newFromFile, "\" at \"", toFile, "\".");
        this->Status.SetError(e);
        return false;
      }
    }
  } else {
    newFromFile = fromFile; // stick with absolute path
  }

  // Compare the symlink value to that at the destination if not
  // always installing.
  bool copy = true;
  if (!this->Always) {
    std::string oldSymlinkTarget;
    if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) {
      if (newFromFile == oldSymlinkTarget) {
        copy = false;
      }
    }
  }

  // Inform the user about this file installation.
  this->ReportCopy(toFile, TypeLink, copy);

  if (copy) {
    // Remove the destination file so we can always create the symlink.
    cmSystemTools::RemoveFile(toFile);

    // Create destination directory if it doesn't exist
    cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(toFile));

    // Create the symlink.
    if (!cmSystemTools::CreateSymlink(newFromFile, toFile)) {
      if (this->InstallMode == cmInstallMode::ABS_SYMLINK_OR_COPY ||
          this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY ||
          this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
        // Failed to create a symbolic link, fall back to copying.
        return this->cmFileCopier::InstallFile(newFromFile, toFile,
                                               match_properties);
      }

      auto e = cmStrCat(this->Name, " cannot create symlink to \"",
                        newFromFile, "\" at \"", toFile,
                        "\": ", cmSystemTools::GetLastSystemError(), "\".");
      this->Status.SetError(e);
      return false;
    }
  }

  return true;
}

void cmFileInstaller::DefaultFilePermissions()
{
  this->cmFileCopier::DefaultFilePermissions();
  // Add execute permissions based on the target type.
  switch (this->InstallType) {
    case cmInstallType_SHARED_LIBRARY:
    case cmInstallType_MODULE_LIBRARY:
      if (this->Makefile->IsOn("CMAKE_INSTALL_SO_NO_EXE")) {
        break;
      }
      CM_FALLTHROUGH;
    case cmInstallType_EXECUTABLE:
    case cmInstallType_PROGRAMS:
      this->FilePermissions |= mode_owner_execute;
      this->FilePermissions |= mode_group_execute;
      this->FilePermissions |= mode_world_execute;
      break;
    default:
      break;
  }
}

bool cmFileInstaller::Parse(std::vector<std::string> const& args)
{
  if (!this->cmFileCopier::Parse(args)) {
    return false;
  }

  if (!this->Rename.empty()) {
    if (!this->FilesFromDir.empty()) {
      this->Status.SetError("INSTALL option RENAME may not be "
                            "combined with FILES_FROM_DIR.");
      return false;
    }
    if (this->InstallType != cmInstallType_FILES &&
        this->InstallType != cmInstallType_PROGRAMS) {
      this->Status.SetError("INSTALL option RENAME may be used "
                            "only with FILES or PROGRAMS.");
      return false;
    }
    if (this->Files.size() > 1) {
      this->Status.SetError("INSTALL option RENAME may be used "
                            "only with one file.");
      return false;
    }
  }

  if (!this->HandleInstallDestination()) {
    return false;
  }

  if (((this->MessageAlways ? 1 : 0) + (this->MessageLazy ? 1 : 0) +
       (this->MessageNever ? 1 : 0)) > 1) {
    this->Status.SetError("INSTALL options MESSAGE_ALWAYS, "
                          "MESSAGE_LAZY, and MESSAGE_NEVER "
                          "are mutually exclusive.");
    return false;
  }

  static const std::map<cm::string_view, cmInstallMode> install_mode_dict{
    { "ABS_SYMLINK"_s, cmInstallMode::ABS_SYMLINK },
    { "ABS_SYMLINK_OR_COPY"_s, cmInstallMode::ABS_SYMLINK_OR_COPY },
    { "REL_SYMLINK"_s, cmInstallMode::REL_SYMLINK },
    { "REL_SYMLINK_OR_COPY"_s, cmInstallMode::REL_SYMLINK_OR_COPY },
    { "SYMLINK"_s, cmInstallMode::SYMLINK },
    { "SYMLINK_OR_COPY"_s, cmInstallMode::SYMLINK_OR_COPY }
  };

  std::string install_mode;
  cmSystemTools::GetEnv("CMAKE_INSTALL_MODE", install_mode);
  if (install_mode.empty() || install_mode == "COPY"_s) {
    this->InstallMode = cmInstallMode::COPY;
  } else {
    auto it = install_mode_dict.find(install_mode);
    if (it != install_mode_dict.end()) {
      this->InstallMode = it->second;
    } else {
      auto e = cmStrCat("Unrecognized value '", install_mode,
                        "' for environment variable CMAKE_INSTALL_MODE");
      this->Status.SetError(e);
      return false;
    }
  }

  return true;
}

bool cmFileInstaller::CheckKeyword(std::string const& arg)
{
  if (arg == "TYPE") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingType;
    }
  } else if (arg == "FILES") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingFiles;
    }
  } else if (arg == "RENAME") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingRename;
    }
  } else if (arg == "OPTIONAL") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->Optional = true;
    }
  } else if (arg == "MESSAGE_ALWAYS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->MessageAlways = true;
    }
  } else if (arg == "MESSAGE_LAZY") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->MessageLazy = true;
    }
  } else if (arg == "MESSAGE_NEVER") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->MessageNever = true;
    }
  } else if (arg == "PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->Doing = DoingPermissionsMatch;
    } else {
      // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
      this->Doing = DoingPermissionsFile;
      this->UseGivenPermissionsFile = true;
    }
  } else if (arg == "DIR_PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
      this->Doing = DoingPermissionsDir;
      this->UseGivenPermissionsDir = true;
    }
  } else if (arg == "COMPONENTS" || arg == "CONFIGURATIONS" ||
             arg == "PROPERTIES") {
    std::ostringstream e;
    e << "INSTALL called with old-style " << arg << " argument.  "
      << "This script was generated with an older version of CMake.  "
      << "Re-run this cmake version on your build tree.";
    this->Status.SetError(e.str());
    this->Doing = DoingError;
  } else {
    return this->cmFileCopier::CheckKeyword(arg);
  }
  return true;
}

bool cmFileInstaller::CheckValue(std::string const& arg)
{
  switch (this->Doing) {
    case DoingType:
      if (!this->GetTargetTypeFromString(arg)) {
        this->Doing = DoingError;
      }
      break;
    case DoingRename:
      this->Rename = arg;
      break;
    default:
      return this->cmFileCopier::CheckValue(arg);
  }
  return true;
}

bool cmFileInstaller::GetTargetTypeFromString(const std::string& stype)
{
  if (stype == "EXECUTABLE") {
    this->InstallType = cmInstallType_EXECUTABLE;
  } else if (stype == "FILE") {
    this->InstallType = cmInstallType_FILES;
  } else if (stype == "PROGRAM") {
    this->InstallType = cmInstallType_PROGRAMS;
  } else if (stype == "STATIC_LIBRARY") {
    this->InstallType = cmInstallType_STATIC_LIBRARY;
  } else if (stype == "SHARED_LIBRARY") {
    this->InstallType = cmInstallType_SHARED_LIBRARY;
  } else if (stype == "MODULE") {
    this->InstallType = cmInstallType_MODULE_LIBRARY;
  } else if (stype == "DIRECTORY") {
    this->InstallType = cmInstallType_DIRECTORY;
  } else {
    std::ostringstream e;
    e << "Option TYPE given unknown value \"" << stype << "\".";
    this->Status.SetError(e.str());
    return false;
  }
  return true;
}

bool cmFileInstaller::HandleInstallDestination()
{
  std::string& destination = this->Destination;

  // allow for / to be a valid destination
  if (destination.size() < 2 && destination != "/") {
    this->Status.SetError("called with inappropriate arguments. "
                          "No DESTINATION provided or .");
    return false;
  }

  std::string sdestdir;
  if (cmSystemTools::GetEnv("DESTDIR", sdestdir) && !sdestdir.empty()) {
    cmSystemTools::ConvertToUnixSlashes(sdestdir);
    char ch1 = destination[0];
    char ch2 = destination[1];
    char ch3 = 0;
    if (destination.size() > 2) {
      ch3 = destination[2];
    }
    int skip = 0;
    if (ch1 != '/') {
      int relative = 0;
      if (((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z')) &&
          ch2 == ':') {
        // Assume windows
        // let's do some destdir magic:
        skip = 2;
        if (ch3 != '/') {
          relative = 1;
        }
      } else {
        relative = 1;
      }
      if (relative) {
        // This is relative path on unix or windows. Since we are doing
        // destdir, this case does not make sense.
        this->Status.SetError(
          "called with relative DESTINATION. This "
          "does not make sense when using DESTDIR. Specify "
          "absolute path or remove DESTDIR environment variable.");
        return false;
      }
    } else {
      if (ch2 == '/') {
        // looks like a network path.
        std::string message =
          cmStrCat("called with network path DESTINATION. This "
                   "does not make sense when using DESTDIR. Specify local "
                   "absolute path or remove DESTDIR environment variable."
                   "\nDESTINATION=\n",
                   destination);
        this->Status.SetError(message);
        return false;
      }
    }
    destination = sdestdir + destination.substr(skip);
    this->DestDirLength = int(sdestdir.size());
  }

  // check if default dir creation permissions were set
  mode_t default_dir_mode_v = 0;
  mode_t* default_dir_mode = &default_dir_mode_v;
  if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
    return false;
  }

  if (this->InstallType != cmInstallType_DIRECTORY) {
    if (!cmSystemTools::FileExists(destination)) {
      if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
        std::string errstring = "cannot create directory: " + destination +
          ". Maybe need administrative privileges.";
        this->Status.SetError(errstring);
        return false;
      }
    }
    if (!cmSystemTools::FileIsDirectory(destination)) {
      std::string errstring =
        "INSTALL destination: " + destination + " is not a directory.";
      this->Status.SetError(errstring);
      return false;
    }
  }
  return true;
}
