From ba1dfa63f29c60189d0005fadd55b41cdefcc24d Mon Sep 17 00:00:00 2001 From: Asaf Romano Date: Mon, 23 Jan 2012 14:05:01 +0200 Subject: [PATCH] Reland Part 1 of Bug 710259 - Add a module for reading property list (plist) files (and add support for lazy-getters values in Dict.jsm). r=mak, sr=mossap for the new module. r=sid0 for the changes to Dict.jsm --- toolkit/content/Dict.jsm | 46 ++ toolkit/content/Makefile.in | 1 + toolkit/content/PropertyListUtils.jsm | 771 ++++++++++++++++++ .../bug710259_propertyListBinary.plist | Bin 0 -> 3277 bytes .../bug710259_propertyListXML.plist | 28 + toolkit/content/tests/unit/test_dict.js | 78 ++ .../tests/unit/test_propertyListsUtils.js | 102 +++ toolkit/content/tests/unit/xpcshell.ini | 1 + 8 files changed, 1027 insertions(+) create mode 100644 toolkit/content/PropertyListUtils.jsm create mode 100644 toolkit/content/tests/unit/propertyLists/bug710259_propertyListBinary.plist create mode 100644 toolkit/content/tests/unit/propertyLists/bug710259_propertyListXML.plist create mode 100644 toolkit/content/tests/unit/test_propertyListsUtils.js diff --git a/toolkit/content/Dict.jsm b/toolkit/content/Dict.jsm index 74d42e190dbf..9a494916b8f9 100644 --- a/toolkit/content/Dict.jsm +++ b/toolkit/content/Dict.jsm @@ -111,6 +111,52 @@ Dict.prototype = Object.freeze({ items[prop] = aValue; }, + /** + * Sets a lazy getter function for a key's value. If the key is a not a string, + * it will be converted to a string before the set happens. + * @param aKey + * The key to set + * @param aThunk + * A getter function to be called the first time the value for aKey is + * retrieved. It is guaranteed that aThunk wouldn't be called more + * than once. Note that the key value may be retrieved either + * directly, by |get|, or indirectly, by |listvalues| or by iterating + * |values|. For the later, the value is only retrieved if and when + * the iterator gets to the value in question. Also note that calling + * |has| for a lazy-key does not invoke aThunk. + * + * @note No context is provided for aThunk when it's invoked. + * Use Function.bind if you wish to run it in a certain context. + */ + setAsLazyGetter: function Dict_setAsLazyGetter(aKey, aThunk) { + let prop = convert(aKey); + let items = this._state.items; + if (!items.hasOwnProperty(prop)) + this._state.count++; + + Object.defineProperty(items, prop, { + get: function() { + delete items[prop]; + return items[prop] = aThunk(); + }, + configurable: true, + enumerable: true + }); + }, + + /** + * Returns whether a key is set as a lazy getter. This returns + * true only if the getter function was not called already. + * @param aKey + * The key to look up. + * @returns whether aKey is set as a lazy getter. + */ + isLazyGetter: function Dict_isLazyGetter(aKey) { + let descriptor = Object.getOwnPropertyDescriptor(this._state.items, + convert(aKey)); + return (descriptor && descriptor.get != null); + }, + /** * Returns whether a key is in the dictionary. If the key is a not a string, * it will be converted to a string before the lookup happens. diff --git a/toolkit/content/Makefile.in b/toolkit/content/Makefile.in index ca1816e4418e..6d8d6f44a7dc 100644 --- a/toolkit/content/Makefile.in +++ b/toolkit/content/Makefile.in @@ -90,6 +90,7 @@ EXTRA_JS_MODULES = \ InlineSpellChecker.jsm \ PopupNotifications.jsm \ Dict.jsm \ + PropertyListUtils.jsm \ $(NULL) EXTRA_PP_JS_MODULES = \ diff --git a/toolkit/content/PropertyListUtils.jsm b/toolkit/content/PropertyListUtils.jsm new file mode 100644 index 000000000000..8925feea27d9 --- /dev/null +++ b/toolkit/content/PropertyListUtils.jsm @@ -0,0 +1,771 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Module for reading Property Lists (.plist) files + * ------------------------------------------------ + * This module functions as a reader for Apple Property Lists (.plist files). + * It supports both binary and xml formatted property lists. It does not + * support the legacy ASCII format. Reading of Cocoa's Keyed Archives serialized + * to binary property lists isn't supported either. + * + * Property Lists objects are represented by standard JS and Mozilla types, + * namely: + * + * XML type Cocoa Class Returned type(s) + * -------------------------------------------------------------------------- + * / NSNumber TYPE_PRIMITIVE boolean + * / NSNumber TYPE_PRIMITIVE number + * TYPE_INT64 String [1] + * Not Available NSNull TYPE_PRIMITIVE null [2] + * TYPE_PRIMITIVE undefined [3] + * NSDate TYPE_DATE Date + * NSData TYPE_UINT8_ARRAY Uint8Array + * NSArray TYPE_ARRAY Array + * Not Available NSSet TYPE_ARRAY Array [2][4] + * NSDictionary TYPE_DICTIONARY Dict (from Dict.jsm) + * + * Use PropertyListUtils.getObjectType to detect the type of a Property list + * object. + * + * ------------- + * 1) Property lists supports storing U/Int64 numbers, while JS can only handle + * numbers that are in this limits of float-64 (±2^53). For numbers that + * do not outbound this limits, simple primitive number are always used. + * Otherwise, a String object. + * 2) About NSNull and NSSet values: While the xml format has no support for + * representing null and set values, the documentation for the binary format + * states that it supports storing both types. However, the Cocoa APIs for + * serializing property lists do not seem to support either types (test with + * NSPropertyListSerialization::propertyList:isValidForFormat). Furthermore, + * if an array or a dictioanry contains a NSNull or a NSSet value, they cannot + * be serialized to a property list. + * As for usage within OS X, not surprisingly there's no known usage of + * storing either of these types in a property list. It seems that, for now, + * Apple is keeping the features of binary and xml formats in sync, probably as + * long as the XML format is not officially deprecated. + * 3) Not used anywhere. + * 4) About NSSet representation: For the time being, we represent those + * theoretical NSSet objects the same way NSArray is represented. + * While this would most certainly work, it is not the right way to handle + * it. A more correct representation for a set is a js generator, which would + * read the set lazily and has no indices semantics. + */ + +"use strict"; + +let EXPORTED_SYMBOLS = ["PropertyListUtils"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Dict", + "resource://gre/modules/Dict.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", + "resource://gre/modules/ctypes.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +let PropertyListUtils = Object.freeze({ + /** + * Asynchronously reads a file as a property list. + * + * @param aFile (nsIDOMBlob/nsILocalFile) + * the file to be read as a property list. + * @param aCallback + * If the property list is read successfully, aPropertyListRoot is set + * to the root object of the property list. + * Use getPropertyListObjectType to detect its type. + * If it's not read successfully, aPropertyListRoot is set to null. + * The reaon for failure is reported to the Error Console. + */ + read: function PLU_read(aFile, aCallback) { + if (!(aFile instanceof Ci.nsILocalFile || aFile instanceof Ci.nsIDOMFile)) + throw new Error("aFile is not a file object"); + if (typeof(aCallback) != "function") + throw new Error("Invalid value for aCallback"); + + // We guarantee not to throw directly for any other exceptions, and always + // call aCallback. + Services.tm.mainThread.dispatch(function() { + let file = aFile; + try { + if (file instanceof Ci.nsILocalFile) { + if (!file.exists()) + throw new Error("The file pointed by aFile does not exist"); + + file = new File(file); + } + + let fileReader = Cc["@mozilla.org/files/filereader;1"]. + createInstance(Ci.nsIDOMFileReader); + let onLoadEnd = function() { + let root = null; + try { + fileReader.removeEventListener("loadend", onLoadEnd, false); + if (fileReader.readyState != fileReader.DONE) + throw new Error("Could not read file contents: " + fileReader.error); + + root = this._readFromArrayBufferSync(fileReader.result); + } + finally { + aCallback(root); + } + }.bind(this); + fileReader.addEventListener("loadend", onLoadEnd, false); + fileReader.readAsArrayBuffer(file); + } + catch(ex) { + aCallback(null); + throw ex; + } + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * DO NOT USE ME. Once Bug 718189 is fixed, this method won't be public. + * + * Synchronously read an ArrayBuffer contents as a property list. + */ + _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) { + if (BinaryPropertyListReader.prototype.canProcess(aBuffer)) + return new BinaryPropertyListReader(aBuffer).root; + + // Convert the buffer into an XML tree. + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + let bytesView = Uint8Array(aBuffer); + try { + let doc = domParser.parseFromBuffer(bytesView, bytesView.length, + "application/xml"); + return new XMLPropertyListReader(doc).root; + } + catch(ex) { + throw new Error("aBuffer cannot be parsed as a DOM document: " + ex); + } + return null; + }, + + TYPE_PRIMITIVE: 0, + TYPE_DATE: 1, + TYPE_UINT8_ARRAY: 2, + TYPE_ARRAY: 3, + TYPE_DICTIONARY: 4, + TYPE_INT64: 5, + + /** + * Get the type in which the given property list object is represented. + * Check the header for the mapping between the TYPE* constants to js types + * and objects. + * + * @return one of the TYPE_* constants listed above. + * @note this method is merely for convenience. It has no magic to detect + * that aObject is indeed a property list object created by this module. + */ + getObjectType: function PLU_getObjectType(aObject) { + if (aObject === null || typeof(aObject) != "object") + return this.TYPE_PRIMITIVE; + + // Given current usage, we could assume that aObject was created in the + // scope of this module, but in future, this util may be used as part of + // serializing js objects to a property list - in which case the object + // would most likely be created in the caller's scope. + let global = Cu.getGlobalForObject(aObject); + + if (global.Dict && aObject instanceof global.Dict) + return this.TYPE_DICTIONARY; + if (Array.isArray(aObject)) + return this.TYPE_ARRAY; + if (aObject instanceof global.Date) + return this.TYPE_DATE; + if (aObject instanceof global.Uint8Array) + return this.TYPE_UINT8_ARRAY; + if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject) + return this.TYPE_INT64; + + throw new Error("aObject is not as a property list object."); + }, + + /** + * Wraps a 64-bit stored in the form of a string primitive as a String object, + * which we can later distiguish from regular string values. + * @param aPrimitive + * a number in the form of either a primitive string or a primitive number. + * @return a String wrapper around aNumberStr that can later be identified + * as holding 64-bit number using getObjectType. + */ + wrapInt64: function PLU_wrapInt64(aPrimitive) { + if (typeof(aPrimitive) != "string" && typeof(aPrimitive) != "number") + throw new Error("aPrimitive should be a string primitive"); + + let wrapped = new String(aPrimitive); + Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true }); + return wrapped; + } +}); + +/** + * Here's the base structure of binary-format property lists. + * 1) Header - magic number + * - 6 bytes - "bplist" + * - 2 bytes - version number. This implementation only supports version 00. + * 2) Objects Table + * Variable-sized objects, see _readObject for how various types of objects + * are constructed. + * 3) Offsets Table + * The offset of each object in the objects table. The integer size is + * specified in the trailer. + * 4) Trailer + * - 6 unused bytes + * - 1 byte: the size of integers in the offsets table + * - 1 byte: the size of object references for arrays, sets and + * dictionaries. + * - 8 bytes: the number of objects in the objects table + * - 8 bytes: the index of the root object's offset in the offsets table. + * - 8 bytes: the offset of the offsets table. + * + * Note: all integers are stored in big-endian form. + */ + +/** + * Reader for binary-format property lists. + * + * @param aBuffer + * ArrayBuffer object from which the binary plist should be read. + */ +function BinaryPropertyListReader(aBuffer) { + this._buffer = aBuffer; + + const JS_MAX_INT = Math.pow(2,53); + this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT); + this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT); + this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT); + + try { + this._readTrailerInfo(); + this._readObjectsOffsets(); + } + catch(ex) { + throw new Error("Could not read aBuffer as a binary property list"); + } + this._objects = []; +} + +BinaryPropertyListReader.prototype = { + /** + * Checks if the given ArrayBuffer can be read as a binary property list. + * It can be called on the prototype. + */ + canProcess: function BPLR_canProcess(aBuffer) + [String.fromCharCode(c) for each (c in Uint8Array(aBuffer, 0, 8))]. + join("") == "bplist00", + + get root() this._readObject(this._rootObjectIndex), + + _readTrailerInfo: function BPLR__readTrailer() { + // The first 6 bytes of the 32-bytes trailer are unused + let trailerOffset = this._buffer.byteLength - 26; + [this._offsetTableIntegerSize, this._objectRefSize] = + this._readUnsignedInts(trailerOffset, 1, 2); + + [this._numberOfObjects, this._rootObjectIndex, this._offsetTableOffset] = + this._readUnsignedInts(trailerOffset + 2, 8, 3); + }, + + _readObjectsOffsets: function BPLR__readObjectsOffsets() { + this._offsetTable = this._readUnsignedInts(this._offsetTableOffset, + this._offsetTableIntegerSize, + this._numberOfObjects); + }, + + // TODO: This should be removed once DataView is implemented (Bug 575688). + _swapForBigEndian: + function BPLR__swapForBigEndian(aByteOffset, aIntSize, aNumberOfInts) { + let bytesCount = aIntSize * aNumberOfInts; + let bytes = new Uint8Array(this._buffer, aByteOffset, bytesCount); + let swapped = new Uint8Array(bytesCount); + for (let i = 0; i < aNumberOfInts; i++) { + for (let j = 0; j < aIntSize; j++) { + swapped[(i * aIntSize) + j] = bytes[(i * aIntSize) + (aIntSize - 1 - j)]; + } + } + return swapped; + }, + + _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) { + let swapped = this._swapForBigEndian(aByteOffset, 8, 1); + let lo = new Uint32Array(swapped.buffer, 0, 1)[0]; + let hi = new Int32Array(swapped.buffer, 4, 1)[0]; + let int64 = ctypes.Int64.join(hi, lo); + if (ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 || + ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1) + return PropertyListUtils.wrapInt64(int64.toString()); + + return parseInt(int64.toString(), 10); + }, + + _readReal: function BPLR__readReal(aByteOffset, aRealSize) { + let swapped = this._swapForBigEndian(aByteOffset, aRealSize, 1); + if (aRealSize == 4) + return Float32Array(swapped.buffer, 0, 1)[0]; + if (aRealSize == 8) + return Float64Array(swapped.buffer, 0, 1)[0]; + + throw new Error("Unsupported real size: " + aRealSize); + }, + + OBJECT_TYPE_BITS: { + SIMPLE: parseInt("0000", 2), + INTEGER: parseInt("0001", 2), + REAL: parseInt("0010", 2), + DATE: parseInt("0011", 2), + DATA: parseInt("0100", 2), + ASCII_STRING: parseInt("0101", 2), + UNICODE_STRING: parseInt("0110", 2), + UID: parseInt("1000", 2), + ARRAY: parseInt("1010", 2), + SET: parseInt("1100", 2), + DICTIONARY: parseInt("1101", 2) + }, + + ADDITIONAL_INFO_BITS: { + // Applies to OBJECT_TYPE_BITS.SIMPLE + NULL: parseInt("0000", 2), + FALSE: parseInt("1000", 2), + TRUE: parseInt("1001", 2), + FILL_BYTE: parseInt("1111", 2), + // Applies to OBJECT_TYPE_BITS.DATE + DATE: parseInt("0011", 2), + // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY, + // SET and DICTIONARY. + LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2) + }, + + /** + * Returns an object descriptor in the form of two integers: object type and + * additional info. + * + * @param aByteOffset + * the descriptor's offset. + * @return [objType, additionalInfo] - the object type and additional info. + * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS + */ + _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) { + // The first four bits hold the object type. For some types, additional + // info is held in the other 4 bits. + let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0]; + return [(byte & 0xF0) >> 4, byte & 0x0F]; + }, + + _readDate: function BPLR__readDate(aByteOffset) { + // That's the reference date of NSDate. + let date = new Date("1 January 2001, GMT"); + + // NSDate values are float values, but setSeconds takes an integer. + date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000); + return date; + }, + + /** + * Reads a portion of the buffer as a string. + * + * @param aByteOffset + * The offset in the buffer at which the string starts + * @param aNumberOfChars + * The length of the string to be read (that is the number of + * characters, not bytes). + * @param aUnicode + * Whether or not it is a unicode string. + * @return the string read. + * + * @note this is tested to work well with unicode surrogate pairs. Because + * all unicode characters are read as 2-byte integers, unicode surrogate + * pairs are read from the buffer in the form of two integers, as required + * by String.fromCharCode. + */ + _readString: + function BPLR__readString(aByteOffset, aNumberOfChars, aUnicode) { + let codes = this._readUnsignedInts(aByteOffset, aUnicode ? 2 : 1, + aNumberOfChars); + return [String.fromCharCode(c) for each (c in codes)].join(""); + }, + + /** + * Reads an array of unsigned integers from the buffer. Integers larger than + * one byte are read in big endian form. + * + * @param aByteOffset + * The offset in the buffer at which the array starts. + * @param aIntSize + * The size of each int in the array. + * @param aLength + * The number of ints in the array. + * @param [optional] aBigIntAllowed (default: false) + * Whether or not to accept integers which outbounds JS limits for + * numbers (±2^53) in the form of a String. + * @return an array of integers (number primitive and/or Strings for large + * numbers (see header)). + * @throws if aBigIntAllowed is false and one of the integers in the array + * cannot be represented by a primitive js number. + */ + _readUnsignedInts: + function BPLR__readUnsignedInts(aByteOffset, aIntSize, aLength, aBigIntAllowed) { + if (aIntSize == 1) + return Uint8Array(this._buffer, aByteOffset, aLength); + + // There are two reasons for the complexity you see here: + // (1) 64-bit integers - For which we use ctypes. When possible, the + // number is converted back to js's default float-64 type. + // (2) The DataView object for ArrayBuffer, which takes care of swaping + // bytes, is not yet implemented (bug 575688). + let swapped = this._swapForBigEndian(aByteOffset, aIntSize, aLength); + if (aIntSize == 2) + return Uint16Array(swapped.buffer); + if (aIntSize == 4) + return Uint32Array(swapped.buffer); + if (aIntSize == 8) { + let intsArray = []; + let lo_hi_view = new Uint32Array(swapped.buffer); + for (let i = 0; i < lo_hi_view.length; i += 2) { + let [lo, hi] = [lo_hi_view[i], lo_hi_view[i+1]]; + let uint64 = ctypes.UInt64.join(hi, lo); + if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) { + if (aBigIntAllowed === true) + intsArray.push(PropertyListUtils.wrapInt64(uint64.toString())); + else + throw new Error("Integer too big to be read as float 64"); + } + else { + intsArray.push(parseInt(uint64.toString(), 10)); + } + } + return intsArray; + } + throw new Error("Unsupported size: " + aIntSize); + }, + + /** + * Reads from the buffer the data object-count and the offset at which the + * first object starts. + * + * @param aObjectOffset + * the object's offset. + * @return [offset, count] - the offset in the buffer at which the first + * object in data starts, and the number of objects. + */ + _readDataOffsetAndCount: + function BPLR__readDataOffsetAndCount(aObjectOffset) { + // The length of some objects in the data can be stored in two ways: + // * If it is small enough, it is stored in the second four bits of the + // object descriptors. + // * Otherwise, those bits are set to 1111, indicating that the next byte + // consists of the integer size of the data-length (also stored in the form + // of an object descriptor). The length follows this byte. + let [, maybeLength] = this._readObjectDescriptor(aObjectOffset); + if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS) + return [aObjectOffset + 1, maybeLength]; + + let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1); + + // The int size is 2^intSizeInfo. + let intSize = Math.pow(2, intSizeInfo); + let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0]; + return [aObjectOffset + 2 + intSize, dataLength]; + }, + + /** + * Read array from the buffer and wrap it as a js array. + * @param aObjectOffset + * the offset in the buffer at which the array starts. + * @param aNumberOfObjects + * the number of objects in the array. + * @return a js array. + */ + _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) { + let refs = this._readUnsignedInts(aObjectOffset, + this._objectRefSize, + aNumberOfObjects); + + let array = new Array(aNumberOfObjects); + let readObjectBound = this._readObject.bind(this); + + // Each index in the returned array is a lazy getter for its object. + Array.prototype.forEach.call(refs, function(ref, objIndex) { + Object.defineProperty(array, objIndex, { + get: function() { + delete array[objIndex]; + return array[objIndex] = readObjectBound(ref); + }, + configurable: true, + enumerable: true + }); + }, this); + return array; + }, + + /** + * Reads dictionary from the buffer and wraps it as a Dict object (as defined + * in Dict.jsm). + * @param aObjectOffset + * the offset in the buffer at which the dictionary starts + * @param aNumberOfObjects + * the number of keys in the dictionary + * @return Dict.jsm-style dictionary. + */ + _wrapDictionary: function(aObjectOffset, aNumberOfObjects) { + // A dictionary in the binary format is stored as a list of references to + // key-objects, followed by a list of references to the value-objects for + // those keys. The size of each list is aNumberOfObjects * this._objectRefSize. + let dict = new Dict(); + if (aNumberOfObjects == 0) + return dict; + + let keyObjsRefs = this._readUnsignedInts(aObjectOffset, this._objectRefSize, + aNumberOfObjects); + let valObjsRefs = + this._readUnsignedInts(aObjectOffset + aNumberOfObjects * this._objectRefSize, + this._objectRefSize, aNumberOfObjects); + for (let i = 0; i < aNumberOfObjects; i++) { + let key = this._readObject(keyObjsRefs[i]); + let readBound = this._readObject.bind(this, valObjsRefs[i]); + dict.setAsLazyGetter(key, readBound); + } + return dict; + }, + + /** + * Reads an object at the spcified index in the object table + * @param aObjectIndex + * index at the object table + * @return the property list object at the given index. + */ + _readObject: function BPLR__readObject(aObjectIndex) { + // If the object was previously read, return the cached object. + if (this._objects[aObjectIndex] !== undefined) + return this._objects[aObjectIndex]; + + let objOffset = this._offsetTable[aObjectIndex]; + let [objType, additionalInfo] = this._readObjectDescriptor(objOffset); + let value; + switch (objType) { + case this.OBJECT_TYPE_BITS.SIMPLE: { + switch (additionalInfo) { + case this.ADDITIONAL_INFO_BITS.NULL: + value = null; + break; + case this.ADDITIONAL_INFO_BITS.FILL_BYTE: + value = undefined; + break; + case this.ADDITIONAL_INFO_BITS.FALSE: + value = false; + break; + case this.ADDITIONAL_INFO_BITS.TRUE: + value = true; + break; + default: + throw new Error("Unexpected value!"); + } + break; + } + + case this.OBJECT_TYPE_BITS.INTEGER: { + // The integer is sized 2^additionalInfo. + let intSize = Math.pow(2, additionalInfo); + + // For objects, 64-bit integers are always signed. Negative integers + // are always represented by a 64-bit integer. + if (intSize == 8) + value = this._readSignedInt64(objOffset + 1); + else + value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0]; + break; + } + + case this.OBJECT_TYPE_BITS.REAL: { + // The real is sized 2^additionalInfo. + value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo)); + break; + } + + case this.OBJECT_TYPE_BITS.DATE: { + if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE) + throw new Error("Unexpected value"); + + value = this._readDate(objOffset + 1); + break; + } + + case this.OBJECT_TYPE_BITS.DATA: { + let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset); + value = this._readUnsignedInts(offset, 1, bytesCount); + break; + } + + case this.OBJECT_TYPE_BITS.ASCII_STRING: { + let [offset, charsCount] = this._readDataOffsetAndCount(objOffset); + value = this._readString(offset, charsCount, false); + break; + } + + case this.OBJECT_TYPE_BITS.UNICODE_STRING: { + let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset); + value = this._readString(offset, unicharsCount, true); + break; + } + + case this.OBJECT_TYPE_BITS.UID: { + // UIDs are only used in Keyed Archives, which are not yet supported. + throw new Error("Keyed Archives are not supported"); + } + + case this.OBJECT_TYPE_BITS.ARRAY: + case this.OBJECT_TYPE_BITS.SET: { + // Note: For now, we fallback to handle sets the same way we handle + // arrays. See comments in the header of this file. + + // The bytes following the count are references to objects (indices). + // Each reference is an unsigned int with size=this._objectRefSize. + let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); + value = this._wrapArray(offset, objectsCount); + break; + } + + case this.OBJECT_TYPE_BITS.DICTIONARY: { + let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); + value = this._wrapDictionary(offset, objectsCount); + break; + } + + default: { + throw new Error("Unknown object type: " + objType); + } + } + + return this._objects[aObjectIndex] = value; + } +}; + +/** + * Reader for XML property lists. + * + * @param aDOMDoc + * the DOM document to be read as a property list. + */ +function XMLPropertyListReader(aDOMDoc) { + let docElt = aDOMDoc.documentElement; + if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild) + throw new Error("aDoc is not a property list document"); + + this._plistRootElement = docElt.firstElementChild; +} + +XMLPropertyListReader.prototype = { + get root() this._readObject(this._plistRootElement), + + /** + * Convert a dom element to a property list object. + * @param aDOMElt + * a dom element in a xml tree of a property list. + * @return a js object representing the property list object. + */ + _readObject: function XPLR__readObject(aDOMElt) { + switch (aDOMElt.localName) { + case "true": + return true; + case "false": + return false; + case "string": + case "key": + return aDOMElt.textContent; + case "integer": + return this._readInteger(aDOMElt); + case "real": { + let number = parseFloat(aDOMElt.textContent.trim()); + if (isNaN(number)) + throw "Could not parse float value"; + return number; + } + case "date": + return new Date(aDOMElt.textContent); + case "data": + // Strip spaces and new lines. + let base64str = aDOMElt.textContent.replace(/\s*/g, ""); + let decoded = atob(base64str); + return Uint8Array([decoded.charCodeAt(i) for (i in decoded)]); + case "dict": + return this._wrapDictionary(aDOMElt); + case "array": + return this._wrapArray(aDOMElt); + default: + throw new Error("Unexpected tagname"); + } + }, + + _readInteger: function XPLR__readInteger(aDOMElt) { + // The integer may outbound js's max/min integer value. We recognize this + // case by comparing the parsed number to the original string value. + // In case of an outbound, we fallback to return the number as a string. + let numberAsString = aDOMElt.textContent.toString(); + let parsedNumber = parseInt(numberAsString, 10); + if (isNaN(parsedNumber)) + throw new Error("Could not parse integer value"); + + if (parsedNumber.toString() == numberAsString) + return parsedNumber; + + return PropertyListUtils.wrapInt64(numberAsString); + }, + + _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) { + // + // my true bool + // + // my string key + // My String Key + // + if (aDOMElt.children.length % 2 != 0) + throw new Error("Invalid dictionary"); + let dict = new Dict(); + for (let i = 0; i < aDOMElt.children.length; i += 2) { + let keyElem = aDOMElt.children[i]; + let valElem = aDOMElt.children[i + 1]; + + if (keyElem.localName != "key") + throw new Error("Invalid dictionary"); + + let keyName = this._readObject(keyElem); + let readBound = this._readObject.bind(this, valElem); + dict.setAsLazyGetter(keyName, readBound); + } + return dict; + }, + + _wrapArray: function XPLR__wrapArray(aDOMElt) { + // + // ... + // + // + // .... + // + // + + // Each element in the array is a lazy getter for its property list object. + let array = []; + let readObjectBound = this._readObject.bind(this); + Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) { + Object.defineProperty(array, elemIndex, { + get: function() { + delete array[elemIndex]; + return array[elemIndex] = readObjectBound(elem); + }, + configurable: true, + enumerable: true + }); + }); + return array; + } +}; diff --git a/toolkit/content/tests/unit/propertyLists/bug710259_propertyListBinary.plist b/toolkit/content/tests/unit/propertyLists/bug710259_propertyListBinary.plist new file mode 100644 index 0000000000000000000000000000000000000000..5888c9c9c545cd63c7b165d9990e6a2ae02daa23 GIT binary patch literal 3277 zcmYc)$jK}&F)+Bq$iyrX>R42iSQ+k=pP!SOn74w3m5rT)lZ!hzF)2A-koiU8C>RZa z(GVE+A&|^^0R;1bSsua~B}YSGGz3ONU^E0qLtr!nhH?m`-C(%G0K>+PTh^<2GcYjt z3y2sQ7#ivt8tEDvh8P-J8Jb!d8%JH?;pO8O5EK#)cgjpx@XRZT4oXeTQSd9xO-e0_ z7vT3xO;0SzEK7xp2*5?48iW}XfW|X0Fe*E+?&s?E^gJ#69}NB#2>-tiVRA5VFz_-6 zGe|S2Flexdut;;i=l;XP$|J+0%A?C;$>Yrv$CJ)ez*EW71U8F-i4j6Gh(c*7#nTJ` DIY9@Z literal 0 HcmV?d00001 diff --git a/toolkit/content/tests/unit/propertyLists/bug710259_propertyListXML.plist b/toolkit/content/tests/unit/propertyLists/bug710259_propertyListXML.plist new file mode 100644 index 000000000000..9b6decc1e6fa --- /dev/null +++ b/toolkit/content/tests/unit/propertyLists/bug710259_propertyListXML.plist @@ -0,0 +1,28 @@ + + + + + Boolean + + Array + + abc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + אאא + אאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאא + 𐀀𐀀𐀀 + 2011-12-31T11:15:23Z + MjAxMS0xMi0zMVQxMToxNTozM1o= + + Negative Number + -400 + Real Number + 2.71828183 + Big Int + 9007199254740993 + Negative Big Int + -9007199254740993 + + + + diff --git a/toolkit/content/tests/unit/test_dict.js b/toolkit/content/tests/unit/test_dict.js index afe058de60a3..ea641c647ed8 100644 --- a/toolkit/content/tests/unit/test_dict.js +++ b/toolkit/content/tests/unit/test_dict.js @@ -208,6 +208,83 @@ function test_set_property_non_strict() { do_check_eq(dict.get, realget); } +/** + * Tests setting a property by a lazy getter. + */ +function test_set_property_lazy_getter() { + let thunkCalled = false; + + let setThunk = function(dict) { + thunkCalled = false; + dict.setAsLazyGetter("foo", function() { + thunkCalled = true; + return "bar"; + }); + }; + + let (dict = new Dict()) { + setThunk(dict); + + // Test that checking for the key existence does not invoke + // the getter function. + do_check_true(dict.has("foo")); + do_check_false(thunkCalled); + do_check_true(dict.isLazyGetter("foo")); + + // Calling get the first time should invoke the getter function + // and unmark the key as a lazy getter. + do_check_eq(dict.get("foo"), "bar"); + do_check_true(thunkCalled); + do_check_false(dict.isLazyGetter("foo")); + + // Calling get again should not invoke the getter function + thunkCalled = false; + do_check_eq(dict.get("foo"), "bar"); + do_check_false(thunkCalled); + do_check_false(dict.isLazyGetter("foo")); + } + + // Test that listvalues works for lazy keys. + let (dict = new Dict()) { + setThunk(dict); + do_check_true(dict.isLazyGetter("foo")); + + let (listvalues = dict.listvalues()) { + do_check_false(dict.isLazyGetter("foo")); + do_check_true(thunkCalled); + do_check_true(listvalues.length, 1); + do_check_eq(listvalues[0], "bar"); + } + + thunkCalled = false; + + // Retrieving the list again shouldn't invoke our getter. + let (listvalues = dict.listvalues()) { + do_check_false(dict.isLazyGetter("foo")); + do_check_false(thunkCalled); + do_check_true(listvalues.length, 1); + do_check_eq(listvalues[0], "bar"); + } + } + + // Test that the values iterator also works as expected. + let (dict = new Dict()) { + setThunk(dict); + let values = dict.values; + + // Our getter shouldn't be called before the iterator reaches it. + do_check_true(dict.isLazyGetter("foo")); + do_check_false(thunkCalled); + do_check_eq(values.next(), "bar"); + do_check_true(thunkCalled); + + thunkCalled = false; + do_check_false(dict.isLazyGetter("foo")); + do_check_eq(dict.get("foo"), "bar"); + do_check_false(thunkCalled); + } +} + var tests = [ test_get_set_has_del, test_get_default, @@ -218,6 +295,7 @@ var tests = [ test_iterators, test_set_property_strict, test_set_property_non_strict, + test_set_property_lazy_getter ]; function run_test() { diff --git a/toolkit/content/tests/unit/test_propertyListsUtils.js b/toolkit/content/tests/unit/test_propertyListsUtils.js new file mode 100644 index 000000000000..0787e1e171fe --- /dev/null +++ b/toolkit/content/tests/unit/test_propertyListsUtils.js @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +Components.utils.import("resource://gre/modules/PropertyListUtils.jsm"); + +function checkValue(aPropertyListObject, aType, aValue) { + do_check_eq(PropertyListUtils.getObjectType(aPropertyListObject), aType); + if (aValue !== undefined) { + // Perform strict equality checks until Bug 714467 is fixed. + let strictEqualityCheck = function(a, b) { + do_check_eq(typeof(a), typeof(b)); + do_check_eq(a, b); + }; + + if (typeof(aPropertyListObject) == "object") + strictEqualityCheck(aPropertyListObject.valueOf(), aValue.valueOf()); + else + strictEqualityCheck(aPropertyListObject, aValue); + } +} + +function checkLazyGetterValue(aObject, aPropertyName, aType, aValue) { + let descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName); + do_check_eq(typeof(descriptor.get), "function"); + do_check_eq(typeof(descriptor.value), "undefined"); + checkValue(aObject[aPropertyName], aType, aValue); + descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName); + do_check_eq(typeof(descriptor.get), "undefined"); + do_check_neq(typeof(descriptor.value), "undefined"); +} + +function checkMainPropertyList(aPropertyListRoot) { + const PRIMITIVE = PropertyListUtils.TYPE_PRIMITIVE; + + checkValue(aPropertyListRoot, PropertyListUtils.TYPE_DICTIONARY); + checkValue(aPropertyListRoot.get("Boolean"), PRIMITIVE, false); + let (array = aPropertyListRoot.get("Array")) { + checkValue(array, PropertyListUtils.TYPE_ARRAY); + do_check_eq(array.length, 8); + + // Test both long and short values, since binary property lists store + // long values a little bit differently (see readDataLengthAndOffset). + + // Short ASCII string + checkLazyGetterValue(array, 0, PRIMITIVE, "abc"); + // Long ASCII string + checkLazyGetterValue(array, 1, PRIMITIVE, new Array(1001).join("a")); + // Short unicode string + checkLazyGetterValue(array, 2, PRIMITIVE, "\u05D0\u05D0\u05D0"); + // Long unicode string + checkLazyGetterValue(array, 3, PRIMITIVE, new Array(1001).join("\u05D0")); + // Unicode surrogate pair + checkLazyGetterValue(array, 4, PRIMITIVE, + "\uD800\uDC00\uD800\uDC00\uD800\uDC00"); + + // Date + checkLazyGetterValue(array, 5, PropertyListUtils.TYPE_DATE, + new Date("2011-12-31T11:15:23Z")); + + // Data + checkLazyGetterValue(array, 6, PropertyListUtils.TYPE_UINT8_ARRAY); + let dataAsString = [String.fromCharCode(b) for each (b in array[6])].join(""); + do_check_eq(dataAsString, "2011-12-31T11:15:33Z"); + + // Dict + let (dict = array[7]) { + checkValue(dict, PropertyListUtils.TYPE_DICTIONARY); + checkValue(dict.get("Negative Number"), PRIMITIVE, -400); + checkValue(dict.get("Real Number"), PRIMITIVE, 2.71828183); + checkValue(dict.get("Big Int"), + PropertyListUtils.TYPE_INT64, + "9007199254740993"); + checkValue(dict.get("Negative Big Int"), + PropertyListUtils.TYPE_INT64, + "-9007199254740993"); + } + } +} + +function readPropertyList(aFile, aCallback) { + PropertyListUtils.read(aFile, function(aPropertyListRoot) { + // Null root indicates failure to read property list. + // Note: It is important not to run do_check_n/eq directly on Dict and array + // objects, because it cases their toString to get invoked, doing away with + // all the lazy getter we'd like to test later. + do_check_true(aPropertyListRoot !== null); + aCallback(aPropertyListRoot); + run_next_test(); + }); +} + +function run_test() { + add_test(readPropertyList.bind(this, + do_get_file("propertyLists/bug710259_propertyListBinary.plist", false), + checkMainPropertyList)); + add_test(readPropertyList.bind(this, + do_get_file("propertyLists/bug710259_propertyListXML.plist", false), + checkMainPropertyList)); + run_next_test(); +} diff --git a/toolkit/content/tests/unit/xpcshell.ini b/toolkit/content/tests/unit/xpcshell.ini index 2f6c2011223b..6efc4cc46a6a 100644 --- a/toolkit/content/tests/unit/xpcshell.ini +++ b/toolkit/content/tests/unit/xpcshell.ini @@ -5,3 +5,4 @@ tail = [test_contentAreaUtils.js] [test_dict.js] [test_privatebrowsing_downloadLastDir_c.js] +[test_propertyListsUtils.js]