/*
 * Copyright 2014 Red Hat Inc., Durham, North Carolina.
 * All Rights Reserved.
 *
 * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * Author:
 *     Šimon Lukašík
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <fcntl.h>
#ifdef OS_WINDOWS
#include <io.h>
#else
#include <unistd.h>
#endif
#include <libxml/parser.h>
#include <libxml/xmlreader.h>
#include <libxml/xmlerror.h>

#include "common/elements.h"
#include "common/_error.h"
#include "common/debug_priv.h"
#include "common/public/oscap.h"
#include "common/util.h"
#include "CPE/public/cpe_lang.h"
#include "CPE/cpedict_priv.h"
#include "CPE/cpelang_priv.h"
#include "doc_type_priv.h"
#include "oscap_source.h"
#include "common/oscap_string.h"
#include "oscap_source_priv.h"
#include "OVAL/oval_parser_impl.h"
#include "OVAL/public/oval_definitions.h"
#include "source/bz2_priv.h"
#include "source/schematron_priv.h"
#include "source/validate_priv.h"
#include "XCCDF/elements.h"
#include "XCCDF/public/xccdf_benchmark.h"
#include "DS/sds_priv.h"

typedef enum oscap_source_type {
	OSCAP_SRC_FROM_USER_XML_FILE = 1,               ///< The source originated from XML file supplied by user
	OSCAP_SRC_FROM_USER_MEMORY,                     ///< The source originated from memory supplied by user
	OSCAP_SRC_FROM_XML_DOM,                         ///< The source originated from XML DOM (most often from DataStream).
	// TODO: downloaded from an http address (XCCDF can refer to remote sources)
} oscap_source_type_t;

struct oscap_source {
	oscap_document_type_t scap_type;                ///< Type of SCAP document (XCCDF, OVAL, ...)
	struct {
		oscap_source_type_t type;               ///< Internal type of the oscap_source
		char *version;                          ///< Version of the particular document type
		char *filepath;                         ///< Filepath (if originated from file)
		char *memory;                           ///< Memory buffer (if originated from memory)
		size_t memory_size;                     ///< Size of the memory buffer (if originated from memory)
	} origin;                                       ///
	struct {
		xmlDoc *doc;                            /// DOM
	} xml;
};

struct oscap_source *oscap_source_new_from_file(const char *filepath)
{
	/* TODO: At the end of the day, this shall be the only place in
	 * the library  where a path to filename is set from the outside.
	 */
	struct oscap_source *source = (struct oscap_source *) calloc(1, sizeof(struct oscap_source));
	source->origin.filepath = oscap_strdup(filepath);
	source->origin.type = OSCAP_SRC_FROM_USER_XML_FILE;
	return source;
}

struct oscap_source *oscap_source_clone(struct oscap_source *old)
{
	struct oscap_source *new = (struct oscap_source *) calloc(1, sizeof(struct oscap_source));
	new->scap_type = old->scap_type;
	new->origin.type = old->origin.type;
	new->origin.version = oscap_strdup(old->origin.version);
	new->origin.filepath = oscap_strdup(old->origin.filepath);
	new->origin.memory = oscap_strdup(old->origin.memory);
	new->origin.memory_size = old->origin.memory_size;
	new->xml.doc = xmlCopyDoc(old->xml.doc, true);
	return new;
}

/**
 * Allocate oscap_source struct and fill for memory data
 * @param size_t size Size of data
 * @param const char* filepath Path of file
 * @return Allocated struct
 */
static struct oscap_source* _create_oscap_source(size_t size, const char* filepath)
{
	struct oscap_source *source = (struct oscap_source *) calloc(1, sizeof(struct oscap_source));
	source->origin.memory_size = size;
	source->origin.type = OSCAP_SRC_FROM_USER_MEMORY;
	source->origin.filepath = oscap_strdup(filepath ? filepath : "NONEXISTENT");
	return source;
}

struct oscap_source *oscap_source_new_from_memory(const char *buffer, size_t size, const char *filepath)
{
	struct oscap_source *source = _create_oscap_source(size, filepath);
	source->origin.memory = calloc(1, size);
	memcpy(source->origin.memory, buffer, size);
	return source;
}

struct oscap_source *oscap_source_new_take_memory(char *buffer, size_t size, const char *filepath)
{
	struct oscap_source* source = _create_oscap_source(size, filepath);
	source->origin.memory = buffer;
	return source;
}

struct oscap_source *oscap_source_new_from_xmlDoc(xmlDoc *doc, const char *filepath)
{
	struct oscap_source *source = (struct oscap_source *) calloc(1, sizeof(struct oscap_source));
	source->origin.type = OSCAP_SRC_FROM_XML_DOM;
	source->origin.filepath = oscap_strdup(filepath ? filepath : "NONEXISTENT");
	source->xml.doc = doc;
	return source;
}

void oscap_source_free(struct oscap_source *source)
{
	if (source != NULL) {
		free(source->origin.filepath);
		free(source->origin.memory);
		if (source->xml.doc != NULL) {
			xmlFreeDoc(source->xml.doc);
		}
		free(source->origin.version);
		free(source);
	}
}

void oscap_source_free_xmlDoc(struct oscap_source *source)
{
	if (source != NULL) {
		if (source->xml.doc != NULL) {
			xmlFreeDoc(source->xml.doc);
			source->xml.doc = NULL;
		}
	}
}

/**
 * Returns human readable description of oscap_source origin
 */
const char *oscap_source_readable_origin(const struct oscap_source *source)
{
	// TODO: This may just return NONEXISTANT for sources from raw memory or xmlDoc
	//       and that's not very useful.
	return source->origin.filepath;
}

xmlTextReader *oscap_source_get_xmlTextReader(struct oscap_source *source)
{
	xmlDoc *doc = oscap_source_get_xmlDoc(source);
	if (doc == NULL) {
		return NULL;
	}
	xmlTextReader *reader = xmlReaderWalker(doc);
	if (reader == NULL) {
		oscap_seterr(OSCAP_EFAMILY_XML, "Unable to create xmlTextReader for %s", oscap_source_readable_origin(source));
		oscap_setxmlerr(xmlGetLastError());
	}
	return reader;
}

oscap_document_type_t oscap_source_get_scap_type(struct oscap_source *source)
{
	if (source->scap_type == OSCAP_DOCUMENT_UNKNOWN) {
		xmlTextReader *reader = oscap_source_get_xmlTextReader(source);
		if (reader == NULL) {
			// the oscap error is already set
			return OSCAP_DOCUMENT_UNKNOWN;
		}
		if (oscap_determine_document_type_reader(reader, &(source->scap_type)) == -1) {
			oscap_seterr(OSCAP_EFAMILY_XML, "Unknown document type: '%s'", oscap_source_readable_origin(source));
			// in case of error scap_type must remain UNKNOWN
			assert(source->scap_type == OSCAP_DOCUMENT_UNKNOWN);
		}
		xmlFreeTextReader(reader);
	}
	return source->scap_type;
}

const char *oscap_source_get_filepath(struct oscap_source *source)
{
	return source->origin.filepath;
}

static void xmlErrorCb(struct oscap_string *buffer, const char * format, ...)
{
	va_list ap;
	va_start(ap, format);

	char* error_msg = oscap_vsprintf(format, ap);
	oscap_string_append_string(buffer, error_msg);
	free(error_msg);

	va_end(ap);
}

static bool fd_file_is_executable(int fd)
{
	int fd_dup = dup(fd);
	if (fd_dup == -1) {
		return false;
	}
	lseek(fd_dup, 0, SEEK_SET);
	FILE* file = fdopen(fd_dup, "r");

	if (file == NULL) {
		// Cannot determine type of file we cannot open
		return false;
	}

	// Check SHEBANG
	// When we read after end of file, we get EOF (-1),
	// So we don't have to do any special check
	bool is_exec = true;
	if ( fgetc(file) != (int)'#' ) is_exec = false;
	if ( fgetc(file) != (int)'!' ) is_exec = false;

	fclose(file);
	lseek(fd, 0, SEEK_SET);
	return is_exec;
}

static bool memory_file_is_executable(const char* memory, const size_t size)
{
	if (size < 2){
		return false; // Cannot read SHEBANG
	}

	// Check that file in memory starts with SHEBANG
	if ( memory[0] != '#' ) return false;
	if ( memory[1] != '!' ) return false;
	return true;
}

xmlDoc *oscap_source_get_xmlDoc(struct oscap_source *source)
{
	// We check origin.memory first because even with it being non-NULL
	// filepath will be non-NULL, it will contain the filepath hint.
	struct oscap_string *xml_error_string = oscap_string_new();
	xmlSetGenericErrorFunc(xml_error_string, (xmlGenericErrorFunc)xmlErrorCb);

	if (source->xml.doc == NULL) {
		if (source->origin.memory != NULL) {
			if (bz2_memory_is_bzip(source->origin.memory, source->origin.memory_size)) {
#ifdef BZIP2_FOUND
				source->xml.doc = bz2_mem_read_doc(source->origin.memory, source->origin.memory_size);
#else
				oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unable to unpack bz2 from buffer memory '%s'. Please compile OpenSCAP with bz2 support.", oscap_source_readable_origin(source));
#endif
			} else
			{
				source->xml.doc = xmlReadMemory(source->origin.memory, source->origin.memory_size, NULL, NULL, 0);
				if (source->xml.doc == NULL) {
					if (memory_file_is_executable(source->origin.memory, source->origin.memory_size)) {
						dI("oscap-source in memory was detected as executable file '%s'. Skipped XML parsing", oscap_source_readable_origin(source));
						oscap_string_clear(xml_error_string);
					} else {
						oscap_setxmlerr(xmlGetLastError());
						const char *error_msg = oscap_string_get_cstr(xml_error_string);
						oscap_seterr(OSCAP_EFAMILY_XML, "%sUnable to parse XML from user memory buffer", error_msg);
						oscap_string_clear(xml_error_string);
					}
				}
			}
		}
		else {
			int fd = open(source->origin.filepath, O_RDONLY);
			if ( fd == -1 ){
				source->xml.doc = NULL;
				oscap_seterr(OSCAP_EFAMILY_GLIBC, "Unable to open file: '%s'", oscap_source_readable_origin(source));
			} else {
				if (bz2_fd_is_bzip(fd)) {
#ifdef BZIP2_FOUND
					source->xml.doc = bz2_fd_read_doc(fd);
#else
					source->xml.doc = NULL;
					oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unable to unpack bz2 file '%s'. Please compile OpenSCAP with bz2 support.", oscap_source_readable_origin(source));
#endif
				} else
				{
					source->xml.doc = xmlReadFd(fd, NULL, NULL, 0);
					if (source->xml.doc == NULL) {
						if (fd_file_is_executable(fd)) {
							dI("oscap-source file was detected as executable file '%s'. Skipped XML parsing", oscap_source_readable_origin(source));
							oscap_string_clear(xml_error_string);
						} else {
							oscap_setxmlerr(xmlGetLastError());
							const char *error_msg = oscap_string_get_cstr(xml_error_string);
							oscap_seterr(OSCAP_EFAMILY_XML, "%sUnable to parse XML at: '%s'", error_msg, oscap_source_readable_origin(source));
							oscap_string_clear(xml_error_string);
						}
					}
				}
				close(fd);
			}
		}
	}

	xmlSetGenericErrorFunc(stderr, NULL);

	if (!oscap_string_empty(xml_error_string)) {
		const char *error_msg = oscap_string_get_cstr(xml_error_string);
		oscap_seterr(OSCAP_EFAMILY_XML, "%sFound xml error.", error_msg);
	}

	oscap_string_free(xml_error_string);
	return source->xml.doc;
}

xmlDoc *oscap_source_pop_xmlDoc(struct oscap_source *source)
{
	xmlDoc *doc = oscap_source_get_xmlDoc(source);
	source->xml.doc = NULL;
	return doc;
}

int oscap_source_validate(struct oscap_source *source, xml_reporter reporter, void *user)
{
	int ret;
	oscap_document_type_t scap_type = oscap_source_get_scap_type(source);

	if (scap_type == OSCAP_DOCUMENT_UNKNOWN) {
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unrecognized document type for: %s", oscap_source_readable_origin(source));
		ret = -1;
	} else {
		const char *schema_version = oscap_source_get_schema_version(source);
		if (!schema_version) {
			schema_version = "unknown schema version";
		}
		const char *type_name = oscap_document_type_to_string(scap_type);
		const char *origin = oscap_source_readable_origin(source);
		dD("Validating %s (%s) document from %s.", type_name, schema_version, origin);
		ret = oscap_source_validate_priv(source, scap_type, schema_version, reporter, user);
		if (ret != 0) {
			oscap_seterr(OSCAP_EFAMILY_OSCAP, "Invalid %s (%s) content in %s.", type_name, schema_version, origin);
		}
	}
	return ret;
}

int oscap_source_validate_schematron(struct oscap_source *source, const char *outfile)
{
	FILE *outfile_fd = stdout;
	if (outfile != NULL) {
		outfile_fd = fopen(outfile, "w");
		if (outfile_fd == NULL) {
			dE("Can't open %s: %s", outfile, strerror(errno));
			return -1;
		}
	}
	oscap_document_type_t scap_type = oscap_source_get_scap_type(source);
	const char *schema_version = oscap_source_get_schema_version(source);
	if (!schema_version) {
		schema_version = "unknown schema version";
	}
	int ret = oscap_source_validate_schematron_priv(source, scap_type,
		schema_version, outfile_fd);
	if (outfile != NULL)
		fclose(outfile_fd);
	if (ret != 0) {
		const char *type_name = oscap_document_type_to_string(scap_type);
		const char *origin = oscap_source_readable_origin(source);
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "Invalid %s (%s) content in %s.", type_name, schema_version, origin);
	}
	return ret;
}

const char *oscap_source_get_schema_version(struct oscap_source *source)
{
	if (source->origin.version == NULL) {
		xmlTextReader *reader = oscap_source_get_xmlTextReader(source);
		if (reader == NULL) {
			return NULL;
		}
		switch (oscap_source_get_scap_type(source)) {
			case OSCAP_DOCUMENT_SDS:
				source->origin.version = ds_sds_detect_version(reader);
				break;
			case OSCAP_DOCUMENT_ARF:
				source->origin.version = oscap_strdup("1.1");
				break;
			case OSCAP_DOCUMENT_OVAL_DEFINITIONS:
			case OSCAP_DOCUMENT_OVAL_VARIABLES:
			case OSCAP_DOCUMENT_OVAL_DIRECTIVES:
			case OSCAP_DOCUMENT_OVAL_SYSCHAR:
			case OSCAP_DOCUMENT_OVAL_RESULTS:
				source->origin.version = oval_determine_document_schema_version_priv(
					reader, oscap_source_get_scap_type(source));
				break;
			case OSCAP_DOCUMENT_XCCDF:
			case OSCAP_DOCUMENT_XCCDF_TAILORING:
				source->origin.version = xccdf_detect_version_priv(reader);
				break;
			case OSCAP_DOCUMENT_CPE_DICTIONARY:
				source->origin.version = cpe_dict_detect_version_priv(reader);
				break;
			case OSCAP_DOCUMENT_CPE_LANGUAGE:
				source->origin.version = cpe_lang_model_detect_version_priv(reader);
				break;
			case OSCAP_DOCUMENT_CVE_FEED:
				source->origin.version = oscap_strdup("2.0");
				break;
			case OSCAP_DOCUMENT_CVRF_FEED:
				source->origin.version = oscap_strdup("1.1");
				break;
			case OSCAP_DOCUMENT_SCE_RESULT:
				source->origin.version = oscap_strdup("1.0");
				break;
			case OSCAP_DOCUMENT_OCIL:
				source->origin.version = oscap_strdup("2.0");
				break;
			default:
				oscap_seterr(OSCAP_EFAMILY_OSCAP, "Could not determine origin.version for document %s: Unknown type: %s",
					oscap_source_readable_origin(source),
					oscap_document_type_to_string(oscap_source_get_scap_type(source)));
				break;
		}
		xmlFreeTextReader(reader);
	}
	return source->origin.version;
}

int oscap_source_save_as(struct oscap_source *source, const char *filename)
{
	// TODO: This assumes XML and xmlDoc being available
	const char *target = filename != NULL ? filename : oscap_source_readable_origin(source);
	xmlDoc *doc = oscap_source_get_xmlDoc(source);
	if (doc == NULL) {
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "Could not save document to %s: DOM representation not available.", target);
		return -1;
	}
	return oscap_xml_save_filename(target, doc) == 1 ? 0 : -1;
}

int oscap_source_get_raw_memory(struct oscap_source *source, char **buffer, size_t *size)
{
	if (source->origin.memory != NULL) {
		char *ret = (char*)malloc(source->origin.memory_size);
		memcpy(ret, source->origin.memory, source->origin.memory_size);
		*buffer = ret;
		*size = source->origin.memory_size;
		return 0;
	}
	else {
		xmlDoc *doc = oscap_source_get_xmlDoc(source);

		if (doc == NULL) {
			oscap_seterr(OSCAP_EFAMILY_OSCAP,
				"Can't retrieve raw memory. Given oscap_source doesn't originate from "
				"raw memory and xmlDoc isn't available.");
			return 1;
		}

		// libxml2 asks us to use xmlFree on the returned buffer,
		// free works fine however. Instead of doing a memory copy dance
		// here we just let libxml2 fill the buffer.
		// However int and size_t can be different so we do the safe thing.
		int isize = 0;
		xmlDocDumpMemory(doc, (xmlChar**)buffer, &isize);
		*size = isize;
		return 0;
	}
}
