Bug 852868 - Disable channel decoding if the "Content-Encoding" header matches the file extension. r=enn
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 19 Aug 2013 17:18:25 +0200
changeset 143065 3bcc3e774eee69e2325a98c2cc07f880dc81d498
parent 143064 ab1d6d80090eba98f105cc95b8c22df97a1d1846
child 143066 0cec459440080e6433ce9d07971ebd81fbef8139
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersenn
bugs852868
milestone26.0a1
Bug 852868 - Disable channel decoding if the "Content-Encoding" header matches the file extension. r=enn
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/test/unit/common_test_Download.js
toolkit/components/jsdownloads/test/unit/head.js
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -62,16 +62,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+           "@mozilla.org/uriloader/external-helper-app-service;1",
+           Ci.nsIExternalHelperAppService);
+
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
 /**
  * Returns true if the given value is a primitive string or a String object.
  */
 function isString(aValue) {
@@ -1403,16 +1407,34 @@ DownloadCopySaver.prototype = {
 
               // Ensure we report the value of "Content-Length", if available,
               // even if the download doesn't generate any progress events
               // later.
               if (channel.contentLength >= 0) {
                 aSetProgressBytesFn(0, channel.contentLength);
               }
 
+              // If the URL we are downloading from includes a file extension
+              // that matches the "Content-Encoding" header, for example ".gz"
+              // with a "gzip" encoding, we should save the file in its encoded
+              // form.  In all other cases, we decode the body while saving.
+              if (channel instanceof Ci.nsIEncodedChannel &&
+                  channel.contentEncodings) {
+                let uri = channel.URI;
+                if (uri instanceof Ci.nsIURL && uri.fileExtension) {
+                  // Only the first, outermost encoding is considered.
+                  let encoding = channel.contentEncodings.getNext();
+                  if (encoding) {
+                    channel.applyConversion =
+                      gExternalHelperAppService.applyDecodingForExtension(
+                                                uri.fileExtension, encoding);
+                  }
+                }
+              }
+
               if (keepPartialData) {
                 // If the source is not resumable, don't keep partial data even
                 // if we were asked to try and do it.
                 if (aRequest instanceof Ci.nsIResumableChannel) {
                   try {
                     // If reading the ID succeeds, the source is resumable.
                     this.entityID = aRequest.entityID;
                   } catch (ex if ex instanceof Components.Exception &&
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1319,29 +1319,69 @@ add_task(function test_with_content_enco
   do_register_cleanup(cleanup);
 
   gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
     aResponse.setHeader("Content-Type", "text/plain", false);
     aResponse.setHeader("Content-Encoding", "gzip", false);
     aResponse.setHeader("Content-Length",
                         "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
 
-    let bos =  new BinaryOutputStream(aResponse.bodyOutputStream);
+    let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
     bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
                        TEST_DATA_SHORT_GZIP_ENCODED.length);
   });
 
   let download = yield promiseStartDownload(sourceUrl);
   yield promiseDownloadStopped(download);
 
   do_check_eq(download.progress, 100);
   do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
 
   // Ensure the content matches the decoded test data.
   yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
+
+  cleanup();
+});
+
+/**
+ * Checks that the file is not decoded if the extension matches the encoding.
+ */
+add_task(function test_with_content_encoding_ignore_extension()
+{
+  let sourcePath = "/test_with_content_encoding_ignore_extension.gz";
+  let sourceUrl = httpUrl("test_with_content_encoding_ignore_extension.gz");
+
+  function cleanup() {
+    gHttpServer.registerPathHandler(sourcePath, null);
+  }
+  do_register_cleanup(cleanup);
+
+  gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+    aResponse.setHeader("Content-Type", "text/plain", false);
+    aResponse.setHeader("Content-Encoding", "gzip", false);
+    aResponse.setHeader("Content-Length",
+                        "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
+
+    let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+    bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
+                       TEST_DATA_SHORT_GZIP_ENCODED.length);
+  });
+
+  let download = yield promiseStartDownload(sourceUrl);
+  yield promiseDownloadStopped(download);
+
+  do_check_eq(download.progress, 100);
+  do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+  // Ensure the content matches the encoded test data.  We convert the data to a
+  // string before executing the content check.
+  yield promiseVerifyContents(download.target.path,
+        String.fromCharCode.apply(String, TEST_DATA_SHORT_GZIP_ENCODED));
+
+  cleanup();
 });
 
 /**
  * Cancels and restarts a download sequentially with content-encoding.
  */
 add_task(function test_cancel_midway_restart_with_content_encoding()
 {
   mustInterruptResponses();
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -251,16 +251,18 @@ function promiseStartLegacyDownload(aSou
   if (aOptions && aOptions.launchWhenSucceeded) {
     do_check_true(mimeInfo != null);
 
     mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
   }
 
   // Apply decoding if required by the "Content-Encoding" header.
   persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
+  persist.persistFlags |=
+    Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
 
   // We must create the nsITransfer implementation using its class ID because
   // the "@mozilla.org/transfer;1" contract is currently implemented in
   // "toolkit/components/downloads".  When the other folder is not included in
   // builds anymore (bug 851471), we'll be able to use the contract ID.
   let transfer =
       Components.classesByID["{1b4c85df-cbdd-4bb6-b04e-613caece083c}"]
                 .createInstance(Ci.nsITransfer);
@@ -419,22 +421,24 @@ function promiseVerifyContents(aPath, aE
       do_throw("File is empty: " + aPath);
     }
 
     let deferred = Promise.defer();
     NetUtil.asyncFetch(file, function(aInputStream, aStatus) {
       do_check_true(Components.isSuccessCode(aStatus));
       let contents = NetUtil.readInputStreamToString(aInputStream,
                                                      aInputStream.available());
-      if (contents.length <= TEST_DATA_SHORT.length * 2) {
-        do_check_eq(contents, aExpectedContents);
-      } else {
+      if (contents.length > TEST_DATA_SHORT.length * 2 ||
+          /[^\x20-\x7E]/.test(contents)) {
         // Do not print the entire content string to the test log.
         do_check_eq(contents.length, aExpectedContents.length);
         do_check_true(contents == aExpectedContents);
+      } else {
+        // Print the string if it is short and made of printable characters.
+        do_check_eq(contents, aExpectedContents);
       }
       deferred.resolve();
     });
     yield deferred.promise;
   });
 }
 
 /**