Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 13 Mar 2015 14:06:57 +0100
changeset 233540 3d6e70f93eaf2382a80904dffb52640ec4d47018
parent 233539 67faef0b6c20a787e22e9085fd48dc1d54ced13e (current diff)
parent 233496 9dbb2d41bb2ca889db2631f1f0993cd0c1e6165c (diff)
child 233541 106237ea5c7e4ef2817c7781b1a4dc6ca49253b2
push id28417
push userryanvm@gmail.com
push dateFri, 13 Mar 2015 19:52:44 +0000
treeherdermozilla-central@977add19414a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone39.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
Merge mozilla-central to mozilla-inbound
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -54,17 +54,21 @@ let RemoteDebugger = {
       return DebuggerServer.AuthenticationResult.DENY;
     }
     this._listen();
 
     this._promptingForAllow = new Promise(resolve => {
       this._handleAllowResult = detail => {
         this._handleAllowResult = null;
         this._promptingForAllow = null;
-        if (detail.value) {
+        // Newer Gaia supplies |authResult|, which is one of the
+        // AuthenticationResult values.
+        if (detail.authResult) {
+          resolve(detail.authResult);
+        } else if (detail.value) {
           resolve(DebuggerServer.AuthenticationResult.ALLOW);
         } else {
           resolve(DebuggerServer.AuthenticationResult.DENY);
         }
       };
 
       shell.sendChromeEvent({
         type: "remote-debugger-prompt",
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="97c3d9b8b87774ca7a08c89145e95b55652459ef"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="97c3d9b8b87774ca7a08c89145e95b55652459ef"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "eabe35cf054d47087b37c1ca7db8143717fbd7f3", 
+        "git_revision": "10dcd335d588997fc12845e9197de89228664f95", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "9139624122bb1343e99250a9643bca07f57579f7", 
+    "revision": "f378c0a3ef97b44ddb7f71a7a65faddc391c4e0e", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="eabe35cf054d47087b37c1ca7db8143717fbd7f3"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="10dcd335d588997fc12845e9197de89228664f95"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="c82a532ee1f14b9733214022b1e2d55a0b030be8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="1a1e326a7804a62de8f61fab705f7e1974cad818"/>
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1493,17 +1493,17 @@
 
             // Change the "remote" attribute.
             let parent = aBrowser.parentNode;
             let permanentKey = aBrowser.permanentKey;
             parent.removeChild(aBrowser);
             aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
             // Tearing down the browser gives a new permanentKey but we want to
             // keep the old one. Re-set it explicitly after unbinding from DOM.
-            aBrowser.permanentKey = permanentKey;
+            aBrowser._permanentKey = permanentKey;
             parent.appendChild(aBrowser);
 
             // Restore the progress listener.
             aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
 
             if (aShouldBeRemote) {
               tab.setAttribute("remote", "true");
               // Switching the browser to be remote will connect to a new child
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -405,16 +405,17 @@ support-files =
 [browser_subframe_favicons_not_used.js]
 [browser_syncui.js]
 skip-if = e10s # Bug 1137087 - browser_tabopen_reflows.js fails if this was previously run with e10s
 [browser_tabDrop.js]
 skip-if = buildapp == 'mulet' || e10s
 [browser_tabMatchesInAwesomebar.js]
 [browser_tabMatchesInAwesomebar_perwindowpb.js]
 skip-if = e10s || os == 'linux' # Bug 1093373, bug 1104755
+[browser_tab_detach_restore.js]
 [browser_tab_drag_drop_perwindow.js]
 skip-if = buildapp == 'mulet'
 [browser_tab_dragdrop.js]
 skip-if = buildapp == 'mulet'
 [browser_tab_dragdrop2.js]
 skip-if = buildapp == 'mulet'
 [browser_tabbar_big_widgets.js]
 skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_tab_detach_restore.js
@@ -0,0 +1,31 @@
+"use strict";
+
+add_task(function*() {
+  let uri = "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+
+  // Clear out the closed windows set to start
+  while (SessionStore.getClosedWindowCount() > 0)
+    SessionStore.forgetClosedWindow(0);
+
+  let tab = gBrowser.addTab();
+  tab.linkedBrowser.loadURI(uri);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  let key = tab.linkedBrowser.permanentKey;
+  let win = gBrowser.replaceTabWithWindow(tab);
+  yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+
+  is(win.gBrowser.selectedBrowser.permanentKey, key, "Should have properly copied the permanentKey");
+  yield promiseWindowClosed(win);
+
+  is(SessionStore.getClosedWindowCount(), 1, "Should have restore data for the closed window");
+
+  win = SessionStore.undoCloseWindow(0);
+  yield BrowserTestUtils.waitForEvent(win, "load");
+  yield BrowserTestUtils.waitForEvent(win.gBrowser.tabs[0], "SSTabRestored");
+
+  is(win.gBrowser.tabs.length, 1, "Should have restored one tab");
+  is(win.gBrowser.selectedBrowser.currentURI.spec, uri, "Should have restored the right page");
+
+  yield promiseWindowClosed(win);
+});
--- a/browser/components/readinglist/ReadingList.jsm
+++ b/browser/components/readinglist/ReadingList.jsm
@@ -189,16 +189,18 @@ ReadingListImpl.prototype = {
    *         is updated.  Rejected with an Error on error.
    */
   addItem: Task.async(function* (obj) {
     obj = stripNonItemProperties(obj);
     yield this._store.addItem(obj);
     this._invalidateIterators();
     let item = this._itemFromObject(obj);
     this._callListeners("onItemAdded", item);
+    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+    mm.broadcastAsyncMessage("Reader:Added", item);
     return item;
   }),
 
   /**
    * Updates the properties of an item that belongs to the list.
    *
    * The passed-in item may have as few or as many properties that you want to
    * set; only the properties that are present are updated.  The item must have
@@ -229,20 +231,34 @@ ReadingListImpl.prototype = {
    *         Error on error.
    */
   deleteItem: Task.async(function* (item) {
     this._ensureItemBelongsToList(item);
     yield this._store.deleteItemByURL(item.url);
     item.list = null;
     this._itemsByURL.delete(item.url);
     this._invalidateIterators();
+    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+    mm.broadcastAsyncMessage("Reader:Removed", item);
     this._callListeners("onItemDeleted", item);
   }),
 
   /**
+   * Find any item that matches a given URL - either the item's URL, or its
+   * resolved URL.
+   *
+   * @param {String/nsIURI} uri - URI to match against. This will be normalized.
+   */
+  getItemForURL: Task.async(function* (uri) {
+    let url = this._normalizeURI(uri).spec;
+    let [item] = yield this.iterator({url: url}, {resolvedURL: url}).items(1);
+    return item;
+  }),
+
+  /**
    * Adds a listener that will be notified when the list changes.  Listeners
    * are objects with the following optional methods:
    *
    *   onItemAdded(item)
    *   onItemUpdated(item)
    *   onItemDeleted(item)
    *
    * @param listener A listener object.
@@ -284,16 +300,32 @@ ReadingListImpl.prototype = {
   // A Set containing nsIWeakReferences that refer to valid iterators produced
   // by the list.
   _iterators: null,
 
   // A Set containing listener objects.
   _listeners: null,
 
   /**
+   * Normalize a URI, stripping away extraneous parts we don't want to store
+   * or compare against.
+   *
+   * @param {nsIURI/String} uri - URI to normalize.
+   * @returns {nsIURI} Cloned and normalized version of the input URI.
+   */
+  _normalizeURI(uri) {
+    if (typeof uri == "string") {
+      uri = Services.io.newURI(uri, "", null);
+    }
+    uri = uri.cloneIgnoringRef();
+    uri.userPass = "";
+    return uri;
+  },
+
+  /**
    * Returns the ReadingListItem represented by the given simple object.  If
    * the item doesn't exist yet, it's created first.
    *
    * @param obj A simple object with item properties.
    * @return The ReadingListItem.
    */
   _itemFromObject(obj) {
     let itemWeakRef = this._itemsByURL.get(obj.url);
@@ -344,26 +376,40 @@ ReadingListImpl.prototype = {
 
   _ensureItemBelongsToList(item) {
     if (item.list != this) {
       throw new Error("The item does not belong to this list");
     }
   },
 };
 
+let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
+
 /**
  * An item in a reading list.
  *
  * Each item belongs to a list, and it's an error to use an item with a
  * ReadingList that the item doesn't belong to.
  *
  * @param props The properties of the item, as few or many as you want.
  */
 function ReadingListItem(props={}) {
   this._properties = {};
+
+  // |this._unserializable| works around a problem when sending one of these
+  // items via a message manager. If |this.list| is set, the item can't be
+  // transferred directly, so .toJSON is implicitly called and the object
+  // returned via that is sent. However, once the item is deleted and |this.list|
+  // is null, the item *can* be directly serialized - so the message handler
+  // sees the "raw" object - ie, it sees "_properties" etc.
+  // We work around this problem by *always* having an unserializable property
+  // on the object - this way the implicit .toJSON call is always made, even
+  // when |this.list| is null.
+  this._unserializable = _unserializable;
+
   this.setProperties(props, false);
 }
 
 ReadingListItem.prototype = {
 
   /**
    * Item's unique ID.
    * @type string
@@ -825,17 +871,17 @@ function hash(str) {
                createInstance(Ci.nsICryptoHash);
   hasher.init(Ci.nsICryptoHash.MD5);
   let stream = Cc["@mozilla.org/io/string-input-stream;1"].
                createInstance(Ci.nsIStringInputStream);
   stream.data = str;
   hasher.updateFromStream(stream, -1);
   let binaryStr = hasher.finish(false);
   let hexStr =
-    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in hash)].
+    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in binaryStr)].
     join("");
   return hexStr;
 }
 
 function clone(obj) {
   return Cu.cloneInto(obj, {}, { cloneFunctions: false });
 }
 
--- a/browser/components/readinglist/test/xpcshell/test_ReadingList.js
+++ b/browser/components/readinglist/test/xpcshell/test_ReadingList.js
@@ -692,12 +692,12 @@ function hash(str) {
                createInstance(Ci.nsICryptoHash);
   hasher.init(Ci.nsICryptoHash.MD5);
   let stream = Cc["@mozilla.org/io/string-input-stream;1"].
                createInstance(Ci.nsIStringInputStream);
   stream.data = str;
   hasher.updateFromStream(stream, -1);
   let binaryStr = hasher.finish(false);
   let hexStr =
-    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in hash)].
+    [("0" + binaryStr.charCodeAt(i).toString(16)).slice(-2) for (i in binaryStr)].
     join("");
   return hexStr;
 }
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-01.js
@@ -74,25 +74,25 @@ function testSetBreakpoint() {
   return deferred.promise;
 }
 
 function testSetBreakpointBlankLine() {
   let deferred = promise.defer();
   let sourceForm = getSourceForm(gSources, COFFEE_URL);
 
   let source = gDebugger.gThreadClient.source(sourceForm);
-  source.setBreakpoint({ line: 3 }, aResponse => {
+  source.setBreakpoint({ line: 7 }, aResponse => {
     ok(!aResponse.error,
       "Should be able to set a breakpoint in a coffee source file on a blank line.");
     ok(aResponse.actualLocation,
       "Because 3 is empty, we should have an actualLocation.");
     is(aResponse.actualLocation.source.url, COFFEE_URL,
       "actualLocation.actor should be source mapped to the coffee file.");
-    is(aResponse.actualLocation.line, 2,
-      "actualLocation.line should be source mapped back to 2.");
+    is(aResponse.actualLocation.line, 8,
+      "actualLocation.line should be source mapped back to 8.");
 
     deferred.resolve();
   });
 
   return deferred.promise;
 }
 
 function testHitBreakpoint() {
@@ -142,17 +142,17 @@ function testStepping() {
       is(aPacket.why.type, "resumeLimit",
         "and the reason we should be paused is because we hit the resume limit.");
 
       // Check that we stopped at the right place, by making sure that the
       // environment is in the state that we expect.
       is(aPacket.frame.environment.bindings.variables.start.value, 0,
          "'start' is 0.");
       is(aPacket.frame.environment.bindings.variables.stop.value, 5,
-         "'stop' hasn't been assigned to yet.");
+         "'stop' is 5.");
       is(aPacket.frame.environment.bindings.variables.pivot.value.type, "undefined",
          "'pivot' hasn't been assigned to yet.");
 
       waitForCaretUpdated(gPanel, 6).then(deferred.resolve);
     });
   });
 
   return deferred.promise;
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-03.js
@@ -40,21 +40,21 @@ function checkInitialSource() {
     "not the whitespace stripped minified version.");
 }
 
 function testSetBreakpoint() {
   let deferred = promise.defer();
   let sourceForm = getSourceForm(gSources, JS_URL);
   let source = gDebugger.gThreadClient.source(sourceForm);
 
-  source.setBreakpoint({ line: 30, column: 21 }, aResponse => {
+  source.setBreakpoint({ line: 30 }, aResponse => {
     ok(!aResponse.error,
       "Should be able to set a breakpoint in a js file.");
     ok(!aResponse.actualLocation,
-      "Should be able to set a breakpoint on line 30 and column 10.");
+      "Should be able to set a breakpoint on line 30.");
 
     gDebugger.gClient.addOneTimeListener("resumed", () => {
       waitForCaretAndScopes(gPanel, 30).then(() => {
         // Make sure that we have the right stack frames.
         is(gFrames.itemCount, 9,
           "Should have nine frames.");
         is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".min.js"), -1,
           "First frame should not be a minified JS frame.");
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -347,30 +347,32 @@ PerformanceFront.prototype = {
     return memoryEndTime;
   }),
 
   /**
    * At regular intervals, pull allocations from the memory actor, and forward
    * them to consumers.
    */
   _pullAllocationSites: Task.async(function *() {
+    let isDetached = (yield this._request("memory", "getState")) !== "attached";
+    if (isDetached) {
+      return;
+    }
+
     let memoryData = yield this._request("memory", "getAllocations");
-    let isStillAttached = yield this._request("memory", "getState") == "attached";
 
     this.emit("allocations", {
       sites: memoryData.allocations,
       timestamps: memoryData.allocationsTimestamps,
       frames: memoryData.frames,
       counts: memoryData.counts
     });
 
-    if (isStillAttached) {
-      let delay = DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT;
-      this._sitesPullTimeout = setTimeout(this._pullAllocationSites, delay);
-    }
+    let delay = DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT;
+    this._sitesPullTimeout = setTimeout(this._pullAllocationSites, delay);
   }),
 
   /**
    * Overrides the options sent to the built-in profiler module when activating,
    * such as the maximum entries count, the sampling interval etc.
    *
    * Used in tests and for older backend implementations.
    */
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -193,44 +193,24 @@
               </vbox>
 
               <hbox id="js-flamegraph-view" flex="1">
               </hbox>
 
               <vbox id="memory-calltree-view" flex="1">
                 <hbox class="call-tree-headers-container">
                   <label class="plain call-tree-header"
-                         type="duration"
-                         crop="end"
-                         value="&profilerUI.table.totalDuration2;"/>
-                  <label class="plain call-tree-header"
-                         type="percentage"
-                         crop="end"
-                         value="&profilerUI.table.totalPercentage;"/>
-                  <label class="plain call-tree-header"
                          type="allocations"
                          crop="end"
                          value="&profilerUI.table.totalAlloc;"/>
                   <label class="plain call-tree-header"
-                         type="self-duration"
-                         crop="end"
-                         value="&profilerUI.table.selfDuration2;"/>
-                  <label class="plain call-tree-header"
-                         type="self-percentage"
-                         crop="end"
-                         value="&profilerUI.table.selfPercentage;"/>
-                  <label class="plain call-tree-header"
                          type="self-allocations"
                          crop="end"
                          value="&profilerUI.table.selfAlloc;"/>
                   <label class="plain call-tree-header"
-                         type="samples"
-                         crop="end"
-                         value="&profilerUI.table.samples;"/>
-                  <label class="plain call-tree-header"
                          type="function"
                          crop="end"
                          value="&profilerUI.table.function;"/>
                 </hbox>
                 <vbox class="call-tree-cells-container" flex="1"/>
               </vbox>
 
               <hbox id="memory-flamegraph-view" flex="1">
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -11,16 +11,18 @@ support-files =
 [browser_perf-aaa-run-first-leaktest.js]
 [browser_perf-allocations-to-samples.js]
 [browser_perf-compatibility-01.js]
 [browser_perf-compatibility-02.js]
 [browser_perf-compatibility-03.js]
 [browser_perf-compatibility-04.js]
 [browser_perf-clear-01.js]
 [browser_perf-clear-02.js]
+[browser_perf-columns-js-calltree.js]
+[browser_perf-columns-memory-calltree.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render.js]
 [browser_perf-details-flamegraph-render.js]
 [browser_perf-details-memory-calltree-render.js]
 [browser_perf-details-memory-flamegraph-render.js]
 [browser_perf-details-waterfall-render.js]
 [browser_perf-details-01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-columns-js-calltree.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the js call tree view renders the correct columns.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+  // Enable platform data to show the `busyWait` function in the tree.
+  Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
+
+  yield DetailsView.selectView("js-calltree");
+  ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
+
+  yield startRecording(panel);
+  yield busyWait(1000);
+
+  let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  testCells($, $$, {
+    "duration": true,
+    "percentage": true,
+    "allocations": false,
+    "self-duration": true,
+    "self-percentage": true,
+    "self-allocations": false,
+    "samples": true,
+    "function": true
+  });
+
+  yield teardown(panel);
+  finish();
+}
+
+function testCells($, $$, visibleCells) {
+  for (let cell in visibleCells) {
+    if (visibleCells[cell]) {
+      ok($(`.call-tree-cell[type=${cell}]`),
+        `At least one ${cell} column was visible in the tree.`);
+    } else {
+      ok(!$(`.call-tree-cell[type=${cell}]`),
+        `No ${cell} columns were visible in the tree.`);
+    }
+  }
+
+  is($$(".call-tree-cell", $(".call-tree-item")).length,
+    Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+    "The correct number of cells were found in the tree.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-columns-memory-calltree.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the memory call tree view renders the correct columns.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, $$, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+  // Enable memory to test.
+  Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+  yield DetailsView.selectView("memory-calltree");
+  ok(DetailsView.isViewSelected(MemoryCallTreeView), "The call tree is now selected.");
+
+  yield startRecording(panel);
+  yield busyWait(1000);
+
+  let rendered = once(MemoryCallTreeView, EVENTS.MEMORY_CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  testCells($, $$, {
+    "duration": false,
+    "percentage": false,
+    "allocations": true,
+    "self-duration": false,
+    "self-percentage": false,
+    "self-allocations": true,
+    "samples": false,
+    "function": true
+  });
+
+  yield teardown(panel);
+  finish();
+}
+
+function testCells($, $$, visibleCells) {
+  for (let cell in visibleCells) {
+    if (visibleCells[cell]) {
+      ok($(`.call-tree-cell[type=${cell}]`),
+        `At least one ${cell} column was visible in the tree.`);
+    } else {
+      ok(!$(`.call-tree-cell[type=${cell}]`),
+        `No ${cell} columns were visible in the tree.`);
+    }
+  }
+
+  is($$(".call-tree-cell", $(".call-tree-item")).length,
+    Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+    "The correct number of cells were found in the tree.");
+}
--- a/browser/devtools/performance/test/browser_perf-front.js
+++ b/browser/devtools/performance/test/browser_perf-front.js
@@ -5,50 +5,65 @@
  * Test basic functionality of PerformanceFront, emitting start and endtime values
  */
 
 let WAIT_TIME = 1000;
 
 function spawnTest () {
   let { target, front } = yield initBackend(SIMPLE_URL);
 
+  let count = 0;
+  let counter = () => count++;
+
   let {
     profilerStartTime,
     timelineStartTime,
     memoryStartTime
   } = yield front.startRecording({
     withAllocations: true
   });
 
   ok(typeof profilerStartTime === "number",
     "The front.startRecording() emits a profiler start time.");
   ok(typeof timelineStartTime === "number",
     "The front.startRecording() emits a timeline start time.");
   ok(typeof memoryStartTime === "number",
     "The front.startRecording() emits a memory start time.");
 
-  yield busyWait(WAIT_TIME);
+  // Record allocation events to ensure it's called more than once
+  // so we know it's polling
+  front.on("allocations", counter);
+
+  yield Promise.all([
+    busyWait(WAIT_TIME),
+    waitUntil(() => count > 1)
+  ]);
 
   let {
     profilerEndTime,
     timelineEndTime,
     memoryEndTime
   } = yield front.stopRecording({
     withAllocations: true
   });
 
+  front.off("allocations", counter);
+
   ok(typeof profilerEndTime === "number",
     "The front.stopRecording() emits a profiler end time.");
   ok(typeof timelineEndTime === "number",
     "The front.stopRecording() emits a timeline end time.");
   ok(typeof memoryEndTime === "number",
     "The front.stopRecording() emits a memory end time.");
 
   ok(profilerEndTime > profilerStartTime,
     "The profilerEndTime is after profilerStartTime.");
   ok(timelineEndTime > timelineStartTime,
     "The timelineEndTime is after timelineStartTime.");
   ok(memoryEndTime > memoryStartTime,
     "The memoryEndTime is after memoryStartTime.");
 
+  is((yield front._request("memory", "getState")), "detached",
+    "memory actor is detached when stopping recording with allocations");
+
   yield removeTab(target.tab);
   finish();
 }
--- a/browser/devtools/performance/test/browser_profiler_tree-view-01.js
+++ b/browser/devtools/performance/test/browser_profiler_tree-view-01.js
@@ -17,59 +17,49 @@ function test() {
   treeRoot.autoExpandDepth = 0;
   treeRoot.attachTo(container);
 
   is(container.childNodes.length, 1,
     "The container node should have one child available.");
   is(container.childNodes[0].className, "call-tree-item",
     "The root node in the tree has the correct class name.");
 
-  is(container.childNodes[0].childNodes.length, 8,
+  is(container.childNodes[0].childNodes.length, 6,
     "The root node in the tree has the correct number of children.");
-  is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 8,
-    "The root node in the tree has only 'call-tree-cell' children.");
+  is(container.childNodes[0].querySelectorAll(".call-tree-cell").length, 6,
+    "The root node in the tree has only 6 'call-tree-cell' children.");
 
   is(container.childNodes[0].childNodes[0].getAttribute("type"), "duration",
     "The root node in the tree has a duration cell.");
   is(container.childNodes[0].childNodes[0].getAttribute("value"), "15 ms",
     "The root node in the tree has the correct duration cell value.");
 
   is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
     "The root node in the tree has a percentage cell.");
   is(container.childNodes[0].childNodes[1].getAttribute("value"), "100%",
     "The root node in the tree has the correct percentage cell value.");
 
-  is(container.childNodes[0].childNodes[2].getAttribute("type"), "allocations",
+  is(container.childNodes[0].childNodes[2].getAttribute("type"), "self-duration",
     "The root node in the tree has a self-duration cell.");
-  is(container.childNodes[0].childNodes[2].getAttribute("value"), "0",
-    "The root node in the tree has the correct self-duration cell value.");
-
-  is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-duration",
-    "The root node in the tree has a self-duration cell.");
-  is(container.childNodes[0].childNodes[3].getAttribute("value"), "0 ms",
+  is(container.childNodes[0].childNodes[2].getAttribute("value"), "0 ms",
     "The root node in the tree has the correct self-duration cell value.");
 
-  is(container.childNodes[0].childNodes[4].getAttribute("type"), "self-percentage",
+  is(container.childNodes[0].childNodes[3].getAttribute("type"), "self-percentage",
     "The root node in the tree has a self-percentage cell.");
-  is(container.childNodes[0].childNodes[4].getAttribute("value"), "0%",
+  is(container.childNodes[0].childNodes[3].getAttribute("value"), "0%",
     "The root node in the tree has the correct self-percentage cell value.");
 
-  is(container.childNodes[0].childNodes[5].getAttribute("type"), "self-allocations",
-    "The root node in the tree has a self-percentage cell.");
-  is(container.childNodes[0].childNodes[5].getAttribute("value"), "0",
-    "The root node in the tree has the correct self-percentage cell value.");
-
-  is(container.childNodes[0].childNodes[6].getAttribute("type"), "samples",
+  is(container.childNodes[0].childNodes[4].getAttribute("type"), "samples",
     "The root node in the tree has an samples cell.");
-  is(container.childNodes[0].childNodes[6].getAttribute("value"), "4",
+  is(container.childNodes[0].childNodes[4].getAttribute("value"), "4",
     "The root node in the tree has the correct samples cell value.");
 
-  is(container.childNodes[0].childNodes[7].getAttribute("type"), "function",
+  is(container.childNodes[0].childNodes[5].getAttribute("type"), "function",
     "The root node in the tree has a function cell.");
-  is(container.childNodes[0].childNodes[7].style.MozMarginStart, "0px",
+  is(container.childNodes[0].childNodes[5].style.MozMarginStart, "0px",
     "The root node in the tree has the correct indentation.");
 
   finish();
 }
 
 let gSamples = [{
   time: 5,
   frames: [
--- a/browser/devtools/performance/test/browser_profiler_tree-view-04.js
+++ b/browser/devtools/performance/test/browser_profiler_tree-view-04.js
@@ -39,36 +39,32 @@ function test() {
     "The .A.B.D node's 'category' attribute is correct.");
   is(D.target.getAttribute("tooltiptext"), "D (http://foo/bar/baz:78)",
     "The .A.B.D node's 'tooltiptext' attribute is correct.");
   ok(!A.target.querySelector(".call-tree-zoom").hidden,
     "The .A.B.D node's zoom button cell should not be hidden.");
   ok(!A.target.querySelector(".call-tree-category").hidden,
     "The .A.B.D node's category label cell should not be hidden.");
 
-  is(D.target.childNodes.length, 8,
+  is(D.target.childNodes.length, 6,
     "The number of columns displayed for tree items is correct.");
   is(D.target.childNodes[0].getAttribute("type"), "duration",
     "The first column displayed for tree items is correct.");
   is(D.target.childNodes[1].getAttribute("type"), "percentage",
     "The third column displayed for tree items is correct.");
-  is(D.target.childNodes[2].getAttribute("type"), "allocations",
+  is(D.target.childNodes[2].getAttribute("type"), "self-duration",
     "The second column displayed for tree items is correct.");
-  is(D.target.childNodes[3].getAttribute("type"), "self-duration",
-    "The second column displayed for tree items is correct.");
-  is(D.target.childNodes[4].getAttribute("type"), "self-percentage",
+  is(D.target.childNodes[3].getAttribute("type"), "self-percentage",
     "The fourth column displayed for tree items is correct.");
-  is(D.target.childNodes[5].getAttribute("type"), "self-allocations",
-    "The fourth column displayed for tree items is correct.");
-  is(D.target.childNodes[6].getAttribute("type"), "samples",
+  is(D.target.childNodes[4].getAttribute("type"), "samples",
     "The fifth column displayed for tree items is correct.");
-  is(D.target.childNodes[7].getAttribute("type"), "function",
+  is(D.target.childNodes[5].getAttribute("type"), "function",
     "The sixth column displayed for tree items is correct.");
 
-  let functionCell = D.target.childNodes[7];
+  let functionCell = D.target.childNodes[5];
 
   is(functionCell.childNodes.length, 9,
     "The number of columns displayed for function cells is correct.");
   is(functionCell.childNodes[0].className, "arrow theme-twisty",
     "The first node displayed for function cells is correct.");
   is(functionCell.childNodes[1].className, "plain call-tree-name",
     "The second node displayed for function cells is correct.");
   is(functionCell.childNodes[2].className, "plain call-tree-url",
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -98,19 +98,16 @@ let JsCallTreeView = Heritage.extend(Det
     // Pipe "focus" events to the view, mostly for tests
     root.on("focus", () => this.emit("focus"));
 
     // Clear out other call trees.
     let container = $("#js-calltree-view > .call-tree-cells-container");
     container.innerHTML = "";
     root.attachTo(container);
 
-    // Profiler data does not contain memory allocations information.
-    root.toggleAllocations(false);
-
     // When platform data isn't shown, hide the cateogry labels, since they're
     // only available for C++ frames.
     let contentOnly = !PerformanceController.getOption("show-platform-data");
     root.toggleCategories(!contentOnly);
   },
 
   toString: () => "[object JsCallTreeView]"
 });
--- a/browser/devtools/performance/views/details-memory-call-tree.js
+++ b/browser/devtools/performance/views/details-memory-call-tree.js
@@ -84,16 +84,23 @@ let MemoryCallTreeView = Heritage.extend
       inverted: options.inverted,
       // Root nodes are hidden in inverted call trees.
       hidden: options.inverted,
       // Memory call trees should be sorted by allocations.
       sortingPredicate: (a, b) => a.frame.allocations < b.frame.allocations ? 1 : -1,
       // Call trees should only auto-expand when not inverted. Passing undefined
       // will default to the CALL_TREE_AUTO_EXPAND depth.
       autoExpandDepth: options.inverted ? 0 : undefined,
+      // Some cells like the time duration and cost percentage don't make sense
+      // for a memory allocations call tree.
+      visibleCells: {
+        allocations: true,
+        selfAllocations: true,
+        function: true
+      }
     });
 
     // Bind events.
     root.on("link", this._onLink);
 
     // Pipe "focus" events to the view, mostly for tests
     root.on("focus", () => this.emit("focus"));
 
--- a/browser/devtools/profiler/ui-profile.js
+++ b/browser/devtools/profiler/ui-profile.js
@@ -504,17 +504,16 @@ let ProfileView = {
     callTreeRoot.attachTo($(".call-tree-cells-container", panel));
 
     if (!options.skipCallTreeFocus) {
       callTreeRoot.focus();
     }
 
     let contentOnly = !Prefs.showPlatformData;
     callTreeRoot.toggleCategories(!contentOnly);
-    callTreeRoot.toggleAllocations(false);
 
     this._callTreeRootByPanel.set(panel, callTreeRoot);
   },
 
   /**
    * Shortcuts for accessing the recording info or widgets for a <panel>.
    * @param nsIDOMNode panel [optional]
    * @return object
--- a/browser/devtools/shared/profiler/tree-view.js
+++ b/browser/devtools/shared/profiler/tree-view.js
@@ -12,19 +12,30 @@ loader.lazyImporter(this, "Heritage",
   "resource:///modules/devtools/ViewHelpers.jsm");
 loader.lazyImporter(this, "AbstractTreeItem",
   "resource:///modules/devtools/AbstractTreeItem.jsm");
 
 const MILLISECOND_UNITS = L10N.getStr("table.ms");
 const PERCENTAGE_UNITS = L10N.getStr("table.percentage");
 const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
 const ZOOM_BUTTON_TOOLTIP = L10N.getStr("table.zoom.tooltiptext");
-const CALL_TREE_AUTO_EXPAND = 3; // depth
 const CALL_TREE_INDENTATION = 16; // px
+
 const DEFAULT_SORTING_PREDICATE = (a, b) => a.frame.samples < b.frame.samples ? 1 : -1;
+const DEFAULT_AUTO_EXPAND_DEPTH = 3; // depth
+const DEFAULT_VISIBLE_CELLS = {
+  duration: true,
+  percentage: true,
+  allocations: false,
+  selfDuration: true,
+  selfPercentage: true,
+  selfAllocations: false,
+  samples: true,
+  function: true
+};
 
 const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
 const sum = vals => vals.reduce((a, b) => a + b, 0);
 
 exports.CallView = CallView;
 
 /**
  * An item in a call tree view, which looks like this:
@@ -50,43 +61,50 @@ exports.CallView = CallView;
  * @param boolean hidden [optional]
  *        Whether this node should be hidden and not contribute to depth/level
  *        calculations. Defaults to false.
  * @param boolean inverted [optional]
  *        Whether the call tree has been inverted (bottom up, rather than
  *        top-down). Defaults to false.
  * @param function sortingPredicate [optional]
  *        The predicate used to sort the tree items when created. Defaults to
- *        the caller's sortingPredicate if a caller exists, otherwise defaults
+ *        the caller's `sortingPredicate` if a caller exists, otherwise defaults
  *        to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
  * @param number autoExpandDepth [optional]
  *        The depth to which the tree should automatically expand. Defualts to
  *        the caller's `autoExpandDepth` if a caller exists, otherwise defaults
- *        to CALL_TREE_AUTO_EXPAND.
+ *        to DEFAULT_AUTO_EXPAND_DEPTH.
+ * @param object visibleCells
+ *        An object specifying which cells are visible in the tree. Defaults to
+ *        the caller's `visibleCells` if a caller exists, otherwise defaults
+ *        to DEFAULT_VISIBLE_CELLS.
  */
-function CallView({ caller, frame, level, hidden, inverted, sortingPredicate, autoExpandDepth }) {
-  // Assume no indentation if this tree item's level is not specified.
-  level = level || 0;
-
-  // Don't increase indentation if this tree item is hidden.
-  if (hidden) {
-    level--;
-  }
-
-  AbstractTreeItem.call(this, { parent: caller, level });
+function CallView({
+  caller, frame, level, hidden, inverted,
+  sortingPredicate, autoExpandDepth, visibleCells
+}) {
+  AbstractTreeItem.call(this, {
+    parent: caller,
+    level: level|0 - (hidden ? 1 : 0)
+  });
 
   this.sortingPredicate = sortingPredicate != null
     ? sortingPredicate
     : caller ? caller.sortingPredicate
              : DEFAULT_SORTING_PREDICATE
 
   this.autoExpandDepth = autoExpandDepth != null
     ? autoExpandDepth
     : caller ? caller.autoExpandDepth
-             : CALL_TREE_AUTO_EXPAND;
+             : DEFAULT_AUTO_EXPAND_DEPTH;
+
+  this.visibleCells = visibleCells != null
+    ? visibleCells
+    : caller ? caller.visibleCells
+             : Object.create(DEFAULT_VISIBLE_CELLS);
 
   this.caller = caller;
   this.frame = frame;
   this.hidden = hidden;
   this.inverted = inverted;
 
   this._onUrlClick = this._onUrlClick.bind(this);
   this._onZoomClick = this._onZoomClick.bind(this);
@@ -105,69 +123,110 @@ CallView.prototype = Heritage.extend(Abs
     let frameInfo = this.frame.getInfo();
     let framePercentage = this._getPercentage(this.frame.samples);
 
     let selfPercentage;
     let selfDuration;
     let totalAllocations;
 
     if (!this._getChildCalls().length) {
-      selfPercentage = framePercentage;
-      selfDuration = this.frame.duration;
-      totalAllocations = this.frame.allocations;
+      if (this.visibleCells.selfPercentage) {
+        selfPercentage = framePercentage;
+      }
+      if (this.visibleCells.selfDuration) {
+        selfDuration = this.frame.duration;
+      }
+      if (this.visibleCells.allocations) {
+        totalAllocations = this.frame.allocations;
+      }
     } else {
-      let childrenPercentage = sum(
-        [this._getPercentage(c.samples) for (c of this._getChildCalls())]);
-      let childrenDuration = sum(
-        [c.duration for (c of this._getChildCalls())]);
-      let childrenAllocations = sum(
-        [c.allocations for (c of this._getChildCalls())]);
-
-      selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
-      selfDuration = this.frame.duration - childrenDuration;
-      totalAllocations = this.frame.allocations + childrenAllocations;
-
+      // Avoid performing costly computations if the respective columns
+      // won't be shown anyway.
+      if (this.visibleCells.selfPercentage) {
+        let childrenPercentage = sum([this._getPercentage(c.samples) for (c of this._getChildCalls())]);
+        selfPercentage = clamp(framePercentage - childrenPercentage, 0, 100);
+      }
+      if (this.visibleCells.selfDuration) {
+        let childrenDuration = sum([c.duration for (c of this._getChildCalls())]);
+        selfDuration = this.frame.duration - childrenDuration;
+      }
+      if (this.visibleCells.allocations) {
+        let childrenAllocations = sum([c.allocations for (c of this._getChildCalls())]);
+        totalAllocations = this.frame.allocations + childrenAllocations;
+      }
       if (this.inverted) {
         selfPercentage = framePercentage - selfPercentage;
         selfDuration = this.frame.duration - selfDuration;
       }
     }
 
-    let durationCell = this._createTimeCell(this.frame.duration);
-    let selfDurationCell = this._createTimeCell(selfDuration, true);
-    let percentageCell = this._createExecutionCell(framePercentage);
-    let selfPercentageCell = this._createExecutionCell(selfPercentage, true);
-    let allocationsCell = this._createAllocationsCell(totalAllocations);
-    let selfAllocationsCell = this._createAllocationsCell(this.frame.allocations, true);
-    let samplesCell = this._createSamplesCell(this.frame.samples);
-    let functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
+    if (this.visibleCells.duration) {
+      var durationCell = this._createTimeCell(this.frame.duration);
+    }
+    if (this.visibleCells.selfDuration) {
+      var selfDurationCell = this._createTimeCell(selfDuration, true);
+    }
+    if (this.visibleCells.percentage) {
+      var percentageCell = this._createExecutionCell(framePercentage);
+    }
+    if (this.visibleCells.selfPercentage) {
+      var selfPercentageCell = this._createExecutionCell(selfPercentage, true);
+    }
+    if (this.visibleCells.allocations) {
+      var allocationsCell = this._createAllocationsCell(totalAllocations);
+    }
+    if (this.visibleCells.selfAllocations) {
+      var selfAllocationsCell = this._createAllocationsCell(this.frame.allocations, true);
+    }
+    if (this.visibleCells.samples) {
+      var samplesCell = this._createSamplesCell(this.frame.samples);
+    }
+    if (this.visibleCells.function) {
+      var functionCell = this._createFunctionCell(arrowNode, frameInfo, this.level);
+    }
 
     let targetNode = document.createElement("hbox");
     targetNode.className = "call-tree-item";
     targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
     targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
     targetNode.setAttribute("tooltiptext", this.frame.location || "");
     if (this.hidden) {
       targetNode.style.display = "none";
     }
 
     let isRoot = frameInfo.nodeType == "Thread";
     if (isRoot) {
       functionCell.querySelector(".call-tree-zoom").hidden = true;
       functionCell.querySelector(".call-tree-category").hidden = true;
     }
 
-    targetNode.appendChild(durationCell);
-    targetNode.appendChild(percentageCell);
-    targetNode.appendChild(allocationsCell);
-    targetNode.appendChild(selfDurationCell);
-    targetNode.appendChild(selfPercentageCell);
-    targetNode.appendChild(selfAllocationsCell);
-    targetNode.appendChild(samplesCell);
-    targetNode.appendChild(functionCell);
+    if (this.visibleCells.duration) {
+      targetNode.appendChild(durationCell);
+    }
+    if (this.visibleCells.percentage) {
+      targetNode.appendChild(percentageCell);
+    }
+    if (this.visibleCells.allocations) {
+      targetNode.appendChild(allocationsCell);
+    }
+    if (this.visibleCells.selfDuration) {
+      targetNode.appendChild(selfDurationCell);
+    }
+    if (this.visibleCells.selfPercentage) {
+      targetNode.appendChild(selfPercentageCell);
+    }
+    if (this.visibleCells.selfAllocations) {
+      targetNode.appendChild(selfAllocationsCell);
+    }
+    if (this.visibleCells.samples) {
+      targetNode.appendChild(samplesCell);
+    }
+    if (this.visibleCells.function) {
+      targetNode.appendChild(functionCell);
+    }
 
     return targetNode;
   },
 
   /**
    * Calculate what percentage of all samples the given number of samples is.
    */
   _getPercentage: function(samples) {
@@ -297,28 +356,16 @@ CallView.prototype = Heritage.extend(Abs
     if (hasDescendants == false) {
       arrowNode.setAttribute("invisible", "");
     }
 
     return cell;
   },
 
   /**
-   * Toggles the allocations information hidden or visible.
-   * @param boolean visible
-   */
-  toggleAllocations: function(visible) {
-    if (!visible) {
-      this.container.setAttribute("allocations-hidden", "");
-    } else {
-      this.container.removeAttribute("allocations-hidden");
-    }
-  },
-
-  /**
    * Toggles the category information hidden or visible.
    * @param boolean visible
    */
   toggleCategories: function(visible) {
     if (!visible) {
       this.container.setAttribute("categories-hidden", "");
     } else {
       this.container.removeAttribute("categories-hidden");
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -9,16 +9,17 @@ const { classes: Cc, interfaces: Ci, uti
 
 this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
 
 const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 let ReaderParent = {
 
   MESSAGES: [
     "Reader:AddToList",
     "Reader:ArticleGet",
@@ -37,38 +38,48 @@ let ReaderParent = {
     for (let msg of this.MESSAGES) {
       mm.addMessageListener(msg, this);
     }
   },
 
   receiveMessage: function(message) {
     switch (message.name) {
       case "Reader:AddToList":
-        // XXX: To implement.
+        ReadingList.addItem(message.data.article);
         break;
 
       case "Reader:ArticleGet":
         this._getArticle(message.data.url, message.target).then((article) => {
           // Make sure the target browser is still alive before trying to send data back.
           if (message.target.messageManager) {
             message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
           }
         });
         break;
 
       case "Reader:FaviconRequest": {
         // XXX: To implement.
         break;
       }
       case "Reader:ListStatusRequest":
-        // XXX: To implement.
+        ReadingList.count(message.data).then(count => {
+          let mm = message.target.messageManager
+          // Make sure the target browser is still alive before trying to send data back.
+          if (mm) {
+            mm.sendAsyncMessage("Reader:ListStatusData",
+                                { inReadingList: !!count, url: message.data.url });
+          }
+        });
         break;
 
       case "Reader:RemoveFromList":
-        // XXX: To implement.
+        // We need to get the "real" item to delete it.
+        ReadingList.getItemForURL(message.data.url).then(item => {
+          ReadingList.deleteItem(item)
+        });
         break;
 
       case "Reader:Share":
         // XXX: To implement.
         break;
 
       case "Reader:SystemUIVisibility":
         // XXX: To implement.
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -119,18 +119,16 @@
 /* Profile call tree */
 
 .call-tree-cells-container {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow: auto;
 }
 
-.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="allocations"],
-.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="self-allocations"],
 .call-tree-cells-container[categories-hidden] .call-tree-category {
   display: none;
 }
 
 .call-tree-header {
   font-size: 90%;
   padding-top: 2px !important;
   padding-bottom: 2px !important;
--- a/browser/themes/shared/devtools/profiler.inc.css
+++ b/browser/themes/shared/devtools/profiler.inc.css
@@ -215,18 +215,16 @@
 }
 
 .call-tree-cells-container {
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
   overflow: auto;
 }
 
-.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="allocations"],
-.call-tree-cells-container[allocations-hidden] .call-tree-cell[type="self-allocations"],
 .call-tree-cells-container[categories-hidden] .call-tree-category {
   display: none;
 }
 
 .call-tree-header[type="duration"],
 .call-tree-cell[type="duration"],
 .call-tree-header[type="self-duration"],
 .call-tree-cell[type="self-duration"] {
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -1156,12 +1156,32 @@ sync_java_files = [
     'sync/UnknownSynchronizerConfigurationVersionException.java',
     'sync/Utils.java',
     'tokenserver/TokenServerClient.java',
     'tokenserver/TokenServerClientDelegate.java',
     'tokenserver/TokenServerException.java',
     'tokenserver/TokenServerToken.java',
 ]
 reading_list_service_java_files = [
+    'reading/ClientMetadata.java',
+    'reading/ClientReadingListRecord.java',
+    'reading/FetchSpec.java',
+    'reading/LocalReadingListStorage.java',
+    'reading/ReadingListChangeAccumulator.java',
+    'reading/ReadingListClient.java',
+    'reading/ReadingListClientContentValuesFactory.java',
+    'reading/ReadingListClientRecordFactory.java',
     'reading/ReadingListConstants.java',
+    'reading/ReadingListDeleteDelegate.java',
+    'reading/ReadingListRecord.java',
+    'reading/ReadingListRecordDelegate.java',
+    'reading/ReadingListRecordResponse.java',
+    'reading/ReadingListRecordUploadDelegate.java',
+    'reading/ReadingListResponse.java',
+    'reading/ReadingListStorage.java',
+    'reading/ReadingListStorageResponse.java',
     'reading/ReadingListSyncAdapter.java',
+    'reading/ReadingListSynchronizer.java',
+    'reading/ReadingListSynchronizerDelegate.java',
     'reading/ReadingListSyncService.java',
+    'reading/ReadingListWipeDelegate.java',
+    'reading/ServerReadingListRecord.java',
 ]
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -535,16 +535,17 @@ public class AndroidFxAccount {
     if (stateLabelString == null || stateString == null) {
       throw new IllegalStateException("stateLabelString and stateString must not be null, but: " +
           "(stateLabelString == null) = " + (stateLabelString == null) +
           " and (stateString == null) = " + (stateString == null));
     }
 
     try {
       StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
+      Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
       return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
     } catch (Exception e) {
       throw new IllegalStateException("could not get state", e);
     }
   }
 
   /**
    * <b>For debugging only!</b>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ClientMetadata.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+public class ClientMetadata {
+  public final long id;                 // A client numeric ID. We don't always have a GUID.
+  public final long lastModified;       // A client timestamp.
+  public final boolean isDeleted;
+  public final boolean isArchived;
+
+  public ClientMetadata(final long id, final long lastModified, final boolean isDeleted, final boolean isArchived) {
+    this.id = id;
+    this.lastModified = lastModified;
+    this.isDeleted = isDeleted;
+    this.isArchived = isArchived;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ClientReadingListRecord.java
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class ClientReadingListRecord extends ReadingListRecord {
+  final ExtendedJSONObject fields;
+  public ClientMetadata clientMetadata;
+
+  private String getDefaultAddedBy() {
+    return "Test Device";                // TODO
+  }
+
+  /**
+   * Provided `fields` are *not copied*.
+   */
+  public ClientReadingListRecord(final ServerMetadata serverMetadata, final ClientMetadata clientMetadata, final ExtendedJSONObject fields) {
+    super(serverMetadata);
+    this.clientMetadata = clientMetadata == null ? new ClientMetadata(-1L, -1L, false, false) : clientMetadata;
+    this.fields = fields;
+  }
+
+  public ClientReadingListRecord(String url, String title, String addedBy) {
+    this(url, title, addedBy, System.currentTimeMillis(), false, false);
+  }
+
+  public ClientReadingListRecord(String url, String title, String addedBy, long lastModified, boolean isDeleted, boolean isArchived) {
+    super(null);
+
+    // Required.
+    if (url == null) {
+      throw new IllegalArgumentException("url must be provided.");
+    }
+
+    final ExtendedJSONObject f = new ExtendedJSONObject();
+    f.put("url", url);
+    f.put("title", title == null ? "" : title);
+    f.put("added_by", addedBy == null ? getDefaultAddedBy() : addedBy);
+
+    this.fields = f;
+    this.clientMetadata = new ClientMetadata(-1L, lastModified, isDeleted, isArchived);
+  }
+
+  public ExtendedJSONObject toJSON() {
+    final ExtendedJSONObject object = this.fields.deepCopy();
+    final String guid = getGUID();
+
+    if (guid != null) {
+      object.put("id", guid);
+    }
+    return object;
+  }
+
+  @Override
+  public String getAddedBy() {
+    return this.fields.getString("added_by");
+  }
+
+  @Override
+  public String getURL() {
+    return this.fields.getString("url");    // TODO: resolved_url
+  }
+
+  @Override
+  public String getTitle() {
+    return this.fields.getString("title");  // TODO: resolved_title
+  }
+
+  /**
+   * Produce a record just like the server record, but with the
+   * appropriate additional metadata, such as the local numeric ID.
+   */
+  public ClientReadingListRecord givenServerRecord(ServerReadingListRecord down) {
+    return new ClientReadingListRecord(down.serverMetadata, this.clientMetadata, down.fields);
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/FetchSpec.java
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import ch.boye.httpclientandroidlib.client.utils.URIBuilder;
+
+/**
+ * Defines the parameters that can be added to a reading list fetch URI.
+ */
+public class FetchSpec {
+  private final String queryString;
+
+  private FetchSpec(final String q) {
+    this.queryString = q;
+  }
+
+  public URI getURI(final URI serviceURI) throws URISyntaxException {
+    return new URIBuilder(serviceURI).setCustomQuery(queryString).build();
+  }
+
+  public URI getURI(final URI serviceURI, final String path) throws URISyntaxException {
+    final String currentPath = serviceURI.getPath();
+    final String newPath = (currentPath == null ? "" : currentPath) + path;
+    return new URIBuilder(serviceURI).setPath(newPath)
+                                     .setCustomQuery(queryString)
+                                     .build();
+  }
+
+  public static class Builder {
+    final StringBuilder b = new StringBuilder();
+    boolean first = true;
+
+    public FetchSpec build() {
+      return new FetchSpec(b.toString());
+    }
+
+    private void ampersand() {
+      if (first) {
+        first = false;
+        return;
+      }
+      b.append('&');
+    }
+
+    public Builder setUnread(boolean unread) {
+      ampersand();
+      b.append("unread=");
+      b.append(unread);
+      return this;
+    }
+
+    private void qualifyAttribute(String qual, String attr) {
+      ampersand();
+      b.append(qual);
+      b.append(attr);
+      b.append('=');
+    }
+
+    public Builder setMinAttribute(String attr, int val) {
+      qualifyAttribute("min_", attr);
+      b.append(val);
+      return this;
+    }
+
+    public Builder setMaxAttribute(String attr, int val) {
+      qualifyAttribute("max_", attr);
+      b.append(val);
+      return this;
+    }
+
+    public Builder setNotAttribute(String attr, String val) {
+      qualifyAttribute("not_", attr);
+      b.append(val);
+      return this;
+    }
+
+    public Builder setSince(long since) {
+      if (since == -1L) {
+        return this;
+      }
+
+      ampersand();
+      b.append("_since=");
+      b.append(since);
+      return this;
+    }
+
+    public Builder setExcludeDeleted() {
+      ampersand();
+      b.append("not_deleted=true");
+      return this;
+    }
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/LocalReadingListStorage.java
@@ -0,0 +1,410 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_FAVORITE_CHANGED;
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_FLAGS;
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_CHANGE_UNREAD_CHANGED;
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS;
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_MODIFIED;
+import static org.mozilla.gecko.db.BrowserContract.ReadingListItems.SYNC_STATUS_NEW;
+
+import java.util.ArrayList;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils;
+
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class LocalReadingListStorage implements ReadingListStorage {
+
+  private static final String WHERE_STATUS_NEW = "(" + SYNC_STATUS + " = " + SYNC_STATUS_NEW + ")";
+
+  final class LocalReadingListChangeAccumulator implements ReadingListChangeAccumulator {
+    private static final String LOG_TAG = "RLChanges";
+
+    /**
+     * These are changes that result from uploading new or changed records to the server.
+     * They always correspond to local records.
+     */
+    private final Queue<ClientReadingListRecord> changes;
+
+    /**
+     * These are deletions that result from uploading new or changed records to the server.
+     * They should always correspond to local records.
+     * These are not common: they should only occur if a conflict occurs.
+     */
+    private final Queue<ClientReadingListRecord> deletions;
+
+    /**
+     * These are additions or changes fetched from the server.
+     * At the point of collection we don't know if they're records
+     * that exist locally.
+     *
+     * Batching these here, rather than in the client or the synchronizer,
+     * puts the storage implementation in control of when batches are flushed,
+     * or if batches are used at all.
+     */
+    private final Queue<ServerReadingListRecord> additionsOrChanges;
+
+    LocalReadingListChangeAccumulator() {
+      this.changes = new ConcurrentLinkedQueue<>();
+      this.deletions = new ConcurrentLinkedQueue<>();
+      this.additionsOrChanges = new ConcurrentLinkedQueue<>();
+    }
+
+    public boolean flushDeletions() throws RemoteException {
+      if (deletions.isEmpty()) {
+        return true;
+      }
+
+      long[] ids = new long[deletions.size()];
+      String[] guids = new String[deletions.size()];
+      int iID = 0;
+      int iGUID = 0;
+      for (ClientReadingListRecord record : deletions) {
+        if (record.clientMetadata.id > -1L) {
+          ids[iID++] = record.clientMetadata.id;
+        } else {
+          final String guid = record.getGUID();
+          if (guid == null) {
+            continue;
+          }
+          guids[iGUID++] = guid;
+        }
+      }
+
+      if (iID > 0) {
+        client.delete(URI_WITH_DELETED, RepoUtils.computeSQLLongInClause(ids, ReadingListItems._ID), null);
+      }
+
+      if (iGUID > 0) {
+        client.delete(URI_WITH_DELETED, RepoUtils.computeSQLInClause(iGUID, ReadingListItems.GUID), guids);
+      }
+
+      deletions.clear();
+      return true;
+    }
+
+    public boolean flushRecordChanges() throws RemoteException {
+      if (changes.isEmpty() && additionsOrChanges.isEmpty()) {
+        return true;
+      }
+
+      // For each returned record, apply it to the local store and clear all sync flags.
+      // We can do this because the server always returns the entire record.
+      //
+      // <https://github.com/mozilla-services/readinglist/issues/138> tracks not doing so
+      // for certain patches, which allows us to optimize here.
+      ArrayList<ContentProviderOperation> operations = new ArrayList<>(changes.size() + additionsOrChanges.size());
+      for (ClientReadingListRecord rec : changes) {
+        operations.add(makeUpdateOp(rec));
+      }
+
+      for (ServerReadingListRecord rec : additionsOrChanges) {
+        // TODO: skip records for which the local copy of the server timestamp
+        // matches the timestamp in the incoming record.
+        // TODO: we can do this by maintaining a lookup table, rather
+        // than hitting the DB. When we do an insert after an upload, say, we
+        // can make a note of it so the next download flush doesn't apply it twice.
+        operations.add(makeUpdateOrInsertOp(rec));
+      }
+
+      // TODO: tell delegate of success or failure.
+      try {
+        Logger.debug(LOG_TAG, "Applying " + operations.size() + " operations.");
+        ContentProviderResult[] results = client.applyBatch(operations);
+      } catch (OperationApplicationException e) {
+        // Oops.
+        Logger.warn(LOG_TAG, "Applying operations failed.", e);
+        return false;
+      }
+      return true;
+    }
+
+    private ContentProviderOperation makeUpdateOrInsertOp(ServerReadingListRecord rec) throws RemoteException {
+      final ClientReadingListRecord clientRec = new ClientReadingListRecord(rec.serverMetadata, null, rec.fields);
+
+      // TODO: use UPDATE OR INSERT equivalent, rather than querying here.
+      if (hasGUID(rec.serverMetadata.guid)) {
+        return makeUpdateOp(clientRec);
+      }
+
+      final ContentValues values = ReadingListClientContentValuesFactory.fromClientRecord(clientRec);
+      return ContentProviderOperation.newInsert(URI_WITHOUT_DELETED)
+                                     .withValues(values)
+                                     .build();
+    }
+
+    private ContentProviderOperation makeUpdateOp(ClientReadingListRecord rec) {
+      // We can't use SQLiteQueryBuilder, because it can't do UPDATE,
+      // nor can it give us a WHERE clause.
+      final StringBuilder selection = new StringBuilder();
+      final String[] selectionArgs;
+
+      // We don't apply changes that we've already applied.
+      // We know they've already been applied because our local copy of the
+      // server's version code/timestamp matches the value in the incoming record.
+      long serverLastModified = rec.getServerLastModified();
+      if (serverLastModified != -1L) {
+        // This should always be the case here.
+        selection.append("(" + ReadingListItems.SERVER_LAST_MODIFIED + " IS NOT ");
+        selection.append(serverLastModified);
+        selection.append(") AND ");
+      }
+
+      if (rec.clientMetadata.id > -1L) {
+        selection.append("(");
+        selection.append(ReadingListItems._ID + " = ");
+        selection.append(rec.clientMetadata.id);
+        selection.append(")");
+        selectionArgs = null;
+      } else if (rec.serverMetadata.guid != null) {
+        selection.append("(" + ReadingListItems.GUID + " = ?)");
+        selectionArgs = new String[] { rec.serverMetadata.guid };
+      } else {
+        final String url = rec.fields.getString("url");
+        final String resolvedURL = rec.fields.getString("resolved_url");
+
+        if (url == null && resolvedURL == null) {
+          // We're outta luck.
+          return null;
+        }
+
+        selection.append("((" + ReadingListItems.URL + " = ?) OR (" + ReadingListItems.RESOLVED_URL + " = ?))");
+        if (url != null && resolvedURL != null) {
+          selectionArgs = new String[] { url, resolvedURL };
+        } else {
+          final String arg = url == null ? resolvedURL : url;
+          selectionArgs = new String[] { arg, arg };
+        }
+      }
+
+      final ContentValues values = ReadingListClientContentValuesFactory.fromClientRecord(rec);
+      return ContentProviderOperation.newUpdate(URI_WITHOUT_DELETED)
+                                     .withSelection(selection.toString(), selectionArgs)
+                                     .withValues(values)
+                                     .build();
+    }
+
+    @Override
+    public void finish() throws Exception {
+      flushDeletions();
+      flushRecordChanges();
+    }
+
+    @Override
+    public void addDeletion(ClientReadingListRecord record) {
+      deletions.add(record);
+    }
+
+    @Override
+    public void addChangedRecord(ClientReadingListRecord record) {
+      changes.add(record);
+    }
+
+    @Override
+    public void addUploadedRecord(ClientReadingListRecord up,
+                                  ServerReadingListRecord down) {
+      // TODO
+    }
+
+    @Override
+    public void addDownloadedRecord(ServerReadingListRecord down) {
+      additionsOrChanges.add(down);
+    }
+  }
+
+  private final ContentProviderClient client;
+  private final Uri URI_WITHOUT_DELETED = BrowserContract.READING_LIST_AUTHORITY_URI
+      .buildUpon()
+      .appendPath("items")
+      .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
+      .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "0")
+      .build();
+
+  private final Uri URI_WITH_DELETED = BrowserContract.READING_LIST_AUTHORITY_URI
+      .buildUpon()
+      .appendPath("items")
+      .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")
+      .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
+      .build();
+
+  public LocalReadingListStorage(final ContentProviderClient client) {
+    this.client = client;
+  }
+
+  public boolean hasGUID(String guid) throws RemoteException {
+    final String[] projection = new String[] { ReadingListItems.GUID };
+    final String selection = ReadingListItems.GUID + " = ?";
+    final String[] selectionArgs = new String[] { guid };
+    final Cursor cursor = this.client.query(URI_WITHOUT_DELETED, projection, selection, selectionArgs, null);
+    try {
+      return cursor.moveToFirst();
+    } finally {
+      cursor.close();
+    }
+  }
+
+  public Cursor getModifiedWithSelection(final String selection) {
+    final String[] projection = new String[] {
+      ReadingListItems.GUID,
+      ReadingListItems.IS_FAVORITE,
+      ReadingListItems.RESOLVED_TITLE,
+      ReadingListItems.RESOLVED_URL,
+      ReadingListItems.EXCERPT,
+    };
+
+
+    try {
+      return client.query(URI_WITHOUT_DELETED, projection, selection, null, null);
+    } catch (RemoteException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+  @Override
+  public Cursor getModified() {
+    final String selection = ReadingListItems.SYNC_STATUS + " = " + ReadingListItems.SYNC_STATUS_MODIFIED;
+    return getModifiedWithSelection(selection);
+  }
+
+  // Return changed items that aren't just status changes.
+  // This isn't necessary because we insist on processing status changes before modified items.
+  // Currently we only need this for tests...
+  public Cursor getNonStatusModified() {
+    final String selection = ReadingListItems.SYNC_STATUS + " = " + ReadingListItems.SYNC_STATUS_MODIFIED +
+                             " AND ((" + ReadingListItems.SYNC_CHANGE_FLAGS + " & " + ReadingListItems.SYNC_CHANGE_RESOLVED + ") > 0)";
+
+    return getModifiedWithSelection(selection);
+  }
+
+  // These will never conflict (in the case of unread status changes), or
+  // we don't care if they overwrite the server value (in the case of favorite changes).
+  // N.B., don't actually send each field if the appropriate change flag isn't set!
+  @Override
+  public Cursor getStatusChanges() {
+    final String[] projection = new String[] {
+      ReadingListItems.GUID,
+      ReadingListItems.IS_FAVORITE,
+      ReadingListItems.IS_UNREAD,
+      ReadingListItems.MARKED_READ_BY,
+      ReadingListItems.MARKED_READ_ON,
+      ReadingListItems.SYNC_CHANGE_FLAGS,
+    };
+
+    final String selection =
+        SYNC_STATUS + " = " + SYNC_STATUS_MODIFIED + " AND " +
+        "((" + SYNC_CHANGE_FLAGS + " & (" + SYNC_CHANGE_UNREAD_CHANGED + " | " + SYNC_CHANGE_FAVORITE_CHANGED + ")) > 0)";
+
+    try {
+      return client.query(URI_WITHOUT_DELETED, projection, selection, null, null);
+    } catch (RemoteException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public Cursor getNew() {
+    // N.B., query for items that have no GUID, regardless of status.
+    // They should all be marked as NEW, but belt and braces.
+    final String selection = WHERE_STATUS_NEW + " OR (" + ReadingListItems.GUID + " IS NULL)";
+
+    try {
+      return client.query(URI_WITHOUT_DELETED, null, selection, null, null);
+    } catch (RemoteException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public Cursor getAll() {
+    try {
+      return client.query(URI_WITHOUT_DELETED, null, null, null, null);
+    } catch (RemoteException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private ContentProviderOperation updateAddedByNames(final String local) {
+    String[] selectionArgs = new String[] {"$local"};
+    String selection = WHERE_STATUS_NEW + " AND (" + ReadingListItems.ADDED_BY + " = ?)";
+    return ContentProviderOperation.newUpdate(URI_WITHOUT_DELETED)
+                                   .withValue(ReadingListItems.ADDED_BY, local)
+                                   .withSelection(selection, selectionArgs)
+                                   .build();
+  }
+
+  private ContentProviderOperation updateMarkedReadByNames(final String local) {
+    String[] selectionArgs = new String[] {"$local"};
+    String selection = ReadingListItems.MARKED_READ_BY + " = ?";
+    return ContentProviderOperation.newUpdate(URI_WITHOUT_DELETED)
+                                   .withValue(ReadingListItems.MARKED_READ_BY, local)
+                                   .withSelection(selection, selectionArgs)
+                                   .build();
+  }
+
+  /**
+   * Consumers of the reading list provider don't know the device name.
+   * Rather than smearing that logic into callers, or requiring the database
+   * to be able to figure out the name of the device, we have the SyncAdapter
+   * do it.
+   *
+   * After all, the SyncAdapter knows everything -- prefs, channels, profiles,
+   * Firefox Account details, etc.
+   *
+   * To allow this, the CP writes the magic string "$local" wherever a device
+   * name is needed. Here in storage, we run a quick UPDATE pass prior to
+   * synchronizing, so the device name is 'calcified' at the time of the first
+   * sync of that record. The SyncAdapter calls this prior to invoking the
+   * synchronizer.
+   */
+  public void updateLocalNames(final String local) {
+    ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(2);
+    ops.add(updateAddedByNames(local));
+    ops.add(updateMarkedReadByNames(local));
+
+    try {
+      client.applyBatch(ops);
+    } catch (RemoteException e) {
+      return;
+    } catch (OperationApplicationException e) {
+      return;
+    }
+  }
+
+  @Override
+  public ReadingListChangeAccumulator getChangeAccumulator() {
+    return new LocalReadingListChangeAccumulator();
+  }
+
+  /**
+   * Unused: we implicitly do this when we apply the server record.
+   */
+  /*
+  public void markStatusChangedItemsAsSynced(Collection<String> uploaded) {
+    ContentValues values = new ContentValues();
+    values.put(ReadingListItems.SYNC_CHANGE_FLAGS, ReadingListItems.SYNC_CHANGE_NONE);
+    values.put(ReadingListItems.SYNC_STATUS, ReadingListItems.SYNC_STATUS_SYNCED);
+    final String where = RepoUtils.computeSQLInClause(uploaded.size(), ReadingListItems.GUID);
+    final String[] args = uploaded.toArray(new String[uploaded.size()]);
+    try {
+      client.update(URI_WITHOUT_DELETED, values, where, args);
+    } catch (RemoteException e) {
+      // Nothing we can do.
+    }
+  }
+  */
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListChangeAccumulator.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+
+/**
+ * Grab one of these, then you can add records to it by parsing
+ * server responses. Finishing it will flush those changes (e.g.,
+ * via UPDATE) to the DB.
+ */
+public interface ReadingListChangeAccumulator {
+  void addDeletion(ClientReadingListRecord record);
+  void addChangedRecord(ClientReadingListRecord record);
+  void addUploadedRecord(ClientReadingListRecord up, ServerReadingListRecord down);
+  void addDownloadedRecord(ServerReadingListRecord down);
+  void finish() throws Exception;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListClient.java
@@ -0,0 +1,607 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.reading.ReadingListResponse.ResponseFactory;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.MozResponse;
+import org.mozilla.gecko.sync.net.Resource;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+
+/**
+ * This client exposes an API for the reading list service, documented at
+ * https://github.com/mozilla-services/readinglist/
+ */
+public class ReadingListClient {
+  static final String LOG_TAG = ReadingListClient.class.getSimpleName();
+  private final AuthHeaderProvider auth;
+
+  private final URI articlesURI;              // .../articles
+  private final URI articlesBaseURI;          // .../articles/
+
+  /**
+   * Use a {@link BasicAuthHeaderProvider} for testing, and an FxA OAuth provider for the real service.
+   */
+  public ReadingListClient(final URI serviceURI, final AuthHeaderProvider auth) {
+    this.articlesURI = serviceURI.resolve("articles");
+    this.articlesBaseURI = serviceURI.resolve("articles/");
+    this.auth = auth;
+  }
+
+  private BaseResource getRelativeArticleResource(final String rel) {
+    return new BaseResource(this.articlesBaseURI.resolve(rel));
+  }
+
+  private static final class DelegatingUploadResourceDelegate extends UploadResourceDelegate<ReadingListRecordResponse> {
+    private final ClientReadingListRecord         up;
+    private final ReadingListRecordUploadDelegate uploadDelegate;
+
+    DelegatingUploadResourceDelegate(Resource resource,
+                                     AuthHeaderProvider auth,
+                                     ResponseFactory<ReadingListRecordResponse> factory,
+                                     ClientReadingListRecord up,
+                                     ReadingListRecordUploadDelegate uploadDelegate) {
+      super(resource, auth, factory);
+      this.up = up;
+      this.uploadDelegate = uploadDelegate;
+    }
+
+    @Override
+    void onFailure(MozResponse response) {
+      Logger.warn(LOG_TAG, "Upload got failure response " + response.httpResponse().getStatusLine());
+      response.logResponseBody(LOG_TAG);
+      if (response.getStatusCode() == 400) {
+        // Error response.
+        uploadDelegate.onBadRequest(up, response);
+      } else {
+        uploadDelegate.onFailure(up, response);
+      }
+    }
+
+    @Override
+    void onFailure(Exception ex) {
+      Logger.warn(LOG_TAG, "Upload failed.", ex);
+      uploadDelegate.onFailure(up, ex);
+    }
+
+    @Override
+    void onSuccess(ReadingListRecordResponse response) {
+      Logger.debug(LOG_TAG, "Upload: onSuccess: " + response.httpResponse().getStatusLine());
+      final ServerReadingListRecord down;
+      try {
+        down = response.getRecord();
+        Logger.debug(LOG_TAG, "Upload succeeded. Got GUID " + down.getGUID());
+      } catch (Exception e) {
+        uploadDelegate.onFailure(up, e);
+        return;
+      }
+
+      uploadDelegate.onSuccess(up, response, down);
+    }
+
+    @Override
+    void onSeeOther(ReadingListRecordResponse response) {
+      uploadDelegate.onConflict(up, response);
+    }
+  }
+
+  private static abstract class ReadingListResourceDelegate<T extends ReadingListResponse> extends BaseResourceDelegate {
+    private final ReadingListResponse.ResponseFactory<T> factory;
+    private final AuthHeaderProvider auth;
+
+    public ReadingListResourceDelegate(Resource resource, AuthHeaderProvider auth, ReadingListResponse.ResponseFactory<T> factory) {
+      super(resource);
+      this.auth = auth;
+      this.factory = factory;
+    }
+
+    abstract void onSuccess(T response);
+    abstract void onNonSuccess(T response);
+    abstract void onFailure(MozResponse response);
+    abstract void onFailure(Exception ex);
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      final T resp = factory.getResponse(response);
+      if (resp.wasSuccessful()) {
+        onSuccess(resp);
+      } else {
+        onNonSuccess(resp);
+      }
+    }
+
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      onFailure(e);
+    }
+
+    @Override
+    public void handleHttpProtocolException(ClientProtocolException e) {
+      onFailure(e);
+    }
+
+    @Override
+    public void handleHttpIOException(IOException e) {
+      onFailure(e);
+    }
+
+    @Override
+    public String getUserAgent() {
+      return ReadingListConstants.USER_AGENT;
+    }
+
+    @Override
+    public AuthHeaderProvider getAuthHeaderProvider() {
+      return auth;
+    }
+
+    @Override
+    public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+    }
+  }
+
+  /**
+   * An intermediate delegate class that handles all of the shared storage behavior,
+   * such as handling If-Modified-Since.
+   */
+  private static abstract class StorageResourceDelegate<T extends ReadingListResponse> extends ReadingListResourceDelegate<T> {
+    private final long ifModifiedSince;
+
+    public StorageResourceDelegate(Resource resource,
+                                   AuthHeaderProvider auth,
+                                   ReadingListResponse.ResponseFactory<T> factory,
+                                   long ifModifiedSince) {
+      super(resource, auth, factory);
+      this.ifModifiedSince = ifModifiedSince;
+    }
+
+    @Override
+    public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+      if (ifModifiedSince != -1L) {
+        // TODO: format?
+        request.addHeader("If-Modified-Since", "" + ifModifiedSince);
+      }
+      super.addHeaders(request, client);
+    }
+  }
+
+  /**
+   * Wraps the @{link ReadingListRecordDelegate} interface to yield a {@link StorageResourceDelegate}.
+   */
+  private static abstract class RecordResourceDelegate<T extends ReadingListResponse> extends StorageResourceDelegate<T> {
+    protected final ReadingListRecordDelegate recordDelegate;
+
+    public RecordResourceDelegate(Resource resource,
+                                  AuthHeaderProvider auth,
+                                  ReadingListRecordDelegate recordDelegate,
+                                  ReadingListResponse.ResponseFactory<T> factory,
+                                  long ifModifiedSince) {
+      super(resource, auth, factory, ifModifiedSince);
+      this.recordDelegate = recordDelegate;
+    }
+
+    abstract void onNotFound(ReadingListResponse resp);
+
+    @Override
+    void onNonSuccess(T resp) {
+      Logger.debug(LOG_TAG, "Got non-success record response " + resp.getStatusCode());
+      resp.logResponseBody(LOG_TAG);
+
+      switch (resp.getStatusCode()) {
+      case 304:
+        onNotModified(resp);
+        break;
+      case 404:
+        onNotFound(resp);
+        break;
+      default:
+        onFailure(resp);
+      }
+    }
+
+    @Override
+    void onFailure(MozResponse response) {
+      recordDelegate.onFailure(response);
+    }
+
+    @Override
+    void onFailure(Exception ex) {
+      recordDelegate.onFailure(ex);
+    }
+
+    void onNotModified(T resp) {
+      recordDelegate.onComplete(resp);
+    }
+  }
+
+  private static final class SingleRecordResourceDelegate extends RecordResourceDelegate<ReadingListRecordResponse> {
+    private final String guid;
+
+    SingleRecordResourceDelegate(Resource resource,
+                                 AuthHeaderProvider auth,
+                                 ReadingListRecordDelegate recordDelegate,
+                                 ResponseFactory<ReadingListRecordResponse> factory,
+                                 long ifModifiedSince, String guid) {
+      super(resource, auth, recordDelegate, factory, ifModifiedSince);
+      this.guid = guid;
+    }
+
+    @Override
+    void onSuccess(ReadingListRecordResponse response) {
+      final ServerReadingListRecord record;
+      try {
+        record = response.getRecord();
+      } catch (Exception e) {
+        recordDelegate.onFailure(e);
+        return;
+      }
+
+      recordDelegate.onRecordReceived(record);
+      recordDelegate.onComplete(response);
+    }
+
+    @Override
+    void onNotFound(ReadingListResponse resp) {
+      recordDelegate.onRecordMissingOrDeleted(guid, resp);
+    }
+  }
+
+  private static final class MultipleRecordResourceDelegate extends RecordResourceDelegate<ReadingListStorageResponse> {
+    MultipleRecordResourceDelegate(Resource resource,
+                                   AuthHeaderProvider auth,
+                                   ReadingListRecordDelegate recordDelegate,
+                                   ResponseFactory<ReadingListStorageResponse> factory,
+                                   long ifModifiedSince) {
+      super(resource, auth, recordDelegate, factory, ifModifiedSince);
+    }
+
+    @Override
+    void onSuccess(ReadingListStorageResponse response) {
+      try {
+        final Iterable<ServerReadingListRecord> records = response.getRecords();
+        for (ServerReadingListRecord readingListRecord : records) {
+          recordDelegate.onRecordReceived(readingListRecord);
+        }
+      } catch (Exception e) {
+        recordDelegate.onFailure(e);
+        return;
+      }
+
+      recordDelegate.onComplete(response);
+    }
+
+    @Override
+    void onNotFound(ReadingListResponse resp) {
+      // Should not occur against articlesURI root.
+      recordDelegate.onFailure(resp);
+    }
+  }
+
+  private static abstract class UploadResourceDelegate<T extends ReadingListResponse> extends StorageResourceDelegate<T> {
+    public UploadResourceDelegate(Resource resource,
+                                  AuthHeaderProvider auth,
+                                  ReadingListResponse.ResponseFactory<T> factory,
+                                  long ifModifiedSince) {
+      super(resource, auth, factory, ifModifiedSince);
+    }
+
+    public UploadResourceDelegate(Resource resource,
+                                  AuthHeaderProvider auth,
+                                  ReadingListResponse.ResponseFactory<T> factory) {
+      this(resource, auth, factory, -1L);
+    }
+
+    @Override
+    void onNonSuccess(T resp) {
+      if (resp.getStatusCode() == 303) {
+        onSeeOther(resp);
+        return;
+      }
+      onFailure(resp);
+    }
+
+    abstract void onSeeOther(T resp);
+  }
+
+
+  /**
+   * Recursively calls `patch` with items from the queue, delivering callbacks
+   * to the provided delegate. Calls `onBatchDone` when the queue is exhausted.
+   *
+   * Uses the provided executor to flatten the recursive call stack.
+   */
+  private abstract class BatchingUploadDelegate implements ReadingListRecordUploadDelegate {
+    private final Queue<ClientReadingListRecord>  queue;
+    private final ReadingListRecordUploadDelegate batchUploadDelegate;
+    private final Executor                        executor;
+
+    BatchingUploadDelegate(Queue<ClientReadingListRecord> queue,
+                           ReadingListRecordUploadDelegate batchUploadDelegate,
+                           Executor executor) {
+      this.queue = queue;
+      this.batchUploadDelegate = batchUploadDelegate;
+      this.executor = executor;
+    }
+
+    abstract void again(ClientReadingListRecord record);
+
+    void next() {
+      final ClientReadingListRecord record = queue.poll();
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          if (record == null) {
+            batchUploadDelegate.onBatchDone();
+            return;
+          }
+
+          again(record);
+        }
+      });
+    }
+
+    @Override
+    public void onSuccess(ClientReadingListRecord up,
+                          ReadingListRecordResponse response,
+                          ServerReadingListRecord down) {
+      batchUploadDelegate.onSuccess(up, response, down);
+      next();
+    }
+
+    @Override
+    public void onInvalidUpload(ClientReadingListRecord up,
+                                ReadingListResponse response) {
+      batchUploadDelegate.onInvalidUpload(up, response);
+      next();
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, MozResponse response) {
+      batchUploadDelegate.onFailure(up, response);
+      next();
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, Exception ex) {
+      batchUploadDelegate.onFailure(up, ex);
+      next();
+    }
+
+    @Override
+    public void onConflict(ClientReadingListRecord up,
+                           ReadingListResponse response) {
+      batchUploadDelegate.onConflict(up, response);
+      next();
+    }
+
+    @Override
+    public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
+      batchUploadDelegate.onBadRequest(up, response);
+      next();
+    }
+
+    @Override
+    public void onBatchDone() {
+      // This should never occur, but if it does, pass through.
+      batchUploadDelegate.onBatchDone();
+    }
+  }
+
+  private class PostBatchingUploadDelegate extends BatchingUploadDelegate {
+    PostBatchingUploadDelegate(Queue<ClientReadingListRecord> queue,
+                               ReadingListRecordUploadDelegate batchUploadDelegate,
+                               Executor executor) {
+      super(queue, batchUploadDelegate, executor);
+    }
+
+    @Override
+    void again(ClientReadingListRecord record) {
+      add(record, PostBatchingUploadDelegate.this);
+    }
+  }
+
+  private class PatchBatchingUploadDelegate extends BatchingUploadDelegate {
+    PatchBatchingUploadDelegate(Queue<ClientReadingListRecord> queue,
+                                ReadingListRecordUploadDelegate batchUploadDelegate,
+                                Executor executor) {
+      super(queue, batchUploadDelegate, executor);
+    }
+
+    @Override
+    void again(ClientReadingListRecord record) {
+      patch(record, PatchBatchingUploadDelegate.this);
+    }
+  }
+
+  // Deliberately declare `delegate` non-final so we can't capture it below. We prefer
+  // to use `recordDelegate` explicitly.
+  public void getOne(final String guid, ReadingListRecordDelegate delegate, final long ifModifiedSince) {
+    final BaseResource r = getRelativeArticleResource(guid);
+    r.delegate = new SingleRecordResourceDelegate(r, auth, delegate, ReadingListRecordResponse.FACTORY, ifModifiedSince, guid);
+    if (ReadingListConstants.DEBUG) {
+      Logger.info(LOG_TAG, "Getting record " + guid);
+    }
+    r.get();
+  }
+
+  // Deliberately declare `delegate` non-final so we can't capture it below. We prefer
+  // to use `recordDelegate` explicitly.
+  public void getAll(final FetchSpec spec, ReadingListRecordDelegate delegate, final long ifModifiedSince) throws URISyntaxException {
+    final BaseResource r = new BaseResource(spec.getURI(this.articlesURI));
+    r.delegate = new MultipleRecordResourceDelegate(r, auth, delegate, ReadingListStorageResponse.FACTORY, ifModifiedSince);
+    if (ReadingListConstants.DEBUG) {
+      Logger.info(LOG_TAG, "Getting all records from " + r.getURIString());
+    }
+    r.get();
+  }
+
+  /**
+   * Mutates the provided queue.
+   */
+  public void patch(final Queue<ClientReadingListRecord> queue, final Executor executor, final ReadingListRecordUploadDelegate batchUploadDelegate) {
+    if (queue.isEmpty()) {
+      batchUploadDelegate.onBatchDone();
+      return;
+    }
+
+    final ReadingListRecordUploadDelegate uploadDelegate = new PatchBatchingUploadDelegate(queue, batchUploadDelegate, executor);
+
+    patch(queue.poll(), uploadDelegate);
+  }
+
+  public void patch(final ClientReadingListRecord up, final ReadingListRecordUploadDelegate uploadDelegate) {
+    final String guid = up.getGUID();
+    if (guid == null) {
+      uploadDelegate.onFailure(up, new IllegalArgumentException("Supplied record must have a GUID."));
+      return;
+    }
+
+    final BaseResource r = getRelativeArticleResource(guid);
+    r.delegate = new DelegatingUploadResourceDelegate(r, auth, ReadingListRecordResponse.FACTORY, up,
+                                                      uploadDelegate);
+
+    final ExtendedJSONObject body = up.toJSON();
+    if (ReadingListConstants.DEBUG) {
+      Logger.info(LOG_TAG, "Patching record " + guid + ": " + body.toJSONString());
+    }
+    r.post(body);
+  }
+
+  /**
+   * Mutates the provided queue.
+   */
+  public void add(final Queue<ClientReadingListRecord> queue, final Executor executor, final ReadingListRecordUploadDelegate batchUploadDelegate) {
+    if (queue.isEmpty()) {
+      batchUploadDelegate.onBatchDone();
+      return;
+    }
+
+    final ReadingListRecordUploadDelegate uploadDelegate = new PostBatchingUploadDelegate(queue, batchUploadDelegate, executor);
+
+    add(queue.poll(), uploadDelegate);
+  }
+
+  public void add(final ClientReadingListRecord up, final ReadingListRecordUploadDelegate uploadDelegate) {
+    final BaseResource r = new BaseResource(this.articlesURI);
+    r.delegate = new DelegatingUploadResourceDelegate(r, auth, ReadingListRecordResponse.FACTORY, up,
+                                                      uploadDelegate);
+
+    final ExtendedJSONObject body = up.toJSON();
+    if (ReadingListConstants.DEBUG) {
+      Logger.info(LOG_TAG, "Uploading new record: " + body.toJSONString());
+    }
+    r.post(body);
+  }
+
+  public void delete(final String guid, final ReadingListDeleteDelegate delegate, final long ifUnmodifiedSince) {
+    final BaseResource r = getRelativeArticleResource(guid);
+
+    // If If-Unmodified-Since is provided, and the record has been modified,
+    // we'll receive a 412 Precondition Failed.
+    // If the record is missing or already deleted, a 404 will be returned.
+    // Otherwise, the response will be the deleted record.
+    r.delegate = new ReadingListResourceDelegate<ReadingListRecordResponse>(r, auth, ReadingListRecordResponse.FACTORY) {
+      @Override
+      public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+        if (ifUnmodifiedSince != -1) {
+          request.addHeader("If-Unmodified-Since", "" + ifUnmodifiedSince);
+        }
+        super.addHeaders(request, client);
+      }
+
+      @Override
+      void onFailure(MozResponse response) {
+        switch (response.getStatusCode()) {
+        case 412:
+          delegate.onPreconditionFailed(guid, response);
+          return;
+        }
+        delegate.onFailure(response);
+      }
+
+      @Override
+      void onSuccess(ReadingListRecordResponse response) {
+        final ReadingListRecord record;
+        try {
+          record = response.getRecord();
+        } catch (Exception e) {
+          delegate.onFailure(e);
+          return;
+        }
+
+        delegate.onSuccess(response, record);
+      }
+
+      @Override
+      void onFailure(Exception ex) {
+        delegate.onFailure(ex);
+      }
+
+      @Override
+      void onNonSuccess(ReadingListRecordResponse response) {
+        if (response.getStatusCode() == 404) {
+          // Already deleted!
+          delegate.onRecordMissingOrDeleted(guid, response);
+        }
+      }
+    };
+
+    if (ReadingListConstants.DEBUG) {
+      Logger.debug(LOG_TAG, "Deleting " + r.getURIString());
+    }
+    r.delete();
+  }
+
+  // TODO: modified times etc.
+  public void wipe(final ReadingListWipeDelegate delegate) {
+    Logger.info(LOG_TAG, "Wiping server.");
+    final BaseResource r = new BaseResource(this.articlesURI);
+
+    r.delegate = new ReadingListResourceDelegate<ReadingListStorageResponse>(r, auth, ReadingListStorageResponse.FACTORY) {
+
+      @Override
+      void onSuccess(ReadingListStorageResponse response) {
+        Logger.info(LOG_TAG, "Wipe succeded.");
+        delegate.onSuccess(response);
+      }
+
+      @Override
+      void onNonSuccess(ReadingListStorageResponse response) {
+        Logger.warn(LOG_TAG, "Wipe failed: " + response.getStatusCode());
+        onFailure(response);
+      }
+
+      @Override
+      void onFailure(MozResponse response) {
+        Logger.warn(LOG_TAG, "Wipe failed: " + response.getStatusCode());
+        delegate.onFailure(response);
+      }
+
+      @Override
+      void onFailure(Exception ex) {
+        Logger.warn(LOG_TAG, "Wipe failed.", ex);
+        delegate.onFailure(ex);
+      }
+    };
+
+    r.delete();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListClientContentValuesFactory.java
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.reading.ReadingListRecord.ServerMetadata;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import android.content.ContentValues;
+
+public class ReadingListClientContentValuesFactory {
+  public static ContentValues fromClientRecord(ClientReadingListRecord record) {
+    // Do each of these.
+    ExtendedJSONObject fields = record.fields;
+    ServerMetadata sm = record.serverMetadata;
+
+    final ContentValues values = new ContentValues();
+
+    if (sm.guid != null) {
+      values.put(ReadingListItems.GUID, sm.guid);
+    }
+
+    if (sm.lastModified > -1L) {
+      values.put(ReadingListItems.SERVER_LAST_MODIFIED, sm.lastModified);
+    }
+
+    final Set<Entry<String, Object>> entries = fields.entrySet();
+
+    for (Entry<String,Object> entry : entries) {
+      final String key = entry.getKey();
+      final String field = mapField(key);
+      if (field == null) {
+        continue;
+      }
+
+      final Object v = entry.getValue();
+      if (v == null) {
+        values.putNull(field);
+      } else if (v instanceof Boolean) {
+        values.put(field, ((Boolean) v) ? 1 : 0);
+      } else if (v instanceof Long) {
+        values.put(field, (Long) v);
+      } else if (v instanceof Integer) {
+        values.put(field, (Integer) v);
+      } else if (v instanceof String) {
+        values.put(field, (String) v);
+      } else if (v instanceof Double) {
+        values.put(field, (Double) v);
+      } else {
+        throw new IllegalArgumentException("Unknown value " + v + " of type " + v.getClass().getSimpleName());
+      }
+    }
+
+    // Clear the sync flags.
+    values.put(ReadingListItems.SYNC_STATUS, ReadingListItems.SYNC_STATUS_SYNCED);
+    values.put(ReadingListItems.SYNC_CHANGE_FLAGS, ReadingListItems.SYNC_CHANGE_NONE);
+
+    return values;
+  }
+
+  /**
+   * Only returns valid columns.
+   */
+  private static String mapField(String key) {
+    if (key == null) {
+      return null;
+    }
+
+    switch (key) {
+    case "unread":
+      return "is_unread";
+    case "favorite":
+      return "is_favorite";
+    case "archived":
+      return "is_archived";
+    case "deleted":
+      return "is_deleted";
+    }
+
+    // Validation.
+    for (int i = 0; i < ReadingListItems.ALL_FIELDS.length; ++i) {
+      if (key.equals(ReadingListItems.ALL_FIELDS[i])) {
+        return key;
+      }
+    }
+
+    return null;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListClientRecordFactory.java
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.reading.ReadingListRecord.ServerMetadata;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import android.annotation.TargetApi;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.sqlite.SQLiteCursor;
+import android.os.Build;
+
+/**
+ * This class converts database rows into {@link ClientReadingListRecord}s.
+ *
+ * In doing so it has to:
+ *
+ *  * Translate column names.
+ *  * Convert INTEGER columns into booleans.
+ *  * Eliminate fields that aren't present in the wire format.
+ *  * Extract fields that are part of {@link ClientMetadata} instances.
+ *
+ * The caller is responsible for closing the cursor.
+ */
+public class ReadingListClientRecordFactory {
+  public static final int MAX_SERVER_STRING_CHARS = 1024;
+
+  private final Cursor cursor;
+
+  private final String[] fields;
+  private final int[] columns;
+
+  public ReadingListClientRecordFactory(final Cursor cursor, final String[] fields) throws IllegalArgumentException {
+    this.cursor = cursor;
+
+    // Does this cursor have an _ID?
+    final int idIndex = cursor.getColumnIndex(ReadingListItems._ID);
+    final int extra = (idIndex != -1) ? 1 : 0;
+    final int cols = fields.length + extra;
+
+    this.fields = new String[cols];
+    this.columns = new int[cols];
+
+    for (int i = 0; i < fields.length; ++i) {
+      final int index = cursor.getColumnIndex(fields[i]);
+      if (index == -1) {
+        continue;
+      }
+      this.fields[i] = mapColumn(fields[i]);
+      this.columns[i] = index;
+    }
+
+    if (idIndex != -1) {
+      this.fields[fields.length] = "_id";
+      this.columns[fields.length] = idIndex;
+    }
+  }
+
+  public ReadingListClientRecordFactory(final Cursor cursor) {
+    this(cursor, ReadingListItems.ALL_FIELDS);
+  }
+
+  private void putNull(ExtendedJSONObject o, String field) {
+    o.put(field, null);
+  }
+
+  /**
+   * Map column names to protocol field names.
+   */
+  private static String mapColumn(final String column) {
+    switch (column) {
+    case "is_unread":
+      return "unread";
+    case "is_favorite":
+      return "favorite";
+    case "is_archived":
+      return "archived";
+    }
+    return column;
+  }
+
+  private void put(ExtendedJSONObject o, String field, String value) {
+    // All server strings are a max of 1024 characters.
+    o.put(field, value.length() > MAX_SERVER_STRING_CHARS ? value.substring(0, MAX_SERVER_STRING_CHARS - 1) + "…" : value);
+  }
+
+  private void put(ExtendedJSONObject o, String field, long value) {
+    // Convert to boolean.
+    switch (field) {
+    case "unread":
+    case "favorite":
+    case "archived":
+    case "is_article":
+      o.put(field, value == 1);
+      return;
+    }
+    o.put(field, value);
+  }
+
+  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+  private final void fillHoneycomb(ExtendedJSONObject o, Cursor c, String f, int i) {
+    if (f == null) {
+      return;
+    }
+    switch (c.getType(i)) {
+    case Cursor.FIELD_TYPE_NULL:
+      putNull(o, f);
+      return;
+    case Cursor.FIELD_TYPE_STRING:
+      put(o, f, c.getString(i));
+      return;
+    case Cursor.FIELD_TYPE_INTEGER:
+      put(o, f, c.getLong(i));
+      return;
+    case Cursor.FIELD_TYPE_FLOAT:
+      o.put(f, c.getDouble(i));
+      return;
+    case Cursor.FIELD_TYPE_BLOB:
+      // TODO: this probably doesn't serialize correctly.
+      o.put(f, c.getBlob(i));
+      return;
+    default:
+      // Do nothing.
+      return;
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  private final void fillGingerbread(ExtendedJSONObject o, Cursor c, String f, int i) {
+    if (!(c instanceof SQLiteCursor)) {
+      throw new IllegalStateException("Unable to handle cursors that don't have a CursorWindow!");
+    }
+
+    final SQLiteCursor sqc = (SQLiteCursor) c;
+    final CursorWindow w = sqc.getWindow();
+    final int pos = c.getPosition();
+    if (w.isNull(pos, i)) {
+      putNull(o, f);
+    } else if (w.isString(pos, i)) {
+      put(o, f, c.getString(i));
+    } else if (w.isLong(pos, i)) {
+      put(o, f, c.getLong(i));
+    } else if (w.isFloat(pos, i)) {
+      o.put(f, c.getDouble(i));
+    } else if (w.isBlob(pos, i)) {
+      // TODO: this probably doesn't serialize correctly.
+      o.put(f, c.getBlob(i));
+    }
+  }
+
+  /**
+   * TODO: optionally produce a partial record by examining SYNC_CHANGE_FLAGS/SYNC_STATUS.
+   */
+  public ClientReadingListRecord fromCursorRow() {
+    final ExtendedJSONObject object = new ExtendedJSONObject();
+    for (int i = 0; i < this.fields.length; ++i) {
+      final String field = fields[i];
+      if (field == null) {
+        continue;
+      }
+      final int column = this.columns[i];
+      if (Versions.feature11Plus) {
+        fillHoneycomb(object, this.cursor, field, column);
+      } else {
+        fillGingerbread(object, this.cursor, field, column);
+      }
+    }
+
+    // Apply cross-field constraints.
+    if (object.containsKey("unread") && object.getBoolean("unread")) {
+      object.remove("marked_read_by");
+      object.remove("marked_read_on");
+    }
+
+    // Construct server metadata and client metadata from the object.
+    final long serverLastModified = object.getLong("last_modified", -1L);
+    final String guid = object.containsKey("guid") ? object.getString("guid") : null;
+    final ServerMetadata sm = new ServerMetadata(guid, serverLastModified);
+
+    final long clientLastModified = object.getLong("client_last_modified", -1L);
+
+    // This has already been translated...
+    final boolean isArchived = object.getBoolean("archived");
+
+    // ... but this is a client-only field, so it needs to be converted.
+    final boolean isDeleted = object.getLong("is_deleted", 0L) == 1L;
+    final long localID = object.getLong("_id", -1L);
+    final ClientMetadata cm = new ClientMetadata(localID, clientLastModified, isDeleted, isArchived);
+
+    // Remove things that aren't part of the spec.
+    object.remove("last_modified");
+    object.remove("guid");
+    object.remove("client_last_modified");
+    object.remove("is_deleted");
+
+    object.remove(ReadingListItems.CONTENT_STATUS);
+    object.remove(ReadingListItems.SYNC_STATUS);
+    object.remove(ReadingListItems.SYNC_CHANGE_FLAGS);
+    object.remove(ReadingListItems.CLIENT_LAST_MODIFIED);
+
+    return new ClientReadingListRecord(sm, cm, object);
+  }
+
+  /**
+   * Return a record from a cursor.
+   * Make sure that the columns you specify in the constructor are a subset
+   * of the columns in the cursor, or you'll have a bad time.
+   */
+  public ClientReadingListRecord getNext() {
+    if (!cursor.moveToNext()) {
+      return null;
+    }
+
+    return fromCursorRow();
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/reading/ReadingListConstants.java
+++ b/mobile/android/base/reading/ReadingListConstants.java
@@ -6,9 +6,13 @@ package org.mozilla.gecko.reading;
 
 import org.mozilla.gecko.AppConstants;
 
 public class ReadingListConstants {
   public static final String GLOBAL_LOG_TAG = "FxReadingList";
   public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_DISPLAYNAME + ")";
   public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
   public static final String DEFAULT_PROD_ENDPOINT = null;     // TODO
+
+  public static final String OAUTH_ENDPOINT_PROD = "https://oauth.accounts.firefox.com/v1";
+
+  public static boolean DEBUG = false;
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListDeleteDelegate.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.net.MozResponse;
+
+/**
+ * Response delegate for a server DELETE.
+ * Only one of these methods will be called, and it will be called precisely once.
+ */
+public interface ReadingListDeleteDelegate {
+  void onSuccess(ReadingListRecordResponse response, ReadingListRecord record);
+  void onPreconditionFailed(String guid, MozResponse response);
+  void onRecordMissingOrDeleted(String guid, MozResponse response);
+  void onFailure(Exception e);
+  void onFailure(MozResponse response);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListRecord.java
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * This models the wire protocol format, not database contents.
+ */
+public abstract class ReadingListRecord {
+  public static class ServerMetadata {
+    public final String guid;             // Null if not yet uploaded successfully.
+    public final long lastModified;       // A server timestamp.
+
+    public ServerMetadata(String guid, long lastModified) {
+      this.guid = guid;
+      this.lastModified = lastModified;
+    }
+
+    /**
+     * From server record.
+     */
+    public ServerMetadata(ExtendedJSONObject obj) {
+      this(obj.getString("id"), obj.getLong("last_modified"));
+    }
+  }
+
+  public final ServerMetadata serverMetadata;
+
+  public String getGUID() {
+    if (serverMetadata == null) {
+      return null;
+    }
+
+    return serverMetadata.guid;
+  }
+
+  public long getServerLastModified() {
+    if (serverMetadata == null) {
+      return -1L;
+    }
+
+    return serverMetadata.lastModified;
+  }
+
+  protected ReadingListRecord(final ServerMetadata serverMetadata) {
+    this.serverMetadata = serverMetadata;
+  }
+
+  public abstract String getURL();
+  public abstract String getTitle();
+  public abstract String getAddedBy();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListRecordDelegate.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.net.MozResponse;
+
+/**
+ * Delegate for downloading records.
+ *
+ * onRecordReceived will be called at most once per record.
+ * onComplete will be called at the end of a successful download.
+ *
+ * Otherwise, one of the failure methods will be called.
+ *
+ * onRecordMissingOrDeleted will only be called when fetching a single
+ * record by ID.
+ */
+public interface ReadingListRecordDelegate {
+  void onRecordReceived(ServerReadingListRecord record);
+  void onComplete(ReadingListResponse response);
+  void onFailure(MozResponse response);
+  void onFailure(Exception error);
+  void onRecordMissingOrDeleted(String guid, ReadingListResponse resp);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListRecordResponse.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.io.IOException;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * A storage response that contains a single record.
+ */
+public class ReadingListRecordResponse extends ReadingListResponse {
+  @Override
+  public boolean wasSuccessful() {
+    final int code = getStatusCode();
+    if (code == 200 || code == 201 || code == 204) {
+      return true;
+    }
+    return super.wasSuccessful();
+  }
+
+  public static final ReadingListResponse.ResponseFactory<ReadingListRecordResponse> FACTORY = new ReadingListResponse.ResponseFactory<ReadingListRecordResponse>() {
+    @Override
+    public ReadingListRecordResponse getResponse(HttpResponse r) {
+      return new ReadingListRecordResponse(r);
+    }
+  };
+
+  public ReadingListRecordResponse(HttpResponse res) {
+    super(res);
+  }
+
+  public ServerReadingListRecord getRecord() throws IllegalStateException, NonObjectJSONException, IOException, ParseException {
+    return new ServerReadingListRecord(jsonObjectBody());
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListRecordUploadDelegate.java
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.net.MozResponse;
+
+public interface ReadingListRecordUploadDelegate {
+  // Called once per batch.
+  public void onBatchDone();
+
+  // One of these is called once per record.
+  public void onSuccess(ClientReadingListRecord up, ReadingListRecordResponse response, ServerReadingListRecord down);
+  public void onConflict(ClientReadingListRecord up, ReadingListResponse response);
+  public void onInvalidUpload(ClientReadingListRecord up, ReadingListResponse response);
+  public void onBadRequest(ClientReadingListRecord up, MozResponse response);
+  public void onFailure(ClientReadingListRecord up, Exception ex);
+  public void onFailure(ClientReadingListRecord up, MozResponse response);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListResponse.java
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.net.MozResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * A MozResponse that knows about all of the general RL-related headers, like Last-Modified.
+ */
+public abstract class ReadingListResponse extends MozResponse {
+  static interface ResponseFactory<T extends ReadingListResponse> {
+    public T getResponse(HttpResponse r);
+  }
+
+  public ReadingListResponse(HttpResponse res) {
+    super(res);
+  }
+
+  public long getLastModified() {
+    return getLongHeader("Last-Modified");
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListStorage.java
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import android.database.Cursor;
+
+public interface ReadingListStorage {
+  Cursor getModified();
+  Cursor getStatusChanges();
+  Cursor getNew();
+  Cursor getAll();
+  ReadingListChangeAccumulator getChangeAccumulator();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListStorageResponse.java
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.UnexpectedJSONException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * A storage response that contains multiple records.
+ */
+public class ReadingListStorageResponse extends ReadingListResponse {
+  public static final ReadingListResponse.ResponseFactory<ReadingListStorageResponse> FACTORY = new ReadingListResponse.ResponseFactory<ReadingListStorageResponse>() {
+    @Override
+    public ReadingListStorageResponse getResponse(HttpResponse r) {
+      return new ReadingListStorageResponse(r);
+    }
+  };
+
+  private static final String LOG_TAG = "StorageResponse";
+
+  public ReadingListStorageResponse(HttpResponse res) {
+    super(res);
+  }
+
+  public Iterable<ServerReadingListRecord> getRecords() throws IOException, ParseException, UnexpectedJSONException {
+    final ExtendedJSONObject body = jsonObjectBody();
+    final JSONArray items = body.getArray("items");
+
+    final int expected = getTotalRecords();
+    final int actual = items.size();
+    if (actual < expected) {
+      Logger.warn(LOG_TAG, "Unexpected number of records. Got " + actual + ", expected " + expected);
+    }
+
+    return new Iterable<ServerReadingListRecord>() {
+      @Override
+      public Iterator<ServerReadingListRecord> iterator() {
+        return new Iterator<ServerReadingListRecord>() {
+          int position = 0;
+
+          @Override
+          public boolean hasNext() {
+            return position < actual;
+          }
+
+          @Override
+          public ServerReadingListRecord next() {
+            final Object o = items.get(position++);
+            return new ServerReadingListRecord(new ExtendedJSONObject((JSONObject) o));
+          }
+
+          @Override
+          public void remove() {
+            throw new RuntimeException("Cannot remove from iterator.");
+          }
+        };
+      }
+    };
+  }
+
+  public int getTotalRecords() {
+    return getIntegerHeader("Total-Records");
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/reading/ReadingListSyncAdapter.java
+++ b/mobile/android/base/reading/ReadingListSyncAdapter.java
@@ -1,25 +1,294 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.reading;
 
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.mozilla.gecko.background.common.PrefsBranch;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
+import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
 
 import android.accounts.Account;
 import android.content.AbstractThreadedSyncAdapter;
 import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.SyncResult;
 import android.os.Bundle;
 
 public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
-    public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
-      super(context, autoInitialize);
+  public static final String PREF_LOCAL_NAME = "device.localname";
+  public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb";
+  public static final String OAUTH_SCOPE_READINGLIST = "readinglist";
+
+  private static final String LOG_TAG = ReadingListSyncAdapter.class.getSimpleName();
+  private static final long TIMEOUT_SECONDS = 60;
+  protected final ExecutorService executor;
+
+  public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
+    super(context, autoInitialize);
+    this.executor = Executors.newSingleThreadExecutor();
+  }
+
+
+  static final class SyncAdapterSynchronizerDelegate implements ReadingListSynchronizerDelegate {
+    private final FxAccountSyncDelegate syncDelegate;
+    private final ContentProviderClient cpc;
+    private final SyncResult result;
+
+    SyncAdapterSynchronizerDelegate(FxAccountSyncDelegate syncDelegate,
+                                    ContentProviderClient cpc,
+                                    SyncResult result) {
+      this.syncDelegate = syncDelegate;
+      this.cpc = cpc;
+      this.result = result;
+    }
+
+    @Override
+    public void onUnableToSync(Exception e) {
+      Logger.warn(LOG_TAG, "Unable to sync.", e);
+      cpc.release();
+      syncDelegate.handleError(e);
+    }
+
+    @Override
+    public void onStatusUploadComplete(Collection<String> uploaded,
+                                       Collection<String> failed) {
+      Logger.debug(LOG_TAG, "Step: onStatusUploadComplete");
+      this.result.stats.numEntries += 1;   // TODO: Bug 1140809.
+    }
+
+    @Override
+    public void onNewItemUploadComplete(Collection<String> uploaded,
+                                        Collection<String> failed) {
+      Logger.debug(LOG_TAG, "Step: onNewItemUploadComplete");
+      this.result.stats.numEntries += 1;   // TODO: Bug 1140809.
+    }
+
+    @Override
+    public void onModifiedUploadComplete() {
+      Logger.debug(LOG_TAG, "Step: onModifiedUploadComplete");
+      this.result.stats.numEntries += 1;   // TODO: Bug 1140809.
+    }
+
+    @Override
+    public void onDownloadComplete() {
+      Logger.debug(LOG_TAG, "Step: onDownloadComplete");
+      this.result.stats.numInserts += 1;   // TODO: Bug 1140809.
     }
 
     @Override
-    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
-      final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account);
+    public void onComplete() {
+      Logger.info(LOG_TAG, "Reading list synchronization complete.");
+      cpc.release();
+      syncDelegate.handleSuccess();
     }
+  }
+
+  @Override
+  public void onPerformSync(final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) {
+    Logger.setThreadLogTag(ReadingListConstants.GLOBAL_LOG_TAG);
+    Logger.resetLogging();
+
+    final Context context = getContext();
+    final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+
+    // If this sync was triggered by user action, this will be true.
+    final boolean isImmediate = (extras != null) &&
+        (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) ||
+            extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
+
+    final CountDownLatch latch = new CountDownLatch(1);
+    final FxAccountSyncDelegate syncDelegate = new FxAccountSyncDelegate(latch, syncResult, fxAccount);
+    try {
+      final State state;
+      try {
+        state = fxAccount.getState();
+      } catch (Exception e) {
+        Logger.error(LOG_TAG, "Unable to sync.", e);
+        return;
+      }
+
+      final String oauthServerUri = ReadingListConstants.OAUTH_ENDPOINT_PROD;
+      final String authServerEndpoint = fxAccount.getAccountServerURI();
+      final String audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token.
+
+      final SharedPreferences sharedPrefs = fxAccount.getReadingListPrefs();
+      final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor);
+      final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
+
+      stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() {
+        @Override
+        public FxAccountClient getClient() {
+          return client;
+        }
+
+        @Override
+        public long getCertificateDurationInMilliseconds() {
+          return 12 * 60 * 60 * 1000;
+        }
+
+        @Override
+        public long getAssertionDurationInMilliseconds() {
+          return 15 * 60 * 1000;
+        }
+
+        @Override
+        public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+          return StateFactory.generateKeyPair();
+        }
+
+        @Override
+        public void handleTransition(Transition transition, State state) {
+          Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
+        }
+
+        @Override
+        public void handleFinal(State state) {
+          Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
+          fxAccount.setState(state);
+
+          // TODO: scheduling, notifications.
+          try {
+            if (state.getStateLabel() != StateLabel.Married) {
+              syncDelegate.handleCannotSync(state);
+              return;
+            }
+
+            final Married married = (Married) state;
+            final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
+            JSONWebTokenUtils.dumpAssertion(assertion);
+
+            final String clientID = OAUTH_CLIENT_ID_FENNEC;
+            final String scope = OAUTH_SCOPE_READINGLIST;
+            syncWithAssertion(clientID, scope, assertion, sharedPrefs, extras);
+          } catch (Exception e) {
+            syncDelegate.handleError(e);
+            return;
+          }
+        }
+
+        private void syncWithAssertion(final String client_id, final String scope, final String assertion,
+                                       final SharedPreferences sharedPrefs, final Bundle extras) {
+          final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor);
+          Logger.debug(LOG_TAG, "OAuth fetch.");
+          oauthClient.authorization(client_id, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() {
+            @Override
+            public void handleSuccess(AuthorizationResponse result) {
+              Logger.debug(LOG_TAG, "OAuth success.");
+              syncWithAuthorization(result, sharedPrefs, extras);
+            }
+
+            @Override
+            public void handleFailure(FxAccountAbstractClientRemoteException e) {
+              Logger.error(LOG_TAG, "OAuth failure.", e);
+              syncDelegate.handleError(e);
+            }
+
+            @Override
+            public void handleError(Exception e) {
+              Logger.error(LOG_TAG, "OAuth error.", e);
+              syncDelegate.handleError(e);
+            }
+          });
+        }
+
+        private void syncWithAuthorization(AuthorizationResponse authResponse,
+                                           SharedPreferences sharedPrefs,
+                                           Bundle extras) {
+          final AuthHeaderProvider auth = new BearerAuthHeaderProvider(authResponse.access_token);
+
+          final String endpointString = ReadingListConstants.DEFAULT_DEV_ENDPOINT;
+          final URI endpoint;
+          Logger.info(LOG_TAG, "XXX Syncing to " + endpointString);
+          try {
+            endpoint = new URI(endpointString);
+          } catch (URISyntaxException e) {
+            // Should never happen.
+            Logger.error(LOG_TAG, "Unexpected malformed URI for reading list service: " + endpointString);
+            syncDelegate.handleError(e);
+            return;
+          }
+
+          final PrefsBranch branch = new PrefsBranch(sharedPrefs, "readinglist.");
+          final ReadingListClient remote = new ReadingListClient(endpoint, auth);
+          final ContentProviderClient cpc = getContentProviderClient(context);     // TODO: make sure I'm always released!
+
+          final LocalReadingListStorage local = new LocalReadingListStorage(cpc);
+          String localName = branch.getString(PREF_LOCAL_NAME, null);
+          if (localName == null) {
+            localName = FxAccountUtils.defaultClientName(context);
+          }
+
+          // Make sure DB rows don't refer to placeholder values.
+          local.updateLocalNames(localName);
+
+          final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local);
+
+          synchronizer.syncAll(new SyncAdapterSynchronizerDelegate(syncDelegate, cpc, syncResult));
+          // TODO: backoffs, and everything else handled by a SessionCallback.
+        }
+      });
+
+      latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      Logger.info(LOG_TAG, "Reading list sync done.");
+
+    } catch (Exception e) {
+      Logger.error(LOG_TAG, "Got error syncing.", e);
+      syncDelegate.handleError(e);
+    }
+    /*
+     * TODO:
+     * * Account error notifications. How do we avoid these overlapping with Sync?
+     * * Pickling. How do we avoid pickling twice if you use both Sync and RL?
+     */
+
+    /*
+     * TODO:
+     * * Auth.
+     * * Server URI lookup.
+     * * Syncing.
+     * * Error handling.
+     * * Backoff and retry-after.
+     * * Sync scheduling.
+     * * Forcing syncs/interactive use.
+     */
+  }
+
+
+  private ContentProviderClient getContentProviderClient(Context context) {
+    final ContentResolver contentResolver = context.getContentResolver();
+    final ContentProviderClient client = contentResolver.acquireContentProviderClient(ReadingListItems.CONTENT_URI);
+    return client;
+  }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSynchronizer.java
@@ -0,0 +1,645 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.PrefsBranch;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.reading.ReadingListRecord.ServerMetadata;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.net.MozResponse;
+
+import android.database.Cursor;
+import android.text.TextUtils;
+
+/**
+ * This class implements the multi-phase synchronizing approach described
+ * at <https://github.com/mozilla-services/readinglist/wiki/Client-phases>.
+ *
+ * This is also where delegate-based control flow comes to die.
+ */
+public class ReadingListSynchronizer {
+  public static final String LOG_TAG = ReadingListSynchronizer.class.getSimpleName();
+
+  public static final String PREF_LAST_MODIFIED = "download.serverlastmodified";
+
+  private final PrefsBranch prefs;
+  private final ReadingListClient remote;
+  private final ReadingListStorage local;
+  private final Executor executor;
+
+  private interface StageDelegate {
+    void next();
+    void fail();
+    void fail(Exception e);
+  }
+
+  private abstract static class NextDelegate implements StageDelegate {
+    private final Executor executor;
+    NextDelegate(final Executor executor) {
+      this.executor = executor;
+    }
+
+    abstract void doNext();
+    abstract void doFail(Exception e);
+
+    @Override
+    public void next() {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          doNext();
+        }
+      });
+    }
+
+    @Override
+    public void fail() {
+      fail(null);
+    }
+
+    @Override
+    public void fail(final Exception e) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          doFail(e);
+        }
+      });
+    }
+  }
+
+  public ReadingListSynchronizer(final PrefsBranch prefs, final ReadingListClient remote, final ReadingListStorage local) {
+    this(prefs, remote, local, Executors.newSingleThreadExecutor());
+  }
+
+  public ReadingListSynchronizer(final PrefsBranch prefs, final ReadingListClient remote, final ReadingListStorage local, Executor executor) {
+    this.prefs = prefs;
+    this.remote = remote;
+    this.local = local;
+    this.executor = executor;
+  }
+
+  private static final class NewItemUploadDelegate implements ReadingListRecordUploadDelegate {
+    public volatile int failures = 0;
+    private final ReadingListChangeAccumulator acc;
+    private final StageDelegate next;
+
+    NewItemUploadDelegate(ReadingListChangeAccumulator acc, StageDelegate next) {
+      this.acc = acc;
+      this.next = next;
+    }
+
+    @Override
+    public void onSuccess(ClientReadingListRecord up,
+                          ReadingListRecordResponse response,
+                          ServerReadingListRecord down) {
+      // Apply the resulting record. The server will have populated some fields.
+      acc.addChangedRecord(up.givenServerRecord(down));
+    }
+
+    @Override
+    public void onConflict(ClientReadingListRecord up, ReadingListResponse response) {
+      ExtendedJSONObject body;
+      try {
+        body = response.jsonObjectBody();
+        String conflicting = body.getString("id");
+        Logger.warn(LOG_TAG, "Conflict detected: remote ID is " + conflicting);
+
+        // TODO: When an operation implies that a server record is a replacement
+        // of what we uploaded, we should ensure that we have a local copy of
+        // that server record!
+      } catch (IllegalStateException | NonObjectJSONException | IOException |
+               ParseException e) {
+        // Oops.
+        // But our workaround is the same either way.
+      }
+
+      // Either the record exists locally, in which case we need to merge,
+      // or it doesn't, and we'll download it shortly.
+      // The simplest thing to do in both cases is to simply delete the local
+      // record we tried to upload. Yes, we might lose some annotations, but
+      // we can leave doing better to a follow-up.
+      // Issues here are so unlikely that we don't do anything sophisticated
+      // (like moving the record to a holding area) -- just delete it ASAP.
+      acc.addDeletion(up);
+    }
+
+    @Override
+    public void onInvalidUpload(ClientReadingListRecord up, ReadingListResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, MozResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, Exception ex) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
+      recordFailed(up);
+    }
+
+    private void recordFailed(ClientReadingListRecord up) {
+      ++failures;
+    }
+
+    @Override
+    public void onBatchDone() {
+      // We mark uploaded records as synced when we apply the server record with the
+      // GUID -- we don't know the GUID yet!
+      if (failures == 0) {
+        try {
+          next.next();
+        } catch (Exception e) {
+          next.fail(e);
+        }
+        return;
+      }
+      next.fail();
+    }
+  }
+
+  private static class StatusUploadDelegate implements ReadingListRecordUploadDelegate {
+    private final ReadingListChangeAccumulator acc;
+
+    public volatile int failures = 0;
+    private final StageDelegate next;
+
+    StatusUploadDelegate(ReadingListChangeAccumulator acc, StageDelegate next) {
+      this.acc = acc;
+      this.next = next;
+    }
+
+    @Override
+    public void onInvalidUpload(ClientReadingListRecord up,
+                                ReadingListResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onConflict(ClientReadingListRecord up,
+                           ReadingListResponse response) {
+      // This should never happen for a status-only change.
+      // TODO: mark this record as requiring a full upload or download.
+      failures++;
+    }
+
+    @Override
+    public void onSuccess(ClientReadingListRecord up,
+                          ReadingListRecordResponse response,
+                          ServerReadingListRecord down) {
+      if (!TextUtils.equals(up.getGUID(), down.getGUID())) {
+        // Uh oh!
+        // This should never occur. We should get an onConflict instead,
+        // so this would imply a server bug, or something like a truncated
+        // over-long GUID string.
+        //
+        // Should we wish to recover from this case, probably the right approach
+        // is to ensure that the GUID is overwritten locally (given that we know
+        // the numeric ID).
+      }
+
+      acc.addChangedRecord(up.givenServerRecord(down));
+    }
+
+    @Override
+    public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, Exception ex) {
+      recordFailed(up);
+    }
+
+    @Override
+    public void onFailure(ClientReadingListRecord up, MozResponse response) {
+      recordFailed(up);
+    }
+
+    private void recordFailed(ClientReadingListRecord up) {
+      ++failures;
+    }
+
+    @Override
+    public void onBatchDone() {
+      try {
+        acc.finish();
+      } catch (Exception e) {
+        next.fail(e);
+        return;
+      }
+
+      if (failures == 0) {
+        try {
+          next.next();
+        } catch (Exception e) {
+        }
+      }
+      next.fail();
+    }
+  }
+
+  private Queue<ClientReadingListRecord> collectStatusChangesFromCursor(final Cursor cursor) {
+    try {
+      final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
+
+      // The columns should come in this order, FWIW.
+      final int columnGUID = cursor.getColumnIndexOrThrow(ReadingListItems.GUID);
+      final int columnIsUnread = cursor.getColumnIndexOrThrow(ReadingListItems.IS_UNREAD);
+      final int columnIsFavorite = cursor.getColumnIndexOrThrow(ReadingListItems.IS_FAVORITE);
+      final int columnMarkedReadBy = cursor.getColumnIndexOrThrow(ReadingListItems.MARKED_READ_BY);
+      final int columnMarkedReadOn = cursor.getColumnIndexOrThrow(ReadingListItems.MARKED_READ_ON);
+      final int columnChangeFlags = cursor.getColumnIndexOrThrow(ReadingListItems.SYNC_CHANGE_FLAGS);
+
+      while (cursor.moveToNext()) {
+        final String guid = cursor.getString(columnGUID);
+        if (guid == null) {
+          // Nothing we can do here.
+          continue;
+        }
+
+        final ExtendedJSONObject o = new ExtendedJSONObject();
+        o.put("id", guid);
+
+        final int changeFlags = cursor.getInt(columnChangeFlags);
+        if ((changeFlags & ReadingListItems.SYNC_CHANGE_FAVORITE_CHANGED) > 0) {
+          o.put("favorite", cursor.getInt(columnIsFavorite) == 1);
+        }
+
+        if ((changeFlags & ReadingListItems.SYNC_CHANGE_UNREAD_CHANGED) > 0) {
+          final boolean isUnread = cursor.getInt(columnIsUnread) == 1;
+          o.put("unread", isUnread);
+          if (!isUnread) {
+            o.put("marked_read_by", cursor.getString(columnMarkedReadBy));
+            o.put("marked_read_on", cursor.getLong(columnMarkedReadOn));
+          }
+        }
+
+        final ClientMetadata cm = null;
+        final ServerMetadata sm = new ServerMetadata(guid, -1L);
+        final ClientReadingListRecord record = new ClientReadingListRecord(sm, cm, o);
+        toUpload.add(record);
+      }
+
+      return toUpload;
+    } finally {
+      cursor.close();
+    }
+  }
+
+  private Queue<ClientReadingListRecord> accumulateNewItems(Cursor cursor) {
+    try {
+      final Queue<ClientReadingListRecord> toUpload = new LinkedList<>();
+      final ReadingListClientRecordFactory factory = new ReadingListClientRecordFactory(cursor);
+
+      ClientReadingListRecord record;
+      while ((record = factory.getNext()) != null) {
+        toUpload.add(record);
+      }
+      return toUpload;
+    } finally {
+      cursor.close();
+    }
+  }
+
+  // N.B., status changes for items that haven't been uploaded yet are dealt with in
+  // uploadNewItems.
+  protected void uploadUnreadChanges(final StageDelegate delegate) {
+    try {
+      final Cursor cursor = local.getStatusChanges();
+
+      if (cursor == null) {
+        delegate.fail(new RuntimeException("Unable to get unread item cursor."));
+        return;
+      }
+
+      final Queue<ClientReadingListRecord> toUpload = collectStatusChangesFromCursor(cursor);
+
+      // Nothing to do.
+      if (toUpload.isEmpty()) {
+        delegate.next();
+        return;
+      }
+
+      // Upload each record. This looks like batching, but it's really chained serial requests.
+      final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
+      final StatusUploadDelegate uploadDelegate = new StatusUploadDelegate(acc, delegate);
+
+      // Don't send I-U-S; in the case of favorites we're
+      // happy to overwrite the server value, and in the case of unread status
+      // the server will reconcile for us.
+      this.remote.patch(toUpload, executor, uploadDelegate);
+    } catch (Exception e) {
+      delegate.fail(e);
+    }
+  }
+
+  protected void uploadNewItems(final StageDelegate delegate) {
+    try {
+      final Cursor cursor = this.local.getNew();
+
+      if (cursor == null) {
+        delegate.fail(new RuntimeException("Unable to get new item cursor."));
+        return;
+      }
+
+      Queue<ClientReadingListRecord> toUpload = accumulateNewItems(cursor);
+
+      // Nothing to do.
+      if (toUpload.isEmpty()) {
+        Logger.debug(LOG_TAG, "No new items to upload. Skipping.");
+        delegate.next();
+        return;
+      }
+
+      final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
+      final NewItemUploadDelegate uploadDelegate = new NewItemUploadDelegate(acc, new StageDelegate() {
+        private boolean tryFlushChanges() {
+          Logger.debug(LOG_TAG, "Flushing post-upload changes.");
+          try {
+            acc.finish();
+            return true;
+          } catch (Exception e) {
+            Logger.warn(LOG_TAG, "Flushing changes failed! This sync went wrong.", e);
+            delegate.fail(e);
+            return false;
+          }
+        }
+
+        @Override
+        public void next() {
+          Logger.debug(LOG_TAG, "New items uploaded successfully.");
+
+          if (tryFlushChanges()) {
+            delegate.next();
+          }
+        }
+
+        @Override
+        public void fail() {
+          Logger.warn(LOG_TAG, "Couldn't upload new items.");
+          if (tryFlushChanges()) {
+            delegate.fail();
+          }
+        }
+
+        @Override
+        public void fail(Exception e) {
+          Logger.warn(LOG_TAG, "Couldn't upload new items.", e);
+          if (tryFlushChanges()) {
+            delegate.fail(e);
+          }
+        }
+      });
+
+      // Handle 201 for success, 400 for invalid, 303 for redirect.
+      // TODO: 200 == "was already on the server, we didn't touch it, here it is."
+      // ... we need to apply it locally.
+      this.remote.add(toUpload, executor, uploadDelegate);
+    } catch (Exception e) {
+      delegate.fail(e);
+      return;
+    }
+  }
+
+  private void uploadModified(final StageDelegate delegate) {
+    // TODO
+    delegate.next();
+  }
+
+  private void downloadIncoming(final long since, final StageDelegate delegate) {
+    final ReadingListChangeAccumulator postDownload = this.local.getChangeAccumulator();
+
+    final FetchSpec spec = new FetchSpec.Builder().setSince(since).build();
+
+    // TODO: should we flush the accumulator if we get a failure?
+    ReadingListRecordDelegate recordDelegate = new ReadingListRecordDelegate() {
+      @Override
+      public void onRecordReceived(ServerReadingListRecord record) {
+        postDownload.addDownloadedRecord(record);
+      }
+
+      @Override
+      public void onRecordMissingOrDeleted(String guid, ReadingListResponse resp) {
+        // Should never occur. Deleted records will be processed by onRecordReceived.
+      }
+
+      @Override
+      public void onFailure(Exception error) {
+        Logger.error(LOG_TAG, "Download failed. since = " + since + ".", error);
+        delegate.fail(error);
+      }
+
+      @Override
+      public void onFailure(MozResponse response) {
+        final int statusCode = response.getStatusCode();
+        Logger.error(LOG_TAG, "Download failed. since = " + since + ". Response: " + statusCode);
+        response.logResponseBody(LOG_TAG);
+        delegate.fail();
+      }
+
+      @Override
+      public void onComplete(ReadingListResponse response) {
+        long lastModified = response.getLastModified();
+        Logger.info(LOG_TAG, "Server last modified: " + lastModified);
+        try {
+          postDownload.finish();
+
+          // Yay. We do this here so that if writing changes fails, we don't advance.
+          advanceLastModified(lastModified);
+          delegate.next();
+        } catch (Exception e) {
+          delegate.fail(e);
+        }
+      }
+    };
+
+    try {
+      remote.getAll(spec, recordDelegate, since);
+    } catch (URISyntaxException e) {
+      delegate.fail(e);
+    }
+  }
+
+  /**
+   * Upload unread changes, then upload new items, then call `done`.
+   * Substantially modified records are uploaded last.
+   *
+   * @param syncDelegate only used for status callbacks.
+   */
+  private void syncUp(final ReadingListSynchronizerDelegate syncDelegate, final StageDelegate done) {
+    // Second.
+    final StageDelegate onNewItemsUploaded = new NextDelegate(executor) {
+      @Override
+      public void doNext() {
+        syncDelegate.onNewItemUploadComplete(null, null);
+        done.next();
+      }
+
+      @Override
+      public void doFail(Exception e) {
+        done.fail(e);
+      }
+    };
+
+    // First.
+    final StageDelegate onUnreadChangesUploaded = new NextDelegate(executor) {
+      @Override
+      public void doNext() {
+        syncDelegate.onStatusUploadComplete(null, null);
+        uploadNewItems(onNewItemsUploaded);
+      }
+
+      @Override
+      public void doFail(Exception e) {
+        Logger.warn(LOG_TAG, "Uploading unread changes failed.", e);
+        done.fail(e);
+      }
+    };
+
+    try {
+      uploadUnreadChanges(onUnreadChangesUploaded);
+    } catch (Exception ee) {
+      done.fail(ee);
+    }
+  }
+
+
+  /**
+   * Do an upload-only sync.
+   * By "upload-only" we mean status-only changes and new items.
+   * To upload modifications, use syncAll.
+   */
+  /*
+   // Not yet used
+  public void syncUp(final ReadingListSynchronizerDelegate syncDelegate) {
+    final StageDelegate onUploadCompleted = new StageDelegate() {
+      @Override
+      public void next() {
+        // TODO
+        syncDelegate.onNewItemUploadComplete(null, null);
+      }
+
+      @Override
+      public void fail(Exception e) {
+        syncDelegate.onUnableToSync(e);
+      }
+    };
+
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          syncUp(onUploadCompleted);
+        } catch (Exception e) {
+          syncDelegate.onUnableToSync(e);
+          return;
+        }
+      }
+    });
+  }
+*/
+
+  /**
+   * Do a bidirectional sync.
+   */
+  public void syncAll(final ReadingListSynchronizerDelegate syncDelegate) {
+    syncAll(getLastModified(), syncDelegate);
+  }
+
+  public void syncAll(final long since, final ReadingListSynchronizerDelegate syncDelegate) {
+    // Fourth: call back to the synchronizer delegate.
+    final StageDelegate onModifiedUploadComplete = new NextDelegate(executor) {
+      @Override
+      public void doNext() {
+        syncDelegate.onModifiedUploadComplete();
+        syncDelegate.onComplete();
+      }
+
+      @Override
+      public void doFail(Exception e) {
+        syncDelegate.onUnableToSync(e);
+      }
+    };
+
+    // Third: upload modified records.
+    final StageDelegate onDownloadCompleted = new NextDelegate(executor) {     // TODO: since.
+      @Override
+      public void doNext() {
+        // TODO: save prefs.
+        syncDelegate.onDownloadComplete();
+        uploadModified(onModifiedUploadComplete);
+      }
+
+      @Override
+      public void doFail(Exception e) {
+        Logger.warn(LOG_TAG, "Download failed.", e);
+        syncDelegate.onUnableToSync(e);
+      }
+    };
+
+    // Second: download incoming changes.
+    final StageDelegate onUploadCompleted = new NextDelegate(executor) {
+      @Override
+      public void doNext() {
+        // N.B., we apply the downloaded versions of all uploaded records.
+        // That means the DB server timestamp matches the server's current
+        // timestamp when we do a fetch; we skip records in this way.
+        // We can also optimize by keeping the (guid, server timestamp) pair
+        // in memory, but of course this runs into invalidation issues if
+        // concurrent writes are occurring.
+        downloadIncoming(since, onDownloadCompleted);
+      }
+
+      @Override
+      public void doFail(Exception e) {
+        Logger.warn(LOG_TAG, "Upload failed.", e);
+        syncDelegate.onUnableToSync(e);
+      }
+    };
+
+    // First: upload changes and new items.
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          syncUp(syncDelegate, onUploadCompleted);
+        } catch (Exception e) {
+          syncDelegate.onUnableToSync(e);
+          return;
+        }
+      }
+    });
+
+    // TODO: ensure that records we identified as conflicts have been downloaded.
+  }
+
+  protected long getLastModified() {
+    return prefs.getLong(PREF_LAST_MODIFIED, -1L);
+  }
+
+  protected void advanceLastModified(final long lastModified) {
+    if (getLastModified() > lastModified) {
+      return;
+    }
+    prefs.edit().putLong(PREF_LAST_MODIFIED, lastModified).apply();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSynchronizerDelegate.java
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import java.util.Collection;
+
+public interface ReadingListSynchronizerDelegate {
+  // Called on failure.
+  void onUnableToSync(Exception e);
+
+  // These are called sequentially, or not at all
+  // if a failure occurs.
+  void onStatusUploadComplete(Collection<String> uploaded, Collection<String> failed);
+  void onNewItemUploadComplete(Collection<String> uploaded, Collection<String> failed);
+  void onDownloadComplete();
+  void onModifiedUploadComplete();
+
+  // If no failure occurred, called at the end.
+  void onComplete();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListWipeDelegate.java
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.net.MozResponse;
+
+public interface ReadingListWipeDelegate {
+  void onSuccess(ReadingListStorageResponse response);
+  void onPreconditionFailed(MozResponse response);
+  void onFailure(Exception e);
+  void onFailure(MozResponse response);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ServerReadingListRecord.java
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class ServerReadingListRecord extends ReadingListRecord {
+  final ExtendedJSONObject fields;
+
+  public ServerReadingListRecord(ExtendedJSONObject obj) {
+    super(new ServerMetadata(obj));
+    this.fields = obj.deepCopy();
+  }
+
+  @Override
+  public String getURL() {
+    return this.fields.getString("url");    // TODO: resolved_url
+  }
+
+  @Override
+  public String getTitle() {
+    return this.fields.getString("title");  // TODO: resolved_title
+  }
+
+  @Override
+  public String getAddedBy() {
+    return this.fields.getString("added_by");
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/sync/net/MozResponse.java
+++ b/mobile/android/base/sync/net/MozResponse.java
@@ -173,9 +173,20 @@ public class MozResponse {
 
   /**
    * @return A number of seconds, or -1 if the 'X-Backoff' header was not
    *         present.
    */
   public int backoffInSeconds() throws NumberFormatException {
     return this.getIntegerHeader("x-backoff");
   }
+
+  public void logResponseBody(final String logTag) {
+    if (!Logger.LOG_PERSONAL_INFORMATION) {
+      return;
+    }
+    try {
+      Logger.pii(logTag, "Response body: " + body());
+    } catch (Throwable e) {
+      Logger.debug(logTag, "No response body.");
+    }
+  }
 }
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -521,16 +521,29 @@ var Addons = {
       element.setAttribute("opType", "needs-restart");
   },
 
   onInstalled: function(aAddon) {
     let list = document.getElementById("addons-list");
     let element = this._getElementForAddon(aAddon.id);
     if (!element) {
       element = this._createItemForAddon(aAddon);
+
+      // Themes aren't considered active on install, so set existing as disabled, and new one enabled.
+      if (aAddon.type == "theme") {
+        let item = list.firstElementChild;
+        while (item) {
+          if (item.addon && (item.addon.type == "theme")) {
+            item.setAttribute("isDisabled", true);
+          }
+          item = item.nextSibling;
+        }
+        element.setAttribute("isDisabled", false);
+      }
+
       list.insertBefore(element, list.firstElementChild);
     }
   },
 
   onUninstalled: function(aAddon) {
     let list = document.getElementById("addons-list");
     let element = this._getElementForAddon(aAddon.id);
     list.removeChild(element);
--- a/mobile/android/modules/DownloadNotifications.jsm
+++ b/mobile/android/modules/DownloadNotifications.jsm
@@ -11,16 +11,19 @@ const { classes: Cc, interfaces: Ci, uti
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
 let Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.i.bind(null, "DownloadNotifications"); 
 
 XPCOMUtils.defineLazyGetter(this, "strings",
                             () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
 
 Object.defineProperty(this, "window",
                       { get: () => Services.wm.getMostRecentWindow("navigator:browser") });
 
@@ -53,16 +56,23 @@ var DownloadNotifications = {
   onDownloadAdded: function (download) {
     // Don't create notifications for pre-existing succeeded downloads.
     // We still add notifications for canceled downloads in case the
     // user decides to retry the download.
     if (download.succeeded && !this._viewAdded) {
       return;
     }
 
+    if (!ParentalControls.isAllowed(ParentalControls.DOWNLOAD)) {
+      download.cancel().catch(Cu.reportError);
+      download.removePartialData().catch(Cu.reportError);
+      window.NativeWindow.toast.show(strings.GetStringFromName("downloads.disabledInGuest"), "long");
+      return;
+    }
+
     let notification = new DownloadNotification(download);
     notifications.set(download, notification);
     notification.showOrUpdate();
 
     // If this is a new download, show a toast as well.
     if (this._viewAdded) {
       window.NativeWindow.toast.show(strings.GetStringFromName("alertDownloadsToast"), "long");
     }
--- a/mobile/android/themes/core/aboutReader.css
+++ b/mobile/android/themes/core/aboutReader.css
@@ -275,37 +275,31 @@ body {
 .content ol {
   -moz-padding-start: 35px !important;
   list-style: decimal !important;
 }
 
 /*======= Controls toolbar =======*/
 
 .toolbar {
-  font-family: "Clear Sans",sans-serif;
-  transition-property: visibility, opacity;
-  transition-duration: 0.7s;
-  visibility: visible;
-  opacity: 1.0;
+  font-family: sans-serif;
+  transition-property: bottom;
+  transition-duration: 0.3s;
   position: fixed;
   width: 100%;
-  bottom: 0px;
-  left: 0px;
+  left: 0;
   margin: 0;
   padding: 0;
   list-style: none;
   background-color: #EBEBF0;
   border-top: 1px solid #D7D9DB;
 }
 
-.toolbar-hidden {
-  transition-property: visibility, opacity;
-  transition-duration: 0.7s;
-  visibility: hidden;
-  opacity: 0.0;
+.toolbar[visible] {
+  bottom: 0;
 }
 
 .toolbar > * {
   float: right;
   width: 33%;
 }
 
 .button {
@@ -585,22 +579,30 @@ body {
     background-image: url('chrome://browser/skin/images/reader-plus-xxhdpi.png');
   }
 }
 
 @media screen and (orientation: portrait) {
   .button {
     height: 56px;
   }
+
+  .toolbar {
+    bottom: -57px;
+  }
 }
 
 @media screen and (orientation: landscape) {
   .button {
     height: 40px;
   }
+
+  .toolbar {
+    bottom: -41px;
+  }
 }
 
 @media screen and (min-width: 960px) {
   .button {
     width: 56px;
     height: 56px;
   }
 
--- a/testing/docker/builder/Dockerfile
+++ b/testing/docker/builder/Dockerfile
@@ -16,16 +16,16 @@ RUN hg clone http://hg.mozilla.org/build
       cd /tools/tools && \
       python setup.py install
 
 # Initialize git (makes repo happy)
 RUN git config --global user.email "mozilla@example.com" && \
     git config --global user.name "mozilla"
 
 # VCS Tools
-RUN npm install -g taskcluster-vcs@2.3.0
+RUN npm install -g taskcluster-vcs@2.3.1
 
 # TODO enable worker
 # TODO volume mount permissions will be an issue
 # USER worker
 
 COPY bin /home/worker/bin
 RUN chmod a+x /home/worker/bin/*
--- a/testing/docker/builder/VERSION
+++ b/testing/docker/builder/VERSION
@@ -1,1 +1,1 @@
-0.5.1
+0.5.2
--- a/testing/docker/decision/Dockerfile
+++ b/testing/docker/decision/Dockerfile
@@ -1,8 +1,8 @@
 FROM          quay.io/mozilla/base-build:0.0.1
 MAINTAINER    Jonas Finnemann Jensen <jopsen@gmail.com>
 
 ENV PATH /home/worker/bin/:$PATH
 
 # Add utilities and configuration
-RUN           npm install -g taskcluster-vcs@0.0.2
+RUN           npm install -g taskcluster-vcs@2.3.1
 ADD           bin                   /home/worker/bin
--- a/testing/docker/decision/VERSION
+++ b/testing/docker/decision/VERSION
@@ -1,1 +1,1 @@
-0.0.3
+0.0.4
--- a/testing/docker/phone-builder/Dockerfile
+++ b/testing/docker/phone-builder/Dockerfile
@@ -1,9 +1,9 @@
-FROM          quay.io/mozilla/builder:0.5.0
+FROM          quay.io/mozilla/builder:0.5.2
 MAINTAINER    Wander Lairson Costa <wcosta@mozilla.com>
 
 # Add utilities and configuration
 ADD           bin                   /home/worker/bin
 ADD           config                /home/worker/.aws/config
 ADD           system-setup.sh       /tmp/system-setup.sh
 
 RUN           /tmp/system-setup.sh
--- a/testing/docker/phone-builder/VERSION
+++ b/testing/docker/phone-builder/VERSION
@@ -1,1 +1,1 @@
-0.0.9
+0.0.10
--- a/testing/docker/tester/Dockerfile
+++ b/testing/docker/tester/Dockerfile
@@ -11,16 +11,16 @@ COPY           buildprops.json          
 ADD            https://raw.githubusercontent.com/taskcluster/buildbot-step/master/buildbot_step /home/worker/bin/buildbot_step
 
 
 # Run test setup script
 RUN chmod u+x /home/worker/bin/buildbot_step
 RUN pip install virtualenv;
 RUN mkdir Documents; mkdir Pictures; mkdir Music; mkdir Videos; mkdir artifacts
 RUN chown -R worker:worker /home/worker/* /home/worker/.*
-RUN npm install -g taskcluster-vcs@2.2.0
+RUN npm install -g taskcluster-vcs@2.3.1
 ENV PATH $PATH:/home/worker/bin
 
 # TODO Re-enable worker when bug 1093833 lands
 #USER          worker
 
 # Set a default command useful for debugging
 CMD ["/bin/bash", "--login"]
--- a/testing/docker/tester/VERSION
+++ b/testing/docker/tester/VERSION
@@ -1,1 +1,1 @@
-0.0.14
+0.0.15
--- a/testing/taskcluster/mach_commands.py
+++ b/testing/taskcluster/mach_commands.py
@@ -203,17 +203,17 @@ class Graph(object):
         help='URL for "head" repository to fetch revision from')
     @CommandArgument('--head-ref',
         default=os.environ.get('GECKO_HEAD_REF'),
         help='Reference (this is same as rev usually for hg)')
     @CommandArgument('--head-rev',
         default=os.environ.get('GECKO_HEAD_REV'),
         help='Commit revision to use from head repository')
     @CommandArgument('--mozharness-rev',
-        default='emulator-perf',
+        default='default',
         help='Commit revision to use from mozharness repository')
     @CommandArgument('--message',
         help='Commit message to be parsed. Example: "try: -b do -p all -u all"')
     @CommandArgument('--revision-hash',
             required=False,
             help='Treeherder revision hash to attach results to')
     @CommandArgument('--project',
         required=True,
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -40,18 +40,16 @@ let AboutReader = function(mm, win) {
   this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
   this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
   this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
   this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
   this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content"));
   this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
   this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
 
-  this._toolbarEnabled = false;
-
   this._scrollOffset = win.pageYOffset;
 
   doc.getElementById("container").addEventListener("click", this, false);
 
   win.addEventListener("unload", this, false);
   win.addEventListener("scroll", this, false);
   win.addEventListener("resize", this, false);
 
@@ -251,17 +249,18 @@ AboutReader.prototype = {
       if (args.url == this._article.url) {
         if (this._isReadingListItem != args.inReadingList) {
           let isInitialStateChange = (this._isReadingListItem == -1);
           this._isReadingListItem = args.inReadingList;
           this._updateToggleButton();
 
           // Display the toolbar when all its initial component states are known
           if (isInitialStateChange) {
-            this._setToolbarVisibility(true);
+            // Hacks! Delay showing the toolbar to avoid position: fixed; jankiness. See bug 975533.
+            this._win.setTimeout(() => this._setToolbarVisibility(true), 500);
           }
         }
       }
     };
 
     this._mm.addMessageListener("Reader:ListStatusData", handleListStatusData);
     this._mm.sendAsyncMessage("Reader:ListStatusRequest", { url: this._article.url });
   },
@@ -519,34 +518,32 @@ AboutReader.prototype = {
 
     this._mm.sendAsyncMessage("Reader:SetCharPref", {
       name: "reader.font_type",
       value: this._fontType
     });
   },
 
   _getToolbarVisibility: function Reader_getToolbarVisibility() {
-    return !this._toolbarElement.classList.contains("toolbar-hidden");
+    return this._toolbarElement.hasAttribute("visible");
   },
 
   _setToolbarVisibility: function Reader_setToolbarVisibility(visible) {
     let dropdown = this._doc.getElementById("style-dropdown");
     dropdown.classList.remove("open");
 
-    if (!this._toolbarEnabled)
-      return;
-
-    // Don't allow visible toolbar until banner state is known
-    if (this._isReadingListItem == -1)
+    if (this._getToolbarVisibility() === visible) {
       return;
+    }
 
-    if (this._getToolbarVisibility() === visible)
-      return;
-
-    this._toolbarElement.classList.toggle("toolbar-hidden");
+    if (visible) {
+      this._toolbarElement.setAttribute("visible", true);
+    } else {
+      this._toolbarElement.removeAttribute("visible");
+    }
     this._setSystemUIVisibility(visible);
 
     if (!visible) {
       this._mm.sendAsyncMessage("Reader:ToolbarHidden");
     }
   },
 
   _toggleToolbarVisibility: function Reader_toggleToolbarVisibility() {
@@ -706,19 +703,16 @@ AboutReader.prototype = {
     this._contentElement.innerHTML = "";
     this._contentElement.appendChild(contentFragment);
     this._maybeSetTextDirection(article);
 
     this._contentElement.style.display = "block";
     this._updateImageMargins();
     this._requestReadingListStatus();
 
-    this._toolbarEnabled = true;
-    this._setToolbarVisibility(true);
-
     this._requestFavicon();
   },
 
   _hideContent: function Reader_hideContent() {
     this._headerElement.style.display = "none";
     this._contentElement.style.display = "none";
   },
 
--- a/toolkit/components/reader/ReaderMode.jsm
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -214,17 +214,22 @@ this.ReaderMode = {
       scheme: uri.scheme,
       pathBase: Services.io.newURI(".", null, uri).spec
     };
 
     let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
                      createInstance(Ci.nsIDOMSerializer);
     let serializedDoc = yield Promise.resolve(serializer.serializeToString(doc));
 
-    let article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
+    let article = null;
+    try {
+      article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
+    } catch (e) {
+      Cu.reportError("Error in ReaderWorker: " + e);
+    }
 
     if (!article) {
       this.log("Worker did not return an article");
       return null;
     }
 
     // Readability returns a URI object, but we only care about the URL.
     article.url = article.uri.spec;
--- a/toolkit/components/reader/content/aboutReader.html
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -21,17 +21,17 @@
 
     <div id="reader-content" class="content">
     </div>
 
     <div id="reader-message" class="message">
     </div>
   </div>
 
-  <ul id="reader-toolbar" class="toolbar toolbar-hidden">
+  <ul id="reader-toolbar" class="toolbar">
     <li><button id="close-button" class="button close-button"/></li>
     <li><button id="share-button" class="button share-button"/></li>
     <ul id="style-dropdown" class="dropdown">
       <li><button class="dropdown-toggle button style-button"/></li>
       <li class="dropdown-popup">
         <div id="font-type-buttons"></div>
         <hr></hr>
         <div id="font-size-buttons">
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -383,19 +383,18 @@
             this._fastFind.init(this.docShell);
           }
           return this._fastFind;
         ]]></getter>
       </property>
 
       <field name="_permanentKey">({})</field>
 
-      <property name="permanentKey"
-                onget="return this._permanentKey;"
-                onset="this._permanentKey = val;"/>
+      <property name="permanentKey" readonly="true"
+                onget="return this._permanentKey;"/>
 
       <property name="outerWindowID" readonly="true">
         <getter><![CDATA[
           return this.contentWindow
                      .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils)
                      .outerWindowID;
         ]]></getter>
--- a/toolkit/devtools/security/auth.js
+++ b/toolkit/devtools/security/auth.js
@@ -10,16 +10,18 @@ let { Ci, Cc } = require("chrome");
 let Services = require("Services");
 let promise = require("promise");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv } = DevToolsUtils;
 loader.lazyRequireGetter(this, "prompt",
   "devtools/toolkit/security/prompt");
 loader.lazyRequireGetter(this, "cert",
   "devtools/toolkit/security/cert");
+loader.lazyRequireGetter(this, "asyncStorage",
+  "devtools/toolkit/shared/async-storage");
 DevToolsUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
 /**
  * A simple enum-like object with keys mirrored to values.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
@@ -336,28 +338,34 @@ OOBCert.Client.prototype = {
               host,
               port,
               cert,
               authResult,
               oob: oobData
             });
             break;
           case AuthenticationResult.ALLOW:
-          case AuthenticationResult.ALLOW_PERSIST:
             // Step B.12
             // Client verifies received value matches K
             if (packet.k != oobData.k) {
               transport.close(new Error("Auth secret mismatch"));
               return;
             }
             // Step B.13
             // Debugging begins
             transport.hooks = null;
             deferred.resolve(transport);
             break;
+          case AuthenticationResult.ALLOW_PERSIST:
+            // Server previously persisted Client as allowed
+            // Step C.5
+            // Debugging begins
+            transport.hooks = null;
+            deferred.resolve(transport);
+            break;
           default:
             transport.close(new Error("Invalid auth result: " + authResult));
             return;
         }
       }.bind(this)),
       onClosed(reason) {
         closeDialog();
         // Transport died before auth completed
@@ -483,40 +491,50 @@ OOBCert.Server.prototype = {
    *          transport
    *        }
    * @return An AuthenticationResult value.
    *         A promise that will be resolved to the above is also allowed.
    */
   authenticate: Task.async(function*({ client, server, transport }) {
     // Step B.3 / C.3
     // TLS connection established, authentication begins
-    // TODO: Bug 1032128: Consult a list of persisted, approved clients
+    const storageKey = `devtools.auth.${this.mode}.approved-clients`;
+    let approvedClients = (yield asyncStorage.getItem(storageKey)) || {};
+    // Step C.4
+    // Server sees that ClientCert is from a known client via hash(ClientCert)
+    if (approvedClients[client.cert.sha256]) {
+      let authResult = AuthenticationResult.ALLOW_PERSIST;
+      transport.send({ authResult });
+      // Step C.5
+      // Debugging begins
+      return authResult;
+    }
+
     // Step B.4
     // Server sees that ClientCert is from a unknown client
     // Tell client they are unknown and should display OOB client UX
     transport.send({
       authResult: AuthenticationResult.PENDING
     });
 
     // Step B.5
     // User is shown a Allow / Deny / Always Allow prompt on the Server
     // with Client name and hash(ClientCert)
-    let result = yield this.allowConnection({
+    let authResult = yield this.allowConnection({
       authentication: this.mode,
       client,
       server
     });
 
-    switch (result) {
+    switch (authResult) {
       case AuthenticationResult.ALLOW_PERSIST:
-        // TODO: Bug 1032128: Persist the client
       case AuthenticationResult.ALLOW:
         break; // Further processing
       default:
-        return result; // Abort for any negative results
+        return authResult; // Abort for any negative results
     }
 
     // Examine additional data for authentication
     let oob = yield this.receiveOOB();
     if (!oob) {
       dumpn("Invalid OOB data received");
       return AuthenticationResult.DENY;
     }
@@ -535,24 +553,30 @@ OOBCert.Server.prototype = {
     // out-of-band channel
     if (client.cert.sha256 != sha256) {
       dumpn("Client cert hash doesn't match OOB data");
       return AuthenticationResult.DENY;
     }
 
     // Step B.11
     // Server sends K to Client over TLS connection
-    transport.send({ authResult: result, k });
+    transport.send({ authResult, k });
+
+    // Persist Client if we want to always allow in the future
+    if (authResult === AuthenticationResult.ALLOW_PERSIST) {
+      approvedClients[client.cert.sha256] = true;
+      yield asyncStorage.setItem(storageKey, approvedClients);
+    }
 
     // Client may decide to abort if K does not match.
     // Server's portion of authentication is now complete.
 
     // Step B.13
     // Debugging begins
-    return result;
+    return authResult;
   }),
 
   /**
    * Prompt the user to accept or decline the incoming connection. The default
    * implementation is used unless this is overridden on a particular
    * authenticator instance.
    *
    * It is expected that the implementation of |allowConnection| will show a
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2588,17 +2588,17 @@ SourceActor.prototype = {
    * source map from b to a. We need to do this because the source map we get
    * from _generatePrettyCodeAndMap goes the opposite way we want it to for
    * debugging.
    *
    * Note that the source map is modified in place.
    */
   _invertSourceMap: function ({ code, mappings }) {
     const generator = new SourceMapGenerator({ file: this.url });
-    return DevToolsUtils.yieldingEach(mappings, m => {
+    return DevToolsUtils.yieldingEach(mappings._array, m => {
       let mapping = {
         generated: {
           line: m.generatedLine,
           column: m.generatedColumn
         }
       };
       if (m.source) {
         mapping.source = m.source;
@@ -5714,17 +5714,18 @@ ThreadSources.prototype = {
         } = originalLocation;
 
         let {
           line: generatedLine,
           column: generatedColumn
         } = map.generatedPositionFor({
           source: originalSourceActor.url,
           line: originalLine,
-          column: originalColumn == null ? Infinity : originalColumn
+          column: originalColumn == null ? 0 : originalColumn,
+          bias: SourceMapConsumer.LEAST_UPPER_BOUND
         });
 
         return new GeneratedLocation(
           this.createNonSourceMappedActor(source),
           generatedLine,
           generatedColumn
         );
       }
--- a/toolkit/devtools/shared/tests/browser/browser_async_storage.js
+++ b/toolkit/devtools/shared/tests/browser/browser_async_storage.js
@@ -4,74 +4,74 @@
 
 "use strict";
 
 // Test the basic functionality of async-storage.
 // Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
 
 const asyncStorage = require("devtools/toolkit/shared/async-storage");
 add_task(function*() {
-  is(typeof asyncStorage.length, 'function', "API exists.");
-  is(typeof asyncStorage.key, 'function', "API exists.");
-  is(typeof asyncStorage.getItem, 'function', "API exists.");
-  is(typeof asyncStorage.setItem, 'function', "API exists.");
-  is(typeof asyncStorage.removeItem, 'function', "API exists.");
-  is(typeof asyncStorage.clear, 'function', "API exists.");
+  is(typeof asyncStorage.length, "function", "API exists.");
+  is(typeof asyncStorage.key, "function", "API exists.");
+  is(typeof asyncStorage.getItem, "function", "API exists.");
+  is(typeof asyncStorage.setItem, "function", "API exists.");
+  is(typeof asyncStorage.removeItem, "function", "API exists.");
+  is(typeof asyncStorage.clear, "function", "API exists.");
 });
 
 add_task(function*() {
-  yield asyncStorage.setItem('foo', 'bar');
-  let value = yield asyncStorage.getItem('foo');
-  is(value, 'bar', 'value is correct');
-  yield asyncStorage.setItem('foo', 'overwritten');
-  value = yield asyncStorage.getItem('foo');
-  is(value, 'overwritten', 'value is correct');
-  yield asyncStorage.removeItem('foo');
-  value = yield asyncStorage.getItem('foo');
-  is(value, null, 'value is correct');
+  yield asyncStorage.setItem("foo", "bar");
+  let value = yield asyncStorage.getItem("foo");
+  is(value, "bar", "value is correct");
+  yield asyncStorage.setItem("foo", "overwritten");
+  value = yield asyncStorage.getItem("foo");
+  is(value, "overwritten", "value is correct");
+  yield asyncStorage.removeItem("foo");
+  value = yield asyncStorage.getItem("foo");
+  is(value, null, "value is correct");
 });
 
 add_task(function*() {
   var object = {
     x: 1,
-    y: 'foo',
+    y: "foo",
     z: true
   };
 
-  yield asyncStorage.setItem('myobj', object);
-  let value = yield asyncStorage.getItem('myobj');
-  is(object.x, value.x, 'value is correct');
-  is(object.y, value.y, 'value is correct');
-  is(object.z, value.z, 'value is correct');
-  yield asyncStorage.removeItem('myobj');
-  value = yield asyncStorage.getItem('myobj');
-  is(value, null, 'value is correct');
+  yield asyncStorage.setItem("myobj", object);
+  let value = yield asyncStorage.getItem("myobj");
+  is(object.x, value.x, "value is correct");
+  is(object.y, value.y, "value is correct");
+  is(object.z, value.z, "value is correct");
+  yield asyncStorage.removeItem("myobj");
+  value = yield asyncStorage.getItem("myobj");
+  is(value, null, "value is correct");
 });
 
 add_task(function*() {
   yield asyncStorage.clear();
   let len = yield asyncStorage.length();
-  is(len, 0, 'length is correct');
-  yield asyncStorage.setItem('key1', 'value1');
+  is(len, 0, "length is correct");
+  yield asyncStorage.setItem("key1", "value1");
   len = yield asyncStorage.length();
-  is(len, 1, 'length is correct');
-  yield asyncStorage.setItem('key2', 'value2');
+  is(len, 1, "length is correct");
+  yield asyncStorage.setItem("key2", "value2");
   len = yield asyncStorage.length();
-  is(len, 2, 'length is correct');
-  yield asyncStorage.setItem('key3', 'value3');
+  is(len, 2, "length is correct");
+  yield asyncStorage.setItem("key3", "value3");
   len = yield asyncStorage.length();
-  is(len, 3, 'length is correct');
+  is(len, 3, "length is correct");
 
   let key = yield asyncStorage.key(0);
-  is(key, 'key1', 'key is correct');
+  is(key, "key1", "key is correct");
   key = yield asyncStorage.key(1);
-  is(key, 'key2', 'key is correct');
+  is(key, "key2", "key is correct");
   key = yield asyncStorage.key(2);
-  is(key, 'key3', 'key is correct');
+  is(key, "key3", "key is correct");
   key = yield asyncStorage.key(3);
-  is(key, null, 'key is correct');
+  is(key, null, "key is correct");
   yield asyncStorage.clear();
   key = yield asyncStorage.key(0);
-  is(key, null, 'key is correct');
+  is(key, null, "key is correct");
 
   len = yield asyncStorage.length();
-  is(len, 0, 'length is correct');
+  is(len, 0, "length is correct");
 });
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -26,128 +26,36 @@ Components.utils.import('resource://gre/
  */
 define('source-map/source-map-consumer', ['require', 'exports', 'module' ,  'source-map/util', 'source-map/binary-search', 'source-map/array-set', 'source-map/base64-vlq'], function(require, exports, module) {
 
   var util = require('source-map/util');
   var binarySearch = require('source-map/binary-search');
   var ArraySet = require('source-map/array-set').ArraySet;
   var base64VLQ = require('source-map/base64-vlq');
 
-  /**
-   * A SourceMapConsumer instance represents a parsed source map which we can
-   * query for information about the original file positions by giving it a file
-   * position in the generated source.
-   *
-   * The only parameter is the raw source map (either as a JSON string, or
-   * already parsed to an object). According to the spec, source maps have the
-   * following attributes:
-   *
-   *   - version: Which version of the source map spec this map is following.
-   *   - sources: An array of URLs to the original source files.
-   *   - names: An array of identifiers which can be referrenced by individual mappings.
-   *   - sourceRoot: Optional. The URL root from which all sources are relative.
-   *   - sourcesContent: Optional. An array of contents of the original source files.
-   *   - mappings: A string of base64 VLQs which contain the actual mappings.
-   *   - file: Optional. The generated file this source map is associated with.
-   *
-   * Here is an example source map, taken from the source map spec[0]:
-   *
-   *     {
-   *       version : 3,
-   *       file: "out.js",
-   *       sourceRoot : "",
-   *       sources: ["foo.js", "bar.js"],
-   *       names: ["src", "maps", "are", "fun"],
-   *       mappings: "AA,AB;;ABCDE;"
-   *     }
-   *
-   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
-   */
   function SourceMapConsumer(aSourceMap) {
     var sourceMap = aSourceMap;
     if (typeof aSourceMap === 'string') {
       sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
     }
 
-    var version = util.getArg(sourceMap, 'version');
-    var sources = util.getArg(sourceMap, 'sources');
-    // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
-    // requires the array) to play nice here.
-    var names = util.getArg(sourceMap, 'names', []);
-    var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
-    var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
-    var mappings = util.getArg(sourceMap, 'mappings');
-    var file = util.getArg(sourceMap, 'file', null);
-
-    // Once again, Sass deviates from the spec and supplies the version as a
-    // string rather than a number, so we use loose equality checking here.
-    if (version != this._version) {
-      throw new Error('Unsupported version: ' + version);
-    }
-
-    // Some source maps produce relative source paths like "./foo.js" instead of
-    // "foo.js".  Normalize these first so that future comparisons will succeed.
-    // See bugzil.la/1090768.
-    sources = sources.map(util.normalize);
-
-    // Pass `true` below to allow duplicate names and sources. While source maps
-    // are intended to be compressed and deduplicated, the TypeScript compiler
-    // sometimes generates source maps with duplicates in them. See Github issue
-    // #72 and bugzil.la/889492.
-    this._names = ArraySet.fromArray(names, true);
-    this._sources = ArraySet.fromArray(sources, true);
-
-    this.sourceRoot = sourceRoot;
-    this.sourcesContent = sourcesContent;
-    this._mappings = mappings;
-    this.file = file;
+    return sourceMap.sections != null
+      ? new IndexedSourceMapConsumer(sourceMap)
+      : new BasicSourceMapConsumer(sourceMap);
   }
 
-  /**
-   * Create a SourceMapConsumer from a SourceMapGenerator.
-   *
-   * @param SourceMapGenerator aSourceMap
-   *        The source map that will be consumed.
-   * @returns SourceMapConsumer
-   */
-  SourceMapConsumer.fromSourceMap =
-    function SourceMapConsumer_fromSourceMap(aSourceMap) {
-      var smc = Object.create(SourceMapConsumer.prototype);
-
-      smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
-      smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
-      smc.sourceRoot = aSourceMap._sourceRoot;
-      smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
-                                                              smc.sourceRoot);
-      smc.file = aSourceMap._file;
-
-      smc.__generatedMappings = aSourceMap._mappings.slice()
-        .sort(util.compareByGeneratedPositions);
-      smc.__originalMappings = aSourceMap._mappings.slice()
-        .sort(util.compareByOriginalPositions);
-
-      return smc;
-    };
+  SourceMapConsumer.fromSourceMap = function(aSourceMap) {
+    return BasicSourceMapConsumer.fromSourceMap(aSourceMap);
+  }
 
   /**
    * The version of the source mapping spec that we are consuming.
    */
   SourceMapConsumer.prototype._version = 3;
 
-  /**
-   * The list of original sources.
-   */
-  Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
-    get: function () {
-      return this._sources.toArray().map(function (s) {
-        return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
-      }, this);
-    }
-  });
-
   // `__generatedMappings` and `__originalMappings` are arrays that hold the
   // parsed mapping coordinates from the source map's "mappings" attribute. They
   // are lazily instantiated, accessed via the `_generatedMappings` and
   // `_originalMappings` getters respectively, and we only parse the mappings
   // and create these arrays once queried for a source location. We jump through
   // these hoops because there can be many thousands of mappings, and parsing
   // them is expensive, so we only want to do it if we must.
   //
@@ -195,354 +103,37 @@ define('source-map/source-map-consumer',
         this._parseMappings(this._mappings, this.sourceRoot);
       }
 
       return this.__originalMappings;
     }
   });
 
   SourceMapConsumer.prototype._nextCharIsMappingSeparator =
-    function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
-      var c = aStr.charAt(0);
+    function SourceMapConsumer_nextCharIsMappingSeparator(aStr, index) {
+      var c = aStr.charAt(index);
       return c === ";" || c === ",";
     };
 
   /**
    * Parse the mappings in a string in to a data structure which we can easily
    * query (the ordered arrays in the `this.__generatedMappings` and
    * `this.__originalMappings` properties).
    */
   SourceMapConsumer.prototype._parseMappings =
     function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
-      var generatedLine = 1;
-      var previousGeneratedColumn = 0;
-      var previousOriginalLine = 0;
-      var previousOriginalColumn = 0;
-      var previousSource = 0;
-      var previousName = 0;
-      var str = aStr;
-      var temp = {};
-      var mapping;
-
-      while (str.length > 0) {
-        if (str.charAt(0) === ';') {
-          generatedLine++;
-          str = str.slice(1);
-          previousGeneratedColumn = 0;
-        }
-        else if (str.charAt(0) === ',') {
-          str = str.slice(1);
-        }
-        else {
-          mapping = {};
-          mapping.generatedLine = generatedLine;
-
-          // Generated column.
-          base64VLQ.decode(str, temp);
-          mapping.generatedColumn = previousGeneratedColumn + temp.value;
-          previousGeneratedColumn = mapping.generatedColumn;
-          str = temp.rest;
-
-          if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
-            // Original source.
-            base64VLQ.decode(str, temp);
-            mapping.source = this._sources.at(previousSource + temp.value);
-            previousSource += temp.value;
-            str = temp.rest;
-            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
-              throw new Error('Found a source, but no line and column');
-            }
-
-            // Original line.
-            base64VLQ.decode(str, temp);
-            mapping.originalLine = previousOriginalLine + temp.value;
-            previousOriginalLine = mapping.originalLine;
-            // Lines are stored 0-based
-            mapping.originalLine += 1;
-            str = temp.rest;
-            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
-              throw new Error('Found a source and line, but no column');
-            }
-
-            // Original column.
-            base64VLQ.decode(str, temp);
-            mapping.originalColumn = previousOriginalColumn + temp.value;
-            previousOriginalColumn = mapping.originalColumn;
-            str = temp.rest;
-
-            if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
-              // Original name.
-              base64VLQ.decode(str, temp);
-              mapping.name = this._names.at(previousName + temp.value);
-              previousName += temp.value;
-              str = temp.rest;
-            }
-          }
-
-          this.__generatedMappings.push(mapping);
-          if (typeof mapping.originalLine === 'number') {
-            this.__originalMappings.push(mapping);
-          }
-        }
-      }
-
-      this.__generatedMappings.sort(util.compareByGeneratedPositions);
-      this.__originalMappings.sort(util.compareByOriginalPositions);
-    };
-
-  /**
-   * Find the mapping that best matches the hypothetical "needle" mapping that
-   * we are searching for in the given "haystack" of mappings.
-   */
-  SourceMapConsumer.prototype._findMapping =
-    function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
-                                           aColumnName, aComparator) {
-      // To return the position we are searching for, we must first find the
-      // mapping for the given position and then return the opposite position it
-      // points to. Because the mappings are sorted, we can use binary search to
-      // find the best mapping.
-
-      if (aNeedle[aLineName] <= 0) {
-        throw new TypeError('Line must be greater than or equal to 1, got '
-                            + aNeedle[aLineName]);
-      }
-      if (aNeedle[aColumnName] < 0) {
-        throw new TypeError('Column must be greater than or equal to 0, got '
-                            + aNeedle[aColumnName]);
-      }
-
-      return binarySearch.search(aNeedle, aMappings, aComparator);
-    };
-
-  /**
-   * Compute the last column for each generated mapping. The last column is
-   * inclusive.
-   */
-  SourceMapConsumer.prototype.computeColumnSpans =
-    function SourceMapConsumer_computeColumnSpans() {
-      for (var index = 0; index < this._generatedMappings.length; ++index) {
-        var mapping = this._generatedMappings[index];
-
-        // Mappings do not contain a field for the last generated columnt. We
-        // can come up with an optimistic estimate, however, by assuming that
-        // mappings are contiguous (i.e. given two consecutive mappings, the
-        // first mapping ends where the second one starts).
-        if (index + 1 < this._generatedMappings.length) {
-          var nextMapping = this._generatedMappings[index + 1];
-
-          if (mapping.generatedLine === nextMapping.generatedLine) {
-            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
-            continue;
-          }
-        }
-
-        // The last mapping for each line spans the entire line.
-        mapping.lastGeneratedColumn = Infinity;
-      }
-    };
-
-  /**
-   * Returns the original source, line, and column information for the generated
-   * source's line and column positions provided. The only argument is an object
-   * with the following properties:
-   *
-   *   - line: The line number in the generated source.
-   *   - column: The column number in the generated source.
-   *
-   * and an object is returned with the following properties:
-   *
-   *   - source: The original source file, or null.
-   *   - line: The line number in the original source, or null.
-   *   - column: The column number in the original source, or null.
-   *   - name: The original identifier, or null.
-   */
-  SourceMapConsumer.prototype.originalPositionFor =
-    function SourceMapConsumer_originalPositionFor(aArgs) {
-      var needle = {
-        generatedLine: util.getArg(aArgs, 'line'),
-        generatedColumn: util.getArg(aArgs, 'column')
-      };
-
-      var index = this._findMapping(needle,
-                                    this._generatedMappings,
-                                    "generatedLine",
-                                    "generatedColumn",
-                                    util.compareByGeneratedPositions);
-
-      if (index >= 0) {
-        var mapping = this._generatedMappings[index];
-
-        if (mapping.generatedLine === needle.generatedLine) {
-          var source = util.getArg(mapping, 'source', null);
-          if (source != null && this.sourceRoot != null) {
-            source = util.join(this.sourceRoot, source);
-          }
-          return {
-            source: source,
-            line: util.getArg(mapping, 'originalLine', null),
-            column: util.getArg(mapping, 'originalColumn', null),
-            name: util.getArg(mapping, 'name', null)
-          };
-        }
-      }
-
-      return {
-        source: null,
-        line: null,
-        column: null,
-        name: null
-      };
-    };
-
-  /**
-   * Returns the original source content. The only argument is the url of the
-   * original source file. Returns null if no original source content is
-   * availible.
-   */
-  SourceMapConsumer.prototype.sourceContentFor =
-    function SourceMapConsumer_sourceContentFor(aSource) {
-      if (!this.sourcesContent) {
-        return null;
-      }
-
-      if (this.sourceRoot != null) {
-        aSource = util.relative(this.sourceRoot, aSource);
-      }
-
-      if (this._sources.has(aSource)) {
-        return this.sourcesContent[this._sources.indexOf(aSource)];
-      }
-
-      var url;
-      if (this.sourceRoot != null
-          && (url = util.urlParse(this.sourceRoot))) {
-        // XXX: file:// URIs and absolute paths lead to unexpected behavior for
-        // many users. We can help them out when they expect file:// URIs to
-        // behave like it would if they were running a local HTTP server. See
-        // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
-        var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
-        if (url.scheme == "file"
-            && this._sources.has(fileUriAbsPath)) {
-          return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
-        }
-
-        if ((!url.path || url.path == "/")
-            && this._sources.has("/" + aSource)) {
-          return this.sourcesContent[this._sources.indexOf("/" + aSource)];
-        }
-      }
-
-      throw new Error('"' + aSource + '" is not in the SourceMap.');
-    };
-
-  /**
-   * Returns the generated line and column information for the original source,
-   * line, and column positions provided. The only argument is an object with
-   * the following properties:
-   *
-   *   - source: The filename of the original source.
-   *   - line: The line number in the original source.
-   *   - column: The column number in the original source.
-   *
-   * and an object is returned with the following properties:
-   *
-   *   - line: The line number in the generated source, or null.
-   *   - column: The column number in the generated source, or null.
-   */
-  SourceMapConsumer.prototype.generatedPositionFor =
-    function SourceMapConsumer_generatedPositionFor(aArgs) {
-      var needle = {
-        source: util.getArg(aArgs, 'source'),
-        originalLine: util.getArg(aArgs, 'line'),
-        originalColumn: util.getArg(aArgs, 'column')
-      };
-
-      if (this.sourceRoot != null) {
-        needle.source = util.relative(this.sourceRoot, needle.source);
-      }
-
-      var index = this._findMapping(needle,
-                                    this._originalMappings,
-                                    "originalLine",
-                                    "originalColumn",
-                                    util.compareByOriginalPositions);
-
-      if (index >= 0) {
-        var mapping = this._originalMappings[index];
-
-        return {
-          line: util.getArg(mapping, 'generatedLine', null),
-          column: util.getArg(mapping, 'generatedColumn', null),
-          lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
-        };
-      }
-
-      return {
-        line: null,
-        column: null,
-        lastColumn: null
-      };
-    };
-
-  /**
-   * Returns all generated line and column information for the original source
-   * and line provided. The only argument is an object with the following
-   * properties:
-   *
-   *   - source: The filename of the original source.
-   *   - line: The line number in the original source.
-   *
-   * and an array of objects is returned, each with the following properties:
-   *
-   *   - line: The line number in the generated source, or null.
-   *   - column: The column number in the generated source, or null.
-   */
-  SourceMapConsumer.prototype.allGeneratedPositionsFor =
-    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
-      // When there is no exact match, SourceMapConsumer.prototype._findMapping
-      // returns the index of the closest mapping less than the needle. By
-      // setting needle.originalColumn to Infinity, we thus find the last
-      // mapping for the given line, provided such a mapping exists.
-      var needle = {
-        source: util.getArg(aArgs, 'source'),
-        originalLine: util.getArg(aArgs, 'line'),
-        originalColumn: Infinity
-      };
-
-      if (this.sourceRoot != null) {
-        needle.source = util.relative(this.sourceRoot, needle.source);
-      }
-
-      var mappings = [];
-
-      var index = this._findMapping(needle,
-                                    this._originalMappings,
-                                    "originalLine",
-                                    "originalColumn",
-                                    util.compareByOriginalPositions);
-      if (index >= 0) {
-        var mapping = this._originalMappings[index];
-
-        while (mapping && mapping.originalLine === needle.originalLine) {
-          mappings.push({
-            line: util.getArg(mapping, 'generatedLine', null),
-            column: util.getArg(mapping, 'generatedColumn', null),
-            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
-          });
-
-          mapping = this._originalMappings[--index];
-        }
-      }
-
-      return mappings.reverse();
+      throw new Error("Subclasses must implement _parseMappings");
     };
 
   SourceMapConsumer.GENERATED_ORDER = 1;
   SourceMapConsumer.ORIGINAL_ORDER = 2;
 
+  SourceMapConsumer.GREATEST_LOWER_BOUND = 1;
+  SourceMapConsumer.LEAST_UPPER_BOUND = 2;
+
   /**
    * Iterate over each mapping between an original source/line/column and a
    * generated line/column in this source map.
    *
    * @param Function aCallback
    *        The function that is called with each mapping.
    * @param Object aContext
    *        Optional. If specified, this object will be the value of `this` every
@@ -583,18 +174,809 @@ define('source-map/source-map-consumer',
           generatedColumn: mapping.generatedColumn,
           originalLine: mapping.originalLine,
           originalColumn: mapping.originalColumn,
           name: mapping.name
         };
       }).forEach(aCallback, context);
     };
 
+  /**
+   * Returns all generated line and column information for the original source,
+   * line, and column provided. If no column is provided, returns all mappings
+   * corresponding to a single line. Otherwise, returns all mappings
+   * corresponding to a single line and column.
+   *
+   * The only argument is an object with the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: Optional. the column number in the original source.
+   *
+   * and an array of objects is returned, each with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  SourceMapConsumer.prototype.allGeneratedPositionsFor =
+    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+      // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping
+      // returns the index of the closest mapping less than the needle. By
+      // setting needle.originalColumn to 0, we thus find the last mapping for
+      // the given line, provided such a mapping exists.
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: util.getArg(aArgs, 'column', 0)
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var mappings = [];
+
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions,
+                                    binarySearch.LEAST_UPPER_BOUND);
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+        var originalLine = mapping.originalLine;
+        var originalColumn = mapping.originalColumn;
+
+        // Iterate until either we run out of mappings, or we run into
+        // a mapping for a different line. Since mappings are sorted, this is
+        // guaranteed to find all mappings for the line we are searching for.
+        while (mapping && mapping.originalLine === originalLine &&
+               (aArgs.column === undefined ||
+                mapping.originalColumn === originalColumn)) {
+          mappings.push({
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          });
+
+          mapping = this._originalMappings[++index];
+        }
+      }
+
+      return mappings;
+    };
+
   exports.SourceMapConsumer = SourceMapConsumer;
 
+  /**
+   * A BasicSourceMapConsumer instance represents a parsed source map which we can
+   * query for information about the original file positions by giving it a file
+   * position in the generated source.
+   *
+   * The only parameter is the raw source map (either as a JSON string, or
+   * already parsed to an object). According to the spec, source maps have the
+   * following attributes:
+   *
+   *   - version: Which version of the source map spec this map is following.
+   *   - sources: An array of URLs to the original source files.
+   *   - names: An array of identifiers which can be referrenced by individual mappings.
+   *   - sourceRoot: Optional. The URL root from which all sources are relative.
+   *   - sourcesContent: Optional. An array of contents of the original source files.
+   *   - mappings: A string of base64 VLQs which contain the actual mappings.
+   *   - file: Optional. The generated file this source map is associated with.
+   *
+   * Here is an example source map, taken from the source map spec[0]:
+   *
+   *     {
+   *       version : 3,
+   *       file: "out.js",
+   *       sourceRoot : "",
+   *       sources: ["foo.js", "bar.js"],
+   *       names: ["src", "maps", "are", "fun"],
+   *       mappings: "AA,AB;;ABCDE;"
+   *     }
+   *
+   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
+   */
+  function BasicSourceMapConsumer(aSourceMap) {
+    var sourceMap = aSourceMap;
+    if (typeof aSourceMap === 'string') {
+      sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+    }
+
+    var version = util.getArg(sourceMap, 'version');
+    var sources = util.getArg(sourceMap, 'sources');
+    // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
+    // requires the array) to play nice here.
+    var names = util.getArg(sourceMap, 'names', []);
+    var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
+    var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
+    var mappings = util.getArg(sourceMap, 'mappings');
+    var file = util.getArg(sourceMap, 'file', null);
+
+    // Once again, Sass deviates from the spec and supplies the version as a
+    // string rather than a number, so we use loose equality checking here.
+    if (version != this._version) {
+      throw new Error('Unsupported version: ' + version);
+    }
+
+    // Some source maps produce relative source paths like "./foo.js" instead of
+    // "foo.js".  Normalize these first so that future comparisons will succeed.
+    // See bugzil.la/1090768.
+    sources = sources.map(util.normalize);
+
+    // Pass `true` below to allow duplicate names and sources. While source maps
+    // are intended to be compressed and deduplicated, the TypeScript compiler
+    // sometimes generates source maps with duplicates in them. See Github issue
+    // #72 and bugzil.la/889492.
+    this._names = ArraySet.fromArray(names, true);
+    this._sources = ArraySet.fromArray(sources, true);
+
+    this.sourceRoot = sourceRoot;
+    this.sourcesContent = sourcesContent;
+    this._mappings = mappings;
+    this.file = file;
+  }
+
+  BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+  BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;
+
+  /**
+   * Create a BasicSourceMapConsumer from a SourceMapGenerator.
+   *
+   * @param SourceMapGenerator aSourceMap
+   *        The source map that will be consumed.
+   * @returns BasicSourceMapConsumer
+   */
+  BasicSourceMapConsumer.fromSourceMap =
+    function SourceMapConsumer_fromSourceMap(aSourceMap) {
+      var smc = Object.create(BasicSourceMapConsumer.prototype);
+
+      smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
+      smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
+      smc.sourceRoot = aSourceMap._sourceRoot;
+      smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
+                                                              smc.sourceRoot);
+      smc.file = aSourceMap._file;
+
+      smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
+      smc.__originalMappings = aSourceMap._mappings.toArray().slice()
+        .sort(util.compareByOriginalPositions);
+
+      return smc;
+    };
+
+  /**
+   * The version of the source mapping spec that we are consuming.
+   */
+  BasicSourceMapConsumer.prototype._version = 3;
+
+  /**
+   * The list of original sources.
+   */
+  Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', {
+    get: function () {
+      return this._sources.toArray().map(function (s) {
+        return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
+      }, this);
+    }
+  });
+
+  /**
+   * Parse the mappings in a string in to a data structure which we can easily
+   * query (the ordered arrays in the `this.__generatedMappings` and
+   * `this.__originalMappings` properties).
+   */
+  BasicSourceMapConsumer.prototype._parseMappings =
+    function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+      var generatedLine = 1;
+      var previousGeneratedColumn = 0;
+      var previousOriginalLine = 0;
+      var previousOriginalColumn = 0;
+      var previousSource = 0;
+      var previousName = 0;
+      var length = aStr.length;
+      var index = 0;
+      var cachedValues = {};
+      var temp = {};
+      var mapping, str, values, end, value;
+
+      while (index < length) {
+        if (aStr.charAt(index) === ';') {
+          generatedLine++;
+          ++index;
+          previousGeneratedColumn = 0;
+        }
+        else if (aStr.charAt(index) === ',') {
+          ++index;
+        }
+        else {
+          mapping = {};
+          mapping.generatedLine = generatedLine;
+
+          // Because each offset is encoded relative to the previous one,
+          // many segments often have the same encoding. We can exploit this
+          // fact by caching the parsed variable length fields of each segment,
+          // allowing us to avoid a second parse if we encounter the same
+          // segment again.
+          for (end = index; end < length; ++end) {
+            if (this._nextCharIsMappingSeparator(aStr, end)) {
+              break;
+            }
+          }
+          str = aStr.slice(index, end);
+
+          values = cachedValues[str];
+          if (values) {
+            index += str.length;
+          } else {
+            values = [];
+            while (index < end) {
+              base64VLQ.decode(aStr, index, temp);
+              value = temp.value;
+              index = temp.rest;
+              values.push(value);
+            }
+            cachedValues[str] = values;
+          }
+
+          // Generated column.
+          mapping.generatedColumn = previousGeneratedColumn + values[0];
+          previousGeneratedColumn = mapping.generatedColumn;
+
+          if (values.length > 1) {
+            // Original source.
+            mapping.source = this._sources.at(previousSource + values[1]);
+            previousSource += values[1];
+            if (values.length === 2) {
+              throw new Error('Found a source, but no line and column');
+            }
+
+            // Original line.
+            mapping.originalLine = previousOriginalLine + values[2];
+            previousOriginalLine = mapping.originalLine;
+            // Lines are stored 0-based
+            mapping.originalLine += 1;
+            if (values.length === 3) {
+              throw new Error('Found a source and line, but no column');
+            }
+
+            // Original column.
+            mapping.originalColumn = previousOriginalColumn + values[3];
+            previousOriginalColumn = mapping.originalColumn;
+
+            if (values.length > 4) {
+              // Original name.
+              mapping.name = this._names.at(previousName + values[4]);
+              previousName += values[4];
+            }
+          }
+
+          this.__generatedMappings.push(mapping);
+          if (typeof mapping.originalLine === 'number') {
+            this.__originalMappings.push(mapping);
+          }
+        }
+      }
+
+      this.__generatedMappings.sort(util.compareByGeneratedPositions);
+      this.__originalMappings.sort(util.compareByOriginalPositions);
+    };
+
+  /**
+   * Find the mapping that best matches the hypothetical "needle" mapping that
+   * we are searching for in the given "haystack" of mappings.
+   */
+  BasicSourceMapConsumer.prototype._findMapping =
+    function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
+                                           aColumnName, aComparator, aBias) {
+      // To return the position we are searching for, we must first find the
+      // mapping for the given position and then return the opposite position it
+      // points to. Because the mappings are sorted, we can use binary search to
+      // find the best mapping.
+
+      if (aNeedle[aLineName] <= 0) {
+        throw new TypeError('Line must be greater than or equal to 1, got '
+                            + aNeedle[aLineName]);
+      }
+      if (aNeedle[aColumnName] < 0) {
+        throw new TypeError('Column must be greater than or equal to 0, got '
+                            + aNeedle[aColumnName]);
+      }
+
+      return binarySearch.search(aNeedle, aMappings, aComparator, aBias);
+    };
+
+  /**
+   * Compute the last column for each generated mapping. The last column is
+   * inclusive.
+   */
+  BasicSourceMapConsumer.prototype.computeColumnSpans =
+    function SourceMapConsumer_computeColumnSpans() {
+      for (var index = 0; index < this._generatedMappings.length; ++index) {
+        var mapping = this._generatedMappings[index];
+
+        // Mappings do not contain a field for the last generated columnt. We
+        // can come up with an optimistic estimate, however, by assuming that
+        // mappings are contiguous (i.e. given two consecutive mappings, the
+        // first mapping ends where the second one starts).
+        if (index + 1 < this._generatedMappings.length) {
+          var nextMapping = this._generatedMappings[index + 1];
+
+          if (mapping.generatedLine === nextMapping.generatedLine) {
+            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+            continue;
+          }
+        }
+
+        // The last mapping for each line spans the entire line.
+        mapping.lastGeneratedColumn = Infinity;
+      }
+    };
+
+  /**
+   * Returns the original source, line, and column information for the generated
+   * source's line and column positions provided. The only argument is an object
+   * with the following properties:
+   *
+   *   - line: The line number in the generated source.
+   *   - column: The column number in the generated source.
+   *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+   *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - source: The original source file, or null.
+   *   - line: The line number in the original source, or null.
+   *   - column: The column number in the original source, or null.
+   *   - name: The original identifier, or null.
+   */
+  BasicSourceMapConsumer.prototype.originalPositionFor =
+    function SourceMapConsumer_originalPositionFor(aArgs) {
+      var needle = {
+        generatedLine: util.getArg(aArgs, 'line'),
+        generatedColumn: util.getArg(aArgs, 'column')
+      };
+
+      var index = this._findMapping(
+        needle,
+        this._generatedMappings,
+        "generatedLine",
+        "generatedColumn",
+        util.compareByGeneratedPositions,
+        util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+      );
+
+      if (index >= 0) {
+        var mapping = this._generatedMappings[index];
+
+        if (mapping.generatedLine === needle.generatedLine) {
+          var source = util.getArg(mapping, 'source', null);
+          if (source != null && this.sourceRoot != null) {
+            source = util.join(this.sourceRoot, source);
+          }
+          return {
+            source: source,
+            line: util.getArg(mapping, 'originalLine', null),
+            column: util.getArg(mapping, 'originalColumn', null),
+            name: util.getArg(mapping, 'name', null)
+          };
+        }
+      }
+
+      return {
+        source: null,
+        line: null,
+        column: null,
+        name: null
+      };
+    };
+
+  /**
+   * Returns the original source content. The only argument is the url of the
+   * original source file. Returns null if no original source content is
+   * availible.
+   */
+  BasicSourceMapConsumer.prototype.sourceContentFor =
+    function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+      if (!this.sourcesContent) {
+        return null;
+      }
+
+      if (this.sourceRoot != null) {
+        aSource = util.relative(this.sourceRoot, aSource);
+      }
+
+      if (this._sources.has(aSource)) {
+        return this.sourcesContent[this._sources.indexOf(aSource)];
+      }
+
+      var url;
+      if (this.sourceRoot != null
+          && (url = util.urlParse(this.sourceRoot))) {
+        // XXX: file:// URIs and absolute paths lead to unexpected behavior for
+        // many users. We can help them out when they expect file:// URIs to
+        // behave like it would if they were running a local HTTP server. See
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
+        var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
+        if (url.scheme == "file"
+            && this._sources.has(fileUriAbsPath)) {
+          return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
+        }
+
+        if ((!url.path || url.path == "/")
+            && this._sources.has("/" + aSource)) {
+          return this.sourcesContent[this._sources.indexOf("/" + aSource)];
+        }
+      }
+
+      // This function is used recursively from
+      // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
+      // don't want to throw if we can't find the source - we just want to
+      // return null, so we provide a flag to exit gracefully.
+      if (nullOnMissing) {
+        return null;
+      }
+      else {
+        throw new Error('"' + aSource + '" is not in the SourceMap.');
+      }
+    };
+
+  /**
+   * Returns the generated line and column information for the original source,
+   * line, and column positions provided. The only argument is an object with
+   * the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: The column number in the original source.
+   *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+   *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  BasicSourceMapConsumer.prototype.generatedPositionFor =
+    function SourceMapConsumer_generatedPositionFor(aArgs) {
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: util.getArg(aArgs, 'column')
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var index = this._findMapping(
+        needle,
+        this._originalMappings,
+        "originalLine",
+        "originalColumn",
+        util.compareByOriginalPositions,
+        util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+      );
+
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
+        if (mapping.source === needle.source) {
+          return {
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          };
+        }
+      }
+
+      return {
+        line: null,
+        column: null,
+        lastColumn: null
+      };
+    };
+
+  exports.BasicSourceMapConsumer = BasicSourceMapConsumer;
+
+  /**
+   * An IndexedSourceMapConsumer instance represents a parsed source map which
+   * we can query for information. It differs from BasicSourceMapConsumer in
+   * that it takes "indexed" source maps (i.e. ones with a "sections" field) as
+   * input.
+   *
+   * The only parameter is a raw source map (either as a JSON string, or already
+   * parsed to an object). According to the spec for indexed source maps, they
+   * have the following attributes:
+   *
+   *   - version: Which version of the source map spec this map is following.
+   *   - file: Optional. The generated file this source map is associated with.
+   *   - sections: A list of section definitions.
+   *
+   * Each value under the "sections" field has two fields:
+   *   - offset: The offset into the original specified at which this section
+   *       begins to apply, defined as an object with a "line" and "column"
+   *       field.
+   *   - map: A source map definition. This source map could also be indexed,
+   *       but doesn't have to be.
+   *
+   * Instead of the "map" field, it's also possible to have a "url" field
+   * specifying a URL to retrieve a source map from, but that's currently
+   * unsupported.
+   *
+   * Here's an example source map, taken from the source map spec[0], but
+   * modified to omit a section which uses the "url" field.
+   *
+   *  {
+   *    version : 3,
+   *    file: "app.js",
+   *    sections: [{
+   *      offset: {line:100, column:10},
+   *      map: {
+   *        version : 3,
+   *        file: "section.js",
+   *        sources: ["foo.js", "bar.js"],
+   *        names: ["src", "maps", "are", "fun"],
+   *        mappings: "AAAA,E;;ABCDE;"
+   *      }
+   *    }],
+   *  }
+   *
+   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt
+   */
+  function IndexedSourceMapConsumer(aSourceMap) {
+    var sourceMap = aSourceMap;
+    if (typeof aSourceMap === 'string') {
+      sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+    }
+
+    var version = util.getArg(sourceMap, 'version');
+    var sections = util.getArg(sourceMap, 'sections');
+
+    if (version != this._version) {
+      throw new Error('Unsupported version: ' + version);
+    }
+
+    var lastOffset = {
+      line: -1,
+      column: 0
+    };
+    this._sections = sections.map(function (s) {
+      if (s.url) {
+        // The url field will require support for asynchronicity.
+        // See https://github.com/mozilla/source-map/issues/16
+        throw new Error('Support for url field in sections not implemented.');
+      }
+      var offset = util.getArg(s, 'offset');
+      var offsetLine = util.getArg(offset, 'line');
+      var offsetColumn = util.getArg(offset, 'column');
+
+      if (offsetLine < lastOffset.line ||
+          (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
+        throw new Error('Section offsets must be ordered and non-overlapping.');
+      }
+      lastOffset = offset;
+
+      return {
+        generatedOffset: {
+          // The offset fields are 0-based, but we use 1-based indices when
+          // encoding/decoding from VLQ.
+          generatedLine: offsetLine + 1,
+          generatedColumn: offsetColumn + 1
+        },
+        consumer: new SourceMapConsumer(util.getArg(s, 'map'))
+      }
+    });
+  }
+
+  IndexedSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+  IndexedSourceMapConsumer.prototype.constructor = SourceMapConsumer;
+
+  /**
+   * The version of the source mapping spec that we are consuming.
+   */
+  IndexedSourceMapConsumer.prototype._version = 3;
+
+  /**
+   * The list of original sources.
+   */
+  Object.defineProperty(IndexedSourceMapConsumer.prototype, 'sources', {
+    get: function () {
+      var sources = [];
+      for (var i = 0; i < this._sections.length; i++) {
+        for (var j = 0; j < this._sections[i].consumer.sources.length; j++) {
+          sources.push(this._sections[i].consumer.sources[j]);
+        }
+      };
+      return sources;
+    }
+  });
+
+  /**
+   * Returns the original source, line, and column information for the generated
+   * source's line and column positions provided. The only argument is an object
+   * with the following properties:
+   *
+   *   - line: The line number in the generated source.
+   *   - column: The column number in the generated source.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - source: The original source file, or null.
+   *   - line: The line number in the original source, or null.
+   *   - column: The column number in the original source, or null.
+   *   - name: The original identifier, or null.
+   */
+  IndexedSourceMapConsumer.prototype.originalPositionFor =
+    function IndexedSourceMapConsumer_originalPositionFor(aArgs) {
+      var needle = {
+        generatedLine: util.getArg(aArgs, 'line'),
+        generatedColumn: util.getArg(aArgs, 'column')
+      };
+
+      // Find the section containing the generated position we're trying to map
+      // to an original position.
+      var sectionIndex = binarySearch.search(needle, this._sections,
+        function(needle, section) {
+          var cmp = needle.generatedLine - section.generatedOffset.generatedLine;
+          if (cmp) {
+            return cmp;
+          }
+
+          return (needle.generatedColumn -
+                  section.generatedOffset.generatedColumn);
+        });
+      var section = this._sections[sectionIndex];
+
+      if (!section) {
+        return {
+          source: null,
+          line: null,
+          column: null,
+          name: null
+        };
+      }
+
+      return section.consumer.originalPositionFor({
+        line: needle.generatedLine -
+          (section.generatedOffset.generatedLine - 1),
+        column: needle.generatedColumn -
+          (section.generatedOffset.generatedLine === needle.generatedLine
+           ? section.generatedOffset.generatedColumn - 1
+           : 0),
+        bias: aArgs.bias
+      });
+    };
+
+  /**
+   * Returns the original source content. The only argument is the url of the
+   * original source file. Returns null if no original source content is
+   * available.
+   */
+  IndexedSourceMapConsumer.prototype.sourceContentFor =
+    function IndexedSourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+
+        var content = section.consumer.sourceContentFor(aSource, true);
+        if (content) {
+          return content;
+        }
+      }
+      if (nullOnMissing) {
+        return null;
+      }
+      else {
+        throw new Error('"' + aSource + '" is not in the SourceMap.');
+      }
+    };
+
+  /**
+   * Returns the generated line and column information for the original source,
+   * line, and column positions provided. The only argument is an object with
+   * the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: The column number in the original source.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  IndexedSourceMapConsumer.prototype.generatedPositionFor =
+    function IndexedSourceMapConsumer_generatedPositionFor(aArgs) {
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+
+        // Only consider this section if the requested source is in the list of
+        // sources of the consumer.
+        if (section.consumer.sources.indexOf(util.getArg(aArgs, 'source')) === -1) {
+          continue;
+        }
+        var generatedPosition = section.consumer.generatedPositionFor(aArgs);
+        if (generatedPosition) {
+          var ret = {
+            line: generatedPosition.line +
+              (section.generatedOffset.generatedLine - 1),
+            column: generatedPosition.column +
+              (section.generatedOffset.generatedLine === generatedPosition.line
+               ? section.generatedOffset.generatedColumn - 1
+               : 0)
+          };
+          return ret;
+        }
+      }
+
+      return {
+        line: null,
+        column: null
+      };
+    };
+
+  /**
+   * Parse the mappings in a string in to a data structure which we can easily
+   * query (the ordered arrays in the `this.__generatedMappings` and
+   * `this.__originalMappings` properties).
+   */
+  IndexedSourceMapConsumer.prototype._parseMappings =
+    function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+      this.__generatedMappings = [];
+      this.__originalMappings = [];
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+        var sectionMappings = section.consumer._generatedMappings;
+        for (var j = 0; j < sectionMappings.length; j++) {
+          var mapping = sectionMappings[i];
+
+          var source = mapping.source;
+          var sourceRoot = section.consumer.sourceRoot;
+
+          if (source != null && sourceRoot != null) {
+            source = util.join(sourceRoot, source);
+          }
+
+          // The mappings coming from the consumer for the section have
+          // generated positions relative to the start of the section, so we
+          // need to offset them to be relative to the start of the concatenated
+          // generated file.
+          var adjustedMapping = {
+            source: source,
+            generatedLine: mapping.generatedLine +
+              (section.generatedOffset.generatedLine - 1),
+            generatedColumn: mapping.column +
+              (section.generatedOffset.generatedLine === mapping.generatedLine)
+              ? section.generatedOffset.generatedColumn - 1
+              : 0,
+            originalLine: mapping.originalLine,
+            originalColumn: mapping.originalColumn,
+            name: mapping.name
+          };
+
+          this.__generatedMappings.push(adjustedMapping);
+          if (typeof adjustedMapping.originalLine === 'number') {
+            this.__originalMappings.push(adjustedMapping);
+          }
+        };
+      };
+
+    this.__generatedMappings.sort(util.compareByGeneratedPositions);
+    this.__originalMappings.sort(util.compareByOriginalPositions);
+  };
+
+  exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;
+
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) {
@@ -846,27 +1228,27 @@ define('source-map/util', ['require', 'e
       return cmp;
     }
 
     cmp = mappingA.originalColumn - mappingB.originalColumn;
     if (cmp || onlyCompareOriginal) {
       return cmp;
     }
 
-    cmp = strcmp(mappingA.name, mappingB.name);
+    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
     if (cmp) {
       return cmp;
     }
 
     cmp = mappingA.generatedLine - mappingB.generatedLine;
     if (cmp) {
       return cmp;
     }
 
-    return mappingA.generatedColumn - mappingB.generatedColumn;
+    return strcmp(mappingA.name, mappingB.name);
   };
   exports.compareByOriginalPositions = compareByOriginalPositions;
 
   /**
    * Comparator between two mappings where the generated positions are
    * compared.
    *
    * Optionally pass in `true` as `onlyCompareGenerated` to consider two
@@ -910,82 +1292,119 @@ define('source-map/util', ['require', 'e
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/binary-search', ['require', 'exports', 'module' , ], function(require, exports, module) {
 
+  exports.GREATEST_LOWER_BOUND = 1;
+  exports.LEAST_UPPER_BOUND = 2;
+
   /**
    * Recursive implementation of binary search.
    *
    * @param aLow Indices here and lower do not contain the needle.
    * @param aHigh Indices here and higher do not contain the needle.
    * @param aNeedle The element being searched for.
    * @param aHaystack The non-empty array being searched.
    * @param aCompare Function which takes two elements and returns -1, 0, or 1.
+   * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+   *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
    */
-  function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) {
+  function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare, aBias) {
     // This function terminates when one of the following is true:
     //
     //   1. We find the exact element we are looking for.
     //
     //   2. We did not find the exact element, but we can return the index of
-    //      the next closest element that is less than that element.
+    //      the next-closest element.
     //
     //   3. We did not find the exact element, and there is no next-closest
-    //      element which is less than the one we are searching for, so we
-    //      return -1.
+    //      element than the one we are searching for, so we return -1.
     var mid = Math.floor((aHigh - aLow) / 2) + aLow;
     var cmp = aCompare(aNeedle, aHaystack[mid], true);
     if (cmp === 0) {
       // Found the element we are looking for.
       return mid;
     }
     else if (cmp > 0) {
-      // aHaystack[mid] is greater than our needle.
+      // Our needle is greater than aHaystack[mid].
       if (aHigh - mid > 1) {
         // The element is in the upper half.
-        return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare);
+        return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare, aBias);
       }
-      // We did not find an exact match, return the next closest one
-      // (termination case 2).
-      return mid;
+
+      // The exact needle element was not found in this haystack. Determine if
+      // we are in termination case (3) or (2) and return the appropriate thing.
+      if (aBias == exports.LEAST_UPPER_BOUND) {
+        return aHigh < aHaystack.length ? aHigh : -1;
+      } else {
+        return mid;
+      }
     }
     else {
-      // aHaystack[mid] is less than our needle.
+      // Our needle is less than aHaystack[mid].
       if (mid - aLow > 1) {
         // The element is in the lower half.
-        return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare);
+        return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare, aBias);
       }
-      // The exact needle element was not found in this haystack. Determine if
-      // we are in termination case (2) or (3) and return the appropriate thing.
-      return aLow < 0 ? -1 : aLow;
+
+      // we are in termination case (3) or (2) and return the appropriate thing.
+      if (aBias == exports.LEAST_UPPER_BOUND) {
+        return mid;
+      } else {
+        return aLow < 0 ? -1 : aLow;
+      }
     }
   }
 
   /**
    * This is an implementation of binary search which will always try and return
-   * the index of next lowest value checked if there is no exact hit. This is
-   * because mappings between original and generated line/col pairs are single
-   * points, and there is an implicit region between each of them, so a miss
-   * just means that you aren't on the very start of a region.
+   * the index of the closest element if there is no exact hit. This is because
+   * mappings between original and generated line/col pairs are single points,
+   * and there is an implicit region between each of them, so a miss just means
+   * that you aren't on the very start of a region.
    *
    * @param aNeedle The element you are looking for.
    * @param aHaystack The array that is being searched.
    * @param aCompare A function which takes the needle and an element in the
    *     array and returns -1, 0, or 1 depending on whether the needle is less
    *     than, equal to, or greater than the element, respectively.
+   * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+   *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'binarySearch.GREATEST_LOWER_BOUND'.
    */
-  exports.search = function search(aNeedle, aHaystack, aCompare) {
+  exports.search = function search(aNeedle, aHaystack, aCompare, aBias) {
     if (aHaystack.length === 0) {
       return -1;
     }
-    return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
+
+    var index = recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack,
+                                aCompare, aBias || exports.GREATEST_LOWER_BOUND);
+    if (index < 0) {
+      return -1;
+    }
+
+    // We have found either the exact element, or the next-closest element than
+    // the one we are searching for. However, there may be more than one such
+    // element. Make sure we always return the smallest of these.
+    while (index - 1 >= 0) {
+      if (aCompare(aHaystack[index], aHaystack[index - 1], true) !== 0) {
+        break;
+      }
+      --index;
+    }
+
+    return index;
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -1138,29 +1557,29 @@ define('source-map/base64-vlq', ['requir
   // binary: 011111
   var VLQ_BASE_MASK = VLQ_BASE - 1;
 
   // binary: 100000
   var VLQ_CONTINUATION_BIT = VLQ_BASE;
 
   /**
    * Converts from a two-complement value to a value where the sign bit is
-   * is placed in the least significant bit.  For example, as decimals:
+   * placed in the least significant bit.  For example, as decimals:
    *   1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
    *   2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
    */
   function toVLQSigned(aValue) {
     return aValue < 0
       ? ((-aValue) << 1) + 1
       : (aValue << 1) + 0;
   }
 
   /**
    * Converts to a two-complement value from a value where the sign bit is
-   * is placed in the least significant bit.  For example, as decimals:
+   * placed in the least significant bit.  For example, as decimals:
    *   2 (10 binary) becomes 1, 3 (11 binary) becomes -1
    *   4 (100 binary) becomes 2, 5 (101 binary) becomes -2
    */
   function fromVLQSigned(aValue) {
     var isNegative = (aValue & 1) === 1;
     var shifted = aValue >> 1;
     return isNegative
       ? -shifted
@@ -1189,36 +1608,35 @@ define('source-map/base64-vlq', ['requir
 
     return encoded;
   };
 
   /**
    * Decodes the next base 64 VLQ value from the given string and returns the
    * value and the rest of the string via the out parameter.
    */
-  exports.decode = function base64VLQ_decode(aStr, aOutParam) {
-    var i = 0;
+  exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {
     var strLen = aStr.length;
     var result = 0;
     var shift = 0;
     var continuation, digit;
 
     do {
-      if (i >= strLen) {
+      if (aIndex >= strLen) {
         throw new Error("Expected more digits in base 64 VLQ value.");
       }
-      digit = base64.decode(aStr.charAt(i++));
+      digit = base64.decode(aStr.charAt(aIndex++));
       continuation = !!(digit & VLQ_CONTINUATION_BIT);
       digit &= VLQ_BASE_MASK;
       result = result + (digit << shift);
       shift += VLQ_BASE_SHIFT;
     } while (continuation);
 
     aOutParam.value = fromVLQSigned(result);
-    aOutParam.rest = aStr.slice(i);
+    aOutParam.rest = aIndex;
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -1257,39 +1675,41 @@ define('source-map/base64', ['require', 
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
-define('source-map/source-map-generator', ['require', 'exports', 'module' ,  'source-map/base64-vlq', 'source-map/util', 'source-map/array-set'], function(require, exports, module) {
+define('source-map/source-map-generator', ['require', 'exports', 'module' ,  'source-map/base64-vlq', 'source-map/util', 'source-map/array-set', 'source-map/mapping-list'], function(require, exports, module) {
 
   var base64VLQ = require('source-map/base64-vlq');
   var util = require('source-map/util');
   var ArraySet = require('source-map/array-set').ArraySet;
+  var MappingList = require('source-map/mapping-list').MappingList;
 
   /**
    * An instance of the SourceMapGenerator represents a source map which is
    * being built incrementally. You may pass an object with the following
    * properties:
    *
    *   - file: The filename of the generated source.
    *   - sourceRoot: A root for all relative URLs in this source map.
    */
   function SourceMapGenerator(aArgs) {
     if (!aArgs) {
       aArgs = {};
     }
     this._file = util.getArg(aArgs, 'file', null);
     this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
+    this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
     this._sources = new ArraySet();
     this._names = new ArraySet();
-    this._mappings = [];
+    this._mappings = new MappingList();
     this._sourcesContents = null;
   }
 
   SourceMapGenerator.prototype._version = 3;
 
   /**
    * Creates a new SourceMapGenerator based on a SourceMapConsumer
    *
@@ -1349,27 +1769,29 @@ define('source-map/source-map-generator'
    */
   SourceMapGenerator.prototype.addMapping =
     function SourceMapGenerator_addMapping(aArgs) {
       var generated = util.getArg(aArgs, 'generated');
       var original = util.getArg(aArgs, 'original', null);
       var source = util.getArg(aArgs, 'source', null);
       var name = util.getArg(aArgs, 'name', null);
 
-      this._validateMapping(generated, original, source, name);
+      if (!this._skipValidation) {
+        this._validateMapping(generated, original, source, name);
+      }
 
       if (source != null && !this._sources.has(source)) {
         this._sources.add(source);
       }
 
       if (name != null && !this._names.has(name)) {
         this._names.add(name);
       }
 
-      this._mappings.push({
+      this._mappings.add({
         generatedLine: generated.line,
         generatedColumn: generated.column,
         originalLine: original != null && original.line,
         originalColumn: original != null && original.column,
         source: source,
         name: name
       });
     };
@@ -1436,17 +1858,17 @@ define('source-map/source-map-generator'
         sourceFile = util.relative(sourceRoot, sourceFile);
       }
       // Applying the SourceMap can add and remove items from the sources and
       // the names array.
       var newSources = new ArraySet();
       var newNames = new ArraySet();
 
       // Find mappings for the "sourceFile"
-      this._mappings.forEach(function (mapping) {
+      this._mappings.unsortedForEach(function (mapping) {
         if (mapping.source === sourceFile && mapping.originalLine != null) {
           // Check if it can be mapped by the source map, then update the mapping.
           var original = aSourceMapConsumer.originalPositionFor({
             line: mapping.originalLine,
             column: mapping.originalColumn
           });
           if (original.source != null) {
             // Copy mapping
@@ -1542,36 +1964,31 @@ define('source-map/source-map-generator'
       var previousGeneratedLine = 1;
       var previousOriginalColumn = 0;
       var previousOriginalLine = 0;
       var previousName = 0;
       var previousSource = 0;
       var result = '';
       var mapping;
 
-      // The mappings must be guaranteed to be in sorted order before we start
-      // serializing them or else the generated line numbers (which are defined
-      // via the ';' separators) will be all messed up. Note: it might be more
-      // performant to maintain the sorting as we insert them, rather than as we
-      // serialize them, but the big O is the same either way.
-      this._mappings.sort(util.compareByGeneratedPositions);
+      var mappings = this._mappings.toArray();
 
-      for (var i = 0, len = this._mappings.length; i < len; i++) {
-        mapping = this._mappings[i];
+      for (var i = 0, len = mappings.length; i < len; i++) {
+        mapping = mappings[i];
 
         if (mapping.generatedLine !== previousGeneratedLine) {
           previousGeneratedColumn = 0;
           while (mapping.generatedLine !== previousGeneratedLine) {
             result += ';';
             previousGeneratedLine++;
           }
         }
         else {
           if (i > 0) {
-            if (!util.compareByGeneratedPositions(mapping, this._mappings[i - 1])) {
+            if (!util.compareByGeneratedPositions(mapping, mappings[i - 1])) {
               continue;
             }
             result += ',';
           }
         }
 
         result += base64VLQ.encode(mapping.generatedColumn
                                    - previousGeneratedColumn);
@@ -1643,39 +2060,127 @@ define('source-map/source-map-generator'
       return map;
     };
 
   /**
    * Render the source map being generated to a string.
    */
   SourceMapGenerator.prototype.toString =
     function SourceMapGenerator_toString() {
-      return JSON.stringify(this);
+      return JSON.stringify(this.toJSON());
     };
 
   exports.SourceMapGenerator = SourceMapGenerator;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
+ * Copyright 2014 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+define('source-map/mapping-list', ['require', 'exports', 'module' ,  'source-map/util'], function(require, exports, module) {
+
+  var util = require('source-map/util');
+
+  /**
+   * Determine whether mappingB is after mappingA with respect to generated
+   * position.
+   */
+  function generatedPositionAfter(mappingA, mappingB) {
+    // Optimized for most common case
+    var lineA = mappingA.generatedLine;
+    var lineB = mappingB.generatedLine;
+    var columnA = mappingA.generatedColumn;
+    var columnB = mappingB.generatedColumn;
+    return lineB > lineA || lineB == lineA && columnB >= columnA ||
+           util.compareByGeneratedPositions(mappingA, mappingB) <= 0;
+  }
+
+  /**
+   * A data structure to provide a sorted view of accumulated mappings in a
+   * performance conscious manner. It trades a neglibable overhead in general
+   * case for a large speedup in case of mappings being added in order.
+   */
+  function MappingList() {
+    this._array = [];
+    this._sorted = true;
+    // Serves as infimum
+    this._last = {generatedLine: -1, generatedColumn: 0};
+  }
+
+  /**
+   * Iterate through internal items. This method takes the same arguments that
+   * `Array.prototype.forEach` takes.
+   *
+   * NOTE: The order of the mappings is NOT guaranteed.
+   */
+  MappingList.prototype.unsortedForEach =
+    function MappingList_forEach(aCallback, aThisArg) {
+      this._array.forEach(aCallback, aThisArg);
+    };
+
+  /**
+   * Add the given source mapping.
+   *
+   * @param Object aMapping
+   */
+  MappingList.prototype.add = function MappingList_add(aMapping) {
+    var mapping;
+    if (generatedPositionAfter(this._last, aMapping)) {
+      this._last = aMapping;
+      this._array.push(aMapping);
+    } else {
+      this._sorted = false;
+      this._array.push(aMapping);
+    }
+  };
+
+  /**
+   * Returns the flat, sorted array of mappings. The mappings are sorted by
+   * generated position.
+   *
+   * WARNING: This method returns internal data without copying, for
+   * performance. The return value must NOT be mutated, and should be treated as
+   * an immutable borrow. If you want to take ownership, you must make your own
+   * copy.
+   */
+  MappingList.prototype.toArray = function MappingList_toArray() {
+    if (!this._sorted) {
+      this._array.sort(util.compareByGeneratedPositions);
+      this._sorted = true;
+    }
+    return this._array;
+  };
+
+  exports.MappingList = MappingList;
+
+});
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/source-node', ['require', 'exports', 'module' ,  'source-map/source-map-generator', 'source-map/util'], function(require, exports, module) {
 
   var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
   var util = require('source-map/util');
 
   // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
   // operating systems these days (capturing the result).
   var REGEX_NEWLINE = /(\r?\n)/;
 
-  // Matches a Windows-style newline, or any character.
-  var REGEX_CHARACTER = /\r\n|[\s\S]/g;
+  // Newline character code for charCodeAt() comparisons
+  var NEWLINE_CODE = 10;
+
+  // Private symbol for identifying `SourceNode`s when multiple versions of
+  // the source-map library are loaded. This MUST NOT CHANGE across
+  // versions!
+  var isSourceNode = "$$$isSourceNode$$$";
 
   /**
    * SourceNodes provide a way to abstract over interpolating/concatenating
    * snippets of generated JavaScript source code while maintaining the line and
    * column information associated with the original source code.
    *
    * @param aLine The original line number.
    * @param aColumn The original column number.
@@ -1686,16 +2191,17 @@ define('source-map/source-node', ['requi
    */
   function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
     this.children = [];
     this.sourceContents = {};
     this.line = aLine == null ? null : aLine;
     this.column = aColumn == null ? null : aColumn;
     this.source = aSource == null ? null : aSource;
     this.name = aName == null ? null : aName;
+    this[isSourceNode] = true;
     if (aChunks != null) this.add(aChunks);
   }
 
   /**
    * Creates a SourceNode from generated code and a SourceMapConsumer.
    *
    * @param aGeneratedCode The generated code
    * @param aSourceMapConsumer The SourceMap for the generated code
@@ -1816,17 +2322,17 @@ define('source-map/source-node', ['requi
    *        SourceNode, or an array where each member is one of those things.
    */
   SourceNode.prototype.add = function SourceNode_add(aChunk) {
     if (Array.isArray(aChunk)) {
       aChunk.forEach(function (chunk) {
         this.add(chunk);
       }, this);
     }
-    else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
+    else if (aChunk[isSourceNode] || typeof aChunk === "string") {
       if (aChunk) {
         this.children.push(aChunk);
       }
     }
     else {
       throw new TypeError(
         "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
       );
@@ -1841,17 +2347,17 @@ define('source-map/source-node', ['requi
    *        SourceNode, or an array where each member is one of those things.
    */
   SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
     if (Array.isArray(aChunk)) {
       for (var i = aChunk.length-1; i >= 0; i--) {
         this.prepend(aChunk[i]);
       }
     }
-    else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
+    else if (aChunk[isSourceNode] || typeof aChunk === "string") {
       this.children.unshift(aChunk);
     }
     else {
       throw new TypeError(
         "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
       );
     }
     return this;
@@ -1863,17 +2369,17 @@ define('source-map/source-node', ['requi
    * snippet and the its original associated source's line/column location.
    *
    * @param aFn The traversal function.
    */
   SourceNode.prototype.walk = function SourceNode_walk(aFn) {
     var chunk;
     for (var i = 0, len = this.children.length; i < len; i++) {
       chunk = this.children[i];
-      if (chunk instanceof SourceNode) {
+      if (chunk[isSourceNode]) {
         chunk.walk(aFn);
       }
       else {
         if (chunk !== '') {
           aFn(chunk, { source: this.source,
                        line: this.line,
                        column: this.column,
                        name: this.name });
@@ -1908,17 +2414,17 @@ define('source-map/source-node', ['requi
    * Call String.prototype.replace on the very right-most source snippet. Useful
    * for trimming whitespace from the end of a source node, etc.
    *
    * @param aPattern The pattern to replace.
    * @param aReplacement The thing to replace the pattern with.
    */
   SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
     var lastChild = this.children[this.children.length - 1];
-    if (lastChild instanceof SourceNode) {
+    if (lastChild[isSourceNode]) {
       lastChild.replaceRight(aPattern, aReplacement);
     }
     else if (typeof lastChild === 'string') {
       this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
     }
     else {
       this.children.push(''.replace(aPattern, aReplacement));
     }
@@ -1941,17 +2447,17 @@ define('source-map/source-node', ['requi
    * Walk over the tree of SourceNodes. The walking function is called for each
    * source file content and is passed the filename and source content.
    *
    * @param aFn The traversal function.
    */
   SourceNode.prototype.walkSourceContents =
     function SourceNode_walkSourceContents(aFn) {
       for (var i = 0, len = this.children.length; i < len; i++) {
-        if (this.children[i] instanceof SourceNode) {
+        if (this.children[i][isSourceNode]) {
           this.children[i].walkSourceContents(aFn);
         }
       }
 
       var sources = Object.keys(this.sourceContents);
       for (var i = 0, len = sources.length; i < len; i++) {
         aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
       }
@@ -2017,22 +2523,22 @@ define('source-map/source-node', ['requi
           generated: {
             line: generated.line,
             column: generated.column
           }
         });
         lastOriginalSource = null;
         sourceMappingActive = false;
       }
-      chunk.match(REGEX_CHARACTER).forEach(function (ch, idx, array) {
-        if (REGEX_NEWLINE.test(ch)) {
+      for (var idx = 0, length = chunk.length; idx < length; idx++) {
+        if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
           generated.line++;
           generated.column = 0;
           // Mappings end at eol
-          if (idx + 1 === array.length) {
+          if (idx + 1 === length) {
             lastOriginalSource = null;
             sourceMappingActive = false;
           } else if (sourceMappingActive) {
             map.addMapping({
               source: original.source,
               original: {
                 line: original.line,
                 column: original.column
@@ -2040,19 +2546,19 @@ define('source-map/source-node', ['requi
               generated: {
                 line: generated.line,
                 column: generated.column
               },
               name: original.name
             });
           }
         } else {
-          generated.column += ch.length;
+          generated.column++;
         }
-      });
+      }
     });
     this.walkSourceContents(function (sourceFile, sourceContent) {
       map.setSourceContent(sourceFile, sourceContent);
     });
 
     return { code: generated.code, map: map };
   };
 
--- a/toolkit/devtools/sourcemap/source-map.js
+++ b/toolkit/devtools/sourcemap/source-map.js
@@ -151,39 +151,41 @@ define.Domain = Domain;
 define.globalDomain = new Domain();
 var require = define.globalDomain.require.bind(define.globalDomain);
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
-define('source-map/source-map-generator', ['require', 'exports', 'module' ,  'source-map/base64-vlq', 'source-map/util', 'source-map/array-set'], function(require, exports, module) {
+define('source-map/source-map-generator', ['require', 'exports', 'module' ,  'source-map/base64-vlq', 'source-map/util', 'source-map/array-set', 'source-map/mapping-list'], function(require, exports, module) {
 
   var base64VLQ = require('./base64-vlq');
   var util = require('./util');
   var ArraySet = require('./array-set').ArraySet;
+  var MappingList = require('./mapping-list').MappingList;
 
   /**
    * An instance of the SourceMapGenerator represents a source map which is
    * being built incrementally. You may pass an object with the following
    * properties:
    *
    *   - file: The filename of the generated source.
    *   - sourceRoot: A root for all relative URLs in this source map.
    */
   function SourceMapGenerator(aArgs) {
     if (!aArgs) {
       aArgs = {};
     }
     this._file = util.getArg(aArgs, 'file', null);
     this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
+    this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
     this._sources = new ArraySet();
     this._names = new ArraySet();
-    this._mappings = [];
+    this._mappings = new MappingList();
     this._sourcesContents = null;
   }
 
   SourceMapGenerator.prototype._version = 3;
 
   /**
    * Creates a new SourceMapGenerator based on a SourceMapConsumer
    *
@@ -243,27 +245,29 @@ define('source-map/source-map-generator'
    */
   SourceMapGenerator.prototype.addMapping =
     function SourceMapGenerator_addMapping(aArgs) {
       var generated = util.getArg(aArgs, 'generated');
       var original = util.getArg(aArgs, 'original', null);
       var source = util.getArg(aArgs, 'source', null);
       var name = util.getArg(aArgs, 'name', null);
 
-      this._validateMapping(generated, original, source, name);
+      if (!this._skipValidation) {
+        this._validateMapping(generated, original, source, name);
+      }
 
       if (source != null && !this._sources.has(source)) {
         this._sources.add(source);
       }
 
       if (name != null && !this._names.has(name)) {
         this._names.add(name);
       }
 
-      this._mappings.push({
+      this._mappings.add({
         generatedLine: generated.line,
         generatedColumn: generated.column,
         originalLine: original != null && original.line,
         originalColumn: original != null && original.column,
         source: source,
         name: name
       });
     };
@@ -330,17 +334,17 @@ define('source-map/source-map-generator'
         sourceFile = util.relative(sourceRoot, sourceFile);
       }
       // Applying the SourceMap can add and remove items from the sources and
       // the names array.
       var newSources = new ArraySet();
       var newNames = new ArraySet();
 
       // Find mappings for the "sourceFile"
-      this._mappings.forEach(function (mapping) {
+      this._mappings.unsortedForEach(function (mapping) {
         if (mapping.source === sourceFile && mapping.originalLine != null) {
           // Check if it can be mapped by the source map, then update the mapping.
           var original = aSourceMapConsumer.originalPositionFor({
             line: mapping.originalLine,
             column: mapping.originalColumn
           });
           if (original.source != null) {
             // Copy mapping
@@ -436,36 +440,31 @@ define('source-map/source-map-generator'
       var previousGeneratedLine = 1;
       var previousOriginalColumn = 0;
       var previousOriginalLine = 0;
       var previousName = 0;
       var previousSource = 0;
       var result = '';
       var mapping;
 
-      // The mappings must be guaranteed to be in sorted order before we start
-      // serializing them or else the generated line numbers (which are defined
-      // via the ';' separators) will be all messed up. Note: it might be more
-      // performant to maintain the sorting as we insert them, rather than as we
-      // serialize them, but the big O is the same either way.
-      this._mappings.sort(util.compareByGeneratedPositions);
+      var mappings = this._mappings.toArray();
 
-      for (var i = 0, len = this._mappings.length; i < len; i++) {
-        mapping = this._mappings[i];
+      for (var i = 0, len = mappings.length; i < len; i++) {
+        mapping = mappings[i];
 
         if (mapping.generatedLine !== previousGeneratedLine) {
           previousGeneratedColumn = 0;
           while (mapping.generatedLine !== previousGeneratedLine) {
             result += ';';
             previousGeneratedLine++;
           }
         }
         else {
           if (i > 0) {
-            if (!util.compareByGeneratedPositions(mapping, this._mappings[i - 1])) {
+            if (!util.compareByGeneratedPositions(mapping, mappings[i - 1])) {
               continue;
             }
             result += ',';
           }
         }
 
         result += base64VLQ.encode(mapping.generatedColumn
                                    - previousGeneratedColumn);
@@ -537,17 +536,17 @@ define('source-map/source-map-generator'
       return map;
     };
 
   /**
    * Render the source map being generated to a string.
    */
   SourceMapGenerator.prototype.toString =
     function SourceMapGenerator_toString() {
-      return JSON.stringify(this);
+      return JSON.stringify(this.toJSON());
     };
 
   exports.SourceMapGenerator = SourceMapGenerator;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
@@ -608,29 +607,29 @@ define('source-map/base64-vlq', ['requir
   // binary: 011111
   var VLQ_BASE_MASK = VLQ_BASE - 1;
 
   // binary: 100000
   var VLQ_CONTINUATION_BIT = VLQ_BASE;
 
   /**
    * Converts from a two-complement value to a value where the sign bit is
-   * is placed in the least significant bit.  For example, as decimals:
+   * placed in the least significant bit.  For example, as decimals:
    *   1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
    *   2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
    */
   function toVLQSigned(aValue) {
     return aValue < 0
       ? ((-aValue) << 1) + 1
       : (aValue << 1) + 0;
   }
 
   /**
    * Converts to a two-complement value from a value where the sign bit is
-   * is placed in the least significant bit.  For example, as decimals:
+   * placed in the least significant bit.  For example, as decimals:
    *   2 (10 binary) becomes 1, 3 (11 binary) becomes -1
    *   4 (100 binary) becomes 2, 5 (101 binary) becomes -2
    */
   function fromVLQSigned(aValue) {
     var isNegative = (aValue & 1) === 1;
     var shifted = aValue >> 1;
     return isNegative
       ? -shifted
@@ -659,36 +658,35 @@ define('source-map/base64-vlq', ['requir
 
     return encoded;
   };
 
   /**
    * Decodes the next base 64 VLQ value from the given string and returns the
    * value and the rest of the string via the out parameter.
    */
-  exports.decode = function base64VLQ_decode(aStr, aOutParam) {
-    var i = 0;
+  exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {
     var strLen = aStr.length;
     var result = 0;
     var shift = 0;
     var continuation, digit;
 
     do {
-      if (i >= strLen) {
+      if (aIndex >= strLen) {
         throw new Error("Expected more digits in base 64 VLQ value.");
       }
-      digit = base64.decode(aStr.charAt(i++));
+      digit = base64.decode(aStr.charAt(aIndex++));
       continuation = !!(digit & VLQ_CONTINUATION_BIT);
       digit &= VLQ_BASE_MASK;
       result = result + (digit << shift);
       shift += VLQ_BASE_SHIFT;
     } while (continuation);
 
     aOutParam.value = fromVLQSigned(result);
-    aOutParam.rest = aStr.slice(i);
+    aOutParam.rest = aIndex;
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -981,27 +979,27 @@ define('source-map/util', ['require', 'e
       return cmp;
     }
 
     cmp = mappingA.originalColumn - mappingB.originalColumn;
     if (cmp || onlyCompareOriginal) {
       return cmp;
     }
 
-    cmp = strcmp(mappingA.name, mappingB.name);
+    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
     if (cmp) {
       return cmp;
     }
 
     cmp = mappingA.generatedLine - mappingB.generatedLine;
     if (cmp) {
       return cmp;
     }
 
-    return mappingA.generatedColumn - mappingB.generatedColumn;
+    return strcmp(mappingA.name, mappingB.name);
   };
   exports.compareByOriginalPositions = compareByOriginalPositions;
 
   /**
    * Comparator between two mappings where the generated positions are
    * compared.
    *
    * Optionally pass in `true` as `onlyCompareGenerated` to consider two
@@ -1133,139 +1131,130 @@ define('source-map/array-set', ['require
     return this._array.slice();
   };
 
   exports.ArraySet = ArraySet;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
+ * Copyright 2014 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+define('source-map/mapping-list', ['require', 'exports', 'module' ,  'source-map/util'], function(require, exports, module) {
+
+  var util = require('./util');
+
+  /**
+   * Determine whether mappingB is after mappingA with respect to generated
+   * position.
+   */
+  function generatedPositionAfter(mappingA, mappingB) {
+    // Optimized for most common case
+    var lineA = mappingA.generatedLine;
+    var lineB = mappingB.generatedLine;
+    var columnA = mappingA.generatedColumn;
+    var columnB = mappingB.generatedColumn;
+    return lineB > lineA || lineB == lineA && columnB >= columnA ||
+           util.compareByGeneratedPositions(mappingA, mappingB) <= 0;
+  }
+
+  /**
+   * A data structure to provide a sorted view of accumulated mappings in a
+   * performance conscious manner. It trades a neglibable overhead in general
+   * case for a large speedup in case of mappings being added in order.
+   */
+  function MappingList() {
+    this._array = [];
+    this._sorted = true;
+    // Serves as infimum
+    this._last = {generatedLine: -1, generatedColumn: 0};
+  }
+
+  /**
+   * Iterate through internal items. This method takes the same arguments that
+   * `Array.prototype.forEach` takes.
+   *
+   * NOTE: The order of the mappings is NOT guaranteed.
+   */
+  MappingList.prototype.unsortedForEach =
+    function MappingList_forEach(aCallback, aThisArg) {
+      this._array.forEach(aCallback, aThisArg);
+    };
+
+  /**
+   * Add the given source mapping.
+   *
+   * @param Object aMapping
+   */
+  MappingList.prototype.add = function MappingList_add(aMapping) {
+    var mapping;
+    if (generatedPositionAfter(this._last, aMapping)) {
+      this._last = aMapping;
+      this._array.push(aMapping);
+    } else {
+      this._sorted = false;
+      this._array.push(aMapping);
+    }
+  };
+
+  /**
+   * Returns the flat, sorted array of mappings. The mappings are sorted by
+   * generated position.
+   *
+   * WARNING: This method returns internal data without copying, for
+   * performance. The return value must NOT be mutated, and should be treated as
+   * an immutable borrow. If you want to take ownership, you must make your own
+   * copy.
+   */
+  MappingList.prototype.toArray = function MappingList_toArray() {
+    if (!this._sorted) {
+      this._array.sort(util.compareByGeneratedPositions);
+      this._sorted = true;
+    }
+    return this._array;
+  };
+
+  exports.MappingList = MappingList;
+
+});
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/source-map-consumer', ['require', 'exports', 'module' ,  'source-map/util', 'source-map/binary-search', 'source-map/array-set', 'source-map/base64-vlq'], function(require, exports, module) {
 
   var util = require('./util');
   var binarySearch = require('./binary-search');
   var ArraySet = require('./array-set').ArraySet;
   var base64VLQ = require('./base64-vlq');
 
-  /**
-   * A SourceMapConsumer instance represents a parsed source map which we can
-   * query for information about the original file positions by giving it a file
-   * position in the generated source.
-   *
-   * The only parameter is the raw source map (either as a JSON string, or
-   * already parsed to an object). According to the spec, source maps have the
-   * following attributes:
-   *
-   *   - version: Which version of the source map spec this map is following.
-   *   - sources: An array of URLs to the original source files.
-   *   - names: An array of identifiers which can be referrenced by individual mappings.
-   *   - sourceRoot: Optional. The URL root from which all sources are relative.
-   *   - sourcesContent: Optional. An array of contents of the original source files.
-   *   - mappings: A string of base64 VLQs which contain the actual mappings.
-   *   - file: Optional. The generated file this source map is associated with.
-   *
-   * Here is an example source map, taken from the source map spec[0]:
-   *
-   *     {
-   *       version : 3,
-   *       file: "out.js",
-   *       sourceRoot : "",
-   *       sources: ["foo.js", "bar.js"],
-   *       names: ["src", "maps", "are", "fun"],
-   *       mappings: "AA,AB;;ABCDE;"
-   *     }
-   *
-   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
-   */
   function SourceMapConsumer(aSourceMap) {
     var sourceMap = aSourceMap;
     if (typeof aSourceMap === 'string') {
       sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
     }
 
-    var version = util.getArg(sourceMap, 'version');
-    var sources = util.getArg(sourceMap, 'sources');
-    // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
-    // requires the array) to play nice here.
-    var names = util.getArg(sourceMap, 'names', []);
-    var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
-    var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
-    var mappings = util.getArg(sourceMap, 'mappings');
-    var file = util.getArg(sourceMap, 'file', null);
-
-    // Once again, Sass deviates from the spec and supplies the version as a
-    // string rather than a number, so we use loose equality checking here.
-    if (version != this._version) {
-      throw new Error('Unsupported version: ' + version);
-    }
-
-    // Some source maps produce relative source paths like "./foo.js" instead of
-    // "foo.js".  Normalize these first so that future comparisons will succeed.
-    // See bugzil.la/1090768.
-    sources = sources.map(util.normalize);
-
-    // Pass `true` below to allow duplicate names and sources. While source maps
-    // are intended to be compressed and deduplicated, the TypeScript compiler
-    // sometimes generates source maps with duplicates in them. See Github issue
-    // #72 and bugzil.la/889492.
-    this._names = ArraySet.fromArray(names, true);
-    this._sources = ArraySet.fromArray(sources, true);
-
-    this.sourceRoot = sourceRoot;
-    this.sourcesContent = sourcesContent;
-    this._mappings = mappings;
-    this.file = file;
+    return sourceMap.sections != null
+      ? new IndexedSourceMapConsumer(sourceMap)
+      : new BasicSourceMapConsumer(sourceMap);
   }
 
-  /**
-   * Create a SourceMapConsumer from a SourceMapGenerator.
-   *
-   * @param SourceMapGenerator aSourceMap
-   *        The source map that will be consumed.
-   * @returns SourceMapConsumer
-   */
-  SourceMapConsumer.fromSourceMap =
-    function SourceMapConsumer_fromSourceMap(aSourceMap) {
-      var smc = Object.create(SourceMapConsumer.prototype);
-
-      smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
-      smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
-      smc.sourceRoot = aSourceMap._sourceRoot;
-      smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
-                                                              smc.sourceRoot);
-      smc.file = aSourceMap._file;
-
-      smc.__generatedMappings = aSourceMap._mappings.slice()
-        .sort(util.compareByGeneratedPositions);
-      smc.__originalMappings = aSourceMap._mappings.slice()
-        .sort(util.compareByOriginalPositions);
-
-      return smc;
-    };
+  SourceMapConsumer.fromSourceMap = function(aSourceMap) {
+    return BasicSourceMapConsumer.fromSourceMap(aSourceMap);
+  }
 
   /**
    * The version of the source mapping spec that we are consuming.
    */
   SourceMapConsumer.prototype._version = 3;
 
-  /**
-   * The list of original sources.
-   */
-  Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
-    get: function () {
-      return this._sources.toArray().map(function (s) {
-        return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
-      }, this);
-    }
-  });
-
   // `__generatedMappings` and `__originalMappings` are arrays that hold the
   // parsed mapping coordinates from the source map's "mappings" attribute. They
   // are lazily instantiated, accessed via the `_generatedMappings` and
   // `_originalMappings` getters respectively, and we only parse the mappings
   // and create these arrays once queried for a source location. We jump through
   // these hoops because there can be many thousands of mappings, and parsing
   // them is expensive, so we only want to do it if we must.
   //
@@ -1313,354 +1302,37 @@ define('source-map/source-map-consumer',
         this._parseMappings(this._mappings, this.sourceRoot);
       }
 
       return this.__originalMappings;
     }
   });
 
   SourceMapConsumer.prototype._nextCharIsMappingSeparator =
-    function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
-      var c = aStr.charAt(0);
+    function SourceMapConsumer_nextCharIsMappingSeparator(aStr, index) {
+      var c = aStr.charAt(index);
       return c === ";" || c === ",";
     };
 
   /**
    * Parse the mappings in a string in to a data structure which we can easily
    * query (the ordered arrays in the `this.__generatedMappings` and
    * `this.__originalMappings` properties).
    */
   SourceMapConsumer.prototype._parseMappings =
     function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
-      var generatedLine = 1;
-      var previousGeneratedColumn = 0;
-      var previousOriginalLine = 0;
-      var previousOriginalColumn = 0;
-      var previousSource = 0;
-      var previousName = 0;
-      var str = aStr;
-      var temp = {};
-      var mapping;
-
-      while (str.length > 0) {
-        if (str.charAt(0) === ';') {
-          generatedLine++;
-          str = str.slice(1);
-          previousGeneratedColumn = 0;
-        }
-        else if (str.charAt(0) === ',') {
-          str = str.slice(1);
-        }
-        else {
-          mapping = {};
-          mapping.generatedLine = generatedLine;
-
-          // Generated column.
-          base64VLQ.decode(str, temp);
-          mapping.generatedColumn = previousGeneratedColumn + temp.value;
-          previousGeneratedColumn = mapping.generatedColumn;
-          str = temp.rest;
-
-          if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
-            // Original source.
-            base64VLQ.decode(str, temp);
-            mapping.source = this._sources.at(previousSource + temp.value);
-            previousSource += temp.value;
-            str = temp.rest;
-            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
-              throw new Error('Found a source, but no line and column');
-            }
-
-            // Original line.
-            base64VLQ.decode(str, temp);
-            mapping.originalLine = previousOriginalLine + temp.value;
-            previousOriginalLine = mapping.originalLine;
-            // Lines are stored 0-based
-            mapping.originalLine += 1;
-            str = temp.rest;
-            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
-              throw new Error('Found a source and line, but no column');
-            }
-
-            // Original column.
-            base64VLQ.decode(str, temp);
-            mapping.originalColumn = previousOriginalColumn + temp.value;
-            previousOriginalColumn = mapping.originalColumn;
-            str = temp.rest;
-
-            if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
-              // Original name.
-              base64VLQ.decode(str, temp);
-              mapping.name = this._names.at(previousName + temp.value);
-              previousName += temp.value;
-              str = temp.rest;
-            }
-          }
-
-          this.__generatedMappings.push(mapping);
-          if (typeof mapping.originalLine === 'number') {
-            this.__originalMappings.push(mapping);
-          }
-        }
-      }
-
-      this.__generatedMappings.sort(util.compareByGeneratedPositions);
-      this.__originalMappings.sort(util.compareByOriginalPositions);
-    };
-
-  /**
-   * Find the mapping that best matches the hypothetical "needle" mapping that
-   * we are searching for in the given "haystack" of mappings.
-   */
-  SourceMapConsumer.prototype._findMapping =
-    function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
-                                           aColumnName, aComparator) {
-      // To return the position we are searching for, we must first find the
-      // mapping for the given position and then return the opposite position it
-      // points to. Because the mappings are sorted, we can use binary search to
-      // find the best mapping.
-
-      if (aNeedle[aLineName] <= 0) {
-        throw new TypeError('Line must be greater than or equal to 1, got '
-                            + aNeedle[aLineName]);
-      }
-      if (aNeedle[aColumnName] < 0) {
-        throw new TypeError('Column must be greater than or equal to 0, got '
-                            + aNeedle[aColumnName]);
-      }
-
-      return binarySearch.search(aNeedle, aMappings, aComparator);
-    };
-
-  /**
-   * Compute the last column for each generated mapping. The last column is
-   * inclusive.
-   */
-  SourceMapConsumer.prototype.computeColumnSpans =
-    function SourceMapConsumer_computeColumnSpans() {
-      for (var index = 0; index < this._generatedMappings.length; ++index) {
-        var mapping = this._generatedMappings[index];
-
-        // Mappings do not contain a field for the last generated columnt. We
-        // can come up with an optimistic estimate, however, by assuming that
-        // mappings are contiguous (i.e. given two consecutive mappings, the
-        // first mapping ends where the second one starts).
-        if (index + 1 < this._generatedMappings.length) {
-          var nextMapping = this._generatedMappings[index + 1];
-
-          if (mapping.generatedLine === nextMapping.generatedLine) {
-            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
-            continue;
-          }
-        }
-
-        // The last mapping for each line spans the entire line.
-        mapping.lastGeneratedColumn = Infinity;
-      }
-    };
-
-  /**
-   * Returns the original source, line, and column information for the generated
-   * source's line and column positions provided. The only argument is an object
-   * with the following properties:
-   *
-   *   - line: The line number in the generated source.
-   *   - column: The column number in the generated source.
-   *
-   * and an object is returned with the following properties:
-   *
-   *   - source: The original source file, or null.
-   *   - line: The line number in the original source, or null.
-   *   - column: The column number in the original source, or null.
-   *   - name: The original identifier, or null.
-   */
-  SourceMapConsumer.prototype.originalPositionFor =
-    function SourceMapConsumer_originalPositionFor(aArgs) {
-      var needle = {
-        generatedLine: util.getArg(aArgs, 'line'),
-        generatedColumn: util.getArg(aArgs, 'column')
-      };
-
-      var index = this._findMapping(needle,
-                                    this._generatedMappings,
-                                    "generatedLine",
-                                    "generatedColumn",
-                                    util.compareByGeneratedPositions);
-
-      if (index >= 0) {
-        var mapping = this._generatedMappings[index];
-
-        if (mapping.generatedLine === needle.generatedLine) {
-          var source = util.getArg(mapping, 'source', null);
-          if (source != null && this.sourceRoot != null) {
-            source = util.join(this.sourceRoot, source);
-          }
-          return {
-            source: source,
-            line: util.getArg(mapping, 'originalLine', null),
-            column: util.getArg(mapping, 'originalColumn', null),
-            name: util.getArg(mapping, 'name', null)
-          };
-        }
-      }
-
-      return {
-        source: null,
-        line: null,
-        column: null,
-        name: null
-      };
-    };
-
-  /**
-   * Returns the original source content. The only argument is the url of the
-   * original source file. Returns null if no original source content is
-   * availible.
-   */
-  SourceMapConsumer.prototype.sourceContentFor =
-    function SourceMapConsumer_sourceContentFor(aSource) {
-      if (!this.sourcesContent) {
-        return null;
-      }
-
-      if (this.sourceRoot != null) {
-        aSource = util.relative(this.sourceRoot, aSource);
-      }
-
-      if (this._sources.has(aSource)) {
-        return this.sourcesContent[this._sources.indexOf(aSource)];
-      }
-
-      var url;
-      if (this.sourceRoot != null
-          && (url = util.urlParse(this.sourceRoot))) {
-        // XXX: file:// URIs and absolute paths lead to unexpected behavior for
-        // many users. We can help them out when they expect file:// URIs to
-        // behave like it would if they were running a local HTTP server. See
-        // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
-        var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
-        if (url.scheme == "file"
-            && this._sources.has(fileUriAbsPath)) {
-          return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
-        }
-
-        if ((!url.path || url.path == "/")
-            && this._sources.has("/" + aSource)) {
-          return this.sourcesContent[this._sources.indexOf("/" + aSource)];
-        }
-      }
-
-      throw new Error('"' + aSource + '" is not in the SourceMap.');
-    };
-
-  /**
-   * Returns the generated line and column information for the original source,
-   * line, and column positions provided. The only argument is an object with
-   * the following properties:
-   *
-   *   - source: The filename of the original source.
-   *   - line: The line number in the original source.
-   *   - column: The column number in the original source.
-   *
-   * and an object is returned with the following properties:
-   *
-   *   - line: The line number in the generated source, or null.
-   *   - column: The column number in the generated source, or null.
-   */
-  SourceMapConsumer.prototype.generatedPositionFor =
-    function SourceMapConsumer_generatedPositionFor(aArgs) {
-      var needle = {
-        source: util.getArg(aArgs, 'source'),
-        originalLine: util.getArg(aArgs, 'line'),
-        originalColumn: util.getArg(aArgs, 'column')
-      };
-
-      if (this.sourceRoot != null) {
-        needle.source = util.relative(this.sourceRoot, needle.source);
-      }
-
-      var index = this._findMapping(needle,
-                                    this._originalMappings,
-                                    "originalLine",
-                                    "originalColumn",
-                                    util.compareByOriginalPositions);
-
-      if (index >= 0) {
-        var mapping = this._originalMappings[index];
-
-        return {
-          line: util.getArg(mapping, 'generatedLine', null),
-          column: util.getArg(mapping, 'generatedColumn', null),
-          lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
-        };
-      }
-
-      return {
-        line: null,
-        column: null,
-        lastColumn: null
-      };
-    };
-
-  /**
-   * Returns all generated line and column information for the original source
-   * and line provided. The only argument is an object with the following
-   * properties:
-   *
-   *   - source: The filename of the original source.
-   *   - line: The line number in the original source.
-   *
-   * and an array of objects is returned, each with the following properties:
-   *
-   *   - line: The line number in the generated source, or null.
-   *   - column: The column number in the generated source, or null.
-   */
-  SourceMapConsumer.prototype.allGeneratedPositionsFor =
-    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
-      // When there is no exact match, SourceMapConsumer.prototype._findMapping
-      // returns the index of the closest mapping less than the needle. By
-      // setting needle.originalColumn to Infinity, we thus find the last
-      // mapping for the given line, provided such a mapping exists.
-      var needle = {
-        source: util.getArg(aArgs, 'source'),
-        originalLine: util.getArg(aArgs, 'line'),
-        originalColumn: Infinity
-      };
-
-      if (this.sourceRoot != null) {
-        needle.source = util.relative(this.sourceRoot, needle.source);
-      }
-
-      var mappings = [];
-
-      var index = this._findMapping(needle,
-                                    this._originalMappings,
-                                    "originalLine",
-                                    "originalColumn",
-                                    util.compareByOriginalPositions);
-      if (index >= 0) {
-        var mapping = this._originalMappings[index];
-
-        while (mapping && mapping.originalLine === needle.originalLine) {
-          mappings.push({
-            line: util.getArg(mapping, 'generatedLine', null),
-            column: util.getArg(mapping, 'generatedColumn', null),
-            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
-          });
-
-          mapping = this._originalMappings[--index];
-        }
-      }
-
-      return mappings.reverse();
+      throw new Error("Subclasses must implement _parseMappings");
     };
 
   SourceMapConsumer.GENERATED_ORDER = 1;
   SourceMapConsumer.ORIGINAL_ORDER = 2;
 
+  SourceMapConsumer.GREATEST_LOWER_BOUND = 1;
+  SourceMapConsumer.LEAST_UPPER_BOUND = 2;
+
   /**
    * Iterate over each mapping between an original source/line/column and a
    * generated line/column in this source map.
    *
    * @param Function aCallback
    *        The function that is called with each mapping.
    * @param Object aContext
    *        Optional. If specified, this object will be the value of `this` every
@@ -1701,93 +1373,921 @@ define('source-map/source-map-consumer',
           generatedColumn: mapping.generatedColumn,
           originalLine: mapping.originalLine,
           originalColumn: mapping.originalColumn,
           name: mapping.name
         };
       }).forEach(aCallback, context);
     };
 
+  /**
+   * Returns all generated line and column information for the original source,
+   * line, and column provided. If no column is provided, returns all mappings
+   * corresponding to a single line. Otherwise, returns all mappings
+   * corresponding to a single line and column.
+   *
+   * The only argument is an object with the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: Optional. the column number in the original source.
+   *
+   * and an array of objects is returned, each with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  SourceMapConsumer.prototype.allGeneratedPositionsFor =
+    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+      // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping
+      // returns the index of the closest mapping less than the needle. By
+      // setting needle.originalColumn to 0, we thus find the last mapping for
+      // the given line, provided such a mapping exists.
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: util.getArg(aArgs, 'column', 0)
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var mappings = [];
+
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions,
+                                    binarySearch.LEAST_UPPER_BOUND);
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+        var originalLine = mapping.originalLine;
+        var originalColumn = mapping.originalColumn;
+
+        // Iterate until either we run out of mappings, or we run into
+        // a mapping for a different line. Since mappings are sorted, this is
+        // guaranteed to find all mappings for the line we are searching for.
+        while (mapping && mapping.originalLine === originalLine &&
+               (aArgs.column === undefined ||
+                mapping.originalColumn === originalColumn)) {
+          mappings.push({
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          });
+
+          mapping = this._originalMappings[++index];
+        }
+      }
+
+      return mappings;
+    };
+
   exports.SourceMapConsumer = SourceMapConsumer;
 
+  /**
+   * A BasicSourceMapConsumer instance represents a parsed source map which we can
+   * query for information about the original file positions by giving it a file
+   * position in the generated source.
+   *
+   * The only parameter is the raw source map (either as a JSON string, or
+   * already parsed to an object). According to the spec, source maps have the
+   * following attributes:
+   *
+   *   - version: Which version of the source map spec this map is following.
+   *   - sources: An array of URLs to the original source files.
+   *   - names: An array of identifiers which can be referrenced by individual mappings.
+   *   - sourceRoot: Optional. The URL root from which all sources are relative.
+   *   - sourcesContent: Optional. An array of contents of the original source files.
+   *   - mappings: A string of base64 VLQs which contain the actual mappings.
+   *   - file: Optional. The generated file this source map is associated with.
+   *
+   * Here is an example source map, taken from the source map spec[0]:
+   *
+   *     {
+   *       version : 3,
+   *       file: "out.js",
+   *       sourceRoot : "",
+   *       sources: ["foo.js", "bar.js"],
+   *       names: ["src", "maps", "are", "fun"],
+   *       mappings: "AA,AB;;ABCDE;"
+   *     }
+   *
+   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
+   */
+  function BasicSourceMapConsumer(aSourceMap) {
+    var sourceMap = aSourceMap;
+    if (typeof aSourceMap === 'string') {
+      sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+    }
+
+    var version = util.getArg(sourceMap, 'version');
+    var sources = util.getArg(sourceMap, 'sources');
+    // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
+    // requires the array) to play nice here.
+    var names = util.getArg(sourceMap, 'names', []);
+    var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
+    var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
+    var mappings = util.getArg(sourceMap, 'mappings');
+    var file = util.getArg(sourceMap, 'file', null);
+
+    // Once again, Sass deviates from the spec and supplies the version as a
+    // string rather than a number, so we use loose equality checking here.
+    if (version != this._version) {
+      throw new Error('Unsupported version: ' + version);
+    }
+
+    // Some source maps produce relative source paths like "./foo.js" instead of
+    // "foo.js".  Normalize these first so that future comparisons will succeed.
+    // See bugzil.la/1090768.
+    sources = sources.map(util.normalize);
+
+    // Pass `true` below to allow duplicate names and sources. While source maps
+    // are intended to be compressed and deduplicated, the TypeScript compiler
+    // sometimes generates source maps with duplicates in them. See Github issue
+    // #72 and bugzil.la/889492.
+    this._names = ArraySet.fromArray(names, true);
+    this._sources = ArraySet.fromArray(sources, true);
+
+    this.sourceRoot = sourceRoot;
+    this.sourcesContent = sourcesContent;
+    this._mappings = mappings;
+    this.file = file;
+  }
+
+  BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+  BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;
+
+  /**
+   * Create a BasicSourceMapConsumer from a SourceMapGenerator.
+   *
+   * @param SourceMapGenerator aSourceMap
+   *        The source map that will be consumed.
+   * @returns BasicSourceMapConsumer
+   */
+  BasicSourceMapConsumer.fromSourceMap =
+    function SourceMapConsumer_fromSourceMap(aSourceMap) {
+      var smc = Object.create(BasicSourceMapConsumer.prototype);
+
+      smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
+      smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
+      smc.sourceRoot = aSourceMap._sourceRoot;
+      smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
+                                                              smc.sourceRoot);
+      smc.file = aSourceMap._file;
+
+      smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
+      smc.__originalMappings = aSourceMap._mappings.toArray().slice()
+        .sort(util.compareByOriginalPositions);
+
+      return smc;
+    };
+
+  /**
+   * The version of the source mapping spec that we are consuming.
+   */
+  BasicSourceMapConsumer.prototype._version = 3;
+
+  /**
+   * The list of original sources.
+   */
+  Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', {
+    get: function () {
+      return this._sources.toArray().map(function (s) {
+        return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
+      }, this);
+    }
+  });
+
+  /**
+   * Parse the mappings in a string in to a data structure which we can easily
+   * query (the ordered arrays in the `this.__generatedMappings` and
+   * `this.__originalMappings` properties).
+   */
+  BasicSourceMapConsumer.prototype._parseMappings =
+    function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+      var generatedLine = 1;
+      var previousGeneratedColumn = 0;
+      var previousOriginalLine = 0;
+      var previousOriginalColumn = 0;
+      var previousSource = 0;
+      var previousName = 0;
+      var length = aStr.length;
+      var index = 0;
+      var cachedValues = {};
+      var temp = {};
+      var mapping, str, values, end, value;
+
+      while (index < length) {
+        if (aStr.charAt(index) === ';') {
+          generatedLine++;
+          ++index;
+          previousGeneratedColumn = 0;
+        }
+        else if (aStr.charAt(index) === ',') {
+          ++index;
+        }
+        else {
+          mapping = {};
+          mapping.generatedLine = generatedLine;
+
+          // Because each offset is encoded relative to the previous one,
+          // many segments often have the same encoding. We can exploit this
+          // fact by caching the parsed variable length fields of each segment,
+          // allowing us to avoid a second parse if we encounter the same
+          // segment again.
+          for (end = index; end < length; ++end) {
+            if (this._nextCharIsMappingSeparator(aStr, end)) {
+              break;
+            }
+          }
+          str = aStr.slice(index, end);
+
+          values = cachedValues[str];
+          if (values) {
+            index += str.length;
+          } else {
+            values = [];
+            while (index < end) {
+              base64VLQ.decode(aStr, index, temp);
+              value = temp.value;
+              index = temp.rest;
+              values.push(value);
+            }
+            cachedValues[str] = values;
+          }
+
+          // Generated column.
+          mapping.generatedColumn = previousGeneratedColumn + values[0];
+          previousGeneratedColumn = mapping.generatedColumn;
+
+          if (values.length > 1) {
+            // Original source.
+            mapping.source = this._sources.at(previousSource + values[1]);
+            previousSource += values[1];
+            if (values.length === 2) {
+              throw new Error('Found a source, but no line and column');
+            }
+
+            // Original line.
+            mapping.originalLine = previousOriginalLine + values[2];
+            previousOriginalLine = mapping.originalLine;
+            // Lines are stored 0-based
+            mapping.originalLine += 1;
+            if (values.length === 3) {
+              throw new Error('Found a source and line, but no column');
+            }
+
+            // Original column.
+            mapping.originalColumn = previousOriginalColumn + values[3];
+            previousOriginalColumn = mapping.originalColumn;
+
+            if (values.length > 4) {
+              // Original name.
+              mapping.name = this._names.at(previousName + values[4]);
+              previousName += values[4];
+            }
+          }
+
+          this.__generatedMappings.push(mapping);
+          if (typeof mapping.originalLine === 'number') {
+            this.__originalMappings.push(mapping);
+          }
+        }
+      }
+
+      this.__generatedMappings.sort(util.compareByGeneratedPositions);
+      this.__originalMappings.sort(util.compareByOriginalPositions);
+    };
+
+  /**
+   * Find the mapping that best matches the hypothetical "needle" mapping that
+   * we are searching for in the given "haystack" of mappings.
+   */
+  BasicSourceMapConsumer.prototype._findMapping =
+    function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
+                                           aColumnName, aComparator, aBias) {
+      // To return the position we are searching for, we must first find the
+      // mapping for the given position and then return the opposite position it
+      // points to. Because the mappings are sorted, we can use binary search to
+      // find the best mapping.
+
+      if (aNeedle[aLineName] <= 0) {
+        throw new TypeError('Line must be greater than or equal to 1, got '
+                            + aNeedle[aLineName]);
+      }
+      if (aNeedle[aColumnName] < 0) {
+        throw new TypeError('Column must be greater than or equal to 0, got '
+                            + aNeedle[aColumnName]);
+      }
+
+      return binarySearch.search(aNeedle, aMappings, aComparator, aBias);
+    };
+
+  /**
+   * Compute the last column for each generated mapping. The last column is
+   * inclusive.
+   */
+  BasicSourceMapConsumer.prototype.computeColumnSpans =
+    function SourceMapConsumer_computeColumnSpans() {
+      for (var index = 0; index < this._generatedMappings.length; ++index) {
+        var mapping = this._generatedMappings[index];
+
+        // Mappings do not contain a field for the last generated columnt. We
+        // can come up with an optimistic estimate, however, by assuming that
+        // mappings are contiguous (i.e. given two consecutive mappings, the
+        // first mapping ends where the second one starts).
+        if (index + 1 < this._generatedMappings.length) {
+          var nextMapping = this._generatedMappings[index + 1];
+
+          if (mapping.generatedLine === nextMapping.generatedLine) {
+            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+            continue;
+          }
+        }
+
+        // The last mapping for each line spans the entire line.
+        mapping.lastGeneratedColumn = Infinity;
+      }
+    };
+
+  /**
+   * Returns the original source, line, and column information for the generated
+   * source's line and column positions provided. The only argument is an object
+   * with the following properties:
+   *
+   *   - line: The line number in the generated source.
+   *   - column: The column number in the generated source.
+   *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+   *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - source: The original source file, or null.
+   *   - line: The line number in the original source, or null.
+   *   - column: The column number in the original source, or null.
+   *   - name: The original identifier, or null.
+   */
+  BasicSourceMapConsumer.prototype.originalPositionFor =
+    function SourceMapConsumer_originalPositionFor(aArgs) {
+      var needle = {
+        generatedLine: util.getArg(aArgs, 'line'),
+        generatedColumn: util.getArg(aArgs, 'column')
+      };
+
+      var index = this._findMapping(
+        needle,
+        this._generatedMappings,
+        "generatedLine",
+        "generatedColumn",
+        util.compareByGeneratedPositions,
+        util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+      );
+
+      if (index >= 0) {
+        var mapping = this._generatedMappings[index];
+
+        if (mapping.generatedLine === needle.generatedLine) {
+          var source = util.getArg(mapping, 'source', null);
+          if (source != null && this.sourceRoot != null) {
+            source = util.join(this.sourceRoot, source);
+          }
+          return {
+            source: source,
+            line: util.getArg(mapping, 'originalLine', null),
+            column: util.getArg(mapping, 'originalColumn', null),
+            name: util.getArg(mapping, 'name', null)
+          };
+        }
+      }
+
+      return {
+        source: null,
+        line: null,
+        column: null,
+        name: null
+      };
+    };
+
+  /**
+   * Returns the original source content. The only argument is the url of the
+   * original source file. Returns null if no original source content is
+   * availible.
+   */
+  BasicSourceMapConsumer.prototype.sourceContentFor =
+    function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+      if (!this.sourcesContent) {
+        return null;
+      }
+
+      if (this.sourceRoot != null) {
+        aSource = util.relative(this.sourceRoot, aSource);
+      }
+
+      if (this._sources.has(aSource)) {
+        return this.sourcesContent[this._sources.indexOf(aSource)];
+      }
+
+      var url;
+      if (this.sourceRoot != null
+          && (url = util.urlParse(this.sourceRoot))) {
+        // XXX: file:// URIs and absolute paths lead to unexpected behavior for
+        // many users. We can help them out when they expect file:// URIs to
+        // behave like it would if they were running a local HTTP server. See
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
+        var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
+        if (url.scheme == "file"
+            && this._sources.has(fileUriAbsPath)) {
+          return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
+        }
+
+        if ((!url.path || url.path == "/")
+            && this._sources.has("/" + aSource)) {
+          return this.sourcesContent[this._sources.indexOf("/" + aSource)];
+        }
+      }
+
+      // This function is used recursively from
+      // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
+      // don't want to throw if we can't find the source - we just want to
+      // return null, so we provide a flag to exit gracefully.
+      if (nullOnMissing) {
+        return null;
+      }
+      else {
+        throw new Error('"' + aSource + '" is not in the SourceMap.');
+      }
+    };
+
+  /**
+   * Returns the generated line and column information for the original source,
+   * line, and column positions provided. The only argument is an object with
+   * the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: The column number in the original source.
+   *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
+   *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  BasicSourceMapConsumer.prototype.generatedPositionFor =
+    function SourceMapConsumer_generatedPositionFor(aArgs) {
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: util.getArg(aArgs, 'column')
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var index = this._findMapping(
+        needle,
+        this._originalMappings,
+        "originalLine",
+        "originalColumn",
+        util.compareByOriginalPositions,
+        util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
+      );
+
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
+        if (mapping.source === needle.source) {
+          return {
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          };
+        }
+      }
+
+      return {
+        line: null,
+        column: null,
+        lastColumn: null
+      };
+    };
+
+  exports.BasicSourceMapConsumer = BasicSourceMapConsumer;
+
+  /**
+   * An IndexedSourceMapConsumer instance represents a parsed source map which
+   * we can query for information. It differs from BasicSourceMapConsumer in
+   * that it takes "indexed" source maps (i.e. ones with a "sections" field) as
+   * input.
+   *
+   * The only parameter is a raw source map (either as a JSON string, or already
+   * parsed to an object). According to the spec for indexed source maps, they
+   * have the following attributes:
+   *
+   *   - version: Which version of the source map spec this map is following.
+   *   - file: Optional. The generated file this source map is associated with.
+   *   - sections: A list of section definitions.
+   *
+   * Each value under the "sections" field has two fields:
+   *   - offset: The offset into the original specified at which this section
+   *       begins to apply, defined as an object with a "line" and "column"
+   *       field.
+   *   - map: A source map definition. This source map could also be indexed,
+   *       but doesn't have to be.
+   *
+   * Instead of the "map" field, it's also possible to have a "url" field
+   * specifying a URL to retrieve a source map from, but that's currently
+   * unsupported.
+   *
+   * Here's an example source map, taken from the source map spec[0], but
+   * modified to omit a section which uses the "url" field.
+   *
+   *  {
+   *    version : 3,
+   *    file: "app.js",
+   *    sections: [{
+   *      offset: {line:100, column:10},
+   *      map: {
+   *        version : 3,
+   *        file: "section.js",
+   *        sources: ["foo.js", "bar.js"],
+   *        names: ["src", "maps", "are", "fun"],
+   *        mappings: "AAAA,E;;ABCDE;"
+   *      }
+   *    }],
+   *  }
+   *
+   * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt
+   */
+  function IndexedSourceMapConsumer(aSourceMap) {
+    var sourceMap = aSourceMap;
+    if (typeof aSourceMap === 'string') {
+      sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
+    }
+
+    var version = util.getArg(sourceMap, 'version');
+    var sections = util.getArg(sourceMap, 'sections');
+
+    if (version != this._version) {
+      throw new Error('Unsupported version: ' + version);
+    }
+
+    var lastOffset = {
+      line: -1,
+      column: 0
+    };
+    this._sections = sections.map(function (s) {
+      if (s.url) {
+        // The url field will require support for asynchronicity.
+        // See https://github.com/mozilla/source-map/issues/16
+        throw new Error('Support for url field in sections not implemented.');
+      }
+      var offset = util.getArg(s, 'offset');
+      var offsetLine = util.getArg(offset, 'line');
+      var offsetColumn = util.getArg(offset, 'column');
+
+      if (offsetLine < lastOffset.line ||
+          (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
+        throw new Error('Section offsets must be ordered and non-overlapping.');
+      }
+      lastOffset = offset;
+
+      return {
+        generatedOffset: {
+          // The offset fields are 0-based, but we use 1-based indices when
+          // encoding/decoding from VLQ.
+          generatedLine: offsetLine + 1,
+          generatedColumn: offsetColumn + 1
+        },
+        consumer: new SourceMapConsumer(util.getArg(s, 'map'))
+      }
+    });
+  }
+
+  IndexedSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
+  IndexedSourceMapConsumer.prototype.constructor = SourceMapConsumer;
+
+  /**
+   * The version of the source mapping spec that we are consuming.
+   */
+  IndexedSourceMapConsumer.prototype._version = 3;
+
+  /**
+   * The list of original sources.
+   */
+  Object.defineProperty(IndexedSourceMapConsumer.prototype, 'sources', {
+    get: function () {
+      var sources = [];
+      for (var i = 0; i < this._sections.length; i++) {
+        for (var j = 0; j < this._sections[i].consumer.sources.length; j++) {
+          sources.push(this._sections[i].consumer.sources[j]);
+        }
+      };
+      return sources;
+    }
+  });
+
+  /**
+   * Returns the original source, line, and column information for the generated
+   * source's line and column positions provided. The only argument is an object
+   * with the following properties:
+   *
+   *   - line: The line number in the generated source.
+   *   - column: The column number in the generated source.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - source: The original source file, or null.
+   *   - line: The line number in the original source, or null.
+   *   - column: The column number in the original source, or null.
+   *   - name: The original identifier, or null.
+   */
+  IndexedSourceMapConsumer.prototype.originalPositionFor =
+    function IndexedSourceMapConsumer_originalPositionFor(aArgs) {
+      var needle = {
+        generatedLine: util.getArg(aArgs, 'line'),
+        generatedColumn: util.getArg(aArgs, 'column')
+      };
+
+      // Find the section containing the generated position we're trying to map
+      // to an original position.
+      var sectionIndex = binarySearch.search(needle, this._sections,
+        function(needle, section) {
+          var cmp = needle.generatedLine - section.generatedOffset.generatedLine;
+          if (cmp) {
+            return cmp;
+          }
+
+          return (needle.generatedColumn -
+                  section.generatedOffset.generatedColumn);
+        });
+      var section = this._sections[sectionIndex];
+
+      if (!section) {
+        return {
+          source: null,
+          line: null,
+          column: null,
+          name: null
+        };
+      }
+
+      return section.consumer.originalPositionFor({
+        line: needle.generatedLine -
+          (section.generatedOffset.generatedLine - 1),
+        column: needle.generatedColumn -
+          (section.generatedOffset.generatedLine === needle.generatedLine
+           ? section.generatedOffset.generatedColumn - 1
+           : 0),
+        bias: aArgs.bias
+      });
+    };
+
+  /**
+   * Returns the original source content. The only argument is the url of the
+   * original source file. Returns null if no original source content is
+   * available.
+   */
+  IndexedSourceMapConsumer.prototype.sourceContentFor =
+    function IndexedSourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+
+        var content = section.consumer.sourceContentFor(aSource, true);
+        if (content) {
+          return content;
+        }
+      }
+      if (nullOnMissing) {
+        return null;
+      }
+      else {
+        throw new Error('"' + aSource + '" is not in the SourceMap.');
+      }
+    };
+
+  /**
+   * Returns the generated line and column information for the original source,
+   * line, and column positions provided. The only argument is an object with
+   * the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: The column number in the original source.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  IndexedSourceMapConsumer.prototype.generatedPositionFor =
+    function IndexedSourceMapConsumer_generatedPositionFor(aArgs) {
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+
+        // Only consider this section if the requested source is in the list of
+        // sources of the consumer.
+        if (section.consumer.sources.indexOf(util.getArg(aArgs, 'source')) === -1) {
+          continue;
+        }
+        var generatedPosition = section.consumer.generatedPositionFor(aArgs);
+        if (generatedPosition) {
+          var ret = {
+            line: generatedPosition.line +
+              (section.generatedOffset.generatedLine - 1),
+            column: generatedPosition.column +
+              (section.generatedOffset.generatedLine === generatedPosition.line
+               ? section.generatedOffset.generatedColumn - 1
+               : 0)
+          };
+          return ret;
+        }
+      }
+
+      return {
+        line: null,
+        column: null
+      };
+    };
+
+  /**
+   * Parse the mappings in a string in to a data structure which we can easily
+   * query (the ordered arrays in the `this.__generatedMappings` and
+   * `this.__originalMappings` properties).
+   */
+  IndexedSourceMapConsumer.prototype._parseMappings =
+    function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) {
+      this.__generatedMappings = [];
+      this.__originalMappings = [];
+      for (var i = 0; i < this._sections.length; i++) {
+        var section = this._sections[i];
+        var sectionMappings = section.consumer._generatedMappings;
+        for (var j = 0; j < sectionMappings.length; j++) {
+          var mapping = sectionMappings[i];
+
+          var source = mapping.source;
+          var sourceRoot = section.consumer.sourceRoot;
+
+          if (source != null && sourceRoot != null) {
+            source = util.join(sourceRoot, source);
+          }
+
+          // The mappings coming from the consumer for the section have
+          // generated positions relative to the start of the section, so we
+          // need to offset them to be relative to the start of the concatenated
+          // generated file.
+          var adjustedMapping = {
+            source: source,
+            generatedLine: mapping.generatedLine +
+              (section.generatedOffset.generatedLine - 1),
+            generatedColumn: mapping.column +
+              (section.generatedOffset.generatedLine === mapping.generatedLine)
+              ? section.generatedOffset.generatedColumn - 1
+              : 0,
+            originalLine: mapping.originalLine,
+            originalColumn: mapping.originalColumn,
+            name: mapping.name
+          };
+
+          this.__generatedMappings.push(adjustedMapping);
+          if (typeof adjustedMapping.originalLine === 'number') {
+            this.__originalMappings.push(adjustedMapping);
+          }
+        };
+      };
+
+    this.__generatedMappings.sort(util.compareByGeneratedPositions);
+    this.__originalMappings.sort(util.compareByOriginalPositions);
+  };
+
+  exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;
+
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/binary-search', ['require', 'exports', 'module' , ], function(require, exports, module) {
 
+  exports.GREATEST_LOWER_BOUND = 1;
+  exports.LEAST_UPPER_BOUND = 2;
+
   /**
    * Recursive implementation of binary search.
    *
    * @param aLow Indices here and lower do not contain the needle.
    * @param aHigh Indices here and higher do not contain the needle.
    * @param aNeedle The element being searched for.
    * @param aHaystack The non-empty array being searched.
    * @param aCompare Function which takes two elements and returns -1, 0, or 1.
+   * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+   *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
    */
-  function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) {
+  function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare, aBias) {
     // This function terminates when one of the following is true:
     //
     //   1. We find the exact element we are looking for.
     //
     //   2. We did not find the exact element, but we can return the index of
-    //      the next closest element that is less than that element.
+    //      the next-closest element.
     //
     //   3. We did not find the exact element, and there is no next-closest
-    //      element which is less than the one we are searching for, so we
-    //      return -1.
+    //      element than the one we are searching for, so we return -1.
     var mid = Math.floor((aHigh - aLow) / 2) + aLow;
     var cmp = aCompare(aNeedle, aHaystack[mid], true);
     if (cmp === 0) {
       // Found the element we are looking for.
       return mid;
     }
     else if (cmp > 0) {
-      // aHaystack[mid] is greater than our needle.
+      // Our needle is greater than aHaystack[mid].
       if (aHigh - mid > 1) {
         // The element is in the upper half.
-        return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare);
+        return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare, aBias);
       }
-      // We did not find an exact match, return the next closest one
-      // (termination case 2).
-      return mid;
+
+      // The exact needle element was not found in this haystack. Determine if
+      // we are in termination case (3) or (2) and return the appropriate thing.
+      if (aBias == exports.LEAST_UPPER_BOUND) {
+        return aHigh < aHaystack.length ? aHigh : -1;
+      } else {
+        return mid;
+      }
     }
     else {
-      // aHaystack[mid] is less than our needle.
+      // Our needle is less than aHaystack[mid].
       if (mid - aLow > 1) {
         // The element is in the lower half.
-        return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare);
+        return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare, aBias);
       }
-      // The exact needle element was not found in this haystack. Determine if
-      // we are in termination case (2) or (3) and return the appropriate thing.
-      return aLow < 0 ? -1 : aLow;
+
+      // we are in termination case (3) or (2) and return the appropriate thing.
+      if (aBias == exports.LEAST_UPPER_BOUND) {
+        return mid;
+      } else {
+        return aLow < 0 ? -1 : aLow;
+      }
     }
   }
 
   /**
    * This is an implementation of binary search which will always try and return
-   * the index of next lowest value checked if there is no exact hit. This is
-   * because mappings between original and generated line/col pairs are single
-   * points, and there is an implicit region between each of them, so a miss
-   * just means that you aren't on the very start of a region.
+   * the index of the closest element if there is no exact hit. This is because
+   * mappings between original and generated line/col pairs are single points,
+   * and there is an implicit region between each of them, so a miss just means
+   * that you aren't on the very start of a region.
    *
    * @param aNeedle The element you are looking for.
    * @param aHaystack The array that is being searched.
    * @param aCompare A function which takes the needle and an element in the
    *     array and returns -1, 0, or 1 depending on whether the needle is less
    *     than, equal to, or greater than the element, respectively.
+   * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
+   *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
+   *     closest element that is smaller than or greater than the one we are
+   *     searching for, respectively, if the exact element cannot be found.
+   *     Defaults to 'binarySearch.GREATEST_LOWER_BOUND'.
    */
-  exports.search = function search(aNeedle, aHaystack, aCompare) {
+  exports.search = function search(aNeedle, aHaystack, aCompare, aBias) {
     if (aHaystack.length === 0) {
       return -1;
     }
-    return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
+
+    var index = recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack,
+                                aCompare, aBias || exports.GREATEST_LOWER_BOUND);
+    if (index < 0) {
+      return -1;
+    }
+
+    // We have found either the exact element, or the next-closest element than
+    // the one we are searching for. However, there may be more than one such
+    // element. Make sure we always return the smallest of these.
+    while (index - 1 >= 0) {
+      if (aCompare(aHaystack[index], aHaystack[index - 1], true) !== 0) {
+        break;
+      }
+      --index;
+    }
+
+    return index;
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -1796,18 +2296,23 @@ define('source-map/source-node', ['requi
 
   var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
   var util = require('./util');
 
   // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
   // operating systems these days (capturing the result).
   var REGEX_NEWLINE = /(\r?\n)/;
 
-  // Matches a Windows-style newline, or any character.
-  var REGEX_CHARACTER = /\r\n|[\s\S]/g;
+  // Newline character code for charCodeAt() comparisons
+  var NEWLINE_CODE = 10;
+
+  // Private symbol for identifying `SourceNode`s when multiple versions of
+  // the source-map library are loaded. This MUST NOT CHANGE across
+  // versions!
+  var isSourceNode = "$$$isSourceNode$$$";
 
   /**
    * SourceNodes provide a way to abstract over interpolating/concatenating
    * snippets of generated JavaScript source code while maintaining the line and
    * column information associated with the original source code.
    *
    * @param aLine The original line number.
    * @param aColumn The original column number.
@@ -1818,16 +2323,17 @@ define('source-map/source-node', ['requi
    */
   function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
     this.children = [];
     this.sourceContents = {};
     this.line = aLine == null ? null : aLine;
     this.column = aColumn == null ? null : aColumn;
     this.source = aSource == null ? null : aSource;
     this.name = aName == null ? null : aName;
+    this[isSourceNode] = true;
     if (aChunks != null) this.add(aChunks);
   }
 
   /**
    * Creates a SourceNode from generated code and a SourceMapConsumer.
    *
    * @param aGeneratedCode The generated code
    * @param aSourceMapConsumer The SourceMap for the generated code
@@ -1948,17 +2454,17 @@ define('source-map/source-node', ['requi
    *        SourceNode, or an array where each member is one of those things.
    */
   SourceNode.prototype.add = function SourceNode_add(aChunk) {
     if (Array.isArray(aChunk)) {
       aChunk.forEach(function (chunk) {
         this.add(chunk);
       }, this);
     }
-    else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
+    else if (aChunk[isSourceNode] || typeof aChunk === "string") {
       if (aChunk) {
         this.children.push(aChunk);
       }
     }
     else {
       throw new TypeError(
         "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
       );
@@ -1973,17 +2479,17 @@ define('source-map/source-node', ['requi
    *        SourceNode, or an array where each member is one of those things.
    */
   SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
     if (Array.isArray(aChunk)) {
       for (var i = aChunk.length-1; i >= 0; i--) {
         this.prepend(aChunk[i]);
       }
     }
-    else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
+    else if (aChunk[isSourceNode] || typeof aChunk === "string") {
       this.children.unshift(aChunk);
     }
     else {
       throw new TypeError(
         "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
       );
     }
     return this;
@@ -1995,17 +2501,17 @@ define('source-map/source-node', ['requi
    * snippet and the its original associated source's line/column location.
    *
    * @param aFn The traversal function.
    */
   SourceNode.prototype.walk = function SourceNode_walk(aFn) {
     var chunk;
     for (var i = 0, len = this.children.length; i < len; i++) {
       chunk = this.children[i];
-      if (chunk instanceof SourceNode) {
+      if (chunk[isSourceNode]) {
         chunk.walk(aFn);
       }
       else {
         if (chunk !== '') {
           aFn(chunk, { source: this.source,
                        line: this.line,
                        column: this.column,
                        name: this.name });
@@ -2040,17 +2546,17 @@ define('source-map/source-node', ['requi
    * Call String.prototype.replace on the very right-most source snippet. Useful
    * for trimming whitespace from the end of a source node, etc.
    *
    * @param aPattern The pattern to replace.
    * @param aReplacement The thing to replace the pattern with.
    */
   SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
     var lastChild = this.children[this.children.length - 1];
-    if (lastChild instanceof SourceNode) {
+    if (lastChild[isSourceNode]) {
       lastChild.replaceRight(aPattern, aReplacement);
     }
     else if (typeof lastChild === 'string') {
       this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
     }
     else {
       this.children.push(''.replace(aPattern, aReplacement));
     }
@@ -2073,17 +2579,17 @@ define('source-map/source-node', ['requi
    * Walk over the tree of SourceNodes. The walking function is called for each
    * source file content and is passed the filename and source content.
    *
    * @param aFn The traversal function.
    */
   SourceNode.prototype.walkSourceContents =
     function SourceNode_walkSourceContents(aFn) {
       for (var i = 0, len = this.children.length; i < len; i++) {
-        if (this.children[i] instanceof SourceNode) {
+        if (this.children[i][isSourceNode]) {
           this.children[i].walkSourceContents(aFn);
         }
       }
 
       var sources = Object.keys(this.sourceContents);
       for (var i = 0, len = sources.length; i < len; i++) {
         aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
       }
@@ -2149,22 +2655,22 @@ define('source-map/source-node', ['requi
           generated: {
             line: generated.line,
             column: generated.column
           }
         });
         lastOriginalSource = null;
         sourceMappingActive = false;
       }
-      chunk.match(REGEX_CHARACTER).forEach(function (ch, idx, array) {
-        if (REGEX_NEWLINE.test(ch)) {
+      for (var idx = 0, length = chunk.length; idx < length; idx++) {
+        if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
           generated.line++;
           generated.column = 0;
           // Mappings end at eol
-          if (idx + 1 === array.length) {
+          if (idx + 1 === length) {
             lastOriginalSource = null;
             sourceMappingActive = false;
           } else if (sourceMappingActive) {
             map.addMapping({
               source: original.source,
               original: {
                 line: original.line,
                 column: original.column
@@ -2172,19 +2678,19 @@ define('source-map/source-node', ['requi
               generated: {
                 line: generated.line,
                 column: generated.column
               },
               name: original.name
             });
           }
         } else {
-          generated.column += ch.length;
+          generated.column++;
         }
-      });
+      }
     });
     this.walkSourceContents(function (sourceFile, sourceContent) {
       map.setSourceContent(sourceFile, sourceContent);
     });
 
     return { code: generated.code, map: map };
   };
 
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -121,16 +121,123 @@ define('test/source-map/util', ['require
   exports.testMapEmptySourceRoot = {
     version: 3,
     file: 'min.js',
     names: ['bar', 'baz', 'n'],
     sources: ['one.js', 'two.js'],
     sourceRoot: '',
     mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
   };
+  // This mapping is identical to above, but uses the indexed format instead.
+  exports.indexedTestMap = {
+    version: 3,
+    file: 'min.js',
+    sections: [
+      {
+        offset: {
+          line: 0,
+          column: 0
+        },
+        map: {
+          version: 3,
+          sources: [
+            "one.js"
+          ],
+          sourcesContent: [
+            ' ONE.foo = function (bar) {\n' +
+            '   return baz(bar);\n' +
+            ' };',
+          ],
+          names: [
+            "bar",
+            "baz"
+          ],
+          mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID",
+          file: "min.js",
+          sourceRoot: "/the/root"
+        }
+      },
+      {
+        offset: {
+          line: 1,
+          column: 0
+        },
+        map: {
+          version: 3,
+          sources: [
+            "two.js"
+          ],
+          sourcesContent: [
+            ' TWO.inc = function (n) {\n' +
+            '   return n + 1;\n' +
+            ' };'
+          ],
+          names: [
+            "n"
+          ],
+          mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOA",
+          file: "min.js",
+          sourceRoot: "/the/root"
+        }
+      }
+    ]
+  };
+  exports.indexedTestMapDifferentSourceRoots = {
+    version: 3,
+    file: 'min.js',
+    sections: [
+      {
+        offset: {
+          line: 0,
+          column: 0
+        },
+        map: {
+          version: 3,
+          sources: [
+            "one.js"
+          ],
+          sourcesContent: [
+            ' ONE.foo = function (bar) {\n' +
+            '   return baz(bar);\n' +
+            ' };',
+          ],
+          names: [
+            "bar",
+            "baz"
+          ],
+          mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID",
+          file: "min.js",
+          sourceRoot: "/the/root"
+        }
+      },
+      {
+        offset: {
+          line: 1,
+          column: 0
+        },
+        map: {
+          version: 3,
+          sources: [
+            "two.js"
+          ],
+          sourcesContent: [
+            ' TWO.inc = function (n) {\n' +
+            '   return n + 1;\n' +
+            ' };'
+          ],
+          names: [
+            "n"
+          ],
+          mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOA",
+          file: "min.js",
+          sourceRoot: "/different/root"
+        }
+      }
+    ]
+  };
   exports.testMapWithSourcesContent = {
     version: 3,
     file: 'min.js',
     names: ['bar', 'baz', 'n'],
     sources: ['one.js', 'two.js'],
     sourcesContent: [
       ' ONE.foo = function (bar) {\n' +
       '   return baz(bar);\n' +
@@ -163,22 +270,23 @@ define('test/source-map/util', ['require
     file: 'min.js',
     names: [],
     sources: [],
     mappings: ''
   };
 
 
   function assertMapping(generatedLine, generatedColumn, originalSource,
-                         originalLine, originalColumn, name, map, assert,
+                         originalLine, originalColumn, name, bias, map, assert,
                          dontTestGenerated, dontTestOriginal) {
     if (!dontTestOriginal) {
       var origMapping = map.originalPositionFor({
         line: generatedLine,
-        column: generatedColumn
+        column: generatedColumn,
+        bias: bias
       });
       assert.equal(origMapping.name, name,
                    'Incorrect name, expected ' + JSON.stringify(name)
                    + ', got ' + JSON.stringify(origMapping.name));
       assert.equal(origMapping.line, originalLine,
                    'Incorrect line, expected ' + JSON.stringify(originalLine)
                    + ', got ' + JSON.stringify(origMapping.line));
       assert.equal(origMapping.column, originalColumn,
@@ -201,17 +309,18 @@ define('test/source-map/util', ['require
                    'Incorrect source, expected ' + JSON.stringify(expectedSource)
                    + ', got ' + JSON.stringify(origMapping.source));
     }
 
     if (!dontTestGenerated) {
       var genMapping = map.generatedPositionFor({
         source: originalSource,
         line: originalLine,
-        column: originalColumn
+        column: originalColumn,
+        bias: bias
       });
       assert.equal(genMapping.line, generatedLine,
                    'Incorrect line, expected ' + JSON.stringify(generatedLine)
                    + ', got ' + JSON.stringify(genMapping.line));
       assert.equal(genMapping.column, generatedColumn,
                    'Incorrect column, expected ' + JSON.stringify(generatedColumn)
                    + ', got ' + JSON.stringify(genMapping.column));
     }
@@ -516,27 +625,27 @@ define('lib/source-map/util', ['require'
       return cmp;
     }
 
     cmp = mappingA.originalColumn - mappingB.originalColumn;
     if (cmp || onlyCompareOriginal) {
       return cmp;
     }
 
-    cmp = strcmp(mappingA.name, mappingB.name);
+    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
     if (cmp) {
       return cmp;
     }
 
     cmp = mappingA.generatedLine - mappingB.generatedLine;
     if (cmp) {
       return cmp;
     }
 
-    return mappingA.generatedColumn - mappingB.generatedColumn;
+    return strcmp(mappingA.name, mappingB.name);
   };
   exports.compareByOriginalPositions = compareByOriginalPositions;
 
   /**
    * Comparator between two mappings where the generated positions are
    * compared.
    *
    * Optionally pass in `true` as `onlyCompareGenerated` to consider two
--- a/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js
@@ -14,18 +14,19 @@ Components.utils.import('resource://test
  */
 define("test/source-map/test-base64-vlq", ["require", "exports", "module"], function (require, exports, module) {
 
   var base64VLQ = require('source-map/base64-vlq');
 
   exports['test normal encoding and decoding'] = function (assert, util) {
     var result = {};
     for (var i = -255; i < 256; i++) {
-      base64VLQ.decode(base64VLQ.encode(i), result);
+      var str = base64VLQ.encode(i);
+      base64VLQ.decode(str, 0, result);
       assert.equal(result.value, i);
-      assert.equal(result.rest, "");
+      assert.equal(result.rest, str.length);
     }
   };
 
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-base64-vlq', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_binary_search.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_binary_search.js
@@ -15,48 +15,96 @@ Components.utils.import('resource://test
 define("test/source-map/test-binary-search", ["require", "exports", "module"], function (require, exports, module) {
 
   var binarySearch = require('source-map/binary-search');
 
   function numberCompare(a, b) {
     return a - b;
   }
 
-  exports['test too high'] = function (assert, util) {
+  exports['test too high with default (glb) bias'] = function (assert, util) {
     var needle = 30;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.doesNotThrow(function () {
       binarySearch.search(needle, haystack, numberCompare);
     });
 
     assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 20);
   };
 
-  exports['test too low'] = function (assert, util) {
+  exports['test too low with default (glb) bias'] = function (assert, util) {
     var needle = 1;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.doesNotThrow(function () {
       binarySearch.search(needle, haystack, numberCompare);
     });
 
     assert.equal(binarySearch.search(needle, haystack, numberCompare), -1);
   };
 
+  exports['test too high with lub bias'] = function (assert, util) {
+    var needle = 30;
+    var haystack = [2,4,6,8,10,12,14,16,18,20];
+
+    assert.doesNotThrow(function () {
+      binarySearch.search(needle, haystack, numberCompare);
+    });
+
+    assert.equal(binarySearch.search(needle, haystack, numberCompare,
+                                     binarySearch.LEAST_UPPER_BOUND), -1);
+  };
+
+  exports['test too low with lub bias'] = function (assert, util) {
+    var needle = 1;
+    var haystack = [2,4,6,8,10,12,14,16,18,20];
+
+    assert.doesNotThrow(function () {
+      binarySearch.search(needle, haystack, numberCompare);
+    });
+
+    assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare,
+                                              binarySearch.LEAST_UPPER_BOUND)], 2);
+  };
+
   exports['test exact search'] = function (assert, util) {
     var needle = 4;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 4);
   };
 
-  exports['test fuzzy search'] = function (assert, util) {
+  exports['test fuzzy search with default (glb) bias'] = function (assert, util) {
     var needle = 19;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 18);
   };
 
+  exports['test fuzzy search with lub bias'] = function (assert, util) {
+    var needle = 19;
+    var haystack = [2,4,6,8,10,12,14,16,18,20];
+
+    assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare,
+                                              binarySearch.LEAST_UPPER_BOUND)], 20);
+  };
+
+  exports['test multiple matches'] = function (assert, util) {
+    var needle = 5;
+    var haystack = [1, 1, 2, 5, 5, 5, 13, 21];
+
+    assert.equal(binarySearch.search(needle, haystack, numberCompare,
+                                     binarySearch.LEAST_UPPER_BOUND), 3);
+  };
+
+  exports['test multiple matches at the beginning'] = function (assert, util) {
+    var needle = 1;
+    var haystack = [1, 1, 2, 5, 5, 5, 13, 21];
+
+    assert.equal(binarySearch.search(needle, haystack, numberCompare,
+                                     binarySearch.LEAST_UPPER_BOUND), 0);
+  };
+
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-binary-search', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js
@@ -51,42 +51,63 @@ define("test/source-map/test-dog-fooding
       source: 'gza.coffee',
       original: { line: 5, column: 10 },
       generated: { line: 6, column: 12 }
     });
 
     var smc = new SourceMapConsumer(smg.toString());
 
     // Exact
-    util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
-    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
-    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
-    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 0, null, smc, assert);
-    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 5, 10, null, smc, assert);
+    util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 0, null, null, smc, assert);
+    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 0, null, null, smc, assert);
+    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 0, null, null, smc, assert);
+    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 0, null, null, smc, assert);
+    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 5, 10, null, null, smc, assert);
 
     // Fuzzy
 
-    // Generated to original
-    util.assertMapping(2, 0, null, null, null, null, smc, assert, true);
-    util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true);
-    util.assertMapping(3, 0, null, null, null, null, smc, assert, true);
-    util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true);
-    util.assertMapping(4, 0, null, null, null, null, smc, assert, true);
-    util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true);
-    util.assertMapping(5, 0, null, null, null, null, smc, assert, true);
-    util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert, true);
-    util.assertMapping(6, 0, null, null, null, null, smc, assert, true);
-    util.assertMapping(6, 9, null, null, null, null, smc, assert, true);
-    util.assertMapping(6, 13, '/wu/tang/gza.coffee', 5, 10, null, smc, assert, true);
+    // Generated to original with default (glb) bias.
+    util.assertMapping(2, 0, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, null, smc, assert, true);
+    util.assertMapping(3, 0, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, null, smc, assert, true);
+    util.assertMapping(4, 0, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, null, smc, assert, true);
+    util.assertMapping(5, 0, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, null, smc, assert, true);
+    util.assertMapping(6, 0, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(6, 9, null, null, null, null, null, smc, assert, true);
+    util.assertMapping(6, 13, '/wu/tang/gza.coffee', 5, 10, null, null, smc, assert, true);
 
-    // Original to generated
-    util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 1, null, smc, assert, null, true);
-    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 3, null, smc, assert, null, true);
-    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 6, null, smc, assert, null, true);
-    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 9, null, smc, assert, null, true);
-    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 5, 9, null, smc, assert, null, true);
-    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 6, 19, null, smc, assert, null, true);
+    // Generated to original with lub bias.
+    util.assertMapping(2, 0, '/wu/tang/gza.coffee', 1, 0, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(2, 9, null, null, null, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(3, 0, '/wu/tang/gza.coffee', 2, 0, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(3, 9, null, null, null, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(4, 0, '/wu/tang/gza.coffee', 3, 0, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(4, 9, null, null, null, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(5, 0, '/wu/tang/gza.coffee', 4, 0, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(5, 9, null, null, null, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(6, 0, '/wu/tang/gza.coffee', 5, 10, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(6, 9, '/wu/tang/gza.coffee', 5, 10, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+    util.assertMapping(6, 13, null, null, null, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, true);
+
+    // Original to generated with default (glb) bias
+    util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 1, null, null, smc, assert, null, true);
+    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 3, null, null, smc, assert, null, true);
+    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 6, null, null, smc, assert, null, true);
+    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 9, null, null, smc, assert, null, true);
+    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 5, 9, null, null, smc, assert, null, true);
+    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 6, 19, null, null, smc, assert, null, true);
+
+    // Original to generated with lub bias.
+    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 1, 1, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
+    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 2, 3, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
+    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 3, 6, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
+    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 4, 9, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
+    util.assertMapping(6, 12, '/wu/tang/gza.coffee', 5, 9, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
+    util.assertMapping(null, null, '/wu/tang/gza.coffee', 6, 19, null, SourceMapConsumer.LEAST_UPPER_BOUND, smc, assert, null, true);
   };
 
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-dog-fooding', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -10,37 +10,63 @@ Components.utils.import('resource://test
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define("test/source-map/test-source-map-consumer", ["require", "exports", "module"], function (require, exports, module) {
 
   var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer;
+  var IndexedSourceMapConsumer = require('source-map/source-map-consumer').IndexedSourceMapConsumer;
+  var BasicSourceMapConsumer = require('source-map/source-map-consumer').BasicSourceMapConsumer;
   var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
 
   exports['test that we can instantiate with a string or an object'] = function (assert, util) {
     assert.doesNotThrow(function () {
       var map = new SourceMapConsumer(util.testMap);
     });
     assert.doesNotThrow(function () {
       var map = new SourceMapConsumer(JSON.stringify(util.testMap));
     });
   };
 
+  exports['test that the object returned from new SourceMapConsumer inherits from SourceMapConsumer'] = function (assert, util) {
+    assert.ok(new SourceMapConsumer(util.testMap) instanceof SourceMapConsumer);
+  }
+
+  exports['test that a BasicSourceMapConsumer is returned for sourcemaps without sections'] = function(assert, util) {
+    assert.ok(new SourceMapConsumer(util.testMap) instanceof BasicSourceMapConsumer);
+  };
+
+  exports['test that an IndexedSourceMapConsumer is returned for sourcemaps with sections'] = function(assert, util) {
+    assert.ok(new SourceMapConsumer(util.indexedTestMap) instanceof IndexedSourceMapConsumer);
+  };
+
   exports['test that the `sources` field has the original sources'] = function (assert, util) {
     var map;
     var sources;
 
     map = new SourceMapConsumer(util.testMap);
     sources = map.sources;
     assert.equal(sources[0], '/the/root/one.js');
     assert.equal(sources[1], '/the/root/two.js');
     assert.equal(sources.length, 2);
 
+    map = new SourceMapConsumer(util.indexedTestMap);
+    sources = map.sources;
+    assert.equal(sources[0], '/the/root/one.js');
+    assert.equal(sources[1], '/the/root/two.js');
+    assert.equal(sources.length, 2);
+
+    map = new SourceMapConsumer(util.indexedTestMapDifferentSourceRoots);
+    sources = map.sources;
+    assert.equal(sources[0], '/the/root/one.js');
+    assert.equal(sources[1], '/different/root/two.js');
+    assert.equal(sources.length, 2);
+
     map = new SourceMapConsumer(util.testMapNoSourceRoot);
     sources = map.sources;
     assert.equal(sources[0], 'one.js');
     assert.equal(sources[1], 'two.js');
     assert.equal(sources.length, 2);
 
     map = new SourceMapConsumer(util.testMapEmptySourceRoot);
     sources = map.sources;
@@ -96,68 +122,149 @@ define("test/source-map/test-source-map-
       column: 1
     });
     assert.equal(mapping.source, 'one.js');
   };
 
   exports['test mapping tokens back exactly'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMap);
 
-    util.assertMapping(1, 1, '/the/root/one.js', 1, 1, null, map, assert);
-    util.assertMapping(1, 5, '/the/root/one.js', 1, 5, null, map, assert);
-    util.assertMapping(1, 9, '/the/root/one.js', 1, 11, null, map, assert);
-    util.assertMapping(1, 18, '/the/root/one.js', 1, 21, 'bar', map, assert);
-    util.assertMapping(1, 21, '/the/root/one.js', 2, 3, null, map, assert);
-    util.assertMapping(1, 28, '/the/root/one.js', 2, 10, 'baz', map, assert);
-    util.assertMapping(1, 32, '/the/root/one.js', 2, 14, 'bar', map, assert);
+    util.assertMapping(1, 1, '/the/root/one.js', 1, 1, null, null, map, assert);
+    util.assertMapping(1, 5, '/the/root/one.js', 1, 5, null, null, map, assert);
+    util.assertMapping(1, 9, '/the/root/one.js', 1, 11, null, null, map, assert);
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 21, 'bar', null, map, assert);
+    util.assertMapping(1, 21, '/the/root/one.js', 2, 3, null, null, map, assert);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 10, 'baz', null, map, assert);
+    util.assertMapping(1, 32, '/the/root/one.js', 2, 14, 'bar', null, map, assert);
+
+    util.assertMapping(2, 1, '/the/root/two.js', 1, 1, null, null, map, assert);
+    util.assertMapping(2, 5, '/the/root/two.js', 1, 5, null, null, map, assert);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 11, null, null, map, assert);
+    util.assertMapping(2, 18, '/the/root/two.js', 1, 21, 'n', null, map, assert);
+    util.assertMapping(2, 21, '/the/root/two.js', 2, 3, null, null, map, assert);
+    util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', null, map, assert);
+  };
+
+  exports['test mapping tokens back exactly in indexed source map'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.indexedTestMap);
+
+    util.assertMapping(1, 1, '/the/root/one.js', 1, 1, null, null, map, assert);
+    util.assertMapping(1, 5, '/the/root/one.js', 1, 5, null, null, map, assert);
+    util.assertMapping(1, 9, '/the/root/one.js', 1, 11, null, null, map, assert);
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 21, 'bar', null, map, assert);
+    util.assertMapping(1, 21, '/the/root/one.js', 2, 3, null, null, map, assert);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 10, 'baz', null, map, assert);
+    util.assertMapping(1, 32, '/the/root/one.js', 2, 14, 'bar', null, map, assert);
 
-    util.assertMapping(2, 1, '/the/root/two.js', 1, 1, null, map, assert);
-    util.assertMapping(2, 5, '/the/root/two.js', 1, 5, null, map, assert);
-    util.assertMapping(2, 9, '/the/root/two.js', 1, 11, null, map, assert);
-    util.assertMapping(2, 18, '/the/root/two.js', 1, 21, 'n', map, assert);
-    util.assertMapping(2, 21, '/the/root/two.js', 2, 3, null, map, assert);
-    util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', map, assert);
+    util.assertMapping(2, 1, '/the/root/two.js', 1, 1, null, null, map, assert);
+    util.assertMapping(2, 5, '/the/root/two.js', 1, 5, null, null, map, assert);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 11, null, null, map, assert);
+    util.assertMapping(2, 18, '/the/root/two.js', 1, 21, 'n', null, map, assert);
+    util.assertMapping(2, 21, '/the/root/two.js', 2, 3, null, null, map, assert);
+    util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', null, map, assert);
+  };
+
+
+  exports['test mapping tokens back exactly'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.testMap);
+
+    util.assertMapping(1, 1, '/the/root/one.js', 1, 1, null, null, map, assert);
+    util.assertMapping(1, 5, '/the/root/one.js', 1, 5, null, null, map, assert);
+    util.assertMapping(1, 9, '/the/root/one.js', 1, 11, null, null, map, assert);
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 21, 'bar', null, map, assert);
+    util.assertMapping(1, 21, '/the/root/one.js', 2, 3, null, null, map, assert);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 10, 'baz', null, map, assert);
+    util.assertMapping(1, 32, '/the/root/one.js', 2, 14, 'bar', null, map, assert);
+
+    util.assertMapping(2, 1, '/the/root/two.js', 1, 1, null, null, map, assert);
+    util.assertMapping(2, 5, '/the/root/two.js', 1, 5, null, null, map, assert);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 11, null, null, map, assert);
+    util.assertMapping(2, 18, '/the/root/two.js', 1, 21, 'n', null, map, assert);
+    util.assertMapping(2, 21, '/the/root/two.js', 2, 3, null, null, map, assert);
+    util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', null, map, assert);
   };
 
   exports['test mapping tokens fuzzy'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMap);
 
-    // Finding original positions
-    util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', map, assert, true);
-    util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', map, assert, true);
-    util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, map, assert, true);
+    // Finding original positions with default (glb) bias.
+    util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', null, map, assert, true);
+    util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', null, map, assert, true);
+    util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, null, map, assert, true);
+
+    // Finding original positions with lub bias.
+    util.assertMapping(1, 16, '/the/root/one.js', 1, 21, 'bar', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+    util.assertMapping(1, 26, '/the/root/one.js', 2, 10, 'baz', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+    util.assertMapping(2, 6, '/the/root/two.js', 1, 11, null, SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+
+    // Finding generated positions with default (glb) bias.
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 22, 'bar', null, map, assert, null, true);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 13, 'baz', null, map, assert, null, true);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, null, map, assert, null, true);
+
+    // Finding generated positions with lub bias.
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 20, 'bar', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 7, 'baz', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 6, null, SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
+  };
 
-    // Finding generated positions
-    util.assertMapping(1, 18, '/the/root/one.js', 1, 22, 'bar', map, assert, null, true);
-    util.assertMapping(1, 28, '/the/root/one.js', 2, 13, 'baz', map, assert, null, true);
-    util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, map, assert, null, true);
+  exports['test mapping tokens fuzzy in indexed source map'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.indexedTestMap);
+
+    // Finding original positions with default (glb) bias.
+    util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', null, map, assert, true);
+    util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', null, map, assert, true);
+    util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, null, map, assert, true);
+
+    // Finding original positions with lub bias.
+    util.assertMapping(1, 16, '/the/root/one.js', 1, 21, 'bar', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+    util.assertMapping(1, 26, '/the/root/one.js', 2, 10, 'baz', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+    util.assertMapping(2, 6, '/the/root/two.js', 1, 11, null, SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, true);
+
+    // Finding generated positions with default (glb) bias.
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 22, 'bar', null, map, assert, null, true);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 13, 'baz', null, map, assert, null, true);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, null, map, assert, null, true);
+
+    // Finding generated positions with lub bias.
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 20, 'bar', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 7, 'baz', SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 6, null, SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
   };
 
   exports['test mappings and end of lines'] = function (assert, util) {
     var smg = new SourceMapGenerator({
       file: 'foo.js'
     });
     smg.addMapping({
       original: { line: 1, column: 1 },
       generated: { line: 1, column: 1 },
       source: 'bar.js'
     });
     smg.addMapping({
       original: { line: 2, column: 2 },
       generated: { line: 2, column: 2 },
       source: 'bar.js'
     });
+    smg.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 1, column: 1 },
+      source: 'baz.js'
+    });
 
     var map = SourceMapConsumer.fromSourceMap(smg);
 
     // When finding original positions, mappings end at the end of the line.
-    util.assertMapping(2, 1, null, null, null, null, map, assert, true)
+    util.assertMapping(2, 1, null, null, null, null, null, map, assert, true)
 
     // When finding generated positions, mappings do not end at the end of the line.
-    util.assertMapping(1, 1, 'bar.js', 2, 1, null, map, assert, null, true);
+    util.assertMapping(1, 1, 'bar.js', 2, 1, null, null, map, assert, null, true);
+
+    // When finding generated positions with, mappings end at the end of the source.
+    util.assertMapping(null, null, 'bar.js', 3, 1, null, SourceMapConsumer.LEAST_UPPER_BOUND, map, assert, null, true);
   };
 
   exports['test creating source map consumers with )]}\' prefix'] = function (assert, util) {
     assert.doesNotThrow(function () {
       var map = new SourceMapConsumer(")]}'" + JSON.stringify(util.testMap));
     });
   };
 
@@ -188,16 +295,39 @@ define("test/source-map/test-source-map-
     });
 
     map = new SourceMapConsumer(util.testMapEmptySourceRoot);
     map.eachMapping(function (mapping) {
       assert.ok(mapping.source === 'one.js' || mapping.source === 'two.js');
     });
   };
 
+  exports['test eachMapping for indexed source maps'] = function(assert, util) {
+    var map = new SourceMapConsumer(util.indexedTestMap);
+    var previousLine = -Infinity;
+    var previousColumn = -Infinity;
+    map.eachMapping(function (mapping) {
+      assert.ok(mapping.generatedLine >= previousLine);
+
+      if (mapping.source) {
+        assert.equal(mapping.source.indexOf(util.testMap.sourceRoot), 0);
+      }
+
+      if (mapping.generatedLine === previousLine) {
+        assert.ok(mapping.generatedColumn >= previousColumn);
+        previousColumn = mapping.generatedColumn;
+      }
+      else {
+        previousLine = mapping.generatedLine;
+        previousColumn = -Infinity;
+      }
+    });
+  };
+
+
   exports['test iterating over mappings in a different order'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMap);
     var previousLine = -Infinity;
     var previousColumn = -Infinity;
     var previousSource = "";
     map.eachMapping(function (mapping) {
       assert.ok(mapping.source >= previousSource);
 
@@ -216,24 +346,60 @@ define("test/source-map/test-source-map-
       else {
         previousSource = mapping.source;
         previousLine = -Infinity;
         previousColumn = -Infinity;
       }
     }, null, SourceMapConsumer.ORIGINAL_ORDER);
   };
 
+  exports['test iterating over mappings in a different order in indexed source maps'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.indexedTestMap);
+    var previousLine = -Infinity;
+    var previousColumn = -Infinity;
+    var previousSource = "";
+    map.eachMapping(function (mapping) {
+      assert.ok(mapping.source >= previousSource);
+
+      if (mapping.source === previousSource) {
+        assert.ok(mapping.originalLine >= previousLine);
+
+        if (mapping.originalLine === previousLine) {
+          assert.ok(mapping.originalColumn >= previousColumn);
+          previousColumn = mapping.originalColumn;
+        }
+        else {
+          previousLine = mapping.originalLine;
+          previousColumn = -Infinity;
+        }
+      }
+      else {
+        previousSource = mapping.source;
+        previousLine = -Infinity;
+        previousColumn = -Infinity;
+      }
+    }, null, SourceMapConsumer.ORIGINAL_ORDER);
+  };
+
   exports['test that we can set the context for `this` in eachMapping'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMap);
     var context = {};
     map.eachMapping(function () {
       assert.equal(this, context);
     }, context);
   };
 
+  exports['test that we can set the context for `this` in eachMapping in indexed source maps'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.indexedTestMap);
+    var context = {};
+    map.eachMapping(function () {
+      assert.equal(this, context);
+    }, context);
+  };
+
   exports['test that the `sourcesContent` field has the original sources'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMapWithSourcesContent);
     var sourcesContent = map.sourcesContent;
 
     asser