Bumping manifests a=b2g-bump
[gecko.git] / services / common / utils.js
blob8ad3c90d1d2192250ce96a3b4a988b60932f193e
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");
15 this.CommonUtils = {
16   /*
17    * Set manipulation methods. These should be lifted into toolkit, or added to
18    * `Set` itself.
19    */
21   /**
22    * Return elements of `a` or `b`.
23    */
24   union: function (a, b) {
25     let out = new Set(a);
26     for (let x of b) {
27       out.add(x);
28     }
29     return out;
30   },
32   /**
33    * Return elements of `a` that are not present in `b`.
34    */
35   difference: function (a, b) {
36     let out = new Set(a);
37     for (let x of b) {
38       out.delete(x);
39     }
40     return out;
41   },
43   /**
44    * Return elements of `a` that are also in `b`.
45    */
46   intersection: function (a, b) {
47     let out = new Set();
48     for (let x of a) {
49       if (b.has(x)) {
50         out.add(x);
51       }
52     }
53     return out;
54   },
56   /**
57    * Return true if `a` and `b` are the same size, and
58    * every element of `a` is in `b`.
59    */
60   setEqual: function (a, b) {
61     if (a.size != b.size) {
62       return false;
63     }
64     for (let x of a) {
65       if (!b.has(x)) {
66         return false;
67       }
68     }
69     return true;
70   },
72   // Import these from Log.jsm for backward compatibility
73   exceptionStr: Log.exceptionStr,
74   stackTrace: Log.stackTrace,
76   /**
77    * Encode byte string as base64URL (RFC 4648).
78    *
79    * @param bytes
80    *        (string) Raw byte string to encode.
81    * @param pad
82    *        (bool) Whether to include padding characters (=). Defaults
83    *        to true for historical reasons.
84    */
85   encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
86     let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g");
88     if (!pad) {
89       s = s.replace("=", "", "g");
90     }
92     return s;
93   },
95   /**
96    * Create a nsIURI instance from a string.
97    */
98   makeURI: function makeURI(URIString) {
99     if (!URIString)
100       return null;
101     try {
102       return Services.io.newURI(URIString, null, null);
103     } catch (e) {
104       let log = Log.repository.getLogger("Common.Utils");
105       log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
106       return null;
107     }
108   },
110   /**
111    * Execute a function on the next event loop tick.
112    *
113    * @param callback
114    *        Function to invoke.
115    * @param thisObj [optional]
116    *        Object to bind the callback to.
117    */
118   nextTick: function nextTick(callback, thisObj) {
119     if (thisObj) {
120       callback = callback.bind(thisObj);
121     }
122     Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
123   },
125   /**
126    * Return a promise resolving on some later tick.
127    *
128    * This a wrapper around Promise.resolve() that prevents stack
129    * accumulation and prevents callers from accidentally relying on
130    * same-tick promise resolution.
131    */
132   laterTickResolvingPromise: function (value, prototype) {
133     let deferred = Promise.defer(prototype);
134     this.nextTick(deferred.resolve.bind(deferred, value));
135     return deferred.promise;
136   },
138   /**
139    * Spin the event loop and return once the next tick is executed.
140    *
141    * This is an evil function and should not be used in production code. It
142    * exists in this module for ease-of-use.
143    */
144   waitForNextTick: function waitForNextTick() {
145     let cb = Async.makeSyncCallback();
146     this.nextTick(cb);
147     Async.waitForSyncCallback(cb);
149     return;
150   },
152   /**
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.
156    */
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!";
160     }
162     // Delay an existing timer if it exists
163     if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
164       thisObj[name].delay = wait;
165       return;
166     }
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;
174       timer.cancel();
175     };
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
181         timer.clear();
182         callback.call(thisObj, timer);
183       }
184     }, wait, timer.TYPE_ONE_SHOT);
186     return thisObj[name] = timer;
187   },
189   encodeUTF8: function encodeUTF8(str) {
190     try {
191       str = this._utf8Converter.ConvertFromUnicode(str);
192       return str + this._utf8Converter.Finish();
193     } catch (ex) {
194       return null;
195     }
196   },
198   decodeUTF8: function decodeUTF8(str) {
199     try {
200       str = this._utf8Converter.ConvertToUnicode(str);
201       return str + this._utf8Converter.Finish();
202     } catch (ex) {
203       return null;
204     }
205   },
207   byteArrayToString: function byteArrayToString(bytes) {
208     return [String.fromCharCode(byte) for each (byte in bytes)].join("");
209   },
211   stringToByteArray: function stringToByteArray(bytesString) {
212     return [String.charCodeAt(byte) for each (byte in bytesString)];
213   },
215   bytesAsHex: function bytesAsHex(bytes) {
216     return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2)
217       for (byte in bytes)].join("");
218   },
220   stringAsHex: function stringAsHex(str) {
221     return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
222   },
224   stringToBytes: function stringToBytes(str) {
225     return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
226   },
228   hexToBytes: function hexToBytes(str) {
229     let bytes = [];
230     for (let i = 0; i < str.length - 1; i += 2) {
231       bytes.push(parseInt(str.substr(i, 2), 16));
232     }
233     return String.fromCharCode.apply(String, bytes);
234   },
236   hexAsString: function hexAsString(hex) {
237     return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
238   },
240   /**
241    * Base32 encode (RFC 4648) a string
242    */
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.
249     if (leftover) {
250       quanta += 1;
251       for (let i = leftover; i < 5; i++)
252         bytes += "\0";
253     }
255     // Chop the string into quanta of 5 bytes (40 bits). Each quantum
256     // is turned into 8 characters from the 32 character base.
257     let ret = "";
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)]
267            + key[c[4] & 0x1f];
268     }
270     switch (leftover) {
271       case 1:
272         return ret.slice(0, -6) + "======";
273       case 2:
274         return ret.slice(0, -4) + "====";
275       case 3:
276         return ret.slice(0, -3) + "===";
277       case 4:
278         return ret.slice(0, -1) + "=";
279       default:
280         return ret;
281     }
282   },
284   /**
285    * Base32 decode (RFC 4648) a string.
286    */
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) {
299       let c, val;
301       // N.B., this relies on
302       //   undefined | foo == foo.
303       function accumulate(val) {
304         ret[rOffset] |= val;
305       }
307       function advance() {
308         c  = str[cOffset++];
309         if (!c || c == "" || c == "=") // Easier than range checking.
310           throw "Done";                // Will be caught far away.
311         val = key.indexOf(c);
312         if (val == -1)
313           throw "Unknown character in base32: " + c;
314       }
316       // Handle a left shift, restricted to bytes.
317       function left(octet, shift)
318         (octet << shift) & 0xff;
320       advance();
321       accumulate(left(val, 3));
322       advance();
323       accumulate(val >> 2);
324       ++rOffset;
325       accumulate(left(val, 6));
326       advance();
327       accumulate(left(val, 1));
328       advance();
329       accumulate(val >> 4);
330       ++rOffset;
331       accumulate(left(val, 4));
332       advance();
333       accumulate(val >> 1);
334       ++rOffset;
335       accumulate(left(val, 7));
336       advance();
337       accumulate(left(val, 2));
338       advance();
339       accumulate(val >> 3);
340       ++rOffset;
341       accumulate(left(val, 5));
342       advance();
343       accumulate(val);
344       ++rOffset;
345     }
347     // Our output. Define to be explicit (and maybe the compiler will be smart).
348     let ret  = new Array(bytes);
349     let i    = 0;
350     let cOff = 0;
351     let rOff = 0;
353     for (; i < blocks; ++i) {
354       try {
355         processBlock(ret, cOff, rOff);
356       } catch (ex) {
357         // Handle the detection of padding.
358         if (ex == "Done")
359           break;
360         throw ex;
361       }
362       cOff += 8;
363       rOff += 5;
364     }
366     // Slice in case our shift overflowed to the right.
367     return CommonUtils.byteArrayToString(ret.slice(0, bytes));
368   },
370   /**
371    * Trim excess padding from a Base64 string and atob().
372    *
373    * See bug 562431 comment 4.
374    */
375   safeAtoB: function safeAtoB(b64) {
376     let len = b64.length;
377     let over = len % 4;
378     return over ? atob(b64.substr(0, len - over)) : atob(b64);
379   },
381   /**
382    * Parses a JSON file from disk using OS.File and promises.
383    *
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.
386    */
387   readJSON: function(path) {
388     return OS.File.read(path, { encoding: "utf-8" }).then((data) => {
389       return JSON.parse(data);
390     });
391   },
393   /**
394    * Write a JSON object to the named file using OS.File and promises.
395    *
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.
399    */
400   writeJSON: function(contents, path) {
401     let data = JSON.stringify(contents);
402     return OS.File.writeAtomic(path, data, {encoding: "utf-8", tmpPath: path + ".tmp"});
403   },
406   /**
407    * Ensure that the specified value is defined in integer milliseconds since
408    * UNIX epoch.
409    *
410    * This throws an error if the value is not an integer, is negative, or looks
411    * like seconds, not milliseconds.
412    *
413    * If the value is null or 0, no exception is raised.
414    *
415    * @param value
416    *        Value to validate.
417    */
418   ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
419     if (!value) {
420       return;
421     }
423     if (!/^[0-9]+$/.test(value)) {
424       throw new Error("Timestamp value is not a positive integer: " + value);
425     }
427     let intValue = parseInt(value, 10);
429     if (!intValue) {
430        return;
431     }
433     // Catch what looks like seconds, not milliseconds.
434     if (intValue < 10000000000) {
435       throw new Error("Timestamp appears to be in seconds: " + intValue);
436     }
437   },
439   /**
440    * Read bytes from an nsIInputStream into a string.
441    *
442    * @param stream
443    *        (nsIInputStream) Stream to read from.
444    * @param count
445    *        (number) Integer number of bytes to read. If not defined, or
446    *        0, all available input is read.
447    */
448   readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
449     let BinaryInputStream = Components.Constructor(
450         "@mozilla.org/binaryinputstream;1",
451         "nsIBinaryInputStream",
452         "setInputStream");
453     if (!count) {
454       count = stream.available();
455     }
457     return new BinaryInputStream(stream).readBytes(count);
458   },
460   /**
461    * Generate a new UUID using nsIUUIDGenerator.
462    *
463    * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
464    *
465    * @return string A hex-formatted UUID string.
466    */
467   generateUUID: function generateUUID() {
468     let uuid = Cc["@mozilla.org/uuid-generator;1"]
469                  .getService(Ci.nsIUUIDGenerator)
470                  .generateUUID()
471                  .toString();
473     return uuid.substring(1, uuid.length - 1);
474   },
476   /**
477    * Obtain an epoch value from a preference.
478    *
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
482    * setDatePref().
483    *
484    * We need to store times as strings because integer preferences are only
485    * 32 bits and likely overflow most dates.
486    *
487    * If the pref contains a non-integer value, the specified default value will
488    * be returned.
489    *
490    * @param branch
491    *        (Preferences) Branch from which to retrieve preference.
492    * @param pref
493    *        (string) The preference to read from.
494    * @param def
495    *        (Number) The default value to use if the preference is not defined.
496    * @param log
497    *        (Log.Logger) Logger to write warnings to.
498    */
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);
502     }
504     let valueStr = branch.get(pref, null);
506     if (valueStr !== null) {
507       let valueInt = parseInt(valueStr, 10);
508       if (Number.isNaN(valueInt)) {
509         if (log) {
510           log.warn("Preference value is not an integer. Using default. " +
511                    pref + "=" + valueStr + " -> " + def);
512         }
514         return def;
515       }
517       return valueInt;
518     }
520     return def;
521   },
523   /**
524    * Obtain a Date from a preference.
525    *
526    * This is a wrapper around getEpochPref. It converts the value to a Date
527    * instance and performs simple range checking.
528    *
529    * The range checking ensures the date is newer than the oldestYear
530    * parameter.
531    *
532    * @param branch
533    *        (Preferences) Branch from which to read preference.
534    * @param pref
535    *        (string) The preference from which to read.
536    * @param def
537    *        (Number) The default value (in milliseconds) if the preference is
538    *        not defined or invalid.
539    * @param log
540    *        (Log.Logger) Logger to write warnings to.
541    * @param oldestYear
542    *        (Number) Oldest year to accept in read values.
543    */
544   getDatePref: function getDatePref(branch, pref, def=0, log=null,
545                                     oldestYear=2010) {
547     let valueInt = this.getEpochPref(branch, pref, def, log);
548     let date = new Date(valueInt);
550     if (valueInt == def || date.getFullYear() >= oldestYear) {
551       return date;
552     }
554     if (log) {
555       log.warn("Unexpected old date seen in pref. Returning default: " +
556                pref + "=" + date + " -> " + def);
557     }
559     return new Date(def);
560   },
562   /**
563    * Store a Date in a preference.
564    *
565    * This is the opposite of getDatePref(). The same notes apply.
566    *
567    * If the range check fails, an Error will be thrown instead of a default
568    * value silently being used.
569    *
570    * @param branch
571    *        (Preference) Branch from which to read preference.
572    * @param pref
573    *        (string) Name of preference to write to.
574    * @param date
575    *        (Date) The value to save.
576    * @param oldestYear
577    *        (Number) The oldest year to accept for values.
578    */
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?");
584     }
586     branch.set(pref, "" + date.getTime());
587   },
589   /**
590    * Convert a string between two encodings.
591    *
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
594    * will occur.
595    *
596    * The returned string is guaranteed to consist of character codes no greater
597    * than 255.
598    *
599    * @param s
600    *        (string) The source string to convert.
601    * @param source
602    *        (string) The current encoding of the string.
603    * @param dest
604    *        (string) The target encoding of the string.
605    *
606    * @return string
607    */
608   convertString: function convertString(s, source, dest) {
609     if (!s) {
610       throw new Error("Input string must be defined.");
611     }
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);
620     let result;
622     listener.init({
623       onStreamComplete: function onStreamComplete(loader, context, status,
624                                                   length, data) {
625         result = String.fromCharCode.apply(this, data);
626       },
627     });
629     let converter = this._converterService.asyncConvertData(source, dest,
630                                                             listener, null);
631     converter.onStartRequest(null, null);
632     converter.onDataAvailable(null, null, is, 0, s.length);
633     converter.onStopRequest(null, null, null);
635     return result;
636   },
639 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
640   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
641                     .createInstance(Ci.nsIScriptableUnicodeConverter);
642   converter.charset = "UTF-8";
643   return converter;
646 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
647   return Cc["@mozilla.org/streamConverters;1"]
648            .getService(Ci.nsIStreamConverterService);