/* * Copyright (c) 2025, Johannes Gustafsson * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "XPath.h" namespace Web::XPath { static xmlNodePtr mirror_node(xmlDocPtr doc, DOM::Node const& node) { switch (node.type()) { case DOM::NodeType::INVALID: { return nullptr; } case DOM::NodeType::ELEMENT_NODE: { auto const& element = static_cast(node); ByteString name = element.local_name().bytes_as_string_view(); auto* xml_element = xmlNewDocNode(doc, nullptr, bit_cast(name.characters()), nullptr); xml_element->_private = bit_cast(&node); for (size_t i = 0; i < element.attribute_list_size(); ++i) { auto const& attribute = *element.attributes()->item(i); ByteString attr_name = attribute.name().bytes_as_string_view(); ByteString attr_value = attribute.value().bytes_as_string_view(); auto* attr = xmlSetProp(xml_element, bit_cast(attr_name.characters()), bit_cast(attr_value.characters())); attr->_private = bit_cast(&attribute); if (attribute.name() == "id") { xmlAddIDSafe(attr, bit_cast(attr_value.characters())); } } auto children = element.children_as_vector(); for (auto& child : children) { xmlAddChild(xml_element, mirror_node(doc, *child)); } return xml_element; } case DOM::NodeType::ATTRIBUTE_NODE: { return nullptr; // Attributes are handled in the elements children above. If this happens, then the attribute is the top node in the document and therefore invalid } case DOM::NodeType::TEXT_NODE: { auto const& text = static_cast(node); auto* xml_text = xmlNewDocText(doc, bit_cast(text.data().to_byte_string().characters())); xml_text->_private = bit_cast(&node); return xml_text; } case DOM::NodeType::CDATA_SECTION_NODE: { auto const& cdata = static_cast(node); ByteString data = cdata.data().to_byte_string(); auto* xml_cdata = xmlNewCDataBlock(doc, bit_cast(data.characters()), data.length()); xml_cdata->_private = bit_cast(&node); return xml_cdata; } case DOM::NodeType::ENTITY_REFERENCE_NODE: // Does not seem to be used at all in ladybird case DOM::NodeType::ENTITY_NODE: // Entity nodes are unused in libxml2 { return nullptr; } case DOM::NodeType::PROCESSING_INSTRUCTION_NODE: { auto const& processing_instruction = static_cast(node); auto* xml_pi = xmlNewDocPI(doc, bit_cast(processing_instruction.target().to_byte_string().characters()), bit_cast(processing_instruction.data().to_byte_string().characters())); xml_pi->_private = bit_cast(&node); return xml_pi; } case DOM::NodeType::COMMENT_NODE: { auto const& comment = static_cast(node); auto* xml_comment = xmlNewDocComment(doc, bit_cast(comment.data().to_byte_string().characters())); xml_comment->_private = bit_cast(&node); return xml_comment; } case DOM::NodeType::DOCUMENT_NODE: { auto const& document = static_cast(node); return mirror_node(doc, *document.document_element()); } case DOM::NodeType::DOCUMENT_TYPE_NODE: { return nullptr; // Unused in libxml2 } case DOM::NodeType::DOCUMENT_FRAGMENT_NODE: { auto const& fragment = static_cast(node); auto* xml_fragment = xmlNewDocFragment(doc); xml_fragment->_private = bit_cast(&node); auto children = fragment.children_as_vector(); for (auto& child : children) { xmlAddChild(xml_fragment, mirror_node(doc, *child)); } return xml_fragment; } case DOM::NodeType::NOTATION_NODE: { return nullptr; // Unused in libxml2 } } return nullptr; } static void convert_xpath_result(xmlXPathObjectPtr xpath_result, XPath::XPathResult* result, unsigned short type) { if (!xpath_result) { return; } switch (xpath_result->type) { case XPATH_UNDEFINED: break; case XPATH_NODESET: { Vector> node_list; if (xpath_result->nodesetval && xpath_result->nodesetval->nodeNr > 0) { node_list.ensure_capacity(xpath_result->nodesetval->nodeNr); for (int i = 0; i < xpath_result->nodesetval->nodeNr; i++) { auto* node = xpath_result->nodesetval->nodeTab[i]; auto* dom_node = static_cast(node->_private); node_list.unchecked_append(dom_node); } } result->set_node_set(move(node_list), type); break; } case XPATH_BOOLEAN: { result->set_boolean(xpath_result->boolval); break; } case XPATH_NUMBER: { result->set_number(xpath_result->floatval); break; } case XPATH_STRING: { ReadonlyBytes bytes(xpath_result->stringval, xmlStrlen(xpath_result->stringval)); result->set_string(String::from_utf8_without_validation(bytes)); break; } case XPATH_USERS: case XPATH_XSLT_TREE: /* An XSLT value tree, non modifiable */ break; } } WebIDL::ExceptionOr> create_expression(JS::Realm& realm, String const& expression, GC::Ptr resolver) { return realm.create(realm, expression, resolver); } WebIDL::ExceptionOr> evaluate(JS::Realm& realm, String const& expression, DOM::Node const& context_node, GC::Ptr /*resolver*/, unsigned short type, GC::Ptr result) { // Parse the expression as xpath ByteString bytes = expression.bytes_as_string_view(); auto* xpath_compiled = xmlXPathCompile(bit_cast(bytes.characters())); if (!xpath_compiled) return WebIDL::SyntaxError::create(realm, "Invalid XPath expression"_utf16); ScopeGuard xpath_compiled_cleanup = [&] { xmlXPathFreeCompExpr(xpath_compiled); }; auto* xml_document = xmlNewDoc(nullptr); ScopeGuard xml_cleanup = [&] { xmlFreeDoc(xml_document); }; if (context_node.type() == DOM::NodeType::DOCUMENT_NODE) { xml_document->_private = bit_cast(&context_node); } else { xml_document->_private = bit_cast(&context_node.document()); } auto* xml_node = mirror_node(xml_document, context_node); if (!xml_node) { return WebIDL::OperationError::create(realm, "XPath evaluation failed"_utf16); } xmlDocSetRootElement(xml_document, xml_node); auto* xpath_context = xmlXPathNewContext(xml_document); xmlXPathSetContextNode(xml_node, xpath_context); auto* xpath_result = xmlXPathCompiledEval(xpath_compiled, xpath_context); ScopeGuard xpath_result_cleanup = [&] { xmlXPathFreeObject(xpath_result); xmlXPathFreeContext(xpath_context); }; if (!result) { result = realm.create(realm); } convert_xpath_result(xpath_result, result, type); return GC::Ref(*result); } }