From 8556dc6089194ab59abd00fde06ac154b9b904ff Mon Sep 17 00:00:00 2001 From: Bernard Kwok Date: Mon, 8 Jun 2026 11:16:15 -0400 Subject: [PATCH 1/3] Add format_mtlx option to pugixml to allow control output of escaped < and > characters. Expose a "strict" XML mode on XMLWriteOptions. Default is false which means to not escape. As read supports both and escaped and non-escaped < and > support has not changed. Added test case to allow writing of either and check for read of escaped. --- .../External/PugiXML/pugixml.cpp | 16 +++++--- .../External/PugiXML/pugixml.hpp | 3 ++ source/MaterialXFormat/XmlIo.cpp | 11 ++++- source/MaterialXFormat/XmlIo.h | 5 +++ .../MaterialXTest/MaterialXFormat/XmlIo.cpp | 41 +++++++++++++++++++ 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.cpp b/source/MaterialXFormat/External/PugiXML/pugixml.cpp index 73b3525028..d6ff34b934 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.cpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.cpp @@ -3927,7 +3927,7 @@ PUGI__NS_BEGIN xml_encoding encoding; }; - PUGI__FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type) + PUGI__FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags) { while (*s) { @@ -3947,12 +3947,18 @@ PUGI__NS_BEGIN break; case '<': // MaterialX: Allow angle brackets in MTLX serialization. - writer.write(*s); + if (flags & format_mtlx) + writer.write('<'); + else + writer.write('&', 'l', 't', ';'); ++s; break; case '>': - // MaterialX: Allow angle brackets in MTLX serialization. - writer.write(*s); + // MaterialX: Allow angle brackets in MTLX serialization. + if (flags & format_mtlx) + writer.write('>'); + else + writer.write('&', 'g', 't', ';'); ++s; break; case '"': @@ -3975,7 +3981,7 @@ PUGI__NS_BEGIN if (flags & format_no_escapes) writer.write_string(s); else - text_output_escaped(writer, s, type); + text_output_escaped(writer, s, type, flags); } PUGI__FN void text_output_cdata(xml_buffered_writer& writer, const char_t* s) diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.hpp b/source/MaterialXFormat/External/PugiXML/pugixml.hpp index 5392259678..2b07e5dbe8 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.hpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.hpp @@ -256,6 +256,9 @@ namespace pugi // Don't output empty element tags, instead writing an explicit start and end tag even if there are no children. This flag is off by default. const unsigned int format_no_empty_element_tags = 0x80; + // MaterialX: This flag determines if MaterialX custom output formatting is enabled. THis flag is on by default + const unsigned int format_mtlx = 0x100; + // The default set of formatting flags. // Nodes are indented depending on their depth in DOM tree, a default declaration is output if document has none. const unsigned int format_default = format_indent; diff --git a/source/MaterialXFormat/XmlIo.cpp b/source/MaterialXFormat/XmlIo.cpp index 83e4721dc1..6cd2db59a8 100644 --- a/source/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXFormat/XmlIo.cpp @@ -347,10 +347,19 @@ void readFromXmlString(DocumentPtr doc, const string& str, const FileSearchPath& void writeToXmlStream(DocumentPtr doc, std::ostream& stream, const XmlWriteOptions* writeOptions) { + // Check for XML strictness + unsigned int flags = format_default; + bool strictXML = writeOptions ? writeOptions->strictXML : false; + if (!strictXML) + { + // If not strict add MaterialX specific output formatting. + flags |= format_mtlx; + } + xml_document xmlDoc; xml_node xmlRoot = xmlDoc.append_child("materialx"); documentToXml(doc, xmlRoot, writeOptions); - xmlDoc.save(stream, " "); + xmlDoc.save(stream, " ", flags); } void writeToXmlFile(DocumentPtr doc, const FilePath& filename, const XmlWriteOptions* writeOptions) diff --git a/source/MaterialXFormat/XmlIo.h b/source/MaterialXFormat/XmlIo.h index 5c80ae8005..2e11c326d5 100644 --- a/source/MaterialXFormat/XmlIo.h +++ b/source/MaterialXFormat/XmlIo.h @@ -70,6 +70,11 @@ class MX_FORMAT_API XmlWriteOptions /// XIncludes rather than explicit data. Defaults to true. bool writeXIncludeEnable; + /// If true output will follow a strict interpretation of the XML specification. + /// If false, then MaterialX customizations which deviate from the XML specification will be allowed. + // Defaults to false. + bool strictXML = false; + /// If provided, this function will be used to exclude specific elements /// (those returning false) from the write operation. Defaults to nullptr. ElementPredicate elementPredicate; diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp index 36fb703d66..687db75654 100644 --- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp @@ -371,3 +371,44 @@ TEST_CASE("Locale region testing", "[xmlio]") // Restore the original locale. std::locale::global(origLocale); } + +TEST_CASE("XML strictness", "[xmlio2]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr stdlib = mx::createDocument(); + mx::loadLibraries({ "libraries" }, searchPath, stdlib); + + mx::DocumentPtr doc = mx::createDocument(); + doc->setDataLibrary(stdlib); + + // Create image node with illegal <> characters + mx::NodePtr imageNode = doc->addNode("image", "testImage"); + imageNode->addInputsFromNodeDef(); + mx::InputPtr fileInput = imageNode->getInput("file"); + fileInput->setValue("<>"); + + // Write to string with default options + mx::XmlWriteOptions writeOptions; + std::string output_string = mx::writeToXmlString(doc, &writeOptions); + // Make sure there are no escaped characters + REQUIRE(output_string.find("<>") == std::string::npos); + + // Write with escaped characters enabled + writeOptions.strictXML = true; + output_string = mx::writeToXmlString(doc, &writeOptions); + // Look for escaped characters + REQUIRE(output_string.find("<>") != std::string::npos); + + // Read in the document with the escaped characters and verify that the value + // is unescaped properly + mx::DocumentPtr readDoc = mx::createDocument(); + readDoc->setDataLibrary(stdlib); + mx::readFromXmlString(readDoc , output_string); + imageNode = readDoc->getNode("testImage"); + REQUIRE(imageNode != nullptr); + fileInput = imageNode->getInput("file"); + REQUIRE(fileInput != nullptr); + std::string unescaped_string = fileInput->getValueString(); + // There should not be any escaped characters in the value string + REQUIRE(unescaped_string.find("<>") == std::string::npos); +} \ No newline at end of file From d82043e076b7c69fd59f2441cea42a23bba49708 Mon Sep 17 00:00:00 2001 From: Bernard Kwok Date: Tue, 9 Jun 2026 10:43:54 -0400 Subject: [PATCH 2/3] Simplify to remove option. --- .../MaterialXFormat/External/PugiXML/pugixml.cpp | 16 ++++------------ .../MaterialXFormat/External/PugiXML/pugixml.hpp | 3 --- source/MaterialXFormat/XmlIo.cpp | 11 +---------- source/MaterialXFormat/XmlIo.h | 5 ----- source/MaterialXTest/MaterialXFormat/XmlIo.cpp | 8 +------- 5 files changed, 6 insertions(+), 37 deletions(-) diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.cpp b/source/MaterialXFormat/External/PugiXML/pugixml.cpp index d6ff34b934..f045214098 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.cpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.cpp @@ -3927,7 +3927,7 @@ PUGI__NS_BEGIN xml_encoding encoding; }; - PUGI__FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags) + PUGI__FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type) { while (*s) { @@ -3946,19 +3946,11 @@ PUGI__NS_BEGIN ++s; break; case '<': - // MaterialX: Allow angle brackets in MTLX serialization. - if (flags & format_mtlx) - writer.write('<'); - else - writer.write('&', 'l', 't', ';'); + writer.write('&', 'l', 't', ';'); ++s; break; case '>': - // MaterialX: Allow angle brackets in MTLX serialization. - if (flags & format_mtlx) - writer.write('>'); - else - writer.write('&', 'g', 't', ';'); + writer.write('&', 'g', 't', ';'); ++s; break; case '"': @@ -3981,7 +3973,7 @@ PUGI__NS_BEGIN if (flags & format_no_escapes) writer.write_string(s); else - text_output_escaped(writer, s, type, flags); + text_output_escaped(writer, s, type); } PUGI__FN void text_output_cdata(xml_buffered_writer& writer, const char_t* s) diff --git a/source/MaterialXFormat/External/PugiXML/pugixml.hpp b/source/MaterialXFormat/External/PugiXML/pugixml.hpp index 2b07e5dbe8..5392259678 100644 --- a/source/MaterialXFormat/External/PugiXML/pugixml.hpp +++ b/source/MaterialXFormat/External/PugiXML/pugixml.hpp @@ -256,9 +256,6 @@ namespace pugi // Don't output empty element tags, instead writing an explicit start and end tag even if there are no children. This flag is off by default. const unsigned int format_no_empty_element_tags = 0x80; - // MaterialX: This flag determines if MaterialX custom output formatting is enabled. THis flag is on by default - const unsigned int format_mtlx = 0x100; - // The default set of formatting flags. // Nodes are indented depending on their depth in DOM tree, a default declaration is output if document has none. const unsigned int format_default = format_indent; diff --git a/source/MaterialXFormat/XmlIo.cpp b/source/MaterialXFormat/XmlIo.cpp index 6cd2db59a8..83e4721dc1 100644 --- a/source/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXFormat/XmlIo.cpp @@ -347,19 +347,10 @@ void readFromXmlString(DocumentPtr doc, const string& str, const FileSearchPath& void writeToXmlStream(DocumentPtr doc, std::ostream& stream, const XmlWriteOptions* writeOptions) { - // Check for XML strictness - unsigned int flags = format_default; - bool strictXML = writeOptions ? writeOptions->strictXML : false; - if (!strictXML) - { - // If not strict add MaterialX specific output formatting. - flags |= format_mtlx; - } - xml_document xmlDoc; xml_node xmlRoot = xmlDoc.append_child("materialx"); documentToXml(doc, xmlRoot, writeOptions); - xmlDoc.save(stream, " ", flags); + xmlDoc.save(stream, " "); } void writeToXmlFile(DocumentPtr doc, const FilePath& filename, const XmlWriteOptions* writeOptions) diff --git a/source/MaterialXFormat/XmlIo.h b/source/MaterialXFormat/XmlIo.h index 2e11c326d5..5c80ae8005 100644 --- a/source/MaterialXFormat/XmlIo.h +++ b/source/MaterialXFormat/XmlIo.h @@ -70,11 +70,6 @@ class MX_FORMAT_API XmlWriteOptions /// XIncludes rather than explicit data. Defaults to true. bool writeXIncludeEnable; - /// If true output will follow a strict interpretation of the XML specification. - /// If false, then MaterialX customizations which deviate from the XML specification will be allowed. - // Defaults to false. - bool strictXML = false; - /// If provided, this function will be used to exclude specific elements /// (those returning false) from the write operation. Defaults to nullptr. ElementPredicate elementPredicate; diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp index 687db75654..935533fc58 100644 --- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp @@ -372,7 +372,7 @@ TEST_CASE("Locale region testing", "[xmlio]") std::locale::global(origLocale); } -TEST_CASE("XML strictness", "[xmlio2]") +TEST_CASE("XML strictness", "[xmlio]") { mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); mx::DocumentPtr stdlib = mx::createDocument(); @@ -393,12 +393,6 @@ TEST_CASE("XML strictness", "[xmlio2]") // Make sure there are no escaped characters REQUIRE(output_string.find("<>") == std::string::npos); - // Write with escaped characters enabled - writeOptions.strictXML = true; - output_string = mx::writeToXmlString(doc, &writeOptions); - // Look for escaped characters - REQUIRE(output_string.find("<>") != std::string::npos); - // Read in the document with the escaped characters and verify that the value // is unescaped properly mx::DocumentPtr readDoc = mx::createDocument(); From 6b275ce76bbb32cf04593f18fa5d2eff18afda51 Mon Sep 17 00:00:00 2001 From: Bernard Kwok Date: Tue, 9 Jun 2026 10:44:20 -0400 Subject: [PATCH 3/3] Update test. --- source/MaterialXTest/MaterialXFormat/XmlIo.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp index 935533fc58..19eabee301 100644 --- a/source/MaterialXTest/MaterialXFormat/XmlIo.cpp +++ b/source/MaterialXTest/MaterialXFormat/XmlIo.cpp @@ -387,11 +387,11 @@ TEST_CASE("XML strictness", "[xmlio]") mx::InputPtr fileInput = imageNode->getInput("file"); fileInput->setValue("<>"); - // Write to string with default options + // Write to string with escaped characters mx::XmlWriteOptions writeOptions; std::string output_string = mx::writeToXmlString(doc, &writeOptions); - // Make sure there are no escaped characters - REQUIRE(output_string.find("<>") == std::string::npos); + // Make sure there are escaped characters + REQUIRE(output_string.find("<>") != std::string::npos); // Read in the document with the escaped characters and verify that the value // is unescaped properly