michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Module for reading Property Lists (.plist) files michael@0: * ------------------------------------------------ michael@0: * This module functions as a reader for Apple Property Lists (.plist files). michael@0: * It supports both binary and xml formatted property lists. It does not michael@0: * support the legacy ASCII format. Reading of Cocoa's Keyed Archives serialized michael@0: * to binary property lists isn't supported either. michael@0: * michael@0: * Property Lists objects are represented by standard JS and Mozilla types, michael@0: * namely: michael@0: * michael@0: * XML type Cocoa Class Returned type(s) michael@0: * -------------------------------------------------------------------------- michael@0: * / NSNumber TYPE_PRIMITIVE boolean michael@0: * / NSNumber TYPE_PRIMITIVE number michael@0: * TYPE_INT64 String [1] michael@0: * Not Available NSNull TYPE_PRIMITIVE null [2] michael@0: * TYPE_PRIMITIVE undefined [3] michael@0: * NSDate TYPE_DATE Date michael@0: * NSData TYPE_UINT8_ARRAY Uint8Array michael@0: * NSArray TYPE_ARRAY Array michael@0: * Not Available NSSet TYPE_ARRAY Array [2][4] michael@0: * NSDictionary TYPE_DICTIONARY Dict (from Dict.jsm) michael@0: * michael@0: * Use PropertyListUtils.getObjectType to detect the type of a Property list michael@0: * object. michael@0: * michael@0: * ------------- michael@0: * 1) Property lists supports storing U/Int64 numbers, while JS can only handle michael@0: * numbers that are in this limits of float-64 (±2^53). For numbers that michael@0: * do not outbound this limits, simple primitive number are always used. michael@0: * Otherwise, a String object. michael@0: * 2) About NSNull and NSSet values: While the xml format has no support for michael@0: * representing null and set values, the documentation for the binary format michael@0: * states that it supports storing both types. However, the Cocoa APIs for michael@0: * serializing property lists do not seem to support either types (test with michael@0: * NSPropertyListSerialization::propertyList:isValidForFormat). Furthermore, michael@0: * if an array or a dictioanry contains a NSNull or a NSSet value, they cannot michael@0: * be serialized to a property list. michael@0: * As for usage within OS X, not surprisingly there's no known usage of michael@0: * storing either of these types in a property list. It seems that, for now, michael@0: * Apple is keeping the features of binary and xml formats in sync, probably as michael@0: * long as the XML format is not officially deprecated. michael@0: * 3) Not used anywhere. michael@0: * 4) About NSSet representation: For the time being, we represent those michael@0: * theoretical NSSet objects the same way NSArray is represented. michael@0: * While this would most certainly work, it is not the right way to handle michael@0: * it. A more correct representation for a set is a js generator, which would michael@0: * read the set lazily and has no indices semantics. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["PropertyListUtils"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Dict", michael@0: "resource://gre/modules/Dict.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ctypes", michael@0: "resource://gre/modules/ctypes.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: michael@0: this.PropertyListUtils = Object.freeze({ michael@0: /** michael@0: * Asynchronously reads a file as a property list. michael@0: * michael@0: * @param aFile (nsIDOMBlob/nsILocalFile) michael@0: * the file to be read as a property list. michael@0: * @param aCallback michael@0: * If the property list is read successfully, aPropertyListRoot is set michael@0: * to the root object of the property list. michael@0: * Use getPropertyListObjectType to detect its type. michael@0: * If it's not read successfully, aPropertyListRoot is set to null. michael@0: * The reaon for failure is reported to the Error Console. michael@0: */ michael@0: read: function PLU_read(aFile, aCallback) { michael@0: if (!(aFile instanceof Ci.nsILocalFile || aFile instanceof Ci.nsIDOMFile)) michael@0: throw new Error("aFile is not a file object"); michael@0: if (typeof(aCallback) != "function") michael@0: throw new Error("Invalid value for aCallback"); michael@0: michael@0: // We guarantee not to throw directly for any other exceptions, and always michael@0: // call aCallback. michael@0: Services.tm.mainThread.dispatch(function() { michael@0: let file = aFile; michael@0: try { michael@0: if (file instanceof Ci.nsILocalFile) { michael@0: if (!file.exists()) michael@0: throw new Error("The file pointed by aFile does not exist"); michael@0: michael@0: file = new File(file); michael@0: } michael@0: michael@0: let fileReader = Cc["@mozilla.org/files/filereader;1"]. michael@0: createInstance(Ci.nsIDOMFileReader); michael@0: let onLoadEnd = function() { michael@0: let root = null; michael@0: try { michael@0: fileReader.removeEventListener("loadend", onLoadEnd, false); michael@0: if (fileReader.readyState != fileReader.DONE) michael@0: throw new Error("Could not read file contents: " + fileReader.error); michael@0: michael@0: root = this._readFromArrayBufferSync(fileReader.result); michael@0: } michael@0: finally { michael@0: aCallback(root); michael@0: } michael@0: }.bind(this); michael@0: fileReader.addEventListener("loadend", onLoadEnd, false); michael@0: fileReader.readAsArrayBuffer(file); michael@0: } michael@0: catch(ex) { michael@0: aCallback(null); michael@0: throw ex; michael@0: } michael@0: }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: /** michael@0: * DO NOT USE ME. Once Bug 718189 is fixed, this method won't be public. michael@0: * michael@0: * Synchronously read an ArrayBuffer contents as a property list. michael@0: */ michael@0: _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) { michael@0: if (BinaryPropertyListReader.prototype.canProcess(aBuffer)) michael@0: return new BinaryPropertyListReader(aBuffer).root; michael@0: michael@0: // Convert the buffer into an XML tree. michael@0: let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. michael@0: createInstance(Ci.nsIDOMParser); michael@0: let bytesView = new Uint8Array(aBuffer); michael@0: try { michael@0: let doc = domParser.parseFromBuffer(bytesView, bytesView.length, michael@0: "application/xml"); michael@0: return new XMLPropertyListReader(doc).root; michael@0: } michael@0: catch(ex) { michael@0: throw new Error("aBuffer cannot be parsed as a DOM document: " + ex); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: TYPE_PRIMITIVE: 0, michael@0: TYPE_DATE: 1, michael@0: TYPE_UINT8_ARRAY: 2, michael@0: TYPE_ARRAY: 3, michael@0: TYPE_DICTIONARY: 4, michael@0: TYPE_INT64: 5, michael@0: michael@0: /** michael@0: * Get the type in which the given property list object is represented. michael@0: * Check the header for the mapping between the TYPE* constants to js types michael@0: * and objects. michael@0: * michael@0: * @return one of the TYPE_* constants listed above. michael@0: * @note this method is merely for convenience. It has no magic to detect michael@0: * that aObject is indeed a property list object created by this module. michael@0: */ michael@0: getObjectType: function PLU_getObjectType(aObject) { michael@0: if (aObject === null || typeof(aObject) != "object") michael@0: return this.TYPE_PRIMITIVE; michael@0: michael@0: // Given current usage, we could assume that aObject was created in the michael@0: // scope of this module, but in future, this util may be used as part of michael@0: // serializing js objects to a property list - in which case the object michael@0: // would most likely be created in the caller's scope. michael@0: let global = Cu.getGlobalForObject(aObject); michael@0: michael@0: if (global.Dict && aObject instanceof global.Dict) michael@0: return this.TYPE_DICTIONARY; michael@0: if (Array.isArray(aObject)) michael@0: return this.TYPE_ARRAY; michael@0: if (aObject instanceof global.Date) michael@0: return this.TYPE_DATE; michael@0: if (aObject instanceof global.Uint8Array) michael@0: return this.TYPE_UINT8_ARRAY; michael@0: if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject) michael@0: return this.TYPE_INT64; michael@0: michael@0: throw new Error("aObject is not as a property list object."); michael@0: }, michael@0: michael@0: /** michael@0: * Wraps a 64-bit stored in the form of a string primitive as a String object, michael@0: * which we can later distiguish from regular string values. michael@0: * @param aPrimitive michael@0: * a number in the form of either a primitive string or a primitive number. michael@0: * @return a String wrapper around aNumberStr that can later be identified michael@0: * as holding 64-bit number using getObjectType. michael@0: */ michael@0: wrapInt64: function PLU_wrapInt64(aPrimitive) { michael@0: if (typeof(aPrimitive) != "string" && typeof(aPrimitive) != "number") michael@0: throw new Error("aPrimitive should be a string primitive"); michael@0: michael@0: let wrapped = new String(aPrimitive); michael@0: Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true }); michael@0: return wrapped; michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Here's the base structure of binary-format property lists. michael@0: * 1) Header - magic number michael@0: * - 6 bytes - "bplist" michael@0: * - 2 bytes - version number. This implementation only supports version 00. michael@0: * 2) Objects Table michael@0: * Variable-sized objects, see _readObject for how various types of objects michael@0: * are constructed. michael@0: * 3) Offsets Table michael@0: * The offset of each object in the objects table. The integer size is michael@0: * specified in the trailer. michael@0: * 4) Trailer michael@0: * - 6 unused bytes michael@0: * - 1 byte: the size of integers in the offsets table michael@0: * - 1 byte: the size of object references for arrays, sets and michael@0: * dictionaries. michael@0: * - 8 bytes: the number of objects in the objects table michael@0: * - 8 bytes: the index of the root object's offset in the offsets table. michael@0: * - 8 bytes: the offset of the offsets table. michael@0: * michael@0: * Note: all integers are stored in big-endian form. michael@0: */ michael@0: michael@0: /** michael@0: * Reader for binary-format property lists. michael@0: * michael@0: * @param aBuffer michael@0: * ArrayBuffer object from which the binary plist should be read. michael@0: */ michael@0: function BinaryPropertyListReader(aBuffer) { michael@0: this._buffer = aBuffer; michael@0: michael@0: const JS_MAX_INT = Math.pow(2,53); michael@0: this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT); michael@0: this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT); michael@0: this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT); michael@0: michael@0: try { michael@0: this._readTrailerInfo(); michael@0: this._readObjectsOffsets(); michael@0: } michael@0: catch(ex) { michael@0: throw new Error("Could not read aBuffer as a binary property list"); michael@0: } michael@0: this._objects = []; michael@0: } michael@0: michael@0: BinaryPropertyListReader.prototype = { michael@0: /** michael@0: * Checks if the given ArrayBuffer can be read as a binary property list. michael@0: * It can be called on the prototype. michael@0: */ michael@0: canProcess: function BPLR_canProcess(aBuffer) michael@0: [String.fromCharCode(c) for each (c in new Uint8Array(aBuffer, 0, 8))]. michael@0: join("") == "bplist00", michael@0: michael@0: get root() this._readObject(this._rootObjectIndex), michael@0: michael@0: _readTrailerInfo: function BPLR__readTrailer() { michael@0: // The first 6 bytes of the 32-bytes trailer are unused michael@0: let trailerOffset = this._buffer.byteLength - 26; michael@0: [this._offsetTableIntegerSize, this._objectRefSize] = michael@0: this._readUnsignedInts(trailerOffset, 1, 2); michael@0: michael@0: [this._numberOfObjects, this._rootObjectIndex, this._offsetTableOffset] = michael@0: this._readUnsignedInts(trailerOffset + 2, 8, 3); michael@0: }, michael@0: michael@0: _readObjectsOffsets: function BPLR__readObjectsOffsets() { michael@0: this._offsetTable = this._readUnsignedInts(this._offsetTableOffset, michael@0: this._offsetTableIntegerSize, michael@0: this._numberOfObjects); michael@0: }, michael@0: michael@0: // TODO: This should be removed once DataView is implemented (Bug 575688). michael@0: _swapForBigEndian: michael@0: function BPLR__swapForBigEndian(aByteOffset, aIntSize, aNumberOfInts) { michael@0: let bytesCount = aIntSize * aNumberOfInts; michael@0: let bytes = new Uint8Array(this._buffer, aByteOffset, bytesCount); michael@0: let swapped = new Uint8Array(bytesCount); michael@0: for (let i = 0; i < aNumberOfInts; i++) { michael@0: for (let j = 0; j < aIntSize; j++) { michael@0: swapped[(i * aIntSize) + j] = bytes[(i * aIntSize) + (aIntSize - 1 - j)]; michael@0: } michael@0: } michael@0: return swapped; michael@0: }, michael@0: michael@0: _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) { michael@0: let swapped = this._swapForBigEndian(aByteOffset, 8, 1); michael@0: let lo = new Uint32Array(swapped.buffer, 0, 1)[0]; michael@0: let hi = new Int32Array(swapped.buffer, 4, 1)[0]; michael@0: let int64 = ctypes.Int64.join(hi, lo); michael@0: if (ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 || michael@0: ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1) michael@0: return PropertyListUtils.wrapInt64(int64.toString()); michael@0: michael@0: return parseInt(int64.toString(), 10); michael@0: }, michael@0: michael@0: _readReal: function BPLR__readReal(aByteOffset, aRealSize) { michael@0: let swapped = this._swapForBigEndian(aByteOffset, aRealSize, 1); michael@0: if (aRealSize == 4) michael@0: return new Float32Array(swapped.buffer, 0, 1)[0]; michael@0: if (aRealSize == 8) michael@0: return new Float64Array(swapped.buffer, 0, 1)[0]; michael@0: michael@0: throw new Error("Unsupported real size: " + aRealSize); michael@0: }, michael@0: michael@0: OBJECT_TYPE_BITS: { michael@0: SIMPLE: parseInt("0000", 2), michael@0: INTEGER: parseInt("0001", 2), michael@0: REAL: parseInt("0010", 2), michael@0: DATE: parseInt("0011", 2), michael@0: DATA: parseInt("0100", 2), michael@0: ASCII_STRING: parseInt("0101", 2), michael@0: UNICODE_STRING: parseInt("0110", 2), michael@0: UID: parseInt("1000", 2), michael@0: ARRAY: parseInt("1010", 2), michael@0: SET: parseInt("1100", 2), michael@0: DICTIONARY: parseInt("1101", 2) michael@0: }, michael@0: michael@0: ADDITIONAL_INFO_BITS: { michael@0: // Applies to OBJECT_TYPE_BITS.SIMPLE michael@0: NULL: parseInt("0000", 2), michael@0: FALSE: parseInt("1000", 2), michael@0: TRUE: parseInt("1001", 2), michael@0: FILL_BYTE: parseInt("1111", 2), michael@0: // Applies to OBJECT_TYPE_BITS.DATE michael@0: DATE: parseInt("0011", 2), michael@0: // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY, michael@0: // SET and DICTIONARY. michael@0: LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2) michael@0: }, michael@0: michael@0: /** michael@0: * Returns an object descriptor in the form of two integers: object type and michael@0: * additional info. michael@0: * michael@0: * @param aByteOffset michael@0: * the descriptor's offset. michael@0: * @return [objType, additionalInfo] - the object type and additional info. michael@0: * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS michael@0: */ michael@0: _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) { michael@0: // The first four bits hold the object type. For some types, additional michael@0: // info is held in the other 4 bits. michael@0: let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0]; michael@0: return [(byte & 0xF0) >> 4, byte & 0x0F]; michael@0: }, michael@0: michael@0: _readDate: function BPLR__readDate(aByteOffset) { michael@0: // That's the reference date of NSDate. michael@0: let date = new Date("1 January 2001, GMT"); michael@0: michael@0: // NSDate values are float values, but setSeconds takes an integer. michael@0: date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000); michael@0: return date; michael@0: }, michael@0: michael@0: /** michael@0: * Reads a portion of the buffer as a string. michael@0: * michael@0: * @param aByteOffset michael@0: * The offset in the buffer at which the string starts michael@0: * @param aNumberOfChars michael@0: * The length of the string to be read (that is the number of michael@0: * characters, not bytes). michael@0: * @param aUnicode michael@0: * Whether or not it is a unicode string. michael@0: * @return the string read. michael@0: * michael@0: * @note this is tested to work well with unicode surrogate pairs. Because michael@0: * all unicode characters are read as 2-byte integers, unicode surrogate michael@0: * pairs are read from the buffer in the form of two integers, as required michael@0: * by String.fromCharCode. michael@0: */ michael@0: _readString: michael@0: function BPLR__readString(aByteOffset, aNumberOfChars, aUnicode) { michael@0: let codes = this._readUnsignedInts(aByteOffset, aUnicode ? 2 : 1, michael@0: aNumberOfChars); michael@0: return [String.fromCharCode(c) for each (c in codes)].join(""); michael@0: }, michael@0: michael@0: /** michael@0: * Reads an array of unsigned integers from the buffer. Integers larger than michael@0: * one byte are read in big endian form. michael@0: * michael@0: * @param aByteOffset michael@0: * The offset in the buffer at which the array starts. michael@0: * @param aIntSize michael@0: * The size of each int in the array. michael@0: * @param aLength michael@0: * The number of ints in the array. michael@0: * @param [optional] aBigIntAllowed (default: false) michael@0: * Whether or not to accept integers which outbounds JS limits for michael@0: * numbers (±2^53) in the form of a String. michael@0: * @return an array of integers (number primitive and/or Strings for large michael@0: * numbers (see header)). michael@0: * @throws if aBigIntAllowed is false and one of the integers in the array michael@0: * cannot be represented by a primitive js number. michael@0: */ michael@0: _readUnsignedInts: michael@0: function BPLR__readUnsignedInts(aByteOffset, aIntSize, aLength, aBigIntAllowed) { michael@0: if (aIntSize == 1) michael@0: return new Uint8Array(this._buffer, aByteOffset, aLength); michael@0: michael@0: // There are two reasons for the complexity you see here: michael@0: // (1) 64-bit integers - For which we use ctypes. When possible, the michael@0: // number is converted back to js's default float-64 type. michael@0: // (2) The DataView object for ArrayBuffer, which takes care of swaping michael@0: // bytes, is not yet implemented (bug 575688). michael@0: let swapped = this._swapForBigEndian(aByteOffset, aIntSize, aLength); michael@0: if (aIntSize == 2) michael@0: return new Uint16Array(swapped.buffer); michael@0: if (aIntSize == 4) michael@0: return new Uint32Array(swapped.buffer); michael@0: if (aIntSize == 8) { michael@0: let intsArray = []; michael@0: let lo_hi_view = new Uint32Array(swapped.buffer); michael@0: for (let i = 0; i < lo_hi_view.length; i += 2) { michael@0: let [lo, hi] = [lo_hi_view[i], lo_hi_view[i+1]]; michael@0: let uint64 = ctypes.UInt64.join(hi, lo); michael@0: if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) { michael@0: if (aBigIntAllowed === true) michael@0: intsArray.push(PropertyListUtils.wrapInt64(uint64.toString())); michael@0: else michael@0: throw new Error("Integer too big to be read as float 64"); michael@0: } michael@0: else { michael@0: intsArray.push(parseInt(uint64.toString(), 10)); michael@0: } michael@0: } michael@0: return intsArray; michael@0: } michael@0: throw new Error("Unsupported size: " + aIntSize); michael@0: }, michael@0: michael@0: /** michael@0: * Reads from the buffer the data object-count and the offset at which the michael@0: * first object starts. michael@0: * michael@0: * @param aObjectOffset michael@0: * the object's offset. michael@0: * @return [offset, count] - the offset in the buffer at which the first michael@0: * object in data starts, and the number of objects. michael@0: */ michael@0: _readDataOffsetAndCount: michael@0: function BPLR__readDataOffsetAndCount(aObjectOffset) { michael@0: // The length of some objects in the data can be stored in two ways: michael@0: // * If it is small enough, it is stored in the second four bits of the michael@0: // object descriptors. michael@0: // * Otherwise, those bits are set to 1111, indicating that the next byte michael@0: // consists of the integer size of the data-length (also stored in the form michael@0: // of an object descriptor). The length follows this byte. michael@0: let [, maybeLength] = this._readObjectDescriptor(aObjectOffset); michael@0: if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS) michael@0: return [aObjectOffset + 1, maybeLength]; michael@0: michael@0: let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1); michael@0: michael@0: // The int size is 2^intSizeInfo. michael@0: let intSize = Math.pow(2, intSizeInfo); michael@0: let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0]; michael@0: return [aObjectOffset + 2 + intSize, dataLength]; michael@0: }, michael@0: michael@0: /** michael@0: * Read array from the buffer and wrap it as a js array. michael@0: * @param aObjectOffset michael@0: * the offset in the buffer at which the array starts. michael@0: * @param aNumberOfObjects michael@0: * the number of objects in the array. michael@0: * @return a js array. michael@0: */ michael@0: _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) { michael@0: let refs = this._readUnsignedInts(aObjectOffset, michael@0: this._objectRefSize, michael@0: aNumberOfObjects); michael@0: michael@0: let array = new Array(aNumberOfObjects); michael@0: let readObjectBound = this._readObject.bind(this); michael@0: michael@0: // Each index in the returned array is a lazy getter for its object. michael@0: Array.prototype.forEach.call(refs, function(ref, objIndex) { michael@0: Object.defineProperty(array, objIndex, { michael@0: get: function() { michael@0: delete array[objIndex]; michael@0: return array[objIndex] = readObjectBound(ref); michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: }, this); michael@0: return array; michael@0: }, michael@0: michael@0: /** michael@0: * Reads dictionary from the buffer and wraps it as a Dict object (as defined michael@0: * in Dict.jsm). michael@0: * @param aObjectOffset michael@0: * the offset in the buffer at which the dictionary starts michael@0: * @param aNumberOfObjects michael@0: * the number of keys in the dictionary michael@0: * @return Dict.jsm-style dictionary. michael@0: */ michael@0: _wrapDictionary: function(aObjectOffset, aNumberOfObjects) { michael@0: // A dictionary in the binary format is stored as a list of references to michael@0: // key-objects, followed by a list of references to the value-objects for michael@0: // those keys. The size of each list is aNumberOfObjects * this._objectRefSize. michael@0: let dict = new Dict(); michael@0: if (aNumberOfObjects == 0) michael@0: return dict; michael@0: michael@0: let keyObjsRefs = this._readUnsignedInts(aObjectOffset, this._objectRefSize, michael@0: aNumberOfObjects); michael@0: let valObjsRefs = michael@0: this._readUnsignedInts(aObjectOffset + aNumberOfObjects * this._objectRefSize, michael@0: this._objectRefSize, aNumberOfObjects); michael@0: for (let i = 0; i < aNumberOfObjects; i++) { michael@0: let key = this._readObject(keyObjsRefs[i]); michael@0: let readBound = this._readObject.bind(this, valObjsRefs[i]); michael@0: dict.setAsLazyGetter(key, readBound); michael@0: } michael@0: return dict; michael@0: }, michael@0: michael@0: /** michael@0: * Reads an object at the spcified index in the object table michael@0: * @param aObjectIndex michael@0: * index at the object table michael@0: * @return the property list object at the given index. michael@0: */ michael@0: _readObject: function BPLR__readObject(aObjectIndex) { michael@0: // If the object was previously read, return the cached object. michael@0: if (this._objects[aObjectIndex] !== undefined) michael@0: return this._objects[aObjectIndex]; michael@0: michael@0: let objOffset = this._offsetTable[aObjectIndex]; michael@0: let [objType, additionalInfo] = this._readObjectDescriptor(objOffset); michael@0: let value; michael@0: switch (objType) { michael@0: case this.OBJECT_TYPE_BITS.SIMPLE: { michael@0: switch (additionalInfo) { michael@0: case this.ADDITIONAL_INFO_BITS.NULL: michael@0: value = null; michael@0: break; michael@0: case this.ADDITIONAL_INFO_BITS.FILL_BYTE: michael@0: value = undefined; michael@0: break; michael@0: case this.ADDITIONAL_INFO_BITS.FALSE: michael@0: value = false; michael@0: break; michael@0: case this.ADDITIONAL_INFO_BITS.TRUE: michael@0: value = true; michael@0: break; michael@0: default: michael@0: throw new Error("Unexpected value!"); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.INTEGER: { michael@0: // The integer is sized 2^additionalInfo. michael@0: let intSize = Math.pow(2, additionalInfo); michael@0: michael@0: // For objects, 64-bit integers are always signed. Negative integers michael@0: // are always represented by a 64-bit integer. michael@0: if (intSize == 8) michael@0: value = this._readSignedInt64(objOffset + 1); michael@0: else michael@0: value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0]; michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.REAL: { michael@0: // The real is sized 2^additionalInfo. michael@0: value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo)); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.DATE: { michael@0: if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE) michael@0: throw new Error("Unexpected value"); michael@0: michael@0: value = this._readDate(objOffset + 1); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.DATA: { michael@0: let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset); michael@0: value = this._readUnsignedInts(offset, 1, bytesCount); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.ASCII_STRING: { michael@0: let [offset, charsCount] = this._readDataOffsetAndCount(objOffset); michael@0: value = this._readString(offset, charsCount, false); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.UNICODE_STRING: { michael@0: let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset); michael@0: value = this._readString(offset, unicharsCount, true); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.UID: { michael@0: // UIDs are only used in Keyed Archives, which are not yet supported. michael@0: throw new Error("Keyed Archives are not supported"); michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.ARRAY: michael@0: case this.OBJECT_TYPE_BITS.SET: { michael@0: // Note: For now, we fallback to handle sets the same way we handle michael@0: // arrays. See comments in the header of this file. michael@0: michael@0: // The bytes following the count are references to objects (indices). michael@0: // Each reference is an unsigned int with size=this._objectRefSize. michael@0: let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); michael@0: value = this._wrapArray(offset, objectsCount); michael@0: break; michael@0: } michael@0: michael@0: case this.OBJECT_TYPE_BITS.DICTIONARY: { michael@0: let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); michael@0: value = this._wrapDictionary(offset, objectsCount); michael@0: break; michael@0: } michael@0: michael@0: default: { michael@0: throw new Error("Unknown object type: " + objType); michael@0: } michael@0: } michael@0: michael@0: return this._objects[aObjectIndex] = value; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Reader for XML property lists. michael@0: * michael@0: * @param aDOMDoc michael@0: * the DOM document to be read as a property list. michael@0: */ michael@0: function XMLPropertyListReader(aDOMDoc) { michael@0: let docElt = aDOMDoc.documentElement; michael@0: if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild) michael@0: throw new Error("aDoc is not a property list document"); michael@0: michael@0: this._plistRootElement = docElt.firstElementChild; michael@0: } michael@0: michael@0: XMLPropertyListReader.prototype = { michael@0: get root() this._readObject(this._plistRootElement), michael@0: michael@0: /** michael@0: * Convert a dom element to a property list object. michael@0: * @param aDOMElt michael@0: * a dom element in a xml tree of a property list. michael@0: * @return a js object representing the property list object. michael@0: */ michael@0: _readObject: function XPLR__readObject(aDOMElt) { michael@0: switch (aDOMElt.localName) { michael@0: case "true": michael@0: return true; michael@0: case "false": michael@0: return false; michael@0: case "string": michael@0: case "key": michael@0: return aDOMElt.textContent; michael@0: case "integer": michael@0: return this._readInteger(aDOMElt); michael@0: case "real": { michael@0: let number = parseFloat(aDOMElt.textContent.trim()); michael@0: if (isNaN(number)) michael@0: throw "Could not parse float value"; michael@0: return number; michael@0: } michael@0: case "date": michael@0: return new Date(aDOMElt.textContent); michael@0: case "data": michael@0: // Strip spaces and new lines. michael@0: let base64str = aDOMElt.textContent.replace(/\s*/g, ""); michael@0: let decoded = atob(base64str); michael@0: return new Uint8Array([decoded.charCodeAt(i) for (i in decoded)]); michael@0: case "dict": michael@0: return this._wrapDictionary(aDOMElt); michael@0: case "array": michael@0: return this._wrapArray(aDOMElt); michael@0: default: michael@0: throw new Error("Unexpected tagname"); michael@0: } michael@0: }, michael@0: michael@0: _readInteger: function XPLR__readInteger(aDOMElt) { michael@0: // The integer may outbound js's max/min integer value. We recognize this michael@0: // case by comparing the parsed number to the original string value. michael@0: // In case of an outbound, we fallback to return the number as a string. michael@0: let numberAsString = aDOMElt.textContent.toString(); michael@0: let parsedNumber = parseInt(numberAsString, 10); michael@0: if (isNaN(parsedNumber)) michael@0: throw new Error("Could not parse integer value"); michael@0: michael@0: if (parsedNumber.toString() == numberAsString) michael@0: return parsedNumber; michael@0: michael@0: return PropertyListUtils.wrapInt64(numberAsString); michael@0: }, michael@0: michael@0: _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) { michael@0: // michael@0: // my true bool michael@0: // michael@0: // my string key michael@0: // My String Key michael@0: // michael@0: if (aDOMElt.children.length % 2 != 0) michael@0: throw new Error("Invalid dictionary"); michael@0: let dict = new Dict(); michael@0: for (let i = 0; i < aDOMElt.children.length; i += 2) { michael@0: let keyElem = aDOMElt.children[i]; michael@0: let valElem = aDOMElt.children[i + 1]; michael@0: michael@0: if (keyElem.localName != "key") michael@0: throw new Error("Invalid dictionary"); michael@0: michael@0: let keyName = this._readObject(keyElem); michael@0: let readBound = this._readObject.bind(this, valElem); michael@0: dict.setAsLazyGetter(keyName, readBound); michael@0: } michael@0: return dict; michael@0: }, michael@0: michael@0: _wrapArray: function XPLR__wrapArray(aDOMElt) { michael@0: // michael@0: // ... michael@0: // michael@0: // michael@0: // .... michael@0: // michael@0: // michael@0: michael@0: // Each element in the array is a lazy getter for its property list object. michael@0: let array = []; michael@0: let readObjectBound = this._readObject.bind(this); michael@0: Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) { michael@0: Object.defineProperty(array, elemIndex, { michael@0: get: function() { michael@0: delete array[elemIndex]; michael@0: return array[elemIndex] = readObjectBound(elem); michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: }); michael@0: return array; michael@0: } michael@0: };