diff --git a/src/core/util.cpp b/src/core/util.cpp index 4a2f529..0ab3869 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -175,15 +175,34 @@ std::string readFile(const std::string& path, bool* ok) { } bool writeFile(const std::string& path, std::string_view data) { - std::filesystem::path p(path); - if (p.has_parent_path()) { - std::error_code ec; - std::filesystem::create_directories(p.parent_path(), ec); + std::filesystem::path target(path); + std::error_code ec; + if (target.has_parent_path()) std::filesystem::create_directories(target.parent_path(), ec); + + auto writeTo = [data](const std::filesystem::path& file) { + std::ofstream out(file, std::ios::binary | std::ios::trunc); + if (!out) return false; + out.write(data.data(), static_cast(data.size())); + out.close(); + return !out.fail(); + }; + + // Written beside the target and moved over it in one step, so a crash mid-write leaves + // the old file whole instead of cut short. + std::filesystem::path temp = target; + temp += ".tmp"; + if (writeTo(temp)) { +#ifdef _WIN32 + if (MoveFileExW(temp.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING)) return true; +#else + std::filesystem::rename(temp, target, ec); + if (!ec) return true; +#endif } - std::ofstream out(path, std::ios::binary | std::ios::trunc); - if (!out) return false; - out.write(data.data(), static_cast(data.size())); - return out.good(); + std::filesystem::remove(temp, ec); + // Something is holding the target in a way that blocks the move; a direct write still + // beats dropping the save. + return writeTo(target); } std::string executableDir() { diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 51df34e..20faaf1 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -311,6 +311,15 @@ void testLogFormat() { "the banner names its rows"); } +void testSafeWrite() { + const std::string path = "build/test-safe-write.json"; + check(util::writeFile(path, "first"), "a file writes"); + check(util::writeFile(path, "second, and longer"), "and overwrites"); + bool ok = false; + check(util::readFile(path, &ok) == "second, and longer" && ok, "with the new content whole"); + check(!util::fileExists(path + ".tmp"), "leaving no temp file behind"); +} + } // namespace int main() { @@ -324,6 +333,7 @@ int main() { testLineupCmdIds(); testIdleTimeout(); testLogFormat(); + testSafeWrite(); testFillPresence(); testLargeMessage(); testAgainstProtoc();