Bug 1669129 - [devtools] Enable devtools.overflow.debugging.enabled. r=jdescottes
[gecko.git] / toolkit / modules / PropertyListUtils.jsm
blobadfaf6cf0108db6207efc9046c588f6b5d94ba14
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/. */
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   Map
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 dictionary (Map) 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  */
56 "use strict";
58 var EXPORTED_SYMBOLS = ["PropertyListUtils"];
60 const { XPCOMUtils } = ChromeUtils.import(
61   "resource://gre/modules/XPCOMUtils.jsm"
64 XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "File", "FileReader"]);
66 ChromeUtils.defineModuleGetter(
67   this,
68   "ctypes",
69   "resource://gre/modules/ctypes.jsm"
71 ChromeUtils.defineModuleGetter(
72   this,
73   "Services",
74   "resource://gre/modules/Services.jsm"
77 var PropertyListUtils = Object.freeze({
78   /**
79    * Asynchronously reads a file as a property list.
80    *
81    * @param aFile (Blob/nsIFile)
82    *        the file to be read as a property list.
83    * @param aCallback
84    *        If the property list is read successfully, aPropertyListRoot is set
85    *        to the root object of the property list.
86    *        Use getPropertyListObjectType to detect its type.
87    *        If it's not read successfully, aPropertyListRoot is set to null.
88    *        The reaon for failure is reported to the Error Console.
89    */
90   read: function PLU_read(aFile, aCallback) {
91     if (!(aFile instanceof Ci.nsIFile || aFile instanceof File)) {
92       throw new Error("aFile is not a file object");
93     }
94     if (typeof aCallback != "function") {
95       throw new Error("Invalid value for aCallback");
96     }
98     // We guarantee not to throw directly for any other exceptions, and always
99     // call aCallback.
100     Services.tm.dispatchToMainThread(() => {
101       let self = this;
102       function readDOMFile(aFile) {
103         let fileReader = new FileReader();
104         let onLoadEnd = function() {
105           let root = null;
106           try {
107             fileReader.removeEventListener("loadend", onLoadEnd);
108             if (fileReader.readyState != fileReader.DONE) {
109               throw new Error(
110                 "Could not read file contents: " + fileReader.error
111               );
112             }
114             root = self._readFromArrayBufferSync(fileReader.result);
115           } finally {
116             aCallback(root);
117           }
118         };
119         fileReader.addEventListener("loadend", onLoadEnd);
120         fileReader.readAsArrayBuffer(aFile);
121       }
123       try {
124         if (aFile instanceof Ci.nsIFile) {
125           if (!aFile.exists()) {
126             throw new Error("The file pointed by aFile does not exist");
127           }
129           File.createFromNsIFile(aFile).then(function(aFile) {
130             readDOMFile(aFile);
131           });
132           return;
133         }
134         readDOMFile(aFile);
135       } catch (ex) {
136         aCallback(null);
137         throw ex;
138       }
139     });
140   },
142   /**
143    * DO NOT USE ME.  Once Bug 718189 is fixed, this method won't be public.
144    *
145    * Synchronously read an ArrayBuffer contents as a property list.
146    */
147   _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) {
148     if (BinaryPropertyListReader.prototype.canProcess(aBuffer)) {
149       return new BinaryPropertyListReader(aBuffer).root;
150     }
152     // Convert the buffer into an XML tree.
153     let domParser = new DOMParser();
154     let bytesView = new Uint8Array(aBuffer);
155     try {
156       let doc = domParser.parseFromBuffer(bytesView, "application/xml");
157       return new XMLPropertyListReader(doc).root;
158     } catch (ex) {
159       throw new Error("aBuffer cannot be parsed as a DOM document: " + ex);
160     }
161   },
163   TYPE_PRIMITIVE: 0,
164   TYPE_DATE: 1,
165   TYPE_UINT8_ARRAY: 2,
166   TYPE_ARRAY: 3,
167   TYPE_DICTIONARY: 4,
168   TYPE_INT64: 5,
170   /**
171    * Get the type in which the given property list object is represented.
172    * Check the header for the mapping between the TYPE* constants to js types
173    * and objects.
174    *
175    * @return one of the TYPE_* constants listed above.
176    * @note this method is merely for convenience.  It has no magic to detect
177    * that aObject is indeed a property list object created by this module.
178    */
179   getObjectType: function PLU_getObjectType(aObject) {
180     if (aObject === null || typeof aObject != "object") {
181       return this.TYPE_PRIMITIVE;
182     }
184     // Given current usage, we could assume that aObject was created in the
185     // scope of this module, but in future, this util may be used as part of
186     // serializing js objects to a property list - in which case the object
187     // would most likely be created in the caller's scope.
188     let global = Cu.getGlobalForObject(aObject);
190     if (aObject instanceof global.Map) {
191       return this.TYPE_DICTIONARY;
192     }
193     if (Array.isArray(aObject)) {
194       return this.TYPE_ARRAY;
195     }
196     if (aObject instanceof global.Date) {
197       return this.TYPE_DATE;
198     }
199     if (aObject instanceof global.Uint8Array) {
200       return this.TYPE_UINT8_ARRAY;
201     }
202     if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject) {
203       return this.TYPE_INT64;
204     }
206     throw new Error("aObject is not as a property list object.");
207   },
209   /**
210    * Wraps a 64-bit stored in the form of a string primitive as a String object,
211    * which we can later distiguish from regular string values.
212    * @param aPrimitive
213    *        a number in the form of either a primitive string or a primitive number.
214    * @return a String wrapper around aNumberStr that can later be identified
215    * as holding 64-bit number using getObjectType.
216    */
217   wrapInt64: function PLU_wrapInt64(aPrimitive) {
218     if (typeof aPrimitive != "string" && typeof aPrimitive != "number") {
219       throw new Error("aPrimitive should be a string primitive");
220     }
222     // The function converts string or number to object
223     // So eslint rule is disabled
224     // eslint-disable-next-line no-new-wrappers
225     let wrapped = new String(aPrimitive);
226     Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true });
227     return wrapped;
228   },
232  * Here's the base structure of binary-format property lists.
233  * 1) Header - magic number
234  *   - 6 bytes - "bplist"
235  *   - 2 bytes - version number. This implementation only supports version 00.
236  * 2) Objects Table
237  *    Variable-sized objects, see _readObject for how various types of objects
238  *    are constructed.
239  * 3) Offsets Table
240  *    The offset of each object in the objects table. The integer size is
241  *    specified in the trailer.
242  * 4) Trailer
243  *    - 6 unused bytes
244  *    - 1 byte:  the size of integers in the offsets table
245  *    - 1 byte:  the size of object references for arrays, sets and
246  *               dictionaries.
247  *    - 8 bytes: the number of objects in the objects table
248  *    - 8 bytes: the index of the root object's offset in the offsets table.
249  *    - 8 bytes: the offset of the offsets table.
251  * Note: all integers are stored in big-endian form.
252  */
255  * Reader for binary-format property lists.
257  * @param aBuffer
258  *        ArrayBuffer object from which the binary plist should be read.
259  */
260 function BinaryPropertyListReader(aBuffer) {
261   this._dataView = new DataView(aBuffer);
263   const JS_MAX_INT = Math.pow(2, 53);
264   this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT);
265   this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT);
266   this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT);
268   try {
269     this._readTrailerInfo();
270     this._readObjectsOffsets();
271   } catch (ex) {
272     throw new Error("Could not read aBuffer as a binary property list");
273   }
274   this._objects = [];
277 BinaryPropertyListReader.prototype = {
278   /**
279    * Checks if the given ArrayBuffer can be read as a binary property list.
280    * It can be called on the prototype.
281    */
282   canProcess: function BPLR_canProcess(aBuffer) {
283     return (
284       Array.from(new Uint8Array(aBuffer, 0, 8))
285         .map(c => String.fromCharCode(c))
286         .join("") == "bplist00"
287     );
288   },
290   get root() {
291     return this._readObject(this._rootObjectIndex);
292   },
294   _readTrailerInfo: function BPLR__readTrailer() {
295     // The first 6 bytes of the 32-bytes trailer are unused
296     let trailerOffset = this._dataView.byteLength - 26;
297     [
298       this._offsetTableIntegerSize,
299       this._objectRefSize,
300     ] = this._readUnsignedInts(trailerOffset, 1, 2);
302     [
303       this._numberOfObjects,
304       this._rootObjectIndex,
305       this._offsetTableOffset,
306     ] = this._readUnsignedInts(trailerOffset + 2, 8, 3);
307   },
309   _readObjectsOffsets: function BPLR__readObjectsOffsets() {
310     this._offsetTable = this._readUnsignedInts(
311       this._offsetTableOffset,
312       this._offsetTableIntegerSize,
313       this._numberOfObjects
314     );
315   },
317   _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) {
318     let lo = this._dataView.getUint32(aByteOffset + 4);
319     let hi = this._dataView.getInt32(aByteOffset);
320     let int64 = ctypes.Int64.join(hi, lo);
321     if (
322       ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 ||
323       ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1
324     ) {
325       return PropertyListUtils.wrapInt64(int64.toString());
326     }
328     return parseInt(int64.toString(), 10);
329   },
331   _readReal: function BPLR__readReal(aByteOffset, aRealSize) {
332     if (aRealSize == 4) {
333       return this._dataView.getFloat32(aByteOffset);
334     }
335     if (aRealSize == 8) {
336       return this._dataView.getFloat64(aByteOffset);
337     }
339     throw new Error("Unsupported real size: " + aRealSize);
340   },
342   OBJECT_TYPE_BITS: {
343     SIMPLE: parseInt("0000", 2),
344     INTEGER: parseInt("0001", 2),
345     REAL: parseInt("0010", 2),
346     DATE: parseInt("0011", 2),
347     DATA: parseInt("0100", 2),
348     ASCII_STRING: parseInt("0101", 2),
349     UNICODE_STRING: parseInt("0110", 2),
350     UID: parseInt("1000", 2),
351     ARRAY: parseInt("1010", 2),
352     SET: parseInt("1100", 2),
353     DICTIONARY: parseInt("1101", 2),
354   },
356   ADDITIONAL_INFO_BITS: {
357     // Applies to OBJECT_TYPE_BITS.SIMPLE
358     NULL: parseInt("0000", 2),
359     FALSE: parseInt("1000", 2),
360     TRUE: parseInt("1001", 2),
361     FILL_BYTE: parseInt("1111", 2),
362     // Applies to OBJECT_TYPE_BITS.DATE
363     DATE: parseInt("0011", 2),
364     // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY,
365     // SET and DICTIONARY.
366     LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2),
367   },
369   /**
370    * Returns an object descriptor in the form of two integers: object type and
371    * additional info.
372    *
373    * @param aByteOffset
374    *        the descriptor's offset.
375    * @return [objType, additionalInfo] - the object type and additional info.
376    * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS
377    */
378   _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) {
379     // The first four bits hold the object type.  For some types, additional
380     // info is held in the other 4 bits.
381     let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0];
382     return [(byte & 0xf0) >> 4, byte & 0x0f];
383   },
385   _readDate: function BPLR__readDate(aByteOffset) {
386     // That's the reference date of NSDate.
387     let date = new Date("1 January 2001, GMT");
389     // NSDate values are float values, but setSeconds takes an integer.
390     date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000);
391     return date;
392   },
394   /**
395    * Reads a portion of the buffer as a string.
396    *
397    * @param aByteOffset
398    *        The offset in the buffer at which the string starts
399    * @param aNumberOfChars
400    *        The length of the string to be read (that is the number of
401    *        characters, not bytes).
402    * @param aUnicode
403    *        Whether or not it is a unicode string.
404    * @return the string read.
405    *
406    * @note this is tested to work well with unicode surrogate pairs.  Because
407    * all unicode characters are read as 2-byte integers, unicode surrogate
408    * pairs are read from the buffer in the form of two integers, as required
409    * by String.fromCharCode.
410    */
411   _readString: function BPLR__readString(
412     aByteOffset,
413     aNumberOfChars,
414     aUnicode
415   ) {
416     let codes = this._readUnsignedInts(
417       aByteOffset,
418       aUnicode ? 2 : 1,
419       aNumberOfChars
420     );
421     return codes.map(c => String.fromCharCode(c)).join("");
422   },
424   /**
425    * Reads an array of unsigned integers from the buffer.  Integers larger than
426    * one byte are read in big endian form.
427    *
428    * @param aByteOffset
429    *        The offset in the buffer at which the array starts.
430    * @param aIntSize
431    *        The size of each int in the array.
432    * @param aLength
433    *        The number of ints in the array.
434    * @param [optional] aBigIntAllowed (default: false)
435    *        Whether or not to accept integers which outbounds JS limits for
436    *        numbers (±2^53) in the form of a String.
437    * @return an array of integers (number primitive and/or Strings for large
438    * numbers (see header)).
439    * @throws if aBigIntAllowed is false and one of the integers in the array
440    * cannot be represented by a primitive js number.
441    */
442   _readUnsignedInts: function BPLR__readUnsignedInts(
443     aByteOffset,
444     aIntSize,
445     aLength,
446     aBigIntAllowed
447   ) {
448     let uints = [];
449     for (
450       let offset = aByteOffset;
451       offset < aByteOffset + aIntSize * aLength;
452       offset += aIntSize
453     ) {
454       if (aIntSize == 1) {
455         uints.push(this._dataView.getUint8(offset));
456       } else if (aIntSize == 2) {
457         uints.push(this._dataView.getUint16(offset));
458       } else if (aIntSize == 3) {
459         let int24 = Uint8Array(4);
460         int24[3] = 0;
461         int24[2] = this._dataView.getUint8(offset);
462         int24[1] = this._dataView.getUint8(offset + 1);
463         int24[0] = this._dataView.getUint8(offset + 2);
464         uints.push(Uint32Array(int24.buffer)[0]);
465       } else if (aIntSize == 4) {
466         uints.push(this._dataView.getUint32(offset));
467       } else if (aIntSize == 8) {
468         let lo = this._dataView.getUint32(offset + 4);
469         let hi = this._dataView.getUint32(offset);
470         let uint64 = ctypes.UInt64.join(hi, lo);
471         if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) {
472           if (aBigIntAllowed === true) {
473             uints.push(PropertyListUtils.wrapInt64(uint64.toString()));
474           } else {
475             throw new Error("Integer too big to be read as float 64");
476           }
477         } else {
478           uints.push(parseInt(uint64, 10));
479         }
480       } else {
481         throw new Error("Unsupported size: " + aIntSize);
482       }
483     }
485     return uints;
486   },
488   /**
489    * Reads from the buffer the data object-count and the offset at which the
490    * first object starts.
491    *
492    * @param aObjectOffset
493    *        the object's offset.
494    * @return [offset, count] - the offset in the buffer at which the first
495    * object in data starts, and the number of objects.
496    */
497   _readDataOffsetAndCount: function BPLR__readDataOffsetAndCount(
498     aObjectOffset
499   ) {
500     // The length of some objects in the data can be stored in two ways:
501     // * If it is small enough, it is stored in the second four bits of the
502     //   object descriptors.
503     // * Otherwise, those bits are set to 1111, indicating that the next byte
504     //   consists of the integer size of the data-length (also stored in the form
505     //   of an object descriptor).  The length follows this byte.
506     let [, maybeLength] = this._readObjectDescriptor(aObjectOffset);
507     if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS) {
508       return [aObjectOffset + 1, maybeLength];
509     }
511     let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1);
513     // The int size is 2^intSizeInfo.
514     let intSize = Math.pow(2, intSizeInfo);
515     let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0];
516     return [aObjectOffset + 2 + intSize, dataLength];
517   },
519   /**
520    * Read array from the buffer and wrap it as a js array.
521    * @param aObjectOffset
522    *        the offset in the buffer at which the array starts.
523    * @param aNumberOfObjects
524    *        the number of objects in the array.
525    * @return a js array.
526    */
527   _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) {
528     let refs = this._readUnsignedInts(
529       aObjectOffset,
530       this._objectRefSize,
531       aNumberOfObjects
532     );
534     let array = new Array(aNumberOfObjects);
535     let readObjectBound = this._readObject.bind(this);
537     // Each index in the returned array is a lazy getter for its object.
538     Array.prototype.forEach.call(
539       refs,
540       function(ref, objIndex) {
541         Object.defineProperty(array, objIndex, {
542           get() {
543             delete array[objIndex];
544             return (array[objIndex] = readObjectBound(ref));
545           },
546           configurable: true,
547           enumerable: true,
548         });
549       },
550       this
551     );
552     return array;
553   },
555   /**
556    * Reads dictionary from the buffer and wraps it as a Map object.
557    * @param aObjectOffset
558    *        the offset in the buffer at which the dictionary starts
559    * @param aNumberOfObjects
560    *        the number of keys in the dictionary
561    * @return Map-style dictionary.
562    */
563   _wrapDictionary(aObjectOffset, aNumberOfObjects) {
564     // A dictionary in the binary format is stored as a list of references to
565     // key-objects, followed by a list of references to the value-objects for
566     // those keys. The size of each list is aNumberOfObjects * this._objectRefSize.
567     let dict = new Proxy(new Map(), LazyMapProxyHandler());
568     if (aNumberOfObjects == 0) {
569       return dict;
570     }
572     let keyObjsRefs = this._readUnsignedInts(
573       aObjectOffset,
574       this._objectRefSize,
575       aNumberOfObjects
576     );
577     let valObjsRefs = this._readUnsignedInts(
578       aObjectOffset + aNumberOfObjects * this._objectRefSize,
579       this._objectRefSize,
580       aNumberOfObjects
581     );
582     for (let i = 0; i < aNumberOfObjects; i++) {
583       let key = this._readObject(keyObjsRefs[i]);
584       let readBound = this._readObject.bind(this, valObjsRefs[i]);
586       dict.setAsLazyGetter(key, readBound);
587     }
588     return dict;
589   },
591   /**
592    * Reads an object at the spcified index in the object table
593    * @param aObjectIndex
594    *        index at the object table
595    * @return the property list object at the given index.
596    */
597   _readObject: function BPLR__readObject(aObjectIndex) {
598     // If the object was previously read, return the cached object.
599     if (this._objects[aObjectIndex] !== undefined) {
600       return this._objects[aObjectIndex];
601     }
603     let objOffset = this._offsetTable[aObjectIndex];
604     let [objType, additionalInfo] = this._readObjectDescriptor(objOffset);
605     let value;
606     switch (objType) {
607       case this.OBJECT_TYPE_BITS.SIMPLE: {
608         switch (additionalInfo) {
609           case this.ADDITIONAL_INFO_BITS.NULL:
610             value = null;
611             break;
612           case this.ADDITIONAL_INFO_BITS.FILL_BYTE:
613             value = undefined;
614             break;
615           case this.ADDITIONAL_INFO_BITS.FALSE:
616             value = false;
617             break;
618           case this.ADDITIONAL_INFO_BITS.TRUE:
619             value = true;
620             break;
621           default:
622             throw new Error("Unexpected value!");
623         }
624         break;
625       }
627       case this.OBJECT_TYPE_BITS.INTEGER: {
628         // The integer is sized 2^additionalInfo.
629         let intSize = Math.pow(2, additionalInfo);
631         // For objects, 64-bit integers are always signed.  Negative integers
632         // are always represented by a 64-bit integer.
633         if (intSize == 8) {
634           value = this._readSignedInt64(objOffset + 1);
635         } else {
636           value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0];
637         }
638         break;
639       }
641       case this.OBJECT_TYPE_BITS.REAL: {
642         // The real is sized 2^additionalInfo.
643         value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo));
644         break;
645       }
647       case this.OBJECT_TYPE_BITS.DATE: {
648         if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE) {
649           throw new Error("Unexpected value");
650         }
652         value = this._readDate(objOffset + 1);
653         break;
654       }
656       case this.OBJECT_TYPE_BITS.DATA: {
657         let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset);
658         value = new Uint8Array(this._readUnsignedInts(offset, 1, bytesCount));
659         break;
660       }
662       case this.OBJECT_TYPE_BITS.ASCII_STRING: {
663         let [offset, charsCount] = this._readDataOffsetAndCount(objOffset);
664         value = this._readString(offset, charsCount, false);
665         break;
666       }
668       case this.OBJECT_TYPE_BITS.UNICODE_STRING: {
669         let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset);
670         value = this._readString(offset, unicharsCount, true);
671         break;
672       }
674       case this.OBJECT_TYPE_BITS.UID: {
675         // UIDs are only used in Keyed Archives, which are not yet supported.
676         throw new Error("Keyed Archives are not supported");
677       }
679       case this.OBJECT_TYPE_BITS.ARRAY:
680       case this.OBJECT_TYPE_BITS.SET: {
681         // Note: For now, we fallback to handle sets the same way we handle
682         // arrays.  See comments in the header of this file.
684         // The bytes following the count are references to objects (indices).
685         // Each reference is an unsigned int with size=this._objectRefSize.
686         let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset);
687         value = this._wrapArray(offset, objectsCount);
688         break;
689       }
691       case this.OBJECT_TYPE_BITS.DICTIONARY: {
692         let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset);
693         value = this._wrapDictionary(offset, objectsCount);
694         break;
695       }
697       default: {
698         throw new Error("Unknown object type: " + objType);
699       }
700     }
702     return (this._objects[aObjectIndex] = value);
703   },
707  * Reader for XML property lists.
709  * @param aDOMDoc
710  *        the DOM document to be read as a property list.
711  */
712 function XMLPropertyListReader(aDOMDoc) {
713   let docElt = aDOMDoc.documentElement;
714   if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild) {
715     throw new Error("aDoc is not a property list document");
716   }
718   this._plistRootElement = docElt.firstElementChild;
721 XMLPropertyListReader.prototype = {
722   get root() {
723     return this._readObject(this._plistRootElement);
724   },
726   /**
727    * Convert a dom element to a property list object.
728    * @param aDOMElt
729    *        a dom element in a xml tree of a property list.
730    * @return a js object representing the property list object.
731    */
732   _readObject: function XPLR__readObject(aDOMElt) {
733     switch (aDOMElt.localName) {
734       case "true":
735         return true;
736       case "false":
737         return false;
738       case "string":
739       case "key":
740         return aDOMElt.textContent;
741       case "integer":
742         return this._readInteger(aDOMElt);
743       case "real": {
744         let number = parseFloat(aDOMElt.textContent.trim());
745         if (isNaN(number)) {
746           throw new Error("Could not parse float value");
747         }
748         return number;
749       }
750       case "date":
751         return new Date(aDOMElt.textContent);
752       case "data":
753         // Strip spaces and new lines.
754         let base64str = aDOMElt.textContent.replace(/\s*/g, "");
755         let decoded = atob(base64str);
756         return new Uint8Array(Array.from(decoded, c => c.charCodeAt(0)));
757       case "dict":
758         return this._wrapDictionary(aDOMElt);
759       case "array":
760         return this._wrapArray(aDOMElt);
761       default:
762         throw new Error("Unexpected tagname");
763     }
764   },
766   _readInteger: function XPLR__readInteger(aDOMElt) {
767     // The integer may outbound js's max/min integer value.  We recognize this
768     // case by comparing the parsed number to the original string value.
769     // In case of an outbound, we fallback to return the number as a string.
770     let numberAsString = aDOMElt.textContent.toString();
771     let parsedNumber = parseInt(numberAsString, 10);
772     if (isNaN(parsedNumber)) {
773       throw new Error("Could not parse integer value");
774     }
776     if (parsedNumber.toString() == numberAsString) {
777       return parsedNumber;
778     }
780     return PropertyListUtils.wrapInt64(numberAsString);
781   },
783   _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) {
784     // <dict>
785     //   <key>my true bool</key>
786     //   <true/>
787     //   <key>my string key</key>
788     //   <string>My String Key</string>
789     // </dict>
790     if (aDOMElt.children.length % 2 != 0) {
791       throw new Error("Invalid dictionary");
792     }
793     let dict = new Proxy(new Map(), LazyMapProxyHandler());
794     for (let i = 0; i < aDOMElt.children.length; i += 2) {
795       let keyElem = aDOMElt.children[i];
796       let valElem = aDOMElt.children[i + 1];
798       if (keyElem.localName != "key") {
799         throw new Error("Invalid dictionary");
800       }
802       let keyName = this._readObject(keyElem);
803       let readBound = this._readObject.bind(this, valElem);
805       dict.setAsLazyGetter(keyName, readBound);
806     }
807     return dict;
808   },
810   _wrapArray: function XPLR__wrapArray(aDOMElt) {
811     // <array>
812     //   <string>...</string>
813     //   <integer></integer>
814     //   <dict>
815     //     ....
816     //   </dict>
817     // </array>
819     // Each element in the array is a lazy getter for its property list object.
820     let array = [];
821     let readObjectBound = this._readObject.bind(this);
822     Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) {
823       Object.defineProperty(array, elemIndex, {
824         get() {
825           delete array[elemIndex];
826           return (array[elemIndex] = readObjectBound(elem));
827         },
828         configurable: true,
829         enumerable: true,
830       });
831     });
832     return array;
833   },
837  * Simple handler method to proxy calls to dict/Map objects to implement the
838  * setAsLazyGetter API. With this, a value can be set as a function that will
839  * evaluate its value and only be called when it's first retrieved.
840  * @member _lazyGetters
841  *         Set() object to hold keys invoking LazyGetter.
842  * @method get
843  *         Trap for getting property values. Ensures that if a lazyGetter is present
844  *         as value for key, then the function is evaluated, the value is cached,
845  *         and its value will be returned.
846  * @param  target
847  *         Target object. (dict/Map)
848  * @param  name
849  *         Name of operation to be invoked on target.
850  * @param  key
851  *         Key to be set, retrieved or deleted. Keys are checked for laziness.
852  * @return Returns value of "name" property of target by default. Otherwise returns
853  *         updated target.
854  */
855 function LazyMapProxyHandler() {
856   return {
857     _lazyGetters: new Set(),
858     get(target, name) {
859       switch (name) {
860         case "setAsLazyGetter":
861           return (key, value) => {
862             this._lazyGetters.add(key);
863             target.set(key, value);
864           };
865         case "get":
866           return key => {
867             if (this._lazyGetters.has(key)) {
868               target.set(key, target.get(key)());
869               this._lazyGetters.delete(key);
870             }
871             return target.get(key);
872           };
873         case "delete":
874           return key => {
875             if (this._lazyGetters.has(key)) {
876               this._lazyGetters.delete(key);
877             }
878             return target.delete(key);
879           };
880         case "has":
881           return key => target.has(key);
882         default:
883           return target[name];
884       }
885     },
886   };