/*
 * Copyright (C) 2004 Andrew Beekhof <andrew@beekhof.net>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <crm_internal.h>

#include <sys/param.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdarg.h>

#include <crm/crm.h>
#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>  /* CRM_XML_LOG_BASE */

#if HAVE_BZLIB_H
#  include <bzlib.h>
#endif

#if HAVE_LIBXML2
#  include <libxml/parser.h>
#  include <libxml/tree.h>
#endif

#define XML_BUFFER_SIZE	4096
#define XML_PARSER_DEBUG 0

static inline int
__get_prefix(const char *prefix, xmlNode *xml, char *buffer, int offset);

typedef struct {
    int found;
    const char *string;
} filter_t;

enum xml_private_flags {
     xpf_none        = 0x0000,
     xpf_dirty       = 0x0001,
     xpf_deleted     = 0x0002,
     xpf_created     = 0x0004,
     xpf_modified    = 0x0008,

     xpf_tracking    = 0x0010,
     xpf_processed   = 0x0020,
     xpf_skip        = 0x0040,
     xpf_moved       = 0x0080,

     xpf_acl_enabled = 0x0100,
     xpf_acl_read    = 0x0200,
     xpf_acl_write   = 0x0400,
     xpf_acl_deny    = 0x0800,

     xpf_acl_create  = 0x1000,
     xpf_acl_denied  = 0x2000,
};

typedef struct xml_private_s 
{
        long check;
        uint32_t flags;
        char *user;
        GListPtr acls;
        GListPtr deleted_objs;
} xml_private_t;

typedef struct xml_acl_s {
        enum xml_private_flags mode;
        char *xpath;
} xml_acl_t;

typedef struct xml_deleted_obj_s {
        char *path;
        int position;
} xml_deleted_obj_t;

/* *INDENT-OFF* */

static filter_t filter[] = {
    { 0, XML_ATTR_ORIGIN },
    { 0, XML_CIB_ATTR_WRITTEN },
    { 0, XML_ATTR_UPDATE_ORIG },
    { 0, XML_ATTR_UPDATE_CLIENT },
    { 0, XML_ATTR_UPDATE_USER },
};
/* *INDENT-ON* */

static xmlNode *subtract_xml_comment(xmlNode * parent, xmlNode * left, xmlNode * right, gboolean * changed);
static xmlNode *find_xml_comment(xmlNode * root, xmlNode * search_comment, gboolean exact);
static int add_xml_comment(xmlNode * parent, xmlNode * target, xmlNode * update);
static bool __xml_acl_check(xmlNode *xml, const char *name, enum xml_private_flags mode);
const char *__xml_acl_to_text(enum xml_private_flags flags);

#define CHUNK_SIZE 1024
static inline bool TRACKING_CHANGES(xmlNode *xml)
{
    if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
        return FALSE;
    } else if(is_set(((xml_private_t *)xml->doc->_private)->flags, xpf_tracking)) {
        return TRUE;
    }
    return FALSE;
}

#define buffer_print(buffer, max, offset, fmt, args...) do {            \
        int rc = (max);                                                 \
        if(buffer) {                                                    \
            rc = snprintf((buffer) + (offset), (max) - (offset), fmt, ##args); \
        }                                                               \
        if(buffer && rc < 0) {                                          \
            crm_perror(LOG_ERR, "snprintf failed at offset %d", offset); \
            (buffer)[(offset)] = 0;                                     \
            break;                                                      \
        } else if(rc >= ((max) - (offset))) {                           \
            char *tmp = NULL;                                           \
            (max) = QB_MAX(CHUNK_SIZE, (max) * 2);                      \
            tmp = realloc_safe((buffer), (max));                        \
            CRM_ASSERT(tmp);                                            \
            (buffer) = tmp;                                             \
        } else {                                                        \
            offset += rc;                                               \
            break;                                                      \
        }                                                               \
    } while(1);

static void
insert_prefix(int options, char **buffer, int *offset, int *max, int depth)
{
    if (options & xml_log_option_formatted) {
        size_t spaces = 2 * depth;

        if ((*buffer) == NULL || spaces >= ((*max) - (*offset))) {
            (*max) = QB_MAX(CHUNK_SIZE, (*max) * 2);
            (*buffer) = realloc_safe((*buffer), (*max));
        }
        memset((*buffer) + (*offset), ' ', spaces);
        (*offset) += spaces;
    }
}

static void
set_parent_flag(xmlNode *xml, long flag) 
{

    for(; xml; xml = xml->parent) {
        xml_private_t *p = xml->_private;

        if(p == NULL) {
            /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
        } else {
            p->flags |= flag;
            /* crm_trace("Setting flag %x due to %s[@id=%s]", flag, xml->name, ID(xml)); */
        }
    }
}

static void
set_doc_flag(xmlNode *xml, long flag) 
{

    if(xml && xml->doc && xml->doc->_private){
        /* During calls to xmlDocCopyNode(), xml->doc may be unset */
        xml_private_t *p = xml->doc->_private;

        p->flags |= flag;
        /* crm_trace("Setting flag %x due to %s[@id=%s]", flag, xml->name, ID(xml)); */
    }
}

static void
__xml_node_dirty(xmlNode *xml) 
{
    set_doc_flag(xml, xpf_dirty);
    set_parent_flag(xml, xpf_dirty);
}

static void
__xml_node_clean(xmlNode *xml) 
{
    xmlNode *cIter = NULL;
    xml_private_t *p = xml->_private;

    if(p) {
        p->flags = 0;
    }

    for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
        __xml_node_clean(cIter);
    }
}

static void
crm_node_created(xmlNode *xml) 
{
    xmlNode *cIter = NULL;
    xml_private_t *p = xml->_private;

    if(p && TRACKING_CHANGES(xml)) {
        if(is_not_set(p->flags, xpf_created)) {
            p->flags |= xpf_created;
            __xml_node_dirty(xml);
        }

        for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
           crm_node_created(cIter);
        }
    }
}

static void
crm_attr_dirty(xmlAttr *a) 
{
    xmlNode *parent = a->parent;
    xml_private_t *p = NULL;

    p = a->_private;
    p->flags |= (xpf_dirty|xpf_modified);
    p->flags = (p->flags & ~xpf_deleted);
    /* crm_trace("Setting flag %x due to %s[@id=%s, @%s=%s]", */
    /*           xpf_dirty, parent?parent->name:NULL, ID(parent), a->name, a->children->content); */

    __xml_node_dirty(parent);
}

int get_tag_name(const char *input, size_t offset, size_t max);
int get_attr_name(const char *input, size_t offset, size_t max);
int get_attr_value(const char *input, size_t offset, size_t max);
gboolean can_prune_leaf(xmlNode * xml_node);

void diff_filter_context(int context, int upper_bound, int lower_bound,
                         xmlNode * xml_node, xmlNode * parent);
int in_upper_context(int depth, int context, xmlNode * xml_node);
int add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * update, gboolean as_diff);

static inline const char *
crm_attr_value(xmlAttr * attr)
{
    if (attr == NULL || attr->children == NULL) {
        return NULL;
    }
    return (const char *)attr->children->content;
}

static inline xmlAttr *
crm_first_attr(xmlNode * xml)
{
    if (xml == NULL) {
        return NULL;
    }
    return xml->properties;
}

#define XML_PRIVATE_MAGIC (long) 0x81726354

static void
__xml_acl_free(void *data)
{
    if(data) {
        xml_acl_t *acl = data;

        free(acl->xpath);
        free(acl);
    }
}

static void
__xml_deleted_obj_free(void *data)
{
    if(data) {
        xml_deleted_obj_t *deleted_obj = data;

        free(deleted_obj->path);
        free(deleted_obj);
    }
}

static void
__xml_private_clean(xml_private_t *p)
{
    if(p) {
        CRM_ASSERT(p->check == XML_PRIVATE_MAGIC);

        free(p->user);
        p->user = NULL;

        if(p->acls) {
            g_list_free_full(p->acls, __xml_acl_free);
            p->acls = NULL;
        }

        if(p->deleted_objs) {
            g_list_free_full(p->deleted_objs, __xml_deleted_obj_free);
            p->deleted_objs = NULL;
        }
    }
}


static void
__xml_private_free(xml_private_t *p)
{
    __xml_private_clean(p);
    free(p);
}

static void
pcmkDeregisterNode(xmlNodePtr node)
{
    __xml_private_free(node->_private);
}

static void
pcmkRegisterNode(xmlNodePtr node)
{
    xml_private_t *p = NULL;

    switch(node->type) {
        case XML_ELEMENT_NODE:
        case XML_DOCUMENT_NODE:
        case XML_ATTRIBUTE_NODE:
        case XML_COMMENT_NODE:
            p = calloc(1, sizeof(xml_private_t));
            p->check = XML_PRIVATE_MAGIC;
            /* Flags will be reset if necessary when tracking is enabled */
            p->flags |= (xpf_dirty|xpf_created);
            node->_private = p;
            break;
        case XML_TEXT_NODE:
        case XML_DTD_NODE:
        case XML_CDATA_SECTION_NODE:
            break;
        default:
            /* Ignore */
            crm_trace("Ignoring %p %d", node, node->type);
            CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
            break;
    }

    if(p && TRACKING_CHANGES(node)) {
        /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
         * not hooked up at the point we are called
         */
        set_doc_flag(node, xpf_dirty);
        __xml_node_dirty(node);
    }
}

static xml_acl_t *
__xml_acl_create(xmlNode * xml, xmlNode *target, enum xml_private_flags mode)
{
    xml_acl_t *acl = NULL;

    xml_private_t *p = NULL;
    const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG);
    const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF);
    const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH);

    if(tag == NULL) {
        /* Compatibility handling for pacemaker < 1.1.12 */
        tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1);
    }
    if(ref == NULL) {
        /* Compatibility handling for pacemaker < 1.1.12 */
        ref = crm_element_value(xml, XML_ACL_ATTR_REFv1);
    }

    if(target == NULL || target->doc == NULL || target->doc->_private == NULL){
        CRM_ASSERT(target);
        CRM_ASSERT(target->doc);
        CRM_ASSERT(target->doc->_private);
        return NULL;

    } else if (tag == NULL && ref == NULL && xpath == NULL) {
        crm_trace("No criteria %p", xml);
        return NULL;
    }

    p = target->doc->_private;
    acl = calloc(1, sizeof(xml_acl_t));
    if (acl) {
        const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE);

        acl->mode = mode;
        if(xpath) {
            acl->xpath = strdup(xpath);
            crm_trace("Using xpath: %s", acl->xpath);

        } else {
            int offset = 0;
            char buffer[XML_BUFFER_SIZE];

            if(tag) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "//%s", tag);
            } else {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "//*");
            }

            if(ref || attr) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "[");
            }

            if(ref) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "@id='%s'", ref);
            }

            if(ref && attr) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, " and ");
            }

            if(attr) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "@%s", attr);
            }

            if(ref || attr) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "]");
            }

            CRM_LOG_ASSERT(offset > 0);
            acl->xpath = strdup(buffer);
            crm_trace("Built xpath: %s", acl->xpath);
        }

        p->acls = g_list_append(p->acls, acl);
    }
    return acl;
}

static gboolean
__xml_acl_parse_entry(xmlNode * acl_top, xmlNode * acl_entry, xmlNode *target)
{
    xmlNode *child = NULL;

    for (child = __xml_first_child(acl_entry); child; child = __xml_next(child)) {
        const char *tag = crm_element_name(child);
        const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND);

        if (strcmp(XML_ACL_TAG_PERMISSION, tag) == 0){
            tag = kind;
        }

        crm_trace("Processing %s %p", tag, child);
        if(tag == NULL) {
            CRM_ASSERT(tag != NULL);

        } else if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0
                   || strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) {
            const char *ref_role = crm_element_value(child, XML_ATTR_ID);

            if (ref_role) {
                xmlNode *role = NULL;

                for (role = __xml_first_child(acl_top); role; role = __xml_next(role)) {
                    if (strcmp(XML_ACL_TAG_ROLE, (const char *)role->name) == 0) {
                        const char *role_id = crm_element_value(role, XML_ATTR_ID);

                        if (role_id && strcmp(ref_role, role_id) == 0) {
                            crm_debug("Unpacking referenced role: %s", role_id);
                            __xml_acl_parse_entry(acl_top, role, target);
                            break;
                        }
                    }
                }
            }

        } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) {
            __xml_acl_create(child, target, xpf_acl_read);

        } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) {
            __xml_acl_create(child, target, xpf_acl_write);

        } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) {
            __xml_acl_create(child, target, xpf_acl_deny);

        } else {
            crm_warn("Unknown ACL entry: %s/%s", tag, kind);
        }
    }

    return TRUE;
}

/*
    <acls>
      <acl_target id="l33t-haxor"><role id="auto-l33t-haxor"/></acl_target>
      <acl_role id="auto-l33t-haxor">
        <acl_permission id="crook-nothing" kind="deny" xpath="/cib"/>
      </acl_role>
      <acl_target id="niceguy">
        <role id="observer"/>
      </acl_target>
      <acl_role id="observer">
        <acl_permission id="observer-read-1" kind="read" xpath="/cib"/>
        <acl_permission id="observer-write-1" kind="write" xpath="//nvpair[@name='stonith-enabled']"/>
        <acl_permission id="observer-write-2" kind="write" xpath="//nvpair[@name='target-role']"/>
      </acl_role>
      <acl_target id="badidea"><role id="auto-badidea"/></acl_target>
      <acl_role id="auto-badidea">
        <acl_permission id="badidea-resources" kind="read" xpath="//meta_attributes"/>
        <acl_permission id="badidea-resources-2" kind="deny" reference="dummy-meta_attributes"/>
      </acl_role>
    </acls>
*/

const char *
__xml_acl_to_text(enum xml_private_flags flags)
{
    if(is_set(flags, xpf_acl_deny)) {
        return "deny";
    }
    if(is_set(flags, xpf_acl_write)) {
        return "read/write";
    }
    if(is_set(flags, xpf_acl_read)) {
        return "read";
    }

    return "none";
}

static void
__xml_acl_apply(xmlNode *xml) 
{
    GListPtr aIter = NULL;
    xml_private_t *p = NULL;
    xmlXPathObjectPtr xpathObj = NULL;

    if(xml_acl_enabled(xml) == FALSE) {
        p = xml->doc->_private;
        crm_trace("Not applying ACLs for %s", p->user);
        return;
    }

    p = xml->doc->_private;
    for(aIter = p->acls; aIter != NULL; aIter = aIter->next) {
        int max = 0, lpc = 0;
        xml_acl_t *acl = aIter->data;

        xpathObj = xpath_search(xml, acl->xpath);
        max = numXpathResults(xpathObj);

        for(lpc = 0; lpc < max; lpc++) {
            xmlNode *match = getXpathResult(xpathObj, lpc);
            char *path = xml_get_path(match);

            p = match->_private;
            crm_trace("Applying %x to %s for %s", acl->mode, path, acl->xpath);

#ifdef SUSE_ACL_COMPAT
            if(is_not_set(p->flags, acl->mode)) {
                if(is_set(p->flags, xpf_acl_read)
                   || is_set(p->flags, xpf_acl_write)
                   || is_set(p->flags, xpf_acl_deny)) {
                    crm_config_warn("Configuration element %s is matched by multiple ACL rules, only the first applies ('%s' wins over '%s')",
                                    path, __xml_acl_to_text(p->flags), __xml_acl_to_text(acl->mode));
                    free(path);
                    continue;
                }
            }
#endif

            p->flags |= acl->mode;
            free(path);
        }
        crm_trace("Now enforcing ACL: %s (%d matches)", acl->xpath, max);
        freeXpathObject(xpathObj);
    }

    p = xml->_private;
    if(is_not_set(p->flags, xpf_acl_read) && is_not_set(p->flags, xpf_acl_write)) {
        p->flags |= xpf_acl_deny;
        p = xml->doc->_private;
        crm_info("Enforcing default ACL for %s to %s", p->user, crm_element_name(xml));
    }

}

static void
__xml_acl_unpack(xmlNode *source, xmlNode *target, const char *user)
{
#if ENABLE_ACL
    xml_private_t *p = NULL;

    if(target == NULL || target->doc == NULL || target->doc->_private == NULL) {
        return;
    }

    p = target->doc->_private;
    if(pcmk_acl_required(user) == FALSE) {
        crm_trace("no acls needed for '%s'", user);

    } else if(p->acls == NULL) {
        xmlNode *acls = get_xpath_object("//"XML_CIB_TAG_ACLS, source, LOG_TRACE);

        free(p->user);
        p->user = strdup(user);

        if(acls) {
            xmlNode *child = NULL;

            for (child = __xml_first_child(acls); child; child = __xml_next(child)) {
                const char *tag = crm_element_name(child);

                if (strcmp(tag, XML_ACL_TAG_USER) == 0 || strcmp(tag, XML_ACL_TAG_USERv1) == 0) {
                    const char *id = crm_element_value(child, XML_ATTR_ID);

                    if(id && strcmp(id, user) == 0) {
                        crm_debug("Unpacking ACLs for %s", id);
                        __xml_acl_parse_entry(acls, child, target);
                    }
                }
            }
        }
    }
#endif
}

static inline bool
__xml_acl_mode_test(enum xml_private_flags allowed, enum xml_private_flags requested)
{
    if(is_set(allowed, xpf_acl_deny)) {
        return FALSE;

    } else if(is_set(allowed, requested)) {
        return TRUE;

    } else if(is_set(requested, xpf_acl_read) && is_set(allowed, xpf_acl_write)) {
        return TRUE;

    } else if(is_set(requested, xpf_acl_create) && is_set(allowed, xpf_acl_write)) {
        return TRUE;

    } else if(is_set(requested, xpf_acl_create) && is_set(allowed, xpf_created)) {
        return TRUE;
    }
    return FALSE;
}

/* rc = TRUE if orig_cib has been filtered
 * That means '*result' rather than 'xml' should be exploited afterwards
 */
static bool
__xml_purge_attributes(xmlNode *xml)
{
    xmlNode *child = NULL;
    xmlAttr *xIter = NULL;
    bool readable_children = FALSE;
    xml_private_t *p = xml->_private;

    if(__xml_acl_mode_test(p->flags, xpf_acl_read)) {
        crm_trace("%s[@id=%s] is readable", crm_element_name(xml), ID(xml));
        return TRUE;
    }

    xIter = crm_first_attr(xml);
    while(xIter != NULL) {
        xmlAttr *tmp = xIter;
        const char *prop_name = (const char *)xIter->name;

        xIter = xIter->next;
        if (strcmp(prop_name, XML_ATTR_ID) == 0) {
            continue;
        }

        xmlUnsetProp(xml, tmp->name);
    }

    child = __xml_first_child(xml);
    while ( child != NULL ) {
        xmlNode *tmp = child;

        child = __xml_next(child);
        readable_children |= __xml_purge_attributes(tmp);
    }

    if(readable_children == FALSE) {
        free_xml(xml); /* Nothing readable under here, purge completely */
    }
    return readable_children;
}

bool
xml_acl_filtered_copy(const char *user, xmlNode* acl_source, xmlNode *xml, xmlNode ** result)
{
    GListPtr aIter = NULL;
    xmlNode *target = NULL;
    xml_private_t *p = NULL;
    xml_private_t *doc = NULL;

    *result = NULL;
    if(xml == NULL || pcmk_acl_required(user) == FALSE) {
        crm_trace("no acls needed for '%s'", user);
        return FALSE;
    }

    crm_trace("filtering copy of %p for '%s'", xml, user);
    target = copy_xml(xml);
    if(target == NULL) {
        return TRUE;
    }

    __xml_acl_unpack(acl_source, target, user);
    set_doc_flag(target, xpf_acl_enabled);
    __xml_acl_apply(target);

    doc = target->doc->_private;
    for(aIter = doc->acls; aIter != NULL && target; aIter = aIter->next) {
        int max = 0;
        xml_acl_t *acl = aIter->data;

        if(acl->mode != xpf_acl_deny) {
            /* Nothing to do */

        } else if(acl->xpath) {
            int lpc = 0;
            xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath);

            max = numXpathResults(xpathObj);
            for(lpc = 0; lpc < max; lpc++) {
                xmlNode *match = getXpathResult(xpathObj, lpc);

                crm_trace("Purging attributes from %s", acl->xpath);
                if(__xml_purge_attributes(match) == FALSE && match == target) {
                    crm_trace("No access to the entire document for %s", user);
                    freeXpathObject(xpathObj);
                    return TRUE;
                }
            }
            crm_trace("Enforced ACL %s (%d matches)", acl->xpath, max);
            freeXpathObject(xpathObj);
        }
    }

    p = target->_private;
    if(is_set(p->flags, xpf_acl_deny) && __xml_purge_attributes(target) == FALSE) {
        crm_trace("No access to the entire document for %s", user);
        return TRUE;
    }

    if(doc->acls) {
        g_list_free_full(doc->acls, __xml_acl_free);
        doc->acls = NULL;

    } else {
        crm_trace("Ordinary user '%s' cannot access the CIB without any defined ACLs", doc->user);
        free_xml(target);
        target = NULL;
    }

    if(target) {
        *result = target;
    }

    return TRUE;
}

static void
__xml_acl_post_process(xmlNode * xml)
{
    xmlNode *cIter = __xml_first_child(xml);
    xml_private_t *p = xml->_private;

    if(is_set(p->flags, xpf_created)) {
        xmlAttr *xIter = NULL;
        char *path = xml_get_path(xml);

        /* Always allow new scaffolding (e.g. node with no attributes or only an
         * 'id'), except in the ACLs section
         */

        for (xIter = crm_first_attr(xml); xIter != NULL; xIter = xIter->next) {
            const char *prop_name = (const char *)xIter->name;

            if (strcmp(prop_name, XML_ATTR_ID) == 0 && strstr(path, "/"XML_CIB_TAG_ACLS"/") == NULL) {
                /* Delay the acl check */
                continue;

            } else if(__xml_acl_check(xml, NULL, xpf_acl_write)) {
                crm_trace("Creation of %s=%s is allowed", crm_element_name(xml), ID(xml));
                break;

            } else {
                crm_trace("Cannot add new node %s at %s", crm_element_name(xml), path);

                if(xml != xmlDocGetRootElement(xml->doc)) {
                    xmlUnlinkNode(xml);
                    xmlFreeNode(xml);
                }
                free(path);
                return;
            }
        }
        free(path);
    }

    while (cIter != NULL) {
        xmlNode *child = cIter;
        cIter = __xml_next(cIter); /* In case it is free'd */
        __xml_acl_post_process(child);
    }
}

bool
xml_acl_denied(xmlNode *xml) 
{
    if(xml && xml->doc && xml->doc->_private){
        xml_private_t *p = xml->doc->_private;

        return is_set(p->flags, xpf_acl_denied);
    }
    return FALSE;
}

void
xml_acl_disable(xmlNode *xml)
{
    if(xml_acl_enabled(xml)) {
        xml_private_t *p = xml->doc->_private;

        /* Catch anything that was created but shouldn't have been */
        __xml_acl_apply(xml);
        __xml_acl_post_process(xml);
        clear_bit(p->flags, xpf_acl_enabled);
    }
}

bool
xml_acl_enabled(xmlNode *xml)
{
    if(xml && xml->doc && xml->doc->_private){
        xml_private_t *p = xml->doc->_private;

        return is_set(p->flags, xpf_acl_enabled);
    }
    return FALSE;
}

void
xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) 
{
    xml_accept_changes(xml);
    crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
    set_doc_flag(xml, xpf_tracking);
    if(enforce_acls) {
        if(acl_source == NULL) {
            acl_source = xml;
        }
        set_doc_flag(xml, xpf_acl_enabled);
        __xml_acl_unpack(acl_source, xml, user);
        __xml_acl_apply(xml);
    }
}

bool xml_tracking_changes(xmlNode * xml)
{
    if(xml == NULL) {
        return FALSE;

    } else if(is_set(((xml_private_t *)xml->doc->_private)->flags, xpf_tracking)) {
        return TRUE;
    }
    return FALSE;
}

bool xml_document_dirty(xmlNode *xml) 
{
    if(xml != NULL && xml->doc && xml->doc->_private) {
        xml_private_t *doc = xml->doc->_private;

        return is_set(doc->flags, xpf_dirty);
    }
    return FALSE;
}

/*
<diff format="2.0">
  <version>
    <source admin_epoch="1" epoch="2" num_updates="3"/>
    <target admin_epoch="1" epoch="3" num_updates="0"/>
  </version>
  <change operation="add" xpath="/cib/configuration/nodes">
    <node id="node2" uname="node2" description="foo"/>
  </change>
  <change operation="add" xpath="/cib/configuration/nodes/node[node2]">
    <instance_attributes id="nodes-node"><!-- NOTE: can be a full tree -->
      <nvpair id="nodes-node2-ram" name="ram" value="1024M"/>
    </instance_attributes>
  </change>
  <change operation="update" xpath="/cib/configuration/nodes[@id='node2']">
    <change-list>
      <change-attr operation="set" name="type" value="member"/>
      <change-attr operation="unset" name="description"/>
    </change-list>
    <change-result>
      <node id="node2" uname="node2" type="member"/><!-- NOTE: not recursive -->
    </change-result>
  </change>
  <change operation="delete" xpath="/cib/configuration/nodes/node[@id='node3'] /">
  <change operation="update" xpath="/cib/configuration/resources/group[@id='g1']">
    <change-list>
      <change-attr operation="set" name="description" value="some grabage here"/>
    </change-list>
    <change-result>
      <group id="g1" description="some grabage here"/><!-- NOTE: not recursive -->
    </change-result>
  </change>
  <change operation="update" xpath="/cib/status/node_state[@id='node2]/lrm[@id='node2']/lrm_resources/lrm_resource[@id='Fence']">
    <change-list>
      <change-attr operation="set" name="oper" value="member"/>
      <change-attr operation="set" name="operation_key" value="Fence_start_0"/>
      <change-attr operation="set" name="operation" value="start"/>
      <change-attr operation="set" name="transition-key" value="2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"/>
      <change-attr operation="set" name="transition-magic" value="0:0;2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"/>
      <change-attr operation="set" name="call-id" value="2"/>
      <change-attr operation="set" name="rc-code" value="0"/>
    </change-list>
    <change-result>
      <lrm_rsc_op id="Fence_last_0" operation_key="Fence_start_0" operation="start" crm-debug-origin="crm_simulate"  transition-key="2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" transition-magic="0:0;2:-1:0:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" call-id="2" rc-code="0" op-status="0" interval="0" exec-time="0" queue-time="0" op-digest="f2317cad3d54cec5d7d7aa7d0bf35cf8"/>
    </change-result>
  </change>
</diff>
 */
static int __xml_offset(xmlNode *xml) 
{
    int position = 0;
    xmlNode *cIter = NULL;

    for(cIter = xml; cIter->prev; cIter = cIter->prev) {
        xml_private_t *p = ((xmlNode*)cIter->prev)->_private;

        if(is_not_set(p->flags, xpf_skip)) {
            position++;
        }
    }

    return position;
}

static int __xml_offset_no_deletions(xmlNode *xml) 
{
    int position = 0;
    xmlNode *cIter = NULL;

    for(cIter = xml; cIter->prev; cIter = cIter->prev) {
        xml_private_t *p = ((xmlNode*)cIter->prev)->_private;

        if(is_not_set(p->flags, xpf_deleted)) {
            position++;
        }
    }

    return position;
}

static void
__xml_build_changes(xmlNode * xml, xmlNode *patchset)
{
    xmlNode *cIter = NULL;
    xmlAttr *pIter = NULL;
    xmlNode *change = NULL;
    xml_private_t *p = xml->_private;

    if(patchset && is_set(p->flags, xpf_created)) {
        int offset = 0;
        char buffer[XML_BUFFER_SIZE];

        if(__get_prefix(NULL, xml->parent, buffer, offset) > 0) {
            int position = __xml_offset_no_deletions(xml);

            change = create_xml_node(patchset, XML_DIFF_CHANGE);

            crm_xml_add(change, XML_DIFF_OP, "create");
            crm_xml_add(change, XML_DIFF_PATH, buffer);
            crm_xml_add_int(change, XML_DIFF_POSITION, position);
            add_node_copy(change, xml);
        }

        return;
    }

    for (pIter = crm_first_attr(xml); pIter != NULL; pIter = pIter->next) {
        xmlNode *attr = NULL;

        p = pIter->_private;
        if(is_not_set(p->flags, xpf_deleted) && is_not_set(p->flags, xpf_dirty)) {
            continue;
        }

        if(change == NULL) {
            int offset = 0;
            char buffer[XML_BUFFER_SIZE];

            if(__get_prefix(NULL, xml, buffer, offset) > 0) {
                change = create_xml_node(patchset, XML_DIFF_CHANGE);

                crm_xml_add(change, XML_DIFF_OP, "modify");
                crm_xml_add(change, XML_DIFF_PATH, buffer);

                change = create_xml_node(change, XML_DIFF_LIST);
            }
        }

        attr = create_xml_node(change, XML_DIFF_ATTR);

        crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name);
        if(p->flags & xpf_deleted) {
            crm_xml_add(attr, XML_DIFF_OP, "unset");

        } else {
            const char *value = crm_element_value(xml, (const char *)pIter->name);

            crm_xml_add(attr, XML_DIFF_OP, "set");
            crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value);
        }
    }

    if(change) {
        xmlNode *result = NULL;

        change = create_xml_node(change->parent, XML_DIFF_RESULT);
        result = create_xml_node(change, (const char *)xml->name);

        for (pIter = crm_first_attr(xml); pIter != NULL; pIter = pIter->next) {
            const char *value = crm_element_value(xml, (const char *)pIter->name);

            p = pIter->_private;
            if (is_not_set(p->flags, xpf_deleted)) {
                crm_xml_add(result, (const char *)pIter->name, value);
            }
        }
    }

    for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
        __xml_build_changes(cIter, patchset);
    }

    p = xml->_private;
    if(patchset && is_set(p->flags, xpf_moved)) {
        int offset = 0;
        char buffer[XML_BUFFER_SIZE];

        crm_trace("%s.%s moved to position %d", xml->name, ID(xml), __xml_offset(xml));
        if(__get_prefix(NULL, xml, buffer, offset) > 0) {
            change = create_xml_node(patchset, XML_DIFF_CHANGE);

            crm_xml_add(change, XML_DIFF_OP, "move");
            crm_xml_add(change, XML_DIFF_PATH, buffer);
            crm_xml_add_int(change, XML_DIFF_POSITION, __xml_offset_no_deletions(xml));
        }
    }
}

static void
__xml_accept_changes(xmlNode * xml)
{
    xmlNode *cIter = NULL;
    xmlAttr *pIter = NULL;
    xml_private_t *p = xml->_private;

    p->flags = xpf_none;
    pIter = crm_first_attr(xml);

    while (pIter != NULL) {
        const xmlChar *name = pIter->name;

        p = pIter->_private;
        pIter = pIter->next;

        if(p->flags & xpf_deleted) {
            xml_remove_prop(xml, (const char *)name);

        } else {
            p->flags = xpf_none;
        }
    }

    for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
        __xml_accept_changes(cIter);
    }
}

static bool
is_config_change(xmlNode *xml)
{
    GListPtr gIter = NULL;
    xml_private_t *p = NULL;
    xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION);

    if(config) {
        p = config->_private;
    }
    if(p && is_set(p->flags, xpf_dirty)) {
        return TRUE;
    }

    if(xml->doc && xml->doc->_private) {
        p = xml->doc->_private;
        for(gIter = p->deleted_objs; gIter; gIter = gIter->next) {
            xml_deleted_obj_t *deleted_obj = gIter->data;

            if(strstr(deleted_obj->path, "/"XML_TAG_CIB"/"XML_CIB_TAG_CONFIGURATION) != NULL) {
                return TRUE;
            }
        }
    }

    return FALSE;
}

static void
xml_repair_v1_diff(xmlNode * last, xmlNode * next, xmlNode * local_diff, gboolean changed)
{
    int lpc = 0;
    xmlNode *cib = NULL;
    xmlNode *diff_child = NULL;

    const char *tag = NULL;

    const char *vfields[] = {
        XML_ATTR_GENERATION_ADMIN,
        XML_ATTR_GENERATION,
        XML_ATTR_NUMUPDATES,
    };

    if (local_diff == NULL) {
        crm_trace("Nothing to do");
        return;
    }

    tag = "diff-removed";
    diff_child = find_xml_node(local_diff, tag, FALSE);
    if (diff_child == NULL) {
        diff_child = create_xml_node(local_diff, tag);
    }

    tag = XML_TAG_CIB;
    cib = find_xml_node(diff_child, tag, FALSE);
    if (cib == NULL) {
        cib = create_xml_node(diff_child, tag);
    }

    for(lpc = 0; last && lpc < DIMOF(vfields); lpc++){
        const char *value = crm_element_value(last, vfields[lpc]);

        crm_xml_add(diff_child, vfields[lpc], value);
        if(changed || lpc == 2) {
            crm_xml_add(cib, vfields[lpc], value);
        }
    }

    tag = "diff-added";
    diff_child = find_xml_node(local_diff, tag, FALSE);
    if (diff_child == NULL) {
        diff_child = create_xml_node(local_diff, tag);
    }

    tag = XML_TAG_CIB;
    cib = find_xml_node(diff_child, tag, FALSE);
    if (cib == NULL) {
        cib = create_xml_node(diff_child, tag);
    }

    for(lpc = 0; next && lpc < DIMOF(vfields); lpc++){
        const char *value = crm_element_value(next, vfields[lpc]);

        crm_xml_add(diff_child, vfields[lpc], value);
    }

    if (next) {
        xmlAttrPtr xIter = NULL;

        for (xIter = next->properties; xIter; xIter = xIter->next) {
            const char *p_name = (const char *)xIter->name;
            const char *p_value = crm_element_value(next, p_name);

            xmlSetProp(cib, (const xmlChar *)p_name, (const xmlChar *)p_value);
        }
    }

    crm_log_xml_explicit(local_diff, "Repaired-diff");
}

static xmlNode *
xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, bool suppress)
{
    xmlNode *patchset = diff_xml_object(source, target, suppress);

    if(patchset) {
        CRM_LOG_ASSERT(xml_document_dirty(target));
        xml_repair_v1_diff(source, target, patchset, config);
        crm_xml_add(patchset, "format", "1");
    }
    return patchset;
}

static xmlNode *
xml_create_patchset_v2(xmlNode *source, xmlNode *target)
{
    int lpc = 0;
    GListPtr gIter = NULL;
    xml_private_t *doc = NULL;

    xmlNode *v = NULL;
    xmlNode *version = NULL;
    xmlNode *patchset = NULL;
    const char *vfields[] = {
        XML_ATTR_GENERATION_ADMIN,
        XML_ATTR_GENERATION,
        XML_ATTR_NUMUPDATES,
    };

    CRM_ASSERT(target);
    if(xml_document_dirty(target) == FALSE) {
        return NULL;
    }

    CRM_ASSERT(target->doc);
    doc = target->doc->_private;

    patchset = create_xml_node(NULL, XML_TAG_DIFF);
    crm_xml_add_int(patchset, "format", 2);

    version = create_xml_node(patchset, XML_DIFF_VERSION);

    v = create_xml_node(version, XML_DIFF_VSOURCE);
    for(lpc = 0; lpc < DIMOF(vfields); lpc++){
        const char *value = crm_element_value(source, vfields[lpc]);

        if(value == NULL) {
            value = "1";
        }
        crm_xml_add(v, vfields[lpc], value);
    }

    v = create_xml_node(version, XML_DIFF_VTARGET);
    for(lpc = 0; lpc < DIMOF(vfields); lpc++){
        const char *value = crm_element_value(target, vfields[lpc]);

        if(value == NULL) {
            value = "1";
        }
        crm_xml_add(v, vfields[lpc], value);
    }

    for(gIter = doc->deleted_objs; gIter; gIter = gIter->next) {
        xml_deleted_obj_t *deleted_obj = gIter->data;
        xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE);

        crm_xml_add(change, XML_DIFF_OP, "delete");
        crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path);
        if (deleted_obj->position >= 0) {
            crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position);
        }
    }

    __xml_build_changes(target, patchset);
    return patchset;
}

static gboolean patch_legacy_mode(void)
{
    static gboolean init = TRUE;
    static gboolean legacy = FALSE;

    if(init) {
        init = FALSE;
        legacy = daemon_option_enabled("cib", "legacy");
        if(legacy) {
            crm_notice("Enabled legacy mode");
        }
    }
    return legacy;
}

xmlNode *
xml_create_patchset(int format, xmlNode *source, xmlNode *target, bool *config_changed, bool manage_version)
{
    int counter = 0;
    bool config = FALSE;
    xmlNode *patch = NULL;
    const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION);

    xml_acl_disable(target);
    if(xml_document_dirty(target) == FALSE) {
        crm_trace("No change %d", format);
        return NULL; /* No change */
    }

    config = is_config_change(target);
    if(config_changed) {
        *config_changed = config;
    }

    if(manage_version && config) {
        crm_trace("Config changed %d", format);
        crm_xml_add(target, XML_ATTR_NUMUPDATES, "0");

        crm_element_value_int(target, XML_ATTR_GENERATION, &counter);
        crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1);

    } else if(manage_version) {
        crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter);
        crm_trace("Status changed %d - %d %s", format, counter, crm_element_value(source, XML_ATTR_NUMUPDATES));
        crm_xml_add_int(target, XML_ATTR_NUMUPDATES, counter+1);
    }

    if(format == 0) {
        if(patch_legacy_mode()) {
            format = 1;

        } else if(compare_version("3.0.8", version) < 0) {
            format = 2;

        } else {
            format = 1;
        }
        crm_trace("Using patch format %d for version: %s", format, version);
    }

    switch(format) {
        case 1:
            patch = xml_create_patchset_v1(source, target, config, FALSE);
            break;
        case 2:
            patch = xml_create_patchset_v2(source, target);
            break;
        default:
            crm_err("Unknown patch format: %d", format);
            return NULL;
    }

    return patch;
}

void
patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest)
{
    int format = 1;
    const char *version = NULL;
    char *digest = NULL;

    if (patch == NULL || source == NULL || target == NULL) {
        return;
    }

    /* NOTE: We should always call xml_accept_changes() before calculating digest. */
    /* Otherwise, with an on-tracking dirty target, we could get a wrong digest. */
    CRM_LOG_ASSERT(xml_document_dirty(target) == FALSE);

    crm_element_value_int(patch, "format", &format);
    if (format > 1 && with_digest == FALSE) {
        return;
    }

    version = crm_element_value(source, XML_ATTR_CRM_VERSION);
    digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version);

    crm_xml_add(patch, XML_ATTR_DIGEST, digest);
    free(digest);

    return;
}

static void
__xml_log_element(int log_level, const char *file, const char *function, int line,
                  const char *prefix, xmlNode * data, int depth, int options);

void
xml_log_patchset(uint8_t log_level, const char *function, xmlNode * patchset)
{
    int format = 1;
    xmlNode *child = NULL;
    xmlNode *added = NULL;
    xmlNode *removed = NULL;
    gboolean is_first = TRUE;

    int add[] = { 0, 0, 0 };
    int del[] = { 0, 0, 0 };

    const char *fmt = NULL;
    const char *digest = NULL;
    int options = xml_log_option_formatted;

    static struct qb_log_callsite *patchset_cs = NULL;

    if (patchset_cs == NULL) {
        patchset_cs = qb_log_callsite_get(function, __FILE__, "xml-patchset", log_level, __LINE__, 0);
    }

    if (patchset == NULL) {
        crm_trace("Empty patch");
        return;

    } else if (log_level == 0) {
        /* Log to stdout */
    } else if (crm_is_callsite_active(patchset_cs, log_level, 0) == FALSE) {
        return;
    }

    xml_patch_versions(patchset, add, del);
    fmt = crm_element_value(patchset, "format");
    digest = crm_element_value(patchset, XML_ATTR_DIGEST);

    if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
        do_crm_log_alias(log_level, __FILE__, function, __LINE__,
                         "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
        do_crm_log_alias(log_level, __FILE__, function, __LINE__,
                         "Diff: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);

    } else if (patchset != NULL && (add[0] || add[1] || add[2])) {
        do_crm_log_alias(log_level, __FILE__, function, __LINE__, 
                         "%s: Local-only Change: %d.%d.%d", function ? function : "",
                         add[0], add[1], add[2]);
    }

    crm_element_value_int(patchset, "format", &format);
    if(format == 2) {
        xmlNode *change = NULL;

        for (change = __xml_first_child(patchset); change != NULL; change = __xml_next(change)) {
            const char *op = crm_element_value(change, XML_DIFF_OP);
            const char *xpath = crm_element_value(change, XML_DIFF_PATH);

            if(op == NULL) {
            } else if(strcmp(op, "create") == 0) {
                int lpc = 0, max = 0;
                char *prefix = crm_strdup_printf("++ %s: ", xpath);

                max = strlen(prefix);
                __xml_log_element(log_level, __FILE__, function, __LINE__, prefix, change->children,
                                  0, xml_log_option_formatted|xml_log_option_open);

                for(lpc = 2; lpc < max; lpc++) {
                    prefix[lpc] = ' ';
                }

                __xml_log_element(log_level, __FILE__, function, __LINE__, prefix, change->children,
                                  0, xml_log_option_formatted|xml_log_option_close|xml_log_option_children);
                free(prefix);

            } else if(strcmp(op, "move") == 0) {
                do_crm_log_alias(log_level, __FILE__, function, __LINE__, "+~ %s moved to offset %s", xpath, crm_element_value(change, XML_DIFF_POSITION));

            } else if(strcmp(op, "modify") == 0) {
                xmlNode *clist = first_named_child(change, XML_DIFF_LIST);
                char buffer_set[XML_BUFFER_SIZE];
                char buffer_unset[XML_BUFFER_SIZE];
                int o_set = 0;
                int o_unset = 0;

                buffer_set[0] = 0;
                buffer_unset[0] = 0;
                for (child = __xml_first_child(clist); child != NULL; child = __xml_next(child)) {
                    const char *name = crm_element_value(child, "name");

                    op = crm_element_value(child, XML_DIFF_OP);
                    if(op == NULL) {
                    } else if(strcmp(op, "set") == 0) {
                        const char *value = crm_element_value(child, "value");

                        if(o_set > 0) {
                            o_set += snprintf(buffer_set + o_set, XML_BUFFER_SIZE - o_set, ", ");
                        }
                        o_set += snprintf(buffer_set + o_set, XML_BUFFER_SIZE - o_set, "@%s=%s", name, value);

                    } else if(strcmp(op, "unset") == 0) {
                        if(o_unset > 0) {
                            o_unset += snprintf(buffer_unset + o_unset, XML_BUFFER_SIZE - o_unset, ", ");
                        }
                        o_unset += snprintf(buffer_unset + o_unset, XML_BUFFER_SIZE - o_unset, "@%s", name);
                    }
                }
                if(o_set) {
                    do_crm_log_alias(log_level, __FILE__, function, __LINE__, "+  %s:  %s", xpath, buffer_set);
                }
                if(o_unset) {
                    do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s:  %s", xpath, buffer_unset);
                }

            } else if(strcmp(op, "delete") == 0) {
                int position = -1;

                crm_element_value_int(change, XML_DIFF_POSITION, &position);
                if (position >= 0) {
                    do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s (%d)", xpath, position);

                } else {
                    do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s", xpath);
                }
            }
        }
        return;
    }

    if (log_level < LOG_DEBUG || function == NULL) {
        options |= xml_log_option_diff_short;
    }

    removed = find_xml_node(patchset, "diff-removed", FALSE);
    for (child = __xml_first_child(removed); child != NULL; child = __xml_next(child)) {
        log_data_element(log_level, __FILE__, function, __LINE__, "- ", child, 0,
                         options | xml_log_option_diff_minus);
        if (is_first) {
            is_first = FALSE;
        } else {
            do_crm_log_alias(log_level, __FILE__, function, __LINE__, " --- ");
        }
    }

    is_first = TRUE;
    added = find_xml_node(patchset, "diff-added", FALSE);
    for (child = __xml_first_child(added); child != NULL; child = __xml_next(child)) {
        log_data_element(log_level, __FILE__, function, __LINE__, "+ ", child, 0,
                         options | xml_log_option_diff_plus);
        if (is_first) {
            is_first = FALSE;
        } else {
            do_crm_log_alias(log_level, __FILE__, function, __LINE__, " +++ ");
        }
    }
}

void
xml_log_changes(uint8_t log_level, const char *function, xmlNode * xml)
{
    GListPtr gIter = NULL;
    xml_private_t *doc = NULL;

    CRM_ASSERT(xml);
    CRM_ASSERT(xml->doc);

    doc = xml->doc->_private;
    if(is_not_set(doc->flags, xpf_dirty)) {
        return;
    }

    for(gIter = doc->deleted_objs; gIter; gIter = gIter->next) {
        xml_deleted_obj_t *deleted_obj = gIter->data;

        if (deleted_obj->position >= 0) {
            do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s (%d)",
                             deleted_obj->path, deleted_obj->position);

        } else {
            do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s",
                             deleted_obj->path);
        }
    }

    log_data_element(log_level, __FILE__, function, __LINE__, "+ ", xml, 0,
                     xml_log_option_formatted|xml_log_option_dirty_add);
}

void
xml_accept_changes(xmlNode * xml)
{
    xmlNode *top = NULL;
    xml_private_t *doc = NULL;

    if(xml == NULL) {
        return;
    }

    crm_trace("Accepting changes to %p", xml);
    doc = xml->doc->_private;
    top = xmlDocGetRootElement(xml->doc);

    __xml_private_clean(xml->doc->_private);

    if(is_not_set(doc->flags, xpf_dirty)) {
        doc->flags = xpf_none;
        return;
    }

    doc->flags = xpf_none;
    __xml_accept_changes(top);
}

static xmlNode *
find_element(xmlNode *haystack, xmlNode *needle, gboolean exact)
{
    CRM_CHECK(needle != NULL, return NULL);
    return (needle->type == XML_COMMENT_NODE)?
           find_xml_comment(haystack, needle, exact)
           : find_entity(haystack, crm_element_name(needle), ID(needle));
}

/* Simplified version for applying v1-style XML patches */
static void
__subtract_xml_object(xmlNode * target, xmlNode * patch)
{
    xmlNode *patch_child = NULL;
    xmlNode *cIter = NULL;
    xmlAttrPtr xIter = NULL;

    char *id = NULL;
    const char *name = NULL;
    const char *value = NULL;

    if (target == NULL || patch == NULL) {
        return;
    }

    if (target->type == XML_COMMENT_NODE) {
        gboolean dummy;

        subtract_xml_comment(target->parent, target, patch, &dummy);
    }

    name = crm_element_name(target);
    CRM_CHECK(name != NULL, return);
    CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(patch)), return);
    CRM_CHECK(safe_str_eq(ID(target), ID(patch)), return);

    /* check for XML_DIFF_MARKER in a child */
    id = crm_element_value_copy(target, XML_ATTR_ID);
    value = crm_element_value(patch, XML_DIFF_MARKER);
    if (value != NULL && strcmp(value, "removed:top") == 0) {
        crm_trace("We are the root of the deletion: %s.id=%s", name, id);
        free_xml(target);
        free(id);
        return;
    }

    for (xIter = crm_first_attr(patch); xIter != NULL; xIter = xIter->next) {
        const char *p_name = (const char *)xIter->name;

        /* Removing and then restoring the id field would change the ordering of properties */
        if (safe_str_neq(p_name, XML_ATTR_ID)) {
            xml_remove_prop(target, p_name);
        }
    }

    /* changes to child objects */
    cIter = __xml_first_child(target);
    while (cIter) {
        xmlNode *target_child = cIter;

        cIter = __xml_next(cIter);
        patch_child = find_element(patch, target_child, FALSE);
        __subtract_xml_object(target_child, patch_child);
    }
    free(id);
}

static void
__add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * patch)
{
    xmlNode *patch_child = NULL;
    xmlNode *target_child = NULL;
    xmlAttrPtr xIter = NULL;

    const char *id = NULL;
    const char *name = NULL;
    const char *value = NULL;

    if (patch == NULL) {
        return;
    } else if (parent == NULL && target == NULL) {
        return;
    }

    /* check for XML_DIFF_MARKER in a child */
    value = crm_element_value(patch, XML_DIFF_MARKER);
    if (target == NULL
        && value != NULL
        && strcmp(value, "added:top") == 0) {
        id = ID(patch);
        name = crm_element_name(patch);
        crm_trace("We are the root of the addition: %s.id=%s", name, id);
        add_node_copy(parent, patch);
        return;

    } else if(target == NULL) {
        id = ID(patch);
        name = crm_element_name(patch);
        crm_err("Could not locate: %s.id=%s", name, id);
        return;
    }

    if (target->type == XML_COMMENT_NODE) {
        add_xml_comment(parent, target, patch);
    }

    name = crm_element_name(target);
    CRM_CHECK(name != NULL, return);
    CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(patch)), return);
    CRM_CHECK(safe_str_eq(ID(target), ID(patch)), return);

    for (xIter = crm_first_attr(patch); xIter != NULL; xIter = xIter->next) {
        const char *p_name = (const char *)xIter->name;
        const char *p_value = crm_element_value(patch, p_name);

        xml_remove_prop(target, p_name); /* Preserve the patch order */
        crm_xml_add(target, p_name, p_value);
    }

    /* changes to child objects */
    for (patch_child = __xml_first_child(patch); patch_child != NULL;
         patch_child = __xml_next(patch_child)) {

        target_child = find_element(target, patch_child, FALSE);
        __add_xml_object(target, target_child, patch_child);
    }
}

/*!
 * \internal
 * \brief Find additions or removals in a patch set
 *
 * \param[in]     patchset   XML of patch
 * \param[in]     format     Patch version
 * \param[in]     added      TRUE if looking for additions, FALSE if removals
 * \param[in,out] patch_node Will be set to node if found
 *
 * \return TRUE if format is valid, FALSE if invalid
 */
static bool
find_patch_xml_node(xmlNode *patchset, int format, bool added,
                    xmlNode **patch_node)
{
    xmlNode *cib_node;
    const char *label;

    switch(format) {
        case 1:
            label = added? "diff-added" : "diff-removed";
            *patch_node = find_xml_node(patchset, label, FALSE);
            cib_node = find_xml_node(*patch_node, "cib", FALSE);
            if (cib_node != NULL) {
                *patch_node = cib_node;
            }
            break;
        case 2:
            label = added? "target" : "source";
            *patch_node = find_xml_node(patchset, "version", FALSE);
            *patch_node = find_xml_node(*patch_node, label, FALSE);
            break;
        default:
            crm_warn("Unknown patch format: %d", format);
            *patch_node = NULL;
            return FALSE;
    }
    return TRUE;
}

bool xml_patch_versions(xmlNode *patchset, int add[3], int del[3])
{
    int lpc = 0;
    int format = 1;
    xmlNode *tmp = NULL;

    const char *vfields[] = {
        XML_ATTR_GENERATION_ADMIN,
        XML_ATTR_GENERATION,
        XML_ATTR_NUMUPDATES,
    };


    crm_element_value_int(patchset, "format", &format);

    /* Process removals */
    if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) {
        return -EINVAL;
    }
    if (tmp) {
        for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
            crm_element_value_int(tmp, vfields[lpc], &(del[lpc]));
            crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]);
        }
    }

    /* Process additions */
    if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) {
        return -EINVAL;
    }
    if (tmp) {
        for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
            crm_element_value_int(tmp, vfields[lpc], &(add[lpc]));
            crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]);
        }
    }

    return pcmk_ok;
}

static int
xml_patch_version_check(xmlNode *xml, xmlNode *patchset, int format) 
{
    int lpc = 0;
    bool changed = FALSE;

    int this[] = { 0, 0, 0 };
    int add[] = { 0, 0, 0 };
    int del[] = { 0, 0, 0 };

    const char *vfields[] = {
        XML_ATTR_GENERATION_ADMIN,
        XML_ATTR_GENERATION,
        XML_ATTR_NUMUPDATES,
    };

    for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
        crm_element_value_int(xml, vfields[lpc], &(this[lpc]));
        crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]);
        if (this[lpc] < 0) {
            this[lpc] = 0;
        }
    }

    /* Set some defaults in case nothing is present */
    add[0] = this[0];
    add[1] = this[1];
    add[2] = this[2] + 1;
    for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
        del[lpc] = this[lpc];
    }

    xml_patch_versions(patchset, add, del);

    for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
        if(this[lpc] < del[lpc]) {
            crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", vfields[lpc],
                      this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2]);
            return -pcmk_err_diff_resync;

        } else if(this[lpc] > del[lpc]) {
            crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", vfields[lpc],
                     this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2], patchset);
            crm_log_xml_info(patchset, "OldPatch");
            return -pcmk_err_old_data;
        }
    }

    for(lpc = 0; lpc < DIMOF(vfields); lpc++) {
        if(add[lpc] > del[lpc]) {
            changed = TRUE;
        }
    }

    if(changed == FALSE) {
        crm_notice("Versions did not change in patch %d.%d.%d", add[0], add[1], add[2]);
        return -pcmk_err_old_data;
    }

    crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
             add[0], add[1], add[2], this[0], this[1], this[2]);
    return pcmk_ok;
}

static int
xml_apply_patchset_v1(xmlNode *xml, xmlNode *patchset)
{
    int rc = pcmk_ok;
    int root_nodes_seen = 0;

    xmlNode *child_diff = NULL;
    xmlNode *added = find_xml_node(patchset, "diff-added", FALSE);
    xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE);
    xmlNode *old = copy_xml(xml);

    crm_trace("Subtraction Phase");
    for (child_diff = __xml_first_child(removed); child_diff != NULL;
         child_diff = __xml_next(child_diff)) {
        CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
        if (root_nodes_seen == 0) {
            __subtract_xml_object(xml, child_diff);
        }
        root_nodes_seen++;
    }

    if (root_nodes_seen > 1) {
        crm_err("(-) Diffs cannot contain more than one change set... saw %d", root_nodes_seen);
        rc = -ENOTUNIQ;
    }

    root_nodes_seen = 0;
    crm_trace("Addition Phase");
    if (rc == pcmk_ok) {
        xmlNode *child_diff = NULL;

        for (child_diff = __xml_first_child(added); child_diff != NULL;
             child_diff = __xml_next(child_diff)) {
            CRM_CHECK(root_nodes_seen == 0, rc = FALSE);
            if (root_nodes_seen == 0) {
                __add_xml_object(NULL, xml, child_diff);
            }
            root_nodes_seen++;
        }
    }

    if (root_nodes_seen > 1) {
        crm_err("(+) Diffs cannot contain more than one change set... saw %d", root_nodes_seen);
        rc = -ENOTUNIQ;
    }

    purge_diff_markers(xml);       /* Purge prior to checking the digest */

    free_xml(old);
    return rc;
}

static xmlNode *
__first_xml_child_match(xmlNode *parent, const char *name, const char *id, int position)
{
    xmlNode *cIter = NULL;

    for (cIter = __xml_first_child(parent); cIter != NULL; cIter = __xml_next(cIter)) {
        if(strcmp((const char *)cIter->name, name) != 0) {
            continue;
        } else if(id) {
            const char *cid = ID(cIter);
            if(cid == NULL || strcmp(cid, id) != 0) {
                continue;
            }
        }

        /* The "position" makes sense only for XML comments for now */
        if (cIter->type == XML_COMMENT_NODE
            && position >= 0
            && __xml_offset(cIter) != position) {
            continue;
        }

        return cIter;
    }
    return NULL;
}

static xmlNode *
__xml_find_path(xmlNode *top, const char *key, int target_position)
{
    xmlNode *target = (xmlNode*)top->doc;
    char *id = malloc(XML_BUFFER_SIZE);
    char *tag = malloc(XML_BUFFER_SIZE);
    char *section = malloc(XML_BUFFER_SIZE);
    char *current = strdup(key);
    char *remainder = malloc(XML_BUFFER_SIZE);
    int rc = 0;

    while(current) {
        rc = sscanf (current, "/%[^/]%s", section, remainder);
        if(rc <= 0) {
            crm_trace("Done");
            break;

        } else if(rc > 2) {
            crm_trace("Aborting on %s", current);
            target = NULL;
            break;

        } else if(tag && section) {
            int f = sscanf (section, "%[^[][@id='%[^']", tag, id);
            int current_position = -1;

            /* The "target_position" is for the target tag */
            if (rc == 1 && target_position >= 0) {
                current_position = target_position;
            }

            switch(f) {
                case 1:
                    target = __first_xml_child_match(target, tag, NULL, current_position);
                    break;
                case 2:
                    target = __first_xml_child_match(target, tag, id, current_position);
                    break;
                default:
                    crm_trace("Aborting on %s", section);
                    target = NULL;
                    break;
            }

            if(rc == 1 || target == NULL) {
                crm_trace("Done");
                break;

            } else {
                char *tmp = current;
                current = remainder;
                remainder = tmp;
            }
        }
    }

    if(target) {
        char *path = (char *)xmlGetNodePath(target);

        crm_trace("Found %s for %s", path, key);
        free(path);
    } else {
        crm_debug("No match for %s", key);
    }

    free(remainder);
    free(current);
    free(section);
    free(tag);
    free(id);
    return target;
}

static int
xml_apply_patchset_v2(xmlNode *xml, xmlNode *patchset)
{
    int rc = pcmk_ok;
    xmlNode *change = NULL;
    for (change = __xml_first_child(patchset); change != NULL; change = __xml_next(change)) {
        xmlNode *match = NULL;
        const char *op = crm_element_value(change, XML_DIFF_OP);
        const char *xpath = crm_element_value(change, XML_DIFF_PATH);
        int position = -1;

        crm_trace("Processing %s %s", change->name, op);
        if(op == NULL) {
            continue;
        }

        if(strcmp(op, "delete") == 0) {
            crm_element_value_int(change, XML_DIFF_POSITION, &position);
        }
#if 0
        match = get_xpath_object(xpath, xml, LOG_TRACE);
#else
        match = __xml_find_path(xml, xpath, position);
#endif
        crm_trace("Performing %s on %s with %p", op, xpath, match);

        if(match == NULL && strcmp(op, "delete") == 0) {
            crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
            continue;

        } else if(match == NULL) {
            crm_err("No %s match for %s in %p", op, xpath, xml->doc);
            rc = -pcmk_err_diff_failed;
            continue;

        } else if(strcmp(op, "create") == 0) {
            int position = 0;
            xmlNode *child = NULL;
            xmlNode *match_child = NULL;

            match_child = match->children;
            crm_element_value_int(change, XML_DIFF_POSITION, &position);

            while(match_child && position != __xml_offset(match_child)) {
                match_child = match_child->next;
            }

            child = xmlDocCopyNode(change->children, match->doc, 1);
            if(match_child) {
                crm_trace("Adding %s at position %d", child->name, position);
                xmlAddPrevSibling(match_child, child);

            } else if(match->last) { /* Add to the end */
                crm_trace("Adding %s at position %d (end)", child->name, position);
                xmlAddNextSibling(match->last, child);

            } else {
                crm_trace("Adding %s at position %d (first)", child->name, position);
                CRM_LOG_ASSERT(position == 0);
                xmlAddChild(match, child);
            }
            crm_node_created(child);

        } else if(strcmp(op, "move") == 0) {
            int position = 0;

            crm_element_value_int(change, XML_DIFF_POSITION, &position);
            if(position != __xml_offset(match)) {
                xmlNode *match_child = NULL;
                int p = position;

                if(p > __xml_offset(match)) {
                    p++; /* Skip ourselves */
                }

                CRM_ASSERT(match->parent != NULL);
                match_child = match->parent->children;

                while(match_child && p != __xml_offset(match_child)) {
                    match_child = match_child->next;
                }

                crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)",
                         match->name, position, __xml_offset(match), match->prev,
                         match_child?"next":"last", match_child?match_child:match->parent->last);

                if(match_child) {
                    xmlAddPrevSibling(match_child, match);

                } else {
                    CRM_ASSERT(match->parent->last != NULL);
                    xmlAddNextSibling(match->parent->last, match);
                }

            } else {
                crm_trace("%s is already in position %d", match->name, position);
            }

            if(position != __xml_offset(match)) {
                crm_err("Moved %s.%d to position %d instead of %d (%p)",
                        match->name, ID(match), __xml_offset(match), position, match->prev);
                rc = -pcmk_err_diff_failed;
            }

        } else if(strcmp(op, "delete") == 0) {
            free_xml(match);

        } else if(strcmp(op, "modify") == 0) {
            xmlAttr *pIter = crm_first_attr(match);
            xmlNode *attrs = __xml_first_child(first_named_child(change, XML_DIFF_RESULT));

            if(attrs == NULL) {
                rc = -ENOMSG;
                continue;
            }
            while(pIter != NULL) {
                const char *name = (const char *)pIter->name;

                pIter = pIter->next;
                xml_remove_prop(match, name);
            }

            for (pIter = crm_first_attr(attrs); pIter != NULL; pIter = pIter->next) {
                const char *name = (const char *)pIter->name;
                const char *value = crm_element_value(attrs, name);

                crm_xml_add(match, name, value);
            }

        } else {
            crm_err("Unknown operation: %s", op);
        }
    }
    return rc;
}

int
xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) 
{
    int format = 1;
    int rc = pcmk_ok;
    xmlNode *old = NULL;
    const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST);

    if(patchset == NULL) {
        return rc;
    }

    xml_log_patchset(LOG_TRACE, __FUNCTION__, patchset);

    crm_element_value_int(patchset, "format", &format);
    if(check_version) {
        rc = xml_patch_version_check(xml, patchset, format);
        if(rc != pcmk_ok) {
            return rc;
        }
    }

    if(digest) {
        /* Make it available for logging if the result doesn't have the expected digest */
        old = copy_xml(xml);
    }

    if(rc == pcmk_ok) {
        switch(format) {
            case 1:
                rc = xml_apply_patchset_v1(xml, patchset);
                break;
            case 2:
                rc = xml_apply_patchset_v2(xml, patchset);
                break;
            default:
                crm_err("Unknown patch format: %d", format);
                rc = -EINVAL;
        }
    }

    if(rc == pcmk_ok && digest) {
        static struct qb_log_callsite *digest_cs = NULL;

        char *new_digest = NULL;
        char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION);

        if (digest_cs == NULL) {
            digest_cs =
                qb_log_callsite_get(__func__, __FILE__, "diff-digest", LOG_TRACE, __LINE__,
                                    crm_trace_nonlog);
        }

        new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version);
        if (safe_str_neq(new_digest, digest)) {
            crm_info("v%d digest mis-match: expected %s, calculated %s", format, digest, new_digest);
            rc = -pcmk_err_diff_failed;

            if (digest_cs && digest_cs->targets) {
                save_xml_to_file(old,     "PatchDigest:input", NULL);
                save_xml_to_file(xml,     "PatchDigest:result", NULL);
                save_xml_to_file(patchset,"PatchDigest:diff", NULL);

            } else {
                crm_trace("%p %.6x", digest_cs, digest_cs ? digest_cs->targets : 0);
            }

        } else {
            crm_trace("v%d digest matched: expected %s, calculated %s", format, digest, new_digest);
        }
        free(new_digest);
        free(version);
    }
    free_xml(old);
    return rc;
}

xmlNode *
find_xml_node(xmlNode * root, const char *search_path, gboolean must_find)
{
    xmlNode *a_child = NULL;
    const char *name = "NULL";

    if (root != NULL) {
        name = crm_element_name(root);
    }

    if (search_path == NULL) {
        crm_warn("Will never find <NULL>");
        return NULL;
    }

    for (a_child = __xml_first_child(root); a_child != NULL; a_child = __xml_next(a_child)) {
        if (strcmp((const char *)a_child->name, search_path) == 0) {
/* 		crm_trace("returning node (%s).", crm_element_name(a_child)); */
            return a_child;
        }
    }

    if (must_find) {
        crm_warn("Could not find %s in %s.", search_path, name);
    } else if (root != NULL) {
        crm_trace("Could not find %s in %s.", search_path, name);
    } else {
        crm_trace("Could not find %s in <NULL>.", search_path);
    }

    return NULL;
}

xmlNode *
find_entity(xmlNode * parent, const char *node_name, const char *id)
{
    xmlNode *a_child = NULL;

    for (a_child = __xml_first_child(parent); a_child != NULL; a_child = __xml_next(a_child)) {
        /* Uncertain if node_name == NULL check is strictly necessary here */
        if (node_name == NULL || strcmp((const char *)a_child->name, node_name) == 0) {
            const char *cid = ID(a_child);
            if (id == NULL || (cid != NULL && strcmp(id, cid) == 0)) {
                return a_child;
            }
        }
    }

    crm_trace("node <%s id=%s> not found in %s.", node_name, id, crm_element_name(parent));
    return NULL;
}

void
copy_in_properties(xmlNode * target, xmlNode * src)
{
    if (src == NULL) {
        crm_warn("No node to copy properties from");

    } else if (target == NULL) {
        crm_err("No node to copy properties into");

    } else {
        xmlAttrPtr pIter = NULL;

        for (pIter = crm_first_attr(src); pIter != NULL; pIter = pIter->next) {
            const char *p_name = (const char *)pIter->name;
            const char *p_value = crm_attr_value(pIter);

            expand_plus_plus(target, p_name, p_value);
        }
    }

    return;
}

void
fix_plus_plus_recursive(xmlNode * target)
{
    /* TODO: Remove recursion and use xpath searches for value++ */
    xmlNode *child = NULL;
    xmlAttrPtr pIter = NULL;

    for (pIter = crm_first_attr(target); pIter != NULL; pIter = pIter->next) {
        const char *p_name = (const char *)pIter->name;
        const char *p_value = crm_attr_value(pIter);

        expand_plus_plus(target, p_name, p_value);
    }
    for (child = __xml_first_child(target); child != NULL; child = __xml_next(child)) {
        fix_plus_plus_recursive(child);
    }
}

void
expand_plus_plus(xmlNode * target, const char *name, const char *value)
{
    int offset = 1;
    int name_len = 0;
    int int_value = 0;
    int value_len = 0;

    const char *old_value = NULL;

    if (value == NULL || name == NULL) {
        return;
    }

    old_value = crm_element_value(target, name);

    if (old_value == NULL) {
        /* if no previous value, set unexpanded */
        goto set_unexpanded;

    } else if (strstr(value, name) != value) {
        goto set_unexpanded;
    }

    name_len = strlen(name);
    value_len = strlen(value);
    if (value_len < (name_len + 2)
        || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) {
        goto set_unexpanded;
    }

    /* if we are expanding ourselves,
     * then no previous value was set and leave int_value as 0
     */
    if (old_value != value) {
        int_value = char2score(old_value);
    }

    if (value[name_len + 1] != '+') {
        const char *offset_s = value + (name_len + 2);

        offset = char2score(offset_s);
    }
    int_value += offset;

    if (int_value > INFINITY) {
        int_value = (int)INFINITY;
    }

    crm_xml_add_int(target, name, int_value);
    return;

  set_unexpanded:
    if (old_value == value) {
        /* the old value is already set, nothing to do */
        return;
    }
    crm_xml_add(target, name, value);
    return;
}

xmlDoc *
getDocPtr(xmlNode * node)
{
    xmlDoc *doc = NULL;

    CRM_CHECK(node != NULL, return NULL);

    doc = node->doc;
    if (doc == NULL) {
        doc = xmlNewDoc((const xmlChar *)"1.0");
        xmlDocSetRootElement(doc, node);
        xmlSetTreeDoc(node, doc);
    }
    return doc;
}

xmlNode *
add_node_copy(xmlNode * parent, xmlNode * src_node)
{
    xmlNode *child = NULL;
    xmlDoc *doc = getDocPtr(parent);

    CRM_CHECK(src_node != NULL, return NULL);

    child = xmlDocCopyNode(src_node, doc, 1);
    xmlAddChild(parent, child);
    crm_node_created(child);
    return child;
}

int
add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child)
{
    add_node_copy(parent, child);
    free_xml(child);
    return 1;
}

static bool
__xml_acl_check(xmlNode *xml, const char *name, enum xml_private_flags mode)
{
    CRM_ASSERT(xml);
    CRM_ASSERT(xml->doc);
    CRM_ASSERT(xml->doc->_private);

#if ENABLE_ACL
    {
        if(TRACKING_CHANGES(xml) && xml_acl_enabled(xml)) {
            int offset = 0;
            xmlNode *parent = xml;
            char buffer[XML_BUFFER_SIZE];
            xml_private_t *docp = xml->doc->_private;

            if(docp->acls == NULL) {
                crm_trace("Ordinary user %s cannot access the CIB without any defined ACLs", docp->user);
                set_doc_flag(xml, xpf_acl_denied);
                return FALSE;
            }

            offset = __get_prefix(NULL, xml, buffer, offset);
            if(name) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "[@%s]", name);
            }
            CRM_LOG_ASSERT(offset > 0);

            /* Walk the tree upwards looking for xml_acl_* flags
             * - Creating an attribute requires write permissions for the node
             * - Creating a child requires write permissions for the parent
             */

            if(name) {
                xmlAttr *attr = xmlHasProp(xml, (const xmlChar *)name);

                if(attr && mode == xpf_acl_create) {
                    mode = xpf_acl_write;
                }
            }

            while(parent && parent->_private) {
                xml_private_t *p = parent->_private;
                if(__xml_acl_mode_test(p->flags, mode)) {
                    return TRUE;

                } else if(is_set(p->flags, xpf_acl_deny)) {
                    crm_trace("%x access denied to %s: parent", mode, buffer);
                    set_doc_flag(xml, xpf_acl_denied);
                    return FALSE;
                }
                parent = parent->parent;
            }

            crm_trace("%x access denied to %s: default", mode, buffer);
            set_doc_flag(xml, xpf_acl_denied);
            return FALSE;
        }
    }
#endif

    return TRUE;
}

const char *
crm_xml_add(xmlNode * node, const char *name, const char *value)
{
    bool dirty = FALSE;
    xmlAttr *attr = NULL;

    CRM_CHECK(node != NULL, return NULL);
    CRM_CHECK(name != NULL, return NULL);

    if (value == NULL) {
        return NULL;
    }
#if XML_PARANOIA_CHECKS
    {
        const char *old_value = NULL;

        old_value = crm_element_value(node, name);

        /* Could be re-setting the same value */
        CRM_CHECK(old_value != value, crm_err("Cannot reset %s with crm_xml_add(%s)", name, value);
                  return value);
    }
#endif

    if(TRACKING_CHANGES(node)) {
        const char *old = crm_element_value(node, name);

        if(old == NULL || value == NULL || strcmp(old, value) != 0) {
            dirty = TRUE;
        }
    }

    if(dirty && __xml_acl_check(node, name, xpf_acl_create) == FALSE) {
        crm_trace("Cannot add %s=%s to %s", name, value, node->name);
        return NULL;
    }

    attr = xmlSetProp(node, (const xmlChar *)name, (const xmlChar *)value);
    if(dirty) {
        crm_attr_dirty(attr);
    }

    CRM_CHECK(attr && attr->children && attr->children->content, return NULL);
    return (char *)attr->children->content;
}

const char *
crm_xml_replace(xmlNode * node, const char *name, const char *value)
{
    bool dirty = FALSE;
    xmlAttr *attr = NULL;
    const char *old_value = NULL;

    CRM_CHECK(node != NULL, return NULL);
    CRM_CHECK(name != NULL && name[0] != 0, return NULL);

    old_value = crm_element_value(node, name);

    /* Could be re-setting the same value */
    CRM_CHECK(old_value != value, return value);

    if(__xml_acl_check(node, name, xpf_acl_write) == FALSE) {
        /* Create a fake object linked to doc->_private instead? */
        crm_trace("Cannot replace %s=%s to %s", name, value, node->name);
        return NULL;

    } else if (old_value != NULL && value == NULL) {
        xml_remove_prop(node, name);
        return NULL;

    } else if (value == NULL) {
        return NULL;
    }

    if(TRACKING_CHANGES(node)) {
        if(old_value == NULL || value == NULL || strcmp(old_value, value) != 0) {
            dirty = TRUE;
        }
    }

    attr = xmlSetProp(node, (const xmlChar *)name, (const xmlChar *)value);
    if(dirty) {
        crm_attr_dirty(attr);
    }
    CRM_CHECK(attr && attr->children && attr->children->content, return NULL);
    return (char *)attr->children->content;
}

const char *
crm_xml_add_int(xmlNode * node, const char *name, int value)
{
    char *number = crm_itoa(value);
    const char *added = crm_xml_add(node, name, number);

    free(number);
    return added;
}

xmlNode *
create_xml_node(xmlNode * parent, const char *name)
{
    xmlDoc *doc = NULL;
    xmlNode *node = NULL;

    if (name == NULL || name[0] == 0) {
        CRM_CHECK(name != NULL && name[0] == 0, return NULL);
        return NULL;
    }

    if (parent == NULL) {
        doc = xmlNewDoc((const xmlChar *)"1.0");
        node = xmlNewDocRawNode(doc, NULL, (const xmlChar *)name, NULL);
        xmlDocSetRootElement(doc, node);

    } else {
        doc = getDocPtr(parent);
        node = xmlNewDocRawNode(doc, NULL, (const xmlChar *)name, NULL);
        xmlAddChild(parent, node);
    }
    crm_node_created(node);
    return node;
}

static inline int
__get_prefix(const char *prefix, xmlNode *xml, char *buffer, int offset)
{
    const char *id = ID(xml);

    if(offset == 0 && prefix == NULL && xml->parent) {
        offset = __get_prefix(NULL, xml->parent, buffer, offset);
    }

    if(id) {
        offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "/%s[@id='%s']", (const char *)xml->name, id);
    } else if(xml->name) {
        offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "/%s", (const char *)xml->name);
    }

    return offset;
}

char *
xml_get_path(xmlNode *xml)
{
    int offset = 0;
    char buffer[XML_BUFFER_SIZE];

    if(__get_prefix(NULL, xml, buffer, offset) > 0) {
        return strdup(buffer);
    }
    return NULL;
}

static void
free_xml_with_position(xmlNode * child, int position)
{
    if (child != NULL) {
        xmlNode *top = NULL;
        xmlDoc *doc = child->doc;
        xml_private_t *p = child->_private;

        if (doc != NULL) {
            top = xmlDocGetRootElement(doc);
        }

        if (doc != NULL && top == child) {
            /* Free everything */
            xmlFreeDoc(doc);

        } else if(__xml_acl_check(child, NULL, xpf_acl_write) == FALSE) {
            int offset = 0;
            char buffer[XML_BUFFER_SIZE];

            __get_prefix(NULL, child, buffer, offset);
            crm_trace("Cannot remove %s %x", buffer, p->flags);
            return;

        } else {
            if(doc && TRACKING_CHANGES(child) && is_not_set(p->flags, xpf_created)) {
                int offset = 0;
                char buffer[XML_BUFFER_SIZE];

                if(__get_prefix(NULL, child, buffer, offset) > 0) {
                    xml_deleted_obj_t *deleted_obj = calloc(1, sizeof(xml_deleted_obj_t));

                    crm_trace("Deleting %s %p from %p", buffer, child, doc);

                    deleted_obj->path = strdup(buffer);

                    deleted_obj->position = -1;
                    /* Record the "position" only for XML comments for now */
                    if (child->type == XML_COMMENT_NODE) {
                        if (position >= 0) {
                            deleted_obj->position = position;

                        } else {
                            deleted_obj->position = __xml_offset(child);
                        }
                    }

                    p = doc->_private;
                    p->deleted_objs = g_list_append(p->deleted_objs, deleted_obj);
                    set_doc_flag(child, xpf_dirty);
                }
            }

            /* Free this particular subtree
             * Make sure to unlink it from the parent first
             */
            xmlUnlinkNode(child);
            xmlFreeNode(child);
        }
    }
}


void
free_xml(xmlNode * child)
{
    free_xml_with_position(child, -1);
}

xmlNode *
copy_xml(xmlNode * src)
{
    xmlDoc *doc = xmlNewDoc((const xmlChar *)"1.0");
    xmlNode *copy = xmlDocCopyNode(src, doc, 1);

    xmlDocSetRootElement(doc, copy);
    xmlSetTreeDoc(copy, doc);
    return copy;
}

static void
crm_xml_err(void *ctx, const char *fmt, ...)
G_GNUC_PRINTF(2, 3);

static void
crm_xml_err(void *ctx, const char *fmt, ...)
{
    va_list ap;
    static struct qb_log_callsite *xml_error_cs = NULL;

    if (xml_error_cs == NULL) {
        xml_error_cs = qb_log_callsite_get(
            __func__, __FILE__, "xml library error", LOG_TRACE, __LINE__, crm_trace_nonlog);
    }

    va_start(ap, fmt);
    if (xml_error_cs && xml_error_cs->targets) {
        CRM_XML_LOG_BASE(LOG_ERR, TRUE,
                         crm_abort(__FILE__, __PRETTY_FUNCTION__, __LINE__, "xml library error",
                                   TRUE, TRUE),
                         "XML Error: ", fmt, ap);
    } else {
        CRM_XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap);
    }
    va_end(ap);
}

xmlNode *
string2xml(const char *input)
{
    xmlNode *xml = NULL;
    xmlDocPtr output = NULL;
    xmlParserCtxtPtr ctxt = NULL;
    xmlErrorPtr last_error = NULL;

    if (input == NULL) {
        crm_err("Can't parse NULL input");
        return NULL;
    }

    /* create a parser context */
    ctxt = xmlNewParserCtxt();
    CRM_CHECK(ctxt != NULL, return NULL);

    /* xmlCtxtUseOptions(ctxt, XML_PARSE_NOBLANKS|XML_PARSE_RECOVER); */

    xmlCtxtResetLastError(ctxt);
    xmlSetGenericErrorFunc(ctxt, crm_xml_err);
    /* initGenericErrorDefaultFunc(crm_xml_err); */
    output =
        xmlCtxtReadDoc(ctxt, (const xmlChar *)input, NULL, NULL,
                       XML_PARSE_NOBLANKS | XML_PARSE_RECOVER);
    if (output) {
        xml = xmlDocGetRootElement(output);
    }
    last_error = xmlCtxtGetLastError(ctxt);
    if (last_error && last_error->code != XML_ERR_OK) {
        /* crm_abort(__FILE__,__FUNCTION__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
        /*
         * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
         * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
         */
        crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s",
                 last_error->domain, last_error->level, last_error->code, last_error->message);

        if (last_error->code == XML_ERR_DOCUMENT_EMPTY) {
            CRM_LOG_ASSERT("Cannot parse an empty string");

        } else if (last_error->code != XML_ERR_DOCUMENT_END) {
            crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input),
                    input);
            if (xml != NULL) {
                crm_log_xml_err(xml, "Partial");
            }

        } else {
            int len = strlen(input);
            int lpc = 0;

            while(lpc < len) {
                crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc);
                lpc += 80;
            }

            CRM_LOG_ASSERT("String parsing error");
        }
    }

    xmlFreeParserCtxt(ctxt);
    return xml;
}

xmlNode *
stdin2xml(void)
{
    size_t data_length = 0;
    size_t read_chars = 0;

    char *xml_buffer = NULL;
    xmlNode *xml_obj = NULL;

    do {
        size_t next = XML_BUFFER_SIZE + data_length + 1;

        if(next <= 0) {
            crm_err("Buffer size exceeded at: %l + %d", data_length, XML_BUFFER_SIZE);
            break;
        }

        xml_buffer = realloc_safe(xml_buffer, next);
        read_chars = fread(xml_buffer + data_length, 1, XML_BUFFER_SIZE, stdin);
        data_length += read_chars;
    } while (read_chars > 0);

    if (data_length == 0) {
        crm_warn("No XML supplied on stdin");
        free(xml_buffer);
        return NULL;
    }

    xml_buffer[data_length] = '\0';

    xml_obj = string2xml(xml_buffer);
    free(xml_buffer);

    crm_log_xml_trace(xml_obj, "Created fragment");
    return xml_obj;
}

static char *
decompress_file(const char *filename)
{
    char *buffer = NULL;

#if HAVE_BZLIB_H
    int rc = 0;
    size_t length = 0, read_len = 0;

    BZFILE *bz_file = NULL;
    FILE *input = fopen(filename, "r");

    if (input == NULL) {
        crm_perror(LOG_ERR, "Could not open %s for reading", filename);
        return NULL;
    }

    bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0);

    if (rc != BZ_OK) {
        BZ2_bzReadClose(&rc, bz_file);
        return NULL;
    }

    rc = BZ_OK;
    while (rc == BZ_OK) {
        buffer = realloc_safe(buffer, XML_BUFFER_SIZE + length + 1);
        read_len = BZ2_bzRead(&rc, bz_file, buffer + length, XML_BUFFER_SIZE);

        crm_trace("Read %ld bytes from file: %d", (long)read_len, rc);

        if (rc == BZ_OK || rc == BZ_STREAM_END) {
            length += read_len;
        }
    }

    buffer[length] = '\0';

    if (rc != BZ_STREAM_END) {
        crm_err("Couldn't read compressed xml from file");
        free(buffer);
        buffer = NULL;
    }

    BZ2_bzReadClose(&rc, bz_file);
    fclose(input);

#else
    crm_err("Cannot read compressed files:" " bzlib was not available at compile time");
#endif
    return buffer;
}

void
strip_text_nodes(xmlNode * xml)
{
    xmlNode *iter = xml->children;

    while (iter) {
        xmlNode *next = iter->next;

        switch (iter->type) {
            case XML_TEXT_NODE:
                /* Remove it */
                xmlUnlinkNode(iter);
                xmlFreeNode(iter);
                break;

            case XML_ELEMENT_NODE:
                /* Search it */
                strip_text_nodes(iter);
                break;

            default:
                /* Leave it */
                break;
        }

        iter = next;
    }
}

xmlNode *
filename2xml(const char *filename)
{
    xmlNode *xml = NULL;
    xmlDocPtr output = NULL;
    gboolean uncompressed = TRUE;
    xmlParserCtxtPtr ctxt = NULL;
    xmlErrorPtr last_error = NULL;
    static int xml_options = XML_PARSE_NOBLANKS | XML_PARSE_RECOVER;

    /* create a parser context */
    ctxt = xmlNewParserCtxt();
    CRM_CHECK(ctxt != NULL, return NULL);

    /* xmlCtxtUseOptions(ctxt, XML_PARSE_NOBLANKS|XML_PARSE_RECOVER); */

    xmlCtxtResetLastError(ctxt);
    xmlSetGenericErrorFunc(ctxt, crm_xml_err);
    /* initGenericErrorDefaultFunc(crm_xml_err); */

    if (filename) {
        uncompressed = !crm_ends_with_ext(filename, ".bz2");
    }

    if (filename == NULL) {
        /* STDIN_FILENO == fileno(stdin) */
        output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, xml_options);

    } else if (uncompressed) {
        output = xmlCtxtReadFile(ctxt, filename, NULL, xml_options);

    } else {
        char *input = decompress_file(filename);

        output = xmlCtxtReadDoc(ctxt, (const xmlChar *)input, NULL, NULL, xml_options);
        free(input);
    }

    if (output && (xml = xmlDocGetRootElement(output))) {
        strip_text_nodes(xml);
    }

    last_error = xmlCtxtGetLastError(ctxt);
    if (last_error && last_error->code != XML_ERR_OK) {
        /* crm_abort(__FILE__,__FUNCTION__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */
        /*
         * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel
         * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
         */
        crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s",
                last_error->domain, last_error->level, last_error->code, last_error->message);

        if (last_error && last_error->code != XML_ERR_OK) {
            crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename);
            if (xml != NULL) {
                crm_log_xml_err(xml, "Partial");
            }
        }
    }

    xmlFreeParserCtxt(ctxt);
    return xml;
}

/*!
 * \internal
 * \brief Add a "last written" attribute to an XML node, set to current time
 *
 * \param[in] xml_node XML node to get attribute
 *
 * \return Value that was set, or NULL on error
 */
const char *
crm_xml_add_last_written(xmlNode *xml_node)
{
    time_t now = time(NULL);
    char *now_str = ctime(&now);

    now_str[24] = EOS; /* replace the newline */
    return crm_xml_add(xml_node, XML_CIB_ATTR_WRITTEN, now_str);
}

/*!
 * \brief Sanitize a string so it is usable as an XML ID
 *
 * \param[in,out] id  String to sanitize
 */
void
crm_xml_sanitize_id(char *id)
{
    char *c;

    for (c = id; *c; ++c) {
        /* @TODO Sanitize more comprehensively */
        switch (*c) {
            case ':':
            case '#':
                *c = '.';
        }
    }
}

/*!
 * \brief Set the ID of an XML element using a format
 *
 * \param[in,out] xml  XML element
 * \param[in]     fmt  printf-style format
 * \param[in]     ...  any arguments required by format
 */
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
    va_list ap;
    int len = 0;
    char *id = NULL;

    /* equivalent to crm_strdup_printf() */
    va_start(ap, format);
    len = vasprintf(&id, format, ap);
    va_end(ap);
    CRM_ASSERT(len > 0);

    crm_xml_sanitize_id(id);
    crm_xml_add(xml, XML_ATTR_ID, id);
    free(id);
}

static int
write_xml_stream(xmlNode * xml_node, const char *filename, FILE * stream, gboolean compress)
{
    int res = 0;
    char *buffer = NULL;
    unsigned int out = 0;

    CRM_CHECK(stream != NULL, return -1);

    crm_trace("Writing XML out to %s", filename);
    if (xml_node == NULL) {
        crm_err("Cannot write NULL to %s", filename);
        fclose(stream);
        return -1;
    }


    crm_log_xml_trace(xml_node, "Writing out");

    buffer = dump_xml_formatted(xml_node);
    CRM_CHECK(buffer != NULL && strlen(buffer) > 0, crm_log_xml_warn(xml_node, "dump:failed");
              goto bail);

    if (compress) {
#if HAVE_BZLIB_H
        int rc = BZ_OK;
        unsigned int in = 0;
        BZFILE *bz_file = NULL;

        bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30);
        if (rc != BZ_OK) {
            crm_err("bzWriteOpen failed: %d", rc);
        } else {
            BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer));
            if (rc != BZ_OK) {
                crm_err("bzWrite() failed: %d", rc);
            }
        }

        if (rc == BZ_OK) {
            BZ2_bzWriteClose(&rc, bz_file, 0, &in, &out);
            if (rc != BZ_OK) {
                crm_err("bzWriteClose() failed: %d", rc);
                out = -1;
            } else {
                crm_trace("%s: In: %d, out: %d", filename, in, out);
            }
        }
#else
        crm_err("Cannot write compressed files:" " bzlib was not available at compile time");
#endif
    }

    if (out <= 0) {
        res = fprintf(stream, "%s", buffer);
        if (res < 0) {
            crm_perror(LOG_ERR, "Cannot write output to %s", filename);
            goto bail;
        }
    }

  bail:

    if (fflush(stream) != 0) {
        crm_perror(LOG_ERR, "fflush for %s failed", filename);
        res = -1;
    }

    /* Don't report error if the file does not support synchronization */
    if (fsync(fileno(stream)) < 0 && errno != EROFS  && errno != EINVAL) {
        crm_perror(LOG_ERR, "fsync for %s failed", filename);
        res = -1;
    }

    fclose(stream);

    crm_trace("Saved %d bytes to the Cib as XML", res);
    free(buffer);

    return res;
}

int
write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress)
{
    FILE *stream = NULL;

    CRM_CHECK(fd > 0, return -1);
    stream = fdopen(fd, "w");
    return write_xml_stream(xml_node, filename, stream, compress);
}

int
write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress)
{
    FILE *stream = NULL;

    stream = fopen(filename, "w");

    return write_xml_stream(xml_node, filename, stream, compress);
}

xmlNode *
get_message_xml(xmlNode * msg, const char *field)
{
    xmlNode *tmp = first_named_child(msg, field);

    return __xml_first_child(tmp);
}

gboolean
add_message_xml(xmlNode * msg, const char *field, xmlNode * xml)
{
    xmlNode *holder = create_xml_node(msg, field);

    add_node_copy(holder, xml);
    return TRUE;
}

static char *
crm_xml_escape_shuffle(char *text, int start, int *length, const char *replace)
{
    int lpc;
    int offset = strlen(replace) - 1;   /* We have space for 1 char already */

    *length += offset;
    text = realloc_safe(text, *length);

    for (lpc = (*length) - 1; lpc > (start + offset); lpc--) {
        text[lpc] = text[lpc - offset];
    }

    memcpy(text + start, replace, offset + 1);
    return text;
}

char *
crm_xml_escape(const char *text)
{
    int index;
    int changes = 0;
    int length = 1 + strlen(text);
    char *copy = strdup(text);

    /*
     * When xmlCtxtReadDoc() parses &lt; and friends in a
     * value, it converts them to their human readable
     * form.
     *
     * If one uses xmlNodeDump() to convert it back to a
     * string, all is well, because special characters are
     * converted back to their escape sequences.
     *
     * However xmlNodeDump() is randomly dog slow, even with the same
     * input. So we need to replicate the escaping in our custom
     * version so that the result can be re-parsed by xmlCtxtReadDoc()
     * when necessary.
     */

    for (index = 0; index < length; index++) {
        switch (copy[index]) {
            case 0:
                break;
            case '<':
                copy = crm_xml_escape_shuffle(copy, index, &length, "&lt;");
                changes++;
                break;
            case '>':
                copy = crm_xml_escape_shuffle(copy, index, &length, "&gt;");
                changes++;
                break;
            case '"':
                copy = crm_xml_escape_shuffle(copy, index, &length, "&quot;");
                changes++;
                break;
            case '\'':
                copy = crm_xml_escape_shuffle(copy, index, &length, "&apos;");
                changes++;
                break;
            case '&':
                copy = crm_xml_escape_shuffle(copy, index, &length, "&amp;");
                changes++;
                break;
            case '\t':
                /* Might as well just expand to a few spaces... */
                copy = crm_xml_escape_shuffle(copy, index, &length, "    ");
                changes++;
                break;
            case '\n':
                /* crm_trace("Convert: \\%.3o", copy[index]); */
                copy = crm_xml_escape_shuffle(copy, index, &length, "\\n");
                changes++;
                break;
            case '\r':
                copy = crm_xml_escape_shuffle(copy, index, &length, "\\r");
                changes++;
                break;
                /* For debugging...
            case '\\':
                crm_trace("Passthrough: \\%c", copy[index+1]);
                break;
                */
            default:
                /* Check for and replace non-printing characters with their octal equivalent */
                if(copy[index] < ' ' || copy[index] > '~') {
                    char *replace = crm_strdup_printf("\\%.3o", copy[index]);

                    /* crm_trace("Convert to octal: \\%.3o", copy[index]); */
                    copy = crm_xml_escape_shuffle(copy, index, &length, replace);
                    free(replace);
                    changes++;
                }
        }
    }

    if (changes) {
        crm_trace("Dumped '%s'", copy);
    }
    return copy;
}

static inline void
dump_xml_attr(xmlAttrPtr attr, int options, char **buffer, int *offset, int *max)
{
    char *p_value = NULL;
    const char *p_name = NULL;
    xml_private_t *p = NULL;

    CRM_ASSERT(buffer != NULL);
    if (attr == NULL || attr->children == NULL) {
        return;
    }

    p = attr->_private;
    if (p && is_set(p->flags, xpf_deleted)) {
        return;
    }

    p_name = (const char *)attr->name;
    p_value = crm_xml_escape((const char *)attr->children->content);
    buffer_print(*buffer, *max, *offset, " %s=\"%s\"", p_name, p_value);
    free(p_value);
}

static void
__xml_log_element(int log_level, const char *file, const char *function, int line,
                  const char *prefix, xmlNode * data, int depth, int options)
{
    int max = 0;
    int offset = 0;
    const char *name = NULL;
    const char *hidden = NULL;

    xmlNode *child = NULL;
    xmlAttrPtr pIter = NULL;

    if(data == NULL) {
        return;
    }

    name = crm_element_name(data);

    if(is_set(options, xml_log_option_open)) {
        char *buffer = NULL;

        insert_prefix(options, &buffer, &offset, &max, depth);

        if (data->type == XML_COMMENT_NODE) {
            buffer_print(buffer, max, offset, "<!--%s-->", data->content);

        } else {
            buffer_print(buffer, max, offset, "<%s", name);

            hidden = crm_element_value(data, "hidden");
            for (pIter = crm_first_attr(data); pIter != NULL; pIter = pIter->next) {
                xml_private_t *p = pIter->_private;
                const char *p_name = (const char *)pIter->name;
                const char *p_value = crm_attr_value(pIter);
                char *p_copy = NULL;

                if(is_set(p->flags, xpf_deleted)) {
                    continue;
                } else if ((is_set(options, xml_log_option_diff_plus)
                     || is_set(options, xml_log_option_diff_minus))
                    && strcmp(XML_DIFF_MARKER, p_name) == 0) {
                    continue;

                } else if (hidden != NULL && p_name[0] != 0 && strstr(hidden, p_name) != NULL) {
                    p_copy = strdup("*****");

                } else {
                    p_copy = crm_xml_escape(p_value);
                }

                buffer_print(buffer, max, offset, " %s=\"%s\"", p_name, p_copy);
                free(p_copy);
            }

            if(xml_has_children(data) == FALSE) {
                buffer_print(buffer, max, offset, "/>");

            } else if(is_set(options, xml_log_option_children)) {
                buffer_print(buffer, max, offset, ">");

            } else {
                buffer_print(buffer, max, offset, "/>");
            }
        }

        do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer);
        free(buffer);
    }

    if(data->type == XML_COMMENT_NODE) {
        return;

    } else if(xml_has_children(data) == FALSE) {
        return;

    } else if(is_set(options, xml_log_option_children)) {
        offset = 0;
        max = 0;

        for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) {
            __xml_log_element(log_level, file, function, line, prefix, child, depth + 1, options|xml_log_option_open|xml_log_option_close);
        }
    }

    if(is_set(options, xml_log_option_close)) {
        char *buffer = NULL;

        insert_prefix(options, &buffer, &offset, &max, depth);
        buffer_print(buffer, max, offset, "</%s>", name);

        do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer);
        free(buffer);
    }
}

static void
__xml_log_change_element(int log_level, const char *file, const char *function, int line,
                         const char *prefix, xmlNode * data, int depth, int options)
{
    xml_private_t *p;
    char *prefix_m = NULL;
    xmlNode *child = NULL;
    xmlAttrPtr pIter = NULL;

    if(data == NULL) {
        return;
    }

    p = data->_private;

    prefix_m = strdup(prefix);
    prefix_m[1] = '+';

    if(is_set(p->flags, xpf_dirty) && is_set(p->flags, xpf_created)) {
        /* Continue and log full subtree */
        __xml_log_element(log_level, file, function, line,
                          prefix_m, data, depth, options|xml_log_option_open|xml_log_option_close|xml_log_option_children);

    } else if(is_set(p->flags, xpf_dirty)) {
        char *spaces = calloc(80, 1);
        int s_count = 0, s_max = 80;
        char *prefix_del = NULL;
        char *prefix_moved = NULL;
        const char *flags = prefix;

        insert_prefix(options, &spaces, &s_count, &s_max, depth);
        prefix_del = strdup(prefix);
        prefix_del[0] = '-';
        prefix_del[1] = '-';
        prefix_moved = strdup(prefix);
        prefix_moved[1] = '~';

        if(is_set(p->flags, xpf_moved)) {
            flags = prefix_moved;
        } else {
            flags = prefix;
        }

        __xml_log_element(log_level, file, function, line,
                          flags, data, depth, options|xml_log_option_open);

        for (pIter = crm_first_attr(data); pIter != NULL; pIter = pIter->next) {
            const char *aname = (const char*)pIter->name;

            p = pIter->_private;
            if(is_set(p->flags, xpf_deleted)) {
                const char *value = crm_element_value(data, aname);
                flags = prefix_del;
                do_crm_log_alias(log_level, file, function, line,
                                 "%s %s @%s=%s", flags, spaces, aname, value);

            } else if(is_set(p->flags, xpf_dirty)) {
                const char *value = crm_element_value(data, aname);

                if(is_set(p->flags, xpf_created)) {
                    flags = prefix_m;

                } else if(is_set(p->flags, xpf_modified)) {
                    flags = prefix;

                } else if(is_set(p->flags, xpf_moved)) {
                    flags = prefix_moved;

                } else {
                    flags = prefix;
                }
                do_crm_log_alias(log_level, file, function, line,
                                 "%s %s @%s=%s", flags, spaces, aname, value);
            }
        }
        free(prefix_moved);
        free(prefix_del);
        free(spaces);

        for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) {
            __xml_log_change_element(log_level, file, function, line, prefix, child, depth + 1, options);
        }

        __xml_log_element(log_level, file, function, line,
                          prefix, data, depth, options|xml_log_option_close);

    } else {
        for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) {
            __xml_log_change_element(log_level, file, function, line, prefix, child, depth + 1, options);
        }
    }

    free(prefix_m);

}

void
log_data_element(int log_level, const char *file, const char *function, int line,
                 const char *prefix, xmlNode * data, int depth, int options)
{
    xmlNode *a_child = NULL;

    char *prefix_m = NULL;

    if (prefix == NULL) {
        prefix = "";
    }

    /* Since we use the same file and line, to avoid confusing libqb, we need to use the same format strings */
    if (data == NULL) {
        do_crm_log_alias(log_level, file, function, line, "%s: %s", prefix,
                         "No data to dump as XML");
        return;
    }

    if(is_set(options, xml_log_option_dirty_add) || is_set(options, xml_log_option_dirty_add)) {
        __xml_log_change_element(log_level, file, function, line, prefix, data, depth, options);
        return;
    }

    if (is_set(options, xml_log_option_formatted)) {
        if (is_set(options, xml_log_option_diff_plus)
            && (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) {
            options |= xml_log_option_diff_all;
            prefix_m = strdup(prefix);
            prefix_m[1] = '+';
            prefix = prefix_m;

        } else if (is_set(options, xml_log_option_diff_minus)
                   && (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) {
            options |= xml_log_option_diff_all;
            prefix_m = strdup(prefix);
            prefix_m[1] = '-';
            prefix = prefix_m;
        }
    }

    if (is_set(options, xml_log_option_diff_short)
               && is_not_set(options, xml_log_option_diff_all)) {
        /* Still searching for the actual change */
        for (a_child = __xml_first_child(data); a_child != NULL; a_child = __xml_next(a_child)) {
            log_data_element(log_level, file, function, line, prefix, a_child, depth + 1, options);
        }
    } else {
        __xml_log_element(log_level, file, function, line, prefix, data, depth,
                          options|xml_log_option_open|xml_log_option_close|xml_log_option_children);
    }
    free(prefix_m);
}

static void
dump_filtered_xml(xmlNode * data, int options, char **buffer, int *offset, int *max)
{
    int lpc;
    xmlAttrPtr xIter = NULL;
    static int filter_len = DIMOF(filter);

    for (lpc = 0; options && lpc < filter_len; lpc++) {
        filter[lpc].found = FALSE;
    }

    for (xIter = crm_first_attr(data); xIter != NULL; xIter = xIter->next) {
        bool skip = FALSE;
        const char *p_name = (const char *)xIter->name;

        for (lpc = 0; skip == FALSE && lpc < filter_len; lpc++) {
            if (filter[lpc].found == FALSE && strcmp(p_name, filter[lpc].string) == 0) {
                filter[lpc].found = TRUE;
                skip = TRUE;
                break;
            }
        }

        if (skip == FALSE) {
            dump_xml_attr(xIter, options, buffer, offset, max);
        }
    }
}

static void
dump_xml_element(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
    const char *name = NULL;

    CRM_ASSERT(max != NULL);
    CRM_ASSERT(offset != NULL);
    CRM_ASSERT(buffer != NULL);

    if (data == NULL) {
        crm_trace("Nothing to dump");
        return;
    }

    if (*buffer == NULL) {
        *offset = 0;
        *max = 0;
    }

    name = crm_element_name(data);
    CRM_ASSERT(name != NULL);

    insert_prefix(options, buffer, offset, max, depth);
    buffer_print(*buffer, *max, *offset, "<%s", name);

    if (options & xml_log_option_filtered) {
        dump_filtered_xml(data, options, buffer, offset, max);

    } else {
        xmlAttrPtr xIter = NULL;

        for (xIter = crm_first_attr(data); xIter != NULL; xIter = xIter->next) {
            dump_xml_attr(xIter, options, buffer, offset, max);
        }
    }

    if (data->children == NULL) {
        buffer_print(*buffer, *max, *offset, "/>");

    } else {
        buffer_print(*buffer, *max, *offset, ">");
    }

    if (options & xml_log_option_formatted) {
        buffer_print(*buffer, *max, *offset, "\n");
    }

    if (data->children) {
        xmlNode *xChild = NULL;
        for(xChild = data->children; xChild != NULL; xChild = xChild->next) {
            crm_xml_dump(xChild, options, buffer, offset, max, depth + 1);
        }

        insert_prefix(options, buffer, offset, max, depth);
        buffer_print(*buffer, *max, *offset, "</%s>", name);

        if (options & xml_log_option_formatted) {
            buffer_print(*buffer, *max, *offset, "\n");
        }
    }
}

static void
dump_xml_text(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
    CRM_ASSERT(max != NULL);
    CRM_ASSERT(offset != NULL);
    CRM_ASSERT(buffer != NULL);

    if (data == NULL) {
        crm_trace("Nothing to dump");
        return;
    }

    if (*buffer == NULL) {
        *offset = 0;
        *max = 0;
    }

    insert_prefix(options, buffer, offset, max, depth);

    buffer_print(*buffer, *max, *offset, "%s", data->content);

    if (options & xml_log_option_formatted) {
        buffer_print(*buffer, *max, *offset, "\n");
    }
}


static void
dump_xml_comment(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
    CRM_ASSERT(max != NULL);
    CRM_ASSERT(offset != NULL);
    CRM_ASSERT(buffer != NULL);

    if (data == NULL) {
        crm_trace("Nothing to dump");
        return;
    }

    if (*buffer == NULL) {
        *offset = 0;
        *max = 0;
    }

    insert_prefix(options, buffer, offset, max, depth);

    buffer_print(*buffer, *max, *offset, "<!--");
    buffer_print(*buffer, *max, *offset, "%s", data->content);
    buffer_print(*buffer, *max, *offset, "-->");

    if (options & xml_log_option_formatted) {
        buffer_print(*buffer, *max, *offset, "\n");
    }
}

void
crm_xml_dump(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth)
{
    if(data == NULL) {
        *offset = 0;
        *max = 0;
        return;
    }
#if 0
    if (is_not_set(options, xml_log_option_filtered)) {
        /* Turning this code on also changes the PE tests for some reason
         * (not just newlines).  Figure out why before considering to
         * enable this permanently.
         *
         * It exists to help debug slowness in xmlNodeDump() and
         * potentially if we ever want to go back to it.
         *
         * In theory it's a good idea (reuse) but our custom version does
         * better for the filtered case and avoids the final strdup() for
         * everything
         */

        time_t now, next;
        xmlDoc *doc = NULL;
        xmlBuffer *xml_buffer = NULL;

        *buffer = NULL;
        doc = getDocPtr(data);
        /* doc will only be NULL if data is */
        CRM_CHECK(doc != NULL, return);

        now = time(NULL);
        xml_buffer = xmlBufferCreate();
        CRM_ASSERT(xml_buffer != NULL);

        /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many
         * realloc()s and it can take upwards of 18 seconds (yes, seconds)
         * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in
         * less than 1 second.
         *
         * We could also use xmlBufferCreateSize() to start with a
         * sane-ish initial size and avoid the first few doubles.
         */
        xmlBufferSetAllocationScheme(xml_buffer, XML_BUFFER_ALLOC_DOUBLEIT);

        *max = xmlNodeDump(xml_buffer, doc, data, 0, (options & xml_log_option_formatted));
        if (*max > 0) {
            *buffer = strdup((char *)xml_buffer->content);
        }

        next = time(NULL);
        if ((now + 1) < next) {
            crm_log_xml_trace(data, "Long time");
            crm_err("xmlNodeDump() -> %dbytes took %ds", *max, next - now);
        }

        xmlBufferFree(xml_buffer);
        return;
    }
#endif

    switch(data->type) {
        case XML_ELEMENT_NODE:
            /* Handle below */
            dump_xml_element(data, options, buffer, offset, max, depth);
            break;
        case XML_TEXT_NODE:
            /* if option xml_log_option_text is enabled, then dump XML_TEXT_NODE */
            if (options & xml_log_option_text) {
                dump_xml_text(data, options, buffer, offset, max, depth);
            }
            return;
        case XML_COMMENT_NODE:
            dump_xml_comment(data, options, buffer, offset, max, depth);
            break;
        default:
            crm_warn("Unhandled type: %d", data->type);
            return;

            /*
            XML_ATTRIBUTE_NODE = 2
            XML_CDATA_SECTION_NODE = 4
            XML_ENTITY_REF_NODE = 5
            XML_ENTITY_NODE = 6
            XML_PI_NODE = 7
            XML_DOCUMENT_NODE = 9
            XML_DOCUMENT_TYPE_NODE = 10
            XML_DOCUMENT_FRAG_NODE = 11
            XML_NOTATION_NODE = 12
            XML_HTML_DOCUMENT_NODE = 13
            XML_DTD_NODE = 14
            XML_ELEMENT_DECL = 15
            XML_ATTRIBUTE_DECL = 16
            XML_ENTITY_DECL = 17
            XML_NAMESPACE_DECL = 18
            XML_XINCLUDE_START = 19
            XML_XINCLUDE_END = 20
            XML_DOCB_DOCUMENT_NODE = 21
            */
    }

}

void
crm_buffer_add_char(char **buffer, int *offset, int *max, char c)
{
    buffer_print(*buffer, *max, *offset, "%c", c);
}

char *
dump_xml_formatted_with_text(xmlNode * an_xml_node)
{
    char *buffer = NULL;
    int offset = 0, max = 0;

    crm_xml_dump(an_xml_node, xml_log_option_formatted|xml_log_option_text, &buffer, &offset, &max, 0);
    return buffer;
}

char *
dump_xml_formatted(xmlNode * an_xml_node)
{
    char *buffer = NULL;
    int offset = 0, max = 0;

    crm_xml_dump(an_xml_node, xml_log_option_formatted, &buffer, &offset, &max, 0);
    return buffer;
}

char *
dump_xml_unformatted(xmlNode * an_xml_node)
{
    char *buffer = NULL;
    int offset = 0, max = 0;

    crm_xml_dump(an_xml_node, 0, &buffer, &offset, &max, 0);
    return buffer;
}

gboolean
xml_has_children(const xmlNode * xml_root)
{
    if (xml_root != NULL && xml_root->children != NULL) {
        return TRUE;
    }
    return FALSE;
}

int
crm_element_value_int(xmlNode * data, const char *name, int *dest)
{
    const char *value = crm_element_value(data, name);

    CRM_CHECK(dest != NULL, return -1);
    if (value) {
        *dest = crm_int_helper(value, NULL);
        return 0;
    }
    return -1;
}

int
crm_element_value_const_int(const xmlNode * data, const char *name, int *dest)
{
    return crm_element_value_int((xmlNode *) data, name, dest);
}

const char *
crm_element_value_const(const xmlNode * data, const char *name)
{
    return crm_element_value((xmlNode *) data, name);
}

char *
crm_element_value_copy(xmlNode * data, const char *name)
{
    char *value_copy = NULL;
    const char *value = crm_element_value(data, name);

    if (value != NULL) {
        value_copy = strdup(value);
    }
    return value_copy;
}

void
xml_remove_prop(xmlNode * obj, const char *name)
{
    if(__xml_acl_check(obj, NULL, xpf_acl_write) == FALSE) {
        crm_trace("Cannot remove %s from %s", name, obj->name);

    } else if(TRACKING_CHANGES(obj)) {
        /* Leave in place (marked for removal) until after the diff is calculated */
        xml_private_t *p = NULL;
        xmlAttr *attr = xmlHasProp(obj, (const xmlChar *)name);

        p = attr->_private;
        set_parent_flag(obj, xpf_dirty);
        p->flags |= xpf_deleted;
        /* crm_trace("Setting flag %x due to %s[@id=%s].%s", xpf_dirty, obj->name, ID(obj), name); */

    } else {
        xmlUnsetProp(obj, (const xmlChar *)name);
    }
}

void
purge_diff_markers(xmlNode * a_node)
{
    xmlNode *child = NULL;

    CRM_CHECK(a_node != NULL, return);

    xml_remove_prop(a_node, XML_DIFF_MARKER);
    for (child = __xml_first_child(a_node); child != NULL; child = __xml_next(child)) {
        purge_diff_markers(child);
    }
}

void
save_xml_to_file(xmlNode * xml, const char *desc, const char *filename)
{
    char *f = NULL;

    if (filename == NULL) {
        char *uuid = crm_generate_uuid();

        f = crm_strdup_printf("/tmp/%s", uuid);
        filename = f;
        free(uuid);
    }

    crm_info("Saving %s to %s", desc, filename);
    write_xml_file(xml, filename, FALSE);
    free(f);
}

gboolean
apply_xml_diff(xmlNode * old, xmlNode * diff, xmlNode ** new)
{
    gboolean result = TRUE;
    int root_nodes_seen = 0;
    static struct qb_log_callsite *digest_cs = NULL;
    const char *digest = crm_element_value(diff, XML_ATTR_DIGEST);
    const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION);

    xmlNode *child_diff = NULL;
    xmlNode *added = find_xml_node(diff, "diff-added", FALSE);
    xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE);

    CRM_CHECK(new != NULL, return FALSE);
    if (digest_cs == NULL) {
        digest_cs =
            qb_log_callsite_get(__func__, __FILE__, "diff-digest", LOG_TRACE, __LINE__,
                                crm_trace_nonlog);
    }

    crm_trace("Subtraction Phase");
    for (child_diff = __xml_first_child(removed); child_diff != NULL;
         child_diff = __xml_next(child_diff)) {
        CRM_CHECK(root_nodes_seen == 0, result = FALSE);
        if (root_nodes_seen == 0) {
            *new = subtract_xml_object(NULL, old, child_diff, FALSE, NULL, NULL);
        }
        root_nodes_seen++;
    }

    if (root_nodes_seen == 0) {
        *new = copy_xml(old);

    } else if (root_nodes_seen > 1) {
        crm_err("(-) Diffs cannot contain more than one change set..." " saw %d", root_nodes_seen);
        result = FALSE;
    }

    root_nodes_seen = 0;
    crm_trace("Addition Phase");
    if (result) {
        xmlNode *child_diff = NULL;

        for (child_diff = __xml_first_child(added); child_diff != NULL;
             child_diff = __xml_next(child_diff)) {
            CRM_CHECK(root_nodes_seen == 0, result = FALSE);
            if (root_nodes_seen == 0) {
                add_xml_object(NULL, *new, child_diff, TRUE);
            }
            root_nodes_seen++;
        }
    }

    if (root_nodes_seen > 1) {
        crm_err("(+) Diffs cannot contain more than one change set..." " saw %d", root_nodes_seen);
        result = FALSE;

    } else if (result && digest) {
        char *new_digest = NULL;

        purge_diff_markers(*new);       /* Purge now so the diff is ok */
        new_digest = calculate_xml_versioned_digest(*new, FALSE, TRUE, version);
        if (safe_str_neq(new_digest, digest)) {
            crm_info("Digest mis-match: expected %s, calculated %s", digest, new_digest);
            result = FALSE;

            crm_trace("%p %.6x", digest_cs, digest_cs ? digest_cs->targets : 0);
            if (digest_cs && digest_cs->targets) {
                save_xml_to_file(old, "diff:original", NULL);
                save_xml_to_file(diff, "diff:input", NULL);
                save_xml_to_file(*new, "diff:new", NULL);
            }

        } else {
            crm_trace("Digest matched: expected %s, calculated %s", digest, new_digest);
        }
        free(new_digest);

    } else if (result) {
        purge_diff_markers(*new);       /* Purge now so the diff is ok */
    }

    return result;
}

static void
__xml_diff_object(xmlNode * old, xmlNode * new)
{
    xmlNode *cIter = NULL;
    xmlAttr *pIter = NULL;

    CRM_CHECK(new != NULL, return);
    if(old == NULL) {
        crm_node_created(new);
        __xml_acl_post_process(new); /* Check creation is allowed */
        return;

    } else {
        xml_private_t *p = new->_private;

        if(p->flags & xpf_processed) {
            /* Avoid re-comparing nodes */
            return;
        }
        p->flags |= xpf_processed;
    }

    for (pIter = crm_first_attr(new); pIter != NULL; pIter = pIter->next) {
        xml_private_t *p = pIter->_private;

        /* Assume everything was just created and take it from there */
        p->flags |= xpf_created;
    }

    for (pIter = crm_first_attr(old); pIter != NULL; ) {
        xmlAttr *prop = pIter;
        xml_private_t *p = NULL;
        const char *name = (const char *)pIter->name;
        const char *old_value = crm_element_value(old, name);
        xmlAttr *exists = xmlHasProp(new, pIter->name);

        pIter = pIter->next;
        if(exists == NULL) {
            p = new->doc->_private;

            /* Prevent the dirty flag being set recursively upwards */
            clear_bit(p->flags, xpf_tracking);
            exists = xmlSetProp(new, (const xmlChar *)name, (const xmlChar *)old_value);
            set_bit(p->flags, xpf_tracking);

            p = exists->_private;
            p->flags = 0;

            crm_trace("Lost %s@%s=%s", old->name, name, old_value);
            xml_remove_prop(new, name);

        } else {
            int p_new = __xml_offset((xmlNode*)exists);
            int p_old = __xml_offset((xmlNode*)prop);
            const char *value = crm_element_value(new, name);

            p = exists->_private;
            p->flags = (p->flags & ~xpf_created);

            if(strcmp(value, old_value) != 0) {
                /* Restore the original value, so we can call crm_xml_add(),
                 * which checks ACLs
                 */
                char *vcopy = crm_element_value_copy(new, name);

                crm_trace("Modified %s@%s %s->%s", old->name, name, old_value, vcopy);
                xmlSetProp(new, prop->name, (const xmlChar *)old_value);
                crm_xml_add(new, name, vcopy);
                free(vcopy);

            } else if(p_old != p_new) {
                crm_info("Moved %s@%s (%d -> %d)", old->name, name, p_old, p_new);
                __xml_node_dirty(new);
                p->flags |= xpf_dirty|xpf_moved;

                if(p_old > p_new) {
                    p = prop->_private;
                    p->flags |= xpf_skip;

                } else {
                    p = exists->_private;
                    p->flags |= xpf_skip;
                }
            }
        }
    }

    for (pIter = crm_first_attr(new); pIter != NULL; ) {
        xmlAttr *prop = pIter;
        xml_private_t *p = pIter->_private;

        pIter = pIter->next;
        if(is_set(p->flags, xpf_created)) {
            char *name = strdup((const char *)prop->name);
            char *value = crm_element_value_copy(new, name);

            crm_trace("Created %s@%s=%s", new->name, name, value);
            /* Remove plus create won't work as it will modify the relative attribute ordering */
            if(__xml_acl_check(new, name, xpf_acl_write)) {
                crm_attr_dirty(prop);
            } else {
                xmlUnsetProp(new, prop->name); /* Remove - change not allowed */
            }

            free(value);
            free(name);
        }
    }

    for (cIter = __xml_first_child(old); cIter != NULL; ) {
        xmlNode *old_child = cIter;
        xmlNode *new_child = find_element(new, cIter, TRUE);

        cIter = __xml_next(cIter);
        if(new_child) {
            __xml_diff_object(old_child, new_child);

        } else {
            /* Create then free (which will check the acls if necessary) */
            xmlNode *candidate = add_node_copy(new, old_child);
            xmlNode *top = xmlDocGetRootElement(candidate->doc);

            __xml_node_clean(candidate);
            __xml_acl_apply(top); /* Make sure any ACLs are applied to 'candidate' */
            /* Record the old position */
            free_xml_with_position(candidate, __xml_offset(old_child));

            if (find_element(new, old_child, TRUE) == NULL) {
                xml_private_t *p = old_child->_private;

                p->flags |= xpf_skip;
            }
        }
    }

    for (cIter = __xml_first_child(new); cIter != NULL; ) {
        xmlNode *new_child = cIter;
        xmlNode *old_child = find_element(old, cIter, TRUE);

        cIter = __xml_next(cIter);
        if(old_child == NULL) {
            xml_private_t *p = new_child->_private;
            p->flags |= xpf_skip;
            __xml_diff_object(old_child, new_child);

        } else {
            /* Check for movement, we already checked for differences */
            int p_new = __xml_offset(new_child);
            int p_old = __xml_offset(old_child);

            if(p_old != p_new) {
                xml_private_t *p = new_child->_private;

                crm_info("%s.%s moved from %d to %d",
                         new_child->name, ID(new_child), p_old, p_new);
                __xml_node_dirty(new);
                p->flags |= xpf_moved;

                if(p_old > p_new) {
                    p = old_child->_private;
                } else {
                    p = new_child->_private;
                }
                p->flags |= xpf_skip;
            }
        }
    }
}

void
xml_calculate_changes(xmlNode * old, xmlNode * new)
{
    CRM_CHECK(safe_str_eq(crm_element_name(old), crm_element_name(new)), return);
    CRM_CHECK(safe_str_eq(ID(old), ID(new)), return);

    if(xml_tracking_changes(new) == FALSE) {
        xml_track_changes(new, NULL, NULL, FALSE);
    }

    __xml_diff_object(old, new);
}

xmlNode *
diff_xml_object(xmlNode * old, xmlNode * new, gboolean suppress)
{
    xmlNode *tmp1 = NULL;
    xmlNode *diff = create_xml_node(NULL, "diff");
    xmlNode *removed = create_xml_node(diff, "diff-removed");
    xmlNode *added = create_xml_node(diff, "diff-added");

    crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET);

    tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top");
    if (suppress && tmp1 != NULL && can_prune_leaf(tmp1)) {
        free_xml(tmp1);
    }

    tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top");
    if (suppress && tmp1 != NULL && can_prune_leaf(tmp1)) {
        free_xml(tmp1);
    }

    if (added->children == NULL && removed->children == NULL) {
        free_xml(diff);
        diff = NULL;
    }

    return diff;
}

gboolean
can_prune_leaf(xmlNode * xml_node)
{
    xmlNode *cIter = NULL;
    xmlAttrPtr pIter = NULL;
    gboolean can_prune = TRUE;
    const char *name = crm_element_name(xml_node);

    if (safe_str_eq(name, XML_TAG_RESOURCE_REF)
        || safe_str_eq(name, XML_CIB_TAG_OBJ_REF)
        || safe_str_eq(name, XML_ACL_TAG_ROLE_REF)
        || safe_str_eq(name, XML_ACL_TAG_ROLE_REFv1)) {
        return FALSE;
    }

    for (pIter = crm_first_attr(xml_node); pIter != NULL; pIter = pIter->next) {
        const char *p_name = (const char *)pIter->name;

        if (strcmp(p_name, XML_ATTR_ID) == 0) {
            continue;
        }
        can_prune = FALSE;
    }

    cIter = __xml_first_child(xml_node);
    while (cIter) {
        xmlNode *child = cIter;

        cIter = __xml_next(cIter);
        if (can_prune_leaf(child)) {
            free_xml(child);
        } else {
            can_prune = FALSE;
        }
    }
    return can_prune;
}

void
diff_filter_context(int context, int upper_bound, int lower_bound,
                    xmlNode * xml_node, xmlNode * parent)
{
    xmlNode *us = NULL;
    xmlNode *child = NULL;
    xmlAttrPtr pIter = NULL;
    xmlNode *new_parent = parent;
    const char *name = crm_element_name(xml_node);

    CRM_CHECK(xml_node != NULL && name != NULL, return);

    us = create_xml_node(parent, name);
    for (pIter = crm_first_attr(xml_node); pIter != NULL; pIter = pIter->next) {
        const char *p_name = (const char *)pIter->name;
        const char *p_value = crm_attr_value(pIter);

        lower_bound = context;
        crm_xml_add(us, p_name, p_value);
    }

    if (lower_bound >= 0 || upper_bound >= 0) {
        crm_xml_add(us, XML_ATTR_ID, ID(xml_node));
        new_parent = us;

    } else {
        upper_bound = in_upper_context(0, context, xml_node);
        if (upper_bound >= 0) {
            crm_xml_add(us, XML_ATTR_ID, ID(xml_node));
            new_parent = us;
        } else {
            free_xml(us);
            us = NULL;
        }
    }

    for (child = __xml_first_child(us); child != NULL; child = __xml_next(child)) {
        diff_filter_context(context, upper_bound - 1, lower_bound - 1, child, new_parent);
    }
}

int
in_upper_context(int depth, int context, xmlNode * xml_node)
{
    if (context == 0) {
        return 0;
    }

    if (xml_node->properties) {
        return depth;

    } else if (depth < context) {
        xmlNode *child = NULL;

        for (child = __xml_first_child(xml_node); child != NULL; child = __xml_next(child)) {
            if (in_upper_context(depth + 1, context, child)) {
                return depth;
            }
        }
    }
    return 0;
}

static xmlNode *
find_xml_comment(xmlNode * root, xmlNode * search_comment, gboolean exact)
{
    xmlNode *a_child = NULL;
    int search_offset = __xml_offset(search_comment);

    CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);

    for (a_child = __xml_first_child(root); a_child != NULL; a_child = __xml_next(a_child)) {
        if (exact) {
            int offset = __xml_offset(a_child);
            xml_private_t *p = a_child->_private;

            if (offset < search_offset) {
                continue;

            } else if (offset > search_offset) {
                return NULL;
            }

            if (is_set(p->flags, xpf_skip)) {
                continue;
            }
        }

        if (a_child->type == XML_COMMENT_NODE
            && safe_str_eq((const char *)a_child->content, (const char *)search_comment->content)) {
            return a_child;

        } else if (exact) {
            return NULL;
        }
    }

    return NULL;
}

static xmlNode *
subtract_xml_comment(xmlNode * parent, xmlNode * left, xmlNode * right,
                     gboolean * changed)
{
    CRM_CHECK(left != NULL, return NULL);
    CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL);

    if (right == NULL
        || safe_str_neq((const char *)left->content, (const char *)right->content)) {
        xmlNode *deleted = NULL;

        deleted = add_node_copy(parent, left);
        *changed = TRUE;

        return deleted;
    }

    return NULL;
}

xmlNode *
subtract_xml_object(xmlNode * parent, xmlNode * left, xmlNode * right,
                    gboolean full, gboolean * changed, const char *marker)
{
    gboolean dummy = FALSE;
    gboolean skip = FALSE;
    xmlNode *diff = NULL;
    xmlNode *right_child = NULL;
    xmlNode *left_child = NULL;
    xmlAttrPtr xIter = NULL;

    const char *id = NULL;
    const char *name = NULL;
    const char *value = NULL;
    const char *right_val = NULL;

    int lpc = 0;
    static int filter_len = DIMOF(filter);

    if (changed == NULL) {
        changed = &dummy;
    }

    if (left == NULL) {
        return NULL;
    }

    if (left->type == XML_COMMENT_NODE) {
        return subtract_xml_comment(parent, left, right, changed);
    }

    id = ID(left);
    if (right == NULL) {
        xmlNode *deleted = NULL;

        crm_trace("Processing <%s id=%s> (complete copy)", crm_element_name(left), id);
        deleted = add_node_copy(parent, left);
        crm_xml_add(deleted, XML_DIFF_MARKER, marker);

        *changed = TRUE;
        return deleted;
    }

    name = crm_element_name(left);
    CRM_CHECK(name != NULL, return NULL);
    CRM_CHECK(safe_str_eq(crm_element_name(left), crm_element_name(right)), return NULL);

    /* check for XML_DIFF_MARKER in a child */
    value = crm_element_value(right, XML_DIFF_MARKER);
    if (value != NULL && strcmp(value, "removed:top") == 0) {
        crm_trace("We are the root of the deletion: %s.id=%s", name, id);
        *changed = TRUE;
        return NULL;
    }

    /* Avoiding creating the full heirarchy would save even more work here */
    diff = create_xml_node(parent, name);

    /* Reset filter */
    for (lpc = 0; lpc < filter_len; lpc++) {
        filter[lpc].found = FALSE;
    }

    /* changes to child objects */
    for (left_child = __xml_first_child(left); left_child != NULL;
         left_child = __xml_next(left_child)) {
        gboolean child_changed = FALSE;

        right_child = find_element(right, left_child, FALSE);
        subtract_xml_object(diff, left_child, right_child, full, &child_changed, marker);
        if (child_changed) {
            *changed = TRUE;
        }
    }

    if (*changed == FALSE) {
        /* Nothing to do */

    } else if (full) {
        xmlAttrPtr pIter = NULL;

        for (pIter = crm_first_attr(left); pIter != NULL; pIter = pIter->next) {
            const char *p_name = (const char *)pIter->name;
            const char *p_value = crm_attr_value(pIter);

            xmlSetProp(diff, (const xmlChar *)p_name, (const xmlChar *)p_value);
        }

        /* We already have everything we need... */
        goto done;

    } else if (id) {
        xmlSetProp(diff, (const xmlChar *)XML_ATTR_ID, (const xmlChar *)id);
    }

    /* changes to name/value pairs */
    for (xIter = crm_first_attr(left); xIter != NULL; xIter = xIter->next) {
        const char *prop_name = (const char *)xIter->name;
        xmlAttrPtr right_attr = NULL;
        xml_private_t *p = NULL;

        if (strcmp(prop_name, XML_ATTR_ID) == 0) {
            continue;
        }

        skip = FALSE;
        for (lpc = 0; skip == FALSE && lpc < filter_len; lpc++) {
            if (filter[lpc].found == FALSE && strcmp(prop_name, filter[lpc].string) == 0) {
                filter[lpc].found = TRUE;
                skip = TRUE;
                break;
            }
        }

        if (skip) {
            continue;
        }

        right_attr = xmlHasProp(right, (const xmlChar *)prop_name);
        if (right_attr) {
            p = right_attr->_private;
        }

        right_val = crm_element_value(right, prop_name);
        if (right_val == NULL || (p && is_set(p->flags, xpf_deleted))) {
            /* new */
            *changed = TRUE;
            if (full) {
                xmlAttrPtr pIter = NULL;

                for (pIter = crm_first_attr(left); pIter != NULL; pIter = pIter->next) {
                    const char *p_name = (const char *)pIter->name;
                    const char *p_value = crm_attr_value(pIter);

                    xmlSetProp(diff, (const xmlChar *)p_name, (const xmlChar *)p_value);
                }
                break;

            } else {
                const char *left_value = crm_element_value(left, prop_name);

                xmlSetProp(diff, (const xmlChar *)prop_name, (const xmlChar *)value);
                crm_xml_add(diff, prop_name, left_value);
            }

        } else {
            /* Only now do we need the left value */
            const char *left_value = crm_element_value(left, prop_name);

            if (strcmp(left_value, right_val) == 0) {
                /* unchanged */

            } else {
                *changed = TRUE;
                if (full) {
                    xmlAttrPtr pIter = NULL;

                    crm_trace("Changes detected to %s in <%s id=%s>", prop_name,
                              crm_element_name(left), id);
                    for (pIter = crm_first_attr(left); pIter != NULL; pIter = pIter->next) {
                        const char *p_name = (const char *)pIter->name;
                        const char *p_value = crm_attr_value(pIter);

                        xmlSetProp(diff, (const xmlChar *)p_name, (const xmlChar *)p_value);
                    }
                    break;

                } else {
                    crm_trace("Changes detected to %s (%s -> %s) in <%s id=%s>",
                              prop_name, left_value, right_val, crm_element_name(left), id);
                    crm_xml_add(diff, prop_name, left_value);
                }
            }
        }
    }

    if (*changed == FALSE) {
        free_xml(diff);
        return NULL;

    } else if (full == FALSE && id) {
        crm_xml_add(diff, XML_ATTR_ID, id);
    }
  done:
    return diff;
}

static int
add_xml_comment(xmlNode * parent, xmlNode * target, xmlNode * update)
{
    CRM_CHECK(update != NULL, return 0);
    CRM_CHECK(update->type == XML_COMMENT_NODE, return 0);

    if (target == NULL) {
        target = find_xml_comment(parent, update, FALSE);
    }

    if (target == NULL) {
        add_node_copy(parent, update);

    /* We won't reach here currently */
    } else if (safe_str_neq((const char *)target->content, (const char *)update->content)) {
        xmlFree(target->content);
        target->content = xmlStrdup(update->content);
    }

    return 0;
}

int
add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * update, gboolean as_diff)
{
    xmlNode *a_child = NULL;
    const char *object_id = NULL;
    const char *object_name = NULL;

#if XML_PARSE_DEBUG
    crm_log_xml_trace("update:", update);
    crm_log_xml_trace("target:", target);
#endif

    CRM_CHECK(update != NULL, return 0);

    if (update->type == XML_COMMENT_NODE) {
        return add_xml_comment(parent, target, update);
    }

    object_name = crm_element_name(update);
    object_id = ID(update);

    CRM_CHECK(object_name != NULL, return 0);

    if (target == NULL && object_id == NULL) {
        /*  placeholder object */
        target = find_xml_node(parent, object_name, FALSE);

    } else if (target == NULL) {
        target = find_entity(parent, object_name, object_id);
    }

    if (target == NULL) {
        target = create_xml_node(parent, object_name);
        CRM_CHECK(target != NULL, return 0);
#if XML_PARSER_DEBUG
        crm_trace("Added  <%s%s%s/>", crm_str(object_name),
                  object_id ? " id=" : "", object_id ? object_id : "");

    } else {
        crm_trace("Found node <%s%s%s/> to update",
                  crm_str(object_name), object_id ? " id=" : "", object_id ? object_id : "");
#endif
    }

    CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(update)), return 0);

    if (as_diff == FALSE) {
        /* So that expand_plus_plus() gets called */
        copy_in_properties(target, update);

    } else {
        /* No need for expand_plus_plus(), just raw speed */
        xmlAttrPtr pIter = NULL;

        for (pIter = crm_first_attr(update); pIter != NULL; pIter = pIter->next) {
            const char *p_name = (const char *)pIter->name;
            const char *p_value = crm_attr_value(pIter);

            /* Remove it first so the ordering of the update is preserved */
            xmlUnsetProp(target, (const xmlChar *)p_name);
            xmlSetProp(target, (const xmlChar *)p_name, (const xmlChar *)p_value);
        }
    }

    for (a_child = __xml_first_child(update); a_child != NULL; a_child = __xml_next(a_child)) {
#if XML_PARSER_DEBUG
        crm_trace("Updating child <%s id=%s>", crm_element_name(a_child), ID(a_child));
#endif
        add_xml_object(target, NULL, a_child, as_diff);
    }

#if XML_PARSER_DEBUG
    crm_trace("Finished with <%s id=%s>", crm_str(object_name), crm_str(object_id));
#endif
    return 0;
}

gboolean
update_xml_child(xmlNode * child, xmlNode * to_update)
{
    gboolean can_update = TRUE;
    xmlNode *child_of_child = NULL;

    CRM_CHECK(child != NULL, return FALSE);
    CRM_CHECK(to_update != NULL, return FALSE);

    if (safe_str_neq(crm_element_name(to_update), crm_element_name(child))) {
        can_update = FALSE;

    } else if (safe_str_neq(ID(to_update), ID(child))) {
        can_update = FALSE;

    } else if (can_update) {
#if XML_PARSER_DEBUG
        crm_log_xml_trace(child, "Update match found...");
#endif
        add_xml_object(NULL, child, to_update, FALSE);
    }

    for (child_of_child = __xml_first_child(child); child_of_child != NULL;
         child_of_child = __xml_next(child_of_child)) {
        /* only update the first one */
        if (can_update) {
            break;
        }
        can_update = update_xml_child(child_of_child, to_update);
    }

    return can_update;
}

int
find_xml_children(xmlNode ** children, xmlNode * root,
                  const char *tag, const char *field, const char *value, gboolean search_matches)
{
    int match_found = 0;

    CRM_CHECK(root != NULL, return FALSE);
    CRM_CHECK(children != NULL, return FALSE);

    if (tag != NULL && safe_str_neq(tag, crm_element_name(root))) {

    } else if (value != NULL && safe_str_neq(value, crm_element_value(root, field))) {

    } else {
        if (*children == NULL) {
            *children = create_xml_node(NULL, __FUNCTION__);
        }
        add_node_copy(*children, root);
        match_found = 1;
    }

    if (search_matches || match_found == 0) {
        xmlNode *child = NULL;

        for (child = __xml_first_child(root); child != NULL; child = __xml_next(child)) {
            match_found += find_xml_children(children, child, tag, field, value, search_matches);
        }
    }

    return match_found;
}

gboolean
replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
{
    gboolean can_delete = FALSE;
    xmlNode *child_of_child = NULL;

    const char *up_id = NULL;
    const char *child_id = NULL;
    const char *right_val = NULL;

    CRM_CHECK(child != NULL, return FALSE);
    CRM_CHECK(update != NULL, return FALSE);

    up_id = ID(update);
    child_id = ID(child);

    if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) {
        can_delete = TRUE;
    }
    if (safe_str_neq(crm_element_name(update), crm_element_name(child))) {
        can_delete = FALSE;
    }
    if (can_delete && delete_only) {
        xmlAttrPtr pIter = NULL;

        for (pIter = crm_first_attr(update); pIter != NULL; pIter = pIter->next) {
            const char *p_name = (const char *)pIter->name;
            const char *p_value = crm_attr_value(pIter);

            right_val = crm_element_value(child, p_name);
            if (safe_str_neq(p_value, right_val)) {
                can_delete = FALSE;
            }
        }
    }

    if (can_delete && parent != NULL) {
        crm_log_xml_trace(child, "Delete match found...");
        if (delete_only || update == NULL) {
            free_xml(child);

        } else {
            xmlNode *tmp = copy_xml(update);
            xmlDoc *doc = tmp->doc;
            xmlNode *old = NULL;

            xml_accept_changes(tmp);
            old = xmlReplaceNode(child, tmp);

            if(xml_tracking_changes(tmp)) {
                /* Replaced sections may have included relevant ACLs */
                __xml_acl_apply(tmp);
            }

            xml_calculate_changes(old, tmp);
            xmlDocSetRootElement(doc, old);
            free_xml(old);
        }
        child = NULL;
        return TRUE;

    } else if (can_delete) {
        crm_log_xml_debug(child, "Cannot delete the search root");
        can_delete = FALSE;
    }

    child_of_child = __xml_first_child(child);
    while (child_of_child) {
        xmlNode *next = __xml_next(child_of_child);

        can_delete = replace_xml_child(child, child_of_child, update, delete_only);

        /* only delete the first one */
        if (can_delete) {
            child_of_child = NULL;
        } else {
            child_of_child = next;
        }
    }

    return can_delete;
}

/*!
 * \brief Create an XML name/value pair
 *
 * \param[in] parent  If not NULL, make new XML node a child of this one
 * \param[in] id      If not NULL, use this as ID (otherwise auto-generate)
 * \param[in] name    Name to use
 * \param[in] value   Value to use
 *
 * \return New XML object on success, NULL otherwise
 */
xmlNode *
crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name,
                      const char *value)
{
    xmlNode *nvp;

    /* id can be NULL so we auto-generate one, and name can be NULL if this
     * will be used to delete a name/value pair by ID, but both can't be NULL
     */
    CRM_CHECK(id || name, return NULL);

    nvp = create_xml_node(parent, XML_CIB_TAG_NVPAIR);
    CRM_CHECK(nvp, return NULL);

    if (id) {
        crm_xml_add(nvp, XML_ATTR_ID, id);
    } else {
        const char *parent_id = ID(parent);

        crm_xml_set_id(nvp, "%s-%s",
                       (parent_id? parent_id : XML_CIB_TAG_NVPAIR), name);
    }
    crm_xml_add(nvp, XML_NVPAIR_ATTR_NAME, name);
    crm_xml_add(nvp, XML_NVPAIR_ATTR_VALUE, value);
    return nvp;
}

void
hash2nvpair(gpointer key, gpointer value, gpointer user_data)
{
    const char *name = key;
    const char *s_value = value;
    xmlNode *xml_node = user_data;

    crm_create_nvpair_xml(xml_node, name, name, s_value);
    crm_trace("dumped: name=%s value=%s", name, s_value);
}

void
hash2smartfield(gpointer key, gpointer value, gpointer user_data)
{
    const char *name = key;
    const char *s_value = value;

    xmlNode *xml_node = user_data;

    if (isdigit(name[0])) {
        xmlNode *tmp = create_xml_node(xml_node, XML_TAG_PARAM);

        crm_xml_add(tmp, XML_NVPAIR_ATTR_NAME, name);
        crm_xml_add(tmp, XML_NVPAIR_ATTR_VALUE, s_value);

    } else if (crm_element_value(xml_node, name) == NULL) {
        crm_xml_add(xml_node, name, s_value);
        crm_trace("dumped: %s=%s", name, s_value);

    } else {
        crm_trace("duplicate: %s=%s", name, s_value);
    }
}

void
hash2field(gpointer key, gpointer value, gpointer user_data)
{
    const char *name = key;
    const char *s_value = value;

    xmlNode *xml_node = user_data;

    if (crm_element_value(xml_node, name) == NULL) {
        crm_xml_add(xml_node, name, s_value);

    } else {
        crm_trace("duplicate: %s=%s", name, s_value);
    }
}

void
hash2metafield(gpointer key, gpointer value, gpointer user_data)
{
    char *crm_name = NULL;

    if (key == NULL || value == NULL) {
        return;
    }

    /* Filter out cluster-generated attributes that contain a '#' or ':'
     * (like fail-count and last-failure).
     */
    for (crm_name = key; *crm_name; ++crm_name) {
        if ((*crm_name == '#') || (*crm_name == ':')) {
            return;
        }
    }

    crm_name = crm_meta_name(key);
    hash2field(crm_name, value, user_data);
    free(crm_name);
}

GHashTable *
xml2list(xmlNode * parent)
{
    xmlNode *child = NULL;
    xmlAttrPtr pIter = NULL;
    xmlNode *nvpair_list = NULL;
    GHashTable *nvpair_hash = crm_str_table_new();

    CRM_CHECK(parent != NULL, return nvpair_hash);

    nvpair_list = find_xml_node(parent, XML_TAG_ATTRS, FALSE);
    if (nvpair_list == NULL) {
        crm_trace("No attributes in %s", crm_element_name(parent));
        crm_log_xml_trace(parent, "No attributes for resource op");
    }

    crm_log_xml_trace(nvpair_list, "Unpacking");

    for (pIter = crm_first_attr(nvpair_list); pIter != NULL; pIter = pIter->next) {
        const char *p_name = (const char *)pIter->name;
        const char *p_value = crm_attr_value(pIter);

        crm_trace("Added %s=%s", p_name, p_value);

        g_hash_table_insert(nvpair_hash, strdup(p_name), strdup(p_value));
    }

    for (child = __xml_first_child(nvpair_list); child != NULL; child = __xml_next(child)) {
        if (strcmp((const char *)child->name, XML_TAG_PARAM) == 0) {
            const char *key = crm_element_value(child, XML_NVPAIR_ATTR_NAME);
            const char *value = crm_element_value(child, XML_NVPAIR_ATTR_VALUE);

            crm_trace("Added %s=%s", key, value);
            if (key != NULL && value != NULL) {
                g_hash_table_insert(nvpair_hash, strdup(key), strdup(value));
            }
        }
    }

    return nvpair_hash;
}

typedef struct name_value_s {
    const char *name;
    const void *value;
} name_value_t;

static gint
sort_pairs(gconstpointer a, gconstpointer b)
{
    int rc = 0;
    const name_value_t *pair_a = a;
    const name_value_t *pair_b = b;

    CRM_ASSERT(a != NULL);
    CRM_ASSERT(pair_a->name != NULL);

    CRM_ASSERT(b != NULL);
    CRM_ASSERT(pair_b->name != NULL);

    rc = strcmp(pair_a->name, pair_b->name);
    if (rc < 0) {
        return -1;
    } else if (rc > 0) {
        return 1;
    }
    return 0;
}

static void
dump_pair(gpointer data, gpointer user_data)
{
    name_value_t *pair = data;
    xmlNode *parent = user_data;

    crm_xml_add(parent, pair->name, pair->value);
}

xmlNode *
sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive)
{
    xmlNode *child = NULL;
    GListPtr sorted = NULL;
    GListPtr unsorted = NULL;
    name_value_t *pair = NULL;
    xmlNode *result = NULL;
    const char *name = NULL;
    xmlAttrPtr pIter = NULL;

    CRM_CHECK(input != NULL, return NULL);

    name = crm_element_name(input);
    CRM_CHECK(name != NULL, return NULL);

    result = create_xml_node(parent, name);

    for (pIter = crm_first_attr(input); pIter != NULL; pIter = pIter->next) {
        const char *p_name = (const char *)pIter->name;
        const char *p_value = crm_attr_value(pIter);

        pair = calloc(1, sizeof(name_value_t));
        pair->name = p_name;
        pair->value = p_value;
        unsorted = g_list_prepend(unsorted, pair);
        pair = NULL;
    }

    sorted = g_list_sort(unsorted, sort_pairs);
    g_list_foreach(sorted, dump_pair, result);
    g_list_free_full(sorted, free);

    for (child = __xml_first_child(input); child != NULL; child = __xml_next(child)) {
        if (recursive) {
            sorted_xml(child, result, recursive);
        } else {
            add_node_copy(result, child);
        }
    }

    return result;
}

xmlNode *
first_named_child(xmlNode * parent, const char *name)
{
    xmlNode *match = NULL;

    for (match = __xml_first_child(parent); match != NULL; match = __xml_next(match)) {
        /*
         * name == NULL gives first child regardless of name; this is
         * semantically incorrect in this function, but may be necessary
         * due to prior use of xml_child_iter_filter
         */
        if (name == NULL || strcmp((const char *)match->name, name) == 0) {
            return match;
        }
    }
    return NULL;
}

/*!
 * \brief Get next instance of same XML tag
 *
 * \param[in] sibling  XML tag to start from
 *
 * \return Next sibling XML tag with same name
 */
xmlNode *
crm_next_same_xml(xmlNode *sibling)
{
    xmlNode *match = __xml_next(sibling);
    const char *name = crm_element_name(sibling);

    while (match != NULL) {
        if (!strcmp(crm_element_name(match), name)) {
            return match;
        }
        match = __xml_next(match);
    }
    return NULL;
}

void
crm_xml_init(void)
{
    static bool init = TRUE;

    if(init) {
        init = FALSE;
        /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many
         * realloc_safe()s and it can take upwards of 18 seconds (yes, seconds)
         * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in
         * less than 1 second.
         */
        xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);

        /* Populate and free the _private field when nodes are created and destroyed */
        xmlDeregisterNodeDefault(pcmkDeregisterNode);
        xmlRegisterNodeDefault(pcmkRegisterNode);

        crm_schema_init();
    }
}

void
crm_xml_cleanup(void)
{
    crm_info("Cleaning up memory from libxml2");
    crm_schema_cleanup();
    xmlCleanupParser();
}

#define XPATH_MAX 512

xmlNode *
expand_idref(xmlNode * input, xmlNode * top)
{
    const char *tag = NULL;
    const char *ref = NULL;
    xmlNode *result = input;
    char *xpath_string = NULL;

    if (result == NULL) {
        return NULL;

    } else if (top == NULL) {
        top = input;
    }

    tag = crm_element_name(result);
    ref = crm_element_value(result, XML_ATTR_IDREF);

    if (ref != NULL) {
        int offset = 0;

        xpath_string = calloc(1, XPATH_MAX);

        offset += snprintf(xpath_string + offset, XPATH_MAX - offset, "//%s[@id='%s']", tag, ref);
        CRM_LOG_ASSERT(offset > 0);

        result = get_xpath_object(xpath_string, top, LOG_ERR);
        if (result == NULL) {
            char *nodePath = (char *)xmlGetNodePath(top);

            crm_err("No match for %s found in %s: Invalid configuration", xpath_string,
                    crm_str(nodePath));
            free(nodePath);
        }
    }

    free(xpath_string);
    return result;
}

const char *
crm_element_value(xmlNode * data, const char *name)
{
    xmlAttr *attr = NULL;

    if (data == NULL) {
        crm_err("Couldn't find %s in NULL", name ? name : "<null>");
        CRM_LOG_ASSERT(data != NULL);
        return NULL;

    } else if (name == NULL) {
        crm_err("Couldn't find NULL in %s", crm_element_name(data));
        return NULL;
    }

    attr = xmlHasProp(data, (const xmlChar *)name);
    if (attr == NULL || attr->children == NULL) {
        return NULL;
    }
    return (const char *)attr->children->content;
}

void
crm_destroy_xml(gpointer data)
{
    free_xml(data);
}
