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