'use strict'; var log4js = require('log4js'); var promises = require('node:fs/promises'); var node_path = require('node:path'); var node_fs = require('node:fs'); var ignore = require('ignore'); var node_crypto = require('node:crypto'); var fastXmlParser = require('fast-xml-parser'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const XML_HEADER = ''; const INDENT = " "; function buildReassembledFile(combinedXmlContents, reassembledPath, xmlElement, xmlRootElementHeader) { return __awaiter(this, void 0, void 0, function* () { let finalXmlContent = combinedXmlContents.join("\n"); finalXmlContent = finalXmlContent.replace(/<\?xml version="1.0" encoding="UTF-8"\?>/g, ""); finalXmlContent = finalXmlContent.replace(new RegExp(`<${xmlElement}\\s*[^>]*>`, "g"), ""); finalXmlContent = finalXmlContent.replace(new RegExp(``, "g"), ""); finalXmlContent = finalXmlContent.replace(//g, function (_, cdataContent) { const trimmedContent = cdataContent.trim(); const lines = trimmedContent.split("\n"); const indentedLines = lines.map(function (line) { return line.replace(/^\s*/, ""); }); return (""); }); finalXmlContent = finalXmlContent.replace(/(\n\s*){2,}/g, `\n${INDENT}`); const closeTag = ``; yield promises.writeFile(reassembledPath, `${XML_HEADER}\n${xmlRootElementHeader}${finalXmlContent}${closeTag}`); logger.debug(`Created reassembled file: ${reassembledPath}`); }); } const XML_PARSER_OPTION = { commentPropName: "!---", ignoreAttributes: false, ignoreNameSpace: false, parseTagValue: false, parseNodeValue: false, parseAttributeValue: false, trimValues: true, processEntities: false, cdataPropName: "![CDATA[", }; const JSON_PARSER_OPTION = Object.assign(Object.assign({}, XML_PARSER_OPTION), { format: true, indentBy: INDENT, suppressBooleanAttributes: false, suppressEmptyNode: false }); function buildXMLString(element, indentLevel = 0) { const xmlBuilder = new fastXmlParser.XMLBuilder(JSON_PARSER_OPTION); const xmlString = xmlBuilder.build(element); const formattedXml = xmlString .split("\n") .map((line) => `${" ".repeat(indentLevel * INDENT.length)}${line}`) .join("\n") .trimEnd(); return formattedXml; } function parseXML(filePath) { return __awaiter(this, void 0, void 0, function* () { const xmlParser = new fastXmlParser.XMLParser(XML_PARSER_OPTION); const xmlContent = yield promises.readFile(filePath, "utf-8"); let xmlParsed; try { xmlParsed = xmlParser.parse(xmlContent, true); return xmlParsed; } catch (err) { logger.error(`${filePath} was unabled to be parsed and will not be processed. Confirm formatting and try again.`); return undefined; } }); } function buildRootElementHeader(rootElement, rootElementName) { let rootElementHeader = `<${rootElementName}`; for (const [attrKey, attrValue] of Object.entries(rootElement)) { if (attrKey.startsWith("@")) { const cleanAttrKey = attrKey.slice(2); rootElementHeader += ` ${cleanAttrKey}="${String(attrValue)}"`; } } rootElementHeader += ">"; return rootElementHeader; } function processFilesForRootElement(xmlParsed) { return __awaiter(this, void 0, void 0, function* () { const rootElementName = Object.keys(xmlParsed)[1]; const rootElement = xmlParsed[rootElementName]; const rootElementHeader = buildRootElementHeader(rootElement, rootElementName); return [rootElementName, rootElementHeader]; }); } class ReassembleXMLFileHandler { processFilesInDirectory(dirPath) { return __awaiter(this, void 0, void 0, function* () { const combinedXmlContents = []; let rootResult = undefined; const files = yield promises.readdir(dirPath); files.sort((fileA, fileB) => { const fullNameA = fileA.split(".")[0].toLowerCase(); const fullNameB = fileB.split(".")[0].toLowerCase(); return fullNameA.localeCompare(fullNameB); }); for (const file of files) { const filePath = node_path.join(dirPath, file); const fileStat = yield promises.stat(filePath); if (fileStat.isFile() && filePath.endsWith(".xml")) { const xmlParsed = yield parseXML(filePath); if (xmlParsed === undefined) continue; const rootResultFromFile = yield processFilesForRootElement(xmlParsed); rootResult = rootResultFromFile; const combinedXmlString = buildXMLString(xmlParsed); combinedXmlContents.push(combinedXmlString); } else if (fileStat.isDirectory()) { const [subCombinedXmlContents, subRootResult] = yield this.processFilesInDirectory(filePath); combinedXmlContents.push(...subCombinedXmlContents); rootResult = subRootResult; } } return [combinedXmlContents, rootResult]; }); } reassemble(xmlAttributes) { return __awaiter(this, void 0, void 0, function* () { const { filePath, fileExtension, postPurge = false } = xmlAttributes; let combinedXmlContents = []; const fileStat = yield promises.stat(filePath); if (!fileStat.isDirectory()) { logger.error(`The provided path to reassemble is not a directory: ${filePath}`); return; } logger.debug(`Parsing directory to reassemble: ${filePath}`); const [subCombinedXmlContents, rootResult] = yield this.processFilesInDirectory(filePath); combinedXmlContents = subCombinedXmlContents; const parentDirectory = node_path.dirname(filePath); const subdirectoryBasename = node_path.basename(filePath); const fileName = fileExtension ? `${subdirectoryBasename}.${fileExtension}` : `${subdirectoryBasename}.xml`; const outputPath = node_path.join(parentDirectory, fileName); if (rootResult !== undefined) { const [rootElementName, rootElementHeader] = rootResult; yield buildReassembledFile(combinedXmlContents, outputPath, rootElementName, rootElementHeader); if (postPurge) yield promises.rm(filePath, { recursive: true }); } else { logger.error(`No files under ${filePath} were parsed successfully. A reassembled XML file was not created.`); } }); } } function findUniqueIdElement(element, uniqueIdElements) { if (uniqueIdElements === undefined) { return getShortHash(element); } const uniqueIdElementsArray = uniqueIdElements.split(","); for (const fieldName of uniqueIdElementsArray) { if (element[fieldName] !== undefined) { if (typeof element[fieldName] === "string") { return element[fieldName]; } } } for (const key in element) { if (typeof element[key] === "object" && element[key] !== null) { const childFieldName = findUniqueIdElement(element[key], uniqueIdElements); if (childFieldName !== undefined) { return childFieldName; } } } return getShortHash(element); } function getShortHash(element) { const hash = node_crypto.createHash("sha256"); hash.update(JSON.stringify(element)); const fullHash = hash.digest("hex"); return fullHash.slice(0, 8); } function buildNestedFile(element, disassembledPath, uniqueIdElements, rootElementName, rootElementHeader, parentKey, indent) { return __awaiter(this, void 0, void 0, function* () { let elementContent = ""; const fieldName = findUniqueIdElement(element, uniqueIdElements); const outputDirectory = node_path.join(disassembledPath, parentKey); const outputFileName = `${fieldName}.${parentKey}-meta.xml`; const outputPath = node_path.join(outputDirectory, outputFileName); yield promises.mkdir(outputDirectory, { recursive: true }); const parentKeyHeader = buildRootElementHeader(element, parentKey); elementContent = buildXMLString(element, 2); let nestedFileContents = `${XML_HEADER}\n`; nestedFileContents += `${rootElementHeader}\n`; nestedFileContents += `${indent}${parentKeyHeader}\n`; nestedFileContents += `${elementContent}\n`; nestedFileContents += `${indent}\n`; nestedFileContents += ``; yield promises.writeFile(outputPath, nestedFileContents); logger.debug(`Created disassembled file: ${outputPath}`); }); } function processElement(params) { return __awaiter(this, void 0, void 0, function* () { const { element, disassembledPath, uniqueIdElements, rootElementName, rootElementHeader, key, indent, leafContent, leafCount, hasNestedElements, } = params; if (typeof element === "object") { yield buildNestedFile(element, disassembledPath, uniqueIdElements, rootElementName, rootElementHeader, key, indent); return [leafContent, leafCount, true]; } else { const updatedLeafContent = `${leafContent}${indent}<${key}>${String(element)}\n`; return [updatedLeafContent, leafCount + 1, hasNestedElements]; } }); } function buildLeafFile(leafContent, disassembledPath, baseName, rootElementName, rootElementHeader) { return __awaiter(this, void 0, void 0, function* () { let leafFile = `${XML_HEADER}\n`; leafFile += `${rootElementHeader}\n`; leafFile += leafContent; leafFile += ``; const leafOutputPath = node_path.join(disassembledPath, `${baseName}.xml`); yield promises.writeFile(leafOutputPath, leafFile); logger.debug(`Created disassembled file: ${leafOutputPath}`); }); } function buildDisassembledFiles(filePath, disassembledPath, uniqueIdElements, baseName, indent, postPurge) { return __awaiter(this, void 0, void 0, function* () { const parsedXml = yield parseXML(filePath); if (parsedXml === undefined) return; const rootElementName = Object.keys(parsedXml)[1]; const rootElement = parsedXml[rootElementName]; const rootElementHeader = buildRootElementHeader(rootElement, rootElementName); let leafContent = ""; let leafCount = 0; let hasNestedElements = false; for (const key of Object.keys(rootElement).filter((key) => !key.startsWith("@"))) { if (Array.isArray(rootElement[key])) { for (const element of rootElement[key]) { const [updatedLeafContent, updatedLeafCount, updatedHasNestedElements] = yield processElement({ element, disassembledPath, uniqueIdElements, rootElementName, rootElementHeader, key, indent, leafContent, leafCount, hasNestedElements, }); leafContent = updatedLeafContent; leafCount = updatedLeafCount; hasNestedElements = updatedHasNestedElements; } } else { const [updatedLeafContent, updatedLeafCount, updatedHasNestedElements] = yield processElement({ element: rootElement[key], disassembledPath, uniqueIdElements, rootElementName, rootElementHeader, key, indent, leafContent, leafCount, hasNestedElements, }); leafContent = updatedLeafContent; leafCount = updatedLeafCount; hasNestedElements = updatedHasNestedElements; } } if (!hasNestedElements) { logger.error(`The XML file ${filePath} only has leaf elements. This file will not be disassembled.`); return; } if (leafCount > 0) { yield buildLeafFile(leafContent, disassembledPath, baseName, rootElementName, rootElementHeader); } if (postPurge) { promises.unlink(filePath); } }); } class DisassembleXMLFileHandler { constructor() { this.ign = ignore(); } disassemble(xmlAttributes) { return __awaiter(this, void 0, void 0, function* () { const { filePath, uniqueIdElements, prePurge = false, postPurge = false, ignorePath = ".xmldisassemblerignore", } = xmlAttributes; const resolvedIgnorePath = node_path.resolve(ignorePath); if (node_fs.existsSync(resolvedIgnorePath)) { const content = yield promises.readFile(resolvedIgnorePath); this.ign.add(content.toString()); } const fileStat = yield promises.stat(filePath); const relativePath = node_path.relative(process.cwd(), filePath); if (fileStat.isFile()) { const resolvedPath = node_path.resolve(filePath); if (!resolvedPath.endsWith(".xml")) { logger.error(`The file path provided is not an XML file: ${resolvedPath}`); return; } if (this.ign.ignores(relativePath)) { logger.warn(`File ignored by ${ignorePath}: ${resolvedPath}`); return; } const dirPath = node_path.dirname(resolvedPath); yield this.processFile({ dirPath, filePath: resolvedPath, uniqueIdElements, prePurge, postPurge, }); } else if (fileStat.isDirectory()) { const subFiles = yield promises.readdir(filePath); for (const subFile of subFiles) { const subFilePath = node_path.join(filePath, subFile); const relativeSubFilePath = node_path.relative(process.cwd(), subFilePath); if (subFilePath.endsWith(".xml") && !this.ign.ignores(relativeSubFilePath)) { yield this.processFile({ dirPath: filePath, filePath: subFilePath, uniqueIdElements, prePurge, postPurge, }); } else if (this.ign.ignores(relativeSubFilePath)) { logger.warn(`File ignored by ${ignorePath}: ${subFilePath}`); } } } }); } processFile(xmlAttributes) { return __awaiter(this, void 0, void 0, function* () { const { dirPath, filePath, uniqueIdElements, prePurge, postPurge } = xmlAttributes; logger.debug(`Parsing file to disassemble: ${filePath}`); const fullName = node_path.basename(filePath, node_path.extname(filePath)); const baseName = fullName.split(".")[0]; let outputPath; outputPath = node_path.join(dirPath, baseName); if (prePurge && node_fs.existsSync(outputPath)) yield promises.rm(outputPath, { recursive: true }); yield buildDisassembledFiles(filePath, outputPath, uniqueIdElements, fullName, INDENT, postPurge); }); } } function setLogLevel(level) { log4js.getLogger().level = level; } const logger = log4js.getLogger(); log4js.configure({ appenders: { disassemble: { type: "file", filename: "disassemble.log" } }, categories: { default: { appenders: ["disassemble"], level: "error" } }, }); exports.DisassembleXMLFileHandler = DisassembleXMLFileHandler; exports.ReassembleXMLFileHandler = ReassembleXMLFileHandler; exports.logger = logger; exports.setLogLevel = setLogLevel; //# sourceMappingURL=index.cjs.map