#include #include "libslic3r/Utils.hpp" #include "test_utils.hpp" #include #include #include #include #include #ifndef _WIN32 #include // getuid #endif using namespace Slic3r; 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") { REQUIRE(per_user_temp_dir(base, "") == base); } SECTION("a non-empty id is appended at the top level") { REQUIRE(per_user_temp_dir(base, "1000") == base + "/orcaslicer_1000"); } SECTION("distinct ids produce distinct roots") { REQUIRE(per_user_temp_dir(base, "1000") != per_user_temp_dir(base, "1001")); } } TEST_CASE("per_user_temp_id follows the platform contract", "[utils]") { const std::string id = per_user_temp_id(); SECTION("stable across calls") { REQUIRE(per_user_temp_id() == id); } #ifdef _WIN32 SECTION("empty on Windows (its temp dir is already per-user)") { REQUIRE(id.empty()); } #else SECTION("the current uid on Linux/macOS") { REQUIRE_FALSE(id.empty()); REQUIRE(id == std::to_string(static_cast(::getuid()))); } #endif } // 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]") { const std::string base = "/tmp"; const std::string root = per_user_temp_dir(base, per_user_temp_id()); #ifdef _WIN32 REQUIRE(root == base); #else REQUIRE(root != base); REQUIRE_THAT(root, Catch::Matchers::StartsWith(base + "/orcaslicer_")); #endif } 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); ofs << "orca"; } REQUIRE(boost::filesystem::exists(source.path())); // A directory that was never created, so the copy fails on every platform. const boost::filesystem::path destination = source.path().parent_path() / "orca-missing-dir" / "copy.txt"; REQUIRE_FALSE(boost::filesystem::exists(destination.parent_path())); std::string error_message; REQUIRE(copy_file(source.string(), destination.string(), error_message) == FAIL_COPY_FILE); REQUIRE_FALSE(error_message.empty()); #ifdef _WIN32 // The Windows branch formats GetLastError() itself. Writing that as // "Error: " + errCode adds an integer to a string literal, which indexes into the // literal instead of appending and runs off its end for any code above 7. const std::string prefix = "Error: "; REQUIRE(error_message.rfind(prefix, 0) == 0); const std::string code = error_message.substr(prefix.size()); REQUIRE_FALSE(code.empty()); REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; })); #endif // _WIN32 } 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(); // Resolve the bare name from the directory holding the file, then move away from it. The guard // restores the directory the test started in, wherever this leaves it. ScopedWorkingDirectory cwd(model.path().parent_path()); const std::string resolved = resolve_cli_input_path(name); boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR)); REQUIRE(boost::filesystem::exists(resolved)); REQUIRE(boost::filesystem::equivalent(resolved, model.path())); // Control: the bare name finds nothing from here, so resolving it this late would have failed. REQUIRE_FALSE(boost::filesystem::exists(name)); } 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(); SECTION("a bare name") { REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string()); } SECTION("a ./ prefix is dropped") { REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string()); } SECTION("a ../ traversal is collapsed") { REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string()); } } 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()); } #ifdef _WIN32 // Every absolute form Windows accepts opens today, so each must come back byte for byte: // normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes. SECTION("an absolute Windows path of any form") { for (const std::string absolute : {R"(C:\models\model.3mf)", R"(C:/models/model.3mf)", R"(\\server\share\model.3mf)", R"(\\?\C:\models\model.3mf)"}) REQUIRE(resolve_cli_input_path(absolute) == absolute); } #endif // These are downloaded rather than opened, and completing one would produce a path, not a URL. SECTION("a custom open protocol URL") { for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf", "prusaslicer://open/?file=https://example.com/model.3mf", "bambustudio://open/?file=https://example.com/model.3mf", "cura://open/?file=https://example.com/model.3mf"}) REQUIRE(resolve_cli_input_path(url) == url); } SECTION("an empty argument") { REQUIRE(resolve_cli_input_path("").empty()); } } TEST_CASE("sanitize_file_basename keeps only a plain file name from an untrusted name", "[Utils]") { const std::string unicode = "\xe6\xa8\xa1\xe5\x9e\x8b \xc3\xa9t\xc3\xa9.3mf"; // UTF-8 CJK and accented Latin const auto [input, expected] = GENERATE_COPY(table({ {"normal.3mf", "normal.3mf"}, {"../../x.3mf", "x.3mf"}, {"..\\..\\x.3mf", "x.3mf"}, {"C:\\x.3mf", "x.3mf"}, {"C:x.3mf", "C_x.3mf"}, {"/etc/x", "x"}, {"a/b\\c.gcode", "c.gcode"}, {"x:stream", "x_stream"}, // no NTFS alternate data stream {"x.", "x."}, {".3mf", ".3mf"}, {unicode, unicode}, })); CAPTURE(input); CHECK(sanitize_file_basename(input) == expected); } TEST_CASE("sanitize_file_basename rejects names that do not name a file", "[Utils]") { const std::string input = GENERATE(as{}, "", ".", "..", "../..", "dir/", "..\\", " ", ". .", "..."); CAPTURE(input); CHECK(sanitize_file_basename(input).empty()); } namespace { void touch(const boost::filesystem::path &path) { std::ofstream(path.string()) << "existing"; } std::string file_contents(const boost::filesystem::path &path) { std::ifstream file(path.string()); return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); } } // namespace TEST_CASE("find_unused_filename keeps a name nothing uses", "[Utils]") { ScopedTemporaryDir dir; std::string name; REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model.3mf"); } TEST_CASE("find_unused_filename versions a name an existing file uses", "[Utils]") { ScopedTemporaryDir dir; touch(dir.path() / "model.3mf"); std::string name; REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model(1).3mf"); } TEST_CASE("find_unused_filename versions a name that maps onto an existing file once sanitized", "[Utils]") { ScopedTemporaryDir dir; touch(dir.path() / "my_model.3mf"); const std::string input = GENERATE(as{}, "my?model.3mf", "my:model.3mf", "my*model.3mf"); CAPTURE(input); std::string name; REQUIRE(find_unused_filename(dir.path(), input, {}, name)); CHECK(name == "my_model(1).3mf"); CHECK(file_contents(dir.path() / "my_model.3mf") == "existing"); } TEST_CASE("find_unused_filename treats the marker of another download as used", "[Utils]") { ScopedTemporaryDir dir; touch(download_marker_path(dir.path(), "model.3mf")); std::string name; REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model(1).3mf"); } TEST_CASE("find_unused_filename ignores the marker of the download asking", "[Utils]") { ScopedTemporaryDir dir; const boost::filesystem::path own_marker = download_marker_path(dir.path(), "model.3mf"); touch(own_marker); std::string name; REQUIRE(find_unused_filename(dir.path(), "model.3mf", own_marker, name)); CHECK(name == "model.3mf"); } TEST_CASE("find_unused_filename gives up after 999 versions", "[Utils]") { ScopedTemporaryDir dir; touch(dir.path() / "model.3mf"); for (int version = 1; version < 999; ++version) touch(dir.path() / ("model(" + std::to_string(version) + ").3mf")); std::string name; REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model(999).3mf"); touch(dir.path() / name); REQUIRE_FALSE(find_unused_filename(dir.path(), "model.3mf", {}, name)); CHECK(name == "model(999).3mf"); } TEST_CASE("is_path_within_root accepts a root given with a trailing separator", "[utils]") { ScopedTemporaryDir tmp; const std::string root = tmp.path().string(); const std::string with_separator = GENERATE_COPY(root + "/", root + std::string(1, static_cast(boost::filesystem::path::preferred_separator))); CAPTURE(with_separator); CHECK(is_path_within_root("vendor.json", with_separator)); CHECK(is_path_within_root("vendor/machine/printer.json", with_separator)); CHECK_FALSE(is_path_within_root("../vendor.json", with_separator)); } TEST_CASE("is_path_within_root treats Windows-specific name forms the same on every platform", "[utils]") { ScopedTemporaryDir tmp; SECTION("names ending in dots or spaces stay inside the root") { const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("dir./file.json"), std::string("dir /file.json")); CAPTURE(name); CHECK(is_path_within_root(name, tmp.path())); } SECTION("drive-relative names are rejected") { const std::string name = GENERATE(std::string("C:x"), std::string("c:x/y.json"), std::string("C:")); CAPTURE(name); CHECK_FALSE(is_path_within_root(name, tmp.path())); } } TEST_CASE("is_path_within_root rejects a name with an embedded NUL", "[utils]") { ScopedTemporaryDir tmp; // The filesystem calls stop at the NUL, so they would act on a different path than the one checked. const std::string name = GENERATE(std::string("..\0", 3), std::string("..\0x/file.json", 14), std::string("sub/..\0x", 8), std::string("file.json\0", 10), std::string("\0file.json", 10)); CAPTURE(name.size()); CHECK_FALSE(is_path_within_root(name, tmp.path())); } TEST_CASE("is_symlink_target_within_root accepts relative targets that stay inside the root", "[utils]") { ScopedTemporaryDir tmp; const auto [link, target] = GENERATE(std::make_pair(std::string("Versions/Current"), std::string("A")), std::make_pair(std::string("Foo.framework/Foo"), std::string("Versions/Current/Foo")), std::make_pair(std::string("libfoo.so"), std::string("libfoo.so.1")), std::make_pair(std::string("a/b/link"), std::string("c/d"))); CAPTURE(link, target); CHECK(is_symlink_target_within_root(link, target, tmp.path())); } TEST_CASE("is_symlink_target_within_root rejects absolute targets and targets that climb out", "[utils]") { ScopedTemporaryDir tmp; const std::string outside = (tmp.path().parent_path() / "outside").generic_string(); const auto [link, target] = GENERATE_COPY(std::make_pair(std::string("sub/link"), outside), std::make_pair(std::string("sub/link"), std::string("/etc/passwd")), std::make_pair(std::string("sub/link"), std::string("\\outside")), std::make_pair(std::string("sub/link"), std::string("C:/outside")), std::make_pair(std::string("sub/link"), std::string("C:outside")), std::make_pair(std::string("sub/link"), std::string("")), std::make_pair(std::string("link"), std::string("..")), std::make_pair(std::string("link"), std::string("../outside")), std::make_pair(std::string("sub/link"), std::string("../../outside")), std::make_pair(std::string("sub/link"), std::string("x/../../../outside")), std::make_pair(std::string("sub/link"), std::string("..\\..\\outside")), // symlink() stops at the NUL, so this target would be created as "..". std::make_pair(std::string("link"), std::string("..\0", 3))); CAPTURE(link, target); CHECK_FALSE(is_symlink_target_within_root(link, target, tmp.path())); } #ifndef _WIN32 TEST_CASE("is_symlink_target_within_root rejects a target that passes through a symlink leading out", "[utils]") { ScopedTemporaryDir tmp; const boost::filesystem::path root = tmp.path() / "root"; const boost::filesystem::path outside = tmp.path() / "outside"; boost::filesystem::create_directories(root); boost::filesystem::create_directories(outside); boost::filesystem::create_symlink(outside, root / "out"); CHECK_FALSE(is_symlink_target_within_root("link", "out/lib.so", root)); CHECK(is_symlink_target_within_root("link", "in/lib.so", root)); } #endif