mysqlshdk/libs/utils/utils_file.cc (1,029 lines of code) (raw):

/* * Copyright (c) 2015, 2024, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "mysqlshdk/libs/utils/utils_file.h" #include <climits> #include <cstdio> #include <cstring> #include <fstream> #include <iostream> #include <memory> #include <random> #include <stdexcept> #include <type_traits> #include "utils/utils_general.h" #include "utils/utils_path.h" #include "utils/utils_string.h" #ifdef _WIN32 #include <AccCtrl.h> #include <AclAPI.h> #include <Lmcons.h> #include <ShlObj.h> #include <Shlwapi.h> #include <comdef.h> #include <direct.h> #include <windows.h> #pragma comment(lib, "Shlwapi.lib") #else #include <dirent.h> #include <errno.h> #include <sys/file.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #ifdef __APPLE__ #include <limits.h> #include <mach-o/dyld.h> #else #ifdef __sun #include <limits.h> #else #include <linux/limits.h> #endif #endif #endif namespace shcore { namespace { #ifdef _WIN32 std::string get_error(DWORD error_code) { return "SystemError: " + last_error_to_string(error_code) + str_format(" (code %lu)", error_code); } #else std::string get_error(int error_code) { return errno_to_string(error_code) + str_format(" (errno %d)", error_code); } #endif } // namespace /* * Returns the config path * (~/.mysqlsh in Unix or %AppData%\MySQL\mysqlsh in Windows). * May be overriden with MYSQLSH_USER_CONFIG_HOME * (specially for tests) */ std::string get_user_config_path() { // Check if there's an override of the config directory // This is needed required for unit-tests const char *usr_config_path = getenv("MYSQLSH_USER_CONFIG_HOME"); if (usr_config_path) { return std::string(usr_config_path); } std::string path; std::vector<std::string> to_append; #ifdef _WIN32 wchar_t szPath[MAX_PATH + 1] = {}; HRESULT hr; if (SUCCEEDED(hr = SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, szPath))) { path = shcore::wide_to_utf8(szPath, wcslen(szPath)); } else { _com_error err(hr); throw std::runtime_error( str_format("Error when gathering the APPDATA folder path: %s", err.ErrorMessage())); } to_append.push_back("MySQL"); to_append.push_back("mysqlsh"); #else char *cpath = std::getenv("HOME"); if (cpath != NULL) { if (access(cpath, X_OK) != 0) throw std::runtime_error(str_format( "Home folder '%s' does not exist or is not accessible", cpath)); path.assign(cpath); } to_append.push_back(".mysqlsh"); #endif // Up to know the path must exist since it was retrieved from OS standard // means we need to guarantee the rest of the path exists if (!path.empty()) { for (const auto &directory_name : to_append) { path += shcore::path::path_separator + directory_name; ensure_dir_exists(path); } path += shcore::path::path_separator; } return path; } /* * Returns the config path (/etc/mysql/mysqlsh in Unix or * %ProgramData%\MySQL\mysqlsh in Windows). */ std::string get_global_config_path() { std::string path; std::vector<std::string> to_append; #ifdef _WIN32 wchar_t szPath[MAX_PATH + 1] = {}; HRESULT hr; if (SUCCEEDED( hr = SHGetFolderPathW(NULL, CSIDL_COMMON_APPDATA, NULL, 0, szPath))) { path = shcore::path::join_path(shcore::wide_to_utf8(szPath, wcslen(szPath)), "MySQL", "mysqlsh"); } else { _com_error err(hr); throw std::runtime_error( str_format("Error when gathering the PROGRAMDATA folder path: %s", err.ErrorMessage())); } #else path = "/etc/mysql/mysqlsh"; #endif return path; } std::string get_binary_path() { std::string exe_path; // todo(.): warning should be printed with log_warning when available #ifdef _WIN32 HMODULE hModule = GetModuleHandleW(NULL); if (hModule) { // todo(kg): check last error from GetModuleFileNameW and if to is equal to // ERROR_INSUFFICIENT_BUFFER, then grow buffer, and retry. As temporary // solution we use 4k (> PATH_MAX) buffer and hope it fits. wchar_t path[4096] = {'\0'}; const auto path_size = GetModuleFileNameW(hModule, path, 4096); const auto last_error = GetLastError(); if (path_size == 0 || last_error == ERROR_INSUFFICIENT_BUFFER) { throw std::runtime_error( "get_binary_folder: GetModuleFileName failed with error " + get_error(last_error)); } else { // on success path_size does not include terminated null character exe_path = shcore::wide_to_utf8(path, path_size); } } else { throw std::runtime_error( "get_binary_folder: GetModuleHandle failed with error " + get_last_error()); } #else #ifdef __APPLE__ char path[PATH_MAX]{'\0'}; char real_path[PATH_MAX]{'\0'}; uint32_t buffsize = sizeof(path); if (!_NSGetExecutablePath(path, &buffsize)) { // _NSGetExecutablePath may return tricky constructs on paths // like symbolic links or things like i.e /path/to/./mysqlsh // we need to normalize that if (realpath(path, real_path)) { exe_path.assign(real_path); } else { throw std::runtime_error(str_format( "get_binary_folder: Readlink failed with error %d", errno)); } } else { throw std::runtime_error("get_binary_folder: _NSGetExecutablePath failed."); } #else #ifdef __sun char cwd[PATH_MAX]{'\0'}; const char *path = getexecname(); if (path && getcwd(cwd, PATH_MAX)) { exe_path = shcore::path::join_path(cwd, path); } else { throw std::runtime_error( str_format("get_binary_folder: Realpath failed with error %d", errno)); } #else #ifdef __linux__ char path[PATH_MAX]{'\0'}; ssize_t len = readlink("/proc/self/exe", path, PATH_MAX); if (-1 != len) { path[len] = '\0'; exe_path.assign(path); } else { throw std::runtime_error( str_format("get_binary_folder: Readlink failed with error %d", errno)); } #endif #endif #endif #endif return exe_path; } std::string get_binary_folder() { // todo(.): warning should be printed with log_warning when available std::string ret_val; std::string exe_path = get_binary_path(); // If the exe path was found now we check if it can be considered the standard // installation by checking the parent folder is "bin" if (!exe_path.empty()) { const std::string path_separator{shcore::path::path_separator}; std::vector<std::string> tokens; tokens = split_string(exe_path, path_separator, true); tokens.erase(tokens.end() - 1); ret_val = str_join(tokens, path_separator); } return ret_val; } std::string get_share_folder() { std::string path = shcore::path::join_path(get_mysqlx_home_path(), "share", "mysqlsh"); if (!shcore::path::exists(path)) throw std::runtime_error( path + ": share folder not found, shell installation likely invalid"); return path; } std::string get_library_folder() { std::string path = shcore::path::join_path(get_mysqlx_home_path(), "lib", "mysqlsh"); if (!shcore::path::exists(path)) throw std::runtime_error( path + ": lib folder not found, shell installation likely invalid"); return path; } #ifdef HAVE_LIBEXEC_DIR std::string get_libexec_folder() { std::string path = shcore::path::join_path(get_mysqlx_home_path(), LIBEXECDIR, "mysqlsh"); if (!shcore::path::exists(path)) throw std::runtime_error( path + ": " + LIBEXECDIR + " folder not found, shell installation likely invalid"); return path; } #endif // HAVE_LIBEXEC_DIR /* * Returns what should be considered the HOME folder for the shell. * If MYSQLSH_HOME is defined, returns its value. * If not, it will try to identify the value based on the binary full path: * In a standard setup the binary will be at <MYSQLX_HOME>/bin * * If that is the case MYSQLX_HOME is determined by trimming out * /bin/mysqlsh from the full executable name. * * An empty value would indicate MYSQLX_HOME is unknown. */ std::string get_mysqlx_home_path() { std::string ret_val; std::string binary_folder; std::string path_separator; const char *env_home = getenv("MYSQLSH_HOME"); if (env_home) { ret_val.assign(env_home); } else { binary_folder = get_binary_folder(); // If the exe path was found now we check if it can be considered the // standard installation by checking the parent folder is "bin" if (!binary_folder.empty()) { if (shcore::path::basename(binary_folder) == "bin") { ret_val = shcore::path::dirname(binary_folder); } } } return ret_val; } /* * Returns whether a file or a directory exists at the given path (true) or * doesn't (false); */ bool path_exists(const std::string &path) { #ifdef _WIN32 const auto wide_path = utf8_to_wide(path); return GetFileAttributesW(wide_path.c_str()) != INVALID_FILE_ATTRIBUTES; #else return ::access(path.c_str(), F_OK) != -1; #endif } #ifdef _WIN32 bool is_file(const char *path, const size_t path_length) { const auto wide_path = utf8_to_wide(path, path_length); DWORD attributes = GetFileAttributesW(wide_path.c_str()); return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY); } #endif bool is_file(const char *path) { #ifdef _WIN32 return is_file(path, strlen(path)); #else struct stat st; if (::stat(path, &st) < 0) return false; return S_ISREG(st.st_mode); #endif } bool is_file(const std::string &path) { #ifdef _WIN32 return is_file(path.c_str(), path.size()); #else return is_file(path.c_str()); #endif } bool is_fifo(const char *path) { #ifdef _WIN32 // There is no FIFO files on windows in linux meaning return false; #else struct stat st; if (::stat(path, &st) < 0) return false; return S_ISFIFO(st.st_mode); #endif } bool is_fifo(const std::string &path) { return is_fifo(path.c_str()); } size_t file_size(const char *path) { #if defined(_WIN32) struct _stat64 file_stat = {}; const auto ret = _wstat64(utf8_to_wide(path).c_str(), &file_stat); #elif defined(__APPLE__) || defined(__SUNPRO_CC) struct stat file_stat = {}; const auto ret = ::stat(path, &file_stat); #else struct stat64 file_stat = {}; const auto ret = stat64(path, &file_stat); #endif if (0 != ret) { throw std::runtime_error( str_format("Failed to get the file size of '%s': %s", path, errno_to_string(errno).c_str())); } return file_stat.st_size; } size_t file_size(const std::string &path) { return file_size(path.c_str()); } /* * Returns true when the specified path is a folder */ bool is_folder(const std::string &path) { #ifdef _WIN32 const auto wide_path = utf8_to_wide(path); DWORD attributes = GetFileAttributesW(wide_path.c_str()); return (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY)); #else struct stat stbuf; if (::stat(path.c_str(), &stbuf) < 0) return false; return S_ISDIR(stbuf.st_mode); #endif } /* * Attempts to create directory if doesn't exists, otherwise just returns. * If there is an error, an exception is thrown. */ void ensure_dir_exists(const std::string &path) { #ifdef _WIN32 const auto wide_path = utf8_to_wide(path); DWORD attributes = GetFileAttributesW(wide_path.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES) { return; } else if (!CreateDirectoryW(wide_path.c_str(), NULL)) { throw std::runtime_error( str_format("Error when creating directory %s with error: %s", path.c_str(), shcore::get_last_error().c_str())); } #else const char *dir_path = path.c_str(); DIR *dir = opendir(dir_path); if (dir) { /* Directory exists. */ closedir(dir); } else if (ENOENT == errno) { /* Directory does not exist. */ if (mkdir(dir_path, 0700) != 0) throw std::runtime_error( str_format("Error when verifying dir %s exists: %s", dir_path, shcore::get_last_error().c_str())); } else { throw std::runtime_error( str_format("Error when verifying dir %s exists: %s", dir_path, shcore::get_last_error().c_str())); } #endif } /* * Recursively create a directory and its parents if they don't exist. */ void SHCORE_PUBLIC create_directory(const std::string &path, bool recursive, int mode) { assert(!path.empty()); for (;;) { #ifdef _WIN32 const auto wide_path = utf8_to_wide(path); if (CreateDirectoryW(wide_path.c_str(), nullptr) != 0) { break; } const auto last_error = GetLastError(); if (ERROR_ALREADY_EXISTS == last_error) { if (!is_folder(path)) { // path already exist but it's not a directory throw std::runtime_error( str_format("Could not create directory %s: %s", path.c_str(), last_error_to_string(last_error).c_str())); } break; } if (ERROR_PATH_NOT_FOUND == last_error && recursive) { create_directory(path::dirname(path), recursive, mode); } else { throw std::runtime_error( str_format("Could not create directory %s: %s", path.c_str(), last_error_to_string(last_error).c_str())); } #else if (mkdir(path.c_str(), mode) == 0 || errno == EEXIST) { const auto error = errno; if (error == EEXIST && !is_folder(path)) { // path already exist but it's not a directory throw std::runtime_error(str_format("Could not create directory %s: %s", path.c_str(), strerror(error))); } break; } if (errno == ENOENT && recursive) { create_directory(path::dirname(path), recursive, mode); } else { throw std::runtime_error(str_format("Could not create directory %s: %s", path.c_str(), strerror(errno))); } #endif } } std::vector<std::string> listdir(const std::string &path) { std::vector<std::string> files; iterdir(path, [&files](const std::string &name) -> bool { files.push_back(name); return true; }); return files; } /** * Iterate contents of given directory, calling the given function on each * entry. */ bool iterdir(const std::string &path, const std::function<bool(const std::string &)> &fun) { bool stopped = false; #ifdef _WIN32 WIN32_FIND_DATAW ffd; HANDLE hFind = INVALID_HANDLE_VALUE; auto wide_path = utf8_to_wide(path); // Add wildcard to search for all contents in path. const std::wstring search_path = wide_path + (path::is_path_separator(path.back()) ? L"*" : L"\\*"); hFind = FindFirstFileW(search_path.c_str(), &ffd); if (hFind == INVALID_HANDLE_VALUE) throw std::runtime_error( str_format("%s: %s", path.c_str(), shcore::get_last_error().c_str())); // Remove all elements in directory (recursively) do { // Skip directories "." and ".." if (!wcscmp(ffd.cFileName, L".") || !wcscmp(ffd.cFileName, L"..")) { continue; } const auto file_name = shcore::wide_to_utf8(ffd.cFileName); if (!fun(file_name)) { stopped = true; break; } } while (FindNextFileW(hFind, &ffd) != 0); FindClose(hFind); #else DIR *dir = opendir(path.c_str()); if (dir) { // Remove all elements in directory (recursively) struct dirent *p_dir_entry; while ((p_dir_entry = readdir(dir))) { // Skip directories "." and ".." if (!strcmp(p_dir_entry->d_name, ".") || !strcmp(p_dir_entry->d_name, "..")) { continue; } if (!fun(p_dir_entry->d_name)) { stopped = true; break; } } closedir(dir); } else { throw std::runtime_error( str_format("%s: %s", path.c_str(), get_last_error().c_str())); } #endif return !stopped; } /* * Remove the specified directory and all its contents. */ void remove_directory(const std::string &path, bool recursive) { const char *dir_path = path.c_str(); #ifdef _WIN32 const auto wide_path = utf8_to_wide(path); if (recursive) { DWORD attributes = GetFileAttributesW(wide_path.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { throw std::runtime_error("Unable to remove directory " + path + ": " + get_last_error()); } else if (!(attributes & FILE_ATTRIBUTE_DIRECTORY)) { throw std::runtime_error("Not a directory, unable to remove " + path); } else { WIN32_FIND_DATAW ffd; HANDLE hFind = INVALID_HANDLE_VALUE; // Add wildcard to search for all contents in path. const std::wstring search_path = wide_path + (path::is_path_separator(path.back()) ? L"*" : L"\\*"); hFind = FindFirstFileW(search_path.c_str(), &ffd); if (hFind == INVALID_HANDLE_VALUE) throw std::runtime_error( str_format("Unable to remove directory %s. Error searching for " "files in directory: %s", dir_path, shcore::get_last_error().c_str())); // Remove all elements in directory (recursively) do { // Skip directories "." and ".." if (!wcscmp(ffd.cFileName, L".") || !wcscmp(ffd.cFileName, L"..")) { continue; } // Use the full path to the dir element. std::string dir_elem = path + "\\" + shcore::wide_to_utf8(ffd.cFileName); if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // It is a directory then do a recursive call to remove it. remove_directory(dir_elem); } else { // It is a file, remove it. const auto delete_file_path = wide_path + (path::is_path_separator(path.back()) ? L"" : L"\\") + ffd.cFileName; int res = DeleteFileW(delete_file_path.c_str()); if (!res) { throw std::runtime_error(str_format( "Unable to remove directory %s. Error removing file %s: %s", dir_path, dir_elem.c_str(), shcore::get_last_error().c_str())); } } } while (FindNextFileW(hFind, &ffd) != 0); FindClose(hFind); } } // The directory is now empty and can be removed. int res = RemoveDirectoryW(wide_path.c_str()); if (!res) { throw std::runtime_error(str_format("Error removing directory %s: %s", dir_path, shcore::get_last_error().c_str())); } #else if (recursive) { DIR *dir = opendir(dir_path); if (dir) { // Remove all elements in directory (recursively) struct dirent *p_dir_entry; while ((p_dir_entry = readdir(dir))) { // Skip directories "." and ".." if (!strcmp(p_dir_entry->d_name, ".") || !strcmp(p_dir_entry->d_name, "..")) { continue; } // Use the full path to the dir element. std::string dir_elem = path + "/" + p_dir_entry->d_name; // Get the information about the dir element to act accordingly // depending if it is a directory or a file. struct stat stat_info; if (!stat(dir_elem.c_str(), &stat_info)) { if (S_ISDIR(stat_info.st_mode)) { // It is a directory then do a recursive call to remove it. remove_directory(dir_elem); } else { // It is a file, remove it. int res = ::remove(dir_elem.c_str()); if (res && errno != ENOENT) { throw std::runtime_error(str_format( "Unable to remove directory %s. Error removing %s: %s", dir_path, dir_elem.c_str(), shcore::get_last_error().c_str())); } } } } closedir(dir); } else if (ENOENT == errno) { throw std::runtime_error("Directory " + path + " does not exist and cannot be removed."); } else if (ENOTDIR == errno) { throw std::runtime_error("Not a directory, unable to remove " + path); } else { throw std::runtime_error(str_format("Unable to remove directory %s: %s", dir_path, shcore::get_last_error().c_str())); } } // The directory is now empty and can be removed. int res = rmdir(dir_path); if (res) { throw std::runtime_error(str_format("Error remove directory %s: %s", dir_path, shcore::get_last_error().c_str())); } #endif } /* * Returns the last system specific error description (using GetLastError in * Windows or errno in Unix/OSX). */ std::string get_last_error() { #ifdef _WIN32 DWORD dwCode = GetLastError(); return get_error(dwCode); #else int errnum = errno; return get_error(errnum); #endif } bool load_text_file(const std::string &path, std::string &data, bool preserve_cr) { // NOTE: File needs to be opened in binary mode to avoid CRLF being treated as // a single character as it will turn on the fail bit when attempting to read // <size> chars from the file #ifdef _WIN32 std::ifstream s(utf8_to_wide(path), preserve_cr ? std::ios::binary : std::ios::in); #else std::ifstream s(path, preserve_cr ? std::ios::binary : std::ios::in); #endif if (s.fail()) { return false; } s.seekg(0, std::ios_base::end); std::streamsize fsize = s.tellg(); s.seekg(0, std::ios_base::beg); data.resize(fsize); s.read(data.data(), fsize); // if preserve_cr is true, we may read less characters, in which case read() // sets both failbit and eofbit data.resize(s.gcount()); // OK if no errors reading if (s.fail() && !s.eof()) { auto err = errno; s.close(); errno = err; return false; } return true; } std::string SHCORE_PUBLIC get_text_file(const std::string &path, bool preserve_cr) { std::string data; if (!load_text_file(path, data, preserve_cr)) { throw std::runtime_error(path + ": " + errno_to_string(errno)); } return data; } /* * Deletes a file in a cross platform manner. If file removal fails, * an exception is thrown. * * If file does not exist, and quiet is true, fails silently. */ void SHCORE_PUBLIC delete_file(const std::string &filename, bool quiet) { if (quiet && !path_exists(filename)) return; #ifdef _WIN32 const auto wide_filename = utf8_to_wide(filename); if (!DeleteFileW(wide_filename.c_str())) throw std::runtime_error("Cannot delete file \"" + filename + "\". " + get_last_error()); #else if (remove(filename.c_str())) throw std::runtime_error("Cannot delete file \"" + filename + "\". " + get_last_error()); #endif } /* * Returns the HOME path (~ in Unix or %AppData%\ in Windows). */ std::string get_home_dir() { std::string path; #ifdef _WIN32 wchar_t szPath[MAX_PATH + 1] = {}; HRESULT hr; if (SUCCEEDED(hr = SHGetFolderPathW(NULL, CSIDL_PROFILE, NULL, 0, szPath))) { path = shcore::wide_to_utf8(szPath, wcslen(szPath)); } else { _com_error err(hr); throw std::runtime_error( str_format("Error when gathering the PROFILE folder path: %s", err.ErrorMessage())); } #else char *cpath = std::getenv("HOME"); if (cpath != NULL) path.assign(cpath); #endif // Up to know the path must exist since it was retrieved from OS standard // means we need to guarantee the rest of the path exists if (!path.empty()) { path += shcore::path::path_separator; } return path; } bool create_file(const std::string &name, const std::string &content, bool binary_mode) { auto open_mode_flags = std::ofstream::out | std::ofstream::trunc; if (binary_mode) { open_mode_flags |= std::ofstream::binary; } #ifdef _WIN32 // msvc c++ lib has non-standard ofstream constructor wchar_t overload const auto file_name = utf8_to_wide(name); std::ofstream file(file_name, open_mode_flags); #else std::ofstream file(name, open_mode_flags); #endif if (file.is_open()) { file << content; file.close(); return true; } return false; } void copy_file(const std::string &from, const std::string &to, bool copy_attributes) { std::ofstream ofile; std::ifstream ifile; ofile.open(to, std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); if (ofile.fail()) { throw std::runtime_error("Could not create file '" + to + "': " + errno_to_string(errno)); } ifile.open(from, std::ofstream::in | std::ofstream::binary); if (ifile.fail()) { throw std::runtime_error("Could not open file '" + from + "': " + errno_to_string(errno)); } ofile << ifile.rdbuf(); ofile.close(); ifile.close(); if (copy_attributes) { #ifndef _WIN32 // Change the destination file ownership and permissions to match the ones // from the source file. struct stat result; if (stat(from.c_str(), &result) == 0) { if (getuid() == 0) { // Only change file ownership if executed by the root user. if (chown(to.c_str(), result.st_uid, result.st_gid) != 0) { throw std::runtime_error("Unable to change ownership for file " + to + " : " + errno_to_string(errno)); } } if (chmod(to.c_str(), result.st_mode) != 0) { throw std::runtime_error("Unable to set file mode to " + to + ": " + errno_to_string(errno)); } } else { throw std::runtime_error("Unable to get file mode from " + from + ": " + errno_to_string(errno)); } #endif } } void rename_file(const std::string &from, const std::string &to) { #ifdef _WIN32 const auto w_from = utf8_to_wide(from); const auto w_to = utf8_to_wide(to); auto ret = _wrename(w_from.c_str(), w_to.c_str()); if (ret < 0 && (EACCES == errno || EEXIST == errno)) { try { // rename on Windows fails if destination file exists, delete it first delete_file(to, true); } catch (const std::runtime_error &e) { throw std::runtime_error( shcore::str_format("Could not rename '%s' to '%s': destination " "exists and could not be deleted: %s", from.c_str(), to.c_str(), e.what())); } ret = _wrename(w_from.c_str(), w_to.c_str()); } #else const auto ret = rename(from.c_str(), to.c_str()); #endif if (ret < 0) { throw std::runtime_error( shcore::str_format("Could not rename '%s' to '%s': %s", from.c_str(), to.c_str(), strerror(errno))); } } void copy_dir(const std::string &from, const std::string &to) { create_directory(to); iterdir(from, [from, to](const std::string &name) -> bool { try { if (is_folder(path::join_path(from, name))) copy_dir(path::join_path(from, name), path::join_path(to, name)); else copy_file(path::join_path(from, name), path::join_path(to, name)); } catch (const std::runtime_error &) { if (errno != ENOENT) throw; } return true; }); } /* * Check if the file has write permissions. If the file does not exist, * checks if it can be created. * * @param filename the full path of the file to be checked * * Throws an exception with the reason if the file cannot be written to or * created. */ void check_file_writable_or_throw(const std::string &filename) { std::ofstream ofs; if (is_file(filename)) { // Use openmode 'out' to open the file for writing #ifdef _WIN32 const auto wide_filename = utf8_to_wide(filename); ofs.open(wide_filename, std::ofstream::out | std::ofstream::app); #else ofs.open(filename.c_str(), std::ofstream::out | std::ofstream::app); #endif std::string error = shcore::errno_to_string(errno); // If it could open the file, it's writable if (ofs.is_open()) { ofs.close(); } else { throw std::runtime_error(error); } } else { // Use openmode 'out' to open the file for writing #ifdef _WIN32 const auto wide_filename = utf8_to_wide(filename); ofs.open(wide_filename, std::ofstream::out | std::ofstream::app); #else ofs.open(filename.c_str(), std::ofstream::out | std::ofstream::app); #endif std::string error = shcore::errno_to_string(errno); // If it could open the file, it's writable if (ofs.is_open()) { ofs.close(); delete_file(filename); } else { throw std::runtime_error(error); } } } /* * Check if the file has read permissions. * * @param filename the full path of the file to be checked * * Throws an exception with the reason if the file is not found or cannot be * read. */ void SHCORE_PUBLIC check_file_readable_or_throw(const std::string &filename) { if (is_file(filename)) { std::ifstream ifs; #ifdef _WIN32 const auto wide_filename = utf8_to_wide(filename); ifs.open(wide_filename, std::ofstream::in); #else ifs.open(filename.c_str(), std::ofstream::in); #endif std::string error = shcore::errno_to_string(errno); // If it could open the file, it's readable if (ifs.is_open()) { ifs.close(); } else { throw std::runtime_error("Unable to open file '" + filename + "': " + error); } } else { throw std::runtime_error("Path '" + filename + "' is not a file'"); } } /** * Changes access attributes to a file to be read only. * @param path The path to the file to be made read only. * @returns 0 on success, -1 on failure */ int SHCORE_PUBLIC make_file_readonly(const std::string &path) { #ifndef _WIN32 // Set permissions on configuration file to 444 (chmod only works on // unix systems). unsigned int ro = S_IRUSR | S_IRGRP | S_IROTH; return chmod(path.c_str(), ro); #else const auto wide_path = utf8_to_wide(path); auto attributes = GetFileAttributesW(wide_path.c_str()); // set permissions on configuration file to read only if (SetFileAttributesW(wide_path.c_str(), attributes | FILE_ATTRIBUTE_READONLY)) { return 0; } return -1; #endif } /** * Changes the file permissions of the indicated path. * @param path The path of the file/folder which will be updated. * @param mode The User/Group/Other permissions to be set. * * Permissions should be given in binary mask format, this is * as a number in the format of 0UGO * * Where: * @li U is the binary mask for User's permissions * @li G is the binary mask for User Group's permissions * @li O is the binary mask for Other's permissions * * Each binary mask contains the a permission combination that includes: * @li 0x001: Indicates execution permission * @li 0x010: Indicates write permission * @li 0x100: Indicates read permission * * On Windows, only the User permissions are considered: * @li if the user write permission is OFF, the file will be set as * Read Only. * @li if the user write permission is ON, the Read Only flag will be * removed from the file. * * This function does not work with Windows folders. */ int SHCORE_PUBLIC ch_mod(const std::string &path, int mode) { #ifndef _WIN32 return chmod(path.c_str(), mode); #else const auto wide_path = utf8_to_wide(path); const auto attributes = GetFileAttributesW(wide_path.c_str()); // If user write permission is off, sets the file read only // otherwise, cleans the file read only int user_write = (2 << 6) & mode; const DWORD new_attributes = user_write ? attributes & ~FILE_ATTRIBUTE_READONLY : attributes | FILE_ATTRIBUTE_READONLY; if (!SetFileAttributesW(wide_path.c_str(), new_attributes)) { return -1; } return 0; #endif } /** * Updates file or folder permissions so it is accessible only to the current * user. * * In linux, sets files with RW permissions and folders with RWX. * In windows, sets all with Full Control for: * @li Current User * @li System User * @li Admin Group */ int SHCORE_PUBLIC set_user_only_permissions(const std::string &path) { int ret_val = -1; #ifndef _WIN32 int mode = 0600; if (shcore::is_folder(path)) mode = 0700; ret_val = chmod(path.c_str(), mode); #else DWORD dwRes = 0; PSID pSystemSID = NULL, pAdminSID = NULL; SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY; PACL pNewDACL = NULL; EXPLICIT_ACCESSW ea[3]; if (!path.empty()) { DWORD inheritance = NO_INHERITANCE; if (shcore::is_folder(path)) inheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; // Create a SIDs for the BUILTIN\Administrators group // and for the SYSTEM if (AllocateAndInitializeSid(&SIDAuthNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdminSID) && AllocateAndInitializeSid(&SIDAuthNT, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &pSystemSID)) { DWORD user_full_control = GENERIC_ALL | STANDARD_RIGHTS_ALL; // Initialize an EXPLICIT_ACCESS structure for the new ACE. // Sets full access to administrators group ZeroMemory(&ea, 3 * sizeof(EXPLICIT_ACCESS)); ea[0].grfAccessPermissions = user_full_control; ea[0].grfAccessMode = SET_ACCESS; ea[0].grfInheritance = inheritance; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP; ea[0].Trustee.ptstrName = (LPWSTR)pAdminSID; // Sets full access to system user ea[1].grfAccessPermissions = user_full_control; ea[1].grfAccessMode = SET_ACCESS; ea[1].grfInheritance = inheritance; ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[1].Trustee.TrusteeType = TRUSTEE_IS_COMPUTER; ea[1].Trustee.ptstrName = (LPWSTR)pSystemSID; // Sets full access to current user wchar_t username[UNLEN + 1] = {}; DWORD username_len = UNLEN + 1; const auto get_username_retval = GetUserNameW(username, &username_len); if (get_username_retval == 0) { throw std::runtime_error( "Cannot obtain user name while setting permissions to \"" + path + "\". " + get_last_error()); } ea[2].grfAccessPermissions = user_full_control; ea[2].grfAccessMode = SET_ACCESS; ea[2].grfInheritance = inheritance; ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME; ea[2].Trustee.TrusteeType = TRUSTEE_IS_USER; ea[2].Trustee.ptstrName = username; const auto wide_target_path = shcore::utf8_to_wide(path); const auto target_path = wide_target_path.c_str(); // Create a new ACL with defined ACEs if (ERROR_SUCCESS == SetEntriesInAclW(3, ea, nullptr, &pNewDACL)) { // Sets the new ACL as the object's DACL. dwRes = SetNamedSecurityInfoW( (LPWSTR)target_path, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, NULL, NULL, pNewDACL, NULL); if (ERROR_SUCCESS == dwRes) { ret_val = 0; } } } // Cleanup if (pAdminSID != NULL) FreeSid(pAdminSID); if (pSystemSID != NULL) FreeSid(pSystemSID); if (pNewDACL != NULL) LocalFree((HLOCAL)pNewDACL); } #endif return ret_val; } std::string get_absolute_path(const std::string &file_path, const std::string &base_dir) { #ifdef _WIN32 const auto long_path = [](const std::string &path) { if (path.length() > 0 && '\\' != path[0]) { return R"(\\?\)" + path; } else { return path; } }; #else // !_WIN32 const auto long_path = [](const std::string &path) -> const std::string & { return path; }; #endif // _WIN32 if (path::is_absolute(file_path)) { return path::normalize(long_path(path::expand_user(file_path))); } else { return path::normalize(long_path(shcore::path::join_path( base_dir.empty() ? path::getcwd() : base_dir, file_path))); } } #ifdef _WIN32 namespace { using Security_descriptor_t = std::unique_ptr<std::remove_pointer_t<PSECURITY_DESCRIPTOR>, decltype(&LocalFree)>; Security_descriptor_t get_security_descriptor(const std::string &file_name) { static constexpr SECURITY_INFORMATION kReqInfo = DACL_SECURITY_INFORMATION; const auto wide_file_name = utf8_to_wide(file_name); PSECURITY_DESCRIPTOR sec_desc = nullptr; const auto ret_val = GetNamedSecurityInfoW(wide_file_name.c_str(), SE_FILE_OBJECT, kReqInfo, nullptr, nullptr, nullptr, nullptr, &sec_desc); if (ret_val != ERROR_SUCCESS) { throw std::system_error(ret_val, std::system_category(), "GetNamedSecurityInfo() failed (" + file_name + "): " + std::to_string(ret_val)); } return {sec_desc, &LocalFree}; } std::unique_ptr<SID, decltype(&free)> create_sid() { DWORD sid_size = SECURITY_MAX_SID_SIZE; std::unique_ptr<SID, decltype(&free)> everyone_sid( static_cast<SID *>(malloc(sid_size)), &free); if (CreateWellKnownSid(WinWorldSid, nullptr, everyone_sid.get(), &sid_size) == FALSE) { throw std::system_error( std::error_code(GetLastError(), std::system_category()), "CreateWellKnownSid() failed"); } return everyone_sid; } /** * Verifies permissions of an access ACE entry. * * @param[in] access_ace Access ACE entry. * * @throw std::exception Everyone has access to the ACE access entry or * an error occurred. */ void check_ace_access_rights(ACCESS_ALLOWED_ACE *access_ace, SID *esid) { SID *sid = reinterpret_cast<SID *>(&access_ace->SidStart); if (EqualSid(sid, esid)) { if (access_ace->Mask & (FILE_EXECUTE)) { throw std::system_error(make_error_code(std::errc::permission_denied), "Expected no 'Execute' for 'Everyone'."); } if (access_ace->Mask & (FILE_WRITE_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) { throw std::system_error(make_error_code(std::errc::permission_denied), "Expected no 'Write' for 'Everyone'."); } if (access_ace->Mask & (FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES)) { throw std::system_error(make_error_code(std::errc::permission_denied), "Expected no 'Read' for 'Everyone'."); } } } /** * Verifies access permissions in a DACL. * * @param[in] dacl DACL to be verified. * * @throw std::exception DACL contains an ACL entry that grants full access to * Everyone or an error occurred. */ void check_acl_access_rights(ACL *dacl) { ACL_SIZE_INFORMATION dacl_size_info; if (GetAclInformation(dacl, &dacl_size_info, sizeof(dacl_size_info), AclSizeInformation) == FALSE) { throw std::system_error( std::error_code(GetLastError(), std::system_category()), "GetAclInformation() failed"); } auto everyone_sid = create_sid(); for (DWORD ace_idx = 0; ace_idx < dacl_size_info.AceCount; ++ace_idx) { LPVOID ace = nullptr; if (GetAce(dacl, ace_idx, &ace) == FALSE) { throw std::system_error( std::error_code(GetLastError(), std::system_category()), "GetAce() failed"); } if (static_cast<ACE_HEADER *>(ace)->AceType == ACCESS_ALLOWED_ACE_TYPE) check_ace_access_rights(static_cast<ACCESS_ALLOWED_ACE *>(ace), everyone_sid.get()); } } /** * Verifies access permissions in a security descriptor. * * @param[in] sec_desc Security descriptor to be verified. * * @throw std::exception Security descriptor grants full access to * Everyone or an error occurred. */ void check_security_descriptor_access_rights( const Security_descriptor_t &sec_desc) { BOOL dacl_present; ACL *dacl; BOOL dacl_defaulted; if (GetSecurityDescriptorDacl(sec_desc.get(), &dacl_present, &dacl, &dacl_defaulted) == FALSE) { throw std::system_error( std::error_code(GetLastError(), std::system_category()), "GetSecurityDescriptorDacl() failed"); } if (!dacl_present) { // No DACL means: no access allowed. Which is fine. return; } if (!dacl) { // Empty DACL means: all access allowed. throw std::system_error(make_error_code(std::errc::permission_denied), "Expected access denied for 'Everyone'."); } check_acl_access_rights(dacl); } } // namespace #endif // _WIN32 void check_file_access_rights_to_open(const std::string &file_name) { #ifdef _WIN32 Security_descriptor_t sec_descr{nullptr, &LocalFree}; try { sec_descr = get_security_descriptor(file_name); } catch (const std::system_error &) { // that means that the system does not support ACL, in that case nothing // really to check return; } try { check_security_descriptor_access_rights(sec_descr); } catch (const std::system_error &e) { if (e.code() == std::errc::permission_denied) { throw std::system_error( e.code(), "'" + file_name + "' has insecure permissions. " + e.what()); } else { throw; } } catch (...) { throw; } #else struct stat status; if (stat(file_name.c_str(), &status) != 0) { if (errno == ENOENT) return; throw std::system_error(errno, std::generic_category(), "stat() failed for " + file_name + "'"); } static constexpr mode_t kFullAccessMask = (S_IRWXU | S_IRWXG | S_IRWXO); static constexpr mode_t kRequiredAccessMask = (S_IRUSR | S_IWUSR); if ((status.st_mode & kFullAccessMask) != kRequiredAccessMask) { throw std::system_error( make_error_code(std::errc::permission_denied), "'" + file_name + "' has insecure permissions. Expected u+rw only"); } #endif // _WIN32 } std::string get_tempfile_path(const std::string &cnf_path) { std::string tmp_file_path = cnf_path + ".tmp"; if (path_exists(tmp_file_path)) { // Attempt use a temp file with a random component if it already exists. int attempts = 0; bool temp_file_not_exist = false; // Setup uniform random generation of integers between [0, INT_MAX] using // Mersenne Twister algorithm and a non-determinist seed. std::random_device rd_seed; std::mt19937 rnd_gen(rd_seed()); std::uniform_int_distribution<int> distribution(0, INT_MAX); // Try at most 5 times to use a random file name that does not exist. while (attempts < 5) { int rand_num = distribution(rnd_gen); tmp_file_path = cnf_path + ".tmp" + std::to_string(rand_num); attempts++; if (!path_exists(tmp_file_path)) { temp_file_not_exist = true; break; } } if (!temp_file_not_exist) { // This error is not expected to be thrown (only if all attempts failed). throw std::runtime_error( "Unable to generate a non existing temporary file to write the " "configuration for the target option file: " + cnf_path + "."); } } return tmp_file_path; } } // namespace shcore