From d0372e82c8b6fe6b5517d850a81847422c861446 Mon Sep 17 00:00:00 2001
From: Cole Robinson <crobinso@redhat.com>
Date: Tue, 23 Sep 2025 10:46:19 -0400
Subject: [PATCH] xmllibxml2: lazily import libxml2

If we switch XML backends in the future, this will save us from
having a hard dep on libxml2

Signed-off-by: Cole Robinson <crobinso@redhat.com>

From d4988b02efb8bba91fd55614fbbff11b3a915d44 Mon Sep 17 00:00:00 2001
From: Cole Robinson <crobinso@redhat.com>
Date: Wed, 17 Sep 2025 10:38:12 -0400
Subject: [PATCH] xmlapi: split out xmlbase.py and xmllibxml2.py

We will be adding new XMLAPI implementations shortly and separate
files helps with code org

Signed-off-by: Cole Robinson <crobinso@redhat.com>

Index: virtinst/xmllibxml2.py
--- virtinst/xmllibxml2.py.orig
+++ virtinst/xmllibxml2.py
@@ -0,0 +1,159 @@
+#
+# XML API using libxml2
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+from . import xmlutil
+from .logger import log
+from .xmlbase import XMLBase, XPath
+
+# pylint: disable=protected-access
+
+
+def node_is_text(n):
+    return bool(n and n.type == "text")
+
+
+class Libxml2API(XMLBase):
+    def __init__(self, xml):
+        XMLBase.__init__(self)
+
+        import libxml2
+
+        self._libxml2 = libxml2
+
+        # Use of gtksourceview in virt-manager changes this libxml
+        # global setting which messes up whitespace after parsing.
+        # We can probably get away with calling this less but it
+        # would take some investigation
+        self._libxml2.keepBlanksDefault(1)
+
+        self._doc = self._libxml2.parseDoc(xml)
+        self._ctx = self._doc.xpathNewContext()
+        self._ctx.setContextNode(self._doc.children)
+        for key, val in self.NAMESPACES.items():
+            self._ctx.xpathRegisterNs(key, val)
+
+    def __del__(self):
+        if not hasattr(self, "_doc"):
+            # In case we error when parsing the doc
+            return
+        self._doc.freeDoc()
+        self._doc = None
+        self._ctx.xpathFreeContext()
+        self._ctx = None
+
+    def _sanitize_xml(self, xml):
+        if not xml.endswith("\n") and "\n" in xml:
+            xml += "\n"
+        return xml
+
+    def copy_api(self):
+        return Libxml2API(self._doc.children.serialize())
+
+    def _find(self, fullxpath):
+        xpath = XPath(fullxpath).xpath
+        try:
+            node = self._ctx.xpathEval(xpath)
+        except Exception as e:
+            log.debug("fullxpath=%s xpath=%s eval failed", fullxpath, xpath, exc_info=True)
+            raise RuntimeError("%s %s" % (fullxpath, str(e))) from None
+        return node and node[0] or None
+
+    def count(self, xpath):
+        return len(self._ctx.xpathEval(xpath))
+
+    def _node_tostring(self, node):
+        return node.serialize()
+
+    def _node_from_xml(self, xml):
+        return self._libxml2.parseDoc(xml).children
+
+    def _node_get_text(self, node):
+        return node.content
+
+    def _node_set_text(self, node, setval):
+        if setval is not None:
+            setval = xmlutil.xml_escape(setval)
+        node.setContent(setval)
+
+    def _node_get_property(self, node, propname):
+        prop = node.hasProp(propname)
+        if prop:
+            return prop.content
+
+    def _node_set_property(self, node, propname, setval):
+        if setval is None:
+            prop = node.hasProp(propname)
+            if prop:
+                prop.unlinkNode()
+                prop.freeNode()
+        else:
+            node.setProp(propname, setval)
+
+    def _node_new(self, xpathseg, parentnode):
+        newnode = self._libxml2.newNode(xpathseg.nodename)
+        if not xpathseg.nsname:
+            return newnode
+
+        def _find_parent_ns():
+            parent = parentnode
+            while parent:
+                for ns in xmlutil.listify(parent.nsDefs()):
+                    if ns.name == xpathseg.nsname:
+                        return ns
+                parent = parent.get_parent()
+
+        ns = _find_parent_ns()
+        if not ns:
+            ns = newnode.newNs(self.NAMESPACES[xpathseg.nsname], xpathseg.nsname)
+        newnode.setNs(ns)
+        return newnode
+
+    def node_clear(self, xpath):
+        node = self._find(xpath)
+        if node:
+            propnames = [p.name for p in (node.properties or [])]
+            for p in propnames:
+                node.unsetProp(p)
+            node.setContent(None)
+
+    def _node_has_content(self, node):
+        return node.type == "element" and (node.children or node.properties)
+
+    def _node_get_name(self, node):
+        return node.name
+
+    def _node_remove_child(self, parentnode, childnode):
+        node = childnode
+
+        # Look for preceding whitespace and remove it
+        white = node.get_prev()
+        if node_is_text(white):
+            white.unlinkNode()
+            white.freeNode()
+
+        node.unlinkNode()
+        node.freeNode()
+        if all([node_is_text(n) for n in parentnode.children]):
+            parentnode.setContent(None)
+
+    def _node_add_child(self, parentxpath, parentnode, newnode):
+        ignore = parentxpath
+        if not node_is_text(parentnode.get_last()):
+            prevsib = parentnode.get_prev()
+            if node_is_text(prevsib):
+                newlast = self._libxml2.newText(prevsib.content)
+            else:
+                newlast = self._libxml2.newText("\n")
+            parentnode.addChild(newlast)
+
+        endtext = parentnode.get_last().content
+        parentnode.addChild(self._libxml2.newText("  "))
+        parentnode.addChild(newnode)
+        parentnode.addChild(self._libxml2.newText(endtext))
+
+    def _node_replace_child(self, xpath, newnode):
+        oldnode = self._find(xpath)
+        oldnode.replaceNode(newnode)
