#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2021 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library 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 IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License // If the library is compiled as a DLL, this ensures symbols are exported #define NUCLEX_STORAGE_SOURCE 1 #include "WindowsFileApi.h" #if defined(NUCLEX_STORAGE_WIN32) #include "../../Helpers/WindowsApi.h" #include "../../Helpers/Utf8/checked.h" #include "Nuclex/Support/Text/StringConverter.h" #include // for std::vector namespace { // ------------------------------------------------------------------------------------------- // /// Converts a UTF-8 path into a UTF-16 path /// String containing a UTF-8 path /// The UTF-16 path with magic prefix to eliminate the path length limit std::wstring utf16FromUtf8Path(const std::string &utf8Path) { if(utf8Path.empty()) { return std::wstring(); } // We guess that we need as many UTF-16 characters as we needed UTF-8 characters // based on the assumption that most text will only use ascii characters. std::wstring utf16Path; utf16Path.reserve(utf8Path.length() + 4); // According to Microsoft, this is how you lift the 260 char MAX_PATH limit. // Also skips the internal call to GetFullPathName() every API method does internally, // so paths have to be normalized and const wchar_t prefix[] = L"\\\\?\\"; utf16Path.push_back(prefix[0]); utf16Path.push_back(prefix[1]); utf16Path.push_back(prefix[2]); utf16Path.push_back(prefix[3]); // Do the conversions. If the vector was too short, it will be grown in factors // of 2 usually (depending on the standard library implementation) utf8::utf8to16(utf8Path.begin(), utf8Path.end(), std::back_inserter(utf16Path)); return utf16Path; } // ------------------------------------------------------------------------------------------- // } // anonymous namespace namespace Nuclex { namespace Storage { namespace FileSystem { namespace Windows { // ------------------------------------------------------------------------------------------- // void WindowsFileApi::CreateDirectory(const std::string &path) { std::wstring utf16Path = utf16FromUtf8Path(path); BOOL result = ::CreateDirectoryW(utf16Path.c_str(), nullptr); if(unlikely(result == FALSE)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not create directory '"); errorMessage.append(path); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::CreateDirectoryRecursively(const std::string &path) { std::wstring utf16Path = utf16FromUtf8Path(path); createDirectoryRecursively(utf16Path); } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::DeleteDirectory(const std::string &path) { deleteDirectoryRecursively(utf16FromUtf8Path(path)); } // ------------------------------------------------------------------------------------------- // bool WindowsFileApi::DeleteFile(const std::string &path) { std::wstring utf16Path = utf16FromUtf8Path(path); BOOL result = ::DeleteFileW(utf16Path.c_str()); if(unlikely(result == FALSE)) { DWORD errorCode = ::GetLastError(); if(errorCode == ERROR_FILE_NOT_FOUND) { return false; } else { std::string errorMessage(u8"Could not delete file '"); errorMessage.append(path); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } return true; } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::MoveFile(const std::string &path, const std::string &newPath) { std::wstring utf16Path = utf16FromUtf8Path(path); std::wstring utf16NewPath = utf16FromUtf8Path(newPath); BOOL result = ::MoveFileW(utf16Path.c_str(), utf16NewPath.c_str()); if(unlikely(result == FALSE)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not move file '"); errorMessage.append(path); errorMessage.append(u8"' to '"); errorMessage.append(newPath); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } // ------------------------------------------------------------------------------------------- // std::string WindowsFileApi::GetTempPath() { std::vector temporaryDirectory(MAX_PATH); // Attempt to obtain the system's temporary directory. If the method returns a length // larger than the provided buffer, that's the required buffer size. DWORD length = static_cast(temporaryDirectory.size()); DWORD result = ::GetTempPathW(length, &temporaryDirectory[0]); if(result >= length) { temporaryDirectory.resize(static_cast(result + 1)); length = static_cast(temporaryDirectory.size()); result = ::GetTempPathW(length, &temporaryDirectory[0]); } // Other error or enlarged buffer still too small? if(unlikely((result == 0) || (result >= length))) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not determine temporary directory"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } return Nuclex::Support::Text::StringConverter::Utf8FromWide( std::wstring(&temporaryDirectory[0], static_cast(result)) ); } // ------------------------------------------------------------------------------------------- // std::string WindowsFileApi::GetModuleFileName(HMODULE moduleHandle /* = nullptr */) { std::vector executablePath(MAX_PATH); // Try to get the executable path with a buffer of MAX_PATH characters. DWORD result = ::GetModuleFileNameW( moduleHandle, &executablePath[0], static_cast(executablePath.size()) ); // As long the function returns the buffer size, it is indicating that the buffer // was too small. Keep enlarging the buffer by a factor of 2 until it fits. while(result == executablePath.size()) { executablePath.resize(executablePath.size() * 2); result = ::GetModuleFileNameW( moduleHandle, &executablePath[0], static_cast(executablePath.size()) ); } // If the function returned 0, something went wrong if(unlikely(result == 0)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not determine executable module path"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } return Nuclex::Support::Text::StringConverter::Utf8FromWide( std::wstring(&executablePath[0], static_cast(result)) ); } // ------------------------------------------------------------------------------------------- // std::string WindowsFileApi::GetCurrentWorkingDirectory() { std::vector utf16WorkingDirectory(MAX_PATH); for(;;) { // Ask for the process' current working directory. If the method returns 0, // something went seriously wrong and we need to abort DWORD result = ::GetCurrentDirectoryW( static_cast(utf16WorkingDirectory.size()), &utf16WorkingDirectory[0] ); if(unlikely(result == 0)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not determine current working directory"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } // If the returned length fits within the provided buffer, the buffer will contain // the working directory, otherwise it indicates the required buffer size if(result <= utf16WorkingDirectory.size()) { return Nuclex::Support::Text::StringConverter::Utf8FromWide( std::wstring(&utf16WorkingDirectory[0], static_cast(result)) ); } else { utf16WorkingDirectory.resize(static_cast(result) + 1); } } // for(;;) } // ------------------------------------------------------------------------------------------- // bool WindowsFileApi::GetFileAttributesEx( const std::string &path, WIN32_FILE_ATTRIBUTE_DATA &fileAttributeData ) { std::wstring utf16Path = utf16FromUtf8Path(path); BOOL result = ::GetFileAttributesExW( utf16Path.c_str(), GetFileExInfoStandard, &fileAttributeData ); if(result == FALSE) { DWORD errorCode = ::GetLastError(); if(likely(errorCode == ERROR_FILE_NOT_FOUND)) { return false; } else { std::string errorMessage(u8"Could not query attributes for '"); errorMessage.append(path); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } return true; } // ------------------------------------------------------------------------------------------- // HANDLE WindowsFileApi::FindFirstFile( const std::string &searchMask, WIN32_FIND_DATAW &findData ) { std::wstring utf16SearchMask = Nuclex::Support::Text::StringConverter::WideFromUtf8( searchMask ); HANDLE searchHandle = ::FindFirstFileExW( utf16SearchMask.c_str(), FindExInfoStandard, &findData, FindExSearchNameMatch, nullptr, 0 ); if(searchHandle == INVALID_HANDLE_VALUE) { DWORD errorCode = ::GetLastError(); if(errorCode != ERROR_FILE_NOT_FOUND) { // or ERROR_NO_MORE_FILES, ERROR_NOT_FOUND? std::string errorMessage(u8"Could not start directory enumeration"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } return searchHandle; } // ------------------------------------------------------------------------------------------- // bool WindowsFileApi::FindNextFile(HANDLE searchHandle, WIN32_FIND_DATAW &findData) { BOOL result = ::FindNextFileW(searchHandle, &findData); if(result == FALSE) { DWORD errorCode = ::GetLastError(); if(unlikely(errorCode != ERROR_NO_MORE_FILES)) { std::string errorMessage(u8"Could not advance directory enumeration"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } return false; } else { return true; } } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::CloseSearch(HANDLE searchHandle, bool throwOnError /* = true */) { BOOL result = ::FindClose(searchHandle); if(throwOnError && (result == FALSE)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not close search handle"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } // ------------------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------------------- // std::string WindowsFileApi::GetFullPathName(const std::string &path) { std::wstring utf16Path = Nuclex::Support::Text::StringConverter::WideFromUtf8(path); std::vector utf16FullPath(MAX_PATH); for(;;) { // Ask for the full path of the caller-specified file. If the method returns 0, // something went seriously wrong and we need to abort DWORD byteCount = ::GetFullPathNameW( utf16Path.c_str(), static_cast(utf16FullPath.size()), &utf16FullPath[0], nullptr ); if(unlikely(byteCount == 0)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not resolve full path for '"); errorMessage.append(path); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } // If the returned length fits within the provided buffer, the buffer will contain // the full file path, otherwise it indicates the required buffer size if(byteCount <= utf16FullPath.size()) { return Nuclex::Support::Text::StringConverter::Utf8FromWide( std::wstring(&utf16FullPath[0]) ); } else { utf16FullPath.resize(byteCount); } } // for(;;) } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::createDirectoryRecursively(const std::wstring &directory) { static const std::wstring separators(L"\\/"); // If the specified directory name doesn't exist, do our thing DWORD fileAttributes = ::GetFileAttributesW(directory.c_str()); if(fileAttributes == INVALID_FILE_ATTRIBUTES) { // Recursively do it all again for the parent directory, if any std::size_t slashIndex = directory.find_last_of(separators); if(slashIndex != std::wstring::npos) { createDirectoryRecursively(directory.substr(0, slashIndex)); } // Create the last directory on the path (the recursive calls will have taken // care of the parent directories by now) BOOL result = ::CreateDirectoryW(directory.c_str(), nullptr); if(result == FALSE) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not create directory '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(directory)); errorMessage.append(u8"'"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } else { // Specified directory name already exists as a file or directory bool isDirectoryOrJunction = ( ((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) || ((fileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) ); if(!isDirectoryOrJunction) { DWORD errorCode = ERROR_FILE_EXISTS; std::string errorMessage(u8"Could not create directory '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(directory)); errorMessage.append(u8"' because a file (not directory) with the same name exists"); Helpers::WindowsApi::ThrowExceptionForSystemError(errorMessage, errorCode); } } } // ------------------------------------------------------------------------------------------- // void WindowsFileApi::deleteDirectoryRecursively(const std::wstring &path) { static const std::wstring allFilesMask(L"\\*"); std::wstring searchMask = path + allFilesMask; std::wstring filePath; // Enumerate the directory contents. If the directory has anything in it, // delete its contents first recursively (depth-first) ::WIN32_FIND_DATAW findData; HANDLE searchHandle = ::FindFirstFileExW( searchMask.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0 ); if(searchHandle == INVALID_HANDLE_VALUE) { DWORD errorCode = ::GetLastError(); // The only acceptable error is when no files are found because the directory is empty if(unlikely(errorCode != ERROR_FILE_NOT_FOUND)) { // ERROR_NO_MORE_FILES, ERROR_NOT_FOUND? std::string errorMessage(u8"Could not start directory enumeration in '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(path)); errorMessage.append(u8"'"); Nuclex::Storage::Helpers::WindowsApi::ThrowExceptionForSystemError( errorMessage, errorCode ); } } else { // This directory had contents. Delete them. Nuclex::Storage::FileSystem::Windows::SearchScope scope(searchHandle); for(;;) { // Do not process the obligatory '.' and '..' directories bool isCurrentOrParentDirectory = ( ((findData.cFileName[0] == L'.') && (findData.cFileName[1] == 0)) || ((findData.cFileName[0] == L'.') && (findData.cFileName[1] == L'.')) ); if(!isCurrentOrParentDirectory) { filePath.clear(); filePath.append(path); filePath.push_back(L'\\'); filePath.append(findData.cFileName); bool isDirectory = ( ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) // || ((findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) ); if(isDirectory) { // Subdirectories are handled by deleting their contents first deleteDirectoryRecursively(filePath); } else { // Files are simply deleted right away BOOL result = ::DeleteFileW(filePath.c_str()); if(unlikely(result == FALSE)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not delete file at '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(filePath)); errorMessage.append(u8"'"); Nuclex::Storage::Helpers::WindowsApi::ThrowExceptionForSystemError( errorMessage, errorCode ); } } } // Advance to the next file in the directory BOOL result = ::FindNextFileW(searchHandle, &findData); if(result == FALSE) { DWORD errorCode = ::GetLastError(); if(unlikely(errorCode != ERROR_NO_MORE_FILES)) { std::string errorMessage(u8"Error enumerating directory '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(path)); errorMessage.append(u8"'"); Nuclex::Storage::Helpers::WindowsApi::ThrowExceptionForSystemError( errorMessage, errorCode ); } break; // All directory contents enumerated and deleted } } // for(;;) } // The directory is empty, we can now safely remove it BOOL result = ::RemoveDirectoryW(path.c_str()); if(unlikely(result == FALSE)) { DWORD errorCode = ::GetLastError(); std::string errorMessage(u8"Could not remove directory '"); errorMessage.append(Nuclex::Support::Text::StringConverter::Utf8FromWide(path)); errorMessage.append(u8"'"); Nuclex::Storage::Helpers::WindowsApi::ThrowExceptionForSystemError( errorMessage, errorCode ); } } // ------------------------------------------------------------------------------------------- // }}}} // namespace Nuclex::Storage::FileSystem::Windows #endif // defined(NUCLEX_STORAGE_WIN32)