From c498dc5b33a724351a6a99c93c368bc3e2a49820 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Mon, 18 Jul 2022 23:53:01 -0500 Subject: [PATCH] Add binary plist parser --- helpers/scanner/parsers/plist.js | 375 +++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 helpers/scanner/parsers/plist.js diff --git a/helpers/scanner/parsers/plist.js b/helpers/scanner/parsers/plist.js new file mode 100644 index 0000000..31718cf --- /dev/null +++ b/helpers/scanner/parsers/plist.js @@ -0,0 +1,375 @@ +// Adpapted for browser+node from https://github.com/joeferner/node-bplist-parser/blob/master/bplistParser.js +import bigInt from 'big-integer' +import bufferApi from 'buffer' + + +// const fs = require('fs'); +// const bigInt = require('big-integer') +// const bufferApi = require('buffer') +const debug = false + +export const maxObjectSize = 100 * 1000 * 1000; // 100Meg +export const maxObjectCount = 32768; + +// EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime(); +// ...but that's annoying in a static initializer because it can throw exceptions, ick. +// So we just hardcode the correct value. +const EPOCH = 978307200000; + +// UID object definition +const UID = function(id) { + this.UID = id; +}; + +export function parsePlistBuffer ( fileBuffer , callback) { + return new Promise(function(resolve, reject) { + function tryParseBuffer(buffer) { + let err = null; + let result; + try { + result = parseBuffer(buffer); + resolve(result); + } catch (ex) { + err = ex; + reject(err); + } finally { + if (callback) callback(err, result); + } + } + + return tryParseBuffer( fileBuffer ) + + // if (bufferApi.Buffer.isBuffer(fileNameOrBuffer)) { + + // } + // fs.readFile(fileNameOrBuffer, function(err, data) { + // if (err) { + // reject(err); + // return callback(err); + // } + // tryParseBuffer(data); + // }); + }); +}; + +export function parseFileSync (fileNameOrBuffer) { + // if (!bufferApi.Buffer.isBuffer(fileNameOrBuffer)) { + // fileNameOrBuffer = fs.readFileSync(fileNameOrBuffer); + // } + return parseBuffer(fileNameOrBuffer); +}; + +function parseBuffer ( buffer ) { + // check header + const header = buffer.slice(0, 'bplist'.length).toString('utf8'); + if (header !== 'bplist') { + throw new Error("Invalid binary plist. Expected 'bplist' at offset 0."); + } + + // Handle trailer, last 32 bytes of the file + const trailer = buffer.slice(buffer.length - 32, buffer.length); + // 6 null bytes (index 0 to 5) + const offsetSize = trailer.readUInt8(6); + if (debug) { + console.log("offsetSize: " + offsetSize); + } + const objectRefSize = trailer.readUInt8(7); + if (debug) { + console.log("objectRefSize: " + objectRefSize); + } + const numObjects = readUInt64BE(trailer, 8); + if (debug) { + console.log("numObjects: " + numObjects); + } + const topObject = readUInt64BE(trailer, 16); + if (debug) { + console.log("topObject: " + topObject); + } + const offsetTableOffset = readUInt64BE(trailer, 24); + if (debug) { + console.log("offsetTableOffset: " + offsetTableOffset); + } + + if (numObjects > maxObjectCount) { + throw new Error("maxObjectCount exceeded"); + } + + // Handle offset table + const offsetTable = []; + + for (let i = 0; i < numObjects; i++) { + const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize); + offsetTable[i] = readUInt(offsetBytes, 0); + if (debug) { + console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]"); + } + } + + // Parses an object inside the currently parsed binary property list. + // For the format specification check + // + // Apple's binary property list parser implementation. + function parseObject(tableOffset) { + const offset = offsetTable[tableOffset]; + const type = buffer[offset]; + const objType = (type & 0xF0) >> 4; //First 4 bits + const objInfo = (type & 0x0F); //Second 4 bits + switch (objType) { + case 0x0: + return parseSimple(); + case 0x1: + return parseInteger(); + case 0x8: + return parseUID(); + case 0x2: + return parseReal(); + case 0x3: + return parseDate(); + case 0x4: + return parseData(); + case 0x5: // ASCII + return parsePlistString(); + case 0x6: // UTF-16 + return parsePlistString(true); + case 0xA: + return parseArray(); + case 0xD: + return parseDictionary(); + default: + throw new Error("Unhandled type 0x" + objType.toString(16)); + } + + function parseSimple() { + //Simple + switch (objInfo) { + case 0x0: // null + return null; + case 0x8: // false + return false; + case 0x9: // true + return true; + case 0xF: // filler byte + return null; + default: + throw new Error("Unhandled simple type 0x" + objType.toString(16)); + } + } + + function bufferToHexString(buffer) { + let str = ''; + let i; + for (i = 0; i < buffer.length; i++) { + if (buffer[i] != 0x00) { + break; + } + } + for (; i < buffer.length; i++) { + const part = '00' + buffer[i].toString(16); + str += part.substr(part.length - 2); + } + return str; + } + + function parseInteger() { + const length = Math.pow(2, objInfo); + if (length < maxObjectSize) { + const data = buffer.slice(offset + 1, offset + 1 + length); + if (length === 16) { + const str = bufferToHexString(data); + return bigInt(str, 16); + } + return data.reduce((acc, curr) => { + acc <<= 8; + acc |= curr & 255; + return acc; + }); + } + throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); + + } + + function parseUID() { + const length = objInfo + 1; + if (length < maxObjectSize) { + return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length))); + } + throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); + } + + function parseReal() { + const length = Math.pow(2, objInfo); + if (length < maxObjectSize) { + const realBuffer = buffer.slice(offset + 1, offset + 1 + length); + if (length === 4) { + return realBuffer.readFloatBE(0); + } + if (length === 8) { + return realBuffer.readDoubleBE(0); + } + } else { + throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); + } + } + + function parseDate() { + if (objInfo != 0x3) { + console.error("Unknown date type :" + objInfo + ". Parsing anyway..."); + } + const dateBuffer = buffer.slice(offset + 1, offset + 9); + return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0))); + } + + function parseData() { + let dataoffset = 1; + let length = objInfo; + if (objInfo == 0xF) { + const int_type = buffer[offset + 1]; + const intType = (int_type & 0xF0) / 0x10; + if (intType != 0x1) { + console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType); + } + const intInfo = int_type & 0x0F; + const intLength = Math.pow(2, intInfo); + dataoffset = 2 + intLength; + if (intLength < 3) { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } else { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } + } + if (length < maxObjectSize) { + return buffer.slice(offset + dataoffset, offset + dataoffset + length); + } + throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); + } + + function parsePlistString(isUtf16) { + isUtf16 = isUtf16 || 0; + let enc = "utf8"; + let length = objInfo; + let stroffset = 1; + if (objInfo == 0xF) { + const int_type = buffer[offset + 1]; + const intType = (int_type & 0xF0) / 0x10; + if (intType != 0x1) { + console.error("UNEXPECTED LENGTH-INT TYPE! " + intType); + } + const intInfo = int_type & 0x0F; + const intLength = Math.pow(2, intInfo); + stroffset = 2 + intLength; + if (intLength < 3) { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } else { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } + } + // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16 + length *= (isUtf16 + 1); + if (length < maxObjectSize) { + let plistString = bufferApi.Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length)); + if (isUtf16) { + plistString = swapBytes(plistString); + enc = "ucs2"; + } + return plistString.toString(enc); + } + throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); + } + + function parseArray() { + let length = objInfo; + let arrayoffset = 1; + if (objInfo == 0xF) { + const int_type = buffer[offset + 1]; + const intType = (int_type & 0xF0) / 0x10; + if (intType != 0x1) { + console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType); + } + const intInfo = int_type & 0x0F; + const intLength = Math.pow(2, intInfo); + arrayoffset = 2 + intLength; + if (intLength < 3) { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } else { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } + } + if (length * objectRefSize > maxObjectSize) { + throw new Error("Too little heap space available!"); + } + const array = []; + for (let i = 0; i < length; i++) { + const objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize)); + array[i] = parseObject(objRef); + } + return array; + } + + function parseDictionary() { + let length = objInfo; + let dictoffset = 1; + if (objInfo == 0xF) { + const int_type = buffer[offset + 1]; + const intType = (int_type & 0xF0) / 0x10; + if (intType != 0x1) { + console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType); + } + const intInfo = int_type & 0x0F; + const intLength = Math.pow(2, intInfo); + dictoffset = 2 + intLength; + if (intLength < 3) { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } else { + length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength)); + } + } + if (length * 2 * objectRefSize > maxObjectSize) { + throw new Error("Too little heap space available!"); + } + if (debug) { + console.log("Parsing dictionary #" + tableOffset); + } + const dict = {}; + for (let i = 0; i < length; i++) { + const keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize)); + const valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize)); + const key = parseObject(keyRef); + const val = parseObject(valRef); + if (debug) { + console.log(" DICT #" + tableOffset + ": Mapped " + key + " to " + val); + } + dict[key] = val; + } + return dict; + } + } + + return parseObject(topObject) +}; + +function readUInt(buffer, start) { + start = start || 0; + + let l = 0; + for (let i = start; i < buffer.length; i++) { + l <<= 8; + l |= buffer[i] & 0xFF; + } + return l; +} + +// we're just going to toss the high order bits because javascript doesn't have 64-bit ints +function readUInt64BE(buffer, start) { + const data = buffer.slice(start, start + 8); + return data.readUInt32BE(4, 8); +} + +function swapBytes(buffer) { + const len = buffer.length; + for (let i = 0; i < len; i += 2) { + const a = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = a; + } + return buffer; +}