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 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
7 this.EXPORTED_SYMBOLS = ["CommonUtils"];
9 Cu.import("resource://gre/modules/Promise.jsm");
10 Cu.import("resource://gre/modules/Services.jsm");
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/osfile.jsm")
13 Cu.import("resource://gre/modules/Log.jsm");
17 * Set manipulation methods. These should be lifted into toolkit, or added to
22 * Return elements of `a` or `b`.
24 union: function (a, b) {
33 * Return elements of `a` that are not present in `b`.
35 difference: function (a, b) {
44 * Return elements of `a` that are also in `b`.
46 intersection: function (a, b) {
57 * Return true if `a` and `b` are the same size, and
58 * every element of `a` is in `b`.
60 setEqual: function (a, b) {
61 if (a.size != b.size) {
72 // Import these from Log.jsm for backward compatibility
73 exceptionStr: Log.exceptionStr,
74 stackTrace: Log.stackTrace,
77 * Encode byte string as base64URL (RFC 4648).
80 * (string) Raw byte string to encode.
82 * (bool) Whether to include padding characters (=). Defaults
83 * to true for historical reasons.
85 encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
86 let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g");
89 s = s.replace("=", "", "g");
96 * Create a nsIURI instance from a string.
98 makeURI: function makeURI(URIString) {
102 return Services.io.newURI(URIString, null, null);
104 let log = Log.repository.getLogger("Common.Utils");
105 log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
111 * Execute a function on the next event loop tick.
114 * Function to invoke.
115 * @param thisObj [optional]
116 * Object to bind the callback to.
118 nextTick: function nextTick(callback, thisObj) {
120 callback = callback.bind(thisObj);
122 Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
126 * Return a promise resolving on some later tick.
128 * This a wrapper around Promise.resolve() that prevents stack
129 * accumulation and prevents callers from accidentally relying on
130 * same-tick promise resolution.
132 laterTickResolvingPromise: function (value, prototype) {
133 let deferred = Promise.defer(prototype);
134 this.nextTick(deferred.resolve.bind(deferred, value));
135 return deferred.promise;
139 * Spin the event loop and return once the next tick is executed.
141 * This is an evil function and should not be used in production code. It
142 * exists in this module for ease-of-use.
144 waitForNextTick: function waitForNextTick() {
145 let cb = Async.makeSyncCallback();
147 Async.waitForSyncCallback(cb);
153 * Return a timer that is scheduled to call the callback after waiting the
154 * provided time or as soon as possible. The timer will be set as a property
155 * of the provided object with the given timer name.
157 namedTimer: function namedTimer(callback, wait, thisObj, name) {
158 if (!thisObj || !name) {
159 throw "You must provide both an object and a property name for the timer!";
162 // Delay an existing timer if it exists
163 if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
164 thisObj[name].delay = wait;
168 // Create a special timer that we can add extra properties
169 let timer = Object.create(Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer));
171 // Provide an easy way to clear out the timer
172 timer.clear = function() {
173 thisObj[name] = null;
177 // Initialize the timer with a smart callback
178 timer.initWithCallback({
179 notify: function notify() {
180 // Clear out the timer once it's been triggered
182 callback.call(thisObj, timer);
184 }, wait, timer.TYPE_ONE_SHOT);
186 return thisObj[name] = timer;
189 encodeUTF8: function encodeUTF8(str) {
191 str = this._utf8Converter.ConvertFromUnicode(str);
192 return str + this._utf8Converter.Finish();
198 decodeUTF8: function decodeUTF8(str) {
200 str = this._utf8Converter.ConvertToUnicode(str);
201 return str + this._utf8Converter.Finish();
207 byteArrayToString: function byteArrayToString(bytes) {
208 return [String.fromCharCode(byte) for each (byte in bytes)].join("");
211 stringToByteArray: function stringToByteArray(bytesString) {
212 return [String.charCodeAt(byte) for each (byte in bytesString)];
215 bytesAsHex: function bytesAsHex(bytes) {
216 return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2)
217 for (byte in bytes)].join("");
220 stringAsHex: function stringAsHex(str) {
221 return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
224 stringToBytes: function stringToBytes(str) {
225 return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
228 hexToBytes: function hexToBytes(str) {
230 for (let i = 0; i < str.length - 1; i += 2) {
231 bytes.push(parseInt(str.substr(i, 2), 16));
233 return String.fromCharCode.apply(String, bytes);
236 hexAsString: function hexAsString(hex) {
237 return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
241 * Base32 encode (RFC 4648) a string
243 encodeBase32: function encodeBase32(bytes) {
244 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
245 let quanta = Math.floor(bytes.length / 5);
246 let leftover = bytes.length % 5;
248 // Pad the last quantum with zeros so the length is a multiple of 5.
251 for (let i = leftover; i < 5; i++)
255 // Chop the string into quanta of 5 bytes (40 bits). Each quantum
256 // is turned into 8 characters from the 32 character base.
258 for (let i = 0; i < bytes.length; i += 5) {
259 let c = [byte.charCodeAt() for each (byte in bytes.slice(i, i + 5))];
260 ret += key[c[0] >> 3]
261 + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)]
262 + key[(c[1] >> 1) & 0x1f]
263 + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)]
264 + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)]
265 + key[(c[3] >> 2) & 0x1f]
266 + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)]
272 return ret.slice(0, -6) + "======";
274 return ret.slice(0, -4) + "====";
276 return ret.slice(0, -3) + "===";
278 return ret.slice(0, -1) + "=";
285 * Base32 decode (RFC 4648) a string.
287 decodeBase32: function decodeBase32(str) {
288 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
290 let padChar = str.indexOf("=");
291 let chars = (padChar == -1) ? str.length : padChar;
292 let bytes = Math.floor(chars * 5 / 8);
293 let blocks = Math.ceil(chars / 8);
295 // Process a chunk of 5 bytes / 8 characters.
296 // The processing of this is known in advance,
297 // so avoid arithmetic!
298 function processBlock(ret, cOffset, rOffset) {
301 // N.B., this relies on
302 // undefined | foo == foo.
303 function accumulate(val) {
309 if (!c || c == "" || c == "=") // Easier than range checking.
310 throw "Done"; // Will be caught far away.
311 val = key.indexOf(c);
313 throw "Unknown character in base32: " + c;
316 // Handle a left shift, restricted to bytes.
317 function left(octet, shift)
318 (octet << shift) & 0xff;
321 accumulate(left(val, 3));
323 accumulate(val >> 2);
325 accumulate(left(val, 6));
327 accumulate(left(val, 1));
329 accumulate(val >> 4);
331 accumulate(left(val, 4));
333 accumulate(val >> 1);
335 accumulate(left(val, 7));
337 accumulate(left(val, 2));
339 accumulate(val >> 3);
341 accumulate(left(val, 5));
347 // Our output. Define to be explicit (and maybe the compiler will be smart).
348 let ret = new Array(bytes);
353 for (; i < blocks; ++i) {
355 processBlock(ret, cOff, rOff);
357 // Handle the detection of padding.
366 // Slice in case our shift overflowed to the right.
367 return CommonUtils.byteArrayToString(ret.slice(0, bytes));
371 * Trim excess padding from a Base64 string and atob().
373 * See bug 562431 comment 4.
375 safeAtoB: function safeAtoB(b64) {
376 let len = b64.length;
378 return over ? atob(b64.substr(0, len - over)) : atob(b64);
382 * Parses a JSON file from disk using OS.File and promises.
384 * @param path the file to read. Will be passed to `OS.File.read()`.
385 * @return a promise that resolves to the JSON contents of the named file.
387 readJSON: function(path) {
388 return OS.File.read(path, { encoding: "utf-8" }).then((data) => {
389 return JSON.parse(data);
394 * Write a JSON object to the named file using OS.File and promises.
396 * @param contents a JS object. Will be serialized.
397 * @param path the path of the file to write.
398 * @return a promise, as produced by OS.File.writeAtomic.
400 writeJSON: function(contents, path) {
401 let data = JSON.stringify(contents);
402 return OS.File.writeAtomic(path, data, {encoding: "utf-8", tmpPath: path + ".tmp"});
407 * Ensure that the specified value is defined in integer milliseconds since
410 * This throws an error if the value is not an integer, is negative, or looks
411 * like seconds, not milliseconds.
413 * If the value is null or 0, no exception is raised.
418 ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
423 if (!/^[0-9]+$/.test(value)) {
424 throw new Error("Timestamp value is not a positive integer: " + value);
427 let intValue = parseInt(value, 10);
433 // Catch what looks like seconds, not milliseconds.
434 if (intValue < 10000000000) {
435 throw new Error("Timestamp appears to be in seconds: " + intValue);
440 * Read bytes from an nsIInputStream into a string.
443 * (nsIInputStream) Stream to read from.
445 * (number) Integer number of bytes to read. If not defined, or
446 * 0, all available input is read.
448 readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
449 let BinaryInputStream = Components.Constructor(
450 "@mozilla.org/binaryinputstream;1",
451 "nsIBinaryInputStream",
454 count = stream.available();
457 return new BinaryInputStream(stream).readBytes(count);
461 * Generate a new UUID using nsIUUIDGenerator.
463 * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
465 * @return string A hex-formatted UUID string.
467 generateUUID: function generateUUID() {
468 let uuid = Cc["@mozilla.org/uuid-generator;1"]
469 .getService(Ci.nsIUUIDGenerator)
473 return uuid.substring(1, uuid.length - 1);
477 * Obtain an epoch value from a preference.
479 * This reads a string preference and returns an integer. The string
480 * preference is expected to contain the integer milliseconds since epoch.
481 * For best results, only read preferences that have been saved with
484 * We need to store times as strings because integer preferences are only
485 * 32 bits and likely overflow most dates.
487 * If the pref contains a non-integer value, the specified default value will
491 * (Preferences) Branch from which to retrieve preference.
493 * (string) The preference to read from.
495 * (Number) The default value to use if the preference is not defined.
497 * (Log.Logger) Logger to write warnings to.
499 getEpochPref: function getEpochPref(branch, pref, def=0, log=null) {
500 if (!Number.isInteger(def)) {
501 throw new Error("Default value is not a number: " + def);
504 let valueStr = branch.get(pref, null);
506 if (valueStr !== null) {
507 let valueInt = parseInt(valueStr, 10);
508 if (Number.isNaN(valueInt)) {
510 log.warn("Preference value is not an integer. Using default. " +
511 pref + "=" + valueStr + " -> " + def);
524 * Obtain a Date from a preference.
526 * This is a wrapper around getEpochPref. It converts the value to a Date
527 * instance and performs simple range checking.
529 * The range checking ensures the date is newer than the oldestYear
533 * (Preferences) Branch from which to read preference.
535 * (string) The preference from which to read.
537 * (Number) The default value (in milliseconds) if the preference is
538 * not defined or invalid.
540 * (Log.Logger) Logger to write warnings to.
542 * (Number) Oldest year to accept in read values.
544 getDatePref: function getDatePref(branch, pref, def=0, log=null,
547 let valueInt = this.getEpochPref(branch, pref, def, log);
548 let date = new Date(valueInt);
550 if (valueInt == def || date.getFullYear() >= oldestYear) {
555 log.warn("Unexpected old date seen in pref. Returning default: " +
556 pref + "=" + date + " -> " + def);
559 return new Date(def);
563 * Store a Date in a preference.
565 * This is the opposite of getDatePref(). The same notes apply.
567 * If the range check fails, an Error will be thrown instead of a default
568 * value silently being used.
571 * (Preference) Branch from which to read preference.
573 * (string) Name of preference to write to.
575 * (Date) The value to save.
577 * (Number) The oldest year to accept for values.
579 setDatePref: function setDatePref(branch, pref, date, oldestYear=2010) {
580 if (date.getFullYear() < oldestYear) {
581 throw new Error("Trying to set " + pref + " to a very old time: " +
582 date + ". The current time is " + new Date() +
583 ". Is the system clock wrong?");
586 branch.set(pref, "" + date.getTime());
590 * Convert a string between two encodings.
592 * Output is only guaranteed if the input stream is composed of octets. If
593 * the input string has characters with values larger than 255, data loss
596 * The returned string is guaranteed to consist of character codes no greater
600 * (string) The source string to convert.
602 * (string) The current encoding of the string.
604 * (string) The target encoding of the string.
608 convertString: function convertString(s, source, dest) {
610 throw new Error("Input string must be defined.");
613 let is = Cc["@mozilla.org/io/string-input-stream;1"]
614 .createInstance(Ci.nsIStringInputStream);
615 is.setData(s, s.length);
617 let listener = Cc["@mozilla.org/network/stream-loader;1"]
618 .createInstance(Ci.nsIStreamLoader);
623 onStreamComplete: function onStreamComplete(loader, context, status,
625 result = String.fromCharCode.apply(this, data);
629 let converter = this._converterService.asyncConvertData(source, dest,
631 converter.onStartRequest(null, null);
632 converter.onDataAvailable(null, null, is, 0, s.length);
633 converter.onStopRequest(null, null, null);
639 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
640 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
641 .createInstance(Ci.nsIScriptableUnicodeConverter);
642 converter.charset = "UTF-8";
646 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
647 return Cc["@mozilla.org/streamConverters;1"]
648 .getService(Ci.nsIStreamConverterService);