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.
This commit is contained in:
Shannon Booth 2025-07-06 21:10:10 +12:00 committed by Tim Ledbetter
commit d5e41f1f72
Notes: github-actions[bot] 2025-07-07 19:27:17 +00:00
3 changed files with 56 additions and 29 deletions

View file

@ -54,16 +54,16 @@ WebIDL::ExceptionOr<String> XMLSerializer::serialize_to_string(GC::Ref<DOM::Node
}
// https://w3c.github.io/DOM-Parsing/#dfn-add
static void add_prefix_to_namespace_prefix_map(HashMap<FlyString, Vector<Optional<FlyString>>>& prefix_map, Optional<FlyString> const& prefix, Optional<FlyString> const& namespace_)
static void add_prefix_to_namespace_prefix_map(HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& prefix_map, Optional<FlyString> const& prefix, Optional<FlyString> 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<Optional<FlyString>> 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<FlyString, Vector<Optiona
}
// https://w3c.github.io/DOM-Parsing/#dfn-retrieving-a-preferred-prefix-string
static Optional<FlyString> retrieve_a_preferred_prefix_string(Optional<FlyString> const& preferred_prefix, HashMap<FlyString, Vector<Optional<FlyString>>> const& namespace_prefix_map, Optional<FlyString> const& namespace_)
static Optional<FlyString> retrieve_a_preferred_prefix_string(Optional<FlyString> const& preferred_prefix, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>> const& namespace_prefix_map, Optional<FlyString> 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<FlyString> retrieve_a_preferred_prefix_string(Optional<FlyString
}
// https://w3c.github.io/DOM-Parsing/#dfn-generating-a-prefix
static FlyString generate_a_prefix(HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, Optional<FlyString> const& new_namespace, u64& prefix_index)
static FlyString generate_a_prefix(HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, Optional<FlyString> 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<FlyString, Vector<Optional<FlyString>
}
// https://w3c.github.io/DOM-Parsing/#dfn-found
static bool prefix_is_in_prefix_map(FlyString const& prefix, HashMap<FlyString, Vector<Optional<FlyString>>> const& namespace_prefix_map, Optional<FlyString> const& namespace_)
static bool prefix_is_in_prefix_map(FlyString const& prefix, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>> const& namespace_prefix_map, Optional<FlyString> 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, HashMap<FlyString,
return candidates_list_iterator->value.contains_slow(prefix);
}
WebIDL::ExceptionOr<String> serialize_node_to_xml_string_impl(GC::Ref<DOM::Node const> root, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
WebIDL::ExceptionOr<String> serialize_node_to_xml_string_impl(GC::Ref<DOM::Node const> root, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
// https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization
WebIDL::ExceptionOr<String> serialize_node_to_xml_string(GC::Ref<DOM::Node const> root, RequireWellFormed require_well_formed)
@ -141,7 +137,7 @@ WebIDL::ExceptionOr<String> serialize_node_to_xml_string(GC::Ref<DOM::Node const
Optional<FlyString> namespace_;
// 2. Let prefix map be a new namespace prefix map.
HashMap<FlyString, Vector<Optional<FlyString>>> prefix_map;
HashMap<Optional<FlyString>, Vector<Optional<FlyString>>> 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<String> serialize_node_to_xml_string(GC::Ref<DOM::Node const
return serialize_node_to_xml_string_impl(root, namespace_, prefix_map, prefix_index, require_well_formed);
}
static WebIDL::ExceptionOr<String> serialize_element(DOM::Element const& element, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_document(DOM::Document const& document, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_element(DOM::Element const& element, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_document(DOM::Document const& document, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_comment(DOM::Comment const& comment, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_text(DOM::Text const& text, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_document_type(DOM::DocumentType const& document_type, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_processing_instruction(DOM::ProcessingInstruction const& processing_instruction, RequireWellFormed require_well_formed);
static WebIDL::ExceptionOr<String> serialize_cdata_section(DOM::CDATASection const& cdata_section, RequireWellFormed require_well_formed);
// https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm
WebIDL::ExceptionOr<String> serialize_node_to_xml_string_impl(GC::Ref<DOM::Node const> root, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
WebIDL::ExceptionOr<String> serialize_node_to_xml_string_impl(GC::Ref<DOM::Node const> root, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& 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<String> serialize_node_to_xml_string_impl(GC::Ref<DOM::Node
}
// https://w3c.github.io/DOM-Parsing/#dfn-recording-the-namespace-information
static Optional<FlyString> record_namespace_information(DOM::Element const& element, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, HashMap<FlyString, Optional<FlyString>>& local_prefix_map)
static Optional<FlyString> record_namespace_information(DOM::Element const& element, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, HashMap<FlyString, Optional<FlyString>>& local_prefix_map)
{
// 1. Let default namespace attr value be null.
Optional<FlyString> 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<String> serialize_element_attributes(DOM::Element const& element, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, HashMap<FlyString, Optional<FlyString>> const& local_prefixes_map, bool ignore_namespace_definition_attribute, RequireWellFormed require_well_formed)
static WebIDL::ExceptionOr<String> serialize_element_attributes(DOM::Element const& element, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, HashMap<FlyString, Optional<FlyString>> const& local_prefixes_map, bool ignore_namespace_definition_attribute, RequireWellFormed require_well_formed)
{
auto& realm = element.realm();
@ -485,7 +481,7 @@ static WebIDL::ExceptionOr<String> serialize_element_attributes(DOM::Element con
}
// https://w3c.github.io/DOM-Parsing/#xml-serializing-an-element-node
static WebIDL::ExceptionOr<String> serialize_element(DOM::Element const& element, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
static WebIDL::ExceptionOr<String> serialize_element(DOM::Element const& element, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
{
auto& realm = element.realm();
@ -512,7 +508,7 @@ static WebIDL::ExceptionOr<String> 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<FlyString, Vector<Optional<FlyString>>> map;
HashMap<Optional<FlyString>, Vector<Optional<FlyString>>> 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<String> serialize_element(DOM::Element const& element
}
// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-document-node
static WebIDL::ExceptionOr<String> serialize_document(DOM::Document const& document, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
static WebIDL::ExceptionOr<String> serialize_document(DOM::Document const& document, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& 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<String> serialize_text(DOM::Text const& text, Require
}
// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-documentfragment-node
static WebIDL::ExceptionOr<String> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<FlyString>& namespace_, HashMap<FlyString, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
static WebIDL::ExceptionOr<String> serialize_document_fragment(DOM::DocumentFragment const& document_fragment, Optional<FlyString>& namespace_, HashMap<Optional<FlyString>, Vector<Optional<FlyString>>>& namespace_prefix_map, u64& prefix_index, RequireWellFormed require_well_formed)
{
// 1. Let markup the empty string.
StringBuilder markup;

View file

@ -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.
@ -28,3 +29,5 @@ 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
Fail Check if a prefix bound to an empty namespace URI ("no namespace") serialize
Pass Attribute nodes are serialized as the empty string

View file

@ -80,6 +80,25 @@ test(function() {
assert_equals(serialize(root), '<root xmlns="uri1"><child xmlns=""/><child2 xmlns="uri2"/><child3/><child4 xmlns="uri4"/><child5 xmlns=""/></root>');
}, 'Check if inconsistent xmlns="..." is dropped.');
test(function() {
const root1 = parse('<package></package>');
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), '<package><manifest/></package>');
const root2 = parse('<package xmlns="http://www.idpf.org/2007/opf"></package>');
const manifest2 = root2.appendChild(root2.ownerDocument.createElement('manifest'));
manifest2.setAttribute('xmlns', 'http://www.idpf.org/2007/opf');
assert_equals(serialize(root2),
'<package xmlns="http://www.idpf.org/2007/opf"><manifest xmlns=""/></package>');
const root3 = parse('<package xmlns="http://www.idpf.org/2007/opf"></package>');
const manifest3 = root3.appendChild(root3.ownerDocument.createElement('manifest'));
assert_equals(serialize(root3),
'<package xmlns="http://www.idpf.org/2007/opf"><manifest xmlns=""/></package>');
}, 'Drop inconsistent xmlns="..." by matching on local name');
test(function() {
let root = parse('<r xmlns:xx="uri"></r>');
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), '<img xmlns=\"http://www.w3.org/1999/xhtml\"><style></style><style></style></img>');
}, 'Check children were included for void elements')
}, 'Check children were included for void elements');
test(function () {
const root = parse('<root xmlns="" xmlns:foo="urn:bar"/>');
root.setAttributeNS(XMLNS_URI, 'xmlns:foo', '');
assert_equals(serialize(root), '<root xmlns="" xmlns:foo=""/>');
}, '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')
</script>
</body>
</html>