/* 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 "cmFileTimes.h" #include "cmMakefile.h" #include "cmSystemTools.h" #include "cmsys/Directory.hxx" #include "cmsys/Glob.hxx" #ifdef _WIN32 # include "cmsys/FStream.hxx" #endif #include #include 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) , FollowSymlinkChain(false) , 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. cmFileTimes file_time_orig(toFile); { cmsys::ofstream permissionStream(mode_t_adt_filename.c_str()); if (permissionStream) { permissionStream << std::oct << permissions << std::endl; } permissionStream.close(); } file_time_orig.Store(toFile); } #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 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 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 == "FOLLOW_SYMLINK_CHAIN") { this->FollowSymlinkChain = true; this->Doing = DoingNone; } 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 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 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; } std::string newFromFile = fromFile; std::string newToFile = toFile; if (this->FollowSymlinkChain && !this->InstallSymlinkChain(newFromFile, newToFile)) { return false; } if (cmSystemTools::FileIsSymlink(newFromFile)) { return this->InstallSymlink(newFromFile, newToFile); } if (cmSystemTools::FileIsDirectory(newFromFile)) { return this->InstallDirectory(newFromFile, newToFile, match_properties); } if (cmSystemTools::FileExists(newFromFile)) { return this->InstallFile(newFromFile, newToFile, match_properties); } return this->ReportMissing(newFromFile); } bool cmFileCopier::InstallSymlinkChain(std::string& fromFile, std::string& toFile) { std::string newFromFile; std::string toFilePath = cmSystemTools::GetFilenamePath(toFile); while (cmSystemTools::ReadSymlink(fromFile, newFromFile)) { if (!cmSystemTools::FileIsFullPath(newFromFile)) { std::string fromFilePath = cmSystemTools::GetFilenamePath(fromFile); newFromFile = fromFilePath + "/" + newFromFile; } std::string symlinkTarget = cmSystemTools::GetFilenameName(newFromFile); bool copy = true; if (!this->Always) { std::string oldSymlinkTarget; if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) { if (symlinkTarget == oldSymlinkTarget) { copy = false; } } } this->ReportCopy(toFile, TypeLink, copy); if (copy) { cmSystemTools::RemoveFile(toFile); cmSystemTools::MakeDirectory(toFilePath); if (!cmSystemTools::CreateSymlink(symlinkTarget, toFile)) { std::ostringstream e; e << this->Name << " cannot create symlink \"" << toFile << "\"."; this->FileCommand->SetError(e.str()); return false; } } fromFile = newFromFile; toFile = toFilePath + "/" + symlinkTarget; } return true; } 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 (!cmFileTimes::Copy(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(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); }