utils.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  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. var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
  5. this.EXPORTED_SYMBOLS = ["CommonUtils"];
  6. Cu.import("resource://gre/modules/Promise.jsm");
  7. Cu.import("resource://gre/modules/Services.jsm");
  8. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  9. Cu.import("resource://gre/modules/osfile.jsm")
  10. Cu.import("resource://gre/modules/Log.jsm");
  11. this.CommonUtils = {
  12. /*
  13. * Set manipulation methods. These should be lifted into toolkit, or added to
  14. * `Set` itself.
  15. */
  16. /**
  17. * Return elements of `a` or `b`.
  18. */
  19. union: function (a, b) {
  20. let out = new Set(a);
  21. for (let x of b) {
  22. out.add(x);
  23. }
  24. return out;
  25. },
  26. /**
  27. * Return elements of `a` that are not present in `b`.
  28. */
  29. difference: function (a, b) {
  30. let out = new Set(a);
  31. for (let x of b) {
  32. out.delete(x);
  33. }
  34. return out;
  35. },
  36. /**
  37. * Return elements of `a` that are also in `b`.
  38. */
  39. intersection: function (a, b) {
  40. let out = new Set();
  41. for (let x of a) {
  42. if (b.has(x)) {
  43. out.add(x);
  44. }
  45. }
  46. return out;
  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: function (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. // Import these from Log.jsm for backward compatibility
  64. exceptionStr: Log.exceptionStr,
  65. stackTrace: Log.stackTrace,
  66. /**
  67. * Encode byte string as base64URL (RFC 4648).
  68. *
  69. * @param bytes
  70. * (string) Raw byte string to encode.
  71. * @param pad
  72. * (bool) Whether to include padding characters (=). Defaults
  73. * to true for historical reasons.
  74. */
  75. encodeBase64URL: function encodeBase64URL(bytes, pad=true) {
  76. let s = btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_");
  77. if (!pad) {
  78. return s.replace(/=+$/, "");
  79. }
  80. return s;
  81. },
  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 = Log.repository.getLogger("Common.Utils");
  92. log.debug("Could not create URI", e);
  93. return null;
  94. }
  95. },
  96. /**
  97. * Execute a function on the next event loop tick.
  98. *
  99. * @param callback
  100. * Function to invoke.
  101. * @param thisObj [optional]
  102. * Object to bind the callback to.
  103. */
  104. nextTick: function nextTick(callback, thisObj) {
  105. if (thisObj) {
  106. callback = callback.bind(thisObj);
  107. }
  108. Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
  109. },
  110. /**
  111. * Return a promise resolving on some later tick.
  112. *
  113. * This a wrapper around Promise.resolve() that prevents stack
  114. * accumulation and prevents callers from accidentally relying on
  115. * same-tick promise resolution.
  116. */
  117. laterTickResolvingPromise: function (value, prototype) {
  118. let deferred = Promise.defer(prototype);
  119. this.nextTick(deferred.resolve.bind(deferred, value));
  120. return deferred.promise;
  121. },
  122. /**
  123. * Spin the event loop and return once the next tick is executed.
  124. *
  125. * This is an evil function and should not be used in production code. It
  126. * exists in this module for ease-of-use.
  127. */
  128. waitForNextTick: function waitForNextTick() {
  129. let cb = Async.makeSyncCallback();
  130. this.nextTick(cb);
  131. Async.waitForSyncCallback(cb);
  132. return;
  133. },
  134. /**
  135. * Return a timer that is scheduled to call the callback after waiting the
  136. * provided time or as soon as possible. The timer will be set as a property
  137. * of the provided object with the given timer name.
  138. */
  139. namedTimer: function namedTimer(callback, wait, thisObj, name) {
  140. if (!thisObj || !name) {
  141. throw "You must provide both an object and a property name for the timer!";
  142. }
  143. // Delay an existing timer if it exists
  144. if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
  145. thisObj[name].delay = wait;
  146. return;
  147. }
  148. // Create a special timer that we can add extra properties
  149. let timer = Object.create(Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer));
  150. // Provide an easy way to clear out the timer
  151. timer.clear = function() {
  152. thisObj[name] = null;
  153. timer.cancel();
  154. };
  155. // Initialize the timer with a smart callback
  156. timer.initWithCallback({
  157. notify: function notify() {
  158. // Clear out the timer once it's been triggered
  159. timer.clear();
  160. callback.call(thisObj, timer);
  161. }
  162. }, wait, timer.TYPE_ONE_SHOT);
  163. return thisObj[name] = timer;
  164. },
  165. encodeUTF8: function encodeUTF8(str) {
  166. try {
  167. str = this._utf8Converter.ConvertFromUnicode(str);
  168. return str + this._utf8Converter.Finish();
  169. } catch (ex) {
  170. return null;
  171. }
  172. },
  173. decodeUTF8: function decodeUTF8(str) {
  174. try {
  175. str = this._utf8Converter.ConvertToUnicode(str);
  176. return str + this._utf8Converter.Finish();
  177. } catch (ex) {
  178. return null;
  179. }
  180. },
  181. byteArrayToString: function byteArrayToString(bytes) {
  182. return bytes.map(byte => String.fromCharCode(byte)).join("");
  183. },
  184. stringToByteArray: function stringToByteArray(bytesString) {
  185. return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0));
  186. },
  187. bytesAsHex: function bytesAsHex(bytes) {
  188. return Array.prototype.slice.call(bytes).map(c => ("0" + c.charCodeAt(0).toString(16)).slice(-2)).join("");
  189. },
  190. stringAsHex: function stringAsHex(str) {
  191. return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
  192. },
  193. stringToBytes: function stringToBytes(str) {
  194. return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
  195. },
  196. hexToBytes: function hexToBytes(str) {
  197. let bytes = [];
  198. for (let i = 0; i < str.length - 1; i += 2) {
  199. bytes.push(parseInt(str.substr(i, 2), 16));
  200. }
  201. return String.fromCharCode.apply(String, bytes);
  202. },
  203. hexAsString: function hexAsString(hex) {
  204. return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
  205. },
  206. /**
  207. * Base32 encode (RFC 4648) a string
  208. */
  209. encodeBase32: function encodeBase32(bytes) {
  210. const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  211. let quanta = Math.floor(bytes.length / 5);
  212. let leftover = bytes.length % 5;
  213. // Pad the last quantum with zeros so the length is a multiple of 5.
  214. if (leftover) {
  215. quanta += 1;
  216. for (let i = leftover; i < 5; i++)
  217. bytes += "\0";
  218. }
  219. // Chop the string into quanta of 5 bytes (40 bits). Each quantum
  220. // is turned into 8 characters from the 32 character base.
  221. let ret = "";
  222. for (let i = 0; i < bytes.length; i += 5) {
  223. let c = Array.prototype.slice.call(bytes.slice(i, i + 5)).map(byte => byte.charCodeAt(0));
  224. ret += key[c[0] >> 3]
  225. + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)]
  226. + key[(c[1] >> 1) & 0x1f]
  227. + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)]
  228. + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)]
  229. + key[(c[3] >> 2) & 0x1f]
  230. + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)]
  231. + key[c[4] & 0x1f];
  232. }
  233. switch (leftover) {
  234. case 1:
  235. return ret.slice(0, -6) + "======";
  236. case 2:
  237. return ret.slice(0, -4) + "====";
  238. case 3:
  239. return ret.slice(0, -3) + "===";
  240. case 4:
  241. return ret.slice(0, -1) + "=";
  242. default:
  243. return ret;
  244. }
  245. },
  246. /**
  247. * Base32 decode (RFC 4648) a string.
  248. */
  249. decodeBase32: function decodeBase32(str) {
  250. const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  251. let padChar = str.indexOf("=");
  252. let chars = (padChar == -1) ? str.length : padChar;
  253. let bytes = Math.floor(chars * 5 / 8);
  254. let blocks = Math.ceil(chars / 8);
  255. // Process a chunk of 5 bytes / 8 characters.
  256. // The processing of this is known in advance,
  257. // so avoid arithmetic!
  258. function processBlock(ret, cOffset, rOffset) {
  259. let c, val;
  260. // N.B., this relies on
  261. // undefined | foo == foo.
  262. function accumulate(val) {
  263. ret[rOffset] |= val;
  264. }
  265. function advance() {
  266. c = str[cOffset++];
  267. if (!c || c == "" || c == "=") // Easier than range checking.
  268. throw "Done"; // Will be caught far away.
  269. val = key.indexOf(c);
  270. if (val == -1)
  271. throw "Unknown character in base32: " + c;
  272. }
  273. // Handle a left shift, restricted to bytes.
  274. function left(octet, shift) {
  275. return (octet << shift) & 0xff;
  276. }
  277. advance();
  278. accumulate(left(val, 3));
  279. advance();
  280. accumulate(val >> 2);
  281. ++rOffset;
  282. accumulate(left(val, 6));
  283. advance();
  284. accumulate(left(val, 1));
  285. advance();
  286. accumulate(val >> 4);
  287. ++rOffset;
  288. accumulate(left(val, 4));
  289. advance();
  290. accumulate(val >> 1);
  291. ++rOffset;
  292. accumulate(left(val, 7));
  293. advance();
  294. accumulate(left(val, 2));
  295. advance();
  296. accumulate(val >> 3);
  297. ++rOffset;
  298. accumulate(left(val, 5));
  299. advance();
  300. accumulate(val);
  301. ++rOffset;
  302. }
  303. // Our output. Define to be explicit (and maybe the compiler will be smart).
  304. let ret = new Array(bytes);
  305. let i = 0;
  306. let cOff = 0;
  307. let rOff = 0;
  308. for (; i < blocks; ++i) {
  309. try {
  310. processBlock(ret, cOff, rOff);
  311. } catch (ex) {
  312. // Handle the detection of padding.
  313. if (ex == "Done")
  314. break;
  315. throw ex;
  316. }
  317. cOff += 8;
  318. rOff += 5;
  319. }
  320. // Slice in case our shift overflowed to the right.
  321. return CommonUtils.byteArrayToString(ret.slice(0, bytes));
  322. },
  323. /**
  324. * Trim excess padding from a Base64 string and atob().
  325. *
  326. * See bug 562431 comment 4.
  327. */
  328. safeAtoB: function safeAtoB(b64) {
  329. let len = b64.length;
  330. let over = len % 4;
  331. return over ? atob(b64.substr(0, len - over)) : atob(b64);
  332. },
  333. /**
  334. * Parses a JSON file from disk using OS.File and promises.
  335. *
  336. * @param path the file to read. Will be passed to `OS.File.read()`.
  337. * @return a promise that resolves to the JSON contents of the named file.
  338. */
  339. readJSON: function(path) {
  340. return OS.File.read(path, { encoding: "utf-8" }).then((data) => {
  341. return JSON.parse(data);
  342. });
  343. },
  344. /**
  345. * Write a JSON object to the named file using OS.File and promises.
  346. *
  347. * @param contents a JS object. Will be serialized.
  348. * @param path the path of the file to write.
  349. * @return a promise, as produced by OS.File.writeAtomic.
  350. */
  351. writeJSON: function(contents, path) {
  352. let data = JSON.stringify(contents);
  353. return OS.File.writeAtomic(path, data, {encoding: "utf-8", tmpPath: path + ".tmp"});
  354. },
  355. /**
  356. * Ensure that the specified value is defined in integer milliseconds since
  357. * UNIX epoch.
  358. *
  359. * This throws an error if the value is not an integer, is negative, or looks
  360. * like seconds, not milliseconds.
  361. *
  362. * If the value is null or 0, no exception is raised.
  363. *
  364. * @param value
  365. * Value to validate.
  366. */
  367. ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
  368. if (!value) {
  369. return;
  370. }
  371. if (!/^[0-9]+$/.test(value)) {
  372. throw new Error("Timestamp value is not a positive integer: " + value);
  373. }
  374. let intValue = parseInt(value, 10);
  375. if (!intValue) {
  376. return;
  377. }
  378. // Catch what looks like seconds, not milliseconds.
  379. if (intValue < 10000000000) {
  380. throw new Error("Timestamp appears to be in seconds: " + intValue);
  381. }
  382. },
  383. /**
  384. * Read bytes from an nsIInputStream into a string.
  385. *
  386. * @param stream
  387. * (nsIInputStream) Stream to read from.
  388. * @param count
  389. * (number) Integer number of bytes to read. If not defined, or
  390. * 0, all available input is read.
  391. */
  392. readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
  393. let BinaryInputStream = Components.Constructor(
  394. "@mozilla.org/binaryinputstream;1",
  395. "nsIBinaryInputStream",
  396. "setInputStream");
  397. if (!count) {
  398. count = stream.available();
  399. }
  400. return new BinaryInputStream(stream).readBytes(count);
  401. },
  402. /**
  403. * Generate a new UUID using nsIUUIDGenerator.
  404. *
  405. * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
  406. *
  407. * @return string A hex-formatted UUID string.
  408. */
  409. generateUUID: function generateUUID() {
  410. let uuid = Cc["@mozilla.org/uuid-generator;1"]
  411. .getService(Ci.nsIUUIDGenerator)
  412. .generateUUID()
  413. .toString();
  414. return uuid.substring(1, uuid.length - 1);
  415. },
  416. /**
  417. * Obtain an epoch value from a preference.
  418. *
  419. * This reads a string preference and returns an integer. The string
  420. * preference is expected to contain the integer milliseconds since epoch.
  421. * For best results, only read preferences that have been saved with
  422. * setDatePref().
  423. *
  424. * We need to store times as strings because integer preferences are only
  425. * 32 bits and likely overflow most dates.
  426. *
  427. * If the pref contains a non-integer value, the specified default value will
  428. * be returned.
  429. *
  430. * @param branch
  431. * (Preferences) Branch from which to retrieve preference.
  432. * @param pref
  433. * (string) The preference to read from.
  434. * @param def
  435. * (Number) The default value to use if the preference is not defined.
  436. * @param log
  437. * (Log.Logger) Logger to write warnings to.
  438. */
  439. getEpochPref: function getEpochPref(branch, pref, def=0, log=null) {
  440. if (!Number.isInteger(def)) {
  441. throw new Error("Default value is not a number: " + def);
  442. }
  443. let valueStr = branch.get(pref, null);
  444. if (valueStr !== null) {
  445. let valueInt = parseInt(valueStr, 10);
  446. if (Number.isNaN(valueInt)) {
  447. if (log) {
  448. log.warn("Preference value is not an integer. Using default. " +
  449. pref + "=" + valueStr + " -> " + def);
  450. }
  451. return def;
  452. }
  453. return valueInt;
  454. }
  455. return def;
  456. },
  457. /**
  458. * Obtain a Date from a preference.
  459. *
  460. * This is a wrapper around getEpochPref. It converts the value to a Date
  461. * instance and performs simple range checking.
  462. *
  463. * The range checking ensures the date is newer than the oldestYear
  464. * parameter.
  465. *
  466. * @param branch
  467. * (Preferences) Branch from which to read preference.
  468. * @param pref
  469. * (string) The preference from which to read.
  470. * @param def
  471. * (Number) The default value (in milliseconds) if the preference is
  472. * not defined or invalid.
  473. * @param log
  474. * (Log.Logger) Logger to write warnings to.
  475. * @param oldestYear
  476. * (Number) Oldest year to accept in read values.
  477. */
  478. getDatePref: function getDatePref(branch, pref, def=0, log=null,
  479. oldestYear=2010) {
  480. let valueInt = this.getEpochPref(branch, pref, def, log);
  481. let date = new Date(valueInt);
  482. if (valueInt == def || date.getFullYear() >= oldestYear) {
  483. return date;
  484. }
  485. if (log) {
  486. log.warn("Unexpected old date seen in pref. Returning default: " +
  487. pref + "=" + date + " -> " + def);
  488. }
  489. return new Date(def);
  490. },
  491. /**
  492. * Store a Date in a preference.
  493. *
  494. * This is the opposite of getDatePref(). The same notes apply.
  495. *
  496. * If the range check fails, an Error will be thrown instead of a default
  497. * value silently being used.
  498. *
  499. * @param branch
  500. * (Preference) Branch from which to read preference.
  501. * @param pref
  502. * (string) Name of preference to write to.
  503. * @param date
  504. * (Date) The value to save.
  505. * @param oldestYear
  506. * (Number) The oldest year to accept for values.
  507. */
  508. setDatePref: function setDatePref(branch, pref, date, oldestYear=2010) {
  509. if (date.getFullYear() < oldestYear) {
  510. throw new Error("Trying to set " + pref + " to a very old time: " +
  511. date + ". The current time is " + new Date() +
  512. ". Is the system clock wrong?");
  513. }
  514. branch.set(pref, "" + date.getTime());
  515. },
  516. /**
  517. * Convert a string between two encodings.
  518. *
  519. * Output is only guaranteed if the input stream is composed of octets. If
  520. * the input string has characters with values larger than 255, data loss
  521. * will occur.
  522. *
  523. * The returned string is guaranteed to consist of character codes no greater
  524. * than 255.
  525. *
  526. * @param s
  527. * (string) The source string to convert.
  528. * @param source
  529. * (string) The current encoding of the string.
  530. * @param dest
  531. * (string) The target encoding of the string.
  532. *
  533. * @return string
  534. */
  535. convertString: function convertString(s, source, dest) {
  536. if (!s) {
  537. throw new Error("Input string must be defined.");
  538. }
  539. let is = Cc["@mozilla.org/io/string-input-stream;1"]
  540. .createInstance(Ci.nsIStringInputStream);
  541. is.setData(s, s.length);
  542. let listener = Cc["@mozilla.org/network/stream-loader;1"]
  543. .createInstance(Ci.nsIStreamLoader);
  544. let result;
  545. listener.init({
  546. onStreamComplete: function onStreamComplete(loader, context, status,
  547. length, data) {
  548. result = String.fromCharCode.apply(this, data);
  549. },
  550. });
  551. let converter = this._converterService.asyncConvertData(source, dest,
  552. listener, null);
  553. converter.onStartRequest(null, null);
  554. converter.onDataAvailable(null, null, is, 0, s.length);
  555. converter.onStopRequest(null, null, null);
  556. return result;
  557. },
  558. };
  559. XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
  560. let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  561. .createInstance(Ci.nsIScriptableUnicodeConverter);
  562. converter.charset = "UTF-8";
  563. return converter;
  564. });
  565. XPCOMUtils.defineLazyGetter(CommonUtils, "_converterService", function() {
  566. return Cc["@mozilla.org/streamConverters;1"]
  567. .getService(Ci.nsIStreamConverterService);
  568. });