#include #include #include #include using namespace yams::extraction; namespace fs = std::filesystem; TEST_CASE("HtmlTextExtractor - No regex crashes on large files", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; SECTION("Very large HTML file does not crash") { // Create a large HTML file with nested tags (~3.8 MB) std::string largeHtml = ""; for (int i = 0; i < 100000; i++) { largeHtml += "

Test paragraph " + std::to_string(i) + "

"; } largeHtml += ""; REQUIRE(largeHtml.size() > 3 * 1024 * 1024); // > 3MB // This should not crash (previously would with regex) auto result = extractor.extractFromBuffer( std::as_bytes(std::span(largeHtml.data(), largeHtml.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().success); REQUIRE_FALSE(result.value().text.empty()); } SECTION("Pathological nested HTML") { // Deep nesting that causes regex backtracking std::string nested = ""; for (int i = 0; i < 1000; i++) { nested += "
"; } nested += "Content"; for (int i = 0; i < 1000; i++) { nested += "
"; } nested += ""; auto result = extractor.extractFromBuffer( std::as_bytes(std::span(nested.data(), nested.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().success); REQUIRE(result.value().text.find("Content") != std::string::npos); } SECTION("Malformed HTML with unclosed tags") { std::string malformed = "

Test

Another

Third"; auto result = extractor.extractFromBuffer( std::as_bytes(std::span(malformed.data(), malformed.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().success); REQUIRE(result.value().text.find("Test") != std::string::npos); } } TEST_CASE("HtmlTextExtractor - Block tag conversion without regex", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; SECTION("Block tags converted to newlines") { std::string html = "

Title

Para 1

Para 2

"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Title") != std::string::npos); REQUIRE(result.value().text.find("Para 1") != std::string::npos); REQUIRE(result.value().text.find("Para 2") != std::string::npos); } SECTION("Mixed case block tags") { std::string html = "
Mixed
Case
"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Mixed") != std::string::npos); REQUIRE(result.value().text.find("Case") != std::string::npos); } SECTION("BR tags converted to newlines") { std::string html = "Line 1
Line 2
Line 3
Line 4"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Line 1") != std::string::npos); REQUIRE(result.value().text.find("Line 2") != std::string::npos); REQUIRE(result.value().text.find("Line 3") != std::string::npos); REQUIRE(result.value().text.find("Line 4") != std::string::npos); } } TEST_CASE("HtmlTextExtractor - Title extraction without regex", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; config.extractMetadata = true; SECTION("Simple title extraction") { std::string html = "Test TitleContent"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("title") > 0); REQUIRE(result.value().metadata["title"] == "Test Title"); } SECTION("Title with attributes") { std::string html = "Title with Attrs"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("title") > 0); REQUIRE(result.value().metadata["title"] == "Title with Attrs"); } SECTION("Mixed case title tag") { std::string html = "CAPS Title"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("title") > 0); REQUIRE(result.value().metadata["title"] == "CAPS Title"); } SECTION("No title present") { std::string html = "No title"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("title") == 0); } } TEST_CASE("HtmlTextExtractor - Meta description extraction without regex", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; config.extractMetadata = true; SECTION("Standard meta description with double quotes") { std::string html = R"()"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("description") > 0); REQUIRE(result.value().metadata["description"] == "Test description"); } SECTION("Meta description with single quotes") { std::string html = R"()"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("description") > 0); REQUIRE(result.value().metadata["description"] == "Single quotes"); } SECTION("OpenGraph description") { std::string html = R"()"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("description") > 0); REQUIRE(result.value().metadata["description"] == "OG description"); } SECTION("Reversed attribute order") { std::string html = R"()"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().metadata.count("description") > 0); REQUIRE(result.value().metadata["description"] == "Reversed"); } } TEST_CASE("HtmlTextExtractor - Entity decoding without regex", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; SECTION("Named entities") { std::string html = "<HTML> & "quotes" 'test'"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("") != std::string::npos); REQUIRE(result.value().text.find("&") != std::string::npos); REQUIRE(result.value().text.find("\"quotes\"") != std::string::npos); } SECTION("Numeric entities") { std::string html = "ABC"; // ABC auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("ABC") != std::string::npos); } SECTION("Hex entities") { std::string html = "ABC"; // ABC auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("ABC") != std::string::npos); } SECTION("Mixed entities") { std::string html = "<A> &"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("") != std::string::npos); REQUIRE(result.value().text.find("&") != std::string::npos); } SECTION("Invalid entities preserved") { std::string html = "&invalid; 󴈿 &#xZZZZ;"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); // Should not crash, invalid entities may be preserved } } TEST_CASE("HtmlTextExtractor - Script and style removal", "[extraction][html][robust]") { HtmlTextExtractor extractor; ExtractionConfig config; SECTION("Script tags removed") { std::string html = "Visible text"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Visible text") != std::string::npos); REQUIRE(result.value().text.find("alert") == std::string::npos); } SECTION("Style tags removed") { std::string html = "Content"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Content") != std::string::npos); REQUIRE(result.value().text.find("color") == std::string::npos); } SECTION("HTML comments removed") { std::string html = "Visible"; auto result = extractor.extractFromBuffer(std::as_bytes(std::span(html.data(), html.size())), config); REQUIRE(result.has_value()); REQUIRE(result.value().text.find("Visible") != std::string::npos); REQUIRE(result.value().text.find("Comment") == std::string::npos); } } TEST_CASE("HtmlTextExtractor - Performance and memory", "[extraction][html][robust][.performance]") { HtmlTextExtractor extractor; ExtractionConfig config; SECTION("20MB HTML file completes in reasonable time") { // Generate 20MB of HTML std::string largeHtml = ""; while (largeHtml.size() < 20 * 1024 * 1024) { largeHtml += "

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

"; } largeHtml += ""; auto start = std::chrono::steady_clock::now(); auto result = extractor.extractFromBuffer( std::as_bytes(std::span(largeHtml.data(), largeHtml.size())), config); auto duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - start); REQUIRE(result.has_value()); REQUIRE(result.value().success); // Should complete in under 5 seconds (no exponential regex backtracking) REQUIRE(duration.count() < 5000); } }