Bug 888271 - Make test_AudioBufferSourceNodeOffset.html fuzzier.
[gecko.git] / services / common / utils.js
blobfb441cd65a7248b687fffead7fcfd82087e25926
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");
15 this.CommonUtils = {
16   exceptionStr: function exceptionStr(e) {
17     if (!e) {
18       return "" + e;
19     }
20     let message = e.message ? e.message : e;
21     return message + " " + CommonUtils.stackTrace(e);
22   },
24   stackTrace: function stackTrace(e) {
25     // Wrapped nsIException
26     if (e.location) {
27       let frame = e.location;
28       let output = [];
29       while (frame) {
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;
36         if (file){
37           str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
38         }
40         if (frame.lineNumber){
41           str += ":" + frame.lineNumber;
42         }
43         if (frame.name){
44           str = frame.name + "()@" + str;
45         }
47         if (str){
48           output.push(str);
49         }
50         frame = frame.caller;
51       }
52       return "Stack trace: " + output.join(" < ");
53     }
54     // Standard JS exception
55     if (e.stack){
56       return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
57         replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
58     }
60     return "No traceback available";
61   },
63   /**
64    * Encode byte string as base64URL (RFC 4648).
65    *
66    * @param bytes
67    *        (string) Raw byte string to encode.
68    * @param pad
69    *        (bool) Whether to include padding characters (=). Defaults
70    *        to true for historical reasons.
71    */
72   encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
73     let s = btoa(bytes).replace("+", "-", "g").replace("/", "_", "g");
75     if (!pad) {
76       s = s.replace("=", "");
77     }
79     return s;
80   },
82   /**
83    * Create a nsIURI instance from a string.
84    */
85   makeURI: function makeURI(URIString) {
86     if (!URIString)
87       return null;
88     try {
89       return Services.io.newURI(URIString, null, null);
90     } catch (e) {
91       let log = Log4Moz.repository.getLogger("Common.Utils");
92       log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
93       return null;
94     }
95   },
97   /**
98    * Execute a function on the next event loop tick.
99    *
100    * @param callback
101    *        Function to invoke.
102    * @param thisObj [optional]
103    *        Object to bind the callback to.
104    */
105   nextTick: function nextTick(callback, thisObj) {
106     if (thisObj) {
107       callback = callback.bind(thisObj);
108     }
109     Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
110   },
112   /**
113    * Return a promise resolving on some later tick.
114    *
115    * This a wrapper around Promise.resolve() that prevents stack
116    * accumulation and prevents callers from accidentally relying on
117    * same-tick promise resolution.
118    */
119   laterTickResolvingPromise: function (value, prototype) {
120     let deferred = Promise.defer(prototype);
121     this.nextTick(deferred.resolve.bind(deferred, value));
122     return deferred.promise;
123   },
125   /**
126    * Spin the event loop and return once the next tick is executed.
127    *
128    * This is an evil function and should not be used in production code. It
129    * exists in this module for ease-of-use.
130    */
131   waitForNextTick: function waitForNextTick() {
132     let cb = Async.makeSyncCallback();
133     this.nextTick(cb);
134     Async.waitForSyncCallback(cb);
136     return;
137   },
139   /**
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.
143    */
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!";
147     }
149     // Delay an existing timer if it exists
150     if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
151       thisObj[name].delay = wait;
152       return;
153     }
155     // Create a special timer that we can add extra properties
156     let timer = {};
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;
162       timer.cancel();
163     };
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
169         timer.clear();
170         callback.call(thisObj, timer);
171       }
172     }, wait, timer.TYPE_ONE_SHOT);
174     return thisObj[name] = timer;
175   },
177   encodeUTF8: function encodeUTF8(str) {
178     try {
179       str = this._utf8Converter.ConvertFromUnicode(str);
180       return str + this._utf8Converter.Finish();
181     } catch (ex) {
182       return null;
183     }
184   },
186   decodeUTF8: function decodeUTF8(str) {
187     try {
188       str = this._utf8Converter.ConvertToUnicode(str);
189       return str + this._utf8Converter.Finish();
190     } catch (ex) {
191       return null;
192     }
193   },
195   byteArrayToString: function byteArrayToString(bytes) {
196     return [String.fromCharCode(byte) for each (byte in bytes)].join("");
197   },
199   bytesAsHex: function bytesAsHex(bytes) {
200     let hex = "";
201     for (let i = 0; i < bytes.length; i++) {
202       hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2);
203     }
204     return hex;
205   },
207   /**
208    * Base32 encode (RFC 4648) a string
209    */
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.
216     if (leftover) {
217       quanta += 1;
218       for (let i = leftover; i < 5; i++)
219         bytes += "\0";
220     }
222     // Chop the string into quanta of 5 bytes (40 bits). Each quantum
223     // is turned into 8 characters from the 32 character base.
224     let ret = "";
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)]
234            + key[c[4] & 0x1f];
235     }
237     switch (leftover) {
238       case 1:
239         return ret.slice(0, -6) + "======";
240       case 2:
241         return ret.slice(0, -4) + "====";
242       case 3:
243         return ret.slice(0, -3) + "===";
244       case 4:
245         return ret.slice(0, -1) + "=";
246       default:
247         return ret;
248     }
249   },
251   /**
252    * Base32 decode (RFC 4648) a string.
253    */
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) {
266       let c, val;
268       // N.B., this relies on
269       //   undefined | foo == foo.
270       function accumulate(val) {
271         ret[rOffset] |= val;
272       }
274       function advance() {
275         c  = str[cOffset++];
276         if (!c || c == "" || c == "=") // Easier than range checking.
277           throw "Done";                // Will be caught far away.
278         val = key.indexOf(c);
279         if (val == -1)
280           throw "Unknown character in base32: " + c;
281       }
283       // Handle a left shift, restricted to bytes.
284       function left(octet, shift)
285         (octet << shift) & 0xff;
287       advance();
288       accumulate(left(val, 3));
289       advance();
290       accumulate(val >> 2);
291       ++rOffset;
292       accumulate(left(val, 6));
293       advance();
294       accumulate(left(val, 1));
295       advance();
296       accumulate(val >> 4);
297       ++rOffset;
298       accumulate(left(val, 4));
299       advance();
300       accumulate(val >> 1);
301       ++rOffset;
302       accumulate(left(val, 7));
303       advance();
304       accumulate(left(val, 2));
305       advance();
306       accumulate(val >> 3);
307       ++rOffset;
308       accumulate(left(val, 5));
309       advance();
310       accumulate(val);
311       ++rOffset;
312     }
314     // Our output. Define to be explicit (and maybe the compiler will be smart).
315     let ret  = new Array(bytes);
316     let i    = 0;
317     let cOff = 0;
318     let rOff = 0;
320     for (; i < blocks; ++i) {
321       try {
322         processBlock(ret, cOff, rOff);
323       } catch (ex) {
324         // Handle the detection of padding.
325         if (ex == "Done")
326           break;
327         throw ex;
328       }
329       cOff += 8;
330       rOff += 5;
331     }
333     // Slice in case our shift overflowed to the right.
334     return CommonUtils.byteArrayToString(ret.slice(0, bytes));
335   },
337   /**
338    * Trim excess padding from a Base64 string and atob().
339    *
340    * See bug 562431 comment 4.
341    */
342   safeAtoB: function safeAtoB(b64) {
343     let len = b64.length;
344     let over = len % 4;
345     return over ? atob(b64.substr(0, len - over)) : atob(b64);
346   },
348   /**
349    * Parses a JSON file from disk using OS.File and promises.
350    *
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.
353    */
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));
359     });
360   },
362   /**
363    * Write a JSON object to the named file using OS.File and promises.
364    *
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.
368    */
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"});
373   },
376   /**
377    * Ensure that the specified value is defined in integer milliseconds since
378    * UNIX epoch.
379    *
380    * This throws an error if the value is not an integer, is negative, or looks
381    * like seconds, not milliseconds.
382    *
383    * If the value is null or 0, no exception is raised.
384    *
385    * @param value
386    *        Value to validate.
387    */
388   ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
389     if (!value) {
390       return;
391     }
393     if (!/^[0-9]+$/.test(value)) {
394       throw new Error("Timestamp value is not a positive integer: " + value);
395     }
397     let intValue = parseInt(value, 10);
399     if (!intValue) {
400        return;
401     }
403     // Catch what looks like seconds, not milliseconds.
404     if (intValue < 10000000000) {
405       throw new Error("Timestamp appears to be in seconds: " + intValue);
406     }
407   },
409   /**
410    * Read bytes from an nsIInputStream into a string.
411    *
412    * @param stream
413    *        (nsIInputStream) Stream to read from.
414    * @param count
415    *        (number) Integer number of bytes to read. If not defined, or
416    *        0, all available input is read.
417    */
418   readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
419     let BinaryInputStream = Components.Constructor(
420         "@mozilla.org/binaryinputstream;1",
421         "nsIBinaryInputStream",
422         "setInputStream");
423     if (!count) {
424       count = stream.available();
425     }
427     return new BinaryInputStream(stream).readBytes(count);
428   },
430   /**
431    * Generate a new UUID using nsIUUIDGenerator.
432    *
433    * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
434    *
435    * @return string A hex-formatted UUID string.
436    */
437   generateUUID: function generateUUID() {
438     let uuid = Cc["@mozilla.org/uuid-generator;1"]
439                  .getService(Ci.nsIUUIDGenerator)
440                  .generateUUID()
441                  .toString();
443     return uuid.substring(1, uuid.length - 1);
444   },
446   /**
447    * Obtain an epoch value from a preference.
448    *
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
452    * setDatePref().
453    *
454    * We need to store times as strings because integer preferences are only
455    * 32 bits and likely overflow most dates.
456    *
457    * If the pref contains a non-integer value, the specified default value will
458    * be returned.
459    *
460    * @param branch
461    *        (Preferences) Branch from which to retrieve preference.
462    * @param pref
463    *        (string) The preference to read from.
464    * @param def
465    *        (Number) The default value to use if the preference is not defined.
466    * @param log
467    *        (Log4Moz.Logger) Logger to write warnings to.
468    */
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);
472     }
474     let valueStr = branch.get(pref, null);
476     if (valueStr !== null) {
477       let valueInt = parseInt(valueStr, 10);
478       if (Number.isNaN(valueInt)) {
479         if (log) {
480           log.warn("Preference value is not an integer. Using default. " +
481                    pref + "=" + valueStr + " -> " + def);
482         }
484         return def;
485       }
487       return valueInt;
488     }
490     return def;
491   },
493   /**
494    * Obtain a Date from a preference.
495    *
496    * This is a wrapper around getEpochPref. It converts the value to a Date
497    * instance and performs simple range checking.
498    *
499    * The range checking ensures the date is newer than the oldestYear
500    * parameter.
501    *
502    * @param branch
503    *        (Preferences) Branch from which to read preference.
504    * @param pref
505    *        (string) The preference from which to read.
506    * @param def
507    *        (Number) The default value (in milliseconds) if the preference is
508    *        not defined or invalid.
509    * @param log
510    *        (Log4Moz.Logger) Logger to write warnings to.
511    * @param oldestYear
512    *        (Number) Oldest year to accept in read values.
513    */
514   getDatePref: function getDatePref(branch, pref, def=0, log=null,
515                                     oldestYear=2010) {
517     let valueInt = this.getEpochPref(branch, pref, def, log);
518     let date = new Date(valueInt);
520     if (valueInt == def || date.getFullYear() >= oldestYear) {
521       return date;
522     }
524     if (log) {
525       log.warn("Unexpected old date seen in pref. Returning default: " +
526                pref + "=" + date + " -> " + def);
527     }
529     return new Date(def);
530   },
532   /**
533    * Store a Date in a preference.
534    *
535    * This is the opposite of getDatePref(). The same notes apply.
536    *
537    * If the range check fails, an Error will be thrown instead of a default
538    * value silently being used.
539    *
540    * @param branch
541    *        (Preference) Branch from which to read preference.
542    * @param pref
543    *        (string) Name of preference to write to.
544    * @param date
545    *        (Date) The value to save.
546    * @param oldestYear
547    *        (Number) The oldest year to accept for values.
548    */
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?");
554     }
556     branch.set(pref, "" + date.getTime());
557   },
559   /**
560    * Convert a string between two encodings.
561    *
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
564    * will occur.
565    *
566    * The returned string is guaranteed to consist of character codes no greater
567    * than 255.
568    *
569    * @param s
570    *        (string) The source string to convert.
571    * @param source
572    *        (string) The current encoding of the string.
573    * @param dest
574    *        (string) The target encoding of the string.
575    *
576    * @return string
577    */
578   convertString: function convertString(s, source, dest) {
579     if (!s) {
580       throw new Error("Input string must be defined.");
581     }
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);
590     let result;
592     listener.init({
593       onStreamComplete: function onStreamComplete(loader, context, status,
594                                                   length, data) {
595         result = String.fromCharCode.apply(this, data);
596       },
597     });
599     let converter = this._converterService.asyncConvertData(source, dest,
600                                                             listener, null);
601     converter.onStartRequest(null, null);
602     converter.onDataAvailable(null, null, is, 0, s.length);
603     converter.onStopRequest(null, null, null);
605     return result;
606   },
609 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
610   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
611                     .createInstance(Ci.nsIScriptableUnicodeConverter);
612   converter.charset = "UTF-8";
613   return converter;
616 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
617   return Cc["@mozilla.org/streamConverters;1"]
618            .getService(Ci.nsIStreamConverterService);