/*
 * libkmipclient - KMIP client library
 *
 * Copyright IBM Corp. 2021
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include <errno.h>
#include <endian.h>
#include <string.h>

#include "kmip.h"
#include "names.h"
#include "utils.h"

#define KMIP_XML_TTLV		"TTLV"
#define KMIP_XML_TAG		"tag"
#define KMIP_XML_NAME		"name"
#define KMIP_XML_TYPE		"type"
#define KMIP_XML_VALUE		"value"

/**
 * Decode a KMIP node from the data in the XML node using the XML encoding.
 *
 * @param xml               the XML node to decode
 * @param parent            the parent node or NULL if no parent exists.
 * @param node              On return: the decoded node.The newly allocated
 *                          node has a reference count of 1.
 * @param max_nesting_level the maximum nesting levels of structures within the
 *                          KMIP node. If the nesting level is reached, E2BIG
 *                          is returned.
 * @param debug             if true, debug messages are printed
 *
 * @returns 0 in case of success, or a negative errno value
 */
int kmip_decode_xml(const xmlNode *xml, struct kmip_node *parent,
		    struct kmip_node **node, size_t max_nesting_level,
		    bool debug)
{
	char *tag_attr = NULL, *name_attr = NULL, *type_attr = NULL;
	enum kmip_tag tag, v1_attr_tag = 0;
	char *tag_name, *value_attr = NULL;
	struct kmip_node *n = NULL, *e;
	uint64_t uint64;
	xmlNode *child;
	int64_t int64;
	int rc = 0, i;

	if (max_nesting_level == 0)
		return -E2BIG;

	if (xml == NULL || node == NULL)
		return -EINVAL;

	if (xml->type != XML_ELEMENT_NODE) {
		kmip_debug(debug, "Invalid XML node type: %d", xml->type);
		return -EINVAL;
	}

	n = calloc(1, sizeof(struct kmip_node));
	if (n == NULL) {
		kmip_debug(debug, "calloc failed");
		return -ENOMEM;
	}
	n->ref_count = 1;

	if (strcmp((char *)xml->name, KMIP_XML_TTLV) == 0) {
		tag_attr = (char *)xmlGetProp(xml, (xmlChar *)KMIP_XML_TAG);
		if (tag_attr == NULL) {
			kmip_debug(debug, "Missing '%s' attribute in XML node",
				   KMIP_XML_TAG);
			rc = -EBADMSG;
			goto out;
		}
		tag_name = tag_attr;
	} else {
		tag_name = (char *)xml->name;
	}
	n->tag = kmip_tag_by_name_or_hex(tag_name);
	if (n->tag == 0) {
		kmip_debug(debug, "Unknown 'tag' in XML object: '%s'",
			   tag_name);
		rc = -EBADMSG;
		goto out;
	}

	name_attr = (char *)xmlGetProp(xml, (xmlChar *)KMIP_XML_NAME);
	if (name_attr != NULL)
		n->name = strdup(name_attr);

	type_attr = (char *)xmlGetProp(xml, (xmlChar *)KMIP_XML_TYPE);
	if (type_attr == NULL) {
		n->type = KMIP_TYPE_STRUCTURE;
	} else {
		n->type = kmip_type_by_name_or_hex(type_attr);
		if (n->type == 0) {
			kmip_debug(debug, "Unknown 'type' in XML object: '%s'",
				   type_attr);
			rc = -EBADMSG;
			goto out;
		}
	}

	value_attr = (char *)xmlGetProp(xml, (xmlChar *)KMIP_XML_VALUE);
	if (n->type != KMIP_TYPE_STRUCTURE && value_attr == NULL) {
		kmip_debug(debug, "Missing '%s' attribute in XML node",
			   KMIP_XML_VALUE);
		rc = -EBADMSG;
		goto out;
	}

	/*
	 * KMIP v1.x attribute values may be Enumerations or Integer Masks.
	 * To correctly decode them, we need to know the tag. This is contained
	 * in a Attribute Name node, which is an element of our parent node.
	 */
	if (n->tag == KMIP_TAG_ATTRIBUTE_VALUE)
		v1_attr_tag = kmip_find_v1_attribute_name_tag(parent);
	tag = (v1_attr_tag != 0 ? v1_attr_tag : n->tag);

	kmip_debug(debug, "tag: 0x%x type: 0x%x", n->tag, n->type);

	switch (n->type) {
	case KMIP_TYPE_STRUCTURE:
		for (child = xml->children, i = 0; child != NULL;
						child = child->next, i++) {
			if (child->type != XML_ELEMENT_NODE)
				continue;

			rc = kmip_decode_xml(child, n, &e,
					     max_nesting_level - 1, debug);
			if (rc != 0) {
				kmip_debug(debug, "Failed to parse child "
					   "element %d", i);
				goto out;
			}
			rc = kmip_node_add_structure_element(n, e);
			kmip_node_free(e);
			if (rc != 0) {
				kmip_debug(debug,
					   "kmip_node_structure_add_element "
					   "failed: rc: %d", rc);
				goto out;
			}
		}
		break;

	case KMIP_TYPE_INTEGER:
	case KMIP_TYPE_LONG_INTEGER:
	case KMIP_TYPE_DATE_TIME_EXTENDED:
		if (n->type == KMIP_TYPE_INTEGER &&
		    kmip_is_tag_mask(tag)) {
			rc = kmip_parse_mask(tag, value_attr, ' ', &int64);
			if (rc != 0) {
				kmip_debug(debug, "Failed to parse "
					   "mask string '%s'", value_attr);
				goto out;
			}
		} else {
			rc = kmip_parse_decimal_int(value_attr, &int64);
			if (rc != 0) {
				kmip_debug(debug, "Failed to parse "
					   "decimal string '%s'", value_attr);
				goto out;
			}
		}

		switch (n->type) {
		case KMIP_TYPE_INTEGER:
			n->integer_value = int64;
			break;
		case KMIP_TYPE_LONG_INTEGER:
			n->long_value = int64;
			break;
		case KMIP_TYPE_DATE_TIME_EXTENDED:
			n->date_time_ext_value = int64;
			break;
		default:
			break;
		}
		break;

	case KMIP_TYPE_INTERVAL:
		rc = kmip_parse_decimal_uint(value_attr, &uint64);
		if (rc != 0) {
			kmip_debug(debug, "Failed to parse "
				   "decimal string '%s'", value_attr);
			goto out;
		}
		n->interval_value = uint64;
		break;

	case KMIP_TYPE_BIG_INTEGER:
		rc = kmip_parse_bignum(value_attr, false,
				       &n->big_integer_value);
		if (rc != 0) {
			kmip_debug(debug,
				   "Failed to parse bignum string '%s'",
				   value_attr);
			goto out;
		}
		break;

	case KMIP_TYPE_ENUMERATION:
		rc = kmip_enum_value_by_tag_name_or_hex(tag, value_attr,
							&n->enumeration_value);
		if (rc != 0) {
			kmip_debug(debug,
				   "Failed to parse enumeration '%s'",
				   value_attr);
			goto out;
		}
		break;

	case KMIP_TYPE_BOOLEAN:
		n->boolean_value = (strcmp(value_attr, "true") == 0 ||
				    strcmp(value_attr, "1") == 0);
		break;

	case KMIP_TYPE_TEXT_STRING:
		n->text_value = strdup(value_attr);
		if (n->text_value == NULL) {
			rc = -ENOMEM;
			goto out;
		}
		n->length = strlen(n->text_value);
		break;

	case KMIP_TYPE_BYTE_STRING:
		rc = kmip_parse_hex(value_attr, false, &n->bytes_value,
				    &n->length);
		if (rc != 0) {
			kmip_debug(debug,
				   "Failed to parse hex string '%s'",
				   value_attr);
			goto out;
		}
		break;

	case KMIP_TYPE_DATE_TIME:
		rc = kmip_parse_timestamp(value_attr, &n->date_time_value);
		if (rc != 0) {
			kmip_debug(debug,
				   "Failed to parse time stamp '%s'",
				   value_attr);
			goto out;
		}
		break;

	default:
		kmip_debug(debug, "unknown type: 0x%x", n->type);
		rc = -EBADMSG;
		goto out;
	}

	*node = n;
	rc = 0;

out:
	if (rc != 0 && n != NULL)
		kmip_node_free(n);
	if (tag_attr != NULL)
		xmlFree(tag_attr);
	if (name_attr != NULL)
		xmlFree(name_attr);
	if (type_attr != NULL)
		xmlFree(type_attr);
	if (value_attr != NULL)
		xmlFree(value_attr);

	return rc;
}

/**
 * Encode a KMIP node into an XML node using the XML encoding.
 *
 * @param node              the node to encode
 * @param xml               On return: the XML node
 * @param debug             if true, debug messages are printed
 *
 * @returns 0 in case of success, or a negative errno value
 */
int kmip_encode_xml(const struct kmip_node *node, xmlNode **xml, bool debug)
{
	enum kmip_tag tag, v1_attr_tag = 0;
	xmlNode *ret_xml = NULL, *elem_xml;
	struct kmip_node *element;
	const char *tag_name;
	char tmp_str[50];
	const char *str;
	struct tm *tm;
	xmlAttr *attr;
	char *tmp;
	int rc;

	if (node == NULL || xml == NULL)
		return -EINVAL;

	kmip_debug(debug, "tag: 0x%x type: 0x%x", node->tag, node->type);

	tag_name = kmip_tag_name_by_tag(node->tag);
	if (tag_name != NULL)
		ret_xml = xmlNewNode(NULL, (xmlChar *)tag_name);
	else
		ret_xml = xmlNewNode(NULL, (xmlChar *)KMIP_XML_TTLV);
	if (ret_xml == NULL) {
		kmip_debug(debug, "Failed to allocate a XML node");
		return -ENOMEM;
	}

	if (tag_name == NULL) {
		sprintf(tmp_str, "0x%06x", node->tag);
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_TAG,
				  (xmlChar *)tmp_str);
		if (attr == NULL) {
			kmip_debug(debug,
				   "Failed to add '%s' attribute to XML node",
				   KMIP_XML_TAG);
			rc = -ENOMEM;
			goto out;
		}

		if (node->name != NULL) {
			attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_NAME,
					  (xmlChar *)node->name);
			if (attr == NULL) {
				kmip_debug(debug, "Failed to add '%s' "
					   "attribute to XML node",
					   KMIP_XML_NAME);
				rc = -ENOMEM;
				goto out;
			}
		}
	}

	if (node->type != KMIP_TYPE_STRUCTURE) {
		str = kmip_type_name_by_type(node->type);
		if (str == NULL) {
			kmip_debug(debug, "unknown type 0x%x", node->type);
			rc = -EINVAL;
			goto out;
		}

		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_TYPE,
				  (xmlChar *)str);
		if (attr == NULL) {
			kmip_debug(debug,
				   "Failed to add '%s' attribute to XML node",
				   KMIP_XML_TYPE);
			rc = -ENOMEM;
			goto out;
		}
	}

	/*
	 * KMIP v1.x attribute values may be Enumerations or Integer Masks.
	 * To correctly encode them, we need to know the tag. This is contained
	 * in a Attribute Name node, which is an element of our parent node.
	 */
	if (node->tag == KMIP_TAG_ATTRIBUTE_VALUE)
		v1_attr_tag = kmip_find_v1_attribute_name_tag(node->parent);
	tag = (v1_attr_tag != 0 ? v1_attr_tag : node->tag);

	switch (node->type) {
	case KMIP_TYPE_STRUCTURE:
		element = node->structure_value;
		while (element != NULL) {
			rc = kmip_encode_xml(element, &elem_xml, debug);
			if (rc != 0) {
				kmip_debug(debug, "kmip_encode_xml failed");
				goto out;
			}
			if (xmlAddChild(ret_xml, elem_xml) == NULL) {
				kmip_debug(debug, "xmlAddChild failed");
				rc = -EIO;
				goto out;
			}
			element = element->next;
		}
		attr = NULL;
		break;

	case KMIP_TYPE_INTEGER:
		if (kmip_is_tag_mask(tag) && node->integer_value != 0) {
			rc = kmip_format_mask(tag, node->integer_value,
					      ' ', &tmp);
			if (rc != 0) {
				kmip_debug(debug, "kmip_format_mask failed");
				goto out;
			}
			attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
					  (xmlChar *)tmp);
			free(tmp);
		} else {
			sprintf(tmp_str, "%d", node->integer_value);
			attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
					  (xmlChar *)tmp_str);
		}
		break;

	case KMIP_TYPE_INTERVAL:
		sprintf(tmp_str, "%u", node->interval_value);
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp_str);
		break;

	case KMIP_TYPE_LONG_INTEGER:
		sprintf(tmp_str, "%ld", node->long_value);
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp_str);
		break;

	case KMIP_TYPE_BIG_INTEGER:
		rc = kmip_format_bignum(node->big_integer_value, false, &tmp);
		if (rc != 0) {
			kmip_debug(debug, "kmip_format_bignum failed");
			goto out;
		}
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp);
		free(tmp);
		break;

	case KMIP_TYPE_ENUMERATION:
		str = kmip_enum_name_by_tag_value(tag, node->enumeration_value);
		if (str != NULL) {
			attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
					  (xmlChar *)str);
		} else {
			sprintf(tmp_str, "0x%08x", node->enumeration_value);
			attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
					  (xmlChar *)tmp_str);
		}
		break;

	case KMIP_TYPE_BOOLEAN:
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)(node->boolean_value ?
							  "true" : "false"));
		break;

	case KMIP_TYPE_TEXT_STRING:
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)node->text_value);
		break;

	case KMIP_TYPE_BYTE_STRING:
		rc = kmip_format_hex(node->bytes_value, node->length,
				     false, &tmp);
		if (rc != 0) {
			kmip_debug(debug, "kmip_format_hex_long failed");
			goto out;
		}
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp);
		free(tmp);
		break;

	case KMIP_TYPE_DATE_TIME:
		tm = gmtime((time_t *)&node->date_time_value);
		if (tm == NULL) {
			rc = -EINVAL;
			goto out;
		}
		strftime(tmp_str, sizeof(tmp_str), KMIP_ISO8601_TIMESTAMP_UTC,
			 tm);
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp_str);
		break;

	case KMIP_TYPE_DATE_TIME_EXTENDED:
		sprintf(tmp_str, "%ld", node->date_time_ext_value);
		attr = xmlSetProp(ret_xml, (xmlChar *)KMIP_XML_VALUE,
				  (xmlChar *)tmp_str);
		break;

	default:
		kmip_debug(debug, "unknown type: 0x%x", node->type);
		rc = -EINVAL;
		goto out;
	}

	if (attr == NULL && node->type != KMIP_TYPE_STRUCTURE) {
		kmip_debug(debug, "Failed to add '%s' "
			   "attribute to XML node",
			   KMIP_XML_VALUE);
		rc = -ENOMEM;
		goto out;
	}

	rc = 0;
	*xml = ret_xml;

out:
	if (rc != 0)
		xmlFreeNode(ret_xml);

	return rc;
}

