services/common/utils.js

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

mercurial