Keep the Sweep Out of the Write Helper and Honour a Read-Only Target

Sweeping the target's directory from inside write_file_atomically() made
every settings export into a user's folder delete their own numbered
files that matched the older config temporary form, and cost a directory
walk per save. The helper writes its target and nothing else; the user
preset scan, the bundle metadata reads and AppConfig::load() sweep the
directories the application owns, once, while they hold the lock.

Replacing a file needs only a writable directory, so a preset or config
the user made read-only was overwritten where the in-place write used to
fail; such a target is refused before anything is written. The CLI's
load_if_exists() takes no lock and creates no lock file, since the CLI
never saves. The lock guard holds the slot mutex through a unique_lock,
so an exception during construction cannot leave the slot locked for
good, and it counts its entry last so a throw leaves the slot as found;
the cool-down after a failed open is set where the failure is seen.

The cloud agent's sync state and secret fallback file and the 3DPrinterOS
session file wrote through a fixed ".tmp" name with a non-Unicode stream;
they call the helper. The vendor cache failure test makes the cache
read-only, which the helper refuses on every platform, and the utility
tests carry the PascalCase tag the test rules ask for.
This commit is contained in:
Hanif Koh
2026-09-24 19:40:26 +08:00
parent 79d7638852
commit a54b0493ce
11 changed files with 87 additions and 83 deletions
+2 -15
View File
@@ -1,9 +1,5 @@
#include <catch2/catch_all.hpp>
#ifndef _WIN32
#include <unistd.h>
#endif
#include <boost/filesystem.hpp>
#include <boost/crc.hpp>
#include <cereal/archives/binary.hpp>
@@ -1360,31 +1356,22 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
}
#ifndef _WIN32
// Permissions are what makes the write fail here, which Windows does not
// express through chmod and root ignores; the helper's own tests cover the rest.
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
{
if (::geteuid() == 0)
SKIP("permissions do not apply to root");
TempDir tmp;
const std::string cache = (tmp.path / "Durable.opc").string();
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
const std::string before = slurp(cache);
// No temp file can be created beside the cache and the cache itself cannot
// be opened for writing: the write cannot complete, and must not have
// destroyed what was already there to find that out.
// A read-only cache (the read-only attribute on Windows) is refused before
// anything is written, so what was there must survive the attempt.
fs::permissions(cache, fs::owner_read | fs::group_read | fs::others_read);
fs::permissions(tmp.path, fs::owner_read | fs::owner_exe | fs::group_read | fs::group_exe | fs::others_read | fs::others_exe);
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
fs::permissions(tmp.path, fs::owner_all | fs::group_read | fs::group_exe | fs::others_read | fs::others_exe);
fs::permissions(cache, fs::owner_read | fs::owner_write | fs::group_read | fs::others_read);
CHECK(slurp(cache) == before);
}
#endif
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
{