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 }