bplistParser.js 12 KB

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