From d5e41f1f720de56667379b2f436d26f5fd93feaa Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sun, 6 Jul 2025 21:10:10 +1200 Subject: [PATCH] LibWeb: Handle null namespace in prefix map serializing XML A "namespace prefix map", see: https://w3c.github.io/DOM-Parsing/#the-namespace-prefix-map Is meant to also hold null namespaces: > where namespaceURI values are the map's unique keys > (which can include the null value representing no namespace) Which we previously neglected. This resulted in a crash for the updated WPT test. --- Libraries/LibWeb/HTML/XMLSerializer.cpp | 44 +++++++++---------- .../XMLSerializer-serializeToString.txt | 11 +++-- .../XMLSerializer-serializeToString.html | 30 ++++++++++++- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/Libraries/LibWeb/HTML/XMLSerializer.cpp b/Libraries/LibWeb/HTML/XMLSerializer.cpp index 5d91979799e..93b0f41d8a6 100644 --- a/Libraries/LibWeb/HTML/XMLSerializer.cpp +++ b/Libraries/LibWeb/HTML/XMLSerializer.cpp @@ -54,16 +54,16 @@ WebIDL::ExceptionOr XMLSerializer::serialize_to_string(GC::Ref>>& prefix_map, Optional const& prefix, Optional const& namespace_) +static void add_prefix_to_namespace_prefix_map(HashMap, Vector>>& prefix_map, Optional const& prefix, Optional const& namespace_) { // 1. Let candidates list be the result of retrieving a list from map where there exists a key in map that matches the value of ns or if there is no such key, then let candidates list be null. - auto candidates_list_iterator = namespace_.has_value() ? prefix_map.find(*namespace_) : prefix_map.end(); + auto candidates_list_iterator = prefix_map.find(namespace_); // 2. If candidates list is null, then create a new list with prefix as the only item in the list, and associate that list with a new key ns in map. if (candidates_list_iterator == prefix_map.end()) { Vector> new_list; new_list.append(prefix); - prefix_map.set(*namespace_, move(new_list)); + prefix_map.set(namespace_, move(new_list)); return; } @@ -72,13 +72,11 @@ static void add_prefix_to_namespace_prefix_map(HashMap retrieve_a_preferred_prefix_string(Optional const& preferred_prefix, HashMap>> const& namespace_prefix_map, Optional const& namespace_) +static Optional retrieve_a_preferred_prefix_string(Optional const& preferred_prefix, HashMap, Vector>> const& namespace_prefix_map, Optional const& namespace_) { // 1. Let candidates list be the result of retrieving a list from map where there exists a key in map that matches the value of ns or if there is no such key, // then stop running these steps, and return the null value. - if (!namespace_.has_value()) - return {}; - auto candidates_list_iterator = namespace_prefix_map.find(*namespace_); + auto candidates_list_iterator = namespace_prefix_map.find(namespace_); if (candidates_list_iterator == namespace_prefix_map.end()) return {}; @@ -100,7 +98,7 @@ static Optional retrieve_a_preferred_prefix_string(Optional>>& namespace_prefix_map, Optional const& new_namespace, u64& prefix_index) +static FlyString generate_a_prefix(HashMap, Vector>>& namespace_prefix_map, Optional const& new_namespace, u64& prefix_index) { // 1. Let generated prefix be the concatenation of the string "ns" and the current numerical value of prefix index. auto generated_prefix = FlyString(MUST(String::formatted("ns{}", prefix_index))); @@ -116,13 +114,11 @@ static FlyString generate_a_prefix(HashMap } // https://w3c.github.io/DOM-Parsing/#dfn-found -static bool prefix_is_in_prefix_map(FlyString const& prefix, HashMap>> const& namespace_prefix_map, Optional const& namespace_) +static bool prefix_is_in_prefix_map(FlyString const& prefix, HashMap, Vector>> const& namespace_prefix_map, Optional const& namespace_) { // 1. Let candidates list be the result of retrieving a list from map where there exists a key in map that matches the value of ns // or if there is no such key, then stop running these steps, and return false. - if (!namespace_.has_value()) - return false; - auto candidates_list_iterator = namespace_prefix_map.find(*namespace_); + auto candidates_list_iterator = namespace_prefix_map.find(namespace_); if (candidates_list_iterator == namespace_prefix_map.end()) return false; @@ -130,7 +126,7 @@ static bool prefix_is_in_prefix_map(FlyString const& prefix, HashMapvalue.contains_slow(prefix); } -WebIDL::ExceptionOr serialize_node_to_xml_string_impl(GC::Ref root, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); +WebIDL::ExceptionOr serialize_node_to_xml_string_impl(GC::Ref root, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); // https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization WebIDL::ExceptionOr serialize_node_to_xml_string(GC::Ref root, RequireWellFormed require_well_formed) @@ -141,7 +137,7 @@ WebIDL::ExceptionOr serialize_node_to_xml_string(GC::Ref namespace_; // 2. Let prefix map be a new namespace prefix map. - HashMap>> prefix_map; + HashMap, Vector>> prefix_map; // 3. Add the XML namespace with prefix value "xml" to prefix map. add_prefix_to_namespace_prefix_map(prefix_map, "xml"_fly_string, Namespace::XML); @@ -157,17 +153,17 @@ WebIDL::ExceptionOr serialize_node_to_xml_string(GC::Ref serialize_element(DOM::Element const& element, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); -static WebIDL::ExceptionOr serialize_document(DOM::Document const& document, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); +static WebIDL::ExceptionOr serialize_element(DOM::Element const& element, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); +static WebIDL::ExceptionOr serialize_document(DOM::Document const& document, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); static WebIDL::ExceptionOr serialize_comment(DOM::Comment const& comment, RequireWellFormed require_well_formed); static WebIDL::ExceptionOr serialize_text(DOM::Text const& text, RequireWellFormed require_well_formed); -static WebIDL::ExceptionOr serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); +static WebIDL::ExceptionOr serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed); static WebIDL::ExceptionOr serialize_document_type(DOM::DocumentType const& document_type, RequireWellFormed require_well_formed); static WebIDL::ExceptionOr serialize_processing_instruction(DOM::ProcessingInstruction const& processing_instruction, RequireWellFormed require_well_formed); static WebIDL::ExceptionOr serialize_cdata_section(DOM::CDATASection const& cdata_section, RequireWellFormed require_well_formed); // https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm -WebIDL::ExceptionOr serialize_node_to_xml_string_impl(GC::Ref root, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) +WebIDL::ExceptionOr serialize_node_to_xml_string_impl(GC::Ref root, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) { // Each of the following algorithms for producing an XML serialization of a DOM node take as input a node to serialize and the following arguments: // - A context namespace namespace @@ -242,7 +238,7 @@ WebIDL::ExceptionOr serialize_node_to_xml_string_impl(GC::Ref record_namespace_information(DOM::Element const& element, HashMap>>& namespace_prefix_map, HashMap>& local_prefix_map) +static Optional record_namespace_information(DOM::Element const& element, HashMap, Vector>>& namespace_prefix_map, HashMap>& local_prefix_map) { // 1. Let default namespace attr value be null. Optional default_namespace_attribute_value; @@ -332,7 +328,7 @@ struct LocalNameSetEntry { }; // https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-of-the-attributes -static WebIDL::ExceptionOr serialize_element_attributes(DOM::Element const& element, HashMap>>& namespace_prefix_map, u64& prefix_index, HashMap> const& local_prefixes_map, bool ignore_namespace_definition_attribute, RequireWellFormed require_well_formed) +static WebIDL::ExceptionOr serialize_element_attributes(DOM::Element const& element, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, HashMap> const& local_prefixes_map, bool ignore_namespace_definition_attribute, RequireWellFormed require_well_formed) { auto& realm = element.realm(); @@ -485,7 +481,7 @@ static WebIDL::ExceptionOr serialize_element_attributes(DOM::Element con } // https://w3c.github.io/DOM-Parsing/#xml-serializing-an-element-node -static WebIDL::ExceptionOr serialize_element(DOM::Element const& element, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) +static WebIDL::ExceptionOr serialize_element(DOM::Element const& element, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) { auto& realm = element.realm(); @@ -512,7 +508,7 @@ static WebIDL::ExceptionOr serialize_element(DOM::Element const& element bool ignore_namespace_definition_attribute = false; // 6. Given prefix map, copy a namespace prefix map and let map be the result. - HashMap>> map; + HashMap, Vector>> map; // https://w3c.github.io/DOM-Parsing/#dfn-copy-a-namespace-prefix-map // NOTE: This is only used here. @@ -727,7 +723,7 @@ static WebIDL::ExceptionOr serialize_element(DOM::Element const& element } // https://w3c.github.io/DOM-Parsing/#xml-serializing-a-document-node -static WebIDL::ExceptionOr serialize_document(DOM::Document const& document, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) +static WebIDL::ExceptionOr serialize_document(DOM::Document const& document, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) { // If the require well-formed flag is set (its value is true), and this node has no documentElement (the documentElement attribute's value is null), // then throw an exception; the serialization of this node would not be a well-formed document. @@ -803,7 +799,7 @@ static WebIDL::ExceptionOr serialize_text(DOM::Text const& text, Require } // https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documentfragment-node -static WebIDL::ExceptionOr serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional& namespace_, HashMap>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) +static WebIDL::ExceptionOr serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional& namespace_, HashMap, Vector>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed) { // 1. Let markup the empty string. StringBuilder markup; diff --git a/Tests/LibWeb/Text/expected/wpt-import/domparsing/XMLSerializer-serializeToString.txt b/Tests/LibWeb/Text/expected/wpt-import/domparsing/XMLSerializer-serializeToString.txt index c8c54dde9e5..a09b3a5f3aa 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/domparsing/XMLSerializer-serializeToString.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/domparsing/XMLSerializer-serializeToString.txt @@ -1,15 +1,16 @@ Harness status: OK -Found 24 tests +Found 27 tests -9 Pass -15 Fail +10 Pass +17 Fail Pass check XMLSerializer.serializeToString method could parsing xmldoc to string Pass check XMLSerializer.serializeToString method could parsing document to string Pass Check if the default namespace is correctly reset. Pass Check if there is no redundant empty namespace declaration. Pass Check if redundant xmlns="..." is dropped. Pass Check if inconsistent xmlns="..." is dropped. +Fail Drop inconsistent xmlns="..." by matching on local name Fail Check if an attribute with namespace and no prefix is serialized with the nearest-declared prefix Fail Check if an attribute with namespace and no prefix is serialized with the nearest-declared prefix even if the prefix is assigned to another namespace. Fail Check if the prefix of an attribute is replaced with another existing prefix mapped to the same namespace URI. @@ -27,4 +28,6 @@ Pass Check if generated prefixes match to "ns${index}". Fail Check if "ns1" is generated even if the element already has xmlns:ns1. Fail Check if no special handling for XLink namespace unlike HTML serializer. Pass Check if document fragment serializes. -Pass Check children were included for void elements \ No newline at end of file +Pass Check children were included for void elements +Fail Check if a prefix bound to an empty namespace URI ("no namespace") serialize +Pass Attribute nodes are serialized as the empty string \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/domparsing/XMLSerializer-serializeToString.html b/Tests/LibWeb/Text/input/wpt-import/domparsing/XMLSerializer-serializeToString.html index ef23bebe645..eb32ccfb459 100644 --- a/Tests/LibWeb/Text/input/wpt-import/domparsing/XMLSerializer-serializeToString.html +++ b/Tests/LibWeb/Text/input/wpt-import/domparsing/XMLSerializer-serializeToString.html @@ -80,6 +80,25 @@ test(function() { assert_equals(serialize(root), ''); }, 'Check if inconsistent xmlns="..." is dropped.'); +test(function() { + const root1 = parse(''); + root1.setAttribute('xmlns', 'http://www.idpf.org/2007/opf'); + const manifest1 = root1.appendChild(root1.ownerDocument.createElement('manifest')); + manifest1.setAttribute('xmlns', 'http://www.idpf.org/2007/opf'); + assert_equals(serialize(root1), ''); + + const root2 = parse(''); + const manifest2 = root2.appendChild(root2.ownerDocument.createElement('manifest')); + manifest2.setAttribute('xmlns', 'http://www.idpf.org/2007/opf'); + assert_equals(serialize(root2), + ''); + + const root3 = parse(''); + const manifest3 = root3.appendChild(root3.ownerDocument.createElement('manifest')); + assert_equals(serialize(root3), + ''); +}, 'Drop inconsistent xmlns="..." by matching on local name'); + test(function() { let root = parse(''); root.setAttributeNS('uri', 'name', 'v'); @@ -230,8 +249,17 @@ test(function () { root.append(document.createElement("style")); root.append(document.createElement("style")); assert_equals(serialize(root), ''); -}, 'Check children were included for void elements') +}, 'Check children were included for void elements'); +test(function () { + const root = parse(''); + root.setAttributeNS(XMLNS_URI, 'xmlns:foo', ''); + assert_equals(serialize(root), ''); +}, 'Check if a prefix bound to an empty namespace URI ("no namespace") serialize'); + +test(function() { + assert_equals(serialize(document.createAttribute("foobar")), "") +}, 'Attribute nodes are serialized as the empty string')