const START_NAMESPACE_SPEC = { hdr: '0x0100', hdrsz: '0x0010', sz: '0x00000018', startline: 4, commentId: 4, nameId: 4, valueId: 4, } const END_NAMESPACE_SPEC = { hdr: '0x0101', hdrsz: '0x0010', sz: '0x00000018', startline: 4, commentId: 4, nameId: 4, valueId: 4, } const BEGIN_NODE_SPEC = { hdr: '0x0102', hdrsz: '0x0010', sz: 4, startline: 4, commentId: 4, namespaceId: 4, nameId: 4, attr: { offset: 2, size: 2, count: 2, }, id_attr_offset: 2, cls_attr_offset: 2, style_attr_offset: 2, attributes: [{ length: main => main.attr.count, element_spec: { ns: 4, nameId: 4, commentId: 4, sz: '0x0008', zero: '0x00', type: 1, value: 4, } }] } const END_NODE_SPEC = { hdr: '0x0103', hdrsz: '0x0010', sz: 4, startline: 4, commentId: 4, namespaceId: 4, nameId: 4, } function decode_spec_value(o, key, value, buf, idx, main) { let byteLength; switch (true) { case typeof value === 'number': { // raw integer value byteLength = value; o[key] = buf.readIntLE(idx, byteLength); break; } case Array.isArray(value): { // known-length array of values const length = value[0].length(main); byteLength = 0; o[key] = new Array(length); for (let i = 0; i < length; i++) { const bytes = decode_spec_value(o[key], i, value[0].element_spec, buf, idx, main); idx += bytes; byteLength += bytes; } break; } case typeof value === 'object': { // named sub-spec o[key] = {}; byteLength = decode_spec(buf, value, o[key], o, idx); break } case /^0x[\da-fA-F]/.test(value): { // exact integer value byteLength = (value.length - 2) / 2; o[key] = buf.readUIntLE(idx, byteLength); if (parseInt(value) !== o[key]) { throw new Error(`Bad value. Expected ${value}, got 0x${o[key].toString(16)}`); } break; } case value === 'length-utf16-null': { // 2-byte length, utf16 chars, null char const string_byte_length = buf.readUInt16LE(idx) * 2; // 1 char = 2 bytes idx += 2; o[key] = buf.slice(idx, idx + string_byte_length).toString('ucs2'); idx += string_byte_length; if (buf.readUInt16LE(idx) !== 0) { throw new Error(`Bad value. Nul char expected but not found.`); } byteLength = 2 + string_byte_length + 2; break; } case /^align:\d+$/.test(value): { // used for arbitrary padding to a specified alignment const align = parseInt(value.split(':')[1], 10); byteLength = idx - (Math.trunc(idx / align) * align); o[key] = buf.slice(idx, idx + byteLength); break; } default: throw new Error(`Unknown spec value definition: ${value}`); } return byteLength; } function decode_spec(buf, spec, o = {}, main = o, idx = 0) { let byteLength = 0; for (let key of Object.keys(spec)) { const value = spec[key]; const bytes = decode_spec_value(o, key, value, buf, idx, main); idx += bytes; byteLength += bytes; } return byteLength; } /** * Converts a binary XML file back into a readable XML document * @param {Buffer} buf binary XMl content */ function decode_binary_xml(buf) { const xml_spec = { header: '0x00080003', headerSize: 4, stringPool: { header: '0x0001', hdrsize: '0x001c', sz: 4, stringCount: 4, styleCount: 4, flags: 4, stringStart: 4, styleStart: 4, stringOffsets: [{ length: main => main.stringPool.stringCount, element_spec: 4, }], strings: [{ length: main => main.stringPool.stringCount, element_spec: 'length-utf16-null', }], padding: 'align:4', }, resourceIDPool: { hdr: '0x0180', hdrsize: '0x0008', sz: 4, resIDs: [{ length: main => (main.resourceIDPool.sz - main.resourceIDPool.hdrsize) / 4, element_spec: 4, }] } } const decoded = {}; let idx = decode_spec(buf, xml_spec, decoded); // after we've extracted the string and id's, it should be time to parse the xml const node_stack = [{ nodes: [] }]; const namespaces = []; while (idx < buf.byteLength) { const id = buf.readUInt16LE(idx); switch (id) { case 0x0100: { // start namespace const node = {}; idx += decode_spec(buf, START_NAMESPACE_SPEC, node, node, idx); namespaces.push(node); break; } case 0x0101: { // end namespace const node = {}; idx += decode_spec(buf, END_NAMESPACE_SPEC, node, node, idx); const i = namespaces.findIndex(ns => ns.nameId === node.nameId); namespaces.splice(i, 1); break; } case 0x0102: { // begin node const node = { nodes: [], }; idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx); node.namespaces = namespaces.slice(); node.namespaces.forEach(ns => { if (!ns.node) ns.node = node; }); node_stack[0].nodes.push(node); node_stack.unshift(node); break; } case 0x0103: { // end node const spec = END_NODE_SPEC; const node = {}; idx += decode_spec(buf, spec, node, node, idx); node_stack.shift(); break; } default: throw new Error(`Unknown XML element ${id.toString(16)}`); } } decoded.nodes = node_stack[0].nodes; const xml = toXMLDocument(decoded); return xml; } /** * Convert the decoded binary XML to a readable XML document * @param {*} decoded */ function toXMLDocument(decoded) { const strings = decoded.stringPool.strings; const format = { nodes: (nodes, indent) => { return nodes.map(node => format.node(node, indent)).join('\n'); }, node: (node, indent) => { const parts = [indent, '<', strings[node.nameId]]; for (let ns of node.namespaces.filter(ns => ns.node === node)) { parts.push(' ', `xmlns:${strings[ns.nameId]}="${strings[ns.valueId]}"`); } const attr_indent = node.attributes.length > 1 ? `\n${indent} ` : ' '; for (let attr of node.attributes) { parts.push(attr_indent, format.attribute(attr, node.namespaces)); } if (node.nodes.length) { parts.push('>\n', format.nodes(node.nodes, indent + ' '), '\n', indent, ''); } else { parts.push(' />'); } return parts.join(''); }, attribute: (attr, namespaces) => { let value = attr.value; switch (attr.type) { case 3: value = strings[value]; break; case 16: value |= 0; // convert to signed integer break; case 18: value = value ? true : false; break; case 1: // resource id case 17: // flags default: value = '0x' + value.toString(`16`); break; } let ns = ''; if (attr.ns >= 0) { ns = `${strings[namespaces.find(ns => ns.valueId === attr.ns).nameId]}:`; } return `${ns}${strings[attr.nameId]}="${value}"`; } } return '\n' + format.nodes(decoded.nodes, ''); } module.exports = { decode_binary_xml, }