tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

utils.sys.mjs (18635B)


      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/. */
      4 
      5 import { Log } from "resource://gre/modules/Log.sys.mjs";
      6 
      7 export var CommonUtils = {
      8  /*
      9   * Set manipulation methods. These should be lifted into toolkit, or added to
     10   * `Set` itself.
     11   */
     12 
     13  /**
     14   * Return elements of `a` or `b`.
     15   */
     16  union(a, b) {
     17    let out = new Set(a);
     18    for (let x of b) {
     19      out.add(x);
     20    }
     21    return out;
     22  },
     23 
     24  /**
     25   * Return elements of `a` that are not present in `b`.
     26   */
     27  difference(a, b) {
     28    let out = new Set(a);
     29    for (let x of b) {
     30      out.delete(x);
     31    }
     32    return out;
     33  },
     34 
     35  /**
     36   * Return elements of `a` that are also in `b`.
     37   */
     38  intersection(a, b) {
     39    let out = new Set();
     40    for (let x of a) {
     41      if (b.has(x)) {
     42        out.add(x);
     43      }
     44    }
     45    return out;
     46  },
     47 
     48  /**
     49   * Return true if `a` and `b` are the same size, and
     50   * every element of `a` is in `b`.
     51   */
     52  setEqual(a, b) {
     53    if (a.size != b.size) {
     54      return false;
     55    }
     56    for (let x of a) {
     57      if (!b.has(x)) {
     58        return false;
     59      }
     60    }
     61    return true;
     62  },
     63 
     64  /**
     65   * Checks elements in two arrays for equality, as determined by the `===`
     66   * operator. This function does not perform a deep comparison; see Sync's
     67   * `Util.deepEquals` for that.
     68   */
     69  arrayEqual(a, b) {
     70    if (a.length !== b.length) {
     71      return false;
     72    }
     73    for (let i = 0; i < a.length; i++) {
     74      if (a[i] !== b[i]) {
     75        return false;
     76      }
     77    }
     78    return true;
     79  },
     80 
     81  /**
     82   * Encode byte string as base64URL (RFC 4648).
     83   *
     84   * @param bytes
     85   *        (string) Raw byte string to encode.
     86   * @param pad
     87   *        (bool) Whether to include padding characters (=). Defaults
     88   *        to true for historical reasons.
     89   */
     90  encodeBase64URL: function encodeBase64URL(bytes, pad = true) {
     91    let s = btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_");
     92 
     93    if (!pad) {
     94      return s.replace(/=+$/, "");
     95    }
     96 
     97    return s;
     98  },
     99 
    100  /**
    101   * Create a nsIURI instance from a string.
    102   */
    103  makeURI: function makeURI(URIString) {
    104    if (!URIString) {
    105      return null;
    106    }
    107    try {
    108      return Services.io.newURI(URIString);
    109    } catch (e) {
    110      let log = Log.repository.getLogger("Common.Utils");
    111      log.debug("Could not create URI", e);
    112      return null;
    113    }
    114  },
    115 
    116  /**
    117   * Execute a function on the next event loop tick.
    118   *
    119   * @param callback
    120   *        Function to invoke.
    121   * @param thisObj [optional]
    122   *        Object to bind the callback to.
    123   */
    124  nextTick: function nextTick(callback, thisObj) {
    125    if (thisObj) {
    126      callback = callback.bind(thisObj);
    127    }
    128    Services.tm.dispatchToMainThread(callback);
    129  },
    130 
    131  /**
    132   * Return a timer that is scheduled to call the callback after waiting the
    133   * provided time or as soon as possible. The timer will be set as a property
    134   * of the provided object with the given timer name.
    135   *
    136   * Note that an existing timer with the same name on the same object will be
    137   * canceled and rescheduled with the new callback if you call this function
    138   * before it fired.
    139   * This may race with the imminent firing of the existing timer, so be
    140   * prepared to see it firing twice once in a while if called multiple times
    141   * for the same timer (the alternative would be to see it firing once right
    142   * now and to see nothing happen after the expected delay).
    143   */
    144  namedTimer: function namedTimer(callback, wait, thisObj, name) {
    145    if (!thisObj || !name) {
    146      throw new Error(
    147        "You must provide both an object and a property name for the timer!"
    148      );
    149    }
    150 
    151    let timer = null;
    152    // Take an existing timer if it exists
    153    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
    154      // Setting just the delay on an existing but inactive timer will not
    155      // schedule the timer again. Let's go through initWithCallback always.
    156      timer = thisObj[name];
    157    } else {
    158      // Create a special timer that we can add extra properties
    159      timer = Object.create(
    160        Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
    161      );
    162      // Provide an easy way to clear out the timer
    163      timer.clear = function () {
    164        thisObj[name] = null;
    165        timer.cancel();
    166      };
    167    }
    168 
    169    // Initialize the timer with a smart callback
    170    timer.initWithCallback(
    171      {
    172        notify: function notify() {
    173          // Clear out the timer once it's been triggered
    174          timer.clear();
    175          callback.call(thisObj, timer);
    176        },
    177      },
    178      wait,
    179      timer.TYPE_ONE_SHOT
    180    );
    181 
    182    return (thisObj[name] = timer);
    183  },
    184 
    185  encodeUTF8: function encodeUTF8(str) {
    186    try {
    187      str = this._utf8Converter.ConvertFromUnicode(str);
    188      return str + this._utf8Converter.Finish();
    189    } catch (ex) {
    190      return null;
    191    }
    192  },
    193 
    194  decodeUTF8: function decodeUTF8(str) {
    195    try {
    196      str = this._utf8Converter.ConvertToUnicode(str);
    197      return str + this._utf8Converter.Finish();
    198    } catch (ex) {
    199      return null;
    200    }
    201  },
    202 
    203  byteArrayToString: function byteArrayToString(bytes) {
    204    return bytes.map(byte => String.fromCharCode(byte)).join("");
    205  },
    206 
    207  stringToByteArray: function stringToByteArray(bytesString) {
    208    return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0));
    209  },
    210 
    211  // A lot of Util methods work with byte strings instead of ArrayBuffers.
    212  // A patch should address this problem, but in the meantime let's provide
    213  // helpers method to convert byte strings to Uint8Array.
    214  byteStringToArrayBuffer(byteString) {
    215    if (byteString === undefined) {
    216      return new Uint8Array();
    217    }
    218    const bytes = new Uint8Array(byteString.length);
    219    for (let i = 0; i < byteString.length; ++i) {
    220      bytes[i] = byteString.charCodeAt(i) & 0xff;
    221    }
    222    return bytes;
    223  },
    224 
    225  arrayBufferToByteString(buffer) {
    226    return CommonUtils.byteArrayToString([...buffer]);
    227  },
    228 
    229  bufferToHex(buffer) {
    230    return Array.prototype.map
    231      .call(buffer, x => ("00" + x.toString(16)).slice(-2))
    232      .join("");
    233  },
    234 
    235  bytesAsHex: function bytesAsHex(bytes) {
    236    let s = "";
    237    for (let i = 0, len = bytes.length; i < len; i++) {
    238      let c = (bytes[i].charCodeAt(0) & 0xff).toString(16);
    239      if (c.length == 1) {
    240        c = "0" + c;
    241      }
    242      s += c;
    243    }
    244    return s;
    245  },
    246 
    247  stringAsHex: function stringAsHex(str) {
    248    return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
    249  },
    250 
    251  stringToBytes: function stringToBytes(str) {
    252    return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
    253  },
    254 
    255  hexToBytes: function hexToBytes(str) {
    256    let bytes = [];
    257    for (let i = 0; i < str.length - 1; i += 2) {
    258      bytes.push(parseInt(str.substr(i, 2), 16));
    259    }
    260    return String.fromCharCode.apply(String, bytes);
    261  },
    262 
    263  hexToArrayBuffer(str) {
    264    const octString = CommonUtils.hexToBytes(str);
    265    return CommonUtils.byteStringToArrayBuffer(octString);
    266  },
    267 
    268  hexAsString: function hexAsString(hex) {
    269    return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
    270  },
    271 
    272  base64urlToHex(b64str) {
    273    return CommonUtils.bufferToHex(
    274      new Uint8Array(ChromeUtils.base64URLDecode(b64str, { padding: "reject" }))
    275    );
    276  },
    277 
    278  /**
    279   * Base32 encode (RFC 4648) a string
    280   */
    281  encodeBase32: function encodeBase32(bytes) {
    282    const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    283    let leftover = bytes.length % 5;
    284 
    285    // Pad the last quantum with zeros so the length is a multiple of 5.
    286    if (leftover) {
    287      for (let i = leftover; i < 5; i++) {
    288        bytes += "\0";
    289      }
    290    }
    291 
    292    // Chop the string into quanta of 5 bytes (40 bits). Each quantum
    293    // is turned into 8 characters from the 32 character base.
    294    let ret = "";
    295    for (let i = 0; i < bytes.length; i += 5) {
    296      let c = Array.prototype.slice
    297        .call(bytes.slice(i, i + 5))
    298        .map(byte => byte.charCodeAt(0));
    299      ret +=
    300        key[c[0] >> 3] +
    301        key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] +
    302        key[(c[1] >> 1) & 0x1f] +
    303        key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] +
    304        key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] +
    305        key[(c[3] >> 2) & 0x1f] +
    306        key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] +
    307        key[c[4] & 0x1f];
    308    }
    309 
    310    switch (leftover) {
    311      case 1:
    312        return ret.slice(0, -6) + "======";
    313      case 2:
    314        return ret.slice(0, -4) + "====";
    315      case 3:
    316        return ret.slice(0, -3) + "===";
    317      case 4:
    318        return ret.slice(0, -1) + "=";
    319      default:
    320        return ret;
    321    }
    322  },
    323 
    324  /**
    325   * Base32 decode (RFC 4648) a string.
    326   */
    327  decodeBase32: function decodeBase32(str) {
    328    const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    329 
    330    let padChar = str.indexOf("=");
    331    let chars = padChar == -1 ? str.length : padChar;
    332    let bytes = Math.floor((chars * 5) / 8);
    333    let blocks = Math.ceil(chars / 8);
    334 
    335    // Process a chunk of 5 bytes / 8 characters.
    336    // The processing of this is known in advance,
    337    // so avoid arithmetic!
    338    function processBlock(ret, cOffset, rOffset) {
    339      let c, val;
    340 
    341      // N.B., this relies on
    342      //   undefined | foo == foo.
    343      function accumulate(val) {
    344        ret[rOffset] |= val;
    345      }
    346 
    347      function advance() {
    348        c = str[cOffset++];
    349        if (!c || c == "" || c == "=") {
    350          // Easier than range checking.
    351          throw new Error("Done");
    352        } // Will be caught far away.
    353        val = key.indexOf(c);
    354        if (val == -1) {
    355          throw new Error(`Unknown character in base32: ${c}`);
    356        }
    357      }
    358 
    359      // Handle a left shift, restricted to bytes.
    360      function left(octet, shift) {
    361        return (octet << shift) & 0xff;
    362      }
    363 
    364      advance();
    365      accumulate(left(val, 3));
    366      advance();
    367      accumulate(val >> 2);
    368      ++rOffset;
    369      accumulate(left(val, 6));
    370      advance();
    371      accumulate(left(val, 1));
    372      advance();
    373      accumulate(val >> 4);
    374      ++rOffset;
    375      accumulate(left(val, 4));
    376      advance();
    377      accumulate(val >> 1);
    378      ++rOffset;
    379      accumulate(left(val, 7));
    380      advance();
    381      accumulate(left(val, 2));
    382      advance();
    383      accumulate(val >> 3);
    384      ++rOffset;
    385      accumulate(left(val, 5));
    386      advance();
    387      accumulate(val);
    388      ++rOffset;
    389    }
    390 
    391    // Our output. Define to be explicit (and maybe the compiler will be smart).
    392    let ret = new Array(bytes);
    393    let i = 0;
    394    let cOff = 0;
    395    let rOff = 0;
    396 
    397    for (; i < blocks; ++i) {
    398      try {
    399        processBlock(ret, cOff, rOff);
    400      } catch (ex) {
    401        // Handle the detection of padding.
    402        if (ex.message == "Done") {
    403          break;
    404        }
    405        throw ex;
    406      }
    407      cOff += 8;
    408      rOff += 5;
    409    }
    410 
    411    // Slice in case our shift overflowed to the right.
    412    return CommonUtils.byteArrayToString(ret.slice(0, bytes));
    413  },
    414 
    415  /**
    416   * Trim excess padding from a Base64 string and atob().
    417   *
    418   * See bug 562431 comment 4.
    419   */
    420  safeAtoB: function safeAtoB(b64) {
    421    let len = b64.length;
    422    let over = len % 4;
    423    return over ? atob(b64.substr(0, len - over)) : atob(b64);
    424  },
    425 
    426  /**
    427   * Ensure that the specified value is defined in integer milliseconds since
    428   * UNIX epoch.
    429   *
    430   * This throws an error if the value is not an integer, is negative, or looks
    431   * like seconds, not milliseconds.
    432   *
    433   * If the value is null or 0, no exception is raised.
    434   *
    435   * @param value
    436   *        Value to validate.
    437   */
    438  ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
    439    if (!value) {
    440      return;
    441    }
    442 
    443    if (!/^[0-9]+$/.test(value)) {
    444      throw new Error("Timestamp value is not a positive integer: " + value);
    445    }
    446 
    447    let intValue = parseInt(value, 10);
    448 
    449    if (!intValue) {
    450      return;
    451    }
    452 
    453    // Catch what looks like seconds, not milliseconds.
    454    if (intValue < 10000000000) {
    455      throw new Error("Timestamp appears to be in seconds: " + intValue);
    456    }
    457  },
    458 
    459  /**
    460   * Read bytes from an nsIInputStream into a string.
    461   *
    462   * @param stream
    463   *        (nsIInputStream) Stream to read from.
    464   * @param count
    465   *        (number) Integer number of bytes to read. If not defined, or
    466   *        0, all available input is read.
    467   */
    468  readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
    469    let BinaryInputStream = Components.Constructor(
    470      "@mozilla.org/binaryinputstream;1",
    471      "nsIBinaryInputStream",
    472      "setInputStream"
    473    );
    474    if (!count) {
    475      count = stream.available();
    476    }
    477 
    478    return new BinaryInputStream(stream).readBytes(count);
    479  },
    480 
    481  /**
    482   * Generate a new UUID using nsIUUIDGenerator.
    483   *
    484   * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
    485   *
    486   * @return string A hex-formatted UUID string.
    487   */
    488  generateUUID: function generateUUID() {
    489    let uuid = Services.uuid.generateUUID().toString();
    490 
    491    return uuid.substring(1, uuid.length - 1);
    492  },
    493 
    494  /**
    495   * Obtain an epoch value from a preference.
    496   *
    497   * This reads a string preference and returns an integer. The string
    498   * preference is expected to contain the integer milliseconds since epoch.
    499   * For best results, only read preferences that have been saved with
    500   * setDatePref().
    501   *
    502   * We need to store times as strings because integer preferences are only
    503   * 32 bits and likely overflow most dates.
    504   *
    505   * If the pref contains a non-integer value, the specified default value will
    506   * be returned.
    507   *
    508   * @param branch
    509   *        (Preferences) Branch from which to retrieve preference.
    510   * @param pref
    511   *        (string) The preference to read from.
    512   * @param def
    513   *        (Number) The default value to use if the preference is not defined.
    514   * @param log
    515   *        (Log.Logger) Logger to write warnings to.
    516   */
    517  getEpochPref: function getEpochPref(branch, pref, def = 0, log = null) {
    518    if (!Number.isInteger(def)) {
    519      throw new Error("Default value is not a number: " + def);
    520    }
    521 
    522    let valueStr = branch.getStringPref(pref, null);
    523 
    524    if (valueStr !== null) {
    525      let valueInt = parseInt(valueStr, 10);
    526      if (Number.isNaN(valueInt)) {
    527        if (log) {
    528          log.warn(
    529            "Preference value is not an integer. Using default. " +
    530              pref +
    531              "=" +
    532              valueStr +
    533              " -> " +
    534              def
    535          );
    536        }
    537 
    538        return def;
    539      }
    540 
    541      return valueInt;
    542    }
    543 
    544    return def;
    545  },
    546 
    547  /**
    548   * Obtain a Date from a preference.
    549   *
    550   * This is a wrapper around getEpochPref. It converts the value to a Date
    551   * instance and performs simple range checking.
    552   *
    553   * The range checking ensures the date is newer than the oldestYear
    554   * parameter.
    555   *
    556   * @param branch
    557   *        (Preferences) Branch from which to read preference.
    558   * @param pref
    559   *        (string) The preference from which to read.
    560   * @param def
    561   *        (Number) The default value (in milliseconds) if the preference is
    562   *        not defined or invalid.
    563   * @param log
    564   *        (Log.Logger) Logger to write warnings to.
    565   * @param oldestYear
    566   *        (Number) Oldest year to accept in read values.
    567   */
    568  getDatePref: function getDatePref(
    569    branch,
    570    pref,
    571    def = 0,
    572    log = null,
    573    oldestYear = 2010
    574  ) {
    575    let valueInt = this.getEpochPref(branch, pref, def, log);
    576    let date = new Date(valueInt);
    577 
    578    if (valueInt == def || date.getFullYear() >= oldestYear) {
    579      return date;
    580    }
    581 
    582    if (log) {
    583      log.warn(
    584        "Unexpected old date seen in pref. Returning default: " +
    585          pref +
    586          "=" +
    587          date +
    588          " -> " +
    589          def
    590      );
    591    }
    592 
    593    return new Date(def);
    594  },
    595 
    596  /**
    597   * Store a Date in a preference.
    598   *
    599   * This is the opposite of getDatePref(). The same notes apply.
    600   *
    601   * If the range check fails, an Error will be thrown instead of a default
    602   * value silently being used.
    603   *
    604   * @param branch
    605   *        (Preference) Branch from which to read preference.
    606   * @param pref
    607   *        (string) Name of preference to write to.
    608   * @param date
    609   *        (Date) The value to save.
    610   * @param oldestYear
    611   *        (Number) The oldest year to accept for values.
    612   */
    613  setDatePref: function setDatePref(branch, pref, date, oldestYear = 2010) {
    614    if (date.getFullYear() < oldestYear) {
    615      throw new Error(
    616        "Trying to set " +
    617          pref +
    618          " to a very old time: " +
    619          date +
    620          ". The current time is " +
    621          new Date() +
    622          ". Is the system clock wrong?"
    623      );
    624    }
    625 
    626    branch.setStringPref(pref, "" + date.getTime());
    627  },
    628 
    629  /**
    630   * Convert a string between two encodings.
    631   *
    632   * Output is only guaranteed if the input stream is composed of octets. If
    633   * the input string has characters with values larger than 255, data loss
    634   * will occur.
    635   *
    636   * The returned string is guaranteed to consist of character codes no greater
    637   * than 255.
    638   *
    639   * @param s
    640   *        (string) The source string to convert.
    641   * @param source
    642   *        (string) The current encoding of the string.
    643   * @param dest
    644   *        (string) The target encoding of the string.
    645   *
    646   * @return string
    647   */
    648  convertString: function convertString(s, source, dest) {
    649    if (!s) {
    650      throw new Error("Input string must be defined.");
    651    }
    652 
    653    let is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    654      Ci.nsIStringInputStream
    655    );
    656    is.setByteStringData(s);
    657 
    658    let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
    659      Ci.nsIStreamLoader
    660    );
    661 
    662    let result;
    663 
    664    listener.init({
    665      onStreamComplete: function onStreamComplete(
    666        loader,
    667        context,
    668        status,
    669        length,
    670        data
    671      ) {
    672        result = String.fromCharCode.apply(this, data);
    673      },
    674    });
    675 
    676    let converter = this._converterService.asyncConvertData(
    677      source,
    678      dest,
    679      listener,
    680      null
    681    );
    682    converter.onStartRequest(null, null);
    683    converter.onDataAvailable(null, is, 0, s.length);
    684    converter.onStopRequest(null, null, null);
    685 
    686    return result;
    687  },
    688 };
    689 
    690 ChromeUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function () {
    691  let converter = Cc[
    692    "@mozilla.org/intl/scriptableunicodeconverter"
    693  ].createInstance(Ci.nsIScriptableUnicodeConverter);
    694  converter.charset = "UTF-8";
    695  return converter;
    696 });
    697 
    698 ChromeUtils.defineLazyGetter(CommonUtils, "_converterService", function () {
    699  return Cc["@mozilla.org/streamConverters;1"].getService(
    700    Ci.nsIStreamConverterService
    701  );
    702 });