summaryrefslogtreecommitdiffstats
path: root/services/common/utils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/common/utils.sys.mjs695
1 files changed, 695 insertions, 0 deletions
diff --git a/services/common/utils.sys.mjs b/services/common/utils.sys.mjs
new file mode 100644
index 0000000000..3c7f9f9d39
--- /dev/null
+++ b/services/common/utils.sys.mjs
@@ -0,0 +1,695 @@
+/* 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
+ );
+});