services/common/utils.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 = {};
   170     timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   172     // Provide an easy way to clear out the timer
   173     timer.clear = function() {
   174       thisObj[name] = null;
   175       timer.cancel();
   176     };
   178     // Initialize the timer with a smart callback
   179     timer.initWithCallback({
   180       notify: function notify() {
   181         // Clear out the timer once it's been triggered
   182         timer.clear();
   183         callback.call(thisObj, timer);
   184       }
   185     }, wait, timer.TYPE_ONE_SHOT);
   187     return thisObj[name] = timer;
   188   },
   190   encodeUTF8: function encodeUTF8(str) {
   191     try {
   192       str = this._utf8Converter.ConvertFromUnicode(str);
   193       return str + this._utf8Converter.Finish();
   194     } catch (ex) {
   195       return null;
   196     }
   197   },
   199   decodeUTF8: function decodeUTF8(str) {
   200     try {
   201       str = this._utf8Converter.ConvertToUnicode(str);
   202       return str + this._utf8Converter.Finish();
   203     } catch (ex) {
   204       return null;
   205     }
   206   },
   208   byteArrayToString: function byteArrayToString(bytes) {
   209     return [String.fromCharCode(byte) for each (byte in bytes)].join("");
   210   },
   212   stringToByteArray: function stringToByteArray(bytesString) {
   213     return [String.charCodeAt(byte) for each (byte in bytesString)];
   214   },
   216   bytesAsHex: function bytesAsHex(bytes) {
   217     return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2)
   218       for (byte in bytes)].join("");
   219   },
   221   stringAsHex: function stringAsHex(str) {
   222     return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
   223   },
   225   stringToBytes: function stringToBytes(str) {
   226     return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
   227   },
   229   hexToBytes: function hexToBytes(str) {
   230     let bytes = [];
   231     for (let i = 0; i < str.length - 1; i += 2) {
   232       bytes.push(parseInt(str.substr(i, 2), 16));
   233     }
   234     return String.fromCharCode.apply(String, bytes);
   235   },
   237   hexAsString: function hexAsString(hex) {
   238     return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
   239   },
   241   /**
   242    * Base32 encode (RFC 4648) a string
   243    */
   244   encodeBase32: function encodeBase32(bytes) {
   245     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
   246     let quanta = Math.floor(bytes.length / 5);
   247     let leftover = bytes.length % 5;
   249     // Pad the last quantum with zeros so the length is a multiple of 5.
   250     if (leftover) {
   251       quanta += 1;
   252       for (let i = leftover; i < 5; i++)
   253         bytes += "\0";
   254     }
   256     // Chop the string into quanta of 5 bytes (40 bits). Each quantum
   257     // is turned into 8 characters from the 32 character base.
   258     let ret = "";
   259     for (let i = 0; i < bytes.length; i += 5) {
   260       let c = [byte.charCodeAt() for each (byte in bytes.slice(i, i + 5))];
   261       ret += key[c[0] >> 3]
   262            + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)]
   263            + key[(c[1] >> 1) & 0x1f]
   264            + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)]
   265            + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)]
   266            + key[(c[3] >> 2) & 0x1f]
   267            + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)]
   268            + key[c[4] & 0x1f];
   269     }
   271     switch (leftover) {
   272       case 1:
   273         return ret.slice(0, -6) + "======";
   274       case 2:
   275         return ret.slice(0, -4) + "====";
   276       case 3:
   277         return ret.slice(0, -3) + "===";
   278       case 4:
   279         return ret.slice(0, -1) + "=";
   280       default:
   281         return ret;
   282     }
   283   },
   285   /**
   286    * Base32 decode (RFC 4648) a string.
   287    */
   288   decodeBase32: function decodeBase32(str) {
   289     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
   291     let padChar = str.indexOf("=");
   292     let chars = (padChar == -1) ? str.length : padChar;
   293     let bytes = Math.floor(chars * 5 / 8);
   294     let blocks = Math.ceil(chars / 8);
   296     // Process a chunk of 5 bytes / 8 characters.
   297     // The processing of this is known in advance,
   298     // so avoid arithmetic!
   299     function processBlock(ret, cOffset, rOffset) {
   300       let c, val;
   302       // N.B., this relies on
   303       //   undefined | foo == foo.
   304       function accumulate(val) {
   305         ret[rOffset] |= val;
   306       }
   308       function advance() {
   309         c  = str[cOffset++];
   310         if (!c || c == "" || c == "=") // Easier than range checking.
   311           throw "Done";                // Will be caught far away.
   312         val = key.indexOf(c);
   313         if (val == -1)
   314           throw "Unknown character in base32: " + c;
   315       }
   317       // Handle a left shift, restricted to bytes.
   318       function left(octet, shift)
   319         (octet << shift) & 0xff;
   321       advance();
   322       accumulate(left(val, 3));
   323       advance();
   324       accumulate(val >> 2);
   325       ++rOffset;
   326       accumulate(left(val, 6));
   327       advance();
   328       accumulate(left(val, 1));
   329       advance();
   330       accumulate(val >> 4);
   331       ++rOffset;
   332       accumulate(left(val, 4));
   333       advance();
   334       accumulate(val >> 1);
   335       ++rOffset;
   336       accumulate(left(val, 7));
   337       advance();
   338       accumulate(left(val, 2));
   339       advance();
   340       accumulate(val >> 3);
   341       ++rOffset;
   342       accumulate(left(val, 5));
   343       advance();
   344       accumulate(val);
   345       ++rOffset;
   346     }
   348     // Our output. Define to be explicit (and maybe the compiler will be smart).
   349     let ret  = new Array(bytes);
   350     let i    = 0;
   351     let cOff = 0;
   352     let rOff = 0;
   354     for (; i < blocks; ++i) {
   355       try {
   356         processBlock(ret, cOff, rOff);
   357       } catch (ex) {
   358         // Handle the detection of padding.
   359         if (ex == "Done")
   360           break;
   361         throw ex;
   362       }
   363       cOff += 8;
   364       rOff += 5;
   365     }
   367     // Slice in case our shift overflowed to the right.
   368     return CommonUtils.byteArrayToString(ret.slice(0, bytes));
   369   },
   371   /**
   372    * Trim excess padding from a Base64 string and atob().
   373    *
   374    * See bug 562431 comment 4.
   375    */
   376   safeAtoB: function safeAtoB(b64) {
   377     let len = b64.length;
   378     let over = len % 4;
   379     return over ? atob(b64.substr(0, len - over)) : atob(b64);
   380   },
   382   /**
   383    * Parses a JSON file from disk using OS.File and promises.
   384    *
   385    * @param path the file to read. Will be passed to `OS.File.read()`.
   386    * @return a promise that resolves to the JSON contents of the named file.
   387    */
   388   readJSON: function(path) {
   389     return OS.File.read(path, { encoding: "utf-8" }).then((data) => {
   390       return JSON.parse(data);
   391     });
   392   },
   394   /**
   395    * Write a JSON object to the named file using OS.File and promises.
   396    *
   397    * @param contents a JS object. Will be serialized.
   398    * @param path the path of the file to write.
   399    * @return a promise, as produced by OS.File.writeAtomic.
   400    */
   401   writeJSON: function(contents, path) {
   402     let encoder = new TextEncoder();
   403     let array = encoder.encode(JSON.stringify(contents));
   404     return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"});
   405   },
   408   /**
   409    * Ensure that the specified value is defined in integer milliseconds since
   410    * UNIX epoch.
   411    *
   412    * This throws an error if the value is not an integer, is negative, or looks
   413    * like seconds, not milliseconds.
   414    *
   415    * If the value is null or 0, no exception is raised.
   416    *
   417    * @param value
   418    *        Value to validate.
   419    */
   420   ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
   421     if (!value) {
   422       return;
   423     }
   425     if (!/^[0-9]+$/.test(value)) {
   426       throw new Error("Timestamp value is not a positive integer: " + value);
   427     }
   429     let intValue = parseInt(value, 10);
   431     if (!intValue) {
   432        return;
   433     }
   435     // Catch what looks like seconds, not milliseconds.
   436     if (intValue < 10000000000) {
   437       throw new Error("Timestamp appears to be in seconds: " + intValue);
   438     }
   439   },
   441   /**
   442    * Read bytes from an nsIInputStream into a string.
   443    *
   444    * @param stream
   445    *        (nsIInputStream) Stream to read from.
   446    * @param count
   447    *        (number) Integer number of bytes to read. If not defined, or
   448    *        0, all available input is read.
   449    */
   450   readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
   451     let BinaryInputStream = Components.Constructor(
   452         "@mozilla.org/binaryinputstream;1",
   453         "nsIBinaryInputStream",
   454         "setInputStream");
   455     if (!count) {
   456       count = stream.available();
   457     }
   459     return new BinaryInputStream(stream).readBytes(count);
   460   },
   462   /**
   463    * Generate a new UUID using nsIUUIDGenerator.
   464    *
   465    * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
   466    *
   467    * @return string A hex-formatted UUID string.
   468    */
   469   generateUUID: function generateUUID() {
   470     let uuid = Cc["@mozilla.org/uuid-generator;1"]
   471                  .getService(Ci.nsIUUIDGenerator)
   472                  .generateUUID()
   473                  .toString();
   475     return uuid.substring(1, uuid.length - 1);
   476   },
   478   /**
   479    * Obtain an epoch value from a preference.
   480    *
   481    * This reads a string preference and returns an integer. The string
   482    * preference is expected to contain the integer milliseconds since epoch.
   483    * For best results, only read preferences that have been saved with
   484    * setDatePref().
   485    *
   486    * We need to store times as strings because integer preferences are only
   487    * 32 bits and likely overflow most dates.
   488    *
   489    * If the pref contains a non-integer value, the specified default value will
   490    * be returned.
   491    *
   492    * @param branch
   493    *        (Preferences) Branch from which to retrieve preference.
   494    * @param pref
   495    *        (string) The preference to read from.
   496    * @param def
   497    *        (Number) The default value to use if the preference is not defined.
   498    * @param log
   499    *        (Log.Logger) Logger to write warnings to.
   500    */
   501   getEpochPref: function getEpochPref(branch, pref, def=0, log=null) {
   502     if (!Number.isInteger(def)) {
   503       throw new Error("Default value is not a number: " + def);
   504     }
   506     let valueStr = branch.get(pref, null);
   508     if (valueStr !== null) {
   509       let valueInt = parseInt(valueStr, 10);
   510       if (Number.isNaN(valueInt)) {
   511         if (log) {
   512           log.warn("Preference value is not an integer. Using default. " +
   513                    pref + "=" + valueStr + " -> " + def);
   514         }
   516         return def;
   517       }
   519       return valueInt;
   520     }
   522     return def;
   523   },
   525   /**
   526    * Obtain a Date from a preference.
   527    *
   528    * This is a wrapper around getEpochPref. It converts the value to a Date
   529    * instance and performs simple range checking.
   530    *
   531    * The range checking ensures the date is newer than the oldestYear
   532    * parameter.
   533    *
   534    * @param branch
   535    *        (Preferences) Branch from which to read preference.
   536    * @param pref
   537    *        (string) The preference from which to read.
   538    * @param def
   539    *        (Number) The default value (in milliseconds) if the preference is
   540    *        not defined or invalid.
   541    * @param log
   542    *        (Log.Logger) Logger to write warnings to.
   543    * @param oldestYear
   544    *        (Number) Oldest year to accept in read values.
   545    */
   546   getDatePref: function getDatePref(branch, pref, def=0, log=null,
   547                                     oldestYear=2010) {
   549     let valueInt = this.getEpochPref(branch, pref, def, log);
   550     let date = new Date(valueInt);
   552     if (valueInt == def || date.getFullYear() >= oldestYear) {
   553       return date;
   554     }
   556     if (log) {
   557       log.warn("Unexpected old date seen in pref. Returning default: " +
   558                pref + "=" + date + " -> " + def);
   559     }
   561     return new Date(def);
   562   },
   564   /**
   565    * Store a Date in a preference.
   566    *
   567    * This is the opposite of getDatePref(). The same notes apply.
   568    *
   569    * If the range check fails, an Error will be thrown instead of a default
   570    * value silently being used.
   571    *
   572    * @param branch
   573    *        (Preference) Branch from which to read preference.
   574    * @param pref
   575    *        (string) Name of preference to write to.
   576    * @param date
   577    *        (Date) The value to save.
   578    * @param oldestYear
   579    *        (Number) The oldest year to accept for values.
   580    */
   581   setDatePref: function setDatePref(branch, pref, date, oldestYear=2010) {
   582     if (date.getFullYear() < oldestYear) {
   583       throw new Error("Trying to set " + pref + " to a very old time: " +
   584                       date + ". The current time is " + new Date() +
   585                       ". Is the system clock wrong?");
   586     }
   588     branch.set(pref, "" + date.getTime());
   589   },
   591   /**
   592    * Convert a string between two encodings.
   593    *
   594    * Output is only guaranteed if the input stream is composed of octets. If
   595    * the input string has characters with values larger than 255, data loss
   596    * will occur.
   597    *
   598    * The returned string is guaranteed to consist of character codes no greater
   599    * than 255.
   600    *
   601    * @param s
   602    *        (string) The source string to convert.
   603    * @param source
   604    *        (string) The current encoding of the string.
   605    * @param dest
   606    *        (string) The target encoding of the string.
   607    *
   608    * @return string
   609    */
   610   convertString: function convertString(s, source, dest) {
   611     if (!s) {
   612       throw new Error("Input string must be defined.");
   613     }
   615     let is = Cc["@mozilla.org/io/string-input-stream;1"]
   616                .createInstance(Ci.nsIStringInputStream);
   617     is.setData(s, s.length);
   619     let listener = Cc["@mozilla.org/network/stream-loader;1"]
   620                      .createInstance(Ci.nsIStreamLoader);
   622     let result;
   624     listener.init({
   625       onStreamComplete: function onStreamComplete(loader, context, status,
   626                                                   length, data) {
   627         result = String.fromCharCode.apply(this, data);
   628       },
   629     });
   631     let converter = this._converterService.asyncConvertData(source, dest,
   632                                                             listener, null);
   633     converter.onStartRequest(null, null);
   634     converter.onDataAvailable(null, null, is, 0, s.length);
   635     converter.onStopRequest(null, null, null);
   637     return result;
   638   },
   639 };
   641 XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
   642   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   643                     .createInstance(Ci.nsIScriptableUnicodeConverter);
   644   converter.charset = "UTF-8";
   645   return converter;
   646 });
   648 XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
   649   return Cc["@mozilla.org/streamConverters;1"]
   650            .getService(Ci.nsIStreamConverterService);
   651 });

mercurial