1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/PropertyListUtils.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,771 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * Module for reading Property Lists (.plist) files 1.10 + * ------------------------------------------------ 1.11 + * This module functions as a reader for Apple Property Lists (.plist files). 1.12 + * It supports both binary and xml formatted property lists. It does not 1.13 + * support the legacy ASCII format. Reading of Cocoa's Keyed Archives serialized 1.14 + * to binary property lists isn't supported either. 1.15 + * 1.16 + * Property Lists objects are represented by standard JS and Mozilla types, 1.17 + * namely: 1.18 + * 1.19 + * XML type Cocoa Class Returned type(s) 1.20 + * -------------------------------------------------------------------------- 1.21 + * <true/> / <false/> NSNumber TYPE_PRIMITIVE boolean 1.22 + * <integer> / <real> NSNumber TYPE_PRIMITIVE number 1.23 + * TYPE_INT64 String [1] 1.24 + * Not Available NSNull TYPE_PRIMITIVE null [2] 1.25 + * TYPE_PRIMITIVE undefined [3] 1.26 + * <date/> NSDate TYPE_DATE Date 1.27 + * <data/> NSData TYPE_UINT8_ARRAY Uint8Array 1.28 + * <array/> NSArray TYPE_ARRAY Array 1.29 + * Not Available NSSet TYPE_ARRAY Array [2][4] 1.30 + * <dict/> NSDictionary TYPE_DICTIONARY Dict (from Dict.jsm) 1.31 + * 1.32 + * Use PropertyListUtils.getObjectType to detect the type of a Property list 1.33 + * object. 1.34 + * 1.35 + * ------------- 1.36 + * 1) Property lists supports storing U/Int64 numbers, while JS can only handle 1.37 + * numbers that are in this limits of float-64 (±2^53). For numbers that 1.38 + * do not outbound this limits, simple primitive number are always used. 1.39 + * Otherwise, a String object. 1.40 + * 2) About NSNull and NSSet values: While the xml format has no support for 1.41 + * representing null and set values, the documentation for the binary format 1.42 + * states that it supports storing both types. However, the Cocoa APIs for 1.43 + * serializing property lists do not seem to support either types (test with 1.44 + * NSPropertyListSerialization::propertyList:isValidForFormat). Furthermore, 1.45 + * if an array or a dictioanry contains a NSNull or a NSSet value, they cannot 1.46 + * be serialized to a property list. 1.47 + * As for usage within OS X, not surprisingly there's no known usage of 1.48 + * storing either of these types in a property list. It seems that, for now, 1.49 + * Apple is keeping the features of binary and xml formats in sync, probably as 1.50 + * long as the XML format is not officially deprecated. 1.51 + * 3) Not used anywhere. 1.52 + * 4) About NSSet representation: For the time being, we represent those 1.53 + * theoretical NSSet objects the same way NSArray is represented. 1.54 + * While this would most certainly work, it is not the right way to handle 1.55 + * it. A more correct representation for a set is a js generator, which would 1.56 + * read the set lazily and has no indices semantics. 1.57 + */ 1.58 + 1.59 +"use strict"; 1.60 + 1.61 +this.EXPORTED_SYMBOLS = ["PropertyListUtils"]; 1.62 + 1.63 +const Cc = Components.classes; 1.64 +const Ci = Components.interfaces; 1.65 +const Cu = Components.utils; 1.66 + 1.67 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.68 + 1.69 +XPCOMUtils.defineLazyModuleGetter(this, "Dict", 1.70 + "resource://gre/modules/Dict.jsm"); 1.71 +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", 1.72 + "resource://gre/modules/ctypes.jsm"); 1.73 +XPCOMUtils.defineLazyModuleGetter(this, "Services", 1.74 + "resource://gre/modules/Services.jsm"); 1.75 + 1.76 +this.PropertyListUtils = Object.freeze({ 1.77 + /** 1.78 + * Asynchronously reads a file as a property list. 1.79 + * 1.80 + * @param aFile (nsIDOMBlob/nsILocalFile) 1.81 + * the file to be read as a property list. 1.82 + * @param aCallback 1.83 + * If the property list is read successfully, aPropertyListRoot is set 1.84 + * to the root object of the property list. 1.85 + * Use getPropertyListObjectType to detect its type. 1.86 + * If it's not read successfully, aPropertyListRoot is set to null. 1.87 + * The reaon for failure is reported to the Error Console. 1.88 + */ 1.89 + read: function PLU_read(aFile, aCallback) { 1.90 + if (!(aFile instanceof Ci.nsILocalFile || aFile instanceof Ci.nsIDOMFile)) 1.91 + throw new Error("aFile is not a file object"); 1.92 + if (typeof(aCallback) != "function") 1.93 + throw new Error("Invalid value for aCallback"); 1.94 + 1.95 + // We guarantee not to throw directly for any other exceptions, and always 1.96 + // call aCallback. 1.97 + Services.tm.mainThread.dispatch(function() { 1.98 + let file = aFile; 1.99 + try { 1.100 + if (file instanceof Ci.nsILocalFile) { 1.101 + if (!file.exists()) 1.102 + throw new Error("The file pointed by aFile does not exist"); 1.103 + 1.104 + file = new File(file); 1.105 + } 1.106 + 1.107 + let fileReader = Cc["@mozilla.org/files/filereader;1"]. 1.108 + createInstance(Ci.nsIDOMFileReader); 1.109 + let onLoadEnd = function() { 1.110 + let root = null; 1.111 + try { 1.112 + fileReader.removeEventListener("loadend", onLoadEnd, false); 1.113 + if (fileReader.readyState != fileReader.DONE) 1.114 + throw new Error("Could not read file contents: " + fileReader.error); 1.115 + 1.116 + root = this._readFromArrayBufferSync(fileReader.result); 1.117 + } 1.118 + finally { 1.119 + aCallback(root); 1.120 + } 1.121 + }.bind(this); 1.122 + fileReader.addEventListener("loadend", onLoadEnd, false); 1.123 + fileReader.readAsArrayBuffer(file); 1.124 + } 1.125 + catch(ex) { 1.126 + aCallback(null); 1.127 + throw ex; 1.128 + } 1.129 + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); 1.130 + }, 1.131 + 1.132 + /** 1.133 + * DO NOT USE ME. Once Bug 718189 is fixed, this method won't be public. 1.134 + * 1.135 + * Synchronously read an ArrayBuffer contents as a property list. 1.136 + */ 1.137 + _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) { 1.138 + if (BinaryPropertyListReader.prototype.canProcess(aBuffer)) 1.139 + return new BinaryPropertyListReader(aBuffer).root; 1.140 + 1.141 + // Convert the buffer into an XML tree. 1.142 + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]. 1.143 + createInstance(Ci.nsIDOMParser); 1.144 + let bytesView = new Uint8Array(aBuffer); 1.145 + try { 1.146 + let doc = domParser.parseFromBuffer(bytesView, bytesView.length, 1.147 + "application/xml"); 1.148 + return new XMLPropertyListReader(doc).root; 1.149 + } 1.150 + catch(ex) { 1.151 + throw new Error("aBuffer cannot be parsed as a DOM document: " + ex); 1.152 + } 1.153 + return null; 1.154 + }, 1.155 + 1.156 + TYPE_PRIMITIVE: 0, 1.157 + TYPE_DATE: 1, 1.158 + TYPE_UINT8_ARRAY: 2, 1.159 + TYPE_ARRAY: 3, 1.160 + TYPE_DICTIONARY: 4, 1.161 + TYPE_INT64: 5, 1.162 + 1.163 + /** 1.164 + * Get the type in which the given property list object is represented. 1.165 + * Check the header for the mapping between the TYPE* constants to js types 1.166 + * and objects. 1.167 + * 1.168 + * @return one of the TYPE_* constants listed above. 1.169 + * @note this method is merely for convenience. It has no magic to detect 1.170 + * that aObject is indeed a property list object created by this module. 1.171 + */ 1.172 + getObjectType: function PLU_getObjectType(aObject) { 1.173 + if (aObject === null || typeof(aObject) != "object") 1.174 + return this.TYPE_PRIMITIVE; 1.175 + 1.176 + // Given current usage, we could assume that aObject was created in the 1.177 + // scope of this module, but in future, this util may be used as part of 1.178 + // serializing js objects to a property list - in which case the object 1.179 + // would most likely be created in the caller's scope. 1.180 + let global = Cu.getGlobalForObject(aObject); 1.181 + 1.182 + if (global.Dict && aObject instanceof global.Dict) 1.183 + return this.TYPE_DICTIONARY; 1.184 + if (Array.isArray(aObject)) 1.185 + return this.TYPE_ARRAY; 1.186 + if (aObject instanceof global.Date) 1.187 + return this.TYPE_DATE; 1.188 + if (aObject instanceof global.Uint8Array) 1.189 + return this.TYPE_UINT8_ARRAY; 1.190 + if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject) 1.191 + return this.TYPE_INT64; 1.192 + 1.193 + throw new Error("aObject is not as a property list object."); 1.194 + }, 1.195 + 1.196 + /** 1.197 + * Wraps a 64-bit stored in the form of a string primitive as a String object, 1.198 + * which we can later distiguish from regular string values. 1.199 + * @param aPrimitive 1.200 + * a number in the form of either a primitive string or a primitive number. 1.201 + * @return a String wrapper around aNumberStr that can later be identified 1.202 + * as holding 64-bit number using getObjectType. 1.203 + */ 1.204 + wrapInt64: function PLU_wrapInt64(aPrimitive) { 1.205 + if (typeof(aPrimitive) != "string" && typeof(aPrimitive) != "number") 1.206 + throw new Error("aPrimitive should be a string primitive"); 1.207 + 1.208 + let wrapped = new String(aPrimitive); 1.209 + Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true }); 1.210 + return wrapped; 1.211 + } 1.212 +}); 1.213 + 1.214 +/** 1.215 + * Here's the base structure of binary-format property lists. 1.216 + * 1) Header - magic number 1.217 + * - 6 bytes - "bplist" 1.218 + * - 2 bytes - version number. This implementation only supports version 00. 1.219 + * 2) Objects Table 1.220 + * Variable-sized objects, see _readObject for how various types of objects 1.221 + * are constructed. 1.222 + * 3) Offsets Table 1.223 + * The offset of each object in the objects table. The integer size is 1.224 + * specified in the trailer. 1.225 + * 4) Trailer 1.226 + * - 6 unused bytes 1.227 + * - 1 byte: the size of integers in the offsets table 1.228 + * - 1 byte: the size of object references for arrays, sets and 1.229 + * dictionaries. 1.230 + * - 8 bytes: the number of objects in the objects table 1.231 + * - 8 bytes: the index of the root object's offset in the offsets table. 1.232 + * - 8 bytes: the offset of the offsets table. 1.233 + * 1.234 + * Note: all integers are stored in big-endian form. 1.235 + */ 1.236 + 1.237 +/** 1.238 + * Reader for binary-format property lists. 1.239 + * 1.240 + * @param aBuffer 1.241 + * ArrayBuffer object from which the binary plist should be read. 1.242 + */ 1.243 +function BinaryPropertyListReader(aBuffer) { 1.244 + this._buffer = aBuffer; 1.245 + 1.246 + const JS_MAX_INT = Math.pow(2,53); 1.247 + this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT); 1.248 + this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT); 1.249 + this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT); 1.250 + 1.251 + try { 1.252 + this._readTrailerInfo(); 1.253 + this._readObjectsOffsets(); 1.254 + } 1.255 + catch(ex) { 1.256 + throw new Error("Could not read aBuffer as a binary property list"); 1.257 + } 1.258 + this._objects = []; 1.259 +} 1.260 + 1.261 +BinaryPropertyListReader.prototype = { 1.262 + /** 1.263 + * Checks if the given ArrayBuffer can be read as a binary property list. 1.264 + * It can be called on the prototype. 1.265 + */ 1.266 + canProcess: function BPLR_canProcess(aBuffer) 1.267 + [String.fromCharCode(c) for each (c in new Uint8Array(aBuffer, 0, 8))]. 1.268 + join("") == "bplist00", 1.269 + 1.270 + get root() this._readObject(this._rootObjectIndex), 1.271 + 1.272 + _readTrailerInfo: function BPLR__readTrailer() { 1.273 + // The first 6 bytes of the 32-bytes trailer are unused 1.274 + let trailerOffset = this._buffer.byteLength - 26; 1.275 + [this._offsetTableIntegerSize, this._objectRefSize] = 1.276 + this._readUnsignedInts(trailerOffset, 1, 2); 1.277 + 1.278 + [this._numberOfObjects, this._rootObjectIndex, this._offsetTableOffset] = 1.279 + this._readUnsignedInts(trailerOffset + 2, 8, 3); 1.280 + }, 1.281 + 1.282 + _readObjectsOffsets: function BPLR__readObjectsOffsets() { 1.283 + this._offsetTable = this._readUnsignedInts(this._offsetTableOffset, 1.284 + this._offsetTableIntegerSize, 1.285 + this._numberOfObjects); 1.286 + }, 1.287 + 1.288 + // TODO: This should be removed once DataView is implemented (Bug 575688). 1.289 + _swapForBigEndian: 1.290 + function BPLR__swapForBigEndian(aByteOffset, aIntSize, aNumberOfInts) { 1.291 + let bytesCount = aIntSize * aNumberOfInts; 1.292 + let bytes = new Uint8Array(this._buffer, aByteOffset, bytesCount); 1.293 + let swapped = new Uint8Array(bytesCount); 1.294 + for (let i = 0; i < aNumberOfInts; i++) { 1.295 + for (let j = 0; j < aIntSize; j++) { 1.296 + swapped[(i * aIntSize) + j] = bytes[(i * aIntSize) + (aIntSize - 1 - j)]; 1.297 + } 1.298 + } 1.299 + return swapped; 1.300 + }, 1.301 + 1.302 + _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) { 1.303 + let swapped = this._swapForBigEndian(aByteOffset, 8, 1); 1.304 + let lo = new Uint32Array(swapped.buffer, 0, 1)[0]; 1.305 + let hi = new Int32Array(swapped.buffer, 4, 1)[0]; 1.306 + let int64 = ctypes.Int64.join(hi, lo); 1.307 + if (ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 || 1.308 + ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1) 1.309 + return PropertyListUtils.wrapInt64(int64.toString()); 1.310 + 1.311 + return parseInt(int64.toString(), 10); 1.312 + }, 1.313 + 1.314 + _readReal: function BPLR__readReal(aByteOffset, aRealSize) { 1.315 + let swapped = this._swapForBigEndian(aByteOffset, aRealSize, 1); 1.316 + if (aRealSize == 4) 1.317 + return new Float32Array(swapped.buffer, 0, 1)[0]; 1.318 + if (aRealSize == 8) 1.319 + return new Float64Array(swapped.buffer, 0, 1)[0]; 1.320 + 1.321 + throw new Error("Unsupported real size: " + aRealSize); 1.322 + }, 1.323 + 1.324 + OBJECT_TYPE_BITS: { 1.325 + SIMPLE: parseInt("0000", 2), 1.326 + INTEGER: parseInt("0001", 2), 1.327 + REAL: parseInt("0010", 2), 1.328 + DATE: parseInt("0011", 2), 1.329 + DATA: parseInt("0100", 2), 1.330 + ASCII_STRING: parseInt("0101", 2), 1.331 + UNICODE_STRING: parseInt("0110", 2), 1.332 + UID: parseInt("1000", 2), 1.333 + ARRAY: parseInt("1010", 2), 1.334 + SET: parseInt("1100", 2), 1.335 + DICTIONARY: parseInt("1101", 2) 1.336 + }, 1.337 + 1.338 + ADDITIONAL_INFO_BITS: { 1.339 + // Applies to OBJECT_TYPE_BITS.SIMPLE 1.340 + NULL: parseInt("0000", 2), 1.341 + FALSE: parseInt("1000", 2), 1.342 + TRUE: parseInt("1001", 2), 1.343 + FILL_BYTE: parseInt("1111", 2), 1.344 + // Applies to OBJECT_TYPE_BITS.DATE 1.345 + DATE: parseInt("0011", 2), 1.346 + // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY, 1.347 + // SET and DICTIONARY. 1.348 + LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2) 1.349 + }, 1.350 + 1.351 + /** 1.352 + * Returns an object descriptor in the form of two integers: object type and 1.353 + * additional info. 1.354 + * 1.355 + * @param aByteOffset 1.356 + * the descriptor's offset. 1.357 + * @return [objType, additionalInfo] - the object type and additional info. 1.358 + * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS 1.359 + */ 1.360 + _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) { 1.361 + // The first four bits hold the object type. For some types, additional 1.362 + // info is held in the other 4 bits. 1.363 + let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0]; 1.364 + return [(byte & 0xF0) >> 4, byte & 0x0F]; 1.365 + }, 1.366 + 1.367 + _readDate: function BPLR__readDate(aByteOffset) { 1.368 + // That's the reference date of NSDate. 1.369 + let date = new Date("1 January 2001, GMT"); 1.370 + 1.371 + // NSDate values are float values, but setSeconds takes an integer. 1.372 + date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000); 1.373 + return date; 1.374 + }, 1.375 + 1.376 + /** 1.377 + * Reads a portion of the buffer as a string. 1.378 + * 1.379 + * @param aByteOffset 1.380 + * The offset in the buffer at which the string starts 1.381 + * @param aNumberOfChars 1.382 + * The length of the string to be read (that is the number of 1.383 + * characters, not bytes). 1.384 + * @param aUnicode 1.385 + * Whether or not it is a unicode string. 1.386 + * @return the string read. 1.387 + * 1.388 + * @note this is tested to work well with unicode surrogate pairs. Because 1.389 + * all unicode characters are read as 2-byte integers, unicode surrogate 1.390 + * pairs are read from the buffer in the form of two integers, as required 1.391 + * by String.fromCharCode. 1.392 + */ 1.393 + _readString: 1.394 + function BPLR__readString(aByteOffset, aNumberOfChars, aUnicode) { 1.395 + let codes = this._readUnsignedInts(aByteOffset, aUnicode ? 2 : 1, 1.396 + aNumberOfChars); 1.397 + return [String.fromCharCode(c) for each (c in codes)].join(""); 1.398 + }, 1.399 + 1.400 + /** 1.401 + * Reads an array of unsigned integers from the buffer. Integers larger than 1.402 + * one byte are read in big endian form. 1.403 + * 1.404 + * @param aByteOffset 1.405 + * The offset in the buffer at which the array starts. 1.406 + * @param aIntSize 1.407 + * The size of each int in the array. 1.408 + * @param aLength 1.409 + * The number of ints in the array. 1.410 + * @param [optional] aBigIntAllowed (default: false) 1.411 + * Whether or not to accept integers which outbounds JS limits for 1.412 + * numbers (±2^53) in the form of a String. 1.413 + * @return an array of integers (number primitive and/or Strings for large 1.414 + * numbers (see header)). 1.415 + * @throws if aBigIntAllowed is false and one of the integers in the array 1.416 + * cannot be represented by a primitive js number. 1.417 + */ 1.418 + _readUnsignedInts: 1.419 + function BPLR__readUnsignedInts(aByteOffset, aIntSize, aLength, aBigIntAllowed) { 1.420 + if (aIntSize == 1) 1.421 + return new Uint8Array(this._buffer, aByteOffset, aLength); 1.422 + 1.423 + // There are two reasons for the complexity you see here: 1.424 + // (1) 64-bit integers - For which we use ctypes. When possible, the 1.425 + // number is converted back to js's default float-64 type. 1.426 + // (2) The DataView object for ArrayBuffer, which takes care of swaping 1.427 + // bytes, is not yet implemented (bug 575688). 1.428 + let swapped = this._swapForBigEndian(aByteOffset, aIntSize, aLength); 1.429 + if (aIntSize == 2) 1.430 + return new Uint16Array(swapped.buffer); 1.431 + if (aIntSize == 4) 1.432 + return new Uint32Array(swapped.buffer); 1.433 + if (aIntSize == 8) { 1.434 + let intsArray = []; 1.435 + let lo_hi_view = new Uint32Array(swapped.buffer); 1.436 + for (let i = 0; i < lo_hi_view.length; i += 2) { 1.437 + let [lo, hi] = [lo_hi_view[i], lo_hi_view[i+1]]; 1.438 + let uint64 = ctypes.UInt64.join(hi, lo); 1.439 + if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) { 1.440 + if (aBigIntAllowed === true) 1.441 + intsArray.push(PropertyListUtils.wrapInt64(uint64.toString())); 1.442 + else 1.443 + throw new Error("Integer too big to be read as float 64"); 1.444 + } 1.445 + else { 1.446 + intsArray.push(parseInt(uint64.toString(), 10)); 1.447 + } 1.448 + } 1.449 + return intsArray; 1.450 + } 1.451 + throw new Error("Unsupported size: " + aIntSize); 1.452 + }, 1.453 + 1.454 + /** 1.455 + * Reads from the buffer the data object-count and the offset at which the 1.456 + * first object starts. 1.457 + * 1.458 + * @param aObjectOffset 1.459 + * the object's offset. 1.460 + * @return [offset, count] - the offset in the buffer at which the first 1.461 + * object in data starts, and the number of objects. 1.462 + */ 1.463 + _readDataOffsetAndCount: 1.464 + function BPLR__readDataOffsetAndCount(aObjectOffset) { 1.465 + // The length of some objects in the data can be stored in two ways: 1.466 + // * If it is small enough, it is stored in the second four bits of the 1.467 + // object descriptors. 1.468 + // * Otherwise, those bits are set to 1111, indicating that the next byte 1.469 + // consists of the integer size of the data-length (also stored in the form 1.470 + // of an object descriptor). The length follows this byte. 1.471 + let [, maybeLength] = this._readObjectDescriptor(aObjectOffset); 1.472 + if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS) 1.473 + return [aObjectOffset + 1, maybeLength]; 1.474 + 1.475 + let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1); 1.476 + 1.477 + // The int size is 2^intSizeInfo. 1.478 + let intSize = Math.pow(2, intSizeInfo); 1.479 + let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0]; 1.480 + return [aObjectOffset + 2 + intSize, dataLength]; 1.481 + }, 1.482 + 1.483 + /** 1.484 + * Read array from the buffer and wrap it as a js array. 1.485 + * @param aObjectOffset 1.486 + * the offset in the buffer at which the array starts. 1.487 + * @param aNumberOfObjects 1.488 + * the number of objects in the array. 1.489 + * @return a js array. 1.490 + */ 1.491 + _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) { 1.492 + let refs = this._readUnsignedInts(aObjectOffset, 1.493 + this._objectRefSize, 1.494 + aNumberOfObjects); 1.495 + 1.496 + let array = new Array(aNumberOfObjects); 1.497 + let readObjectBound = this._readObject.bind(this); 1.498 + 1.499 + // Each index in the returned array is a lazy getter for its object. 1.500 + Array.prototype.forEach.call(refs, function(ref, objIndex) { 1.501 + Object.defineProperty(array, objIndex, { 1.502 + get: function() { 1.503 + delete array[objIndex]; 1.504 + return array[objIndex] = readObjectBound(ref); 1.505 + }, 1.506 + configurable: true, 1.507 + enumerable: true 1.508 + }); 1.509 + }, this); 1.510 + return array; 1.511 + }, 1.512 + 1.513 + /** 1.514 + * Reads dictionary from the buffer and wraps it as a Dict object (as defined 1.515 + * in Dict.jsm). 1.516 + * @param aObjectOffset 1.517 + * the offset in the buffer at which the dictionary starts 1.518 + * @param aNumberOfObjects 1.519 + * the number of keys in the dictionary 1.520 + * @return Dict.jsm-style dictionary. 1.521 + */ 1.522 + _wrapDictionary: function(aObjectOffset, aNumberOfObjects) { 1.523 + // A dictionary in the binary format is stored as a list of references to 1.524 + // key-objects, followed by a list of references to the value-objects for 1.525 + // those keys. The size of each list is aNumberOfObjects * this._objectRefSize. 1.526 + let dict = new Dict(); 1.527 + if (aNumberOfObjects == 0) 1.528 + return dict; 1.529 + 1.530 + let keyObjsRefs = this._readUnsignedInts(aObjectOffset, this._objectRefSize, 1.531 + aNumberOfObjects); 1.532 + let valObjsRefs = 1.533 + this._readUnsignedInts(aObjectOffset + aNumberOfObjects * this._objectRefSize, 1.534 + this._objectRefSize, aNumberOfObjects); 1.535 + for (let i = 0; i < aNumberOfObjects; i++) { 1.536 + let key = this._readObject(keyObjsRefs[i]); 1.537 + let readBound = this._readObject.bind(this, valObjsRefs[i]); 1.538 + dict.setAsLazyGetter(key, readBound); 1.539 + } 1.540 + return dict; 1.541 + }, 1.542 + 1.543 + /** 1.544 + * Reads an object at the spcified index in the object table 1.545 + * @param aObjectIndex 1.546 + * index at the object table 1.547 + * @return the property list object at the given index. 1.548 + */ 1.549 + _readObject: function BPLR__readObject(aObjectIndex) { 1.550 + // If the object was previously read, return the cached object. 1.551 + if (this._objects[aObjectIndex] !== undefined) 1.552 + return this._objects[aObjectIndex]; 1.553 + 1.554 + let objOffset = this._offsetTable[aObjectIndex]; 1.555 + let [objType, additionalInfo] = this._readObjectDescriptor(objOffset); 1.556 + let value; 1.557 + switch (objType) { 1.558 + case this.OBJECT_TYPE_BITS.SIMPLE: { 1.559 + switch (additionalInfo) { 1.560 + case this.ADDITIONAL_INFO_BITS.NULL: 1.561 + value = null; 1.562 + break; 1.563 + case this.ADDITIONAL_INFO_BITS.FILL_BYTE: 1.564 + value = undefined; 1.565 + break; 1.566 + case this.ADDITIONAL_INFO_BITS.FALSE: 1.567 + value = false; 1.568 + break; 1.569 + case this.ADDITIONAL_INFO_BITS.TRUE: 1.570 + value = true; 1.571 + break; 1.572 + default: 1.573 + throw new Error("Unexpected value!"); 1.574 + } 1.575 + break; 1.576 + } 1.577 + 1.578 + case this.OBJECT_TYPE_BITS.INTEGER: { 1.579 + // The integer is sized 2^additionalInfo. 1.580 + let intSize = Math.pow(2, additionalInfo); 1.581 + 1.582 + // For objects, 64-bit integers are always signed. Negative integers 1.583 + // are always represented by a 64-bit integer. 1.584 + if (intSize == 8) 1.585 + value = this._readSignedInt64(objOffset + 1); 1.586 + else 1.587 + value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0]; 1.588 + break; 1.589 + } 1.590 + 1.591 + case this.OBJECT_TYPE_BITS.REAL: { 1.592 + // The real is sized 2^additionalInfo. 1.593 + value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo)); 1.594 + break; 1.595 + } 1.596 + 1.597 + case this.OBJECT_TYPE_BITS.DATE: { 1.598 + if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE) 1.599 + throw new Error("Unexpected value"); 1.600 + 1.601 + value = this._readDate(objOffset + 1); 1.602 + break; 1.603 + } 1.604 + 1.605 + case this.OBJECT_TYPE_BITS.DATA: { 1.606 + let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset); 1.607 + value = this._readUnsignedInts(offset, 1, bytesCount); 1.608 + break; 1.609 + } 1.610 + 1.611 + case this.OBJECT_TYPE_BITS.ASCII_STRING: { 1.612 + let [offset, charsCount] = this._readDataOffsetAndCount(objOffset); 1.613 + value = this._readString(offset, charsCount, false); 1.614 + break; 1.615 + } 1.616 + 1.617 + case this.OBJECT_TYPE_BITS.UNICODE_STRING: { 1.618 + let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset); 1.619 + value = this._readString(offset, unicharsCount, true); 1.620 + break; 1.621 + } 1.622 + 1.623 + case this.OBJECT_TYPE_BITS.UID: { 1.624 + // UIDs are only used in Keyed Archives, which are not yet supported. 1.625 + throw new Error("Keyed Archives are not supported"); 1.626 + } 1.627 + 1.628 + case this.OBJECT_TYPE_BITS.ARRAY: 1.629 + case this.OBJECT_TYPE_BITS.SET: { 1.630 + // Note: For now, we fallback to handle sets the same way we handle 1.631 + // arrays. See comments in the header of this file. 1.632 + 1.633 + // The bytes following the count are references to objects (indices). 1.634 + // Each reference is an unsigned int with size=this._objectRefSize. 1.635 + let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); 1.636 + value = this._wrapArray(offset, objectsCount); 1.637 + break; 1.638 + } 1.639 + 1.640 + case this.OBJECT_TYPE_BITS.DICTIONARY: { 1.641 + let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset); 1.642 + value = this._wrapDictionary(offset, objectsCount); 1.643 + break; 1.644 + } 1.645 + 1.646 + default: { 1.647 + throw new Error("Unknown object type: " + objType); 1.648 + } 1.649 + } 1.650 + 1.651 + return this._objects[aObjectIndex] = value; 1.652 + } 1.653 +}; 1.654 + 1.655 +/** 1.656 + * Reader for XML property lists. 1.657 + * 1.658 + * @param aDOMDoc 1.659 + * the DOM document to be read as a property list. 1.660 + */ 1.661 +function XMLPropertyListReader(aDOMDoc) { 1.662 + let docElt = aDOMDoc.documentElement; 1.663 + if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild) 1.664 + throw new Error("aDoc is not a property list document"); 1.665 + 1.666 + this._plistRootElement = docElt.firstElementChild; 1.667 +} 1.668 + 1.669 +XMLPropertyListReader.prototype = { 1.670 + get root() this._readObject(this._plistRootElement), 1.671 + 1.672 + /** 1.673 + * Convert a dom element to a property list object. 1.674 + * @param aDOMElt 1.675 + * a dom element in a xml tree of a property list. 1.676 + * @return a js object representing the property list object. 1.677 + */ 1.678 + _readObject: function XPLR__readObject(aDOMElt) { 1.679 + switch (aDOMElt.localName) { 1.680 + case "true": 1.681 + return true; 1.682 + case "false": 1.683 + return false; 1.684 + case "string": 1.685 + case "key": 1.686 + return aDOMElt.textContent; 1.687 + case "integer": 1.688 + return this._readInteger(aDOMElt); 1.689 + case "real": { 1.690 + let number = parseFloat(aDOMElt.textContent.trim()); 1.691 + if (isNaN(number)) 1.692 + throw "Could not parse float value"; 1.693 + return number; 1.694 + } 1.695 + case "date": 1.696 + return new Date(aDOMElt.textContent); 1.697 + case "data": 1.698 + // Strip spaces and new lines. 1.699 + let base64str = aDOMElt.textContent.replace(/\s*/g, ""); 1.700 + let decoded = atob(base64str); 1.701 + return new Uint8Array([decoded.charCodeAt(i) for (i in decoded)]); 1.702 + case "dict": 1.703 + return this._wrapDictionary(aDOMElt); 1.704 + case "array": 1.705 + return this._wrapArray(aDOMElt); 1.706 + default: 1.707 + throw new Error("Unexpected tagname"); 1.708 + } 1.709 + }, 1.710 + 1.711 + _readInteger: function XPLR__readInteger(aDOMElt) { 1.712 + // The integer may outbound js's max/min integer value. We recognize this 1.713 + // case by comparing the parsed number to the original string value. 1.714 + // In case of an outbound, we fallback to return the number as a string. 1.715 + let numberAsString = aDOMElt.textContent.toString(); 1.716 + let parsedNumber = parseInt(numberAsString, 10); 1.717 + if (isNaN(parsedNumber)) 1.718 + throw new Error("Could not parse integer value"); 1.719 + 1.720 + if (parsedNumber.toString() == numberAsString) 1.721 + return parsedNumber; 1.722 + 1.723 + return PropertyListUtils.wrapInt64(numberAsString); 1.724 + }, 1.725 + 1.726 + _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) { 1.727 + // <dict> 1.728 + // <key>my true bool</key> 1.729 + // <true/> 1.730 + // <key>my string key</key> 1.731 + // <string>My String Key</string> 1.732 + // </dict> 1.733 + if (aDOMElt.children.length % 2 != 0) 1.734 + throw new Error("Invalid dictionary"); 1.735 + let dict = new Dict(); 1.736 + for (let i = 0; i < aDOMElt.children.length; i += 2) { 1.737 + let keyElem = aDOMElt.children[i]; 1.738 + let valElem = aDOMElt.children[i + 1]; 1.739 + 1.740 + if (keyElem.localName != "key") 1.741 + throw new Error("Invalid dictionary"); 1.742 + 1.743 + let keyName = this._readObject(keyElem); 1.744 + let readBound = this._readObject.bind(this, valElem); 1.745 + dict.setAsLazyGetter(keyName, readBound); 1.746 + } 1.747 + return dict; 1.748 + }, 1.749 + 1.750 + _wrapArray: function XPLR__wrapArray(aDOMElt) { 1.751 + // <array> 1.752 + // <string>...</string> 1.753 + // <integer></integer> 1.754 + // <dict> 1.755 + // .... 1.756 + // </dict> 1.757 + // </array> 1.758 + 1.759 + // Each element in the array is a lazy getter for its property list object. 1.760 + let array = []; 1.761 + let readObjectBound = this._readObject.bind(this); 1.762 + Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) { 1.763 + Object.defineProperty(array, elemIndex, { 1.764 + get: function() { 1.765 + delete array[elemIndex]; 1.766 + return array[elemIndex] = readObjectBound(elem); 1.767 + }, 1.768 + configurable: true, 1.769 + enumerable: true 1.770 + }); 1.771 + }); 1.772 + return array; 1.773 + } 1.774 +};