1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { Log } from "resource://gre/modules/Log.sys.mjs";
9 export var CommonUtils = {
11 * Set manipulation methods. These should be lifted into toolkit, or added to
16 * Return elements of `a` or `b`.
27 * Return elements of `a` that are not present in `b`.
38 * Return elements of `a` that are also in `b`.
51 * Return true if `a` and `b` are the same size, and
52 * every element of `a` is in `b`.
55 if (a.size != b.size) {
67 * Checks elements in two arrays for equality, as determined by the `===`
68 * operator. This function does not perform a deep comparison; see Sync's
69 * `Util.deepEquals` for that.
72 if (a.length !== b.length) {
75 for (let i = 0; i < a.length; i++) {
84 * Encode byte string as base64URL (RFC 4648).
87 * (string) Raw byte string to encode.
89 * (bool) Whether to include padding characters (=). Defaults
90 * to true for historical reasons.
92 encodeBase64URL: function encodeBase64URL(bytes, pad = true) {
93 let s = btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_");
96 return s.replace(/=+$/, "");
103 * Create a nsIURI instance from a string.
105 makeURI: function makeURI(URIString) {
110 return Services.io.newURI(URIString);
112 let log = Log.repository.getLogger("Common.Utils");
113 log.debug("Could not create URI", e);
119 * Execute a function on the next event loop tick.
122 * Function to invoke.
123 * @param thisObj [optional]
124 * Object to bind the callback to.
126 nextTick: function nextTick(callback, thisObj) {
128 callback = callback.bind(thisObj);
130 Services.tm.dispatchToMainThread(callback);
134 * Return a timer that is scheduled to call the callback after waiting the
135 * provided time or as soon as possible. The timer will be set as a property
136 * of the provided object with the given timer name.
138 namedTimer: function namedTimer(callback, wait, thisObj, name) {
139 if (!thisObj || !name) {
141 "You must provide both an object and a property name for the timer!"
145 // Delay an existing timer if it exists
146 if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
147 thisObj[name].delay = wait;
148 return thisObj[name];
151 // Create a special timer that we can add extra properties
152 let timer = Object.create(
153 Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
156 // Provide an easy way to clear out the timer
157 timer.clear = function () {
158 thisObj[name] = null;
162 // Initialize the timer with a smart callback
163 timer.initWithCallback(
165 notify: function notify() {
166 // Clear out the timer once it's been triggered
168 callback.call(thisObj, timer);
175 return (thisObj[name] = timer);
178 encodeUTF8: function encodeUTF8(str) {
180 str = this._utf8Converter.ConvertFromUnicode(str);
181 return str + this._utf8Converter.Finish();
187 decodeUTF8: function decodeUTF8(str) {
189 str = this._utf8Converter.ConvertToUnicode(str);
190 return str + this._utf8Converter.Finish();
196 byteArrayToString: function byteArrayToString(bytes) {
197 return bytes.map(byte => String.fromCharCode(byte)).join("");
200 stringToByteArray: function stringToByteArray(bytesString) {
201 return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0));
204 // A lot of Util methods work with byte strings instead of ArrayBuffers.
205 // A patch should address this problem, but in the meantime let's provide
206 // helpers method to convert byte strings to Uint8Array.
207 byteStringToArrayBuffer(byteString) {
208 if (byteString === undefined) {
209 return new Uint8Array();
211 const bytes = new Uint8Array(byteString.length);
212 for (let i = 0; i < byteString.length; ++i) {
213 bytes[i] = byteString.charCodeAt(i) & 0xff;
218 arrayBufferToByteString(buffer) {
219 return CommonUtils.byteArrayToString([...buffer]);
222 bufferToHex(buffer) {
223 return Array.prototype.map
224 .call(buffer, x => ("00" + x.toString(16)).slice(-2))
228 bytesAsHex: function bytesAsHex(bytes) {
230 for (let i = 0, len = bytes.length; i < len; i++) {
231 let c = (bytes[i].charCodeAt(0) & 0xff).toString(16);
240 stringAsHex: function stringAsHex(str) {
241 return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
244 stringToBytes: function stringToBytes(str) {
245 return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
248 hexToBytes: function hexToBytes(str) {
250 for (let i = 0; i < str.length - 1; i += 2) {
251 bytes.push(parseInt(str.substr(i, 2), 16));
253 return String.fromCharCode.apply(String, bytes);
256 hexToArrayBuffer(str) {
257 const octString = CommonUtils.hexToBytes(str);
258 return CommonUtils.byteStringToArrayBuffer(octString);
261 hexAsString: function hexAsString(hex) {
262 return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
265 base64urlToHex(b64str) {
266 return CommonUtils.bufferToHex(
267 new Uint8Array(ChromeUtils.base64URLDecode(b64str, { padding: "reject" }))
272 * Base32 encode (RFC 4648) a string
274 encodeBase32: function encodeBase32(bytes) {
275 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
276 let leftover = bytes.length % 5;
278 // Pad the last quantum with zeros so the length is a multiple of 5.
280 for (let i = leftover; i < 5; i++) {
285 // Chop the string into quanta of 5 bytes (40 bits). Each quantum
286 // is turned into 8 characters from the 32 character base.
288 for (let i = 0; i < bytes.length; i += 5) {
289 let c = Array.prototype.slice
290 .call(bytes.slice(i, i + 5))
291 .map(byte => byte.charCodeAt(0));
294 key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] +
295 key[(c[1] >> 1) & 0x1f] +
296 key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] +
297 key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] +
298 key[(c[3] >> 2) & 0x1f] +
299 key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] +
305 return ret.slice(0, -6) + "======";
307 return ret.slice(0, -4) + "====";
309 return ret.slice(0, -3) + "===";
311 return ret.slice(0, -1) + "=";
318 * Base32 decode (RFC 4648) a string.
320 decodeBase32: function decodeBase32(str) {
321 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
323 let padChar = str.indexOf("=");
324 let chars = padChar == -1 ? str.length : padChar;
325 let bytes = Math.floor((chars * 5) / 8);
326 let blocks = Math.ceil(chars / 8);
328 // Process a chunk of 5 bytes / 8 characters.
329 // The processing of this is known in advance,
330 // so avoid arithmetic!
331 function processBlock(ret, cOffset, rOffset) {
334 // N.B., this relies on
335 // undefined | foo == foo.
336 function accumulate(val) {
342 if (!c || c == "" || c == "=") {
343 // Easier than range checking.
344 throw new Error("Done");
345 } // Will be caught far away.
346 val = key.indexOf(c);
348 throw new Error(`Unknown character in base32: ${c}`);
352 // Handle a left shift, restricted to bytes.
353 function left(octet, shift) {
354 return (octet << shift) & 0xff;
358 accumulate(left(val, 3));
360 accumulate(val >> 2);
362 accumulate(left(val, 6));
364 accumulate(left(val, 1));
366 accumulate(val >> 4);
368 accumulate(left(val, 4));
370 accumulate(val >> 1);
372 accumulate(left(val, 7));
374 accumulate(left(val, 2));
376 accumulate(val >> 3);
378 accumulate(left(val, 5));
384 // Our output. Define to be explicit (and maybe the compiler will be smart).
385 let ret = new Array(bytes);
390 for (; i < blocks; ++i) {
392 processBlock(ret, cOff, rOff);
394 // Handle the detection of padding.
395 if (ex.message == "Done") {
404 // Slice in case our shift overflowed to the right.
405 return CommonUtils.byteArrayToString(ret.slice(0, bytes));
409 * Trim excess padding from a Base64 string and atob().
411 * See bug 562431 comment 4.
413 safeAtoB: function safeAtoB(b64) {
414 let len = b64.length;
416 return over ? atob(b64.substr(0, len - over)) : atob(b64);
420 * Ensure that the specified value is defined in integer milliseconds since
423 * This throws an error if the value is not an integer, is negative, or looks
424 * like seconds, not milliseconds.
426 * If the value is null or 0, no exception is raised.
431 ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
436 if (!/^[0-9]+$/.test(value)) {
437 throw new Error("Timestamp value is not a positive integer: " + value);
440 let intValue = parseInt(value, 10);
446 // Catch what looks like seconds, not milliseconds.
447 if (intValue < 10000000000) {
448 throw new Error("Timestamp appears to be in seconds: " + intValue);
453 * Read bytes from an nsIInputStream into a string.
456 * (nsIInputStream) Stream to read from.
458 * (number) Integer number of bytes to read. If not defined, or
459 * 0, all available input is read.
461 readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
462 let BinaryInputStream = Components.Constructor(
463 "@mozilla.org/binaryinputstream;1",
464 "nsIBinaryInputStream",
468 count = stream.available();
471 return new BinaryInputStream(stream).readBytes(count);
475 * Generate a new UUID using nsIUUIDGenerator.
477 * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
479 * @return string A hex-formatted UUID string.
481 generateUUID: function generateUUID() {
482 let uuid = Services.uuid.generateUUID().toString();
484 return uuid.substring(1, uuid.length - 1);
488 * Obtain an epoch value from a preference.
490 * This reads a string preference and returns an integer. The string
491 * preference is expected to contain the integer milliseconds since epoch.
492 * For best results, only read preferences that have been saved with
495 * We need to store times as strings because integer preferences are only
496 * 32 bits and likely overflow most dates.
498 * If the pref contains a non-integer value, the specified default value will
502 * (Preferences) Branch from which to retrieve preference.
504 * (string) The preference to read from.
506 * (Number) The default value to use if the preference is not defined.
508 * (Log.Logger) Logger to write warnings to.
510 getEpochPref: function getEpochPref(branch, pref, def = 0, log = null) {
511 if (!Number.isInteger(def)) {
512 throw new Error("Default value is not a number: " + def);
515 let valueStr = branch.getCharPref(pref, null);
517 if (valueStr !== null) {
518 let valueInt = parseInt(valueStr, 10);
519 if (Number.isNaN(valueInt)) {
522 "Preference value is not an integer. Using default. " +
541 * Obtain a Date from a preference.
543 * This is a wrapper around getEpochPref. It converts the value to a Date
544 * instance and performs simple range checking.
546 * The range checking ensures the date is newer than the oldestYear
550 * (Preferences) Branch from which to read preference.
552 * (string) The preference from which to read.
554 * (Number) The default value (in milliseconds) if the preference is
555 * not defined or invalid.
557 * (Log.Logger) Logger to write warnings to.
559 * (Number) Oldest year to accept in read values.
561 getDatePref: function getDatePref(
568 let valueInt = this.getEpochPref(branch, pref, def, log);
569 let date = new Date(valueInt);
571 if (valueInt == def || date.getFullYear() >= oldestYear) {
577 "Unexpected old date seen in pref. Returning default: " +
586 return new Date(def);
590 * Store a Date in a preference.
592 * This is the opposite of getDatePref(). The same notes apply.
594 * If the range check fails, an Error will be thrown instead of a default
595 * value silently being used.
598 * (Preference) Branch from which to read preference.
600 * (string) Name of preference to write to.
602 * (Date) The value to save.
604 * (Number) The oldest year to accept for values.
606 setDatePref: function setDatePref(branch, pref, date, oldestYear = 2010) {
607 if (date.getFullYear() < oldestYear) {
611 " to a very old time: " +
613 ". The current time is " +
615 ". Is the system clock wrong?"
619 branch.setCharPref(pref, "" + date.getTime());
623 * Convert a string between two encodings.
625 * Output is only guaranteed if the input stream is composed of octets. If
626 * the input string has characters with values larger than 255, data loss
629 * The returned string is guaranteed to consist of character codes no greater
633 * (string) The source string to convert.
635 * (string) The current encoding of the string.
637 * (string) The target encoding of the string.
641 convertString: function convertString(s, source, dest) {
643 throw new Error("Input string must be defined.");
646 let is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
647 Ci.nsIStringInputStream
649 is.setData(s, s.length);
651 let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
658 onStreamComplete: function onStreamComplete(
665 result = String.fromCharCode.apply(this, data);
669 let converter = this._converterService.asyncConvertData(
675 converter.onStartRequest(null, null);
676 converter.onDataAvailable(null, is, 0, s.length);
677 converter.onStopRequest(null, null, null);
683 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function () {
685 "@mozilla.org/intl/scriptableunicodeconverter"
686 ].createInstance(Ci.nsIScriptableUnicodeConverter);
687 converter.charset = "UTF-8";
691 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function () {
692 return Cc["@mozilla.org/streamConverters;1"].getService(
693 Ci.nsIStreamConverterService