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