Repositorio del curso CCOM4030 el semestre B91 del proyecto Artesanías con el Instituto de Cultura

bplistParser.js 12KB

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