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