/** * Utility functions for web applications. * * @author Dave Longley * * Copyright (c) 2010-2014 Digital Bazaar, Inc. */ (function() { /* ########## Begin module implementation ########## */ function initModule(forge) { /* Utilities API */ var util = forge.util = forge.util || {}; // define setImmediate and nextTick if(typeof process === 'undefined' || !process.nextTick) { if(typeof setImmediate === 'function') { util.setImmediate = setImmediate; util.nextTick = function(callback) { return setImmediate(callback); }; } else { util.setImmediate = function(callback) { setTimeout(callback, 0); }; util.nextTick = util.setImmediate; } } else { util.nextTick = process.nextTick; if(typeof setImmediate === 'function') { util.setImmediate = setImmediate; } else { util.setImmediate = util.nextTick; } } // define isArray util.isArray = Array.isArray || function(x) { return Object.prototype.toString.call(x) === '[object Array]'; }; // define isArrayBuffer util.isArrayBuffer = function(x) { return typeof ArrayBuffer !== 'undefined' && x instanceof ArrayBuffer; }; // define isArrayBufferView var _arrayBufferViews = []; if(typeof DataView !== 'undefined') { _arrayBufferViews.push(DataView); } if(typeof Int8Array !== 'undefined') { _arrayBufferViews.push(Int8Array); } if(typeof Uint8Array !== 'undefined') { _arrayBufferViews.push(Uint8Array); } if(typeof Uint8ClampedArray !== 'undefined') { _arrayBufferViews.push(Uint8ClampedArray); } if(typeof Int16Array !== 'undefined') { _arrayBufferViews.push(Int16Array); } if(typeof Uint16Array !== 'undefined') { _arrayBufferViews.push(Uint16Array); } if(typeof Int32Array !== 'undefined') { _arrayBufferViews.push(Int32Array); } if(typeof Uint32Array !== 'undefined') { _arrayBufferViews.push(Uint32Array); } if(typeof Float32Array !== 'undefined') { _arrayBufferViews.push(Float32Array); } if(typeof Float64Array !== 'undefined') { _arrayBufferViews.push(Float64Array); } util.isArrayBufferView = function(x) { for(var i = 0; i < _arrayBufferViews.length; ++i) { if(x instanceof _arrayBufferViews[i]) { return true; } } return false; }; // TODO: set ByteBuffer to best available backing util.ByteBuffer = ByteStringBuffer; /** * Concatenates an array of ByteBuffers into a single ByteBuffer (this performs * a copy operation on the ByteBuffers in the list). * * @param list the list of ByteBuffers to concatenate. * * @return the new buffer. */ util.ByteBuffer.concat = function(list) { var rval = new util.ByteBuffer(); for(var i = 0; i < list.length; ++i) { rval.putBytes(list[i].bytes()); } return rval; }; /** Buffer w/BinaryString backing */ // FIXME: add method to convert an array of integers to a buffer /** * Constructor for a binary string backed byte buffer. * * @param [b] the bytes to wrap (either encoded as string, one byte per * character, or as an ArrayBuffer or Typed Array). * @param [encoding|options] the encoding for the first parameter or the * options to use: * [encoding] the encoding ('binary', 'utf8', 'utf16', 'hex') for the * first parameter, if it is a string (default: 'binary'). */ function ByteStringBuffer(b, options) { // FIXME: if a string is given, force an encoding to be given as well // TODO: update to match DataBuffer API if(typeof options === 'string') { options = {encoding: options}; } options = options || {}; // the data in this buffer this.data = ''; // the pointer for reading from this buffer this.read = 0; if(typeof b === 'string') { // FIXME: copy implementation from DataBuffer if(options.encoding === 'utf8') { b = util.encodeUtf8(b); } else if(options.encoding === 'hex') { b = util.hexToBytes(b); } else if(options.encoding === 'base64') { b = util.decode64(b); } this.data = b; } else if(util.isArrayBuffer(b) || util.isArrayBufferView(b)) { // convert native buffer to forge buffer // FIXME: support native buffers internally instead var arr = new Uint8Array(b); try { this.data = String.fromCharCode.apply(null, arr); } catch(e) { for(var i = 0; i < arr.length; ++i) { this.putByte(arr[i]); } } } else if(util.isArray(b)) { // assume 'b' is an array of integers for(var i = 0; i < b.length; ++i) { this.putByte(b[i]); } } else if(b instanceof ByteStringBuffer || (typeof b === 'object' && typeof b.data === 'string' && typeof b.read === 'number')) { // copy existing buffer this.data = b.data; this.read = b.read; } } util.ByteStringBuffer = ByteStringBuffer; /** * Gets the number of bytes in this buffer. * * @return the number of bytes in this buffer. */ util.ByteStringBuffer.prototype.length = function() { return this.data.length - this.read; }; /** * Gets whether or not this buffer is empty. * * @return true if this buffer is empty, false if not. */ util.ByteStringBuffer.prototype.isEmpty = function() { return this.length() <= 0; }; /** * Puts a byte in this buffer. * * @param b the byte to put. * * @return this buffer. */ util.ByteStringBuffer.prototype.putByte = function(b) { this.data += String.fromCharCode(b); return this; }; /** * Puts a byte in this buffer N times. * * @param b the byte to put. * @param n the number of bytes of value b to put. * * @return this buffer. */ util.ByteStringBuffer.prototype.fillWithByte = function(b, n) { b = String.fromCharCode(b); var d = this.data; while(n > 0) { if(n & 1) { d += b; } n >>>= 1; if(n > 0) { b += b; } } this.data = d; return this; }; /** * Puts bytes in this buffer. * * @param bytes the bytes (as a UTF-8 encoded string) to put. * * @return this buffer. */ util.ByteStringBuffer.prototype.putBytes = function(bytes) { // TODO: update to match DataBuffer API this.data += bytes; return this; }; /** * Puts a UTF-16 encoded string into this buffer. * * @param str the string to put. * * @return this buffer. */ util.ByteStringBuffer.prototype.putString = function(str) { this.data += util.encodeUtf8(str); return this; }; /** * Puts a 16-bit integer in this buffer in big-endian order. * * @param i the 16-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt16 = function(i) { this.data += String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i & 0xFF); return this; }; /** * Puts a 24-bit integer in this buffer in big-endian order. * * @param i the 24-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt24 = function(i) { this.data += String.fromCharCode(i >> 16 & 0xFF) + String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i & 0xFF); return this; }; /** * Puts a 32-bit integer in this buffer in big-endian order. * * @param i the 32-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt32 = function(i) { this.data += String.fromCharCode(i >> 24 & 0xFF) + String.fromCharCode(i >> 16 & 0xFF) + String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i & 0xFF); return this; }; /** * Puts a 16-bit integer in this buffer in little-endian order. * * @param i the 16-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt16Le = function(i) { this.data += String.fromCharCode(i & 0xFF) + String.fromCharCode(i >> 8 & 0xFF); return this; }; /** * Puts a 24-bit integer in this buffer in little-endian order. * * @param i the 24-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt24Le = function(i) { this.data += String.fromCharCode(i & 0xFF) + String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i >> 16 & 0xFF); return this; }; /** * Puts a 32-bit integer in this buffer in little-endian order. * * @param i the 32-bit integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt32Le = function(i) { this.data += String.fromCharCode(i & 0xFF) + String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i >> 16 & 0xFF) + String.fromCharCode(i >> 24 & 0xFF); return this; }; /** * Puts an n-bit integer in this buffer in big-endian order. * * @param i the n-bit integer. * @param n the number of bits in the integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putInt = function(i, n) { do { n -= 8; this.data += String.fromCharCode((i >> n) & 0xFF); } while(n > 0); return this; }; /** * Puts a signed n-bit integer in this buffer in big-endian order. Two's * complement representation is used. * * @param i the n-bit integer. * @param n the number of bits in the integer. * * @return this buffer. */ util.ByteStringBuffer.prototype.putSignedInt = function(i, n) { if(i < 0) { i += 2 << (n - 1); } return this.putInt(i, n); }; /** * Puts the given buffer into this buffer. * * @param buffer the buffer to put into this one. * * @return this buffer. */ util.ByteStringBuffer.prototype.putBuffer = function(buffer) { this.data += buffer.getBytes(); return this; }; /** * Gets a byte from this buffer and advances the read pointer by 1. * * @return the byte. */ util.ByteStringBuffer.prototype.getByte = function() { return this.data.charCodeAt(this.read++); }; /** * Gets a uint16 from this buffer in big-endian order and advances the read * pointer by 2. * * @return the uint16. */ util.ByteStringBuffer.prototype.getInt16 = function() { var rval = ( this.data.charCodeAt(this.read) << 8 ^ this.data.charCodeAt(this.read + 1)); this.read += 2; return rval; }; /** * Gets a uint24 from this buffer in big-endian order and advances the read * pointer by 3. * * @return the uint24. */ util.ByteStringBuffer.prototype.getInt24 = function() { var rval = ( this.data.charCodeAt(this.read) << 16 ^ this.data.charCodeAt(this.read + 1) << 8 ^ this.data.charCodeAt(this.read + 2)); this.read += 3; return rval; }; /** * Gets a uint32 from this buffer in big-endian order and advances the read * pointer by 4. * * @return the word. */ util.ByteStringBuffer.prototype.getInt32 = function() { var rval = ( this.data.charCodeAt(this.read) << 24 ^ this.data.charCodeAt(this.read + 1) << 16 ^ this.data.charCodeAt(this.read + 2) << 8 ^ this.data.charCodeAt(this.read + 3)); this.read += 4; return rval; }; /** * Gets a uint16 from this buffer in little-endian order and advances the read * pointer by 2. * * @return the uint16. */ util.ByteStringBuffer.prototype.getInt16Le = function() { var rval = ( this.data.charCodeAt(this.read) ^ this.data.charCodeAt(this.read + 1) << 8); this.read += 2; return rval; }; /** * Gets a uint24 from this buffer in little-endian order and advances the read * pointer by 3. * * @return the uint24. */ util.ByteStringBuffer.prototype.getInt24Le = function() { var rval = ( this.data.charCodeAt(this.read) ^ this.data.charCodeAt(this.read + 1) << 8 ^ this.data.charCodeAt(this.read + 2) << 16); this.read += 3; return rval; }; /** * Gets a uint32 from this buffer in little-endian order and advances the read * pointer by 4. * * @return the word. */ util.ByteStringBuffer.prototype.getInt32Le = function() { var rval = ( this.data.charCodeAt(this.read) ^ this.data.charCodeAt(this.read + 1) << 8 ^ this.data.charCodeAt(this.read + 2) << 16 ^ this.data.charCodeAt(this.read + 3) << 24); this.read += 4; return rval; }; /** * Gets an n-bit integer from this buffer in big-endian order and advances the * read pointer by n/8. * * @param n the number of bits in the integer. * * @return the integer. */ util.ByteStringBuffer.prototype.getInt = function(n) { var rval = 0; do { rval = (rval << 8) + this.data.charCodeAt(this.read++); n -= 8; } while(n > 0); return rval; }; /** * Gets a signed n-bit integer from this buffer in big-endian order, using * two's complement, and advances the read pointer by n/8. * * @param n the number of bits in the integer. * * @return the integer. */ util.ByteStringBuffer.prototype.getSignedInt = function(n) { var x = this.getInt(n); var max = 2 << (n - 2); if(x >= max) { x -= max << 1; } return x; }; /** * Reads bytes out into a UTF-8 string and clears them from the buffer. * * @param count the number of bytes to read, undefined or null for all. * * @return a UTF-8 string of bytes. */ util.ByteStringBuffer.prototype.getBytes = function(count) { var rval; if(count) { // read count bytes count = Math.min(this.length(), count); rval = this.data.slice(this.read, this.read + count); this.read += count; } else if(count === 0) { rval = ''; } else { // read all bytes, optimize to only copy when needed rval = (this.read === 0) ? this.data : this.data.slice(this.read); this.clear(); } return rval; }; /** * Gets a UTF-8 encoded string of the bytes from this buffer without modifying * the read pointer. * * @param count the number of bytes to get, omit to get all. * * @return a string full of UTF-8 encoded characters. */ util.ByteStringBuffer.prototype.bytes = function(count) { return (typeof(count) === 'undefined' ? this.data.slice(this.read) : this.data.slice(this.read, this.read + count)); }; /** * Gets a byte at the given index without modifying the read pointer. * * @param i the byte index. * * @return the byte. */ util.ByteStringBuffer.prototype.at = function(i) { return this.data.charCodeAt(this.read + i); }; /** * Puts a byte at the given index without modifying the read pointer. * * @param i the byte index. * @param b the byte to put. * * @return this buffer. */ util.ByteStringBuffer.prototype.setAt = function(i, b) { this.data = this.data.substr(0, this.read + i) + String.fromCharCode(b) + this.data.substr(this.read + i + 1); return this; }; /** * Gets the last byte without modifying the read pointer. * * @return the last byte. */ util.ByteStringBuffer.prototype.last = function() { return this.data.charCodeAt(this.data.length - 1); }; /** * Creates a copy of this buffer. * * @return the copy. */ // TODO: add .copyView to get a shallow copy // Note: need to be careful with semantics as the underlying byte array // can't actually be shared with this particular implementation (ByteString) util.ByteStringBuffer.prototype.copy = function() { var c = util.createBuffer(this.data); c.read = this.read; return c; }; /** * Compacts this buffer. * * @return this buffer. */ util.ByteStringBuffer.prototype.compact = function() { if(this.read > 0) { this.data = this.data.slice(this.read); this.read = 0; } return this; }; /** * Clears this buffer. * * @return this buffer. */ util.ByteStringBuffer.prototype.clear = function() { this.data = ''; this.read = 0; return this; }; /** * Shortens this buffer by triming bytes off of the end of this buffer. * * @param count the number of bytes to trim off. * * @return this buffer. */ util.ByteStringBuffer.prototype.truncate = function(count) { var len = Math.max(0, this.length() - count); this.data = this.data.substr(this.read, len); this.read = 0; return this; }; /** * Converts this buffer to a hexadecimal string. * * @return a hexadecimal string. */ util.ByteStringBuffer.prototype.toHex = function() { var rval = ''; for(var i = this.read; i < this.data.length; ++i) { var b = this.data.charCodeAt(i); if(b < 16) { rval += '0'; } rval += b.toString(16); } return rval; }; /** * Converts this buffer to a string, using the given encoding. The encoding * specifies how to interpret the bytes in this buffer as characters to * put in the string output. If no encoding is given, 'utf8' (UTF-8) is used. * * @param [encoding] the encoding to use: 'binary', 'utf8', 'utf16', 'hex', * 'base64' (default: 'utf8'). * @param [options] any special options supported by a particular encoding, * (eg: for base64, {maxline: 64}). * * @return a string representation of the bytes in this buffer. */ util.ByteStringBuffer.prototype.toString = function(encoding, options) { encoding = encoding || 'utf8'; options = options || {}; // encode to string if(encoding === 'binary' || encoding === 'raw') { return this.bytes(); } if(encoding === 'hex') { return this.toHex(); } if(encoding === 'base64') { if(options.maxline) { return util.encode64(this.bytes(), options.maxline); } else { return util.encode64(this.bytes()); } } // decode to text if(encoding === 'utf8') { return util.decodeUtf8(this.bytes()); } if(encoding === 'utf16') { throw new Error('Not implemented.'); } throw new Error('Invalid encoding: ' + encoding); }; /** End Buffer w/BinaryString backing */ /** Buffer w/UInt8Array backing */ /** * FIXME: Experimental. Do not use yet. * * Constructor for an ArrayBuffer-backed byte buffer. * * The buffer may be constructed from a string, an ArrayBuffer, DataView, or a * TypedArray. * * If a string is given, its encoding should be provided as an option, * otherwise it will default to 'binary'. A 'binary' string is encoded such * that each character is one byte in length and size. * * If an ArrayBuffer, DataView, or TypedArray is given, it will be used * *directly* without any copying. Note that, if a write to the buffer requires * more space, the buffer will allocate a new backing ArrayBuffer to * accommodate. The starting read and write offsets for the buffer may be * given as options. * * @param [b] the initial bytes for this buffer. * @param [encoding|options] the encoding for the first parameter or the * options to use: * [readOffset] the starting read offset to use (default: 0). * [writeOffset] the starting write offset to use (default: the * length of the first parameter). * [growSize] the minimum amount, in bytes, to grow the buffer by to * accommodate writes (default: 1024). * [encoding] the encoding ('binary', 'utf8', 'utf16', 'hex') for the * first parameter, if it is a string (default: 'binary'). */ function DataBuffer(b, options) { // FIXME: support 'b' as an array of integers representing byte values // FIXME: if a string is given, force an encoding to be given as well if(typeof options === 'string') { options = {encoding: options}; } options = options || {}; // pointers for read from/write to buffer this.read = options.readOffset || 0; this.growSize = options.growSize || 1024; var isArrayBuffer = util.isArrayBuffer(b); var isArrayBufferView = util.isArrayBufferView(b); if(isArrayBuffer || isArrayBufferView) { // use ArrayBuffer directly if(isArrayBuffer) { this.data = new DataView(b); } else { // TODO: adjust read/write offset based on the type of view // or specify that this must be done in the options ... that the // offsets are byte-based this.data = new DataView(b.buffer, b.byteOffset, b.byteLength); } this.write = ('writeOffset' in options ? options.writeOffset : 0); return; } // initialize to empty array buffer and add any given bytes using putBytes this.data = new DataView(new ArrayBuffer(0)); this.write = 0; if(b !== null && b !== undefined) { this.putBytes(b); } if('writeOffset' in options) { this.write = options.writeOffset; } } util.DataBuffer = DataBuffer; /** * Gets the number of bytes in this buffer. * * @return the number of bytes in this buffer. */ util.DataBuffer.prototype.length = function() { //debug("read: "+this.read); //debug("write: "+this.write); return this.data.byteLength - this.read; }; /** * Gets whether or not this buffer is empty. * * @return true if this buffer is empty, false if not. */ util.DataBuffer.prototype.isEmpty = function() { return this.length() <= 0; }; /** * Ensures this buffer has enough empty space to accommodate the given number * of bytes. An optional parameter may be given that indicates a minimum * amount to grow the buffer if necessary. If the parameter is not given, * the buffer will be grown by some previously-specified default amount * or heuristic. * * @param amount the number of bytes to accommodate. * @param [growSize] the minimum amount, in bytes, to grow the buffer by if * necessary. */ util.DataBuffer.prototype.accommodate = function(amount, growSize) { if(this.length() >= amount) { return this; } debugger; growSize = Math.max(growSize || this.growSize, amount); // grow buffer var src = new Uint8Array( this.data.buffer, this.data.byteOffset, this.data.byteLength); var dst = new Uint8Array(this.length() + growSize); dst.set(src); this.data = new DataView(dst.buffer); return this; }; /** * Puts a byte in this buffer. * * @param b the byte to put. * * @return this buffer. */ util.DataBuffer.prototype.putByte = function(b) { this.accommodate(1); this.data.setUint8(this.write++, b); return this; }; /** * Puts a byte in this buffer N times. * * @param b the byte to put. * @param n the number of bytes of value b to put. * * @return this buffer. */ util.DataBuffer.prototype.fillWithByte = function(b, n) { this.accommodate(n); for(var i = 0; i < n; ++i) { this.data.setUint8(b); } return this; }; /** * Puts bytes in this buffer. The bytes may be given as a string, an * ArrayBuffer, a DataView, or a TypedArray. * * @param bytes the bytes to put. * @param [encoding] the encoding for the first parameter ('binary', 'utf8', * 'utf16', 'hex'), if it is a string (default: 'binary'). * * @return this buffer. */ util.DataBuffer.prototype.putBytes = function(bytes, encoding) { if(util.isArrayBufferView(bytes)) { var src = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength); var len = src.byteLength - src.byteOffset; this.accommodate(len); var dst = new Uint8Array(this.data.buffer, this.write); dst.set(src); this.write += len; return this; } if(util.isArrayBuffer(bytes)) { var src = new Uint8Array(bytes); this.accommodate(src.byteLength); var dst = new Uint8Array(this.data.buffer); dst.set(src, this.write); this.write += src.byteLength; return this; } // bytes is a util.DataBuffer or equivalent if(bytes instanceof util.DataBuffer || (typeof bytes === 'object' && typeof bytes.read === 'number' && typeof bytes.write === 'number' && util.isArrayBufferView(bytes.data))) { var src = new Uint8Array(bytes.data.byteLength, bytes.read, bytes.length()); this.accommodate(src.byteLength); var dst = new Uint8Array(bytes.data.byteLength, this.write); dst.set(src); this.write += src.byteLength; return this; } if(bytes instanceof util.ByteStringBuffer) { // copy binary string and process as the same as a string parameter below bytes = bytes.data; encoding = 'binary'; } // string conversion encoding = encoding || 'binary'; if(typeof bytes === 'string') { var view; // decode from string if(encoding === 'hex') { this.accommodate(Math.ceil(bytes.length / 2)); view = new Uint8Array(this.data.buffer, this.write); this.write += util.binary.hex.decode(bytes, view, this.write); return this; } if(encoding === 'base64') { this.accommodate(Math.ceil(bytes.length / 4) * 3); view = new Uint8Array(this.data.buffer, this.write); this.write += util.binary.base64.decode(bytes, view, this.write); return this; } // encode text as UTF-8 bytes if(encoding === 'utf8') { // encode as UTF-8 then decode string as raw binary bytes = util.encodeUtf8(bytes); encoding = 'binary'; } // decode string as raw binary if(encoding === 'binary' || encoding === 'raw') { // one byte per character this.accommodate(bytes.length); view = new Uint8Array(this.data.buffer, this.write); this.write += util.binary.raw.decode(view); return this; } // encode text as UTF-16 bytes if(encoding === 'utf16') { // two bytes per character this.accommodate(bytes.length * 2); view = new Uint16Array(this.data.buffer, this.write); this.write += util.text.utf16.encode(view); return this; } throw new Error('Invalid encoding: ' + encoding); } throw Error('Invalid parameter: ' + bytes); }; /** * Puts the given buffer into this buffer. * * @param buffer the buffer to put into this one. * * @return this buffer. */ util.DataBuffer.prototype.putBuffer = function(buffer) { this.putBytes(buffer); buffer.clear(); return this; }; /** * Puts a string into this buffer. * * @param str the string to put. * @param [encoding] the encoding for the string (default: 'utf16'). * * @return this buffer. */ util.DataBuffer.prototype.putString = function(str) { return this.putBytes(str, 'utf16'); }; /** * Puts a 16-bit integer in this buffer in big-endian order. * * @param i the 16-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt16 = function(i) { this.accommodate(2); this.data.setInt16(this.write, i); this.write += 2; return this; }; /** * Puts a 24-bit integer in this buffer in big-endian order. * * @param i the 24-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt24 = function(i) { this.accommodate(3); this.data.setInt16(this.write, i >> 8 & 0xFFFF); this.data.setInt8(this.write, i >> 16 & 0xFF); this.write += 3; return this; }; /** * Puts a 32-bit integer in this buffer in big-endian order. * * @param i the 32-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt32 = function(i) { this.accommodate(4); // console.log(this.write); this.data.setInt32(this.write, i); this.write += 4; return this; }; /** * Puts a 16-bit integer in this buffer in little-endian order. * * @param i the 16-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt16Le = function(i) { this.accommodate(2); this.data.setInt16(this.write, i, true); this.write += 2; return this; }; /** * Puts a 24-bit integer in this buffer in little-endian order. * * @param i the 24-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt24Le = function(i) { this.accommodate(3); this.data.setInt8(this.write, i >> 16 & 0xFF); this.data.setInt16(this.write, i >> 8 & 0xFFFF, true); this.write += 3; return this; }; /** * Puts a 32-bit integer in this buffer in little-endian order. * * @param i the 32-bit integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt32Le = function(i) { this.accommodate(4); this.data.setInt32(this.write, i, true); this.write += 4; return this; }; /** * Puts an n-bit integer in this buffer in big-endian order. * * @param i the n-bit integer. * @param n the number of bits in the integer. * * @return this buffer. */ util.DataBuffer.prototype.putInt = function(i, n) { this.accommodate(n / 8); do { n -= 8; this.data.setInt8(this.write++, (i >> n) & 0xFF); } while(n > 0); return this; }; /** * Puts a signed n-bit integer in this buffer in big-endian order. Two's * complement representation is used. * * @param i the n-bit integer. * @param n the number of bits in the integer. * * @return this buffer. */ util.DataBuffer.prototype.putSignedInt = function(i, n) { this.accommodate(n / 8); if(i < 0) { i += 2 << (n - 1); } return this.putInt(i, n); }; /** * Gets a byte from this buffer and advances the read pointer by 1. * * @return the byte. */ util.DataBuffer.prototype.getByte = function() { return this.data.getInt8(this.read++); }; /** * Gets a uint16 from this buffer in big-endian order and advances the read * pointer by 2. * * @return the uint16. */ util.DataBuffer.prototype.getInt16 = function() { var rval = this.data.getInt16(this.read); this.read += 2; return rval; }; /** * Gets a uint24 from this buffer in big-endian order and advances the read * pointer by 3. * * @return the uint24. */ util.DataBuffer.prototype.getInt24 = function() { var rval = ( this.data.getInt16(this.read) << 8 ^ this.data.getInt8(this.read + 2)); this.read += 3; return rval; }; /** * Gets a uint32 from this buffer in big-endian order and advances the read * pointer by 4. * * @return the word. */ util.DataBuffer.prototype.getInt32 = function() { var rval = this.data.getInt32(this.read); this.read += 4; return rval; }; /** * Gets a uint16 from this buffer in little-endian order and advances the read * pointer by 2. * * @return the uint16. */ util.DataBuffer.prototype.getInt16Le = function() { var rval = this.data.getInt16(this.read, true); this.read += 2; return rval; }; /** * Gets a uint24 from this buffer in little-endian order and advances the read * pointer by 3. * * @return the uint24. */ util.DataBuffer.prototype.getInt24Le = function() { var rval = ( this.data.getInt8(this.read) ^ this.data.getInt16(this.read + 1, true) << 8); this.read += 3; return rval; }; /** * Gets a uint32 from this buffer in little-endian order and advances the read * pointer by 4. * * @return the word. */ util.DataBuffer.prototype.getInt32Le = function() { var rval = this.data.getInt32(this.read, true); this.read += 4; return rval; }; /** * Gets an n-bit integer from this buffer in big-endian order and advances the * read pointer by n/8. * * @param n the number of bits in the integer. * * @return the integer. */ util.DataBuffer.prototype.getInt = function(n) { var rval = 0; do { rval = (rval << 8) + this.data.getInt8(this.read++); n -= 8; } while(n > 0); return rval; }; /** * Gets a signed n-bit integer from this buffer in big-endian order, using * two's complement, and advances the read pointer by n/8. * * @param n the number of bits in the integer. * * @return the integer. */ util.DataBuffer.prototype.getSignedInt = function(n) { var x = this.getInt(n); var max = 2 << (n - 2); if(x >= max) { x -= max << 1; } return x; }; /** * Reads bytes out into a UTF-8 string and clears them from the buffer. * * @param count the number of bytes to read, undefined or null for all. * * @return a UTF-8 string of bytes. */ util.DataBuffer.prototype.getBytes = function(count) { // TODO: deprecate this method, it is poorly named and // this.toString('binary') replaces it // add a toTypedArray()/toArrayBuffer() function var rval; if(count) { // read count bytes count = Math.min(this.length(), count); rval = this.data.slice(this.read, this.read + count); this.read += count; } else if(count === 0) { rval = ''; } else { // read all bytes, optimize to only copy when needed rval = (this.read === 0) ? this.data : this.data.slice(this.read); this.clear(); } return rval; }; /** * Gets a UTF-8 encoded string of the bytes from this buffer without modifying * the read pointer. * * @param count the number of bytes to get, omit to get all. * * @return a string full of UTF-8 encoded characters. */ util.DataBuffer.prototype.bytes = function(count) { // TODO: deprecate this method, it is poorly named, add "getString()" return (typeof(count) === 'undefined' ? this.data.slice(this.read) : this.data.slice(this.read, this.read + count)); }; /** * Gets a byte at the given index without modifying the read pointer. * * @param i the byte index. * * @return the byte. */ util.DataBuffer.prototype.at = function(i) { return this.data.getUint8(this.read + i); }; /** * Puts a byte at the given index without modifying the read pointer. * * @param i the byte index. * @param b the byte to put. * * @return this buffer. */ util.DataBuffer.prototype.setAt = function(i, b) { this.data.setUint8(i, b); return this; }; /** * Gets the last byte without modifying the read pointer. * * @return the last byte. */ util.DataBuffer.prototype.last = function() { return this.data.getUint8(this.write - 1); }; /** * Creates a copy of this buffer. * * @return the copy. */ // TODO: add .copyView to get a shallow copy util.DataBuffer.prototype.copy = function() { return new util.DataBuffer(this); }; /** * Compacts this buffer. * * @return this buffer. */ util.DataBuffer.prototype.compact = function() { if(this.read > 0) { var src = new Uint8Array(this.data.buffer, this.read); var dst = new Uint8Array(src.byteLength); dst.set(src); this.data = new DataView(dst); this.write -= this.read; this.read = 0; } return this; }; /** * Clears this buffer. * * @return this buffer. */ util.DataBuffer.prototype.clear = function() { this.data = new DataView(new ArrayBuffer(0)); this.read = this.write = 0; return this; }; /** * Shortens this buffer by triming bytes off of the end of this buffer. * * @param count the number of bytes to trim off. * * @return this buffer. */ util.DataBuffer.prototype.truncate = function(count) { this.write = Math.max(0, this.length() - count); this.read = Math.min(this.read, this.write); return this; }; /** * Converts this buffer to a hexadecimal string. * * @return a hexadecimal string. */ util.DataBuffer.prototype.toHex = function() { var rval = ''; for(var i = this.read; i < this.data.byteLength; ++i) { var b = this.data.getUint8(i); if(b < 16) { rval += '0'; } rval += b.toString(16); } return rval; }; /** * Converts this buffer to a string, using the given encoding. If no * encoding is given, 'utf8' (UTF-8) is used. * * @param [encoding] the encoding to use: 'binary', 'utf8', 'utf16', 'hex', * 'base64' (default: 'utf8'). * @param [options] any special options supported by a particular encoding, * (eg: for base64, {maxline: 64}). * * @return a string representation of the bytes in this buffer. */ util.DataBuffer.prototype.toString = function(encoding, options) { encoding = encoding || 'utf8'; options = options || {}; var view = new Uint8Array(this.data, this.read, this.length()); // encode to string if(encoding === 'binary' || encoding === 'raw') { return util.binary.raw.encode(view); } if(encoding === 'hex') { return util.binary.hex.encode(view); } if(encoding === 'base64') { return util.binary.base64.encode(view, options); } // decode to text if(encoding === 'utf8') { return util.text.utf8.decode(view); } if(encoding === 'utf16') { return util.text.utf16.decode(view); } throw new Error('Invalid encoding: ' + encoding); }; /** End Buffer w/UInt8Array backing */ /** * Deprecated. Use new ByteBuffer() instead. * * Creates a buffer that stores bytes. A value may be given to put into the * buffer that is either a string of bytes or a UTF-16 string that will * be encoded using UTF-8 (to do the latter, specify 'utf8' as the encoding). * * @param [input] the bytes to wrap (as a string) or a UTF-16 string to encode * as UTF-8. * @param [encoding] (default: 'raw', other: 'utf8'). */ util.createBuffer = function(input, encoding) { if(input === undefined) { return new util.ByteBuffer(); } encoding = encoding || 'raw'; if(encoding === 'utf8') { input = util.encodeUtf8(input); } else if(encoding === 'hex') { input = util.hexToBytes(input); } return new util.ByteBuffer(input); }; /** * Fills a string with a particular value. If you want the string to be a byte * string, pass in String.fromCharCode(theByte). * * @param c the character to fill the string with, use String.fromCharCode * to fill the string with a byte value. * @param n the number of characters of value c to fill with. * * @return the filled string. */ util.fillString = function(c, n) { var s = ''; while(n > 0) { if(n & 1) { s += c; } n >>>= 1; if(n > 0) { c += c; } } return s; }; /** * Performs a per byte XOR between two byte strings and returns the result as a * string of bytes. * * @param s1 first string of bytes. * @param s2 second string of bytes. * @param n the number of bytes to XOR. * * @return the XOR'd result. */ util.xorBytes = function(s1, s2, n) { var s3 = ''; var b = ''; var t = ''; var i = 0; var c = 0; for(; n > 0; --n, ++i) { b = s1.charCodeAt(i) ^ s2.charCodeAt(i); if(c >= 10) { s3 += t; t = ''; c = 0; } t += String.fromCharCode(b); ++c; } s3 += t; return s3; }; /** * Converts a hex string into a 'binary' encoded string of bytes. * * @param hex the hexadecimal string to convert. * * @return the binary-encoded string of bytes. */ util.hexToBytes = function(hex) { // TODO: deprecate: "Deprecated. Use util.binary.hex.decode instead." var rval = ''; var i = 0; if(hex.length & 1 == 1) { // odd number of characters, convert first character alone i = 1; rval += String.fromCharCode(parseInt(hex[0], 16)); } // convert 2 characters (1 byte) at a time for(; i < hex.length; i += 2) { rval += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return rval; }; /** * Converts a 'binary' encoded string of bytes to hex. * * @param bytes the byte string to convert. * * @return the string of hexadecimal characters. */ util.bytesToHex = function(bytes) { // TODO: deprecate: "Deprecated. Use util.binary.hex.encode instead." return util.createBuffer(bytes).toHex(); }; /** * Converts an 32-bit integer to 4-big-endian byte string. * * @param i the integer. * * @return the byte string. */ util.int32ToBytes = function(i) { return ( String.fromCharCode(i >> 24 & 0xFF) + String.fromCharCode(i >> 16 & 0xFF) + String.fromCharCode(i >> 8 & 0xFF) + String.fromCharCode(i & 0xFF)); }; // base64 characters, reverse mapping var _base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; var _base64Idx = [ /*43 -43 = 0*/ /*'+', 1, 2, 3,'/' */ 62, -1, -1, -1, 63, /*'0','1','2','3','4','5','6','7','8','9' */ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, /*15, 16, 17,'=', 19, 20, 21 */ -1, -1, -1, 64, -1, -1, -1, /*65 - 43 = 22*/ /*'A','B','C','D','E','F','G','H','I','J','K','L','M', */ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, /*'N','O','P','Q','R','S','T','U','V','W','X','Y','Z' */ 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, /*91 - 43 = 48 */ /*48, 49, 50, 51, 52, 53 */ -1, -1, -1, -1, -1, -1, /*97 - 43 = 54*/ /*'a','b','c','d','e','f','g','h','i','j','k','l','m' */ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, /*'n','o','p','q','r','s','t','u','v','w','x','y','z' */ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 ]; /** * Base64 encodes a 'binary' encoded string of bytes. * * @param input the binary encoded string of bytes to base64-encode. * @param maxline the maximum number of encoded characters per line to use, * defaults to none. * * @return the base64-encoded output. */ util.encode64 = function(input, maxline) { // TODO: deprecate: "Deprecated. Use util.binary.base64.encode instead." var line = ''; var output = ''; var chr1, chr2, chr3; var i = 0; while(i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); // encode 4 character group line += _base64.charAt(chr1 >> 2); line += _base64.charAt(((chr1 & 3) << 4) | (chr2 >> 4)); if(isNaN(chr2)) { line += '=='; } else { line += _base64.charAt(((chr2 & 15) << 2) | (chr3 >> 6)); line += isNaN(chr3) ? '=' : _base64.charAt(chr3 & 63); } if(maxline && line.length > maxline) { output += line.substr(0, maxline) + '\r\n'; line = line.substr(maxline); } } output += line; return output; }; /** * Base64 decodes a string into a 'binary' encoded string of bytes. * * @param input the base64-encoded input. * * @return the binary encoded string. */ util.decode64 = function(input) { // TODO: deprecate: "Deprecated. Use util.binary.base64.decode instead." // remove all non-base64 characters input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); var output = ''; var enc1, enc2, enc3, enc4; var i = 0; while(i < input.length) { enc1 = _base64Idx[input.charCodeAt(i++) - 43]; enc2 = _base64Idx[input.charCodeAt(i++) - 43]; enc3 = _base64Idx[input.charCodeAt(i++) - 43]; enc4 = _base64Idx[input.charCodeAt(i++) - 43]; output += String.fromCharCode((enc1 << 2) | (enc2 >> 4)); if(enc3 !== 64) { // decoded at least 2 bytes output += String.fromCharCode(((enc2 & 15) << 4) | (enc3 >> 2)); if(enc4 !== 64) { // decoded 3 bytes output += String.fromCharCode(((enc3 & 3) << 6) | enc4); } } } return output; }; /** * UTF-8 encodes the given UTF-16 encoded string (a standard JavaScript * string). Non-ASCII characters will be encoded as multiple bytes according * to UTF-8. * * @param str the string to encode. * * @return the UTF-8 encoded string. */ util.encodeUtf8 = function(str) { return unescape(encodeURIComponent(str)); }; /** * Decodes a UTF-8 encoded string into a UTF-16 string. * * @param str the string to decode. * * @return the UTF-16 encoded string (standard JavaScript string). */ util.decodeUtf8 = function(str) { return decodeURIComponent(escape(str)); }; // binary encoding/decoding tools // FIXME: Experimental. Do not use yet. util.binary = { raw: {}, hex: {}, base64: {} }; /** * Encodes a Uint8Array as a binary-encoded string. This encoding uses * a value between 0 and 255 for each character. * * @param bytes the Uint8Array to encode. * * @return the binary-encoded string. */ util.binary.raw.encode = function(bytes) { return String.fromCharCode.apply(null, bytes); }; /** * Decodes a binary-encoded string to a Uint8Array. This encoding uses * a value between 0 and 255 for each character. * * @param str the binary-encoded string to decode. * @param [output] an optional Uint8Array to write the output to; if it * is too small, an exception will be thrown. * @param [offset] the start offset for writing to the output (default: 0). * * @return the Uint8Array or the number of bytes written if output was given. */ util.binary.raw.decode = function(str, output, offset) { var out = output; if(!out) { out = new Uint8Array(str.length); } offset = offset || 0; var j = offset; for(var i = 0; i < str.length; ++i) { out[j++] = str.charCodeAt(i); } return output ? (j - offset) : out; }; /** * Encodes a 'binary' string, ArrayBuffer, DataView, TypedArray, or * ByteBuffer as a string of hexadecimal characters. * * @param bytes the bytes to convert. * * @return the string of hexadecimal characters. */ util.binary.hex.encode = util.bytesToHex; /** * Decodes a hex-encoded string to a Uint8Array. * * @param hex the hexadecimal string to convert. * @param [output] an optional Uint8Array to write the output to; if it * is too small, an exception will be thrown. * @param [offset] the start offset for writing to the output (default: 0). * * @return the Uint8Array or the number of bytes written if output was given. */ util.binary.hex.decode = function(hex, output, offset) { var out = output; if(!out) { out = new Uint8Array(Math.ceil(hex.length / 2)); } offset = offset || 0; var i = 0, j = offset; if(hex.length & 1) { // odd number of characters, convert first character alone i = 1; out[j++] = parseInt(hex[0], 16); } // convert 2 characters (1 byte) at a time for(; i < hex.length; i += 2) { out[j++] = parseInt(hex.substr(i, 2), 16); } return output ? (j - offset) : out; }; /** * Base64-encodes a Uint8Array. * * @param input the Uint8Array to encode. * @param [options] the options to use: * [maxline] the maximum number of encoded characters per line to use, * defaults to none. * * @return the base64-encoded output string. */ util.binary.base64.encode = function(input, options) { options = options || {}; var maxline = options.maxline; var line = ''; var output = ''; var chr1, chr2, chr3; var i = 0; while(i < input.byteLength) { chr1 = input[i++]; chr2 = input[i++]; chr3 = input[i++]; // encode 4 character group line += _base64.charAt(chr1 >> 2); line += _base64.charAt(((chr1 & 3) << 4) | (chr2 >> 4)); if(isNaN(chr2)) { line += '=='; } else { line += _base64.charAt(((chr2 & 15) << 2) | (chr3 >> 6)); line += isNaN(chr3) ? '=' : _base64.charAt(chr3 & 63); } if(maxline && line.length > maxline) { output += line.substr(0, maxline) + '\r\n'; line = line.substr(maxline); } } output += line; return output; }; /** * Decodes a base64-encoded string to a Uint8Array. * * @param input the base64-encoded input string. * @param [output] an optional Uint8Array to write the output to; if it * is too small, an exception will be thrown. * @param [offset] the start offset for writing to the output (default: 0). * * @return the Uint8Array or the number of bytes written if output was given. */ util.binary.base64.decode = function(input, output, offset) { var out = output; if(!out) { out = new Uint8Array(Math.ceil(input.length / 4) * 3); } // remove all non-base64 characters input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); offset = offset || 0; var enc1, enc2, enc3, enc4; var i = 0, j = offset; while(i < input.length) { enc1 = _base64Idx[input.charCodeAt(i++) - 43]; enc2 = _base64Idx[input.charCodeAt(i++) - 43]; enc3 = _base64Idx[input.charCodeAt(i++) - 43]; enc4 = _base64Idx[input.charCodeAt(i++) - 43]; out[j++] = (enc1 << 2) | (enc2 >> 4); if(enc3 !== 64) { // decoded at least 2 bytes out[j++] = ((enc2 & 15) << 4) | (enc3 >> 2); if(enc4 !== 64) { // decoded 3 bytes out[j++] = ((enc3 & 3) << 6) | enc4; } } } return output ? (j - offset) : out; }; // text encoding/decoding tools // FIXME: Experimental. Do not use yet. util.text = { utf8: {}, utf16: {} }; /** * Encodes the given string as UTF-8 in a Uint8Array. * * @param str the string to encode. * @param [output] an optional Uint8Array to write the output to; if it * is too small, an exception will be thrown. * @param [offset] the start offset for writing to the output (default: 0). * * @return the Uint8Array or the number of bytes written if output was given. */ util.text.utf8.encode = function(str, output, offset) { str = util.encodeUtf8(str); var out = output; if(!out) { out = new Uint8Array(str.length); } offset = offset || 0; var j = offset; for(var i = 0; i < str.length; ++i) { out[j++] = str.charCodeAt(i); } return output ? (j - offset) : out; }; /** * Decodes the UTF-8 contents from a Uint8Array. * * @param bytes the Uint8Array to decode. * * @return the resulting string. */ util.text.utf8.decode = function(bytes) { return util.decodeUtf8(String.fromCharCode.apply(null, bytes)); }; /** * Encodes the given string as UTF-16 in a Uint8Array. * * @param str the string to encode. * @param [output] an optional Uint8Array to write the output to; if it * is too small, an exception will be thrown. * @param [offset] the start offset for writing to the output (default: 0). * * @return the Uint8Array or the number of bytes written if output was given. */ util.text.utf16.encode = function(str, output, offset) { var out = output; if(!out) { out = new Uint8Array(str.length); } var view = new Uint16Array(out); offset = offset || 0; var j = offset; var k = offset; for(var i = 0; i < str.length; ++i) { view[k++] = str.charCodeAt(i); j += 2; } return output ? (j - offset) : out; }; /** * Decodes the UTF-16 contents from a Uint8Array. * * @param bytes the Uint8Array to decode. * * @return the resulting string. */ util.text.utf16.decode = function(bytes) { return String.fromCharCode.apply(null, new Uint16Array(bytes)); }; /** * Deflates the given data using a flash interface. * * @param api the flash interface. * @param bytes the data. * @param raw true to return only raw deflate data, false to include zlib * header and trailer. * * @return the deflated data as a string. */ util.deflate = function(api, bytes, raw) { bytes = util.decode64(api.deflate(util.encode64(bytes)).rval); // strip zlib header and trailer if necessary if(raw) { // zlib header is 2 bytes (CMF,FLG) where FLG indicates that // there is a 4-byte DICT (alder-32) block before the data if // its 5th bit is set var start = 2; var flg = bytes.charCodeAt(1); if(flg & 0x20) { start = 6; } // zlib trailer is 4 bytes of adler-32 bytes = bytes.substring(start, bytes.length - 4); } return bytes; }; /** * Inflates the given data using a flash interface. * * @param api the flash interface. * @param bytes the data. * @param raw true if the incoming data has no zlib header or trailer and is * raw DEFLATE data. * * @return the inflated data as a string, null on error. */ util.inflate = function(api, bytes, raw) { // TODO: add zlib header and trailer if necessary/possible var rval = api.inflate(util.encode64(bytes)).rval; return (rval === null) ? null : util.decode64(rval); }; /** * Sets a storage object. * * @param api the storage interface. * @param id the storage ID to use. * @param obj the storage object, null to remove. */ var _setStorageObject = function(api, id, obj) { if(!api) { throw new Error('WebStorage not available.'); } var rval; if(obj === null) { rval = api.removeItem(id); } else { // json-encode and base64-encode object obj = util.encode64(JSON.stringify(obj)); rval = api.setItem(id, obj); } // handle potential flash error if(typeof(rval) !== 'undefined' && rval.rval !== true) { var error = new Error(rval.error.message); error.id = rval.error.id; error.name = rval.error.name; throw error; } }; /** * Gets a storage object. * * @param api the storage interface. * @param id the storage ID to use. * * @return the storage object entry or null if none exists. */ var _getStorageObject = function(api, id) { if(!api) { throw new Error('WebStorage not available.'); } // get the existing entry var rval = api.getItem(id); /* Note: We check api.init because we can't do (api == localStorage) on IE because of "Class doesn't support Automation" exception. Only the flash api has an init method so this works too, but we need a better solution in the future. */ // flash returns item wrapped in an object, handle special case if(api.init) { if(rval.rval === null) { if(rval.error) { var error = new Error(rval.error.message); error.id = rval.error.id; error.name = rval.error.name; throw error; } // no error, but also no item rval = null; } else { rval = rval.rval; } } // handle decoding if(rval !== null) { // base64-decode and json-decode data rval = JSON.parse(util.decode64(rval)); } return rval; }; /** * Stores an item in local storage. * * @param api the storage interface. * @param id the storage ID to use. * @param key the key for the item. * @param data the data for the item (any javascript object/primitive). */ var _setItem = function(api, id, key, data) { // get storage object var obj = _getStorageObject(api, id); if(obj === null) { // create a new storage object obj = {}; } // update key obj[key] = data; // set storage object _setStorageObject(api, id, obj); }; /** * Gets an item from local storage. * * @param api the storage interface. * @param id the storage ID to use. * @param key the key for the item. * * @return the item. */ var _getItem = function(api, id, key) { // get storage object var rval = _getStorageObject(api, id); if(rval !== null) { // return data at key rval = (key in rval) ? rval[key] : null; } return rval; }; /** * Removes an item from local storage. * * @param api the storage interface. * @param id the storage ID to use. * @param key the key for the item. */ var _removeItem = function(api, id, key) { // get storage object var obj = _getStorageObject(api, id); if(obj !== null && key in obj) { // remove key delete obj[key]; // see if entry has no keys remaining var empty = true; for(var prop in obj) { empty = false; break; } if(empty) { // remove entry entirely if no keys are left obj = null; } // set storage object _setStorageObject(api, id, obj); } }; /** * Clears the local disk storage identified by the given ID. * * @param api the storage interface. * @param id the storage ID to use. */ var _clearItems = function(api, id) { _setStorageObject(api, id, null); }; /** * Calls a storage function. * * @param func the function to call. * @param args the arguments for the function. * @param location the location argument. * * @return the return value from the function. */ var _callStorageFunction = function(func, args, location) { var rval = null; // default storage types if(typeof(location) === 'undefined') { location = ['web', 'flash']; } // apply storage types in order of preference var type; var done = false; var exception = null; for(var idx in location) { type = location[idx]; try { if(type === 'flash' || type === 'both') { if(args[0] === null) { throw new Error('Flash local storage not available.'); } rval = func.apply(this, args); done = (type === 'flash'); } if(type === 'web' || type === 'both') { args[0] = localStorage; rval = func.apply(this, args); done = true; } } catch(ex) { exception = ex; } if(done) { break; } } if(!done) { throw exception; } return rval; }; /** * Stores an item on local disk. * * The available types of local storage include 'flash', 'web', and 'both'. * * The type 'flash' refers to flash local storage (SharedObject). In order * to use flash local storage, the 'api' parameter must be valid. The type * 'web' refers to WebStorage, if supported by the browser. The type 'both' * refers to storing using both 'flash' and 'web', not just one or the * other. * * The location array should list the storage types to use in order of * preference: * * ['flash']: flash only storage * ['web']: web only storage * ['both']: try to store in both * ['flash','web']: store in flash first, but if not available, 'web' * ['web','flash']: store in web first, but if not available, 'flash' * * The location array defaults to: ['web', 'flash'] * * @param api the flash interface, null to use only WebStorage. * @param id the storage ID to use. * @param key the key for the item. * @param data the data for the item (any javascript object/primitive). * @param location an array with the preferred types of storage to use. */ util.setItem = function(api, id, key, data, location) { _callStorageFunction(_setItem, arguments, location); }; /** * Gets an item on local disk. * * Set setItem() for details on storage types. * * @param api the flash interface, null to use only WebStorage. * @param id the storage ID to use. * @param key the key for the item. * @param location an array with the preferred types of storage to use. * * @return the item. */ util.getItem = function(api, id, key, location) { return _callStorageFunction(_getItem, arguments, location); }; /** * Removes an item on local disk. * * Set setItem() for details on storage types. * * @param api the flash interface. * @param id the storage ID to use. * @param key the key for the item. * @param location an array with the preferred types of storage to use. */ util.removeItem = function(api, id, key, location) { _callStorageFunction(_removeItem, arguments, location); }; /** * Clears the local disk storage identified by the given ID. * * Set setItem() for details on storage types. * * @param api the flash interface if flash is available. * @param id the storage ID to use. * @param location an array with the preferred types of storage to use. */ util.clearItems = function(api, id, location) { _callStorageFunction(_clearItems, arguments, location); }; /** * Parses the scheme, host, and port from an http(s) url. * * @param str the url string. * * @return the parsed url object or null if the url is invalid. */ util.parseUrl = function(str) { // FIXME: this regex looks a bit broken var regex = /^(https?):\/\/([^:&^\/]*):?(\d*)(.*)$/g; regex.lastIndex = 0; var m = regex.exec(str); var url = (m === null) ? null : { full: str, scheme: m[1], host: m[2], port: m[3], path: m[4] }; if(url) { url.fullHost = url.host; if(url.port) { if(url.port !== 80 && url.scheme === 'http') { url.fullHost += ':' + url.port; } else if(url.port !== 443 && url.scheme === 'https') { url.fullHost += ':' + url.port; } } else if(url.scheme === 'http') { url.port = 80; } else if(url.scheme === 'https') { url.port = 443; } url.full = url.scheme + '://' + url.fullHost; } return url; }; /* Storage for query variables */ var _queryVariables = null; /** * Returns the window location query variables. Query is parsed on the first * call and the same object is returned on subsequent calls. The mapping * is from keys to an array of values. Parameters without values will have * an object key set but no value added to the value array. Values are * unescaped. * * ...?k1=v1&k2=v2: * { * "k1": ["v1"], * "k2": ["v2"] * } * * ...?k1=v1&k1=v2: * { * "k1": ["v1", "v2"] * } * * ...?k1=v1&k2: * { * "k1": ["v1"], * "k2": [] * } * * ...?k1=v1&k1: * { * "k1": ["v1"] * } * * ...?k1&k1: * { * "k1": [] * } * * @param query the query string to parse (optional, default to cached * results from parsing window location search query). * * @return object mapping keys to variables. */ util.getQueryVariables = function(query) { var parse = function(q) { var rval = {}; var kvpairs = q.split('&'); for(var i = 0; i < kvpairs.length; i++) { var pos = kvpairs[i].indexOf('='); var key; var val; if(pos > 0) { key = kvpairs[i].substring(0,pos); val = kvpairs[i].substring(pos+1); } else { key = kvpairs[i]; val = null; } if(!(key in rval)) { rval[key] = []; } // disallow overriding object prototype keys if(!(key in Object.prototype) && val !== null) { rval[key].push(unescape(val)); } } return rval; }; var rval; if(typeof(query) === 'undefined') { // set cached variables if needed if(_queryVariables === null) { if(typeof(window) === 'undefined') { // no query variables available _queryVariables = {}; } else { // parse window search query _queryVariables = parse(window.location.search.substring(1)); } } rval = _queryVariables; } else { // parse given query rval = parse(query); } return rval; }; /** * Parses a fragment into a path and query. This method will take a URI * fragment and break it up as if it were the main URI. For example: * /bar/baz?a=1&b=2 * results in: * { * path: ["bar", "baz"], * query: {"k1": ["v1"], "k2": ["v2"]} * } * * @return object with a path array and query object. */ util.parseFragment = function(fragment) { // default to whole fragment var fp = fragment; var fq = ''; // split into path and query if possible at the first '?' var pos = fragment.indexOf('?'); if(pos > 0) { fp = fragment.substring(0,pos); fq = fragment.substring(pos+1); } // split path based on '/' and ignore first element if empty var path = fp.split('/'); if(path.length > 0 && path[0] === '') { path.shift(); } // convert query into object var query = (fq === '') ? {} : util.getQueryVariables(fq); return { pathString: fp, queryString: fq, path: path, query: query }; }; /** * Makes a request out of a URI-like request string. This is intended to * be used where a fragment id (after a URI '#') is parsed as a URI with * path and query parts. The string should have a path beginning and * delimited by '/' and optional query parameters following a '?'. The * query should be a standard URL set of key value pairs delimited by * '&'. For backwards compatibility the initial '/' on the path is not * required. The request object has the following API, (fully described * in the method code): * { * path: . * query: , * getPath(i): get part or all of the split path array, * getQuery(k, i): get part or all of a query key array, * getQueryLast(k, _default): get last element of a query key array. * } * * @return object with request parameters. */ util.makeRequest = function(reqString) { var frag = util.parseFragment(reqString); var req = { // full path string path: frag.pathString, // full query string query: frag.queryString, /** * Get path or element in path. * * @param i optional path index. * * @return path or part of path if i provided. */ getPath: function(i) { return (typeof(i) === 'undefined') ? frag.path : frag.path[i]; }, /** * Get query, values for a key, or value for a key index. * * @param k optional query key. * @param i optional query key index. * * @return query, values for a key, or value for a key index. */ getQuery: function(k, i) { var rval; if(typeof(k) === 'undefined') { rval = frag.query; } else { rval = frag.query[k]; if(rval && typeof(i) !== 'undefined') { rval = rval[i]; } } return rval; }, getQueryLast: function(k, _default) { var rval; var vals = req.getQuery(k); if(vals) { rval = vals[vals.length - 1]; } else { rval = _default; } return rval; } }; return req; }; /** * Makes a URI out of a path, an object with query parameters, and a * fragment. Uses jQuery.param() internally for query string creation. * If the path is an array, it will be joined with '/'. * * @param path string path or array of strings. * @param query object with query parameters. (optional) * @param fragment fragment string. (optional) * * @return string object with request parameters. */ util.makeLink = function(path, query, fragment) { // join path parts if needed path = jQuery.isArray(path) ? path.join('/') : path; var qstr = jQuery.param(query || {}); fragment = fragment || ''; return path + ((qstr.length > 0) ? ('?' + qstr) : '') + ((fragment.length > 0) ? ('#' + fragment) : ''); }; /** * Follows a path of keys deep into an object hierarchy and set a value. * If a key does not exist or it's value is not an object, create an * object in it's place. This can be destructive to a object tree if * leaf nodes are given as non-final path keys. * Used to avoid exceptions from missing parts of the path. * * @param object the starting object. * @param keys an array of string keys. * @param value the value to set. */ util.setPath = function(object, keys, value) { // need to start at an object if(typeof(object) === 'object' && object !== null) { var i = 0; var len = keys.length; while(i < len) { var next = keys[i++]; if(i == len) { // last object[next] = value; } else { // more var hasNext = (next in object); if(!hasNext || (hasNext && typeof(object[next]) !== 'object') || (hasNext && object[next] === null)) { object[next] = {}; } object = object[next]; } } } }; /** * Follows a path of keys deep into an object hierarchy and return a value. * If a key does not exist, create an object in it's place. * Used to avoid exceptions from missing parts of the path. * * @param object the starting object. * @param keys an array of string keys. * @param _default value to return if path not found. * * @return the value at the path if found, else default if given, else * undefined. */ util.getPath = function(object, keys, _default) { var i = 0; var len = keys.length; var hasNext = true; while(hasNext && i < len && typeof(object) === 'object' && object !== null) { var next = keys[i++]; hasNext = next in object; if(hasNext) { object = object[next]; } } return (hasNext ? object : _default); }; /** * Follow a path of keys deep into an object hierarchy and delete the * last one. If a key does not exist, do nothing. * Used to avoid exceptions from missing parts of the path. * * @param object the starting object. * @param keys an array of string keys. */ util.deletePath = function(object, keys) { // need to start at an object if(typeof(object) === 'object' && object !== null) { var i = 0; var len = keys.length; while(i < len) { var next = keys[i++]; if(i == len) { // last delete object[next]; } else { // more if(!(next in object) || (typeof(object[next]) !== 'object') || (object[next] === null)) { break; } object = object[next]; } } } }; /** * Check if an object is empty. * * Taken from: * http://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object-from-json/679937#679937 * * @param object the object to check. */ util.isEmpty = function(obj) { for(var prop in obj) { if(obj.hasOwnProperty(prop)) { return false; } } return true; }; /** * Format with simple printf-style interpolation. * * %%: literal '%' * %s,%o: convert next argument into a string. * * @param format the string to format. * @param ... arguments to interpolate into the format string. */ util.format = function(format) { var re = /%./g; // current match var match; // current part var part; // current arg index var argi = 0; // collected parts to recombine later var parts = []; // last index found var last = 0; // loop while matches remain while((match = re.exec(format))) { part = format.substring(last, re.lastIndex - 2); // don't add empty strings (ie, parts between %s%s) if(part.length > 0) { parts.push(part); } last = re.lastIndex; // switch on % code var code = match[0][1]; switch(code) { case 's': case 'o': // check if enough arguments were given if(argi < arguments.length) { parts.push(arguments[argi++ + 1]); } else { parts.push(''); } break; // FIXME: do proper formating for numbers, etc //case 'f': //case 'd': case '%': parts.push('%'); break; default: parts.push('<%' + code + '?>'); } } // add trailing part of format string parts.push(format.substring(last)); return parts.join(''); }; /** * Formats a number. * * http://snipplr.com/view/5945/javascript-numberformat--ported-from-php/ */ util.formatNumber = function(number, decimals, dec_point, thousands_sep) { // http://kevin.vanzonneveld.net // + original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // + bugfix by: Michael White (http://crestidg.com) // + bugfix by: Benjamin Lupton // + bugfix by: Allan Jensen (http://www.winternet.no) // + revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) // * example 1: number_format(1234.5678, 2, '.', ''); // * returns 1: 1234.57 var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; var d = dec_point === undefined ? ',' : dec_point; var t = thousands_sep === undefined ? '.' : thousands_sep, s = n < 0 ? '-' : ''; var i = parseInt((n = Math.abs(+n || 0).toFixed(c)), 10) + ''; var j = (i.length > 3) ? i.length % 3 : 0; return s + (j ? i.substr(0, j) + t : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ''); }; /** * Formats a byte size. * * http://snipplr.com/view/5949/format-humanize-file-byte-size-presentation-in-javascript/ */ util.formatSize = function(size) { if(size >= 1073741824) { size = util.formatNumber(size / 1073741824, 2, '.', '') + ' GiB'; } else if(size >= 1048576) { size = util.formatNumber(size / 1048576, 2, '.', '') + ' MiB'; } else if(size >= 1024) { size = util.formatNumber(size / 1024, 0) + ' KiB'; } else { size = util.formatNumber(size, 0) + ' bytes'; } return size; }; /** * Converts an IPv4 or IPv6 string representation into bytes (in network order). * * @param ip the IPv4 or IPv6 address to convert. * * @return the 4-byte IPv6 or 16-byte IPv6 address or null if the address can't * be parsed. */ util.bytesFromIP = function(ip) { if(ip.indexOf('.') !== -1) { return util.bytesFromIPv4(ip); } if(ip.indexOf(':') !== -1) { return util.bytesFromIPv6(ip); } return null; }; /** * Converts an IPv4 string representation into bytes (in network order). * * @param ip the IPv4 address to convert. * * @return the 4-byte address or null if the address can't be parsed. */ util.bytesFromIPv4 = function(ip) { ip = ip.split('.'); if(ip.length !== 4) { return null; } var b = util.createBuffer(); for(var i = 0; i < ip.length; ++i) { var num = parseInt(ip[i], 10); if(isNaN(num)) { return null; } b.putByte(num); } return b.getBytes(); }; /** * Converts an IPv6 string representation into bytes (in network order). * * @param ip the IPv6 address to convert. * * @return the 16-byte address or null if the address can't be parsed. */ util.bytesFromIPv6 = function(ip) { var blanks = 0; ip = ip.split(':').filter(function(e) { if(e.length === 0) ++blanks; return true; }); var zeros = (8 - ip.length + blanks) * 2; var b = util.createBuffer(); for(var i = 0; i < 8; ++i) { if(!ip[i] || ip[i].length === 0) { b.fillWithByte(0, zeros); zeros = 0; continue; } var bytes = util.hexToBytes(ip[i]); if(bytes.length < 2) { b.putByte(0); } b.putBytes(bytes); } return b.getBytes(); }; /** * Converts 4-bytes into an IPv4 string representation or 16-bytes into * an IPv6 string representation. The bytes must be in network order. * * @param bytes the bytes to convert. * * @return the IPv4 or IPv6 string representation if 4 or 16 bytes, * respectively, are given, otherwise null. */ util.bytesToIP = function(bytes) { if(bytes.length === 4) { return util.bytesToIPv4(bytes); } if(bytes.length === 16) { return util.bytesToIPv6(bytes); } return null; }; /** * Converts 4-bytes into an IPv4 string representation. The bytes must be * in network order. * * @param bytes the bytes to convert. * * @return the IPv4 string representation or null for an invalid # of bytes. */ util.bytesToIPv4 = function(bytes) { if(bytes.length !== 4) { return null; } var ip = []; for(var i = 0; i < bytes.length; ++i) { ip.push(bytes.charCodeAt(i)); } return ip.join('.'); }; /** * Converts 16-bytes into an IPv16 string representation. The bytes must be * in network order. * * @param bytes the bytes to convert. * * @return the IPv16 string representation or null for an invalid # of bytes. */ util.bytesToIPv6 = function(bytes) { if(bytes.length !== 16) { return null; } var ip = []; var zeroGroups = []; var zeroMaxGroup = 0; for(var i = 0; i < bytes.length; i += 2) { var hex = util.bytesToHex(bytes[i] + bytes[i + 1]); // canonicalize zero representation while(hex[0] === '0' && hex !== '0') { hex = hex.substr(1); } if(hex === '0') { var last = zeroGroups[zeroGroups.length - 1]; var idx = ip.length; if(!last || idx !== last.end + 1) { zeroGroups.push({start: idx, end: idx}); } else { last.end = idx; if((last.end - last.start) > (zeroGroups[zeroMaxGroup].end - zeroGroups[zeroMaxGroup].start)) { zeroMaxGroup = zeroGroups.length - 1; } } } ip.push(hex); } if(zeroGroups.length > 0) { var group = zeroGroups[zeroMaxGroup]; // only shorten group of length > 0 if(group.end - group.start > 0) { ip.splice(group.start, group.end - group.start + 1, ''); if(group.start === 0) { ip.unshift(''); } if(group.end === 7) { ip.push(''); } } } return ip.join(':'); }; /** * Estimates the number of processes that can be run concurrently. If * creating Web Workers, keep in mind that the main JavaScript process needs * its own core. * * @param options the options to use: * update true to force an update (not use the cached value). * @param callback(err, max) called once the operation completes. */ util.estimateCores = function(options, callback) { if(typeof options === 'function') { callback = options; options = {}; } options = options || {}; if('cores' in util && !options.update) { return callback(null, util.cores); } if(typeof navigator !== 'undefined' && 'hardwareConcurrency' in navigator && navigator.hardwareConcurrency > 0) { util.cores = navigator.hardwareConcurrency; return callback(null, util.cores); } if(typeof Worker === 'undefined') { // workers not available util.cores = 1; return callback(null, util.cores); } if(typeof Blob === 'undefined') { // can't estimate, default to 2 util.cores = 2; return callback(null, util.cores); } // create worker concurrency estimation code as blob var blobUrl = URL.createObjectURL(new Blob(['(', function() { self.addEventListener('message', function(e) { // run worker for 4 ms var st = Date.now(); var et = st + 4; while(Date.now() < et); self.postMessage({st: st, et: et}); }); }.toString(), ')()'], {type: 'application/javascript'})); // take 5 samples using 16 workers sample([], 5, 16); function sample(max, samples, numWorkers) { if(samples === 0) { // get overlap average var avg = Math.floor(max.reduce(function(avg, x) { return avg + x; }, 0) / max.length); util.cores = Math.max(1, avg); URL.revokeObjectURL(blobUrl); return callback(null, util.cores); } map(numWorkers, function(err, results) { max.push(reduce(numWorkers, results)); sample(max, samples - 1, numWorkers); }); } function map(numWorkers, callback) { var workers = []; var results = []; for(var i = 0; i < numWorkers; ++i) { var worker = new Worker(blobUrl); worker.addEventListener('message', function(e) { results.push(e.data); if(results.length === numWorkers) { for(var i = 0; i < numWorkers; ++i) { workers[i].terminate(); } callback(null, results); } }); workers.push(worker); } for(var i = 0; i < numWorkers; ++i) { workers[i].postMessage(i); } } function reduce(numWorkers, results) { // find overlapping time windows var overlaps = []; for(var n = 0; n < numWorkers; ++n) { var r1 = results[n]; var overlap = overlaps[n] = []; for(var i = 0; i < numWorkers; ++i) { if(n === i) { continue; } var r2 = results[i]; if((r1.st > r2.st && r1.st < r2.et) || (r2.st > r1.st && r2.st < r1.et)) { overlap.push(i); } } } // get maximum overlaps ... don't include overlapping worker itself // as the main JS process was also being scheduled during the work and // would have to be subtracted from the estimate anyway return overlaps.reduce(function(max, overlap) { return Math.max(max, overlap.length); }, 0); } }; } // end module implementation /* ########## Begin module wrapper ########## */ var name = 'util'; if(typeof define !== 'function') { // NodeJS -> AMD if(typeof module === 'object' && module.exports) { var nodeJS = true; define = function(ids, factory) { factory(require, module); }; } else { //