toolkit/modules/PropertyListUtils.jsm

changeset 0
6474c204b198
     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 +};

mercurial