/ .. / / -> download
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <libxml/tree.h>
#include <libxml/xmlschemas.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#include "xml-utils.h"

#define PROG_NAME "xml-validate"
#define VERSION "1.1.0"

#define ERR_PREFIX PROG_NAME ": ERROR: "
#define SUCCESS_PREFIX PROG_NAME ": SUCCESS: "
#define FAILED_PREFIX PROG_NAME ": FAILED: "

#define E_BAD_LIST ERR_PREFIX "Could not read list file: %s\n"
#define E_MAX_SCHEMA_PARSERS ERR_PREFIX "Maximum number of schemas reached: %d\n"
#define E_BAD_IDREF ERR_PREFIX "%s (%ld): No matching ID for '%s'.\n"

#define EXIT_MAX_SCHEMAS 2
#define EXIT_MISSING_SCHEMA 3

#define XML_SCHEMA_URI BAD_CAST "http://www.w3.org/2001/XMLSchema"
#define XSI_URI BAD_CAST "http://www.w3.org/2001/XMLSchema-instance"

static enum verbosity_level {SILENT, NORMAL, VERBOSE} verbosity = NORMAL;

enum show_fnames { SHOW_NONE, SHOW_INVALID, SHOW_VALID };

/* Cache schemas to prevent parsing them twice (mainly needed when accessing
 * the schema over a network)
 */
struct xml_schema_parser {
	char *url;
	xmlDocPtr doc;
	xmlSchemaParserCtxtPtr ctxt;
	xmlSchemaPtr schema;
	xmlSchemaValidCtxtPtr valid_ctxt;
	xmlXPathContextPtr xpath_ctx;
	xmlXPathObjectPtr id;
	xmlXPathObjectPtr idref;
	xmlXPathObjectPtr idrefs;
};

/* Initial max schema parsers. */
static unsigned SCHEMA_PARSERS_MAX = 1;

static struct xml_schema_parser *schema_parsers;

static int schema_parser_count = 0;

static void print_error(void *userData, xmlErrorPtr error)
{
	if (error->file) {
		fprintf(userData, ERR_PREFIX "%s (%d): %s", error->file, error->line, error->message);
	} else {
		fprintf(userData, ERR_PREFIX "%s", error->message);
	}
}

static void suppress_error(void *userData, xmlErrorPtr error)
{
}

xmlStructuredErrorFunc schema_errfunc = print_error;

/* Find a schema parser by URL. */
static struct xml_schema_parser *get_schema_parser(const char *url)
{
	int i;

	for (i = 0; i < schema_parser_count; ++i) {
		if (strcmp(schema_parsers[i].url, url) == 0) {
			return &(schema_parsers[i]);
		}
	}

	return NULL;
}

/* Create a new schema parser from a URL. */
static struct xml_schema_parser *add_schema_parser(char *url)
{
	struct xml_schema_parser *parser;

	xmlDocPtr doc;
	xmlSchemaParserCtxtPtr ctxt;
	xmlSchemaPtr schema;
	xmlSchemaValidCtxtPtr valid_ctxt;
	xmlXPathContextPtr xpath_ctx;
	xmlXPathObjectPtr id, idref, idrefs;

	/* Read the schema document and create a validating context. */
	doc = read_xml_doc(url);
	ctxt = xmlSchemaNewDocParserCtxt(doc);
	schema = xmlSchemaParse(ctxt);
	valid_ctxt = xmlSchemaNewValidCtxt(schema);

	/* Set custom error functions. */
	xmlSchemaSetParserStructuredErrors(ctxt, schema_errfunc, stderr);
	xmlSchemaSetValidStructuredErrors(valid_ctxt, schema_errfunc, stderr);

	/* Locate xs:ID, xs:IDREF and xs:IDREFS types. */
	xpath_ctx = xmlXPathNewContext(doc);
	xmlXPathRegisterNs(xpath_ctx, BAD_CAST "xs", XML_SCHEMA_URI);
	id = xmlXPathEvalExpression(BAD_CAST "//xs:attribute[@type='xs:ID']|//xs:element[@type='xs:ID']", xpath_ctx);
	idref = xmlXPathEvalExpression(BAD_CAST "//xs:attribute[@type='xs:IDREF']|//xs:element[@type='xs:IDREF']", xpath_ctx);
	idrefs = xmlXPathEvalExpression(BAD_CAST "//xs:attribute[@type='xs:IDREFS']|//xs:element[@type='xs:IDREFS']", xpath_ctx);

	/* Initialize the parser. */
	schema_parsers[schema_parser_count].url = url;
	schema_parsers[schema_parser_count].doc = doc;
	schema_parsers[schema_parser_count].ctxt = ctxt;
	schema_parsers[schema_parser_count].schema = schema;
	schema_parsers[schema_parser_count].valid_ctxt = valid_ctxt;
	schema_parsers[schema_parser_count].xpath_ctx = xpath_ctx;
	schema_parsers[schema_parser_count].id = id;
	schema_parsers[schema_parser_count].idref = idref;
	schema_parsers[schema_parser_count].idrefs = idrefs;

	parser = &schema_parsers[schema_parser_count];

	++schema_parser_count;

	return parser;
}

/* Show help/usage message. */
static void show_help(void)
{
	puts("Usage: " PROG_NAME " [-s <path>] [-F|-f] [-lqvh?] [<file>...]");
	puts("");
	puts("Options:");
	puts("  -F, --valid-filenames  List valid files.");
	puts("  -f, --filenames        List invalid files.");
	puts("  -h, -?, --help         Show help/usage message.");
	puts("  -l, --list             Treat input as list of filenames.");
	puts("  -q, --quiet            Silent (no output).");
	puts("  -s, --schema <path>    Validate against the given schema.");
	puts("  -v, --verbose          Verbose output.");
	puts("  --version              Show version information.");
	puts("  <file>                 Any number of XML documents to validate.");
	LIBXML2_PARSE_LONGOPT_HELP
}

/* Show version information. */
static void show_version(void)
{
	printf("%s (xml-utils) %s\n", PROG_NAME, VERSION);
	printf("Using libxml %s\n", xmlParserVersion);
}

/* Check if a given ID attribute with a given name exists with a given ID value. */
static int check_id_exists_in_doc(const xmlDocPtr doc, const char *fname, bool attr, const xmlChar *name, const xmlChar *id)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	int err;

	ctx = xmlXPathNewContext(doc);
	xmlXPathRegisterVariable(ctx, BAD_CAST "name", xmlXPathNewString(name));
	xmlXPathRegisterVariable(ctx, BAD_CAST "id", xmlXPathNewString(id));

	if (attr) {
		obj = xmlXPathEvalExpression(BAD_CAST "//@*[name()=$name and .=$id]", ctx);
	} else {
		obj = xmlXPathEvalExpression(BAD_CAST "//*[name()=$name and .=$id]", ctx);
	}

	err = xmlXPathNodeSetIsEmpty(obj->nodesetval);

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return err;
}

/* Check if a given ID exists in a document. */
static int check_id_exists(const struct xml_schema_parser *parser, const xmlDocPtr doc, const char *fname, const xmlChar *id)
{
	if (!xmlXPathNodeSetIsEmpty(parser->id->nodesetval)) {
		int i;

		for (i = 0; i < parser->id->nodesetval->nodeNr; ++i) {
			xmlNodePtr node = parser->id->nodesetval->nodeTab[i];
			xmlChar *name = xmlGetProp(node, BAD_CAST "name");
			bool matched;

			/* Determine if the given ID matched in the document. */
			matched = check_id_exists_in_doc(
				doc,
				fname,
				xmlStrcmp(node->name, BAD_CAST "attribute") == 0,
				name,
				id) == 0;

			xmlFree(name);

			/* If the IDREF matches any ID in the doc, then exit
			 * successfully. */
			if (matched) {
				return 0;
			}
		}
	}

	/* At this point, no ID in the document matched the IDREF. */
	return 1;
}

/* Check if a specific IDREF value is valid. */
static int check_specific_idref(const struct xml_schema_parser *parser, const xmlDocPtr doc, const char *fname, bool attr, const xmlChar *name)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	int err = 0;

	ctx = xmlXPathNewContext(doc);
	xmlXPathRegisterVariable(ctx, BAD_CAST "name", xmlXPathNewString(name));

	if (attr) {
		obj = xmlXPathEvalExpression(BAD_CAST "//@*[name()=$name]", ctx);
	} else {
		obj = xmlXPathEvalExpression(BAD_CAST "//*[name()=$name]", ctx);
	}

	if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		int i;

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			int e;
			xmlNodePtr node = obj->nodesetval->nodeTab[i];
			xmlChar *id = xmlNodeGetContent(node);

			e = check_id_exists(parser, doc, fname, id);

			if (e) {
				fprintf(stderr, E_BAD_IDREF, fname, xmlGetLineNo(node), (char *) id);
			}

			err += e;

			xmlFree(id);
		}
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return err;
}

/* Check all IDREF values in a document. */
static int check_idref(const struct xml_schema_parser *parser, const xmlDocPtr doc, const char *fname)
{
	int err = 0;

	if (!xmlXPathNodeSetIsEmpty(parser->idref->nodesetval)) {
		int i;
		for (i = 0; i < parser->idref->nodesetval->nodeNr; ++i) {
			xmlNodePtr node = parser->idref->nodesetval->nodeTab[i];
			xmlChar *name = xmlGetProp(node, BAD_CAST "name");
			err += check_specific_idref(
				parser,
				doc,
				fname,
				xmlStrcmp(node->name, BAD_CAST "attribute") == 0,
				name);
			xmlFree(name);
		}
	}

	return err;
}

/* Check all IDREFS values in a document. */
static int check_specific_idrefs(const struct xml_schema_parser *parser, const xmlDocPtr doc, const char *fname, bool attr, const xmlChar *name)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	int err = 0;

	ctx = xmlXPathNewContext(doc);
	xmlXPathRegisterVariable(ctx, BAD_CAST "name", xmlXPathNewString(name));
	
	if (attr) {
		obj = xmlXPathEvalExpression(BAD_CAST "//@*[name()=$name]", ctx);
	} else {
		obj = xmlXPathEvalExpression(BAD_CAST "//*[name()=$name]", ctx);
	}

	if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		int i;

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			char *ids, *id = NULL;
			xmlNodePtr node = obj->nodesetval->nodeTab[i];

			ids = (char *) xmlNodeGetContent(node);

			while ((id = strtok(id ? NULL : ids, " "))) {
				int e;

				e = check_id_exists(parser, doc, fname, BAD_CAST id);

				if (e) {
					fprintf(stderr, E_BAD_IDREF, fname, xmlGetLineNo(node), (char *) id);
				}

				err += e;
			}

			xmlFree(ids);
		}
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return err;
}

static int check_idrefs(const struct xml_schema_parser *parser, const xmlDocPtr doc, const char *fname)
{
	int err = 0;

	if (!xmlXPathNodeSetIsEmpty(parser->idrefs->nodesetval)) {
		int i;
		for (i = 0; i < parser->idrefs->nodesetval->nodeNr; ++i) {
			xmlNodePtr node = parser->idrefs->nodesetval->nodeTab[i];
			xmlChar *name = xmlGetProp(node, BAD_CAST "name");
			err += check_specific_idrefs(
				parser,
				doc,
				fname,
				xmlStrcmp(node->name, BAD_CAST "attribute") == 0,
				name);
			xmlFree(name);
		}
	}

	return err;
}

static void resize_schema_parsers(void)
{
	if (!(schema_parsers = realloc(schema_parsers, (SCHEMA_PARSERS_MAX *= 2) * sizeof(struct xml_schema_parser)))) {
		fprintf(stderr, E_MAX_SCHEMA_PARSERS, schema_parser_count);
		exit(EXIT_MAX_SCHEMAS);
	}
}

static int validate_file(const char *fname, const char *schema, int list)
{
	xmlDocPtr doc;
	xmlNodePtr root;
	char *url;
	struct xml_schema_parser *parser;
	int err = 0;

	if (!(doc = read_xml_doc(fname))) {
		return 1;
	}

	root = xmlDocGetRootElement(doc);

	if (schema) {
		url = strdup(schema);
	} else {
		url = (char *) xmlGetNsProp(root, BAD_CAST "noNamespaceSchemaLocation", XSI_URI);
	}

	if (!url) {
		if (verbosity > SILENT) {
			fprintf(stderr, ERR_PREFIX "%s has no schema.\n", fname);
		}
		return 1;
	}

	if ((parser = get_schema_parser(url))) {
		xmlFree(url);
	} else {
		if (schema_parser_count == SCHEMA_PARSERS_MAX) {
			resize_schema_parsers();
		}

		parser = add_schema_parser(url);
	}

	/* libxml2's XML Schema validator currently does not check ID and
	 * IDREF/IDREFS relationships, so these have been implemented
	 * separately. */
	err += check_idref(parser, doc, fname);
	err += check_idrefs(parser, doc, fname);

	if (xmlSchemaValidateDoc(parser->valid_ctxt, doc)) {
		++err;
	}

	if (verbosity >= VERBOSE) {
		if (err) {
			fprintf(stderr, FAILED_PREFIX "%s fails to validate against schema %s\n", fname, parser->url);
		} else {
			fprintf(stderr, SUCCESS_PREFIX "%s validates against schema %s\n", fname, parser->url);
		}
	}

	if (list && err) {
		printf("%s\n", fname);
	}

	xmlFreeDoc(doc);

	return err;
}

static int validate_file_list(const char *fname, const char *schema, enum show_fnames show_fnames)
{
	FILE *f;
	char path[PATH_MAX];
	int err;

	if (fname) {
		if (!(f = fopen(fname, "r"))) {
			fprintf(stderr, E_BAD_LIST, fname);
			return 0;
		}
	} else {
		f = stdin;
	}

	err = 0;

	while (fgets(path, PATH_MAX, f)) {
		strtok(path, "\t\r\n");
		err += validate_file(path, schema, show_fnames);
	}

	if (fname) {
		fclose(f);
	}

	return err;
}

int main(int argc, char *argv[])
{
	int c, i;
	int err = 0;
	enum show_fnames show_fnames = SHOW_NONE;
	int is_list = 0;
	char *schema = NULL;

	const char *sopts = "flqvs:h?";
	struct option lopts[] = {
		{"version"       , no_argument      , 0, 0},
		{"help"          , no_argument      , 0, 'h'},
		{"filenames"     , no_argument      , 0, 'f'},
		{"list"          , no_argument      , 0, 'l'},
		{"quiet"         , no_argument      , 0, 'q'},
		{"verbose"       , no_argument      , 0, 'v'},
		{"schema"        , required_argument, 0, 's'},
		LIBXML2_PARSE_LONGOPT_DEFS
		{0, 0, 0, 0}
	};
	int loptind = 0;

	schema_parsers = malloc(SCHEMA_PARSERS_MAX * sizeof(struct xml_schema_parser));

	while ((c = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
		switch (c) {
			case 0:
				if (strcmp(lopts[loptind].name, "version") == 0) {
					show_version();
					return EXIT_SUCCESS;
				}
				LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
				break;
			case 'F': show_fnames = SHOW_VALID; break;
			case 'f': show_fnames = SHOW_INVALID; break;
			case 'l': is_list = 1; break;
			case 'q': verbosity = SILENT; break;
			case 'v': verbosity = VERBOSE; break;
			case 's': schema = strdup(optarg); break;
			case 'h': 
			case '?': show_help(); return EXIT_SUCCESS;
		}
	}

	LIBXML2_PARSE_INIT

	if (verbosity == SILENT) {
		schema_errfunc = suppress_error;
	}

	xmlSetStructuredErrorFunc(stderr, schema_errfunc);

	if (optind < argc) {
		for (i = optind; i < argc; ++i) {
			if (is_list) {
				err += validate_file_list(argv[i], schema, show_fnames);
			} else {
				err += validate_file(argv[i], schema, show_fnames);
			}
		}
	} else if (is_list) {
		err = validate_file_list(NULL, schema, show_fnames);
	} else {
		err = validate_file("-", schema, show_fnames);
	}

	for (i = 0; i < schema_parser_count; ++i) {
		xmlFree(schema_parsers[i].url);
		xmlSchemaFreeValidCtxt(schema_parsers[i].valid_ctxt);
		xmlSchemaFree(schema_parsers[i].schema);
		xmlSchemaFreeParserCtxt(schema_parsers[i].ctxt);
		xmlXPathFreeObject(schema_parsers[i].id);
		xmlXPathFreeObject(schema_parsers[i].idref);
		xmlXPathFreeObject(schema_parsers[i].idrefs);
		xmlXPathFreeContext(schema_parsers[i].xpath_ctx);
		xmlFreeDoc(schema_parsers[i].doc);
	}

	free(schema_parsers);
	free(schema);
	xmlCleanupParser();

	return err ? EXIT_FAILURE : EXIT_SUCCESS;
}


/ gopher://khzae.net/0/s1000d/xml/xml-utils/src/utils/xml-validate/xml-validate.c
Styles: Light Dark Classic