From f2cb9f0fa0a2f31f41c732a572e6cc97818cf485 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 11 Mar 2023 16:38:35 +0000 Subject: [PATCH] file_util: Implement `RecursivelyCreateDir` With this, we no longer require `std::filesystem` support to create the save file directory when it does not exist. Note that by default the save file directory exists because it is created by SDL, so this is mostly useful on platforms that override the save directory or use `UNPACKED_SAVES`. --- Packaging/windows/CMakePresets.json | 9 +- Source/mpq/mpq_writer.cpp | 11 +-- Source/options.cpp | 10 +-- Source/utils/file_util.cpp | 122 ++++++++++++++++++++++++++-- Source/utils/file_util.h | 19 +++++ Source/utils/paths.h | 8 -- test/file_util_test.cpp | 21 +++++ 7 files changed, 166 insertions(+), 34 deletions(-) diff --git a/Packaging/windows/CMakePresets.json b/Packaging/windows/CMakePresets.json index 788b20272..c59a2c30c 100644 --- a/Packaging/windows/CMakePresets.json +++ b/Packaging/windows/CMakePresets.json @@ -74,15 +74,18 @@ "testPresets": [ { "name": "ninja-vcpkg-debug", - "configurePreset": "ninja-vcpkg-debug" + "configurePreset": "ninja-vcpkg-debug", + "output": {"outputOnFailure": true} }, { "name": "ninja-vcpkg-relwithdebinfo", - "configurePreset": "ninja-vcpkg-relwithdebinfo" + "configurePreset": "ninja-vcpkg-relwithdebinfo", + "output": {"outputOnFailure": true} }, { "name": "ninja-vcpkg-discord-relwithdebinfo", - "configurePreset": "ninja-vcpkg-discord-relwithdebinfo" + "configurePreset": "ninja-vcpkg-discord-relwithdebinfo", + "output": {"outputOnFailure": true} } ] } diff --git a/Source/mpq/mpq_writer.cpp b/Source/mpq/mpq_writer.cpp index fb448aaf0..56dc39e8f 100644 --- a/Source/mpq/mpq_writer.cpp +++ b/Source/mpq/mpq_writer.cpp @@ -88,15 +88,8 @@ bool IsUnallocatedBlock(const MpqBlockEntry *block) MpqWriter::MpqWriter(const char *path) { -#ifdef DVL_HAS_FILESYSTEM - { - std::error_code error; - std::filesystem::create_directories(std::filesystem::path(path).parent_path(), error); - if (error) { - LogError("failed to create directory: {}", error.message()); - } - } -#endif + const std::string dir = std::string(Dirname(path)); + RecursivelyCreateDir(dir.c_str()); LogVerbose("Opening {}", path); std::string error; bool exists = FileExists(path); diff --git a/Source/options.cpp b/Source/options.cpp index 02c34e617..0ece5640e 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -236,15 +236,7 @@ void SaveIni() { if (!IniChanged) return; -#ifdef DVL_HAS_FILESYSTEM - { - std::error_code error; - std::filesystem::create_directories(paths::ConfigPath(), error); - if (error) { - LogError("failed to create directory: {}", error.message()); - } - } -#endif + RecursivelyCreateDir(paths::ConfigPath().c_str()); const std::string iniPath = GetIniPath(); FILE *file = OpenFile(iniPath.c_str(), "wb"); if (file != nullptr) { diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index e803bb3b2..114905adc 100644 --- a/Source/utils/file_util.cpp +++ b/Source/utils/file_util.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -38,6 +39,13 @@ #include #endif +// We include sys/stat.h for mkdir where available. +#if !defined(DVL_HAS_FILESYSTEM) && defined(__has_include) && !defined(_WIN32) +#if __has_include() +#include +#endif +#endif + namespace devilution { #if (defined(_WIN64) || defined(_WIN32)) && !defined(NXDK) @@ -55,6 +63,18 @@ std::unique_ptr ToWideChar(string_view path) } #endif +string_view Dirname(string_view path) +{ + while (path.size() > 1 && path.back() == DirectorySeparator) + path.remove_suffix(1); + if (path.size() == 1 && path.back() == DirectorySeparator) + return DIRECTORY_SEPARATOR_STR; + const size_t sep = path.find_last_of(DIRECTORY_SEPARATOR_STR); + if (sep == string_view::npos) + return "."; + return string_view { path.data(), sep }; +} + bool FileExists(const char *path) { #if defined(_WIN64) || defined(_WIN32) @@ -95,9 +115,10 @@ bool FileExists(const char *path) #endif } -bool FileExistsAndIsWriteable(const char *path) -{ #if defined(_WIN64) || defined(_WIN32) +namespace { +DWORD WindowsGetFileAttributes(const char *path) +{ #if defined(NXDK) const DWORD attr = ::GetFileAttributesA(path); #else @@ -108,8 +129,7 @@ bool FileExistsAndIsWriteable(const char *path) } const DWORD attr = ::GetFileAttributesW(&pathUtf16[0]); #endif - const bool exists = attr != INVALID_FILE_ATTRIBUTES; - if (!exists) { + if (attr == INVALID_FILE_ATTRIBUTES) { if (::GetLastError() == ERROR_FILE_NOT_FOUND || ::GetLastError() == ERROR_PATH_NOT_FOUND) { ::SetLastError(ERROR_SUCCESS); } else { @@ -120,7 +140,31 @@ bool FileExistsAndIsWriteable(const char *path) #endif } } - return exists && (attr & FILE_ATTRIBUTE_READONLY) == 0; + return attr; +} +} // namespace + +#endif + +bool DirectoryExists(const char *path) +{ +#if defined(_WIN64) || defined(_WIN32) + const DWORD attr = WindowsGetFileAttributes(path); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) != 0; +#elif defined(DVL_HAS_FILESYSTEM) + std::error_code error; + return std::filesystem::is_directory(path, error); +#elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) + struct ::stat statResult; + return ::stat(path, &statResult) == 0 && S_ISDIR(statResult.st_mode); +#endif +} + +bool FileExistsAndIsWriteable(const char *path) +{ +#if defined(_WIN64) || defined(_WIN32) + const DWORD attr = WindowsGetFileAttributes(path); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_READONLY) == 0; #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) return ::access(path, W_OK) == 0; #else @@ -164,6 +208,74 @@ bool GetFileSize(const char *path, std::uintmax_t *size) #endif } +bool CreateDir(const char *path) +{ +#ifdef DVL_HAS_FILESYSTEM + std::error_code error; + std::filesystem::create_directory(path, error); + if (error) { + LogError("failed to create directory {}: {}", path, error.message()); + return false; + } + return true; +#elif defined(_WIN64) || defined(_WIN32) +#ifdef NXDK + if (::CreateDirectoryA(path, /*lpSecurityAttributes=*/nullptr) != 0) + return true; + if (::GetLastError() == ERROR_ALREADY_EXISTS) + return true; + LogError("failed to create directory {}", path); + return false; +#else + const auto pathUtf16 = ToWideChar(path); + if (pathUtf16 == nullptr) { + LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); + return false; + } + if (::CreateDirectoryW(&pathUtf16[0], /*lpSecurityAttributes=*/nullptr) != 0) + return true; + if (::GetLastError() == ERROR_ALREADY_EXISTS) + return true; + LogError("failed to create directory {}", path); + return false; +#endif +#else + const int result = ::mkdir(path, 0777); + if (result != 0 && result != EEXIST) { + LogError("failed to create directory {}", path); + return false; + } + return true; +#endif +} + +void RecursivelyCreateDir(const char *path) +{ +#ifdef DVL_HAS_FILESYSTEM + std::error_code error; + std::filesystem::create_directories(path, error); + if (error) { + LogError("failed to create directory {}: {}", path, error.message()); + } +#else + if (DirectoryExists(path)) + return; + std::vector paths; + std::string cur { path }; + do { + paths.push_back(cur); + string_view parent = Dirname(cur); + if (parent == cur) + break; + cur.assign(parent.data(), parent.size()); + } while (!DirectoryExists(cur.c_str())); + for (auto it = std::rbegin(paths); it != std::rend(paths); ++it) { + if (!CreateDir(it->c_str())) + return; + } +#endif +} + bool ResizeFile(const char *path, std::uintmax_t size) { #if defined(_WIN64) || defined(_WIN32) diff --git a/Source/utils/file_util.h b/Source/utils/file_util.h index 9de80cae4..6485775e1 100644 --- a/Source/utils/file_util.h +++ b/Source/utils/file_util.h @@ -9,6 +9,14 @@ namespace devilution { +#ifdef _WIN32 +constexpr char DirectorySeparator = '\\'; +#define DIRECTORY_SEPARATOR_STR "\\" +#else +constexpr char DirectorySeparator = '/'; +#define DIRECTORY_SEPARATOR_STR "/" +#endif + bool FileExists(const char *path); inline bool FileExists(const std::string &str) @@ -16,8 +24,19 @@ inline bool FileExists(const std::string &str) return FileExists(str.c_str()); } +bool DirectoryExists(const char *path); +string_view Dirname(string_view path); bool FileExistsAndIsWriteable(const char *path); bool GetFileSize(const char *path, std::uintmax_t *size); + +/** + * @brief Creates a single directory (non-recursively). + * + * @return True if the directory already existed or has been created sucessfully. + */ +bool CreateDir(const char *path); + +void RecursivelyCreateDir(const char *path); bool ResizeFile(const char *path, std::uintmax_t size); void RenameFile(const char *from, const char *to); void CopyFileOverwrite(const char *from, const char *to); diff --git a/Source/utils/paths.h b/Source/utils/paths.h index 406104f5e..639059a74 100644 --- a/Source/utils/paths.h +++ b/Source/utils/paths.h @@ -6,14 +6,6 @@ namespace devilution { -#ifdef _WIN32 -constexpr char DirectorySeparator = '\\'; -#define DIRECTORY_SEPARATOR_STR "\\" -#else -constexpr char DirectorySeparator = '/'; -#define DIRECTORY_SEPARATOR_STR "/" -#endif - namespace paths { const std::string &BasePath(); diff --git a/test/file_util_test.cpp b/test/file_util_test.cpp index 44143263f..edfe5899f 100644 --- a/test/file_util_test.cpp +++ b/test/file_util_test.cpp @@ -61,4 +61,25 @@ TEST(FileUtil, ResizeFile) EXPECT_EQ(size, 30); } +TEST(FileUtil, Dirname) +{ + EXPECT_EQ(Dirname(""), "."); + EXPECT_EQ(Dirname("abc"), "."); + EXPECT_EQ(Dirname(DIRECTORY_SEPARATOR_STR), DIRECTORY_SEPARATOR_STR); + EXPECT_EQ(Dirname(DIRECTORY_SEPARATOR_STR DIRECTORY_SEPARATOR_STR), DIRECTORY_SEPARATOR_STR); + EXPECT_EQ(Dirname("a" DIRECTORY_SEPARATOR_STR "b"), "a"); + EXPECT_EQ(Dirname("a" DIRECTORY_SEPARATOR_STR "b" DIRECTORY_SEPARATOR_STR "c"), + "a" DIRECTORY_SEPARATOR_STR "b"); + EXPECT_EQ(Dirname("a" DIRECTORY_SEPARATOR_STR "b" DIRECTORY_SEPARATOR_STR "c"), + "a" DIRECTORY_SEPARATOR_STR "b"); +} + +TEST(FileUtil, RecursivelyCreateDir) +{ + const std::string path = GetTmpPathName(/*suffix=*/"") + DIRECTORY_SEPARATOR_STR + + "a" + DIRECTORY_SEPARATOR_STR + "b" + DIRECTORY_SEPARATOR_STR + "c"; + RecursivelyCreateDir(path.c_str()); + EXPECT_TRUE(DirectoryExists(path.c_str())); +} + } // namespace