/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2014-2025 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:as-validator
 * @short_description: Validator and report-generator about AppStream XML metadata
 * @include: appstream.h
 *
 * This object is able to validate AppStream XML metadata (catalog and metainfo)
 * and to generate a report about issues found with it.
 *
 * See also: #AsMetadata
 */

#include <config.h>
#include <glib.h>
#include <gio/gio.h>
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <string.h>

#include "as-validator.h"
#include "as-validator-issue.h"
#include "as-validator-issue-tag.h"

#include "as-utils.h"
#include "as-utils-private.h"
#include "as-curl.h"
#include "as-vercmp.h"
#include "as-spdx.h"
#include "as-component.h"
#include "as-component-private.h"
#include "as-yaml.h"
#include "as-desktop-entry.h"
#include "as-content-rating-private.h"
#include "as-zstd-decompressor.h"

typedef struct {
	GHashTable *issue_tags; /* of utf8:AsValidatorIssueTag */

	GHashTable *issues;	     /* of utf8:AsValidatorIssue */
	GHashTable *issues_per_file; /* of utf8:GPtrArray<AsValidatorIssue> */

	AsComponent *current_cpt;
	gchar *current_fname;
	gchar *current_dir;
	GPtrArray *release_data; /* of AsReleaseDataPair */

	gboolean check_urls;
	gboolean strict;
	AsCurl *acurl;
} AsValidatorPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AsValidator, as_validator, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (as_validator_get_instance_private (o))

/**
 * as_validator_error_quark:
 *
 * Return value: An error quark.
 *
 * Since: 0.15.4
 **/
G_DEFINE_QUARK (as-validator-error-quark, as_validator_error)

typedef struct {
	gchar *fname;
	GBytes *bytes;
} AsReleaseDataPair;

static AsReleaseDataPair *
as_release_data_pair_new (const gchar *fname, GBytes *bytes)
{
	AsReleaseDataPair *pair;
	pair = g_new0 (AsReleaseDataPair, 1);
	pair->fname = g_strdup (fname);
	pair->bytes = g_bytes_ref (bytes);
	return pair;
}

static void
as_release_data_pair_free (AsReleaseDataPair *pair)
{
	g_free (pair->fname);
	g_bytes_unref (pair->bytes);
	g_free (pair);
}

/**
 * as_validator_init:
 **/
static void
as_validator_init (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);

	priv->issue_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	for (guint i = 0; as_validator_issue_tag_list[i].tag != NULL; i++) {
		gboolean r = g_hash_table_insert (priv->issue_tags,
						  g_strdup (as_validator_issue_tag_list[i].tag),
						  &as_validator_issue_tag_list[i]);
		if (G_UNLIKELY (!r))
			g_critical ("Duplicate issue-tag '%s' found in tag list. This is a bug in "
				    "the validator.",
				    as_validator_issue_tag_list[i].tag);
	}

	/* set of issues */
	priv->issues = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
	/* filename -> issue list mapping */
	priv->issues_per_file = g_hash_table_new_full (g_str_hash,
						       g_str_equal,
						       g_free,
						       (GDestroyNotify) g_ptr_array_unref);
	/* registry for injected release metadata */
	priv->release_data = g_ptr_array_new_with_free_func (
	    (GDestroyNotify) as_release_data_pair_free);

	priv->current_fname = NULL;
	priv->current_cpt = NULL;
	priv->check_urls = FALSE;
	priv->strict = FALSE;
}

/**
 * as_validator_finalize:
 **/
static void
as_validator_finalize (GObject *object)
{
	AsValidator *validator = AS_VALIDATOR (object);
	AsValidatorPrivate *priv = GET_PRIVATE (validator);

	g_hash_table_unref (priv->issue_tags);

	g_hash_table_unref (priv->issues_per_file);
	g_hash_table_unref (priv->issues);

	g_free (priv->current_fname);
	g_free (priv->current_dir);
	if (priv->current_cpt != NULL)
		g_object_unref (priv->current_cpt);
	g_ptr_array_unref (priv->release_data);

	if (priv->acurl != NULL)
		g_object_unref (priv->acurl);

	G_OBJECT_CLASS (as_validator_parent_class)->finalize (object);
}

static void as_validator_add_issue (AsValidator *validator,
				    xmlNode *node,
				    const gchar *tag,
				    const gchar *format,
				    ...) G_GNUC_PRINTF (4, 5);

/**
 * as_validator_add_issue:
 **/
static void
as_validator_add_issue (AsValidator *validator,
			xmlNode *node,
			const gchar *tag,
			const gchar *format,
			...)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	va_list args;
	g_autofree gchar *buffer = NULL;
	g_autofree gchar *tag_final = NULL;
	const gchar *explanation;
	AsIssueSeverity severity;
	g_autofree gchar *location = NULL;
	AsValidatorIssue *issue;
	gchar *id_str;
	const AsValidatorIssueTag *tag_data = g_hash_table_lookup (priv->issue_tags, tag);

	if (tag_data == NULL) {
		/* we requested data about an invalid tag */
		g_warning ("Validator invoked invalid issue tag: %s", tag);
		severity = AS_ISSUE_SEVERITY_ERROR;
		explanation = _("The emitted issue tag is unknown in the tag registry of AppStream. "
				"This is a bug in the validator itself, please report this issue in our bugtracker.");
		tag_final = g_strdup_printf ("__error__%s", tag);
	} else {
		tag_final = g_strdup (tag);
		severity = tag_data->severity;
		explanation = tag_data->explanation;
	}

	if (format != NULL) {
		va_start (args, format);
		buffer = g_strdup_vprintf (format, args);
		va_end (args);
	}

	issue = as_validator_issue_new ();
	as_validator_issue_set_tag (issue, tag_final);
	as_validator_issue_set_severity (issue, severity);
	as_validator_issue_set_hint (issue, buffer);
	as_validator_issue_set_explanation (issue, explanation);

	/* update location information */
	if (priv->current_fname != NULL)
		as_validator_issue_set_filename (issue, priv->current_fname);

	if (priv->current_cpt != NULL)
		as_validator_issue_set_cid (issue, as_component_get_id (priv->current_cpt));

	if (node != NULL)
		as_validator_issue_set_line (issue, xmlGetLineNo (node));

	location = as_validator_issue_get_location (issue);
	id_str = g_strdup_printf ("%s/%s/%s", location, tag_final, buffer);
	/* str ownership is transferred to the hashtable */
	if (g_hash_table_insert (priv->issues, id_str, issue)) {
		/* the issue is new, we can add it to our by-file listing */
		const gchar *fname_key = priv->current_fname ? priv->current_fname : "";
		GPtrArray *ilist = g_hash_table_lookup (priv->issues_per_file, fname_key);
		if (ilist == NULL) {
			ilist = g_ptr_array_new_with_free_func (g_object_unref);
			g_hash_table_insert (priv->issues_per_file, g_strdup (fname_key), ilist);
		}
		g_ptr_array_add (ilist, g_object_ref (issue));
	}
}

/**
 * as_validator_set_current_fname:
 *
 * Sets the basename of the file we are currently dealing with.
 **/
static void
as_validator_set_current_fname (AsValidator *validator, const gchar *fname)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_free (priv->current_fname);
	priv->current_fname = g_strdup (fname);
}

/**
 * as_validator_set_current_dir:
 *
 * Sets the path to the directory with the metainfo file that we are currently dealing with.
 **/
static void
as_validator_set_current_dir (AsValidator *validator, const gchar *dirname)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_free (priv->current_dir);
	priv->current_dir = g_strdup (dirname);
}

/**
 * as_validator_clear_current_fname:
 *
 * Clears the current filename.
 **/
static void
as_validator_clear_current_fname (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_free (g_steal_pointer (&priv->current_fname));
}

/**
 * as_validator_set_current_cpt:
 *
 * Sets the #AsComponent we are currently analyzing.
 **/
static void
as_validator_set_current_cpt (AsValidator *validator, AsComponent *cpt)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	if (priv->current_cpt != NULL)
		g_object_unref (priv->current_cpt);
	priv->current_cpt = g_object_ref (cpt);
}

/**
 * as_validator_clear_current_cpt:
 *
 * Clears the current component.
 **/
static void
as_validator_clear_current_cpt (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	if (priv->current_cpt != NULL)
		g_object_unref (priv->current_cpt);
	priv->current_cpt = NULL;
}

/**
 * as_validator_clear_issues:
 * @validator: An instance of #AsValidator.
 *
 * Clears the list of issues
 **/
static void
as_validator_clear_issues (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_hash_table_remove_all (priv->issues_per_file);
	g_hash_table_remove_all (priv->issues);
}

/**
 * as_validator_check_success:
 * @validator: An instance of #AsValidator.
 *
 * Check the current registered values again and return %TRUE
 * if no issues were found that would make the previously validated
 * files fail validation.
 *
 * Usually you do not need to call this function explicitly, as
 * the as_validator_validate_* functions will already return whether
 * data was valid as return value.
 *
 * Returns: %TRUE if previously validated files were valid.
 */
gboolean
as_validator_check_success (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	GHashTableIter iter;
	gpointer value;

	g_hash_table_iter_init (&iter, priv->issues);
	while (g_hash_table_iter_next (&iter, NULL, &value)) {
		AsIssueSeverity severity;
		AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (value);
		severity = as_validator_issue_get_severity (issue);

		if (priv->strict) {
			/* in strict mode we fail for anything that's not a pedantic issue */
			if (severity != AS_ISSUE_SEVERITY_PEDANTIC)
				return FALSE;
		} else {
			/* any error or warning means validation has failed */
			if (severity == AS_ISSUE_SEVERITY_ERROR ||
			    severity == AS_ISSUE_SEVERITY_WARNING)
				return FALSE;
		}
	}

	return TRUE;
}

/**
 * as_validator_setup_networking:
 */
static gboolean
as_validator_setup_networking (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_autoptr(GError) tmp_error = NULL;

	/* check if we are already initialized */
	if (priv->acurl != NULL)
		return TRUE;

	/* don't initialize if no URLs should be checked */
	if (!priv->check_urls)
		return TRUE;

	priv->acurl = as_curl_new (&tmp_error);
	if (priv->acurl == NULL) {
		g_critical ("Failed to set up networking support: %s", tmp_error->message);
		return FALSE;
	}
	return TRUE;
}

/**
 * as_validator_check_web_url:
 *
 * Check if an URL is valid and is reachable, creating a new
 * issue tag of value @tag in case of errors.
 */
static gboolean
as_validator_check_web_url (AsValidator *validator,
			    xmlNode *node,
			    const gchar *url,
			    const gchar *tag)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_autoptr(GError) tmp_error = NULL;

	if (g_str_has_prefix (url, "ftp:")) {
		/* we can't check FTP URLs here, and those shouldn't generally be used in AppStream */
		as_validator_add_issue (validator, node, "url-uses-ftp", "%s", url);
		return FALSE;
	}

	if (!as_curl_is_url (url)) {
		as_validator_add_issue (validator,
					node,
					tag,
					"%s - %s",
					url,
					_("URL format is invalid."));
		return FALSE;
	}

	/* do nothing and assume the URL exists if we shouldn't check URLs */
	if (!priv->check_urls)
		return TRUE;

	g_debug ("Checking URL availability: %s", url);

	/* try to download first few bytes of the file, get error if that fails */
	if (!as_curl_check_url_exists (priv->acurl, url, &tmp_error)) {
		as_validator_add_issue (validator, node, tag, "%s - %s", url, tmp_error->message);
		return FALSE;
	}

	/* if we we din't get a zero-length file, we just assume everything is fine here */
	return TRUE;
}

/**
 * as_validator_clear_release_data:
 * @validator: a #AsValidator instance.
 *
 * Clear all release information that was explicitly added to the
 * validation process.
 *
 * Since: 0.16.0
 */
void
as_validator_clear_release_data (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_ptr_array_set_size (priv->release_data, 0);
}

/**
 * as_validator_add_release_bytes:
 * @validator: a #AsValidator instance.
 * @release_fname: File basename of the release metadata file to add.
 * @release_metadata: Data of the release metadata file.
 * @error: a #GError or %NULL
 *
 * Add release metadata explicitly from bytes.
 *
 * Since: 0.16.0
 */
gboolean
as_validator_add_release_bytes (AsValidator *validator,
				const gchar *release_fname,
				GBytes *release_metadata,
				GError **error)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);

	/* sanity check */
	if (!g_str_has_suffix (release_fname, ".releases.xml") &&
	    !g_str_has_suffix (release_fname, ".releases.xml.in")) {
		g_set_error (error,
			     AS_VALIDATOR_ERROR,
			     AS_VALIDATOR_ERROR_INVALID_FILENAME,
			     _("The release metadata file '%s' is named incorrectly."),
			       release_fname);
		return FALSE;
	}
	if (g_strstr_len (release_fname, -1, "/") != NULL) {
		g_set_error (
		    error,
		    AS_VALIDATOR_ERROR,
		    AS_VALIDATOR_ERROR_INVALID_FILENAME,
		    "Expected a basename for release file '%s', but got a full path instead.",
		    release_fname);
		return FALSE;
	}

	g_ptr_array_add (priv->release_data,
			 as_release_data_pair_new (release_fname, release_metadata));
	return TRUE;
}

/**
 * as_validator_add_release_file:
 * @validator: a #AsValidator instance.
 * @release_file: Release metadata file to add.
 * @error: a #GError or %NULL
 *
 * Add a release metadata file to the validation process.
 *
 * Since: 0.16.0
 */
gboolean
as_validator_add_release_file (AsValidator *validator, GFile *release_file, GError **error)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_autoptr(GFileInputStream) input_stream = NULL;
	g_autoptr(GByteArray) byte_array = NULL;
	g_autoptr(GBytes) bytes = NULL;
	g_autofree gchar *basename = NULL;
	gsize bytes_read;

	basename = g_file_get_basename (release_file);
	if (!g_str_has_suffix (basename, ".releases.xml") &&
	    !g_str_has_suffix (basename, ".releases.xml.in")) {
		g_set_error (error,
			     AS_VALIDATOR_ERROR,
			     AS_VALIDATOR_ERROR_INVALID_FILENAME,
			     _("The release metadata file '%s' is named incorrectly."), basename);
		return FALSE;
	}

	input_stream = g_file_read (release_file, NULL, error);
	if (input_stream == NULL)
		return FALSE;

	byte_array = g_byte_array_new ();
	do {
		guint8 buffer[1024];
		if (!g_input_stream_read_all (G_INPUT_STREAM (input_stream),
					      buffer,
					      sizeof (buffer),
					      &bytes_read,
					      NULL,
					      error))
			return FALSE;

		if (bytes_read > 0)
			g_byte_array_append (byte_array, buffer, bytes_read);
	} while (bytes_read > 0);

	bytes = g_byte_array_free_to_bytes (g_steal_pointer (&byte_array));
	g_ptr_array_add (priv->release_data, as_release_data_pair_new (basename, bytes));
	return TRUE;
}

/**
 * as_validator_get_allow_net:
 * @validator: a #AsValidator instance.
 *
 * Returns: %TRUE in case we allow network access.
 */
gboolean
as_validator_get_allow_net (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	return priv->check_urls;
}

/**
 * as_validator_set_allow_net:
 * @validator: a #AsValidator instance.
 * @value: %TRUE if remote URLs should be checked for availability.
 *
 * If set to %TRUE, the validator will be allowed to connect
 * to the internet to e.g. check URLs for validity.
 */
void
as_validator_set_allow_net (AsValidator *validator, gboolean value)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	priv->check_urls = value;

	/* initialize networking, in case URLs should be checked */
	as_validator_setup_networking (validator);
}

/**
 * as_validator_get_strict:
 * @validator: a #AsValidator instance.
 *
 * Returns: %TRUE in case we are in strict mode and consider any issues as fatal.
 *
 * Since: 0.15.4
 */
gboolean
as_validator_get_strict (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	return priv->strict;
}

/**
 * as_validator_set_strict:
 * @validator: a #AsValidator instance.
 * @is_strict: %TRUE to enable strict mode.
 *
 * Enable or disable strict mode. In strict mode, any found issue will result
 * in a failed validation (except for issues of "pedantic" severity).
 * Otherwise, only a "warning" or "error" will cause the validation to fail.
 *
 * Since: 0.15.4
 */
void
as_validator_set_strict (AsValidator *validator, gboolean is_strict)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	priv->strict = is_strict;
}

/**
 * as_validator_add_override:
 * @validator: a #AsValidator instance.
 * @tag: the issue tag to override, e.g. "release-time-missing"
 * @severity_override: the new severity for the tag.
 * @error: a #GError or %NULL
 *
 * Override the severity of a selected tag. For most tags, the severity
 * can not be lowered to a value that makes a validation
 * that would otherwise fail pass (so e.g. an ERROR can not become an INFO).
 * Some tags are explicitly allowed to have their severity lowered to enable
 * validation of e.g. incomplete metadata during development.
 * Increasing the severity of any tag is always allowed.
 *
 * Since: 0.15.4
 */
gboolean
as_validator_add_override (AsValidator *validator,
			   const gchar *tag,
			   AsIssueSeverity severity_override,
			   GError **error)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	AsValidatorIssueTag *tag_data = NULL;
	AsIssueSeverity real_severity;

	/* we only allow some tags to be downgraded in order to keep validation pass/fail consistent
	 * and prevent people from ignoring truly fatal errors instead of fixing them */
	const gchar *demotion_allowlist[] = {
		/* needed for some in-development metainfo which doesn't have a release time yet */
		"release-time-missing",
		/* allowed for apps which intentionally *must* keep the old ID for some reason */
		"cid-desktopapp-is-not-rdns",
		/* in case the empty tag was actually intended */
		"tag-empty",
		/* allow GNOME to validate metadata using its new versioning scheme (until a better solution is found) */
		"releases-not-in-order",
		/* very rarely (e.g. for apps like Wine) not having a launchable is actually okay */
		"desktop-app-launchable-missing",
		/* allowed for a while, as part of the deprecation phase */
		"developer-name-tag-deprecated",
		/* temporarily allowed to ease transition a bit */
		"developer-id-missing",
		/* allow in case a component really doesn't have an active homepage */
		"url-homepage-missing",
		NULL
	};

	/* sanity checks */
	if (severity_override == AS_ISSUE_SEVERITY_UNKNOWN ||
	    severity_override >= AS_ISSUE_SEVERITY_LAST) {
		g_set_error (
		    error,
		    AS_VALIDATOR_ERROR,
		    AS_VALIDATOR_ERROR_INVALID_OVERRIDE,
		    /* TRANSLATORS: The user tried to set an invalid severity for a validator issue tag */
		    _("The new issue severity for tag '%s' is invalid."), tag);
		return FALSE;
	}

	tag_data = g_hash_table_lookup (priv->issue_tags, tag);
	if (tag_data == NULL) {
		g_set_error (
		    error,
		    AS_VALIDATOR_ERROR,
		    AS_VALIDATOR_ERROR_INVALID_OVERRIDE,
		    /* TRANSLATORS: The user tried to override a validator issue tag that we don't know */
		    _("The issue tag '%s' is not recognized."), tag);
		return FALSE;
	}

	real_severity = tag_data->severity;

	if (real_severity == AS_ISSUE_SEVERITY_ERROR ||
	    real_severity == AS_ISSUE_SEVERITY_WARNING) {
		if (severity_override != AS_ISSUE_SEVERITY_ERROR &&
		    severity_override != AS_ISSUE_SEVERITY_WARNING) {
			/* check if we can downgrade the severity of this issue */
			gboolean severity_downgrade_allowed = FALSE;

			for (guint i = 0; demotion_allowlist[i] != NULL; i++) {
				if (g_strcmp0 (demotion_allowlist[i], tag) == 0) {
					severity_downgrade_allowed = TRUE;
					break;
				}
			}

			if (!severity_downgrade_allowed) {
				g_set_error (
				    error,
				    AS_VALIDATOR_ERROR,
				    AS_VALIDATOR_ERROR_INVALID_OVERRIDE,
				    /* TRANSLATORS: The user tried to override an issue tag and make it non-fatal, even though the tag is not
						   whitelisted for that. */
				    _("It is not allowed to downgrade the severity of tag '%s' to one that allows validation to pass."),
				      tag);
				return FALSE;
			}
		}
	}

	/* actually apply the override, if we are here everything was fine with it */
	g_debug ("Overriding severity of validator issue tag: %s == %s",
		 tag,
		 as_issue_severity_to_string (severity_override));
	tag_data->severity = severity_override;

	return TRUE;
}

/**
 * as_validator_check_type_property:
 **/
static gchar *
as_validator_check_type_property (AsValidator *validator, AsComponent *cpt, xmlNode *node)
{
	g_autofree gchar *prop = NULL;
	g_autofree gchar *content = NULL;

	prop = as_xml_get_prop_value (node, "type");
	content = as_xml_get_node_value_raw (node);
	if (prop == NULL) {
		as_validator_add_issue (validator,
					node,
					"type-property-required",
					"%s (%s)",
					(const gchar *) node->name,
					content);
	}

	return g_steal_pointer (&prop);
}

/**
 * as_validator_check_content:
 **/
static void
as_validator_check_content_empty (AsValidator *validator, xmlNode *node, const gchar *tag_path)
{
	g_autofree gchar *node_content = NULL;

	node_content = as_xml_get_node_value (node);
	if (!as_is_empty (node_content))
		return;

	as_validator_add_issue (validator, node, "tag-empty", "%s", tag_path);
}

/**
 * as_validate_has_hyperlink:
 *
 * Check if @text contains a hyperlink.
 */
static gboolean
as_validate_has_hyperlink (const gchar *text)
{
	gchar *tmp;
	if (text == NULL)
		return FALSE;

	tmp = g_strstr_len (text, -1, "https://");
	if ((tmp != NULL) && (!g_str_has_prefix (tmp + 8, " ")))
		return TRUE;

	tmp = g_strstr_len (text, -1, "http://");
	if ((tmp != NULL) && (!g_str_has_prefix (tmp + 7, " ")))
		return TRUE;

	tmp = g_strstr_len (text, -1, "ftp://");
	if ((tmp != NULL) && (!g_str_has_prefix (tmp + 6, " ")))
		return TRUE;

	return FALSE;
}

/**
 * as_validate_is_url:
 *
 * Check if @str is an URL.
 */
static gboolean
as_validate_is_url (const gchar *str)
{
	if (str == NULL)
		return FALSE;
	if (g_str_has_prefix (str, "http://"))
		return TRUE;
	if (g_str_has_prefix (str, "https://"))
		return TRUE;
	if (g_str_has_prefix (str, "ftp://"))
		return TRUE;
	return FALSE;
}

/**
 * as_validate_is_secure_url:
 *
 * Check if @str is a secure (HTTPS) URL.
 */
static gboolean
as_validate_is_secure_url (const gchar *str)
{
	if (g_str_has_prefix (str, "https://"))
		return TRUE;
	return FALSE;
}

/**
 * as_validator_ensure_node_no_text:
 *
 * Check that the given node has no text content.
 **/
static void
as_validator_ensure_node_no_text (AsValidator *validator, xmlNode *node)
{
	if (node == NULL)
		return;
	if (xmlNodeIsText (node) || xmlNodeIsText (node->children))
		as_validator_add_issue (validator,
					node,
					"tag-invalid-text-content",
					"%s",
					(const gchar *) node->name);
}

/**
 * as_validator_check_children_quick:
 **/
static void
as_validator_check_children_quick (AsValidator *validator,
				   xmlNode *node,
				   const gchar *allowed_tagname,
				   gboolean allow_empty)
{
	as_validator_ensure_node_no_text (validator, node);
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if (g_strcmp0 (node_name, allowed_tagname) == 0) {
			g_autofree gchar *tag_path = NULL;
			tag_path = g_strdup_printf ("%s/%s", (const gchar *) node->name, node_name);
			if (!allow_empty)
				as_validator_check_content_empty (validator, iter, tag_path);
		} else {
			as_validator_add_issue (
			    validator,
			    node,
			    "invalid-child-tag-name",
			    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
			    _("Found: %s - Allowed: %s"), node_name, allowed_tagname);
		}
	}
}

static void as_validator_check_nolocalized (AsValidator *validator,
					    xmlNode *node,
					    const gchar *tag,
					    const gchar *format,
					    ...) G_GNUC_PRINTF (4, 5);

/**
 * as_validator_check_nolocalized:
 **/
static void
as_validator_check_nolocalized (AsValidator *validator,
				xmlNode *node,
				const gchar *tag,
				const gchar *format,
				...)
{
	va_list args;
	g_autofree gchar *message = NULL;
	g_autofree gchar *lang = NULL;

	lang = as_xml_get_prop_value (node, "lang");
	if (lang == NULL)
		return;

	va_start (args, format);
	message = g_strdup_vprintf (format, args);
	va_end (args);

	as_validator_add_issue (validator, node, tag, "%s", message);
}

/**
 * as_validator_first_word_capitalized:
 */
static gboolean
as_validator_first_word_capitalized (AsValidator *validator,
				     const gchar *text,
				     gboolean allow_punct)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_autofree gchar *first_word = NULL;
	gchar *tmp;

	if (text == NULL || text[0] == '\0')
		return TRUE;

	/* text starts with a number, that's fine */
	if (g_ascii_isdigit (text[0]))
		return TRUE;

	/* allow punctuation in some cases */
	if (allow_punct && g_ascii_ispunct (text[0]))
		return TRUE;

	/* get the first word */
	first_word = g_strdup (text);
	tmp = g_strstr_len (first_word, -1, " ");
	if (tmp != NULL)
		*tmp = '\0';

	/* we accept a capitalization anywhere in the first word */
	for (guint i = 0; first_word[i] != '\0'; i++) {
		if (first_word[i] >= 'A' && first_word[i] <= 'Z')
			return TRUE;
	}

	/* if the first word is the project's name, we accept whatever spelling they prefer */
	if (g_strcmp0 (first_word, as_component_get_name (priv->current_cpt)) == 0)
		return TRUE;

	return FALSE;
}

/**
 * as_validator_check_description_paragraph:
 **/
static void
as_validator_check_description_paragraph (AsValidator *validator, xmlNode *node)
{
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if ((g_strcmp0 (node_name, "em") == 0) || (g_strcmp0 (node_name, "code") == 0))
			continue;

		as_validator_add_issue (validator,
					iter,
					"description-para-markup-invalid",
					"%s",
					node_name);
	}
}

/**
 * as_validator_check_description_enumeration:
 **/
static void
as_validator_check_description_enumeration (AsValidator *validator,
					    AsFormatStyle mode,
					    xmlNode *node)
{
	as_validator_check_nolocalized (validator,
					node,
					"description-enum-group-translated",
					"description/%s",
					(const gchar *) node->name);

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if (as_str_equal0 (node_name, "li")) {
			g_autofree gchar *tag_path = NULL;

			tag_path = g_strdup_printf ("%s/%s", (const gchar *) node->name, node_name);
			as_validator_check_content_empty (validator, iter, tag_path);
			as_validator_check_description_paragraph (validator, iter);

			if (mode == AS_FORMAT_STYLE_CATALOG) {
				as_validator_check_nolocalized (
				    validator,
				    iter,
				    "catalog-localized-description-section",
				    "description/%s/li",
				    (const gchar *) node->name);
			}
		} else {
			as_validator_add_issue (validator,
						iter,
						"description-enum-item-invalid",
						"%s",
						node_name);
		}
	}
}

/**
 * as_validator_check_description_tag:
 **/
static void
as_validator_check_description_tag (AsValidator *validator,
				    xmlNode *node,
				    AsFormatStyle mode,
				    gboolean main_description)
{
	gboolean first_paragraph = TRUE;
	gboolean is_localized = FALSE;
	gboolean has_node_content = FALSE;

	if (mode == AS_FORMAT_STYLE_METAINFO) {
		as_validator_check_nolocalized (validator,
						node,
						"metainfo-localized-description-tag",
						"%s",
						node->name);
	} else {
		g_autofree gchar *lang = as_xml_get_prop_value (node, "lang");
		is_localized = lang != NULL;
	}

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name = (gchar *) iter->name;
		g_autofree gchar *node_content = as_xml_get_node_value_raw (iter);

		if (iter->type == XML_TEXT_NODE)
			as_validator_add_issue (validator, iter, "description-spurious-text", NULL);

		/* discard spaces & any non-node elements */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		has_node_content = TRUE;

		if ((g_strcmp0 (node_name, "ul") != 0) && (g_strcmp0 (node_name, "ol") != 0)) {
			as_validator_check_content_empty (validator, iter, node_name);
		}

		if (g_strcmp0 (node_name, "p") == 0) {
			g_autofree gchar *p_content = as_xml_get_node_value (iter);

			if (mode == AS_FORMAT_STYLE_CATALOG) {
				as_validator_check_nolocalized (
				    validator,
				    iter,
				    "catalog-localized-description-section",
				    "description/p");
			}
			if (main_description) {
				if (node_content != NULL)
					g_strstrip (node_content);
				if ((first_paragraph) && (strlen (node_content) < 80)) {
					as_validator_add_issue (validator,
								iter,
								"description-first-para-too-short",
								"%s",
								node_content);
				}
			}
			first_paragraph = FALSE;

			/* in metainfo mode, we need to check every node for localization,
			 * otherwise we just honor the is_localized var */
			if (mode == AS_FORMAT_STYLE_METAINFO) {
				g_autofree gchar *lang = as_xml_get_prop_value (iter, "lang");
				is_localized = lang != NULL;
			}

			/* validate spelling */
			if (!is_localized &&
			    !as_validator_first_word_capitalized (validator,
								  p_content,
								  !main_description))
				as_validator_add_issue (validator,
							node,
							"description-first-word-not-capitalized",
							NULL);

			/* validate common stuff */
			as_validator_check_description_paragraph (validator, iter);
		} else if (g_strcmp0 (node_name, "ul") == 0) {
			as_validator_check_description_enumeration (validator, mode, iter);

		} else if (g_strcmp0 (node_name, "ol") == 0) {
			as_validator_check_description_enumeration (validator, mode, iter);

		} else {
			as_validator_add_issue (validator,
						iter,
						"description-markup-invalid",
						"%s",
						node_name);
		}

		if (as_validate_has_hyperlink (node_content)) {
			as_validator_add_issue (validator,
						iter,
						"description-has-plaintext-url",
						"%s",
						node_name);
		}
	}

	/* If we have unexpected content that isn't nodes, we warn. Fully empty tags are allowed */
	if (node->children != NULL && !has_node_content)
		as_validator_add_issue (validator, node, "description-no-valid-content", NULL);
}

/**
 * as_validator_check_appear_once:
 **/
static void
as_validator_check_appear_once (AsValidator *validator,
				xmlNode *node,
				GHashTable *known_tags,
				gboolean translatable)
{
	g_autofree gchar *lang = NULL;
	gchar *tag_id;
	const gchar *node_name;

	/* generate tag-id to make a unique identifier for localized and unlocalized tags */
	node_name = (const gchar *) node->name;
	lang = as_xml_get_prop_value (node, "lang");
	if (lang == NULL) {
		tag_id = g_strdup (node_name);
	} else {
		if (translatable) {
			tag_id = g_strdup_printf ("%s (lang=%s)", node_name, lang);
		} else {
			tag_id = g_strdup (node_name);
			as_validator_add_issue (validator,
						node,
						"tag-not-translatable",
						"%s",
						node_name);
		}
	}

	if (g_hash_table_contains (known_tags, tag_id)) {
		as_validator_add_issue (validator, node, "tag-duplicated", "%s", tag_id);
	}

	/* add to list of known tags (takes ownership/frees tag_id) */
	g_hash_table_add (known_tags, tag_id);
}

static gboolean
as_validate_string_lowercase (const gchar *str)
{
	for (guint i = 0; str[i] != '\0'; i++) {
		if (g_ascii_isalpha (str[i]) && g_ascii_isupper (str[i]))
			return FALSE;
	}
	return TRUE;
}

/**
 * as_validator_validate_component_id:
 *
 * Validate the component-ID.
 */
static void
as_validator_validate_component_id (AsValidator *validator, xmlNode *idnode, AsComponent *cpt)
{
	g_auto(GStrv) cid_parts = NULL;
	guint cid_parts_n;
	gboolean hyphen_found = FALSE;
	g_autofree gchar *cid = as_xml_get_node_value_raw (idnode);
	g_return_if_fail (cid != NULL);

	if (cid[0] != '\0' && g_ascii_ispunct (cid[0]))
		as_validator_add_issue (validator, idnode, "cid-punctuation-prefix", "%s", cid);

	/* There is no point in validating component IDs for merge components, as those must follow
	 * their counterparts, so they may need to replicate any error present there. We therefore
	 * skip this entire check if we have a merge component. */
	if (as_component_get_merge_kind (cpt) != AS_MERGE_KIND_NONE)
		return;

	cid_parts = g_strsplit (cid, ".", -1);
	cid_parts_n = g_strv_length (cid_parts);
	if (cid_parts_n < 3) {
		if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) {
			/* since the ID and .desktop-file-id are tied together, we can't make this an error for desktop apps */
			as_validator_add_issue (validator,
						idnode,
						"cid-desktopapp-is-not-rdns",
						"%s",
						cid);

		} else {
			/* anything which isn't a .desktop app must follow the schema though */
			as_validator_add_issue (validator, idnode, "cid-is-not-rdns", "%s", cid);
		}
	} else {
		/* some people just add random dots to their ID - check if we have an actual known TLD as first part, to be more certain that this is a reverse domain name
		 * (this issue happens quite often with old .desktop files) */
		if (!as_utils_is_tld (cid_parts[0])) {
			as_validator_add_issue (validator, idnode, "cid-maybe-not-rdns", "%s", cid);
		}

		/* ensure first parts of the rDNS ID are always lowercase */
		if (!as_validate_string_lowercase (cid_parts[0]))
			as_validator_add_issue (validator,
						idnode,
						"cid-domain-not-lowercase",
						"%s",
						cid);
		if (!as_validate_string_lowercase (cid_parts[1]))
			as_validator_add_issue (validator,
						idnode,
						"cid-domain-not-lowercase",
						"%s",
						cid);
	}

	/* validate characters in AppStream ID */
	for (guint i = 0; cid[i] != '\0'; i++) {
		/* check if we have a printable, alphanumeric ASCII character or a dot, hyphen or underscore */
		if ((!g_ascii_isalnum (cid[i])) && (cid[i] != '.') && (cid[i] != '-') &&
		    (cid[i] != '_')) {
			g_autofree gchar *c = NULL;
			c = g_utf8_substring (cid, i, i + 1);
			as_validator_add_issue (validator,
						idnode,
						"cid-invalid-character",
						"%s: '%s'",
						cid,
						c);
		}

		if (g_ascii_isalpha (cid[i]) && g_ascii_isupper (cid[i]))
			as_validator_add_issue (validator,
						idnode,
						"cid-contains-uppercase-letter",
						"%s",
						cid);
	}

	/* check if any segment starts with a number */
	for (guint i = 0; cid_parts[i] != NULL; i++) {
		gboolean is_last_segment = cid_parts[i + 1] == NULL;

		if (g_ascii_isdigit (cid_parts[i][0])) {
			as_validator_add_issue (validator,
						idnode,
						"cid-has-number-prefix",
						"%s: %s → _%s",
						cid,
						cid_parts[i],
						cid_parts[i]);
		}

		/* check for hyphens in anything but the last segment of an ID, and only if we have
		 * a rDNS-style ID */
		if (is_last_segment || hyphen_found || cid_parts_n < 3)
			continue;
		for (guint j = 0; cid_parts[i][j] != '\0'; j++) {
			if (cid_parts[i][j] == '-') {
				hyphen_found = TRUE;
				/* a hyphen in the ID is bad news, because we can't use the ID on DBus and it also clashes with other naming schemes */
				as_validator_add_issue (validator,
							idnode,
							"cid-rdns-contains-hyphen",
							"%s",
							cid);
				break;
			}
		}
	}

	/* project-group specific constraints on the ID */
	if ((g_strcmp0 (as_component_get_project_group (cpt), "Freedesktop") == 0) ||
	    (g_strcmp0 (as_component_get_project_group (cpt), "FreeDesktop") == 0)) {
		if (!g_str_has_prefix (cid, "org.freedesktop."))
			as_validator_add_issue (validator,
						idnode,
						"cid-missing-affiliation-freedesktop",
						"%s",
						cid);
	} else if (g_strcmp0 (as_component_get_project_group (cpt), "KDE") == 0) {
		if (!g_str_has_prefix (cid, "org.kde."))
			as_validator_add_issue (validator,
						idnode,
						"cid-missing-affiliation-kde",
						"%s",
						cid);
	} else if (g_strcmp0 (as_component_get_project_group (cpt), "GNOME") == 0) {
		if (!g_str_has_prefix (cid, "org.gnome."))
			as_validator_add_issue (validator,
						idnode,
						"cid-missing-affiliation-gnome",
						"%s",
						cid);
	}
}

/**
 * as_validator_validate_project_license:
 */
static void
as_validator_validate_project_license (AsValidator *validator, xmlNode *license_node)
{
	guint i;
	g_auto(GStrv) licenses = NULL;
	g_autofree gchar *license_id = as_xml_get_node_value_raw (license_node);

	licenses = as_spdx_license_tokenize (license_id);
	if (licenses == NULL) {
		as_validator_add_issue (validator,
					license_node,
					"spdx-expression-invalid",
					"%s",
					license_id);
		return;
	}
	for (i = 0; licenses[i] != NULL; i++) {
		g_strstrip (licenses[i]);
		if (g_strcmp0 (licenses[i], "&") == 0 || g_strcmp0 (licenses[i], "|") == 0 ||
		    g_strcmp0 (licenses[i], "+") == 0 || g_strcmp0 (licenses[i], "(") == 0 ||
		    g_strcmp0 (licenses[i], ")") == 0 || g_strcmp0 (licenses[i], "^") == 0)
			continue;

		if (licenses[i][0] != '@') {
			if (!as_is_spdx_license_id (licenses[i] + 1) &&
			    !as_is_spdx_license_exception_id (licenses[i] + 1)) {
				as_validator_add_issue (validator,
							license_node,
							"spdx-license-unknown",
							"%s",
							licenses[i]);
				return;
			}
		}
	}
}

/**
 * as_validator_validate_metadata_license:
 */
static void
as_validator_validate_metadata_license (AsValidator *validator, xmlNode *license_node)
{
	gboolean requires_all_tokens = TRUE;
	guint license_bad_cnt = 0;
	guint license_good_cnt = 0;
	g_auto(GStrv) tokens = NULL;
	g_autofree gchar *license_expression = as_xml_get_node_value_raw (license_node);

	tokens = as_spdx_license_tokenize (license_expression);
	if (tokens == NULL) {
		as_validator_add_issue (validator,
					license_node,
					"spdx-expression-invalid",
					"%s",
					license_expression);
		return;
	}

	/* this is too complicated to process */
	for (guint i = 0; tokens[i] != NULL; i++) {
		if (g_strcmp0 (tokens[i], "(") == 0 || g_strcmp0 (tokens[i], ")") == 0) {
			as_validator_add_issue (validator,
						license_node,
						"metadata-license-too-complex",
						"%s",
						license_expression);
			return;
		}
	}

	/* this is a simple expression parser and can be easily tricked */
	for (guint i = 0; tokens[i] != NULL; i++) {
		if (g_strcmp0 (tokens[i], "+") == 0)
			continue;
		if (g_strcmp0 (tokens[i], "|") == 0) {
			requires_all_tokens = FALSE;
			continue;
		}
		if (g_strcmp0 (tokens[i], "&") == 0) {
			requires_all_tokens = TRUE;
			continue;
		}
		if (as_license_is_metadata_license_id (tokens[i])) {
			license_good_cnt++;
		} else {
			license_bad_cnt++;
		}
	}

	/* any valid token makes this valid */
	if (!requires_all_tokens && license_good_cnt > 0)
		return;

	/* all tokens are required to be valid */
	if (requires_all_tokens && license_bad_cnt == 0)
		return;

	/* looks like the license was bad */
	as_validator_add_issue (validator,
				license_node,
				"metadata-license-invalid",
				"%s",
				license_expression);
}

/**
 * as_validator_validate_update_contact:
 */
static void
as_validator_validate_update_contact (AsValidator *validator, xmlNode *uc_node)
{
	g_autofree gchar *text = as_xml_get_node_value_raw (uc_node);

	if ((g_strstr_len (text, -1, "@") == NULL) && (g_strstr_len (text, -1, "_at_") == NULL) &&
	    (g_strstr_len (text, -1, "_AT_") == NULL)) {
		if (g_strstr_len (text, -1, ".") == NULL) {
			as_validator_add_issue (validator,
						uc_node,
						"update-contact-no-mail",
						"%s",
						text);
		}
	}
}

/**
 * as_id_string_valid:
 */
static gboolean
as_id_string_valid (const gchar *str, gboolean allow_uppercase)
{
	if (str == NULL)
		return FALSE;

	for (guint i = 0; str[i] != '\0'; i++) {
		/* check if we have a printable, alphanumeric ASCII character or a dot, hyphen or underscore */
		if ((!g_ascii_isalnum (str[i])) && (str[i] != '.') && (str[i] != '-') &&
		    (str[i] != '_')) {
			/* found a character that is not whitelisted */
			return FALSE;
		}

		if (!allow_uppercase && g_ascii_isalpha (str[i]) && g_ascii_isupper (str[i]))
			return FALSE;
	}

	return TRUE;
}

/**
 * as_validator_check_tags:
 **/
static void
as_validator_check_tags (AsValidator *validator, xmlNode *node)
{
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		g_autofree gchar *ns = NULL;
		g_autofree gchar *value = NULL;

		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if (g_strcmp0 (node_name, "tag") != 0) {
			as_validator_add_issue (
			    validator,
			    node,
			    "invalid-child-tag-name",
			    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
			    _("Found: %s - Allowed: %s"), (const gchar *) node->name, "tag");
			continue;
		}
		as_validator_check_content_empty (validator, iter, "tags/tag");

		ns = as_xml_get_prop_value (iter, "namespace");
		if (ns == NULL) {
			as_validator_add_issue (validator, iter, "usertag-missing-namespace", NULL);
			continue;
		}

		if (!as_id_string_valid (ns, FALSE)) {
			as_validator_add_issue (validator, iter, "usertag-invalid", "%s", ns);
			continue;
		}

		value = as_xml_get_node_value (iter);
		if (!as_id_string_valid (value, FALSE))
			as_validator_add_issue (validator, iter, "usertag-invalid", "%s", value);
	}
}

/**
 * as_validator_check_references:
 **/
static void
as_validator_check_references (AsValidator *validator, xmlNode *node)
{
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		g_autofree gchar *value = NULL;
		if (iter->type != XML_ELEMENT_NODE)
			continue;

		value = as_xml_get_node_value (iter);
		if (as_is_empty (value))
			as_validator_add_issue (validator, iter, "reference-value-missing", NULL);

		if (as_str_equal0 (iter->name, "doi")) {
			if (as_validate_has_hyperlink (value) ||
			    g_strstr_len (value, -1, "/") == NULL)
				as_validator_add_issue (validator,
							iter,
							"reference-doi-invalid",
							"%s",
							value);

		} else if (as_str_equal0 (iter->name, "citation_cff")) {
			if (!as_validate_has_hyperlink (value) || g_str_has_suffix (value, ".cff"))
				as_validator_add_issue (validator,
							iter,
							"reference-citation-url-invalid",
							"%s",
							value);
		} else if (as_str_equal0 (iter->name, "registry")) {
			g_autofree gchar *registry_name = as_xml_get_prop_value (iter, "name");
			if (registry_name == NULL)
				as_validator_add_issue (validator,
							iter,
							"reference-registry-name-missing",
							NULL);
			else if (!as_utils_is_reference_registry (registry_name))
				as_validator_add_issue (validator,
							iter,
							"reference-registry-name-unknown",
							"%s",
							registry_name);
		}
	}
}

/**
 * as_validator_check_developer:
 **/
static void
as_validator_check_developer (AsValidator *validator, xmlNode *node)
{
	g_autofree gchar *devid = NULL;
	gboolean developer_name_found = FALSE;

	devid = as_xml_get_prop_value (node, "id");
	if (devid == NULL) {
		as_validator_add_issue (validator, node, "developer-id-missing", NULL);
	} else {
		if (g_strstr_len (devid, -1, "@") == NULL && g_strstr_len (devid, -1, ".") == NULL)
			as_validator_add_issue (validator, node, "developer-id-invalid", NULL);

		for (guint i = 0; devid[i] != '\0'; ++i) {
			if (g_ascii_isalpha (devid[i]) && !g_ascii_islower (devid[i])) {
				as_validator_add_issue (validator,
							node,
							"developer-id-invalid",
							"%s",
							devid);
				break;
			}

			if (!g_ascii_isalnum (devid[i]) && !g_ascii_ispunct (devid[i])) {
				as_validator_add_issue (validator,
							node,
							"developer-id-invalid",
							"%s",
							devid);
				break;
			}
		}
	}

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		if (iter->type != XML_ELEMENT_NODE)
			continue;

		if (as_str_equal0 (iter->name, "name")) {
			g_autofree gchar *content = as_xml_get_node_value (iter);

			developer_name_found = TRUE;
			if (as_validate_has_hyperlink (content))
				as_validator_add_issue (validator,
							iter,
							"developer-name-has-url",
							NULL);
		}
	}

	if (!developer_name_found) {
		as_validator_add_issue (validator, node, "developer-name-missing", NULL);
	}
}

/**
 * as_validator_check_screenshots:
 *
 * Validate a "screenshots" tag.
 **/
static void
as_validator_check_screenshots (AsValidator *validator, xmlNode *node, AsComponent *cpt)
{
	gboolean have_default_screenshot = FALSE;

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		xmlNode *iter2;
		gboolean scale1image_found = FALSE;
		gboolean image_found = FALSE;
		gboolean video_found = FALSE;
		gboolean caption_found = FALSE;
		gboolean is_default_screenshot = FALSE;
		g_autofree gchar *scr_kind_str = NULL;
		g_autofree gchar *scr_env_style = NULL;
		gboolean have_source_image = FALSE;
		g_autoptr(GHashTable) known_source_locale = NULL;

		known_source_locale = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

		if (iter->type != XML_ELEMENT_NODE)
			continue;

		scr_kind_str = as_xml_get_prop_value (iter, "type");
		if (as_screenshot_kind_from_string (scr_kind_str) == AS_SCREENSHOT_KIND_DEFAULT) {
			have_default_screenshot = TRUE;
			is_default_screenshot = TRUE;
		}

		scr_env_style = as_xml_get_prop_value (iter, "environment");
		if (scr_env_style != NULL && !as_utils_is_gui_environment_style (scr_env_style)) {
			as_validator_add_issue (validator,
						iter,
						"screenshot-invalid-env-style",
						"%s",
						scr_env_style);
		}

		if (g_strcmp0 ((const gchar *) iter->name, "screenshot") != 0) {
			as_validator_add_issue (
			    validator,
			    iter,
			    "invalid-child-tag-name",
			    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
			    _("Found: %s - Allowed: %s"), (const gchar *) iter->name, "screenshot");
		}

		for (iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) {
			g_autofree gchar *screenshot_width = NULL;
			g_autofree gchar *screenshot_height = NULL;
			g_autofree gchar *screenshot_scale = NULL;
			const gchar *node_name = (const gchar *) iter2->name;

			if (iter2->type != XML_ELEMENT_NODE)
				continue;

			screenshot_width = as_xml_get_prop_value (iter2, "width");
			if (screenshot_width != NULL &&
			    !as_str_verify_integer (screenshot_width, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter2,
							"screenshot-invalid-width",
							"%s",
							screenshot_width);

			screenshot_height = as_xml_get_prop_value (iter2, "height");
			if (screenshot_height != NULL &&
			    !as_str_verify_integer (screenshot_height, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter2,
							"screenshot-invalid-height",
							"%s",
							screenshot_height);

			screenshot_scale = as_xml_get_prop_value (iter2, "scale");
			if (screenshot_scale != NULL &&
			    !as_str_verify_integer (screenshot_scale, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter2,
							"screenshot-invalid-scale",
							"%s",
							screenshot_scale);

			/* check if we have an @1 scaled image */
			if (screenshot_scale == NULL ||
			    g_ascii_strtoll (screenshot_scale, NULL, 10) == 1)
				scale1image_found = TRUE;

			if (g_strcmp0 (node_name, "image") == 0) {
				AsImageKind image_kind;
				g_autofree gchar *image_url = as_xml_get_node_value (iter2);
				g_autofree gchar *image_kind_str = NULL;

				image_found = TRUE;
				image_kind_str = as_xml_get_prop_value (iter2, "type");
				image_kind = as_image_kind_from_string (image_kind_str);

				if (image_kind == AS_IMAGE_KIND_UNKNOWN)
					as_validator_add_issue (validator,
								iter2,
								"screenshot-image-invalid-type",
								"%s",
								image_kind_str);

				if (image_kind == AS_IMAGE_KIND_THUMBNAIL) {
					if (screenshot_width == NULL)
						as_validator_add_issue (
						    validator,
						    iter2,
						    "screenshot-image-missing-width",
						    NULL);
					if (screenshot_height == NULL)
						as_validator_add_issue (
						    validator,
						    iter2,
						    "screenshot-image-missing-height",
						    NULL);
				}

				if (image_kind == AS_IMAGE_KIND_SOURCE) {
					g_autofree gchar *image_lang = as_xml_get_prop_value (
					    iter2,
					    "lang");

					if (image_lang == NULL) {
						if (have_source_image)
							as_validator_add_issue (
							    validator,
							    iter2,
							    "screenshot-image-source-duplicated",
							    NULL);

						have_source_image = TRUE;
					} else {
						if (g_hash_table_contains (known_source_locale,
									   image_lang))
							as_validator_add_issue (
							    validator,
							    iter2,
							    "screenshot-image-source-duplicated",
							    "%s",
							    image_lang);

						g_hash_table_add (known_source_locale,
								  g_steal_pointer (&image_lang));
					}
				}

				if (!as_validate_is_url (image_url)) {
					as_validator_add_issue (validator,
								iter2,
								"web-url-expected",
								"%s",
								image_url);
					continue;
				}
				if (!as_validate_is_secure_url (image_url)) {
					as_validator_add_issue (validator,
								iter2,
								"screenshot-media-url-not-secure",
								"%s",
								image_url);
				}

				/* check if we can reach the URL */
				as_validator_check_web_url (validator,
							    iter2,
							    image_url,
							    "screenshot-image-not-found");
			} else if (g_strcmp0 (node_name, "video") == 0) {
				g_autofree gchar *codec_str = NULL;
				g_autofree gchar *container_str = NULL;
				g_autofree gchar *video_url_basename = NULL;
				g_autofree gchar *video_url_base_lower = NULL;
				g_autofree gchar *video_url = as_xml_get_node_value (iter2);

				video_found = TRUE;

				/* the default screenshot must not be a video */
				if (is_default_screenshot)
					as_validator_add_issue (validator,
								iter,
								"screenshot-default-contains-video",
								NULL);

				as_validator_check_web_url (validator,
							    iter2,
							    video_url,
							    "screenshot-video-not-found");

				if (!as_validate_is_url (video_url)) {
					as_validator_add_issue (validator,
								iter2,
								"web-url-expected",
								"%s",
								video_url);
					continue;
				}
				if (!as_validate_is_secure_url (video_url)) {
					as_validator_add_issue (validator,
								iter2,
								"screenshot-media-url-not-secure",
								"%s",
								video_url);
				}

				codec_str = as_xml_get_prop_value (iter2, "codec");
				if (codec_str == NULL) {
					as_validator_add_issue (validator,
								iter2,
								"screenshot-video-codec-missing",
								NULL);
				} else {
					AsVideoCodecKind codec = as_video_codec_kind_from_string (
					    codec_str);
					if (codec == AS_VIDEO_CODEC_KIND_UNKNOWN)
						as_validator_add_issue (
						    validator,
						    iter2,
						    "screenshot-video-codec-invalid",
						    "%s",
						    codec_str);
				}

				container_str = as_xml_get_prop_value (iter2, "container");
				if (container_str == NULL) {
					as_validator_add_issue (
					    validator,
					    iter2,
					    "screenshot-video-container-missing",
					    NULL);
				} else {
					AsVideoContainerKind container =
					    as_video_container_kind_from_string (container_str);
					if (container == AS_VIDEO_CONTAINER_KIND_UNKNOWN)
						as_validator_add_issue (
						    validator,
						    iter2,
						    "screenshot-video-container-invalid",
						    "%s",
						    container_str);
				}

				video_url_basename = as_filebasename_from_uri (video_url);
				video_url_base_lower = g_utf8_strdown (video_url_basename, -1);
				if (g_strstr_len (video_url_base_lower, -1, ".") != NULL) {
					if (!g_str_has_suffix (video_url_base_lower, ".mkv") &&
					    !g_str_has_suffix (video_url_base_lower, ".webm")) {
						as_validator_add_issue (
						    validator,
						    iter2,
						    "screenshot-video-file-wrong-container",
						    "%s",
						    video_url_basename);
					}
				}

			} else if (g_strcmp0 (node_name, "caption") == 0) {
				caption_found = TRUE;
			} else {
				as_validator_add_issue (
				    validator,
				    iter2,
				    "invalid-child-tag-name",
				    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
				    _("Found: %s - Allowed: %s"),
				      (const gchar *) iter2->name,
				      "caption; image; video");
			}
		}

		if (!image_found && !video_found) {
			as_validator_add_issue (validator, iter, "screenshot-no-media", NULL);
		} else if (image_found && video_found) {
			as_validator_add_issue (validator,
						iter,
						"screenshot-mixed-images-videos",
						NULL);
		} else if (image_found && !have_source_image) {
			if (g_hash_table_contains (known_source_locale, "en")) {
				as_validator_add_issue (validator,
							iter,
							"screenshot-image-no-source-but-en-locale",
							NULL);

			} else {
				as_validator_add_issue (validator,
							iter,
							"screenshot-image-no-source",
							NULL);
			}
		}

		if (image_found && !scale1image_found)
			as_validator_add_issue (validator,
						iter,
						"screenshot-no-unscaled-image",
						NULL);

		if (!caption_found)
			as_validator_add_issue (validator, iter, "screenshot-no-caption", NULL);
	}

	if (!have_default_screenshot)
		as_validator_add_issue (validator, node, "screenshot-default-missing", NULL);
}

/**
 * as_validator_check_relations:
 **/
static void
as_validator_check_relations (AsValidator *validator,
			      xmlNode *node,
			      AsComponent *cpt,
			      GHashTable *known_entries,
			      AsRelationKind kind)
{
	as_validator_ensure_node_no_text (validator, node);
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		const gchar *rel_dupe_type = NULL;
		g_autofree gchar *content = NULL;
		g_autofree gchar *version = NULL;
		g_autofree gchar *compare_str = NULL;
		g_autofree gchar *rel_item_id = NULL;
		gboolean can_have_version = FALSE;
		gboolean can_have_compare = FALSE;
		AsRelationItemKind item_kind;
		AsRelationCompare compare;

		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;
		content = as_xml_get_node_value (iter);
		as_strstripnl (content);

		item_kind = as_relation_item_kind_from_string (node_name);
		if (item_kind == AS_RELATION_ITEM_KIND_UNKNOWN) {
			as_validator_add_issue (validator,
						iter,
						"relation-invalid-tag",
						"%s",
						node_name);
			continue;
		}

		if (g_strcmp0 (content, "") == 0) {
			/* only firmware relations are permitted to be empty, and only if the component is of type=firmware */
			if ((as_component_get_kind (cpt) != AS_COMPONENT_KIND_FIRMWARE) &&
			    (item_kind != AS_RELATION_ITEM_KIND_FIRMWARE)) {
				as_validator_add_issue (validator,
							iter,
							"relation-item-no-value",
							NULL);
				continue;
			}
		}

		/* check for circular relation */
		if (g_strcmp0 (content, as_component_get_id (cpt)) == 0) {
			as_validator_add_issue (validator,
						iter,
						"circular-component-relation",
						NULL);
		}

		switch (item_kind) {
		case AS_RELATION_ITEM_KIND_MODALIAS:
		case AS_RELATION_ITEM_KIND_CONTROL:
		case AS_RELATION_ITEM_KIND_HARDWARE:
		case AS_RELATION_ITEM_KIND_INTERNET:
			can_have_version = FALSE;
			can_have_compare = FALSE;
			break;
		case AS_RELATION_ITEM_KIND_MEMORY:
		case AS_RELATION_ITEM_KIND_DISPLAY_LENGTH:
			can_have_version = FALSE;
			can_have_compare = TRUE;
			break;
		default:
			can_have_version = TRUE;
			can_have_compare = TRUE;
		}

		version = as_xml_get_prop_value (iter, "version");
		compare_str = as_xml_get_prop_value (iter, "compare");
		compare = as_relation_compare_from_string (compare_str);
		if (version != NULL) {
			if (!can_have_version) {
				as_validator_add_issue (
				    validator,
				    iter,
				    "relation-item-has-version",
				    "%s",
				    as_relation_item_kind_to_string (item_kind));
				continue;
			}

			if (compare_str == NULL) {
				as_validator_add_issue (validator,
							iter,
							"relation-item-missing-compare",
							NULL);
				continue;
			}
		}

		if (can_have_compare && compare == AS_RELATION_COMPARE_UNKNOWN) {
			as_validator_add_issue (validator,
						iter,
						"relation-item-invalid-vercmp",
						"%s",
						compare_str);
		} else if (!can_have_compare && compare_str != NULL) {
			as_validator_add_issue (validator,
						iter,
						"relation-item-has-vercmp",
						"%s",
						compare_str);
		}

		if (kind == AS_RELATION_KIND_REQUIRES) {
			if (item_kind == AS_RELATION_ITEM_KIND_MEMORY)
				as_validator_add_issue (validator,
							iter,
							"relation-memory-in-requires",
							NULL);
			else if (item_kind == AS_RELATION_ITEM_KIND_CONTROL)
				as_validator_add_issue (validator,
							iter,
							"relation-control-in-requires",
							NULL);
		}

		/* check input control names for sanity */
		if (item_kind == AS_RELATION_ITEM_KIND_CONTROL) {
			if (as_control_kind_from_string (content) == AS_CONTROL_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"relation-control-value-invalid",
							"%s",
							content);
		}

		/* check display length for sanity */
		if (item_kind == AS_RELATION_ITEM_KIND_DISPLAY_LENGTH) {
			g_autofree gchar *side_str = NULL;

			if (!as_str_verify_integer (content, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter,
							"relation-display-length-value-invalid",
							"%s",
							content);

			side_str = as_xml_get_prop_value (iter, "side");
			if (as_display_side_kind_from_string (side_str) ==
			    AS_DISPLAY_SIDE_KIND_UNKNOWN)
				as_validator_add_issue (
				    validator,
				    iter,
				    "relation-display-length-side-property-invalid",
				    "%s",
				    side_str);
		}

		/* check hardware for sanity */
		if (item_kind == AS_RELATION_ITEM_KIND_HARDWARE) {
			guint dash_count = 0;
			for (guint i = 0; content[i] != '\0'; i++)
				if (content[i] == '-')
					dash_count++;

			if (g_str_has_prefix (content, "{") || g_str_has_suffix (content, "}"))
				as_validator_add_issue (validator,
							iter,
							"relation-hardware-value-invalid",
							"%s",
							content);
			else if (dash_count != 4)
				as_validator_add_issue (validator,
							iter,
							"relation-hardware-value-invalid",
							"%s",
							content);
		}

		/* check memory for sanity */
		if (item_kind == AS_RELATION_ITEM_KIND_MEMORY)
			if (!as_str_verify_integer (content, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter,
							"relation-memory-value-invalid",
							"%s",
							content);

		/* check internet for sanity */
		if (item_kind == AS_RELATION_ITEM_KIND_INTERNET) {
			g_autofree gchar *bandwidth_str = as_xml_get_prop_value (
			    iter,
			    "bandwidth_mbitps");
			g_autofree gchar *internet_tag_id = g_strdup_printf (
			    "rel::%s/internet",
			    as_relation_kind_to_string (kind));
			if (as_internet_kind_from_string (content) == AS_INTERNET_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"relation-internet-value-invalid",
							"%s",
							content);

			/* check if bandwidth_mbitps does not exists when value is offline-only */
			if (bandwidth_str != NULL &&
			    as_internet_kind_from_string (content) == AS_INTERNET_KIND_OFFLINE_ONLY)
				as_validator_add_issue (validator,
							iter,
							"relation-internet-bandwidth-offline",
							NULL);

			/* check if bandwidth_mbitps is a integer */
			if (bandwidth_str != NULL &&
			    !as_str_verify_integer (bandwidth_str, 1, G_MAXINT64))
				as_validator_add_issue (validator,
							iter,
							"relation-internet-bandwidth-value-invalid",
							"%s",
							bandwidth_str);

			/* the internet item must only appear once per relation kind */
			if (g_hash_table_lookup (known_entries, internet_tag_id) == NULL)
				g_hash_table_insert (known_entries,
						     g_steal_pointer (&internet_tag_id),
						     g_strdup (content));
			else
				as_validator_add_issue (validator,
							iter,
							"tag-duplicated",
							"internet");
		}

		/* check for redefinition */
		rel_item_id = g_strdup_printf ("rel::%s/%s%s%s",
					       node_name,
					       content,
					       compare_str,
					       version);
		rel_dupe_type = g_hash_table_lookup (known_entries, rel_item_id);
		if (rel_dupe_type == NULL) {
			g_hash_table_insert (known_entries,
					     g_steal_pointer (&rel_item_id),
					     g_strdup (as_relation_kind_to_string (kind)));
		} else {
			as_validator_add_issue (validator,
						iter,
						"relation-item-redefined",
						"%s & %s",
						rel_dupe_type,
						as_relation_kind_to_string (kind));
		}
	}
}

/**
 * as_validator_check_provides:
 **/
static void
as_validator_check_provides (AsValidator *validator, xmlNode *node, AsComponent *cpt)
{
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		g_autofree gchar *node_content = NULL;
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;
		node_content = as_xml_get_node_value (iter);
		g_strstrip (node_content);
		if (as_is_empty (node_content)) {
			as_validator_add_issue (validator, iter, "tag-empty", "%s", node_name);
		}

		if (g_strcmp0 (node_name, "library") == 0) {
		} else if (g_strcmp0 (node_name, "binary") == 0) {
		} else if (g_strcmp0 (node_name, "font") == 0) {
		} else if (g_strcmp0 (node_name, "modalias") == 0) {
		} else if (g_strcmp0 (node_name, "firmware") == 0) {
		} else if (g_strcmp0 (node_name, "python3") == 0) {
		} else if (g_strcmp0 (node_name, "dbus") == 0) {
		} else if (g_strcmp0 (node_name, "mediatype") == 0) {
		} else if (g_strcmp0 (node_name, "id") == 0) {
			/* check for circular relation */
			if (g_strcmp0 (node_content, as_component_get_id (cpt)) == 0) {
				as_validator_add_issue (validator,
							iter,
							"circular-component-relation",
							NULL);
			}
		} else {
			as_validator_add_issue (validator,
						iter,
						"unknown-provides-item-type",
						"%s",
						node_name);
		}
	}
}

/**
 * as_validator_validate_iso8601_complete_date:
 */
static void
as_validator_validate_iso8601_complete_date (AsValidator *validator,
					     xmlNode *node,
					     const gchar *date)
{
	g_autoptr(GDateTime) time = as_iso8601_to_datetime (date);
	if (time == NULL)
		as_validator_add_issue (validator, node, "invalid-iso8601-date", "%s", date);
}

/**
 * as_validator_check_platform_triplet:
 **/
static void
as_validator_check_platform_triplet (AsValidator *validator, xmlNode *node, const gchar *triplet)
{
	g_auto(GStrv) parts = g_strsplit (triplet, "-", 3);
	if (g_strv_length (parts) != 3) {
		as_validator_add_issue (validator,
					node,
					"artifact-invalid-platform-triplet",
					"%u != 3",
					g_strv_length (parts));
		return;
	}
	if (!as_utils_is_platform_triplet_arch (parts[0])) {
		as_validator_add_issue (validator,
					node,
					"artifact-invalid-platform-triplet",
					"Architecture invalid: %s",
					parts[0]);
		return;
	}
	if (!as_utils_is_platform_triplet_oskernel (parts[1])) {
		as_validator_add_issue (validator,
					node,
					"artifact-invalid-platform-triplet",
					"OS/Kernel invalid: %s",
					parts[1]);
		return;
	}
	if (!as_utils_is_platform_triplet_osenv (parts[2])) {
		as_validator_add_issue (validator,
					node,
					"artifact-invalid-platform-triplet",
					"OS/Environment invalid: %s",
					parts[2]);
		return;
	}
}

/**
 * as_validator_check_artifact:
 **/
static void
as_validator_check_artifact (AsValidator *validator, xmlNode *node)
{
	gchar *prop;

	/* validate type */
	prop = as_xml_get_prop_value (node, "type");
	if (prop != NULL) {
		if (as_artifact_kind_from_string (prop) == AS_ARTIFACT_KIND_UNKNOWN)
			as_validator_add_issue (validator,
						node,
						"artifact-type-invalid",
						"%s",
						prop);
		g_free (prop);
	}

	/* validate platform */
	prop = as_xml_get_prop_value (node, "platform");
	if (prop != NULL) {
		as_validator_check_platform_triplet (validator, node, prop);
		g_free (prop);
	}

	/* validate bundle type */
	prop = as_xml_get_prop_value (node, "bundle");
	if (prop != NULL) {
		if (as_bundle_kind_from_string (prop) == AS_BUNDLE_KIND_UNKNOWN)
			as_validator_add_issue (validator,
						node,
						"artifact-bundle-type-invalid",
						"%s",
						prop);
		g_free (prop);
	}

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if (g_strcmp0 (node_name, "location") == 0) {
			g_autofree gchar *url = as_xml_get_node_value (iter);
			if (!as_validate_is_url (url)) {
				as_validator_add_issue (validator,
							iter,
							"web-url-expected",
							"%s",
							url);
				continue;
			}
			if (!as_validate_is_secure_url (url))
				as_validator_add_issue (validator,
							iter,
							"url-not-secure",
							"%s",
							url);
			continue;
		} else if (g_strcmp0 (node_name, "checksum") == 0) {
			g_autofree gchar *tmp = as_xml_get_prop_value (iter, "type");
			if (as_checksum_kind_from_string (tmp) == AS_CHECKSUM_KIND_NONE)
				as_validator_add_issue (validator,
							iter,
							"artifact-checksum-type-invalid",
							"%s",
							tmp);
			continue;
		} else if (g_strcmp0 (node_name, "size") == 0) {
			g_autofree gchar *tmp = as_xml_get_prop_value (iter, "type");
			if (as_size_kind_from_string (tmp) == AS_SIZE_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"artifact-size-type-invalid",
							"%s",
							tmp);
			continue;
		} else if (g_strcmp0 (node_name, "filename") == 0) {
			g_autofree gchar *fname = as_xml_get_node_value (iter);
			if (g_path_is_absolute (fname) || g_str_has_prefix (fname, "."))
				as_validator_add_issue (validator,
							iter,
							"artifact-filename-not-basename",
							"%s",
							fname);
			continue;
		} else {
			/* unknown tag */
			as_validator_add_issue (validator, iter, "unknown-tag", "%s", node_name);
		}
	}
}

/**
 * as_validator_check_release_issue:
 **/
static void
as_validator_check_release_issue (AsValidator *validator, xmlNode *node)
{
	gchar *prop;
	AsIssueKind kind = AS_ISSUE_KIND_UNKNOWN;

	/* validate type */
	prop = as_xml_get_prop_value (node, "type");
	kind = as_issue_kind_from_string (prop);
	if (prop != NULL) {
		if (kind == AS_ISSUE_KIND_UNKNOWN)
			as_validator_add_issue (validator,
						node,
						"release-issue-type-invalid",
						"%s",
						prop);
		g_free (prop);
	}

	/* validate url */
	prop = as_xml_get_prop_value (node, "url");
	if (prop != NULL) {
		if (!as_validate_is_url (prop))
			as_validator_add_issue (validator, node, "web-url-expected", "%s", prop);
		else if (!as_validate_is_secure_url (prop))
			as_validator_add_issue (validator, node, "url-not-secure", "%s", prop);
		g_free (prop);
	}

	/* validate value */
	if (kind == AS_ISSUE_KIND_CVE) {
		g_autofree gchar *value = as_xml_get_node_value (node);
		if (!g_str_has_prefix (value, "CVE-"))
			as_validator_add_issue (validator,
						node,
						"release-issue-is-cve-but-no-cve-id",
						"%s",
						value);
	}
}

/**
 * as_validator_check_release:
 **/
static void
as_validator_check_release (AsValidator *validator, xmlNode *node, AsFormatStyle mode)
{
	gchar *prop;
	AsReleaseKind rel_kind = AS_RELEASE_KIND_UNKNOWN;

	/* validate presence of version property */
	prop = as_xml_get_prop_value (node, "version");
	if (prop == NULL)
		as_validator_add_issue (validator, node, "release-version-missing", "version");
	g_free (prop);

	/* validate type */
	prop = as_xml_get_prop_value (node, "type");
	if (prop != NULL) {
		rel_kind = as_release_kind_from_string (prop);
		if (rel_kind == AS_RELEASE_KIND_UNKNOWN)
			as_validator_add_issue (validator,
						node,
						"release-type-invalid",
						"%s",
						prop);
		g_free (prop);
	}

	/* validate date strings */
	prop = as_xml_get_prop_value (node, "date");
	if (prop != NULL) {
		as_validator_validate_iso8601_complete_date (validator, node, prop);
		g_free (prop);
	} else {
		g_autofree gchar *timestamp = as_xml_get_prop_value (node, "timestamp");
		if (timestamp == NULL) {
			/* Neither timestamp, nor date property exists */
			if (rel_kind == AS_RELEASE_KIND_SNAPSHOT)
				as_validator_add_issue (validator,
							node,
							"release-time-missing-for-snapshot",
							"date");
			else
				as_validator_add_issue (validator,
							node,
							"release-time-missing",
							"date");
		} else {
			/* check if the timestamp is both a number and higher than 3000. The 3000 is used to check that it is not a year */
			if (!as_str_verify_integer (timestamp, 3000, G_MAXINT64))
				as_validator_add_issue (validator,
							node,
							"release-timestamp-invalid",
							"%s",
							timestamp);
		}
	}

	prop = as_xml_get_prop_value (node, "date_eol");
	if (prop != NULL) {
		as_validator_validate_iso8601_complete_date (validator, node, prop);
		g_free (prop);
	}

	/* validate urgency */
	prop = as_xml_get_prop_value (node, "urgency");
	if (prop != NULL) {
		if (as_urgency_kind_from_string (prop) == AS_URGENCY_KIND_UNKNOWN)
			as_validator_add_issue (validator,
						node,
						"release-urgency-invalid",
						"%s",
						prop);
		g_free (prop);
	}

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		/* validate description */
		if (g_strcmp0 (node_name, "description") == 0) {
			as_validator_check_description_tag (validator, iter, mode, FALSE);
			continue;
		}

		/* validate url */
		if (g_strcmp0 (node_name, "url") == 0) {
			g_autofree gchar *url = as_xml_get_node_value (iter);
			if (!as_validate_is_url (url)) {
				as_validator_add_issue (validator,
							iter,
							"web-url-expected",
							"%s",
							url);
				continue;
			}
			if (!as_validate_is_secure_url (url))
				as_validator_add_issue (validator,
							iter,
							"url-not-secure",
							"%s",
							url);
			continue;
		}

		/* validate artifacts */
		if (g_strcmp0 (node_name, "artifacts") == 0) {
			for (xmlNode *iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) {
				if (iter2->type != XML_ELEMENT_NODE)
					continue;
				if (g_strcmp0 ((const gchar *) iter2->name, "artifact") != 0) {
					as_validator_add_issue (
					    validator,
					    iter2,
					    "invalid-child-tag-name",
					    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
					    _("Found: %s - Allowed: %s"),
					      (const gchar *) iter2->name,
					      "artifact");
					continue;
				}
				/* validate artifact */
				as_validator_check_artifact (validator, iter2);
			}
			continue;
		}

		/* validate issues */
		if (g_strcmp0 (node_name, "issues") == 0) {
			for (xmlNode *iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) {
				if (iter2->type != XML_ELEMENT_NODE)
					continue;
				if (g_strcmp0 ((const gchar *) iter2->name, "issue") != 0) {
					as_validator_add_issue (
					    validator,
					    iter2,
					    "invalid-child-tag-name",
					    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
					    _("Found: %s - Allowed: %s"),
					      (const gchar *) iter2->name,
					      "issue");
					continue;
				}
				/* validate issue */
				as_validator_check_release_issue (validator, iter2);
			}
			continue;
		}

		if (g_strcmp0 (node_name, "tags") == 0) {
			as_validator_check_tags (validator, iter);
			continue;
		}

		/* checks if the description is put outside a description tag */
		if (as_str_equal0 (node_name, "p") || as_str_equal0 (node_name, "ol") ||
		    as_str_equal0 (node_name, "ul") || as_str_equal0 (node_name, "li")) {
			as_validator_add_issue (validator,
						node,
						"release-description-outside-tag",
						"%s",
						node_name);
			continue;
		}

		/* if we are here, we have an unknown tag */
		as_validator_add_issue (validator, iter, "unknown-tag", "%s", node_name);
	}
}

/**
 * as_validator_find_release_data_for_current:
 *
 * Find release metadata for the current component, if any data was provided.
 */
static AsReleaseDataPair *
as_validator_find_release_data_for_current (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	g_autofree gchar *expected_name = NULL;
	const gchar *cid = as_component_get_id (priv->current_cpt);

	expected_name = g_strconcat (cid, ".releases.xml", NULL);
	for (guint i = 0; i < priv->release_data->len; i++) {
		AsReleaseDataPair *pair = g_ptr_array_index (priv->release_data, i);
		if (g_str_has_prefix (pair->fname, expected_name))
			return pair;
	}

	/* it's not explicitly provided, try to cheat and apply some heuristics */
	if (priv->current_dir != NULL) {
		g_autofree gchar *guessed_path = NULL;
		gchar *contents;
		gsize contents_len;
		g_autoptr(GError) error = NULL;

		guessed_path = g_build_filename (priv->current_dir,
						 "releases",
						 expected_name,
						 NULL);
		g_debug ("Trying to find release metadata in %s", guessed_path);
		if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS)) {
			g_free (guessed_path);
			guessed_path = g_build_filename (priv->current_dir, expected_name, NULL);
			g_debug ("Trying to find release metadata in %s", guessed_path);
			if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS)) {
				g_autofree gchar *tmp = g_strconcat (expected_name, ".in", NULL);
				guessed_path = g_build_filename (priv->current_dir, tmp, NULL);
				g_debug ("Trying to find release metadata in %s", guessed_path);
				if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS))
					g_free (g_steal_pointer (&guessed_path));
			}
		}

		if (guessed_path == NULL)
			return NULL;

		if (g_file_get_contents (guessed_path, &contents, &contents_len, &error)) {
			AsReleaseDataPair *pair;
			g_autofree gchar *basename = NULL;
			g_autoptr(GBytes) bytes = g_bytes_new_take (contents, contents_len);
			basename = g_path_get_basename (guessed_path);
			pair = as_release_data_pair_new (basename, bytes);
			g_ptr_array_add (priv->release_data, pair);
			return pair;
		} else {
			g_autofree gchar *cpt_fname = g_steal_pointer (&priv->current_fname);
			as_validator_set_current_fname (validator, expected_name);

			as_validator_add_issue (validator,
						NULL,
						"file-read-failed",
						"%s",
						error->message);

			/* restore currently analyzed file name */
			as_validator_set_current_fname (validator, cpt_fname);
		}
	}

	return NULL;
}

/**
 * as_validator_check_releases_node:
 **/
static void
as_validator_check_releases_node (AsValidator *validator, xmlNode *node, AsFormatStyle mode)
{
	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;

		if (g_strcmp0 (node_name, "release") != 0) {
			as_validator_add_issue (
			    validator,
			    iter,
			    "invalid-child-tag-name",
			    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted name. */
			    _("Found: %s - Allowed: %s"), (const gchar *) iter->name, "release");
			continue;
		}

		/* validate the individual release */
		as_validator_check_release (validator, iter, mode);
	}
}

/**
 * as_validator_check_external_releases:
 **/
static void
as_validator_check_external_releases (AsValidator *validator,
				      xmlNode *rels_node,
				      GBytes *bytes,
				      const gchar *releases_uri,
				      AsFormatStyle mode)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	const gchar *rel_data = NULL;
	gsize rel_data_len;
	xmlDoc *xdoc;
	xmlNode *xroot;
	g_autofree gchar *rel_basename = NULL;
	g_autoptr(GError) error = NULL;
	g_autofree gchar *cpt_fname = g_steal_pointer (&priv->current_fname);

	if (g_str_has_prefix (releases_uri, "http"))
		rel_basename = g_strdup (releases_uri);
	else
		rel_basename = g_path_get_basename (releases_uri);
	as_validator_set_current_fname (validator, rel_basename);

	rel_data = g_bytes_get_data (bytes, &rel_data_len);
	xdoc = as_xml_parse_document (rel_data,
				      rel_data_len,
				      TRUE, /* pedantic */
				      &error);
	if (xdoc == NULL) {
		as_validator_add_issue (validator,
					rels_node,
					"xml-markup-invalid",
					"%s",
					error->message);
		goto out;
	}

	/* check remote releases */
	xroot = xmlDocGetRootElement (xdoc);
	as_validator_check_releases_node (validator, xroot, mode);
	xmlFreeDoc (xdoc);

out:
	/* restore currently analyzed file name */
	as_validator_set_current_fname (validator, cpt_fname);
}

/**
 * as_validator_check_releases:
 **/
static void
as_validator_check_releases (AsValidator *validator, xmlNode *node, AsFormatStyle mode)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	AsReleaseListKind releases_kind;
	AsReleaseDataPair *rel_pair;
	g_autofree gchar *release_url_prop = NULL;
	g_autofree gchar *releases_kind_str = as_xml_get_prop_value (node, "type");
	releases_kind = as_release_list_kind_from_string (releases_kind_str);

	if (releases_kind == AS_RELEASE_LIST_KIND_UNKNOWN) {
		as_validator_add_issue (validator,
					node,
					"releases-type-invalid",
					"%s",
					releases_kind_str);
	}

	if (releases_kind != AS_RELEASE_LIST_KIND_EXTERNAL) {
		as_validator_check_releases_node (validator, node, mode);
		return;
	}

	/* if we are here, we have external release metadata and need to find and validate it */

	release_url_prop = as_xml_get_prop_value (node, "url");
	if (release_url_prop != NULL) {
		if (!g_str_has_prefix (release_url_prop, "https:"))
			as_validator_add_issue (validator,
						node,
						"releases-url-insecure",
						"%s",
						release_url_prop);

		/* only download & validate the file if network access is allowed */
		if (priv->check_urls) {
			g_autoptr(GBytes) bytes = NULL;
			GError *tmp_error = NULL;

			g_debug ("Downloading release metadata: %s", release_url_prop);
			bytes = as_curl_download_bytes (priv->acurl, release_url_prop, &tmp_error);
			if (bytes == NULL) {
				as_validator_add_issue (validator,
							node,
							"releases-download-failed",
							"%s",
							tmp_error->message);
			} else {
				as_validator_check_external_releases (validator,
								      node,
								      bytes,
								      release_url_prop,
								      mode);
			}
		}
	}

	rel_pair = as_validator_find_release_data_for_current (validator);
	if (rel_pair == NULL) {
		as_validator_add_issue (validator, node, "releases-external-not-found", NULL);
		return;
	}

	as_validator_check_external_releases (validator,
					      node,
					      rel_pair->bytes,
					      rel_pair->fname,
					      mode);
	as_release_list_load_from_bytes (as_component_get_releases_plain (priv->current_cpt),
					 NULL,
					 rel_pair->bytes,
					 NULL);
}

/**
 * as_validator_check_content_rating:
 **/
static void
as_validator_check_content_rating (AsValidator *validator, xmlNode *node)
{
	g_autofree gchar *oars_type = NULL;
	AsOarsVersion oars_version;

	g_autoptr(GHashTable)
		      known_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

	oars_type = as_xml_get_prop_value (node, "type");

	if (as_is_empty (oars_type)) {
		as_validator_add_issue (validator, node, "content-rating-type-missing", NULL);
		return;
	}

	oars_version = as_oars_version_from_string (oars_type);

	if (oars_version == AS_OARS_VERSION_UNKNOWN) {
		as_validator_add_issue (validator,
					node,
					"content-rating-type-invalid",
					"%s",
					oars_type);
		return;
	}

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		AsContentRatingValue content_rating;
		g_autofree gchar *value_data = NULL;
		g_autofree gchar *id = NULL;

		if (iter->type != XML_ELEMENT_NODE)
			continue;

		if (!as_str_equal0 (iter->name, "content_attribute")) {
			as_validator_add_issue (validator,
						iter,
						"content-rating-invalid-tag",
						"%s",
						(gchar *) iter->name);
			continue;
		}

		id = as_xml_get_prop_value (iter, "id");

		if (as_is_empty (id)) {
			as_validator_add_issue (validator,
						iter,
						"content-attribute-id-missing",
						NULL);
			continue;
		}

		if (!as_is_oars_key (id, oars_version)) {
			as_validator_add_issue (validator,
						iter,
						"content-attribute-id-invalid",
						"%s",
						id);
			continue;
		}

		value_data = as_xml_get_node_value (iter);

		if (as_is_empty (value_data)) {
			as_validator_add_issue (validator,
						iter,
						"content-attribute-value-empty",
						NULL);
			continue;
		}

		content_rating = as_content_rating_value_from_string (value_data);
		if (content_rating == AS_CONTENT_RATING_VALUE_UNKNOWN) {
			as_validator_add_issue (validator,
						iter,
						"content-attribute-value-unknown",
						"%s",
						value_data);
			continue;
		}

		if (!as_content_rating_id_is_valid (id, content_rating)) {
			g_autofree gchar *hint_msg = g_strdup_printf ("%s @ %s", id, value_data);
			as_validator_add_issue (validator,
						iter,
						"content-attribute-value-invalid",
						"%s",
						hint_msg);
			continue;
		}

		if (g_hash_table_contains (known_ids, id))
			as_validator_add_issue (validator,
						iter,
						"content-attribute-id-duplicated",
						"%s",
						id);
		else
			g_hash_table_add (known_ids, g_steal_pointer (&id));
	}
}

/**
 * as_validator_check_branding:
 **/
static void
as_validator_check_branding (AsValidator *validator, xmlNode *node)
{
	g_autoptr(GHashTable) known_colors = NULL;
	known_colors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		if (iter->type != XML_ELEMENT_NODE)
			continue;

		if (as_str_equal0 (iter->name, "color")) {
			AsColorKind color_kind;
			AsColorSchemeKind color_scheme;
			g_autofree gchar *color_value = NULL;
			gchar *tmp;
			guint len;

			tmp = as_xml_get_prop_value (iter, "type");
			color_kind = as_color_kind_from_string (tmp);
			if (color_kind == AS_COLOR_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"branding-color-type-invalid",
							"%s",
							tmp ? tmp : "?");
			g_free (g_steal_pointer (&tmp));

			tmp = as_xml_get_prop_value (iter, "scheme_preference");
			color_scheme = as_color_scheme_kind_from_string (tmp);
			if (tmp != NULL && color_scheme == AS_COLOR_SCHEME_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"branding-color-scheme-type-invalid",
							"%s",
							tmp);
			g_free (g_steal_pointer (&tmp));

			/* catch a common typo */
			tmp = as_xml_get_prop_value (iter, "schema_preference");
			if (tmp != NULL)
				as_validator_add_issue (validator,
							iter,
							"branding-color-scheme-wrong-property",
							"%s",
							"schema_preference");
			g_free (g_steal_pointer (&tmp));

			color_value = as_xml_get_node_value (iter);
			len = strlen (color_value);
			if (!g_str_has_prefix (color_value, "#") || (len != 7 && len != 9))
				as_validator_add_issue (validator,
							iter,
							"branding-color-invalid",
							"%s",
							color_value);

			/* check for duplicates */
			tmp = g_strconcat (as_color_kind_to_string (color_kind),
					   "/",
					   as_color_scheme_kind_to_string (color_scheme),
					   NULL);
			if (g_hash_table_contains (known_colors, tmp)) {
				as_validator_add_issue (validator,
							iter,
							"branding-color-duplicated",
							"%s",
							tmp);
				g_free (g_steal_pointer (&tmp));
			} else {
				g_hash_table_add (known_colors, g_steal_pointer (&tmp));
			}

		} else {
			as_validator_add_issue (
			    validator,
			    iter,
			    "invalid-child-tag-name",
			    /* TRANSLATORS: An invalid XML element was found, "Found" refers to the tag name found, "Allowed" to the permitted names. */
			    _("Found: %s - Allowed: %s"), (const gchar *) iter->name, "color");
		}
	}
}

/**
 * as_validator_check_custom:
 **/
static void
as_validator_check_custom (AsValidator *validator, xmlNode *node)
{
	g_autoptr(GHashTable) known_keys = NULL;
	known_keys = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

	for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) {
		g_autofree gchar *value_data = NULL;
		g_autofree gchar *key_name = NULL;

		if (iter->type != XML_ELEMENT_NODE)
			continue;

		if (!as_str_equal0 (iter->name, "value")) {
			as_validator_add_issue (validator,
						iter,
						"custom-invalid-tag",
						"%s",
						(gchar *) iter->name);
			continue;
		}

		key_name = as_xml_get_prop_value (iter, "key");
		if (key_name == NULL) {
			as_validator_add_issue (validator, iter, "custom-key-missing", NULL);
			continue;
		}

		if (g_hash_table_contains (known_keys, key_name))
			as_validator_add_issue (validator,
						iter,
						"custom-key-duplicated",
						"%s",
						key_name);
		else
			g_hash_table_add (known_keys, g_steal_pointer (&key_name));

		value_data = as_xml_get_node_value (iter);
		if (as_is_empty (value_data))
			as_validator_add_issue (validator, iter, "custom-value-empty", NULL);
	}
}

/**
 * as_validator_validate_component_node:
 **/
static AsComponent *
as_validator_validate_component_node (AsValidator *validator, AsContext *ctx, xmlNode *root)
{
	AsComponent *cpt;
	g_autofree gchar *cpttype = NULL;
	g_autoptr(GHashTable) found_tags = NULL;
	g_autoptr(GHashTable) known_relation_items = NULL;
	g_autofree gchar *date_eol_str = NULL;
	guint64 known_url_kinds = 0;
	gboolean is_merge_cpt = FALSE;

	AsFormatStyle mode;
	gboolean has_metadata_license = FALSE;

	/* hash tables for finding duplicates */
	found_tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	known_relation_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

	mode = as_context_get_style (ctx);

	/* We set the locale to generic English (loading it, if it exists) to allow for simple
	 * checks for cases of xml:lang="en" being used on template elements */
	as_context_set_locale (ctx, "en");

	/* validate the resulting AsComponent for sanity */
	cpt = as_component_new ();
	as_component_load_from_xml (cpt, ctx, root, NULL);
	as_component_set_context_locale (cpt, "C");
	as_validator_set_current_cpt (validator, cpt);
	is_merge_cpt = as_component_get_merge_kind (cpt) != AS_MERGE_KIND_NONE;

	/* check if component type is valid */
	cpttype = as_xml_get_prop_value (root, "type");
	if (cpttype != NULL) {
		if (as_component_kind_from_string (cpttype) == AS_COMPONENT_KIND_UNKNOWN) {
			as_validator_add_issue (validator,
						root,
						"component-type-invalid",
						"%s",
						cpttype);
		}
	}

	/* validate EOL date format */
	date_eol_str = as_xml_get_prop_value (root, "date_eol");
	if (date_eol_str != NULL)
		as_validator_validate_iso8601_complete_date (validator, root, date_eol_str);

	/* validate other component properties */
	if ((as_component_get_priority (cpt) != 0) && (mode == AS_FORMAT_STYLE_METAINFO))
		as_validator_add_issue (validator, root, "component-priority-in-metainfo", NULL);

	if (is_merge_cpt && mode == AS_FORMAT_STYLE_METAINFO)
		as_validator_add_issue (validator, root, "component-merge-in-metainfo", NULL);

	/* the component must have an id */
	if (as_is_empty (as_component_get_id (cpt))) {
		/* we don't have an id */
		as_validator_add_issue (validator, NULL, "component-id-missing", NULL);
	}

	/* the component must have a name */
	if (as_is_empty (as_component_get_name (cpt)) && !is_merge_cpt) {
		/* we don't have a name */
		as_validator_add_issue (validator, NULL, "component-name-missing", NULL);
	}

	/* the component must have a summary */
	if (as_is_empty (as_component_get_summary (cpt)) && !is_merge_cpt) {
		/* we don't have a summary */
		as_validator_add_issue (validator, NULL, "component-summary-missing", NULL);
	}

	for (xmlNode *iter = root->children; iter != NULL; iter = iter->next) {
		const gchar *node_name;
		g_autofree gchar *node_content = NULL;
		gboolean tag_valid = TRUE;
		gboolean can_be_empty = FALSE;

		/* discard spaces */
		if (iter->type != XML_ELEMENT_NODE)
			continue;
		node_name = (const gchar *) iter->name;
		node_content = as_xml_get_node_value_raw (iter);
		if (node_content != NULL)
			node_content = as_strstripnl (node_content);

		if (g_strcmp0 (node_name, "id") == 0) {
			g_autofree gchar *prop = as_xml_get_prop_value (iter, "type");
			if (prop != NULL) {
				as_validator_add_issue (validator,
							iter,
							"id-tag-has-type",
							"%s",
							node_content);
			}

			/* validate the AppStream ID */
			as_validator_validate_component_id (validator, iter, cpt);
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
		} else if (g_strcmp0 (node_name, "metadata_license") == 0) {
			has_metadata_license = TRUE;
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);

			/* the license must allow easy mixing of metadata in metainfo files */
			if (mode == AS_FORMAT_STYLE_METAINFO)
				as_validator_validate_metadata_license (validator, iter);
		} else if (g_strcmp0 (node_name, "pkgname") == 0) {
			if (g_hash_table_contains (found_tags, node_name))
				as_validator_add_issue (validator, iter, "multiple-pkgname", NULL);
		} else if (g_strcmp0 (node_name, "source_pkgname") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
		} else if (g_strcmp0 (node_name, "name") == 0) {
			g_autofree gchar *lang = as_xml_get_prop_value (iter, "lang");

			as_validator_check_appear_once (validator, iter, found_tags, TRUE);
			if (g_str_has_suffix (node_content, "."))
				as_validator_add_issue (validator,
							iter,
							"name-has-dot-suffix",
							"%s",
							node_content);

			if (lang == NULL && node_content != NULL && strlen (node_content) > 40) {
				as_validator_add_issue (validator,
							iter,
							"component-name-too-long",
							"%s",
							node_content);
			}

		} else if (g_strcmp0 (node_name, "summary") == 0) {
			g_autofree gchar *lang = NULL;
			const gchar *summary = node_content;
			lang = as_xml_get_prop_value (iter, "lang");

			as_validator_check_appear_once (validator, iter, found_tags, TRUE);
			if (g_str_has_suffix (summary, ".") && lang == NULL)
				as_validator_add_issue (validator,
							iter,
							"summary-has-dot-suffix",
							"%s",
							summary);

			if ((summary != NULL) && ((strstr (summary, "\n") != NULL) ||
						  (strstr (summary, "\t") != NULL))) {
				as_validator_add_issue (validator,
							iter,
							"summary-has-tabs-or-linebreaks",
							NULL);
			}

			if (as_validate_has_hyperlink (summary)) {
				as_validator_add_issue (validator,
							iter,
							"summary-has-url",
							"%s",
							node_name);
			}

			if (lang == NULL &&
			    !as_validator_first_word_capitalized (validator, summary, FALSE))
				as_validator_add_issue (validator,
							iter,
							"summary-first-word-not-capitalized",
							NULL);

			if (lang == NULL && summary != NULL && strlen (summary) > 90) {
				as_validator_add_issue (validator,
							iter,
							"summary-too-long",
							"%s",
							summary);
			}

		} else if (g_strcmp0 (node_name, "description") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, TRUE);
			as_validator_check_description_tag (validator, iter, mode, TRUE);
		} else if (g_strcmp0 (node_name, "icon") == 0) {
			g_autofree gchar *prop = as_validator_check_type_property (validator,
										   cpt,
										   iter);
			if ((g_strcmp0 (prop, "cached") == 0) || (g_strcmp0 (prop, "stock") == 0)) {
				if ((g_strrstr (node_content, "/") != NULL) ||
				    (as_validate_is_url (node_content)))
					as_validator_add_issue (validator,
								iter,
								"icon-stock-cached-has-url",
								NULL);
			}

			if (g_strcmp0 (prop, "remote") == 0) {
				if (as_validate_is_url (node_content)) {
					if (!as_validate_is_secure_url (node_content))
						as_validator_add_issue (validator,
									iter,
									"icon-remote-not-secure",
									"%s",
									node_content);

					as_validator_check_web_url (validator,
								    iter,
								    node_content,
								    "icon-remote-not-found");
				} else {
					as_validator_add_issue (validator,
								iter,
								"icon-remote-no-url",
								"%s",
								node_content);
				}
			}

			if (mode == AS_FORMAT_STYLE_METAINFO) {
				if ((prop != NULL) && (g_strcmp0 (prop, "stock") != 0) &&
				    (g_strcmp0 (prop, "remote") != 0))
					as_validator_add_issue (validator,
								iter,
								"metainfo-invalid-icon-type",
								"%s",
								prop);
			}

		} else if (g_strcmp0 (node_name, "url") == 0) {
			AsUrlKind url_kind;
			g_autofree gchar *prop = as_validator_check_type_property (validator,
										   cpt,
										   iter);

			url_kind = as_url_kind_from_string (prop);
			if (url_kind == AS_URL_KIND_UNKNOWN) {
				as_validator_add_issue (validator,
							iter,
							"url-invalid-type",
							"%s",
							prop);
			} else {
				/* check for URL kind duplicates */
				guint64 url_kind_flag = 1 << url_kind;
				if (as_flags_contains (known_url_kinds, url_kind_flag))
					as_validator_add_issue (validator,
								iter,
								"url-redefined",
								"%s",
								as_url_kind_to_string (url_kind));
				else
					as_flags_add (known_url_kinds, url_kind_flag);
			}

			if (!as_validate_is_url (node_content)) {
				as_validator_add_issue (validator,
							iter,
							"web-url-expected",
							"%s",
							node_content);
				continue;
			}

			if (!as_validate_is_secure_url (node_content))
				as_validator_add_issue (validator,
							iter,
							"url-not-secure",
							"%s",
							node_content);

			as_validator_check_web_url (validator,
						    iter,
						    node_content,
						    "url-not-reachable");

		} else if (g_strcmp0 (node_name, "categories") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_children_quick (validator, iter, "category", FALSE);
		} else if (g_strcmp0 (node_name, "keywords") == 0) {
			as_validator_check_children_quick (validator, iter, "keyword", FALSE);

			if (mode == AS_FORMAT_STYLE_METAINFO) {
				as_validator_check_appear_once (validator, iter, found_tags, TRUE);
				as_validator_check_nolocalized (validator,
								iter,
								"metainfo-localized-keywords-tag",
								"%s",
								iter->name);
			}
		} else if (g_strcmp0 (node_name, "mimetypes") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_children_quick (validator, iter, "mimetype", FALSE);
			as_validator_add_issue (validator, iter, "mimetypes-tag-deprecated", NULL);
		} else if (g_strcmp0 (node_name, "provides") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_provides (validator, iter, cpt);
		} else if (g_strcmp0 (node_name, "screenshots") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_screenshots (validator, iter, cpt);
		} else if (g_strcmp0 (node_name, "project_license") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_validate_project_license (validator, iter);
		} else if (g_strcmp0 (node_name, "project_group") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
		} else if (g_strcmp0 (node_name, "developer") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, TRUE);
			as_validator_check_developer (validator, iter);

		} else if (g_strcmp0 (node_name, "developer_name") == 0) {
			g_autofree gchar *lang = as_xml_get_prop_value (iter, "lang");
			if (lang == NULL)
				as_validator_add_issue (validator,
							iter,
							"developer-name-tag-deprecated",
							NULL);

		} else if (g_strcmp0 (node_name, "compulsory_for_desktop") == 0) {
			if (!as_utils_is_desktop_environment (node_content)) {
				as_validator_add_issue (validator,
							iter,
							"unknown-desktop-id",
							"%s",
							node_content);
			}
		} else if (g_strcmp0 (node_name, "releases") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_releases (validator, iter, mode);
			can_be_empty = TRUE;
		} else if (g_strcmp0 (node_name, "languages") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_children_quick (validator, iter, "lang", FALSE);
		} else if ((g_strcmp0 (node_name, "translation") == 0) &&
			   (mode == AS_FORMAT_STYLE_METAINFO)) {
			g_autofree gchar *prop = NULL;
			AsTranslationKind trkind;
			prop = as_validator_check_type_property (validator, cpt, iter);
			trkind = as_translation_kind_from_string (prop);
			if (prop != NULL && trkind == AS_TRANSLATION_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"unknown-desktop-id",
							"%s",
							prop);

		} else if (g_strcmp0 (node_name, "launchable") == 0) {
			g_autofree gchar *prop = as_validator_check_type_property (validator,
										   cpt,
										   iter);
			if (as_launchable_kind_from_string (prop) == AS_LAUNCHABLE_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"launchable-unknown-type",
							"%s",
							prop);

		} else if (g_strcmp0 (node_name, "extends") == 0) {
			/* check for circular extends */
			if (g_strcmp0 (node_content, as_component_get_id (cpt)) == 0) {
				as_validator_add_issue (validator,
							iter,
							"circular-component-relation",
							NULL);
			}

		} else if (g_strcmp0 (node_name, "bundle") == 0) {
			g_autofree gchar *prop = as_validator_check_type_property (validator,
										   cpt,
										   iter);
			if (prop != NULL &&
			    as_bundle_kind_from_string (prop) == AS_BUNDLE_KIND_UNKNOWN)
				as_validator_add_issue (validator,
							iter,
							"bundle-unknown-type",
							"%s",
							prop);

		} else if (g_strcmp0 (node_name, "update_contact") == 0) {
			if (mode == AS_FORMAT_STYLE_CATALOG) {
				as_validator_add_issue (validator,
							iter,
							"update-contact-in-catalog-data",
							NULL);
			} else {
				as_validator_check_appear_once (validator, iter, found_tags, FALSE);
				as_validator_validate_update_contact (validator, iter);
			}
		} else if (g_strcmp0 (node_name, "suggests") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_children_quick (validator, iter, "id", FALSE);
		} else if (g_strcmp0 (node_name, "content_rating") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_content_rating (validator, iter);
			can_be_empty = TRUE;
		} else if (g_strcmp0 (node_name, "replaces") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_children_quick (validator, iter, "id", FALSE);
		} else if (g_strcmp0 (node_name, "requires") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_relations (validator,
						      iter,
						      cpt,
						      known_relation_items,
						      AS_RELATION_KIND_REQUIRES);
		} else if (g_strcmp0 (node_name, "recommends") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_relations (validator,
						      iter,
						      cpt,
						      known_relation_items,
						      AS_RELATION_KIND_RECOMMENDS);
		} else if (g_strcmp0 (node_name, "supports") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_relations (validator,
						      iter,
						      cpt,
						      known_relation_items,
						      AS_RELATION_KIND_SUPPORTS);
		} else if (g_strcmp0 (node_name, "agreement") == 0) {
			as_validator_check_children_quick (validator,
							   iter,
							   "agreement_section",
							   FALSE);
		} else if (g_strcmp0 (node_name, "branding") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_branding (validator, iter);
		} else if (g_strcmp0 (node_name, "tags") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_tags (validator, iter);
		} else if (g_strcmp0 (node_name, "references") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_references (validator, iter);
		} else if (g_strcmp0 (node_name, "name_variant_suffix") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
		} else if (g_strcmp0 (node_name, "custom") == 0) {
			as_validator_check_appear_once (validator, iter, found_tags, FALSE);
			as_validator_check_custom (validator, iter);
		} else if ((g_strcmp0 (node_name, "metadata") == 0) ||
			   (g_strcmp0 (node_name, "kudos") == 0)) {
			/* these tags are GNOME / Fedora specific extensions and are therefore quite common. They shouldn't make the validation fail,
			 * especially if we might standardize at leat the <kudos/> tag one day, but we should still complain about those tags to make
			 * it obvious that they are not supported by all implementations */
			as_validator_add_issue (validator,
						iter,
						"nonstandard-gnome-extension",
						"%s",
						node_name);
			tag_valid = FALSE;
		} else if (!g_str_has_prefix (node_name, "x-")) {
			as_validator_add_issue (validator, iter, "unknown-tag", "%s", node_name);
			tag_valid = FALSE;
		}

		if (tag_valid && !can_be_empty) {
			as_validator_check_content_empty (validator, iter, node_name);
		}
	}

	/* emit an error if we are missing the metadata license in metainfo files */
	if ((!has_metadata_license) && (mode == AS_FORMAT_STYLE_METAINFO))
		as_validator_add_issue (validator, NULL, "metadata-license-missing", NULL);

	/* check if we have a description */
	if (as_is_empty (as_component_get_description (cpt))) {
		AsComponentKind cpt_kind;
		cpt_kind = as_component_get_kind (cpt);

		if ((cpt_kind == AS_COMPONENT_KIND_DESKTOP_APP) ||
		    (cpt_kind == AS_COMPONENT_KIND_CONSOLE_APP) ||
		    (cpt_kind == AS_COMPONENT_KIND_WEB_APP)) {
			as_validator_add_issue (validator, NULL, "app-description-required", NULL);
		} else if (cpt_kind == AS_COMPONENT_KIND_FONT) {
			as_validator_add_issue (validator, NULL, "font-description-missing", NULL);
		} else if ((cpt_kind == AS_COMPONENT_KIND_DRIVER) ||
			   (cpt_kind == AS_COMPONENT_KIND_FIRMWARE)) {
			as_validator_add_issue (validator,
						NULL,
						"driver-firmware-description-missing",
						NULL);
		} else if (cpt_kind != AS_COMPONENT_KIND_GENERIC) {
			as_validator_add_issue (validator, NULL, "description-missing", NULL);
		}

		as_component_set_context_locale (cpt, "en");
		if (!as_is_empty (as_component_get_description (cpt)))
			as_validator_add_issue (validator,
						NULL,
						"untranslated-description-missing",
						NULL);

		as_component_set_context_locale (cpt, "C");
	}

	/* check if we have a homepage */
	if (as_component_get_url (cpt, AS_URL_KIND_HOMEPAGE) == NULL) {
		AsComponentKind ckind;
		ckind = as_component_get_kind (cpt);

		/* we require a homepage for anything but generic components and language packs */
		if (ckind != AS_COMPONENT_KIND_GENERIC && ckind != AS_COMPONENT_KIND_LOCALIZATION)
			as_validator_add_issue (validator, NULL, "url-homepage-missing", NULL);
	}

	/* validate GUI app specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) {
		AsIcon *stock_icon = NULL;
		AsLaunchable *launchable = NULL;

		/* try to find desktop-id launchable */
		launchable = as_component_get_launchable (cpt, AS_LAUNCHABLE_KIND_DESKTOP_ID);

		/* try to find a stock icon for this component */
		stock_icon = as_component_get_icon_stock (cpt);
		if (stock_icon == NULL) {
			/* no stock icon, we require a desktop-entry file association */
			if (launchable == NULL ||
			    as_launchable_get_entries (launchable)->len == 0) {
				/* legacy compatibility: this is only an error if the component ID doesn't have a .desktop suffix */
				if (g_str_has_suffix (as_component_get_id (cpt), ".desktop"))
					as_validator_add_issue (validator,
								NULL,
								"desktop-app-launchable-omitted",
								NULL);
				else
					as_validator_add_issue (validator,
								NULL,
								"desktop-app-launchable-missing",
								NULL);
			}
		} else {
			/* this app has a stock icon, so we may not need a desktop-entry file */

			/* check if we have any categories */
			if (as_component_get_categories (cpt)->len == 0) {
				as_validator_add_issue (validator,
							NULL,
							"app-categories-missing",
							NULL);
			}

			/* check for launchable, but this time the omission may actually be intended */
			if (launchable == NULL || as_launchable_get_entries (launchable)->len == 0)
				as_validator_add_issue (validator,
							NULL,
							"desktop-app-launchable-omitted",
							NULL);
		}
	}

	/* validate console-app specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_CONSOLE_APP) {
		if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_BINARY) == NULL)
			as_validator_add_issue (validator, NULL, "console-app-no-binary", NULL);
	}

	/* validate webapp specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_WEB_APP) {
		AsLaunchable *launch = as_component_get_launchable (cpt, AS_LAUNCHABLE_KIND_URL);
		if (launch == NULL || as_launchable_get_entries (launch)->len == 0)
			as_validator_add_issue (validator, NULL, "web-app-no-url-launchable", NULL);

		if (as_component_get_icons (cpt)->len <= 0)
			as_validator_add_issue (validator, NULL, "web-app-no-icon", NULL);

		if (as_component_get_categories (cpt)->len <= 0) {
			as_validator_add_issue (validator, NULL, "web-app-no-category", NULL);
		}
	}

	/* validate font specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_FONT) {
		if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_FONT) == NULL)
			as_validator_add_issue (validator, NULL, "font-no-font-data", NULL);
	}

	/* validate driver specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DRIVER) {
		if (as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_MODALIAS) == NULL)
			as_validator_add_issue (validator, NULL, "driver-no-modalias", NULL);
	}

	/* validate addon specific stuff */
	if (as_component_get_extends (cpt)->len > 0) {
		AsComponentKind kind = as_component_get_kind (cpt);
		if ((kind != AS_COMPONENT_KIND_ADDON) && (kind != AS_COMPONENT_KIND_LOCALIZATION) &&
		    (kind != AS_COMPONENT_KIND_REPOSITORY))
			as_validator_add_issue (validator, NULL, "extends-not-allowed", NULL);
	} else {
		if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_ADDON)
			as_validator_add_issue (validator, NULL, "addon-extends-missing", NULL);
	}

	/* validate l10n specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_LOCALIZATION) {
		if (as_component_get_extends (cpt)->len == 0) {
			as_validator_add_issue (validator,
						NULL,
						"localization-extends-missing",
						NULL);
		}
		if (g_hash_table_size (as_component_get_languages_table (cpt)) == 0) {
			as_validator_add_issue (validator, NULL, "localization-no-languages", NULL);
		}
	}

	/* validate service specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_SERVICE) {
		AsLaunchable *launch = as_component_get_launchable (cpt,
								    AS_LAUNCHABLE_KIND_SERVICE);
		if (launch == NULL || as_launchable_get_entries (launch)->len == 0)
			as_validator_add_issue (validator,
						NULL,
						"service-no-service-launchable",
						NULL);
	}

	/* validate runtime specific stuff */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_RUNTIME) {
		const gchar *project_license = as_component_get_project_license (cpt);
		if ((project_license == NULL) ||
		    (!g_str_has_prefix (project_license, "LicenseRef")))
			as_validator_add_issue (validator,
						NULL,
						"runtime-project-license-no-ref",
						NULL);

		if (as_component_get_provided (cpt)->len == 0)
			as_validator_add_issue (validator, NULL, "runtime-no-provides", NULL);
	}

	/* validate suggestions */
	if (as_component_get_suggested (cpt)->len > 0) {
		guint j;
		GPtrArray *sug_array;

		sug_array = as_component_get_suggested (cpt);
		for (j = 0; j < sug_array->len; j++) {
			AsSuggested *prov = AS_SUGGESTED (g_ptr_array_index (sug_array, j));
			if (mode == AS_FORMAT_STYLE_METAINFO) {
				if (as_suggested_get_kind (prov) != AS_SUGGESTED_KIND_UPSTREAM)
					as_validator_add_issue (validator,
								NULL,
								"metainfo-suggestion-type-invalid",
								"%s",
								as_suggested_kind_to_string (
								    as_suggested_get_kind (prov)));
			}
		}
	}

	/* validate categories */
	if (as_component_get_categories (cpt)->len > 0) {
		GPtrArray *cat_array;
		gboolean have_valid_category = FALSE;

		cat_array = as_component_get_categories (cpt);
		for (guint j = 0; j < cat_array->len; j++) {
			const gchar *category_name = (const gchar *) g_ptr_array_index (cat_array,
											j);

			if (!as_utils_is_category_name (category_name)) {
				as_validator_add_issue (validator,
							NULL,
							"category-invalid",
							"%s",
							category_name);
				continue;
			}

			/* check if the category would be ignored during catalog composition */
			if (as_utils_category_name_is_bad (category_name))
				continue;

			have_valid_category = TRUE;
		}

		if (!have_valid_category) {
			/* the user clearly intended there to be categories, yet we ended up with no valid ones */
			as_validator_add_issue (validator, NULL, "all-categories-ignored", NULL);
		}
	}

	/* validate screenshots */
	if (as_component_get_screenshots_all (cpt)->len > 0) {
		guint j;
		GPtrArray *scr_array;

		scr_array = as_component_get_screenshots_all (cpt);
		for (j = 0; j < scr_array->len; j++) {
			AsScreenshot *scr = AS_SCREENSHOT (g_ptr_array_index (scr_array, j));
			const gchar *scr_caption = as_screenshot_get_caption (scr);

			if ((scr_caption != NULL) && (strlen (scr_caption) > 100)) {
				as_validator_add_issue (validator,
							NULL,
							"screenshot-caption-too-long",
							"%s",
							scr_caption);
			}
		}
	}

	/* validate releases */
	if (!as_release_list_is_empty (as_component_get_releases_plain (cpt))) {
		AsReleaseList *releases = as_component_get_releases_plain (cpt);
		AsRelease *release_prev = as_release_list_index (releases, 0);

		for (guint i = 1; i < as_release_list_len (releases); i++) {
			AsRelease *release = as_release_list_index (releases, i);
			const gchar *version = as_release_get_version (release);
			const gchar *version_prev = as_release_get_version (release_prev);
			if (version == NULL || version_prev == NULL)
				continue;
			if (as_vercmp_simple (version_prev, version) < 0) {
				as_validator_add_issue (validator,
							NULL,
							"releases-not-in-order",
							"%s << %s",
							version_prev,
							version);
			}
			release_prev = release;
		}
	} else {
		/* we have no release information! */
		AsComponentKind kind = as_component_get_kind (cpt);
		if (kind == AS_COMPONENT_KIND_DESKTOP_APP ||
		    kind == AS_COMPONENT_KIND_CONSOLE_APP) {
			/* show an info about missing age rating for application-type components */
			as_validator_add_issue (validator, NULL, "releases-info-missing", NULL);
		}
	}

	/* check content_rating */
	if (as_component_get_content_ratings (cpt)->len == 0 && !is_merge_cpt) {
		AsComponentKind kind = as_component_get_kind (cpt);
		if (kind == AS_COMPONENT_KIND_DESKTOP_APP ||
		    kind == AS_COMPONENT_KIND_CONSOLE_APP || kind == AS_COMPONENT_KIND_WEB_APP) {
			/* show an info about missing age rating for application-type components */
			as_validator_add_issue (validator, NULL, "content-rating-missing", NULL);
		}
	}

	/* check if we had a developer tag */
	if (!g_hash_table_contains (found_tags, "developer")) {
		as_validator_add_issue (validator, NULL, "developer-info-missing", NULL);
	}

	as_validator_clear_current_cpt (validator);
	return cpt;
}

/**
 * as_validator_validate_file:
 * @validator: An instance of #AsValidator.
 * @metadata_file: An AppStream XML file.
 *
 * Validate an AppStream XML file.
 *
 * Returns: %TRUE if file validated successfully.
 **/
gboolean
as_validator_validate_file (AsValidator *validator, GFile *metadata_file)
{
	g_autoptr(GFileInfo) info = NULL;
	g_autoptr(GInputStream) file_stream = NULL;
	g_autoptr(GInputStream) stream_data = NULL;
	g_autoptr(GConverter) conv = NULL;
	g_autoptr(GString) asxmldata = NULL;
	g_autoptr(GBytes) bytes = NULL;
	g_autofree gchar *fname = NULL;
	g_autofree gchar *dirname = NULL;
	g_autofree gchar *tmp = NULL;
	gssize len;
	const gsize buffer_size = 1024 * 32;
	g_autofree gchar *buffer = NULL;
	const gchar *content_type = NULL;
	g_autoptr(GError) tmp_error = NULL;
	gboolean ret;

	/* clear any issues from previous runs */
	as_validator_clear_issues (validator);

	info = g_file_query_info (metadata_file,
				  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
				  G_FILE_QUERY_INFO_NONE,
				  NULL,
				  NULL);
	if (info != NULL)
		content_type = g_file_info_get_attribute_string (
		    info,
		    G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);

	fname = g_file_get_basename (metadata_file);
	tmp = g_file_get_path (metadata_file);
	dirname = g_path_get_dirname (tmp);
	as_validator_set_current_fname (validator, fname);
	as_validator_set_current_dir (validator, dirname);

	file_stream = G_INPUT_STREAM (g_file_read (metadata_file, NULL, &tmp_error));
	if (tmp_error != NULL) {
		as_validator_add_issue (validator,
					NULL,
					"file-read-failed",
					"%s",
					tmp_error->message);
		return FALSE;
	}
	if (file_stream == NULL)
		return FALSE;

	if (as_str_equal0 (content_type, "application/zstd")) {
		/* decompress the Zstd stream */
		conv = G_CONVERTER (as_zstd_decompressor_new ());
		stream_data = g_converter_input_stream_new (file_stream, conv);

	} else if (as_str_equal0 (content_type, "application/gzip") ||
		   as_str_equal0 (content_type, "application/x-gzip")) {
		/* decompress the GZip stream */
		conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP));
		stream_data = g_converter_input_stream_new (file_stream, conv);

	} else {
		stream_data = g_object_ref (file_stream);
	}

	asxmldata = g_string_new ("");
	buffer = g_malloc (buffer_size);
	while ((len = g_input_stream_read (stream_data, buffer, buffer_size, NULL, &tmp_error)) >
	       0) {
		g_string_append_len (asxmldata, buffer, len);
	}
	if (tmp_error != NULL) {
		as_validator_add_issue (validator,
					NULL,
					"file-read-failed",
					"%s",
					tmp_error->message);
		return FALSE;
	}
	/* check if there was an error */
	if (len < 0)
		return FALSE;

	bytes = g_bytes_new_static (asxmldata->str, asxmldata->len);
	ret = as_validator_validate_bytes (validator, bytes);
	as_validator_clear_current_fname (validator);

	return ret;
}

/**
 * as_validator_open_xml_document:
 */
static xmlDoc *
as_validator_open_xml_document (AsValidator *validator, const gchar *xmldata, gssize len)
{
	xmlDoc *doc;
	g_autoptr(GError) error = NULL;

	doc = as_xml_parse_document (xmldata,
				     len,
				     TRUE, /* pedantic */
				     &error);
	if (doc == NULL) {
		if (error != NULL) {
			as_validator_add_issue (validator,
						NULL,
						"xml-markup-invalid",
						"%s",
						error->message);
		}

		return NULL;
	}

	return doc;
}

/**
 * as_validator_validate_bytes:
 * @validator: An instance of #AsValidator.
 * @metadata: XML metadata as #GBytes.
 *
 * Validate AppStream XML data from a byte array.
 * Remember to run %as_validator_clear_issues if you do not want previous
 * validation runs to affect the outcome of this validation.
 *
 * Returns: %TRUE if bytes validated successfully.
 *
 * Since: 0.14.0
 **/
gboolean
as_validator_validate_bytes (AsValidator *validator, GBytes *metadata)
{
	gboolean ret;
	xmlNode *root;
	xmlDoc *doc;
	g_autoptr(AsContext) ctx = NULL;
	const gchar *data;
	gsize data_len;
	AsComponent *cpt;

	/* setup networking, in case we want to check URLs */
	as_validator_setup_networking (validator);

	/* clear any issues from previous runs */
	as_validator_clear_issues (validator);

	/* load the XML data */
	ctx = as_context_new ();
	as_context_set_locale (ctx, "C");

	data = g_bytes_get_data (metadata, &data_len);
	doc = as_validator_open_xml_document (validator, data, data_len);
	if (doc == NULL)
		return FALSE;
	root = xmlDocGetRootElement (doc);

	ret = TRUE;
	if (g_strcmp0 ((gchar *) root->name, "component") == 0) {
		as_context_set_style (ctx, AS_FORMAT_STYLE_METAINFO);
		cpt = as_validator_validate_component_node (validator, ctx, root);
		if (cpt != NULL)
			g_object_unref (cpt);
	} else if (g_strcmp0 ((gchar *) root->name, "components") == 0) {
		const gchar *node_name;

		as_context_set_style (ctx, AS_FORMAT_STYLE_CATALOG);
		for (xmlNode *iter = root->children; iter != NULL; iter = iter->next) {
			/* discard spaces */
			if (iter->type != XML_ELEMENT_NODE)
				continue;
			node_name = (const gchar *) iter->name;
			if (g_strcmp0 (node_name, "component") == 0) {
				cpt = as_validator_validate_component_node (validator, ctx, iter);
				if (cpt != NULL)
					g_object_unref (cpt);
			} else {
				as_validator_add_issue (validator,
							iter,
							"component-catalog-tag-invalid",
							"%s",
							node_name);
				ret = FALSE;
			}
		}
	} else if (g_str_has_prefix ((gchar *) root->name, "application")) {
		as_validator_add_issue (validator, root, "metainfo-ancient", NULL);
		ret = FALSE;
	} else {
		as_validator_add_issue (validator, root, "root-tag-unknown", "%s", root->name);
		ret = FALSE;
	}

	xmlFreeDoc (doc);
	return ret && as_validator_check_success (validator);
}

/**
 * as_validator_validate_data:
 * @validator: An instance of #AsValidator.
 * @metadata: XML metadata.
 *
 * Validate AppStream XML data.
 *
 * Returns: %TRUE if data validated successfully.
 **/
gboolean
as_validator_validate_data (AsValidator *validator, const gchar *metadata)
{
	g_autoptr(GBytes) bytes = g_bytes_new_static (metadata, strlen (metadata));
	return as_validator_validate_bytes (validator, bytes);
}

/**
 * MInfoCheckData:
 *
 * Helper for HashTable iteration
 */
struct MInfoCheckData {
	AsValidator *validator;
	GHashTable *desktop_fnames;
	gchar *apps_dir;
};

/**
 * as_matches_metainfo:
 *
 * Check if filname matches %basename.(appdata|metainfo).xml
 */
static gboolean
as_matches_metainfo (const gchar *fname, const gchar *basename)
{
	g_autofree gchar *tmp = NULL;

	tmp = g_strdup_printf ("%s.metainfo.xml", basename);
	if (g_strcmp0 (fname, tmp) == 0)
		return TRUE;
	g_free (tmp);
	tmp = g_strdup_printf ("%s.appdata.xml", basename);
	if (g_strcmp0 (fname, tmp) == 0)
		return TRUE;

	return FALSE;
}

/**
 * as_validator_analyze_component_metainfo_relation_cb:
 *
 * Helper function for GHashTable foreach iteration.
 */
static void
as_validator_analyze_component_metainfo_relation_cb (const gchar *fname,
						     AsComponent *cpt,
						     struct MInfoCheckData *data)
{
	AsValidatorPrivate *priv = GET_PRIVATE (data->validator);
	g_autofree gchar *cid_base = NULL;

	/* if we have no component-id, we can't check anything */
	if (as_component_get_id (cpt) == NULL)
		return;

	as_validator_set_current_cpt (data->validator, cpt);
	as_validator_set_current_fname (data->validator, fname);

	/* check if the fname and the component-id match */
	if (g_str_has_suffix (as_component_get_id (cpt), ".desktop")) {
		cid_base = g_strndup (as_component_get_id (cpt),
				      g_strrstr (as_component_get_id (cpt), ".") -
					  as_component_get_id (cpt));
	} else {
		cid_base = g_strdup (as_component_get_id (cpt));
	}
	if (!as_matches_metainfo (fname, cid_base)) {
		/* the name-without-type didn't match - check for the full ID in the component name */
		if (!as_matches_metainfo (fname, as_component_get_id (cpt))) {
			as_validator_add_issue (data->validator,
						NULL,
						"metainfo-filename-cid-mismatch",
						NULL);
		}
	}

	/* check if the referenced .desktop file exists */
	if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) {
		AsLaunchable *de_launchable = as_component_get_launchable (
		    cpt,
		    AS_LAUNCHABLE_KIND_DESKTOP_ID);
		if ((de_launchable != NULL) &&
		    (as_launchable_get_entries (de_launchable)->len > 0)) {
			const gchar *desktop_id = g_ptr_array_index (
			    as_launchable_get_entries (de_launchable),
			    0);

			if (g_hash_table_contains (data->desktop_fnames, desktop_id)) {
				g_autofree gchar *desktop_fname_full = NULL;
				g_autoptr(GFile) dfile = NULL;
				g_autoptr(GPtrArray) de_issues = NULL;
				g_autoptr(GError) tmp_error = NULL;

				desktop_fname_full = g_build_filename (data->apps_dir,
								       desktop_id,
								       NULL);
				dfile = g_file_new_for_path (desktop_fname_full);
				de_issues = g_ptr_array_new_with_free_func (g_object_unref);
				as_desktop_entry_parse_file (cpt,
							     dfile,
							     AS_FORMAT_VERSION_LATEST,
							     TRUE, /* ignore NoDisplay */
							     de_issues,
							     NULL,
							     NULL,
							     &tmp_error);
				if (tmp_error != NULL) {
					as_validator_add_issue (data->validator,
								NULL,
								"desktop-file-load-failed",
								"%s",
								tmp_error->message);
				} else {
					/* we successfully loaded the desktop-entry data into the component, now perform some checks */

					/* pass on the found desktop-entry issues */
					for (guint i = 0; i < de_issues->len; i++) {
						AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (
						    g_ptr_array_index (de_issues, i));
						g_autofree gchar *issue_id_str = NULL;

						as_validator_issue_set_filename (issue, desktop_id);
						issue_id_str = g_strdup_printf (
						    "%s/%s/%s",
						    desktop_id,
						    as_validator_issue_get_tag (issue),
						    as_validator_issue_get_hint (issue));

						/* str ownership is transferred to the hashtable */
						if (g_hash_table_insert (
							priv->issues,
							g_steal_pointer (&issue_id_str),
							issue)) {
							/* the issue is new, we can add it to our by-file listing */
							GPtrArray *ilist = g_hash_table_lookup (
							    priv->issues_per_file,
							    desktop_id);
							if (ilist == NULL) {
								ilist =
								    g_ptr_array_new_with_free_func (
									g_object_unref);
								g_hash_table_insert (
								    priv->issues_per_file,
								    g_strdup (desktop_id),
								    ilist);
							}
							g_ptr_array_add (ilist,
									 g_object_ref (issue));
						}
					}

					/* check if we have any categories */
					if (as_component_get_categories (cpt)->len == 0) {
						as_validator_add_issue (data->validator,
									NULL,
									"app-categories-missing",
									NULL);
					}
				}
			} else {
				as_validator_add_issue (data->validator,
							NULL,
							"desktop-file-not-found",
							NULL);
			}
		}
	}

	as_validator_clear_current_cpt (data->validator);
	as_validator_clear_current_fname (data->validator);
}

/**
 * as_validator_validate_tree:
 * @validator: An instance of #AsValidator.
 * @root_dir: The root directory of the filesystem tree that should be validated.
 *
 * Validate a full directory tree for issues in AppStream metadata.
 *
 * Returns: %TRUE if file validated successfully.
 **/
gboolean
as_validator_validate_tree (AsValidator *validator, const gchar *root_dir)
{
	g_autofree gchar *metainfo_dir = NULL;
	g_autofree gchar *apps_dir = NULL;
	g_autoptr(GPtrArray) mfiles = NULL;
	g_autoptr(GPtrArray) dfiles = NULL;
	GHashTable *dfilenames = NULL;
	GHashTable *validated_cpts = NULL;
	guint i;
	gboolean ret = TRUE;
	g_autoptr(AsContext) ctx = NULL;
	struct MInfoCheckData ht_helper;

	/* cleanup */
	as_validator_clear_issues (validator);

	metainfo_dir = g_build_filename (root_dir, "usr", "share", "metainfo", NULL);
	apps_dir = g_build_filename (root_dir, "usr", "share", "applications", NULL);

	/* check if we actually have a directory which could hold metadata */
	if (!g_file_test (metainfo_dir, G_FILE_TEST_IS_DIR)) {
		g_clear_pointer (&metainfo_dir, g_free);
		metainfo_dir = g_build_filename (root_dir, "share", "metainfo", NULL);
		if (g_file_test (metainfo_dir, G_FILE_TEST_IS_DIR)) {
			/* success, we found some metadata without /usr prefix! */
			g_clear_pointer (&apps_dir, g_free);
			apps_dir = g_build_filename (root_dir, "share", "applications", NULL);
		} else {
			/* no metadata directory */
			as_validator_add_issue (validator, NULL, "dir-no-metadata-found", NULL);
			goto out;
		}
	}

	g_debug ("Looking for metadata in %s", metainfo_dir);

	/* check if we actually have a directory which could hold application information */
	if (!g_file_test (apps_dir, G_FILE_TEST_IS_DIR)) {
		as_validator_add_issue (validator, NULL, "dir-applications-not-found", NULL);
	}

	/* initialize networking (only actually happens if URLs should be checked) */
	as_validator_setup_networking (validator);

	/* holds a filename -> component mapping */
	validated_cpts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

	/* set up XML parser */
	ctx = as_context_new ();
	as_context_set_locale (ctx, "C");
	as_context_set_style (ctx, AS_FORMAT_STYLE_METAINFO);

	/* validate all metainfo files */
	mfiles = as_utils_find_files_matching (metainfo_dir, "*.xml", FALSE, NULL);

	/* we ignore errors entirely and create a dummy array in case there were any
	 * - having no metainfo directory is not an issue */
	if (mfiles == NULL)
		mfiles = g_ptr_array_new_with_free_func (g_free);

	for (i = 0; i < mfiles->len; i++) {
		const gchar *fname;
		g_autoptr(GFile) file = NULL;
		g_autoptr(GInputStream) file_stream = NULL;
		g_autoptr(GError) tmp_error = NULL;
		g_autoptr(GString) asdata = NULL;
		gssize len;
		const gsize buffer_size = 1024 * 24;
		g_autofree gchar *buffer = NULL;
		xmlNode *root;
		xmlDoc *doc;
		g_autofree gchar *fname_basename = NULL;

		fname = (const gchar *) g_ptr_array_index (mfiles, i);
		file = g_file_new_for_path (fname);
		if (!g_file_query_exists (file, NULL)) {
			g_warning ("File '%s' suddenly vanished.", fname);
			g_object_unref (file);
			continue;
		}

		fname_basename = g_path_get_basename (fname);
		as_validator_set_current_fname (validator, fname_basename);

		/* load a plaintext file */
		file_stream = G_INPUT_STREAM (g_file_read (file, NULL, &tmp_error));
		if (tmp_error != NULL) {
			as_validator_add_issue (validator,
						NULL,
						"file-read-failed",
						"%s",
						tmp_error->message);
			continue;
		}

		asdata = g_string_new ("");
		buffer = g_malloc (buffer_size);
		while ((len = g_input_stream_read (file_stream,
						   buffer,
						   buffer_size,
						   NULL,
						   &tmp_error)) > 0) {
			g_string_append_len (asdata, buffer, len);
		}
		/* check if there was an error */
		if (tmp_error != NULL) {
			as_validator_add_issue (validator,
						NULL,
						"file-read-failed",
						"%s",
						tmp_error->message);
			continue;
		}

		/* now read the XML */
		doc = as_validator_open_xml_document (validator, asdata->str, asdata->len);
		if (doc == NULL) {
			as_validator_clear_current_fname (validator);
			continue;
		}
		root = xmlDocGetRootElement (doc);

		if (g_strcmp0 ((gchar *) root->name, "component") == 0) {
			AsComponent *cpt;
			cpt = as_validator_validate_component_node (validator, ctx, root);
			if (cpt != NULL)
				g_hash_table_insert (validated_cpts,
						     g_strdup (fname_basename),
						     cpt);
		} else if (g_strcmp0 ((gchar *) root->name, "components") == 0) {
			as_validator_add_issue (validator,
						root,
						"metainfo-multiple-components",
						NULL);
			ret = FALSE;
		} else if (g_str_has_prefix ((gchar *) root->name, "application")) {
			as_validator_add_issue (validator, root, "metainfo-ancient", NULL);
			ret = FALSE;
		}

		as_validator_clear_current_fname (validator);
		xmlFreeDoc (doc);
	}

	/* check if we have matching .desktop files */
	dfilenames = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	dfiles = as_utils_find_files_matching (apps_dir, "*.desktop", FALSE, NULL);
	if (dfiles != NULL) {
		for (i = 0; i < dfiles->len; i++) {
			const gchar *fname;
			fname = (const gchar *) g_ptr_array_index (dfiles, i);
			g_hash_table_add (dfilenames, g_path_get_basename (fname));
		}
	}

	/* validate the component-id <-> filename relations and availability of other metadata */
	ht_helper.validator = validator;
	ht_helper.desktop_fnames = dfilenames;
	ht_helper.apps_dir = apps_dir;
	g_hash_table_foreach (validated_cpts,
			      (GHFunc) as_validator_analyze_component_metainfo_relation_cb,
			      &ht_helper);

out:
	if (dfilenames != NULL)
		g_hash_table_unref (dfilenames);
	if (validated_cpts != NULL)
		g_hash_table_unref (validated_cpts);

	return ret && as_validator_check_success (validator);
}

/**
 * as_validator_get_issue_files_count:
 * @validator: An instance of #AsValidator.
 *
 * Get the number of files for which issues have been found.
 *
 * Returns: The number of files that have issues.
 *
 * Since: 0.16.0
 */
guint
as_validator_get_issue_files_count (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	return g_hash_table_size (priv->issues_per_file);
}

/**
 * as_validator_get_issues:
 * @validator: An instance of #AsValidator.
 *
 * Get a list of found metadata format issues.
 *
 * Returns: (element-type AsValidatorIssue) (transfer container): a list of #AsValidatorIssue instances, free with g_list_free()
 */
GList *
as_validator_get_issues (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	return g_hash_table_get_values (priv->issues);
}

/**
 * as_validator_get_issues_per_file:
 * @validator: An instance of #AsValidator.
 *
 * Get a hash table of filenames mapped to lists of issues.
 * This is useful if validation was requested for multiple files and
 * a list of issues per-file is desired without prior explicit sorting.
 *
 * Returns: (element-type utf8 GPtrArray(AsValidatorIssue)) (transfer none): a file to issue list mapping
 *
 * Since: 0.12.8
 */
GHashTable *
as_validator_get_issues_per_file (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	return priv->issues_per_file;
}

/**
 * as_validator_yaml_write_handler_cb:
 *
 * Helper function to store the emitted YAML document.
 */
static int
as_validator_yaml_write_handler_cb (void *ptr, unsigned char *buffer, size_t size)
{
	GString *str;
	str = (GString *) ptr;
	g_string_append_len (str, (const gchar *) buffer, size);

	return 1;
}

/**
 * as_validator_get_issues_as_yaml:
 * @validator: An instance of #AsValidator.
 * @error: a #GError or %NULL
 *
 * Get an issue report as a YAML document.
 *
 * Returns: The YAML data as string, or %NULL if the report could not be created.
 *
 * Since: 0.12.8
 */
gchar *
as_validator_get_report_yaml (AsValidator *validator, GError **error)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	GHashTableIter ht_iter;
	gpointer ht_key, ht_value;
	yaml_emitter_t emitter;
	yaml_event_t event;
	gboolean res = FALSE;
	GString *yaml_result = g_string_new ("");

	yaml_emitter_initialize (&emitter);
	yaml_emitter_set_indent (&emitter, 2);
	yaml_emitter_set_unicode (&emitter, TRUE);
	yaml_emitter_set_width (&emitter, 100);
	yaml_emitter_set_output (&emitter, as_validator_yaml_write_handler_cb, yaml_result);

	/* emit start event */
	yaml_stream_start_event_initialize (&event, YAML_UTF8_ENCODING);
	if (!yaml_emitter_emit (&emitter, &event)) {
		g_set_error_literal (error,
				     AS_VALIDATOR_ERROR,
				     AS_VALIDATOR_ERROR_FAILED,
				     "Failed to initialize YAML emitter.");
		g_string_free (yaml_result, TRUE);
		yaml_emitter_delete (&emitter);
		return NULL;
	}

	g_hash_table_iter_init (&ht_iter, priv->issues_per_file);
	while (g_hash_table_iter_next (&ht_iter, &ht_key, &ht_value)) {
		const gchar *filename = (const gchar *) ht_key;
		GPtrArray *issues = (GPtrArray *) ht_value;
		gboolean validation_passed = TRUE;

		/* new document for this file */
		yaml_document_start_event_initialize (&event, NULL, NULL, NULL, FALSE);
		res = yaml_emitter_emit (&emitter, &event);
		g_assert (res);

		/* main dict start */
		as_yaml_mapping_start (&emitter);

		as_yaml_emit_entry (&emitter, "File", filename);
		as_yaml_emit_entry (&emitter, "Validator", PACKAGE_VERSION);

		as_yaml_emit_scalar (&emitter, "Issues");
		as_yaml_sequence_start (&emitter);

		for (guint i = 0; i < issues->len; i++) {
			AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (
			    g_ptr_array_index (issues, i));
			gint line = as_validator_issue_get_line (issue);
			const gchar *hint = as_validator_issue_get_hint (issue);
			const gchar *cid = as_validator_issue_get_cid (issue);
			AsIssueSeverity severity = as_validator_issue_get_severity (issue);
			as_yaml_mapping_start (&emitter);

			as_yaml_emit_entry (&emitter, "tag", as_validator_issue_get_tag (issue));
			as_yaml_emit_entry (&emitter,
					    "severity",
					    as_issue_severity_to_string (severity));

			if (cid != NULL)
				as_yaml_emit_entry (&emitter, "component", cid);
			if (line > 0)
				as_yaml_emit_entry_uint64 (&emitter, "line", (guint) line);
			if (hint != NULL)
				as_yaml_emit_entry (&emitter, "hint", hint);
			as_yaml_emit_long_entry (&emitter,
						 "explanation",
						 as_validator_issue_get_explanation (issue));

			if ((severity == AS_ISSUE_SEVERITY_WARNING) ||
			    (severity == AS_ISSUE_SEVERITY_ERROR))
				validation_passed = FALSE;

			as_yaml_mapping_end (&emitter);
		}

		/* end "Issues" sequence */
		as_yaml_sequence_end (&emitter);

		as_yaml_emit_entry (&emitter, "Passed", validation_passed ? "yes" : "no");

		/* main dict end */
		as_yaml_mapping_end (&emitter);
		/* finalize the document */
		yaml_document_end_event_initialize (&event, 1);
		res = yaml_emitter_emit (&emitter, &event);
		g_assert (res);
	}

	/* end stream */
	yaml_stream_end_event_initialize (&event);
	res = yaml_emitter_emit (&emitter, &event);
	g_assert (res);

	yaml_emitter_flush (&emitter);
	yaml_emitter_delete (&emitter);

	return g_string_free (yaml_result, FALSE);
}

/**
 * as_validator_get_tag_explanation:
 * @validator: An instance of #AsValidator.
 *
 * Get the explanatory text for a given issue tag.
 *
 * Returns: Explanation text.
 */
const gchar *
as_validator_get_tag_explanation (AsValidator *validator, const gchar *tag)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	const AsValidatorIssueTag *tag_data = g_hash_table_lookup (priv->issue_tags, tag);
	if (tag_data == NULL)
		return NULL;
	return tag_data->explanation;
}

/**
 * as_validator_get_tag_severity:
 * @validator: An instance of #AsValidator.
 *
 * Get the severity for a given issue tag.
 *
 * Returns: The #AsIssueSeverity
 */
AsIssueSeverity
as_validator_get_tag_severity (AsValidator *validator, const gchar *tag)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	const AsValidatorIssueTag *tag_data = g_hash_table_lookup (priv->issue_tags, tag);
	if (tag_data == NULL)
		return AS_ISSUE_SEVERITY_UNKNOWN;
	return tag_data->severity;
}

/**
 * as_validator_get_tags:
 * @validator: An instance of #AsValidator.
 *
 * Get an array of all tags known to the validator.
 *
 * Returns: (transfer full): A string array of tags
 */
gchar **
as_validator_get_tags (AsValidator *validator)
{
	AsValidatorPrivate *priv = GET_PRIVATE (validator);
	GHashTableIter iter;
	gpointer ht_key;
	guint i = 0;
	gchar **result;

	result = g_new0 (gchar *, g_hash_table_size (priv->issue_tags) + 1);
	g_hash_table_iter_init (&iter, priv->issue_tags);
	while (g_hash_table_iter_next (&iter, &ht_key, NULL)) {
		result[i++] = g_strdup ((const gchar *) ht_key);
	}

	return result;
}

/**
 * as_validator_class_init:
 **/
static void
as_validator_class_init (AsValidatorClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = as_validator_finalize;
}

/**
 * as_validator_new:
 *
 * Creates a new #AsValidator.
 *
 * Returns: (transfer full): an #AsValidator
 **/
AsValidator *
as_validator_new (void)
{
	AsValidator *validator;
	validator = g_object_new (AS_TYPE_VALIDATOR, NULL);
	return AS_VALIDATOR (validator);
}
