/** * Module dependencies. */ const { DOMParser } = require("@xmldom/xmldom"); /** * Module exports. */ exports.parse = parse; var TEXT_NODE = 3; var CDATA_NODE = 4; var COMMENT_NODE = 8; /** * We ignore raw text (usually whitespace), , * and raw CDATA nodes. * * @param {Element} node * @returns {Boolean} * @api private */ function shouldIgnoreNode(node) { return ( node.nodeType === TEXT_NODE || node.nodeType === COMMENT_NODE || node.nodeType === CDATA_NODE ); } /** * Check if the node is empty. Some plist file has such node: * * this node shoud be ignored. * * @see https://github.com/TooTallNate/plist.js/issues/66 * @param {Element} node * @returns {Boolean} * @api private */ function isEmptyNode(node) { if (!node.childNodes || node.childNodes.length === 0) { return true; } else { return false; } } function invariant(test, message) { if (!test) { throw new Error(message); } } /** * Parses a Plist XML string. Returns an Object. * * @param {String} xml - the XML String to decode * @returns {Mixed} the decoded value from the Plist XML * @api public */ function parse(xml) { var doc = new DOMParser().parseFromString(xml, "text/xml"); invariant( doc.documentElement.nodeName === "plist", "malformed document. First element should be ", ); var plist = parsePlistXML(doc.documentElement); // the root node gets interpreted as an Array, // so pull out the inner data first if (plist.length == 1) plist = plist[0]; return plist; } /** * Convert an XML based plist document into a JSON representation. * * @param {Object} xml_node - current XML node in the plist * @returns {Mixed} built up JSON object * @api private */ function parsePlistXML(node) { var i, new_obj, key, val, new_arr, res, counter, type; if (!node) return null; if (node.nodeName === "plist") { new_arr = []; if (isEmptyNode(node)) { return new_arr; } for (i = 0; i < node.childNodes.length; i++) { if (!shouldIgnoreNode(node.childNodes[i])) { new_arr.push(parsePlistXML(node.childNodes[i])); } } return new_arr; } else if (node.nodeName === "dict") { new_obj = {}; key = null; counter = 0; if (isEmptyNode(node)) { return new_obj; } for (i = 0; i < node.childNodes.length; i++) { if (shouldIgnoreNode(node.childNodes[i])) continue; if (counter % 2 === 0) { invariant( node.childNodes[i].nodeName === "key", "Missing key while parsing .", ); key = parsePlistXML(node.childNodes[i]); } else { invariant( node.childNodes[i].nodeName !== "key", 'Unexpected key "' + parsePlistXML(node.childNodes[i]) + '" while parsing .', ); new_obj[key] = parsePlistXML(node.childNodes[i]); } counter += 1; } if (counter % 2 === 1) { new_obj[key] = ""; } return new_obj; } else if (node.nodeName === "array") { new_arr = []; if (isEmptyNode(node)) { return new_arr; } for (i = 0; i < node.childNodes.length; i++) { if (!shouldIgnoreNode(node.childNodes[i])) { res = parsePlistXML(node.childNodes[i]); if (null != res) new_arr.push(res); } } return new_arr; } else if (node.nodeName === "#text") { // TODO: what should we do with text types? (CDATA sections) } else if (node.nodeName === "key") { if (isEmptyNode(node)) { return ""; } invariant( node.childNodes[0].nodeValue !== "__proto__", "__proto__ keys can lead to prototype pollution. More details on CVE-2022-22912", ); return node.childNodes[0].nodeValue; } else if (node.nodeName === "string") { res = ""; if (isEmptyNode(node)) { return res; } for (i = 0; i < node.childNodes.length; i++) { var type = node.childNodes[i].nodeType; if (type === TEXT_NODE || type === CDATA_NODE) { res += node.childNodes[i].nodeValue; } } return res; } else if (node.nodeName === "integer") { invariant(!isEmptyNode(node), 'Cannot parse "" as integer.'); return parseInt(node.childNodes[0].nodeValue, 10); } else if (node.nodeName === "real") { invariant(!isEmptyNode(node), 'Cannot parse "" as real.'); res = ""; for (i = 0; i < node.childNodes.length; i++) { if (node.childNodes[i].nodeType === TEXT_NODE) { res += node.childNodes[i].nodeValue; } } return parseFloat(res); } else if (node.nodeName === "data") { res = ""; if (isEmptyNode(node)) { return Buffer.from(res, "base64"); } for (i = 0; i < node.childNodes.length; i++) { if (node.childNodes[i].nodeType === TEXT_NODE) { res += node.childNodes[i].nodeValue.replace(/\s+/g, ""); } } return Buffer.from(res, "base64"); } else if (node.nodeName === "date") { invariant(!isEmptyNode(node), 'Cannot parse "" as Date.'); return new Date(node.childNodes[0].nodeValue); } else if (node.nodeName === "null") { return null; } else if (node.nodeName === "true") { return true; } else if (node.nodeName === "false") { return false; } else { throw new Error("Invalid PLIST tag " + node.nodeName); } }