]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/storage.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / storage.jsm
1 // Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("storage", {
8     exports: ["File", "Storage", "storage"],
9     require: ["promises", "services", "util"]
10 });
11
12 lazyRequire("config", ["config"]);
13 lazyRequire("io", ["IO"]);
14 lazyRequire("overlay", ["overlay"]);
15
16 lazyRequire("resource://gre/modules/osfile.jsm", ["OS"]);
17
18 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
19 var myObject = JSON.parse("{}").constructor;
20
21 var global = Cu.getGlobalForObject(this);
22
23 var StoreBase = Class("StoreBase", {
24     OPTIONS: ["privateData", "replacer"],
25
26     fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
27
28     get serial() JSON.stringify(this._object, this.replacer),
29
30     init: function init(name, store, load, options) {
31         this._load = load;
32         this._options = options;
33
34         this.__defineGetter__("store", () => store);
35         this.__defineGetter__("name", () => name);
36         for (let [k, v] in Iterator(options))
37             if (this.OPTIONS.indexOf(k) >= 0)
38                 this[k] = v;
39         this.reload();
40     },
41
42     clone: function clone(storage) {
43         let store = storage.privateMode ? false : this.store;
44         let res = this.constructor(this.name, store, this._load, this._options);
45         res.storage = storage;
46         return res;
47     },
48
49     makeOwn: function makeOwn(val) {
50         if (typeof val != "object")
51             return val;
52         if (Cu.getGlobalForObject(val) == global)
53             return val;
54         return JSON.parse(JSON.stringify(val, this.replacer));
55     },
56
57     changed: function () { this.timer && this.timer.tell(); },
58
59     reload: function reload() {
60         this._object = this._load() || this._constructor();
61         this.fireEvent("change", null);
62     },
63
64     delete: function delete_() {
65         delete storage.keys[this.name];
66         delete storage[this.name];
67         return OS.File.remove(
68             storage.infoPath.child(this.name).path);
69     },
70
71     save: function () { (self.storage || storage)._saveData(this); },
72
73     __iterator__: function () Iterator(this._object)
74 });
75
76 var ArrayStore = Class("ArrayStore", StoreBase, {
77     _constructor: Array,
78
79     get length() this._object.length,
80
81     set: function set(index, value, quiet) {
82         var orig = this._object[index];
83         this._object[index] = this.makeOwn(value);
84         if (!quiet)
85             this.fireEvent("change", index);
86
87         return orig;
88     },
89
90     push: function push(value) {
91         this._object.push(this.makeOwn(value));
92         this.fireEvent("push", this._object.length);
93     },
94
95     pop: function pop(value, ord) {
96         if (ord == null)
97             var res = this._object.pop();
98         else
99             res = this._object.splice(ord, 1)[0];
100
101         this.fireEvent("pop", this._object.length, ord);
102         return res;
103     },
104
105     shift: function shift(value) {
106         var res = this._object.shift();
107         this.fireEvent("shift", this._object.length);
108         return res;
109     },
110
111     insert: function insert(value, ord) {
112         value = this.makeOwn(value);
113         if (ord == 0)
114             this._object.unshift(value);
115         else
116             this._object = this._object.slice(0, ord)
117                                .concat([value])
118                                .concat(this._object.slice(ord));
119         this.fireEvent("insert", this._object.length, ord);
120     },
121
122     truncate: function truncate(length, fromEnd) {
123         var res = this._object.length;
124         if (this._object.length > length) {
125             if (fromEnd)
126                 this._object.splice(0, this._object.length - length);
127             this._object.length = length;
128             this.fireEvent("truncate", length);
129         }
130         return res;
131     },
132
133     // XXX: Awkward.
134     mutate: function mutate(funcName) {
135         var _funcName = funcName;
136         arguments[0] = this._object;
137         this._object = Array[_funcName].apply(Array, arguments)
138                                        .map(this.makeOwn.bind(this));
139         this.fireEvent("change", null);
140     },
141
142     get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
143 });
144
145 var ObjectStore = Class("ObjectStore", StoreBase, {
146     _constructor: myObject,
147
148     clear: function () {
149         this._object = {};
150         this.fireEvent("clear");
151     },
152
153     get: function get(key, default_) {
154         return this.has(key)        ? this._object[key] :
155                arguments.length > 1 ? this.set(key, default_) :
156                                       undefined;
157     },
158
159     has: function has(key) hasOwnProperty(this._object, key),
160
161     keys: function keys() Object.keys(this._object),
162
163     remove: function remove(key) {
164         var res = this._object[key];
165         delete this._object[key];
166         this.fireEvent("remove", key);
167         return res;
168     },
169
170     set: function set(key, val) {
171         var defined = key in this._object;
172         var orig = this._object[key];
173         this._object[key] = this.makeOwn(val);
174         if (!defined)
175             this.fireEvent("add", key);
176         else if (orig != val)
177             this.fireEvent("change", key);
178         return val;
179     }
180 });
181
182 var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
183
184 var Storage = Module("Storage", {
185     Local: function Local(dactyl, modules, window) ({
186         init: function init() {
187             this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
188         }
189     }),
190
191     alwaysReload: {},
192
193     init: function init() {
194         this.cleanup();
195
196         let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
197         if (!Services.dactylSession)
198             Services.dactylSession = Cu.createObjectIn(sessionGlobal);
199         this.session = Services.dactylSession;
200     },
201
202     cleanup: function () {
203         this.saveAll();
204
205         for (let key in keys(this.keys)) {
206             if (this[key].timer)
207                 this[key].timer.flush();
208             delete this[key];
209         }
210
211         this.keys = {};
212         this.observers = {};
213     },
214
215     _loadData: function loadData(name, store, type) {
216         try {
217             let file = storage.infoPath.child(name);
218             if (file.exists()) {
219                 let data = file.read();
220                 let result = JSON.parse(data);
221                 if (result instanceof type)
222                     return result;
223             }
224         }
225         catch (e) {
226             util.reportError(e);
227         }
228     },
229
230     _saveData: promises.task(function saveData(obj) {
231         if (obj.privateData && storage.privateMode)
232             return;
233         if (obj.store && storage.infoPath) {
234             var { path } = storage.infoPath.child(obj.name);
235             yield OS.File.makeDir(storage.infoPath.path,
236                                   { ignoreExisting: true });
237             yield OS.File.writeAtomic(
238                 path, obj.serial,
239                 { tmpPath: path + ".part" });
240         }
241     }),
242
243     storeForSession: function storeForSession(key, val) {
244         if (val)
245             this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
246         else
247             delete this.dactylSession[key];
248     },
249
250     infoPath: Class.Memoize(() =>
251         File(IO.runtimePath.replace(/,.*/, ""))
252             .child("info").child(config.profileName)),
253
254     exists: function exists(key) this.infoPath.child(key).exists(),
255
256     remove: function remove(key) {
257         if (this.exists(key)) {
258             if (this[key] && this[key].timer)
259                 this[key].timer.flush();
260             delete this[key];
261             delete this.keys[key];
262             return OS.File.remove(
263                 this.infoPath.child(key).path);
264         }
265     },
266
267     newObject: function newObject(key, constructor, params={}) {
268         if (params == null || !isObject(params))
269             throw Error("Invalid argument type");
270
271         if (this.isLocalModule) {
272             this.globalInstance.newObject.apply(this.globalInstance, arguments);
273
274             if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
275                 let obj = this.globalInstance.keys[key];
276                 this.keys[key] = this._privatize(obj);
277             }
278
279             return this.keys[key];
280         }
281
282         let reload = params.reload || this.alwaysReload[key];
283         if (!(key in this.keys) || reload) {
284             if (key in this && !reload)
285                 throw Error("Cannot add storage key with that name.");
286
287             let load = () => this._loadData(key, params.store, params.type || myObject);
288
289             this.keys[key] = new constructor(key, params.store, load, params);
290             this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
291             this.__defineGetter__(key, function () this.keys[key]);
292         }
293         return this.keys[key];
294     },
295
296     newMap: function newMap(key, options={}) {
297         return this.newObject(key, ObjectStore, options);
298     },
299
300     newArray: function newArray(key, options={}) {
301         return this.newObject(key, ArrayStore, update({ type: Array }, options));
302     },
303
304     get observerMaps() {
305         yield this.observers;
306         for (let window of overlay.windows)
307             yield overlay.getData(window, "storage-observers", Object);
308     },
309
310     addObserver: function addObserver(key, callback, window) {
311         var { observers } = this;
312         if (window instanceof Ci.nsIDOMWindow)
313             observers = overlay.getData(window, "storage-observers", Object);
314
315         if (!hasOwnProperty(observers, key))
316             observers[key] = RealSet();
317
318         observers[key].add(callback);
319     },
320
321     removeObserver: function (key, callback) {
322         for (let observers in this.observerMaps)
323             if (key in observers)
324                 observers[key].remove(callback);
325     },
326
327     fireEvent: function fireEvent(key, event, arg) {
328         for (let observers in this.observerMaps)
329             for (let observer of observers[key] || [])
330                 observer(key, event, arg);
331
332         if (key in this.keys && this.keys[key].timer)
333             this[key].timer.tell();
334     },
335
336     load: function load(key) {
337         if (this[key].store && this[key].reload)
338             this[key].reload();
339     },
340
341     save: function save(key) {
342         if (this[key])
343             this._saveData(this.keys[key]);
344     },
345
346     saveAll: function storeAll() {
347         for each (let obj in this.keys)
348             this._saveData(obj);
349     },
350
351     _privateMode: false,
352     get privateMode() this._privateMode,
353     set privateMode(enabled) {
354         this._privateMode = Boolean(enabled);
355
356         if (this.isLocalModule) {
357             this.saveAll();
358
359             if (!enabled)
360                 delete this.keys;
361             else {
362                 let { keys } = this;
363                 this.keys = {};
364                 for (let [k, v] in Iterator(keys))
365                     this.keys[k] = this._privatize(v);
366             }
367         }
368         return this._privateMode;
369     },
370
371     _privatize: function privatize(obj) {
372         if (obj.privateData && obj.clone)
373             return obj.clone(this);
374         return obj;
375     },
376 }, {
377     Replacer: {
378         skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
379     }
380 }, {
381     cleanup: function (dactyl, modules, window) {
382         overlay.setData(window, "storage-callbacks", undefined);
383     }
384 });
385
386 /**
387  * @class File A class to wrap nsIFile objects and simplify operations
388  * thereon.
389  *
390  * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
391  * @param {boolean} checkPWD Whether to allow expansion relative to the
392  *          current directory. @default true
393  * @param {string} charset The charset of the file. @default File.defaultEncoding
394  */
395 var File = Class("File", {
396     init: function (path, checkPWD, charset) {
397         let file = services.File();
398
399         if (charset)
400             this.charset = charset;
401
402         if (path instanceof Ci.nsIFileURL)
403             path = path.file;
404
405         if (path instanceof Ci.nsIFile || path instanceof File)
406             file = path.clone();
407         else if (/file:\/\//.test(path))
408             file = services["file:"].getFileFromURLSpec(path);
409         else {
410             try {
411                 let expandedPath = File.expandPath(path);
412
413                 if (!File.isAbsolutePath(expandedPath) && checkPWD)
414                     file = checkPWD.child(expandedPath);
415                 else
416                     file.initWithPath(expandedPath);
417             }
418             catch (e) {
419                 util.reportError(e);
420                 return File.DoesNotExist(path, e);
421             }
422         }
423         this.file = file.QueryInterface(Ci.nsILocalFile);
424         return this;
425     },
426
427     charset: Class.Memoize(() => File.defaultEncoding),
428
429     /**
430      * @property {nsIFileURL} Returns the nsIFileURL object for this file.
431      */
432     URI: Class.Memoize(function () {
433         let uri = services.io.newFileURI(this.file)
434                           .QueryInterface(Ci.nsIFileURL);
435         uri.QueryInterface(Ci.nsIMutable).mutable = false;
436         return uri;
437     }),
438
439     /**
440      * Iterates over the objects in this directory.
441      */
442     iterDirectory: function iterDirectory() {
443         if (!this.exists())
444             throw Error(_("io.noSuchFile"));
445         if (!this.isDirectory())
446             throw Error(_("io.eNotDir"));
447         for (let file in iter(this.directoryEntries))
448             yield File(file);
449     },
450
451     /**
452      * Returns a new file for the given child of this directory entry.
453      */
454     child: function child() {
455         let f = this.constructor(this);
456         for (let [, name] in Iterator(arguments))
457             for (let elem of name.split(File.pathSplit))
458                 f.append(elem);
459         return f;
460     },
461
462     /**
463      * Returns an iterator for all lines in a file.
464      */
465     get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
466                                this.charset),
467
468     /**
469      * Reads this file's entire contents in "text" mode and returns the
470      * content as a string.
471      *
472      * @param {string} encoding The encoding from which to decode the file.
473      *          @default #charset
474      * @returns {string}
475      */
476     read: function read(encoding) {
477         let ifstream = services.FileInStream(this.file, -1, 0, 0);
478
479         return File.readStream(ifstream, encoding || this.charset);
480     },
481
482     /**
483      * Returns the list of files in this directory.
484      *
485      * @param {boolean} sort Whether to sort the returned directory
486      *     entries.
487      * @returns {[nsIFile]}
488      */
489     readDirectory: function readDirectory(sort) {
490         if (!this.isDirectory())
491             throw Error(_("io.eNotDir"));
492
493         let array = [e for (e in this.iterDirectory())];
494         if (sort)
495             array.sort((a, b) => (b.isDirectory() - a.isDirectory() ||
496                                   String.localeCompare(a.path, b.path)));
497         return array;
498     },
499
500     /**
501      * Returns a new nsIFileURL object for this file.
502      *
503      * @returns {nsIFileURL}
504      */
505     toURI: function toURI() services.io.newFileURI(this.file),
506
507     /**
508      * Writes the string *buf* to this file.
509      *
510      * @param {string} buf The file content.
511      * @param {string|number} mode The file access mode, a bitwise OR of
512      *     the following flags:
513      *       {@link #MODE_RDONLY}:   0x01
514      *       {@link #MODE_WRONLY}:   0x02
515      *       {@link #MODE_RDWR}:     0x04
516      *       {@link #MODE_CREATE}:   0x08
517      *       {@link #MODE_APPEND}:   0x10
518      *       {@link #MODE_TRUNCATE}: 0x20
519      *       {@link #MODE_SYNC}:     0x40
520      *     Alternatively, the following abbreviations may be used:
521      *       ">"  is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
522      *       ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
523      * @default ">"
524      * @param {number} perms The file mode bits of the created file. This
525      *     is only used when creating a new file and does not change
526      *     permissions if the file exists.
527      * @default 0644
528      * @param {string} encoding The encoding to used to write the file.
529      * @default #charset
530      */
531     write: function write(buf, mode, perms, encoding) {
532         function getStream(defaultChar) {
533             return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
534         }
535         if (buf instanceof File)
536             buf = buf.read();
537
538         if (!encoding)
539             encoding = this.charset;
540
541         if (mode == ">>")
542             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
543         else if (!mode || mode == ">")
544             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
545
546         if (!perms)
547             perms = octal(644);
548         if (!this.exists()) // OCREAT won't create the directory
549             this.create(this.NORMAL_FILE_TYPE, perms);
550
551         let ofstream = services.FileOutStream(this.file, mode, perms, 0);
552         try {
553             var ocstream = getStream(0);
554             ocstream.writeString(buf);
555         }
556         catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
557             ocstream.close();
558             ocstream = getStream("?".charCodeAt(0));
559             ocstream.writeString(buf);
560             return false;
561         }
562         finally {
563             try {
564                 ocstream.close();
565             }
566             catch (e) {}
567             ofstream.close();
568         }
569         return true;
570     },
571
572     // Wrapped native methods:
573     copyTo: function copyTo(dir, name)
574         this.file.copyTo(this.constructor(dir).file,
575                          name),
576
577     copyToFollowingLinks: function copyToFollowingLinks(dir, name)
578         this.file.copyToFollowingLinks(this.constructor(dir).file,
579                                        name),
580
581     moveTo: function moveTo(dir, name)
582         this.file.moveTo(this.constructor(dir).file,
583                          name),
584
585     equals: function equals(file)
586         this.file.equals(this.constructor(file).file),
587
588     contains: function contains(dir, recur)
589         this.file.contains(this.constructor(dir).file,
590                            recur),
591
592     getRelativeDescriptor: function getRelativeDescriptor(file)
593         this.file.getRelativeDescriptor(this.constructor(file).file),
594
595     setRelativeDescriptor: function setRelativeDescriptor(file, path)
596         this.file.setRelativeDescriptor(this.constructor(file).file,
597                                         path)
598 }, {
599     /**
600      * @property {number} Open for reading only.
601      * @final
602      */
603     MODE_RDONLY: 0x01,
604
605     /**
606      * @property {number} Open for writing only.
607      * @final
608      */
609     MODE_WRONLY: 0x02,
610
611     /**
612      * @property {number} Open for reading and writing.
613      * @final
614      */
615     MODE_RDWR: 0x04,
616
617     /**
618      * @property {number} If the file does not exist, the file is created.
619      *     If the file exists, this flag has no effect.
620      * @final
621      */
622     MODE_CREATE: 0x08,
623
624     /**
625      * @property {number} The file pointer is set to the end of the file
626      *     prior to each write.
627      * @final
628      */
629     MODE_APPEND: 0x10,
630
631     /**
632      * @property {number} If the file exists, its length is truncated to 0.
633      * @final
634      */
635     MODE_TRUNCATE: 0x20,
636
637     /**
638      * @property {number} If set, each write will wait for both the file
639      *     data and file status to be physically updated.
640      * @final
641      */
642     MODE_SYNC: 0x40,
643
644     /**
645      * @property {number} With MODE_CREATE, if the file does not exist, the
646      *     file is created. If the file already exists, no action and NULL
647      *     is returned.
648      * @final
649      */
650     MODE_EXCL: 0x80,
651
652     /**
653      * @property {string} The current platform's path separator.
654      */
655     PATH_SEP: Class.Memoize(function () {
656         let f = services.directory.get("CurProcD", Ci.nsIFile);
657         f.append("foo");
658         return f.path.substr(f.parent.path.length, 1);
659     }),
660
661     pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
662
663     DoesNotExist: function DoesNotExist(path, error) ({
664         path: path,
665         exists: function () false,
666         __noSuchMethod__: function () { throw error || Error("Does not exist"); }
667     }),
668
669     defaultEncoding: "UTF-8",
670
671     /**
672      * Expands "~" and environment variables in *path*.
673      *
674      * "~" is expanded to to the value of $HOME. On Windows if this is not
675      * set then the following are tried in order:
676      *   $USERPROFILE
677      *   ${HOMDRIVE}$HOMEPATH
678      *
679      * The variable notation is $VAR (terminated by a non-word character)
680      * or ${VAR}. %VAR% is also supported on Windows.
681      *
682      * @param {string} path The unexpanded path string.
683      * @param {boolean} relative Whether the path is relative or absolute.
684      * @returns {string}
685      */
686     expandPath: function expandPath(path, relative) {
687         function getenv(name) services.environment.get(name);
688
689         // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
690         // TODO: Vim does not expand variables set to an empty string (and documents it).
691         // Kris reckons we shouldn't replicate this 'bug'. --djk
692         // TODO: should we be doing this for all paths?
693         function expand(path) path.replace(
694             win32 ? /\$(\w+)\b|\${(\w+)}|%(\w+)%/g
695                   : /\$(\w+)\b|\${(\w+)}/g,
696             (m, n1, n2, n3) => (getenv(n1 || n2 || n3) || m));
697         path = expand(path);
698
699         // expand ~
700         // Yuck.
701         if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
702             // Try $HOME first, on all systems
703             let home = getenv("HOME");
704
705             // Windows has its own idiosyncratic $HOME variables.
706             if (win32 && (!home || !File(home).exists()))
707                 home = getenv("USERPROFILE") ||
708                        getenv("HOMEDRIVE") + getenv("HOMEPATH");
709
710             path = home + path.substr(1);
711         }
712
713         // TODO: Vim expands paths twice, once before checking for ~, once
714         // after, but doesn't document it. Is this just a bug? --Kris
715         path = expand(path);
716         return path.replace("/", File.PATH_SEP, "g");
717     },
718
719     expandPathList: function (list) list.map(this.expandPath),
720
721     readURL: function readURL(url, encoding) {
722         let channel = services.io.newChannel(url, null, null);
723         channel.contentType = "text/plain";
724         return this.readStream(channel.open(), encoding);
725     },
726
727     readStream: function readStream(ifstream, encoding) {
728         try {
729             var icstream = services.CharsetStream(
730                     ifstream, encoding || File.defaultEncoding, 4096, // buffer size
731                     services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
732
733             let buffer = [];
734             let str = {};
735             while (icstream.readString(4096, str) != 0)
736                 buffer.push(str.value);
737             return buffer.join("");
738         }
739         finally {
740             icstream.close();
741             ifstream.close();
742         }
743     },
744
745     readLines: function readLines(ifstream, encoding) {
746         try {
747             var icstream = services.CharsetStream(
748                     ifstream, encoding || File.defaultEncoding, 4096, // buffer size
749                     services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
750
751             var value = {};
752             while (icstream.readLine(value))
753                 yield value.value;
754         }
755         finally {
756             icstream.close();
757             ifstream.close();
758         }
759     },
760
761     isAbsolutePath: function isAbsolutePath(path) {
762         try {
763             services.File().initWithPath(path);
764             return true;
765         }
766         catch (e) {
767             return false;
768         }
769     },
770
771     joinPaths: function joinPaths(head, tail, cwd) {
772         let path = this(head, cwd);
773         try {
774             // FIXME: should only expand environment vars and normalize path separators
775             path.appendRelativePath(this.expandPath(tail, true));
776         }
777         catch (e) {
778             return File.DoesNotExist(e);
779         }
780         return path;
781     },
782
783     replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
784 });
785
786 let (file = services.directory.get("ProfD", Ci.nsIFile)) {
787     Object.keys(file).forEach(function (prop) {
788         if (!(prop in File.prototype)) {
789             let isFunction;
790             try {
791                 isFunction = callable(file[prop]);
792             }
793             catch (e) {}
794
795             if (isFunction)
796                 File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
797             else
798                 Object.defineProperty(File.prototype, prop, {
799                     configurable: true,
800                     get: function wrap_get() this.file[prop],
801                     set: function wrap_set(val) { this.file[prop] = val; }
802                 });
803         }
804     });
805     file = null;
806 }
807
808 endModule();
809
810 // catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
811
812 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: