1717#include <libxml/xmlmemory.h>
1818#include <libxml/parser.h>
1919#include <libxml/dict.h>
20+ #include <libxml/xmlsave.h>
2021
2122#include <stdint.h>
2223
@@ -435,18 +436,26 @@ static int PyXmlSec_LxmlShadowTagNodes(PyXmlSec_LxmlShadow* shadow, xmlNodePtr n
435436// exception set.
436437static int PyXmlSec_LxmlShadowMark (PyXmlSec_LxmlShadow * shadow ) {
437438 int count = PyXmlSec_LxmlShadowCountNodes (shadow -> doc -> children , 0 );
439+ // The unlinked subtree is in the document but not in its tree, so the
440+ // walk over the tree does not reach it; it is pre-existing all the same.
441+ int unlinked = shadow -> unlinked ? PyXmlSec_LxmlShadowCountNodes (shadow -> root , 0 ) : 0 ;
442+ int next ;
438443
439- if (count < 0 ) {
444+ if (count < 0 || unlinked < 0 ) {
440445 PyErr_SetString (PyXmlSec_InternalError , "the document is nested too deeply." );
441446 return -1 ;
442447 }
448+ count += unlinked ;
443449 shadow -> tags = (PyXmlSec_LxmlShadowTag * )PyMem_Malloc ((count > 0 ? count : 1 ) * sizeof (* shadow -> tags ));
444450 if (shadow -> tags == NULL ) {
445451 PyErr_NoMemory ();
446452 return -1 ;
447453 }
448454 shadow -> ntags = count ;
449- PyXmlSec_LxmlShadowTagNodes (shadow , shadow -> doc -> children , 0 );
455+ next = PyXmlSec_LxmlShadowTagNodes (shadow , shadow -> doc -> children , 0 );
456+ if (shadow -> unlinked ) {
457+ PyXmlSec_LxmlShadowTagNodes (shadow , shadow -> root , next );
458+ }
450459 return 0 ;
451460}
452461
@@ -593,6 +602,33 @@ static PyObject* PyXmlSec_LxmlShadowDumpCopy(PyXmlSec_LxmlShadow* shadow) {
593602 xmlChar * dump = NULL ;
594603 int dump_size = 0 ;
595604
605+ if (shadow -> unlinked ) {
606+ // The paths the reflection carries are relative to the unlinked
607+ // subtree, so that is what has to be re-parsed — the document dump
608+ // does not hold it.
609+ xmlBufferPtr buffer = xmlBufferCreate ();
610+ xmlSaveCtxtPtr save = buffer != NULL ? xmlSaveToBuffer (buffer , "UTF-8" , XML_SAVE_NO_DECL ) : NULL ;
611+ int failed = 1 ;
612+
613+ if (save != NULL ) {
614+ failed = xmlSaveTree (save , shadow -> root ) < 0 ;
615+ failed = xmlSaveClose (save ) < 0 || failed ;
616+ }
617+ if (!failed ) {
618+ bytes = PyBytes_FromStringAndSize ((const char * )xmlBufferContent (buffer ), (Py_ssize_t )xmlBufferLength (buffer ));
619+ if (bytes != NULL ) {
620+ result = PyXmlSec_LxmlElementFromBytes (bytes );
621+ }
622+ Py_XDECREF (bytes );
623+ } else {
624+ PyErr_SetString (PyXmlSec_InternalError , "cannot serialize the private copy." );
625+ }
626+ if (buffer != NULL ) {
627+ xmlBufferFree (buffer );
628+ }
629+ return result ;
630+ }
631+
596632 xmlDocDumpMemory (shadow -> doc , & dump , & dump_size );
597633 if (dump == NULL || dump_size <= 0 ) {
598634 PyErr_SetString (PyXmlSec_InternalError , "cannot serialize the private copy." );
@@ -695,13 +731,15 @@ int PyXmlSec_LxmlShadowBegin(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElementPt
695731 int depth = 0 ;
696732 int dtd = 0 ;
697733 int extdtd = 0 ;
734+ int whole = 0 ;
698735
699736 shadow -> element = element ;
700737 shadow -> owned = NULL ;
701738 shadow -> doc = NULL ;
702739 shadow -> root = NULL ;
703740 shadow -> tags = NULL ;
704741 shadow -> ntags = 0 ;
742+ shadow -> unlinked = 0 ;
705743
706744 // Fast path: lxml links the same libxml2 (the import guard passed), so
707745 // xmlsec can mutate lxml's nodes directly and no copy is needed; End sees
@@ -725,12 +763,22 @@ int PyXmlSec_LxmlShadowBegin(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElementPt
725763 // in the document's internal subset — serializing the element alone
726764 // would leave them undefined and the parse would fail. Copy the whole
727765 // document, declarations included, and cut it back to the element.
766+ // Unless the element was removed from that document: the dump would
767+ // not hold it at all, and the element alone is all there is to copy.
728768 PyObject * live_top = NULL ;
769+ PyObject * live_root = PyObject_CallMethod (tree , "getroot" , NULL );
770+ if (live_root == NULL ) {
771+ goto ON_FAIL ;
772+ }
729773 depth = PyXmlSec_LxmlLivePathTo ((PyObject * )element , path , & live_top );
774+ whole = depth >= 0 && live_top == live_root ;
730775 Py_XDECREF (live_top );
776+ Py_DECREF (live_root );
731777 if (depth < 0 ) {
732778 goto ON_FAIL ;
733779 }
780+ }
781+ if (whole ) {
734782 bytes = PyObject_CallFunctionObjArgs (PyXmlSec_LxmlEtreeToString , tree , NULL );
735783 } else {
736784 bytes = PyXmlSec_LxmlElementToBytes ((PyObject * )element );
@@ -745,7 +793,7 @@ int PyXmlSec_LxmlShadowBegin(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElementPt
745793 if (shadow -> doc == NULL ) {
746794 goto ON_FAIL ;
747795 }
748- if (dtd && PyXmlSec_LxmlShadowReroot (shadow , path , depth ) < 0 ) {
796+ if (whole && PyXmlSec_LxmlShadowReroot (shadow , path , depth ) < 0 ) {
749797 goto ON_FAIL ;
750798 }
751799 shadow -> root = xmlDocGetRootElement (shadow -> doc );
@@ -769,6 +817,7 @@ xmlDocPtr PyXmlSec_LxmlShadowBeginNewDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_L
769817 shadow -> root = NULL ;
770818 shadow -> tags = NULL ;
771819 shadow -> ntags = 0 ;
820+ shadow -> unlinked = 0 ;
772821
773822 // Fast path: allocate the detached subtree straight in the element's own
774823 // document, as the raw code always did.
@@ -785,9 +834,15 @@ xmlDocPtr PyXmlSec_LxmlShadowBeginNewDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_L
785834
786835void PyXmlSec_LxmlShadowDiscard (PyXmlSec_LxmlShadow * shadow ) {
787836 if (shadow -> doc != NULL ) {
837+ if (shadow -> unlinked && shadow -> root != NULL ) {
838+ // Outside the document's tree: freeing the document does not
839+ // reach it.
840+ xmlFreeNode (shadow -> root );
841+ }
788842 xmlFreeDoc (shadow -> doc );
789843 shadow -> doc = NULL ;
790844 shadow -> root = NULL ;
845+ shadow -> unlinked = 0 ;
791846 }
792847 PyMem_Free (shadow -> tags );
793848 shadow -> tags = NULL ;
@@ -1523,8 +1578,10 @@ static void PyXmlSec_LxmlShadowApplyIdSpec(xmlDocPtr doc, xmlNodePtr node, const
15231578}
15241579
15251580// Replays the specs recorded for the shadow's live document onto the copy,
1526- // each at the copy's counterpart of the element it was registered for.
1527- static int PyXmlSec_LxmlShadowReplayIds (PyXmlSec_LxmlShadow * shadow ) {
1581+ // each at the copy's counterpart of the element it was registered for: a spec
1582+ // recorded for an element under the live `live_top` is applied under its
1583+ // counterpart `copy_top`. Returns 0, or -1 with an exception set.
1584+ static int PyXmlSec_LxmlShadowReplayIds (PyXmlSec_LxmlShadow * shadow , PyObject * live_top , xmlNodePtr copy_top ) {
15281585 PyObject * key ;
15291586 PyObject * entry ;
15301587 PyObject * nodes ;
@@ -1567,19 +1624,49 @@ static int PyXmlSec_LxmlShadowReplayIds(PyXmlSec_LxmlShadow* shadow) {
15671624 return -1 ;
15681625 }
15691626 // lxml hands out one proxy per node, so identity settles whether the
1570- // element still hangs under the root being copied ; a registration for
1571- // an element that has since left this tree applies to nothing here.
1572- if (top == ( PyObject * ) shadow -> element ) {
1573- PyXmlSec_LxmlShadowApplyIdSpec (shadow -> doc , PyXmlSec_LxmlShadowWalkNode (shadow -> root , path , depth ),
1627+ // element still hangs under the tree being replayed ; a registration
1628+ // for an element that has since left it applies to nothing here.
1629+ if (top == live_top ) {
1630+ PyXmlSec_LxmlShadowApplyIdSpec (shadow -> doc , PyXmlSec_LxmlShadowWalkNode (copy_top , path , depth ),
15741631 (const xmlChar * )name , (const xmlChar * )ns , subtree );
15751632 }
15761633 Py_DECREF (top );
15771634 }
15781635 return 0 ;
15791636}
15801637
1638+ // Copies the live subtree `element` — removed from its document, so the copy
1639+ // of that document does not hold it either — into the private document, as an
1640+ // unlinked node beside its tree. That is the shape the raw path hands xmlsec:
1641+ // a node outside the tree whose `doc` still answers `#id` references.
1642+ // Returns 0, or -1 with an exception set.
1643+ static int PyXmlSec_LxmlShadowUnlinkedCopy (PyXmlSec_LxmlShadow * shadow , PyObject * element ) {
1644+ PyObject * bytes = PyXmlSec_LxmlElementToBytes (element );
1645+ xmlDocPtr tdoc ;
1646+ xmlNodePtr copy ;
1647+
1648+ if (bytes == NULL ) {
1649+ return -1 ;
1650+ }
1651+ tdoc = PyXmlSec_LxmlShadowParse (bytes , NULL , 0 , "cannot make a private copy of the element." );
1652+ Py_DECREF (bytes );
1653+ if (tdoc == NULL ) {
1654+ return -1 ;
1655+ }
1656+ copy = xmlDocCopyNode (xmlDocGetRootElement (tdoc ), shadow -> doc , 1 );
1657+ xmlFreeDoc (tdoc );
1658+ if (copy == NULL ) {
1659+ PyErr_SetString (PyXmlSec_InternalError , "cannot make a private copy of the element." );
1660+ return -1 ;
1661+ }
1662+ shadow -> root = copy ;
1663+ shadow -> unlinked = 1 ;
1664+ return 0 ;
1665+ }
1666+
15811667int PyXmlSec_LxmlShadowBeginDoc (PyXmlSec_LxmlShadow * shadow , PyXmlSec_LxmlElementPtr element , xmlNodePtr * target ) {
15821668 PyObject * cur = NULL ;
1669+ PyObject * live_root = NULL ;
15831670 PyObject * tree = NULL ;
15841671 PyObject * bytes = NULL ;
15851672 PyObject * url_holder = NULL ;
@@ -1595,6 +1682,7 @@ int PyXmlSec_LxmlShadowBeginDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElemen
15951682 shadow -> root = NULL ;
15961683 shadow -> tags = NULL ;
15971684 shadow -> ntags = 0 ;
1685+ shadow -> unlinked = 0 ;
15981686 * target = NULL ;
15991687
16001688 if (!PyXmlSec_LxmlShadowActive ) {
@@ -1617,6 +1705,13 @@ int PyXmlSec_LxmlShadowBeginDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElemen
16171705 if (tree == NULL ) {
16181706 goto ON_FAIL ;
16191707 }
1708+ // `cur` is the document's root element unless `element` — or an ancestor
1709+ // of it — was removed from the tree: lxml leaves such a subtree pointing
1710+ // at the document it left, which the dump below therefore does not hold.
1711+ live_root = PyObject_CallMethod (tree , "getroot" , NULL );
1712+ if (live_root == NULL ) {
1713+ goto ON_FAIL ;
1714+ }
16201715 // The whole tree is dumped either way here, so only the external subset
16211716 // matters: it has to be loaded for the copy to know the IDs it declares.
16221717 bytes = PyObject_CallFunctionObjArgs (PyXmlSec_LxmlEtreeToString , tree , NULL );
@@ -1632,6 +1727,9 @@ int PyXmlSec_LxmlShadowBeginDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElemen
16321727 goto ON_FAIL ;
16331728 }
16341729 shadow -> root = xmlDocGetRootElement (shadow -> doc );
1730+ if (live_root != cur && PyXmlSec_LxmlShadowUnlinkedCopy (shadow , cur ) < 0 ) {
1731+ goto ON_FAIL ;
1732+ }
16351733 if (PyXmlSec_LxmlShadowMark (shadow ) < 0 ) {
16361734 goto ON_FAIL ;
16371735 }
@@ -1649,14 +1747,23 @@ int PyXmlSec_LxmlShadowBeginDoc(PyXmlSec_LxmlShadow* shadow, PyXmlSec_LxmlElemen
16491747 cur = NULL ;
16501748
16511749 // The registered IDs live in lxml's document, which the copy knows
1652- // nothing about; replay them so that #id references resolve.
1653- if (PyXmlSec_LxmlShadowReplayIds (shadow ) < 0 ) {
1750+ // nothing about; replay them so that #id references resolve. An unlinked
1751+ // subtree carries its own registrations, and the document it left keeps
1752+ // the rest — a reference from the subtree into that document resolves on
1753+ // the raw path too.
1754+ if (PyXmlSec_LxmlShadowReplayIds (shadow , (PyObject * )shadow -> element , shadow -> root ) < 0 ) {
16541755 goto ON_FAIL ;
16551756 }
1757+ if (shadow -> unlinked
1758+ && PyXmlSec_LxmlShadowReplayIds (shadow , live_root , xmlDocGetRootElement (shadow -> doc )) < 0 ) {
1759+ goto ON_FAIL ;
1760+ }
1761+ Py_CLEAR (live_root );
16561762 return 0 ;
16571763
16581764ON_FAIL :
16591765 Py_XDECREF (cur );
1766+ Py_XDECREF (live_root );
16601767 Py_XDECREF (tree );
16611768 Py_XDECREF (bytes );
16621769 Py_XDECREF (url_holder );
@@ -1993,20 +2100,27 @@ static int PyXmlSec_LxmlShadowReflectSites(PyXmlSec_LxmlShadow* shadow) {
19932100 int rv = -1 ;
19942101
19952102 // Re-fetch the root: replacement operations may swap nodes at the top.
1996- list .top = xmlDocGetRootElement (shadow -> doc );
2103+ // An unlinked subtree cannot be replaced at all (libxml2 needs a parent
2104+ // to put the replacement in), so its top is the one the shadow made.
2105+ list .top = shadow -> unlinked ? shadow -> root : xmlDocGetRootElement (shadow -> doc );
19972106 if (list .top == NULL ) {
19982107 PyErr_SetString (PyXmlSec_InternalError , "unexpected mutation site." );
19992108 goto DONE ;
20002109 }
20012110 if (!PYXMLSEC_SHADOW_TAGGED (shadow , list .top )) {
2111+ xmlNodePtr n ;
2112+ int elements = 0 ;
2113+ int others = 0 ;
2114+
2115+ if (shadow -> unlinked ) {
2116+ PyErr_SetString (PyXmlSec_InternalError , "unexpected mutation site." );
2117+ goto DONE ;
2118+ }
20022119 // A fresh root: the call replaced the root itself, so there are no
20032120 // sites to graft — the live root is morphed into it wholesale. lxml
20042121 // holds one element (plus comments and PIs) at document level, so a
20052122 // root replaced by anything else (a Type=Content decryption of the
20062123 // root) cannot be reflected.
2007- xmlNodePtr n ;
2008- int elements = 0 ;
2009- int others = 0 ;
20102124 for (n = shadow -> doc -> children ; n != NULL ; n = n -> next ) {
20112125 if (n -> type == XML_ELEMENT_NODE ) {
20122126 ++ elements ;
0 commit comments