diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 7f95f6113b..4ae316a055 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include #include #include @@ -5076,39 +5075,39 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _BBS_3MF_Importer::_generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects) { - // A chain of component references longer than the number of objects has to visit an object - // twice, so the component graph contains a cycle and the expansion below would not stop. - const size_t max_depth = current_objects.size(); - // An acyclic graph may still expand exponentially, so bound the number of expanded components - // as well. Way above the number of parts of any real object. + // A cycle in the component graph would expand forever, and an acyclic graph can still expand + // exponentially, so bound the number of component references queued. Checking before they are + // queued bounds the work list itself, whatever the fan-out. A valid file over the budget is + // rejected too, but the budget is way above the component references of any real object. static constexpr size_t max_components = 100000; - std::list> id_list; - id_list.push_back(std::make_tuple(Component(object_id, Transform3d::Identity()), Transform3d::Identity(), 0)); + std::list> id_list; + id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity())); size_t num_components = 0; while (!id_list.empty()) { auto current_item = id_list.front(); - Component current_id = std::get<0>(current_item); + Component current_id = current_item.first; id_list.pop_front(); - if (std::get<2>(current_item) > max_depth || ++ num_components > max_components) { - add_error("invalid 3mf: cyclic or too deeply nested components"); - sub_objects.clear(); - return false; - } IdToCurrentObjectMap::iterator current_object = current_objects.find(current_id.object_id); if (current_object != current_objects.end()) { //found one if (!current_object->second.components.empty()) { + num_components += current_object->second.components.size(); + if (num_components > max_components) { + add_error("invalid 3mf: cyclic or too many component references"); + sub_objects.clear(); + return false; + } for (const Component &comp : current_object->second.components) { - id_list.push_back(std::make_tuple(comp, std::get<1>(current_item) * comp.transform, std::get<2>(current_item) + 1)); + id_list.push_back(std::pair(comp, current_item.second * comp.transform)); } } else if (!(current_object->second.geometry.empty())) { //CurrentObject* ptr = &(current_objects[current_id]); //CurrentObject* ptr2 = &(current_object->second); - sub_objects.push_back({ current_object->first, std::get<1>(current_item)}); + sub_objects.push_back({ current_object->first, current_item.second}); } } } diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index be88acd5dd..8439e8f846 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -418,18 +418,29 @@ TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]" TEST_CASE("A project whose components reference themselves fails to load", "[3mf][Regression]") { + // One self-reference keeps the expansion going without ever reaching a mesh. A thousand also make + // each expansion queue a thousand more, so the bound has to hold the work list, not just the loop. + const int references = GENERATE(1, 1000); + INFO("self-references " << references); + ScopedTemporaryFile temp(".3mf"); store_painted_cube(temp.string()); - // Point the component back at the object that holds it. Expanding that reference used to push - // into the work list forever, growing it until the process ran out of memory. - REQUIRE(rewrite_3mf_entries(temp.string(), [](std::string& name, std::string& data) { + // Point the component back at the object that holds it, repeated `references` times. + REQUIRE(rewrite_3mf_entries(temp.string(), [references](std::string& name, std::string& data) { if (!boost::algorithm::ends_with(name, "3dmodel.model")) return false; std::smatch match; if (!std::regex_search(data, match, std::regex("]*>\\s*]*/>"))) + return false; + std::string repeated; + for (int i = 0; i < references; ++i) + repeated += component.str(); + data.replace(component.position(), component.length(), repeated); return true; })); @@ -440,6 +451,33 @@ TEST_CASE("A project whose components reference themselves fails to load", "[3mf REQUIRE_FALSE(loaded); } +TEST_CASE("An object loads up to the component reference budget and fails past it", "[3mf][Regression]") +{ + // The importer queues at most 100000 component references per object. Every reference besides the + // cube's own points at an object the file does not define: it counts toward the budget, then expands + // to nothing, so the object stays a single part whatever the count. + const auto [references, loads] = GENERATE(table({ { 100000, true }, { 100001, false } })); + INFO("component references " << references); + + ScopedTemporaryFile temp(".3mf"); + store_painted_cube(temp.string()); + + std::string dangling; + for (int i = 1; i < references; ++i) + dangling += ""; + REQUIRE(replace_in_3mf_entry(temp.string(), "3dmodel.model", "", dangling + "")); + + ScopedTemporaryDir backup_dir("orca_budget_dst"); + Model model; + bool loaded = !loads; + REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir)); + REQUIRE(loaded == loads); + if (loads) { + REQUIRE(model.objects.size() == 1); + CHECK(model.objects.front()->volumes.size() == 1); + } +} + TEST_CASE("A project with malformed paint data loads without the damaged facet", "[3mf][Regression]") { ScopedTemporaryFile temp(".3mf");