/* 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 <sstream>

#include "cm_sys_stat.h"

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

using namespace cmFSPermissions;

cmFileInstaller::cmFileInstaller(cmExecutionStatus& status)
  : cmFileCopier(status, "INSTALL")
  , InstallType(cmInstallType_FILES)
  , Optional(false)
  , MessageAlways(false)
  , MessageLazy(false)
  , MessageNever(false)
  , DestDirLength(0)
{
  // Installation does not use source permissions by default.
  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)) {
    Always = cmIsOn(install_always);
  }
  // Get the current manifest.
  Manifest = Makefile->GetSafeDefinition("CMAKE_INSTALL_MANIFEST_FILES");
}
cmFileInstaller::~cmFileInstaller()
{
  // Save the updated install manifest.
  Makefile->AddDefinition("CMAKE_INSTALL_MANIFEST_FILES", Manifest);
}

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

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

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

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

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

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

  if (!HandleInstallDestination()) {
    return false;
  }

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

  return true;
}

bool cmFileInstaller::CheckKeyword(std::string const& arg)
{
  if (arg == "TYPE") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingType;
    }
  } else if (arg == "FILES") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingFiles;
    }
  } else if (arg == "RENAME") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingRename;
    }
  } else if (arg == "OPTIONAL") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingNone;
      Optional = true;
    }
  } else if (arg == "MESSAGE_ALWAYS") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingNone;
      MessageAlways = true;
    }
  } else if (arg == "MESSAGE_LAZY") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingNone;
      MessageLazy = true;
    }
  } else if (arg == "MESSAGE_NEVER") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      Doing = DoingNone;
      MessageNever = true;
    }
  } else if (arg == "PERMISSIONS") {
    if (CurrentMatchRule) {
      Doing = DoingPermissionsMatch;
    } else {
      // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
      Doing = DoingPermissionsFile;
      UseGivenPermissionsFile = true;
    }
  } else if (arg == "DIR_PERMISSIONS") {
    if (CurrentMatchRule) {
      NotAfterMatch(arg);
    } else {
      // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
      Doing = DoingPermissionsDir;
      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.";
    Status.SetError(e.str());
    Doing = DoingError;
  } else {
    return cmFileCopier::CheckKeyword(arg);
  }
  return true;
}

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

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

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

  // allow for / to be a valid destination
  if (destination.size() < 2 && destination != "/") {
    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.
        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);
        Status.SetError(message);
        return false;
      }
    }
    destination = sdestdir + destination.substr(skip);
    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 (!GetDefaultDirectoryPermissions(&default_dir_mode)) {
    return false;
  }

  if (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.";
        Status.SetError(errstring);
        return false;
      }
    }
    if (!cmSystemTools::FileIsDirectory(destination)) {
      std::string errstring =
        "INSTALL destination: " + destination + " is not a directory.";
      Status.SetError(errstring);
      return false;
    }
  }
  return true;
}
