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://services-common/log4moz.js");
16 exceptionStr: function exceptionStr(e) {
20 let message = e.message ? e.message : e;
21 return message + " " + CommonUtils.stackTrace(e);
24 stackTrace: function stackTrace(e) {
25 // Wrapped nsIException
27 let frame = e.location;
30 // Works on frames or exceptions, munges file:// URIs to shorten the paths
31 // FIXME: filename munging is sort of hackish, might be confusing if
32 // there are multiple extensions with similar filenames
33 let str = "<file:unknown>";
35 let file = frame.filename || frame.fileName;
37 str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
40 if (frame.lineNumber){
41 str += ":" + frame.lineNumber;
44 str = frame.name + "()@" + str;
52 return "Stack trace: " + output.join(" < ");
54 // Standard JS exception
56 return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
57 replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
60 return "No traceback available";
64 * Encode byte string as base64URL (RFC 4648).
67 * (string) Raw byte string to encode.
69 * (bool) Whether to include padding characters (=). Defaults
70 * to true for historical reasons.
72 encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
73 let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g");
76 s = s.replace("=", "");
83 * Create a nsIURI instance from a string.
85 makeURI: function makeURI(URIString) {
89 return Services.io.newURI(URIString, null, null);
91 let log = Log4Moz.repository.getLogger("Common.Utils");
92 log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
98 * Execute a function on the next event loop tick.
101 * Function to invoke.
102 * @param thisObj [optional]
103 * Object to bind the callback to.
105 nextTick: function nextTick(callback, thisObj) {
107 callback = callback.bind(thisObj);
109 Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
113 * Return a promise resolving on some later tick.
115 * This a wrapper around Promise.resolve() that prevents stack
116 * accumulation and prevents callers from accidentally relying on
117 * same-tick promise resolution.
119 laterTickResolvingPromise: function (value, prototype) {
120 let deferred = Promise.defer(prototype);
121 this.nextTick(deferred.resolve.bind(deferred, value));
122 return deferred.promise;
126 * Spin the event loop and return once the next tick is executed.
128 * This is an evil function and should not be used in production code. It
129 * exists in this module for ease-of-use.
131 waitForNextTick: function waitForNextTick() {
132 let cb = Async.makeSyncCallback();
134 Async.waitForSyncCallback(cb);
140 * Return a timer that is scheduled to call the callback after waiting the
141 * provided time or as soon as possible. The timer will be set as a property
142 * of the provided object with the given timer name.
144 namedTimer: function namedTimer(callback, wait, thisObj, name) {
145 if (!thisObj || !name) {
146 throw "You must provide both an object and a property name for the timer!";
149 // Delay an existing timer if it exists
150 if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
151 thisObj[name].delay = wait;
155 // Create a special timer that we can add extra properties
157 timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
159 // Provide an easy way to clear out the timer
160 timer.clear = function() {
161 thisObj[name] = null;
165 // Initialize the timer with a smart callback
166 timer.initWithCallback({
167 notify: function notify() {
168 // Clear out the timer once it's been triggered
170 callback.call(thisObj, timer);
172 }, wait, timer.TYPE_ONE_SHOT);
174 return thisObj[name] = timer;
177 encodeUTF8: function encodeUTF8(str) {
179 str = this._utf8Converter.ConvertFromUnicode(str);
180 return str + this._utf8Converter.Finish();
186 decodeUTF8: function decodeUTF8(str) {
188 str = this._utf8Converter.ConvertToUnicode(str);
189 return str + this._utf8Converter.Finish();
195 byteArrayToString: function byteArrayToString(bytes) {
196 return [String.fromCharCode(byte) for each (byte in bytes)].join("");
199 bytesAsHex: function bytesAsHex(bytes) {
201 for (let i = 0; i < bytes.length; i++) {
202 hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2);
208 * Base32 encode (RFC 4648) a string
210 encodeBase32: function encodeBase32(bytes) {
211 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
212 let quanta = Math.floor(bytes.length / 5);
213 let leftover = bytes.length % 5;
215 // Pad the last quantum with zeros so the length is a multiple of 5.
218 for (let i = leftover; i < 5; i++)
222 // Chop the string into quanta of 5 bytes (40 bits). Each quantum
223 // is turned into 8 characters from the 32 character base.
225 for (let i = 0; i < bytes.length; i += 5) {
226 let c = [byte.charCodeAt() for each (byte in bytes.slice(i, i + 5))];
227 ret += key[c[0] >> 3]
228 + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)]
229 + key[(c[1] >> 1) & 0x1f]
230 + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)]
231 + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)]
232 + key[(c[3] >> 2) & 0x1f]
233 + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)]
239 return ret.slice(0, -6) + "======";
241 return ret.slice(0, -4) + "====";
243 return ret.slice(0, -3) + "===";
245 return ret.slice(0, -1) + "=";
252 * Base32 decode (RFC 4648) a string.
254 decodeBase32: function decodeBase32(str) {
255 const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
257 let padChar = str.indexOf("=");
258 let chars = (padChar == -1) ? str.length : padChar;
259 let bytes = Math.floor(chars * 5 / 8);
260 let blocks = Math.ceil(chars / 8);
262 // Process a chunk of 5 bytes / 8 characters.
263 // The processing of this is known in advance,
264 // so avoid arithmetic!
265 function processBlock(ret, cOffset, rOffset) {
268 // N.B., this relies on
269 // undefined | foo == foo.
270 function accumulate(val) {
276 if (!c || c == "" || c == "=") // Easier than range checking.
277 throw "Done"; // Will be caught far away.
278 val = key.indexOf(c);
280 throw "Unknown character in base32: " + c;
283 // Handle a left shift, restricted to bytes.
284 function left(octet, shift)
285 (octet << shift) & 0xff;
288 accumulate(left(val, 3));
290 accumulate(val >> 2);
292 accumulate(left(val, 6));
294 accumulate(left(val, 1));
296 accumulate(val >> 4);
298 accumulate(left(val, 4));
300 accumulate(val >> 1);
302 accumulate(left(val, 7));
304 accumulate(left(val, 2));
306 accumulate(val >> 3);
308 accumulate(left(val, 5));
314 // Our output. Define to be explicit (and maybe the compiler will be smart).
315 let ret = new Array(bytes);
320 for (; i < blocks; ++i) {
322 processBlock(ret, cOff, rOff);
324 // Handle the detection of padding.
333 // Slice in case our shift overflowed to the right.
334 return CommonUtils.byteArrayToString(ret.slice(0, bytes));
338 * Trim excess padding from a Base64 string and atob().
340 * See bug 562431 comment 4.
342 safeAtoB: function safeAtoB(b64) {
343 let len = b64.length;
345 return over ? atob(b64.substr(0, len - over)) : atob(b64);
349 * Parses a JSON file from disk using OS.File and promises.
351 * @param path the file to read. Will be passed to `OS.File.read()`.
352 * @return a promise that resolves to the JSON contents of the named file.
354 readJSON: function(path) {
355 let decoder = new TextDecoder();
356 let promise = OS.File.read(path);
357 return promise.then(function onSuccess(array) {
358 return JSON.parse(decoder.decode(array));
363 * Write a JSON object to the named file using OS.File and promises.
365 * @param contents a JS object. Will be serialized.
366 * @param path the path of the file to write.
367 * @return a promise, as produced by OS.File.writeAtomic.
369 writeJSON: function(contents, path) {
370 let encoder = new TextEncoder();
371 let array = encoder.encode(JSON.stringify(contents));
372 return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"});
377 * Ensure that the specified value is defined in integer milliseconds since
380 * This throws an error if the value is not an integer, is negative, or looks
381 * like seconds, not milliseconds.
383 * If the value is null or 0, no exception is raised.
388 ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
393 if (!/^[0-9]+$/.test(value)) {
394 throw new Error("Timestamp value is not a positive integer: " + value);
397 let intValue = parseInt(value, 10);
403 // Catch what looks like seconds, not milliseconds.
404 if (intValue < 10000000000) {
405 throw new Error("Timestamp appears to be in seconds: " + intValue);
410 * Read bytes from an nsIInputStream into a string.
413 * (nsIInputStream) Stream to read from.
415 * (number) Integer number of bytes to read. If not defined, or
416 * 0, all available input is read.
418 readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
419 let BinaryInputStream = Components.Constructor(
420 "@mozilla.org/binaryinputstream;1",
421 "nsIBinaryInputStream",
424 count = stream.available();
427 return new BinaryInputStream(stream).readBytes(count);
431 * Generate a new UUID using nsIUUIDGenerator.
433 * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
435 * @return string A hex-formatted UUID string.
437 generateUUID: function generateUUID() {
438 let uuid = Cc["@mozilla.org/uuid-generator;1"]
439 .getService(Ci.nsIUUIDGenerator)
443 return uuid.substring(1, uuid.length - 1);
447 * Obtain an epoch value from a preference.
449 * This reads a string preference and returns an integer. The string
450 * preference is expected to contain the integer milliseconds since epoch.
451 * For best results, only read preferences that have been saved with
454 * We need to store times as strings because integer preferences are only
455 * 32 bits and likely overflow most dates.
457 * If the pref contains a non-integer value, the specified default value will
461 * (Preferences) Branch from which to retrieve preference.
463 * (string) The preference to read from.
465 * (Number) The default value to use if the preference is not defined.
467 * (Log4Moz.Logger) Logger to write warnings to.
469 getEpochPref: function getEpochPref(branch, pref, def=0, log=null) {
470 if (!Number.isInteger(def)) {
471 throw new Error("Default value is not a number: " + def);
474 let valueStr = branch.get(pref, null);
476 if (valueStr !== null) {
477 let valueInt = parseInt(valueStr, 10);
478 if (Number.isNaN(valueInt)) {
480 log.warn("Preference value is not an integer. Using default. " +
481 pref + "=" + valueStr + " -> " + def);
494 * Obtain a Date from a preference.
496 * This is a wrapper around getEpochPref. It converts the value to a Date
497 * instance and performs simple range checking.
499 * The range checking ensures the date is newer than the oldestYear
503 * (Preferences) Branch from which to read preference.
505 * (string) The preference from which to read.
507 * (Number) The default value (in milliseconds) if the preference is
508 * not defined or invalid.
510 * (Log4Moz.Logger) Logger to write warnings to.
512 * (Number) Oldest year to accept in read values.
514 getDatePref: function getDatePref(branch, pref, def=0, log=null,
517 let valueInt = this.getEpochPref(branch, pref, def, log);
518 let date = new Date(valueInt);
520 if (valueInt == def || date.getFullYear() >= oldestYear) {
525 log.warn("Unexpected old date seen in pref. Returning default: " +
526 pref + "=" + date + " -> " + def);
529 return new Date(def);
533 * Store a Date in a preference.
535 * This is the opposite of getDatePref(). The same notes apply.
537 * If the range check fails, an Error will be thrown instead of a default
538 * value silently being used.
541 * (Preference) Branch from which to read preference.
543 * (string) Name of preference to write to.
545 * (Date) The value to save.
547 * (Number) The oldest year to accept for values.
549 setDatePref: function setDatePref(branch, pref, date, oldestYear=2010) {
550 if (date.getFullYear() < oldestYear) {
551 throw new Error("Trying to set " + pref + " to a very old time: " +
552 date + ". The current time is " + new Date() +
553 ". Is the system clock wrong?");
556 branch.set(pref, "" + date.getTime());
560 * Convert a string between two encodings.
562 * Output is only guaranteed if the input stream is composed of octets. If
563 * the input string has characters with values larger than 255, data loss
566 * The returned string is guaranteed to consist of character codes no greater
570 * (string) The source string to convert.
572 * (string) The current encoding of the string.
574 * (string) The target encoding of the string.
578 convertString: function convertString(s, source, dest) {
580 throw new Error("Input string must be defined.");
583 let is = Cc["@mozilla.org/io/string-input-stream;1"]
584 .createInstance(Ci.nsIStringInputStream);
585 is.setData(s, s.length);
587 let listener = Cc["@mozilla.org/network/stream-loader;1"]
588 .createInstance(Ci.nsIStreamLoader);
593 onStreamComplete: function onStreamComplete(loader, context, status,
595 result = String.fromCharCode.apply(this, data);
599 let converter = this._converterService.asyncConvertData(source, dest,
601 converter.onStartRequest(null, null);
602 converter.onDataAvailable(null, null, is, 0, s.length);
603 converter.onStopRequest(null, null, null);
609 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
610 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
611 .createInstance(Ci.nsIScriptableUnicodeConverter);
612 converter.charset = "UTF-8";
616 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
617 return Cc["@mozilla.org/streamConverters;1"]
618 .getService(Ci.nsIStreamConverterService);