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

#include "cmFileCopier.h"

#include "cmFSPermissions.h"
#include "cmFileCommand.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"
#include "cmsys/Directory.hxx"
#include "cmsys/Glob.hxx"

#ifdef _WIN32
#  include "cmsys/FStream.hxx"
#endif

#include <sstream>
#include <string.h>

using namespace cmFSPermissions;

cmFileCopier::cmFileCopier(cmFileCommand* command, const char* name)
  : FileCommand(command)
  , Makefile(command->GetMakefile())
  , Name(name)
  , Always(false)
  , MatchlessFiles(true)
  , FilePermissions(0)
  , DirPermissions(0)
  , CurrentMatchRule(nullptr)
  , UseGivenPermissionsFile(false)
  , UseGivenPermissionsDir(false)
  , UseSourcePermissions(true)
  , Doing(DoingNone)
{
}

cmFileCopier::~cmFileCopier() = default;

cmFileCopier::MatchProperties cmFileCopier::CollectMatchProperties(
  const std::string& file)
{
  // Match rules are case-insensitive on some platforms.
#if defined(_WIN32) || defined(__APPLE__) || defined(__CYGWIN__)
  const std::string file_to_match = cmSystemTools::LowerCase(file);
#else
  const std::string& file_to_match = file;
#endif

  // Collect properties from all matching rules.
  bool matched = false;
  MatchProperties result;
  for (MatchRule& mr : this->MatchRules) {
    if (mr.Regex.find(file_to_match)) {
      matched = true;
      result.Exclude |= mr.Properties.Exclude;
      result.Permissions |= mr.Properties.Permissions;
    }
  }
  if (!matched && !this->MatchlessFiles) {
    result.Exclude = !cmSystemTools::FileIsDirectory(file);
  }
  return result;
}

bool cmFileCopier::SetPermissions(const std::string& toFile,
                                  mode_t permissions)
{
  if (permissions) {
#ifdef WIN32
    if (Makefile->IsOn("CMAKE_CROSSCOMPILING")) {
      // Store the mode in an NTFS alternate stream.
      std::string mode_t_adt_filename = toFile + ":cmake_mode_t";

      // Writing to an NTFS alternate stream changes the modification
      // time, so we need to save and restore its original value.
      cmSystemToolsFileTime* file_time_orig = cmSystemTools::FileTimeNew();
      cmSystemTools::FileTimeGet(toFile, file_time_orig);

      cmsys::ofstream permissionStream(mode_t_adt_filename.c_str());

      if (permissionStream) {
        permissionStream << std::oct << permissions << std::endl;
      }

      permissionStream.close();

      cmSystemTools::FileTimeSet(toFile, file_time_orig);

      cmSystemTools::FileTimeDelete(file_time_orig);
    }
#endif

    if (!cmSystemTools::SetPermissions(toFile, permissions)) {
      std::ostringstream e;
      e << this->Name << " cannot set permissions on \"" << toFile << "\"";
      this->FileCommand->SetError(e.str());
      return false;
    }
  }
  return true;
}

// Translate an argument to a permissions bit.
bool cmFileCopier::CheckPermissions(std::string const& arg,
                                    mode_t& permissions)
{
  if (!cmFSPermissions::stringToModeT(arg, permissions)) {
    std::ostringstream e;
    e << this->Name << " given invalid permission \"" << arg << "\".";
    this->FileCommand->SetError(e.str());
    return false;
  }
  return true;
}

std::string const& cmFileCopier::ToName(std::string const& fromName)
{
  return fromName;
}

bool cmFileCopier::ReportMissing(const std::string& fromFile)
{
  // The input file does not exist and installation is not optional.
  std::ostringstream e;
  e << this->Name << " cannot find \"" << fromFile << "\".";
  this->FileCommand->SetError(e.str());
  return false;
}

void cmFileCopier::NotBeforeMatch(std::string const& arg)
{
  std::ostringstream e;
  e << "option " << arg << " may not appear before PATTERN or REGEX.";
  this->FileCommand->SetError(e.str());
  this->Doing = DoingError;
}

void cmFileCopier::NotAfterMatch(std::string const& arg)
{
  std::ostringstream e;
  e << "option " << arg << " may not appear after PATTERN or REGEX.";
  this->FileCommand->SetError(e.str());
  this->Doing = DoingError;
}

void cmFileCopier::DefaultFilePermissions()
{
  // Use read/write permissions.
  this->FilePermissions = 0;
  this->FilePermissions |= mode_owner_read;
  this->FilePermissions |= mode_owner_write;
  this->FilePermissions |= mode_group_read;
  this->FilePermissions |= mode_world_read;
}

void cmFileCopier::DefaultDirectoryPermissions()
{
  // Use read/write/executable permissions.
  this->DirPermissions = 0;
  this->DirPermissions |= mode_owner_read;
  this->DirPermissions |= mode_owner_write;
  this->DirPermissions |= mode_owner_execute;
  this->DirPermissions |= mode_group_read;
  this->DirPermissions |= mode_group_execute;
  this->DirPermissions |= mode_world_read;
  this->DirPermissions |= mode_world_execute;
}

bool cmFileCopier::GetDefaultDirectoryPermissions(mode_t** mode)
{
  // check if default dir creation permissions were set
  const char* default_dir_install_permissions = this->Makefile->GetDefinition(
    "CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS");
  if (default_dir_install_permissions && *default_dir_install_permissions) {
    std::vector<std::string> items;
    cmSystemTools::ExpandListArgument(default_dir_install_permissions, items);
    for (const auto& arg : items) {
      if (!this->CheckPermissions(arg, **mode)) {
        std::ostringstream e;
        e << this->FileCommand->GetError()
          << " Set with CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS "
             "variable.";
        this->FileCommand->SetError(e.str());
        return false;
      }
    }
  } else {
    *mode = nullptr;
  }

  return true;
}

bool cmFileCopier::Parse(std::vector<std::string> const& args)
{
  this->Doing = DoingFiles;
  for (unsigned int i = 1; i < args.size(); ++i) {
    // Check this argument.
    if (!this->CheckKeyword(args[i]) && !this->CheckValue(args[i])) {
      std::ostringstream e;
      e << "called with unknown argument \"" << args[i] << "\".";
      this->FileCommand->SetError(e.str());
      return false;
    }

    // Quit if an argument is invalid.
    if (this->Doing == DoingError) {
      return false;
    }
  }

  // Require a destination.
  if (this->Destination.empty()) {
    std::ostringstream e;
    e << this->Name << " given no DESTINATION";
    this->FileCommand->SetError(e.str());
    return false;
  }

  // If file permissions were not specified set default permissions.
  if (!this->UseGivenPermissionsFile && !this->UseSourcePermissions) {
    this->DefaultFilePermissions();
  }

  // If directory permissions were not specified set default permissions.
  if (!this->UseGivenPermissionsDir && !this->UseSourcePermissions) {
    this->DefaultDirectoryPermissions();
  }

  return true;
}

bool cmFileCopier::CheckKeyword(std::string const& arg)
{
  if (arg == "DESTINATION") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingDestination;
    }
  } else if (arg == "FILES_FROM_DIR") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingFilesFromDir;
    }
  } else if (arg == "PATTERN") {
    this->Doing = DoingPattern;
  } else if (arg == "REGEX") {
    this->Doing = DoingRegex;
  } else if (arg == "EXCLUDE") {
    // Add this property to the current match rule.
    if (this->CurrentMatchRule) {
      this->CurrentMatchRule->Properties.Exclude = true;
      this->Doing = DoingNone;
    } else {
      this->NotBeforeMatch(arg);
    }
  } else if (arg == "PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->Doing = DoingPermissionsMatch;
    } else {
      this->NotBeforeMatch(arg);
    }
  } else if (arg == "FILE_PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingPermissionsFile;
      this->UseGivenPermissionsFile = true;
    }
  } else if (arg == "DIRECTORY_PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingPermissionsDir;
      this->UseGivenPermissionsDir = true;
    }
  } else if (arg == "USE_SOURCE_PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->UseSourcePermissions = true;
    }
  } else if (arg == "NO_SOURCE_PERMISSIONS") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->UseSourcePermissions = false;
    }
  } else if (arg == "FILES_MATCHING") {
    if (this->CurrentMatchRule) {
      this->NotAfterMatch(arg);
    } else {
      this->Doing = DoingNone;
      this->MatchlessFiles = false;
    }
  } else {
    return false;
  }
  return true;
}

bool cmFileCopier::CheckValue(std::string const& arg)
{
  switch (this->Doing) {
    case DoingFiles:
      this->Files.push_back(arg);
      break;
    case DoingDestination:
      if (arg.empty() || cmSystemTools::FileIsFullPath(arg)) {
        this->Destination = arg;
      } else {
        this->Destination = this->Makefile->GetCurrentBinaryDirectory();
        this->Destination += "/" + arg;
      }
      this->Doing = DoingNone;
      break;
    case DoingFilesFromDir:
      if (cmSystemTools::FileIsFullPath(arg)) {
        this->FilesFromDir = arg;
      } else {
        this->FilesFromDir = this->Makefile->GetCurrentSourceDirectory();
        this->FilesFromDir += "/" + arg;
      }
      cmSystemTools::ConvertToUnixSlashes(this->FilesFromDir);
      this->Doing = DoingNone;
      break;
    case DoingPattern: {
      // Convert the pattern to a regular expression.  Require a
      // leading slash and trailing end-of-string in the matched
      // string to make sure the pattern matches only whole file
      // names.
      std::string regex = "/";
      regex += cmsys::Glob::PatternToRegex(arg, false);
      regex += "$";
      this->MatchRules.emplace_back(regex);
      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
      if (this->CurrentMatchRule->Regex.is_valid()) {
        this->Doing = DoingNone;
      } else {
        std::ostringstream e;
        e << "could not compile PATTERN \"" << arg << "\".";
        this->FileCommand->SetError(e.str());
        this->Doing = DoingError;
      }
    } break;
    case DoingRegex:
      this->MatchRules.emplace_back(arg);
      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
      if (this->CurrentMatchRule->Regex.is_valid()) {
        this->Doing = DoingNone;
      } else {
        std::ostringstream e;
        e << "could not compile REGEX \"" << arg << "\".";
        this->FileCommand->SetError(e.str());
        this->Doing = DoingError;
      }
      break;
    case DoingPermissionsFile:
      if (!this->CheckPermissions(arg, this->FilePermissions)) {
        this->Doing = DoingError;
      }
      break;
    case DoingPermissionsDir:
      if (!this->CheckPermissions(arg, this->DirPermissions)) {
        this->Doing = DoingError;
      }
      break;
    case DoingPermissionsMatch:
      if (!this->CheckPermissions(
            arg, this->CurrentMatchRule->Properties.Permissions)) {
        this->Doing = DoingError;
      }
      break;
    default:
      return false;
  }
  return true;
}

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

  for (std::string const& f : this->Files) {
    std::string file;
    if (!f.empty() && !cmSystemTools::FileIsFullPath(f)) {
      if (!this->FilesFromDir.empty()) {
        file = this->FilesFromDir;
      } else {
        file = this->Makefile->GetCurrentSourceDirectory();
      }
      file += "/";
      file += f;
    } else if (!this->FilesFromDir.empty()) {
      this->FileCommand->SetError("option FILES_FROM_DIR requires all files "
                                  "to be specified as relative paths.");
      return false;
    } else {
      file = f;
    }

    // Split the input file into its directory and name components.
    std::vector<std::string> fromPathComponents;
    cmSystemTools::SplitPath(file, fromPathComponents);
    std::string fromName = *(fromPathComponents.end() - 1);
    std::string fromDir = cmSystemTools::JoinPath(
      fromPathComponents.begin(), fromPathComponents.end() - 1);

    // Compute the full path to the destination file.
    std::string toFile = this->Destination;
    if (!this->FilesFromDir.empty()) {
      std::string dir = cmSystemTools::GetFilenamePath(f);
      if (!dir.empty()) {
        toFile += "/";
        toFile += dir;
      }
    }
    std::string const& toName = this->ToName(fromName);
    if (!toName.empty()) {
      toFile += "/";
      toFile += toName;
    }

    // Construct the full path to the source file.  The file name may
    // have been changed above.
    std::string fromFile = fromDir;
    if (!fromName.empty()) {
      fromFile += "/";
      fromFile += fromName;
    }

    if (!this->Install(fromFile, toFile)) {
      return false;
    }
  }
  return true;
}

bool cmFileCopier::Install(const std::string& fromFile,
                           const std::string& toFile)
{
  if (fromFile.empty()) {
    std::ostringstream e;
    e << "INSTALL encountered an empty string input file name.";
    this->FileCommand->SetError(e.str());
    return false;
  }

  // Collect any properties matching this file name.
  MatchProperties match_properties = this->CollectMatchProperties(fromFile);

  // Skip the file if it is excluded.
  if (match_properties.Exclude) {
    return true;
  }

  if (cmSystemTools::SameFile(fromFile, toFile)) {
    return true;
  }
  if (cmSystemTools::FileIsSymlink(fromFile)) {
    return this->InstallSymlink(fromFile, toFile);
  }
  if (cmSystemTools::FileIsDirectory(fromFile)) {
    return this->InstallDirectory(fromFile, toFile, match_properties);
  }
  if (cmSystemTools::FileExists(fromFile)) {
    return this->InstallFile(fromFile, toFile, match_properties);
  }
  return this->ReportMissing(fromFile);
}

bool cmFileCopier::InstallSymlink(const std::string& fromFile,
                                  const std::string& toFile)
{
  // Read the original symlink.
  std::string symlinkTarget;
  if (!cmSystemTools::ReadSymlink(fromFile, symlinkTarget)) {
    std::ostringstream e;
    e << this->Name << " cannot read symlink \"" << fromFile
      << "\" to duplicate at \"" << toFile << "\".";
    this->FileCommand->SetError(e.str());
    return false;
  }

  // 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 (symlinkTarget == 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(symlinkTarget, toFile)) {
      std::ostringstream e;
      e << this->Name << " cannot duplicate symlink \"" << fromFile
        << "\" at \"" << toFile << "\".";
      this->FileCommand->SetError(e.str());
      return false;
    }
  }

  return true;
}

bool cmFileCopier::InstallFile(const std::string& fromFile,
                               const std::string& toFile,
                               MatchProperties match_properties)
{
  // Determine whether we will copy the file.
  bool copy = true;
  if (!this->Always) {
    // If both files exist with the same time do not copy.
    if (!this->FileTimes.DifferS(fromFile, toFile)) {
      copy = false;
    }
  }

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

  // Copy the file.
  if (copy && !cmSystemTools::CopyAFile(fromFile, toFile, true)) {
    std::ostringstream e;
    e << this->Name << " cannot copy file \"" << fromFile << "\" to \""
      << toFile << "\".";
    this->FileCommand->SetError(e.str());
    return false;
  }

  // Set the file modification time of the destination file.
  if (copy && !this->Always) {
    // Add write permission so we can set the file time.
    // Permissions are set unconditionally below anyway.
    mode_t perm = 0;
    if (cmSystemTools::GetPermissions(toFile, perm)) {
      cmSystemTools::SetPermissions(toFile, perm | mode_owner_write);
    }
    if (!cmSystemTools::CopyFileTime(fromFile, toFile)) {
      std::ostringstream e;
      e << this->Name << " cannot set modification time on \"" << toFile
        << "\"";
      this->FileCommand->SetError(e.str());
      return false;
    }
  }

  // Set permissions of the destination file.
  mode_t permissions =
    (match_properties.Permissions ? match_properties.Permissions
                                  : this->FilePermissions);
  if (!permissions) {
    // No permissions were explicitly provided but the user requested
    // that the source file permissions be used.
    cmSystemTools::GetPermissions(fromFile, permissions);
  }
  return this->SetPermissions(toFile, permissions);
}

bool cmFileCopier::InstallDirectory(const std::string& source,
                                    const std::string& destination,
                                    MatchProperties match_properties)
{
  // Inform the user about this directory installation.
  this->ReportCopy(destination, TypeDir,
                   !cmSystemTools::FileIsDirectory(destination));

  // 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;
  }

  // Make sure the destination directory exists.
  if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
    std::ostringstream e;
    e << this->Name << " cannot make directory \"" << destination
      << "\": " << cmSystemTools::GetLastSystemError();
    this->FileCommand->SetError(e.str());
    return false;
  }

  // Compute the requested permissions for the destination directory.
  mode_t permissions =
    (match_properties.Permissions ? match_properties.Permissions
                                  : this->DirPermissions);
  if (!permissions) {
    // No permissions were explicitly provided but the user requested
    // that the source directory permissions be used.
    cmSystemTools::GetPermissions(source, permissions);
  }

  // Compute the set of permissions required on this directory to
  // recursively install files and subdirectories safely.
  mode_t required_permissions =
    mode_owner_read | mode_owner_write | mode_owner_execute;

  // If the required permissions are specified it is safe to set the
  // final permissions now.  Otherwise we must add the required
  // permissions temporarily during file installation.
  mode_t permissions_before = 0;
  mode_t permissions_after = 0;
  if ((permissions & required_permissions) == required_permissions) {
    permissions_before = permissions;
  } else {
    permissions_before = permissions | required_permissions;
    permissions_after = permissions;
  }

  // Set the required permissions of the destination directory.
  if (!this->SetPermissions(destination, permissions_before)) {
    return false;
  }

  // Load the directory contents to traverse it recursively.
  cmsys::Directory dir;
  if (!source.empty()) {
    dir.Load(source);
  }
  unsigned long numFiles = static_cast<unsigned long>(dir.GetNumberOfFiles());
  for (unsigned long fileNum = 0; fileNum < numFiles; ++fileNum) {
    if (!(strcmp(dir.GetFile(fileNum), ".") == 0 ||
          strcmp(dir.GetFile(fileNum), "..") == 0)) {
      std::string fromPath = source;
      fromPath += "/";
      fromPath += dir.GetFile(fileNum);
      std::string toPath = destination;
      toPath += "/";
      toPath += dir.GetFile(fileNum);
      if (!this->Install(fromPath, toPath)) {
        return false;
      }
    }
  }

  // Set the requested permissions of the destination directory.
  return this->SetPermissions(destination, permissions_after);
}
