time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

bplistParser.js (11860B)


      1 'use strict';
      2 
      3 // adapted from http://code.google.com/p/plist/source/browse/trunk/src/com/dd/plist/BinaryPropertyListParser.java
      4 
      5 const fs = require('fs');
      6 const bigInt = require("big-integer");
      7 const debug = false;
      8 
      9 exports.maxObjectSize = 100 * 1000 * 1000; // 100Meg
     10 exports.maxObjectCount = 32768;
     11 
     12 // EPOCH = new SimpleDateFormat("yyyy MM dd zzz").parse("2001 01 01 GMT").getTime();
     13 // ...but that's annoying in a static initializer because it can throw exceptions, ick.
     14 // So we just hardcode the correct value.
     15 const EPOCH = 978307200000;
     16 
     17 // UID object definition
     18 const UID = exports.UID = function(id) {
     19   this.UID = id;
     20 };
     21 
     22 const parseFile = exports.parseFile = function (fileNameOrBuffer, callback) {
     23   return new Promise(function (resolve, reject) {
     24     function tryParseBuffer(buffer) {
     25       let err = null;
     26       let result;
     27       try {
     28         result = parseBuffer(buffer);
     29         resolve(result);
     30       } catch (ex) {
     31         err = ex;
     32         reject(err);
     33       } finally {
     34         if (callback) callback(err, result);
     35       }
     36     }
     37 
     38     if (Buffer.isBuffer(fileNameOrBuffer)) {
     39       return tryParseBuffer(fileNameOrBuffer);
     40     }
     41     fs.readFile(fileNameOrBuffer, function (err, data) {
     42       if (err) {
     43         reject(err);
     44         return callback(err);
     45       }
     46       tryParseBuffer(data);
     47     });
     48   });
     49 };
     50 
     51 const parseBuffer = exports.parseBuffer = function (buffer) {
     52   // check header
     53   const header = buffer.slice(0, 'bplist'.length).toString('utf8');
     54   if (header !== 'bplist') {
     55     throw new Error("Invalid binary plist. Expected 'bplist' at offset 0.");
     56   }
     57 
     58   // Handle trailer, last 32 bytes of the file
     59   const trailer = buffer.slice(buffer.length - 32, buffer.length);
     60   // 6 null bytes (index 0 to 5)
     61   const offsetSize = trailer.readUInt8(6);
     62   if (debug) {
     63     console.log("offsetSize: " + offsetSize);
     64   }
     65   const objectRefSize = trailer.readUInt8(7);
     66   if (debug) {
     67     console.log("objectRefSize: " + objectRefSize);
     68   }
     69   const numObjects = readUInt64BE(trailer, 8);
     70   if (debug) {
     71     console.log("numObjects: " + numObjects);
     72   }
     73   const topObject = readUInt64BE(trailer, 16);
     74   if (debug) {
     75     console.log("topObject: " + topObject);
     76   }
     77   const offsetTableOffset = readUInt64BE(trailer, 24);
     78   if (debug) {
     79     console.log("offsetTableOffset: " + offsetTableOffset);
     80   }
     81 
     82   if (numObjects > exports.maxObjectCount) {
     83     throw new Error("maxObjectCount exceeded");
     84   }
     85 
     86   // Handle offset table
     87   const offsetTable = [];
     88 
     89   for (let i = 0; i < numObjects; i++) {
     90     const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize);
     91     offsetTable[i] = readUInt(offsetBytes, 0);
     92     if (debug) {
     93       console.log("Offset for Object #" + i + " is " + offsetTable[i] + " [" + offsetTable[i].toString(16) + "]");
     94     }
     95   }
     96 
     97   // Parses an object inside the currently parsed binary property list.
     98   // For the format specification check
     99   // <a href="http://www.opensource.apple.com/source/CF/CF-635/CFBinaryPList.c">
    100   // Apple's binary property list parser implementation</a>.
    101   function parseObject(tableOffset) {
    102     const offset = offsetTable[tableOffset];
    103     const type = buffer[offset];
    104     const objType = (type & 0xF0) >> 4; //First  4 bits
    105     const objInfo = (type & 0x0F);      //Second 4 bits
    106     switch (objType) {
    107     case 0x0:
    108       return parseSimple();
    109     case 0x1:
    110       return parseInteger();
    111     case 0x8:
    112       return parseUID();
    113     case 0x2:
    114       return parseReal();
    115     case 0x3:
    116       return parseDate();
    117     case 0x4:
    118       return parseData();
    119     case 0x5: // ASCII
    120       return parsePlistString();
    121     case 0x6: // UTF-16
    122       return parsePlistString(true);
    123     case 0xA:
    124       return parseArray();
    125     case 0xD:
    126       return parseDictionary();
    127     default:
    128       throw new Error("Unhandled type 0x" + objType.toString(16));
    129     }
    130 
    131     function parseSimple() {
    132       //Simple
    133       switch (objInfo) {
    134       case 0x0: // null
    135         return null;
    136       case 0x8: // false
    137         return false;
    138       case 0x9: // true
    139         return true;
    140       case 0xF: // filler byte
    141         return null;
    142       default:
    143         throw new Error("Unhandled simple type 0x" + objType.toString(16));
    144       }
    145     }
    146 
    147     function bufferToHexString(buffer) {
    148       let str = '';
    149       let i;
    150       for (i = 0; i < buffer.length; i++) {
    151         if (buffer[i] != 0x00) {
    152           break;
    153         }
    154       }
    155       for (; i < buffer.length; i++) {
    156         const part = '00' + buffer[i].toString(16);
    157         str += part.substr(part.length - 2);
    158       }
    159       return str;
    160     }
    161 
    162     function parseInteger() {
    163       const length = Math.pow(2, objInfo);
    164 
    165       if (objInfo == 0x4) {
    166         const data = buffer.slice(offset + 1, offset + 1 + length);
    167         const str = bufferToHexString(data);
    168         return bigInt(str, 16);
    169       }
    170       if (objInfo == 0x3) {
    171         return buffer.readInt32BE(offset + 1);
    172       }
    173       if (length < exports.maxObjectSize) {
    174         return readUInt(buffer.slice(offset + 1, offset + 1 + length));
    175       }
    176       throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
    177     }
    178 
    179     function parseUID() {
    180       const length = objInfo + 1;
    181       if (length < exports.maxObjectSize) {
    182         return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length)));
    183       }
    184       throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
    185     }
    186 
    187     function parseReal() {
    188       const length = Math.pow(2, objInfo);
    189       if (length < exports.maxObjectSize) {
    190         const realBuffer = buffer.slice(offset + 1, offset + 1 + length);
    191         if (length === 4) {
    192           return realBuffer.readFloatBE(0);
    193         }
    194         if (length === 8) {
    195           return realBuffer.readDoubleBE(0);
    196         }
    197       } else {
    198         throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
    199       }
    200     }
    201 
    202     function parseDate() {
    203       if (objInfo != 0x3) {
    204         console.error("Unknown date type :" + objInfo + ". Parsing anyway...");
    205       }
    206       const dateBuffer = buffer.slice(offset + 1, offset + 9);
    207       return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0)));
    208     }
    209 
    210     function parseData() {
    211       let dataoffset = 1;
    212       let length = objInfo;
    213       if (objInfo == 0xF) {
    214         const int_type = buffer[offset + 1];
    215         const intType = (int_type & 0xF0) / 0x10;
    216         if (intType != 0x1) {
    217           console.error("0x4: UNEXPECTED LENGTH-INT TYPE! " + intType);
    218         }
    219         const intInfo = int_type & 0x0F;
    220         const intLength = Math.pow(2, intInfo);
    221         dataoffset = 2 + intLength;
    222         if (intLength < 3) {
    223           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    224         } else {
    225           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    226         }
    227       }
    228       if (length < exports.maxObjectSize) {
    229         return buffer.slice(offset + dataoffset, offset + dataoffset + length);
    230       }
    231       throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
    232     }
    233 
    234     function parsePlistString (isUtf16) {
    235       isUtf16 = isUtf16 || 0;
    236       let enc = "utf8";
    237       let length = objInfo;
    238       let stroffset = 1;
    239       if (objInfo == 0xF) {
    240         const int_type = buffer[offset + 1];
    241         const intType = (int_type & 0xF0) / 0x10;
    242         if (intType != 0x1) {
    243           console.err("UNEXPECTED LENGTH-INT TYPE! " + intType);
    244         }
    245         const intInfo = int_type & 0x0F;
    246         const intLength = Math.pow(2, intInfo);
    247         stroffset = 2 + intLength;
    248         if (intLength < 3) {
    249           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    250         } else {
    251           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    252         }
    253       }
    254       // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16
    255       length *= (isUtf16 + 1);
    256       if (length < exports.maxObjectSize) {
    257         let plistString = Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length));
    258         if (isUtf16) {
    259           plistString = swapBytes(plistString);
    260           enc = "ucs2";
    261         }
    262         return plistString.toString(enc);
    263       }
    264       throw new Error("To little heap space available! Wanted to read " + length + " bytes, but only " + exports.maxObjectSize + " are available.");
    265     }
    266 
    267     function parseArray() {
    268       let length = objInfo;
    269       let arrayoffset = 1;
    270       if (objInfo == 0xF) {
    271         const int_type = buffer[offset + 1];
    272         const intType = (int_type & 0xF0) / 0x10;
    273         if (intType != 0x1) {
    274           console.error("0xa: UNEXPECTED LENGTH-INT TYPE! " + intType);
    275         }
    276         const intInfo = int_type & 0x0F;
    277         const intLength = Math.pow(2, intInfo);
    278         arrayoffset = 2 + intLength;
    279         if (intLength < 3) {
    280           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    281         } else {
    282           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    283         }
    284       }
    285       if (length * objectRefSize > exports.maxObjectSize) {
    286         throw new Error("To little heap space available!");
    287       }
    288       const array = [];
    289       for (let i = 0; i < length; i++) {
    290         const objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize));
    291         array[i] = parseObject(objRef);
    292       }
    293       return array;
    294     }
    295 
    296     function parseDictionary() {
    297       let length = objInfo;
    298       let dictoffset = 1;
    299       if (objInfo == 0xF) {
    300         const int_type = buffer[offset + 1];
    301         const intType = (int_type & 0xF0) / 0x10;
    302         if (intType != 0x1) {
    303           console.error("0xD: UNEXPECTED LENGTH-INT TYPE! " + intType);
    304         }
    305         const intInfo = int_type & 0x0F;
    306         const intLength = Math.pow(2, intInfo);
    307         dictoffset = 2 + intLength;
    308         if (intLength < 3) {
    309           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    310         } else {
    311           length = readUInt(buffer.slice(offset + 2, offset + 2 + intLength));
    312         }
    313       }
    314       if (length * 2 * objectRefSize > exports.maxObjectSize) {
    315         throw new Error("To little heap space available!");
    316       }
    317       if (debug) {
    318         console.log("Parsing dictionary #" + tableOffset);
    319       }
    320       const dict = {};
    321       for (let i = 0; i < length; i++) {
    322         const keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize));
    323         const valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize));
    324         const key = parseObject(keyRef);
    325         const val = parseObject(valRef);
    326         if (debug) {
    327           console.log("  DICT #" + tableOffset + ": Mapped " + key + " to " + val);
    328         }
    329         dict[key] = val;
    330       }
    331       return dict;
    332     }
    333   }
    334 
    335   return [ parseObject(topObject) ];
    336 };
    337 
    338 function readUInt(buffer, start) {
    339   start = start || 0;
    340 
    341   let l = 0;
    342   for (let i = start; i < buffer.length; i++) {
    343     l <<= 8;
    344     l |= buffer[i] & 0xFF;
    345   }
    346   return l;
    347 }
    348 
    349 // we're just going to toss the high order bits because javascript doesn't have 64-bit ints
    350 function readUInt64BE(buffer, start) {
    351   const data = buffer.slice(start, start + 8);
    352   return data.readUInt32BE(4, 8);
    353 }
    354 
    355 function swapBytes(buffer) {
    356   const len = buffer.length;
    357   for (let i = 0; i < len; i += 2) {
    358     const a = buffer[i];
    359     buffer[i] = buffer[i+1];
    360     buffer[i+1] = a;
    361   }
    362   return buffer;
    363 }