Bug 703269 - Mirror peptest harness into mozilla-central r=jhammel
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 29 Nov 2011 11:43:15 -0500
changeset 82601 604476c594958311b57e6cb0a78fb2d350d74eb2
parent 82600 3a5731a6000237f39d311dcb8b3ad56fb9af09f4
child 82602 b10b930500f1df703e4c1b11a1ec395a9c4c89b9
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjhammel
bugs703269
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 703269 - Mirror peptest harness into mozilla-central r=jhammel
testing/peptest/MANIFEST.in
testing/peptest/Makefile.in
testing/peptest/README.md
testing/peptest/peptest/__init__.py
testing/peptest/peptest/extension/README
testing/peptest/peptest/extension/build.xml
testing/peptest/peptest/extension/chrome.manifest
testing/peptest/peptest/extension/chrome/content/init.js
testing/peptest/peptest/extension/chrome/content/overlay.xul
testing/peptest/peptest/extension/chrome/content/quit.js
testing/peptest/peptest/extension/components/pep-cmdline.js
testing/peptest/peptest/extension/install.rdf
testing/peptest/peptest/extension/resource/mozmill/README.md
testing/peptest/peptest/extension/resource/mozmill/driver/controller.js
testing/peptest/peptest/extension/resource/mozmill/driver/elementslib.js
testing/peptest/peptest/extension/resource/mozmill/driver/mozelement.js
testing/peptest/peptest/extension/resource/mozmill/driver/mozmill.js
testing/peptest/peptest/extension/resource/mozmill/driver/msgbroker.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/EventUtils.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/arrays.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/dom.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/httpd.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/json2.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/objects.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/os.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/securable-module.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/strings.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/utils.js
testing/peptest/peptest/extension/resource/mozmill/stdlib/withs.js
testing/peptest/peptest/extension/resource/pep/api.js
testing/peptest/peptest/extension/resource/pep/logger.js
testing/peptest/peptest/extension/resource/pep/results.js
testing/peptest/peptest/extension/resource/pep/testsuite.js
testing/peptest/peptest/extension/resource/pep/utils.js
testing/peptest/peptest/pepprocess.py
testing/peptest/peptest/pepresults.py
testing/peptest/peptest/peputils.py
testing/peptest/peptest/runpeptests.py
testing/peptest/setup.py
testing/peptest/tests/firefox/examples/example_tests.ini
testing/peptest/tests/firefox/examples/test_contextMenu.js
testing/peptest/tests/firefox/examples/test_openBlankTab.js
testing/peptest/tests/firefox/examples/test_openBookmarksMenu.js
testing/peptest/tests/firefox/examples/test_openWindow.js
testing/peptest/tests/firefox/examples/test_resizeWindow.js
testing/peptest/tests/firefox/examples/test_searchGoogle.js
testing/peptest/tests/thunderbird/examples/example_tests.ini
new file mode 100644
--- /dev/null
+++ b/testing/peptest/MANIFEST.in
@@ -0,0 +1,1 @@
+recursive-include peptest/extension *
\ No newline at end of file
--- a/testing/peptest/Makefile.in
+++ b/testing/peptest/Makefile.in
@@ -9,17 +9,17 @@
 # Software distributed under the License is distributed on an "AS IS" basis,
 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 # for the specific language governing rights and limitations under the
 # License.
 #
 # The Original Code is peptest.
 #
 # The Initial Developer of the Original Code is
-#   Mozilla Corporation 
+#   The Mozilla Foundation 
 # Portions created by the Initial Developer are Copyright (C) 2011
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #   Andrew Halberstadt <ahalberstadt@mozilla.com> (Original author)
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either of the GNU General Public License Version 2 or later (the "GPL"),
@@ -41,16 +41,28 @@ srcdir = @srcdir@
 VPATH = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = testing_peptest
 
 include $(topsrcdir)/config/rules.mk
 
-TEST_FILES = \
+PEPTEST_HARNESS = \
+  peptest \
+  $(NULL)
+
+PEPTEST_EXTRAS = \
+  setup.py \
+  MANIFEST.in \
+  README.md \
+  $(NULL)
+
+PEPTEST_TESTS = \
   tests \
   $(NULL)
 
 stage-package: PKG_STAGE = $(DIST)/test-package-stage
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/peptest
-	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_FILES)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_HARNESS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_EXTRAS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_TESTS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/testing/peptest/README.md
@@ -0,0 +1,12 @@
+[Peptest](https://wiki.mozilla.org/Auto-tools/Projects/peptest) 
+is a Mozilla automated testing harness for running responsiveness tests.
+These tests measure how long events spend away from the event loop.
+
+# Running Tests
+
+Currently tests are run from the command line with python. 
+Peptest currently depends on some external Mozilla python packages, namely: 
+mozrunner, mozprocess, mozprofile, mozinfo, mozlog, mozhttpd and manifestdestiny. 
+
+See [running tests](https://wiki.mozilla.org/Auto-tools/Projects/peptest#Running_Tests) 
+for more information.
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/__init__.py
@@ -0,0 +1,38 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is peptest.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2___
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Andrew Halberstadt <halbersa@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from runpeptests import *
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/README
@@ -0,0 +1,4 @@
+Firefox/Fennec Responsiveness Testing
+
+This tool is meant to measure and report Firefox's responsiveness for various user interactions.  This will be important for measuring Electrolysis performance gains.
+For more information see: https://wiki.mozilla.org/Electrolysis/Firefox/20101117
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/build.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+
+<!--
+build.xml adapted from Shawn Wilsher's rtse
+(http://shawnwilsher.com/extensions/rtse/)
+ -->
+
+<project name="pep" default="pepxpi">
+  <tstamp>
+    <format property="build.number" pattern="yyyyMMdd" offset="-1" unit="hour"/>
+  </tstamp>
+  <property name="build.version" value="1.1.${build.number}"/>
+
+  <target name="pepxpi" depends="createjar">
+    <delete file="pep.xpi"/>
+    <zip destfile="pep.xpi">
+      <zipfileset dir="" includes="jar/pep.jar"/>
+      <zipfileset dir="" includes="install.rdf"/>
+      <zipfileset dir="" includes="README"/>
+      <zipfileset dir="" includes="chrome-jar.manifest" fullpath="chrome.manifest"/>
+    </zip>
+    <antcall target="cleanup"/>
+  </target>
+
+  <target name="createjar">
+    <mkdir dir="jar"/>
+    <zip destfile="jar/pep.jar">
+      <zipfileset dir="" includes="components/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="chrome/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="resource/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="locale/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="skin/**" excludes="**GIT"/>
+    </zip>
+  </target>
+
+  <target name="unpacked">
+    <delete file="pep.xpi"/>
+    <zip destfile="pep.xpi">
+      <zipfileset dir="" includes="components/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="chrome/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="resource/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="locale/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="skin/**" excludes="**GIT"/>
+      <zipfileset dir="" includes="install.rdf"/>
+      <zipfileset dir="" includes="readme.txt"/>
+      <zipfileset dir="" includes="chrome.manifest" fullpath="chrome.manifest"/>
+    </zip>
+  </target>
+
+  <target name="cleanup">
+    <!-- Delete the chrome directory, any other cleanup actions go here -->
+    <delete dir="jar"/>
+  </target>
+</project>
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/chrome.manifest
@@ -0,0 +1,14 @@
+resource mozmill resource/mozmill/
+resource stdlib resource/stdlib/
+resource pep resource/pep/
+content pep chrome/content/
+locale	pep en-US	locale/en-US/
+skin	pep classic/1.0	skin/
+
+style	chrome://global/content/customizeToolbar.xul	chrome://pep/skin/overlay.css
+
+component {807b1ae9-df22-40bd-8d0a-2a583da551bb} components/pep-cmdline.js
+contract @mozilla.org/commandlinehandler/general-startup;1?type=pep {807b1ae9-df22-40bd-8d0a-2a583da551bb}
+category command-line-handler m-pep @mozilla.org/commandlinehandler/general-startup;1?type=pep
+
+overlay chrome://browser/content/browser.xul chrome://pep/content/overlay.xul
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/chrome/content/init.js
@@ -0,0 +1,111 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is peptest.
+ *
+ * The Initial Developer of the Original Code is
+ *   The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011.
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Andrew Halberstadt <halbersa@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ ***** END LICENSE BLOCK ***** */
+
+const cmdLineHandler =
+        Cc["@mozilla.org/commandlinehandler/general-startup;1?type=pep"]
+        .getService(Ci.nsICommandLineHandler);
+
+
+var log = {};       // basic logger
+var utils = {};     // utility object
+var broker = {};    // mozmill message broker for listening to mozmill events
+// test suite object that will run the tests
+Components.utils.import('resource://pep/testsuite.js');
+Components.utils.import('resource://pep/logger.js', log);
+Components.utils.import('resource://pep/utils.js', utils);
+Components.utils.import('resource://mozmill/driver/msgbroker.js', broker);
+
+var APPCONTENT;
+
+/**
+ * This is the entry point for peptest.
+ * Gets called when the browser is first loaded.
+ */
+function initialize() {
+  window.removeEventListener("load", initialize, false);
+  let cmd = cmdLineHandler.wrappedJSObject;
+  // cmd.firstRun is used so the tests don't
+  // get run again if a second window is opened
+  if (cmd.firstRun) {
+    cmd.firstRun = false;
+    try {
+      // get json manifest object
+      let manifest = cmd.manifest;
+      let data = utils.readFile(manifest);
+      let obj = JSON.parse(data.join(' '));
+
+      // register mozmill listener
+      broker.addObject(new MozmillMsgListener());
+
+      // set a load listener on the content and run the tests when loaded
+      APPCONTENT = document.getElementById('appcontent');
+      function runTests() {
+        APPCONTENT.removeEventListener('pageshow', runTests);
+        suite = new TestSuite(obj.tests);
+        suite.run();
+        goQuitApplication();
+      };
+      APPCONTENT.addEventListener('pageshow', runTests);
+    } catch(e) {
+      log.error(e.toString());
+      log.debug('Traceback:');
+      lines = e.stack.split('\n');
+      for (let i = 0; i < lines.length - 1; ++i) {
+        log.debug('\t' + lines[i]);
+      }
+      goQuitApplication();
+    }
+  }
+};
+
+/**
+ * A listener to receive Mozmill events
+ */
+function MozmillMsgListener() {}
+MozmillMsgListener.prototype.pass = function(obj) {
+  log.debug('MOZMILL pass ' + JSON.stringify(obj) + '\n');
+};
+MozmillMsgListener.prototype.fail = function(obj) {
+  // TODO Should this cause an error?
+  log.warning('MOZMILL fail ' + JSON.stringify(obj) + '\n');
+};
+MozmillMsgListener.prototype.log = function(obj) {
+  log.debug('MOZMILL log ' + JSON.stringify(obj) + '\n');
+};
+
+// register load listener for command line argument handling.
+window.addEventListener("load", initialize, false);
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/chrome/content/overlay.xul
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://pep/skin/overlay.css" type="text/css"?>
+<!DOCTYPE overlay SYSTEM "chrome://pep/locale/overlay.dtd">
+<overlay id="pep-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/x-javascript" src="chrome://pep/content/quit.js"/>
+  <script type="application/x-javascript" src="chrome://pep/content/init.js"/>
+</overlay>
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/chrome/content/quit.js
@@ -0,0 +1,94 @@
+/* ***** BEGIN LICENSE BLOCK *****
+* Version: MPL 1.1/GPL 2.0/LGPL 2.1
+*
+* The contents of this file are subject to the Mozilla Public License Version
+* 1.1 (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+* http://www.mozilla.org/MPL/
+*
+* Software distributed under the License is distributed on an "AS IS" basis,
+* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+* for the specific language governing rights and limitations under the
+* License.
+*
+* The Original Code is The Original Code is Mozilla Automated Testing Code
+*
+* The Initial Developer of the Original Code is
+*   The Mozilla Foundation.
+* Portions created by the Initial Developer are Copyright (C) 2005
+* the Initial Developer. All Rights Reserved.
+*
+* Contributor(s):
+*   Bob Clary <bob@bclary.com>
+*
+* Alternatively, the contents of this file may be used under the terms of
+* either the GNU General Public License Version 2 or later (the "GPL"), or
+* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+* in which case the provisions of the GPL or the LGPL are applicable instead
+* of those above. If you wish to allow use of your version of this file only
+* under the terms of either the GPL or the LGPL, and not to allow others to
+* use your version of this file under the terms of the MPL, indicate your
+* decision by deleting the provisions above and replace them with the notice
+* and other provisions required by the GPL or the LGPL. If you do not delete
+* the provisions above, a recipient may use your version of this file under
+* the terms of any one of the MPL, the GPL or the LGPL.
+*
+* ***** END LICENSE BLOCK ***** */
+
+/*
+From mozilla/toolkit/content
+These files did not have a license
+*/
+
+function canQuitApplication() {
+  var os = Components.classes["@mozilla.org/observer-service;1"]
+    .getService(Components.interfaces.nsIObserverService);
+  if (!os) {
+    return true;
+  }
+
+  try {
+    var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+      .createInstance(Components.interfaces.nsISupportsPRBool);
+    os.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+    // Something aborted the quit process.
+    if (cancelQuit.data) {
+      return false;
+    }
+  }
+  catch (ex) {
+  }
+  return true;
+}
+
+function goQuitApplication() {
+  if (!canQuitApplication()) {
+    return false;
+  }
+
+  const kAppStartup = '@mozilla.org/toolkit/app-startup;1';
+  const kAppShell = '@mozilla.org/appshell/appShellService;1';
+  var appService;
+  var forceQuit;
+
+  if (kAppStartup in Components.classes) {
+    appService = Components.classes[kAppStartup].
+      getService(Components.interfaces.nsIAppStartup);
+    forceQuit = Components.interfaces.nsIAppStartup.eForceQuit;
+  } else if (kAppShell in Components.classes) {
+    appService = Components.classes[kAppShell].
+      getService(Components.interfaces.nsIAppShellService);
+    forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
+  } else {
+    throw 'goQuitApplication: no AppStartup/appShell';
+  }
+
+  try {
+    appService.quit(forceQuit);
+  }
+  catch(ex) {
+    throw('goQuitApplication: ' + ex);
+  }
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/components/pep-cmdline.js
@@ -0,0 +1,91 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is peptest.
+ *
+ * The Initial Developer of the Original Code is
+ *    The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *    Andrew Halberstadt <halbersa@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const PEP_CONTRACTID  = "@mozilla.org/commandlinehandler/general-startup;1?type=pep";
+const PEP_CID         = Components.ID('{807b1ae9-df22-40bd-8d0a-2a583da551bb}');
+const PEP_CATEGORY    = "m-pep";
+const PEP_DESCRIPTION = "Responsiveness Testing Harness";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Command Line Handler
+function CommandLineHandler() {
+    this.wrappedJSObject = this;
+    this.firstRun = true;
+};
+
+CommandLineHandler.prototype = {
+  classID: PEP_CID,
+  classDescription: PEP_DESCRIPTION,
+  contractID: PEP_CONTRACTID,
+
+  QueryInterface: XPCOMUtils.generateQI([
+      Components.interfaces.nsISupports,
+      Components.interfaces.nsICommandLineHandler
+  ]),
+
+  _xpcom_categories: [{
+      category: "command-line-handler",
+      entry: PEP_CATEGORY,
+  }],
+
+  /* nsICommandLineHandler */
+  handle : function (cmdLine) {
+    try {
+      this.manifest = cmdLine.handleFlagWithParam("pep-start", false);
+      if (cmdLine.handleFlag("pep-noisy", false)) {
+        this.noisy = true;
+      }
+    }
+    catch (e) {
+      dump("incorrect parameter passed to pep on the command line.");
+      return;
+    }
+  },
+
+  helpInfo : "  -pep-start <file>    Run peptests described in given manifest\n" +
+             "  -pep-noisy           Dump debug messages to console during test run\n"
+};
+
+/**
+* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
+* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6).
+*/
+if (XPCOMUtils.generateNSGetFactory)
+    var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]);
+else
+    var NSGetModule = XPCOMUtils.generateNSGetModule([CommandLineHandler]);
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+   <Description about="urn:mozilla:install-manifest">
+     <em:id>pep@mozilla.com</em:id>
+     <em:name>Pep</em:name>
+     <em:version>1.0</em:version>
+     <em:creator>Andrew Halberstadt</em:creator>
+     <em:description>Harness for running responsiveness tests</em:description>
+     <em:targetApplication>
+       <!-- Firefox -->
+       <Description>
+         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+         <em:minVersion>3.6</em:minVersion>
+         <em:maxVersion>15.*</em:maxVersion>
+       </Description>
+     </em:targetApplication>
+     <em:targetApplication>
+       <!-- Thunderbird -->
+       <Description>
+         <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
+         <em:minVersion>3.0a1pre</em:minVersion>
+         <em:maxVersion>15.*</em:maxVersion>
+       </Description>
+     </em:targetApplication>
+   </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/README.md
@@ -0,0 +1,4 @@
+These folders are pulled from https://github.com/mozautomation/mozmill/tree/master/mozmill/mozmill/extension/resource.
+
+To update them, simply checkout the mozmill repo at https://github.com/mozautomation/mozmill, 
+then copy and paste the 'driver' and 'stdlib' folders to this location. 
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/driver/controller.js
@@ -0,0 +1,1038 @@
+// ***** BEGIN LICENSE BLOCK *****
+// Version: MPL 1.1/GPL 2.0/LGPL 2.1
+//
+// The contents of this file are subject to the Mozilla Public License Version
+// 1.1 (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+// http://www.mozilla.org/MPL/
+//
+// Software distributed under the License is distributed on an "AS IS" basis,
+// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+// for the specific language governing rights and limitations under the
+// License.
+//
+// The Original Code is Mozilla Corporation Code.
+//
+// The Initial Developer of the Original Code is
+// Adam Christian.
+// Portions created by the Initial Developer are Copyright (C) 2008
+// the Initial Developer. All Rights Reserved.
+//
+// Contributor(s):
+//  Adam Christian <adam.christian@gmail.com>
+//  Mikeal Rogers <mikeal.rogers@gmail.com>
+//  Henrik Skupin <hskupin@mozilla.com>
+//  Aaron Train <atrain@mozilla.com>
+//
+// Alternatively, the contents of this file may be used under the terms of
+// either the GNU General Public License Version 2 or later (the "GPL"), or
+// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+// in which case the provisions of the GPL or the LGPL are applicable instead
+// of those above. If you wish to allow use of your version of this file only
+// under the terms of either the GPL or the LGPL, and not to allow others to
+// use your version of this file under the terms of the MPL, indicate your
+// decision by deleting the provisions above and replace them with the notice
+// and other provisions required by the GPL or the LGPL. If you do not delete
+// the provisions above, a recipient may use your version of this file under
+// the terms of any one of the MPL, the GPL or the LGPL.
+//
+// ***** END LICENSE BLOCK *****
+
+var EXPORTED_SYMBOLS = ["MozMillController", "globalEventRegistry", "sleep"];
+
+var EventUtils = {}; Components.utils.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
+
+var utils = {}; Components.utils.import('resource://mozmill/stdlib/utils.js', utils);
+var elementslib = {}; Components.utils.import('resource://mozmill/driver/elementslib.js', elementslib);
+var mozelement = {}; Components.utils.import('resource://mozmill/driver/mozelement.js', mozelement);
+var broker = {}; Components.utils.import('resource://mozmill/driver/msgbroker.js', broker);
+
+var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
+                .getService(Components.interfaces.nsIAppShellService)
+                .hiddenDOMWindow;
+var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"].
+     getService(Components.interfaces.nsIConsoleService);
+
+// Declare most used utils functions in the controller namespace
+var sleep = utils.sleep;
+var assert = utils.assert;
+var waitFor = utils.waitFor;
+
+waitForEvents = function() {}
+
+waitForEvents.prototype = {
+  /**
+   * Initialize list of events for given node
+   */
+  init : function waitForEvents_init(node, events) {
+    if (node.getNode != undefined)
+      node = node.getNode();
+  
+    this.events = events;
+    this.node = node;
+    node.firedEvents = {};
+    this.registry = {};
+  
+    for each(e in events) {
+      var listener = function(event) {
+        this.firedEvents[event.type] = true;
+      }
+      this.registry[e] = listener;
+      this.registry[e].result = false;
+      this.node.addEventListener(e, this.registry[e], true);
+    }
+  },
+
+  /**
+   * Wait until all assigned events have been fired
+   */
+  wait : function waitForEvents_wait(timeout, interval)
+  {
+    for (var e in this.registry) {
+      utils.waitFor(function() {
+        return this.node.firedEvents[e] == true;
+      }, "Timeout happened before event '" + ex +"' was fired.", timeout, interval);
+  
+      this.node.removeEventListener(e, this.registry[e], true);
+    }
+  }
+}
+
+/**
+ * Class to handle menus and context menus
+ *
+ * @constructor
+ * @param {MozMillController} controller
+ *        Mozmill controller of the window under test
+ * @param {string} menuSelector
+ *        jQuery like selector string of the element
+ * @param {object} document
+ *        Document to use for finding the menu
+ *        [optional - default: aController.window.document]
+ */
+var Menu = function(controller, menuSelector, document) {
+  this._controller = controller;
+  this._menu = null;
+
+  document = document || controller.window.document;
+  var node = document.querySelector(menuSelector);
+  if (node) {
+    // We don't unwrap nodes automatically yet (Bug 573185)
+    node = node.wrappedJSObject || node;
+    this._menu = new mozelement.Elem(node);
+  }
+  else {
+    throw new Error("Menu element '" + menuSelector + "' not found.");
+  }
+}
+
+Menu.prototype = {
+
+  /**
+   * Open and populate the menu
+   *
+   * @param {ElemBase} contextElement
+   *        Element whose context menu has to be opened
+   * @returns {Menu} The Menu instance
+   */
+  open : function(contextElement) {
+    // We have to open the context menu
+    var menu = this._menu.getNode();
+    if ((menu.localName == "popup" || menu.localName == "menupopup") &&
+        contextElement && contextElement.exists()) {
+      this._controller.rightClick(contextElement);
+      this._controller.waitFor(function() {
+        return menu.state == "open";
+      }, "Context menu has been opened.");
+    }
+
+    // Run through the entire menu and populate with dynamic entries
+    this._buildMenu(menu);
+
+    return this;
+  },
+
+  /**
+   * Close the menu
+   *
+   * @returns {Menu} The Menu instance
+   */
+  close : function() {
+    var menu = this._menu.getNode();
+
+    this._controller.keypress(this._menu, "VK_ESCAPE", {});
+    this._controller.waitFor(function() {
+      return menu.state == "closed";
+    }, "Context menu has been closed.");
+
+    return this;
+  },
+
+  /**
+   * Retrieve the specified menu entry
+   *
+   * @param {string} itemSelector
+   *        jQuery like selector string of the menu item
+   * @returns {ElemBase} Menu element
+   * @throws Error If menu element has not been found
+   */
+  getItem : function(itemSelector) {
+    var node = this._menu.getNode().querySelector(itemSelector);
+
+    if (!node) {
+      throw new Error("Menu entry '" + itemSelector + "' not found.");
+    }
+
+    return new mozelement.Elem(node);
+  },
+
+  /**
+   * Click the specified menu entry
+   *
+   * @param {string} itemSelector
+   *        jQuery like selector string of the menu item
+   *
+   * @returns {Menu} The Menu instance
+   */
+  click : function(itemSelector) {
+    this._controller.click(this.getItem(itemSelector));
+
+    return this;
+  },
+
+  /**
+   * Synthesize a keypress against the menu
+   *
+   * @param {string} key
+   *        Key to press
+   * @param {object} modifier
+   *        Key modifiers
+   * @see MozMillController#keypress
+   *
+   * @returns {Menu} The Menu instance
+   */
+  keypress : function(key, modifier) {
+    this._controller.keypress(this._menu, key, modifier);
+
+    return this;
+  },
+
+  /**
+   * Opens the context menu, click the specified entry and
+   * make sure that the menu has been closed.
+   *
+   * @param {string} itemSelector
+   *        jQuery like selector string of the element
+   * @param {ElemBase} contextElement
+   *        Element whose context menu has to be opened
+   *
+   * @returns {Menu} The Menu instance
+   */
+  select : function(itemSelector, contextElement) {
+    this.open(contextElement);
+    this.click(itemSelector);
+    this.close();
+  },
+
+  /**
+   * Recursive function which iterates through all menu elements and
+   * populates the menus with dynamic menu entries.
+   *
+   * @param {node} menu
+   *        Top menu node whose elements have to be populated
+   */
+  _buildMenu : function(menu) {
+    var items = menu ? menu.childNodes : null;
+
+    Array.forEach(items, function(item) {
+      // When we have a menu node, fake a click onto it to populate
+      // the sub menu with dynamic entries
+      if (item.tagName == "menu") {
+        var popup = item.querySelector("menupopup");
+        if (popup) {
+          if (popup.allowevents) {
+            var popupEvent = this._controller.window.document.createEvent("MouseEvent");
+            popupEvent.initMouseEvent("popupshowing", true, true, this._controller.window, 0,
+                                             0, 0, 0, 0, false, false, false, false, 0, null);
+            popup.dispatchEvent(popupEvent);
+          }
+          this._buildMenu(popup);
+        }
+      }
+    }, this);
+  }
+};
+
+var MozMillController = function (window) {
+  this.window = window;
+
+  this.mozmillModule = {};
+  Components.utils.import('resource://mozmill/driver/mozmill.js', this.mozmillModule);
+
+  utils.waitFor(function() {
+    return window != null && this.isLoaded();
+  }, "controller(): Window could not be initialized.", undefined, undefined, this);
+
+  if ( controllerAdditions[window.document.documentElement.getAttribute('windowtype')] != undefined ) {
+    this.prototype = new utils.Copy(this.prototype);
+    controllerAdditions[window.document.documentElement.getAttribute('windowtype')](this);
+    this.windowtype = window.document.documentElement.getAttribute('windowtype');
+  }
+}
+
+// constructs a MozMillElement from the controller's window
+MozMillController.prototype.__defineGetter__("windowElement", function() {
+  if (this._windowElement == undefined) 
+    this._windowElement = new mozelement.MozMillElement(undefined, undefined, {'element': this.window});
+  return this._windowElement;
+});
+
+MozMillController.prototype.sleep = utils.sleep;
+
+// Open the specified url in the current tab
+MozMillController.prototype.open = function(url)
+{
+  switch(this.mozmillModule.Application) {
+    case "Firefox":
+      this.window.gBrowser.loadURI(url);
+      break;
+    case "SeaMonkey":
+      this.window.getBrowser().loadURI(url);
+      break;
+    default:
+      throw new Error("MozMillController.open not supported.");
+  }
+
+  broker.pass({'function':'Controller.open()'});
+}
+
+/**
+ * Take a screenshot of specified node
+ * 
+ * @param {element} node
+ *   the window or DOM element to capture
+ * @param {string} name
+ *   the name of the screenshot used in reporting and as filename
+ * @param {boolean} save
+ *   if true saves the screenshot as 'name.png' in tempdir, otherwise returns a dataURL
+ * @param {element list} highlights
+ *   a list of DOM elements to highlight by drawing a red rectangle around them
+ */
+MozMillController.prototype.screenShot = function _screenShot(node, name, save, highlights) {
+  if (!node) {
+    throw new Error("node is undefined");
+  }
+  
+  // Unwrap the node and highlights
+  if ("getNode" in node) node = node.getNode();
+  if (highlights) {
+    for (var i = 0; i < highlights.length; ++i) {
+      if ("getNode" in highlights[i]) {
+        highlights[i] = highlights[i].getNode();
+      }
+    }
+  }
+  
+  // If save is false, a dataURL is used
+  // Include both in the report anyway to avoid confusion and make the report easier to parse
+  var filepath, dataURL;
+  try {
+    if (save) {
+      filepath = utils.takeScreenshot(node, name, highlights);
+    } else {
+      dataURL = utils.takeScreenshot(node, undefined, highlights);
+    }
+  } catch (e) {
+    throw new Error("controller.screenShot() failed: " + e);
+  }
+
+  // Create a timestamp
+  var d = new Date();
+  // Report object
+  var obj = { "filepath": filepath,
+              "dataURL": dataURL,
+              "name": name,
+              "timestamp": d.toLocaleString(),
+            }
+  // Send the screenshot object to python over jsbridge
+  broker.sendMessage("screenShot", obj);
+  broker.pass({'function':'controller.screenShot()'});
+}
+
+/**
+ * Checks if the specified window has been loaded
+ *
+ * @param {DOMWindow} [window=this.window] Window object to check for loaded state
+ */
+MozMillController.prototype.isLoaded = function(window) {
+  var win = window || this.window;
+
+  return ("mozmillDocumentLoaded" in win) && win.mozmillDocumentLoaded;
+};
+
+MozMillController.prototype.waitFor = function(callback, message, timeout,
+                                               interval, thisObject) {
+  utils.waitFor(callback, message, timeout, interval, thisObject);
+
+  broker.pass({'function':'controller.waitFor()'});
+}
+
+MozMillController.prototype.__defineGetter__("waitForEvents", function() {
+  if (this._waitForEvents == undefined)
+    this._waitForEvents = new waitForEvents();
+  return this._waitForEvents;
+});
+
+/**
+ * Wrapper function to create a new instance of a menu
+ * @see Menu
+ */
+MozMillController.prototype.getMenu = function (menuSelector, document) {
+  return new Menu(this, menuSelector, document);
+};
+
+MozMillController.prototype.__defineGetter__("mainMenu", function() {
+  return this.getMenu("menubar");
+});
+
+MozMillController.prototype.__defineGetter__("menus", function() {
+        throw('controller.menus - DEPRECATED Use controller.mainMenu instead.');
+
+});
+
+MozMillController.prototype.waitForImage = function (elem, timeout, interval) {
+  this.waitFor(function() {
+    return elem.getNode().complete == true;
+  }, "timeout exceeded for waitForImage " + elem.getInfo(), timeout, interval);
+
+  broker.pass({'function':'Controller.waitForImage()'});
+}
+
+MozMillController.prototype.startUserShutdown = function (timeout, restart, next, resetProfile) {
+  if (restart && resetProfile) {
+      throw new Error("You can't have a user-restart and reset the profile; there is a race condition");
+  }
+  broker.sendMessage('userShutdown', {'user': true,
+                                  'restart': Boolean(restart),
+                                  'next': next,
+                                  'resetProfile': Boolean(resetProfile)});
+  this.window.setTimeout(broker.sendMessage, timeout, 'userShutdown', 0);
+}
+
+MozMillController.prototype.restartApplication = function (next, resetProfile) 
+{
+  // restart the application via the python runner
+  // - next : name of the next test function to run after restart
+  // - resetProfile : whether to reset the profile after restart
+  broker.sendMessage('userShutdown', {'user': false,
+                                  'restart': true,
+                                  'next': next,
+                                  'resetProfile': Boolean(resetProfile)});
+  broker.sendMessage('endTest');
+  broker.sendMessage('persist');
+  utils.getMethodInWindows('goQuitApplication')();
+}
+
+MozMillController.prototype.stopApplication = function (resetProfile) 
+{
+  // stop the application via the python runner
+  // - resetProfile : whether to reset the profile after shutdown
+  broker.sendMessage('userShutdown', {'user': false,
+                                  'restart': false,
+                                  'resetProfile': Boolean(resetProfile)});
+  broker.sendMessage('endTest');
+  broker.sendMessage('persist');
+  utils.getMethodInWindows('goQuitApplication')();
+}
+
+//Browser navigation functions
+MozMillController.prototype.goBack = function(){
+  this.window.content.history.back();
+  broker.pass({'function':'Controller.goBack()'});
+  return true;
+}
+MozMillController.prototype.goForward = function(){
+  this.window.content.history.forward();
+  broker.pass({'function':'Controller.goForward()'});
+  return true;
+}
+MozMillController.prototype.refresh = function(){
+  this.window.content.location.reload(true);
+  broker.pass({'function':'Controller.refresh()'});
+  return true;
+}
+
+function logDeprecated(funcName, message) {
+   broker.log({'function': funcName + '() - DEPRECATED', 'message': funcName + '() is deprecated' + message});
+}
+
+function logDeprecatedAssert(funcName) {
+   logDeprecated('controller.' + funcName, '. use the generic `assert` module instead');
+}
+
+MozMillController.prototype.assertText = function (el, text) {
+  logDeprecatedAssert("assertText");
+  //this.window.focus();
+  var n = el.getNode();
+
+  if (n && n.innerHTML == text){
+    broker.pass({'function':'Controller.assertText()'});
+    return true;
+   }
+
+  throw new Error("could not validate element " + el.getInfo()+" with text "+ text);
+  return false;
+
+};
+
+//Assert that a specified node exists
+MozMillController.prototype.assertNode = function (el) {
+  logDeprecatedAssert("assertNode");
+  
+  //this.window.focus();
+  var element = el.getNode();
+  if (!element){
+    throw new Error("could not find element " + el.getInfo());
+    return false;
+  }
+  broker.pass({'function':'Controller.assertNode()'});
+  return true;
+};
+
+// Assert that a specified node doesn't exist
+MozMillController.prototype.assertNodeNotExist = function (el) {
+  logDeprecatedAssert("assertNodeNotExist");
+  
+  //this.window.focus();
+  try {
+    var element = el.getNode();
+  } catch(err){
+    broker.pass({'function':'Controller.assertNodeNotExist()'});
+    return true;
+  }
+
+  if (element) {
+    throw new Error("Unexpectedly found element " + el.getInfo());
+    return false;
+  } else {
+    broker.pass({'function':'Controller.assertNodeNotExist()'});
+    return true;
+  }
+};
+
+//Assert that a form element contains the expected value
+MozMillController.prototype.assertValue = function (el, value) {
+  logDeprecatedAssert("assertValue");
+  
+  //this.window.focus();
+  var n = el.getNode();
+
+  if (n && n.value == value){
+    broker.pass({'function':'Controller.assertValue()'});
+    return true;
+  }
+  throw new Error("could not validate element " + el.getInfo()+" with value "+ value);
+  return false;
+};
+
+/**
+ * Check if the callback function evaluates to true
+ */
+MozMillController.prototype.assert = function(callback, message, thisObject)
+{
+  logDeprecatedAssert("assert");
+  utils.assert(callback, message, thisObject);
+
+  broker.pass({'function': ": controller.assert('" + callback + "')"});
+  return true;
+}
+
+//Assert that a provided value is selected in a select element
+MozMillController.prototype.assertSelected = function (el, value) {
+  logDeprecatedAssert("assertSelected");
+  
+  //this.window.focus();
+  var n = el.getNode();
+  var validator = value;
+
+  if (n && n.options[n.selectedIndex].value == validator){
+    broker.pass({'function':'Controller.assertSelected()'});
+    return true;
+    }
+  throw new Error("could not assert value for element " + el.getInfo()+" with value "+ value);
+  return false;
+};
+
+//Assert that a provided checkbox is checked
+MozMillController.prototype.assertChecked = function (el) {
+  logDeprecatedAssert("assertChecked");
+  
+  //this.window.focus();
+  var element = el.getNode();
+
+  if (element && element.checked == true){
+    broker.pass({'function':'Controller.assertChecked()'});
+    return true;
+    }
+  throw new Error("assert failed for checked element " + el.getInfo());
+  return false;
+};
+
+// Assert that a provided checkbox is not checked
+MozMillController.prototype.assertNotChecked = function (el) {
+  logDeprecatedAssert("assertNotChecked");
+  
+  var element = el.getNode();
+
+  if (!element) {
+    throw new Error("Could not find element" + el.getInfo());
+  }
+
+  if (!element.hasAttribute("checked") || element.checked != true){
+    broker.pass({'function':'Controller.assertNotChecked()'});
+    return true;
+    }
+  throw new Error("assert failed for not checked element " + el.getInfo());
+  return false;
+};
+
+/** 
+ * Assert that an element's javascript property exists or has a particular value
+ *
+ * if val is undefined, will return true if the property exists.
+ * if val is specified, will return true if the property exists and has the correct value
+ */
+MozMillController.prototype.assertJSProperty = function(el, attrib, val) {
+  logDeprecatedAssert("assertJSProperty");
+  
+  var element = el.getNode();
+  if (!element){
+    throw new Error("could not find element " + el.getInfo());
+    return false;
+  }
+  var value = element[attrib];
+  var res = (value !== undefined && (val === undefined ? true : String(value) == String(val)));
+  if (res) {
+    broker.pass({'function':'Controller.assertJSProperty("' + el.getInfo() + '") : ' + val});
+  } else {
+    throw new Error("Controller.assertJSProperty(" + el.getInfo() + ") : " + 
+                     (val === undefined ? "property '" + attrib + "' doesn't exist" : val + " == " + value));
+  }
+  return res;
+};
+
+/** 
+ * Assert that an element's javascript property doesn't exist or doesn't have a particular value
+ *
+ * if val is undefined, will return true if the property doesn't exist.
+ * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
+ */
+MozMillController.prototype.assertNotJSProperty = function(el, attrib, val) {
+  logDeprecatedAssert("assertNotJSProperty");
+  
+  var element = el.getNode();
+  if (!element){
+    throw new Error("could not find element " + el.getInfo());
+    return false;
+  }
+  var value = element[attrib];
+  var res = (val === undefined ? value === undefined : String(value) != String(val));
+  if (res) {
+    broker.pass({'function':'Controller.assertNotProperty("' + el.getInfo() + '") : ' + val});
+  } else {
+    throw new Error("Controller.assertNotJSProperty(" + el.getInfo() + ") : " +
+                     (val === undefined ? "property '" + attrib + "' exists" : val + " != " + value));
+  }
+  return res;
+};
+
+/** 
+ * Assert that an element's dom property exists or has a particular value
+ *
+ * if val is undefined, will return true if the property exists.
+ * if val is specified, will return true if the property exists and has the correct value
+ */
+MozMillController.prototype.assertDOMProperty = function(el, attrib, val) {
+  logDeprecatedAssert("assertDOMProperty");
+  
+  var element = el.getNode();
+  if (!element){
+    throw new Error("could not find element " + el.getInfo());
+    return false;
+  }
+  var value, res = element.hasAttribute(attrib);
+  if (res && val !== undefined) {
+    value = element.getAttribute(attrib);
+    res = (String(value) == String(val));
+  }   
+ 
+  if (res) {
+    broker.pass({'function':'Controller.assertDOMProperty("' + el.getInfo() + '") : ' + val});
+  } else {
+    throw new Error("Controller.assertDOMProperty(" + el.getInfo() + ") : " + 
+                     (val === undefined ? "property '" + attrib + "' doesn't exist" : val + " == " + value));
+  }
+  return res;
+};
+
+/** 
+ * Assert that an element's dom property doesn't exist or doesn't have a particular value
+ *
+ * if val is undefined, will return true if the property doesn't exist.
+ * if val is specified, will return true if the property doesn't exist or doesn't have the specified value
+ */
+MozMillController.prototype.assertNotDOMProperty = function(el, attrib, val) {
+  logDeprecatedAssert("assertNotDOMProperty");
+  
+  var element = el.getNode();
+  if (!element){
+    throw new Error("could not find element " + el.getInfo());
+    return false;
+  }
+  var value, res = element.hasAttribute(attrib);
+  if (res && val !== undefined) {
+    value = element.getAttribute(attrib);
+    res = (String(value) == String(val));
+  }   
+  if (!res) {
+    broker.pass({'function':'Controller.assertNotDOMProperty("' + el.getInfo() + '") : ' + val});
+  } else {
+    throw new Error("Controller.assertNotDOMProperty(" + el.getInfo() + ") : " + 
+                     (val == undefined ? "property '" + attrib + "' exists" : val + " == " + value));
+  }
+  return !res;
+};
+
+// deprecated - Use assertNotJSProperty or assertNotDOMProperty instead
+MozMillController.prototype.assertProperty = function(el, attrib, val) {
+  logDeprecatedAssert("assertProperty");
+  return this.assertJSProperty(el, attrib, val);
+};
+
+// deprecated - Use assertNotJSProperty or assertNotDOMProperty instead
+MozMillController.prototype.assertPropertyNotExist = function(el, attrib) {
+  logDeprecatedAssert("assertPropertyNotExist");
+  return this.assertNotJSProperty(el, attrib);
+};
+
+// Assert that a specified image has actually loaded
+// The Safari workaround results in additional requests
+// for broken images (in Safari only) but works reliably
+MozMillController.prototype.assertImageLoaded = function (el) {
+  logDeprecatedAssert("assertImageLoaded");
+  
+  //this.window.focus();
+  var img = el.getNode();
+  if (!img || img.tagName != 'IMG') {
+    throw new Error('Controller.assertImageLoaded() failed.')
+    return false;
+  }
+  var comp = img.complete;
+  var ret = null; // Return value
+
+  // Workaround for Safari -- it only supports the
+  // complete attrib on script-created images
+  if (typeof comp == 'undefined') {
+    test = new Image();
+    // If the original image was successfully loaded,
+    // src for new one should be pulled from cache
+    test.src = img.src;
+    comp = test.complete;
+  }
+
+  // Check the complete attrib. Note the strict
+  // equality check -- we don't want undefined, null, etc.
+  // --------------------------
+  // False -- Img failed to load in IE/Safari, or is
+  // still trying to load in FF
+  if (comp === false) {
+    ret = false;
+  }
+  // True, but image has no size -- image failed to
+  // load in FF
+  else if (comp === true && img.naturalWidth == 0) {
+    ret = false;
+  }
+  // Otherwise all we can do is assume everything's
+  // hunky-dory
+  else {
+    ret = true;
+  }
+  if (ret) {
+    broker.pass({'function':'Controller.assertImageLoaded'});
+  } else {
+    throw new Error('Controller.assertImageLoaded() failed.')
+  }
+
+  return ret;
+};
+
+// Drag one element to the top x,y coords of another specified element
+MozMillController.prototype.mouseMove = function (doc, start, dest) {
+  // if one of these elements couldn't be looked up
+  if (typeof start != 'object'){
+    throw new Error("received bad coordinates");
+    return false;
+  }
+  if (typeof dest != 'object'){
+    throw new Error("received bad coordinates");
+    return false;
+  }
+
+  var triggerMouseEvent = function(element, clientX, clientY) {
+    clientX = clientX ? clientX: 0;
+    clientY = clientY ? clientY: 0;
+
+    // make the mouse understand where it is on the screen
+    var screenX = element.boxObject.screenX ? element.boxObject.screenX : 0;
+    var screenY = element.boxObject.screenY ? element.boxObject.screenY : 0;
+
+    var evt = element.ownerDocument.createEvent('MouseEvents');
+    if (evt.initMouseEvent) {
+      evt.initMouseEvent('mousemove', true, true, element.ownerDocument.defaultView, 1, screenX, screenY, clientX, clientY)
+    }
+    else {
+      //LOG.warn("element doesn't have initMouseEvent; firing an event which should -- but doesn't -- have other mouse-event related attributes here, as well as controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown");
+      evt.initEvent('mousemove', true, true);
+    }
+    element.dispatchEvent(evt);
+  };
+
+  // Do the initial move to the drag element position
+  triggerMouseEvent(doc.body, start[0], start[1]);
+  triggerMouseEvent(doc.body, dest[0], dest[1]);
+  broker.pass({'function':'Controller.mouseMove()'});
+  return true;
+}
+
+// Drag an element to the specified offset on another element, firing mouse and drag events.
+// Returns the captured dropEffect. Adapted from EventUtils' synthesizeDrop()
+MozMillController.prototype.dragToElement = function(src, dest, offsetX,
+    offsetY, aWindow, dropEffect, dragData) {
+  srcElement = src.getNode();
+  destElement = dest.getNode();
+  aWindow = aWindow || srcElement.ownerDocument.defaultView;
+  offsetX = offsetX || 20;
+  offsetY = offsetY || 20;
+
+  var dataTransfer;
+
+  var trapDrag = function(event) {
+    dataTransfer = event.dataTransfer;
+    if(!dragData)
+      return;
+
+    for (var i = 0; i < dragData.length; i++) {
+      var item = dragData[i];
+      for (var j = 0; j < item.length; j++) {
+        dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+      }
+    }
+    dataTransfer.dropEffect = dropEffect || "move";
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  aWindow.addEventListener("dragstart", trapDrag, true);
+  EventUtils.synthesizeMouse(srcElement, 2, 2, { type: "mousedown" }, aWindow); // fire mousedown 2 pixels from corner of element
+  EventUtils.synthesizeMouse(srcElement, 11, 11, { type: "mousemove" }, aWindow);
+  EventUtils.synthesizeMouse(srcElement, offsetX, offsetY, { type: "mousemove" }, aWindow);
+  aWindow.removeEventListener("dragstart", trapDrag, true);
+
+  var event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent("dragenter", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+  destElement.dispatchEvent(event);
+
+  var event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent("dragover", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+  if (destElement.dispatchEvent(event)) {
+    EventUtils.synthesizeMouse(destElement, offsetX, offsetY, { type: "mouseup" }, aWindow);
+    return "none";
+  }
+
+  event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent("drop", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+  destElement.dispatchEvent(event);
+  EventUtils.synthesizeMouse(destElement, offsetX, offsetY, { type: "mouseup" }, aWindow);
+
+  return dataTransfer.dropEffect;
+}
+
+function preferencesAdditions(controller) {
+  var mainTabs = controller.window.document.getAnonymousElementByAttribute(controller.window.document.documentElement, 'anonid', 'selector');
+  controller.tabs = {};
+  for (var i = 0; i < mainTabs.childNodes.length; i++) {
+    var node  = mainTabs.childNodes[i];
+    var obj = {'button':node}
+    controller.tabs[i] = obj;
+    var label = node.attributes.item('label').value.replace('pane', '');
+    controller.tabs[label] = obj;
+  }
+  controller.prototype.__defineGetter__("activeTabButton",
+    function () {return mainTabs.getElementsByAttribute('selected', true)[0];
+  })
+}
+
+function Tabs (controller) {
+  this.controller = controller;
+}
+Tabs.prototype.getTab = function(index) {
+  return this.controller.window.gBrowser.browsers[index].contentDocument;
+}
+Tabs.prototype.__defineGetter__("activeTab", function() {
+  return this.controller.window.gBrowser.selectedBrowser.contentDocument;
+})
+Tabs.prototype.selectTab = function(index) {
+  // GO in to tab manager and grab the tab by index and call focus.
+}
+Tabs.prototype.findWindow = function (doc) {
+  for (var i = 0; i <= (this.controller.window.frames.length - 1); i++) {
+    if (this.controller.window.frames[i].document == doc) {
+      return this.controller.window.frames[i];
+    }
+  }
+  throw new Error("Cannot find window for document. Doc title == " + doc.title);
+}
+Tabs.prototype.getTabWindow = function(index) {
+  return this.findWindow(this.getTab(index));
+}
+Tabs.prototype.__defineGetter__("activeTabWindow", function () {
+  return this.findWindow(this.activeTab);
+})
+Tabs.prototype.__defineGetter__("length", function () {
+  return this.controller.window.gBrowser.browsers.length;
+})
+Tabs.prototype.__defineGetter__("activeTabIndex", function() {
+  return this.controller.window.gBrowser.tabContainer.selectedIndex;
+})
+Tabs.prototype.selectTabIndex = function(i) {
+  this.controller.window.gBrowser.selectTabAtIndex(i);
+}
+
+function browserAdditions (controller) {
+  controller.tabs = new Tabs(controller);
+
+  controller.waitForPageLoad = function(aDocument, aTimeout, aInterval) {
+    var timeout = aTimeout || 30000;
+    var win = null;
+
+    // If a user tries to do waitForPageLoad(2000), this will assign the
+    // interval the first arg which is most likely what they were expecting
+    if (typeof(aDocument) == "number"){
+      timeout = aDocument;
+    }
+
+    // If we have a real document use its default view
+    if (aDocument && (typeof(aDocument) === "object") &&
+        "defaultView" in aDocument)
+      win = aDocument.defaultView;
+
+    // If no document has been specified, fallback to the default view of the
+    // currently selected tab browser
+    win = win || this.window.gBrowser.selectedBrowser.contentWindow;
+
+    // Wait until the content in the tab has been loaded
+    this.waitFor(function () {
+        var loaded = this.isLoaded(win);
+        var firstRun = !('mozmillWaitForPageLoad' in win);
+        var ret = firstRun && loaded;
+        if (ret) {
+          win.mozmillWaitForPageLoad = true;
+        }
+        return ret;
+    }, "controller.waitForPageLoad(): Timeout waiting for page loaded.",
+        timeout, aInterval, this);
+
+    broker.pass({'function':'controller.waitForPageLoad()'});
+  }
+}
+
+controllerAdditions = {
+  'Browser:Preferences':preferencesAdditions,
+  'navigator:browser'  :browserAdditions,
+}
+
+/**
+ *  DEPRECATION WARNING
+ *
+ * The following methods have all been DEPRECATED as of Mozmill 2.0
+ * Use the MozMillElement object instead (https://developer.mozilla.org/en/Mozmill/Mozmill_Element_Object)
+ */
+MozMillController.prototype.select = function (elem, index, option, value) {
+  return elem.select(index, option, value); 
+};
+
+MozMillController.prototype.keypress = function(aTarget, aKey, aModifiers, aExpectedEvent) {
+  if (aTarget == null) { aTarget = this.windowElement; }
+  return aTarget.keypress(aKey, aModifiers, aExpectedEvent);
+}
+
+MozMillController.prototype.type = function (aTarget, aText, aExpectedEvent) {
+  if (aTarget == null) { aTarget = this.windowElement; }
+
+  var that = this;
+  var retval = true;
+  Array.forEach(aText, function(letter) {
+    if (!that.keypress(aTarget, letter, {}, aExpectedEvent)) {
+      retval = false; }
+  });
+
+  return retval;
+}
+
+MozMillController.prototype.mouseEvent = function(aTarget, aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
+  return aTarget.mouseEvent(aOffsetX, aOffsetY, aEvent, aExpectedEvent);
+}
+
+MozMillController.prototype.click = function(elem, left, top, expectedEvent) {
+  return elem.click(left, top, expectedEvent);
+}
+
+MozMillController.prototype.doubleClick = function(elem, left, top, expectedEvent) {
+  return elem.doubleClick(left, top, expectedEvent);
+}
+
+MozMillController.prototype.mouseDown = function (elem, button, left, top, expectedEvent) {
+  return elem.mouseDown(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseOut = function (elem, button, left, top, expectedEvent) {
+  return elem.mouseOut(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseOver = function (elem, button, left, top, expectedEvent) {
+  return elem.mouseOver(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.mouseUp = function (elem, button, left, top, expectedEvent) {
+  return elem.mouseUp(button, left, top, expectedEvent);
+};
+
+MozMillController.prototype.middleClick = function(elem, left, top, expectedEvent) {
+  return elem.middleClick(elem, left, top, expectedEvent);
+}
+
+MozMillController.prototype.rightClick = function(elem, left, top, expectedEvent) {
+  return elem.rightClick(left, top, expectedEvent);
+}
+
+MozMillController.prototype.check = function(elem, state) {
+  return elem.check(state);
+}
+
+MozMillController.prototype.radio = function(elem) {
+  return elem.select();
+}
+
+MozMillController.prototype.waitThenClick = function (elem, timeout, interval) {
+  return elem.waitThenClick(timeout, interval);
+}
+
+MozMillController.prototype.waitForElement = function(elem, timeout, interval) {
+  return elem.waitForElement(timeout, interval);
+}
+
+MozMillController.prototype.waitForElementNotPresent = function(elem, timeout, interval) {
+  return elem.waitForElementNotPresent(timeout, interval);
+}
+
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/driver/elementslib.js
@@ -0,0 +1,478 @@
+// ***** BEGIN LICENSE BLOCK *****// ***** BEGIN LICENSE BLOCK *****
+// Version: MPL 1.1/GPL 2.0/LGPL 2.1
+// 
+// The contents of this file are subject to the Mozilla Public License Version
+// 1.1 (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+// http://www.mozilla.org/MPL/
+// 
+// Software distributed under the License is distributed on an "AS IS" basis,
+// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+// for the specific language governing rights and limitations under the
+// License.
+// 
+// The Original Code is Mozilla Corporation Code.
+// 
+// The Initial Developer of the Original Code is
+// Adam Christian.
+// Portions created by the Initial Developer are Copyright (C) 2008
+// the Initial Developer. All Rights Reserved.
+// 
+// Contributor(s):
+//  Adam Christian <adam.christian@gmail.com>
+//  Mikeal Rogers <mikeal.rogers@gmail.com>
+// 
+// Alternatively, the contents of this file may be used under the terms of
+// either the GNU General Public License Version 2 or later (the "GPL"), or
+// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+// in which case the provisions of the GPL or the LGPL are applicable instead
+// of those above. If you wish to allow use of your version of this file only
+// under the terms of either the GPL or the LGPL, and not to allow others to
+// use your version of this file under the terms of the MPL, indicate your
+// decision by deleting the provisions above and replace them with the notice
+// and other provisions required by the GPL or the LGPL. If you do not delete
+// the provisions above, a recipient may use your version of this file under
+// the terms of any one of the MPL, the GPL or the LGPL.
+// 
+// ***** END LICENSE BLOCK *****
+
+var EXPORTED_SYMBOLS = ["Elem", "ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath",
+                        "Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib",
+                       ];
+
+var utils = {}; Components.utils.import('resource://mozmill/stdlib/utils.js', utils);
+var strings = {}; Components.utils.import('resource://mozmill/stdlib/strings.js', strings);
+var arrays = {}; Components.utils.import('resource://mozmill/stdlib/arrays.js', arrays);
+var json2 = {}; Components.utils.import('resource://mozmill/stdlib/json2.js', json2);
+var withs = {}; Components.utils.import('resource://mozmill/stdlib/withs.js', withs);
+var dom = {}; Components.utils.import('resource://mozmill/stdlib/dom.js', dom);
+var objects = {}; Components.utils.import('resource://mozmill/stdlib/objects.js', objects);
+
+var countQuotes = function(str){
+  var count = 0;
+  var i = 0;
+  while(i < str.length) {
+    i = str.indexOf('"', i);
+    if (i != -1) {
+      count++;
+      i++;
+    } else {
+      break;
+    }
+  }
+  return count;
+};
+
+/**
+ * smartSplit()
+ *
+ * Takes a lookup string as input and returns
+ * a list of each node in the string
+ */
+var smartSplit = function (str) {
+  // Ensure we have an even number of quotes
+  if (countQuotes(str) % 2 != 0) {
+    throw new Error ("Invalid Lookup Expression");
+  }
+  
+  /**
+   * This regex matches a single "node" in a lookup string.
+   * In otherwords, it matches the part between the two '/'s
+   *
+   * Regex Explanation:
+   * \/ - start matching at the first forward slash
+   * ([^\/"]*"[^"]*")* - match as many pairs of quotes as possible until we hit a slash (ignore slashes inside quotes)
+   * [^\/]* - match the remainder of text outside of last quote but before next slash
+   */
+  var re = /\/([^\/"]*"[^"]*")*[^\/]*/g
+  var ret = []
+  var match = re.exec(str);
+  while (match != null) {
+    ret.push(match[0].replace(/^\//, ""));
+    match = re.exec(str);
+  }
+  return ret;
+};
+
+/**
+ * defaultDocuments()
+ *
+ * Returns a list of default documents in which to search for elements
+ * if no document is provided
+ */
+function defaultDocuments() {
+  var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'].getService(Components.interfaces.nsIWindowMediator);
+  win = windowManager.getMostRecentWindow("navigator:browser");
+  return [win.gBrowser.selectedBrowser.contentDocument, win.document];
+};
+
+/**
+ * nodeSearch()
+ *
+ * Takes an optional document, callback and locator string
+ * Returns a handle to the located element or null
+ */
+function nodeSearch(doc, func, string) {
+  if (doc != undefined) {
+    var documents = [doc];
+  } else {
+    var documents = defaultDocuments();
+  }
+  var e = null;
+  var element = null;
+  //inline function to recursively find the element in the DOM, cross frame.
+  var search = function(win, func, string) {
+    if (win == null)
+      return;
+
+    //do the lookup in the current window
+    element = func.call(win, string);
+    
+    if (!element || (element.length == 0)) {
+      var frames = win.frames;
+      for (var i=0; i < frames.length; i++) {
+        search(frames[i], func, string);
+      }
+    }
+    else { e = element; }
+  };
+  
+  for (var i = 0; i < documents.length; ++i) {
+    var win = documents[i].defaultView;
+    search(win, func, string);
+    if (e) break;
+  }
+  return e;
+};
+
+/**
+ * Selector()
+ *
+ * Finds an element by selector string
+ */
+function Selector(_document, selector, index) {
+  if (selector == undefined) {
+    throw new Error('Selector constructor did not recieve enough arguments.');
+  }
+  this.selector = selector;
+  this.getNodeForDocument = function (s) {
+    return this.document.querySelectorAll(s);
+  };
+  var nodes = nodeSearch(_document, this.getNodeForDocument, this.selector);
+  return nodes ? nodes[index || 0] : null;
+};
+
+/**
+ * ID()
+ *
+ * Finds an element by ID
+ */
+function ID(_document, nodeID) {
+  if (nodeID == undefined) {
+    throw new Error('ID constructor did not recieve enough arguments.');
+  }
+  this.getNodeForDocument = function (nodeID) {
+    return this.document.getElementById(nodeID);
+  };
+  return nodeSearch(_document, this.getNodeForDocument, nodeID);
+};
+
+/**
+ * Link()
+ *
+ * Finds a link by innerHTML
+ */
+function Link(_document, linkName) {
+  if (linkName == undefined) {
+    throw new Error('Link constructor did not recieve enough arguments.');
+  }
+  
+  this.getNodeForDocument = function (linkName) {
+    var getText = function(el){
+      var text = "";
+      if (el.nodeType == 3){ //textNode
+        if (el.data != undefined){
+          text = el.data;
+        } else {
+          text = el.innerHTML;
+        }
+      text = text.replace(/n|r|t/g, " ");
+      }
+      if (el.nodeType == 1){ //elementNode
+        for (var i = 0; i < el.childNodes.length; i++) {
+          var child = el.childNodes.item(i);
+          text += getText(child);
+        }
+        if (el.tagName == "P" || el.tagName == "BR" || el.tagName == "HR" || el.tagName == "DIV") {
+          text += "n";
+        }
+      }
+      return text;
+    };
+  
+    //sometimes the windows won't have this function
+    try { 
+      var links = this.document.getElementsByTagName('a'); }
+    catch(err){ // ADD LOG LINE mresults.write('Error: '+ err, 'lightred'); 
+    }
+    for (var i = 0; i < links.length; i++) {
+      var el = links[i];
+      //if (getText(el).indexOf(this.linkName) != -1) {
+      if (el.innerHTML.indexOf(linkName) != -1){
+        return el;
+      }
+    }
+    return null;
+  };
+  
+  return nodeSearch(_document, this.getNodeForDocument, linkName);
+};
+
+/**
+ * XPath()
+ *
+ * Finds an element by XPath
+ */
+function XPath(_document, expr) {
+  if (expr == undefined) {
+    throw new Error('XPath constructor did not recieve enough arguments.');
+  }
+  
+  this.getNodeForDocument = function (s) {
+    var aNode = this.document;
+    var aExpr = s;
+    var xpe = null;
+
+    if (this.document.defaultView == null) {
+      xpe = new getMethodInWindows('XPathEvaluator')();
+    } else {
+      xpe = new this.document.defaultView.XPathEvaluator();
+    }
+    var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement : aNode.ownerDocument.documentElement);
+    var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
+    var found = [];
+    var res;
+    while (res = result.iterateNext())
+      found.push(res);
+    return found[0];
+  };
+  return nodeSearch(_document, this.getNodeForDocument, expr);
+};
+
+/**
+ * Name()
+ *
+ * Finds an element by Name
+ */
+function Name(_document, nName) {
+  if (nName == undefined) {
+    throw new Error('Name constructor did not recieve enough arguments.');
+  }
+  this.getNodeForDocument = function (s) {
+    try{
+      var els = this.document.getElementsByName(s);
+      if (els.length > 0) { return els[0]; }
+    }
+    catch(err){};
+    return null;
+  };
+  return nodeSearch(_document, this.getNodeForDocument, nName);
+};
+
+
+var _returnResult = function (results) {
+  if (results.length == 0) {
+    return null
+  } else if (results.length == 1) {
+    return results[0];
+  } else {
+    return results;
+  }
+}
+var _forChildren = function (element, name, value) {
+  var results = [];
+  var nodes = [e for each (e in element.childNodes) if (e)]
+  for (var i in nodes) {
+    var n = nodes[i];
+    if (n[name] == value) {
+      results.push(n);
+    }
+  }
+  return results;
+}
+var _forAnonChildren = function (_document, element, name, value) {
+  var results = [];
+  var nodes = [e for each (e in _document.getAnoymousNodes(element)) if (e)];
+  for (var i in nodes ) {
+    var n = nodes[i];
+    if (n[name] == value) {
+      results.push(n);
+    }
+  }
+  return results;
+}
+var _byID = function (_document, parent, value) {
+  return _returnResult(_forChildren(parent, 'id', value));
+}
+var _byName = function (_document, parent, value) {
+  return _returnResult(_forChildren(parent, 'tagName', value));
+}
+var _byAttrib = function (parent, attributes) {
+  var results = [];
+
+  var nodes = parent.childNodes;
+  for (var i in nodes) {
+    var n = nodes[i];
+    requirementPass = 0;
+    requirementLength = 0;
+    for (var a in attributes) {
+      requirementLength++;
+      try {
+        if (n.getAttribute(a) == attributes[a]) {
+          requirementPass++;
+        }
+      } catch (err) {
+        // Workaround any bugs in custom attribute crap in XUL elements
+      }
+    }
+    if (requirementPass == requirementLength) {
+      results.push(n);
+    }
+  }
+  return _returnResult(results)
+}
+var _byAnonAttrib = function (_document, parent, attributes) {
+  var results = [];
+  
+  if (objects.getLength(attributes) == 1) {
+    for (var i in attributes) {var k = i; var v = attributes[i]; }
+    var result = _document.getAnonymousElementByAttribute(parent, k, v)
+    if (result) {
+      return result;
+      
+    } 
+  }
+  var nodes = [n for each (n in _document.getAnonymousNodes(parent)) if (n.getAttribute)];
+  function resultsForNodes (nodes) {
+    for (var i in nodes) {
+      var n = nodes[i];
+      requirementPass = 0;
+      requirementLength = 0;
+      for (var a in attributes) {
+        requirementLength++;
+        if (n.getAttribute(a) == attributes[a]) {
+          requirementPass++;
+        }
+      }
+      if (requirementPass == requirementLength) {
+        results.push(n);
+      }
+    }
+  }  
+  resultsForNodes(nodes)  
+  if (results.length == 0) {
+    resultsForNodes([n for each (n in parent.childNodes) if (n != undefined && n.getAttribute)])
+  }
+  return _returnResult(results)
+}
+var _byIndex = function (_document, parent, i) {
+  if (parent instanceof Array) {
+    return parent[i];
+  }
+  return parent.childNodes[i];
+}
+var _anonByName = function (_document, parent, value) {
+  return _returnResult(_forAnonChildren(_document, parent, 'tagName', value));
+}
+var _anonByAttrib = function (_document, parent, value) {
+  return _byAnonAttrib(_document, parent, value);
+}
+var _anonByIndex = function (_document, parent, i) {
+  return _document.getAnonymousNodes(parent)[i];
+}
+
+/**
+ * Lookup()
+ *
+ * Finds an element by Lookup expression
+ */
+function Lookup (_document, expression) {
+  if (expression == undefined) {
+    throw new Error('Lookup constructor did not recieve enough arguments.');
+  }
+
+  var expSplit = [e for each (e in smartSplit(expression) ) if (e != '')];
+  expSplit.unshift(_document)
+  var nCases = {'id':_byID, 'name':_byName, 'attrib':_byAttrib, 'index':_byIndex};
+  var aCases = {'name':_anonByName, 'attrib':_anonByAttrib, 'index':_anonByIndex};
+  
+ 
+  var reduceLookup = function (parent, exp) {
+    // Handle case where only index is provided
+    var cases = nCases;
+    
+    // Handle ending index before any of the expression gets mangled
+    if (withs.endsWith(exp, ']')) {
+      var expIndex = json2.JSON.parse(strings.vslice(exp, '[', ']'));
+    }
+    // Handle anon
+    if (withs.startsWith(exp, 'anon')) {
+      var exp = strings.vslice(exp, '(', ')');
+      var cases = aCases;
+    }
+    if (withs.startsWith(exp, '[')) {
+      try {
+        var obj = json2.JSON.parse(strings.vslice(exp, '[', ']'));
+      } catch (err) {
+        throw new Error(err+'. String to be parsed was || '+strings.vslice(exp, '[', ']')+' ||');
+      }
+      var r = cases['index'](_document, parent, obj);
+      if (r == null) {
+        throw new Error('Expression "'+exp+'" returned null. Anonymous == '+(cases == aCases));
+      }
+      return r;
+    }
+    
+    for (var c in cases) {
+      if (withs.startsWith(exp, c)) {
+        try {
+          var obj = json2.JSON.parse(strings.vslice(exp, '(', ')'))
+        } catch(err) {
+           throw new Error(err+'. String to be parsed was || '+strings.vslice(exp, '(', ')')+'  ||');
+        }
+        var result = cases[c](_document, parent, obj);
+      }
+    }
+    
+    if (!result) {
+      if ( withs.startsWith(exp, '{') ) {
+        try {
+          var obj = json2.JSON.parse(exp)
+        } catch(err) {
+          throw new Error(err+'. String to be parsed was || '+exp+' ||');
+        }
+        
+        if (cases == aCases) {
+          var result = _anonByAttrib(_document, parent, obj)
+        } else {
+          var result = _byAttrib(parent, obj)
+        }
+      }
+      if (!result) {
+        throw new Error('Expression "'+exp+'" returned null. Anonymous == '+(cases == aCases));
+      }
+    }
+    
+    // Final return
+    if (expIndex) {
+      // TODO: Check length and raise error
+      return result[expIndex];
+    } else {
+      // TODO: Check length and raise error
+      return result;
+    }
+    // Maybe we should cause an exception here
+    return false;
+  };
+  return expSplit.reduce(reduceLookup);
+};
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/driver/mozelement.js
@@ -0,0 +1,702 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozmill Elements.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation
+ *
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Halberstadt <halbersa@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["Elem", "Selector", "ID", "Link", "XPath", "Name", "Lookup", 
+                        "MozMillElement", "MozMillCheckBox", "MozMillRadio", "MozMillDropList",
+                        "MozMillTextBox", "subclasses",
+                       ];
+
+var EventUtils = {};  Components.utils.import('resource://mozmill/stdlib/EventUtils.js', EventUtils);
+var utils = {};       Components.utils.import('resource://mozmill/stdlib/utils.js', utils);
+var elementslib = {}; Components.utils.import('resource://mozmill/driver/elementslib.js', elementslib);
+var broker = {};      Components.utils.import('resource://mozmill/driver/msgbroker.js', broker);
+
+// A list of all the subclasses available.  Shared modules can push their own subclasses onto this list
+var subclasses = [MozMillCheckBox, MozMillRadio, MozMillDropList, MozMillTextBox];
+
+/**
+ * createInstance()
+ *
+ * Returns an new instance of a MozMillElement
+ * The type of the element is automatically determined
+ */
+function createInstance(locatorType, locator, elem) {
+  if (elem) {
+    var args = {"element":elem};
+    for (var i = 0; i < subclasses.length; ++i) {
+      if (subclasses[i].isType(elem)) {
+        return new subclasses[i](locatorType, locator, args);
+      }
+    }
+    if (MozMillElement.isType(elem)) return new MozMillElement(locatorType, locator, args);
+  }
+  throw new Error("could not find element " + locatorType + ": " + locator);
+};
+
+var Elem = function(node) {
+  return createInstance("Elem", node, node);
+};
+
+var Selector = function(_document, selector, index) {
+  return createInstance("Selector", selector, elementslib.Selector(_document, selector, index));
+};
+
+var ID = function(_document, nodeID) {
+  return createInstance("ID", nodeID, elementslib.ID(_document, nodeID));
+};
+
+var Link = function(_document, linkName) {
+  return createInstance("Link", linkName, elementslib.Link(_document, linkName));
+};
+
+var XPath = function(_document, expr) {
+  return createInstance("XPath", expr, elementslib.XPath(_document, expr));
+};
+
+var Name = function(_document, nName) {
+  return createInstance("Name", nName, elementslib.Name(_document, nName));
+};
+
+var Lookup = function(_document, expression) {
+  return createInstance("Lookup", expression, elementslib.Lookup(_document, expression));
+};
+
+
+/**
+ * MozMillElement
+ * The base class for all mozmill elements
+ */
+function MozMillElement(locatorType, locator, args) {
+  args = args || {};
+  this._locatorType = locatorType;
+  this._locator = locator;
+  this._element = args["element"];
+  this._document = args["document"];
+  this._owner = args["owner"];
+  // Used to maintain backwards compatibility with controller.js
+  this.isElement = true;
+}
+
+// Static method that returns true if node is of this element type
+MozMillElement.isType = function(node) {
+  return true;
+};
+
+// This getter is the magic behind lazy loading (note distinction between _element and element)
+MozMillElement.prototype.__defineGetter__("element", function() {
+  if (this._element == undefined) {
+    if (elementslib[this._locatorType]) {
+      this._element = elementslib[this._locatorType](this._document, this._locator); 
+    } else if (this._locatorType == "Elem") {
+      this._element = this._locator;
+    } else {
+      throw new Error("Unknown locator type: " + this._locatorType);
+    }
+  }
+  return this._element;
+});
+
+// Returns the actual wrapped DOM node
+MozMillElement.prototype.getNode = function() {
+  return this.element;
+};
+
+MozMillElement.prototype.getInfo = function() {
+  return this._locatorType + ": " + this._locator;
+};
+
+/**
+ * Sometimes an element which once existed will no longer exist in the DOM
+ * This function re-searches for the element
+ */
+MozMillElement.prototype.exists = function() {
+  this._element = undefined;
+  if (this.element) return true;
+  return false;
+};
+
+/**
+ * Synthesize a keypress event on the given element
+ *
+ * @param {string} aKey
+ *        Key to use for synthesizing the keypress event. It can be a simple
+ *        character like "k" or a string like "VK_ESCAPE" for command keys
+ * @param {object} aModifiers
+ *        Information about the modifier keys to send
+ *        Elements: accelKey   - Hold down the accelerator key (ctrl/meta)
+ *                               [optional - default: false]
+ *                  altKey     - Hold down the alt key
+ *                              [optional - default: false]
+ *                  ctrlKey    - Hold down the ctrl key
+ *                               [optional - default: false]
+ *                  metaKey    - Hold down the meta key (command key on Mac)
+ *                               [optional - default: false]
+ *                  shiftKey   - Hold down the shift key
+ *                               [optional - default: false]
+ * @param {object} aExpectedEvent
+ *        Information about the expected event to occur
+ *        Elements: target     - Element which should receive the event
+ *                               [optional - default: current element]
+ *                  type       - Type of the expected key event
+ */
+MozMillElement.prototype.keypress = function(aKey, aModifiers, aExpectedEvent) {
+  if (!this.element) {
+    throw new Error("Could not find element " + this.getInfo());
+  }
+
+  var win = this.element.ownerDocument? this.element.ownerDocument.defaultView : this.element;
+  this.element.focus();
+
+  if (aExpectedEvent) {
+    var target = aExpectedEvent.target? aExpectedEvent.target.getNode() : this.element;
+    EventUtils.synthesizeKeyExpectEvent(aKey, aModifiers || {}, target, aExpectedEvent.type,
+                                                            "MozMillElement.keypress()", win);
+  } else {
+    EventUtils.synthesizeKey(aKey, aModifiers || {}, win);
+  }
+
+  broker.pass({'function':'MozMillElement.keypress()'});
+  return true;
+};
+
+
+/**
+ * Synthesize a general mouse event on the given element
+ *
+ * @param {ElemBase} aTarget
+ *        Element which will receive the mouse event
+ * @param {number} aOffsetX
+ *        Relative x offset in the elements bounds to click on
+ * @param {number} aOffsetY
+ *        Relative y offset in the elements bounds to click on
+ * @param {object} aEvent
+ *        Information about the event to send
+ *        Elements: accelKey   - Hold down the accelerator key (ctrl/meta)
+ *                               [optional - default: false]
+ *                  altKey     - Hold down the alt key
+ *                               [optional - default: false]
+ *                  button     - Mouse button to use
+ *                               [optional - default: 0]
+ *                  clickCount - Number of counts to click
+ *                               [optional - default: 1]
+ *                  ctrlKey    - Hold down the ctrl key
+ *                               [optional - default: false]
+ *                  metaKey    - Hold down the meta key (command key on Mac)
+ *                               [optional - default: false]
+ *                  shiftKey   - Hold down the shift key
+ *                               [optional - default: false]
+ *                  type       - Type of the mouse event ('click', 'mousedown',
+ *                               'mouseup', 'mouseover', 'mouseout')
+ *                               [optional - default: 'mousedown' + 'mouseup']
+ * @param {object} aExpectedEvent
+ *        Information about the expected event to occur
+ *        Elements: target     - Element which should receive the event
+ *                               [optional - default: current element]
+ *                  type       - Type of the expected mouse event
+ */
+MozMillElement.prototype.mouseEvent = function(aOffsetX, aOffsetY, aEvent, aExpectedEvent) {
+  if (!this.element) {
+    throw new Error(arguments.callee.name + ": could not find element " + this.getInfo());
+  }
+
+  // If no offset is given we will use the center of the element to click on.
+  var rect = this.element.getBoundingClientRect();
+  if (isNaN(aOffsetX)) {
+    aOffsetX = rect.width / 2;
+  }
+  if (isNaN(aOffsetY)) {
+    aOffsetY = rect.height / 2;
+  }
+
+  // Scroll element into view otherwise the click will fail
+  if (this.element.scrollIntoView) {
+    this.element.scrollIntoView();
+  }
+
+  if (aExpectedEvent) {
+    // The expected event type has to be set
+    if (!aExpectedEvent.type)
+      throw new Error(arguments.callee.name + ": Expected event type not specified");
+
+    // If no target has been specified use the specified element
+    var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() : this.element;
+    if (!target) {
+      throw new Error(arguments.callee.name + ": could not find element " + aExpectedEvent.target.getInfo());
+    }
+
+    EventUtils.synthesizeMouseExpectEvent(this.element, aOffsetX, aOffsetY, aEvent,
+                                          target, aExpectedEvent.event,
+                                          "MozMillElement.mouseEvent()",
+                                          this.element.ownerDocument.defaultView);
+  } else {
+    EventUtils.synthesizeMouse(this.element, aOffsetX, aOffsetY, aEvent,
+                               this.element.ownerDocument.defaultView);
+  }
+};
+
+/**
+ * Synthesize a mouse click event on the given element
+ */
+MozMillElement.prototype.click = function(left, top, expectedEvent) {
+  // Handle menu items differently
+  if (this.element && this.element.tagName == "menuitem") {
+    this.element.click();
+  } else {
+    this.mouseEvent(left, top, {}, expectedEvent);
+  }
+
+  broker.pass({'function':'MozMillElement.click()'});
+};
+
+/**
+ * Synthesize a double click on the given element
+ */
+MozMillElement.prototype.doubleClick = function(left, top, expectedEvent) {
+  this.mouseEvent(left, top, {clickCount: 2}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.doubleClick()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse down event on the given element
+ */
+MozMillElement.prototype.mouseDown = function (button, left, top, expectedEvent) {
+  this.mouseEvent(left, top, {button: button, type: "mousedown"}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.mouseDown()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse out event on the given element
+ */
+MozMillElement.prototype.mouseOut = function (button, left, top, expectedEvent) {
+  this.mouseEvent(left, top, {button: button, type: "mouseout"}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.mouseOut()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse over event on the given element
+ */
+MozMillElement.prototype.mouseOver = function (button, left, top, expectedEvent) {
+  this.mouseEvent(left, top, {button: button, type: "mouseover"}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.mouseOver()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse up event on the given element
+ */
+MozMillElement.prototype.mouseUp = function (button, left, top, expectedEvent) {
+  this.mouseEvent(left, top, {button: button, type: "mouseup"}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.mouseUp()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse middle click event on the given element
+ */
+MozMillElement.prototype.middleClick = function(left, top, expectedEvent) {
+  this.mouseEvent(left, top, {button: 1}, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.middleClick()'});
+  return true;
+};
+
+/**
+ * Synthesize a mouse right click event on the given element
+ */
+MozMillElement.prototype.rightClick = function(left, top, expectedEvent) {
+  this.mouseEvent(left, top, {type : "contextmenu", button: 2 }, expectedEvent);
+
+  broker.pass({'function':'MozMillElement.rightClick()'});
+  return true;
+};
+
+MozMillElement.prototype.waitForElement = function(timeout, interval) {
+  var elem = this;
+  utils.waitFor(function() {
+    return elem.exists();
+  }, "Timeout exceeded for waitForElement " + this.getInfo(), timeout, interval);
+
+  broker.pass({'function':'MozMillElement.waitForElement()'});
+};
+
+MozMillElement.prototype.waitForElementNotPresent = function(timeout, interval) {
+  var elem = this;
+  utils.waitFor(function() {
+    return !elem.exists();
+  }, "Timeout exceeded for waitForElementNotPresent " + this.getInfo(), timeout, interval);
+
+  broker.pass({'function':'MozMillElement.waitForElementNotPresent()'});
+};
+
+MozMillElement.prototype.waitThenClick = function (timeout, interval, left, top, expectedEvent) {
+  this.waitForElement(timeout, interval);
+  this.click(left, top, expectedEvent);
+};
+
+// Dispatches an HTMLEvent
+MozMillElement.prototype.dispatchEvent = function (eventType, canBubble, modifiers) {
+  canBubble = canBubble || true;
+  var evt = this.element.ownerDocument.createEvent('HTMLEvents');
+  evt.shiftKey = modifiers["shift"];
+  evt.metaKey = modifiers["meta"];
+  evt.altKey = modifiers["alt"];
+  evt.ctrlKey = modifiers["ctrl"];
+  evt.initEvent(eventType, canBubble, true);
+  this.element.dispatchEvent(evt);
+};
+
+
+//---------------------------------------------------------------------------------------------------------------------------------------
+
+
+/**
+ * MozMillCheckBox
+ * Checkbox element, inherits from MozMillElement
+ */
+MozMillCheckBox.prototype = new MozMillElement();
+MozMillCheckBox.prototype.parent = MozMillElement.prototype;
+MozMillCheckBox.prototype.constructor = MozMillCheckBox;
+function MozMillCheckBox(locatorType, locator, args) {
+  this.parent.constructor.call(this, locatorType, locator, args);
+}
+
+// Static method returns true if node is this type of element
+MozMillCheckBox.isType = function(node) {
+  if ((node.localName.toLowerCase() == "input" && node.getAttribute("type") == "checkbox") ||
+      (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'checkbox') ||
+      (node.localName.toLowerCase() == 'checkbox')) {
+    return true;
+  }
+  return false;
+};
+
+/**
+ * Enable/Disable a checkbox depending on the target state
+ */
+MozMillCheckBox.prototype.check = function(state) {
+  var result = false;
+
+  if (!this.element) {
+    throw new Error("could not find element " + this.getInfo());
+    return false;
+  }
+
+  // If we have a XUL element, unwrap its XPCNativeWrapper
+  if (this.element.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
+    this.element = utils.unwrapNode(this.element);
+  }
+
+  state = (typeof(state) == "boolean") ? state : false;
+  if (state != this.element.checked) {
+    this.click();
+    var element = this.element;
+    utils.waitFor(function() {
+      return element.checked == state;
+    }, "Checkbox " + this.getInfo() + " could not be checked/unchecked", 500);
+
+    result = true;
+  }
+
+  broker.pass({'function':'MozMillCheckBox.check(' + this.getInfo() + ', state: ' + state + ')'});
+  return result;
+};
+
+//----------------------------------------------------------------------------------------------------------------------------------------
+
+
+/**
+ * MozMillRadio
+ * Radio button inherits from MozMillElement
+ */
+MozMillRadio.prototype = new MozMillElement();
+MozMillRadio.prototype.parent = MozMillElement.prototype;
+MozMillRadio.prototype.constructor = MozMillRadio;
+function MozMillRadio(locatorType, locator, args) {
+  this.parent.constructor.call(this, locatorType, locator, args);
+}
+
+// Static method returns true if node is this type of element
+MozMillRadio.isType = function(node) {
+  if ((node.localName.toLowerCase() == 'input' && node.getAttribute('type') == 'radio') ||
+      (node.localName.toLowerCase() == 'toolbarbutton' && node.getAttribute('type') == 'radio') ||
+      (node.localName.toLowerCase() == 'radio') ||
+      (node.localName.toLowerCase() == 'radiogroup')) {
+    return true;
+  }
+  return false;
+};
+
+/**
+ * Select the given radio button
+ *
+ * index - Specifies which radio button in the group to select (only applicable to radiogroup elements)
+ *         Defaults to the first radio button in the group
+ */
+MozMillRadio.prototype.select = function(index) {
+  if (!this.element) {
+    throw new Error("could not find element " + this.getInfo());
+  }
+  
+  if (this.element.localName.toLowerCase() == "radiogroup") {
+    var element = this.element.getElementsByTagName("radio")[index || 0];
+    new MozMillRadio("Elem", element).click();
+  } else {
+    var element = this.element;
+    this.click();
+  }
+  
+  utils.waitFor(function() {
+    // If we have a XUL element, unwrap its XPCNativeWrapper
+    if (element.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
+      element = utils.unwrapNode(element);
+      return element.selected == true;
+    }
+    return element.checked == true;
+  }, "Radio button " + this.getInfo() + " could not be selected", 500);
+
+  broker.pass({'function':'MozMillRadio.select(' + this.getInfo() + ')'});
+  return true;
+};
+
+//----------------------------------------------------------------------------------------------------------------------------------------
+
+
+/**
+ * MozMillDropList
+ * DropList inherits from MozMillElement
+ */
+MozMillDropList.prototype = new MozMillElement();
+MozMillDropList.prototype.parent = MozMillElement.prototype;
+MozMillDropList.prototype.constructor = MozMillDropList;
+function MozMillDropList(locatorType, locator, args) {
+  this.parent.constructor.call(this, locatorType, locator, args);
+};
+
+// Static method returns true if node is this type of element
+MozMillDropList.isType = function(node) {
+  if ((node.localName.toLowerCase() == 'toolbarbutton' && (node.getAttribute('type') == 'menu' || node.getAttribute('type') == 'menu-button')) ||
+      (node.localName.toLowerCase() == 'menu') ||
+      (node.localName.toLowerCase() == 'menulist') ||
+      (node.localName.toLowerCase() == 'select' )) {
+    return true;
+  }
+  return false;
+};
+
+/* Select the specified option and trigger the relevant events of the element */
+MozMillDropList.prototype.select = function (indx, option, value) {
+  if (!this.element){
+    throw new Error("Could not find element " + this.getInfo());
+  }
+
+  //if we have a select drop down
+  if (this.element.localName.toLowerCase() == "select"){
+    var item = null;
+
+    // The selected item should be set via its index
+    if (indx != undefined) {
+      // Resetting a menulist has to be handled separately
+      if (indx == -1) {
+        this.dispatchEvent('focus', false);
+        this.element.selectedIndex = indx;
+        this.dispatchEvent('change', true);
+
+        broker.pass({'function':'MozMillDropList.select()'});
+        return true;
+      } else {
+        item = this.element.options.item(indx);
+      }
+    } else {
+      for (var i = 0; i < this.element.options.length; i++) {
+        var entry = this.element.options.item(i);
+        if (option != undefined && entry.innerHTML == option ||
+            value != undefined && entry.value == value) {
+          item = entry;
+          break;
+        }
+      }
+    }
+
+    // Click the item
+    try {
+      // EventUtils.synthesizeMouse doesn't work.
+      this.dispatchEvent('focus', false);
+      item.selected = true;
+      this.dispatchEvent('change', true);
+
+      broker.pass({'function':'MozMillDropList.select()'});
+      return true;
+    } catch (ex) {
+      throw new Error("No item selected for element " + this.getInfo());
+      return false;
+    }
+  }
+  //if we have a xul menupopup select accordingly
+  else if (this.element.namespaceURI.toLowerCase() == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") {
+    var ownerDoc = this.element.ownerDocument;
+    // Unwrap the XUL element's XPCNativeWrapper
+    this.element = utils.unwrapNode(this.element);
+    // Get the list of menuitems
+    menuitems = this.element.getElementsByTagName("menupopup")[0].getElementsByTagName("menuitem");
+    
+    var item = null;
+
+    if (indx != undefined) {
+      if (indx == -1) {
+        this.dispatchEvent('focus', false);
+        this.element.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject).activeChild = null;
+        this.dispatchEvent('change', true);
+
+        broker.pass({'function':'MozMillDropList.select()'});
+        return true;
+      } else {
+        item = menuitems[indx];
+      }
+    } else {
+      for (var i = 0; i < menuitems.length; i++) {
+        var entry = menuitems[i];
+        if (option != undefined && entry.label == option ||
+            value != undefined && entry.value == value) {
+          item = entry;
+          break;
+        }
+      }
+    }
+
+    // Click the item
+    try {
+      EventUtils.synthesizeMouse(this.element, 1, 1, {}, ownerDoc.defaultView);
+
+      // Scroll down until item is visible
+      for (var i = 0; i <= menuitems.length; ++i) {
+        var selected = this.element.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject).activeChild;
+        if (item == selected) {
+          break;
+        }
+        EventUtils.synthesizeKey("VK_DOWN", {}, ownerDoc.defaultView);
+      }
+
+      EventUtils.synthesizeMouse(item, 1, 1, {}, ownerDoc.defaultView);
+
+      broker.pass({'function':'MozMillDropList.select()'});
+      return true;
+    } catch (ex) {
+      throw new Error('No item selected for element ' + this.getInfo());
+      return false;
+    }
+  }
+};
+
+
+//----------------------------------------------------------------------------------------------------------------------------------------
+
+
+/**
+ * MozMillTextBox
+ * TextBox inherits from MozMillElement
+ */
+MozMillTextBox.prototype = new MozMillElement();
+MozMillTextBox.prototype.parent = MozMillElement.prototype;
+MozMillTextBox.prototype.constructor = MozMillTextBox;
+function MozMillTextBox(locatorType, locator, args) {
+  this.parent.constructor.call(this, locatorType, locator, args);
+};
+
+// Static method returns true if node is this type of element
+MozMillTextBox.isType = function(node) {
+  if ((node.localName.toLowerCase() == 'input' && (node.getAttribute('type') == 'text' || node.getAttribute('type') == 'search')) ||
+      (node.localName.toLowerCase() == 'textarea') ||
+      (node.localName.toLowerCase() == 'textbox')) {
+    return true;
+  }
+  return false;
+};
+
+/**
+ * Synthesize keypress events for each character on the given element
+ *
+ * @param {string} aText
+ *        The text to send as single keypress events
+ * @param {object} aModifiers
+ *        Information about the modifier keys to send
+ *        Elements: accelKey   - Hold down the accelerator key (ctrl/meta)
+ *                               [optional - default: false]
+ *                  altKey     - Hold down the alt key
+ *                              [optional - default: false]
+ *                  ctrlKey    - Hold down the ctrl key
+ *                               [optional - default: false]
+ *                  metaKey    - Hold down the meta key (command key on Mac)
+ *                               [optional - default: false]
+ *                  shiftKey   - Hold down the shift key
+ *                               [optional - default: false]
+ * @param {object} aExpectedEvent
+ *        Information about the expected event to occur
+ *        Elements: target     - Element which should receive the event
+ *                               [optional - default: current element]
+ *                  type       - Type of the expected key event
+ */
+MozMillTextBox.prototype.sendKeys = function (aText, aModifiers, aExpectedEvent) {
+  if (!this.element) {
+    throw new Error("could not find element " + this.getInfo());
+  }
+
+  var element = this.element;
+  Array.forEach(aText, function(letter) {
+    var win = element.ownerDocument? element.ownerDocument.defaultView : element;
+    element.focus();
+
+    if (aExpectedEvent) {
+      var target = aExpectedEvent.target ? aExpectedEvent.target.getNode() : element;
+      EventUtils.synthesizeKeyExpectEvent(letter, aModifiers || {}, target, aExpectedEvent.type,
+                                                              "MozMillTextBox.sendKeys()", win);
+    } else {
+      EventUtils.synthesizeKey(letter, aModifiers || {}, win);
+    }
+  });
+
+  broker.pass({'function':'MozMillTextBox.type()'});
+  return true;
+};
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/driver/mozmill.js
@@ -0,0 +1,426 @@
+// ***** BEGIN LICENSE BLOCK *****
+// Version: MPL 1.1/GPL 2.0/LGPL 2.1
+// 
+// The contents of this file are subject to the Mozilla Public License Version
+// 1.1 (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+// http://www.mozilla.org/MPL/
+// 
+// Software distributed under the License is distributed on an "AS IS" basis,
+// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+// for the specific language governing rights and limitations under the
+// License.
+// 
+// The Original Code is Mozilla Corporation Code.
+// 
+// The Initial Developer of the Original Code is
+// Mikeal Rogers.
+// Portions created by the Initial Developer are Copyright (C) 2008
+// the Initial Developer. All Rights Reserved.
+// 
+// Contributor(s):
+//  Mikeal Rogers <mikeal.rogers@gmail.com>
+//  Gary Kwong <nth10sd@gmail.com>
+// 
+// Alternatively, the contents of this file may be used under the terms of
+// either the GNU General Public License Version 2 or later (the "GPL"), or
+// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+// in which case the provisions of the GPL or the LGPL are applicable instead
+// of those above. If you wish to allow use of your version of this file only
+// under the terms of either the GPL or the LGPL, and not to allow others to
+// use your version of this file under the terms of the MPL, indicate your
+// decision by deleting the provisions above and replace them with the notice
+// and other provisions required by the GPL or the LGPL. If you do not delete
+// the provisions above, a recipient may use your version of this file under
+// the terms of any one of the MPL, the GPL or the LGPL.
+// 
+// ***** END LICENSE BLOCK *****
+
+var EXPORTED_SYMBOLS = ["controller", "utils", "elementslib", "os",
+                        "getBrowserController", "newBrowserController", 
+                        "getAddonsController", "getPreferencesController", 
+                        "newMail3PaneController", "getMail3PaneController", 
+                        "wm", "platform", "getAddrbkController", 
+                        "getMsgComposeController", "getDownloadsController",
+                        "Application", "cleanQuit", "findElement",
+                        "getPlacesController", 'isMac', 'isLinux', 'isWindows',
+                        "firePythonCallback"
+                       ];
+                        
+// imports
+var controller = {};  Components.utils.import('resource://mozmill/driver/controller.js', controller);
+var elementslib = {}; Components.utils.import('resource://mozmill/driver/elementslib.js', elementslib);
+var broker = {};      Components.utils.import('resource://mozmill/driver/msgbroker.js', broker);
+var findElement = {}; Components.utils.import('resource://mozmill/driver/mozelement.js', findElement);
+var utils = {};       Components.utils.import('resource://mozmill/stdlib/utils.js', utils);
+var os = {}; Components.utils.import('resource://mozmill/stdlib/os.js', os);
+
+try {
+  Components.utils.import("resource://gre/modules/AddonManager.jsm");
+} catch(e) { /* Firefox 4 only */ }
+
+// platform information
+var platform = os.getPlatform();
+var isMac = false;
+var isWindows = false;
+var isLinux = false;
+if (platform == "darwin"){
+  isMac = true;
+}
+if (platform == "winnt"){
+  isWindows = true;
+}
+if (platform == "linux"){
+  isLinux = true;
+}
+
+var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+           .getService(Components.interfaces.nsIWindowMediator);
+           
+var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
+               .getService(Components.interfaces.nsIXULAppInfo);
+
+var locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+               .getService(Components.interfaces.nsIXULChromeRegistry)
+               .getSelectedLocale("global");
+
+var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"].
+    getService(Components.interfaces.nsIConsoleService);
+
+                       
+applicationDictionary = {
+  "{718e30fb-e89b-41dd-9da7-e25a45638b28}": "Sunbird",    
+  "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "SeaMonkey",
+  "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "Firefox",
+  "{3550f703-e582-4d05-9a08-453d09bdfdc6}": 'Thunderbird',
+}                 
+                       
+var Application = applicationDictionary[appInfo.ID];
+
+if (Application == undefined) {
+  // Default to Firefox
+  var Application = 'Firefox';
+}
+
+// get startup time if available
+// see http://blog.mozilla.com/tglek/2011/04/26/measuring-startup-speed-correctly/
+var startupInfo = {};
+try {
+    var _startupInfo = Components.classes["@mozilla.org/toolkit/app-startup;1"]
+        .getService(Components.interfaces.nsIAppStartup).getStartupInfo();
+    for (var i in _startupInfo) {
+        startupInfo[i] = _startupInfo[i].getTime(); // convert from Date object to ms since epoch
+    }
+} catch(e) {
+    startupInfo = null; 
+}
+
+
+// keep list of installed addons to send to jsbridge for test run report
+var addons = "null"; // this will be JSON parsed
+if(typeof AddonManager != "undefined") {
+  AddonManager.getAllAddons(function(addonList) {
+      var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+          .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+      converter.charset = 'utf-8';
+
+      function replacer(key, value) {
+          if (typeof(value) == "string") {
+              try {
+                  return converter.ConvertToUnicode(value);
+              } catch(e) {
+                  var newstring = '';
+                  for (var i=0; i < value.length; i++) {
+                      replacement = '';
+                      if ((32 <= value.charCodeAt(i)) && (value.charCodeAt(i) < 127)) {
+                          // eliminate non-convertable characters;
+                          newstring += value.charAt(i);
+                      } else {
+                          newstring += replacement;
+                      }
+                  }
+                  return newstring;
+              }
+          }
+          return value;
+      }
+
+      addons = converter.ConvertToUnicode(JSON.stringify(addonList, replacer))
+  });
+}
+
+function cleanQuit () {
+  utils.getMethodInWindows('goQuitApplication')();
+}
+
+function addHttpResource (directory, namespace) {
+  return 'http://localhost:4545/'+namespace;
+}
+
+function newBrowserController () {
+  return new controller.MozMillController(utils.getMethodInWindows('OpenBrowserWindow')());
+}
+
+function getBrowserController () {
+  var browserWindow = wm.getMostRecentWindow("navigator:browser");
+  if (browserWindow == null) {
+    return newBrowserController();
+  }
+  else {
+    return new controller.MozMillController(browserWindow);
+  }
+}
+
+function getPlacesController () {
+  utils.getMethodInWindows('PlacesCommandHook').showPlacesOrganizer('AllBookmarks');
+  return new controller.MozMillController(wm.getMostRecentWindow(''));
+}
+
+function getAddonsController () {
+  if (Application == 'SeaMonkey') {
+    utils.getMethodInWindows('toEM')();
+  } else if (Application == 'Thunderbird') {
+    utils.getMethodInWindows('openAddonsMgr')();
+  } else if (Application == 'Sunbird') {
+    utils.getMethodInWindows('goOpenAddons')();
+  } else {
+    utils.getMethodInWindows('BrowserOpenAddonsMgr')();
+  }
+  return new controller.MozMillController(wm.getMostRecentWindow(''));
+}
+
+function getDownloadsController() {
+  utils.getMethodInWindows('BrowserDownloadsUI')();
+  return new controller.MozMillController(wm.getMostRecentWindow(''));
+}
+
+function getPreferencesController() {
+  if (Application == 'Thunderbird') {
+    utils.getMethodInWindows('openOptionsDialog')();
+  } else {
+    utils.getMethodInWindows('openPreferences')();
+  }
+  return new controller.MozMillController(wm.getMostRecentWindow(''));
+}
+
+// Thunderbird functions
+function newMail3PaneController () {
+  return new controller.MozMillController(utils.getMethodInWindows('toMessengerWindow')());
+}
+ 
+function getMail3PaneController () {
+  var mail3PaneWindow = wm.getMostRecentWindow("mail:3pane");
+  if (mail3PaneWindow == null) {
+    return newMail3PaneController();
+  }
+  else {
+    return new controller.MozMillController(mail3PaneWindow);
+  }
+}
+
+// Thunderbird - Address book window
+function newAddrbkController () {
+  utils.getMethodInWindows("toAddressBook")();
+  utils.sleep(2000);
+  var addyWin = wm.getMostRecentWindow("mail:addressbook");
+  return new controller.MozMillController(addyWin);
+}
+
+function getAddrbkController () {
+  var addrbkWindow = wm.getMostRecentWindow("mail:addressbook");
+  if (addrbkWindow == null) {
+    return newAddrbkController();
+  }
+  else {
+    return new controller.MozMillController(addrbkWindow);
+  }
+}
+
+function firePythonCallback (filename, method, args, kwargs) {
+  obj = {'filename': filename, 'method': method};
+  obj['args'] = args || [];
+  obj['kwargs'] = kwargs || {};
+  broker.sendMessage("firePythonCallback", obj);
+}
+
+function timer (name) {
+  this.name = name;
+  this.timers = {};
+  frame.timers.push(this);
+  this.actions = [];
+}
+timer.prototype.start = function (name) {
+  this.timers[name].startTime = (new Date).getTime();
+} 
+timer.prototype.stop = function (name) {
+  var t = this.timers[name];
+  t.endTime = (new Date).getTime();
+  t.totalTime = (t.endTime - t.startTime);
+}
+timer.prototype.end = function () {
+  frame.events.fireEvent("timer", this);
+  frame.timers.remove(this);
+}
+
+// Initialization
+
+/**
+ * Console listener which listens for error messages in the console and forwards
+ * them to the Mozmill reporting system for output.
+ */
+function ConsoleListener() {
+ this.register();
+}
+ConsoleListener.prototype = {
+ observe: function(aMessage) {
+   var msg = aMessage.message;
+   var re = /^\[.*Error:.*(chrome|resource):\/\/.*/i;
+   if (msg.match(re)) {
+     broker.fail(aMessage);
+   }
+ },
+ QueryInterface: function (iid) {
+	if (!iid.equals(Components.interfaces.nsIConsoleListener) && !iid.equals(Components.interfaces.nsISupports)) {
+		throw Components.results.NS_ERROR_NO_INTERFACE;
+   }
+   return this;
+ },
+ register: function() {
+   var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
+                              .getService(Components.interfaces.nsIConsoleService);
+   aConsoleService.registerListener(this);
+ },
+ unregister: function() {
+   var aConsoleService = Components.classes["@mozilla.org/consoleservice;1"]
+                              .getService(Components.interfaces.nsIConsoleService);
+   aConsoleService.unregisterListener(this);
+ }
+}
+
+// start listening
+var consoleListener = new ConsoleListener();
+  
+// Observer for new top level windows
+var windowObserver = {
+  observe: function(subject, topic, data) {
+    attachEventListeners(subject);
+  }
+};
+  
+/**
+ * Attach event listeners
+ */
+function attachEventListeners(aWindow) {
+  // These are the event handlers
+  function pageShowHandler(event) {
+    var doc = event.originalTarget;
+
+    // Only update the flag if we have a document as target
+    // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829
+    if ("defaultView" in doc) {
+      doc.defaultView.mozmillDocumentLoaded = true;
+    }
+
+    // We need to add/remove the unload/pagehide event listeners to preserve caching.
+    aWindow.gBrowser.addEventListener("beforeunload", beforeUnloadHandler, true);
+    aWindow.gBrowser.addEventListener("pagehide", pageHideHandler, true);
+  };
+
+  function DOMContentLoadedHandler(event) {
+    var doc = event.originalTarget;
+
+    var errorRegex = /about:.+(error)|(blocked)\?/;
+    if (errorRegex.exec(doc.baseURI)) {
+      // Wait about 1s to be sure the DOM is ready
+      utils.sleep(1000);
+
+      // Only update the flag if we have a document as target
+      if ("defaultView" in doc) {
+        doc.defaultView.mozmillDocumentLoaded = true;
+      }
+
+      // We need to add/remove the unload event listener to preserve caching.
+      aWindow.gBrowser.addEventListener("beforeunload", beforeUnloadHandler, true);
+    }
+  };
+  
+  // beforeunload is still needed because pagehide doesn't fire before the page is unloaded.
+  // still use pagehide for cases when beforeunload doesn't get fired
+  function beforeUnloadHandler(event) {
+    var doc = event.originalTarget;
+
+    // Only update the flag if we have a document as target
+    if ("defaultView" in doc) {
+      doc.defaultView.mozmillDocumentLoaded = false;
+    }
+
+    aWindow.gBrowser.removeEventListener("beforeunload", beforeUnloadHandler, true);
+  };
+
+  function pageHideHandler(event) {
+    // If event.persisted is false, the beforeUnloadHandler should fire
+    // and there is no need for this event handler.
+    if (event.persisted) {
+      var doc = event.originalTarget;
+
+      // Only update the flag if we have a document as target
+      if ("defaultView" in doc) {
+        doc.defaultView.mozmillDocumentLoaded = false;
+      }
+
+      aWindow.gBrowser.removeEventListener("beforeunload", beforeUnloadHandler, true);
+    }
+
+  };
+
+  function onWindowLoaded(event) {
+    aWindow.mozmillDocumentLoaded = true;
+
+    if ("gBrowser" in aWindow) {
+      // Page is ready
+      aWindow.gBrowser.addEventListener("pageshow", pageShowHandler, true);
+
+      // Note: Error pages will never fire a "load" event. For those we
+      // have to wait for the "DOMContentLoaded" event. That's the final state.
+      // Error pages will always have a baseURI starting with
+      // "about:" followed by "error" or "blocked".
+      aWindow.gBrowser.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true);
+    
+      // Leave page (use caching)
+      aWindow.gBrowser.addEventListener("pagehide", pageHideHandler, true);
+    }
+
+  }
+
+  // Add the event handlers to the tabbedbrowser once its window has loaded
+  if (aWindow.content) {
+    onWindowLoaded();
+  } else {
+    aWindow.addEventListener("load", onWindowLoaded, false);
+  }
+}
+  
+/**
+ * Initialize Mozmill
+ */
+function initialize() {
+  // Activate observer for new top level windows
+  var observerService = Components.classes["@mozilla.org/observer-service;1"].
+                        getService(Components.interfaces.nsIObserverService);
+  observerService.addObserver(windowObserver, "toplevel-window-ready", false);
+
+  // Attach event listeners to all open windows
+  var enumerator = Components.classes["@mozilla.org/appshell/window-mediator;1"].
+                   getService(Components.interfaces.nsIWindowMediator).getEnumerator("");
+  while (enumerator.hasMoreElements()) {
+    var win = enumerator.getNext();
+    attachEventListeners(win);
+
+    // For windows or dialogs already open we have to explicitly set the property
+    // otherwise windows which load really quick never gets the property set and
+    // we fail to create the controller
+    win.mozmillDocumentLoaded = true;
+  };
+}
+
+initialize();
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/driver/msgbroker.js
@@ -0,0 +1,91 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozmill.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation
+ *
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Halberstadt <halbersa@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ['addListener', 'addObject',
+                        'removeListener',
+                        'sendMessage', 'log', 'pass', 'fail'];
+
+var listeners = {};
+
+// add a listener for a specific message type
+function addListener(msgType, listener) {
+
+  if (listeners[msgType] === undefined) {
+    listeners[msgType] = [];
+  }
+  listeners[msgType].push(listener);
+}
+
+// add each method in an object as a message listener
+function addObject(object) {
+  for (var msgType in object) {
+    addListener(msgType, object[msgType]);
+  }
+}
+
+// remove a listener for all message types
+function removeListener(listener) {
+  for (var msgType in listeners) {
+    for (let i = 0; i < listeners.length; ++i) {
+      if (listeners[msgType][i] == listener) {
+        listeners[msgType].splice(i, 1); // remove listener from array
+      }
+    }
+  }
+}
+
+function sendMessage(msgType, obj) {
+  if (listeners[msgType] === undefined) {
+    return;
+  }
+  for (let i = 0; i < listeners[msgType].length; ++i) {
+    listeners[msgType][i](obj);
+  }
+}
+
+function log(obj) {
+  sendMessage('log', obj);
+}
+
+function pass(obj) {
+  sendMessage('pass', obj);
+}
+
+function fail(obj) {
+  sendMessage('fail', obj);
+}
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/stdlib/EventUtils.js
@@ -0,0 +1,820 @@
+// Export all available functions for Mozmill
+var EXPORTED_SYMBOLS = ["sendMouseEvent", "sendChar", "sendString", "sendKey",
+                        "synthesizeMouse", "synthesizeMouseScroll", "synthesizeKey",
+                        "synthesizeMouseExpectEvent", "synthesizeKeyExpectEvent",
+                        "synthesizeDragStart", "synthesizeDrop", "synthesizeText",
+                        "disableNonTestMouseEvents", "synthesizeComposition", 
+                        "synthesizeQuerySelectedText", "synthesizeQueryTextContent",
+                        "synthesizeQueryCaretRect", "synthesizeQueryTextRect",
+                        "synthesizeQueryEditorRect", "synthesizeCharAtPoint",
+                        "synthesizeSelectionSet"];
+
+/**
+ * Get the array with available key events
+ */
+function getKeyEvent(aWindow) {
+  var win = aWindow.wrappedJSObject ? aWindow.wrappedJSObject : aWindow;
+  return win.KeyEvent;
+}
+
+/**
+ * EventUtils provides some utility methods for creating and sending DOM events.
+ * Current methods:
+ *  sendMouseEvent
+ *  sendChar
+ *  sendString
+ *  sendKey
+ */
+
+/**
+ * Send a mouse event to the node aTarget (aTarget can be an id, or an
+ * actual node) . The "event" passed in to aEvent is just a JavaScript
+ * object with the properties set that the real mouse event object should
+ * have. This includes the type of the mouse event.
+ * E.g. to send an click event to the node with id 'node' you might do this:
+ *
+ * sendMouseEvent({type:'click'}, 'node');
+ */
+function sendMouseEvent(aEvent, aTarget, aWindow) {
+  if (['click', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) {
+    throw new Error("sendMouseEvent doesn't know about event type '"+aEvent.type+"'");
+  }
+
+  if (!aWindow) {
+    aWindow = window;
+  }
+
+  if (!(aTarget instanceof Element)) {
+    aTarget = aWindow.document.getElementById(aTarget);
+  }
+
+  var event = aWindow.document.createEvent('MouseEvent');
+
+  var typeArg          = aEvent.type;
+  var canBubbleArg     = true;
+  var cancelableArg    = true;
+  var viewArg          = aWindow;
+  var detailArg        = aEvent.detail        || (aEvent.type == 'click'     ||
+                                                  aEvent.type == 'mousedown' ||
+                                                  aEvent.type == 'mouseup' ? 1 : 0);
+  var screenXArg       = aEvent.screenX       || 0;
+  var screenYArg       = aEvent.screenY       || 0;
+  var clientXArg       = aEvent.clientX       || 0;
+  var clientYArg       = aEvent.clientY       || 0;
+  var ctrlKeyArg       = aEvent.ctrlKey       || false;
+  var altKeyArg        = aEvent.altKey        || false;
+  var shiftKeyArg      = aEvent.shiftKey      || false;
+  var metaKeyArg       = aEvent.metaKey       || false;
+  var buttonArg        = aEvent.button        || 0;
+  var relatedTargetArg = aEvent.relatedTarget || null;
+
+  event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg,
+                       screenXArg, screenYArg, clientXArg, clientYArg,
+                       ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
+                       buttonArg, relatedTargetArg);
+
+  aTarget.dispatchEvent(event);
+}
+
+/**
+ * Send the char aChar to the node with id aTarget.  If aTarget is not
+ * provided, use "target".  This method handles casing of chars (sends the
+ * right charcode, and sends a shift key for uppercase chars).  No other
+ * modifiers are handled at this point.
+ *
+ * For now this method only works for English letters (lower and upper case)
+ * and the digits 0-9.
+ *
+ * Returns true if the keypress event was accepted (no calls to preventDefault
+ * or anything like that), false otherwise.
+ */
+function sendChar(aChar, aTarget) {
+  // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9.
+  var hasShift = (aChar == aChar.toUpperCase());
+  var charCode = aChar.charCodeAt(0);
+  var keyCode = charCode;
+  if (!hasShift) {
+    // For lowercase letters, the keyCode is actually 32 less than the charCode
+    keyCode -= 0x20;
+  }
+
+  return __doEventDispatch(aTarget, charCode, keyCode, hasShift);
+}
+
+/**
+ * Send the string aStr to the node with id aTarget.  If aTarget is not
+ * provided, use "target".
+ *
+ * For now this method only works for English letters (lower and upper case)
+ * and the digits 0-9.
+ */
+function sendString(aStr, aTarget) {
+  for (var i = 0; i < aStr.length; ++i) {
+    sendChar(aStr.charAt(i), aTarget);
+  }
+}
+
+/**
+ * Send the non-character key aKey to the node with id aTarget. If aTarget is
+ * not provided, use "target".  The name of the key should be a lowercase
+ * version of the part that comes after "DOM_VK_" in the KeyEvent constant
+ * name for this key.  No modifiers are handled at this point.
+ *
+ * Returns true if the keypress event was accepted (no calls to preventDefault
+ * or anything like that), false otherwise.
+ */
+function sendKey(aKey, aTarget, aWindow) {
+  if (!aWindow)
+    aWindow = window;
+
+  keyName = "DOM_VK_" + aKey.toUpperCase();
+
+  if (!getKeyEvent(aWindow)[keyName]) {
+    throw "Unknown key: " + keyName;
+  }
+
+  return __doEventDispatch(aTarget, 0, getKeyEvent(aWindow)[keyName], false);
+}
+
+/**
+ * Actually perform event dispatch given a charCode, keyCode, and boolean for
+ * whether "shift" was pressed.  Send the event to the node with id aTarget.  If
+ * aTarget is not provided, use "target".
+ *
+ * Returns true if the keypress event was accepted (no calls to preventDefault
+ * or anything like that), false otherwise.
+ */
+function __doEventDispatch(aTarget, aCharCode, aKeyCode, aHasShift) {
+  if (aTarget === undefined) {
+    aTarget = "target";
+  }
+
+  var event = document.createEvent("KeyEvents");
+  event.initKeyEvent("keydown", true, true, document.defaultView,
+                     false, false, aHasShift, false,
+                     aKeyCode, 0);
+  var accepted = $(aTarget).dispatchEvent(event);
+
+  // Preventing the default keydown action also prevents the default
+  // keypress action.
+  event = document.createEvent("KeyEvents");
+  if (aCharCode) {
+    event.initKeyEvent("keypress", true, true, document.defaultView,
+                       false, false, aHasShift, false,
+                       0, aCharCode);
+  } else {
+    event.initKeyEvent("keypress", true, true, document.defaultView,
+                       false, false, aHasShift, false,
+                       aKeyCode, 0);
+  }
+  if (!accepted) {
+    event.preventDefault();
+  }
+  accepted = $(aTarget).dispatchEvent(event);
+
+  // Always send keyup
+  var event = document.createEvent("KeyEvents");
+  event.initKeyEvent("keyup", true, true, document.defaultView,
+                     false, false, aHasShift, false,
+                     aKeyCode, 0);
+  $(aTarget).dispatchEvent(event);
+  return accepted;
+}
+
+/**
+ * Parse the key modifier flags from aEvent. Used to share code between
+ * synthesizeMouse and synthesizeKey.
+ */
+function _parseModifiers(aEvent)
+{
+  var hwindow = Components.classes["@mozilla.org/appshell/appShellService;1"]
+                          .getService(Components.interfaces.nsIAppShellService)
+                          .hiddenDOMWindow;
+
+  const masks = Components.interfaces.nsIDOMNSEvent;
+  var mval = 0;
+  if (aEvent.shiftKey)
+    mval |= masks.SHIFT_MASK;
+  if (aEvent.ctrlKey)
+    mval |= masks.CONTROL_MASK;
+  if (aEvent.altKey)
+    mval |= masks.ALT_MASK;
+  if (aEvent.metaKey)
+    mval |= masks.META_MASK;
+  if (aEvent.accelKey)
+    mval |= (hwindow.navigator.platform.indexOf("Mac") >= 0) ? masks.META_MASK :
+                                                               masks.CONTROL_MASK;
+
+  return mval;
+}
+
+/**
+ * Synthesize a mouse event on a target. The actual client point is determined
+ * by taking the aTarget's client box and offseting it by aOffsetX and
+ * aOffsetY. This allows mouse clicks to be simulated by calling this method.
+ *
+ * aEvent is an object which may contain the properties:
+ *   shiftKey, ctrlKey, altKey, metaKey, accessKey, clickCount, button, type
+ *
+ * If the type is specified, an mouse event of that type is fired. Otherwise,
+ * a mousedown followed by a mouse up is performed.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
+{
+  if (!aWindow)
+    aWindow = window;
+
+  var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                      getInterface(Components.interfaces.nsIDOMWindowUtils);
+  if (utils) {
+    var button = aEvent.button || 0;
+    var clickCount = aEvent.clickCount || 1;
+    var modifiers = _parseModifiers(aEvent);
+
+    var rect = aTarget.getBoundingClientRect();
+
+    var left = rect.left + aOffsetX;
+    var top = rect.top + aOffsetY;
+
+    if (aEvent.type) {
+      utils.sendMouseEvent(aEvent.type, left, top, button, clickCount, modifiers);
+    }
+    else {
+      utils.sendMouseEvent("mousedown", left, top, button, clickCount, modifiers);
+      utils.sendMouseEvent("mouseup", left, top, button, clickCount, modifiers);
+    }
+  }
+}
+
+/**
+ * Synthesize a mouse scroll event on a target. The actual client point is determined
+ * by taking the aTarget's client box and offseting it by aOffsetX and
+ * aOffsetY.
+ *
+ * aEvent is an object which may contain the properties:
+ *   shiftKey, ctrlKey, altKey, metaKey, accessKey, button, type, axis, delta, hasPixels
+ *
+ * If the type is specified, a mouse scroll event of that type is fired. Otherwise,
+ * "DOMMouseScroll" is used.
+ *
+ * If the axis is specified, it must be one of "horizontal" or "vertical". If not specified,
+ * "vertical" is used.
+ *
+ * 'delta' is the amount to scroll by (can be positive or negative). It must
+ * be specified.
+ *
+ * 'hasPixels' specifies whether kHasPixels should be set in the scrollFlags.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
+{
+  if (!aWindow)
+    aWindow = window;
+
+  var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                      getInterface(Components.interfaces.nsIDOMWindowUtils);
+  if (utils) {
+    // See nsMouseScrollFlags in nsGUIEvent.h
+    const kIsVertical = 0x02;
+    const kIsHorizontal = 0x04;
+    const kHasPixels = 0x08;
+
+    var button = aEvent.button || 0;
+    var modifiers = _parseModifiers(aEvent);
+
+    var rect = aTarget.getBoundingClientRect();
+
+    var left = rect.left;
+    var top = rect.top;
+
+    var type = aEvent.type || "DOMMouseScroll";
+    var axis = aEvent.axis || "vertical";
+    var scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical;
+    if (aEvent.hasPixels) {
+      scrollFlags |= kHasPixels;
+    }
+    utils.sendMouseScrollEvent(type, left + aOffsetX, top + aOffsetY, button,
+                               scrollFlags, aEvent.delta, modifiers);
+  }
+}
+
+/**
+ * Synthesize a key event. It is targeted at whatever would be targeted by an
+ * actual keypress by the user, typically the focused element.
+ *
+ * aKey should be either a character or a keycode starting with VK_ such as
+ * VK_ENTER.
+ *
+ * aEvent is an object which may contain the properties:
+ *   shiftKey, ctrlKey, altKey, metaKey, accessKey, type
+ *
+ * If the type is specified, a key event of that type is fired. Otherwise,
+ * a keydown, a keypress and then a keyup event are fired in sequence.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeKey(aKey, aEvent, aWindow)
+{
+  if (!aWindow)
+    aWindow = window;
+
+  var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                      getInterface(Components.interfaces.nsIDOMWindowUtils);
+  if (utils) {
+    var keyCode = 0, charCode = 0;
+    if (aKey.indexOf("VK_") == 0)
+      keyCode = getKeyEvent(aWindow)["DOM_" + aKey];
+    else
+      charCode = aKey.charCodeAt(0);
+
+    var modifiers = _parseModifiers(aEvent);
+
+    if (aEvent.type) {
+      utils.sendKeyEvent(aEvent.type, keyCode, charCode, modifiers);
+    }
+    else {
+      var keyDownDefaultHappened =
+          utils.sendKeyEvent("keydown", keyCode, charCode, modifiers);
+      utils.sendKeyEvent("keypress", keyCode, charCode, modifiers,
+                         !keyDownDefaultHappened);
+      utils.sendKeyEvent("keyup", keyCode, charCode, modifiers);
+    }
+  }
+}
+
+var _gSeenEvent = false;
+
+/**
+ * Indicate that an event with an original target of aExpectedTarget and
+ * a type of aExpectedEvent is expected to be fired, or not expected to
+ * be fired.
+ */
+function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName)
+{
+  if (!aExpectedTarget || !aExpectedEvent)
+    return null;
+
+  _gSeenEvent = false;
+
+  var type = (aExpectedEvent.charAt(0) == "!") ?
+             aExpectedEvent.substring(1) : aExpectedEvent;
+  var eventHandler = function(event) {
+    var epassed = (!_gSeenEvent && event.originalTarget == aExpectedTarget &&
+                   event.type == type);
+    if (!epassed)
+      throw new Error(aTestName + " " + type + " event target " +
+                      (_gSeenEvent ? "twice" : ""));
+    _gSeenEvent = true;
+  };
+
+  aExpectedTarget.addEventListener(type, eventHandler, false);
+  return eventHandler;
+}
+
+/**
+ * Check if the event was fired or not. The event handler aEventHandler
+ * will be removed.
+ */
+function _checkExpectedEvent(aExpectedTarget, aExpectedEvent, aEventHandler, aTestName)
+{
+  if (aEventHandler) {
+    var expectEvent = (aExpectedEvent.charAt(0) != "!");
+    var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
+    aExpectedTarget.removeEventListener(type, aEventHandler, false);
+    var desc = type + " event";
+    if (expectEvent)
+      desc += " not";
+    if (_gSeenEvent != expectEvent)
+      throw new Error(aTestName + ": " + desc + " fired.");
+  }
+
+  _gSeenEvent = false;
+}
+
+/**
+ * Similar to synthesizeMouse except that a test is performed to see if an
+ * event is fired at the right target as a result.
+ *
+ * aExpectedTarget - the expected originalTarget of the event.
+ * aExpectedEvent - the expected type of the event, such as 'select'.
+ * aTestName - the test name when outputing results
+ *
+ * To test that an event is not fired, use an expected type preceded by an
+ * exclamation mark, such as '!select'. This might be used to test that a
+ * click on a disabled element doesn't fire certain events for instance.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeMouseExpectEvent(aTarget, aOffsetX, aOffsetY, aEvent,
+                                    aExpectedTarget, aExpectedEvent, aTestName,
+                                    aWindow)
+{
+  var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
+  synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
+  _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
+}
+
+/**
+ * Similar to synthesizeKey except that a test is performed to see if an
+ * event is fired at the right target as a result.
+ *
+ * aExpectedTarget - the expected originalTarget of the event.
+ * aExpectedEvent - the expected type of the event, such as 'select'.
+ * aTestName - the test name when outputing results
+ *
+ * To test that an event is not fired, use an expected type preceded by an
+ * exclamation mark, such as '!select'.
+ *
+ * aWindow is optional, and defaults to the current window object.
+ */
+function synthesizeKeyExpectEvent(key, aEvent, aExpectedTarget, aExpectedEvent,
+                                  aTestName, aWindow)
+{
+  var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
+  synthesizeKey(key, aEvent, aWindow);
+  _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
+}
+
+/**
+ * Emulate a dragstart event.
+ *  element - element to fire the dragstart event on
+ *  expectedDragData - the data you expect the data transfer to contain afterwards
+ *                      This data is in the format:
+ *                         [ [ {type: value, data: value, test: function}, ... ], ... ]
+ *                     can be null
+ *  aWindow - optional; defaults to the current window object.
+ *  x - optional; initial x coordinate
+ *  y - optional; initial y coordinate
+ * Returns null if data matches.
+ * Returns the event.dataTransfer if data does not match
+ *
+ * eqTest is an optional function if comparison can't be done with x == y;
+ *   function (actualData, expectedData) {return boolean}
+ *   @param actualData from dataTransfer
+ *   @param expectedData from expectedDragData
+ * see bug 462172 for example of use
+ *
+ */
+function synthesizeDragStart(element, expectedDragData, aWindow, x, y)
+{
+  if (!aWindow)
+    aWindow = window;
+  x = x || 2;
+  y = y || 2;
+  const step = 9;
+
+  var result = "trapDrag was not called";
+  var trapDrag = function(event) {
+    try {
+      var dataTransfer = event.dataTransfer;
+      result = null;
+      if (!dataTransfer)
+        throw "no dataTransfer";
+      if (expectedDragData == null ||
+          dataTransfer.mozItemCount != expectedDragData.length)
+        throw dataTransfer;
+      for (var i = 0; i < dataTransfer.mozItemCount; i++) {
+        var dtTypes = dataTransfer.mozTypesAt(i);
+        if (dtTypes.length != expectedDragData[i].length)
+          throw dataTransfer;
+        for (var j = 0; j < dtTypes.length; j++) {
+          if (dtTypes[j] != expectedDragData[i][j].type)
+            throw dataTransfer;
+          var dtData = dataTransfer.mozGetDataAt(dtTypes[j],i);
+          if (expectedDragData[i][j].eqTest) {
+            if (!expectedDragData[i][j].eqTest(dtData, expectedDragData[i][j].data))
+              throw dataTransfer;
+          }
+          else if (expectedDragData[i][j].data != dtData)
+            throw dataTransfer;
+        }
+      }
+    } catch(ex) {
+      result = ex;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+  }
+  aWindow.addEventListener("dragstart", trapDrag, false);
+  synthesizeMouse(element, x, y, { type: "mousedown" }, aWindow);
+  x += step; y += step;
+  synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
+  x += step; y += step;
+  synthesizeMouse(element, x, y, { type: "mousemove" }, aWindow);
+  aWindow.removeEventListener("dragstart", trapDrag, false);
+  synthesizeMouse(element, x, y, { type: "mouseup" }, aWindow);
+  return result;
+}
+
+/**
+ * Emulate a drop by emulating a dragstart and firing events dragenter, dragover, and drop.
+ *  srcElement - the element to use to start the drag, usually the same as destElement
+ *               but if destElement isn't suitable to start a drag on pass a suitable
+ *               element for srcElement
+ *  destElement - the element to fire the dragover, dragleave and drop events
+ *  dragData - the data to supply for the data transfer
+ *                     This data is in the format:
+ *                       [ [ {type: value, data: value}, ...], ... ]
+ *  dropEffect - the drop effect to set during the dragstart event, or 'move' if null
+ *  aWindow - optional; defaults to the current window object.
+ *
+ * Returns the drop effect that was desired.
+ */
+function synthesizeDrop(srcElement, destElement, dragData, dropEffect, aWindow)
+{
+  if (!aWindow)
+    aWindow = window;
+
+  var dataTransfer;
+  var trapDrag = function(event) {
+    dataTransfer = event.dataTransfer;
+    for (var i = 0; i < dragData.length; i++) {
+      var item = dragData[i];
+      for (var j = 0; j < item.length; j++) {
+        dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+      }
+    }
+    dataTransfer.dropEffect = dropEffect || "move";
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  // need to use real mouse action
+  aWindow.addEventListener("dragstart", trapDrag, true);
+  synthesizeMouse(srcElement, 2, 2, { type: "mousedown" }, aWindow);
+  synthesizeMouse(srcElement, 11, 11, { type: "mousemove" }, aWindow);
+  synthesizeMouse(srcElement, 20, 20, { type: "mousemove" }, aWindow);
+  aWindow.removeEventListener("dragstart", trapDrag, true);
+
+  event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent("dragenter", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+  destElement.dispatchEvent(event);
+
+  var event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent("dragover", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+  if (destElement.dispatchEvent(event)) {
+    synthesizeMouse(destElement, 20, 20, { type: "mouseup" }, aWindow);
+    return "none";
+  }
+
+  if (dataTransfer.dropEffect != "none") {
+    event = aWindow.document.createEvent("DragEvents");
+    event.initDragEvent("drop", true, true, aWindow, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
+    destElement.dispatchEvent(event);
+  }
+  synthesizeMouse(destElement, 20, 20, { type: "mouseup" }, aWindow);
+
+  return dataTransfer.dropEffect;
+}
+
+function disableNonTestMouseEvents(aDisable)
+{
+  var utils =
+    window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+           getInterface(Components.interfaces.nsIDOMWindowUtils);
+  if (utils)
+    utils.disableNonTestMouseEvents(aDisable);
+}
+
+function _getDOMWindowUtils(aWindow)
+{
+  if (!aWindow) {
+    aWindow = window;
+  }
+  return aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                 getInterface(Components.interfaces.nsIDOMWindowUtils);
+}
+
+/**
+ * Synthesize a composition event.
+ *
+ * @param aIsCompositionStart  If true, this synthesize compositionstart event.
+ *                             Otherwise, compositionend event.
+ * @param aWindow              Optional (If null, current |window| will be used)
+ */
+function synthesizeComposition(aIsCompositionStart, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return;
+  }
+
+  utils.sendCompositionEvent(aIsCompositionStart ?
+                               "compositionstart" : "compositionend");
+}
+
+/**
+ * Synthesize a text event.
+ *
+ * @param aEvent   The text event's information, this has |composition|
+ *                 and |caret| members.  |composition| has |string| and
+ *                 |clauses| members.  |clauses| must be array object.  Each
+ *                 object has |length| and |attr|.  And |caret| has |start| and
+ *                 |length|.  See the following tree image.
+ *
+ *                 aEvent
+ *                   +-- composition
+ *                   |     +-- string
+ *                   |     +-- clauses[]
+ *                   |           +-- length
+ *                   |           +-- attr
+ *                   +-- caret
+ *                         +-- start
+ *                         +-- length
+ *
+ *                 Set the composition string to |composition.string|.  Set its
+ *                 clauses information to the |clauses| array.
+ *
+ *                 When it's composing, set the each clauses' length to the
+ *                 |composition.clauses[n].length|.  The sum of the all length
+ *                 values must be same as the length of |composition.string|.
+ *                 Set nsIDOMWindowUtils.COMPOSITION_ATTR_* to the
+ *                 |composition.clauses[n].attr|.
+ *
+ *                 When it's not composing, set 0 to the
+ *                 |composition.clauses[0].length| and
+ *                 |composition.clauses[0].attr|.
+ *
+ *                 Set caret position to the |caret.start|. It's offset from
+ *                 the start of the composition string.  Set caret length to
+ *                 |caret.length|.  If it's larger than 0, it should be wide
+ *                 caret.  However, current nsEditor doesn't support wide
+ *                 caret, therefore, you should always set 0 now.
+ *
+ * @param aWindow  Optional (If null, current |window| will be used)
+ */
+function synthesizeText(aEvent, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return;
+  }
+
+  if (!aEvent.composition || !aEvent.composition.clauses ||
+      !aEvent.composition.clauses[0]) {
+    return;
+  }
+
+  var firstClauseLength = aEvent.composition.clauses[0].length;
+  var firstClauseAttr   = aEvent.composition.clauses[0].attr;
+  var secondClauseLength = 0;
+  var secondClauseAttr = 0;
+  var thirdClauseLength = 0;
+  var thirdClauseAttr = 0;
+  if (aEvent.composition.clauses[1]) {
+    secondClauseLength = aEvent.composition.clauses[1].length;
+    secondClauseAttr   = aEvent.composition.clauses[1].attr;
+    if (aEvent.composition.clauses[2]) {
+      thirdClauseLength = aEvent.composition.clauses[2].length;
+      thirdClauseAttr   = aEvent.composition.clauses[2].attr;
+    }
+  }
+
+  var caretStart = -1;
+  var caretLength = 0;
+  if (aEvent.caret) {
+    caretStart = aEvent.caret.start;
+    caretLength = aEvent.caret.length;
+  }
+
+  utils.sendTextEvent(aEvent.composition.string,
+                      firstClauseLength, firstClauseAttr,
+                      secondClauseLength, secondClauseAttr,
+                      thirdClauseLength, thirdClauseAttr,
+                      caretStart, caretLength);
+}
+
+/**
+ * Synthesize a query selected text event.
+ *
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQuerySelectedText(aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
+}
+
+/**
+ * Synthesize a query text content event.
+ *
+ * @param aOffset  The character offset.  0 means the first character in the
+ *                 selection root.
+ * @param aLength  The length of getting text.  If the length is too long,
+ *                 the extra length is ignored.
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQueryTextContent(aOffset, aLength, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_TEXT_CONTENT,
+                                     aOffset, aLength, 0, 0);
+}
+
+/**
+ * Synthesize a query caret rect event.
+ *
+ * @param aOffset  The caret offset.  0 means left side of the first character
+ *                 in the selection root.
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQueryCaretRect(aOffset, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_CARET_RECT,
+                                     aOffset, 0, 0, 0);
+}
+
+/**
+ * Synthesize a query text rect event.
+ *
+ * @param aOffset  The character offset.  0 means the first character in the
+ *                 selection root.
+ * @param aLength  The length of the text.  If the length is too long,
+ *                 the extra length is ignored.
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQueryTextRect(aOffset, aLength, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_TEXT_RECT,
+                                     aOffset, aLength, 0, 0);
+}
+
+/**
+ * Synthesize a query editor rect event.
+ *
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeQueryEditorRect(aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_EDITOR_RECT, 0, 0, 0, 0);
+}
+
+/**
+ * Synthesize a character at point event.
+ *
+ * @param aX, aY   The offset in the client area of the DOM window.
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         An nsIQueryContentEventResult object.  If this failed,
+ *                 the result might be null.
+ */
+function synthesizeCharAtPoint(aX, aY, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return nsnull;
+  }
+  return utils.sendQueryContentEvent(utils.QUERY_CHARACTER_AT_POINT,
+                                     0, 0, aX, aY);
+}
+
+/**
+ * Synthesize a selection set event.
+ *
+ * @param aOffset  The character offset.  0 means the first character in the
+ *                 selection root.
+ * @param aLength  The length of the text.  If the length is too long,
+ *                 the extra length is ignored.
+ * @param aReverse If true, the selection is from |aOffset + aLength| to
+ *                 |aOffset|.  Otherwise, from |aOffset| to |aOffset + aLength|.
+ * @param aWindow  Optional (If null, current |window| will be used)
+ * @return         True, if succeeded.  Otherwise false.
+ */
+function synthesizeSelectionSet(aOffset, aLength, aReverse, aWindow)
+{
+  var utils = _getDOMWindowUtils(aWindow);
+  if (!utils) {
+    return false;
+  }
+  return utils.sendSelectionSetEvent(aOffset, aLength, aReverse);
+}
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/stdlib/arrays.js
@@ -0,0 +1,93 @@
+// ***** BEGIN LICENSE BLOCK *****// ***** BEGIN LICENSE BLOCK *****
+// Version: MPL 1.1/GPL 2.0/LGPL 2.1
+// 
+// The contents of this file are subject to the Mozilla Public License Version
+// 1.1 (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+// http://www.mozilla.org/MPL/
+// 
+// Software distributed under the License is distributed on an "AS IS" basis,
+// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+// for the specific language governing rights and limitations under the
+// License.
+// 
+// The Original Code is Mozilla Corporation Code.
+// 
+// The Initial Developer of the Original Code is
+// Mikeal Rogers.
+// Portions created by the Initial Developer are Copyright (C) 2008
+// the Initial Developer. All Rights Reserved.
+// 
+// Contributor(s):
+//  Mikeal Rogers <mikeal.rogers@gmail.com>
+// 
+// Alternatively, the contents of this file may be used under the terms of
+// either the GNU General Public License Version 2 or later (the "GPL"), or
+// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+// in which case the provisions of the GPL or the LGPL are applicable instead
+// of those above. If you wish to allow use of your version of this file only
+// under the terms of either the GPL or the LGPL, and not to allow others to
+// use your version of this file under the terms of the MPL, indicate your
+// decision by deleting the provisions above and replace them with the notice
+// and other provisions required by the GPL or the LGPL. If you do not delete
+// the provisions above, a recipient may use your version of this file under
+// the terms of any one of the MPL, the GPL or the LGPL.
+// 
+// ***** END LICENSE BLOCK *****
+
+var EXPORTED_SYMBOLS = ['inArray', 'getSet', 'indexOf', 'rindexOf', 'compare'];
+
+function inArray (array, value) {
+  for (i in array) {
+    if (value == array[i]) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function getSet (array) {
+  var narray = [];
+  for (i in array) {
+    if ( !inArray(narray, array[i]) ) {
+      narray.push(array[i]);
+    } 
+  }
+  return narray;
+}
+
+function indexOf (array, v, offset) {
+  for (i in array) {
+    if (offset == undefined || i >= offset) {
+      if ( !isNaN(i) && array[i] == v) {
+        return new Number(i);
+      }
+    }
+  }
+  return -1;
+}
+
+function rindexOf (array, v) {
+  var l = array.length;
+  for (i in array) {
+    if (!isNaN(i)) {
+      var i = new Number(i)
+    }
+    if (!isNaN(i) && array[l - i] == v) {
+      return l - i;
+    }
+  }
+  return -1;
+}
+
+function compare (array, carray) {
+  if (array.length != carray.length) {
+    return false;
+  }
+  for (i in array) {
+    if (array[i] != carray[i]) {
+      return false;
+    }
+  }
+  return true;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/stdlib/dom.js
@@ -0,0 +1,54 @@
+// ***** BEGIN LICENSE BLOCK *****// ***** BEGIN LICENSE BLOCK *****
+// Version: MPL 1.1/GPL 2.0/LGPL 2.1
+// 
+// The contents of this file are subject to the Mozilla Public License Version
+// 1.1 (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+// http://www.mozilla.org/MPL/
+// 
+// Software distributed under the License is distributed on an "AS IS" basis,
+// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+// for the specific language governing rights and limitations under the
+// License.
+// 
+// The Original Code is Mozilla Corporation Code.
+// 
+// The Initial Developer of the Original Code is
+// Mikeal Rogers.
+// Portions created by the Initial Developer are Copyright (C) 2008
+// the Initial Developer. All Rights Reserved.
+// 
+// Contributor(s):
+//  Mikeal Rogers <mikeal.rogers@gmail.com>
+// 
+// Alternatively, the contents of this file may be used under the terms of
+// either the GNU General Public License Version 2 or later (the "GPL"), or
+// the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+// in which case the provisions of the GPL or the LGPL are applicable instead
+// of those above. If you wish to allow use of your version of this file only
+// under the terms of either the GPL or the LGPL, and not to allow others to
+// use your version of this file under the terms of the MPL, indicate your
+// decision by deleting the provisions above and replace them with the notice
+// and other provisions required by the GPL or the LGPL. If you do not delete
+// the provisions above, a recipient may use your version of this file under
+// the terms of any one of the MPL, the GPL or the LGPL.
+// 
+// ***** END LICENSE BLOCK *****
+
+var EXPORTED_SYMBOLS = ['getAttributes'];
+
+
+var getAttributes = function (node) {
+  var attributes = {};
+  for (i in node.attributes) {
+    if ( !isNaN(i) ) {
+      try {
+        var attr = node.attributes[i];
+        attributes[attr.name] = attr.value;
+      } catch (err) {
+      }
+    }
+  }
+  return attributes;
+}
+
new file mode 100644
--- /dev/null
+++ b/testing/peptest/peptest/extension/resource/mozmill/stdlib/httpd.js
@@ -0,0 +1,5202 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the httpd.js server.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2006
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Darin Fisher (v1, netwerk/test/TestServ.js)
+ *   Christian Biesinger (v2, netwerk/test/unit/head_http_server.js)
+ *   Jeff Walden <jwalden+code@mit.edu> (v3, netwerk/test/httpserver/httpd.js)
+ *   Robert Sayre <sayrer@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * An implementation of an HTTP server both as a loadable script and as an XPCOM
+ * component.  See the accompanying README file for user documentation on
+ * httpd.js.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var EXPORTED_SYMBOLS = ['getServer'];
+
+/**
+ * Overwrite both dump functions because we do not wanna have this output for Mozmill
+ */
+function dump() {}
+function dumpn() {}
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const CC = Components.Constructor;
+
+const PR_UINT32_MAX = Math.pow(2, 32) - 1;
+
+/** True if debugging output is enabled, false otherwise. */
+var DEBUG = false; // non-const *only* so tweakable in server tests
+
+/** True if debugging output should be timestamped. */
+var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
+
+var gGlobalObject = this;
+
+/**
+ * Asserts that the given condition holds.  If it doesn't, the given message is
+ * dumped, a stack trace is printed, and an exception is thrown to attempt to
+ * stop execution (which unfortunately must rely upon the exception not being
+ * accidentally swallowed by the code that uses it).
+ */
+function NS_ASSERT(cond, msg)
+{
+  if (DEBUG && !cond)
+  {
+    dumpn("###!!!");
+    dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
+    dumpn("###!!! Stack follows:");
+
+    var stack = new Error().stack.split(/\n/);
+    dumpn(stack.map(function(val) { return "###!!!   " + val; }).join("\n"));
+
+    throw Cr.NS_ERROR_ABORT;
+  }
+}
+
+/** Constructs an HTTP error object. */
+function HttpError(code, description)
+{
+  this.code = code;
+  this.description = description;
+}
+HttpError.prototype =
+{
+  toString: function()
+  {
+    return this.code + " " + this.description;
+  }
+};
+
+/**
+ * Errors thrown to trigger specific HTTP server responses.
+ */
+const HTTP_400 = new HttpError(400, "Bad Request");
+const HTTP_401 = new HttpError(401, "Unauthorized");
+const HTTP_402 = new HttpError(402, "Payment Required");
+const HTTP_403 = new HttpError(403, "Forbidden");
+const HTTP_404 = new HttpError(404, "Not Found");
+const HTTP_405 = new HttpError(405, "Method Not Allowed");
+const HTTP_406 = new HttpError(406, "Not Acceptable");
+const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
+const HTTP_408 = new HttpError(408, "Request Timeout");
+const HTTP_409 = new HttpError(409, "Conflict");
+const HTTP_410 = new HttpError(410, "Gone");
+const HTTP_411 = new HttpError(411, "Length Required");
+const HTTP_412 = new HttpError(412, "Precondition Failed");
+const HTTP_413 = new HttpError(413, "Request Entity Too Large");
+const HTTP_414 = new HttpError(414, "Request-URI Too Long");
+const HTTP_415 = new HttpError(415, "Unsupported Media Type");
+const HTTP_417 = new HttpError(417, "Expectation Failed");
+
+const HTTP_500 = new HttpError(500, "Internal Server Error");
+const HTTP_501 = new HttpError(501, "Not Implemented");
+const HTTP_502 = new HttpError(502, "Bad Gateway");
+const HTTP_503 = new HttpError(503, "Service Unavailable");
+const HTTP_504 = new HttpError(504, "Gateway Timeout");
+const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
+
+/** Creates a hash with fields corresponding to the values in arr. */
+function array2obj(arr)
+{
+  var obj = {};
+  for (var i = 0; i < arr.length; i++)
+    obj[arr[i]] = arr[i];
+  return obj;
+}
+
+/** Returns an array of the integers x through y, inclusive. */
+function range(x, y)
+{
+  var arr = [];
+  for (var i = x; i <= y; i++)
+    arr.push(i);
+  return arr;
+}
+
+/** An object (hash) whose fields are the numbers of all HTTP error codes. */
+const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
+
+
+/**
+ * The character used to distinguish hidden files from non-hidden files, a la
+ * the leading dot in Apache.  Since that mechanism also hides files from
+ * easy display in LXR, ls output, etc. however, we choose instead to use a
+ * suffix character.  If a requested file ends with it, we append another
+ * when getting the file on the server.  If it doesn't, we just look up that
+ * file.  Therefore, any file whose name ends with exactly one of the character
+ * is "hidden" and available for use by the server.
+ */
+const HIDDEN_CHAR = "^";
+
+/**
+ * The file name suffix indicating the file containing overridden headers for
+ * a requested file.
+ */
+const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
+
+/** Type used to denote SJS scripts for CGI-like functionality. */
+const SJS_TYPE = "sjs";
+
+/** Base for relative timestamps produced by dumpn(). */
+var firstStamp = 0;
+
+/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
+function dumpn(str)
+{
+  if (DEBUG)
+  {
+    var prefix = "HTTPD-INFO | ";
+    if (DEBUG_TIMESTAMP)
+    {
+      if (firstStamp === 0)
+        firstStamp = Date.now();
+
+      var elapsed = Date.now() - firstStamp; // milliseconds
+      var min = Math.floor(elapsed / 60000);
+      var sec = (elapsed % 60000) / 1000;
+
+      if (sec < 10)
+        prefix += min + ":0" + sec.toFixed(3) + " | ";
+      else
+        prefix += min + ":" + sec.toFixed(3) + " | ";
+    }
+
+    dump(prefix + str + "\n");
+  }
+}
+
+/** Dumps the current JS stack if DEBUG. */
+function dumpStack()
+{
+  // peel off the frames for dumpStack() and Error()
+  var stack = new Error().stack.split(/\n/).slice(2);
+  stack.forEach(dumpn);
+}
+
+
+/** The XPCOM thread manager. */
+var gThreadManager = null;
+
+/** The XPCOM prefs service. */
+var gRootPrefBranch = null;
+function getRootPrefBranch()
+{
+  if (!gRootPrefBranch)
+  {
+    gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"]
+                        .getService(Ci.nsIPrefBranch);
+  }
+  return gRootPrefBranch;
+}
+
+/**
+ * JavaScript constructors for commonly-used classes; precreating these is a
+ * speedup over doing the same from base principles.  See the docs at
+ * http://developer.mozilla.org/en/docs/Components.Constructor for details.
+ */
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+                        "nsIServerSocket",
+                        "init");
+const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
+                                 "nsIScriptableInputStream",
+                                 "init");
+const Pipe = CC("@mozilla.org/pipe;1",
+                "nsIPipe",
+                "init");
+const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
+                           "nsIFileInputStream",
+                           "init");
+const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
+                                "nsIConverterInputStream",
+                                "init");
+const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
+                               "nsIWritablePropertyBag2");
+const SupportsString = CC("@mozilla.org/supports-string;1",
+                          "nsISupportsString");
+
+/* These two are non-const only so a test can overwrite them. */
+var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                           "nsIBinaryInputStream",
+                           "setInputStream");
+var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+                            "nsIBinaryOutputStream",
+                            "setOutputStream");
+
+/**
+ * Returns the RFC 822/1123 representation of a date.
+ *
+ * @param date : Number
+ *   the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
+ * @returns string
+ *   the representation of the given date
+ */
+function toDateString(date)
+{
+  //
+  // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
+  // date1        = 2DIGIT SP month SP 4DIGIT
+  //                ; day month year (e.g., 02 Jun 1982)
+  // time         = 2DIGIT ":" 2DIGIT ":" 2DIGIT
+  //                ; 00:00:00 - 23:59:59
+  // wkday        = "Mon" | "Tue" | "Wed"
+  //              | "Thu" | "Fri" | "Sat" | "Sun"
+  // month        = "Jan" | "Feb" | "Mar" | "Apr"
+  //              | "May" | "Jun" | "Jul" | "Aug"
+  //              | "Sep" | "Oct" | "Nov" | "Dec"
+  //
+
+  const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+  const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+  /**
+   * Processes a date and returns the encoded UTC time as a string according to
+   * the format specified in RFC 2616.
+   *
+   * @param date : Date
+   *   the date to process
+   * @returns string
+   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
+   */
+  function toTime(date)
+  {
+    var hrs = date.getUTCHours();
+    var rv  = (hrs < 10) ? "0" + hrs : hrs;
+    
+    var mins = date.getUTCMinutes();
+    rv += ":";
+    rv += (mins < 10) ? "0" + mins : mins;
+
+    var secs = date.getUTCSeconds();
+    rv += ":";
+    rv += (secs < 10) ? "0" + secs : secs;
+
+    return rv;
+  }
+
+  /**
+   * Processes a date and returns the encoded UTC date as a string according to
+   * the date1 format specified in RFC 2616.
+   *
+   * @param date : Date
+   *   the date to process
+   * @returns string
+   *   a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
+   */
+  function toDate1(date)
+  {
+    var day = date.getUTCDate();
+    var month = date.getUTCMonth();
+    var year = date.getUTCFullYear();
+
+    var rv = (day < 10) ? "0" + day : day;
+    rv += " " + monthStrings[month];
+    rv += " " + year;
+
+    return rv;
+  }
+
+  date = new Date(date);
+
+  const fmtString = "%wkday%, %date1% %time% GMT";
+  var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
+  rv = rv.replace("%time%", toTime(date));
+  return rv.replace("%date1%", toDate1(date));
+}
+
+/**
+ * Prints out a human-readable representation of the object o and its fields,
+ * omitting those whose names begin with "_" if showMembers != true (to ignore
+ * "private" properties exposed via getters/setters).
+ */
+function printObj(o, showMembers)
+{
+  var s = "******************************\n";
+  s +=    "o = {\n";
+  for (var i in o)
+  {
+    if (typeof(i) != "string" ||
+        (showMembers || (i.length > 0 && i[0] != "_")))
+      s+= "      " + i + ": " + o[i] + ",\n";
+  }
+  s +=    "    };\n";
+  s +=    "******************************";
+  dumpn(s);
+}
+
+/**
+ * Instantiates a new HTTP server.
+ */
+function nsHttpServer()
+{
+  if (!gThreadManager)
+    gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+  /** The port on which this server listens. */
+  this._port = undefined;
+
+  /** The socket associated with this. */
+  this._socket = null;
+
+  /** The handler used to process requests to this server. */
+  this._handler = new ServerHandler(this);
+
+  /** Naming information for this server. */
+  this._identity = new ServerIdentity();
+
+  /**
+   * Indicates when the server is to be shut down at the end of the request.
+   */
+  this._doQuit = false;
+
+  /**
+   * True if the socket in this is closed (and closure notifications have been
+   * sent and processed if the socket was ever opened), false otherwise.
+   */
+  this._socketClosed = true;
+
+  /**
+   * Used for tracking existing connections and ensuring that all connections
+   * are properly cleaned up before server shutdown; increases by 1 for every
+   * new incoming connection.
+   */
+  this._connectionGen = 0;
+
+  /**
+   * Hash of all open connections, indexed by connection number at time of
+   * creation.
+   */
+  this._connections = {};
+}
+nsHttpServer.prototype =
+{
+  classID: Components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
+
+  // NSISERVERSOCKETLISTENER
+
+  /**
+   * Processes an incoming request coming in on the given socket and contained
+   * in the given transport.
+   *
+   * @param socket : nsIServerSocket
+   *   the socket through which the request was served
+   * @param trans : nsISocketTransport
+   *   the transport for the request/response
+   * @see nsIServerSocketListener.onSocketAccepted
+   */
+  onSocketAccepted: function(socket, trans)
+  {
+    dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
+
+    dumpn(">>> new connection on " + trans.host + ":" + trans.port);
+
+    const SEGMENT_SIZE = 8192;
+    const SEGMENT_COUNT = 1024;
+    try
+    {
+      var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+                       .QueryInterface(Ci.nsIAsyncInputStream);
+      var output = trans.openOutputStream(0, 0, 0);
+    }
+    catch (e)
+    {
+      dumpn("*** error opening transport streams: " + e);
+      trans.close(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
+    var connectionNumber = ++this._connectionGen;
+
+    try
+    {
+      var conn = new Connection(input, output, this, socket.port, trans.port,
+                                connectionNumber);
+      var reader = new RequestReader(conn);
+
+      // XXX add request timeout functionality here!
+
+      // Note: must use main thread here, or we might get a GC that will cause
+      //       threadsafety assertions.  We really need to fix XPConnect so that
+      //       you can actually do things in multi-threaded JS.  :-(
+      input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
+    }
+    catch (e)
+    {
+      // Assume this connection can't be salvaged and bail on it completely;
+      // don't attempt to close it so that we can assert that any connection
+      // being closed is in this._connections.
+      dumpn("*** error in initial request-processing stages: " + e);
+      trans.close(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
+    this._connections[connectionNumber] = conn;
+    dumpn("*** starting connection " + connectionNumber);
+  },
+
+  /**
+   * Called when the socket associated with this is closed.
+   *
+   * @param socket : nsIServerSocket
+   *   the socket being closed
+   * @param status : nsresult
+   *   the reason the socket stopped listening (NS_BINDING_ABORTED if the server
+   *   was stopped using nsIHttpServer.stop)
+   * @see nsIServerSocketListener.onStopListening
+   */
+  onStopListening: function(socket, status)
+  {
+    dumpn(">>> shutting down server on port " + socket.port);
+    this._socketClosed = true;
+    if (!this._hasOpenConnections())
+    {
+      dumpn("*** no open connections, notifying async from onStopListening");
+
+      // Notify asynchronously so that any pending teardown in stop() has a
+      // chance to run first.
+      var self = this;
+      var stopEvent =
+        {
+          run: function()
+          {
+            dumpn("*** _notifyStopped async callback");
+            self._notifyStopped();
+          }
+        };
+      gThreadManager.currentThread
+                    .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL);
+    }
+  },
+
+  // NSIHTTPSERVER
+
+  //
+  // see nsIHttpServer.start
+  //
+  start: function(port)
+  {
+    this._start(port, "localhost")
+  },
+
+  _start: function(port, host)
+  {
+    if (this._socket)
+      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
+
+    this._port = port;
+    this._doQuit = this._socketClosed = false;
+
+    this._host = host;
+
+    // The listen queue needs to be long enough to handle
+    // network.http.max-connections-per-server concurrent connections,
+    // plus a safety margin in case some other process is talking to
+    // the server as well.
+    var prefs = getRootPrefBranch();
+    var maxConnections =
+      prefs.getIntPref("network.http.max-connections-per-server") + 5;
+
+    try
+    {
+      var loopback = true;
+      if (this._host != "127.0.0.1" && this._host != "localhost") {
+        var loopback = false;
+      }
+
+      var socket = new ServerSocket(this._port,
+                                    loopback, // true = localhost, false = everybody
+                                    maxConnections);
+      dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
+            " pending connections");
+      socket.asyncListen(this);
+      this._identity._initialize(port, host, true);
+      this._socket = socket;
+    }
+    catch (e)
+    {
+      dumpn("!!! could not start server on port " + port + ": " + e);
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    }
+  },
+
+  //
+  // see nsIHttpServer.stop
+  //
+  stop: function(callback)
+  {
+    if (!callback)
+      throw Cr.NS_ERROR_NULL_POINTER;
+    if (!this._socket)
+      throw Cr.NS_ERROR_UNEXPECTED;
+
+    this._stopCallback = typeof callback === "function"
+                       ? callback
+                       : function() { callback.onStopped(); };
+
+    dumpn(">>> stopping listening on port " + this._socket.port);
+    this._socket.close();
+    this._socket = null;
+
+    // We can't have this identity any more, and the port on which we're running
+    // this server now could be meaningless the next time around.
+    this._identity._teardown();
+
+    this._doQuit = false;
+
+    // socket-close notification and pending request completion happen async
+  },
+
+  //
+  // see nsIHttpServer.registerFile
+  //
+  registerFile: function(path, file)
+  {
+    if (file && (!file.exists() || file.isDirectory()))
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    this._handler.registerFile(path, file);
+  },
+
+  //
+  // see nsIHttpServer.registerDirectory
+  //
+  registerDirectory: function(path, directory)
+  {
+    // XXX true path validation!
+    if (path.charAt(0) != "/" ||
+        path.charAt(path.length - 1) != "/" ||
+        (directory &&
+         (!directory.exists() || !directory.isDirectory())))
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
+    //     exists!
+
+    this._handler.registerDirectory(path, directory);
+  },
+
+  //
+  // see nsIHttpServer.registerPathHandler
+  //
+  registerPathHandler: function(path, handler)
+  {
+    this._handler.registerPathHandler(path, handler);
+  },
+
+  //
+  // see nsIHttpServer.registerErrorHandler
+  //
+  registerErrorHandler: function(code, handler)
+  {
+    this._handler.registerErrorHandler(code, handler);
+  },
+
+  //
+  // see nsIHttpServer.setIndexHandler
+  //
+  setIndexHandler: function(handler)
+  {
+    this._handler.setIndexHandler(handler);
+  },
+
+  //
+  // see nsIHttpServer.registerContentType
+  //
+  registerContentType: function(ext, type)
+  {
+    this._handler.registerContentType(ext, type);
+  },
+
+  //
+  // see nsIHttpServer.serverIdentity
+  //
+  get identity()
+  {
+    return this._identity;
+  },
+
+  //
+  // see nsIHttpServer.getState
+  //
+  getState: function(path, k)
+  {
+    return this._handler._getState(path, k);
+  },
+
+  //
+  // see nsIHttpServer.setState
+  //
+  setState: function(path, k, v)
+  {
+    return this._handler._setState(path, k, v);
+  },
+
+  //
+  // see nsIHttpServer.getSharedState
+  //
+  getSharedState: function(k)
+  {
+    return this._handler._getSharedState(k);
+  },
+
+  //
+  // see nsIHttpServer.setSharedState
+  //
+  setSharedState: function(k, v)
+  {
+    return this._handler._setSharedState(k, v);
+  },
+
+  //
+  // see nsIHttpServer.getObjectState
+  //
+  getObjectState: function(k)
+  {
+    return this._handler._getObjectState(k);
+  },
+
+  //
+  // see nsIHttpServer.setObjectState
+  //
+  setObjectState: function(k, v)
+  {
+    return this._handler._setObjectState(k, v);
+  },
+
+
+  // NSISUPPORTS
+
+  //
+  // see nsISupports.QueryInterface
+  //
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIHttpServer) ||
+        iid.equals(Ci.nsIServerSocketListener) ||
+        iid.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // NON-XPCOM PUBLIC API
+
+  /**
+   * Returns true iff this server is not running (and is not in the process of
+   * serving any requests still to be processed when the server was last
+   * stopped after being run).
+   */
+  isStopped: function()
+  {
+    return this._socketClosed && !this._hasOpenConnections();
+  },
+
+  // PRIVATE IMPLEMENTATION
+
+  /** True if this server has any open connections to it, false otherwise. */
+  _hasOpenConnections: function()
+  {
+    //
+    // If we have any open connections, they're tracked as numeric properties on
+    // |this._connections|.  The non-standard __count__ property could be used
+    // to check whether there are any properties, but standard-wise, even
+    // looking forward to ES5, there's no less ugly yet still O(1) way to do
+    // this.
+    //
+    for (var n in this._connections)
+      return true;
+    return false;
+  },
+
+  /** Calls the server-stopped callback provided when stop() was called. */
+  _notifyStopped: function()
+  {
+    NS_ASSERT(this._stopCallback !== null, "double-notifying?");
+    NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
+
+    //
+    // NB: We have to grab this now, null out the member, *then* call the
+    //     callback here, or otherwise the callback could (indirectly) futz with
+    //     this._stopCallback by starting and immediately stopping this, at
+    //     which point we'd be nulling out a field we no longer have a right to
+    //     modify.
+    //
+    var callback = this._stopCallback;
+    this._stopCallback = null;
+    try
+    {
+      callback();
+    }
+    catch (e)
+    {
+      // not throwing because this is specified as being usually (but not
+      // always) asynchronous
+      dump("!!! error running onStopped callback: " + e + "\n");
+    }
+  },
+
+  /**
+   * Notifies this server that the given connection has been closed.
+   *
+   * @param connection : Connection
+   *   the connection that was closed
+   */
+  _connectionClosed: function(connection)
+  {
+    NS_ASSERT(connection.number in this._connections,
+              "closing a connection " + this + " that we never added to the " +
+              "set of open connections?");
+    NS_ASSERT(this._connections[connection.number] === connection,
+              "connection number mismatch?  " +
+              this._connections[connection.number]);
+    delete this._connections[connection.number];
+
+    // Fire a pending server-stopped notification if it's our responsibility.
+    if (!this._hasOpenConnections() && this._socketClosed)
+      this._notifyStopped();
+  },
+
+  /**
+   * Requests that the server be shut down when possible.
+   */
+  _requestQuit: function()
+  {
+    dumpn(">>> requesting a quit");
+    dumpStack();
+    this._doQuit = true;
+  }
+};
+
+
+//
+// RFC 2396 section 3.2.2:
+//
+// host        = hostname | IPv4address
+// hostname    = *( domainlabel "." ) toplabel [ "." ]
+// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+// toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
+// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
+//
+
+const HOST_REGEX =
+  new RegExp("^(?:" +
+               // *( domainlabel "." )
+               "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
+               // toplabel
+               "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
+             "|" +
+               // IPv4 address 
+               "\\d+\\.\\d+\\.\\d+\\.\\d+" +
+             ")$",
+             "i");
+
+
+/**
+ * Represents the identity of a server.  An identity consists of a set of
+ * (scheme, host, port) tuples denoted as locations (allowing a single server to
+ * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
+ * host/port).  Any incoming request must be to one of these locations, or it
+ * will be rejected with an HTTP 400 error.  One location, denoted as the
+ * primary location, is the location assigned in contexts where a location
+ * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
+ *
+ * A single identity may contain at most one location per unique host/port pair;
+ * other than that, no restrictions are placed upon what locations may
+ * constitute an identity.
+ */
+function ServerIdentity()
+{
+  /** The scheme of the primary location. */
+  this._primaryScheme = "http";
+
+  /** The hostname of the primary location. */
+  this._primaryHost = "127.0.0.1"
+
+  /** The port number of the primary location. */
+  this._primaryPort = -1;
+
+  /**
+   * The current port number for the corresponding server, stored so that a new
+   * primary location can always be set if the current one is removed.
+   */
+  this._defaultPort = -1;
+
+  /**
+   * Maps hosts to maps of ports to schemes, e.g. the following would represent
+   * https://example.com:789/ and http://example.org/:
+   *
+   *   {
+   *     "xexample.com": { 789: "https" },
+   *     "xexample.org": { 80: "http" }
+   *   }
+   *
+   * Note the "x" prefix on hostnames, which prevents collisions with special
+   * JS names like "prototype".
+   */
+  this._locations = { "xlocalhost": {} };
+}
+ServerIdentity.prototype =
+{
+  // NSIHTTPSERVERIDENTITY
+
+  //
+  // see nsIHttpServerIdentity.primaryScheme
+  //
+  get primaryScheme()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryScheme;
+  },
+
+  //
+  // see nsIHttpServerIdentity.primaryHost
+  //
+  get primaryHost()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryHost;
+  },
+
+  //
+  // see nsIHttpServerIdentity.primaryPort
+  //
+  get primaryPort()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryPort;
+  },
+
+  //
+  // see nsIHttpServerIdentity.add
+  //
+  add: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    var entry = this._locations["x" + host];
+    if (!entry)
+      this._locations["x" + host] = entry = {};
+
+    entry[port] = scheme;
+  },
+
+  //
+  // see nsIHttpServerIdentity.remove
+  //
+  remove: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    var entry = this._locations["x" + host];
+    if (!entry)
+      return false;
+
+    var present = port in entry;
+    delete entry[port];
+
+    if (this._primaryScheme == scheme &&
+        this._primaryHost == host &&
+        this._primaryPort == port &&
+        this._defaultPort !== -1)
+    {
+      // Always keep at least one identity in existence at any time, unless
+      // we're in the process of shutting down (the last condition above).
+      this._primaryPort = -1;
+      this._initialize(this._defaultPort, host, false);
+    }
+
+    return present;
+  },
+
+  //
+  // see nsIHttpServerIdentity.has
+  //
+  has: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    return "x" + host in this._locations &&
+           scheme === this._locations["x" + host][port];
+  },
+
+  //
+  // see nsIHttpServerIdentity.has
+  //
+  getScheme: function(host, port)
+  {
+    this._validate("http", host, port);
+
+    var entry = this._locations["x" + host];
+    if (!entry)
+      return "";
+
+    return entry[port] || "";
+  },
+
+  //
+  // see nsIHttpServerIdentity.setPrimary
+  //
+  setPrimary: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    this.add(scheme, host, port);
+
+    this._primaryScheme = scheme;
+    this._primaryHost = host;
+    this._primaryPort = port;
+  },
+
+
+  // NSISUPPORTS
+
+  //
+  // see nsISupports.QueryInterface
+  //
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // PRIVATE IMPLEMENTATION
+
+  /**
+   * Initializes the primary name for the corresponding server, based on the
+   * provided port number.
+   */
+  _initialize: function(port, host, addSecondaryDefault)
+  {
+    this._host = host;
+    if (this._primaryPort !== -1)
+      this.add("http", host, port);
+    else
+      this.setPrimary("http", "localhost", port);
+    this._defaultPort = port;
+
+    // Only add this if we're being called at server startup
+    if (addSecondaryDefault && host != "127.0.0.1")
+      this.add("http", "127.0.0.1", port);
+  },
+
+  /**
+   * Called at server shutdown time, unsets the primary location only if it was
+   * the default-assigned location and removes the default location from the
+   * set of locations used.
+   */
+  _teardown: function()
+  {
+    if (this._host != "127.0.0.1") {
+      // Not the default primary location, nothing special to do here
+      this.remove("http", "127.0.0.1", this._defaultPort);
+    }
+    
+    // This is a *very* tricky bit of reasoning here; make absolutely sure the
+    // tests for this code pass before you commit changes to it.
+    if (this._primaryScheme == "http" &&
+        this._primaryHost == this._host &&
+        this._primaryPort == this._defaultPort)
+    {
+      // Make sure we don't trigger the readding logic in .remove(), then remove
+      // the default location.
+      var port = this._defaultPort;
+      this._defaultPort = -1;
+      this.remove("http", this._host, port);
+
+      // Ensure a server start triggers the setPrimary() path in ._initialize()
+      this._primaryPort = -1;
+    }
+    else
+    {
+      // No reason not to remove directly as it's not our primary location
+      this.remove("http", this._host, this._defaultPort);
+    }
+  },
+
+  /**
+   * Ensures scheme, host, and port are all valid with respect to RFC 2396.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if any argument doesn't match the corresponding production
+   */
+  _validate: function(scheme, host, port)
+  {
+    if (scheme !== "http" && scheme !== "https")
+    {
+      dumpn("*** server only supports http/https schemes: '" + scheme + "'");
+      dumpStack();
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+    if (!HOST_REGEX.test(host))
+    {
+      dumpn("*** unexpected host: '" + host + "'");
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+    if (port < 0 || port > 65535)
+    {
+      dumpn("*** unexpected port: '" + port + "'");
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+  }
+};
+
+
+/**
+ * Represents a connection to the server (and possibly in the future the thread
+ * on which the connection is processed).
+ *
+ * @param input : nsIInputStream
+ *   stream from which incoming data on the connection is read
+ * @param output : nsIOutputStream
+ *   stream to write data out the connection
+ * @param server : nsHttpServer
+ *   the server handling the connection
+ * @param port : int
+ *   the port on which the server is running
+ * @param outgoingPort : int
+ *   the outgoing port used by this connection
+ * @param number : uint
+ *   a serial number used to uniquely identify this connection
+ */
+function Connection(input, output, server, port, outgoingPort, number)
+{
+  dumpn("*** opening new connection " + number + " on port " + outgoingPort);
+
+  /** Stream of incoming data. */
+  this.input = input;
+
+  /** Stream for outgoing data. */
+  this.output = output;
+
+  /** The server associated with this request. */
+  this.server = server;
+
+  /** The port on which the server is running. */
+  this.port = port;
+
+  /** The outgoing poort used by this connection. */
+  this._outgoingPort = outgoingPort;
+
+  /** The serial number of this connection. */
+  this.number = number;
+
+  /**
+   * The request for which a response is being generated, null if the
+   * incoming request has not been fully received or if it had errors.
+   */
+  this.request = null;
+
+  /** State variables for debugging. */
+  this._closed = this._processed = false;
+}
+Connection.prototype =
+{
+  /** Closes this connection's input/output streams. */
+  close: function()
+  {
+    dumpn("*** closing connection " + this.number +
+          " on port " + this._outgoingPort);
+
+    this.input.close();
+    this.output.close();
+    this._closed = true;
+
+    var server = this.server;
+    server._connectionClosed(this);
+
+    // If an error triggered a server shutdown, act on it now
+    if (server._doQuit)
+      server.stop(function() { /* not like we can do anything better */ });
+  },
+
+  /**
+   * Initiates processing of this connection, using the data in the given
+   * request.
+   *
+   * @param request : Request
+   *   the request which should be processed
+   */
+  process: function(request)
+  {
+    NS_ASSERT(!this._closed && !this._processed);
+
+    this._processed = true;
+
+    this.request = request;
+    this.server._handler.handleResponse(this);
+  },
+
+  /**
+   * Initiates processing of this connection, generating a response with the
+   * given HTTP error code.
+   *
+   * @param code : uint
+   *   an HTTP code, so in the range [0, 1000)
+   * @param request : Request
+   *   incomplete data about the incoming request (since there were errors
+   *   during its processing
+   */
+  processError: function(code, request)
+  {
+    NS_ASSERT(!this._closed && !this._processed);
+
+    this._processed = true;
+    this.request = request;
+    this.server._handler.handleError(code, this);
+  },
+
+  /** Converts this to a string for debugging purposes. */
+  toString: function()
+  {
+    return "<Connection(" + this.number +
+           (this.request ? ", " + this.request.path : "") +"): " +
+           (this._closed ? "closed" : "open") + ">";
+  }
+};
+
+
+
+/** Returns an array of count bytes from the given input stream. */
+function readBytes(inputStream, count)
+{
+  return new BinaryInputStream(inputStream).readByteArray(count);
+}
+
+
+
+/** Request reader processing states; see RequestReader for details. */
+const READER_IN_REQUEST_LINE = 0;
+const READER_IN_HEADERS      = 1;
+const READER_IN_BODY         = 2;
+const READER_FINISHED        = 3;
+
+
+/**
+ * Reads incoming request data asynchronously, does any necessary preprocessing,
+ * and forwards it to the request handler.  Processing occurs in three states:
+ *
+ *   READER_IN_REQUEST_LINE     Reading the request's status line
+ *   READER_IN_HEADERS          Reading headers in the request
+ *   READER_IN_BODY             Reading the body of the request
+ *   READER_FINISHED            Entire request has been read and processed
+ *
+ * During the first two stages, initial metadata about the request is gathered
+ * into a Request object.  Once the status line and headers have been processed,
+ * we start processing the body of the request into the Request.  Finally, when
+ * the entire body has been read, we create a Response and hand it off to the
+ * ServerHandler to be given to the appropriate request handler.
+ *
+ * @param connection : Connection
+ *   the connection for the request being read
+ */
+function RequestReader(connection)
+{
+  /** Connection metadata for this request. */
+  this._connection = connection;
+
+  /**
+   * A container providing line-by-line access to the raw bytes that make up the
+   * data which has been read from the connection but has not yet been acted
+   * upon (by passing it to the request handler or by extracting request
+   * metadata from it).
+   */
+  this._data = new LineData();
+
+  /**
+   * The amount of data remaining to be read from the body of this request.
+   * After all headers in the request have been read this is the value in the
+   * Content-Length header, but as the body is read its value decreases to zero.
+   */
+  this._contentLength = 0;
+
+  /** The current state of parsing the incoming request. */
+  this._state = READER_IN_REQUEST_LINE;
+
+  /** Metadata constructed from the incoming request for the request handler. */
+  this._metadata = new Request(connection.port);
+
+  /**
+   * Used to preserve state if we run out of line data midway through a
+   * multi-line header.  _lastHeaderName stores the name of the header, while
+   * _lastHeaderValue stores the value we've seen so far for the header.
+   *
+   * These fields are always either both undefined or both strings.
+   */
+  this._lastHeaderName = this._lastHeaderValue = undefined;
+}
+RequestReader.prototype =
+{
+  // NSIINPUTSTREAMCALLBACK
+
+  /**
+   * Called when more data from the incoming request is available.  This method
+   * then reads the available data from input and deals with that data as
+   * necessary, depending upon the syntax of already-downloaded data.
+   *
+   * @param input : nsIAsyncInputStream
+   *   the stream of incoming data from the connection
+   */
+  onInputStreamReady: function(input)
+  {
+    dumpn("*** onInputStreamReady(input=" + input + ") on thread " +
+          gThreadManager.currentThread + " (main is " +
+          gThreadManager.mainThread + ")");
+    dumpn("*** this._state == " + this._state);
+
+    // Handle cases where we get more data after a request error has been
+    // discovered but *before* we can close the connection.
+    var data = this._data;
+    if (!data)
+      return;
+
+    try
+    {
+      data.appendBytes(readBytes(input, input.available()));
+    }
+    catch (e)
+    {
+      if (streamClosed(e))
+      {
+        dumpn("*** WARNING: unexpected error when reading from socket; will " +
+              "be treated as if the input stream had been closed");
+        dumpn("*** WARNING: actual error was: " + e);
+      }
+
+      // We've lost a race -- input has been closed, but we're still expecting
+      // to read more data.  available() will throw in this case, and since
+      // we're dead in the water now, destroy the connection.
+      dumpn("*** onInputStreamReady called on a closed input, destroying " +
+            "connection");
+      this._connection.close();
+      return;
+    }
+
+    switch (this._state)
+    {
+      default:
+        NS_ASSERT(false, "invalid state: " + this._state);
+        break;
+
+      case READER_IN_REQUEST_LINE:
+        if (!this._processRequestLine())
+          break;
+        /* fall through */
+
+      case READER_IN_HEADERS:
+        if (!this._processHeaders())
+          break;
+        /* fall through */
+
+      case READER_IN_BODY:
+        this._processBody();
+    }
+
+    if (this._state != READER_FINISHED)
+      input.asyncWait(this, 0, 0, gThreadManager.currentThread);
+  },
+
+  //
+  // see nsISupports.QueryInterface
+  //
+  QueryInterface: function(aIID)
+  {
+    if (aIID.equals(Ci.nsIInputStreamCallback) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // PRIVATE API
+
+  /**
+   * Processes unprocessed, downloaded data as a request line.
+   *
+   * @returns boolean
+   *   true iff the request line has been fully processed
+   */
+  _processRequestLine: function()
+  {
+    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
+
+    // Servers SHOULD ignore any empty line(s) received where a Request-Line
+    // is expected (section 4.1).
+    var data = this._data;
+    var line = {};
+    var readSuccess;
+    while ((readSuccess = data.readLine(line)) && line.value == "")
+      dumpn("*** ignoring beginning blank line...");
+
+    // if we don't have a full line, wait until we do
+    if (!readSuccess)
+      return false;
+
+    // we have the first non-blank line
+    try
+    {
+      this._parseRequestLine(line.value);
+      this._state = READER_IN_HEADERS;
+      return true;
+    }
+    catch (e)
+    {
+      this._handleError(e);
+      return false;
+    }
+  },
+
+  /**
+   * Processes stored data, assuming it is either at the beginning or in
+   * the middle of processing request headers.
+   *
+   * @returns boolean
+   *   true iff header data in the request has been fully processed
+   */
+  _processHeaders: function()
+  {
+    NS_ASSERT(this._state == READER_IN_HEADERS);
+
+    // XXX things to fix here:
+    //
+    // - need to support RFC 2047-encoded non-US-ASCII characters
+
+    try
+    {
+      var done = this._parseHeaders();
+      if (done)
+      {
+        var request = this._metadata;
+
+        // XXX this is wrong for requests with transfer-encodings applied to
+        //     them, particularly chunked (which by its nature can have no
+        //     meaningful Content-Length header)!
+        this._contentLength = request.hasHeader("Content-Length")
+                            ? parseInt(request.getHeader("Content-Length"), 10)
+                            : 0;
+        dumpn("_processHeaders, Content-length=" + this._contentLength);
+
+        this._state = READER_IN_BODY;
+      }
+      return done;
+    }
+    catch (e)
+    {
+      this._handleError(e);
+      return false;
+    }
+  },
+
+  /**
+   * Processes stored data, assuming it is either at the beginning or in
+   * the middle of processing the request body.
+   *
+   * @returns boolean
+   *   true iff the request body has been fully processed
+   */
+  _processBody: function()
+  {
+    NS_ASSERT(this._state == READER_IN_BODY);
+
+    // XXX handle chunked transfer-coding request bodies!
+
+    try
+    {
+      if (this._contentLength > 0)
+      {
+        var data = this._data.purge();
+        var count = Math.min(data.length, this._contentLength);
+        dumpn("*** loading data=" + data + " len=" + data.length +
+              " excess=" + (data.length - count));
+
+        var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
+        bos.writeByteArray(data, count);
+        this._contentLength -= count;
+      }
+
+      dumpn("*** remaining body data len=" + this._contentLength);
+      if (this._contentLength == 0)
+      {
+        this._validateRequest();
+        this._state = READER_FINISHED;
+        this._handleResponse();
+        return true;
+      }
+      
+      return false;
+    }
+    catch (e)
+    {
+      this._handleError(e);
+      return false;
+    }
+  },
+
+  /**
+   * Does various post-header checks on the data in this request.
+   *
+   * @throws : HttpError
+   *   if the request was malformed in some way
+   */
+  _validateRequest: function()
+  {
+    NS_ASSERT(this._state == READER_IN_BODY);
+
+    dumpn("*** _validateRequest");
+
+    var metadata = this._metadata;
+    var headers = metadata._headers;
+
+    // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
+    var identity = this._connection.server.identity;
+    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
+    {
+      if (!headers.hasHeader("Host"))
+      {
+        dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
+        throw HTTP_400;
+      }
+
+      // If the Request-URI wasn't absolute, then we need to determine our host.
+      // We have to determine what scheme was used to access us based on the
+      // server identity data at this point, because the request just doesn't
+      // contain enough data on its own to do this, sadly.
+      if (!metadata._host)
+      {
+        var host, port;
+        var hostPort = headers.getHeader("Host");
+        var colon = hostPort.indexOf(":");
+        if (colon < 0)
+        {
+          host = hostPort;
+          port = "";
+        }
+        else
+        {
+          host = hostPort.substring(0, colon);
+          port = hostPort.substring(colon + 1);
+        }
+
+        // NB: We allow an empty port here because, oddly, a colon may be
+        //     present even without a port number, e.g. "example.com:"; in this
+        //     case the default port applies.
+        if (!HOST_REGEX.test(host) || !/^\d*$/.test(port))
+        {
+          dumpn("*** malformed hostname (" + hostPort + ") in Host " +
+                "header, 400 time");
+          throw HTTP_400;
+        }
+
+        // If we're not given a port, we're stuck, because we don't know what
+        // scheme to use to look up the correct port here, in general.  Since
+        // the HTTPS case requires a tunnel/proxy and thus requires that the
+        // requested URI be absolute (and thus contain the necessary
+        // information), let's assume HTTP will prevail and use that.
+        port = +port || 80;
+
+        var scheme = identity.getScheme(host, port);
+        if (!scheme)
+        {
+          dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
+                "header, 400 time");
+          throw HTTP_400;
+        }
+
+        metadata._scheme = scheme;
+        metadata._host = host;
+        metadata._port = port;
+      }
+    }
+    else
+    {
+      NS_ASSERT(metadata._host === undefined,
+                "HTTP/1.0 doesn't allow absolute paths in the request line!");
+
+      metadata._scheme = identity.primaryScheme;
+      metadata._host = identity.primaryHost;
+      metadata._port = identity.primaryPort;
+    }
+
+    NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
+              "must have a location we recognize by now!");
+  },
+
+  /**
+   * Handles responses in case of error, either in the server or in the request.
+   *
+   * @param e
+   *   the specific error encountered, which is an HttpError in the case where
+   *   the request is in some way invalid or cannot be fulfilled; if this isn't
+   *   an HttpError we're going to be paranoid and shut down, because that
+   *   shouldn't happen, ever
+   */
+  _handleError: function(e)
+  {
+    // Don't fall back into normal processing!
+    this._state = READER_FINISHED;
+
+    var server = this._connection.server;
+    if (e instanceof HttpError)
+    {
+      var code = e.code;
+    }
+    else
+    {
+      dumpn("!!! UNEXPECTED ERROR: " + e +
+            (e.lineNumber ? ", line " + e.lineNumber : ""));
+
+      // no idea what happened -- be paranoid and shut down
+      code = 500;
+      server._requestQuit();
+    }
+
+    // make attempted reuse of data an error
+    this._data = null;
+
+    this._connection.processError(code, this._metadata);
+  },
+
+  /**
+   * Now that we've read the request line and headers, we can actually hand off
+   * the request to be handled.
+   *
+   * This method is called once per request, after the request line and all
+   * headers and the body, if any, have been received.
+   */
+  _handleResponse: function()
+  {
+    NS_ASSERT(this._state == READER_FINISHED);
+
+    // We don't need the line-based data any more, so make attempted reuse an
+    // error.
+    this._data = null;
+
+    this._connection.process(this._metadata);
+  },
+
+
+  // PARSING
+
+  /**
+   * Parses the request line for the HTTP request associated with this.
+   *
+   * @param line : string
+   *   the request line
+   */
+  _parseRequestLine: function(line)
+  {
+    NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
+
+    dumpn("*** _parseRequestLine('" + line + "')");
+
+    var metadata = this._metadata;
+
+    // clients and servers SHOULD accept any amount of SP or HT characters
+    // between fields, even though only a single SP is required (section 19.3)
+    var request = line.split(/[ \t]+/);
+    if (!request || request.length != 3)
+      throw HTTP_400;
+
+    metadata._method = request[0];
+
+    // get the HTTP version
+    var ver = request[2];
+    var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
+    if (!match)
+      throw HTTP_400;
+
+    // determine HTTP version
+    try
+    {
+      metadata._httpVersion = new nsHttpVersion(match[1]);
+      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
+        throw "unsupported HTTP version";
+    }
+    catch (e)
+    {
+      // we support HTTP/1.0 and HTTP/1.1 only
+      throw HTTP_501;
+    }
+
+
+    var fullPath = request[1];
+    var serverIdentity = this._connection.server.identity;
+
+    var scheme, host, port;
+
+    if (fullPath.charAt(0) != "/")
+    {
+      // No absolute paths in the request line in HTTP prior to 1.1
+      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
+        throw HTTP_400;
+
+      try
+      {
+        var uri = Cc["@mozilla.org/network/io-service;1"]
+                    .getService(Ci.nsIIOService)
+                    .newURI(fullPath, null, null);
+        fullPath = uri.path;
+        scheme = uri.scheme;
+        host = metadata._host = uri.asciiHost;
+        port = uri.port;
+        if (port === -1)
+        {
+          if (scheme === "http")
+            port = 80;
+          else if (scheme === "https")
+            port = 443;
+          else
+            throw HTTP_400;
+        }
+      }
+      catch (e)
+      {
+        // If the host is not a valid host on the server, the response MUST be a
+        // 400 (Bad Request) error message (section 5.2).  Alternately, the URI
+        // is malformed.
+        throw HTTP_400;
+      }
+
+      if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
+        throw HTTP_400;
+    }
+
+    var splitter = fullPath.indexOf("?");
+    if (splitter < 0)
+    {
+      // _queryString already set in ctor
+      metadata._path = fullPath;
+    }
+    else
+    {
+      metadata._path = fullPath.substring(0, splitter);
+      metadata._queryString = fullPath.substring(splitter + 1);
+    }
+
+    metadata._scheme = scheme;
+    metadata._host = host;
+    metadata._port = port;
+  },
+
+  /**
+   * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
+   * adding them to the store of headers in the request.
+   *
+   * @throws
+   *   HTTP_400 if the headers are malformed
+   * @returns boolean
+   *   true if all headers have now been processed, false otherwise
+   */
+  _parseHeaders: function()
+  {
+    NS_ASSERT(this._state == READER_IN_HEADERS);
+
+    dumpn("*** _parseHeaders");
+
+    var data = this._data;
+
+    var headers = this._metadata._headers;
+    var lastName = this._lastHeaderName;
+    var lastVal = this._lastHeaderValue;
+
+    var line = {};
+    while (true)
+    {
+      NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
+                lastName === undefined ?
+                  "lastVal without lastName?  lastVal: '" + lastVal + "'" :
+                  "lastName without lastVal?  lastName: '" + lastName + "'");
+
+      if (!data.readLine(line))
+      {
+        // save any data we have from the header we might still be processing
+        this._lastHeaderName = lastName;
+        this._lastHeaderValue = lastVal;
+        return false;
+      }
+
+      var lineText = line.value;
+      var firstChar = lineText.charAt(0);
+
+      // blank line means end of headers
+      if (lineText == "")
+      {
+        // we're finished with the previous header
+        if (lastName)
+        {
+          try
+          {
+            headers.setHeader(lastName, lastVal, true);
+          }
+          catch (e)
+          {
+            dumpn("*** e == " + e);
+            throw HTTP_400;
+          }
+        }
+        else
+        {
+          // no headers in request -- valid for HTTP/1.0 requests
+        }
+
+        // either way, we're done processing headers
+        this._state = READER_IN_BODY;
+        return true;
+      }
+      else if (firstChar == " " || firstChar == "\t")
+      {
+        // multi-line header if we've already seen a header line
+        if (!lastName)
+        {
+          // we don't have a header to continue!
+          throw HTTP_400;
+        }
+
+        // append this line's text to the value; starts with SP/HT, so no need
+        // for separating whitespace
+        lastVal += lineText;
+      }
+      else
+      {
+        // we have a new header, so set the old one (if one existed)
+        if (lastName)
+        {
+          try
+          {
+            headers.setHeader(lastName, lastVal, true);
+          }
+          catch (e)
+          {
+            dumpn("*** e == " + e);
+            throw HTTP_400;
+          }
+        }
+
+        var colon = lineText.indexOf(":"); // first colon must be splitter
+        if (colon < 1)
+        {
+          // no colon or missing header field-name
+          throw HTTP_400;
+        }
+
+        // set header name, value (to be set in the next loop, usually)
+        lastName = lineText.substring(0, colon);
+        lastVal = lineText.substring(colon + 1);
+      } // empty, continuation, start of header
+    } // while (true)
+  }
+};
+
+
+/** The character codes for CR and LF. */
+const CR = 0x0D, LF = 0x0A;
+
+/**
+ * Calculates the number of characters before the first CRLF pair in array, or
+ * -1 if the array contains no CRLF pair.
+ *
+ * @param array : Array
+ *   an array of numbers in the range [0, 256), each representing a single
+ *   character; the first CRLF is the lowest index i where
+ *   |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
+ *   if such an |i| exists, and -1 otherwise
+ * @returns int
+ *   the index of the first CRLF if any were present, -1 otherwise
+ */
+function findCRLF(array)
+{
+  for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
+  {
+    if (array[i + 1] == LF)
+      return i;
+  }
+  return -1;
+}
+
+
+/**
+ * A container which provides line-by-line access to the arrays of bytes with
+ * which it is seeded.
+ */
+function LineData()
+{
+  /** An array of queued bytes from which to get line-based characters. */
+  this._data = [];
+}
+LineData.prototype =
+{
+  /**
+   * Appends the bytes in the given array to the internal data cache maintained
+   * by this.
+   */
+  appendBytes: function(bytes)
+  {
+    Array.prototype.push.apply(this._data, bytes);
+  },
+
+  /**
+   * Removes and returns a line of data, delimited by CRLF, from this.
+   *
+   * @param out
+   *   an object whose "value" property will be set to the first line of text
+   *   present in this, sans CRLF, if this contains a full CRLF-delimited line
+   *   of text; if this doesn't contain enough data, the value of the property
+   *   is undefined
+   * @returns boolean
+   *   true if a full line of data could be read from the data in this, false
+   *   otherwise
+   */
+  readLine: function(out)
+  {
+    var data = this._data;
+    var length = findCRLF(data);
+    if (length < 0)
+      return false;
+
+    //
+    // We have the index of the CR, so remove all the characters, including
+    // CRLF, from the array with splice, and convert the removed array into the
+    // corresponding string, from which we then strip the trailing CRLF.
+    //
+    // Getting the line in this matter acknowledges that substring is an O(1)
+    // operation in SpiderMonkey because strings are immutable, whereas two
+    // splices, both from the beginning of the data, are less likely to be as
+    // cheap as a single splice plus two extra character conversions.
+    //
+    var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
+    out.value = line.substring(0, length);
+
+    return true;
+  },
+
+  /**
+   * Removes the bytes currently within this and returns them in an array.
+   *
+   * @returns Array
+   *   the bytes within this when this method is called
+   */
+  purge: function()
+  {
+    var data = this._data;
+    this._data = [];
+    return data;
+  }
+};
+
+
+
+/**
+ * Creates a request-handling function for an nsIHttpRequestHandler object.
+ */
+function createHandlerFunc(handler)
+{
+  return function(metadata, response) { handler.handle(metadata, response); };
+}
+
+
+/**
+ * The default handler for directories; writes an HTML response containing a
+ * slightly-formatted directory listing.
+ */
+function defaultIndexHandler(metadata, response)
+{
+  response.setHeader("Content-Type", "text/html", false);
+
+  var path = htmlEscape(decodeURI(metadata.path));
+
+  //
+  // Just do a very basic bit of directory listings -- no need for too much
+  // fanciness, especially since we don't have a style sheet in which we can
+  // stick rules (don't want to pollute the default path-space).
+  //
+
+  var body = '<html>\
+                <head>\
+                  <title>' + path + '</title>\
+                </head>\
+                <body>\
+                  <h1>' + path + '</h1>\
+                  <ol style="list-style-type: none">';
+
+  var directory = metadata.getProperty("directory");
+  NS_ASSERT(directory && directory.isDirectory());
+
+  var fileList = [];
+  var files = directory.directoryEntries;
+  while (files.hasMoreElements())
+  {
+    var f = files.getNext().QueryInterface(Ci.nsIFile);
+    var name = f.leafName;
+    if (!f.isHidden() &&
+        (name.charAt(name.length - 1) != HIDDEN_CHAR ||
+         name.charAt(name.length - 2) == HIDDEN_CHAR))
+      fileList.push(f);
+  }
+
+  fileList.sort(fileSort);
+
+  for (var i = 0; i < fileList.length; i++)
+  {
+    var file = fileList[i];
+    try
+    {
+      var name = file.leafName;
+      if (name.charAt(name.length - 1) == HIDDEN_CHAR)
+        name = name.substring(0, name.length - 1);
+      var sep = file.isDirectory() ? "/" : "";
+
+      // Note: using " to delimit the attribute here because encodeURIComponent
+      //       passes through '.
+      var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' +
+                   htmlEscape(name) + sep +
+                 '</a></li>';
+
+      body += item;
+    }
+    catch (e) { /* some file system error, ignore the file */ }
+  }
+
+  body    += '    </ol>\
+                </body>\
+              </html>';
+
+  response.bodyOutputStream.write(body, body.length);
+}
+
+/**
+ * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
+ */
+function fileSort(a, b)
+{
+  var dira = a.isDirectory(), dirb = b.isDirectory();
+
+  if (dira && !dirb)
+    return -1;
+  if (dirb && !dira)
+    return 1;
+
+  var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase();
+  return nameb > namea ? -1 : 1;
+}
+
+
+/**
+ * Converts an externally-provided path into an internal path for use in
+ * determining file mappings.
+ *
+ * @param path
+ *   the path to convert
+ * @param encoded
+ *   true if the given path should be passed through decodeURI prior to
+ *   conversion
+ * @throws URIError
+ *   if path is incorrectly encoded
+ */
+function toInternalPath(path, encoded)
+{
+  if (encoded)
+    path = decodeURI(path);
+
+  var comps = path.split("/");
+  for (var i = 0, sz = comps.length; i < sz; i++)
+  {
+    var comp = comps[i];
+    if (comp.charAt(comp.length - 1) == HIDDEN_CHAR)
+      comps[i] = comp + HIDDEN_CHAR;
+  }
+  return comps.join("/");
+}
+
+
+/**
+ * Adds custom-specified headers for the given file to the given response, if
+ * any such headers are specified.
+ *
+ * @param file
+ *   the file on the disk which is to be written
+ * @param metadata
+ *   metadata about the incoming request
+ * @param response
+ *   the Response to which any specified headers/data should be written
+ * @throws HTTP_500
+ *   if an error occurred while processing custom-specified headers
+ */
+function maybeAddHeaders(file, metadata, response)
+{
+  var name = file.leafName;
+  if (name.charAt(name.length - 1) == HIDDEN_CHAR)
+    name = name.substring(0, name.length - 1);
+
+  var headerFile = file.parent;
+  headerFile.append(name + HEADERS_SUFFIX);
+
+  if (!headerFile.exists())
+    return;
+
+  const PR_RDONLY = 0x01;
+  var fis = new FileInputStream(headerFile, PR_RDONLY, 0444,
+                                Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+  try
+  {
+    var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
+    lis.QueryInterface(Ci.nsIUnicharLineInputStream);
+
+    var line = {value: ""};
+    var more = lis.readLine(line);
+
+    if (!more && line.value == "")
+      return;
+
+
+    // request line
+
+    var status = line.value;
+    if (status.indexOf("HTTP ") == 0)
+    {
+      status = status.substring(5);
+      var space = status.indexOf(" ");
+      var code, description;
+      if (space < 0)
+      {
+        code = status;
+        description = "";
+      }
+      else
+      {
+        code = status.substring(0, space);
+        description = status.substring(space + 1, status.length);
+      }
+    
+      response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
+
+      line.value = "";
+      more = lis.readLine(line);
+    }
+
+    // headers
+    while (more || line.value != "")
+    {
+      var header = line.value;
+      var colon = header.indexOf(":");
+
+      response.setHeader(header.substring(0, colon),
+                         header.substring(colon + 1, header.length),
+                         false); // allow overriding server-set headers
+
+      line.value = "";
+      more = lis.readLine(line);
+    }
+  }
+  catch (e)
+  {
+    dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
+    throw HTTP_500;
+  }
+  finally
+  {
+    fis.close();
+  }
+}
+
+
+/**
+ * An object which handles requests for a server, executing default and
+ * overridden behaviors as instructed by the code which uses and manipulates it.
+ * Default behavior includes the paths / and /trace (diagnostics), with some
+ * support for HTTP error pages for various codes and fallback to HTTP 500 if
+ * those codes fail for any reason.
+ *
+ * @param server : nsHttpServer
+ *   the server in which this handler is being used
+ */
+function ServerHandler(server)
+{
+  // FIELDS
+
+  /**
+   * The nsHttpServer instance associated with this handler.
+   */
+  this._server = server;
+
+  /**
+   * A FileMap object containing the set of path->nsILocalFile mappings for
+   * all directory mappings set in the server (e.g., "/" for /var/www/html/,
+   * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
+   *
+   * Note carefully: the leading and trailing "/" in each path (not file) are
+   * removed before insertion to simplify the code which uses this.  You have
+   * been warned!
+   */
+  this._pathDirectoryMap = new FileMap();
+
+  /**
+   * Custom request handlers for the server in which this resides.  Path-handler
+   * pairs are stored as property-value pairs in this property.
+   *
+   * @see ServerHandler.prototype._defaultPaths
+   */
+  this._overridePaths = {};
+  
+  /**
+   * Custom request handlers for the error handlers in the server in which this
+   * resides.  Path-handler pairs are stored as property-value pairs in this
+   * property.
+   *
+   * @see ServerHandler.prototype._defaultErrors
+   */
+  this._overrideErrors = {};
+
+  /**
+   * Maps file extensions to their MIME types in the server, overriding any
+   * mapping that might or might not exist in the MIME service.
+   */
+  this._mimeMappings = {};
+
+  /**
+   * The default handler for requests for directories, used to serve directories
+   * when no index file is present.
+   */
+  this._indexHandler = defaultIndexHandler;
+
+  /** Per-path state storage for the server. */
+  this._state = {};
+
+  /** Entire-server state storage. */
+  this._sharedState = {};
+
+  /** Entire-server state storage for nsISupports values. */
+  this._objectState = {};
+}
+ServerHandler.prototype =
+{
+  // PUBLIC API
+
+  /**
+   * Handles a request to this server, responding to the request appropriately
+   * and initiating server shutdown if necessary.
+   *
+   * This method never throws an exception.
+   *
+   * @param connection : Connection
+   *   the connection for this request
+   */
+  handleResponse: function(connection)
+  {
+    var request = connection.request;
+    var response = new Response(connection);
+
+    var path = request.path;
+    dumpn("*** path == " + path);
+
+    try
+    {
+      try
+      {
+        if (path in this._overridePaths)
+        {
+          // explicit paths first, then files based on existing directory mappings,
+          // then (if the file doesn't exist) built-in server default paths
+          dumpn("calling override for " + path);
+          this._overridePaths[path](request, response);
+        }
+        else
+        {
+          this._handleDefault(request, response);
+        }
+      }
+      catch (e)
+      {
+        if (response.partiallySent())
+        {
+          response.abort(e);
+          return;
+        }
+
+        if (!(e instanceof HttpError))
+        {
+          dumpn("*** unexpected error: e == " + e);
+          throw HTTP_500;
+        }
+        if (e.code !== 404)
+          throw e;
+
+        dumpn("*** default: " + (path in this._defaultPaths));
+
+        response = new Response(connection);
+        if (path in this._defaultPaths)
+          this._defaultPaths[path](request, response);
+        else
+          throw HTTP_404;
+      }
+    }
+    catch (e)
+    {
+      if (response.partiallySent())
+      {
+        response.abort(e);
+        return;
+      }
+
+      var errorCode = "internal";
+
+      try
+      {
+        if (!(e instanceof HttpError))
+          throw e;
+
+        errorCode = e.code;
+        dumpn("*** errorCode == " + errorCode);
+
+        response = new Response(connection);
+        if (e.customErrorHandling)
+          e.customErrorHandling(response);
+        this._handleError(errorCode, request, response);
+        return;
+      }
+      catch (e2)
+      {
+        dumpn("*** error handling " + errorCode + " error: " +
+              "e2 == " + e2 + ", shutting down server");
+
+        connection.server._requestQuit();
+        response.abort(e2);
+        return;
+      }
+    }
+
+    response.complete();
+  },
+
+  //
+  // see nsIHttpServer.registerFile
+  //
+  registerFile: function(path, file)
+  {
+    if (!file)
+    {
+      dumpn("*** unregistering '" + path + "' mapping");
+      delete this._overridePaths[path];
+      return;
+    }
+
+    dumpn("*** registering '" + path + "' as mapping to " + file.path);
+    file = file.clone();
+
+    var self = this;
+    this._overridePaths[path] =
+      function(request, response)
+      {
+        if (!file.exists())
+          throw HTTP_404;
+
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        self._writeFileResponse(request, file, response, 0, file.fileSize);
+      };
+  },
+
+  //
+  // see nsIHttpServer.registerPathHandler
+  //
+  registerPathHandler: function(path, handler)
+  {
+    // XXX true path validation!
+    if (path.charAt(0) != "/")
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    this._handlerToField(handler, this._overridePaths, path);
+  },
+
+  //
+  // see nsIHttpServer.registerDirectory
+  //
+  registerDirectory: function(path, directory)
+  {
+    // strip off leading and trailing '/' so that we can use lastIndexOf when
+    // determining exactly how a path maps onto a mapped directory --
+    // conditional is required here to deal with "/".substring(1, 0) being
+    // converted to "/".substring(0, 1) per the JS specification
+    var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
+
+    // the path-to-directory mapping code requires that the first character not
+    // be "/", or it will go into an infinite loop
+    if (key.charAt(0) == "/")
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    key = toInternalPath(key, false);
+
+    if (directory)
+    {
+      dumpn("*** mapping '" + path + "' to the location " + directory.path);
+      this._pathDirectoryMap.put(key, directory);
+    }
+    else
+    {
+      dumpn("*** removing mapping for '" + path + "'");
+      this._pathDirectoryMap.put(key, null);
+    }
+  },
+
+  //
+  // see nsIHttpServer.registerErrorHandler
+  //
+  registerErrorHandler: function(err, handler)
+  {
+    if (!(err in HTTP_ERROR_CODES))
+      dumpn("*** WARNING: registering non-HTTP/1.1 error code " +
+            "(" + err + ") handler -- was this intentional?");
+
+    this._handlerToField(handler, this._overrideErrors, err);
+  },
+
+  //
+  // see nsIHttpServer.setIndexHandler
+  //
+  setIndexHandler: function(handler)
+  {
+    if (!handler)
+      handler = defaultIndexHandler;
+    else if (typeof(handler) != "function")
+      handler = createHandlerFunc(handler);
+
+    this._indexHandler = handler;
+  },
+
+  //
+  // see nsIHttpServer.registerContentType
+  //
+  registerContentType: function(ext, type)
+  {
+    if (!type)
+      delete this._mimeMappings[ext];
+    else
+      this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
+  },
+
+  // PRIVATE API
+
+  /**
+   * Sets or remove (if handler is null) a handler in an object with a key.
+   *
+   * @param handler
+   *   a handler, either function or an nsIHttpRequestHandler
+   * @param dict
+   *   The object to attach the handler to.
+   * @param key
+   *   The field name of the handler.
+   */
+  _handlerToField: function(handler, dict, key)
+  {
+    // for convenience, handler can be a function if this is run from xpcshell
+    if (typeof(handler) == "function")
+      dict[key] = handler;
+    else if (handler)
+      dict[key] = createHandlerFunc(handler);
+    else
+      delete dict[key];
+  },
+
+  /**
+   * Handles a request which maps to a file in the local filesystem (if a base
+   * path has already been set; otherwise the 404 error is thrown).
+   *
+   * @param metadata : Request
+   *   metadata for the incoming request
+   * @param response : Response
+   *   an uninitialized Response to the given request, to be initialized by a
+   *   request handler
+   * @throws HTTP_###
+   *   if an HTTP error occurred (usually HTTP_404); note that in this case the
+   *   calling code must handle post-processing of the response
+   */
+  _handleDefault: function(metadata, response)
+  {
+    dumpn("*** _handleDefault()");
+
+    response.setStatusLine(metadata.httpVersion, 200, "OK");
+
+    var path = metadata.path;
+    NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
+
+    // determine the actual on-disk file; this requires finding the deepest
+    // path-to-directory mapping in the requested URL
+    var file = this._getFileForPath(path);
+
+    // the "file" might be a directory, in which case we either serve the
+    // contained index.html or make the index handler write the response
+    if (file.exists() && file.isDirectory())
+    {
+      file.append("index.html"); // make configurable?
+      if (!file.exists() || file.isDirectory())
+      {
+        metadata._ensurePropertyBag();
+        metadata._bag.setPropertyAsInterface("directory", file.parent);
+        this._indexHandler(metadata, response);
+        return;
+      }
+    }
+
+    // alternately, the file might not exist
+    if (!file.exists())
+      throw HTTP_404;
+
+    var start, end;
+    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
+        metadata.hasHeader("Range") &&
+        this._getTypeFromFile(file) !== SJS_TYPE)
+    {
+      var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
+      if (!rangeMatch)
+        throw HTTP_400;
+
+      if (rangeMatch[1] !== undefined)
+        start = parseInt(rangeMatch[1], 10);
+
+      if (rangeMatch[2] !== undefined)
+        end = parseInt(rangeMatch[2], 10);
+
+      if (start === undefined && end === undefined)
+        throw HTTP_400;
+
+      // No start given, so the end is really the count of bytes from the
+      // end of the file.
+      if (start === undefined)
+      {
+        start = Math.max(0, file.fileSize - end);
+        end   = file.fileSize - 1;
+      }
+
+      // start and end are inclusive
+      if (end === undefined || end >= file.fileSize)
+        end = file.fileSize - 1;
+
+      if (start !== undefined && start >= file.fileSize) {
+        var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
+        HTTP_416.customErrorHandling = function(errorResponse)
+        {
+          maybeAddHeaders(file, metadata, errorResponse);
+        };
+        throw HTTP_416;
+      }
+
+      if (end < start)
+      {
+        response.setStatusLine(metadata.httpVersion, 200, "OK");
+        start = 0;
+        end = file.fileSize - 1;
+      }
+      else
+      {
+        response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
+        var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
+        response.setHeader("Content-Range", contentRange);
+      }
+    }
+    else
+    {
+      start = 0;
+      end = file.fileSize - 1;
+    }
+
+    // finally...
+    dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " +
+          start + " to " + end + " inclusive");
+    this._writeFileResponse(metadata, file, response, start, end - start + 1);
+  },
+
+  /**
+   * Writes an HTTP response for the given file, including setting headers for
+   * file metadata.
+   *
+   * @param metadata : Request
+   *   the Request for which a response is being generated
+   * @param file : nsILocalFile
+   *   the file which is to be sent in the response
+   * @param response : Response
+   *   the response to which the file should be written
+   * @param offset: uint
+   *   the byte offset to skip to when writing
+   * @param count: uint
+   *   the number of bytes to write
+   */
+  _writeFileResponse: function(metadata, file, response, offset, count)
+  {
+    const PR_RDONLY = 0x01;
+
+    var type = this._getTypeFromFile(file);
+    if (type === SJS_TYPE)
+    {
+      var fis = new FileInputStream(file, PR_RDONLY, 0444,
+                                    Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+      try
+      {
+        var sis = new ScriptableInputStream(fis);
+        var s = Cu.Sandbox(gGlobalObject);
+        s.importFunction(dump, "dump");
+
+        // Define a basic key-value state-preservation API across requests, with
+        // keys initially corresponding to the empty string.
+        var self = this;
+        var path = metadata.path;
+        s.importFunction(function getState(k)
+        {
+          return self._getState(path, k);
+        });
+        s.importFunction(function setState(k, v)
+        {
+          self._setState(path, k, v);
+        });
+        s.importFunction(function getSharedState(k)
+        {
+          return self._getSharedState(k);
+        });
+        s.importFunction(function setSharedState(k, v)
+        {
+          self._setSharedState(k, v);
+        });
+        s.importFunction(function getObjectState(k, callback)
+        {
+          callback(self._getObjectState(k));
+        });
+        s.importFunction(function setObjectState(k, v)
+        {
+          self._setObjectState(k, v);
+        });
+
+        // Make it possible for sjs files to access their location
+        this._setState(path, "__LOCATION__", file.path);
+
+        try
+        {
+          // Alas, the line number in errors dumped to console when calling the
+          // request handler is simply an offset from where we load the SJS file.
+          // Work around this in a reasonably non-fragile way by dynamically
+          // getting the line number where we evaluate the SJS file.  Don't
+          // separate these two lines!
+          var line = new Error().lineNumber;
+          Cu.evalInSandbox(sis.read(file.fileSize), s);
+        }
+        catch (e)
+        {
+          dumpn("*** syntax error in SJS at " + file.path + ": " + e);
+          throw HTTP_500;
+        }
+
+        try
+        {
+          s.handleRequest(metadata, response);
+        }
+        catch (e)
+        {
+          dump("*** error running SJS at " + file.path + ": " +
+               e + " on line " +
+               (e instanceof Error
+               ? e.lineNumber + " in httpd.js"
+               : (e.lineNumber - line)) + "\n");
+          throw HTTP_500;
+        }
+      }
+      finally
+      {
+        fis.close();
+      }
+    }
+    else
+    {
+      try
+      {
+        response.setHeader("Last-Modified",
+                           toDateString(file.lastModifiedTime),
+                           false);
+      }
+      catch (e) { /* lastModifiedTime threw, ignore */ }
+
+      response.setHeader("Content-Type", type, false);
+      maybeAddHeaders(file, metadata, response);
+      response.setHeader("Content-Length", "" + count, false);
+
+      var fis = new FileInputStream(file, PR_RDONLY, 0444,
+                                    Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+      offset = offset || 0;
+      count  = count || file.fileSize;
+      NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
+      NS_ASSERT(count >= 0, "bad count");
+      NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
+
+      try
+      {
+        if (offset !== 0)
+        {
+          // Seek (or read, if seeking isn't supported) to the correct offset so
+          // the data sent to the client matches the requested range.
+          if (fis instanceof Ci.nsISeekableStream)
+            fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
+          else
+            new ScriptableInputStream(fis).read(offset);
+        }
+      }
+      catch (e)
+      {
+        fis.close();
+        throw e;
+      }
+
+      function writeMore()
+      {
+        gThreadManager.currentThread
+                      .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
+      }
+
+      var input = new BinaryInputStream(fis);
+      var output = new BinaryOutputStream(response.bodyOutputStream);
+      var writeData =
+        {
+          run: function()
+          {
+            var chunkSize = Math.min(65536, count);
+            count -= chunkSize;
+            NS_ASSERT(count >= 0, "underflow");
+
+            try
+            {
+              var data = input.readByteArray(chunkSize);
+              NS_ASSERT(data.length === chunkSize,
+                        "incorrect data returned?  got " + data.length +
+                        ", expected " + chunkSize);
+              output.writeByteArray(data, data.length);
+              if (count === 0)
+              {
+                fis.close();
+                response.finish();
+              }
+              else
+              {
+                writeMore();
+              }
+            }
+            catch (e)
+            {
+              try
+              {
+                fis.close();
+              }
+              finally
+              {
+                response.finish();
+              }
+              throw e;
+            }
+          }
+        };
+
+      writeMore();
+
+      // Now that we know copying will start, flag the response as async.
+      response.processAsync();
+    }
+  },
+
+  /**
+   * Get the value corresponding to a given key for the given path for SJS state
+   * preservation across requests.
+   *
+   * @param path : string
+   *   the path from which the given state is to be retrieved
+   * @param k : string
+   *   the key whose corresponding value is to be returned
+   * @returns string
+   *   the corresponding value, which is initially the empty string
+   */
+  _getState: function(path, k)
+  {
+    var state = this._state;
+    if (path in state && k in state[path])
+      return state[path][k];
+    return "";
+  },
+
+  /**
+   * Set the value corresponding to a given key for the given path for SJS state
+   * preservation across requests.
+   *
+   * @param path : string
+   *   the path from which the given state is to be retrieved
+   * @param k : string
+   *   the key whose corresponding value is to be set
+   * @param v : string
+   *   the value to be set
+   */
+  _setState: function(path, k, v)
+  {
+    if (typeof v !== "string")
+      throw new Error("non-string value passed");
+    var state = this._state;
+    if (!(path in state))
+      state[path] = {};
+    state[path][k] = v;
+  },
+
+  /**
+   * Get the value corresponding to a given key for SJS state preservation
+   * across requests.
+   *
+   * @param k : string
+   *   the key whose corresponding value is to be returned
+   * @returns string
+   *   the corresponding value, which is initially the empty string
+   */
+  _getSharedState: function(k)
+  {
+    var state = this._sharedState;
+    if (k in state)
+      return state[k];
+    return "";
+  },
+
+  /**
+   * Set the value corresponding to a given key for SJS state preservation
+   * across requests.
+   *
+   * @param k : string
+   *   the key whose corresponding value is to be set
+   * @param v : string
+   *   the value to be set
+   */
+  _setSharedState: function(k, v)
+  {
+    if (typeof v !== "string")
+      throw new Error("non-string value passed");
+    this._sharedState[k] = v;
+  },
+
+  /**
+   * Returns the object associated with the given key in the server for SJS
+   * state preservation across requests.
+   *
+   * @param k : string
+   *  the key whose corresponding object is to be returned
+   * @returns nsISupports
+   *  the corresponding object, or null if none was present
+   */
+  _getObjectState: function(k)
+  {
+    if (typeof k !== "string")
+      throw new Error("non-string key passed");
+    return this._objectState[k] || null;
+  },
+
+  /**
+   * Sets the object associated with the given key in the server for SJS
+   * state preservation across requests.
+   *
+   * @param k : string
+   *  the key whose corresponding object is to be set
+   * @param v : nsISupports
+   *  the object to be associated with the given key; may be null
+   */
+  _setObjectState: function(k, v)
+  {
+    if (typeof k !== "string")
+      throw new Error("non-string key passed");
+    if (typeof v !== "object")
+      throw new Error("non-object value passed");
+    if (v && !("QueryInterface" in v))
+    {
+      throw new Error("must pass an nsISupports; use wrappedJSObject to ease " +
+                      "pain when using the server from JS");
+    }
+
+    this._objectState[k] = v;
+  },
+
+  /**
+   * Gets a content-type for the given file, first by checking for any custom
+   * MIME-types registered with this handler for the file's extension, second by
+   * asking the global MIME service for a content-type, and finally by failing
+   * over to application/octet-stream.
+   *
+   * @param file : nsIFile
+   *   the nsIFile for which to get a file type
+   * @returns string
+   *   the best content-type which can be determined for the file
+   */
+  _getTypeFromFile: function(file)
+  {
+    try
+    {
+      var name = file.leafName;
+      var dot = name.lastIndexOf(".");
+      if (dot > 0)
+      {
+        var ext = name.slice(dot + 1);
+        if (ext in this._mimeMappings)
+          return this._mimeMappings[ext];
+      }
+      return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+               .getService(Ci.nsIMIMEService)
+               .getTypeFromFile(file);
+    }
+    catch (e)
+    {
+      return "application/octet-stream";
+    }
+  },
+
+  /**
+   * Returns the nsILocalFile which corresponds to the path, as determined using
+   * all registered path->directory mappings and any paths which are explicitly
+   * overridden.
+   *
+   * @param path : string
+   *   the server path for which a file should be retrieved, e.g. "/foo/bar"
+   * @throws HttpError
+   *   when the correct action is the corresponding HTTP error (i.e., because no
+   *   mapping was found for a directory in path, the referenced file doesn't
+   *   exist, etc.)
+   * @returns nsILocalFile
+   *   the file to be sent as the response to a request for the path
+   */
+  _getFileForPath: function(path)
+  {
+    // decode and add underscores as necessary
+    try
+    {
+      path = toInternalPath(path, true);
+    }
+    catch (e)
+    {
+      throw HTTP_400; // malformed path
+    }
+
+    // next, get the directory which contains this path
+    var pathMap = this._pathDirectoryMap;
+
+    // An example progression of tmp for a path "/foo/bar/baz/" might be:
+    // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
+    var tmp = path.substring(1);
+    while (true)
+    {
+      // do we have a match for current head of the path?
+      var file = pathMap.get(tmp);
+      if (file)
+      {
+        // XXX hack; basically disable showing mapping for /foo/bar/ when the
+        //     requested path was /foo/bar, because relative links on the page
+        //     will all be incorrect -- we really need the ability to easily
+        //     redirect here instead
+        if (tmp == path.substring(1) &&
+            tmp.length != 0 &&
+            tmp.charAt(tmp.length - 1) != "/")
+          file = null;
+        else
+          break;
+      }
+
+      // if we've finished trying all prefixes, exit
+      if (tmp == "")
+        break;
+
+      tmp = tmp.substring(0, tmp.lastIndexOf("/"));
+    }
+
+    // no mapping applies, so 404
+    if (!file)
+      throw HTTP_404;
+
+
+    // last, get the file for the path within the determined directory
+    var parentFolder = file.parent;
+    var dirIsRoot = (parentFolder == null);
+
+    // Strategy here is to append components individually, making sure we
+    // never move above the given directory; this allows paths such as
+    // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
+    // this component-wise approach also means the code works even on platforms
+    // which don't use "/" as the directory separator, such as Windows
+    var leafPath = path.substring(tmp.length + 1);
+    var comps = leafPath.split("/");
+    for (var i = 0, sz = comps.length; i < sz; i++)
+    {
+      var comp = comps[i];
+
+      if (comp == "..")
+        file = file.parent;
+      else if (comp == "." || comp == "")
+        continue;
+      else
+        file.append(comp);
+
+      if (!dirIsRoot && file.equals(parentFolder))
+        throw HTTP_403;
+    }
+
+    return file;
+  },
+
+  /**
+   * Writes the error page for the given HTTP error code over the given
+   * connection.
+   *
+   * @param errorCode : uint
+   *   the HTTP error code to be used
+   * @param connection : Connection
+   *   the connection on which the error occurred
+   */
+  handleError: function(errorCode, connection)
+  {
+    var response = new Response(connection);
+
+    dumpn("*** error in request: " + errorCode);
+
+    this._handleError(errorCode, new Request(connection.port), response);
+  }, 
+
+  /**
+   * Handles a request which generates the given error code, using the
+   * user-defined error handler if one has been set, gracefully falling back to
+   * the x00 status code if the code has no handler, and failing to status code
+   * 500 if all else fails.
+   *
+   * @param errorCode : uint
+   *   the HTTP error which is to be returned
+   * @param metadata : Request
+   *   metadata for the request, which will often be incomplete since this is an
+   *   error
+   * @param response : Response
+   *   an uninitialized Response should be initialized when this method
+   *   completes with information which represents the desired error code in the
+   *   ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
+   *   fallback for 505, per HTTP specs)
+   */
+  _handleError: function(errorCode, metadata, response)
+  {
+    if (!metadata)
+      throw Cr.NS_ERROR_NULL_POINTER;
+
+    var errorX00 = errorCode - (errorCode % 100);
+
+    try
+    {
+      if (!(errorCode in HTTP_ERROR_CODES))
+        dumpn("*** WARNING: requested invalid error: " + errorCode);
+
+      // RFC 2616 says that we should try to handle an error by its class if we
+      // can't otherwise handle it -- if that fails, we revert to handling it as
+      // a 500 internal server error, and if that fails we throw and shut down
+      // the server
+
+      // actually handle the error
+      try
+      {
+        if (errorCode in this._overrideErrors)
+          this._overrideErrors[errorCode](metadata, response);
+        else
+          this._defaultErrors[errorCode](metadata, response);
+      }
+      catch (e)
+      {
+        if (response.partiallySent())
+        {
+          response.abort(e);
+          return;
+        }
+
+        // don't retry the handler that threw
+        if (errorX00 == errorCode)
+          throw HTTP_500;
+
+        dumpn("*** error in handling for error code " + errorCode + ", " +
+              "falling back to " + errorX00 + "...");
+        response = new Response(response._connection);
+        if (errorX00 in this._overrideErrors)
+          this._overrideErrors[errorX00](metadata, response);
+        else if (errorX00 in this._defaultErrors)
+          this._defaultErrors[errorX00](metadata, response);
+        else
+          throw HTTP_500;
+      }
+    }
+    catch (e)
+    {
+      if (response.partiallySent())
+      {
+        response.abort();
+        return;
+      }
+
+      // we've tried everything possible for a meaningful error -- now try 500
+      dumpn("*** error in handling for error code " + errorX00 + ", falling " +
+            "back to 500...");
+
+      try
+      {
+        response = new Response(response._connection);
+        if (500 in this._overrideErrors)
+          this._overrideErrors[500](metadata, response);
+        else
+          this._defaultErrors[500](metadata, response);
+      }
+      catch (e2)
+      {
+        dumpn("*** multiple errors in default error handlers!");
+        dumpn("*** e == " + e + ", e2 == " + e2);
+        response.abort(e2);
+        return;
+      }
+    }
+
+    response.complete();
+  },
+
+  // FIELDS
+
+  /**
+   * This object contains the default handlers for the various HTTP error codes.
+   */
+  _defaultErrors:
+  {
+    400: function(metadata, response)
+    {
+      // none of the data in metadata is reliable, so hard-code everything here
+      response.setStatusLine("1.1", 400, "Bad Request");
+      response.setHeader("Content-Type", "text/plain", false);
+
+      var body = "Bad request\n";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    403: function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>403 Forbidden</title></head>\
+                    <body>\
+                      <h1>403 Forbidden</h1>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    404: function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>404 Not Found</title></head>\
+                    <body>\
+                      <h1>404 Not Found</h1>\
+                      <p>\
+                        <span style='font-family: monospace;'>" +
+                          htmlEscape(metadata.path) +
+                       "</span> was not found.\
+                      </p>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    416: function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion,
+                            416,
+                            "Requested Range Not Satisfiable");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                   <head>\
+                    <title>416 Requested Range Not Satisfiable</title></head>\
+                    <body>\
+                     <h1>416 Requested Range Not Satisfiable</h1>\
+                     <p>The byte range was not valid for the\
+                        requested resource.\
+                     </p>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    500: function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion,
+                             500,
+                             "Internal Server Error");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>500 Internal Server Error</title></head>\
+                    <body>\
+                      <h1>500 Internal Server Error</h1>\
+                      <p>Something's broken in this server and\
+                        needs to be fixed.</p>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    501: function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>501 Not Implemented</title></head>\
+                    <body>\
+                      <h1>501 Not Implemented</h1>\
+                      <p>This server is not (yet) Apache.</p>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    },
+    505: function(metadata, response)
+    {
+      response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>505 HTTP Version Not Supported</title></head>\
+                    <body>\
+                      <h1>505 HTTP Version Not Supported</h1>\
+                      <p>This server only supports HTTP/1.0 and HTTP/1.1\
+                        connections.</p>\
+                    </body>\
+                  </html>";
+      response.bodyOutputStream.write(body, body.length);
+    }
+  },
+
+  /**
+   * Contains handlers for the default set of URIs contained in this server.
+   */
+  _defaultPaths:
+  {
+    "/": function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "text/html", false);
+
+      var body = "<html>\
+                    <head><title>httpd.js</title></head>\
+                    <body>\
+                      <h1>httpd.js</h1>\
+                      <p>If you're seeing this page, httpd.js is up and\
+                        serving requests!  Now set a base path and serve some\
+                        files!</p>\
+                    </body>\
+                  </html>";
+
+      response.bodyOutputStream.write(body, body.length);
+    },
+
+    "/trace": function(metadata, response)
+    {
+      response.setStatusLine(metadata.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "text/plain", false);
+
+      var body = "Request-URI: " +
+                 metadata.scheme + "://" + metadata.host + ":" + metadata.port +
+                 metadata.path + "\n\n";
+      body += "Request (semantically equivalent, slightly reformatted):\n\n";
+      body += metadata.method + " " + metadata.path;
+
+      if (metadata.queryString)
+        body +=  "?" + metadata.queryString;
+        
+      body += " HTTP/" + metadata.httpVersion + "\r\n";
+
+      var headEnum = metadata.headers;
+      while (headEnum.hasMoreElements())
+      {
+        var fieldName = headEnum.getNext()
+                                .QueryInterface(Ci.nsISupportsString)
+                                .data;
+        body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
+      }
+
+      response.bodyOutputStream.write(body, body.length);
+    }
+  }
+};
+
+
+/**
+ * Maps absolute paths to files on the local file system (as nsILocalFiles).
+ */
+function FileMap()
+{
+  /** Hash which will map paths to nsILocalFiles. */
+  this._map = {};
+}
+FileMap.prototype =
+{
+  // PUBLIC API
+
+  /**
+   * Maps key to a clone of the nsILocalFile value if value is non-null;
+   * otherwise, removes any extant mapping for key.
+   *
+   * @param key : string
+   *   string to which a clone of value is mapped
+   * @param value : nsILocalFile
+   *   the file to map to key, or null to remove a mapping
+   */
+  put: function(key, value)
+  {
+    if (value)
+      this._map[key] = value.clone();
+    else
+      delete this._map[key];
+  },
+
+  /**
+   * Returns a clone of the nsILocalFile mapped to key, or null if no such
+   * mapping exists.
+   *
+   * @param key : string
+   *   key to which the returned file maps
+   * @returns nsILocalFile
+   *   a clone of the mapped file, or null if no mapping exists
+   */
+  get: function(key)
+  {
+    var val = this._map[key];
+    return val ? val.clone() : null;
+  }
+};
+
+
+// Response CONSTANTS
+
+// token       = *<any CHAR except CTLs or separators>
+// CHAR        = <any US-ASCII character (0-127)>
+// CTL         = <any US-ASCII control character (0-31) and DEL (127)>
+// separators  = "(" | ")" | "<" | ">" | "@"
+//             | "," | ";" | ":" | "\" | <">
+//             | "/" | "[" | "]" | "?" | "="
+//             | "{" | "}" | SP  | HT
+const IS_TOKEN_ARRAY =
+  [0, 0, 0, 0, 0, 0, 0, 0, //   0
+   0, 0, 0, 0, 0, 0, 0, 0, //   8
+   0, 0, 0, 0, 0, 0, 0, 0, //  16
+   0, 0, 0, 0, 0, 0, 0, 0, //  24
+
+   0, 1, 0, 1, 1, 1, 1, 1, //  32
+   0, 0, 1, 1, 0, 1, 1, 0, //  40
+   1, 1, 1, 1, 1, 1, 1, 1, //  48
+   1, 1, 0, 0, 0, 0, 0, 0, //  56
+
+   0, 1, 1, 1, 1, 1, 1, 1, //  64
+   1, 1, 1, 1, 1, 1, 1, 1, //  72
+   1, 1, 1, 1, 1, 1, 1, 1, //  80
+   1, 1, 1, 0, 0, 0, 1, 1, //  88
+
+   1, 1, 1, 1, 1, 1, 1, 1, //  96
+   1, 1, 1, 1, 1, 1, 1, 1, // 104
+   1, 1, 1, 1, 1, 1, 1, 1, // 112
+   1, 1, 1, 0, 1, 0, 1];   // 120
+
+
+/**
+ * Determines whether the given character code is a CTL.
+ *
+ * @param code : uint
+ *   the character code
+ * @returns boolean
+ *   true if code is a CTL, false otherwise
+ */
+function isCTL(code)
+{
+  return (code >= 0 && code <= 31) || (code == 127);
+}
+
+/**
+ * Represents a response to an HTTP request, encapsulating all details of that
+ * response.  This includes all headers, the HTTP version, status code and
+ * explanation, and the entity itself.
+ *
+ * @param connection : Connection
+ *   the connection over which this response is to be written
+ */
+function Response(connection)
+{
+  /** The connection over which this response will be written. */
+  this._connection = connection;
+
+  /**
+   * The HTTP version of this response; defaults to 1.1 if not set by the
+   * handler.
+   */
+  this._httpVersion = nsHttpVersion.HTTP_1_1;
+
+  /**
+   * The HTTP code of this response; defaults to 200.
+   */
+  this._httpCode = 200;
+
+  /**
+   * The description of the HTTP code in this response; defaults to "OK".
+   */
+  this._httpDescription = "OK";
+
+  /**
+   * An nsIHttpHeaders object in which the headers in this response should be
+   * stored.  This property is null after the status line and headers have been
+   * written to the network, and it may be modified up until it is cleared,
+   * except if this._finished is set first (in which case headers are written
+   * asynchronously in response to a finish() call not preceded by
+   * flushHeaders()).
+   */
+  this._headers = new nsHttpHeaders();
+
+  /**
+   * Set to true when this response is ended (completely constructed if possible
+   * and the connection closed); further actions on this will then fail.
+   */
+  this._ended = false;
+
+  /**
+   * A stream used to hold data written to the body of this response.
+   */
+  this._bodyOutputStream = null;
+
+  /**
+   * A stream containing all data that has been written to the body of this
+   * response so far.  (Async handlers make the data contained in this
+   * unreliable as a way of determining content length in general, but auxiliary
+   * saved information can sometimes be used to guarantee reliability.)
+   */
+  this._bodyInputStream = null;
+
+  /**
+   * A stream copier which copies data to the network.  It is initially null
+   * until replaced with a copier for response headers; when headers have been
+   * fully sent it is replaced with a copier for the response body, remaining
+   * so for the duration of response processing.
+   */
+  this._asyncCopier = null;
+
+  /**
+   * True if this response has been designated as being processed
+   * asynchronously rather than for the duration of a single call to
+   * nsIHttpRequestHandler.handle.
+   */
+  this._processAsync = false;
+
+  /**
+   * True iff finish() has been called on this, signaling that no more changes
+   * to this may be made.
+   */
+  this._finished = false;
+
+  /**
+   * True iff powerSeized() has been called on this, signaling that this
+   * response is to be handled manually by the response handler (which may then
+   * send arbitrary data in response, even non-HTTP responses).
+   */
+  this._powerSeized = false;
+}
+Response.prototype =
+{
+  // PUBLIC CONSTRUCTION API
+
+  //
+  // see nsIHttpResponse.bodyOutputStream
+  //
+  get bodyOutputStream()
+  {
+    if (this._finished)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+    if (!this._bodyOutputStream)
+    {
+      var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
+                          null);
+      this._bodyOutputStream = pipe.outputStream;
+      this._bodyInputStream = pipe.inputStream;
+      if (this._processAsync || this._powerSeized)
+        this._startAsyncProcessor();
+    }
+
+    return this._bodyOutputStream;
+  },
+
+  //
+  // see nsIHttpResponse.write
+  //
+  write: function(data)
+  {
+    if (this._finished)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+    var dataAsString = String(data);
+    this.bodyOutputStream.write(dataAsString, dataAsString.length);
+  },
+
+  //
+  // see nsIHttpResponse.setStatusLine
+  //
+  setStatusLine: function(httpVersion, code, description)
+  {
+    if (!this._headers || this._finished || this._powerSeized)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    this._ensureAlive();
+
+    if (!(code >= 0 && code < 1000))
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    try
+    {
+      var httpVer;
+      // avoid version construction for the most common cases
+      if (!httpVersion || httpVersion == "1.1")
+        httpVer = nsHttpVersion.HTTP_1_1;
+      else if (httpVersion == "1.0")
+        httpVer = nsHttpVersion.HTTP_1_0;
+      else
+        httpVer = new nsHttpVersion(httpVersion);
+    }
+    catch (e)
+    {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    // Reason-Phrase = *<TEXT, excluding CR, LF>
+    // TEXT          = <any OCTET except CTLs, but including LWS>
+    //
+    // XXX this ends up disallowing octets which aren't Unicode, I think -- not
+    //     much to do if description is IDL'd as string
+    if (!description)
+      description = "";
+    for (var i = 0; i < description.length; i++)
+      if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t")
+        throw Cr.NS_ERROR_INVALID_ARG;
+
+    // set the values only after validation to preserve atomicity
+    this._httpDescription = description;
+    this._httpCode = code;
+    this._httpVersion = httpVer;
+  },
+
+  //
+  // see nsIHttpResponse.setHeader
+  //
+  setHeader: function(name, value, merge)
+  {
+    if (!this._headers || this._finished || this._powerSeized)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    this._ensureAlive();
+
+    this._headers.setHeader(name, value, merge);
+  },
+
+  //
+  // see nsIHttpResponse.processAsync
+  //
+  processAsync: function()
+  {
+    if (this._finished)
+      throw Cr.NS_ERROR_UNEXPECTED;
+    if (this._powerSeized)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    if (this._processAsync)
+      return;
+    this._ensureAlive();
+
+    dumpn("*** processing connection " + this._connection.number + " async");
+    this._processAsync = true;
+
+    /*
+     * Either the bodyOutputStream getter or this method is responsible for
+     * starting the asynchronous processor and catching writes of data to the
+     * response body of async responses as they happen, for the purpose of
+     * forwarding those writes to the actual connection's output stream.
+     * If bodyOutputStream is accessed first, calling this method will create
+     * the processor (when it first is clear that body data is to be written
+     * immediately, not buffered).  If this method is called first, accessing
+     * bodyOutputStream will create the processor.  If only this method is
+     * called, we'll write nothing, neither headers nor the nonexistent body,
+     * until finish() is called.  Since that delay is easily avoided by simply
+     * getting bodyOutputStream or calling write(""), we don't worry about it.
+     */
+    if (this._bodyOutputStream && !this._asyncCopier)
+      this._startAsyncProcessor();
+  },
+
+  //
+  // see nsIHttpResponse.seizePower
+  //
+  seizePower: function()
+  {
+    if (this._processAsync)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    if (this._finished)
+      throw Cr.NS_ERROR_UNEXPECTED;
+    if (this._powerSeized)
+      return;
+    this._ensureAlive();
+
+    dumpn("*** forcefully seizing power over connection " +
+          this._connection.number + "...");
+
+    // Purge any already-written data without sending it.  We could as easily
+    // swap out the streams entirely, but that makes it possible to acquire and
+    // unknowingly use a stale reference, so we require there only be one of
+    // each stream ever for any response to avoid this complication.
+    if (this._asyncCopier)
+      this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
+    this._asyncCopier = null;
+    if (this._bodyOutputStream)
+    {
+      var input = new BinaryInputStream(this._bodyInputStream);
+      var avail;
+      while ((avail = input.available()) > 0)
+        input.readByteArray(avail);
+    }
+
+    this._powerSeized = true;
+    if (this._bodyOutputStream)
+      this._startAsyncProcessor();
+  },
+
+  //
+  // see nsIHttpResponse.finish
+  //
+  finish: function()
+  {
+    if (!this._processAsync && !this._powerSeized)
+      throw Cr.NS_ERROR_UNEXPECTED;
+    if (this._finished)
+      return;
+
+    dumpn("*** finishing connection " + this._connection.number);
+    this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
+    if (this._bodyOutputStream)
+      this._bodyOutputStream.close();
+    this._finished = true;
+  },
+
+
+  // NSISUPPORTS
+
+  //
+  // see nsISupports.QueryInterface
+  //
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // POST-CONSTRUCTION API (not exposed externally)
+
+  /**
+   * The HTTP version number of this, as a string (e.g. "1.1").
+   */
+  get httpVersion()
+  {
+    this._ensureAlive();
+    return this._httpVersion.toString();
+  },
+
+  /**
+   * The HTTP status code of this response, as a string of three characters per
+   * RFC 2616.
+   */
+  get httpCode()
+  {
+    this._ensureAlive();
+
+    var codeString = (this._httpCode < 10 ? "0" : "") +
+                     (this._httpCode < 100 ? "0" : "") +
+                     this._httpCode;
+    return codeString;
+  },
+
+  /**
+   * The description of the HTTP status code of this response, or "" if none is
+   * set.
+   */
+  get httpDescription()
+  {
+    this._ensureAlive();
+
+    return this._httpDescription;
+  },
+
+  /**
+   * The headers in this response, as an nsHttpHeaders object.
+   */
+  get headers()
+  {
+    this._ensureAlive();
+
+    return this._headers;
+  },
+
+  //
+  // see nsHttpHeaders.getHeader
+  //
+  getHeader: function(name)
+  {
+    this._ensureAlive();
+
+    return this._headers.getHeader(name);
+  },
+
+  /**
+   * Determines whether this response may be abandoned in favor of a newly
+   * constructed response.  A response may be abandoned only if it is not being
+   * sent asynchronously and if raw control over it has not been taken from the
+   * server.
+   *
+   * @returns boolean
+   *   true iff no data has been written to the network
+   */
+  partiallySent: function()
+  {
+    dumpn("*** partiallySent()");
+    return this._processAsync || this._powerSeized;
+  },
+
+  /**
+   * If necessary, kicks off the remaining request processing needed to be done
+   * after a request handler performs its initial work upon this response.
+   */
+  complete: function()
+  {
+    dumpn("*** complete()");
+    if (this._processAsync || this._powerSeized)
+    {
+      NS_ASSERT(this._processAsync ^ this._powerSeized,
+                "can't both send async and relinquish power");
+      return;
+    }
+
+    NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
+
+    this._startAsyncProcessor();
+
+    // Now make sure we finish processing this request!
+    if (this._bodyOutputStream)
+      this._bodyOutputStream.close();
+  },
+
+  /**
+   * Abruptly ends processing of this response, usually due to an error in an
+   * incoming request but potentially due to a bad error handler.  Since we
+   * cannot handle the error in the usual way (giving an HTTP error page in
+   * response) because data may already have been sent (or because the response
+   * might be expected to have been generated asynchronously or completely from
+   * scratch by the handler), we stop processing this response and abruptly
+   * close the connection.
+   *
+   * @param e : Error
+   *   the exception which precipitated this abort, or null if no such exception
+   *   was generated
+   */
+  abort: function(e)
+  {
+    dumpn("*** abort(<" + e + ">)");
+
+    // This response will be ended by the processor if one was created.
+    var copier = this._asyncCopier;
+    if (copier)
+    {
+      // We dispatch asynchronously here so that any pending writes of data to
+      // the connection will be deterministically written.  This makes it easier
+      // to specify exact behavior, and it makes observable behavior more
+      // predictable for clients.  Note that the correctness of this depends on
+      // callbacks in response to _waitToReadData in WriteThroughCopier
+      // happening asynchronously with respect to the actual writing of data to
+      // bodyOutputStream, as they currently do; if they happened synchronously,
+      // an event which ran before this one could write more data to the
+      // response body before we get around to canceling the copier.  We have
+      // tests for this in test_seizepower.js, however, and I can't think of a
+      // way to handle both cases without removing bodyOutputStream access and
+      // moving its effective write(data, length) method onto Response, which
+      // would be slower and require more code than this anyway.
+      gThreadManager.currentThread.dispatch({
+        run: function()
+        {
+          dumpn("*** canceling copy asynchronously...");
+          copier.cancel(Cr.NS_ERROR_UNEXPECTED);
+        }
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+    }
+    else
+    {
+      this.end();
+    }
+  },
+
+  /**
+   * Closes this response's network connection, marks the response as finished,
+   * and notifies the server handler that the request is done being processed.
+   */
+  end: function()
+  {
+    NS_ASSERT(!this._ended, "ending this response twice?!?!");
+
+    this._connection.close();
+    if (this._bodyOutputStream)
+      this._bodyOutputStream.close();
+
+    this._finished = true;
+    this._ended = true;
+  },
+
+  // PRIVATE IMPLEMENTATION
+
+  /**
+   * Sends the status line and headers of this response if they haven't been
+   * sent and initiates the process of copying data written to this response's
+   * body to the network.
+   */
+  _startAsyncProcessor: function()
+  {
+    dumpn("*** _startAsyncProcessor()");
+
+    // Handle cases where we're being called a second time.  The former case
+    // happens when this is triggered both by complete() and by processAsync(),
+    // while the latter happens when processAsync() in conjunction with sent
+    // data causes abort() to be called.
+    if (this._asyncCopier || this._ended)
+    {
+      dumpn("*** ignoring second call to _startAsyncProcessor");
+      return;
+    }
+
+    // Send headers if they haven't been sent already and should be sent, then
+    // asynchronously continue to send the body.
+    if (this._headers && !this._powerSeized)
+    {
+      this._sendHeaders();
+      return;
+    }
+
+    this._headers = null;
+    this._sendBody();
+  },
+
+  /**
+   * Signals that all modifications to the response status line and headers are
+   * complete and then sends that data over the network to the client.  Once
+   * this method completes, a different response to the request that resulted
+   * in this response cannot be sent -- the only possible action in case of
+   * error is to abort the response and close the connection.
+   */
+  _sendHeaders: function()
+  {
+    dumpn("*** _sendHeaders()");
+
+    NS_ASSERT(this._headers);
+    NS_ASSERT(!this._powerSeized);
+
+    // request-line
+    var statusLine = "HTTP/" + this.httpVersion + " " +
+                     this.httpCode + " " +
+                     this.httpDescription + "\r\n";
+
+    // header post-processing
+
+    var headers = this._headers;
+    headers.setHeader("Connection", "close", false);
+    headers.setHeader("Server", "httpd.js", false);
+    if (!headers.hasHeader("Date"))
+      headers.setHeader("Date", toDateString(Date.now()), false);
+
+    // Any response not being processed asynchronously must have an associated
+    // Content-Length header for reasons of backwards compatibility with the
+    // initial server, which fully buffered every response before sending it.
+    // Beyond that, however, it's good to do this anyway because otherwise it's
+    // impossible to test behaviors that depend on the presence or absence of a
+    // Content-Length header.
+    if (!this._processAsync)
+    {
+      dumpn("*** non-async response, set Content-Length");
+
+      var bodyStream = this._bodyInputStream;
+      var avail = bodyStream ? bodyStream.available() : 0;
+
+      // XXX assumes stream will always report the full amount of data available
+      headers.setHeader("Content-Length", "" + avail, false);
+    }
+
+
+    // construct and send response
+    dumpn("*** header post-processing completed, sending response head...");
+
+    // request-line
+    var preambleData = [statusLine];
+
+    // headers
+    var headEnum = headers.enumerator;
+    while (headEnum.hasMoreElements())
+    {
+      var fieldName = headEnum.getNext()
+                              .QueryInterface(Ci.nsISupportsString)
+                              .data;
+      var values = headers.getHeaderValues(fieldName);
+      for (var i = 0, sz = values.length; i < sz; i++)
+        preambleData.push(fieldName + ": " + values[i] + "\r\n");
+    }
+
+    // end request-line/headers
+    preambleData.push("\r\n");
+
+    var preamble = preambleData.join("");
+
+    var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
+    responseHeadPipe.outputStream.write(preamble, preamble.length);
+
+    var response = this;
+    var copyObserver =
+      {
+        onStartRequest: function(request, cx)
+        {
+          dumpn("*** preamble copying started");
+        },
+
+        onStopRequest: function(request, cx, statusCode)
+        {
+          dumpn("*** preamble copying complete " +
+                "[status=0x" + statusCode.toString(16) + "]");
+
+          if (!Components.isSuccessCode(statusCode))
+          {
+            dumpn("!!! header copying problems: non-success statusCode, " +
+                  "ending response");
+
+            response.end();
+          }
+          else
+          {
+            response._sendBody();
+          }
+        },
+
+        QueryInterface: function(aIID)
+        {
+          if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
+            return this;
+
+          throw Cr.NS_ERROR_NO_INTERFACE;
+        }
+      };
+
+    var headerCopier = this._asyncCopier =
+      new WriteThroughCopier(responseHeadPipe.inputStream,
+                             this._connection.output,
+                             copyObserver, null);
+
+    responseHeadPipe.outputStream.close();
+
+    // Forbid setting any more headers or modifying the request line.
+    this._headers = null;
+  },
+
+  /**
+   * Asynchronously writes the body of the response (or the entire response, if
+   * seizePower() has been called) to the network.
+   */
+  _sendBody: function()
+  {
+    dumpn("*** _sendBody");
+
+    NS_ASSERT(!this._headers, "still have headers around but sending body?");
+
+    // If no body data was written, we're done
+    if (!this._bodyInputStream)
+    {
+      dumpn("*** empty body, response finished");
+      this.end();
+      return;
+    }
+
+    var response = this;
+    var copyObserver =
+      {
+        onStartRequest: function(request, context)
+        {
+          dumpn("*** onStartRequest");
+        },
+
+        onStopRequest: function(request, cx, statusCode)
+        {
+          dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
+
+          if (statusCode === Cr.NS_BINDING_ABORTED)
+          {
+            dumpn("*** terminating copy observer without ending the response");
+          }
+          else
+          {
+            if (!Components.isSuccessCode(statusCode))
+              dumpn("*** WARNING: non-success statusCode in onStopRequest");
+
+            response.end();
+          }
+        },
+
+        QueryInterface: function(aIID)
+        {
+          if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
+            return this;
+
+          throw Cr.NS_ERROR_NO_INTERFACE;
+        }
+      };
+
+    dumpn("*** starting async copier of body data...");
+    this._asyncCopier =
+      new WriteThroughCopier(this._bodyInputStream, this._connection.output,
+                            copyObserver, null);
+  },
+
+  /** Ensures that this hasn't been ended. */
+  _ensureAlive: function()
+  {
+    NS_ASSERT(!this._ended, "not handling response lifetime correctly");
+  }
+};
+
+/**
+ * Size of the segments in the buffer used in storing response data and writing
+ * it to the socket.
+ */
+Response.SEGMENT_SIZE = 8192;
+
+/** Serves double duty in WriteThroughCopier implementation. */
+function notImplemented()
+{
+  throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/** Returns true iff the given exception represents stream closure. */
+function streamClosed(e)
+{
+  return e === Cr.NS_BASE_STREAM_CLOSED ||
+         (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED);
+}
+
+/** Returns true iff the given exception represents a blocked stream. */
+function wouldBlock(e)
+{
+  return e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
+         (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK);
+}
+
+/**
+ * Copies data from source to sink as it becomes available, when that data can
+ * be written to sink without blocking.
+ *
+ * @param source : nsIAsyncInputStream
+ *   the stream from which data is to be read
+ * @param sink : nsIAsyncOutputStream
+ *   the stream to which data is to be copied
+ * @param observer : nsIRequestObserver
+ *   an observer which will be notified when the copy starts and finishes
+ * @param context : nsISupports
+ *   context passed to observer when notified of start/stop
+ * @throws NS_ERROR_NULL_POINTER
+ *   if source, sink, or observer are null
+ */
+function WriteThroughCopier(source, sink, observer, context)
+{
+  if (!source || !sink || !observer)
+    throw Cr.NS_ERROR_NULL_POINTER;
+
+  /** Stream from which data is being read. */
+  this._source = source;
+
+  /** Stream to which data is being written. */
+  this._sink = sink;
+
+  /** Observer watching this copy. */
+  this._observer = observer;
+
+  /** Context for the observer watching this. */
+  this._context = context;
+
+  /**
+   * True iff this is currently being canceled (cancel has been called, the
+   * callback may not yet have been made).
+   */
+  this._canceled = false;
+
+  /**
+   * False until all data has been read from input and written to output, at
+   * which point this copy is completed and cancel() is asynchronously called.
+   */
+  this._completed = false;
+
+  /** Required by nsIRequest, meaningless. */
+  this.loadFlags = 0;
+  /** Required by nsIRequest, meaningless. */
+  this.loadGroup = null;
+  /** Required by nsIRequest, meaningless. */
+  this.name = "response-body-copy";
+
+  /** Status of this request. */
+  this.status = Cr.NS_OK;
+
+  /** Arrays of byte strings waiting to be written to output. */
+  this._pendingData = [];
+
+  // start copying
+  try
+  {
+    observer.onStartRequest(this, context);
+    this._waitToReadData();
+    this._waitForSinkClosure();
+  }
+  catch (e)
+  {
+    dumpn("!!! error starting copy: " + e +
+          ("lineNumber" in e ? ", line " + e.lineNumber : ""));
+    dumpn(e.stack);
+    this.cancel(Cr.NS_ERROR_UNEXPECTED);
+  }
+}
+WriteThroughCopier.prototype =
+{
+  /* nsISupports implementation */
+
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIInputStreamCallback) ||
+        iid.equals(Ci.nsIOutputStreamCallback) ||
+        iid.equals(Ci.nsIRequest) ||
+        iid.equals(Ci.nsISupports))
+    {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // NSIINPUTSTREAMCALLBACK
+
+  /**
+   * Receives a more-data-in-input notification and writes the corresponding
+   * data to the output.
+   *
+   * @param input : nsIAsyncInputStream
+   *   the input stream on whose data we have been waiting
+   */
+  onInputStreamReady: function(input)
+  {
+    if (this._source === null)
+      return;
+
+    dumpn("*** onInputStreamReady");
+
+    //
+    // Ordinarily we'll read a non-zero amount of data from input, queue it up
+    // to be written and then wait for further callbacks.  The complications in
+    // this method are the cases where we deviate from that behavior when errors
+    // occur or when copying is drawing to a finish.
+    //
+    // The edge cases when reading data are:
+    //
+    //   Zero data is read
+    //     If zero data was read, we're at the end of available data, so we can
+    //     should stop reading and move on to writing out what we have (or, if
+    //     we've already done that, onto notifying of completion).
+    //   A stream-closed exception is thrown
+    //     This is effectively a less kind version of zero data being read; the
+    //     only difference is that we notify of completion with that result
+    //     rather than with NS_OK.
+    //   Some other exception is thrown
+    //     This is the least kind result.  We don't know what happened, so we
+    //     act as though the stream closed except that we notify of completion
+    //     with the result NS_ERROR_UNEXPECTED.
+    //
+
+    var bytesWanted = 0, bytesConsumed = -1;
+    try
+    {
+      input = new BinaryInputStream(input);
+
+      bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
+      dumpn("*** input wanted: " + bytesWanted);
+
+      if (bytesWanted > 0)
+      {
+        var data = input.readByteArray(bytesWanted);
+        bytesConsumed = data.length;
+        this._pendingData.push(String.fromCharCode.apply(String, data));
+      }
+
+      dumpn("*** " + bytesConsumed + " bytes read");
+
+      // Handle the zero-data edge case in the same place as all other edge
+      // cases are handled.
+      if (bytesWanted === 0)
+        throw Cr.NS_BASE_STREAM_CLOSED;
+    }
+    catch (e)
+    {
+      if (streamClosed(e))
+      {
+        dumpn("*** input stream closed");
+        e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
+      }
+      else
+      {
+        dumpn("!!! unexpected error reading from input, canceling: " + e);
+        e = Cr.NS_ERROR_UNEXPECTED;
+      }
+
+      this._doneReadingSource(e);
+      return;
+    }
+
+    var pendingData = this._pendingData;
+
+    NS_ASSERT(bytesConsumed > 0);
+    NS_ASSERT(pendingData.length > 0, "no pending data somehow?");
+    NS_ASSERT(pendingData[pendingData.length - 1].length > 0,
+              "buffered zero bytes of data?");
+
+    NS_ASSERT(this._source !== null);
+
+    // Reading has gone great, and we've gotten data to write now.  What if we
+    // don't have a place to write that data, because output went away just
+    // before this read?  Drop everything on the floor, including new data, and
+    // cancel at this point.
+    if (this._sink === null)
+    {
+      pendingData.length = 0;
+      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
+      return;
+    }
+
+    // Okay, we've read the data, and we know we have a place to write it.  We
+    // need to queue up the data to be written, but *only* if none is queued
+    // already -- if data's already queued, the code that actually writes the
+    // data will make sure to wait on unconsumed pending data.
+    try
+    {
+      if (pendingData.length === 1)
+        this._waitToWriteData();
+    }
+    catch (e)
+    {
+      dumpn("!!! error waiting to write data just read, swallowing and " +
+            "writing only what we already have: " + e);
+      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+      return;
+    }
+
+    // Whee!  We successfully read some data, and it's successfully queued up to
+    // be written.  All that remains now is to wait for more data to read.
+    try
+    {
+      this._waitToReadData();
+    }
+    catch (e)
+    {
+      dumpn("!!! error waiting to read more data: " + e);
+      this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
+    }
+  },
+
+
+  // NSIOUTPUTSTREAMCALLBACK
+
+  /**
+   * Callback when data may be written to the output stream without blocking, or
+   * when the output stream has been closed.
+   *
+   * @param output : nsIAsyncOutputStream
+   *   the output stream on whose writability we've been waiting, also known as
+   *   this._sink
+   */
+  onOutputStreamReady: function(output)
+  {
+    if (this._sink === null)
+      return;
+
+    dumpn("*** onOutputStreamReady");
+
+    var pendingData = this._pendingData;
+    if (pendingData.length === 0)
+    {
+      // There's no pending data to write.  The only way this can happen is if
+      // we're waiting on the output stream's closure, so we can respond to a
+      // copying failure as quickly as possible (rather than waiting for data to
+      // be available to read and then fail to be copied).  Therefore, we must
+      // be done now -- don't bother to attempt to write anything and wrap
+      // things up.
+      dumpn("!!! output stream closed prematurely, ending copy");
+
+      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+      return;
+    }
+
+
+    NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?");
+
+    //
+    // Write out the first pending quantum of data.  The possible errors here
+    // are:
+    //
+    //   The write might fail because we can't write that much data
+    //     Okay, we've written what we can now, so re-queue what's left and
+    //     finish writing it out later.
+    //   The write failed because the stream was closed
+    //     Discard pending data that we can no longer write, stop reading, and
+    //     signal that copying finished.
+    //   Some other error occurred.
+    //     Same as if the stream were closed, but notify with the status
+    //     NS_ERROR_UNEXPECTED so the observer knows something was wonky.
+    //
+
+    try
+    {
+      var quantum = pendingData[0];
+
+      // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
+      //     undefined behavior!  We're only using this because writeByteArray
+      //     is unusably broken for asynchronous output streams; see bug 532834
+      //     for details.
+      var bytesWritten = output.write(quantum, quantum.length);
+      if (bytesWritten === quantum.length)
+        pendingData.shift();
+      else
+        pendingData[0] = quantum.substring(bytesWritten);
+
+      dumpn("*** wrote " + bytesWritten + " bytes of data");
+    }
+    catch (e)
+    {
+      if (wouldBlock(e))
+      {
+        NS_ASSERT(pendingData.length > 0,
+                  "stream-blocking exception with no data to write?");
+        NS_ASSERT(pendingData[0].length > 0,
+                  "stream-blocking exception with empty quantum?");
+        this._waitToWriteData();
+        return;
+      }
+
+      if (streamClosed(e))
+        dumpn("!!! output stream prematurely closed, signaling error...");
+      else
+        dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
+
+      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+      return;
+    }
+
+    // The day is ours!  Quantum written, now let's see if we have more data
+    // still to write.
+    try
+    {
+      if (pendingData.length > 0)
+      {
+        this._waitToWriteData();
+        return;
+      }
+    }
+    catch (e)
+    {
+      dumpn("!!! unexpected error waiting to write pending data: " + e);
+      this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
+      return;
+    }
+
+    // Okay, we have no more pending data to write -- but might we get more in
+    // the future?
+    if (this._source !== null)
+    {
+      /*
+       * If we might, then wait for the output stream to be closed.  (We wait
+       * only for closure because we have no data to write -- and if we waited
+       * for a specific amount of data, we would get repeatedly notified for no
+       * reason if over time the output stream permitted more and more data to
+       * be written to it without blocking.)
+       */
+       this._waitForSinkClosure();
+    }
+    else
+    {
+      /*
+       * On the other hand, if we can't have more data because the input
+       * stream's gone away, then it's time to notify of copy completion.
+       * Victory!
+       */
+      this._sink = null;
+      this._cancelOrDispatchCancelCallback(Cr.NS_OK);
+    }
+  },
+
+
+  // NSIREQUEST
+
+  /** Returns true if the cancel observer hasn't been notified yet. */
+  isPending: function()
+  {
+    return !this._completed;
+  },
+
+  /** Not implemented, don't use! */
+  suspend: notImplemented,
+  /** Not implemented, don't use! */
+  resume: notImplemented,
+
+  /**
+   * Cancels data reading from input, asynchronously writes out any pending
+   * data, and causes the observer to be notified with the given error code when
+   * all writing has finished.
+   *
+   * @param status : nsresult
+   *   the status to pass to the observer when data copying has been canceled
+   */
+  cancel: function(status)
+  {
+    dumpn("*** cancel(" + status.toString(16) + ")");
+
+    if (this._canceled)
+    {
+      dumpn("*** suppressing a late cancel");
+      return;
+    }
+
+    this._canceled = true;
+    this.status = status;
+
+    // We could be in the middle of absolutely anything at this point.  Both
+    // input and output might still be around, we might have pending data to
+    // write, and in general we know nothing about the state of the world.  We
+    // therefore must assume everything's in progress and take everything to its
+    // final steady state (or so far as it can go before we need to finish
+    // writing out remaining data).
+
+    this._doneReadingSource(status);
+  },
+
+
+  // PRIVATE IMPLEMENTATION
+
+  /**
+   * Stop reading input if we haven't already done so, passing e as the status
+   * when closing the stream, and kick off a copy-completion notice if no more
+   * data remains to be written.
+   *
+   * @param e : nsresult
+   *   the status to be used when closing the input stream
+   */
+  _doneReadingSource: function(e)
+  {
+    dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
+
+    this._finishSource(e);
+    if (this._pendingData.length === 0)
+      this._sink = null;
+    else
+      NS_ASSERT(this._sink !== null, "null output?");
+
+    // If we've written out all data read up to this point, then it's time to
+    // signal completion.
+    if (this._sink === null)
+    {
+      NS_ASSERT(this._pendingData.length === 0, "pending data still?");
+      this._cancelOrDispatchCancelCallback(e);
+    }
+  },
+
+  /**
+   * Stop writing output if we haven't already done so, discard any data that
+   * remained to be sent, close off input if it wasn't already closed, and kick
+   * off a copy-completion notice.
+   *
+   * @param e : nsresult
+   *   the status to be used when closing input if it wasn't already closed
+   */
+  _doneWritingToSink: function(e)
+  {
+    dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
+
+    this._pendingData.length = 0;
+    this._sink = null;
+    this._doneReadingSource(e);
+  },
+
+  /**
+   * Completes processing of this copy: either by canceling the copy if it
+   * hasn't already been canceled using the provided status, or by dispatching
+   * the cancel callback event (with the originally provided status, of course)
+   * if it already has been canceled.
+   *
+   * @param status : nsresult
+   *   the status code to use to cancel this, if this hasn't already been
+   *   canceled
+   */
+  _cancelOrDispatchCancelCallback: function(status)
+  {
+    dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
+
+    NS_ASSERT(this._source === null, "should have finished input");
+    NS_ASSERT(this._sink === null, "should have finished output");
+    NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
+
+    if (!this._canceled)
+    {
+      this.cancel(status);
+      return;
+    }
+
+    var self = this;
+    var event =
+      {
+        run: function()
+        {
+          dumpn("*** onStopRequest async callback");
+
+          self._completed = true;
+          try
+          {
+            self._observer.onStopRequest(self, self._context, self.status);
+          }
+          catch (e)
+          {
+            NS_ASSERT(false,
+                      "how are we throwing an exception here?  we control " +
+                      "all the callers!  " + e);
+          }
+        }
+      };
+
+    gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Kicks off another wait for more data to be available from the input stream.
+   */
+  _waitToReadData: function()
+  {
+    dumpn("*** _waitToReadData");
+    this._source.asyncWait(this, 0, Response.SEGMENT_SIZE,
+                           gThreadManager.mainThread);
+  },
+
+  /**
+   * Kicks off another wait until data can be written to the output stream.
+   */
+  _waitToWriteData: function()
+  {
+    dumpn("*** _waitToWriteData");
+
+    var pendingData = this._pendingData;
+    NS_ASSERT(pendingData.length > 0, "no pending data to write?");
+    NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?");
+
+    this._sink.asyncWait(this, 0, pendingData[0].length,
+                         gThreadManager.mainThread);
+  },
+
+  /**
+   * Kicks off a wait for the sink to which data is being copied to be closed.
+   * We wait for stream closure when we don't have any data to be copied, rather
+   * than waiting to write a specific amount of data.  We can't wait to write
+   * data because the sink might be infinitely writable, and if no data appears
+   * in the source for a long time we might have to spin quite a bit waiting to
+   * write, waiting to write again, &c.  Waiting on stream closure instead means
+   * we'll get just one notification if the sink dies.  Note that when data
+   * starts arriving from the sink we'll resume waiting for data to be written,
+   * dropping this closure-only callback entirely.
+   */
+  _waitForSinkClosure: function()
+  {
+    dumpn("*** _waitForSinkClosure");
+
+    this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0,
+                         gThreadManager.mainThread);
+  },
+
+  /**
+   * Closes input with the given status, if it hasn't already been closed;
+   * otherwise a no-op.
+   *
+   * @param status : nsresult
+   *   status code use to close the source stream if necessary
+   */
+  _finishSource: function(status)
+  {
+    dumpn("*** _finishSource(" + status.toString(16) + ")");
+
+    if (this._source !== null)
+    {
+      this._source.closeWithStatus(status);
+      this._source = null;
+    }
+  }
+};
+
+
+/**
+ * A container for utility functions used with HTTP headers.
+ */
+const headerUtils =
+{
+  /**
+   * Normalizes fieldName (by converting it to lowercase) and ensures it is a
+   * valid header field name (although not necessarily one specified in RFC
+   * 2616).
+   *
+   * @throws NS_ERROR_INVALID_ARG
+   *   if fieldName does not match the field-name production in RFC 2616
+   * @returns string
+   *   fieldName converted to lowercase if it is a valid header, for characters
+   *   where case conversion is possible
+   */
+  normalizeFieldName: function(fieldName)
+  {
+    if (fieldName == "")
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    for (var i = 0, sz = fieldName.length; i < sz; i++)
+    {
+      if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)])
+      {
+        dumpn(fieldName + " is not a valid header field name!");
+        throw Cr.NS_ERROR_INVALID_ARG;
+      }
+    }
+
+    return fieldName.toLowerCase();
+  },
+
+  /**
+   * Ensures that fieldValue is a valid header field value (although not
+   * necessarily as specified in RFC 2616 if the corresponding field name is
+   * part of the HTTP protocol), normalizes the value if it is, and
+   * returns the normalized value.
+   *
+   * @param fieldValue : string
+   *   a value to be normalized as an HTTP header field value
+   * @throws NS_ERROR_INVALID_ARG
+   *   if fieldValue does not match the field-value production in RFC 2616
+   * @returns string
+   *   fieldValue as a normalized HTTP header field value
+   */
+  normalizeFieldValue: function(fieldValue)
+  {
+    // field-value    = *( field-content | LWS )
+    // field-content  = <the OCTETs making up the field-value
+    //                  and consisting of either *TEXT or combinations
+    //                  of token, separators, and quoted-string>
+    // TEXT           = <any OCTET except CTLs,
+    //                  but including LWS>
+    // LWS            = [CRLF] 1*( SP | HT )
+    //
+    // quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
+    // qdtext         = <any TEXT except <">>
+    // quoted-pair    = "\" CHAR
+    // CHAR           = <any US-ASCII character (octets 0 - 127)>
+
+    // Any LWS that occurs between field-content MAY be replaced with a single
+    // SP before interpreting the field value or forwarding the message
+    // downstream (section 4.2); we replace 1*LWS with a single SP
+    var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
+
+    // remove leading/trailing LWS (which has been converted to SP)
+    val = val.replace(/^ +/, "").replace(/ +$/, "");
+
+    // that should have taken care of all CTLs, so val should contain no CTLs
+    for (var i = 0, len = val.length; i < len; i++)
+      if (isCTL(val.charCodeAt(i)))
+        throw Cr.NS_ERROR_INVALID_ARG;
+
+    // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
+    //     normalize, however, so this can be construed as a tightening of the
+    //     spec and not entirely as a bug
+    return val;
+  }
+};
+
+
+
+/**
+ * Converts the given string into a string which is safe for use in an HTML
+ * context.
+ *
+ * @param str : string
+ *   the string to make HTML-safe
+ * @returns string
+ *   an HTML-safe version of str
+ */
+function htmlEscape(str)
+{
+  // this is naive, but it'll work
+  var s = "";
+  for (var i = 0; i < str.length; i++)
+    s += "&#" + str.charCodeAt(i) + ";";
+  return s;
+}
+
+
+/**
+ * Constructs an object representing an HTTP version (see section 3.1).
+ *
+ * @param versionString
+ *   a string of the form "#.#", where # is an non-negative decimal integer with
+ *   or without leading zeros
+ * @throws
+ *   if versionString does not specify a valid HTTP version number
+ */
+function nsHttpVersion(versionString)
+{
+  var matches = /^(\d+)\.(\d+)$/.exec(versionString);
+  if (!matches)
+    throw "Not a valid HTTP version!";
+
+  /** The major version number of this, as a number. */
+  this.major = parseInt(matches[1], 10);
+
+  /** The minor version number of this, as a number. */
+  this.minor = parseInt(matches[2], 10);
+
+  if (isNaN(this.major) || isNaN(this.minor) ||
+      this.major < 0    || this.minor < 0)
+    throw "Not a valid HTTP version!";
+}
+nsHttpVersion.prototype =
+{
+  /**
+   * Returns the standard string representation of the HTTP version represented
+   * by this (e.g., "1.1").
+   */
+  toString: function ()
+  {
+    return this.major + "." + this.minor;
+  },
+
+  /**
+   * Returns true if this represents the same HTTP version as otherVersion,
+   * false otherwise.
+   *
+   * @param otherVersion : nsHttpVersion
+   *   the version to compare against this
+   */
+  equals: function (otherVersion)
+  {
+    return this.major == otherVersion.major &&
+           this.minor == otherVersion.minor;
+  },
+
+  /** True if this >= otherVersion, false otherwise. */
+  atLeast: function(otherVersion)
+  {
+    return this.major > otherVersion.major ||
+           (this.major == otherVersion.major &&
+            this.minor >= otherVersion.minor);
+  }
+};
+
+nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
+nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
+
+
+/**
+ * An object which stores HTTP headers for a request or response.
+ *
+ * Note that since headers are case-insensitive, this object converts headers to
+ * lowercase before storing them.  This allows the getHeader and hasHeader
+ * methods to work correctly for any case of a header, but it means that the
+ * values returned by .enumerator may not be equal case-sensitively to the
+ * values passed to setHeader when adding headers to this.
+ */
+function nsHttpHeaders()
+{
+  /**
+   * A hash of headers, with header field names as the keys and header field
+   * values as the values.  Header field names are case-insensitive, but upon
+   * insertion here they are converted to lowercase.  Header field values are
+   * normalized upon insertion to contain no leading or trailing whitespace.
+   *
+   * Note also that per RFC 2616, section 4.2, two headers with the same name in
+   * a message may be treated as one header with the same field name and a field
+   * value consisting of the separate field values joined together with a "," in
+   * their original order.  This hash stores multiple headers with the same name
+   * in this manner.
+   */
+  this._headers = {};
+}
+nsHttpHeaders.prototype =
+{
+  /**
+   * Sets the header represented by name and value in this.
+   *
+   * @param name : string
+   *   the header name
+   * @param value : string
+   *   the header value
+   * @throws NS_ERROR_INVALID_ARG
+   *   if name or value is not a valid header component
+   */
+  setHeader: function(fieldName, fieldValue, merge)
+  {
+    var name = headerUtils.normalizeFieldName(fieldName);
+    var value = headerUtils.normalizeFieldValue(fieldValue);
+
+    // The following three headers are stored as arrays because their real-world
+    // syntax prevents joining individual headers into a single header using 
+    // ",".  See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
+    if (merge && name in this._headers)
+    {
+      if (name === "www-authenticate" ||
+          name === "proxy-authenticate" ||
+          name === "set-cookie") 
+      {
+        this._headers[name].push(value);
+      }
+      else 
+      {
+        this._headers[name][0] += "," + value;
+        NS_ASSERT(this._headers[name].length === 1,
+            "how'd a non-special header have multiple values?")
+      }
+    }
+    else
+    {
+      this._headers[name] = [value];
+    }
+  },
+
+  /**
+   * Returns the value for the header specified by this.
+   *
+   * @throws NS_ERROR_INVALID_ARG
+   *   if fieldName does not constitute a valid header field name
+   * @throws NS_ERROR_NOT_AVAILABLE
+   *   if the given header does not exist in this
+   * @returns string
+   *   the field value for the given header, possibly with non-semantic changes
+   *   (i.e., leading/trailing whitespace stripped, whitespace runs replaced
+   *   with spaces, etc.) at the option of the implementation; multiple 
+   *   instances of the header will be combined with a comma, except for 
+   *   the three headers noted in the description of getHeaderValues
+   */
+  getHeader: function(fieldName)
+  {
+    return this.getHeaderValues(fieldName).join("\n");
+  },
+
+  /**
+   * Returns the value for the header specified by fieldName as an array.
+   *
+   * @throws NS_ERROR_INVALID_ARG
+   *   if fieldName does not constitute a valid header field name
+   * @throws NS_ERROR_NOT_AVAILABLE
+   *   if the given header does not exist in this
+   * @returns [string]
+   *   an array of all the header values in this for the given
+   *   header name.  Header values will generally be collapsed
+   *   into a single header by joining all header values together
+   *   with commas, but certain headers (Proxy-Authenticate,
+   *   WWW-Authenticate, and Set-Cookie) violate the HTTP spec
+   *   and cannot be collapsed in this manner.  For these headers
+   *   only, the returned array may contain multiple elements if
+   *   that header has been added more than once.
+   */
+  getHeaderValues: function(fieldName)
+  {
+    var name = headerUtils.normalizeFieldName(fieldName);
+
+    if (name in this._headers)
+      return this._headers[name];
+    else
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  /**
+   * Returns true if a header with the given field name exists in this, false
+   * otherwise.
+   *
+   * @param fieldName : string
+   *   the field name whose existence is to be determined in this
+   * @throws NS_ERROR_INVALID_ARG
+   *   if fieldName does not constitute a valid header field name
+   * @returns boolean
+   *   true if the header's present, false otherwise
+   */
+  hasHeader: function(fieldName)
+  {
+    var name = headerUtils.normalizeFieldName(fieldName);
+    return (name in this._headers);
+  },
+
+  /**
+   * Returns a new enumerator over the field names of the headers in this, as
+   * nsISupportsStrings.  The names returned will be in lowercase, regardless of
+   * how they were input using setHeader (header names are case-insensitive per
+   * RFC 2616).
+   */
+  get enumerator()
+  {
+    var headers = [];
+    for (var i in this._headers)
+    {
+      var supports = new SupportsString();
+      supports.data = i;
+      headers.push(supports);
+    }
+
+    return new nsSimpleEnumerator(headers);
+  }
+};
+
+
+/**
+ * Constructs an nsISimpleEnumerator for the given array of items.
+ *
+ * @param items : Array
+ *   the items, which must all implement nsISupports
+ */
+function nsSimpleEnumerator(items)
+{
+  this._items = items;
+  this._nextIndex = 0;
+}
+nsSimpleEnumerator.prototype =
+{
+  hasMoreElements: function()
+  {
+    return this._nextIndex < this._items.length;
+  },
+  getNext: function()
+  {
+    if (!this.hasMoreElements())
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+    return this._items[this._nextIndex++];
+  },
+  QueryInterface: function(aIID)
+  {
+    if (Ci.nsISimpleEnumerator.equals(aIID) ||
+        Ci.nsISupports.equals(aIID))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+
+/**
+ * A representation of the data in an HTTP request.
+ *
+ * @param port : uint
+ *   the port on which the server receiving this request runs
+ */
+function Request(port)
+{
+  /** Method of this request, e.g. GET or POST. */
+  this._method = "";
+
+  /** Path of the requested resource; empty paths are converted to '/'. */
+  this._path = "";
+
+  /** Query string, if any, associated with this request (not including '?'). */
+  this._queryString = "";
+
+  /** Scheme of requested resource, usually http, always lowercase. */
+  this._scheme = "http";
+
+  /** Hostname on which the requested resource resides. */
+  this._host = undefined;
+
+  /** Port number over which the request was received. */
+  this._port = port;
+
+  var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
+
+  /** Stream from which data in this request's body may be read. */
+  this._bodyInputStream = bodyPipe.inputStream;
+
+  /** Stream to which data in this request's body is written. */
+  this._bodyOutputStream = bodyPipe.outputStream;
+
+  /**
+   * The headers in this request.
+   */
+  this._headers = new nsHttpHeaders();
+
+  /**
+   * For the addition of ad-hoc properties and new functionality without having
+   * to change nsIHttpRequest every time; currently lazily created, as its only
+   * use is in directory listings.
+   */
+  this._bag = null;
+}
+Request.prototype =
+{
+  // SERVER METADATA
+
+  //
+  // see nsIHttpRequest.scheme
+  //
+  get scheme()
+  {
+    return this._scheme;
+  },
+
+  //
+  // see nsIHttpRequest.host
+  //
+  get host()
+  {
+    return this._host;
+  },
+
+  //
+  // see nsIHttpRequest.port
+  //
+  get port()
+  {
+    return this._port;
+  },
+
+  // REQUEST LINE
+
+  //
+  // see nsIHttpRequest.method
+  //
+  get method()
+  {
+    return this._method;
+  },
+
+  //
+  // see nsIHttpRequest.httpVersion
+  //
+  get httpVersion()
+  {
+    return this._httpVersion.toString();
+  },
+
+  //
+  // see nsIHttpRequest.path
+  //
+  get path()
+  {
+    return this._path;
+  },
+
+  //
+  // see nsIHttpRequest.queryString
+  //
+  get queryString()
+  {
+    return this._queryString;
+  },
+
+  // HEADERS
+
+  //
+  // see nsIHttpRequest.getHeader
+  //
+  getHeader: function(name)
+  {
+    return this._headers.getHeader(name);
+  },
+
+  //
+  // see nsIHttpRequest.hasHeader
+  //
+  hasHeader: function(name)
+  {
+    return this._headers.hasHeader(name);
+  },
+
+  //
+  // see nsIHttpRequest.headers
+  //
+  get headers()
+  {
+    return this._headers.enumerator;
+  },
+
+  //
+  // see nsIPropertyBag.enumerator
+  //
+  get enumerator()
+  {
+    this._ensurePropertyBag();
+    return this._bag.enumerator;
+  },
+
+  //
+  // see nsIHttpRequest.headers
+  //
+  get bodyInputStream()
+  {
+    return this._bodyInputStream;
+  },
+
+  //
+  // see nsIPropertyBag.getProperty
+  //
+  getProperty: function(name) 
+  {
+    this._ensurePropertyBag();
+    return this._bag.getProperty(name);
+  },
+
+
+  // NSISUPPORTS
+
+  //
+  // see nsISupports.QueryInterface
+  //
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+
+  // PRIVATE IMPLEMENTATION
+  
+  /** Ensures a property bag has been created for ad-hoc behaviors. */
+  _ensurePropertyBag: function()
+  {
+    if (!this._bag)
+      this._bag = new WritablePropertyBag();
+  }
+};
+
+
+// XPCOM trappings
+if (XPCOMUtils.generateNSGetFactory)
+  var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
+else
+  var NSGetModule = XPCOMUtils.generateNSGetModule([nsHttpServer]);
+
+/**
+ * Creates a new HTTP server listening for loopback traffic on the given port,
+ * starts it, and runs the server until the server processes a shutdown request,
+ * spinning an event loop so that events posted by the server's socket are
+ * processed.
+ *
+ * This method is primarily intended for use in running this script from within
+ * xpcshell and running a functional HTTP server without having to deal with
+ * non-essential details.
+ *
+ * Note that running multiple servers using variants of this method probably
+ * doesn't work, simply due to how the internal event loop is spun and stopped.
+ *
+ * @note
+ *   This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code);
+ *   you should use this server as a component in Mozilla 1.8.
+ * @param port
+ *   the port on which the server will run, or -1 if there exists no preference
+ *   for a specific port; note that attempting to use some values for this
+ *   parameter (particularly those below 1024) may cause this method to throw or
+ *   may result in the server being prematurely shut down
+ * @param basePath
+ *   a local directory from which requests will be served (i.e., if this is
+ *   "/home/jwalden/" then a request to /index.html will load
+ *   /home/jwalden/index.html); if this is omitted, only the default URLs in
+ *   this server implementation will be functional
+ */
+function server(port, basePath)
+{
+  if (basePath)
+  {
+    var lp = Cc["@mozilla.org/file/local;1"]
+               .createInstance(Ci.nsILocalFile);
+    lp.initWithPath(basePath);
+  }
+