|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /** |
|
6 * Module for reading Property Lists (.plist) files |
|
7 * ------------------------------------------------ |
|
8 * This module functions as a reader for Apple Property Lists (.plist files). |
|
9 * It supports both binary and xml formatted property lists. It does not |
|
10 * support the legacy ASCII format. Reading of Cocoa's Keyed Archives serialized |
|
11 * to binary property lists isn't supported either. |
|
12 * |
|
13 * Property Lists objects are represented by standard JS and Mozilla types, |
|
14 * namely: |
|
15 * |
|
16 * XML type Cocoa Class Returned type(s) |
|
17 * -------------------------------------------------------------------------- |
|
18 * <true/> / <false/> NSNumber TYPE_PRIMITIVE boolean |
|
19 * <integer> / <real> NSNumber TYPE_PRIMITIVE number |
|
20 * TYPE_INT64 String [1] |
|
21 * Not Available NSNull TYPE_PRIMITIVE null [2] |
|
22 * TYPE_PRIMITIVE undefined [3] |
|
23 * <date/> NSDate TYPE_DATE Date |
|
24 * <data/> NSData TYPE_UINT8_ARRAY Uint8Array |
|
25 * <array/> NSArray TYPE_ARRAY Array |
|
26 * Not Available NSSet TYPE_ARRAY Array [2][4] |
|
27 * <dict/> NSDictionary TYPE_DICTIONARY Dict (from Dict.jsm) |
|
28 * |
|
29 * Use PropertyListUtils.getObjectType to detect the type of a Property list |
|
30 * object. |
|
31 * |
|
32 * ------------- |
|
33 * 1) Property lists supports storing U/Int64 numbers, while JS can only handle |
|
34 * numbers that are in this limits of float-64 (±2^53). For numbers that |
|
35 * do not outbound this limits, simple primitive number are always used. |
|
36 * Otherwise, a String object. |
|
37 * 2) About NSNull and NSSet values: While the xml format has no support for |
|
38 * representing null and set values, the documentation for the binary format |
|
39 * states that it supports storing both types. However, the Cocoa APIs for |
|
40 * serializing property lists do not seem to support either types (test with |
|
41 * NSPropertyListSerialization::propertyList:isValidForFormat). Furthermore, |
|
42 * if an array or a dictioanry contains a NSNull or a NSSet value, they cannot |
|
43 * be serialized to a property list. |
|
44 * As for usage within OS X, not surprisingly there's no known usage of |
|
45 * storing either of these types in a property list. It seems that, for now, |
|
46 * Apple is keeping the features of binary and xml formats in sync, probably as |
|
47 * long as the XML format is not officially deprecated. |
|
48 * 3) Not used anywhere. |
|
49 * 4) About NSSet representation: For the time being, we represent those |
|
50 * theoretical NSSet objects the same way NSArray is represented. |
|
51 * While this would most certainly work, it is not the right way to handle |
|
52 * it. A more correct representation for a set is a js generator, which would |
|
53 * read the set lazily and has no indices semantics. |
|
54 */ |
|
55 |
|
56 "use strict"; |
|
57 |
|
58 this.EXPORTED_SYMBOLS = ["PropertyListUtils"]; |
|
59 |
|
60 const Cc = Components.classes; |
|
61 const Ci = Components.interfaces; |
|
62 const Cu = Components.utils; |
|
63 |
|
64 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
65 |
|
66 XPCOMUtils.defineLazyModuleGetter(this, "Dict", |
|
67 "resource://gre/modules/Dict.jsm"); |
|
68 XPCOMUtils.defineLazyModuleGetter(this, "ctypes", |
|
69 "resource://gre/modules/ctypes.jsm"); |
|
70 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
71 "resource://gre/modules/Services.jsm"); |
|
72 |
|
73 this.PropertyListUtils = Object.freeze({ |
|
74 /** |
|
75 * Asynchronously reads a file as a property list. |
|
76 * |
|
77 * @param aFile (nsIDOMBlob/nsILocalFile) |
|
78 * the file to be read as a property list. |
|
79 * @param aCallback |
|
80 * If the property list is read successfully, aPropertyListRoot is set |
|
81 * to the root object of the property list. |
|
82 * Use getPropertyListObjectType to detect its type. |
|
83 * If it's not read successfully, aPropertyListRoot is set to null. |
|
84 * The reaon for failure is reported to the Error Console. |
|
85 */ |
|
86 read: function PLU_read(aFile, aCallback) { |
|
87 if (!(aFile instanceof Ci.nsILocalFile || aFile instanceof Ci.nsIDOMFile)) |
|
88 throw new Error("aFile is not a file object"); |
|
89 if (typeof(aCallback) != "function") |
|
90 throw new Error("Invalid value for aCallback"); |
|
91 |
|
92 // We guarantee not to throw directly for any other exceptions, and always |
|
93 // call aCallback. |
|
94 Services.tm.mainThread.dispatch(function() { |
|
95 let file = aFile; |
|
96 try { |
|
97 if (file instanceof Ci.nsILocalFile) { |
|
98 if (!file.exists()) |
|
99 throw new Error("The file pointed by aFile does not exist"); |
|
100 |
|
101 file = new File(file); |
|
102 } |
|
103 |
|
104 let fileReader = Cc["@mozilla.org/files/filereader;1"]. |
|
105 createInstance(Ci.nsIDOMFileReader); |
|
106 let onLoadEnd = function() { |
|
107 let root = null; |
|
108 try { |
|
109 fileReader.removeEventListener("loadend", onLoadEnd, false); |
|
110 if (fileReader.readyState != fileReader.DONE) |
|
111 throw new Error("Could not read file contents: " + fileReader.error); |
|
112 |
|
113 root = this._readFromArrayBufferSync(fileReader.result); |
|
114 } |
|
115 finally { |
|
116 aCallback(root); |
|
117 } |
|
118 }.bind(this); |
|
119 fileReader.addEventListener("loadend", onLoadEnd, false); |
|
120 fileReader.readAsArrayBuffer(file); |
|
121 } |
|
122 catch(ex) { |
|
123 aCallback(null); |
|
124 throw ex; |
|
125 } |
|
126 }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); |
|
127 }, |
|
128 |
|
129 /** |
|
130 * DO NOT USE ME. Once Bug 718189 is fixed, this method won't be public. |
|
131 * |
|
132 * Synchronously read an ArrayBuffer contents as a property list. |
|
133 */ |
|
134 _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) { |
|
135 if (BinaryPropertyListReader.prototype.canProcess(aBuffer)) |
|
136 return new BinaryPropertyListReader(aBuffer).root; |
|
137 |
|
138 // Convert the buffer into an XML tree. |
|
139 let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. |
|
140 createInstance(Ci.nsIDOMParser); |
|
141 let bytesView = new Uint8Array(aBuffer); |
|
142 try { |
|
143 let doc = domParser.parseFromBuffer(bytesView, bytesView.length, |
|
144 "application/xml"); |
|
145 return new XMLPropertyListReader(doc).root; |
|
146 } |
|
147 catch(ex) { |
|
148 throw new Error("aBuffer cannot be parsed as a DOM document: " + ex); |
|
149 } |
|
150 return null; |
|
151 }, |
|
152 |
|
153 TYPE_PRIMITIVE: 0, |
|
154 TYPE_DATE: 1, |
|
155 TYPE_UINT8_ARRAY: 2, |
|
156 TYPE_ARRAY: 3, |
|
157 TYPE_DICTIONARY: 4, |
|
158 TYPE_INT64: 5, |
|
159 |
|
160 /** |
|
161 * Get the type in which the given property list object is represented. |
|
162 * Check the header for the mapping between the TYPE* constants to js types |
|
163 * and objects. |
|
164 * |
|
165 * @return one of the TYPE_* constants listed above. |
|
166 * @note this method is merely for convenience. It has no magic to detect |
|
167 * that aObject is indeed a property list object created by this module. |
|
168 */ |
|
169 getObjectType: function PLU_getObjectType(aObject) { |
|
170 if (aObject === null || typeof(aObject) != "object") |
|
171 return this.TYPE_PRIMITIVE; |
|
172 |
|
173 // Given current usage, we could assume that aObject was created in the |
|
174 // scope of this module, but in future, this util may be used as part of |
|
175 // serializing js objects to a property list - in which case the object |
|
176 // would most likely be created in the caller's scope. |
|
177 let global = Cu.getGlobalForObject(aObject); |
|
178 |
|
179 if (global.Dict && aObject instanceof global.Dict) |
|
180 return this.TYPE_DICTIONARY; |
|
181 if (Array.isArray(aObject)) |
|
182 return this.TYPE_ARRAY; |
|
183 if (aObject instanceof global.Date) |
|
184 return this.TYPE_DATE; |
|
185 if (aObject instanceof global.Uint8Array) |
|
186 return this.TYPE_UINT8_ARRAY; |
|
187 if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject) |
|
188 return this.TYPE_INT64; |
|
189 |
|
190 throw new Error("aObject is not as a property list object."); |
|
191 }, |
|
192 |
|
193 /** |
|
194 * Wraps a 64-bit stored in the form of a string primitive as a String object, |
|
195 * which we can later distiguish from regular string values. |
|
196 * @param aPrimitive |
|
197 * a number in the form of either a primitive string or a primitive number. |
|
198 * @return a String wrapper around aNumberStr that can later be identified |
|
199 * as holding 64-bit number using getObjectType. |
|
200 */ |
|
201 wrapInt64: function PLU_wrapInt64(aPrimitive) { |
|
202 if (typeof(aPrimitive) != "string" && typeof(aPrimitive) != "number") |
|
203 throw new Error("aPrimitive should be a string primitive"); |
|
204 |
|
205 let wrapped = new String(aPrimitive); |
|
206 Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true }); |
|
207 return wrapped; |
|
208 } |
|
209 }); |
|
210 |
|
211 /** |
|
212 * Here's the base structure of binary-format property lists. |
|
213 * 1) Header - magic number |
|
214 * - 6 bytes - "bplist" |
|
215 * - 2 bytes - version number. This implementation only supports version 00. |
|
216 * 2) Objects Table |
|
217 * Variable-sized objects, see _readObject for how various types of objects |
|
218 * are constructed. |
|
219 * 3) Offsets Table |
|
220 * The offset of each object in the objects table. The integer size is |
|
221 * specified in the trailer. |
|
222 * 4) Trailer |
|
223 * - 6 unused bytes |
|
224 * - 1 byte: the size of integers in the offsets table |
|
225 * - 1 byte: the size of object references for arrays, sets and |
|
226 * dictionaries. |
|
227 * - 8 bytes: the number of objects in the objects table |
|
228 * - 8 bytes: the index of the root object's offset in the offsets table. |
|
229 * - 8 bytes: the offset of the offsets table. |
|
230 * |
|
231 * Note: all integers are stored in big-endian form. |
|
232 */ |
|
233 |
|
234 /** |
|
235 * Reader for binary-format property lists. |
|
236 * |
|
237 * @param aBuffer |
|
238 * ArrayBuffer object from which the binary plist should be read. |
|
239 */ |
|
240 function BinaryPropertyListReader(aBuffer) { |
|
241 this._buffer = aBuffer; |
|
242 |
|
243 const JS_MAX_INT = Math.pow(2,53); |
|
244 this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT); |
|
245 this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT); |
|
246 this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT); |
|
247 |
|
248 try { |
|
249 this._readTrailerInfo(); |
|
250 this._readObjectsOffsets(); |
|
251 } |
|
252 catch(ex) { |
|
253 throw new Error("Could not read aBuffer as a binary property list"); |
|
254 } |
|
255 this._objects = []; |
|
256 } |
|
257 |
|
258 BinaryPropertyListReader.prototype = { |
|
259 /** |
|
260 * Checks if the given ArrayBuffer can be read as a binary property list. |
|
261 * It can be called on the prototype. |
|
262 */ |
|
263 canProcess: function BPLR_canProcess(aBuffer) |
|
264 [String.fromCharCode(c) for each (c in new Uint8Array(aBuffer, 0, 8))]. |
|
265 join("") == "bplist00", |
|
266 |
|
267 get root() this._readObject(this._rootObjectIndex), |
|
268 |
|
269 _readTrailerInfo: function BPLR__readTrailer() { |
|
270 // The first 6 bytes of the 32-bytes trailer are unused |
|
271 let trailerOffset = this._buffer.byteLength - 26; |
|
272 [this._offsetTableIntegerSize, this._objectRefSize] = |
|
273 this._readUnsignedInts(trailerOffset, 1, 2); |
|
274 |
|
275 [this._numberOfObjects, this._rootObjectIndex, this._offsetTableOffset] = |
|
276 this._readUnsignedInts(trailerOffset + 2, 8, 3); |
|
277 }, |
|
278 |
|
279 _readObjectsOffsets: function BPLR__readObjectsOffsets() { |
|
280 this._offsetTable = this._readUnsignedInts(this._offsetTableOffset, |
|
281 this._offsetTableIntegerSize, |
|
282 this._numberOfObjects); |
|
283 }, |
|
284 |
|
285 // TODO: This should be removed once DataView is implemented (Bug 575688). |
|
286 _swapForBigEndian: |
|
287 function BPLR__swapForBigEndian(aByteOffset, aIntSize, aNumberOfInts) { |
|
288 let bytesCount = aIntSize * aNumberOfInts; |
|
289 let bytes = new Uint8Array(this._buffer, aByteOffset, bytesCount); |
|
290 let swapped = new Uint8Array(bytesCount); |
|
291 for (let i = 0; i < aNumberOfInts; i++) { |
|
292 for (let j = 0; j < aIntSize; j++) { |
|
293 swapped[(i * aIntSize) + j] = bytes[(i * aIntSize) + (aIntSize - 1 - j)]; |
|
294 } |
|
295 } |
|
296 return swapped; |
|
297 }, |
|
298 |
|
299 _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) { |
|
300 let swapped = this._swapForBigEndian(aByteOffset, 8, 1); |
|
301 let lo = new Uint32Array(swapped.buffer, 0, 1)[0]; |
|
302 let hi = new Int32Array(swapped.buffer, 4, 1)[0]; |
|
303 let int64 = ctypes.Int64.join(hi, lo); |
|
304 if (ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 || |
|
305 ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1) |
|
306 return PropertyListUtils.wrapInt64(int64.toString()); |
|
307 |
|
308 return parseInt(int64.toString(), 10); |
|
309 }, |
|
310 |
|
311 _readReal: function BPLR__readReal(aByteOffset, aRealSize) { |
|
312 let swapped = this._swapForBigEndian(aByteOffset, aRealSize, 1); |
|
313 if (aRealSize == 4) |
|
314 return new Float32Array(swapped.buffer, 0, 1)[0]; |
|
315 if (aRealSize == 8) |
|
316 return new Float64Array(swapped.buffer, 0, 1)[0]; |
|
317 |
|
318 throw new Error("Unsupported real size: " + aRealSize); |
|
319 }, |
|
320 |
|
321 OBJECT_TYPE_BITS: { |
|
322 SIMPLE: parseInt("0000", 2), |
|
323 INTEGER: parseInt("0001", 2), |
|
324 REAL: parseInt("0010", 2), |
|
325 DATE: parseInt("0011", 2), |
|
326 DATA: parseInt("0100", 2), |
|
327 ASCII_STRING: parseInt("0101", 2), |
|
328 UNICODE_STRING: parseInt("0110", 2), |
|
329 UID: parseInt("1000", 2), |
|
330 ARRAY: parseInt("1010", 2), |
|
331 SET: parseInt("1100", 2), |
|
332 DICTIONARY: parseInt("1101", 2) |
|
333 }, |
|
334 |
|
335 ADDITIONAL_INFO_BITS: { |
|
336 // Applies to OBJECT_TYPE_BITS.SIMPLE |
|
337 NULL: parseInt("0000", 2), |
|
338 FALSE: parseInt("1000", 2), |
|
339 TRUE: parseInt("1001", 2), |
|
340 FILL_BYTE: parseInt("1111", 2), |
|
341 // Applies to OBJECT_TYPE_BITS.DATE |
|
342 DATE: parseInt("0011", 2), |
|
343 // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY, |
|
344 // SET and DICTIONARY. |
|
345 LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2) |
|
346 }, |
|
347 |
|
348 /** |
|
349 * Returns an object descriptor in the form of two integers: object type and |
|
350 * additional info. |
|
351 * |
|
352 * @param aByteOffset |
|
353 * the descriptor's offset. |
|
354 * @return [objType, additionalInfo] - the object type and additional info. |
|
355 * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS |
|
356 */ |
|
357 _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) { |
|
358 // The first four bits hold the object type. For some types, additional |
|
359 // info is held in the other 4 bits. |
|
360 let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0]; |
|
361 return [(byte & 0xF0) >> 4, byte & 0x0F]; |
|
362 }, |
|
363 |
|
364 _readDate: function BPLR__readDate(aByteOffset) { |
|
365 // That's the reference date of NSDate. |
|
366 let date = new Date("1 January 2001, GMT"); |
|
367 |
|
368 // NSDate values are float values, but setSeconds takes an integer. |
|
369 date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000); |
|
370 return date; |
|
371 }, |
|
372 |
|
373 /** |
|
374 * Reads a portion of the buffer as a string. |
|
375 * |
|
376 * @param aByteOffset |
|
377 * The offset in the buffer at which the string starts |
|
378 * @param aNumberOfChars |
|
379 * The length of the string to be read (that is the number of |
|
380 * characters, not bytes). |
|
381 * @param aUnicode |
|
382 * Whether or not it is a unicode string. |
|
383 * @return the string read. |
|
384 * |
|
385 * @note this is tested to work well with unicode surrogate pairs. Because |
|
386 * all unicode characters are read as 2-byte integers, unicode surrogate |
|
387 * pairs are read from the buffer in the form of two integers, as required |
|
388 * by String.fromCharCode. |
|
389 */ |
|
390 _readString: |
|
391 function BPLR__readString(aByteOffset, aNumberOfChars, aUnicode) { |
|
392 let codes = this._readUnsignedInts(aByteOffset, aUnicode ? 2 : 1, |
|
393 aNumberOfChars); |
|
394 return [String.fromCharCode(c) for each (c in codes)].join(""); |
|
395 }, |
|
396 |
|
397 /** |
|
398 * Reads an array of unsigned integers from the buffer. Integers larger than |
|
399 * one byte are read in big endian form. |
|
400 * |
|
401 * @param aByteOffset |
|
402 * The offset in the buffer at which the array starts. |
|
403 * @param aIntSize |
|
404 * The size of each int in the array. |
|
405 * @param aLength |
|
406 * The number of ints in the array. |
|
407 * @param [optional] aBigIntAllowed (default: false) |
|
408 * Whether or not to accept integers which outbounds JS limits for |
|
409 * numbers (±2^53) in the form of a String. |
|
410 * @return an array of integers (number primitive and/or Strings for large |
|
411 * numbers (see header)). |
|
412 * @throws if aBigIntAllowed is false and one of the integers in the array |
|
413 * cannot be represented by a primitive js number. |
|
414 */ |
|
415 _readUnsignedInts: |
|
416 function BPLR__readUnsignedInts(aByteOffset, aIntSize, aLength, aBigIntAllowed) { |
|
417 if (aIntSize == 1) |
|
418 return new Uint8Array(this._buffer, aByteOffset, aLength); |
|
419 |
|
420 // There are two reasons for the complexity you see here: |
|
421 // (1) 64-bit integers - For which we use ctypes. When possible, the |
|
422 // number is converted back to js's default float-64 type. |
|
423 // (2) The DataView object for ArrayBuffer, which takes care of swaping |
|
424 // bytes, is not yet implemented (bug 575688). |
|
425 let swapped = this._swapForBigEndian(aByteOffset, aIntSize, aLength); |
|
426 if (aIntSize == 2) |
|
427 return new Uint16Array(swapped.buffer); |
|
428 if (aIntSize == 4) |
|
429 return new Uint32Array(swapped.buffer); |
|
430 if (aIntSize == 8) { |
|
431 let intsArray = []; |
|
432 let lo_hi_view = new Uint32Array(swapped.buffer); |
|
433 for (let i = 0; i < lo_hi_view.length; i += 2) { |
|
434 let [lo, hi] = [lo_hi_view[i], lo_hi_view[i+1]]; |
|
435 let uint64 = ctypes.UInt64.join(hi, lo); |
|
436 if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) { |
|
437 if (aBigIntAllowed === true) |
|
438 intsArray.push(PropertyListUtils.wrapInt64(uint64.toString())); |
|
439 else |
|
440 throw new Error("Integer too big to be read as float 64"); |
|
441 } |
|
442 else { |
|
443 intsArray.push(parseInt(uint64.toString(), 10)); |
|
444 } |
|
445 } |
|
446 return intsArray; |
|
447 } |
|
448 throw new Error("Unsupported size: " + aIntSize); |
|
449 }, |
|
450 |
|
451 /** |
|
452 * Reads from the buffer the data object-count and the offset at which the |
|
453 * first object starts. |
|
454 * |
|
455 * @param aObjectOffset |
|
456 * the object's offset. |
|
457 * @return [offset, count] - the offset in the buffer at which the first |
|
458 * object in data starts, and the number of objects. |
|
459 */ |
|
460 _readDataOffsetAndCount: |
|
461 function BPLR__readDataOffsetAndCount(aObjectOffset) { |
|
462 // The length of some objects in the data can be stored in two ways: |
|
463 // * If it is small enough, it is stored in the second four bits of the |
|
464 // object descriptors. |
|
465 // * Otherwise, those bits are set to 1111, indicating that the next byte |
|
466 // consists of the integer size of the data-length (also stored in the form |
|
467 // of an object descriptor). The length follows this byte. |
|
468 let [, maybeLength] = this._readObjectDescriptor(aObjectOffset); |
|
469 if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS) |
|
470 return [aObjectOffset + 1, maybeLength]; |
|
471 |
|
472 let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1); |
|
473 |
|
474 // The int size is 2^intSizeInfo. |
|
475 let intSize = Math.pow(2, intSizeInfo); |
|
476 let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0]; |
|
477 return [aObjectOffset + 2 + intSize, dataLength]; |
|
478 }, |
|
479 |
|
480 /** |
|
481 * Read array from the buffer and wrap it as a js array. |
|
482 * @param aObjectOffset |
|
483 * the offset in the buffer at which the array starts. |
|
484 * @param aNumberOfObjects |
|
485 * the number of objects in the array. |
|
486 * @return a js array. |
|
487 */ |
|
488 _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) { |
|
489 let refs = this._readUnsignedInts(aObjectOffset, |
|
490 this._objectRefSize, |
|
491 aNumberOfObjects); |
|
492 |
|
493 let array = new Array(aNumberOfObjects); |
|
494 let readObjectBound = this._readObject.bind(this); |
|
495 |
|
496 // Each index in the returned array is a lazy getter for its object. |
|
497 Array.prototype.forEach.call(refs, function(ref, objIndex) { |
|
498 Object.defineProperty(array, objIndex, { |
|
499 get: function() { |
|
500 delete array[objIndex]; |
|
501 return array[objIndex] = readObjectBound(ref); |
|
502 }, |
|
503 configurable: true, |
|
504 enumerable: true |
|
505 }); |
|
506 }, this); |
|
507 return array; |
|
508 }, |
|
509 |
|
510 /** |
|
511 * Reads dictionary from the buffer and wraps it as a Dict object (as defined |
|
512 * in Dict.jsm). |
|
513 * @param aObjectOffset |
|
514 * the offset in the buffer at which the dictionary starts |
|
515 * @param aNumberOfObjects |
|
516 * the number of keys in the dictionary |
|
517 * @return Dict.jsm-style dictionary. |
|
518 */ |
|
519 _wrapDictionary: function(aObjectOffset, aNumberOfObjects) { |
|
520 // A dictionary in the binary format is stored as a list of references to |
|
521 // key-objects, followed by a list of references to the value-objects for |
|
522 // those keys. The size of each list is aNumberOfObjects * this._objectRefSize. |
|
523 let dict = new Dict(); |
|
524 if (aNumberOfObjects == 0) |
|
525 return dict; |
|
526 |
|
527 let keyObjsRefs = this._readUnsignedInts(aObjectOffset, this._objectRefSize, |
|
528 aNumberOfObjects); |
|
529 let valObjsRefs = |
|
530 this._readUnsignedInts(aObjectOffset + aNumberOfObjects * this._objectRefSize, |
|
531 this._objectRefSize, aNumberOfObjects); |
|
532 for (let i = 0; i < aNumberOfObjects; i++) { |
|
533 let key = this._readObject(keyObjsRefs[i]); |
|
534 let readBound = this._readObject.bind(this, valObjsRefs[i]); |
|
535 dict.setAsLazyGetter(key, readBound); |
|
536 } |
|
537 return dict; |
|
538 }, |
|
539 |
|
540 /** |
|
541 * Reads an object at the spcified index in the object table |
|
542 * @param aObjectIndex |
|
543 * index at the object table |
|
544 * @return the property list object at the given index. |
|
545 */ |
|
546 _readObject: function BPLR__readObject(aObjectIndex) { |
|
547 // If the object was previously read, return the cached object. |
|
548 if (this._objects[aObjectIndex] !== undefined) |
|
549 return this._objects[aObjectIndex]; |
|
550 |
|
551 let objOffset = this._offsetTable[aObjectIndex]; |
|
552 let [objType, additionalInfo] = this._readObjectDescriptor(objOffset); |
|
553 let value; |
|
554 switch (objType) { |
|
555 case this.OBJECT_TYPE_BITS.SIMPLE: { |
|
556 switch (additionalInfo) { |
|
557 case this.ADDITIONAL_INFO_BITS.NULL: |
|
558 value = null; |
|
559 break; |
|
560 case this.ADDITIONAL_INFO_BITS.FILL_BYTE: |
|
561 value = undefined; |
|
562 break; |
|
563 case this.ADDITIONAL_INFO_BITS.FALSE: |
|
564 value = false; |
|
565 break; |
|
566 case this.ADDITIONAL_INFO_BITS.TRUE: |
|
567 value = true; |
|
568 break; |
|
569 default: |
|
570 throw new Error("Unexpected value!"); |
|
571 } |
|
572 break; |
|
573 } |
|
574 |
|
575 case this.OBJECT_TYPE_BITS.INTEGER: { |
|
576 // The integer is sized 2^additionalInfo. |
|
577 let intSize = Math.pow(2, additionalInfo); |
|
578 |
|
579 // For objects, 64-bit integers are always signed. Negative integers |
|
580 // are always represented by a 64-bit integer. |
|
581 if (intSize == 8) |
|
582 value = this._readSignedInt64(objOffset + 1); |
|
583 else |
|
584 value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0]; |
|
585 break; |
|
586 } |
|
587 |
|
588 case this.OBJECT_TYPE_BITS.REAL: { |
|
589 // The real is sized 2^additionalInfo. |
|
590 value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo)); |
|
591 break; |
|
592 } |
|
593 |
|
594 case this.OBJECT_TYPE_BITS.DATE: { |
|
595 if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE) |
|
596 throw new Error("Unexpected value"); |
|
597 |
|
598 value = this._readDate(objOffset + 1); |
|
599 break; |
|
600 } |
|
601 |
|
602 case this.OBJECT_TYPE_BITS.DATA: { |
|
603 let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset); |
|
604 value = this._readUnsignedInts(offset, 1, bytesCount); |
|
605 break; |
|
606 } |
|
607 |
|
608 case this.OBJECT_TYPE_BITS.ASCII_STRING: { |
|
609 let [offset, charsCount] = this._readDataOffsetAndCount(objOffset); |
|
610 value = this._readString(offset, charsCount, false); |
|
611 break; |
|
612 } |
|
613 |
|
614 case this.OBJECT_TYPE_BITS.UNICODE_STRING: { |
|
615 let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset); |
|
616 value = this._readString(offset, unicharsCount, true); |
|
617 break; |
|
618 } |
|
619 |
|
620 case this.OBJECT_TYPE_BITS.UID: { |
|
621 // UIDs are only used in Keyed Archives, which are not yet supported. |
|
622 throw new Error("Keyed Archives are not supported"); |
|
623 } |
|
624 |
|
625 case this.OBJECT_TYPE_BITS.ARRAY: |
|
626 case this.OBJECT_TYPE_BITS.SET: { |
|
627 // Note: For now, we fallback to handle sets the same way we handle |
|
628 // arrays. See comments in the header of this file. |
|
629 |
|
630 // The bytes following the count are references to objects (indices). |
|
631 // Each reference is an unsigned int with size=this._objectRefSize. |
|
632 let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); |
|
633 value = this._wrapArray(offset, objectsCount); |
|
634 break; |
|
635 } |
|
636 |
|
637 case this.OBJECT_TYPE_BITS.DICTIONARY: { |
|
638 let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); |
|
639 value = this._wrapDictionary(offset, objectsCount); |
|
640 break; |
|
641 } |
|
642 |
|
643 default: { |
|
644 throw new Error("Unknown object type: " + objType); |
|
645 } |
|
646 } |
|
647 |
|
648 return this._objects[aObjectIndex] = value; |
|
649 } |
|
650 }; |
|
651 |
|
652 /** |
|
653 * Reader for XML property lists. |
|
654 * |
|
655 * @param aDOMDoc |
|
656 * the DOM document to be read as a property list. |
|
657 */ |
|
658 function XMLPropertyListReader(aDOMDoc) { |
|
659 let docElt = aDOMDoc.documentElement; |
|
660 if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild) |
|
661 throw new Error("aDoc is not a property list document"); |
|
662 |
|
663 this._plistRootElement = docElt.firstElementChild; |
|
664 } |
|
665 |
|
666 XMLPropertyListReader.prototype = { |
|
667 get root() this._readObject(this._plistRootElement), |
|
668 |
|
669 /** |
|
670 * Convert a dom element to a property list object. |
|
671 * @param aDOMElt |
|
672 * a dom element in a xml tree of a property list. |
|
673 * @return a js object representing the property list object. |
|
674 */ |
|
675 _readObject: function XPLR__readObject(aDOMElt) { |
|
676 switch (aDOMElt.localName) { |
|
677 case "true": |
|
678 return true; |
|
679 case "false": |
|
680 return false; |
|
681 case "string": |
|
682 case "key": |
|
683 return aDOMElt.textContent; |
|
684 case "integer": |
|
685 return this._readInteger(aDOMElt); |
|
686 case "real": { |
|
687 let number = parseFloat(aDOMElt.textContent.trim()); |
|
688 if (isNaN(number)) |
|
689 throw "Could not parse float value"; |
|
690 return number; |
|
691 } |
|
692 case "date": |
|
693 return new Date(aDOMElt.textContent); |
|
694 case "data": |
|
695 // Strip spaces and new lines. |
|
696 let base64str = aDOMElt.textContent.replace(/\s*/g, ""); |
|
697 let decoded = atob(base64str); |
|
698 return new Uint8Array([decoded.charCodeAt(i) for (i in decoded)]); |
|
699 case "dict": |
|
700 return this._wrapDictionary(aDOMElt); |
|
701 case "array": |
|
702 return this._wrapArray(aDOMElt); |
|
703 default: |
|
704 throw new Error("Unexpected tagname"); |
|
705 } |
|
706 }, |
|
707 |
|
708 _readInteger: function XPLR__readInteger(aDOMElt) { |
|
709 // The integer may outbound js's max/min integer value. We recognize this |
|
710 // case by comparing the parsed number to the original string value. |
|
711 // In case of an outbound, we fallback to return the number as a string. |
|
712 let numberAsString = aDOMElt.textContent.toString(); |
|
713 let parsedNumber = parseInt(numberAsString, 10); |
|
714 if (isNaN(parsedNumber)) |
|
715 throw new Error("Could not parse integer value"); |
|
716 |
|
717 if (parsedNumber.toString() == numberAsString) |
|
718 return parsedNumber; |
|
719 |
|
720 return PropertyListUtils.wrapInt64(numberAsString); |
|
721 }, |
|
722 |
|
723 _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) { |
|
724 // <dict> |
|
725 // <key>my true bool</key> |
|
726 // <true/> |
|
727 // <key>my string key</key> |
|
728 // <string>My String Key</string> |
|
729 // </dict> |
|
730 if (aDOMElt.children.length % 2 != 0) |
|
731 throw new Error("Invalid dictionary"); |
|
732 let dict = new Dict(); |
|
733 for (let i = 0; i < aDOMElt.children.length; i += 2) { |
|
734 let keyElem = aDOMElt.children[i]; |
|
735 let valElem = aDOMElt.children[i + 1]; |
|
736 |
|
737 if (keyElem.localName != "key") |
|
738 throw new Error("Invalid dictionary"); |
|
739 |
|
740 let keyName = this._readObject(keyElem); |
|
741 let readBound = this._readObject.bind(this, valElem); |
|
742 dict.setAsLazyGetter(keyName, readBound); |
|
743 } |
|
744 return dict; |
|
745 }, |
|
746 |
|
747 _wrapArray: function XPLR__wrapArray(aDOMElt) { |
|
748 // <array> |
|
749 // <string>...</string> |
|
750 // <integer></integer> |
|
751 // <dict> |
|
752 // .... |
|
753 // </dict> |
|
754 // </array> |
|
755 |
|
756 // Each element in the array is a lazy getter for its property list object. |
|
757 let array = []; |
|
758 let readObjectBound = this._readObject.bind(this); |
|
759 Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) { |
|
760 Object.defineProperty(array, elemIndex, { |
|
761 get: function() { |
|
762 delete array[elemIndex]; |
|
763 return array[elemIndex] = readObjectBound(elem); |
|
764 }, |
|
765 configurable: true, |
|
766 enumerable: true |
|
767 }); |
|
768 }); |
|
769 return array; |
|
770 } |
|
771 }; |