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