mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
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:
@@ -20,7 +20,7 @@
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("per_user_temp_dir composes a per-user temp root", "[utils]") {
|
||||
TEST_CASE("per_user_temp_dir composes a per-user temp root", "[Utils]") {
|
||||
const std::string base = "/tmp";
|
||||
|
||||
SECTION("an empty id returns base unchanged") {
|
||||
@@ -34,7 +34,7 @@ TEST_CASE("per_user_temp_dir composes a per-user temp root", "[utils]") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("per_user_temp_id follows the platform contract", "[utils]") {
|
||||
TEST_CASE("per_user_temp_id follows the platform contract", "[Utils]") {
|
||||
const std::string id = per_user_temp_id();
|
||||
|
||||
SECTION("stable across calls") {
|
||||
@@ -54,7 +54,7 @@ TEST_CASE("per_user_temp_id follows the platform contract", "[utils]") {
|
||||
|
||||
// The end-to-end contract callers depend on: the temp root is left alone on
|
||||
// Windows and isolated per user on Linux/macOS.
|
||||
TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[utils]") {
|
||||
TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[Utils]") {
|
||||
const std::string base = "/tmp";
|
||||
const std::string root = per_user_temp_dir(base, per_user_temp_id());
|
||||
#ifdef _WIN32
|
||||
@@ -65,7 +65,7 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
|
||||
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path target = dir.path() / "preset.json";
|
||||
|
||||
@@ -83,7 +83,7 @@ TEST_CASE("write_file_atomically replaces the target and leaves no temporary fil
|
||||
REQUIRE(entries == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
|
||||
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
|
||||
|
||||
@@ -92,7 +92,22 @@ TEST_CASE("write_file_atomically reports a missing directory and writes nothing"
|
||||
REQUIRE_FALSE(boost::filesystem::exists(target));
|
||||
}
|
||||
|
||||
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
|
||||
TEST_CASE("write_file_atomically refuses a read-only target and leaves it untouched", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path target = dir.path() / "pinned.json";
|
||||
REQUIRE_FALSE(write_file_atomically(target.string(), "pinned"));
|
||||
boost::filesystem::permissions(target, boost::filesystem::owner_read | boost::filesystem::group_read | boost::filesystem::others_read);
|
||||
|
||||
const std::error_code ec = write_file_atomically(target.string(), "replaced");
|
||||
boost::filesystem::permissions(target, boost::filesystem::owner_read | boost::filesystem::owner_write | boost::filesystem::group_read | boost::filesystem::others_read);
|
||||
|
||||
REQUIRE(ec == std::errc::permission_denied);
|
||||
std::string content;
|
||||
load_string_file(target, content);
|
||||
REQUIRE(content == "pinned");
|
||||
}
|
||||
|
||||
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path target = dir.path() / "blob.bin";
|
||||
const std::string bytes("a\r\nb\0c", 6);
|
||||
@@ -102,7 +117,7 @@ TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]")
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
|
||||
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path real = dir.path() / "real.json";
|
||||
const boost::filesystem::path link = dir.path() / "link.json";
|
||||
@@ -123,7 +138,7 @@ TEST_CASE("write_file_atomically writes through a symlink and keeps the target's
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
|
||||
TEST_CASE("write_file_atomically survives two threads writing one target", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
const boost::filesystem::path target = dir.path() / "shared.json";
|
||||
const std::string a(20000, 'a'), b(20000, 'b');
|
||||
@@ -148,7 +163,7 @@ TEST_CASE("write_file_atomically survives two threads writing one target", "[uti
|
||||
REQUIRE(entries == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("remove_stale_temp_files removes only old <name>.<pid>.tmp files", "[utils]") {
|
||||
TEST_CASE("remove_stale_temp_files removes only old <name>.<pid>.tmp files", "[Utils]") {
|
||||
ScopedTemporaryDir dir;
|
||||
for (const char *name : { "a.json.123.tmp", "b.info.4.tmp", "c.json", "d.tmp", "e.json.x.tmp", "f.json..tmp", "a.json.99", "a.json.12.3.tmp" }) {
|
||||
REQUIRE_FALSE(write_file_atomically((dir.path() / name).string(), "x"));
|
||||
@@ -178,7 +193,7 @@ TEST_CASE("remove_stale_temp_files removes only old <name>.<pid>.tmp files", "[u
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
||||
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[Utils]") {
|
||||
ScopedTemporaryFile source(".txt");
|
||||
{
|
||||
std::ofstream ofs(source.string(), std::ios::binary);
|
||||
@@ -207,7 +222,7 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written
|
||||
#endif // _WIN32
|
||||
}
|
||||
|
||||
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") {
|
||||
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[Utils]") {
|
||||
ScopedTemporaryFile model(".3mf");
|
||||
{ std::ofstream out(model.string()); out << "3mf"; }
|
||||
const std::string name = model.path().filename().string();
|
||||
@@ -224,7 +239,7 @@ TEST_CASE("A resolved input path still names the same file after the working dir
|
||||
REQUIRE_FALSE(boost::filesystem::exists(name));
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") {
|
||||
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[Utils]") {
|
||||
ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path());
|
||||
// Read back rather than reusing temp_directory_path(): changing to it resolves any symlink.
|
||||
const boost::filesystem::path here = boost::filesystem::current_path();
|
||||
@@ -240,7 +255,7 @@ TEST_CASE("resolve_cli_input_path completes a relative path against the working
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") {
|
||||
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[Utils]") {
|
||||
SECTION("an absolute path") {
|
||||
const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred();
|
||||
REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string());
|
||||
|
||||
@@ -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]")
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user