/**
 * XML Security Library (http://www.aleksey.com/xmlsec).
 *
 * This is free software; see the Copyright file in the source distribution for precise wording.
 *
 * Copyright (C) 2002-2026 Aleksey Sanin <aleksey@aleksey.com>. All Rights Reserved.
 */
/**
 * @addtogroup xmlsec_core_nodeset
 * @brief XML nodes set functions
 */
#include "globals.h"

#include <stdlib.h>
#include <string.h>

#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>

#include <xmlsec/xmlsec.h>
#include <xmlsec/nodeset.h>
#include <xmlsec/xmltree.h>
#include <xmlsec/errors.h>
#include <xmlsec/private.h>

#include "cast_helpers.h"

#define xmlSecGetParent(node)           \
    (((node)->type != XML_NAMESPACE_DECL) ? \
        (node)->parent : \
        (xmlNodePtr)((xmlNsPtr)(node))->next)


static int      xmlSecNodeSetContainsNode                (xmlSecNodeSetPtr nset,
                                                         xmlNodePtr node,
                                                         xmlNodePtr parent);
static int      xmlSecNodeSetWalkRecursive              (xmlSecNodeSetPtr nset,
                                                         xmlNodePtr startNode,
                                                         xmlSecNodeSetWalkCallback walkFunc,
                                                         void* data);

/**
 * @brief Creates a new nodes set.
 * @details Creates new nodes set. Caller is responsible for freeing returned object
 * by calling #xmlSecNodeSetDestroy function.
 * @param doc the pointer to parent XML document.
 * @param nodes the list of nodes.
 * @param type the nodes set type.
 * @return pointer to newly allocated node set or NULL if an error occurs.
 */
xmlSecNodeSetPtr
xmlSecNodeSetCreate(xmlDocPtr doc, xmlNodeSetPtr nodes, xmlSecNodeSetType type) {
    xmlSecNodeSetPtr nset;

    nset = (xmlSecNodeSetPtr)xmlMalloc(sizeof(xmlSecNodeSet));
    if(nset == NULL) {
        xmlSecMallocError(sizeof(xmlSecNodeSet), NULL);
        return(NULL);
    }
    memset(nset, 0,  sizeof(xmlSecNodeSet));

    nset->doc   = doc;
    nset->nodes = nodes;
    nset->type  = type;
    nset->next  = nset->prev = nset;
    return(nset);
}

/**
 * @brief Destroys a nodes set.
 * @details Destroys the nodes set created with #xmlSecNodeSetCreate function.
 * @param nset the pointer to node set.
 */
void
xmlSecNodeSetDestroy(xmlSecNodeSetPtr nset) {
    xmlSecNodeSetPtr tmp;
    xmlDocPtr destroyDoc = NULL;

    xmlSecAssert(nset != NULL);

    while((tmp = nset) != NULL) {
        if((nset->next != NULL) && (nset->next != nset)) {
            nset->next->prev = nset->prev;
            nset->prev->next = nset->next;
            nset = nset->next;
        } else {
            nset = NULL;
        }

        if(tmp->nodes != NULL) {
            xmlXPathFreeNodeSet(tmp->nodes);
        }
        if((tmp->doc != NULL) && (tmp->destroyDoc != 0)) {
            /* all nodesets should belong to the same doc */
            xmlSecAssert((destroyDoc == NULL) || (tmp->doc == destroyDoc));
            destroyDoc = tmp->doc; /* can't destroy here because other node sets can refer to it */
        }
        memset(tmp, 0,  sizeof(xmlSecNodeSet));
        xmlFree(tmp);
    }

    /* finally, destroy the doc if needed */
    if(destroyDoc != NULL) {
        xmlFreeDoc(destroyDoc);
    }
}

/**
 * @brief Marks node set to destroy the parent document.
 * @details Instructs node set to destroy nodes parent doc when node set is destroyed.
 * @param nset the pointer to node set.
 */
void
xmlSecNodeSetDocDestroy(xmlSecNodeSetPtr nset) {
    xmlSecAssert(nset != NULL);

    nset->destroyDoc = 1;
}

/* checks node against LibXML2 nodeset */
static int
xmlSecNodeSetCheckNode(xmlNodeSetPtr nodes, xmlNodePtr node, xmlNodePtr parent) {
    xmlSecAssert2(node != NULL, 0);

    /* assume whole tree is included if nodes is NULL */
    if(nodes == NULL) {
        return(1);
    }

    if(node->type != XML_NAMESPACE_DECL) {
        return(xmlXPathNodeSetContains(nodes, node));
    } else {
        xmlNs ns;

        memcpy(&ns, node, sizeof(ns));

        /* this is a libxml hack! check xpath.c for details */
        if((parent != NULL) && (parent->type == XML_ATTRIBUTE_NODE)) {
            ns.next = (xmlNsPtr)parent->parent;
        } else {
            ns.next = (xmlNsPtr)parent;
        }

        /** If the input is an XPath node-set, then the node-set must explicitly
         * contain every node to be rendered to the canonical form.
         */
        return(xmlXPathNodeSetContains(nodes, (xmlNodePtr)&ns));
    }
}

/* checks node or parents against LibXML2 nodeset */
static int
xmlSecNodeSetCheckNodeOrParent(xmlNodeSetPtr nodes, xmlNodePtr node, xmlNodePtr parent) {
    xmlSecAssert2(node != NULL, 0);

    /* assume whole tree is included if nodes is NULL */
    if(nodes == NULL) {
        return(1);
    }
    do {
        if(xmlSecNodeSetCheckNode(nodes, node, parent)) {
            return(1);
        }

        /* traverse up the tree, only element nodes can have children */
        if((parent != NULL) && (parent->type == XML_ELEMENT_NODE)) {
            node = parent;
            parent = parent->parent;
        } else {
            node = NULL;
            parent = NULL;
        }
    } while(node != NULL);

    /* done */
    return(0);
}

/* checks node against THIS nodeset only */
static int
xmlSecNodeSetContainsNode(xmlSecNodeSetPtr nset, xmlNodePtr node, xmlNodePtr parent) {
    xmlSecAssert2(nset != NULL, 0);
    xmlSecAssert2(node != NULL, 0);

    switch(nset->type) {
    case xmlSecNodeSetNormal:
        /* simple case */
        return(xmlSecNodeSetCheckNode(nset->nodes, node, parent));
    case xmlSecNodeSetInvert:
        /* simple case: return inverted result */
        return(!xmlSecNodeSetCheckNode(nset->nodes, node, parent));
    case xmlSecNodeSetTree:
        /* just traverse up the tree to see if any parents are in the nodeset */
        return(xmlSecNodeSetCheckNodeOrParent(nset->nodes, node, parent));
    case xmlSecNodeSetTreeWithoutComments:
        /* drop comments */
        if(node->type == XML_COMMENT_NODE) {
            return(0);
        }
        /* just traverse up the tree to see if any parents are in the nodeset */
        return(xmlSecNodeSetCheckNodeOrParent(nset->nodes, node, parent));
    case xmlSecNodeSetTreeInvert:
        /* just traverse up the tree to see if any parents are in the nodeset and invert result */
        return(!xmlSecNodeSetCheckNodeOrParent(nset->nodes, node, parent));
    case xmlSecNodeSetTreeWithoutCommentsInvert:
        /* drop comments */
        if(node->type == XML_COMMENT_NODE) {
            return(0);
        }
        /* just traverse up the tree to see if any parents are in the nodeset and invert result */
        return(!xmlSecNodeSetCheckNodeOrParent(nset->nodes, node, parent));
    default:
        xmlSecUnsupportedEnumValueError("node set type", nset->type, NULL);
        return(0);
    }
}

/**
 * @brief Checks if a node is in the nodes set.
 * @details Checks whether the @p node is in the nodes set or not.
 * @param nset the pointer to node set.
 * @param node the pointer to XML node to check.
 * @param parent the pointer to @p node parent node.
 * @return 1 if the @p node is in the nodes set @p nset, 0 if it is not
 * and a negative value if an error occurs.
 */
int
xmlSecNodeSetContains(xmlSecNodeSetPtr nset, xmlNodePtr node, xmlNodePtr parent) {
    int status = 1;
    xmlSecNodeSetPtr curNset;

    xmlSecAssert2(node != NULL, 0);

    /* special cases: */
    if(nset == NULL) {
        return(1);
    }

    /* iterate through the nodesets list */
    status = 1;
    curNset = nset;
    do {
        switch(curNset->op) {
        case xmlSecNodeSetIntersection:
            if(status && !xmlSecNodeSetContainsNode(curNset, node, parent)) {
                status = 0;
            }
            break;
        case xmlSecNodeSetSubtraction:
            if(status && xmlSecNodeSetContainsNode(curNset, node, parent)) {
                status = 0;
            }
            break;
        case xmlSecNodeSetUnion:
            if(!status && xmlSecNodeSetContainsNode(curNset, node, parent)) {
                status = 1;
            }
            break;
        default:
            xmlSecOtherError2(XMLSEC_ERRORS_R_INVALID_OPERATION, NULL,
                "node set operation=" XMLSEC_ENUM_FMT, XMLSEC_ENUM_CAST(curNset->op));
            return(-1);
        }
        curNset = curNset->next;
    } while(curNset != nset);

    /* done */
    return(status);
}

/**
 * @brief Adds a nodes set to another with an operation.
 * @details Adds @p newNSet to the @p nset using operation @p op.
 * @param nset the pointer to current nodes set (or NULL).
 * @param newNSet the pointer to new nodes set.
 * @param op the operation type.
 * @return the pointer to combined nodes set or NULL if an error
 * occurs.
 */
xmlSecNodeSetPtr
xmlSecNodeSetAdd(xmlSecNodeSetPtr nset, xmlSecNodeSetPtr newNSet, xmlSecNodeSetOp op) {
    xmlSecAssert2(newNSet != NULL, NULL);
    xmlSecAssert2(newNSet->next == newNSet, NULL);

    newNSet->op = op;
    if(nset == NULL) {
        return(newNSet);
    }

    /* all nodesets should belong to the same doc */
    xmlSecAssert2(nset->doc == newNSet->doc, NULL);

    newNSet->next = nset;
    newNSet->prev = nset->prev;
    nset->prev->next = newNSet;
    nset->prev       = newNSet;
    return(nset);
}

/**
 * @brief DEPRECATED: Adds a nodes set as a child list.
 * @details Adds @p newNSet to the @p nset as child using operation @p op.
 * @param nset the pointer to current nodes set (or NULL).
 * @param newNSet the pointer to new nodes set.
 * @param op the operation type.
 * @return the pointer to combined nodes set or NULL if an error
 * occurs.
 */
xmlSecNodeSetPtr
xmlSecNodeSetAddList(xmlSecNodeSetPtr nset XMLSEC_ATTRIBUTE_UNUSED,
    xmlSecNodeSetPtr newNSet XMLSEC_ATTRIBUTE_UNUSED,
    xmlSecNodeSetOp op XMLSEC_ATTRIBUTE_UNUSED
) {
    UNREFERENCED_PARAMETER(nset);
    UNREFERENCED_PARAMETER(newNSet);
    UNREFERENCED_PARAMETER(op);

    xmlSecNotImplementedError("xmlSecNodeSetAddList is deprecated");
    return(NULL);
}


/**
 * @brief Walks all nodes in a set calling a callback function.
 * @details Calls the function @p walkFunc once per each node in the nodes set @p nset.
 * If the @p walkFunc returns a negative value, then the walk procedure
 * is interrupted.
 * @param nset the pointer to node set.
 * @param walkFunc the callback functions.
 * @param data the application specific data passed to the @p walkFunc.
 * @return 0 on success or a negative value if an error occurs.
 */
int
xmlSecNodeSetWalk(xmlSecNodeSetPtr nset, xmlSecNodeSetWalkCallback walkFunc, void* data) {
    xmlNodePtr cur;
    int ret = 0;

    xmlSecAssert2(nset != NULL, -1);
    xmlSecAssert2(nset->doc != NULL, -1);
    xmlSecAssert2(walkFunc != NULL, -1);

    /* special cases */
    if(nset->nodes != NULL) {
        int ii;

        switch(nset->type) {
        case xmlSecNodeSetNormal:
        case xmlSecNodeSetTree:
        case xmlSecNodeSetTreeWithoutComments:
            for(ii = 0; (ret >= 0) && (ii < nset->nodes->nodeNr); ++ii) {
                ret = xmlSecNodeSetWalkRecursive(nset, nset->nodes->nodeTab[ii], walkFunc, data);
                if(ret < 0) {
                    xmlSecInternalError("xmlSecNodeSetWalkRecursive", NULL);
                    return(ret);
                }
            }
            return(ret);
        default:
            break;
        }
    }

    for(cur = nset->doc->children; (cur != NULL) && (ret >= 0); cur = cur->next) {
        ret = xmlSecNodeSetWalkRecursive(nset, cur, walkFunc, data);
        if(ret < 0) {
            xmlSecInternalError("xmlSecNodeSetWalkRecursive", NULL);
            return(ret);
        }
    }
    return(ret);
}

typedef struct {
    xmlSecNodeSetPtr nset;
    xmlSecNodeSetWalkCallback walkFunc;
    void* data;
} xmlSecNodeSetWalkCtx;

static int
xmlSecNodeSetWalkRecursiveCallback(xmlNodePtr cur, void* data) {
    xmlSecNodeSetWalkCtx* ctx = (xmlSecNodeSetWalkCtx*)data;
    xmlNodePtr parent = xmlSecGetParent(cur);
    int ret;

    xmlSecAssert2(ctx != NULL, -1);
    xmlSecAssert2(ctx->nset != NULL, -1);
    xmlSecAssert2(ctx->walkFunc != NULL, -1);
    xmlSecAssert2(cur != NULL, -1);

    /* the node itself */
    if(xmlSecNodeSetContains(ctx->nset, cur, parent)) {
        ret = ctx->walkFunc(ctx->nset, cur, parent, ctx->data);
        if(ret < 0) {
            return(-1);
        }
    }

    /* element and document nodes have children */
    if((cur->type == XML_ELEMENT_NODE) || (cur->type == XML_DOCUMENT_NODE)) {
        xmlAttrPtr attr;
        xmlNodePtr node;
        xmlNsPtr ns, tmp;

        attr = (xmlAttrPtr)cur->properties;
        while(attr != NULL) {
            if(xmlSecNodeSetContains(ctx->nset, (xmlNodePtr)attr, cur)) {
                ret = ctx->walkFunc(ctx->nset, (xmlNodePtr)attr, cur, ctx->data);
                if(ret < 0) {
                    return(-1);
                }
            }
            attr = attr->next;
        }

        node = cur;
        while(node != NULL) {
            ns = node->nsDef;
            while(ns != NULL) {
                tmp = xmlSearchNs(ctx->nset->doc, cur, ns->prefix);
                if((tmp == ns) && xmlSecNodeSetContains(ctx->nset, (xmlNodePtr)ns, cur)) {
                    ret = ctx->walkFunc(ctx->nset, (xmlNodePtr)ns, cur, ctx->data);
                    if(ret < 0) {
                        return(-1);
                    }
                }
                ns = ns->next;
            }
            node = node->parent;
        }
    }

    /* done and continue the walk */
    return(1);
}

static int
xmlSecNodeSetWalkRecursive(xmlSecNodeSetPtr nset, xmlNodePtr startNode, xmlSecNodeSetWalkCallback walkFunc, void* data) {
    xmlSecNodeSetWalkCtx ctx;
    int ret;

    xmlSecAssert2(nset != NULL, -1);
    xmlSecAssert2(startNode != NULL, -1);
    xmlSecAssert2(walkFunc != NULL, -1);

    ctx.nset = nset;
    ctx.walkFunc = walkFunc;
    ctx.data = data;

    ret = xmlSecDepthFirstTreeWalk(startNode, xmlSecNodeSetWalkRecursiveCallback, &ctx);
    if(ret < 0) {
        xmlSecInternalError("xmlSecDepthFirstTreeWalk", NULL);
        return(-1);
    }

    return(0);
}

/**
 * @brief Creates a nodes set from parent subtree children.
 * @details Creates a new nodes set that contains:
 *  - if @p withComments is not 0 and @p invert is 0:
 *    all nodes in the @p parent subtree;
 *  - if @p withComments is 0 and @p invert is 0:
 *    all nodes in the @p parent subtree except comment nodes;
 *  - if @p withComments is not 0 and @p invert not is 0:
 *    all nodes in the @p doc except nodes in the @p parent subtree;
 *  - if @p withComments is 0 and @p invert is 0:
 *    all nodes in the @p doc except nodes in the @p parent subtree
 *    and comment nodes.
 * @param doc the pointer to an XML document.
 * @param parent the pointer to parent XML node or NULL if we want to include all document nodes.
 * @param withComments the flag include  comments or not.
 * @param invert the "invert" flag.
 * @return pointer to the newly created xmlSecNodeSet structure
 * or NULL if an error occurs.
 */
xmlSecNodeSetPtr
xmlSecNodeSetGetChildren(xmlDocPtr doc, const xmlNodePtr parent, int withComments, int invert) {
    xmlSecNodeSetPtr nset;
    xmlNodeSetPtr nodes;
    xmlSecNodeSetType type;

    xmlSecAssert2(doc != NULL, NULL);

    nodes = xmlXPathNodeSetCreate(parent);
    if(nodes == NULL) {
        xmlSecXmlError("xmlXPathNodeSetCreate", NULL);
        return(NULL);
    }

    /* if parent is NULL then we add all the doc children */
    if(parent == NULL) {
        xmlNodePtr cur;
        for(cur = doc->children; cur != NULL; cur = cur->next) {
            if(withComments || (cur->type != XML_COMMENT_NODE)) {
                xmlXPathNodeSetAdd(nodes, cur);
            }
        }
    }

    if(withComments && invert) {
        type = xmlSecNodeSetTreeInvert;
    } else if(withComments && !invert) {
        type = xmlSecNodeSetTree;
    } else if(!withComments && invert) {
        type = xmlSecNodeSetTreeWithoutCommentsInvert;
    } else { /* if(!withComments && !invert) */
        type = xmlSecNodeSetTreeWithoutComments;
    }

    nset = xmlSecNodeSetCreate(doc, nodes, type);
    if(nset == NULL) {
        xmlSecInternalError("xmlSecNodeSetCreate", NULL);
        xmlXPathFreeNodeSet(nodes);
        return(NULL);
    }
    return(nset);
}

static int
xmlSecNodeSetDumpTextNodesWalkCallback(xmlSecNodeSetPtr nset, xmlNodePtr cur,
                                   xmlNodePtr parent XMLSEC_ATTRIBUTE_UNUSED,
                                   void* data) {
    int ret;
    xmlSecAssert2(nset != NULL, -1);
    xmlSecAssert2(cur != NULL, -1);
    xmlSecAssert2(data != NULL, -1);

    UNREFERENCED_PARAMETER(parent);

    if(cur->type != XML_TEXT_NODE) {
        return(0);
    }
    ret = xmlOutputBufferWriteString((xmlOutputBufferPtr)data,
            (char*)(cur->content));
    if(ret < 0) {
        xmlSecXmlError("xmlOutputBufferWriteString", NULL);
        return(-1);
    }
    return(0);
}

/**
 * @brief Dumps text node content from a nodes set.
 * @details Dumps content of all the text nodes from @p nset to @p out.
 * @param nset the pointer to node set.
 * @param out the output buffer.
 * @return 0 on success or a negative value otherwise.
 */
int
xmlSecNodeSetDumpTextNodes(xmlSecNodeSetPtr nset, xmlOutputBufferPtr out) {
    xmlSecAssert2(nset != NULL, -1);
    xmlSecAssert2(out != NULL, -1);

    return(xmlSecNodeSetWalk(nset, xmlSecNodeSetDumpTextNodesWalkCallback, out));
}
/**
 * @brief Prints information about @p nset to the @p output.
 * @param nset the pointer to node set.
 * @param output the pointer to output FILE.
 */
void
xmlSecNodeSetDebugDump(xmlSecNodeSetPtr nset, FILE *output) {
    int ii, len;
    xmlNodePtr cur;

    xmlSecAssert(nset != NULL);
    xmlSecAssert(output != NULL);

    fprintf(output, "== Nodes set ");
    switch(nset->type) {
    case xmlSecNodeSetNormal:
        fprintf(output, "(xmlSecNodeSetNormal)\n");
        break;
    case xmlSecNodeSetInvert:
        fprintf(output, "(xmlSecNodeSetInvert)\n");
        break;
    case xmlSecNodeSetTree:
        fprintf(output, "(xmlSecNodeSetTree)\n");
        break;
    case xmlSecNodeSetTreeWithoutComments:
        fprintf(output, "(xmlSecNodeSetTreeWithoutComments)\n");
        break;
    case xmlSecNodeSetTreeInvert:
        fprintf(output, "(xmlSecNodeSetTreeInvert)\n");
        break;
    case xmlSecNodeSetTreeWithoutCommentsInvert:
        fprintf(output, "(xmlSecNodeSetTreeWithoutCommentsInvert)\n");
        break;
    case xmlSecNodeSetList:
        xmlSecNotImplementedError("xmlSecNodeSetList is deprecated");
        fprintf(output, "(xmlSecNodeSetList)\n");
        break;
    }

    len = xmlXPathNodeSetGetLength(nset->nodes);
    for(ii = 0; ii < len; ++ii) {
        cur = xmlXPathNodeSetItem(nset->nodes, ii);
        xmlSecAssert(cur != NULL);

        if(cur->type != XML_NAMESPACE_DECL) {
            fprintf(output, XMLSEC_ENUM_FMT ": %s\n",
                XMLSEC_ENUM_CAST(cur->type),
                (cur->name) ? cur->name : BAD_CAST "null");
        } else {
            xmlNsPtr ns = (xmlNsPtr)cur;
            fprintf(output, XMLSEC_ENUM_FMT ": %s=%s (%s:%s)\n",
                XMLSEC_ENUM_CAST(cur->type),
                (ns->prefix) ? ns->prefix : BAD_CAST "null",
                (ns->href) ? ns->href : BAD_CAST "null",
                (((xmlNodePtr)ns->next)->ns &&
                 ((xmlNodePtr)ns->next)->ns->prefix) ?
                  ((xmlNodePtr)ns->next)->ns->prefix : BAD_CAST "null",
                ((xmlNodePtr)ns->next)->name);
        }
    }
}
