Bug 1852754: part 9) Add tests for dynamically loading <link rel="prefetch"> elements...
[gecko.git] / services / common / utils.sys.mjs
blob384971f4bf9551c2e39d551b6cd7ec48724341e2
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 import { Log } from "resource://gre/modules/Log.sys.mjs";
7 export var CommonUtils = {
8   /*
9    * Set manipulation methods. These should be lifted into toolkit, or added to
10    * `Set` itself.
11    */
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   },
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   },
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   },
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   },
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   },
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, "_");
93     if (!pad) {
94       return s.replace(/=+$/, "");
95     }
97     return s;
98   },
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   },
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   },
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   namedTimer: function namedTimer(callback, wait, thisObj, name) {
137     if (!thisObj || !name) {
138       throw new Error(
139         "You must provide both an object and a property name for the timer!"
140       );
141     }
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 thisObj[name];
147     }
149     // Create a special timer that we can add extra properties
150     let timer = Object.create(
151       Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
152     );
154     // Provide an easy way to clear out the timer
155     timer.clear = function () {
156       thisObj[name] = null;
157       timer.cancel();
158     };
160     // Initialize the timer with a smart callback
161     timer.initWithCallback(
162       {
163         notify: function notify() {
164           // Clear out the timer once it's been triggered
165           timer.clear();
166           callback.call(thisObj, timer);
167         },
168       },
169       wait,
170       timer.TYPE_ONE_SHOT
171     );
173     return (thisObj[name] = timer);
174   },
176   encodeUTF8: function encodeUTF8(str) {
177     try {
178       str = this._utf8Converter.ConvertFromUnicode(str);
179       return str + this._utf8Converter.Finish();
180     } catch (ex) {
181       return null;
182     }
183   },
185   decodeUTF8: function decodeUTF8(str) {
186     try {
187       str = this._utf8Converter.ConvertToUnicode(str);
188       return str + this._utf8Converter.Finish();
189     } catch (ex) {
190       return null;
191     }
192   },
194   byteArrayToString: function byteArrayToString(bytes) {
195     return bytes.map(byte => String.fromCharCode(byte)).join("");
196   },
198   stringToByteArray: function stringToByteArray(bytesString) {
199     return Array.prototype.slice.call(bytesString).map(c => c.charCodeAt(0));
200   },
202   // A lot of Util methods work with byte strings instead of ArrayBuffers.
203   // A patch should address this problem, but in the meantime let's provide
204   // helpers method to convert byte strings to Uint8Array.
205   byteStringToArrayBuffer(byteString) {
206     if (byteString === undefined) {
207       return new Uint8Array();
208     }
209     const bytes = new Uint8Array(byteString.length);
210     for (let i = 0; i < byteString.length; ++i) {
211       bytes[i] = byteString.charCodeAt(i) & 0xff;
212     }
213     return bytes;
214   },
216   arrayBufferToByteString(buffer) {
217     return CommonUtils.byteArrayToString([...buffer]);
218   },
220   bufferToHex(buffer) {
221     return Array.prototype.map
222       .call(buffer, x => ("00" + x.toString(16)).slice(-2))
223       .join("");
224   },
226   bytesAsHex: function bytesAsHex(bytes) {
227     let s = "";
228     for (let i = 0, len = bytes.length; i < len; i++) {
229       let c = (bytes[i].charCodeAt(0) & 0xff).toString(16);
230       if (c.length == 1) {
231         c = "0" + c;
232       }
233       s += c;
234     }
235     return s;
236   },
238   stringAsHex: function stringAsHex(str) {
239     return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
240   },
242   stringToBytes: function stringToBytes(str) {
243     return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
244   },
246   hexToBytes: function hexToBytes(str) {
247     let bytes = [];
248     for (let i = 0; i < str.length - 1; i += 2) {
249       bytes.push(parseInt(str.substr(i, 2), 16));
250     }
251     return String.fromCharCode.apply(String, bytes);
252   },
254   hexToArrayBuffer(str) {
255     const octString = CommonUtils.hexToBytes(str);
256     return CommonUtils.byteStringToArrayBuffer(octString);
257   },
259   hexAsString: function hexAsString(hex) {
260     return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
261   },
263   base64urlToHex(b64str) {
264     return CommonUtils.bufferToHex(
265       new Uint8Array(ChromeUtils.base64URLDecode(b64str, { padding: "reject" }))
266     );
267   },
269   /**
270    * Base32 encode (RFC 4648) a string
271    */
272   encodeBase32: function encodeBase32(bytes) {
273     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
274     let leftover = bytes.length % 5;
276     // Pad the last quantum with zeros so the length is a multiple of 5.
277     if (leftover) {
278       for (let i = leftover; i < 5; i++) {
279         bytes += "\0";
280       }
281     }
283     // Chop the string into quanta of 5 bytes (40 bits). Each quantum
284     // is turned into 8 characters from the 32 character base.
285     let ret = "";
286     for (let i = 0; i < bytes.length; i += 5) {
287       let c = Array.prototype.slice
288         .call(bytes.slice(i, i + 5))
289         .map(byte => byte.charCodeAt(0));
290       ret +=
291         key[c[0] >> 3] +
292         key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] +
293         key[(c[1] >> 1) & 0x1f] +
294         key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] +
295         key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] +
296         key[(c[3] >> 2) & 0x1f] +
297         key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] +
298         key[c[4] & 0x1f];
299     }
301     switch (leftover) {
302       case 1:
303         return ret.slice(0, -6) + "======";
304       case 2:
305         return ret.slice(0, -4) + "====";
306       case 3:
307         return ret.slice(0, -3) + "===";
308       case 4:
309         return ret.slice(0, -1) + "=";
310       default:
311         return ret;
312     }
313   },
315   /**
316    * Base32 decode (RFC 4648) a string.
317    */
318   decodeBase32: function decodeBase32(str) {
319     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
321     let padChar = str.indexOf("=");
322     let chars = padChar == -1 ? str.length : padChar;
323     let bytes = Math.floor((chars * 5) / 8);
324     let blocks = Math.ceil(chars / 8);
326     // Process a chunk of 5 bytes / 8 characters.
327     // The processing of this is known in advance,
328     // so avoid arithmetic!
329     function processBlock(ret, cOffset, rOffset) {
330       let c, val;
332       // N.B., this relies on
333       //   undefined | foo == foo.
334       function accumulate(val) {
335         ret[rOffset] |= val;
336       }
338       function advance() {
339         c = str[cOffset++];
340         if (!c || c == "" || c == "=") {
341           // Easier than range checking.
342           throw new Error("Done");
343         } // Will be caught far away.
344         val = key.indexOf(c);
345         if (val == -1) {
346           throw new Error(`Unknown character in base32: ${c}`);
347         }
348       }
350       // Handle a left shift, restricted to bytes.
351       function left(octet, shift) {
352         return (octet << shift) & 0xff;
353       }
355       advance();
356       accumulate(left(val, 3));
357       advance();
358       accumulate(val >> 2);
359       ++rOffset;
360       accumulate(left(val, 6));
361       advance();
362       accumulate(left(val, 1));
363       advance();
364       accumulate(val >> 4);
365       ++rOffset;
366       accumulate(left(val, 4));
367       advance();
368       accumulate(val >> 1);
369       ++rOffset;
370       accumulate(left(val, 7));
371       advance();
372       accumulate(left(val, 2));
373       advance();
374       accumulate(val >> 3);
375       ++rOffset;
376       accumulate(left(val, 5));
377       advance();
378       accumulate(val);
379       ++rOffset;
380     }
382     // Our output. Define to be explicit (and maybe the compiler will be smart).
383     let ret = new Array(bytes);
384     let i = 0;
385     let cOff = 0;
386     let rOff = 0;
388     for (; i < blocks; ++i) {
389       try {
390         processBlock(ret, cOff, rOff);
391       } catch (ex) {
392         // Handle the detection of padding.
393         if (ex.message == "Done") {
394           break;
395         }
396         throw ex;
397       }
398       cOff += 8;
399       rOff += 5;
400     }
402     // Slice in case our shift overflowed to the right.
403     return CommonUtils.byteArrayToString(ret.slice(0, bytes));
404   },
406   /**
407    * Trim excess padding from a Base64 string and atob().
408    *
409    * See bug 562431 comment 4.
410    */
411   safeAtoB: function safeAtoB(b64) {
412     let len = b64.length;
413     let over = len % 4;
414     return over ? atob(b64.substr(0, len - over)) : atob(b64);
415   },
417   /**
418    * Ensure that the specified value is defined in integer milliseconds since
419    * UNIX epoch.
420    *
421    * This throws an error if the value is not an integer, is negative, or looks
422    * like seconds, not milliseconds.
423    *
424    * If the value is null or 0, no exception is raised.
425    *
426    * @param value
427    *        Value to validate.
428    */
429   ensureMillisecondsTimestamp: function ensureMillisecondsTimestamp(value) {
430     if (!value) {
431       return;
432     }
434     if (!/^[0-9]+$/.test(value)) {
435       throw new Error("Timestamp value is not a positive integer: " + value);
436     }
438     let intValue = parseInt(value, 10);
440     if (!intValue) {
441       return;
442     }
444     // Catch what looks like seconds, not milliseconds.
445     if (intValue < 10000000000) {
446       throw new Error("Timestamp appears to be in seconds: " + intValue);
447     }
448   },
450   /**
451    * Read bytes from an nsIInputStream into a string.
452    *
453    * @param stream
454    *        (nsIInputStream) Stream to read from.
455    * @param count
456    *        (number) Integer number of bytes to read. If not defined, or
457    *        0, all available input is read.
458    */
459   readBytesFromInputStream: function readBytesFromInputStream(stream, count) {
460     let BinaryInputStream = Components.Constructor(
461       "@mozilla.org/binaryinputstream;1",
462       "nsIBinaryInputStream",
463       "setInputStream"
464     );
465     if (!count) {
466       count = stream.available();
467     }
469     return new BinaryInputStream(stream).readBytes(count);
470   },
472   /**
473    * Generate a new UUID using nsIUUIDGenerator.
474    *
475    * Example value: "1e00a2e2-1570-443e-bf5e-000354124234"
476    *
477    * @return string A hex-formatted UUID string.
478    */
479   generateUUID: function generateUUID() {
480     let uuid = Services.uuid.generateUUID().toString();
482     return uuid.substring(1, uuid.length - 1);
483   },
485   /**
486    * Obtain an epoch value from a preference.
487    *
488    * This reads a string preference and returns an integer. The string
489    * preference is expected to contain the integer milliseconds since epoch.
490    * For best results, only read preferences that have been saved with
491    * setDatePref().
492    *
493    * We need to store times as strings because integer preferences are only
494    * 32 bits and likely overflow most dates.
495    *
496    * If the pref contains a non-integer value, the specified default value will
497    * be returned.
498    *
499    * @param branch
500    *        (Preferences) Branch from which to retrieve preference.
501    * @param pref
502    *        (string) The preference to read from.
503    * @param def
504    *        (Number) The default value to use if the preference is not defined.
505    * @param log
506    *        (Log.Logger) Logger to write warnings to.
507    */
508   getEpochPref: function getEpochPref(branch, pref, def = 0, log = null) {
509     if (!Number.isInteger(def)) {
510       throw new Error("Default value is not a number: " + def);
511     }
513     let valueStr = branch.getCharPref(pref, null);
515     if (valueStr !== null) {
516       let valueInt = parseInt(valueStr, 10);
517       if (Number.isNaN(valueInt)) {
518         if (log) {
519           log.warn(
520             "Preference value is not an integer. Using default. " +
521               pref +
522               "=" +
523               valueStr +
524               " -> " +
525               def
526           );
527         }
529         return def;
530       }
532       return valueInt;
533     }
535     return def;
536   },
538   /**
539    * Obtain a Date from a preference.
540    *
541    * This is a wrapper around getEpochPref. It converts the value to a Date
542    * instance and performs simple range checking.
543    *
544    * The range checking ensures the date is newer than the oldestYear
545    * parameter.
546    *
547    * @param branch
548    *        (Preferences) Branch from which to read preference.
549    * @param pref
550    *        (string) The preference from which to read.
551    * @param def
552    *        (Number) The default value (in milliseconds) if the preference is
553    *        not defined or invalid.
554    * @param log
555    *        (Log.Logger) Logger to write warnings to.
556    * @param oldestYear
557    *        (Number) Oldest year to accept in read values.
558    */
559   getDatePref: function getDatePref(
560     branch,
561     pref,
562     def = 0,
563     log = null,
564     oldestYear = 2010
565   ) {
566     let valueInt = this.getEpochPref(branch, pref, def, log);
567     let date = new Date(valueInt);
569     if (valueInt == def || date.getFullYear() >= oldestYear) {
570       return date;
571     }
573     if (log) {
574       log.warn(
575         "Unexpected old date seen in pref. Returning default: " +
576           pref +
577           "=" +
578           date +
579           " -> " +
580           def
581       );
582     }
584     return new Date(def);
585   },
587   /**
588    * Store a Date in a preference.
589    *
590    * This is the opposite of getDatePref(). The same notes apply.
591    *
592    * If the range check fails, an Error will be thrown instead of a default
593    * value silently being used.
594    *
595    * @param branch
596    *        (Preference) Branch from which to read preference.
597    * @param pref
598    *        (string) Name of preference to write to.
599    * @param date
600    *        (Date) The value to save.
601    * @param oldestYear
602    *        (Number) The oldest year to accept for values.
603    */
604   setDatePref: function setDatePref(branch, pref, date, oldestYear = 2010) {
605     if (date.getFullYear() < oldestYear) {
606       throw new Error(
607         "Trying to set " +
608           pref +
609           " to a very old time: " +
610           date +
611           ". The current time is " +
612           new Date() +
613           ". Is the system clock wrong?"
614       );
615     }
617     branch.setCharPref(pref, "" + date.getTime());
618   },
620   /**
621    * Convert a string between two encodings.
622    *
623    * Output is only guaranteed if the input stream is composed of octets. If
624    * the input string has characters with values larger than 255, data loss
625    * will occur.
626    *
627    * The returned string is guaranteed to consist of character codes no greater
628    * than 255.
629    *
630    * @param s
631    *        (string) The source string to convert.
632    * @param source
633    *        (string) The current encoding of the string.
634    * @param dest
635    *        (string) The target encoding of the string.
636    *
637    * @return string
638    */
639   convertString: function convertString(s, source, dest) {
640     if (!s) {
641       throw new Error("Input string must be defined.");
642     }
644     let is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
645       Ci.nsIStringInputStream
646     );
647     is.setData(s, s.length);
649     let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
650       Ci.nsIStreamLoader
651     );
653     let result;
655     listener.init({
656       onStreamComplete: function onStreamComplete(
657         loader,
658         context,
659         status,
660         length,
661         data
662       ) {
663         result = String.fromCharCode.apply(this, data);
664       },
665     });
667     let converter = this._converterService.asyncConvertData(
668       source,
669       dest,
670       listener,
671       null
672     );
673     converter.onStartRequest(null, null);
674     converter.onDataAvailable(null, is, 0, s.length);
675     converter.onStopRequest(null, null, null);
677     return result;
678   },
681 ChromeUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function () {
682   let converter = Cc[
683     "@mozilla.org/intl/scriptableunicodeconverter"
684   ].createInstance(Ci.nsIScriptableUnicodeConverter);
685   converter.charset = "UTF-8";
686   return converter;
689 ChromeUtils.defineLazyGetter(CommonUtils, "_converterService", function () {
690   return Cc["@mozilla.org/streamConverters;1"].getService(
691     Ci.nsIStreamConverterService
692   );