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