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