From fc4d1126ad9b838b8d4eee26f62795fe006ddc8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:25:08 +0000 Subject: [PATCH 1/5] Rewrite XML parser core and tests --- lib/xml/CMakeLists.txt | 1 + lib/xml/static-dtd.hpp | 6 + lib/xml/xmlconstants.hpp | 17 +- lib/xml/xmlparser.cpp | 700 ++++++----- lib/xml/xmlparser.hpp | 105 +- test/integration/CMakeLists.txt | 1 + test/integration/xml-parser/CMakeLists.txt | 4 + .../xml-parser/test-xml-parser-e2e.cpp | 41 + test/performance/CMakeLists.txt | 1 + test/performance/xml-parser/CMakeLists.txt | 4 + .../test-xml-parser-performance.cpp | 47 + test/unit/xml-parser/CMakeLists.txt | 21 +- test/unit/xml-parser/test-xml-parser-fuzz.cpp | 69 ++ test/unit/xml-parser/test-xml-parser.cpp | 1019 ++--------------- 14 files changed, 630 insertions(+), 1406 deletions(-) create mode 100644 lib/xml/static-dtd.hpp create mode 100644 test/integration/xml-parser/CMakeLists.txt create mode 100644 test/integration/xml-parser/test-xml-parser-e2e.cpp create mode 100644 test/performance/xml-parser/CMakeLists.txt create mode 100644 test/performance/xml-parser/test-xml-parser-performance.cpp create mode 100644 test/unit/xml-parser/test-xml-parser-fuzz.cpp diff --git a/lib/xml/CMakeLists.txt b/lib/xml/CMakeLists.txt index 3b52fdb..8943160 100644 --- a/lib/xml/CMakeLists.txt +++ b/lib/xml/CMakeLists.txt @@ -31,5 +31,6 @@ install( FILES ./xmlparser.hpp ./xmlconstants.hpp + ./static-dtd.hpp DESTINATION /usr/local/include ) diff --git a/lib/xml/static-dtd.hpp b/lib/xml/static-dtd.hpp new file mode 100644 index 0000000..f107f61 --- /dev/null +++ b/lib/xml/static-dtd.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +static constexpr std::array NLAP_DTD_HEX = {0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3D, 0x22, 0x31, 0x2E, 0x30, 0x22, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x3D, 0x22, 0x55, 0x54, 0x46, 0x2D, 0x38, 0x22, 0x3F, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x29, 0x20, 0x44, 0x54, 0x44, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x30, 0x2E, 0x31, 0x0A, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x53, 0x75, 0x62, 0x2D, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x73, 0x20, 0x63, 0x6F, 0x76, 0x65, 0x72, 0x65, 0x64, 0x3A, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x4E, 0x4C, 0x41, 0x4D, 0x50, 0x20, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x4D, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x4E, 0x4C, 0x41, 0x46, 0x50, 0x20, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x4E, 0x4C, 0x41, 0x53, 0x50, 0x20, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x50, 0x20, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x50, 0x72, 0x6F, 0x78, 0x79, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2D, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x53, 0x20, 0x20, 0x28, 0x4E, 0x65, 0x78, 0x74, 0x20, 0x4C, 0x65, 0x76, 0x65, 0x6C, 0x20, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x20, 0x45, 0x78, 0x74, 0x65, 0x6E, 0x73, 0x69, 0x6F, 0x6E, 0x29, 0x0A, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x4E, 0x4F, 0x54, 0x45, 0x3A, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x20, 0x63, 0x6F, 0x64, 0x65, 0x73, 0x20, 0x2F, 0x20, 0x65, 0x6E, 0x75, 0x6D, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6E, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x3C, 0x43, 0x6F, 0x64, 0x65, 0x3E, 0x2C, 0x20, 0x3C, 0x53, 0x75, 0x62, 0x74, 0x79, 0x70, 0x65, 0x3E, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x43, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x3E, 0x2C, 0x20, 0x3C, 0x4D, 0x69, 0x6D, 0x65, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3E, 0x2C, 0x20, 0x65, 0x74, 0x63, 0x2E, 0x20, 0x61, 0x72, 0x65, 0x20, 0x77, 0x6F, 0x72, 0x6B, 0x20, 0x69, 0x6E, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x77, 0x69, 0x6C, 0x6C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x66, 0x69, 0x6E, 0x65, 0x64, 0x20, 0x6D, 0x61, 0x6E, 0x75, 0x61, 0x6C, 0x6C, 0x79, 0x2E, 0x0A, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x65, 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x20, 0x28, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x7C, 0x20, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x28, 0x55, 0x55, 0x49, 0x44, 0x2C, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2C, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x53, 0x75, 0x62, 0x74, 0x79, 0x70, 0x65, 0x2C, 0x20, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2C, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x3F, 0x2C, 0x20, 0x50, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x3F, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x28, 0x55, 0x55, 0x49, 0x44, 0x2C, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2C, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x53, 0x75, 0x62, 0x74, 0x79, 0x70, 0x65, 0x2C, 0x20, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2C, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x3F, 0x2C, 0x20, 0x50, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x3F, 0x2C, 0x20, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3F, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x43, 0x6F, 0x6D, 0x6D, 0x6F, 0x6E, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x55, 0x6E, 0x69, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6C, 0x6C, 0x79, 0x20, 0x75, 0x6E, 0x69, 0x71, 0x75, 0x65, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x20, 0x28, 0x52, 0x46, 0x43, 0x20, 0x34, 0x31, 0x32, 0x32, 0x29, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2F, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x63, 0x6F, 0x72, 0x72, 0x65, 0x6C, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x55, 0x55, 0x49, 0x44, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x41, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x20, 0x22, 0x4E, 0x4C, 0x41, 0x50, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x50, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x30, 0x2E, 0x31, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x53, 0x75, 0x62, 0x2D, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x73, 0x65, 0x6C, 0x65, 0x63, 0x74, 0x6F, 0x72, 0x3A, 0x20, 0x4E, 0x4C, 0x41, 0x4D, 0x50, 0x20, 0x7C, 0x20, 0x4E, 0x4C, 0x41, 0x46, 0x50, 0x20, 0x7C, 0x20, 0x4E, 0x4C, 0x41, 0x53, 0x50, 0x20, 0x7C, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x50, 0x20, 0x7C, 0x20, 0x4E, 0x4C, 0x41, 0x50, 0x53, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x75, 0x62, 0x74, 0x79, 0x70, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x6C, 0x6C, 0x20, 0x63, 0x68, 0x69, 0x6C, 0x64, 0x20, 0x65, 0x6C, 0x65, 0x6D, 0x65, 0x6E, 0x74, 0x73, 0x20, 0x61, 0x72, 0x65, 0x20, 0x6F, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x74, 0x6F, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x62, 0x6F, 0x74, 0x68, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x61, 0x6E, 0x64, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x20, 0x61, 0x63, 0x72, 0x6F, 0x73, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x73, 0x75, 0x62, 0x2D, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x73, 0x2E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x28, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x48, 0x6F, 0x73, 0x74, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x55, 0x52, 0x4C, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2D, 0x41, 0x67, 0x65, 0x6E, 0x74, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x75, 0x73, 0x65, 0x72, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x43, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2D, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x4D, 0x69, 0x6D, 0x65, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x45, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x2D, 0x46, 0x75, 0x6C, 0x6C, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x43, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x55, 0x55, 0x49, 0x44, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x2D, 0x53, 0x75, 0x6D, 0x3F, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x3F, 0x0A, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6C, 0x20, 0x68, 0x6F, 0x73, 0x74, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x74, 0x65, 0x73, 0x74, 0x61, 0x70, 0x70, 0x31, 0x2E, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x48, 0x6F, 0x73, 0x74, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x70, 0x61, 0x74, 0x68, 0x20, 0x2F, 0x20, 0x72, 0x65, 0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x55, 0x52, 0x4C, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x2F, 0x74, 0x65, 0x73, 0x74, 0x70, 0x61, 0x74, 0x68, 0x2F, 0x69, 0x6E, 0x64, 0x65, 0x78, 0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x55, 0x52, 0x4C, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x43, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x46, 0x61, 0x6C, 0x63, 0x6F, 0x6E, 0x2D, 0x42, 0x72, 0x6F, 0x77, 0x73, 0x65, 0x72, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x55, 0x73, 0x65, 0x72, 0x2D, 0x41, 0x67, 0x65, 0x6E, 0x74, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x74, 0x79, 0x20, 0x28, 0x4E, 0x4C, 0x41, 0x50, 0x53, 0x29, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x75, 0x73, 0x65, 0x72, 0x31, 0x40, 0x64, 0x6F, 0x6D, 0x61, 0x69, 0x6E, 0x2E, 0x63, 0x6F, 0x6D, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x20, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x73, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x53, 0x69, 0x67, 0x6E, 0x61, 0x6C, 0x20, 0x63, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x74, 0x65, 0x61, 0x72, 0x64, 0x6F, 0x77, 0x6E, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x3A, 0x20, 0x30, 0x20, 0x7C, 0x20, 0x31, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x43, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x2D, 0x43, 0x6C, 0x6F, 0x73, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x4D, 0x49, 0x4D, 0x45, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x68, 0x74, 0x6D, 0x6C, 0x22, 0x2C, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x6A, 0x73, 0x6F, 0x6E, 0x22, 0x2C, 0x20, 0x22, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, 0x67, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x4D, 0x69, 0x6D, 0x65, 0x2D, 0x54, 0x79, 0x70, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x20, 0x2F, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x20, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x22, 0x55, 0x54, 0x46, 0x2D, 0x38, 0x22, 0x2C, 0x20, 0x22, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x22, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x45, 0x6E, 0x63, 0x6F, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x73, 0x69, 0x7A, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6E, 0x67, 0x6C, 0x65, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x73, 0x69, 0x7A, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6F, 0x6D, 0x70, 0x6C, 0x65, 0x74, 0x65, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x28, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x29, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x2D, 0x46, 0x75, 0x6C, 0x6C, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x42, 0x79, 0x74, 0x65, 0x20, 0x73, 0x69, 0x7A, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6E, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6C, 0x20, 0x70, 0x61, 0x72, 0x74, 0x20, 0x28, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x29, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x42, 0x79, 0x74, 0x65, 0x2D, 0x53, 0x69, 0x7A, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x43, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x61, 0x6C, 0x67, 0x6F, 0x72, 0x69, 0x74, 0x68, 0x6D, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x3A, 0x20, 0x6E, 0x6F, 0x6E, 0x65, 0x20, 0x7C, 0x20, 0x67, 0x7A, 0x69, 0x70, 0x20, 0x7C, 0x20, 0x62, 0x7A, 0x69, 0x70, 0x32, 0x20, 0x7C, 0x20, 0x2E, 0x2E, 0x2E, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x43, 0x6F, 0x6D, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x55, 0x55, 0x49, 0x44, 0x20, 0x69, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x66, 0x79, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x61, 0x63, 0x72, 0x6F, 0x73, 0x73, 0x20, 0x61, 0x6C, 0x6C, 0x20, 0x70, 0x61, 0x72, 0x74, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x61, 0x20, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x55, 0x55, 0x49, 0x44, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x20, 0x6E, 0x75, 0x6D, 0x62, 0x65, 0x72, 0x20, 0x6F, 0x66, 0x20, 0x70, 0x61, 0x72, 0x74, 0x73, 0x20, 0x69, 0x6E, 0x20, 0x61, 0x20, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x2D, 0x53, 0x75, 0x6D, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x49, 0x6E, 0x64, 0x65, 0x78, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x61, 0x72, 0x74, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x75, 0x6C, 0x74, 0x69, 0x2D, 0x70, 0x61, 0x72, 0x74, 0x20, 0x74, 0x72, 0x61, 0x6E, 0x73, 0x66, 0x65, 0x72, 0x20, 0x28, 0x31, 0x2D, 0x62, 0x61, 0x73, 0x65, 0x64, 0x29, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x2D, 0x50, 0x61, 0x72, 0x74, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x28, 0x4E, 0x4C, 0x41, 0x50, 0x53, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x28, 0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x2C, 0x20, 0x53, 0x69, 0x67, 0x6E, 0x61, 0x74, 0x75, 0x72, 0x65, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x66, 0x6C, 0x61, 0x67, 0x3A, 0x20, 0x30, 0x20, 0x3D, 0x20, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x2C, 0x20, 0x31, 0x20, 0x3D, 0x20, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x2D, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x65, 0x64, 0x20, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6F, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x61, 0x74, 0x75, 0x72, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x69, 0x67, 0x6E, 0x61, 0x74, 0x75, 0x72, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x50, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x4E, 0x59, 0x20, 0x69, 0x73, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x61, 0x63, 0x63, 0x6F, 0x6D, 0x6D, 0x6F, 0x64, 0x61, 0x74, 0x65, 0x20, 0x70, 0x6C, 0x61, 0x69, 0x6E, 0x20, 0x74, 0x65, 0x78, 0x74, 0x2C, 0x20, 0x4A, 0x53, 0x4F, 0x4E, 0x2C, 0x20, 0x69, 0x6E, 0x6C, 0x69, 0x6E, 0x65, 0x20, 0x48, 0x54, 0x4D, 0x4C, 0x2F, 0x58, 0x4D, 0x4C, 0x2C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79, 0x20, 0x28, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x2D, 0x65, 0x6E, 0x63, 0x6F, 0x64, 0x65, 0x64, 0x29, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x74, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6E, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x6E, 0x20, 0x73, 0x75, 0x62, 0x2D, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x2E, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x50, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x20, 0x41, 0x4E, 0x59, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x62, 0x6C, 0x6F, 0x63, 0x6B, 0x20, 0x28, 0x72, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x20, 0x6F, 0x6E, 0x6C, 0x79, 0x29, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x28, 0x43, 0x6F, 0x64, 0x65, 0x2C, 0x20, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x3F, 0x2C, 0x20, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x3F, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x4E, 0x75, 0x6D, 0x65, 0x72, 0x69, 0x63, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x20, 0x63, 0x6F, 0x64, 0x65, 0x3B, 0x20, 0x30, 0x20, 0x3D, 0x20, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x3B, 0x20, 0x6E, 0x6F, 0x6E, 0x2D, 0x7A, 0x65, 0x72, 0x6F, 0x20, 0x3D, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x28, 0x57, 0x49, 0x50, 0x3A, 0x20, 0x65, 0x6E, 0x75, 0x6D, 0x20, 0x54, 0x42, 0x44, 0x29, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x43, 0x6F, 0x64, 0x65, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x48, 0x75, 0x6D, 0x61, 0x6E, 0x2D, 0x72, 0x65, 0x61, 0x64, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x6F, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x73, 0x75, 0x6C, 0x74, 0x20, 0x6F, 0x72, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x63, 0x6F, 0x6E, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A, 0x0A, 0x3C, 0x21, 0x2D, 0x2D, 0x20, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6C, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x2C, 0x20, 0x65, 0x2E, 0x67, 0x2E, 0x20, 0x61, 0x20, 0x50, 0x79, 0x74, 0x68, 0x6F, 0x6E, 0x20, 0x74, 0x72, 0x61, 0x63, 0x65, 0x62, 0x61, 0x63, 0x6B, 0x20, 0x6C, 0x69, 0x6E, 0x65, 0x20, 0x2D, 0x2D, 0x3E, 0x0A, 0x3C, 0x21, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x20, 0x45, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x28, 0x23, 0x50, 0x43, 0x44, 0x41, 0x54, 0x41, 0x29, 0x3E, 0x0A}; diff --git a/lib/xml/xmlconstants.hpp b/lib/xml/xmlconstants.hpp index c2f37e0..2bb3c2f 100644 --- a/lib/xml/xmlconstants.hpp +++ b/lib/xml/xmlconstants.hpp @@ -1,20 +1,25 @@ #pragma once -#include +#include +#include + +//- NLAP XML start marker: configurable tag used to identify message boundaries +static constexpr std::string_view NLAP_XML_START_MARKER(""); //- NLAP XML end marker: used to split stream data into individual NLAP messages -static const std::string NLAP_XML_END_MARKER(""); +static constexpr std::string_view NLAP_XML_END_MARKER(""); //- XML declaration prepended to messages that do not already contain it -static const std::string NLAP_XML_DECLARATION(""); +static constexpr std::string_view NLAP_XML_DECLARATION(""); //- Path to the NLAP DTD used for validation (override at compile time via -DNLAP_DTD_PATH=...) #ifndef NLAP_DTD_PATH #define NLAP_DTD_PATH "specs/xml/nlap.dtd" #endif -static const std::string NLAP_DTD_SYSTEM_PATH(NLAP_DTD_PATH); +static const std::string_view NLAP_DTD_SYSTEM_PATH(NLAP_DTD_PATH); //- constant expressions (error) -constexpr uint16_t XML_ERROR_PARSE_BUFFER_EXCEEDED = 10; -constexpr uint16_t XML_ERROR_BAD_REQUEST = 400; +constexpr uint16_t XML_ERROR_INVALID_CONTENT_DTD = 10; +constexpr uint16_t XML_ERROR_INVALID_SYNTAX = 20; +constexpr uint16_t XML_ERROR_INVALID_FRAMING = 30; diff --git a/lib/xml/xmlparser.cpp b/lib/xml/xmlparser.cpp index 2389471..be4c291 100644 --- a/lib/xml/xmlparser.cpp +++ b/lib/xml/xmlparser.cpp @@ -1,483 +1,457 @@ #include "xmlparser.hpp" + +#include "static-dtd.hpp" #include "xmlconstants.hpp" +#include #include +#include +#include +#include +#include -#include -#include -#include -#include #include -#include #include #include +#include +#include +#include +#include #include XERCES_CPP_NAMESPACE_USE -using namespace std; - +namespace +{ -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- +static std::atomic_uint32_t XercesRefCount{0}; -//- RAII wrapper: char* transcoded from XMLCh* (auto-release on destruction) -class _CStr +template +class Generator { public: - explicit _CStr(const XMLCh* str) : _p(XMLString::transcode(str)) {} - ~_CStr() { if (_p) XMLString::release(&_p); } - string str() const { return _p ? string(_p) : string(); } - const char* c_str() const { return _p; } -private: - char* _p; -}; - -//- RAII wrapper: XMLCh* transcoded from char* (auto-release on destruction) -class _XStr -{ -public: - explicit _XStr(const char* str) : _p(XMLString::transcode(str)) {} - ~_XStr() { if (_p) XMLString::release(&_p); } - operator const XMLCh*() const { return _p; } -private: - XMLCh* _p; -}; + struct promise_type { + T CurrentValue{}; + Generator get_return_object() + { + return Generator(std::coroutine_handle::from_promise(*this)); + } -// --------------------------------------------------------------------------- -// Simple SAX error handler used during DOM parsing -// --------------------------------------------------------------------------- + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + std::suspend_always yield_value(T Value) noexcept + { + CurrentValue = std::move(Value); + return {}; + } + void return_void() {} + void unhandled_exception() { std::terminate(); } + }; -class _NLAPErrorHandler : public HandlerBase -{ -public: - _NLAPErrorHandler() : _hasError(false) {} + class iterator + { + public: + explicit iterator(std::coroutine_handle Handle) : _Handle(Handle) {} + + iterator& operator++() + { + _Handle.resume(); + if (_Handle.done()) { + _Handle = {}; + } + return *this; + } - bool hasError() const { return _hasError; } + const T& operator*() const + { + return _Handle.promise().CurrentValue; + } - void error(const SAXParseException&) override { _hasError = true; } - void fatalError(const SAXParseException&) override { _hasError = true; } - void warning(const SAXParseException&) override {} - void resetErrors() override { _hasError = false; } + bool operator==(std::default_sentinel_t) const + { + return !_Handle; + } -private: - bool _hasError; -}; + private: + std::coroutine_handle _Handle; + }; + explicit Generator(std::coroutine_handle Handle) : _Handle(Handle) {} + Generator(Generator&& Other) noexcept : _Handle(Other._Handle) { Other._Handle = {}; } -// --------------------------------------------------------------------------- -// Entity resolver that blocks external entity loading (XXE prevention). -// Only the known NLAP DTD SYSTEM identifier is allowed to pass through -// (the pre-loaded grammar pool handles it without disk I/O). All other -// external entity references — which could be injected by a malicious -// client — are resolved to an empty document. -// --------------------------------------------------------------------------- + ~Generator() + { + if (_Handle) { + _Handle.destroy(); + } + } -class _BlockingEntityResolver : public HandlerBase -{ -public: - InputSource* resolveEntity(const XMLCh* /*publicId*/, const XMLCh* systemId) override + iterator begin() { - if (systemId != nullptr) { - char* sysStr = XMLString::transcode(systemId); - bool isNLAPDtd = (string(sysStr).find("nlap.dtd") != string::npos); - XMLString::release(&sysStr); - if (isNLAPDtd) { - //- let Xerces handle it; the grammar pool will serve the - //- pre-loaded DTD without loading from the file system - return nullptr; + if (_Handle) { + _Handle.resume(); + if (_Handle.done()) { + return iterator({}); } } - //- block all other external entities (XXE prevention) - static const char emptyDoc[] = ""; - return new MemBufInputSource( - reinterpret_cast(emptyDoc), 0, "blocked-entity" - ); + return iterator(_Handle); } -}; + std::default_sentinel_t end() const { return {}; } -// --------------------------------------------------------------------------- -// Xerces reference counting for Initialize / Terminate -// Thread-safe via std::atomic. -// --------------------------------------------------------------------------- +private: + std::coroutine_handle _Handle; +}; -static atomic _xercesRefCount{0}; +std::string transcodeXMLCh(const XMLCh* Input) +{ + if (Input == nullptr) { + return std::string(); + } + char* Buffer = XMLString::transcode(Input); + if (Buffer == nullptr) { + return std::string(); + } -// --------------------------------------------------------------------------- -// DOM tree helper functions -// --------------------------------------------------------------------------- + std::string Value(Buffer); + XMLString::release(&Buffer); + return Value; +} -//- Return the trimmed text content of a DOM element (concatenates TEXT and -//- CDATA child nodes; does NOT recurse into element children). -static string _getElementText(DOMElement* elem) +std::string trimCopy(const std::string& Value) { - string result; - DOMNodeList* children = elem->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - DOMNode::NodeType t = child->getNodeType(); - if (t == DOMNode::TEXT_NODE || t == DOMNode::CDATA_SECTION_NODE) { - _CStr val(child->getNodeValue()); - result += val.str(); - } + const auto Begin = Value.find_first_not_of(" \t\n\r"); + if (Begin == std::string::npos) { + return std::string(); } - //- trim leading and trailing whitespace - size_t start = result.find_first_not_of(" \t\r\n"); - if (start == string::npos) { return string(); } - size_t end = result.find_last_not_of(" \t\r\n"); - return result.substr(start, end - start + 1); + + const auto End = Value.find_last_not_of(" \t\n\r"); + return Value.substr(Begin, End - Begin + 1); } -//- Return all child text content recursively (for Payload ANY elements). -static string _getAllText(DOMNode* node) +std::string getElementText(DOMElement* Element) { - string result; - DOMNodeList* children = node->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - DOMNode::NodeType t = child->getNodeType(); - if (t == DOMNode::TEXT_NODE || t == DOMNode::CDATA_SECTION_NODE) { - _CStr val(child->getNodeValue()); - result += val.str(); - } else if (t == DOMNode::ELEMENT_NODE) { - result += _getAllText(child); + std::string Value; + DOMNodeList* Children = Element->getChildNodes(); + for (XMLSize_t Index = 0; Index < Children->getLength(); ++Index) { + DOMNode* Child = Children->item(Index); + const auto Type = Child->getNodeType(); + if (Type == DOMNode::TEXT_NODE || Type == DOMNode::CDATA_SECTION_NODE) { + Value += transcodeXMLCh(Child->getNodeValue()); } } - return result; + + return trimCopy(Value); } -//- Parse
element child nodes into the NLAPHeader_t map. -static void _parseHeader(DOMElement* headerElem, NLAPHeader_t& headerMap) +Generator iterateElementChildren(DOMElement* Parent) { - DOMNodeList* children = headerElem->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - if (child->getNodeType() != DOMNode::ELEMENT_NODE) { continue; } - - DOMElement* field = static_cast(child); - _CStr name(field->getTagName()); - string value = _getElementText(field); - - if (!value.empty()) { - headerMap.emplace(name.str(), value); + DOMNodeList* Children = Parent->getChildNodes(); + for (XMLSize_t Index = 0; Index < Children->getLength(); ++Index) { + DOMNode* Child = Children->item(Index); + if (Child->getNodeType() == DOMNode::ELEMENT_NODE) { + co_yield static_cast(Child); } } } -//- Parse element into Encryption / Signature fields. -static void _parseSecurity(DOMElement* secElem, RequestProperties_t& props) +struct MessageSlice { - DOMNodeList* children = secElem->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - if (child->getNodeType() != DOMNode::ELEMENT_NODE) { continue; } - - DOMElement* field = static_cast(child); - _CStr name(field->getTagName()); - string value = _getElementText(field); - string nameStr = name.str(); - - if (nameStr == "Encryption") { props.Encryption = value; } - else if (nameStr == "Signature") { props.Signature = value; } - } -} + std::string_view Slice; + std::size_t StartOffset; +}; -//- Parse element into StatusCode / StatusDescription / StatusException. -static void _parseStatus(DOMElement* statusElem, RequestProperties_t& props) +Generator splitMessages(std::string_view Input, bool& FramingError) { - DOMNodeList* children = statusElem->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - if (child->getNodeType() != DOMNode::ELEMENT_NODE) { continue; } - - DOMElement* field = static_cast(child); - _CStr name(field->getTagName()); - string value = _getElementText(field); - string nameStr = name.str(); - - if (nameStr == "Code") { props.StatusCode = value; } - else if (nameStr == "Description") { props.StatusDescription = value; } - else if (nameStr == "Exception") { props.StatusException = value; } + std::size_t Cursor = 0; + std::size_t LastEnd = std::string_view::npos; + + while (true) { + const std::size_t Start = Input.find(NLAP_XML_START_MARKER, Cursor); + if (Start == std::string_view::npos) { + break; + } + + if (LastEnd != std::string_view::npos && Start != LastEnd) { + FramingError = true; + co_return; + } + + const std::size_t End = Input.find(NLAP_XML_END_MARKER, Start); + if (End == std::string_view::npos) { + break; + } + + const std::size_t MessageEnd = End + NLAP_XML_END_MARKER.size(); + co_yield MessageSlice{Input.substr(Start, MessageEnd - Start), Start}; + + Cursor = MessageEnd; + LastEnd = MessageEnd; } } +std::string ensureParseableXML(std::string_view Message) +{ + std::string Parseable(Message); -// --------------------------------------------------------------------------- -// Build a parseable XML string: ensure "; - const string doctype = ""; - - size_t declStartPos = result.find(declTag); - - if (declStartPos == string::npos) { - //- no XML declaration at all: prepend both - result = NLAP_XML_DECLARATION + doctype + result; - } else { - //- XML declaration is present: inject DOCTYPE right after it - size_t declEndPos = result.find(declEnd, declStartPos); - if (declEndPos != string::npos) { - result.insert(declEndPos + declEnd.length(), doctype); + if (Parseable.find(""); + const std::string Doctype = ""; + if (DeclEnd != std::string::npos) { + Parseable.insert(DeclEnd + 2, Doctype); + } else { + Parseable.insert(0, Doctype); } } - return result; + return Parseable; } +class ParseErrorHandler : public HandlerBase +{ +public: + ParseErrorHandler() = default; -// --------------------------------------------------------------------------- -// StringHelper (shared split utility – mirrors httpparser.hpp) -// --------------------------------------------------------------------------- + void error(const SAXParseException& Exception) override + { + classify(Exception); + } -class StringHelper { -public: - static void split(string& StringRef, const string& Delimiter, vector& ResultRef) + void fatalError(const SAXParseException& Exception) override { - string SplitElement; - auto pos = StringRef.find(Delimiter); - - while (pos != string::npos) { - SplitElement = StringRef.substr(0, pos); - ResultRef.push_back(SplitElement); - StringRef.erase(0, pos + Delimiter.length()); - pos = StringRef.find(Delimiter); - } + classify(Exception); } -}; + void warning(const SAXParseException&) override {} -// --------------------------------------------------------------------------- -// XMLParser implementation -// --------------------------------------------------------------------------- + void resetErrors() override + { + _HasSyntaxError = false; + _HasDTDContentError = false; + } -XMLParser::XMLParser(const uint16_t BufferSize) : - _RequestParseError(0), - _ReqAddIndex(0), - _ReqNextIndex(0), - _XMLRequestBuffer(""), - _grammarPool(nullptr) -{ - _XMLRequestBuffer.reserve(BufferSize); - _XMLRequestBufferMax = BufferSize; + bool hasSyntaxError() const { return _HasSyntaxError; } + bool hasDTDContentError() const { return _HasDTDContentError; } - if (_xercesRefCount.fetch_add(1) == 0) { - XMLPlatformUtils::Initialize(); - } +private: + void classify(const SAXParseException& Exception) + { + const std::string Message = transcodeXMLCh(Exception.getMessage()); + if (Message.find("element") != std::string::npos && Message.find("must") != std::string::npos) { + _HasDTDContentError = true; + return; + } - //- pre-load the NLAP DTD into a locked grammar pool so that individual - //- parse calls can use the cached grammar without loading external files - auto* pool = new XMLGrammarPoolImpl(XMLPlatformUtils::fgMemoryManager); - _grammarPool = pool; + if (Message.find("valid") != std::string::npos && Message.find("content") != std::string::npos) { + _HasDTDContentError = true; + return; + } - try { - XercesDOMParser dtdLoader(nullptr, XMLPlatformUtils::fgMemoryManager, pool); - dtdLoader.loadGrammar(NLAP_DTD_SYSTEM_PATH.c_str(), Grammar::DTDGrammarType, true); - } - catch (...) { - //- DTD loading failed (file not found or malformed DTD). - //- The pool remains empty; validation will fail for every incoming message, - //- which is the correct safe behaviour: no DTD → no accepted messages. + _HasSyntaxError = true; } - pool->lockPool(); -} + bool _HasSyntaxError = false; + bool _HasDTDContentError = false; +}; -XMLParser::~XMLParser() +void populateTree(DOMElement* Element, XMLNode& Node, std::string_view RawMessage, std::size_t& SearchOffset) { - delete static_cast(_grammarPool); - _grammarPool = nullptr; + bool HasElementChildren = false; - if (_xercesRefCount.fetch_sub(1) == 1) { - XMLPlatformUtils::Terminate(); + for (DOMElement* Child : iterateElementChildren(Element)) { + HasElementChildren = true; + const std::string ChildName = transcodeXMLCh(Child->getTagName()); + XMLNode& ChildNode = Node[ChildName]; + populateTree(Child, ChildNode, RawMessage, SearchOffset); } -} -void XMLParser::appendBuffer(const char* BufferRef, const uint16_t RecvBytes) -{ - if (_XMLRequestBuffer.length() + RecvBytes > _XMLRequestBufferMax) { - _RequestParseError = XML_ERROR_PARSE_BUFFER_EXCEEDED; + if (HasElementChildren) { return; } - _XMLRequestBuffer.append(BufferRef, RecvBytes); - - //- reset split vector and process only when at least one complete message arrived - const size_t EndMarkerFound = _XMLRequestBuffer.find(NLAP_XML_END_MARKER); + const std::string Value = getElementText(Element); + if (Value.empty()) { + return; + } - if (EndMarkerFound != string::npos) { - _SplittedRequests.clear(); - _processRequests(); + std::size_t Found = RawMessage.find(Value, SearchOffset); + if (Found == std::string_view::npos) { + Found = RawMessage.find(Value); + } + if (Found == std::string_view::npos) { + return; } -} -RequestsMap_t XMLParser::getRequests() -{ - return _Requests; + Node.Address = RawMessage.data() + Found; + Node.Length = Value.size(); + SearchOffset = Found + Value.size(); } -RequestPropertiesPtr_t XMLParser::getNextRequest() +uint16_t parseMessage( + const std::shared_ptr& GrammarPool, + std::string_view RawMessage, + ResultTree_t& OutputTree) { - if (_Requests.size() > 0) { - _ReqNextIndex += 1; - return make_shared(_Requests.at(_ReqNextIndex - 1)); + const std::string Parseable = ensureParseableXML(RawMessage); + + ParseErrorHandler SyntaxHandler; + XercesDOMParser SyntaxParser; + SyntaxParser.setErrorHandler(&SyntaxHandler); + SyntaxParser.setValidationScheme(XercesDOMParser::Val_Never); + SyntaxParser.setDoNamespaces(false); + SyntaxParser.setDoSchema(false); + + MemBufInputSource SyntaxSource( + reinterpret_cast(Parseable.data()), + Parseable.size(), + "nlap-syntax", + false + ); + SyntaxParser.parse(SyntaxSource); + + if (SyntaxHandler.hasSyntaxError()) { + return XML_ERROR_INVALID_SYNTAX; } - return nullptr; -} -void XMLParser::removeRequest(uint16_t Index) -{ - if (_Requests.find(Index) != _Requests.end()) { - _Requests.erase(Index); + ParseErrorHandler DTDHandler; + auto* CastedPool = static_cast(GrammarPool.get()); + XercesDOMParser DTDParser(nullptr, XMLPlatformUtils::fgMemoryManager, CastedPool); + + DTDParser.setErrorHandler(&DTDHandler); + DTDParser.setValidationScheme(XercesDOMParser::Val_Always); + DTDParser.setDoNamespaces(false); + DTDParser.setDoSchema(false); + DTDParser.setLoadExternalDTD(false); + DTDParser.useCachedGrammarInParse(true); + DTDParser.setCreateEntityReferenceNodes(false); + DTDParser.setValidationConstraintFatal(true); + + MemBufInputSource DTDSource( + reinterpret_cast(Parseable.data()), + Parseable.size(), + "nlap-dtd-validation", + false + ); + DTDParser.parse(DTDSource); + + if (DTDHandler.hasDTDContentError()) { + return XML_ERROR_INVALID_CONTENT_DTD; } -} - -inline void XMLParser::_processRequests() -{ - //- split stream buffer into individual NLAP messages on the end marker - StringHelper::split(_XMLRequestBuffer, NLAP_XML_END_MARKER, _SplittedRequests); - - //- iterate over split messages - for (size_t i = 0; i < _SplittedRequests.size(); ++i) { - if (_processRequestProperties(i) == false) { - _RequestParseError = XML_ERROR_BAD_REQUEST; - } + if (DTDHandler.hasSyntaxError()) { + return XML_ERROR_INVALID_SYNTAX; } -} -inline bool XMLParser::_processRequestProperties(const size_t Index) -{ - //- get the raw split piece at this index - auto& Request = _SplittedRequests.at(Index); + DOMDocument* Document = DTDParser.getDocument(); + if (Document == nullptr) { + return XML_ERROR_INVALID_SYNTAX; + } - //- skip empty pieces (e.g. trailing split result) - if (Request.empty()) { return false; } + DOMElement* Root = Document->getDocumentElement(); + if (Root == nullptr) { + return XML_ERROR_INVALID_SYNTAX; + } - //- reassemble a complete, valid XML message: - //- 1. append (was consumed as the delimiter during split) - //- 2. prepend getTagName()); + XMLNode& RootNode = OutputTree[RootName]; - if (XMLMessage.find("getTagName()); + XMLNode& ChildNode = RootNode[ChildName]; + populateTree(Child, ChildNode, RawMessage, SearchOffset); } - //- reset properties - _RequestProperties = RequestProperties_t{}; - _RequestProperties.XMLRawMessage = XMLMessage; + return 0; +} - //- parse XML and populate remaining fields - if (_parseXML(XMLMessage, _RequestProperties) == false) { return false; } +} // namespace - //- add to requests map - _Requests.emplace(_ReqAddIndex, _RequestProperties); - _ReqAddIndex += 1; +XMLParser::XMLParser(std::size_t ParseBufferSize) + : _ParseBufferSize(ParseBufferSize) +{ + if (XercesRefCount.fetch_add(1) == 0) { + XMLPlatformUtils::Initialize(); + } - return true; + auto* GrammarPool = new XMLGrammarPoolImpl(XMLPlatformUtils::fgMemoryManager); + _GrammarPool = std::shared_ptr( + GrammarPool, + [](void* Pointer) { delete static_cast(Pointer); } + ); + + XercesDOMParser Loader(nullptr, XMLPlatformUtils::fgMemoryManager, GrammarPool); + const std::string DTDSourceId(NLAP_DTD_SYSTEM_PATH); + MemBufInputSource DTDSource( + reinterpret_cast(NLAP_DTD_HEX.data()), + NLAP_DTD_HEX.size(), + DTDSourceId.c_str(), + false + ); + + Loader.loadGrammar(DTDSource, Grammar::DTDGrammarType, true); + GrammarPool->lockPool(); } -inline bool XMLParser::_parseXML(const string& XMLMessage, RequestProperties_t& Props) +XMLParser::~XMLParser() { - //- build a version of the XML that includes a DOCTYPE so Xerces can - //- identify the grammar to use from the pre-loaded grammar pool - string parseableXML = _buildParseableXML(XMLMessage); - - auto* pool = static_cast(_grammarPool); - XercesDOMParser parser(nullptr, XMLPlatformUtils::fgMemoryManager, pool); - _NLAPErrorHandler errHandler; - - parser.setErrorHandler(&errHandler); - parser.setValidationScheme(XercesDOMParser::Val_Always); - parser.setDoNamespaces(false); - parser.setDoSchema(false); - - //- use the pre-loaded DTD grammar; disable loading of any external - //- entities from user-supplied XML to prevent XXE attacks - parser.setLoadExternalDTD(false); - parser.useCachedGrammarInParse(true); - parser.setCreateEntityReferenceNodes(false); - parser.setValidationConstraintFatal(true); - - //- block all external entity resolution at the SAX layer (XXE defence) - _BlockingEntityResolver blockingResolver; - parser.setEntityResolver(&blockingResolver); - - try { - MemBufInputSource xmlInput( - reinterpret_cast(parseableXML.c_str()), - static_cast(parseableXML.size()), - "nlap-xml-input" - ); - parser.parse(xmlInput); + _GrammarPool.reset(); + if (XercesRefCount.fetch_sub(1) == 1) { + XMLPlatformUtils::Terminate(); } - catch (const XMLException&) { return false; } - catch (const DOMException&) { return false; } - catch (...) { return false; } - - if (errHandler.hasError()) { return false; } +} - DOMDocument* doc = parser.getDocument(); - if (!doc) { return false; } +void XMLParser::setParseBufferSize(std::size_t ParseBufferSize) +{ + _ParseBufferSize = ParseBufferSize; +} - //- root element: - DOMElement* root = doc->getDocumentElement(); - if (!root) { return false; } +std::size_t XMLParser::getParseBufferSize() const +{ + return _ParseBufferSize; +} - //- first element child of is either or - DOMNodeList* rootChildren = root->getChildNodes(); - DOMElement* reqresElem = nullptr; +ParseResult_t XMLParser::parse(char* InputBuffer) const +{ + ParseResult_t Result; - for (XMLSize_t i = 0; i < rootChildren->getLength(); ++i) { - DOMNode* node = rootChildren->item(i); - if (node->getNodeType() == DOMNode::ELEMENT_NODE) { - reqresElem = static_cast(node); - break; - } + if (InputBuffer == nullptr) { + Result.ErrorCode = XML_ERROR_INVALID_SYNTAX; + return Result; } - if (!reqresElem) { return false; } + std::string_view InputBufferSV(InputBuffer); - { - _CStr tagName(reqresElem->getTagName()); - Props.RequestType = tagName.str(); + if (InputBufferSV.size() > _ParseBufferSize) { + Result.ErrorCode = XML_ERROR_INVALID_FRAMING; + return Result; } - //- only accept or - if (Props.RequestType != "Request" && Props.RequestType != "Response") { - return false; + bool FramingError = false; + for (const MessageSlice& Slice : splitMessages(InputBufferSV, FramingError)) { + ResultTree_t Tree; + const uint16_t MessageError = parseMessage(_GrammarPool, Slice.Slice, Tree); + + if (MessageError != 0) { + Result.ErrorCode = MessageError; + Result.Results.clear(); + return Result; + } + + Result.Results.push_back(std::move(Tree)); } - //- iterate over child elements of / - DOMNodeList* children = reqresElem->getChildNodes(); - for (XMLSize_t i = 0; i < children->getLength(); ++i) { - DOMNode* child = children->item(i); - if (child->getNodeType() != DOMNode::ELEMENT_NODE) { continue; } - - DOMElement* elem = static_cast(child); - _CStr nameCS(elem->getTagName()); - string name = nameCS.str(); - - if (name == "UUID") { Props.UUID = _getElementText(elem); } - else if (name == "Protocol") { Props.Protocol = _getElementText(elem); } - else if (name == "Version") { Props.Version = _getElementText(elem); } - else if (name == "Subtype") { Props.Subtype = _getElementText(elem); } - else if (name == "Header") { _parseHeader(elem, Props.Header); } - else if (name == "Security") { _parseSecurity(elem, Props); } - else if (name == "Payload") { Props.Payload = _getAllText(child); } - else if (name == "Status") { _parseStatus(elem, Props); } + if (FramingError) { + Result.ErrorCode = XML_ERROR_INVALID_FRAMING; + Result.Results.clear(); + return Result; } - return true; + return Result; } diff --git a/lib/xml/xmlparser.hpp b/lib/xml/xmlparser.hpp index 5191ae6..3986876 100644 --- a/lib/xml/xmlparser.hpp +++ b/lib/xml/xmlparser.hpp @@ -1,91 +1,50 @@ #pragma once -#include -#include -#include +#include #include +#include +#include +#include #include -using namespace std; - -//- Header fields map: key = element name (e.g. "Host", "URL", "Mime-Type"), value = text content -typedef unordered_map NLAPHeader_t; -typedef NLAPHeader_t& NLAPHeaderRef_t; +#include -//- Unified request/response properties covering all NLAP sub-protocols: -//- NLAMP (Next Level Application Metadata Protocol) -//- NLAFP (Next Level Application File Protocol) -//- NLASP (Next Level Application Session Protocol) -//- NLAPP (Next Level Application Proxy Protocol) -//- NLAPS (Next Level Application Protocol Secure Extension) -struct RequestProperties_t +struct XMLNode { - //- full, valid, parsable XML NLAP message (beginning with Children; + const char* Address = nullptr; + std::size_t Length = 0; + + XMLNode& operator[](const std::string& Key) + { + return Children[Key]; + } + + const XMLNode& at(const std::string& Key) const + { + return Children.at(Key); + } }; -typedef RequestProperties_t& RequestPropertiesRef_t; -typedef shared_ptr RequestPropertiesPtr_t; - -typedef unordered_map RequestsMap_t; +using ResultTree_t = std::unordered_map; +struct ParseResult_t +{ + std::vector Results; + std::uint16_t ErrorCode = 0; +}; class XMLParser { - public: - - XMLParser(const uint16_t BufferSize); + explicit XMLParser(std::size_t ParseBufferSize = 4096); ~XMLParser(); - void appendBuffer(const char* BufferRef, const uint16_t RecvBytes); - RequestsMap_t getRequests(); - RequestPropertiesPtr_t getNextRequest(); - void removeRequest(uint16_t Index); - -private: - - void _processRequests(); - bool _processRequestProperties(const size_t Index); - bool _parseXML(const string& XMLMessage, RequestProperties_t& Props); - - vector _SplittedRequests; - - uint16_t _RequestParseError; + void setParseBufferSize(std::size_t ParseBufferSize); + std::size_t getParseBufferSize() const; - uint16_t _ReqAddIndex; - uint16_t _ReqNextIndex; + ParseResult_t parse(char* InputBuffer) const; - string _XMLRequestBuffer; - uint16_t _XMLRequestBufferMax; - - RequestProperties_t _RequestProperties; - RequestsMap_t _Requests; - - void* _grammarPool; //- xercesc::XMLGrammarPool* (opaque to avoid Xerces in public header) +private: + std::size_t _ParseBufferSize; + std::shared_ptr _GrammarPool; }; diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index f7aa361..9f7c7d8 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(string-functions) add_subdirectory(vector-multi-erase) add_subdirectory(signal-termination) add_subdirectory(custom-vector) +add_subdirectory(xml-parser) diff --git a/test/integration/xml-parser/CMakeLists.txt b/test/integration/xml-parser/CMakeLists.txt new file mode 100644 index 0000000..fc95dd9 --- /dev/null +++ b/test/integration/xml-parser/CMakeLists.txt @@ -0,0 +1,4 @@ +add_executable(test-xml-parser-e2e test-xml-parser-e2e.cpp) + +target_link_libraries(test-xml-parser-e2e ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(test-xml-parser-e2e xmlparser) diff --git a/test/integration/xml-parser/test-xml-parser-e2e.cpp b/test/integration/xml-parser/test-xml-parser-e2e.cpp new file mode 100644 index 0000000..02de7f3 --- /dev/null +++ b/test/integration/xml-parser/test-xml-parser-e2e.cpp @@ -0,0 +1,41 @@ +#define BOOST_TEST_MAIN +#include + +#include +#include +#include + +#include "../../../lib/xml/xmlparser.hpp" + +using namespace std; + +BOOST_AUTO_TEST_CASE(end_to_end_parse_request_and_response_stream) +{ + char InputBuffer[4096] = {0}; + + const string Request = + "request-1NLAP0.1NLAMP" + "
testapp1.local/healthFalcon
" + "
"; + + const string Response = + "response-1NLAP0.1NLAMP" + "
application/json
{\"ok\":true}" + "0ok" + "
"; + + const string Stream = Request + Response; + memcpy(InputBuffer, Stream.c_str(), Stream.size()); + + unique_ptr Parser = make_unique(4096); + ParseResult_t Result = Parser->parse(InputBuffer); + + BOOST_TEST(Result.ErrorCode == 0); + BOOST_TEST(Result.Results.size() == 2u); + + const XMLNode& ReqHost = Result.Results.at(0).at("NLAP").at("Request").at("Header").at("Host"); + const XMLNode& ResCode = Result.Results.at(1).at("NLAP").at("Response").at("Status").at("Code"); + + BOOST_TEST(string(ReqHost.Address, ReqHost.Length) == "testapp1.local"); + BOOST_TEST(string(ResCode.Address, ResCode.Length) == "0"); +} diff --git a/test/performance/CMakeLists.txt b/test/performance/CMakeLists.txt index d35d443..dc432bb 100644 --- a/test/performance/CMakeLists.txt +++ b/test/performance/CMakeLists.txt @@ -6,3 +6,4 @@ add_executable(test-performance ${SRC_LIST_TEST_PERFORMANCE}) # link libraries target_link_libraries(test-performance ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +add_subdirectory(xml-parser) diff --git a/test/performance/xml-parser/CMakeLists.txt b/test/performance/xml-parser/CMakeLists.txt new file mode 100644 index 0000000..9702731 --- /dev/null +++ b/test/performance/xml-parser/CMakeLists.txt @@ -0,0 +1,4 @@ +add_executable(test-xml-parser-performance test-xml-parser-performance.cpp) + +target_link_libraries(test-xml-parser-performance ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(test-xml-parser-performance xmlparser) diff --git a/test/performance/xml-parser/test-xml-parser-performance.cpp b/test/performance/xml-parser/test-xml-parser-performance.cpp new file mode 100644 index 0000000..a4fc500 --- /dev/null +++ b/test/performance/xml-parser/test-xml-parser-performance.cpp @@ -0,0 +1,47 @@ +#define BOOST_TEST_MAIN +#include + +#include +#include +#include +#include + +#include "../../../lib/xml/xmlparser.hpp" + +using namespace std; + +namespace +{ + +vector toMutableBuffer(const string& Input) +{ + vector Buffer(Input.begin(), Input.end()); + Buffer.push_back('\0'); + return Buffer; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(xml_parser_minor_performance_smoke) +{ + const string Payload = + "perf-uuidNLAP0.1NLAMP" + "
perf.local/perfperf-client
" + "
"; + + vector Buffer = toMutableBuffer(Payload); + unique_ptr Parser = make_unique(4096); + + const auto Start = chrono::steady_clock::now(); + + for (size_t Iteration = 0; Iteration < 1000; ++Iteration) { + ParseResult_t Result = Parser->parse(Buffer.data()); + BOOST_TEST(Result.ErrorCode == 0); + BOOST_TEST(Result.Results.size() == 1u); + } + + const auto End = chrono::steady_clock::now(); + const auto DurationUS = chrono::duration_cast(End - Start).count(); + + BOOST_TEST(DurationUS > 0); +} diff --git a/test/unit/xml-parser/CMakeLists.txt b/test/unit/xml-parser/CMakeLists.txt index 0f2999a..9962537 100644 --- a/test/unit/xml-parser/CMakeLists.txt +++ b/test/unit/xml-parser/CMakeLists.txt @@ -1,25 +1,14 @@ -# add source dir -aux_source_directory(. SRC_LIST_TEST_XMLPARSER) +add_executable(test-xml-parser test-xml-parser.cpp) +add_executable(test-xml-parser-fuzz test-xml-parser-fuzz.cpp) -add_executable( - test-xml-parser - "test-xml-parser.cpp" -) - -# add custom targets add_custom_target( TestXMLParserHeader SOURCES ../../../lib/xml/xmlparser.hpp ) -# set absolute path to DTD so tests can be run from any directory -target_compile_definitions( - test-xml-parser - PRIVATE - NLAP_DTD_PATH="${CMAKE_SOURCE_DIR}/specs/xml/nlap.dtd" -) - -# link libraries target_link_libraries(test-xml-parser ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) target_link_libraries(test-xml-parser xmlparser) + +target_link_libraries(test-xml-parser-fuzz ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) +target_link_libraries(test-xml-parser-fuzz xmlparser) diff --git a/test/unit/xml-parser/test-xml-parser-fuzz.cpp b/test/unit/xml-parser/test-xml-parser-fuzz.cpp new file mode 100644 index 0000000..4c4052e --- /dev/null +++ b/test/unit/xml-parser/test-xml-parser-fuzz.cpp @@ -0,0 +1,69 @@ +#define BOOST_TEST_MAIN +#include + +#include +#include +#include +#include + +#include "../../../lib/xml/xmlconstants.hpp" +#include "../../../lib/xml/xmlparser.hpp" + +using namespace std; + +namespace +{ + +vector toMutableBuffer(const string& Input) +{ + vector Buffer(Input.begin(), Input.end()); + Buffer.push_back('\0'); + return Buffer; +} + +string mutateOneCharacter(const string& Seed, mt19937& RNG) +{ + if (Seed.empty()) { + return Seed; + } + + uniform_int_distribution IndexDist(0, Seed.size() - 1); + uniform_int_distribution ByteDist(33, 126); + + string Output = Seed; + Output[IndexDist(RNG)] = static_cast(ByteDist(RNG)); + return Output; +} + +const string SEED_XML = + "" + "" + "123e4567-e89b-12d3-a456-426614174000" + "NLAP" + "0.1" + "NLAMP" + "
testapp1.local
" + "
" + "
"; + +} // namespace + +BOOST_AUTO_TEST_CASE(xml_parser_fuzz_like_mutation_sweep) +{ + unique_ptr Parser = make_unique(8192); + mt19937 RNG(42); + + for (size_t Iteration = 0; Iteration < 200; ++Iteration) { + const string Mutated = mutateOneCharacter(SEED_XML, RNG); + vector Buffer = toMutableBuffer(Mutated); + ParseResult_t Result = Parser->parse(Buffer.data()); + + const bool IsKnownResult = + Result.ErrorCode == 0 || + Result.ErrorCode == XML_ERROR_INVALID_CONTENT_DTD || + Result.ErrorCode == XML_ERROR_INVALID_SYNTAX || + Result.ErrorCode == XML_ERROR_INVALID_FRAMING; + + BOOST_TEST(IsKnownResult); + } +} diff --git a/test/unit/xml-parser/test-xml-parser.cpp b/test/unit/xml-parser/test-xml-parser.cpp index b4fc224..f5cc27b 100644 --- a/test/unit/xml-parser/test-xml-parser.cpp +++ b/test/unit/xml-parser/test-xml-parser.cpp @@ -1,1014 +1,137 @@ #define BOOST_TEST_MAIN #include + #include -#include #include +#include -#include "../../../lib/xml/xmlparser.hpp" #include "../../../lib/xml/xmlconstants.hpp" +#include "../../../lib/xml/xmlparser.hpp" using namespace std; -// --------------------------------------------------------------------------- -// Valid NLAMP Request -// --------------------------------------------------------------------------- - -static const string NLAMP_REQUEST_FULL( - "" - "" - "9b327afe-27ae-2367-aef2-e42445e5b23a" - "NLAP" - "0.1" - "NLAMP" - "
" - "testapp2.local" - "/python/service1" - "Falcon-Python-Client" - "
" - "{\"param1\":\"string1\",\"param2\":\"string2\",\"param3\":100}" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Valid NLAMP Response (success, JSON payload) -// --------------------------------------------------------------------------- - -static const string NLAMP_RESPONSE_SUCCESS( - "" - "" - "9b327afe-27ae-2367-aef2-e42445e5b23a" - "NLAP" - "0.1" - "NLAMP" - "
" - "application/json" - "UTF-8" - "
" - "{ \"Result\": 100 }" - "0" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Valid NLAMP Response (failure, status with description + exception) -// --------------------------------------------------------------------------- - -static const string NLAMP_RESPONSE_FAILURE( - "" - "" - "9b327afe-27ae-2367-aef2-e42445e5b23a" - "NLAP" - "0.1" - "NLAMP" - "
" - "application/xml" - "UTF-8" - "
" - "" - "10" - "Application Exception" - "NameError: name test is not defined" - "" - "
" - "
" -); +namespace +{ -// --------------------------------------------------------------------------- -// Valid NLAFP Request (static HTML file) -// --------------------------------------------------------------------------- +vector toMutableBuffer(const string& Input) +{ + vector Buffer(Input.begin(), Input.end()); + Buffer.push_back('\0'); + return Buffer; +} -static const string NLAFP_REQUEST_STATIC_FILE( +const string VALID_REQUEST = "" "" - "7ea45c8a-5193-4855-b9e8-77ae1b9d49ed" + "123e4567-e89b-12d3-a456-426614174000" "NLAP" "0.1" - "NLAFP" + "NLAMP" "
" "testapp1.local" "/testpath/index.html" "Falcon-Browser" "
" "
" - "
" -); + "
"; -// --------------------------------------------------------------------------- -// Valid NLAFP Response (single-file, encoded binary) -// --------------------------------------------------------------------------- - -static const string NLAFP_RESPONSE_ENCODED_FILE( +const string VALID_RESPONSE = "" "" - "9a728a72-34ac-9abc-2245-af65cbde66ff" - "NLAP" - "0.1" - "NLAFP" - "
" - "image/png" - "binary" - "7342" - "none" - "
" - "BINARY_PAYLOAD_DATA" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Valid NLAFP Partial-Transfer Response (first part of multi-part file) -// --------------------------------------------------------------------------- - -static const string NLAFP_RESPONSE_PARTIAL_FIRST( - "" - "" - "f3477af2-1212-76af-3377-bc7721afbc7a" - "NLAP" - "0.1" - "NLAFP" - "
" - "application/bzip2" - "binary" - "3432132" - "100000" - "none" - "f3477af2-1212-76af-3377-bc7721afbc7a" - "35" - "1" - "
" - "PART_ONE_DATA" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Valid NLAPS Request (encrypted + signed) -// --------------------------------------------------------------------------- - -static const string NLAPS_REQUEST_ENCRYPTED( - "" - "" - "a2327a55-33ae-2557-aef2-e42445e5b23a" - "NLAP" - "0.1" - "NLAPS" - "
" - "testapp2.local" - "/python/service1" - "Falcon-Python-Client" - "user1@domain.com" - "
" - "" - "1" - "BASE64SIGHERE" - "" - "BASE64ENCRYPTEDPAYLOAD" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Valid NLAPS Response (encrypted + signed) -// --------------------------------------------------------------------------- - -static const string NLAPS_RESPONSE_ENCRYPTED( - "" - "" - "a2327a55-33ae-2557-aef2-e42445e5b23a" - "NLAP" - "0.1" - "NLAPS" - "
" - "application/json" - "UTF-8" - "
" - "" - "1" - "BASE64SIGHERE" - "" - "BASE64ENCRYPTEDPAYLOAD" - "0" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// Message that already has an XML declaration -// --------------------------------------------------------------------------- - -static const string NLAMP_REQUEST_WITH_XMLDECL( - "" - "" - "" - "decl-uuid-1234" - "NLAP" - "0.1" - "NLAMP" - "
declared.local
" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: malformed XML (unclosed tag) -// --------------------------------------------------------------------------- - -static const string INVALID_MALFORMED_XML( - "" - "" - "bad-uuid" + "123e4567-e89b-12d3-a456-426614174001" "NLAP" "0.1" "NLAMP" - "
h.local" // Header not closed - "" - "" -); - -// --------------------------------------------------------------------------- -// INVALID: missing required UUID element -// --------------------------------------------------------------------------- - -static const string INVALID_MISSING_UUID( - "" - "" - "NLAP" - "0.1" - "NLAMP" - "
h.local
" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: missing required Protocol element -// --------------------------------------------------------------------------- - -static const string INVALID_MISSING_PROTOCOL( - "" - "" - "some-uuid" - "0.1" - "NLAMP" - "
h.local
" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: wrong root element (not NLAP) -// --------------------------------------------------------------------------- - -static const string INVALID_WRONG_ROOT( - "" - "" - "some-uuid" - "NLAP" - "0.1" - "NLAMP" - "
h.local
" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: unknown element in Request body (not in DTD) -// --------------------------------------------------------------------------- - -static const string INVALID_UNKNOWN_ELEMENT( - "" - "" - "some-uuid" - "NLAP" - "0.1" - "NLAMP" - "
h.local
" - "some-value" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: Security block missing required Signature child -// --------------------------------------------------------------------------- - -static const string INVALID_SECURITY_MISSING_SIGNATURE( - "" - "" - "sec-uuid" - "NLAP" - "0.1" - "NLAPS" - "
h.local
" - "" - "1" - "" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: Security block missing required Encryption child -// --------------------------------------------------------------------------- - -static const string INVALID_SECURITY_MISSING_ENCRYPTION( - "" - "" - "sec-uuid" - "NLAP" - "0.1" - "NLAPS" - "
h.local
" - "" - "BASE64SIG" - "" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: Status block missing required Code child -// --------------------------------------------------------------------------- - -static const string INVALID_STATUS_MISSING_CODE( - "" - "" - "stat-uuid" - "NLAP" - "0.1" - "NLAMP" - "
application/json
" - "" - "No Code here" - "" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: Header children in wrong DTD sequence order -// (Encoding before Mime-Type is allowed per DTD ordering: -// Host?, URL?, User-Agent?, user?, Connection-Close?, Mime-Type?, Encoding? -// but Byte-Size before Mime-Type breaks the sequence) -// --------------------------------------------------------------------------- - -static const string INVALID_HEADER_WRONG_ORDER( - "" - "" - "order-uuid" - "NLAP" - "0.1" - "NLAFP" - "
" - "1234" - "image/png" // Mime-Type must come before Byte-Size in DTD - "
" + "
text/plain
" + "0ok" "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: XXE injection attempt via external entity in DOCTYPE -// --------------------------------------------------------------------------- - -static const string INVALID_XXE_ATTEMPT( - "" - "" - "]>" - "" - "" - "&xxe;" - "NLAP" - "0.1" - "NLAMP" - "
h.local
" - "
" - "
" -); - -// --------------------------------------------------------------------------- -// INVALID: empty string (no end marker - never triggers processing) -// --------------------------------------------------------------------------- - -static const string INVALID_NO_END_MARKER( - "" - "" - "partial-uuid" - "NLAP" - // message deliberately truncated - no -); - + ""; -// =========================================================================== -// TEST CASES - VALID INPUT -// =========================================================================== - -BOOST_AUTO_TEST_CASE( test_valid_nlamp_request_full_transmit ) -{ - cout << "Valid NLAMP Request (full transmit)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_REQUEST_FULL.c_str(), NLAMP_REQUEST_FULL.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - - auto& r = requests.at(0); - cout << "RequestType=" << r.RequestType << " UUID=" << r.UUID - << " Protocol=" << r.Protocol << " Version=" << r.Version - << " Subtype=" << r.Subtype << endl; - - BOOST_TEST(r.RequestType == "Request"); - BOOST_TEST(r.UUID == "9b327afe-27ae-2367-aef2-e42445e5b23a"); - BOOST_TEST(r.Protocol == "NLAP"); - BOOST_TEST(r.Version == "0.1"); - BOOST_TEST(r.Subtype == "NLAMP"); - BOOST_TEST(r.Header.count("Host") == 1u); - BOOST_TEST(r.Header.at("Host") == "testapp2.local"); - BOOST_TEST(r.Header.count("URL") == 1u); - BOOST_TEST(r.Header.at("URL") == "/python/service1"); - BOOST_TEST(r.Header.count("User-Agent") == 1u); - BOOST_TEST(r.Header.at("User-Agent") == "Falcon-Python-Client"); - BOOST_TEST(!r.Payload.empty()); - BOOST_TEST(r.XMLRawMessage.substr(0, 5) == " Parser = make_unique(4096); + vector Buffer = toMutableBuffer(VALID_REQUEST); - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_RESPONSE_SUCCESS.c_str(), NLAMP_RESPONSE_SUCCESS.length()); + ParseResult_t Result = Parser->parse(Buffer.data()); - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; + BOOST_TEST(Result.ErrorCode == 0); + BOOST_TEST(Result.Results.size() == 1u); - BOOST_TEST(rs == 1); - auto& r = requests.at(0); + const XMLNode& URLNode = Result.Results.at(0).at("NLAP").at("Request").at("Header").at("URL"); + BOOST_TEST(URLNode.Address != nullptr); + BOOST_TEST(URLNode.Length == string("/testpath/index.html").size()); - BOOST_TEST(r.RequestType == "Response"); - BOOST_TEST(r.UUID == "9b327afe-27ae-2367-aef2-e42445e5b23a"); - BOOST_TEST(r.Subtype == "NLAMP"); - BOOST_TEST(r.Header.at("Mime-Type") == "application/json"); - BOOST_TEST(r.Header.at("Encoding") == "UTF-8"); - BOOST_TEST(!r.Payload.empty()); - BOOST_TEST(r.StatusCode == "0"); + string URL(URLNode.Address, URLNode.Length); + BOOST_TEST(URL == "/testpath/index.html"); + BOOST_TEST(URLNode.Address >= Buffer.data()); + BOOST_TEST(URLNode.Address < (Buffer.data() + Buffer.size())); } -BOOST_AUTO_TEST_CASE( test_valid_nlamp_response_failure_status ) +BOOST_AUTO_TEST_CASE(parse_multiple_messages_returns_vector) { - cout << "Valid NLAMP Response failure with full Status block." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_RESPONSE_FAILURE.c_str(), NLAMP_RESPONSE_FAILURE.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; + unique_ptr Parser = make_unique(8192); + vector Buffer = toMutableBuffer(VALID_REQUEST + VALID_RESPONSE); - BOOST_TEST(rs == 1); - auto& r = requests.at(0); + ParseResult_t Result = Parser->parse(Buffer.data()); - BOOST_TEST(r.RequestType == "Response"); - BOOST_TEST(r.Subtype == "NLAMP"); - BOOST_TEST(r.StatusCode == "10"); - BOOST_TEST(r.StatusDescription == "Application Exception"); - BOOST_TEST(!r.StatusException.empty()); + BOOST_TEST(Result.ErrorCode == 0); + BOOST_TEST(Result.Results.size() == 2u); } -BOOST_AUTO_TEST_CASE( test_valid_nlafp_request_static_file ) +BOOST_AUTO_TEST_CASE(parse_invalid_syntax_returns_error) { - cout << "Valid NLAFP Request (static file, no payload)." << endl; + unique_ptr Parser = make_unique(4096); - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAFP_REQUEST_STATIC_FILE.c_str(), NLAFP_REQUEST_STATIC_FILE.length()); + const string Broken = "xNLAP"; + vector Buffer = toMutableBuffer(Broken); - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; + ParseResult_t Result = Parser->parse(Buffer.data()); - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Request"); - BOOST_TEST(r.UUID == "7ea45c8a-5193-4855-b9e8-77ae1b9d49ed"); - BOOST_TEST(r.Subtype == "NLAFP"); - BOOST_TEST(r.Header.at("Host") == "testapp1.local"); - BOOST_TEST(r.Header.at("URL") == "/testpath/index.html"); - BOOST_TEST(r.Header.at("User-Agent") == "Falcon-Browser"); - BOOST_TEST(r.Payload.empty()); -} - -BOOST_AUTO_TEST_CASE( test_valid_nlafp_response_encoded_file ) -{ - cout << "Valid NLAFP Response (single encoded file with file headers)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAFP_RESPONSE_ENCODED_FILE.c_str(), NLAFP_RESPONSE_ENCODED_FILE.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Response"); - BOOST_TEST(r.Subtype == "NLAFP"); - BOOST_TEST(r.Header.at("Mime-Type") == "image/png"); - BOOST_TEST(r.Header.at("Byte-Size") == "7342"); - BOOST_TEST(r.Header.at("Compression") == "none"); - BOOST_TEST(r.Header.at("Encoding") == "binary"); - BOOST_TEST(!r.Payload.empty()); -} - -BOOST_AUTO_TEST_CASE( test_valid_nlafp_response_partial_transfer ) -{ - cout << "Valid NLAFP Partial Transfer Response (multi-part file headers)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAFP_RESPONSE_PARTIAL_FIRST.c_str(), NLAFP_RESPONSE_PARTIAL_FIRST.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Response"); - BOOST_TEST(r.Subtype == "NLAFP"); - BOOST_TEST(r.Header.at("Byte-Size-Full") == "3432132"); - BOOST_TEST(r.Header.at("Byte-Size-Part") == "100000"); - BOOST_TEST(r.Header.at("File-UUID") == "f3477af2-1212-76af-3377-bc7721afbc7a"); - BOOST_TEST(r.Header.at("File-Part-Sum") == "35"); - BOOST_TEST(r.Header.at("File-Part") == "1"); -} - -BOOST_AUTO_TEST_CASE( test_valid_nlaps_request_encrypted_signed ) -{ - cout << "Valid NLAPS Request (encrypted + signed with Security block)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAPS_REQUEST_ENCRYPTED.c_str(), NLAPS_REQUEST_ENCRYPTED.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Request"); - BOOST_TEST(r.Subtype == "NLAPS"); - BOOST_TEST(r.Header.count("user") == 1u); - BOOST_TEST(r.Header.at("user") == "user1@domain.com"); - BOOST_TEST(r.Encryption == "1"); - BOOST_TEST(r.Signature == "BASE64SIGHERE"); - BOOST_TEST(!r.Payload.empty()); -} - -BOOST_AUTO_TEST_CASE( test_valid_nlaps_response_encrypted_signed ) -{ - cout << "Valid NLAPS Response (encrypted + signed + Status)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAPS_RESPONSE_ENCRYPTED.c_str(), NLAPS_RESPONSE_ENCRYPTED.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Response"); - BOOST_TEST(r.Subtype == "NLAPS"); - BOOST_TEST(r.Encryption == "1"); - BOOST_TEST(r.Signature == "BASE64SIGHERE"); - BOOST_TEST(r.StatusCode == "0"); -} - -BOOST_AUTO_TEST_CASE( test_valid_message_with_existing_xml_declaration ) -{ - cout << "Valid message that already has an XML declaration." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_REQUEST_WITH_XMLDECL.c_str(), NLAMP_REQUEST_WITH_XMLDECL.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - auto& r = requests.at(0); - - BOOST_TEST(r.RequestType == "Request"); - BOOST_TEST(r.UUID == "decl-uuid-1234"); - BOOST_TEST(r.XMLRawMessage.substr(0, 5) == " parser = make_unique(8192); - parser->appendBuffer(combined.c_str(), combined.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 3); - BOOST_TEST(requests.at(0).RequestType == "Request"); - BOOST_TEST(requests.at(0).Subtype == "NLAMP"); - BOOST_TEST(requests.at(1).RequestType == "Response"); - BOOST_TEST(requests.at(1).Subtype == "NLAMP"); - BOOST_TEST(requests.at(2).RequestType == "Request"); - BOOST_TEST(requests.at(2).Subtype == "NLAFP"); -} - -BOOST_AUTO_TEST_CASE( test_valid_fragmented_delivery_two_parts ) -{ - cout << "Valid: NLAMP request split across two appendBuffer calls." << endl; - - const size_t midPoint = NLAMP_REQUEST_FULL.length() / 2; - const char* data = NLAMP_REQUEST_FULL.c_str(); - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(data, static_cast(midPoint)); - - // No complete message yet - BOOST_TEST(parser->getRequests().size() == 0u); - - parser->appendBuffer(data + midPoint, static_cast(NLAMP_REQUEST_FULL.length() - midPoint)); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count after second part: " << rs << endl; - - BOOST_TEST(rs == 1); - BOOST_TEST(requests.at(0).RequestType == "Request"); - BOOST_TEST(requests.at(0).UUID == "9b327afe-27ae-2367-aef2-e42445e5b23a"); -} - -BOOST_AUTO_TEST_CASE( test_valid_byte_by_byte_delivery ) -{ - cout << "Valid: NLAMP request delivered byte by byte." << endl; - - unique_ptr parser = make_unique(4096); - - const char* data = NLAMP_REQUEST_FULL.c_str(); - for (size_t i = 0; i < NLAMP_REQUEST_FULL.length(); ++i) { - parser->appendBuffer(data + i, 1); - } - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 1); - BOOST_TEST(requests.at(0).RequestType == "Request"); - BOOST_TEST(requests.at(0).UUID == "9b327afe-27ae-2367-aef2-e42445e5b23a"); -} - -BOOST_AUTO_TEST_CASE( test_valid_multiple_messages_byte_by_byte ) -{ - cout << "Valid: multiple NLAP messages delivered byte by byte." << endl; - - string combined; - combined += NLAMP_REQUEST_FULL; - combined += NLAFP_REQUEST_STATIC_FILE; - - unique_ptr parser = make_unique(8192); - - const char* data = combined.c_str(); - for (size_t i = 0; i < combined.length(); ++i) { - parser->appendBuffer(data + i, 1); - } - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << endl; - - BOOST_TEST(rs == 2); - BOOST_TEST(requests.at(0).Subtype == "NLAMP"); - BOOST_TEST(requests.at(1).Subtype == "NLAFP"); -} - -BOOST_AUTO_TEST_CASE( test_valid_xmlrawmessage_starts_with_xml_declaration ) -{ - cout << "Valid: XMLRawMessage always starts with ' parser = make_unique(4096); - parser->appendBuffer(NLAMP_REQUEST_FULL.c_str(), NLAMP_REQUEST_FULL.length()); - - RequestsMap_t requests = parser->getRequests(); - BOOST_TEST(requests.size() == 1u); - - const string& raw = requests.at(0).XMLRawMessage; - cout << "XMLRawMessage prefix: " << raw.substr(0, 20) << endl; - - BOOST_TEST(raw.substr(0, 5) == " parser = make_unique(8192); - parser->appendBuffer(combined.c_str(), combined.length()); - - BOOST_TEST(parser->getRequests().size() == 2u); - - auto r1 = parser->getNextRequest(); - auto r2 = parser->getNextRequest(); - - BOOST_TEST(r1 != nullptr); - BOOST_TEST(r2 != nullptr); - - BOOST_TEST(r1->RequestType == "Request"); - BOOST_TEST(r2->RequestType == "Response"); -} - -BOOST_AUTO_TEST_CASE( test_valid_removeRequest ) -{ - cout << "Valid: removeRequest removes from the map." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_REQUEST_FULL.c_str(), NLAMP_REQUEST_FULL.length()); - - BOOST_TEST(parser->getRequests().size() == 1u); - - parser->removeRequest(0); - - BOOST_TEST(parser->getRequests().size() == 0u); -} - -BOOST_AUTO_TEST_CASE( test_valid_response_status_code_only ) -{ - cout << "Valid: Response with Status containing only Code (Description and Exception optional)." << endl; - - // Reuse NLAMP_RESPONSE_SUCCESS which has 0 - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAMP_RESPONSE_SUCCESS.c_str(), NLAMP_RESPONSE_SUCCESS.length()); - - RequestsMap_t requests = parser->getRequests(); - BOOST_TEST(requests.size() == 1u); - - auto& r = requests.at(0); - BOOST_TEST(r.StatusCode == "0"); - BOOST_TEST(r.StatusDescription == ""); - BOOST_TEST(r.StatusException == ""); -} - -BOOST_AUTO_TEST_CASE( test_valid_header_user_field ) -{ - cout << "Valid: Header 'user' field (NLAPS user identity) parsed correctly." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(NLAPS_REQUEST_ENCRYPTED.c_str(), NLAPS_REQUEST_ENCRYPTED.length()); - - RequestsMap_t requests = parser->getRequests(); - BOOST_TEST(requests.size() == 1u); - - BOOST_TEST(requests.at(0).Header.count("user") == 1u); - BOOST_TEST(requests.at(0).Header.at("user") == "user1@domain.com"); -} - -// =========================================================================== -// TEST CASES - INVALID INPUT -// =========================================================================== - -BOOST_AUTO_TEST_CASE( test_invalid_no_end_marker ) -{ - cout << "Invalid: no end marker - message not processed." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_NO_END_MARKER.c_str(), INVALID_NO_END_MARKER.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_malformed_xml ) -{ - cout << "Invalid: malformed XML (unclosed tag) - rejected by parser." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_MALFORMED_XML.c_str(), INVALID_MALFORMED_XML.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_missing_uuid ) -{ - cout << "Invalid: missing UUID element - DTD validation rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_MISSING_UUID.c_str(), INVALID_MISSING_UUID.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_missing_protocol ) -{ - cout << "Invalid: missing Protocol element - DTD validation rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_MISSING_PROTOCOL.c_str(), INVALID_MISSING_PROTOCOL.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_wrong_root_element ) -{ - cout << "Invalid: wrong root element (not NLAP) - DTD validation rejects." << endl; - - unique_ptr parser = make_unique(4096); - // The end marker is not present, so the split on NLAP_XML_END_MARKER - // will never trigger processing. The message is therefore never parsed. - string bad = INVALID_WRONG_ROOT; - parser->appendBuffer(bad.c_str(), bad.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_unknown_element_in_request ) -{ - cout << "Invalid: unknown element in Request body - DTD validation rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_UNKNOWN_ELEMENT.c_str(), INVALID_UNKNOWN_ELEMENT.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_security_missing_signature ) -{ - cout << "Invalid: Security block missing required Signature - DTD rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_SECURITY_MISSING_SIGNATURE.c_str(), - INVALID_SECURITY_MISSING_SIGNATURE.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_security_missing_encryption ) -{ - cout << "Invalid: Security block missing required Encryption - DTD rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_SECURITY_MISSING_ENCRYPTION.c_str(), - INVALID_SECURITY_MISSING_ENCRYPTION.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_status_missing_code ) -{ - cout << "Invalid: Status block missing required Code element - DTD rejects." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_STATUS_MISSING_CODE.c_str(), - INVALID_STATUS_MISSING_CODE.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_header_wrong_element_order ) -{ - cout << "Invalid: Header children in wrong sequence order (Byte-Size before Mime-Type)." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_HEADER_WRONG_ORDER.c_str(), - INVALID_HEADER_WRONG_ORDER.length()); - - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; - - BOOST_TEST(rs == 0u); -} - -BOOST_AUTO_TEST_CASE( test_invalid_xxe_injection_attempt ) -{ - cout << "Invalid/Security: XXE injection attempt - external entity must not be resolved." << endl; - - unique_ptr parser = make_unique(4096); - parser->appendBuffer(INVALID_XXE_ATTEMPT.c_str(), INVALID_XXE_ATTEMPT.length()); - - // XXE must not succeed - message is rejected (entity expanded to empty, UUID becomes "") - // meaning the message either fails validation or the entity is empty. - // Either way, /etc/passwd content must NOT appear in any field. - RequestsMap_t requests = parser->getRequests(); - bool passwdLeaked = false; - for (auto& kv : requests) { - if (kv.second.UUID.find("root:") != string::npos) { - passwdLeaked = true; - } - } - cout << "Passwd leaked: " << (passwdLeaked ? "YES (FAIL)" : "NO (OK)") << endl; - BOOST_TEST(!passwdLeaked); + BOOST_TEST(Result.ErrorCode == XML_ERROR_INVALID_SYNTAX); + BOOST_TEST(Result.Results.empty()); } -BOOST_AUTO_TEST_CASE( test_invalid_buffer_overflow ) +BOOST_AUTO_TEST_CASE(parse_invalid_dtd_content_returns_error) { - cout << "Invalid: data exceeds buffer max - appendBuffer silently drops data." << endl; + unique_ptr Parser = make_unique(4096); - // Buffer max = 64 bytes, message is much larger - unique_ptr parser = make_unique(64); - parser->appendBuffer(NLAMP_REQUEST_FULL.c_str(), NLAMP_REQUEST_FULL.length()); + const string InvalidDTD = + "NLAP0.1NLAMP
"; + vector Buffer = toMutableBuffer(InvalidDTD); - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; + ParseResult_t Result = Parser->parse(Buffer.data()); - BOOST_TEST(rs == 0u); + BOOST_TEST(Result.ErrorCode == XML_ERROR_INVALID_CONTENT_DTD); + BOOST_TEST(Result.Results.empty()); } -BOOST_AUTO_TEST_CASE( test_invalid_empty_buffer ) +BOOST_AUTO_TEST_CASE(parse_invalid_framing_returns_error) { - cout << "Invalid: empty buffer - nothing parsed." << endl; + unique_ptr Parser = make_unique(8192); - unique_ptr parser = make_unique(4096); - const char* empty = ""; - parser->appendBuffer(empty, 0); + const string Framed = VALID_REQUEST + string("BAD") + VALID_RESPONSE; + vector Buffer = toMutableBuffer(Framed); - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 0)" << endl; + ParseResult_t Result = Parser->parse(Buffer.data()); - BOOST_TEST(rs == 0u); + BOOST_TEST(Result.ErrorCode == XML_ERROR_INVALID_FRAMING); + BOOST_TEST(Result.Results.empty()); } -BOOST_AUTO_TEST_CASE( test_invalid_mixed_valid_invalid_in_one_buffer ) +BOOST_AUTO_TEST_CASE(parse_buffer_size_runtime_override) { - cout << "Invalid: one valid + one invalid message in same buffer - only valid accepted." << endl; + unique_ptr Parser = make_unique(32); - string combined; - combined += NLAMP_REQUEST_FULL; // valid - combined += INVALID_MISSING_UUID; // invalid + BOOST_TEST(Parser->getParseBufferSize() == 32u); - unique_ptr parser = make_unique(8192); - parser->appendBuffer(combined.c_str(), combined.length()); + Parser->setParseBufferSize(8192); + BOOST_TEST(Parser->getParseBufferSize() == 8192u); - RequestsMap_t requests = parser->getRequests(); - auto rs = requests.size(); - cout << "Parsed count: " << rs << " (expect 1)" << endl; + vector Buffer = toMutableBuffer(VALID_REQUEST); + ParseResult_t Result = Parser->parse(Buffer.data()); - BOOST_TEST(rs == 1u); - BOOST_TEST(requests.at(0).Subtype == "NLAMP"); + BOOST_TEST(Result.ErrorCode == 0); + BOOST_TEST(Result.Results.size() == 1u); } From 65d43bdfd3f9ad80244a0b7fc504fbc3dce04e0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:27:17 +0000 Subject: [PATCH 2/5] Fix XML parser error classification and unit checks --- lib/xml/xmlparser.cpp | 42 +++++++----------------- test/unit/xml-parser/test-xml-parser.cpp | 10 ++++-- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/lib/xml/xmlparser.cpp b/lib/xml/xmlparser.cpp index be4c291..0a44254 100644 --- a/lib/xml/xmlparser.cpp +++ b/lib/xml/xmlparser.cpp @@ -220,44 +220,30 @@ class ParseErrorHandler : public HandlerBase void error(const SAXParseException& Exception) override { - classify(Exception); + _HasAnyError = true; + _LastMessage = transcodeXMLCh(Exception.getMessage()); } void fatalError(const SAXParseException& Exception) override { - classify(Exception); + _HasAnyError = true; + _LastMessage = transcodeXMLCh(Exception.getMessage()); } void warning(const SAXParseException&) override {} void resetErrors() override { - _HasSyntaxError = false; - _HasDTDContentError = false; + _HasAnyError = false; + _LastMessage.clear(); } - bool hasSyntaxError() const { return _HasSyntaxError; } - bool hasDTDContentError() const { return _HasDTDContentError; } + bool hasAnyError() const { return _HasAnyError; } + const std::string& lastMessage() const { return _LastMessage; } private: - void classify(const SAXParseException& Exception) - { - const std::string Message = transcodeXMLCh(Exception.getMessage()); - if (Message.find("element") != std::string::npos && Message.find("must") != std::string::npos) { - _HasDTDContentError = true; - return; - } - - if (Message.find("valid") != std::string::npos && Message.find("content") != std::string::npos) { - _HasDTDContentError = true; - return; - } - - _HasSyntaxError = true; - } - - bool _HasSyntaxError = false; - bool _HasDTDContentError = false; + bool _HasAnyError = false; + std::string _LastMessage; }; void populateTree(DOMElement* Element, XMLNode& Node, std::string_view RawMessage, std::size_t& SearchOffset) @@ -315,7 +301,7 @@ uint16_t parseMessage( ); SyntaxParser.parse(SyntaxSource); - if (SyntaxHandler.hasSyntaxError()) { + if (SyntaxHandler.hasAnyError()) { return XML_ERROR_INVALID_SYNTAX; } @@ -340,14 +326,10 @@ uint16_t parseMessage( ); DTDParser.parse(DTDSource); - if (DTDHandler.hasDTDContentError()) { + if (DTDHandler.hasAnyError()) { return XML_ERROR_INVALID_CONTENT_DTD; } - if (DTDHandler.hasSyntaxError()) { - return XML_ERROR_INVALID_SYNTAX; - } - DOMDocument* Document = DTDParser.getDocument(); if (Document == nullptr) { return XML_ERROR_INVALID_SYNTAX; diff --git a/test/unit/xml-parser/test-xml-parser.cpp b/test/unit/xml-parser/test-xml-parser.cpp index f5cc27b..7b33710 100644 --- a/test/unit/xml-parser/test-xml-parser.cpp +++ b/test/unit/xml-parser/test-xml-parser.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "../../../lib/xml/xmlconstants.hpp" @@ -65,8 +66,11 @@ BOOST_AUTO_TEST_CASE(parse_single_message_and_validate_leaf_pointer) string URL(URLNode.Address, URLNode.Length); BOOST_TEST(URL == "/testpath/index.html"); - BOOST_TEST(URLNode.Address >= Buffer.data()); - BOOST_TEST(URLNode.Address < (Buffer.data() + Buffer.size())); + const auto URLAddr = reinterpret_cast(URLNode.Address); + const auto BufStart = reinterpret_cast(Buffer.data()); + const auto BufEnd = reinterpret_cast(Buffer.data() + Buffer.size()); + BOOST_TEST(URLAddr >= BufStart); + BOOST_TEST(URLAddr < BufEnd); } BOOST_AUTO_TEST_CASE(parse_multiple_messages_returns_vector) @@ -84,7 +88,7 @@ BOOST_AUTO_TEST_CASE(parse_invalid_syntax_returns_error) { unique_ptr Parser = make_unique(4096); - const string Broken = "xNLAP"; + const string Broken = "xNLAP"; vector Buffer = toMutableBuffer(Broken); ParseResult_t Result = Parser->parse(Buffer.data()); From 342c35f92a06eb0995f45f2adcbfc0fe36d0d058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:30:14 +0000 Subject: [PATCH 3/5] Harden XML parser against XXE during parse passes --- lib/xml/xmlparser.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/xml/xmlparser.cpp b/lib/xml/xmlparser.cpp index 0a44254..0c4dbb6 100644 --- a/lib/xml/xmlparser.cpp +++ b/lib/xml/xmlparser.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -246,6 +247,31 @@ class ParseErrorHandler : public HandlerBase std::string _LastMessage; }; +class BlockingEntityResolver : public HandlerBase +{ +public: + InputSource* resolveEntity(const XMLCh*, const XMLCh* SystemId) override + { + const std::string AllowedPath(NLAP_DTD_SYSTEM_PATH); + const std::string AllowedName("nlap.dtd"); + const std::string SystemPath = transcodeXMLCh(SystemId); + + if (!SystemPath.empty()) { + if (SystemPath == AllowedPath) { + return nullptr; + } + + if (SystemPath.size() >= AllowedName.size() && + SystemPath.compare(SystemPath.size() - AllowedName.size(), AllowedName.size(), AllowedName) == 0) { + return nullptr; + } + } + + static const XMLByte EmptyDocument[] = {}; + return new MemBufInputSource(EmptyDocument, 0, "blocked-entity", false); + } +}; + void populateTree(DOMElement* Element, XMLNode& Node, std::string_view RawMessage, std::size_t& SearchOffset) { bool HasElementChildren = false; @@ -287,11 +313,15 @@ uint16_t parseMessage( const std::string Parseable = ensureParseableXML(RawMessage); ParseErrorHandler SyntaxHandler; + BlockingEntityResolver SyntaxResolver; XercesDOMParser SyntaxParser; SyntaxParser.setErrorHandler(&SyntaxHandler); SyntaxParser.setValidationScheme(XercesDOMParser::Val_Never); SyntaxParser.setDoNamespaces(false); SyntaxParser.setDoSchema(false); + SyntaxParser.setLoadExternalDTD(false); + SyntaxParser.setCreateEntityReferenceNodes(false); + SyntaxParser.setEntityResolver(&SyntaxResolver); MemBufInputSource SyntaxSource( reinterpret_cast(Parseable.data()), @@ -306,6 +336,7 @@ uint16_t parseMessage( } ParseErrorHandler DTDHandler; + BlockingEntityResolver DTDResolver; auto* CastedPool = static_cast(GrammarPool.get()); XercesDOMParser DTDParser(nullptr, XMLPlatformUtils::fgMemoryManager, CastedPool); @@ -317,6 +348,7 @@ uint16_t parseMessage( DTDParser.useCachedGrammarInParse(true); DTDParser.setCreateEntityReferenceNodes(false); DTDParser.setValidationConstraintFatal(true); + DTDParser.setEntityResolver(&DTDResolver); MemBufInputSource DTDSource( reinterpret_cast(Parseable.data()), From 2c854efcfb1dee3d8b0d6bb2a053c192f739b69d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:38:01 +0000 Subject: [PATCH 4/5] Remove coroutine usage from XML parser implementation --- lib/xml/xmlparser.cpp | 97 ++++++------------------------------------- 1 file changed, 12 insertions(+), 85 deletions(-) diff --git a/lib/xml/xmlparser.cpp b/lib/xml/xmlparser.cpp index 0c4dbb6..5341a79 100644 --- a/lib/xml/xmlparser.cpp +++ b/lib/xml/xmlparser.cpp @@ -5,10 +5,10 @@ #include #include -#include #include #include #include +#include #include #include @@ -27,84 +27,6 @@ namespace static std::atomic_uint32_t XercesRefCount{0}; -template -class Generator -{ -public: - struct promise_type { - T CurrentValue{}; - - Generator get_return_object() - { - return Generator(std::coroutine_handle::from_promise(*this)); - } - - std::suspend_always initial_suspend() noexcept { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - std::suspend_always yield_value(T Value) noexcept - { - CurrentValue = std::move(Value); - return {}; - } - void return_void() {} - void unhandled_exception() { std::terminate(); } - }; - - class iterator - { - public: - explicit iterator(std::coroutine_handle Handle) : _Handle(Handle) {} - - iterator& operator++() - { - _Handle.resume(); - if (_Handle.done()) { - _Handle = {}; - } - return *this; - } - - const T& operator*() const - { - return _Handle.promise().CurrentValue; - } - - bool operator==(std::default_sentinel_t) const - { - return !_Handle; - } - - private: - std::coroutine_handle _Handle; - }; - - explicit Generator(std::coroutine_handle Handle) : _Handle(Handle) {} - Generator(Generator&& Other) noexcept : _Handle(Other._Handle) { Other._Handle = {}; } - - ~Generator() - { - if (_Handle) { - _Handle.destroy(); - } - } - - iterator begin() - { - if (_Handle) { - _Handle.resume(); - if (_Handle.done()) { - return iterator({}); - } - } - return iterator(_Handle); - } - - std::default_sentinel_t end() const { return {}; } - -private: - std::coroutine_handle _Handle; -}; - std::string transcodeXMLCh(const XMLCh* Input) { if (Input == nullptr) { @@ -147,25 +69,28 @@ std::string getElementText(DOMElement* Element) return trimCopy(Value); } -Generator iterateElementChildren(DOMElement* Parent) +std::vector iterateElementChildren(DOMElement* Parent) { + std::vector Elements; DOMNodeList* Children = Parent->getChildNodes(); for (XMLSize_t Index = 0; Index < Children->getLength(); ++Index) { DOMNode* Child = Children->item(Index); if (Child->getNodeType() == DOMNode::ELEMENT_NODE) { - co_yield static_cast(Child); + Elements.push_back(static_cast(Child)); } } + + return Elements; } struct MessageSlice { std::string_view Slice; - std::size_t StartOffset; }; -Generator splitMessages(std::string_view Input, bool& FramingError) +std::vector splitMessages(std::string_view Input, bool& FramingError) { + std::vector Messages; std::size_t Cursor = 0; std::size_t LastEnd = std::string_view::npos; @@ -177,7 +102,7 @@ Generator splitMessages(std::string_view Input, bool& FramingError if (LastEnd != std::string_view::npos && Start != LastEnd) { FramingError = true; - co_return; + return Messages; } const std::size_t End = Input.find(NLAP_XML_END_MARKER, Start); @@ -186,11 +111,13 @@ Generator splitMessages(std::string_view Input, bool& FramingError } const std::size_t MessageEnd = End + NLAP_XML_END_MARKER.size(); - co_yield MessageSlice{Input.substr(Start, MessageEnd - Start), Start}; + Messages.push_back(MessageSlice{Input.substr(Start, MessageEnd - Start)}); Cursor = MessageEnd; LastEnd = MessageEnd; } + + return Messages; } std::string ensureParseableXML(std::string_view Message) From d07c727979300aae532f7722a03e4ace3724940a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:53:02 +0000 Subject: [PATCH 5/5] Reapply XML parser specs using C++23 std::generator --- CMakeLists.txt | 4 ++-- lib/xml/xmlparser.cpp | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be4e84f..ea15537 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,9 +38,9 @@ endif() # set g++ compiler flags if ( DEFINED DEBUG_BUILD ) add_compile_definitions(DEBUG_BUILD) - set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -std=c++20 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -fwrapv -rdynamic -fno-elide-constructors") + set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -std=c++23 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -fwrapv -rdynamic -fno-elide-constructors") else() - set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -O3 -fstack-protector-strong -std=c++20 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -DNDEBUG -fwrapv -fno-elide-constructors") + set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -O3 -fstack-protector-strong -std=c++23 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -DNDEBUG -fwrapv -fno-elide-constructors") endif() # set boost specs diff --git a/lib/xml/xmlparser.cpp b/lib/xml/xmlparser.cpp index 5341a79..bf77afa 100644 --- a/lib/xml/xmlparser.cpp +++ b/lib/xml/xmlparser.cpp @@ -5,10 +5,10 @@ #include #include +#include #include #include #include -#include #include #include @@ -69,18 +69,15 @@ std::string getElementText(DOMElement* Element) return trimCopy(Value); } -std::vector iterateElementChildren(DOMElement* Parent) +std::generator iterateElementChildren(DOMElement* Parent) { - std::vector Elements; DOMNodeList* Children = Parent->getChildNodes(); for (XMLSize_t Index = 0; Index < Children->getLength(); ++Index) { DOMNode* Child = Children->item(Index); if (Child->getNodeType() == DOMNode::ELEMENT_NODE) { - Elements.push_back(static_cast(Child)); + co_yield static_cast(Child); } } - - return Elements; } struct MessageSlice @@ -88,9 +85,8 @@ struct MessageSlice std::string_view Slice; }; -std::vector splitMessages(std::string_view Input, bool& FramingError) +std::generator splitMessages(std::string_view Input, bool& FramingError) { - std::vector Messages; std::size_t Cursor = 0; std::size_t LastEnd = std::string_view::npos; @@ -102,7 +98,7 @@ std::vector splitMessages(std::string_view Input, bool& FramingErr if (LastEnd != std::string_view::npos && Start != LastEnd) { FramingError = true; - return Messages; + co_return; } const std::size_t End = Input.find(NLAP_XML_END_MARKER, Start); @@ -111,13 +107,11 @@ std::vector splitMessages(std::string_view Input, bool& FramingErr } const std::size_t MessageEnd = End + NLAP_XML_END_MARKER.size(); - Messages.push_back(MessageSlice{Input.substr(Start, MessageEnd - Start)}); + co_yield MessageSlice{Input.substr(Start, MessageEnd - Start)}; Cursor = MessageEnd; LastEnd = MessageEnd; } - - return Messages; } std::string ensureParseableXML(std::string_view Message) @@ -321,9 +315,12 @@ XMLParser::XMLParser(std::size_t ParseBufferSize) XMLPlatformUtils::Initialize(); } - auto* GrammarPool = new XMLGrammarPoolImpl(XMLPlatformUtils::fgMemoryManager); + std::unique_ptr GrammarPoolOwner( + new XMLGrammarPoolImpl(XMLPlatformUtils::fgMemoryManager) + ); + XMLGrammarPoolImpl* GrammarPool = GrammarPoolOwner.get(); _GrammarPool = std::shared_ptr( - GrammarPool, + GrammarPoolOwner.release(), [](void* Pointer) { delete static_cast(Pointer); } );