diff --git a/dCommon/Lxfml.cpp b/dCommon/Lxfml.cpp index 7187cd8f..6cc450ac 100644 --- a/dCommon/Lxfml.cpp +++ b/dCommon/Lxfml.cpp @@ -32,10 +32,18 @@ namespace { Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) { Result toReturn; + + // Handle empty or invalid input + if (data.empty()) { + return toReturn; + } + + // Ensure null-terminated string for tinyxml2::Parse + std::string nullTerminatedData(data); + tinyxml2::XMLDocument doc; - const auto err = doc.Parse(data.data()); + const auto err = doc.Parse(nullTerminatedData.c_str()); if (err != tinyxml2::XML_SUCCESS) { - LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); return toReturn; } @@ -44,7 +52,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin auto lxfml = reader["LXFML"]; if (!lxfml) { - LOG("Failed to find LXFML element."); return toReturn; } @@ -73,16 +80,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin // Calculate the lowest and highest points on the entire model for (const auto& transformation : transformations | std::views::values) { auto split = GeneralUtils::SplitString(transformation, ','); - if (split.size() < 12) { - LOG("Not enough in the split?"); - continue; - } - - auto x = GeneralUtils::TryParse(split[9]).value(); - auto y = GeneralUtils::TryParse(split[10]).value(); - auto z = GeneralUtils::TryParse(split[11]).value(); - if (x < lowest.x) lowest.x = x; - if (y < lowest.y) lowest.y = y; + if (split.size() < 12) continue; + + auto xOpt = GeneralUtils::TryParse(split[9]); + auto yOpt = GeneralUtils::TryParse(split[10]); + auto zOpt = GeneralUtils::TryParse(split[11]); + + if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue; + + auto x = xOpt.value(); + auto y = yOpt.value(); + auto z = zOpt.value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; if (z < lowest.z) lowest.z = z; if (highest.x < x) highest.x = x; @@ -111,13 +121,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin for (auto& transformation : transformations | std::views::values) { auto split = GeneralUtils::SplitString(transformation, ','); if (split.size() < 12) { - LOG("Not enough in the split?"); continue; } - auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x + curPosition.x; - auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y + curPosition.y; - auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z + curPosition.z; + auto xOpt = GeneralUtils::TryParse(split[9]); + auto yOpt = GeneralUtils::TryParse(split[10]); + auto zOpt = GeneralUtils::TryParse(split[11]); + + if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) { + continue; + } + auto x = xOpt.value() - newRootPos.x + curPosition.x; + auto y = yOpt.value() - newRootPos.y + curPosition.y; + auto z = zOpt.value() - newRootPos.z + curPosition.z; std::stringstream stream; for (int i = 0; i < 9; i++) { stream << split[i]; @@ -181,16 +197,29 @@ static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, t std::vector Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) { std::vector results; + + // Handle empty or invalid input + if (data.empty()) { + return results; + } + + // Prevent processing extremely large inputs that could cause hangs + if (data.size() > 10000000) { // 10MB limit + return results; + } + + // Ensure null-terminated string for tinyxml2::Parse + // string_view::data() may not be null-terminated, causing undefined behavior + std::string nullTerminatedData(data); + tinyxml2::XMLDocument doc; - const auto err = doc.Parse(data.data()); + const auto err = doc.Parse(nullTerminatedData.c_str()); if (err != tinyxml2::XML_SUCCESS) { - LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); return results; } auto* lxfml = doc.FirstChildElement("LXFML"); if (!lxfml) { - LOG("Failed to find LXFML element."); return results; } @@ -284,7 +313,13 @@ std::vector Lxfml::Split(const std::string_view data, const NiPoi tinyxml2::XMLPrinter printer; outDoc.Print(&printer); // Normalize position and compute center using existing helper - auto normalized = NormalizePosition(printer.CStr(), curPosition); + std::string xmlString = printer.CStr(); + if (xmlString.size() > 5000000) { // 5MB limit for normalization + Result emptyResult; + emptyResult.lxfml = xmlString; + return emptyResult; + } + auto normalized = NormalizePosition(xmlString, curPosition); return normalized; }; @@ -324,8 +359,11 @@ std::vector Lxfml::Split(const std::string_view data, const NiPoi // Iteratively include any RigidSystems that reference any boneRefsIncluded bool changed = true; std::vector rigidSystemsToInclude; - while (changed) { + int maxIterations = 1000; // Safety limit to prevent infinite loops + int iteration = 0; + while (changed && iteration < maxIterations) { changed = false; + iteration++; for (auto* rs : rigidSystems) { if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue; // parse boneRefs of this rigid system (from its children) @@ -357,7 +395,12 @@ std::vector Lxfml::Split(const std::string_view data, const NiPoi } } } - + + if (iteration >= maxIterations) { + // Iteration limit reached, stop processing to prevent infinite loops + // The file is likely malformed, so just skip further processing + return results; + } // include bricks from bricksIncluded into used set for (const auto& b : bricksIncluded) usedBrickRefs.insert(b); diff --git a/tests/dCommonTests/CMakeLists.txt b/tests/dCommonTests/CMakeLists.txt index ca5cd132..e1311f1d 100644 --- a/tests/dCommonTests/CMakeLists.txt +++ b/tests/dCommonTests/CMakeLists.txt @@ -10,7 +10,7 @@ set(DCOMMONTEST_SOURCES "TestLUString.cpp" "TestLUWString.cpp" "dCommonDependencies.cpp" - "LxfmlSplitTests.cpp" + "LxfmlTests.cpp" ) add_subdirectory(dEnumsTests) diff --git a/tests/dCommonTests/LxfmlSplitTests.cpp b/tests/dCommonTests/LxfmlSplitTests.cpp deleted file mode 100644 index ba3288ef..00000000 --- a/tests/dCommonTests/LxfmlSplitTests.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include "gtest/gtest.h" - -#include "Lxfml.h" -#include "TinyXmlUtils.h" - -#include -#include -#include -#include - -using namespace TinyXmlUtils; - -static std::string ReadFile(const std::string& path) { - std::ifstream in(path, std::ios::in | std::ios::binary); - std::ostringstream ss; - ss << in.rdbuf(); - return ss.str(); -} - -TEST(LxfmlSplitTests, SplitUsesAllBricksAndNoDuplicates) { - // Read the sample test.lxfml included in tests. Resolve path relative to this source file. - std::filesystem::path srcDir = std::filesystem::path(__FILE__).parent_path(); - std::filesystem::path filePath = srcDir / "test.lxfml"; - std::ifstream in(filePath, std::ios::in | std::ios::binary); - std::ostringstream ss; - ss << in.rdbuf(); - std::string data = ss.str(); - ASSERT_FALSE(data.empty()) << "Failed to read " << filePath.string(); - - - auto results = Lxfml::Split(data); - ASSERT_GT(results.size(), 0); - - // Write split outputs to disk for manual inspection - std::filesystem::path outDir = srcDir / "lxfml_splits"; - std::error_code ec; - std::filesystem::create_directories(outDir, ec); - for (size_t i = 0; i < results.size(); ++i) { - auto outPath = outDir / ("split_" + std::to_string(i) + ".lxfml"); - std::ofstream ofs(outPath, std::ios::out | std::ios::binary); - ASSERT_TRUE(ofs) << "Failed to open output file: " << outPath.string(); - ofs << results[i].lxfml; - ofs.close(); - } - - // parse original to count bricks - tinyxml2::XMLDocument doc; - ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS); - DocumentReader reader(doc); - auto lxfml = reader["LXFML"]; - ASSERT_TRUE(lxfml); - - // Collect original RigidSystems and Groups (serialize each element string) - auto serializeElement = [](tinyxml2::XMLElement* elem) { - tinyxml2::XMLPrinter p; - elem->Accept(&p); - return std::string(p.CStr()); - }; - - std::unordered_set originalRigidSet; - if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { - for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { - originalRigidSet.insert(serializeElement(rs)); - } - } - - std::unordered_set originalGroupSet; - if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { - for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { - for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { - // collect this group and nested groups - std::function collectGroups = [&](tinyxml2::XMLElement* grp) { - originalGroupSet.insert(serializeElement(grp)); - for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child); - }; - collectGroups(g); - } - } - } - - std::unordered_set originalBricks; - for (const auto& brick : lxfml["Bricks"]) { - const auto* ref = brick.Attribute("refID"); - if (ref) originalBricks.insert(ref); - } - ASSERT_GT(originalBricks.size(), 0); - - // Collect bricks across all results and ensure no duplicates and all used - std::unordered_set usedBricks; - // Track used rigid systems and groups (serialized strings) - std::unordered_set usedRigidSet; - std::unordered_set usedGroupSet; - for (const auto& res : results) { - tinyxml2::XMLDocument outDoc; - ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS); - DocumentReader outReader(outDoc); - auto outLxfml = outReader["LXFML"]; - ASSERT_TRUE(outLxfml); - // collect rigid systems in this output - if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { - for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { - auto s = serializeElement(rs); - // no duplicate allowed across outputs - ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits"; - usedRigidSet.insert(s); - } - } - // collect groups in this output - if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { - for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { - for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { - std::function collectGroupsOut = [&](tinyxml2::XMLElement* grp) { - auto s = serializeElement(grp); - ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits"; - usedGroupSet.insert(s); - for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child); - }; - collectGroupsOut(g); - } - } - } - for (const auto& brick : outLxfml["Bricks"]) { - const auto* ref = brick.Attribute("refID"); - if (ref) { - // no duplicate allowed - ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref; - usedBricks.insert(ref); - } - } - } - - // Every original brick must be used in one of the outputs - for (const auto& bref : originalBricks) { - ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref; - } - - // And usedBricks should not contain anything outside original - for (const auto& ub : usedBricks) { - ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub; - } - - // Ensure all original rigid systems and groups were used exactly once - ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch"; - for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits"; - - ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch"; - for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits"; -} diff --git a/tests/dCommonTests/test.lxfml b/tests/dCommonTests/LxfmlTestFiles/test.lxfml similarity index 100% rename from tests/dCommonTests/test.lxfml rename to tests/dCommonTests/LxfmlTestFiles/test.lxfml diff --git a/tests/dCommonTests/LxfmlTests.cpp b/tests/dCommonTests/LxfmlTests.cpp new file mode 100644 index 00000000..e366679e --- /dev/null +++ b/tests/dCommonTests/LxfmlTests.cpp @@ -0,0 +1,298 @@ +#include "gtest/gtest.h" + +#include "Lxfml.h" +#include "TinyXmlUtils.h" +#include "dCommonDependencies.h" + +#include +#include +#include +#include + +using namespace TinyXmlUtils; + +static std::string ReadFile(const std::string& path) { + std::ifstream in(path, std::ios::in | std::ios::binary); + std::ostringstream ss; + ss << in.rdbuf(); + return ss.str(); +} + +TEST(LxfmlTests, SplitUsesAllBricksAndNoDuplicates) { + // Read the sample test.lxfml included in tests. Resolve path relative to this source file. + std::filesystem::path srcDir = std::filesystem::path(__FILE__).parent_path(); + std::filesystem::path filePath = srcDir / "LxfmlTestFiles" / "test.lxfml"; + std::ifstream in(filePath, std::ios::in | std::ios::binary); + std::ostringstream ss; + ss << in.rdbuf(); + std::string data = ss.str(); + ASSERT_FALSE(data.empty()) << "Failed to read " << filePath.string(); + + + auto results = Lxfml::Split(data); + ASSERT_GT(results.size(), 0); + + // Write split outputs to disk for manual inspection + std::filesystem::path outDir = srcDir / "LxfmlTestFiles" / "lxfml_splits"; + std::error_code ec; + std::filesystem::create_directories(outDir, ec); + for (size_t i = 0; i < results.size(); ++i) { + auto outPath = outDir / ("split_" + std::to_string(i) + ".lxfml"); + std::ofstream ofs(outPath, std::ios::out | std::ios::binary); + ASSERT_TRUE(ofs) << "Failed to open output file: " << outPath.string(); + ofs << results[i].lxfml; + ofs.close(); + } + + // parse original to count bricks + tinyxml2::XMLDocument doc; + ASSERT_EQ(doc.Parse(data.c_str()), tinyxml2::XML_SUCCESS); + DocumentReader reader(doc); + auto lxfml = reader["LXFML"]; + ASSERT_TRUE(lxfml); + + // Collect original RigidSystems and Groups (serialize each element string) + auto serializeElement = [](tinyxml2::XMLElement* elem) { + tinyxml2::XMLPrinter p; + elem->Accept(&p); + return std::string(p.CStr()); + }; + + std::unordered_set originalRigidSet; + if (auto* rsParent = doc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { + for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { + originalRigidSet.insert(serializeElement(rs)); + } + } + + std::unordered_set originalGroupSet; + if (auto* gsParent = doc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { + for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { + for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { + // collect this group and nested groups + std::function collectGroups = [&](tinyxml2::XMLElement* grp) { + originalGroupSet.insert(serializeElement(grp)); + for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroups(child); + }; + collectGroups(g); + } + } + } + + std::unordered_set originalBricks; + for (const auto& brick : lxfml["Bricks"]) { + const auto* ref = brick.Attribute("refID"); + if (ref) originalBricks.insert(ref); + } + ASSERT_GT(originalBricks.size(), 0); + + // Collect bricks across all results and ensure no duplicates and all used + std::unordered_set usedBricks; + // Track used rigid systems and groups (serialized strings) + std::unordered_set usedRigidSet; + std::unordered_set usedGroupSet; + for (const auto& res : results) { + tinyxml2::XMLDocument outDoc; + ASSERT_EQ(outDoc.Parse(res.lxfml.c_str()), tinyxml2::XML_SUCCESS); + DocumentReader outReader(outDoc); + auto outLxfml = outReader["LXFML"]; + ASSERT_TRUE(outLxfml); + // collect rigid systems in this output + if (auto* rsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("RigidSystems")) { + for (auto* rs = rsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) { + auto s = serializeElement(rs); + // no duplicate allowed across outputs + ASSERT_EQ(usedRigidSet.find(s), usedRigidSet.end()) << "Duplicate RigidSystem across splits"; + usedRigidSet.insert(s); + } + } + // collect groups in this output + if (auto* gsParent = outDoc.FirstChildElement("LXFML")->FirstChildElement("GroupSystems")) { + for (auto* gs = gsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) { + for (auto* g = gs->FirstChildElement("Group"); g; g = g->NextSiblingElement("Group")) { + std::function collectGroupsOut = [&](tinyxml2::XMLElement* grp) { + auto s = serializeElement(grp); + ASSERT_EQ(usedGroupSet.find(s), usedGroupSet.end()) << "Duplicate Group across splits"; + usedGroupSet.insert(s); + for (auto* child = grp->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectGroupsOut(child); + }; + collectGroupsOut(g); + } + } + } + for (const auto& brick : outLxfml["Bricks"]) { + const auto* ref = brick.Attribute("refID"); + if (ref) { + // no duplicate allowed + ASSERT_EQ(usedBricks.find(ref), usedBricks.end()) << "Duplicate brick ref across splits: " << ref; + usedBricks.insert(ref); + } + } + } + + // Every original brick must be used in one of the outputs + for (const auto& bref : originalBricks) { + ASSERT_NE(usedBricks.find(bref), usedBricks.end()) << "Brick not used in splits: " << bref; + } + + // And usedBricks should not contain anything outside original + for (const auto& ub : usedBricks) { + ASSERT_NE(originalBricks.find(ub), originalBricks.end()) << "Split produced unknown brick: " << ub; + } + + // Ensure all original rigid systems and groups were used exactly once + ASSERT_EQ(originalRigidSet.size(), usedRigidSet.size()) << "RigidSystem count mismatch"; + for (const auto& s : originalRigidSet) ASSERT_NE(usedRigidSet.find(s), usedRigidSet.end()) << "RigidSystem missing in splits"; + + ASSERT_EQ(originalGroupSet.size(), usedGroupSet.size()) << "Group count mismatch"; + for (const auto& s : originalGroupSet) ASSERT_NE(usedGroupSet.find(s), usedGroupSet.end()) << "Group missing in splits"; +} + +// Tests for invalid input handling - now working with the improved Split function + +TEST(LxfmlTests, InvalidLxfmlHandling) { + // Test LXFML with invalid transformation matrices + std::string invalidTransformData = R"( + + + + + + + + + + + + + + + + + + +)"; + + // The Split function should handle invalid transformation matrices gracefully + std::vector results; + EXPECT_NO_FATAL_FAILURE({ + results = Lxfml::Split(invalidTransformData); + }) << "Split should not crash on invalid transformation matrices"; + + // Function should handle invalid transforms gracefully, possibly returning empty or partial results + // The exact behavior depends on how the function handles invalid numeric parsing +} + +TEST(LxfmlTests, EmptyLxfmlHandling) { + // Test with completely empty input + std::string emptyData = ""; + std::vector results; + + EXPECT_NO_FATAL_FAILURE({ + results = Lxfml::Split(emptyData); + }) << "Split should not crash on empty input"; + + EXPECT_EQ(results.size(), 0) << "Empty input should return empty results"; +} + +TEST(LxfmlTests, InvalidTransformHandling) { + // Test with various types of invalid transformation matrices + std::vector invalidTransformTests = { + // LXFML with empty transformation + R"()", + + // LXFML with too few transformation values (needs 12, has 6) + R"()", + + // LXFML with non-numeric transformation values + R"()", + + // LXFML with mixed valid/invalid transformation values + R"()", + + // LXFML with no Bricks section (should return empty gracefully) + R"()" + }; + + for (size_t i = 0; i < invalidTransformTests.size(); ++i) { + std::vector results; + EXPECT_NO_FATAL_FAILURE({ + results = Lxfml::Split(invalidTransformTests[i]); + }) << "Split should not crash on invalid transform test case " << i; + + // The function should handle invalid transforms gracefully + // May return empty results or skip invalid bricks + } +} + +TEST(LxfmlTests, MixedValidInvalidTransformsHandling) { + // Test LXFML with mix of valid and invalid transformation data + std::string mixedValidData = R"( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)"; + + // The Split function should handle mixed valid/invalid transforms gracefully + std::vector results; + EXPECT_NO_FATAL_FAILURE({ + results = Lxfml::Split(mixedValidData); + }) << "Split should not crash on mixed valid/invalid transforms"; + + // Should process valid bricks and handle invalid ones gracefully + if (results.size() > 0) { + EXPECT_NO_FATAL_FAILURE({ + for (size_t i = 0; i < results.size(); ++i) { + // Each result should have valid LXFML structure + tinyxml2::XMLDocument doc; + auto parseResult = doc.Parse(results[i].lxfml.c_str()); + EXPECT_EQ(parseResult, tinyxml2::XML_SUCCESS) + << "Result " << i << " should produce valid XML"; + + if (parseResult == tinyxml2::XML_SUCCESS) { + auto* lxfml = doc.FirstChildElement("LXFML"); + EXPECT_NE(lxfml, nullptr) << "Result " << i << " should have LXFML root element"; + } + } + }) << "Mixed valid/invalid transform processing should not cause fatal errors"; + } +}