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