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 });