toolkit/modules/PropertyListUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * Module for reading Property Lists (.plist) files
michael@0 7 * ------------------------------------------------
michael@0 8 * This module functions as a reader for Apple Property Lists (.plist files).
michael@0 9 * It supports both binary and xml formatted property lists. It does not
michael@0 10 * support the legacy ASCII format. Reading of Cocoa's Keyed Archives serialized
michael@0 11 * to binary property lists isn't supported either.
michael@0 12 *
michael@0 13 * Property Lists objects are represented by standard JS and Mozilla types,
michael@0 14 * namely:
michael@0 15 *
michael@0 16 * XML type Cocoa Class Returned type(s)
michael@0 17 * --------------------------------------------------------------------------
michael@0 18 * <true/> / <false/> NSNumber TYPE_PRIMITIVE boolean
michael@0 19 * <integer> / <real> NSNumber TYPE_PRIMITIVE number
michael@0 20 * TYPE_INT64 String [1]
michael@0 21 * Not Available NSNull TYPE_PRIMITIVE null [2]
michael@0 22 * TYPE_PRIMITIVE undefined [3]
michael@0 23 * <date/> NSDate TYPE_DATE Date
michael@0 24 * <data/> NSData TYPE_UINT8_ARRAY Uint8Array
michael@0 25 * <array/> NSArray TYPE_ARRAY Array
michael@0 26 * Not Available NSSet TYPE_ARRAY Array [2][4]
michael@0 27 * <dict/> NSDictionary TYPE_DICTIONARY Dict (from Dict.jsm)
michael@0 28 *
michael@0 29 * Use PropertyListUtils.getObjectType to detect the type of a Property list
michael@0 30 * object.
michael@0 31 *
michael@0 32 * -------------
michael@0 33 * 1) Property lists supports storing U/Int64 numbers, while JS can only handle
michael@0 34 * numbers that are in this limits of float-64 (±2^53). For numbers that
michael@0 35 * do not outbound this limits, simple primitive number are always used.
michael@0 36 * Otherwise, a String object.
michael@0 37 * 2) About NSNull and NSSet values: While the xml format has no support for
michael@0 38 * representing null and set values, the documentation for the binary format
michael@0 39 * states that it supports storing both types. However, the Cocoa APIs for
michael@0 40 * serializing property lists do not seem to support either types (test with
michael@0 41 * NSPropertyListSerialization::propertyList:isValidForFormat). Furthermore,
michael@0 42 * if an array or a dictioanry contains a NSNull or a NSSet value, they cannot
michael@0 43 * be serialized to a property list.
michael@0 44 * As for usage within OS X, not surprisingly there's no known usage of
michael@0 45 * storing either of these types in a property list. It seems that, for now,
michael@0 46 * Apple is keeping the features of binary and xml formats in sync, probably as
michael@0 47 * long as the XML format is not officially deprecated.
michael@0 48 * 3) Not used anywhere.
michael@0 49 * 4) About NSSet representation: For the time being, we represent those
michael@0 50 * theoretical NSSet objects the same way NSArray is represented.
michael@0 51 * While this would most certainly work, it is not the right way to handle
michael@0 52 * it. A more correct representation for a set is a js generator, which would
michael@0 53 * read the set lazily and has no indices semantics.
michael@0 54 */
michael@0 55
michael@0 56 "use strict";
michael@0 57
michael@0 58 this.EXPORTED_SYMBOLS = ["PropertyListUtils"];
michael@0 59
michael@0 60 const Cc = Components.classes;
michael@0 61 const Ci = Components.interfaces;
michael@0 62 const Cu = Components.utils;
michael@0 63
michael@0 64 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 65
michael@0 66 XPCOMUtils.defineLazyModuleGetter(this, "Dict",
michael@0 67 "resource://gre/modules/Dict.jsm");
michael@0 68 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
michael@0 69 "resource://gre/modules/ctypes.jsm");
michael@0 70 XPCOMUtils.defineLazyModuleGetter(this, "Services",
michael@0 71 "resource://gre/modules/Services.jsm");
michael@0 72
michael@0 73 this.PropertyListUtils = Object.freeze({
michael@0 74 /**
michael@0 75 * Asynchronously reads a file as a property list.
michael@0 76 *
michael@0 77 * @param aFile (nsIDOMBlob/nsILocalFile)
michael@0 78 * the file to be read as a property list.
michael@0 79 * @param aCallback
michael@0 80 * If the property list is read successfully, aPropertyListRoot is set
michael@0 81 * to the root object of the property list.
michael@0 82 * Use getPropertyListObjectType to detect its type.
michael@0 83 * If it's not read successfully, aPropertyListRoot is set to null.
michael@0 84 * The reaon for failure is reported to the Error Console.
michael@0 85 */
michael@0 86 read: function PLU_read(aFile, aCallback) {
michael@0 87 if (!(aFile instanceof Ci.nsILocalFile || aFile instanceof Ci.nsIDOMFile))
michael@0 88 throw new Error("aFile is not a file object");
michael@0 89 if (typeof(aCallback) != "function")
michael@0 90 throw new Error("Invalid value for aCallback");
michael@0 91
michael@0 92 // We guarantee not to throw directly for any other exceptions, and always
michael@0 93 // call aCallback.
michael@0 94 Services.tm.mainThread.dispatch(function() {
michael@0 95 let file = aFile;
michael@0 96 try {
michael@0 97 if (file instanceof Ci.nsILocalFile) {
michael@0 98 if (!file.exists())
michael@0 99 throw new Error("The file pointed by aFile does not exist");
michael@0 100
michael@0 101 file = new File(file);
michael@0 102 }
michael@0 103
michael@0 104 let fileReader = Cc["@mozilla.org/files/filereader;1"].
michael@0 105 createInstance(Ci.nsIDOMFileReader);
michael@0 106 let onLoadEnd = function() {
michael@0 107 let root = null;
michael@0 108 try {
michael@0 109 fileReader.removeEventListener("loadend", onLoadEnd, false);
michael@0 110 if (fileReader.readyState != fileReader.DONE)
michael@0 111 throw new Error("Could not read file contents: " + fileReader.error);
michael@0 112
michael@0 113 root = this._readFromArrayBufferSync(fileReader.result);
michael@0 114 }
michael@0 115 finally {
michael@0 116 aCallback(root);
michael@0 117 }
michael@0 118 }.bind(this);
michael@0 119 fileReader.addEventListener("loadend", onLoadEnd, false);
michael@0 120 fileReader.readAsArrayBuffer(file);
michael@0 121 }
michael@0 122 catch(ex) {
michael@0 123 aCallback(null);
michael@0 124 throw ex;
michael@0 125 }
michael@0 126 }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
michael@0 127 },
michael@0 128
michael@0 129 /**
michael@0 130 * DO NOT USE ME. Once Bug 718189 is fixed, this method won't be public.
michael@0 131 *
michael@0 132 * Synchronously read an ArrayBuffer contents as a property list.
michael@0 133 */
michael@0 134 _readFromArrayBufferSync: function PLU__readFromArrayBufferSync(aBuffer) {
michael@0 135 if (BinaryPropertyListReader.prototype.canProcess(aBuffer))
michael@0 136 return new BinaryPropertyListReader(aBuffer).root;
michael@0 137
michael@0 138 // Convert the buffer into an XML tree.
michael@0 139 let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
michael@0 140 createInstance(Ci.nsIDOMParser);
michael@0 141 let bytesView = new Uint8Array(aBuffer);
michael@0 142 try {
michael@0 143 let doc = domParser.parseFromBuffer(bytesView, bytesView.length,
michael@0 144 "application/xml");
michael@0 145 return new XMLPropertyListReader(doc).root;
michael@0 146 }
michael@0 147 catch(ex) {
michael@0 148 throw new Error("aBuffer cannot be parsed as a DOM document: " + ex);
michael@0 149 }
michael@0 150 return null;
michael@0 151 },
michael@0 152
michael@0 153 TYPE_PRIMITIVE: 0,
michael@0 154 TYPE_DATE: 1,
michael@0 155 TYPE_UINT8_ARRAY: 2,
michael@0 156 TYPE_ARRAY: 3,
michael@0 157 TYPE_DICTIONARY: 4,
michael@0 158 TYPE_INT64: 5,
michael@0 159
michael@0 160 /**
michael@0 161 * Get the type in which the given property list object is represented.
michael@0 162 * Check the header for the mapping between the TYPE* constants to js types
michael@0 163 * and objects.
michael@0 164 *
michael@0 165 * @return one of the TYPE_* constants listed above.
michael@0 166 * @note this method is merely for convenience. It has no magic to detect
michael@0 167 * that aObject is indeed a property list object created by this module.
michael@0 168 */
michael@0 169 getObjectType: function PLU_getObjectType(aObject) {
michael@0 170 if (aObject === null || typeof(aObject) != "object")
michael@0 171 return this.TYPE_PRIMITIVE;
michael@0 172
michael@0 173 // Given current usage, we could assume that aObject was created in the
michael@0 174 // scope of this module, but in future, this util may be used as part of
michael@0 175 // serializing js objects to a property list - in which case the object
michael@0 176 // would most likely be created in the caller's scope.
michael@0 177 let global = Cu.getGlobalForObject(aObject);
michael@0 178
michael@0 179 if (global.Dict && aObject instanceof global.Dict)
michael@0 180 return this.TYPE_DICTIONARY;
michael@0 181 if (Array.isArray(aObject))
michael@0 182 return this.TYPE_ARRAY;
michael@0 183 if (aObject instanceof global.Date)
michael@0 184 return this.TYPE_DATE;
michael@0 185 if (aObject instanceof global.Uint8Array)
michael@0 186 return this.TYPE_UINT8_ARRAY;
michael@0 187 if (aObject instanceof global.String && "__INT_64_WRAPPER__" in aObject)
michael@0 188 return this.TYPE_INT64;
michael@0 189
michael@0 190 throw new Error("aObject is not as a property list object.");
michael@0 191 },
michael@0 192
michael@0 193 /**
michael@0 194 * Wraps a 64-bit stored in the form of a string primitive as a String object,
michael@0 195 * which we can later distiguish from regular string values.
michael@0 196 * @param aPrimitive
michael@0 197 * a number in the form of either a primitive string or a primitive number.
michael@0 198 * @return a String wrapper around aNumberStr that can later be identified
michael@0 199 * as holding 64-bit number using getObjectType.
michael@0 200 */
michael@0 201 wrapInt64: function PLU_wrapInt64(aPrimitive) {
michael@0 202 if (typeof(aPrimitive) != "string" && typeof(aPrimitive) != "number")
michael@0 203 throw new Error("aPrimitive should be a string primitive");
michael@0 204
michael@0 205 let wrapped = new String(aPrimitive);
michael@0 206 Object.defineProperty(wrapped, "__INT_64_WRAPPER__", { value: true });
michael@0 207 return wrapped;
michael@0 208 }
michael@0 209 });
michael@0 210
michael@0 211 /**
michael@0 212 * Here's the base structure of binary-format property lists.
michael@0 213 * 1) Header - magic number
michael@0 214 * - 6 bytes - "bplist"
michael@0 215 * - 2 bytes - version number. This implementation only supports version 00.
michael@0 216 * 2) Objects Table
michael@0 217 * Variable-sized objects, see _readObject for how various types of objects
michael@0 218 * are constructed.
michael@0 219 * 3) Offsets Table
michael@0 220 * The offset of each object in the objects table. The integer size is
michael@0 221 * specified in the trailer.
michael@0 222 * 4) Trailer
michael@0 223 * - 6 unused bytes
michael@0 224 * - 1 byte: the size of integers in the offsets table
michael@0 225 * - 1 byte: the size of object references for arrays, sets and
michael@0 226 * dictionaries.
michael@0 227 * - 8 bytes: the number of objects in the objects table
michael@0 228 * - 8 bytes: the index of the root object's offset in the offsets table.
michael@0 229 * - 8 bytes: the offset of the offsets table.
michael@0 230 *
michael@0 231 * Note: all integers are stored in big-endian form.
michael@0 232 */
michael@0 233
michael@0 234 /**
michael@0 235 * Reader for binary-format property lists.
michael@0 236 *
michael@0 237 * @param aBuffer
michael@0 238 * ArrayBuffer object from which the binary plist should be read.
michael@0 239 */
michael@0 240 function BinaryPropertyListReader(aBuffer) {
michael@0 241 this._buffer = aBuffer;
michael@0 242
michael@0 243 const JS_MAX_INT = Math.pow(2,53);
michael@0 244 this._JS_MAX_INT_SIGNED = ctypes.Int64(JS_MAX_INT);
michael@0 245 this._JS_MAX_INT_UNSIGNED = ctypes.UInt64(JS_MAX_INT);
michael@0 246 this._JS_MIN_INT = ctypes.Int64(-JS_MAX_INT);
michael@0 247
michael@0 248 try {
michael@0 249 this._readTrailerInfo();
michael@0 250 this._readObjectsOffsets();
michael@0 251 }
michael@0 252 catch(ex) {
michael@0 253 throw new Error("Could not read aBuffer as a binary property list");
michael@0 254 }
michael@0 255 this._objects = [];
michael@0 256 }
michael@0 257
michael@0 258 BinaryPropertyListReader.prototype = {
michael@0 259 /**
michael@0 260 * Checks if the given ArrayBuffer can be read as a binary property list.
michael@0 261 * It can be called on the prototype.
michael@0 262 */
michael@0 263 canProcess: function BPLR_canProcess(aBuffer)
michael@0 264 [String.fromCharCode(c) for each (c in new Uint8Array(aBuffer, 0, 8))].
michael@0 265 join("") == "bplist00",
michael@0 266
michael@0 267 get root() this._readObject(this._rootObjectIndex),
michael@0 268
michael@0 269 _readTrailerInfo: function BPLR__readTrailer() {
michael@0 270 // The first 6 bytes of the 32-bytes trailer are unused
michael@0 271 let trailerOffset = this._buffer.byteLength - 26;
michael@0 272 [this._offsetTableIntegerSize, this._objectRefSize] =
michael@0 273 this._readUnsignedInts(trailerOffset, 1, 2);
michael@0 274
michael@0 275 [this._numberOfObjects, this._rootObjectIndex, this._offsetTableOffset] =
michael@0 276 this._readUnsignedInts(trailerOffset + 2, 8, 3);
michael@0 277 },
michael@0 278
michael@0 279 _readObjectsOffsets: function BPLR__readObjectsOffsets() {
michael@0 280 this._offsetTable = this._readUnsignedInts(this._offsetTableOffset,
michael@0 281 this._offsetTableIntegerSize,
michael@0 282 this._numberOfObjects);
michael@0 283 },
michael@0 284
michael@0 285 // TODO: This should be removed once DataView is implemented (Bug 575688).
michael@0 286 _swapForBigEndian:
michael@0 287 function BPLR__swapForBigEndian(aByteOffset, aIntSize, aNumberOfInts) {
michael@0 288 let bytesCount = aIntSize * aNumberOfInts;
michael@0 289 let bytes = new Uint8Array(this._buffer, aByteOffset, bytesCount);
michael@0 290 let swapped = new Uint8Array(bytesCount);
michael@0 291 for (let i = 0; i < aNumberOfInts; i++) {
michael@0 292 for (let j = 0; j < aIntSize; j++) {
michael@0 293 swapped[(i * aIntSize) + j] = bytes[(i * aIntSize) + (aIntSize - 1 - j)];
michael@0 294 }
michael@0 295 }
michael@0 296 return swapped;
michael@0 297 },
michael@0 298
michael@0 299 _readSignedInt64: function BPLR__readSignedInt64(aByteOffset) {
michael@0 300 let swapped = this._swapForBigEndian(aByteOffset, 8, 1);
michael@0 301 let lo = new Uint32Array(swapped.buffer, 0, 1)[0];
michael@0 302 let hi = new Int32Array(swapped.buffer, 4, 1)[0];
michael@0 303 let int64 = ctypes.Int64.join(hi, lo);
michael@0 304 if (ctypes.Int64.compare(int64, this._JS_MAX_INT_SIGNED) == 1 ||
michael@0 305 ctypes.Int64.compare(int64, this._JS_MIN_INT) == -1)
michael@0 306 return PropertyListUtils.wrapInt64(int64.toString());
michael@0 307
michael@0 308 return parseInt(int64.toString(), 10);
michael@0 309 },
michael@0 310
michael@0 311 _readReal: function BPLR__readReal(aByteOffset, aRealSize) {
michael@0 312 let swapped = this._swapForBigEndian(aByteOffset, aRealSize, 1);
michael@0 313 if (aRealSize == 4)
michael@0 314 return new Float32Array(swapped.buffer, 0, 1)[0];
michael@0 315 if (aRealSize == 8)
michael@0 316 return new Float64Array(swapped.buffer, 0, 1)[0];
michael@0 317
michael@0 318 throw new Error("Unsupported real size: " + aRealSize);
michael@0 319 },
michael@0 320
michael@0 321 OBJECT_TYPE_BITS: {
michael@0 322 SIMPLE: parseInt("0000", 2),
michael@0 323 INTEGER: parseInt("0001", 2),
michael@0 324 REAL: parseInt("0010", 2),
michael@0 325 DATE: parseInt("0011", 2),
michael@0 326 DATA: parseInt("0100", 2),
michael@0 327 ASCII_STRING: parseInt("0101", 2),
michael@0 328 UNICODE_STRING: parseInt("0110", 2),
michael@0 329 UID: parseInt("1000", 2),
michael@0 330 ARRAY: parseInt("1010", 2),
michael@0 331 SET: parseInt("1100", 2),
michael@0 332 DICTIONARY: parseInt("1101", 2)
michael@0 333 },
michael@0 334
michael@0 335 ADDITIONAL_INFO_BITS: {
michael@0 336 // Applies to OBJECT_TYPE_BITS.SIMPLE
michael@0 337 NULL: parseInt("0000", 2),
michael@0 338 FALSE: parseInt("1000", 2),
michael@0 339 TRUE: parseInt("1001", 2),
michael@0 340 FILL_BYTE: parseInt("1111", 2),
michael@0 341 // Applies to OBJECT_TYPE_BITS.DATE
michael@0 342 DATE: parseInt("0011", 2),
michael@0 343 // Applies to OBJECT_TYPE_BITS.DATA, ASCII_STRING, UNICODE_STRING, ARRAY,
michael@0 344 // SET and DICTIONARY.
michael@0 345 LENGTH_INT_SIZE_FOLLOWS: parseInt("1111", 2)
michael@0 346 },
michael@0 347
michael@0 348 /**
michael@0 349 * Returns an object descriptor in the form of two integers: object type and
michael@0 350 * additional info.
michael@0 351 *
michael@0 352 * @param aByteOffset
michael@0 353 * the descriptor's offset.
michael@0 354 * @return [objType, additionalInfo] - the object type and additional info.
michael@0 355 * @see OBJECT_TYPE_BITS and ADDITIONAL_INFO_BITS
michael@0 356 */
michael@0 357 _readObjectDescriptor: function BPLR__readObjectDescriptor(aByteOffset) {
michael@0 358 // The first four bits hold the object type. For some types, additional
michael@0 359 // info is held in the other 4 bits.
michael@0 360 let byte = this._readUnsignedInts(aByteOffset, 1, 1)[0];
michael@0 361 return [(byte & 0xF0) >> 4, byte & 0x0F];
michael@0 362 },
michael@0 363
michael@0 364 _readDate: function BPLR__readDate(aByteOffset) {
michael@0 365 // That's the reference date of NSDate.
michael@0 366 let date = new Date("1 January 2001, GMT");
michael@0 367
michael@0 368 // NSDate values are float values, but setSeconds takes an integer.
michael@0 369 date.setMilliseconds(this._readReal(aByteOffset, 8) * 1000);
michael@0 370 return date;
michael@0 371 },
michael@0 372
michael@0 373 /**
michael@0 374 * Reads a portion of the buffer as a string.
michael@0 375 *
michael@0 376 * @param aByteOffset
michael@0 377 * The offset in the buffer at which the string starts
michael@0 378 * @param aNumberOfChars
michael@0 379 * The length of the string to be read (that is the number of
michael@0 380 * characters, not bytes).
michael@0 381 * @param aUnicode
michael@0 382 * Whether or not it is a unicode string.
michael@0 383 * @return the string read.
michael@0 384 *
michael@0 385 * @note this is tested to work well with unicode surrogate pairs. Because
michael@0 386 * all unicode characters are read as 2-byte integers, unicode surrogate
michael@0 387 * pairs are read from the buffer in the form of two integers, as required
michael@0 388 * by String.fromCharCode.
michael@0 389 */
michael@0 390 _readString:
michael@0 391 function BPLR__readString(aByteOffset, aNumberOfChars, aUnicode) {
michael@0 392 let codes = this._readUnsignedInts(aByteOffset, aUnicode ? 2 : 1,
michael@0 393 aNumberOfChars);
michael@0 394 return [String.fromCharCode(c) for each (c in codes)].join("");
michael@0 395 },
michael@0 396
michael@0 397 /**
michael@0 398 * Reads an array of unsigned integers from the buffer. Integers larger than
michael@0 399 * one byte are read in big endian form.
michael@0 400 *
michael@0 401 * @param aByteOffset
michael@0 402 * The offset in the buffer at which the array starts.
michael@0 403 * @param aIntSize
michael@0 404 * The size of each int in the array.
michael@0 405 * @param aLength
michael@0 406 * The number of ints in the array.
michael@0 407 * @param [optional] aBigIntAllowed (default: false)
michael@0 408 * Whether or not to accept integers which outbounds JS limits for
michael@0 409 * numbers (±2^53) in the form of a String.
michael@0 410 * @return an array of integers (number primitive and/or Strings for large
michael@0 411 * numbers (see header)).
michael@0 412 * @throws if aBigIntAllowed is false and one of the integers in the array
michael@0 413 * cannot be represented by a primitive js number.
michael@0 414 */
michael@0 415 _readUnsignedInts:
michael@0 416 function BPLR__readUnsignedInts(aByteOffset, aIntSize, aLength, aBigIntAllowed) {
michael@0 417 if (aIntSize == 1)
michael@0 418 return new Uint8Array(this._buffer, aByteOffset, aLength);
michael@0 419
michael@0 420 // There are two reasons for the complexity you see here:
michael@0 421 // (1) 64-bit integers - For which we use ctypes. When possible, the
michael@0 422 // number is converted back to js's default float-64 type.
michael@0 423 // (2) The DataView object for ArrayBuffer, which takes care of swaping
michael@0 424 // bytes, is not yet implemented (bug 575688).
michael@0 425 let swapped = this._swapForBigEndian(aByteOffset, aIntSize, aLength);
michael@0 426 if (aIntSize == 2)
michael@0 427 return new Uint16Array(swapped.buffer);
michael@0 428 if (aIntSize == 4)
michael@0 429 return new Uint32Array(swapped.buffer);
michael@0 430 if (aIntSize == 8) {
michael@0 431 let intsArray = [];
michael@0 432 let lo_hi_view = new Uint32Array(swapped.buffer);
michael@0 433 for (let i = 0; i < lo_hi_view.length; i += 2) {
michael@0 434 let [lo, hi] = [lo_hi_view[i], lo_hi_view[i+1]];
michael@0 435 let uint64 = ctypes.UInt64.join(hi, lo);
michael@0 436 if (ctypes.UInt64.compare(uint64, this._JS_MAX_INT_UNSIGNED) == 1) {
michael@0 437 if (aBigIntAllowed === true)
michael@0 438 intsArray.push(PropertyListUtils.wrapInt64(uint64.toString()));
michael@0 439 else
michael@0 440 throw new Error("Integer too big to be read as float 64");
michael@0 441 }
michael@0 442 else {
michael@0 443 intsArray.push(parseInt(uint64.toString(), 10));
michael@0 444 }
michael@0 445 }
michael@0 446 return intsArray;
michael@0 447 }
michael@0 448 throw new Error("Unsupported size: " + aIntSize);
michael@0 449 },
michael@0 450
michael@0 451 /**
michael@0 452 * Reads from the buffer the data object-count and the offset at which the
michael@0 453 * first object starts.
michael@0 454 *
michael@0 455 * @param aObjectOffset
michael@0 456 * the object's offset.
michael@0 457 * @return [offset, count] - the offset in the buffer at which the first
michael@0 458 * object in data starts, and the number of objects.
michael@0 459 */
michael@0 460 _readDataOffsetAndCount:
michael@0 461 function BPLR__readDataOffsetAndCount(aObjectOffset) {
michael@0 462 // The length of some objects in the data can be stored in two ways:
michael@0 463 // * If it is small enough, it is stored in the second four bits of the
michael@0 464 // object descriptors.
michael@0 465 // * Otherwise, those bits are set to 1111, indicating that the next byte
michael@0 466 // consists of the integer size of the data-length (also stored in the form
michael@0 467 // of an object descriptor). The length follows this byte.
michael@0 468 let [, maybeLength] = this._readObjectDescriptor(aObjectOffset);
michael@0 469 if (maybeLength != this.ADDITIONAL_INFO_BITS.LENGTH_INT_SIZE_FOLLOWS)
michael@0 470 return [aObjectOffset + 1, maybeLength];
michael@0 471
michael@0 472 let [, intSizeInfo] = this._readObjectDescriptor(aObjectOffset + 1);
michael@0 473
michael@0 474 // The int size is 2^intSizeInfo.
michael@0 475 let intSize = Math.pow(2, intSizeInfo);
michael@0 476 let dataLength = this._readUnsignedInts(aObjectOffset + 2, intSize, 1)[0];
michael@0 477 return [aObjectOffset + 2 + intSize, dataLength];
michael@0 478 },
michael@0 479
michael@0 480 /**
michael@0 481 * Read array from the buffer and wrap it as a js array.
michael@0 482 * @param aObjectOffset
michael@0 483 * the offset in the buffer at which the array starts.
michael@0 484 * @param aNumberOfObjects
michael@0 485 * the number of objects in the array.
michael@0 486 * @return a js array.
michael@0 487 */
michael@0 488 _wrapArray: function BPLR__wrapArray(aObjectOffset, aNumberOfObjects) {
michael@0 489 let refs = this._readUnsignedInts(aObjectOffset,
michael@0 490 this._objectRefSize,
michael@0 491 aNumberOfObjects);
michael@0 492
michael@0 493 let array = new Array(aNumberOfObjects);
michael@0 494 let readObjectBound = this._readObject.bind(this);
michael@0 495
michael@0 496 // Each index in the returned array is a lazy getter for its object.
michael@0 497 Array.prototype.forEach.call(refs, function(ref, objIndex) {
michael@0 498 Object.defineProperty(array, objIndex, {
michael@0 499 get: function() {
michael@0 500 delete array[objIndex];
michael@0 501 return array[objIndex] = readObjectBound(ref);
michael@0 502 },
michael@0 503 configurable: true,
michael@0 504 enumerable: true
michael@0 505 });
michael@0 506 }, this);
michael@0 507 return array;
michael@0 508 },
michael@0 509
michael@0 510 /**
michael@0 511 * Reads dictionary from the buffer and wraps it as a Dict object (as defined
michael@0 512 * in Dict.jsm).
michael@0 513 * @param aObjectOffset
michael@0 514 * the offset in the buffer at which the dictionary starts
michael@0 515 * @param aNumberOfObjects
michael@0 516 * the number of keys in the dictionary
michael@0 517 * @return Dict.jsm-style dictionary.
michael@0 518 */
michael@0 519 _wrapDictionary: function(aObjectOffset, aNumberOfObjects) {
michael@0 520 // A dictionary in the binary format is stored as a list of references to
michael@0 521 // key-objects, followed by a list of references to the value-objects for
michael@0 522 // those keys. The size of each list is aNumberOfObjects * this._objectRefSize.
michael@0 523 let dict = new Dict();
michael@0 524 if (aNumberOfObjects == 0)
michael@0 525 return dict;
michael@0 526
michael@0 527 let keyObjsRefs = this._readUnsignedInts(aObjectOffset, this._objectRefSize,
michael@0 528 aNumberOfObjects);
michael@0 529 let valObjsRefs =
michael@0 530 this._readUnsignedInts(aObjectOffset + aNumberOfObjects * this._objectRefSize,
michael@0 531 this._objectRefSize, aNumberOfObjects);
michael@0 532 for (let i = 0; i < aNumberOfObjects; i++) {
michael@0 533 let key = this._readObject(keyObjsRefs[i]);
michael@0 534 let readBound = this._readObject.bind(this, valObjsRefs[i]);
michael@0 535 dict.setAsLazyGetter(key, readBound);
michael@0 536 }
michael@0 537 return dict;
michael@0 538 },
michael@0 539
michael@0 540 /**
michael@0 541 * Reads an object at the spcified index in the object table
michael@0 542 * @param aObjectIndex
michael@0 543 * index at the object table
michael@0 544 * @return the property list object at the given index.
michael@0 545 */
michael@0 546 _readObject: function BPLR__readObject(aObjectIndex) {
michael@0 547 // If the object was previously read, return the cached object.
michael@0 548 if (this._objects[aObjectIndex] !== undefined)
michael@0 549 return this._objects[aObjectIndex];
michael@0 550
michael@0 551 let objOffset = this._offsetTable[aObjectIndex];
michael@0 552 let [objType, additionalInfo] = this._readObjectDescriptor(objOffset);
michael@0 553 let value;
michael@0 554 switch (objType) {
michael@0 555 case this.OBJECT_TYPE_BITS.SIMPLE: {
michael@0 556 switch (additionalInfo) {
michael@0 557 case this.ADDITIONAL_INFO_BITS.NULL:
michael@0 558 value = null;
michael@0 559 break;
michael@0 560 case this.ADDITIONAL_INFO_BITS.FILL_BYTE:
michael@0 561 value = undefined;
michael@0 562 break;
michael@0 563 case this.ADDITIONAL_INFO_BITS.FALSE:
michael@0 564 value = false;
michael@0 565 break;
michael@0 566 case this.ADDITIONAL_INFO_BITS.TRUE:
michael@0 567 value = true;
michael@0 568 break;
michael@0 569 default:
michael@0 570 throw new Error("Unexpected value!");
michael@0 571 }
michael@0 572 break;
michael@0 573 }
michael@0 574
michael@0 575 case this.OBJECT_TYPE_BITS.INTEGER: {
michael@0 576 // The integer is sized 2^additionalInfo.
michael@0 577 let intSize = Math.pow(2, additionalInfo);
michael@0 578
michael@0 579 // For objects, 64-bit integers are always signed. Negative integers
michael@0 580 // are always represented by a 64-bit integer.
michael@0 581 if (intSize == 8)
michael@0 582 value = this._readSignedInt64(objOffset + 1);
michael@0 583 else
michael@0 584 value = this._readUnsignedInts(objOffset + 1, intSize, 1, true)[0];
michael@0 585 break;
michael@0 586 }
michael@0 587
michael@0 588 case this.OBJECT_TYPE_BITS.REAL: {
michael@0 589 // The real is sized 2^additionalInfo.
michael@0 590 value = this._readReal(objOffset + 1, Math.pow(2, additionalInfo));
michael@0 591 break;
michael@0 592 }
michael@0 593
michael@0 594 case this.OBJECT_TYPE_BITS.DATE: {
michael@0 595 if (additionalInfo != this.ADDITIONAL_INFO_BITS.DATE)
michael@0 596 throw new Error("Unexpected value");
michael@0 597
michael@0 598 value = this._readDate(objOffset + 1);
michael@0 599 break;
michael@0 600 }
michael@0 601
michael@0 602 case this.OBJECT_TYPE_BITS.DATA: {
michael@0 603 let [offset, bytesCount] = this._readDataOffsetAndCount(objOffset);
michael@0 604 value = this._readUnsignedInts(offset, 1, bytesCount);
michael@0 605 break;
michael@0 606 }
michael@0 607
michael@0 608 case this.OBJECT_TYPE_BITS.ASCII_STRING: {
michael@0 609 let [offset, charsCount] = this._readDataOffsetAndCount(objOffset);
michael@0 610 value = this._readString(offset, charsCount, false);
michael@0 611 break;
michael@0 612 }
michael@0 613
michael@0 614 case this.OBJECT_TYPE_BITS.UNICODE_STRING: {
michael@0 615 let [offset, unicharsCount] = this._readDataOffsetAndCount(objOffset);
michael@0 616 value = this._readString(offset, unicharsCount, true);
michael@0 617 break;
michael@0 618 }
michael@0 619
michael@0 620 case this.OBJECT_TYPE_BITS.UID: {
michael@0 621 // UIDs are only used in Keyed Archives, which are not yet supported.
michael@0 622 throw new Error("Keyed Archives are not supported");
michael@0 623 }
michael@0 624
michael@0 625 case this.OBJECT_TYPE_BITS.ARRAY:
michael@0 626 case this.OBJECT_TYPE_BITS.SET: {
michael@0 627 // Note: For now, we fallback to handle sets the same way we handle
michael@0 628 // arrays. See comments in the header of this file.
michael@0 629
michael@0 630 // The bytes following the count are references to objects (indices).
michael@0 631 // Each reference is an unsigned int with size=this._objectRefSize.
michael@0 632 let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset);
michael@0 633 value = this._wrapArray(offset, objectsCount);
michael@0 634 break;
michael@0 635 }
michael@0 636
michael@0 637 case this.OBJECT_TYPE_BITS.DICTIONARY: {
michael@0 638 let [offset, objectsCount] = this._readDataOffsetAndCount(objOffset);
michael@0 639 value = this._wrapDictionary(offset, objectsCount);
michael@0 640 break;
michael@0 641 }
michael@0 642
michael@0 643 default: {
michael@0 644 throw new Error("Unknown object type: " + objType);
michael@0 645 }
michael@0 646 }
michael@0 647
michael@0 648 return this._objects[aObjectIndex] = value;
michael@0 649 }
michael@0 650 };
michael@0 651
michael@0 652 /**
michael@0 653 * Reader for XML property lists.
michael@0 654 *
michael@0 655 * @param aDOMDoc
michael@0 656 * the DOM document to be read as a property list.
michael@0 657 */
michael@0 658 function XMLPropertyListReader(aDOMDoc) {
michael@0 659 let docElt = aDOMDoc.documentElement;
michael@0 660 if (!docElt || docElt.localName != "plist" || !docElt.firstElementChild)
michael@0 661 throw new Error("aDoc is not a property list document");
michael@0 662
michael@0 663 this._plistRootElement = docElt.firstElementChild;
michael@0 664 }
michael@0 665
michael@0 666 XMLPropertyListReader.prototype = {
michael@0 667 get root() this._readObject(this._plistRootElement),
michael@0 668
michael@0 669 /**
michael@0 670 * Convert a dom element to a property list object.
michael@0 671 * @param aDOMElt
michael@0 672 * a dom element in a xml tree of a property list.
michael@0 673 * @return a js object representing the property list object.
michael@0 674 */
michael@0 675 _readObject: function XPLR__readObject(aDOMElt) {
michael@0 676 switch (aDOMElt.localName) {
michael@0 677 case "true":
michael@0 678 return true;
michael@0 679 case "false":
michael@0 680 return false;
michael@0 681 case "string":
michael@0 682 case "key":
michael@0 683 return aDOMElt.textContent;
michael@0 684 case "integer":
michael@0 685 return this._readInteger(aDOMElt);
michael@0 686 case "real": {
michael@0 687 let number = parseFloat(aDOMElt.textContent.trim());
michael@0 688 if (isNaN(number))
michael@0 689 throw "Could not parse float value";
michael@0 690 return number;
michael@0 691 }
michael@0 692 case "date":
michael@0 693 return new Date(aDOMElt.textContent);
michael@0 694 case "data":
michael@0 695 // Strip spaces and new lines.
michael@0 696 let base64str = aDOMElt.textContent.replace(/\s*/g, "");
michael@0 697 let decoded = atob(base64str);
michael@0 698 return new Uint8Array([decoded.charCodeAt(i) for (i in decoded)]);
michael@0 699 case "dict":
michael@0 700 return this._wrapDictionary(aDOMElt);
michael@0 701 case "array":
michael@0 702 return this._wrapArray(aDOMElt);
michael@0 703 default:
michael@0 704 throw new Error("Unexpected tagname");
michael@0 705 }
michael@0 706 },
michael@0 707
michael@0 708 _readInteger: function XPLR__readInteger(aDOMElt) {
michael@0 709 // The integer may outbound js's max/min integer value. We recognize this
michael@0 710 // case by comparing the parsed number to the original string value.
michael@0 711 // In case of an outbound, we fallback to return the number as a string.
michael@0 712 let numberAsString = aDOMElt.textContent.toString();
michael@0 713 let parsedNumber = parseInt(numberAsString, 10);
michael@0 714 if (isNaN(parsedNumber))
michael@0 715 throw new Error("Could not parse integer value");
michael@0 716
michael@0 717 if (parsedNumber.toString() == numberAsString)
michael@0 718 return parsedNumber;
michael@0 719
michael@0 720 return PropertyListUtils.wrapInt64(numberAsString);
michael@0 721 },
michael@0 722
michael@0 723 _wrapDictionary: function XPLR__wrapDictionary(aDOMElt) {
michael@0 724 // <dict>
michael@0 725 // <key>my true bool</key>
michael@0 726 // <true/>
michael@0 727 // <key>my string key</key>
michael@0 728 // <string>My String Key</string>
michael@0 729 // </dict>
michael@0 730 if (aDOMElt.children.length % 2 != 0)
michael@0 731 throw new Error("Invalid dictionary");
michael@0 732 let dict = new Dict();
michael@0 733 for (let i = 0; i < aDOMElt.children.length; i += 2) {
michael@0 734 let keyElem = aDOMElt.children[i];
michael@0 735 let valElem = aDOMElt.children[i + 1];
michael@0 736
michael@0 737 if (keyElem.localName != "key")
michael@0 738 throw new Error("Invalid dictionary");
michael@0 739
michael@0 740 let keyName = this._readObject(keyElem);
michael@0 741 let readBound = this._readObject.bind(this, valElem);
michael@0 742 dict.setAsLazyGetter(keyName, readBound);
michael@0 743 }
michael@0 744 return dict;
michael@0 745 },
michael@0 746
michael@0 747 _wrapArray: function XPLR__wrapArray(aDOMElt) {
michael@0 748 // <array>
michael@0 749 // <string>...</string>
michael@0 750 // <integer></integer>
michael@0 751 // <dict>
michael@0 752 // ....
michael@0 753 // </dict>
michael@0 754 // </array>
michael@0 755
michael@0 756 // Each element in the array is a lazy getter for its property list object.
michael@0 757 let array = [];
michael@0 758 let readObjectBound = this._readObject.bind(this);
michael@0 759 Array.prototype.forEach.call(aDOMElt.children, function(elem, elemIndex) {
michael@0 760 Object.defineProperty(array, elemIndex, {
michael@0 761 get: function() {
michael@0 762 delete array[elemIndex];
michael@0 763 return array[elemIndex] = readObjectBound(elem);
michael@0 764 },
michael@0 765 configurable: true,
michael@0 766 enumerable: true
michael@0 767 });
michael@0 768 });
michael@0 769 return array;
michael@0 770 }
michael@0 771 };

mercurial