/ .. / / -> download
#include <stdio.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <stdbool.h>
#include <ctype.h>
#include <libgen.h>
#include <sys/stat.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/debugXML.h>
#include <libxml/xpathInternals.h>
#include "s1kd_tools.h"

#define PROG_NAME "s1kd-refs"
#define VERSION "4.17.2"

#define ERR_PREFIX PROG_NAME ": ERROR: "
#define SUCC_PREFIX PROG_NAME ": SUCCESS: "
#define FAIL_PREFIX PROG_NAME ": FAILURE: "
#define INF_PREFIX PROG_NAME ": INFO: "

#define E_BAD_LIST ERR_PREFIX "Could not read list: %s\n"
#define E_OUT_OF_MEMORY ERR_PREFIX "Too many files in recursive listing.\n"
#define E_BAD_STDIN ERR_PREFIX "stdin does not contain valid XML.\n"
#define E_BAD_CSN_CODE ERR_PREFIX "Invalid non-chapterized IPD SNS: %s\n"

#define S_UNMATCHED SUCC_PREFIX "No unmatched references in %s\n"
#define F_UNMATCHED FAIL_PREFIX "Unmatched references in %s\n"

#define I_WHEREUSED INF_PREFIX "Searching for references to %s...\n"
#define I_UPDATE_REF INF_PREFIX "%s: Updating reference %s to match %s...\n"

#define EXIT_UNMATCHED_REF 1
#define EXIT_OUT_OF_MEMORY 2
#define EXIT_BAD_STDIN 3
#define EXIT_BAD_CSN_CODE 4

/* List only references found in the content section. */
static bool contentOnly = false;

static enum verbosity { QUIET, NORMAL, VERBOSE, DEBUG } verbosity = NORMAL;

/* Assume objects were created with the -N option. */
static bool noIssue = false;

/* Show unmatched references instead of an error. */
static bool showUnmatched = false;

/* Show references which are matched in the filesystem. */
static bool showMatched = true;

/* Recurse in to child directories. */
static bool recursive = false;

/* Directory to start search in. */
static char *directory;

/* Ignore issue info when matching. */
static bool ignoreIss = false;

/* Include the source object as a reference. */
static bool listSrc = false;

/* List references in matched objects recursively. */
static bool listRecursively = false;

/* Update the address information of references. */
static bool updateRefs = false;

/* Update the ident and address info from the latest matched issue. */
static bool updateRefIdent = false;

/* Overwrite updated input objects. */
static bool overwriteUpdated = false;

/* Remove unmatched references from the input objects. */
static bool tagUnmatched = false;

/* Command string to execute with the -e option. */
static char *execStr = NULL;

/* Non-chapterized IPD SNS. */
static bool nonChapIpdSns = false;
static char nonChapIpdSystemCode[4] = "";
static char nonChapIpdSubSystemCode[2] = "";
static char nonChapIpdSubSubSystemCode[2] = "";
static char nonChapIpdAssyCode[5] = "";

/* Figure number variant format string. */
static xmlChar *figNumVarFormat;

/* When listing references recursively, keep track of files which have already
 * been listed to avoid loops.
 */
static char (*listedFiles)[PATH_MAX] = NULL;
static int numListedFiles = 0;
static long unsigned maxListedFiles = 1;

/* Possible objects to list references to. */
#define SHOW_COM 0x0001 /* Comments */
#define SHOW_DMC 0x0002 /* Data modules */
#define SHOW_ICN 0x0004 /* ICNs */
#define SHOW_PMC 0x0008 /* Publication modules */
#define SHOW_EPR 0x0010 /* External publications */
#define SHOW_HOT 0x0020 /* Hotspots */
#define SHOW_FRG 0x0040 /* Fragments */
#define SHOW_DML 0x0080 /* DMLs */
#define SHOW_SMC 0x0100 /* SCORM content packages */
#define SHOW_SRC 0x0200 /* Source ident */
#define SHOW_REP 0x0400 /* Repository source ident */
#define SHOW_IPD 0x0800 /* IPD data modules */
#define SHOW_CSN 0x1000 /* CSN items */

/* All possible objects. */
#define SHOW_ALL \
	SHOW_COM | \
	SHOW_DMC | \
	SHOW_ICN | \
	SHOW_PMC | \
	SHOW_EPR | \
	SHOW_HOT | \
	SHOW_FRG | \
	SHOW_DML | \
	SHOW_SMC | \
	SHOW_SRC | \
	SHOW_REP | \
	SHOW_IPD | \
	SHOW_CSN

/* All objects relevant to -w mode. */
#define SHOW_WHERE_USED \
	SHOW_COM | \
	SHOW_DMC | \
	SHOW_PMC | \
	SHOW_DML | \
	SHOW_SMC | \
	SHOW_ICN | \
	SHOW_SRC | \
	SHOW_REP | \
	SHOW_IPD | \
        SHOW_EPR

/* Write valid CSDB objects to stdout. */
static bool outputTree = false;

/* External pub list. */
static xmlDocPtr externalPubs = NULL;

/* Allow matching of filenames which only start with the code.
 *
 * If this is false, then the filenames must match the exact code up to the
 * extension (last .)
 *
 * For example, with loose matching a code of ABC would match a file named
 * ABC_001.PDF, while without loose matching it will not.
 */
static bool looseMatch = true;

/* XPath for matching hotspots. */
#define DEFAULT_HOTSPOT_XPATH BAD_CAST "/X3D//*[@DEF=$id]|//*[@id=$id]"
static xmlChar *hotspotXPath = NULL;
static xmlNodePtr hotspotNs = NULL;

/* Delimiter for the format string. */
#define FMTSTR_DELIM '%'
/* Custom format for printed references. */
static char *printFormat = NULL;

/* Remove elements marked as "delete". */
static bool remDelete = false;

/* Return the first node matching an XPath expression. */
static xmlNodePtr firstXPathNode(xmlDocPtr doc, xmlNodePtr root, const xmlChar *path)
{
	xmlNodePtr node;

	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;

	if (!doc && root)
		doc = root->doc;

	if (!doc)
		return NULL;

	ctx = xmlXPathNewContext(doc);

	if (root)
		ctx->node = root;

	obj = xmlXPathEvalExpression(BAD_CAST path, ctx);

	if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		node = NULL;
	} else {
		node = obj->nodesetval->nodeTab[0];
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return node;
}

/* Return the value of the first node matching an XPath expression. */
static xmlChar *firstXPathValue(xmlDocPtr doc, xmlNodePtr root, const xmlChar *path)
{
	return xmlNodeGetContent(firstXPathNode(doc, root, path));
}

/* Process and print info based on a format string. */
static void processFormatStr(FILE *f, xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	int i;

	for (i = 0; printFormat[i]; ++i) {
		if (printFormat[i] == FMTSTR_DELIM) {
			if (printFormat[i + 1] == FMTSTR_DELIM) {
				fputc(FMTSTR_DELIM, f);
				++i;
			} else {
				const char *k, *e;
				int n;

				k = printFormat + i + 1;
				e = strchr(k, FMTSTR_DELIM);
				if (!e) break;
				n = e - k;

				if (strncmp(k, "src", n) == 0) {
					fprintf(f, "%s", src);
				} else if (strncmp(k, "ref", n) == 0) {
					fprintf(f, "%s", ref);
				} else if (strncmp(k, "file", n) == 0 && fname) {
					fprintf(f, "%s", fname);
				} else if (strncmp(k, "line", n) == 0) {
					fprintf(f, "%ld", xmlGetLineNo(node));
				} else if (strncmp(k, "xpath", n) == 0) {
					xmlChar *xpath = xpath_of(node);
					fprintf(f, "%s", (char *) xpath);
					xmlFree(xpath);
				}

				i += n + 1;
			}
		} else if (printFormat[i] == '\\') {
			switch (printFormat[i + 1]) {
				case 'n': fputc('\n', f); i++; break;
				case 't': fputc('\t', f); i++; break;
				case '0': fputc('\0', f); i++; break;
				default:  fputc(printFormat[i], f);
			}
		} else {
			fputc(printFormat[i], f);
		}
	}

	fputc('\n', f);
}

/* Print a reference which is matched in the filesystem. */
static void printMatched(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	puts(ref);
}
static void printMatchedSrc(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	printf("%s: %s\n", src, ref);
}
static void printMatchedSrcLine(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	printf("%s (%ld): %s\n", src, xmlGetLineNo(node), ref);
}
static void printMatchedXml(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	xmlChar *s, *r, *f, *xpath;
	xmlDocPtr doc;

	if (!node) {
		return;
	}

	s = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST src);
	r = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST ref);
	f = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST fname);
	xpath = xpath_of(node);

	printf("<found>");

	printf("<ref>");
	doc = xmlNewDoc(BAD_CAST "1.0");
	if (node->type == XML_ATTRIBUTE_NODE) {
		xmlDocSetRootElement(doc, xmlCopyNode(node->parent, 1));
	} else {
		xmlDocSetRootElement(doc, xmlCopyNode(node, 1));
	}
	xmlShellPrintNode(xmlDocGetRootElement(doc));
	xmlFreeDoc(doc);
	printf("</ref>");

	printf("<source line=\"%ld\" xpath=\"%s\">%s</source>", xmlGetLineNo(node), xpath, s);
	printf("<code>%s</code>", r);
	if (f) {
		printf("<filename>%s</filename>", f);
	}

	printf("</found>");

	xmlFree(s);
	xmlFree(r);
	xmlFree(xpath);
}
static void printMatchedWhereUsed(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	printf("%s\n", src);
}
static void printMatchedCustom(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	processFormatStr(stdout, node, src, ref, fname);
}

static void execMatched(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	execfile(execStr, fname);
}

/* Print an error for references which are unmatched. */
static void printUnmatched(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	fprintf(stderr, ERR_PREFIX "Unmatched reference: %s\n", ref);
}
static void printUnmatchedSrc(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	fprintf(stderr, ERR_PREFIX "%s: Unmatched reference: %s\n", src, ref);
}
static void printUnmatchedSrcLine(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	fprintf(stderr, ERR_PREFIX "%s (%ld): Unmatched reference: %s\n", src, xmlGetLineNo(node), ref);
}
static void printUnmatchedXml(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	xmlChar *s, *r, *f, *xpath;
	xmlDocPtr doc;

	s = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST src);
	r = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST ref);
	f = xmlEncodeEntitiesReentrant(node->doc, BAD_CAST fname);
	xpath = xpath_of(node);

	printf("<missing>");

	printf("<ref>");
	doc = xmlNewDoc(BAD_CAST "1.0");
	if (node->type == XML_ATTRIBUTE_NODE) {
		xmlDocSetRootElement(doc, xmlCopyNode(node->parent, 1));
	} else {
		xmlDocSetRootElement(doc, xmlCopyNode(node, 1));
	}
	xmlShellPrintNode(xmlDocGetRootElement(doc));
	xmlFreeDoc(doc);
	printf("</ref>");

	printf("<source line=\"%ld\" xpath=\"%s\">%s</source>", xmlGetLineNo(node), xpath, s);
	printf("<code>%s</code>", r);
	if (f) {
		printf("<filename>%s</filename>", f);
	}

	printf("</missing>");

	xmlFree(s);
	xmlFree(r);
	xmlFree(xpath);
}
static void printUnmatchedCustom(xmlNodePtr node, const char *src, const char *ref, const char *fname)
{
	fputs(ERR_PREFIX "Unmatched reference: ", stderr);
	processFormatStr(stderr, node, src, ref, fname);
}

static void (*printMatchedFn)(xmlNodePtr, const char *, const char *, const char *) = printMatched;
static void (*printUnmatchedFn)(xmlNodePtr, const char *, const char*, const char *) = printUnmatched;

static bool exact_match(char *dst, const char *code)
{
	char *s, *base;
	bool match;

	s = strdup(dst);
	base = basename(s);

	match = strrchr(base, '.') - base == strlen(code);

	free(s);

	return match;
}

/* Match a code to a file name. */
static bool find_object_fname(char *dst, const char *dir, const char *code, bool recursive)
{
	return find_csdb_object(dst, dir, code, NULL, recursive) && (looseMatch || exact_match(dst, code));
}

/* Tag unmatched references in the source object. */
static void tagUnmatchedRef(xmlNodePtr ref)
{
	add_first_child(ref, xmlNewPI(BAD_CAST "unmatched", NULL));
}

/* Get the DMC as a string from a dmRef. */
static void getDmCode(char *dst, xmlNodePtr dmRef)
{
	char *modelIdentCode;
	char *systemDiffCode;
	char *systemCode;
	char *subSystemCode;
	char *subSubSystemCode;
	char *assyCode;
	char *disassyCode;
	char *disassyCodeVariant;
	char *infoCode;
	char *infoCodeVariant;
	char *itemLocationCode;
	char *learnCode;
	char *learnEventCode;

	xmlNodePtr identExtension, dmCode, issueInfo, language;

	identExtension = firstXPathNode(NULL, dmRef, BAD_CAST "dmRefIdent/identExtension|dmcextension");
	dmCode = firstXPathNode(NULL, dmRef, BAD_CAST "dmRefIdent/dmCode|dmc/avee|avee");

	if (ignoreIss) {
		issueInfo = NULL;
	} else {
		issueInfo = firstXPathNode(NULL, dmRef, BAD_CAST "dmRefIdent/issueInfo|issno");
	}

	language = firstXPathNode(NULL, dmRef, BAD_CAST "dmRefIdent/language|language");

	strcpy(dst, "");

	if (identExtension) {
		char *extensionProducer, *extensionCode;

		extensionProducer = (char *) firstXPathValue(NULL, identExtension, BAD_CAST "@extensionProducer|dmeproducer");
		extensionCode     = (char *) firstXPathValue(NULL, identExtension, BAD_CAST "@extensionCode|dmecode");

		strcat(dst, "DME-");

		strcat(dst, extensionProducer);
		strcat(dst, "-");
		strcat(dst, extensionCode);
		strcat(dst, "-");

		xmlFree(extensionProducer);
		xmlFree(extensionCode);
	} else {
		strcat(dst, "DMC-");
	}

	modelIdentCode     = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@modelIdentCode|modelic");
	systemDiffCode     = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@systemDiffCode|sdc");
	systemCode         = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@systemCode|chapnum");
	subSystemCode      = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@subSystemCode|section");
	subSubSystemCode   = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@subSubSystemCode|subsect");
	assyCode           = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@assyCode|subject");
	disassyCode        = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@disassyCode|discode");
	disassyCodeVariant = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@disassyCodeVariant|discodev");
	infoCode           = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@infoCode|incode");
	infoCodeVariant    = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@infoCodeVariant|incodev");
	itemLocationCode   = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@itemLocationCode|itemloc");
	learnCode          = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@learnCode");
	learnEventCode     = (char *) firstXPathValue(NULL, dmCode, BAD_CAST "@learnEventCode");

	if (modelIdentCode) {
		strcat(dst, modelIdentCode);
		strcat(dst, "-");
		strcat(dst, systemDiffCode);
		strcat(dst, "-");
		strcat(dst, systemCode);
		strcat(dst, "-");
		strcat(dst, subSystemCode);
		strcat(dst, subSubSystemCode);
		strcat(dst, "-");
		strcat(dst, assyCode);
		strcat(dst, "-");
		strcat(dst, disassyCode);
		strcat(dst, disassyCodeVariant);
		strcat(dst, "-");
		strcat(dst, infoCode);
		strcat(dst, infoCodeVariant);
		strcat(dst, "-");
		strcat(dst, itemLocationCode);

		if (learnCode) {
			strcat(dst, "-");
			strcat(dst, learnCode);
			strcat(dst, learnEventCode);
		}
	}

	xmlFree(modelIdentCode);
	xmlFree(systemDiffCode);
	xmlFree(systemCode);
	xmlFree(subSystemCode);
	xmlFree(subSubSystemCode);
	xmlFree(assyCode);
	xmlFree(disassyCode);
	xmlFree(disassyCodeVariant);
	xmlFree(infoCode);
	xmlFree(infoCodeVariant);
	xmlFree(itemLocationCode);
	xmlFree(learnCode);
	xmlFree(learnEventCode);

	if (!noIssue) {
		if (issueInfo) {
			char *issueNumber, *inWork;

			issueNumber = (char *) firstXPathValue(NULL, issueInfo, BAD_CAST "@issueNumber|@issno");
			inWork      = (char *) firstXPathValue(NULL, issueInfo, BAD_CAST "@inWork|@inwork");

			if (!inWork) {
				inWork = strdup("00");
			}

			strcat(dst, "_");
			strcat(dst, issueNumber);
			strcat(dst, "-");
			strcat(dst, inWork);

			xmlFree(issueNumber);
			xmlFree(inWork);
		} else if (language) {
			strcat(dst, "_\?\?\?-\?\?");
		}
	}

	if (language) {
		char *languageIsoCode, *countryIsoCode;

		languageIsoCode = (char *) firstXPathValue(NULL, language, BAD_CAST "@languageIsoCode|@language");
		countryIsoCode  = (char *) firstXPathValue(NULL, language, BAD_CAST "@countryIsoCode|@country");

		uppercase(languageIsoCode);

		strcat(dst, "_");
		strcat(dst, languageIsoCode);
		strcat(dst, "-");
		strcat(dst, countryIsoCode);

		xmlFree(languageIsoCode);
		xmlFree(countryIsoCode);
	}
}

/* Get the PMC as a string from a pmRef. */
static void getPmCode(char *dst, xmlNodePtr pmRef)
{
	xmlNodePtr identExtension, pmCode, issueInfo, language;

	char *modelIdentCode;
	char *pmIssuer;
	char *pmNumber;
	char *pmVolume;

	identExtension = firstXPathNode(NULL, pmRef, BAD_CAST "pmRefIdent/identExtension");
	pmCode         = firstXPathNode(NULL, pmRef, BAD_CAST "pmRefIdent/pmCode|pmc");

	if (ignoreIss) {
		issueInfo = NULL;
	} else {
		issueInfo = firstXPathNode(NULL, pmRef, BAD_CAST "pmRefIdent/issueInfo|issno");
	}

	language = firstXPathNode(NULL, pmRef, BAD_CAST "pmRefIdent/language|language");

	strcpy(dst, "");

	if (identExtension) {
		char *extensionProducer, *extensionCode;

		extensionProducer = (char *) xmlGetProp(identExtension, BAD_CAST "extensionProducer");
		extensionCode     = (char *) xmlGetProp(identExtension, BAD_CAST "extensionCode");

		strcat(dst, "PME-");

		strcat(dst, extensionProducer);
		strcat(dst, "-");
		strcat(dst, extensionCode);
		strcat(dst, "-");

		xmlFree(extensionProducer);
		xmlFree(extensionCode);
	} else {
		strcat(dst, "PMC-");
	}

	modelIdentCode = (char *) firstXPathValue(NULL, pmCode, BAD_CAST "@modelIdentCode|modelic");
	pmIssuer       = (char *) firstXPathValue(NULL, pmCode, BAD_CAST "@pmIssuer|pmissuer");
	pmNumber       = (char *) firstXPathValue(NULL, pmCode, BAD_CAST "@pmNumber|pmnumber");
	pmVolume       = (char *) firstXPathValue(NULL, pmCode, BAD_CAST "@pmVolume|pmvolume");

	strcat(dst, modelIdentCode);
	strcat(dst, "-");
	strcat(dst, pmIssuer);
	strcat(dst, "-");
	strcat(dst, pmNumber);
	strcat(dst, "-");
	strcat(dst, pmVolume);

	xmlFree(modelIdentCode);
	xmlFree(pmIssuer);
	xmlFree(pmNumber);
	xmlFree(pmVolume);

	if (!noIssue) {
		if (issueInfo) {
			char *issueNumber, *inWork;

			issueNumber = (char *) firstXPathValue(NULL, issueInfo, BAD_CAST "@issueNumber|@issno");
			inWork      = (char *) firstXPathValue(NULL, issueInfo, BAD_CAST "@inWork|@inwork");

			strcat(dst, "_");
			strcat(dst, issueNumber);
			strcat(dst, "-");
			strcat(dst, inWork);

			xmlFree(issueNumber);
			xmlFree(inWork);
		} else if (language) {
			strcat(dst, "_\?\?\?-\?\?");
		}
	}

	if (language) {
		char *languageIsoCode, *countryIsoCode;

		languageIsoCode = (char *) firstXPathValue(NULL, language, BAD_CAST "@languageIsoCode|@language");
		countryIsoCode  = (char *) firstXPathValue(NULL, language, BAD_CAST "@countryIsoCode|@country");

		uppercase(languageIsoCode);

		strcat(dst, "_");
		strcat(dst, languageIsoCode);
		strcat(dst, "-");
		strcat(dst, countryIsoCode);

		xmlFree(languageIsoCode);
		xmlFree(countryIsoCode);
	}
}

/* Get the code of the source DM or PM. */
static void getSourceIdent(char *dst, xmlNodePtr sourceIdent)
{
	xmlDocPtr refdoc;
	xmlNodePtr ref, ident;

	refdoc = xmlNewDoc(BAD_CAST "1.0");
	ident = xmlCopyNode(sourceIdent, 1);

	if (xmlStrcmp(sourceIdent->name, BAD_CAST "sourcePmIdent") == 0) {
		ref = xmlNewNode(NULL, BAD_CAST "pmRef");
		xmlDocSetRootElement(refdoc, ref);
		xmlNodeSetName(ident, BAD_CAST "pmRefIdent");
		ident = xmlAddChild(ref, ident);

		getPmCode(dst, ref);
	} else {
		ref = xmlNewNode(NULL, BAD_CAST "dmRef");
		xmlDocSetRootElement(refdoc, ref);
		xmlNodeSetName(ident, BAD_CAST "dmRefIdent");
		ident = xmlAddChild(ref, ident);

		getDmCode(dst, ref);
	}

	xmlFreeDoc(refdoc);
}

/* Get the SMC as a string from a scormContentPackageRef. */
static void getSmcCode(char *dst, xmlNodePtr smcRef)
{
	xmlNodePtr identExtension, smcCode, issueInfo, language;

	char *modelIdentCode;
	char *smcIssuer;
	char *smcNumber;
	char *smcVolume;

	identExtension = firstXPathNode(NULL, smcRef, BAD_CAST "scormContentPackageRefIdent/identExtension");
	smcCode        = firstXPathNode(NULL, smcRef, BAD_CAST "scormContentPackageRefIdent/scormContentPackageCode");

	if (ignoreIss) {
		issueInfo = NULL;
	} else {
		issueInfo = firstXPathNode(NULL, smcRef, BAD_CAST "scormContentPackageRefIdent/issueInfo");
	}

	language = firstXPathNode(NULL, smcRef, BAD_CAST "scormContentPackageRefIdent/language");

	strcpy(dst, "");

	if (identExtension) {
		char *extensionProducer, *extensionCode;

		extensionProducer = (char *) xmlGetProp(identExtension, BAD_CAST "extensionProducer");
		extensionCode     = (char *) xmlGetProp(identExtension, BAD_CAST "extensionCode");

		strcat(dst, "SME-");

		if (extensionProducer && extensionCode) {
			strcat(dst, extensionProducer);
			strcat(dst, "-");
			strcat(dst, extensionCode);
			strcat(dst, "-");
		}

		xmlFree(extensionProducer);
		xmlFree(extensionCode);
	} else {
		strcat(dst, "SMC-");
	}

	modelIdentCode = (char *) xmlGetProp(smcCode, BAD_CAST "modelIdentCode");
	smcIssuer      = (char *) xmlGetProp(smcCode, BAD_CAST "scormContentPackageIssuer");
	smcNumber      = (char *) xmlGetProp(smcCode, BAD_CAST "scormContentPackageNumber");
	smcVolume      = (char *) xmlGetProp(smcCode, BAD_CAST "scormContentPackageVolume");

	if (modelIdentCode && smcIssuer && smcNumber && smcVolume) {
		strcat(dst, modelIdentCode);
		strcat(dst, "-");
		strcat(dst, smcIssuer);
		strcat(dst, "-");
		strcat(dst, smcNumber);
		strcat(dst, "-");
		strcat(dst, smcVolume);
	}

	xmlFree(modelIdentCode);
	xmlFree(smcIssuer);
	xmlFree(smcNumber);
	xmlFree(smcVolume);

	if (!noIssue) {
		if (issueInfo) {
			char *issueNumber, *inWork;

			issueNumber = (char *) xmlGetProp(issueInfo, BAD_CAST "issueNumber");
			inWork      = (char *) xmlGetProp(issueInfo, BAD_CAST "inWork");

			if (issueNumber && inWork) {
				strcat(dst, "_");
				strcat(dst, issueNumber);
				strcat(dst, "-");
				strcat(dst, inWork);
			}

			xmlFree(issueNumber);
			xmlFree(inWork);
		} else if (language) {
			strcat(dst, "_\?\?\?-\?\?");
		}
	}

	if (language) {
		char *languageIsoCode, *countryIsoCode;

		languageIsoCode = (char *) xmlGetProp(language, BAD_CAST "languageIsoCode");
		countryIsoCode  = (char *) xmlGetProp(language, BAD_CAST "countryIsoCode");

		if (languageIsoCode && countryIsoCode) {
			uppercase(languageIsoCode);

			strcat(dst, "_");
			strcat(dst, languageIsoCode);
			strcat(dst, "-");
			strcat(dst, countryIsoCode);
		}

		xmlFree(languageIsoCode);
		xmlFree(countryIsoCode);
	}
}

/* Get the ICN as a string from an ICN reference. */
static void getICN(char *dst, xmlNodePtr ref)
{
	char *icn;
	icn = (char *) xmlGetProp(ref, BAD_CAST "infoEntityRefIdent");
	strcpy(dst, icn);
	xmlFree(icn);
}

/* Get the ICN as a string from an ICN entity reference. */
static void getICNAttr(char *dst, xmlNodePtr ref)
{
	xmlChar *icn;
	xmlEntityPtr ent;
	icn = xmlNodeGetContent(ref);
	if ((ent = xmlGetDocEntity(ref->doc, icn)) && ent->URI) {
		char uri[PATH_MAX], *base;
		strcpy(uri, (char *) ent->URI);
		base = basename(uri);
		strcpy(dst, base);
	} else {
		strcpy(dst, (char *) icn);
	}

	/* Remove issue number when not doing a full match. */
	if (ignoreIss) {
		char *e = strrchr(dst, '-');
		char *s = e - 3;

		if (e && s >= dst) {
			*s = 0;
		}
	}

	xmlFree(icn);
}

/* Match each hotspot against the ICN. */
static int matchHotspot(xmlNodePtr ref, xmlDocPtr doc, const char *code, const char *fname, const char *src)
{
	xmlChar *apsid;
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	xmlNodePtr node;
	char *s;
	int err = doc == NULL;

	apsid = xmlNodeGetContent(ref);

	if (doc) {
		xmlNodePtr cur;

		ctx = xmlXPathNewContext(doc);

		/* Register namespaces for the hotspot XPath. */
		for (cur = hotspotNs->children; cur; cur = cur->next) {
			xmlChar *prefix, *uri;

			prefix = xmlGetProp(cur, BAD_CAST "prefix");
			uri = xmlGetProp(cur, BAD_CAST "uri");

			xmlXPathRegisterNs(ctx, prefix, uri);

			xmlFree(prefix);
			xmlFree(uri);
		}

		xmlXPathRegisterVariable(ctx, BAD_CAST "id", xmlXPathNewString(apsid));
		obj = xmlXPathEvalExpression(hotspotXPath, ctx);

		if (!obj || xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
			node = NULL;
		} else {
			node = obj->nodesetval->nodeTab[0];
		}

		xmlXPathFreeObject(obj);
		xmlXPathFreeContext(ctx);

		if (node) {
			if (showMatched && !tagUnmatched) {
				s = malloc(strlen(fname) + strlen((char *) apsid) + 2);
				strcpy(s, fname);
				strcat(s, "#");
				strcat(s, (char *) apsid);
				printMatchedFn(ref, src, s, fname);
				free(s);
			}
		} else {
			++err;
		}
	}

	if (err) {
		s = malloc(strlen(code) + strlen((char *) apsid) + 2);
		strcpy(s, code);
		strcat(s, "#");
		strcat(s, (char *) apsid);

		if (tagUnmatched) {
			tagUnmatchedRef(ref);
		} else if (showUnmatched) {
			printMatchedFn(ref, src, s, fname);
		} else if (verbosity >= NORMAL) {
			printUnmatchedFn(ref, src, s, fname);
		}

		free(s);
	}

	xmlFree(apsid);
	return err;
}

/* Match the hotspots for an XML-based ICN. */
static int getHotspots(xmlNodePtr ref, const char *src)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	int err = 0;

	ctx = xmlXPathNewContext(ref->doc);
	xmlXPathSetContextNode(ref, ctx);

	/* Select all hotspots that have an APS ID, meaning they point to some
	 * object in the ICN (vs. using coordinates).
	 */
	obj = xmlXPathEvalExpression(BAD_CAST ".//hotspot/@applicationStructureIdent|.//hotspot/@apsid", ctx);

	if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		xmlNodePtr icn;
		char code[PATH_MAX], fname[PATH_MAX];
		int i;
		xmlDocPtr doc;

		icn = firstXPathNode(ref->doc, ref, BAD_CAST "@infoEntityIdent|@boardno");

		getICNAttr(code, icn);

		if (find_object_fname(fname, directory, code, recursive)) {
			doc = read_xml_doc(fname);
		} else {
			doc = NULL;
		}

		if (remDelete) {
			rem_delete_elems(doc);
		}

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			err += matchHotspot(obj->nodesetval->nodeTab[i], doc, code, doc ? fname : code, src);
		}

		xmlFreeDoc(doc);
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return err;
}

/* Match a single referred fragment in another DM. */
static int matchFragment(xmlDocPtr doc, xmlNodePtr ref, const char *code, const char *fname, const char *src)
{
	xmlChar *id;
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	xmlNodePtr node;
	char *s;
	int err = doc == NULL;

	id = xmlNodeGetContent(ref);

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

		if (!obj || xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
			node = NULL;
		} else {
			node = obj->nodesetval->nodeTab[0];
		}

		xmlXPathFreeObject(obj);
		xmlXPathFreeContext(ctx);

		if (node) {
			if (showMatched && !tagUnmatched) {
				s = malloc(strlen(fname) + strlen((char *) id) + 2);
				strcpy(s, fname);
				strcat(s, "#");
				strcat(s, (char *) id);
				printMatchedFn(ref, src, s, fname);
				free(s);
			}
		} else {
			++err;
		}
	}

	if (err) {
		s = malloc(strlen(doc ? fname : code) + strlen((char *) id) + 2);
		strcpy(s, doc ? fname : code);
		strcat(s, "#");
		strcat(s, (char *) id);

		if (tagUnmatched) {
			tagUnmatchedRef(ref);
		} else if (showUnmatched) {
			printMatchedFn(ref, src, s, doc ? fname : NULL);
		} else if (verbosity >= NORMAL) {
			printUnmatchedFn(ref, src, s, doc ? fname : NULL);
		}

		free(s);
	}

	xmlFree(id);
	return err;

}

/* Match the referred fragments in another DM. */
static int getFragment(xmlNodePtr ref, const char *src)
{
	xmlNodePtr dmref;
	char code[PATH_MAX], fname[PATH_MAX];
	xmlDocPtr doc;
	int err;

	dmref = firstXPathNode(ref->doc, ref, BAD_CAST "ancestor::dmRef");

	getDmCode(code, dmref);

	if (find_object_fname(fname, directory, code, recursive)) {
		doc = read_xml_doc(fname);
	} else {
		doc = NULL;
	}

	if (remDelete) {
		rem_delete_elems(doc);
	}

	err = matchFragment(doc, ref, code, doc ? fname : code, src);

	xmlFreeDoc(doc);

	return err;
}

/* Get the comment code as a string from a commentRef. */
static void getComCode(char *dst, xmlNodePtr ref)
{
	xmlNodePtr commentCode, language;

	char *modelIdentCode;
	char *senderIdent;
	char *yearOfDataIssue;
	char *seqNumber;
	char *commentType;

	commentCode = firstXPathNode(NULL, ref, BAD_CAST "commentRefIdent/commentCode");

	language = firstXPathNode(NULL, ref, BAD_CAST "commentRefIdent/language");

	modelIdentCode  = (char *) xmlGetProp(commentCode, BAD_CAST "modelIdentCode");
	senderIdent     = (char *) xmlGetProp(commentCode, BAD_CAST "senderIdent");
	yearOfDataIssue = (char *) xmlGetProp(commentCode, BAD_CAST "yearOfDataIssue");
	seqNumber       = (char *) xmlGetProp(commentCode, BAD_CAST "seqNumber");
	commentType     = (char *) xmlGetProp(commentCode, BAD_CAST "commentType");

	strcpy(dst, "COM-");
	strcat(dst, modelIdentCode);
	strcat(dst, "-");
	strcat(dst, senderIdent);
	strcat(dst, "-");
	strcat(dst, yearOfDataIssue);
	strcat(dst, "-");
	strcat(dst, seqNumber);
	strcat(dst, "-");
	strcat(dst, commentType);

	xmlFree(modelIdentCode);
	xmlFree(senderIdent);
	xmlFree(yearOfDataIssue);
	xmlFree(seqNumber);
	xmlFree(commentType);

	if (language) {
		char *languageIsoCode, *countryIsoCode;

		languageIsoCode = (char *) xmlGetProp(language, BAD_CAST "languageIsoCode");
		countryIsoCode  = (char *) xmlGetProp(language, BAD_CAST "countryIsoCode");

		uppercase(languageIsoCode);

		strcat(dst, "_");
		strcat(dst, languageIsoCode);
		strcat(dst, "-");
		strcat(dst, countryIsoCode);

		xmlFree(languageIsoCode);
		xmlFree(countryIsoCode);
	}
}

/* Get the DML code as a string from a dmlRef. */
static void getDmlCode(char *dst, xmlNodePtr ref)
{
	xmlNodePtr dmlCode, issueInfo;

	char *modelIdentCode;
	char *senderIdent;
	char *dmlType;
	char *yearOfDataIssue;
	char *seqNumber;

	dmlCode   = firstXPathNode(NULL, ref, BAD_CAST "dmlRefIdent/dmlCode");
	issueInfo = firstXPathNode(NULL, ref, BAD_CAST "dmlRefIdent/issueInfo");

	modelIdentCode  = (char *) xmlGetProp(dmlCode, BAD_CAST "modelIdentCode");
	senderIdent     = (char *) xmlGetProp(dmlCode, BAD_CAST "senderIdent");
	dmlType         = (char *) xmlGetProp(dmlCode, BAD_CAST "dmlType");
	yearOfDataIssue = (char *) xmlGetProp(dmlCode, BAD_CAST "yearOfDataIssue");
	seqNumber       = (char *) xmlGetProp(dmlCode, BAD_CAST "seqNumber");

	uppercase(dmlType);

	strcpy(dst, "DML-");
	strcat(dst, modelIdentCode);
	strcat(dst, "-");
	strcat(dst, senderIdent);
	strcat(dst, "-");
	strcat(dst, dmlType);
	strcat(dst, "-");
	strcat(dst, yearOfDataIssue);
	strcat(dst, "-");
	strcat(dst, seqNumber);

	xmlFree(modelIdentCode);
	xmlFree(senderIdent);
	xmlFree(dmlType);
	xmlFree(yearOfDataIssue);
	xmlFree(seqNumber);

	if (issueInfo) {
		char *issueNumber, *inWork;

		issueNumber = (char *) xmlGetProp(issueInfo, BAD_CAST "issueNumber");
		inWork      = (char *) xmlGetProp(issueInfo, BAD_CAST "inWork");

		strcat(dst, "_");
		strcat(dst, issueNumber);
		strcat(dst, "-");
		strcat(dst, inWork);

		xmlFree(issueNumber);
		xmlFree(inWork);
	}
}

/* Get the external pub code as a string from an externalPubRef. */
static void getExternalPubCode(char *dst, xmlNodePtr ref)
{
	xmlNodePtr externalPubCode;
	char *code;

	externalPubCode = firstXPathNode(NULL, ref,
		BAD_CAST "externalPubRefIdent/externalPubCode|externalPubRefIdent/externalPubTitle|pubcode");

	if (externalPubCode) {
		code = (char *) xmlNodeGetContent(externalPubCode);
	} else {
		code = (char *) xmlNodeGetContent(ref);
	}

	strcpy(dst, code);

	xmlFree(code);
}

/* Get filename from DDN item. */
static void getDispatchFileName(char *dst, xmlNodePtr ref)
{
	char *fname;
	fname = (char *) xmlNodeGetContent(ref);
	strcpy(dst, fname);
	xmlFree(fname);
}

/* Get the disassembly code variant pattern using the figure number variant and
 * the specified format string. */
static xmlChar *formatFigNumVar(const xmlChar *figureNumberVariant)
{
	int i;
	xmlChar *disassyCodeVariant;

	disassyCodeVariant = xmlStrdup(figNumVarFormat);

	for (i = 0; disassyCodeVariant[i]; ++i) {
		switch (disassyCodeVariant[i]) {
			case '%':
				disassyCodeVariant[i] = figureNumberVariant[0];
				break;
			default:
				break;
		}
	}

	return disassyCodeVariant;
}

/* Parse an old (< 4.1) style CSN reference.
 *
 * refcsn (2.0-3.0)/catalogSeqNumberValue (4.0) is a 13-16 digit code:
 *
 * 13:  YY|Y|Y|  YY|YY|Y|NNN|Y (2-character system, 2-character assembly)
 * 14: YYY|Y|Y|  YY|YY|Y|NNN|Y (3-character system, 2-character assembly)
 * 15:  YY|Y|Y|YYYY|YY|Y|NNN|Y (2-character system, 4-character assembly)
 * 16: YYY|Y|Y|YYYY|YY|Y|NNN|Y (3-character system, 4-character assembly)
 *
 * Y = [A-Z0-9 ] (alphanumeric + space)
 * N = [0-9]     (numeric)
 */
#define CSN_VALUE_PATTERN_16 "%3[A-Z0-9 ]%1[A-Z0-9 ]%1[A-Z0-9 ]%4[A-Z0-9 ]%2[A-Z0-9]%1[A-Z0-9 ]%3[0-9]%1[A-Z0-9 ]"
#define CSN_VALUE_PATTERN_15 "%2[A-Z0-9 ]%1[A-Z0-9 ]%1[A-Z0-9 ]%4[A-Z0-9 ]%2[A-Z0-9]%1[A-Z0-9 ]%3[0-9]%1[A-Z0-9 ]"
#define CSN_VALUE_PATTERN_14 "%3[A-Z0-9 ]%1[A-Z0-9 ]%1[A-Z0-9 ]%2[A-Z0-9 ]%2[A-Z0-9]%1[A-Z0-9 ]%3[0-9]%1[A-Z0-9 ]"
#define CSN_VALUE_PATTERN_13 "%2[A-Z0-9 ]%1[A-Z0-9 ]%1[A-Z0-9 ]%2[A-Z0-9 ]%2[A-Z0-9]%1[A-Z0-9 ]%3[0-9]%1[A-Z0-9 ]"

static int str_is_blank(const char *s) {
	int i;
	for (i = 0; s[i]; ++i) {
		if (!isspace((unsigned char) s[i])) {
			return 0;
		}
	}
	return 1;
}

static void parseCsnValue(const xmlChar *csnValue,
	xmlChar **systemCode,
	xmlChar **subSystemCode,
	xmlChar **subSubSystemCode,
	xmlChar **assyCode,
	xmlChar **figureNumber,
	xmlChar **figureNumberVariant,
	xmlChar **item,
	xmlChar **itemVariant)
{
	char system[4];
	char subsys[2];
	char subsub[2];
	char assemb[5];
	char fignum[3];
	char figvar[2];
	char itemno[4];
	char itemva[2];
	const char *pattern;

	switch (xmlStrlen(csnValue)) {
		case 16: pattern = CSN_VALUE_PATTERN_16; break;
		case 15: pattern = CSN_VALUE_PATTERN_15; break;
		case 14: pattern = CSN_VALUE_PATTERN_14; break;
		case 13: pattern = CSN_VALUE_PATTERN_13; break;
		default: pattern = NULL; break;
	}

	if (pattern && sscanf((char *) csnValue, pattern,
			system,
			subsys,
			subsub,
			assemb,
			fignum,
			figvar,
			itemno,
			itemva) == 8) {
		*systemCode          = str_is_blank(system) ? NULL : xmlCharStrdup(system);
		*subSystemCode       = str_is_blank(subsys) ? NULL : xmlCharStrdup(subsys);
		*subSubSystemCode    = str_is_blank(subsub) ? NULL : xmlCharStrdup(subsub);
		*assyCode            = str_is_blank(assemb) ? NULL : xmlCharStrdup(assemb);
		*figureNumber        = xmlCharStrdup(fignum);
		*figureNumberVariant = str_is_blank(figvar) ? NULL : xmlCharStrdup(figvar);
		*item                = xmlCharStrdup(itemno);
		*itemVariant         = str_is_blank(itemva) ? NULL : xmlCharStrdup(itemva);
	} else {
		*systemCode          = NULL;
		*subSystemCode       = NULL;
		*subSubSystemCode    = NULL;
		*assyCode            = NULL;
		*figureNumber        = NULL;
		*figureNumberVariant = NULL;
		*item                = NULL;
		*itemVariant         = NULL;
	}
}

/* Get the code of a CSN ref, including IPD data module code, CSN and item number. */
static void getCsnCode(char *dst, xmlNodePtr ref, xmlChar **csnValue, xmlChar **item, xmlChar **itemVariant)
{
	xmlChar *modelIdentCode;
	xmlChar *systemDiffCode;
	xmlChar *systemCode;
	xmlChar *subSystemCode;
	xmlChar *subSubSystemCode;
	xmlChar *assyCode;
	xmlChar *figureNumber;
	xmlChar *figureNumberVariant;
	xmlChar *itemLocationCode;

	*csnValue = firstXPathValue(NULL, ref, BAD_CAST "@catalogSeqNumberValue|@refcsn");

	if (*csnValue) {
		modelIdentCode = NULL;
		systemDiffCode = NULL;
		itemLocationCode = NULL;

		parseCsnValue(*csnValue,
			&systemCode,
			&subSystemCode,
			&subSubSystemCode,
			&assyCode,
			&figureNumber,
			&figureNumberVariant,
			item,
			itemVariant);
	} else {
		modelIdentCode      = xmlGetProp(ref, BAD_CAST "modelIdentCode");
		systemDiffCode      = xmlGetProp(ref, BAD_CAST "systemDiffCode");
		systemCode          = xmlGetProp(ref, BAD_CAST "systemCode");
		subSystemCode       = xmlGetProp(ref, BAD_CAST "subSystemCode");
		subSubSystemCode    = xmlGetProp(ref, BAD_CAST "subSubSystemCode");
		assyCode            = xmlGetProp(ref, BAD_CAST "assyCode");
		figureNumber        = xmlGetProp(ref, BAD_CAST "figureNumber");
		figureNumberVariant = xmlGetProp(ref, BAD_CAST "figureNumberVariant");
		itemLocationCode    = xmlGetProp(ref, BAD_CAST "itemLocationCode");

		*item               = xmlGetProp(ref, BAD_CAST "item");
		*itemVariant        = xmlGetProp(ref, BAD_CAST "itemVariant");
	}

	/* Apply attributes to non-chapterized or old style CSN refs. */
	if (nonChapIpdSns || *csnValue) {
		xmlNodePtr dmCode = firstXPathNode(NULL, ref, BAD_CAST "ancestor::dmodule/identAndStatusSection/dmAddress/dmIdent/dmCode|ancestor::dmodule/idstatus/dmaddres/dmc/avee");

		if (dmCode) {
			/* These attributes are always interpreted as relative to the current DM. */
			if (!modelIdentCode) {
				modelIdentCode = firstXPathValue(NULL, dmCode, BAD_CAST "@modelIdentCode|modelic");
			}
			if (!systemDiffCode) {
				systemDiffCode = firstXPathValue(NULL, dmCode, BAD_CAST "@systemDiffCode|sdc");
			}

			/* Use wildcard for itemLocationCode if not given. */
			if (!itemLocationCode) {
				itemLocationCode = xmlCharStrdup("?");
			}

			/* If a non-chapterized IPD SNS is given, apply it. */
			if (nonChapIpdSns) {
				/* "-" indicates the SNS is also relative to the current DM. */
				if (strcmp(nonChapIpdSystemCode, "-") == 0) {
					if (!systemCode) {
						systemCode = firstXPathValue(NULL, dmCode,
							BAD_CAST "@systemCode|chapnum");
					}
					if (!subSystemCode) {
						subSystemCode = firstXPathValue(NULL, dmCode,
							BAD_CAST "@subSystemCode|section");
					}
					if (!subSubSystemCode) {
						subSubSystemCode = firstXPathValue(NULL, dmCode,
							BAD_CAST "@subSubSystemCode|subsect");
					}
					if (!assyCode) {
						assyCode = firstXPathValue(NULL, dmCode,
							BAD_CAST "@assyCode|subject");
					}
				/* Otherwise, construct the SNS from the given code. */
				} else {
					if (!systemCode) {
						systemCode = xmlCharStrdup(nonChapIpdSystemCode);
					}
					if (!subSystemCode) {
						subSystemCode = xmlCharStrdup(nonChapIpdSubSystemCode);
					}
					if (!subSubSystemCode) {
						subSubSystemCode = xmlCharStrdup(nonChapIpdSubSubSystemCode);
					}
					if (!assyCode) {
						assyCode = xmlCharStrdup(nonChapIpdAssyCode);
					}
				}
			}
		}
	}

	/* If CSN is chapterized, attempt to match it to a DMC. */
	if (modelIdentCode && systemDiffCode && systemCode && subSystemCode && subSubSystemCode && assyCode && figureNumber) {
		xmlDocPtr tmp;
		xmlNodePtr dmRef, dmRefIdent, dmCode;
		xmlChar *disassyCodeVariant;

		tmp = xmlNewDoc(BAD_CAST "1.0");

		dmRef = xmlNewNode(NULL, BAD_CAST "dmRef");
		dmRefIdent = xmlNewChild(dmRef, NULL, BAD_CAST "dmRefIdent", NULL);
		dmCode = xmlNewChild(dmRefIdent, NULL, BAD_CAST "dmCode", NULL);

		xmlDocSetRootElement(tmp, dmRef);

		if (!figureNumberVariant) {
			figureNumberVariant = xmlCharStrdup("0");
		}

		/* The figure number variant alone cannot fully determine the
		 * disassembly code variant of the IPD data module (for example,
		 * in projects where the disassembly code variant is more than 1
		 * character). Therefore, the figNumVarFormat pattern is used to
		 * construct the full disassemby code variant. */
		disassyCodeVariant = formatFigNumVar(figureNumberVariant);

		if (!itemLocationCode) {
			itemLocationCode = xmlCharStrdup("?");
		}

		xmlSetProp(dmCode, BAD_CAST "modelIdentCode", modelIdentCode);
		xmlSetProp(dmCode, BAD_CAST "systemDiffCode", systemDiffCode);
		xmlSetProp(dmCode, BAD_CAST "systemCode", systemCode);
		xmlSetProp(dmCode, BAD_CAST "subSystemCode", subSystemCode);
		xmlSetProp(dmCode, BAD_CAST "subSubSystemCode", subSubSystemCode);
		xmlSetProp(dmCode, BAD_CAST "assyCode", assyCode);
		xmlSetProp(dmCode, BAD_CAST "disassyCode", figureNumber);
		xmlSetProp(dmCode, BAD_CAST "disassyCodeVariant", disassyCodeVariant);
		xmlSetProp(dmCode, BAD_CAST "infoCode", BAD_CAST "941");
		xmlSetProp(dmCode, BAD_CAST "infoCodeVariant", BAD_CAST "A");
		xmlSetProp(dmCode, BAD_CAST "itemLocationCode", itemLocationCode);

		getDmCode(dst, dmRef);

		xmlFree(disassyCodeVariant);
		xmlFreeDoc(tmp);
	/* Otherwise, just return a generic IPD figure name. */
	} else {
		strcpy(dst, "Fig ");
		if (figureNumber) {
			strcat(dst, (char *) figureNumber);
		} else {
			strcat(dst, "??");
		}
		if (figureNumberVariant) {
			strcat(dst, (char *) figureNumberVariant);
		}
	}

	xmlFree(modelIdentCode);
	xmlFree(systemDiffCode);
	xmlFree(systemCode);
	xmlFree(subSystemCode);
	xmlFree(subSubSystemCode);
	xmlFree(assyCode);
	xmlFree(figureNumber);
	xmlFree(figureNumberVariant);
	xmlFree(itemLocationCode);
}

/* Get the code of an IPD data module only, discarding item number. */
static void getIpdCode(char *dst, xmlNodePtr ref)
{
	xmlChar *csn;
	xmlChar *item;
	xmlChar *itemVariant;

	getCsnCode(dst, ref, &csn, &item, &itemVariant);

	xmlFree(csn);
	xmlFree(item);
	xmlFree(itemVariant);
}

/* Match a CSN item in an IPD. */
static int matchCsnItem(xmlDocPtr doc, xmlNodePtr ref, xmlChar *csn,
	xmlChar *item, xmlChar *itemVariant, const char *code,
	const char *fname, const char *src)
{
	xmlChar *itemSeqNumberValue, *id;
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	xmlNodePtr node;
	char *s;
	int err = doc == NULL;

	itemSeqNumberValue = firstXPathValue(NULL, ref, BAD_CAST "@itemSeqNumberValue|@refisn");

	id = xmlCharStrdup("Item ");
	id = xmlStrcat(id, item);
	id = xmlStrcat(id, itemVariant);
	if (itemSeqNumberValue) {
		id = xmlStrcat(id, BAD_CAST " ISN ");
		id = xmlStrcat(id, itemSeqNumberValue);
	}

	if (doc) {
		ctx = xmlXPathNewContext(doc);
		xmlXPathRegisterVariable(ctx, BAD_CAST "item", xmlXPathNewString(item));
		xmlXPathRegisterVariable(ctx, BAD_CAST "itemVariant", xmlXPathNewString(itemVariant));
		xmlXPathRegisterVariable(ctx, BAD_CAST "csn", xmlXPathNewString(csn));

		if (itemSeqNumberValue) {
			xmlXPathRegisterVariable(ctx, BAD_CAST "isn", xmlXPathNewString(itemSeqNumberValue));
			obj = xmlXPathEvalExpression(BAD_CAST
				/* 4.1+ */ "//catalogSeqNumber[@item=$item and (not(@itemVariant) or not($itemVariant) or @itemVariant=$itemVariant)]/itemSeqNumber[@itemSeqNumberValue=$isn]|"
				/* 4.0  */ "//catalogSeqNumber[@catalogSeqNumberValue=$csn]/itemSequenceNumber[@itemSeqNumberValue=$isn]|"
				/* 3.0- */ "//csn[@csn=$csn]/isn[@isn=$isn]",
				ctx);
		} else {
			obj = xmlXPathEvalExpression(BAD_CAST
				/* 4.1+ */ "//catalogSeqNumber[@item=$item and (not(@itemVariant) or not($itemVariant) or @itemVariant=$itemVariant)]|"
				/* 4.0  */ "//catalogSeqNumber[@catalogSeqNumberValue=$csn]|"
				/* 3.0- */ "//csn[@csn=$csn]",
				ctx);
		}

		if (!obj || xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
			node = NULL;
		} else {
			node = obj->nodesetval->nodeTab[0];
		}

		xmlXPathFreeObject(obj);
		xmlXPathFreeContext(ctx);

		if (node) {
			if (showMatched && !tagUnmatched) {
				s = malloc(strlen(fname) + strlen((char *) id) + 2);

				strcpy(s, fname);
				strcat(s, " ");
				strcat(s, (char *) id);

				printMatchedFn(ref, src, s, fname);

				free(s);
			}
		} else {
			++err;
		}
	}

	if (err) {
		s = malloc(strlen(doc ? fname : code) + strlen((char *) id) + 2);
		strcpy(s, doc ? fname : code);
		strcat(s, " ");
		strcat(s, (char *) id);

		if (tagUnmatched) {
			tagUnmatchedRef(ref);
		} else if (showUnmatched) {
			printMatchedFn(ref, src, s, doc ? fname : NULL);
		} else if (verbosity >= NORMAL) {
			printUnmatchedFn(ref, src, s, doc ? fname : NULL);
		}

		free(s);
	}

	xmlFree(itemSeqNumberValue);
	xmlFree(id);

	return err;

}

/* Match the CSN items in another DM. */
static int getCsnItem(xmlNodePtr ref, const char *src)
{
	xmlNodePtr csnref;
	char code[PATH_MAX], fname[PATH_MAX];
	xmlDocPtr doc;
	int err;
	xmlChar *csn;
	xmlChar *item;
	xmlChar *itemVariant;

	csnref = ref->parent;

	getCsnCode(code, csnref, &csn, &item, &itemVariant);

	if (find_object_fname(fname, directory, code, recursive)) {
		doc = read_xml_doc(fname);
	} else {
		doc = NULL;
	}

	if (remDelete) {
		rem_delete_elems(doc);
	}

	err = matchCsnItem(doc, csnref, csn, item, itemVariant, code, doc ? fname : code, src);

	xmlFree(csn);
	xmlFree(item);
	xmlFree(itemVariant);

	xmlFreeDoc(doc);

	return err;
}

/* Update address items using the matched referenced object. */
static void updateRef(xmlNodePtr *refptr, const char *src, const char *code, const char *fname)
{
	xmlNodePtr ref = *refptr;

	if (verbosity >= DEBUG) {
		fprintf(stderr, I_UPDATE_REF, src, code, fname);
	}

	if (xmlStrcmp(ref->name, BAD_CAST "dmRef") == 0) {
		xmlDocPtr doc;
		xmlNodePtr dmRefAddressItems, dmTitle;
		xmlChar *techName, *infoName, *infoNameVariant;

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

		if (updateRefIdent) {
			xmlNodePtr dmRefIdent, refIssueInfo, refLanguage, issueInfo, language;

			dmRefIdent   = firstXPathNode(NULL, ref, BAD_CAST "dmRefIdent");
			refIssueInfo = firstXPathNode(NULL, dmRefIdent, BAD_CAST "issueInfo");
			refLanguage  = firstXPathNode(NULL, dmRefIdent, BAD_CAST "language");

			issueInfo = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//issueInfo|//issno"), 1);
			language  = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//language"), 1);

			/* 4.x references a 3.0 DM */
			if (xmlStrcmp(issueInfo->name, BAD_CAST "issno") == 0) {
				xmlNodeSetName(issueInfo, BAD_CAST "issueInfo");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(issueInfo,
					BAD_CAST "issno"),
					BAD_CAST "issueNumber");
				if (xmlHasProp(issueInfo, BAD_CAST "inwork")) {
					xmlNodeSetName((xmlNodePtr) xmlHasProp(issueInfo,
						BAD_CAST "inwork"),
						BAD_CAST "inWork");
				} else {
					xmlSetProp(issueInfo, BAD_CAST "inWork", BAD_CAST "00");
				}
				xmlUnsetProp(issueInfo, BAD_CAST "type");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(language,
					BAD_CAST "language"),
					BAD_CAST "languageIsoCode");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(language,
					BAD_CAST "country"),
					BAD_CAST "countryIsoCode");
			}

			if (refIssueInfo) {
				xmlUnlinkNode(refIssueInfo);
				xmlFreeNode(refIssueInfo);
			}
			if (refLanguage) {
				xmlUnlinkNode(refLanguage);
				xmlFreeNode(refLanguage);
			}

			xmlAddChild(dmRefIdent, issueInfo);
			xmlAddChild(dmRefIdent, language);
		}

		if ((dmRefAddressItems = firstXPathNode(NULL, ref, BAD_CAST "dmRefAddressItems"))) {
			xmlUnlinkNode(dmRefAddressItems);
			xmlFreeNode(dmRefAddressItems);
		}
		dmRefAddressItems = xmlNewChild(ref, NULL, BAD_CAST "dmRefAddressItems", NULL);

		techName = firstXPathValue(doc, NULL, BAD_CAST "//techName|//techname");
		infoName = firstXPathValue(doc, NULL, BAD_CAST "//infoName|//infoname");
		infoNameVariant = firstXPathValue(doc, NULL, BAD_CAST "//infoNameVariant");

		dmTitle = xmlNewChild(dmRefAddressItems, NULL, BAD_CAST "dmTitle", NULL);
		xmlNewTextChild(dmTitle, NULL, BAD_CAST "techName", techName);
		if (infoName) {
			xmlNewTextChild(dmTitle, NULL, BAD_CAST "infoName", infoName);
		}
		if (infoNameVariant) {
			xmlNewTextChild(dmTitle, NULL, BAD_CAST "infoNameVariant", infoNameVariant);
		}

		xmlFree(techName);
		xmlFree(infoName);

		if (updateRefIdent) {
			xmlNodePtr issueDate;

			issueDate = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//issueDate|//issdate"), 1);

			if (xmlStrcmp(issueDate->name, BAD_CAST "issdate")) {
				xmlNodeSetName(issueDate, BAD_CAST "issueDate");
			}

			xmlAddChild(dmRefAddressItems, issueDate);
		}

		xmlFreeDoc(doc);
	} else if (xmlStrcmp(ref->name, BAD_CAST "pmRef") == 0) {
		xmlDocPtr doc;
		xmlNodePtr pmRefAddressItems;
		xmlChar *pmTitle;

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

		if (updateRefIdent) {
			xmlNodePtr pmRefIdent, refIssueInfo, refLanguage, issueInfo, language;

			pmRefIdent   = firstXPathNode(NULL, ref, BAD_CAST "pmRefIdent");
			refIssueInfo = firstXPathNode(NULL, pmRefIdent, BAD_CAST "issueInfo");
			refLanguage  = firstXPathNode(NULL, pmRefIdent, BAD_CAST "language");
			issueInfo    = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//issueInfo|//issno"), 1);
			language     = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//language"), 1);

			/* 4.x references a 3.0 DM */
			if (xmlStrcmp(issueInfo->name, BAD_CAST "issno") == 0) {
				xmlNodeSetName(issueInfo, BAD_CAST "issueInfo");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(issueInfo,
					BAD_CAST "issno"),
					BAD_CAST "issueNumber");
				if (xmlHasProp(issueInfo, BAD_CAST "inwork")) {
					xmlNodeSetName((xmlNodePtr) xmlHasProp(issueInfo,
						BAD_CAST "inwork"),
						BAD_CAST "inWork");
				} else {
					xmlSetProp(issueInfo, BAD_CAST "inWork", BAD_CAST "00");
				}
				xmlUnsetProp(issueInfo, BAD_CAST "type");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(language,
					BAD_CAST "language"),
					BAD_CAST "languageIsoCode");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(language,
					BAD_CAST "country"),
					BAD_CAST "countryIsoCode");
			}

			if (refIssueInfo) {
				xmlUnlinkNode(refIssueInfo);
				xmlFreeNode(refIssueInfo);
			}
			if (refLanguage) {
				xmlUnlinkNode(refLanguage);
				xmlFreeNode(refLanguage);
			}

			xmlAddChild(pmRefIdent, issueInfo);
			xmlAddChild(pmRefIdent, language);
		}

		if ((pmRefAddressItems = firstXPathNode(NULL, ref, BAD_CAST "pmRefAddressItems"))) {
			xmlUnlinkNode(pmRefAddressItems);
			xmlFreeNode(pmRefAddressItems);
		}
		pmRefAddressItems = xmlNewChild(ref, NULL, BAD_CAST "pmRefAddressItems", NULL);

		pmTitle = firstXPathValue(doc, NULL, BAD_CAST "//pmTitle|//pmtitle");

		xmlNewTextChild(pmRefAddressItems, NULL, BAD_CAST "pmTitle", pmTitle);

		xmlFree(pmTitle);

		if (updateRefIdent) {
			xmlNodePtr issueDate;

			issueDate = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//issueDate|//issdate"), 1);

			if (xmlStrcmp(issueDate->name, BAD_CAST "issdate")) {
				xmlNodeSetName(issueDate, BAD_CAST "issueDate");
			}

			xmlAddChild(pmRefAddressItems, issueDate);
		}

		xmlFreeDoc(doc);
	} else if (xmlStrcmp(ref->name, BAD_CAST "refdm") == 0) {
		xmlDocPtr doc;
		xmlNodePtr oldtitle, newtitle;
		xmlChar *techname, *infoname;

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

		if (updateRefIdent) {
			xmlNodePtr oldissno, newissno, oldlanguage, newlanguage;

			newissno    = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//issueInfo|//issno"), 1);
			newlanguage = xmlCopyNode(firstXPathNode(doc, NULL, BAD_CAST "//language"), 1);

			/* 3.0 references a 4.x DM */
			if (xmlStrcmp(newissno->name, BAD_CAST "issueInfo") == 0) {
				xmlNodeSetName(newissno, BAD_CAST "issno");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(newissno,
					BAD_CAST "issueNumber"),
					BAD_CAST "issno");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(newissno,
					BAD_CAST "inWork"),
					BAD_CAST "inwork");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(newlanguage,
					BAD_CAST "languageIsoCode"),
					BAD_CAST "language");
				xmlNodeSetName((xmlNodePtr) xmlHasProp(newlanguage,
					BAD_CAST "countryIsoCode"),
					BAD_CAST "country");
			}

			if ((oldissno = firstXPathNode(NULL, ref, BAD_CAST "issno"))) {
				newissno = xmlAddNextSibling(oldissno, newissno);
			} else {
				newissno = xmlAddNextSibling(firstXPathNode(NULL, ref, BAD_CAST "avee"), newissno);
			}
			xmlUnlinkNode(oldissno);
			xmlFreeNode(oldissno);

			if ((oldlanguage = firstXPathNode(NULL, ref, BAD_CAST "language"))) {
				newlanguage = xmlAddNextSibling(oldlanguage, newlanguage);
			} else {
				newlanguage = xmlAddNextSibling(firstXPathNode(NULL, ref, BAD_CAST "issno"), newlanguage);
			}
			xmlUnlinkNode(oldlanguage);
			xmlFreeNode(oldlanguage);
		}

		techname = firstXPathValue(doc, NULL, BAD_CAST "//techName|//techname");
		infoname = firstXPathValue(doc, NULL, BAD_CAST "//infoName|//infoname");

		newtitle = xmlNewNode(NULL, BAD_CAST "dmtitle");

		if ((oldtitle = firstXPathNode(NULL, ref, BAD_CAST "dmtitle"))) {
			newtitle = xmlAddNextSibling(oldtitle, newtitle);
		} else {
			newtitle = xmlAddNextSibling(firstXPathNode(NULL, ref, BAD_CAST "(avee|issno)[last()]"), newtitle);
		}

		xmlNewTextChild(newtitle, NULL, BAD_CAST "techname", techname);
		if (infoname) {
			xmlNewTextChild(newtitle, NULL, BAD_CAST "infoname", infoname);
		}

		xmlFree(techname);
		xmlFree(infoname);

		xmlUnlinkNode(oldtitle);
		xmlFreeNode(oldtitle);
		xmlFreeDoc(doc);
	} else if (xmlStrcmp(ref->name, BAD_CAST "infoEntityIdent") == 0) {
		xmlChar *icn;
		xmlEntityPtr e;

		/* Remove old ICN entity. */
		icn = xmlNodeGetContent(ref);
		if ((e = xmlGetDocEntity(ref->doc, icn))) {
			xmlUnlinkNode((xmlNodePtr) e);
			xmlFreeEntity(e);
		}
		xmlFree(icn);

		/* Add new ICN entity. */
		e = add_icn(ref->doc, fname, false);
		xmlNodeSetContent(ref, e->name);
	} else if (xmlStrcmp(ref->name, BAD_CAST "externalPubRef") == 0) {
		xmlNodePtr new;
		xmlChar xpath[512];

		xmlStrPrintf(xpath, 512, "//externalPubRef[externalPubRefIdent/externalPubCode='%s']", code);

		if (!(new = firstXPathNode(externalPubs, NULL, xpath))) {
			return;
		}

		xmlAddNextSibling(ref, xmlCopyNode(new, 1));

		xmlUnlinkNode(ref);
		xmlFreeNode(ref);
		*refptr = NULL;
	}
}

static int listReferences(const char *path, int show, const char *targetRef, int targetShow);
static int listWhereUsed(const char *path, int show);

/* Print a reference found in an object. */
static int printReference(xmlNodePtr *refptr, const char *src, int show, const char *targetRef, int targetShow)
{
	char code[PATH_MAX];
	char fname[PATH_MAX];
	xmlNodePtr ref = *refptr;

	if ((show & SHOW_DMC) == SHOW_DMC &&
	    (xmlStrcmp(ref->name, BAD_CAST "dmRef") == 0 ||
	     xmlStrcmp(ref->name, BAD_CAST "refdm") == 0 ||
	     xmlStrcmp(ref->name, BAD_CAST "addresdm") == 0))
		getDmCode(code, ref);
	else if ((show & SHOW_PMC) == SHOW_PMC &&
		 (xmlStrcmp(ref->name, BAD_CAST "pmRef") == 0 ||
	          xmlStrcmp(ref->name, BAD_CAST "refpm") == 0))
		getPmCode(code, ref);
	else if ((show & SHOW_SMC) == SHOW_SMC && xmlStrcmp(ref->name, BAD_CAST "scormContentPackageRef") == 0)
		getSmcCode(code, ref);
	else if ((show & SHOW_ICN) == SHOW_ICN &&
	         (xmlStrcmp(ref->name, BAD_CAST "infoEntityRef") == 0))
		getICN(code, ref);
	else if ((show & SHOW_COM) == SHOW_COM && (xmlStrcmp(ref->name, BAD_CAST "commentRef") == 0))
		getComCode(code, ref);
	else if ((show & SHOW_DML) == SHOW_DML && (xmlStrcmp(ref->name, BAD_CAST "dmlRef") == 0))
		getDmlCode(code, ref);
	else if ((show & SHOW_ICN) == SHOW_ICN &&
		 (xmlStrcmp(ref->name, BAD_CAST "infoEntityIdent") == 0 ||
	          xmlStrcmp(ref->name, BAD_CAST "boardno") == 0))
		getICNAttr(code, ref);
	else if ((show & SHOW_EPR) == SHOW_EPR &&
	         (xmlStrcmp(ref->name, BAD_CAST "externalPubRef") == 0 ||
		  xmlStrcmp(ref->name, BAD_CAST "reftp") == 0))
		getExternalPubCode(code, ref);
	else if (xmlStrcmp(ref->name, BAD_CAST "dispatchFileName") == 0 ||
	         xmlStrcmp(ref->name, BAD_CAST "ddnfilen") == 0)
		getDispatchFileName(code, ref);
	else if ((show & SHOW_SRC) == SHOW_SRC &&
		(xmlStrcmp(ref->name, BAD_CAST "sourceDmIdent") == 0 ||
		 xmlStrcmp(ref->name, BAD_CAST "sourcePmIdent") == 0))
		getSourceIdent(code, ref);
	else if ((show & SHOW_REP) == SHOW_REP &&
		xmlStrcmp(ref->name, BAD_CAST "repositorySourceDmIdent") == 0)
		getSourceIdent(code, ref);
	else if ((show & SHOW_HOT) == SHOW_HOT &&
		 xmlStrcmp(ref->name, BAD_CAST "graphic") == 0)
		return getHotspots(ref, src);
	else if ((show & SHOW_FRG) == SHOW_FRG &&
		 (xmlStrcmp(ref->name, BAD_CAST "referredFragment") == 0 ||
		  xmlStrcmp(ref->name, BAD_CAST "target") == 0))
		return getFragment(ref, src);
	else if ((show & SHOW_IPD) == SHOW_IPD &&
		 (xmlStrcmp(ref->name, BAD_CAST "catalogSeqNumberRef") == 0 ||
		  xmlStrcmp(ref->name, BAD_CAST "csnref") == 0))
		getIpdCode(code, ref);
	else if ((show & SHOW_CSN) == SHOW_CSN &&
		 (xmlStrcmp(ref->name, BAD_CAST "item") == 0 ||
		  xmlStrcmp(ref->name, BAD_CAST "catalogSeqNumberValue") == 0 ||
		  xmlStrcmp(ref->name, BAD_CAST "refcsn") == 0))
		return getCsnItem(ref, src);
	else
		return 0;

	if (targetRef) {
		/* If looking for a particular ref in -w mode, skip any others. */
		if (!strnmatch(targetRef, code, strlen(code))) {
			return 0;
		}

		/* Replace the code with the target ref so as to match that
		 * specific object rather than the latest object with the same
		 * code. */
		strcpy(code, targetRef);
	}

	if (find_object_fname(fname, directory, code, recursive)) {
		if (updateRefs) {
			updateRef(refptr, src, code, fname);
		} else if (!tagUnmatched) {
			if (showMatched) {
				printMatchedFn(ref, src, fname, fname);
			}

			if (listRecursively) {
				if (targetRef) {
					listWhereUsed(src, targetShow);
				} else {
					listReferences(fname, show, NULL, 0);
				}
			}
		}
		return 0;
	} else if (tagUnmatched) {
		tagUnmatchedRef(ref);
	} else if (showUnmatched) {
		printMatchedFn(ref, src, code, NULL);
	} else if (verbosity >= NORMAL) {
		printUnmatchedFn(ref, src, code, NULL);
	}

	/* Update metadata for unmatched external pubs. */
	if (updateRefs && externalPubs && xmlStrcmp(ref->name, BAD_CAST "externalPubRef") == 0) {
		updateRef(refptr, src, code, fname);
	}

	return 1;
}

/* Check if a file has already been listed when listing recursively. */
static bool listedFile(const char *path)
{
	int i;
	for (i = 0; i < numListedFiles; ++i) {
		if (strcmp(listedFiles[i], path) == 0) {
			return true;
		}
	}
	return false;
}

/* Add a file to the list of files already checked. */
static void addFile(const char *path)
{
	if (!listedFiles || numListedFiles == maxListedFiles) {
		if (!(listedFiles = realloc(listedFiles, (maxListedFiles *= 2) * PATH_MAX))) {
			fprintf(stderr, E_OUT_OF_MEMORY);
			exit(EXIT_OUT_OF_MEMORY);
		}
	}

	strcpy(listedFiles[numListedFiles++], path);
}

/* XPath to select all possible types of references. */
#define REFS_XPATH BAD_CAST \
	".//dmRef|.//refdm|.//addresdm|" \
	".//pmRef|.//refpm|" \
	".//infoEntityRef|//@infoEntityIdent|//@boardno|" \
	".//commentRef|" \
	".//dmlRef|" \
	".//externalPubRef|.//reftp|" \
	".//dispatchFileName|.//ddnfilen|" \
	".//graphic[hotspot]|" \
	".//dmRef/@referredFragment|.//refdm/@target|" \
	".//scormContentPackageRef|" \
	".//sourceDmIdent|.//sourcePmIdent|.//repositorySourceDmIdent|" \
	".//catalogSeqNumberRef|.//csnref|" \
	".//catalogSeqNumberRef/@item|.//catalogSeqNumberRef/@catalogSeqNumberValue|.//@refcsn"

/* List all references in the given object. */
static int listReferences(const char *path, int show, const char *targetRef, int targetShow)
{
	xmlDocPtr doc;
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	int unmatched = 0;
	xmlDocPtr validTree = NULL;

	/* In recursive mode, keep a record of which files have been listed
	 * to avoid infinite loops.
	 *
	 * If this is invoked in -w mode (targetRef != NULL), don't update the
	 * record, as that is handled by listWhereUsed.
	 */
	if (listRecursively && targetRef == NULL) {
		if (listedFile(path)) {
			return 0;
		}

		addFile(path);
	}

	if (listSrc) {
		printMatchedFn(NULL, path, path, path);
	}

	if (!(doc = read_xml_doc(path))) {
		if (strcmp(path, "-") == 0) {
			fprintf(stderr, E_BAD_STDIN);
			exit(EXIT_BAD_STDIN);
		}

		return 0;
	}

	/* Make a copy of the XML tree before performing extra
	 * processing on it. */
	if (outputTree) {
		validTree = xmlCopyDoc(doc, 1);
	}

	/* Remove elements marked as "delete". */
	if (remDelete) {
		rem_delete_elems(doc);
	}

	ctx = xmlXPathNewContext(doc);

	if (contentOnly)
		ctx->node = firstXPathNode(doc, NULL,
			BAD_CAST "//content|//dmlContent|//dml|//ddnContent|//delivlst");
	else
		ctx->node = xmlDocGetRootElement(doc);

	obj = xmlXPathEvalExpression(REFS_XPATH, ctx);

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

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			unmatched += printReference(&(obj->nodesetval->nodeTab[i]), path, show, targetRef, targetShow);
		}
	}

	/* Write valid CSDB object to stdout. */
	if (outputTree) {
		if (unmatched == 0) {
			save_xml_doc(validTree, "-");
		}
		xmlFreeDoc(validTree);
	}

	/* If the given object was modified by updating matched refs or
	 * tagging unmatched refs, write the changes.
	 */
	if (updateRefs || tagUnmatched) {
		if (overwriteUpdated) {
			save_xml_doc(doc, path);
		} else {
			save_xml_doc(doc, "-");
		}
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);
	xmlFreeDoc(doc);

	if (verbosity >= VERBOSE && !targetRef) {
		fprintf(stderr, unmatched ? F_UNMATCHED : S_UNMATCHED, path);
	}

	return unmatched;
}

/* Parse a list of filenames as input. */
static int listReferencesInList(const char *path, int show)
{
	FILE *f;
	char line[PATH_MAX];
	int unmatched = 0;

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

	while (fgets(line, PATH_MAX, f)) {
		strtok(line, "\t\r\n");
		unmatched += listReferences(line, show, NULL, 0);
	}

	if (path) {
		fclose(f);
	}

	return unmatched;
}

/* Register a NS for the hotspot XPath expression. */
static void addHotspotNs(char *s)
{
	char *prefix, *uri;
	xmlNodePtr node;

	prefix = strtok(s, "=");
	uri = strtok(NULL, "");

	node = xmlNewChild(hotspotNs, NULL, BAD_CAST "ns", NULL);
	xmlSetProp(node, BAD_CAST "prefix", BAD_CAST prefix);
	xmlSetProp(node, BAD_CAST "uri", BAD_CAST uri);
}

/* Determine if an object is a type that may contain references to other
 * objects. */
static bool isUsedTarget(const char *name, int show)
{
	return
		(optset(show, SHOW_COM) && is_com(name)) ||
		(optset(show, SHOW_DMC) && is_dm(name))  ||
		(optset(show, SHOW_DML) && is_dml(name)) ||
		(optset(show, SHOW_PMC) && is_pm(name))  ||
		(optset(show, SHOW_SMC) && is_smc(name));
}

/* Search objects in a given directory for references to a target object. */
static int findWhereUsed(const char *dpath, const char *ref, int show)
{
	DIR *dir;
	struct dirent *cur;
	char fpath[PATH_MAX], cpath[PATH_MAX];
	int unmatched = 0;

	if (!(dir = opendir(dpath))) {
		return 1;
	}

	if (strcmp(dpath, ".") == 0) {
		strcpy(fpath, "");
	} else if (dpath[strlen(dpath) - 1] != '/') {
		strcpy(fpath, dpath);
		strcat(fpath, "/");
	} else {
		strcpy(fpath, dpath);
	}

	while ((cur = readdir(dir))) {
		strcpy(cpath, fpath);
		strcat(cpath, cur->d_name);

		if (recursive && isdir(cpath, true)) {
			unmatched += findWhereUsed(cpath, ref, show);
		} else if (isUsedTarget(cur->d_name, show)) {
			unmatched += listReferences(cpath, SHOW_WHERE_USED, ref, show);
		}
	}

	closedir(dir);

	return unmatched;
}

/* List objects that reference a target object. */
static int listWhereUsed(const char *path, int show)
{
	char code[PATH_MAX] = "";
	xmlDocPtr doc;

	/* In recursive mode, keep a record of which objects have been listed
	 * to avoid infinite loops. */
	if (listRecursively) {
		if (listedFile(path)) {
			return 0;
		}

		addFile(path);
	}

	if (verbosity >= VERBOSE) {
		fprintf(stderr, I_WHEREUSED, path);
	}

	/* If the target object is an ICN, get the ICN from the file name. */
	if (is_icn(path)) {
		strcpy(code, path);
		strtok(code, ".");
	/* If the target object is an XML file, read the object and get the
	 * appropriate code from the IDSTATUS section. */
	} else if ((doc = read_xml_doc(path))) {
		xmlDocPtr tmp;
		xmlNodePtr ident, node;

		if (remDelete) {
			rem_delete_elems(doc);
		}

		ident = firstXPathNode(doc, NULL, BAD_CAST "//dmIdent|//pmIdent|//commentIdent|//dmlIdent|//scormContentPackageIdent");
		node  = xmlNewNode(NULL, BAD_CAST "ref");
		ident = xmlAddChild(node, xmlCopyNode(ident, 1));
		tmp   = xmlNewDoc(BAD_CAST "1.0");
		xmlDocSetRootElement(tmp, node);

		if (xmlStrcmp(ident->name, BAD_CAST "commentIdent") == 0) {
			xmlNodeSetName(ident, BAD_CAST "commentRefIdent");
			xmlNodeSetName(ident, BAD_CAST "commentRef");
			getComCode(code, node);
		} else if (xmlStrcmp(ident->name, BAD_CAST "dmIdent") == 0) {
			xmlNodeSetName(ident, BAD_CAST "dmRefIdent");
			xmlNodeSetName(node , BAD_CAST "dmRef");
			getDmCode(code, node);
		} else if (xmlStrcmp(ident->name, BAD_CAST "dmlIdent") == 0) {
			xmlNodeSetName(ident, BAD_CAST "dmlRefIdent");
			xmlNodeSetName(node , BAD_CAST "dmlRef");
			getDmlCode(code, node);
		} else if (xmlStrcmp(ident->name, BAD_CAST "pmIdent") == 0) {
			xmlNodeSetName(ident, BAD_CAST "pmRefIdent");
			xmlNodeSetName(node , BAD_CAST "pmRef");
			getPmCode(code, node);
		} else if (xmlStrcmp(ident->name, BAD_CAST "scormContentPackageIdent") == 0) {
			xmlNodeSetName(ident, BAD_CAST "scormContentPackageRefIdent");
			xmlNodeSetName(node , BAD_CAST "scormContentPackageRef");
			getSmcCode(code, node);
		} else if (xmlStrcmp(ident->name, BAD_CAST "sourceDmIdent") == 0 ||
			   xmlStrcmp(ident->name, BAD_CAST "sourcePmIdent") == 0 ||
			   xmlStrcmp(ident->name, BAD_CAST "repositorySourceDmIdent") == 0) {
			getSourceIdent(code, ident);
		}

		xmlFreeDoc(tmp);
		xmlFreeDoc(doc);
	/* Otherwise, interpret the path as a literal code. */
	} else {
		strcpy(code, path);
	}

	/* If no code could be determined, give up. */
	if (strcmp(code, "") == 0) {
		return 1;
	}

	return findWhereUsed(directory, code, show);
}

/* List objects that reference any of a list of target objects. */
static int listWhereUsedList(const char *path, int show)
{
	FILE *f;
	char line[PATH_MAX];
	int unmatched = 0;

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

	while (fgets(line, PATH_MAX, f)) {
		strtok(line, "\t\r\n");
		unmatched += listWhereUsed(line, show);
	}

	if (path) {
		fclose(f);
	}

	return unmatched;
}

/* Read a non-chapterized IPD SNS code. */
static void readnonChapIpdSns(const char *s)
{
	if (strcmp(s, "-") == 0) {
		strcpy(nonChapIpdSystemCode, s);
	} else {
		int n;

		n = sscanf(s, "%3[0-9A-Z]-%1[0-9A-Z]%1[0-9A-Z]-%4[0-9A-Z]",
			nonChapIpdSystemCode,
			nonChapIpdSubSystemCode,
			nonChapIpdSubSubSystemCode,
			nonChapIpdAssyCode);

		if (n != 4) {
			fprintf(stderr, E_BAD_CSN_CODE, s);
			exit(EXIT_BAD_CSN_CODE);
		}
	}
}

/* Display the usage message. */
static void show_help(void)
{
	puts("Usage: s1kd-refs [-aBCcDEFfGHIiKLlmNnoPqrSsTUuvwXxYZ^h?] [-b <SNS>] [-d <dir>] [-e <cmd>] [-J <ns=URL> ...] [-j <xpath>] [-k <pattern>] [-t <fmt>] [-3 <file>] [<object>...]");
	puts("");
	puts("Options:");
	puts("  -a, --all                    Print unmatched codes.");
	puts("  -B, --ipd                    List IPD references.");
	puts("  -b, --ipd-sns <SNS>          The SNS for non-chapterized IPDs.");
	puts("  -C, --com                    List comment references.");
	puts("  -c, --content                Only show references in content section.");
	puts("  -D, --dm                     List data module references.");
	puts("  -d, --dir                    Directory to search for matches in.");
	puts("  -E, --epr                    List external pub refs.");
	puts("  -e, --exec <cmd>             Execute <cmd> for each CSDB object matched.");
	puts("  -F, --overwrite              Overwrite updated (-U) or tagged (-X) objects.");
	puts("  -f, --filename               Print the source filename for each reference.");
	puts("  -G, --icn                    List ICN references.");
	puts("  -H, --hotspot                List hotspot matches in ICNs.");
	puts("  -h, -?, --help               Show help/usage message.");
	puts("  -I, --update-issue           Update references to point to the latest matched object.");
	puts("  -i, --ignore-issue           Ignore issue info when matching.");
	puts("  -J, --namespace <ns=URL>     Register a namespace for the hotspot XPath.");
	puts("  -j, --hotspot-xpath <xpath>  XPath to use for matching hotspots (-H).");
	puts("  -K, --csn                    List CSN references.");
	puts("  -k, --ipd-dcv <pattern>      Pattern for IPD disassembly code variant.");
	puts("  -L, --dml                    List DML references.");
	puts("  -l, --list                   Treat input as list of CSDB objects.");
	puts("  -m, --strict-match           Be more strict when matching filenames of objects.");
	puts("  -N, --omit-issue             Assume filenames omit issue info.");
	puts("  -n, --lineno                 Print the source filename and line number for each reference.");
	puts("  -o, --output-valid           Output valid CSDB objects to stdout.");
	puts("  -P, --pm                     List publication module references.");
	puts("  -q, --quiet                  Quiet mode.");
	puts("  -R, --recursively            List references in matched objects recursively.");
	puts("  -r, --recursive              Search for matches in directories recursively.");
	puts("  -S, --smc                    List SCORM content package references.");
	puts("  -s, --include-src            Include the source object as a reference.");
	puts("  -T, --fragment               List referred fragments in other DMs.");
	puts("  -t, --format <fmt>           The format to use when printing references.");
	puts("  -U, --update                 Update address items in matched references.");
	puts("  -u, --unmatched              Show only unmatched references.");
	puts("  -v, --verbose                Verbose output.");
	puts("  -w, --where-used             List places where an object is referenced.");
	puts("  -X, --tag-unmatched          Tag unmatched references.");
	puts("  -x, --xml                    Output XML report.");
	puts("  -Y, --repository             List repository source DMs.");
	puts("  -Z, --source                 List source DM or PM.");
	puts("  -3, --externalpubs <file>    Use custom .externalpubs file.");
	puts("  -^, --remove-deleted         List refs with elements marked as \"delete\" removed.");
	puts("  --version                    Show version information.");
	puts("  <object>                     CSDB object to list references in.");
	LIBXML2_PARSE_LONGOPT_HELP
}

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

int main(int argc, char **argv)
{
	int i, unmatched = 0;

	bool isList = false;
	bool xmlOutput = false;
	bool inclSrcFname = false;
	bool inclLineNum = false;
	char extpubsFname[PATH_MAX] = "";
	bool findUsed = false;

	/* Which types of object references will be listed. */
	int showObjects = 0;

	const char *sopts = "qcNaFfLlUuCDGPRrd:IinEXxSsove:mHj:J:Tt:3:wYZBKb:k:^h?";
	struct option lopts[] = {
		{"version"       , no_argument      , 0, 0},
		{"help"          , no_argument      , 0, 'h'},
		{"quiet"         , no_argument      , 0, 'q'},
		{"content"       , no_argument      , 0, 'c'},
		{"externalpubs"  , required_argument, 0, '3'},
		{"omit-issue"    , no_argument      , 0, 'N'},
		{"all"           , no_argument      , 0, 'a'},
		{"overwrite"     , no_argument      , 0, 'F'},
		{"filename"      , no_argument      , 0, 'f'},
		{"dml"           , no_argument      , 0, 'L'},
		{"list"          , no_argument      , 0, 'l'},
		{"update"        , no_argument      , 0, 'U'},
		{"unmatched"     , no_argument      , 0, 'u'},
		{"com"           , no_argument      , 0, 'C'},
		{"dm"            , no_argument      , 0, 'D'},
		{"icn"           , no_argument      , 0, 'G'},
		{"pm"            , no_argument      , 0, 'P'},
		{"recursively"   , no_argument      , 0, 'R'},
		{"recursive"     , no_argument      , 0, 'r'},
		{"dir"           , required_argument, 0, 'd'},
		{"update-issue"  , no_argument      , 0, 'I'},
		{"ignore-issue"  , no_argument      , 0, 'i'},
		{"lineno"        , no_argument      , 0, 'n'},
		{"epr"           , no_argument      , 0, 'E'},
		{"exec"          , required_argument, 0, 'e'},
		{"tag-unmatched" , no_argument      , 0, 'X'},
		{"xml"           , no_argument      , 0, 'x'},
		{"smc"           , no_argument      , 0, 'S'},
		{"include-src"   , no_argument      , 0, 's'},
		{"output-valid"  , no_argument      , 0, 'o'},
		{"verbose"       , no_argument      , 0, 'v'},
		{"strict-match"  , no_argument      , 0, 'm'},
		{"hotspot"       , no_argument      , 0, 'H'},
		{"hotspot-xpath" , required_argument, 0, 'j'},
		{"namespace"     , required_argument, 0, 'J'},
		{"fragment"      , no_argument      , 0, 'T'},
		{"format"        , required_argument, 0, 't'},
		{"where-used"    , no_argument      , 0, 'w'},
		{"repository"    , no_argument      , 0, 'Y'},
		{"source"        , no_argument      , 0, 'Z'},
		{"ipd"           , no_argument      , 0, 'B'},
		{"csn"           , no_argument      , 0, 'K'},
		{"ipd-sns"       , required_argument, 0, 'b'},
		{"ipd-dcv"       , required_argument, 0, 'k'},
		{"remove-deleted", no_argument      , 0, '^'},
		LIBXML2_PARSE_LONGOPT_DEFS
		{0, 0, 0, 0}
	};
	int loptind = 0;

	directory = strdup(".");
	hotspotXPath = xmlStrdup(DEFAULT_HOTSPOT_XPATH);
	hotspotNs = xmlNewNode(NULL, BAD_CAST "hotspotNs");

	figNumVarFormat = xmlCharStrdup("%");

	while ((i = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
		switch (i) {
			case 0:
				if (strcmp(lopts[loptind].name, "version") == 0) {
					show_version();
					return 0;
				}
				LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
				break;
			case 'q':
				--verbosity;
				break;
			case 'c':
				contentOnly = true;
				break;
			case '3':
				strncpy(extpubsFname, optarg, PATH_MAX - 1);
				break;
			case 'N':
				noIssue = true;
				break;
			case 'a':
				showUnmatched = true;
				break;
			case 'F':
				overwriteUpdated = true;
				break;
			case 'f':
				inclSrcFname = true;
				break;
			case 'H':
				showObjects |= SHOW_HOT;
				break;
			case 'L':
				showObjects |= SHOW_DML;
				break;
			case 'l':
				isList = true;
				break;
			case 'U':
				updateRefs = true;
				break;
			case 'u':
				showMatched = false;
				break;
			case 'C':
				showObjects |= SHOW_COM;
				break;
			case 'D':
				showObjects |= SHOW_DMC;
				break;
			case 'G':
				showObjects |= SHOW_ICN;
				break;
			case 'P':
				showObjects |= SHOW_PMC;
				break;
			case 'R':
				listRecursively = true;
				break;
			case 'r':
				recursive = true;
				break;
			case 'd':
				free(directory);
				directory = strdup(optarg);
				break;
			case 'I':
				updateRefs = true;
				ignoreIss = true;
				updateRefIdent = true;
				break;
			case 'i':
				ignoreIss = true;
				break;
			case 'n':
				inclSrcFname = true;
				inclLineNum = true;
				break;
			case 'E':
				showObjects |= SHOW_EPR;
				break;
			case 'X':
				tagUnmatched = true;
				break;
			case 'x':
				xmlOutput = true;
				break;
			case 'S':
				showObjects |= SHOW_SMC;
				break;
			case 's':
				listSrc = true;
				break;
			case 'o':
				outputTree = true;
				break;
			case 'v':
				++verbosity;
				break;
			case 'm':
				looseMatch = false;
				break;
			case 'j':
				xmlFree(hotspotXPath);
				hotspotXPath = xmlStrdup(BAD_CAST optarg);
				break;
			case 'J':
				addHotspotNs(optarg);
				break;
			case 'T':
				showObjects |= SHOW_FRG;
				break;
			case 't':
				printFormat = strdup(optarg);
				break;
			case 'e':
				execStr = strdup(optarg);
				break;
			case 'w':
				findUsed = true;
				printMatchedFn = printMatchedWhereUsed;
				printUnmatchedFn = printUnmatchedSrc;
				break;
			case 'Y':
				showObjects |= SHOW_REP;
				break;
			case 'Z':
				showObjects |= SHOW_SRC;
				break;
			case 'B':
				showObjects |= SHOW_IPD;
				break;
			case 'K':
				showObjects |= SHOW_CSN;
				break;
			case 'b':
				readnonChapIpdSns(optarg);
				nonChapIpdSns = true;
				break;
			case 'k':
				xmlFree(figNumVarFormat);
				figNumVarFormat = xmlCharStrdup(optarg);
				break;
			case '^':
				remDelete = true;
				break;
			case 'h':
			case '?':
				show_help();
				return 0;
		}
	}

	/* If none of -CDEGHLPST are given, show all types of objects. */
	if (!showObjects) {
		showObjects = SHOW_ALL;
	}

	/* Load .externalpubs config file. */
	if (strcmp(extpubsFname, "") != 0 || find_config(extpubsFname, DEFAULT_EXTPUBS_FNAME)) {
		externalPubs = read_xml_doc(extpubsFname);
	}

	/* Print opening of XML report. */
	if (xmlOutput) {
		puts("<?xml version=\"1.0\"?>");
		printf("<results>");
	}

	/* Set the functions for printing matched/unmatched refs. */
	if (execStr) {
		printMatchedFn = execMatched;
	} else if (printFormat) {
		printMatchedFn   = printMatchedCustom;
		printUnmatchedFn = printUnmatchedCustom;
	} else if (xmlOutput) {
		printMatchedFn = printMatchedXml;
		printUnmatchedFn = printUnmatchedXml;
	} else if (inclSrcFname) {
		if (inclLineNum) {
			printMatchedFn = printMatchedSrcLine;
			printUnmatchedFn = printUnmatchedSrcLine;
		} else {
			printMatchedFn = printMatchedSrc;
			printUnmatchedFn = printUnmatchedSrc;
		}
	}

	if (optind < argc) {
		for (i = optind; i < argc; ++i) {
			if (isList) {
				if (findUsed) {
					unmatched += listWhereUsedList(argv[i], showObjects);
				} else {
					unmatched += listReferencesInList(argv[i], showObjects);
				}
			} else {
				if (findUsed) {
					unmatched += listWhereUsed(argv[i], showObjects);
				} else {
					unmatched += listReferences(argv[i], showObjects, NULL, 0);
				}
			}
		}
	} else if (isList) {
		if (findUsed) {
			unmatched += listWhereUsedList(NULL, showObjects);
		} else {
			unmatched += listReferencesInList(NULL, showObjects);
		}
	} else {
		if (findUsed) {
			unmatched += listWhereUsed("-", showObjects);
		} else {
			unmatched += listReferences("-", showObjects, NULL, 0);
		}
	}

	if (xmlOutput) {
		printf("</results>\n");
	}

	free(directory);
	xmlFree(hotspotXPath);
	xmlFreeNode(hotspotNs);
	free(listedFiles);
	free(execStr);
	free(printFormat);
	xmlFree(figNumVarFormat);
	xmlFreeDoc(externalPubs);
	xmlCleanupParser();

	return unmatched > 0 ? EXIT_UNMATCHED_REF : EXIT_SUCCESS;
}


/ gopher://khzae.net/0/s1kd/s1kd-tools/src/tools/s1kd-refs/s1kd-refs.c
Styles: Light Dark Classic