michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CommonUtils"]; michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm") michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: michael@0: this.CommonUtils = { michael@0: /* michael@0: * Set manipulation methods. These should be lifted into toolkit, or added to michael@0: * `Set` itself. michael@0: */ michael@0: michael@0: /** michael@0: * Return elements of `a` or `b`. michael@0: */ michael@0: union: function (a, b) { michael@0: let out = new Set(a); michael@0: for (let x of b) { michael@0: out.add(x); michael@0: } michael@0: return out; michael@0: }, michael@0: michael@0: /** michael@0: * Return elements of `a` that are not present in `b`. michael@0: */ michael@0: difference: function (a, b) { michael@0: let out = new Set(a); michael@0: for (let x of b) { michael@0: out.delete(x); michael@0: } michael@0: return out; michael@0: }, michael@0: michael@0: /** michael@0: * Return elements of `a` that are also in `b`. michael@0: */ michael@0: intersection: function (a, b) { michael@0: let out = new Set(); michael@0: for (let x of a) { michael@0: if (b.has(x)) { michael@0: out.add(x); michael@0: } michael@0: } michael@0: return out; michael@0: }, michael@0: michael@0: /** michael@0: * Return true if `a` and `b` are the same size, and michael@0: * every element of `a` is in `b`. michael@0: */ michael@0: setEqual: function (a, b) { michael@0: if (a.size != b.size) { michael@0: return false; michael@0: } michael@0: for (let x of a) { michael@0: if (!b.has(x)) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: // Import these from Log.jsm for backward compatibility michael@0: exceptionStr: Log.exceptionStr, michael@0: stackTrace: Log.stackTrace, michael@0: michael@0: /** michael@0: * Encode byte string as base64URL (RFC 4648). michael@0: * michael@0: * @param bytes michael@0: * (string) Raw byte string to encode. michael@0: * @param pad michael@0: * (bool) Whether to include padding characters (=). Defaults michael@0: * to true for historical reasons. michael@0: */ michael@0: encodeBase64URL: function encodeBase64URL(bytes, pad=true) { michael@0: let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g"); michael@0: michael@0: if (!pad) { michael@0: s = s.replace("=", "", "g"); michael@0: } michael@0: michael@0: return s; michael@0: }, michael@0: michael@0: /** michael@0: * Create a nsIURI instance from a string. michael@0: */ michael@0: makeURI: function makeURI(URIString) { michael@0: if (!URIString) michael@0: return null; michael@0: try { michael@0: return Services.io.newURI(URIString, null, null); michael@0: } catch (e) { michael@0: let log = Log.repository.getLogger("Common.Utils"); michael@0: log.debug("Could not create URI: " + CommonUtils.exceptionStr(e)); michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Execute a function on the next event loop tick. michael@0: * michael@0: * @param callback michael@0: * Function to invoke. michael@0: * @param thisObj [optional] michael@0: * Object to bind the callback to. michael@0: */ michael@0: nextTick: function nextTick(callback, thisObj) { michael@0: if (thisObj) { michael@0: callback = callback.bind(thisObj); michael@0: } michael@0: Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: /** michael@0: * Return a promise resolving on some later tick. michael@0: * michael@0: * This a wrapper around Promise.resolve() that prevents stack michael@0: * accumulation and prevents callers from accidentally relying on michael@0: * same-tick promise resolution. michael@0: */ michael@0: laterTickResolvingPromise: function (value, prototype) { michael@0: let deferred = Promise.defer(prototype); michael@0: this.nextTick(deferred.resolve.bind(deferred, value)); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Spin the event loop and return once the next tick is executed. michael@0: * michael@0: * This is an evil function and should not be used in production code. It michael@0: * exists in this module for ease-of-use. michael@0: */ michael@0: waitForNextTick: function waitForNextTick() { michael@0: let cb = Async.makeSyncCallback(); michael@0: this.nextTick(cb); michael@0: Async.waitForSyncCallback(cb); michael@0: michael@0: return; michael@0: }, michael@0: michael@0: /** michael@0: * Return a timer that is scheduled to call the callback after waiting the michael@0: * provided time or as soon as possible. The timer will be set as a property michael@0: * of the provided object with the given timer name. michael@0: */ michael@0: namedTimer: function namedTimer(callback, wait, thisObj, name) { michael@0: if (!thisObj || !name) { michael@0: throw "You must provide both an object and a property name for the timer!"; michael@0: } michael@0: michael@0: // Delay an existing timer if it exists michael@0: if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) { michael@0: thisObj[name].delay = wait; michael@0: return; michael@0: } michael@0: michael@0: // Create a special timer that we can add extra properties michael@0: let timer = {}; michael@0: timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: michael@0: // Provide an easy way to clear out the timer michael@0: timer.clear = function() { michael@0: thisObj[name] = null; michael@0: timer.cancel(); michael@0: }; michael@0: michael@0: // Initialize the timer with a smart callback michael@0: timer.initWithCallback({ michael@0: notify: function notify() { michael@0: // Clear out the timer once it's been triggered michael@0: timer.clear(); michael@0: callback.call(thisObj, timer); michael@0: } michael@0: }, wait, timer.TYPE_ONE_SHOT); michael@0: michael@0: return thisObj[name] = timer; michael@0: }, michael@0: michael@0: encodeUTF8: function encodeUTF8(str) { michael@0: try { michael@0: str = this._utf8Converter.ConvertFromUnicode(str); michael@0: return str + this._utf8Converter.Finish(); michael@0: } catch (ex) { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: decodeUTF8: function decodeUTF8(str) { michael@0: try { michael@0: str = this._utf8Converter.ConvertToUnicode(str); michael@0: return str + this._utf8Converter.Finish(); michael@0: } catch (ex) { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: byteArrayToString: function byteArrayToString(bytes) { michael@0: return [String.fromCharCode(byte) for each (byte in bytes)].join(""); michael@0: }, michael@0: michael@0: stringToByteArray: function stringToByteArray(bytesString) { michael@0: return [String.charCodeAt(byte) for each (byte in bytesString)]; michael@0: }, michael@0: michael@0: bytesAsHex: function bytesAsHex(bytes) { michael@0: return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2) michael@0: for (byte in bytes)].join(""); michael@0: }, michael@0: michael@0: stringAsHex: function stringAsHex(str) { michael@0: return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str)); michael@0: }, michael@0: michael@0: stringToBytes: function stringToBytes(str) { michael@0: return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str)); michael@0: }, michael@0: michael@0: hexToBytes: function hexToBytes(str) { michael@0: let bytes = []; michael@0: for (let i = 0; i < str.length - 1; i += 2) { michael@0: bytes.push(parseInt(str.substr(i, 2), 16)); michael@0: } michael@0: return String.fromCharCode.apply(String, bytes); michael@0: }, michael@0: michael@0: hexAsString: function hexAsString(hex) { michael@0: return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex)); michael@0: }, michael@0: michael@0: /** michael@0: * Base32 encode (RFC 4648) a string michael@0: */ michael@0: encodeBase32: function encodeBase32(bytes) { michael@0: const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; michael@0: let quanta = Math.floor(bytes.length / 5); michael@0: let leftover = bytes.length % 5; michael@0: michael@0: // Pad the last quantum with zeros so the length is a multiple of 5. michael@0: if (leftover) { michael@0: quanta += 1; michael@0: for (let i = leftover; i < 5; i++) michael@0: bytes += "\0"; michael@0: } michael@0: michael@0: // Chop the string into quanta of 5 bytes (40 bits). Each quantum michael@0: // is turned into 8 characters from the 32 character base. michael@0: let ret = ""; michael@0: for (let i = 0; i < bytes.length; i += 5) { michael@0: let c = [byte.charCodeAt() for each (byte in bytes.slice(i, i + 5))]; michael@0: ret += key[c[0] >> 3] michael@0: + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] michael@0: + key[(c[1] >> 1) & 0x1f] michael@0: + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] michael@0: + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] michael@0: + key[(c[3] >> 2) & 0x1f] michael@0: + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] michael@0: + key[c[4] & 0x1f]; michael@0: } michael@0: michael@0: switch (leftover) { michael@0: case 1: michael@0: return ret.slice(0, -6) + "======"; michael@0: case 2: michael@0: return ret.slice(0, -4) + "===="; michael@0: case 3: michael@0: return ret.slice(0, -3) + "==="; michael@0: case 4: michael@0: return ret.slice(0, -1) + "="; michael@0: default: michael@0: return ret; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Base32 decode (RFC 4648) a string. michael@0: */ michael@0: decodeBase32: function decodeBase32(str) { michael@0: const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; michael@0: michael@0: let padChar = str.indexOf("="); michael@0: let chars = (padChar == -1) ? str.length : padChar; michael@0: let bytes = Math.floor(chars * 5 / 8); michael@0: let blocks = Math.ceil(chars / 8); michael@0: michael@0: // Process a chunk of 5 bytes / 8 characters. michael@0: // The processing of this is known in advance, michael@0: // so avoid arithmetic! michael@0: function processBlock(ret, cOffset, rOffset) { michael@0: let c, val; michael@0: michael@0: // N.B., this relies on michael@0: // undefined | foo == foo. michael@0: function accumulate(val) { michael@0: ret[rOffset] |= val; michael@0: } michael@0: michael@0: function advance() { michael@0: c = str[cOffset++]; michael@0: if (!c || c == "" || c == "=") // Easier than range checking. michael@0: throw "Done"; // Will be caught far away. michael@0: val = key.indexOf(c); michael@0: if (val == -1) michael@0: throw "Unknown character in base32: " + c; michael@0: } michael@0: michael@0: // Handle a left shift, restricted to bytes. michael@0: function left(octet, shift) michael@0: (octet << shift) & 0xff; michael@0: michael@0: advance(); michael@0: accumulate(left(val, 3)); michael@0: advance(); michael@0: accumulate(val >> 2); michael@0: ++rOffset; michael@0: accumulate(left(val, 6)); michael@0: advance(); michael@0: accumulate(left(val, 1)); michael@0: advance(); michael@0: accumulate(val >> 4); michael@0: ++rOffset; michael@0: accumulate(left(val, 4)); michael@0: advance(); michael@0: accumulate(val >> 1); michael@0: ++rOffset; michael@0: accumulate(left(val, 7)); michael@0: advance(); michael@0: accumulate(left(val, 2)); michael@0: advance(); michael@0: accumulate(val >> 3); michael@0: ++rOffset; michael@0: accumulate(left(val, 5)); michael@0: advance(); michael@0: accumulate(val); michael@0: ++rOffset; michael@0: } michael@0: michael@0: // Our output. Define to be explicit (and maybe the compiler will be smart). michael@0: let ret = new Array(bytes); michael@0: let i = 0; michael@0: let cOff = 0; michael@0: let rOff = 0; michael@0: michael@0: for (; i < blocks; ++i) { michael@0: try { michael@0: processBlock(ret, cOff, rOff); michael@0: } catch (ex) { michael@0: // Handle the detection of padding. michael@0: if (ex == "Done") michael@0: break; michael@0: throw ex; michael@0: } michael@0: cOff += 8; michael@0: rOff += 5; michael@0: } michael@0: michael@0: // Slice in case our shift overflowed to the right. michael@0: return CommonUtils.byteArrayToString(ret.slice(0, bytes)); michael@0: }, michael@0: michael@0: /** michael@0: * Trim excess padding from a Base64 string and atob(). michael@0: * michael@0: * See bug 562431 comment 4. michael@0: */ michael@0: safeAtoB: function safeAtoB(b64) { michael@0: let len = b64.length; michael@0: let over = len % 4; michael@0: return over ? atob(b64.substr(0, len - over)) : atob(b64); michael@0: }, michael@0: michael@0: /** michael@0: * Parses a JSON file from disk using OS.File and promises. michael@0: * michael@0: * @param path the file to read. Will be passed to `OS.File.read()`. michael@0: * @return a promise that resolves to the JSON contents of the named file. michael@0: */ michael@0: readJSON: function(path) { michael@0: return OS.File.read(path, { encoding: "utf-8" }).then((data) => { michael@0: return JSON.parse(data); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Write a JSON object to the named file using OS.File and promises. michael@0: * michael@0: * @param contents a JS object. Will be serialized. michael@0: * @param path the path of the file to write. michael@0: * @return a promise, as produced by OS.File.writeAtomic. michael@0: */ michael@0: writeJSON: function(contents, path) { michael@0: let encoder = new TextEncoder(); michael@0: let array = encoder.encode(JSON.stringify(contents)); michael@0: return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"}); michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Ensure that the specified value is defined in integer milliseconds since michael@0: * UNIX epoch. michael@0: * michael@0: * This throws an error if the value is not an integer, is negative, or looks michael@0: * like seconds, not milliseconds. michael@0: * michael@0: * If the value is null or 0, no exception is raised. michael@0: * michael@0: * @param value michael@0: * Value to validate. michael@0: */ michael@0: ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) { michael@0: if (!value) { michael@0: return; michael@0: } michael@0: michael@0: if (!/^[0-9]+$/.test(value)) { michael@0: throw new Error("Timestamp value is not a positive integer: " + value); michael@0: } michael@0: michael@0: let intValue = parseInt(value, 10); michael@0: michael@0: if (!intValue) { michael@0: return; michael@0: } michael@0: michael@0: // Catch what looks like seconds, not milliseconds. michael@0: if (intValue < 10000000000) { michael@0: throw new Error("Timestamp appears to be in seconds: " + intValue); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Read bytes from an nsIInputStream into a string. michael@0: * michael@0: * @param stream michael@0: * (nsIInputStream) Stream to read from. michael@0: * @param count michael@0: * (number) Integer number of bytes to read. If not defined, or michael@0: * 0, all available input is read. michael@0: */ michael@0: readBytesFromInputStream: function readBytesFromInputStream(stream, count) { michael@0: let BinaryInputStream = Components.Constructor( michael@0: "@mozilla.org/binaryinputstream;1", michael@0: "nsIBinaryInputStream", michael@0: "setInputStream"); michael@0: if (!count) { michael@0: count = stream.available(); michael@0: } michael@0: michael@0: return new BinaryInputStream(stream).readBytes(count); michael@0: }, michael@0: michael@0: /** michael@0: * Generate a new UUID using nsIUUIDGenerator. michael@0: * michael@0: * Example value: "1e00a2e2-1570-443e-bf5e-000354124234" michael@0: * michael@0: * @return string A hex-formatted UUID string. michael@0: */ michael@0: generateUUID: function generateUUID() { michael@0: let uuid = Cc["@mozilla.org/uuid-generator;1"] michael@0: .getService(Ci.nsIUUIDGenerator) michael@0: .generateUUID() michael@0: .toString(); michael@0: michael@0: return uuid.substring(1, uuid.length - 1); michael@0: }, michael@0: michael@0: /** michael@0: * Obtain an epoch value from a preference. michael@0: * michael@0: * This reads a string preference and returns an integer. The string michael@0: * preference is expected to contain the integer milliseconds since epoch. michael@0: * For best results, only read preferences that have been saved with michael@0: * setDatePref(). michael@0: * michael@0: * We need to store times as strings because integer preferences are only michael@0: * 32 bits and likely overflow most dates. michael@0: * michael@0: * If the pref contains a non-integer value, the specified default value will michael@0: * be returned. michael@0: * michael@0: * @param branch michael@0: * (Preferences) Branch from which to retrieve preference. michael@0: * @param pref michael@0: * (string) The preference to read from. michael@0: * @param def michael@0: * (Number) The default value to use if the preference is not defined. michael@0: * @param log michael@0: * (Log.Logger) Logger to write warnings to. michael@0: */ michael@0: getEpochPref: function getEpochPref(branch, pref, def=0, log=null) { michael@0: if (!Number.isInteger(def)) { michael@0: throw new Error("Default value is not a number: " + def); michael@0: } michael@0: michael@0: let valueStr = branch.get(pref, null); michael@0: michael@0: if (valueStr !== null) { michael@0: let valueInt = parseInt(valueStr, 10); michael@0: if (Number.isNaN(valueInt)) { michael@0: if (log) { michael@0: log.warn("Preference value is not an integer. Using default. " + michael@0: pref + "=" + valueStr + " -> " + def); michael@0: } michael@0: michael@0: return def; michael@0: } michael@0: michael@0: return valueInt; michael@0: } michael@0: michael@0: return def; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a Date from a preference. michael@0: * michael@0: * This is a wrapper around getEpochPref. It converts the value to a Date michael@0: * instance and performs simple range checking. michael@0: * michael@0: * The range checking ensures the date is newer than the oldestYear michael@0: * parameter. michael@0: * michael@0: * @param branch michael@0: * (Preferences) Branch from which to read preference. michael@0: * @param pref michael@0: * (string) The preference from which to read. michael@0: * @param def michael@0: * (Number) The default value (in milliseconds) if the preference is michael@0: * not defined or invalid. michael@0: * @param log michael@0: * (Log.Logger) Logger to write warnings to. michael@0: * @param oldestYear michael@0: * (Number) Oldest year to accept in read values. michael@0: */ michael@0: getDatePref: function getDatePref(branch, pref, def=0, log=null, michael@0: oldestYear=2010) { michael@0: michael@0: let valueInt = this.getEpochPref(branch, pref, def, log); michael@0: let date = new Date(valueInt); michael@0: michael@0: if (valueInt == def || date.getFullYear() >= oldestYear) { michael@0: return date; michael@0: } michael@0: michael@0: if (log) { michael@0: log.warn("Unexpected old date seen in pref. Returning default: " + michael@0: pref + "=" + date + " -> " + def); michael@0: } michael@0: michael@0: return new Date(def); michael@0: }, michael@0: michael@0: /** michael@0: * Store a Date in a preference. michael@0: * michael@0: * This is the opposite of getDatePref(). The same notes apply. michael@0: * michael@0: * If the range check fails, an Error will be thrown instead of a default michael@0: * value silently being used. michael@0: * michael@0: * @param branch michael@0: * (Preference) Branch from which to read preference. michael@0: * @param pref michael@0: * (string) Name of preference to write to. michael@0: * @param date michael@0: * (Date) The value to save. michael@0: * @param oldestYear michael@0: * (Number) The oldest year to accept for values. michael@0: */ michael@0: setDatePref: function setDatePref(branch, pref, date, oldestYear=2010) { michael@0: if (date.getFullYear() < oldestYear) { michael@0: throw new Error("Trying to set " + pref + " to a very old time: " + michael@0: date + ". The current time is " + new Date() + michael@0: ". Is the system clock wrong?"); michael@0: } michael@0: michael@0: branch.set(pref, "" + date.getTime()); michael@0: }, michael@0: michael@0: /** michael@0: * Convert a string between two encodings. michael@0: * michael@0: * Output is only guaranteed if the input stream is composed of octets. If michael@0: * the input string has characters with values larger than 255, data loss michael@0: * will occur. michael@0: * michael@0: * The returned string is guaranteed to consist of character codes no greater michael@0: * than 255. michael@0: * michael@0: * @param s michael@0: * (string) The source string to convert. michael@0: * @param source michael@0: * (string) The current encoding of the string. michael@0: * @param dest michael@0: * (string) The target encoding of the string. michael@0: * michael@0: * @return string michael@0: */ michael@0: convertString: function convertString(s, source, dest) { michael@0: if (!s) { michael@0: throw new Error("Input string must be defined."); michael@0: } michael@0: michael@0: let is = Cc["@mozilla.org/io/string-input-stream;1"] michael@0: .createInstance(Ci.nsIStringInputStream); michael@0: is.setData(s, s.length); michael@0: michael@0: let listener = Cc["@mozilla.org/network/stream-loader;1"] michael@0: .createInstance(Ci.nsIStreamLoader); michael@0: michael@0: let result; michael@0: michael@0: listener.init({ michael@0: onStreamComplete: function onStreamComplete(loader, context, status, michael@0: length, data) { michael@0: result = String.fromCharCode.apply(this, data); michael@0: }, michael@0: }); michael@0: michael@0: let converter = this._converterService.asyncConvertData(source, dest, michael@0: listener, null); michael@0: converter.onStartRequest(null, null); michael@0: converter.onDataAvailable(null, null, is, 0, s.length); michael@0: converter.onStopRequest(null, null, null); michael@0: michael@0: return result; michael@0: }, michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() { michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: return converter; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() { michael@0: return Cc["@mozilla.org/streamConverters;1"] michael@0: .getService(Ci.nsIStreamConverterService); michael@0: });