bplist-universal
Version:
Binary plist parser for node
324 lines (303 loc) • 10.8 kB
text/typescript
import bigInt from "big-integer";
const defaultMaxObjectSize = 100 * 1000 * 1000; // 100Meg
const defaultMaxObjectCount = 32768;
interface Opts {
maxObjectSize?: number;
maxObjectCount?: number;
}
// 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;
export const parseBuffer = function (buffer: Buffer, opts?: Opts) {
const { maxObjectSize = defaultMaxObjectSize, maxObjectCount = defaultMaxObjectCount } = opts || {};
// check header
const header = buffer.subarray(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.subarray(buffer.length - 32, buffer.length);
// 6 null bytes (index 0 to 5)
const offsetSize = trailer.readUInt8(6);
const objectRefSize = trailer.readUInt8(7);
const numObjects = readUInt64BE(trailer, 8);
const topObject = readUInt64BE(trailer, 16);
const offsetTableOffset = readUInt64BE(trailer, 24);
if (numObjects > maxObjectCount) {
throw new Error("maxObjectCount exceeded");
}
// Handle offset table
const offsetTable: number[] = [];
for (let i = 0; i < numObjects; i++) {
const offsetBytes = buffer.subarray(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize);
offsetTable[i] = readUInt(offsetBytes, 0);
}
// Parses an object inside the currently parsed binary property list.
// For the format specification check
// <a href="https://www.opensource.apple.com/source/CF/CF-635/CFBinaryPList.c">
// Apple's binary property list parser implementation</a>.
function parseObject(tableOffset: number): any {
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(1);
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: 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(): number | bigInt.BigInteger {
const length = Math.pow(2, objInfo);
if (length < maxObjectSize) {
const data = buffer.subarray(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) {
const uint = readUInt(buffer.subarray(offset + 1, offset + 1 + length));
return { UID: uint };
}
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.subarray(offset + 1, offset + 1 + length);
if (length === 4) {
return realBuffer.readFloatBE(0);
}
if (length === 8) {
return realBuffer.readDoubleBE(0);
}
throw new Error("Unhandled real length " + length);
} 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.subarray(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.subarray(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.subarray(offset + 2, offset + 2 + intLength));
}
}
if (length < maxObjectSize) {
return buffer.subarray(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: 0 | 1 = 0) {
let enc: BufferEncoding = "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.subarray(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.subarray(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 = Buffer.from(buffer.subarray(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(): any[] {
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.subarray(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.subarray(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.subarray(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.subarray(offset + 2, offset + 2 + intLength));
} else {
length = readUInt(buffer.subarray(offset + 2, offset + 2 + intLength));
}
}
if (length * 2 * objectRefSize > maxObjectSize) {
throw new Error("Too little heap space available!");
}
const dict = {};
for (let i = 0; i < length; i++) {
const keyRef = readUInt(
buffer.subarray(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize),
);
const valRef = readUInt(
buffer.subarray(
offset + dictoffset + length * objectRefSize + i * objectRefSize,
offset + dictoffset + length * objectRefSize + (i + 1) * objectRefSize,
),
);
const key = parseObject(keyRef);
const val = parseObject(valRef);
// @ts-ignore
dict[key] = val;
}
return dict;
}
}
return [parseObject(topObject)];
};
export default parseBuffer;
function readUInt(buffer: Buffer, start = 0) {
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: Buffer, start: number) {
const data = buffer.subarray(start, start + 8);
return data.readUInt32BE(4);
}
function swapBytes(buffer: 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;
}