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