Bug 1497925 - CreateImageBitmap must ignore the Blob.type value, r=aosmond
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 23 Oct 2018 23:35:43 +0200
changeset 490923 cdc621a21acc1cd486abd0342a5b3fec89f594a5
parent 490922 dcaf26b9678bf7183892c56fe3dfcb52f7c393d9
child 490924 dac317f3cd311d70772b55ee65c5ba3051e60fe3
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersaosmond
bugs1497925
milestone65.0a1
Bug 1497925 - CreateImageBitmap must ignore the Blob.type value, r=aosmond
dom/canvas/ImageBitmap.cpp
dom/canvas/test/mochitest.ini
dom/canvas/test/test_invalid_mime_type_blob.html
testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-blob-invalidtype.html
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -13,19 +13,22 @@
 #include "mozilla/dom/StructuredCloneTags.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/Swizzle.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/ScopeExit.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
 #include "ImageBitmapColorUtils.h"
 #include "ImageBitmapUtils.h"
 #include "ImageUtils.h"
+#include "imgLoader.h"
 #include "imgTools.h"
 
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 using mozilla::dom::HTMLMediaElement_Binding::NETWORK_EMPTY;
 using mozilla::dom::HTMLMediaElement_Binding::HAVE_METADATA;
 
 namespace mozilla {
@@ -1229,58 +1232,58 @@ AsyncFulfillImageBitmapPromise(Promise* 
     task->Dispatch(); // Actually, to the current worker-thread.
   }
 }
 
 class CreateImageBitmapFromBlobRunnable;
 
 class CreateImageBitmapFromBlob final : public CancelableRunnable
                                       , public imgIContainerCallback
+                                      , public nsIInputStreamCallback
 {
   friend class CreateImageBitmapFromBlobRunnable;
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_IMGICONTAINERCALLBACK
+  NS_DECL_NSIINPUTSTREAMCALLBACK
 
   static already_AddRefed<CreateImageBitmapFromBlob>
   Create(Promise* aPromise,
          nsIGlobalObject* aGlobal,
          Blob& aBlob,
          const Maybe<IntRect>& aCropRect,
          nsIEventTarget* aMainThreadEventTarget);
 
   NS_IMETHOD Run() override
   {
     MOZ_ASSERT(IsCurrentThread());
 
-    nsresult rv = StartDecodeAndCropBlob();
+    nsresult rv = StartMimeTypeAndDecodeAndCropBlob();
     if (NS_WARN_IF(NS_FAILED(rv))) {
-      DecodeAndCropBlobCompletedMainThread(nullptr, rv);
+      MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr, rv);
     }
 
     return NS_OK;
   }
 
   // Called by the WorkerRef.
   void WorkerShuttingDown();
 
 private:
   CreateImageBitmapFromBlob(Promise* aPromise,
                             nsIGlobalObject* aGlobal,
                             already_AddRefed<nsIInputStream> aInputStream,
-                            const nsACString& aMimeType,
                             const Maybe<IntRect>& aCropRect,
                             nsIEventTarget* aMainThreadEventTarget)
     : CancelableRunnable("dom::CreateImageBitmapFromBlob")
     , mMutex("dom::CreateImageBitmapFromBlob::mMutex")
     , mPromise(aPromise)
     , mGlobalObject(aGlobal)
     , mInputStream(std::move(aInputStream))
-    , mMimeType(aMimeType)
     , mCropRect(aCropRect)
     , mOriginalCropRect(aCropRect)
     , mMainThreadEventTarget(aMainThreadEventTarget)
     , mThread(GetCurrentVirtualThread())
   {
   }
 
   virtual ~CreateImageBitmapFromBlob()
@@ -1288,55 +1291,63 @@ private:
   }
 
   bool IsCurrentThread() const
   {
     return mThread == GetCurrentVirtualThread();
   }
 
   // Called on the owning thread.
-  nsresult StartDecodeAndCropBlob();
+  nsresult StartMimeTypeAndDecodeAndCropBlob();
 
   // Will be called when the decoding + cropping is completed on the
   // main-thread. This could the not the owning thread!
-  void DecodeAndCropBlobCompletedMainThread(layers::Image* aImage,
-                                            nsresult aStatus);
+  void MimeTypeAndDecodeAndCropBlobCompletedMainThread(layers::Image* aImage,
+                                                       nsresult aStatus);
 
   // Will be called when the decoding + cropping is completed on the owning
   // thread.
-  void DecodeAndCropBlobCompletedOwningThread(layers::Image* aImage,
-                                              nsresult aStatus);
+  void MimeTypeAndDecodeAndCropBlobCompletedOwningThread(layers::Image* aImage,
+                                                         nsresult aStatus);
+
+  // This is called on the main-thread only.
+  nsresult MimeTypeAndDecodeAndCropBlob();
 
   // This is called on the main-thread only.
-  nsresult DecodeAndCropBlob();
+  nsresult DecodeAndCropBlob(const nsACString& aMimeType);
+
+  // This is called on the main-thread only.
+  nsresult GetMimeTypeSync(nsACString& aMimeType);
+
+  // This is called on the main-thread only.
+  nsresult GetMimeTypeAsync();
 
   Mutex mMutex;
 
   // The access to this object is protected by mutex but is always nullified on
   // the owning thread.
   RefPtr<ThreadSafeWorkerRef> mWorkerRef;
 
   // Touched only on the owning thread.
   RefPtr<Promise> mPromise;
 
   // Touched only on the owning thread.
   nsCOMPtr<nsIGlobalObject> mGlobalObject;
 
   nsCOMPtr<nsIInputStream> mInputStream;
-  nsCString mMimeType;
   Maybe<IntRect> mCropRect;
   Maybe<IntRect> mOriginalCropRect;
   IntSize mSourceSize;
 
   nsCOMPtr<nsIEventTarget> mMainThreadEventTarget;
   void* mThread;
 };
 
 NS_IMPL_ISUPPORTS_INHERITED(CreateImageBitmapFromBlob, CancelableRunnable,
-                            imgIContainerCallback)
+                            imgIContainerCallback, nsIInputStreamCallback)
 
 class CreateImageBitmapFromBlobRunnable : public WorkerRunnable
 {
 public:
   explicit CreateImageBitmapFromBlobRunnable(WorkerPrivate* aWorkerPrivate,
                                              CreateImageBitmapFromBlob* aTask,
                                              layers::Image* aImage,
                                              nsresult aStatus)
@@ -1344,17 +1355,17 @@ public:
     , mTask(aTask)
     , mImage(aImage)
     , mStatus(aStatus)
   {}
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
-    mTask->DecodeAndCropBlobCompletedOwningThread(mImage, mStatus);
+    mTask->MimeTypeAndDecodeAndCropBlobCompletedOwningThread(mImage, mStatus);
     return true;
   }
 
 private:
   RefPtr<CreateImageBitmapFromBlob> mTask;
   RefPtr<layers::Image> mImage;
   nsresult mStatus;
 };
@@ -2162,24 +2173,29 @@ CreateImageBitmapFromBlob::Create(Promis
   // Get the internal stream of the blob.
   nsCOMPtr<nsIInputStream> stream;
   ErrorResult error;
   aBlob.Impl()->CreateInputStream(getter_AddRefs(stream), error);
   if (NS_WARN_IF(error.Failed())) {
     return nullptr;
   }
 
-  // Get the MIME type string of the blob.
-  // The type will be checked in the DecodeImageAsync() method.
-  nsAutoString mimeTypeUTF16;
-  aBlob.Impl()->GetType(mimeTypeUTF16);
-  NS_ConvertUTF16toUTF8 mimeType(mimeTypeUTF16);
+  if (!NS_InputStreamIsBuffered(stream)) {
+    nsCOMPtr<nsIInputStream> bufferedStream;
+    nsresult rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream),
+                                   stream.forget(), 4096);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return nullptr;
+    }
+
+    stream = bufferedStream;
+  }
 
   RefPtr<CreateImageBitmapFromBlob> task =
-    new CreateImageBitmapFromBlob(aPromise, aGlobal, stream.forget(), mimeType,
+    new CreateImageBitmapFromBlob(aPromise, aGlobal, stream.forget(),
                                   aCropRect, aMainThreadEventTarget);
 
   // Nothing to do for the main-thread.
   if (NS_IsMainThread()) {
     return task.forget();
   }
 
   // Let's use a WorkerRef to keep the worker alive if this is not the
@@ -2196,86 +2212,150 @@ CreateImageBitmapFromBlob::Create(Promis
     return nullptr;
   }
 
   task->mWorkerRef = new ThreadSafeWorkerRef(workerRef);
   return task.forget();
 }
 
 nsresult
-CreateImageBitmapFromBlob::StartDecodeAndCropBlob()
+CreateImageBitmapFromBlob::StartMimeTypeAndDecodeAndCropBlob()
 {
   MOZ_ASSERT(IsCurrentThread());
 
   // Workers.
   if (!NS_IsMainThread()) {
     RefPtr<CreateImageBitmapFromBlob> self = this;
     nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
-      "CreateImageBitmapFromBlob::DecodeAndCropBlob",
+      "CreateImageBitmapFromBlob::MimeTypeAndDecodeAndCropBlob",
       [self]() {
-        nsresult rv = self->DecodeAndCropBlob();
+        nsresult rv = self->MimeTypeAndDecodeAndCropBlob();
         if (NS_WARN_IF(NS_FAILED(rv))) {
-          self->DecodeAndCropBlobCompletedMainThread(nullptr, rv);
+          self->MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr, rv);
         }
       });
 
     return mMainThreadEventTarget->Dispatch(r.forget());
   }
 
   // Main-thread.
-  return DecodeAndCropBlob();
+  return MimeTypeAndDecodeAndCropBlob();
 }
 
 nsresult
-CreateImageBitmapFromBlob::DecodeAndCropBlob()
+CreateImageBitmapFromBlob::MimeTypeAndDecodeAndCropBlob()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  nsAutoCString mimeType;
+  nsresult rv = GetMimeTypeSync(mimeType);
+  if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
+    return GetMimeTypeAsync();
+  }
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return DecodeAndCropBlob(mimeType);
+}
+
+nsresult
+CreateImageBitmapFromBlob::DecodeAndCropBlob(const nsACString& aMimeType)
+{
   // Get the Component object.
   nsCOMPtr<imgITools> imgtool = do_GetService(NS_IMGTOOLS_CID);
   if (NS_WARN_IF(!imgtool)) {
     return NS_ERROR_FAILURE;
   }
 
   // Decode image.
-  nsCOMPtr<imgIContainer> imgContainer;
-  nsresult rv = imgtool->DecodeImageAsync(mInputStream, mMimeType, this,
+  nsresult rv = imgtool->DecodeImageAsync(mInputStream, aMimeType, this,
                                           mMainThreadEventTarget);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
+static nsresult
+sniff_cb(nsIInputStream* aInputStream,
+         void* aClosure,
+         const char* aFromRawSegment,
+         uint32_t aToOffset,
+         uint32_t aCount,
+         uint32_t* aWriteCount)
+{
+  nsACString* mimeType = static_cast<nsACString*>(aClosure);
+  MOZ_ASSERT(mimeType);
+
+  if (aCount > 0) {
+    imgLoader::GetMimeTypeFromContent(aFromRawSegment, aCount, *mimeType);
+  }
+
+  *aWriteCount = 0;
+
+  // We don't want to consume data from the stream.
+  return NS_ERROR_FAILURE;
+}
+
+nsresult
+CreateImageBitmapFromBlob::GetMimeTypeSync(nsACString& aMimeType)
+{
+  uint32_t dummy;
+  return mInputStream->ReadSegments(sniff_cb, &aMimeType, 128, &dummy);
+}
+
+nsresult
+CreateImageBitmapFromBlob::GetMimeTypeAsync()
+{
+  nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
+    do_QueryInterface(mInputStream);
+  if (NS_WARN_IF(!asyncInputStream)) {
+    // If the stream is not async, why are we here?
+    return NS_ERROR_FAILURE;
+  }
+
+  return asyncInputStream->AsyncWait(this, 0, 128, mMainThreadEventTarget);
+}
+
+NS_IMETHODIMP
+CreateImageBitmapFromBlob::OnInputStreamReady(nsIAsyncInputStream* aStream)
+{
+  // The stream should have data now. Let's start from scratch again.
+  return MimeTypeAndDecodeAndCropBlob();
+}
+
 NS_IMETHODIMP
 CreateImageBitmapFromBlob::OnImageReady(imgIContainer* aImgContainer,
                                         nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (NS_FAILED(aStatus)) {
-    DecodeAndCropBlobCompletedMainThread(nullptr, aStatus);
+    MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr, aStatus);
     return NS_OK;
   }
 
   MOZ_ASSERT(aImgContainer);
 
   // Get the surface out.
   uint32_t frameFlags = imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_WANT_DATA_SURFACE;
   uint32_t whichFrame = imgIContainer::FRAME_FIRST;
   RefPtr<SourceSurface> surface = aImgContainer->GetFrame(whichFrame, frameFlags);
 
   if (NS_WARN_IF(!surface)) {
-    DecodeAndCropBlobCompletedMainThread(nullptr,
-                                         NS_ERROR_DOM_INVALID_STATE_ERR);
+    MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr,
+                                                    NS_ERROR_DOM_INVALID_STATE_ERR);
     return NS_OK;
   }
 
-  // Store the sourceSize value for the DecodeAndCropBlobCompletedMainThread call.
+  // Store the sourceSize value for the
+  // MimeTypeAndDecodeAndCropBlobCompletedMainThread call.
   mSourceSize = surface->GetSize();
 
   // Crop the source surface if needed.
   RefPtr<SourceSurface> croppedSurface = surface;
 
   if (mCropRect.isSome()) {
     // The blob is just decoded into a RasterImage and not optimized yet, so the
     // _surface_ we get is a DataSourceSurface which wraps the RasterImage's
@@ -2290,37 +2370,37 @@ CreateImageBitmapFromBlob::OnImageReady(
     //       decode the blob off the main thread. Re-check if we should do
     //       cropping at this moment again there.
     RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
     croppedSurface = CropAndCopyDataSourceSurface(dataSurface, mCropRect.ref());
     mCropRect->MoveTo(0, 0);
   }
 
   if (NS_WARN_IF(!croppedSurface)) {
-    DecodeAndCropBlobCompletedMainThread(nullptr,
-                                         NS_ERROR_DOM_INVALID_STATE_ERR);
+    MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr,
+                                                    NS_ERROR_DOM_INVALID_STATE_ERR);
     return NS_OK;
   }
 
   // Create an Image from the source surface.
   RefPtr<layers::Image> image = CreateImageFromSurface(croppedSurface);
 
   if (NS_WARN_IF(!image)) {
-    DecodeAndCropBlobCompletedMainThread(nullptr,
-                                         NS_ERROR_DOM_INVALID_STATE_ERR);
+    MimeTypeAndDecodeAndCropBlobCompletedMainThread(nullptr,
+                                                    NS_ERROR_DOM_INVALID_STATE_ERR);
     return NS_OK;
   }
 
-  DecodeAndCropBlobCompletedMainThread(image, NS_OK);
+  MimeTypeAndDecodeAndCropBlobCompletedMainThread(image, NS_OK);
   return NS_OK;
 }
 
 void
-CreateImageBitmapFromBlob::DecodeAndCropBlobCompletedMainThread(layers::Image* aImage,
-                                                                nsresult aStatus)
+CreateImageBitmapFromBlob::MimeTypeAndDecodeAndCropBlobCompletedMainThread(layers::Image* aImage,
+                                                                           nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!IsCurrentThread()) {
     MutexAutoLock lock(mMutex);
 
     if (!mWorkerRef) {
       // The worker is already gone.
@@ -2329,22 +2409,22 @@ CreateImageBitmapFromBlob::DecodeAndCrop
 
     RefPtr<CreateImageBitmapFromBlobRunnable> r =
       new CreateImageBitmapFromBlobRunnable(mWorkerRef->Private(),
                                             this, aImage, aStatus);
     r->Dispatch();
     return;
   }
 
-  DecodeAndCropBlobCompletedOwningThread(aImage, aStatus);
+  MimeTypeAndDecodeAndCropBlobCompletedOwningThread(aImage, aStatus);
 }
 
 void
-CreateImageBitmapFromBlob::DecodeAndCropBlobCompletedOwningThread(layers::Image* aImage,
-                                                                  nsresult aStatus)
+CreateImageBitmapFromBlob::MimeTypeAndDecodeAndCropBlobCompletedOwningThread(layers::Image* aImage,
+                                                                             nsresult aStatus)
 {
   MOZ_ASSERT(IsCurrentThread());
 
   if (!mPromise) {
     // The worker is going to be released soon. No needs to continue.
     return;
   }
 
--- a/dom/canvas/test/mochitest.ini
+++ b/dom/canvas/test/mochitest.ini
@@ -306,8 +306,9 @@ skip-if = 1
 [test_offscreencanvas_sizechange.html]
 subsuite = gpu
 tags = offscreencanvas
 skip-if = 1
 [test_offscreencanvas_subworker.html]
 subsuite = gpu
 tags = offscreencanvas
 skip-if = 1
+[test_invalid_mime_type_blob.html]
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/test_invalid_mime_type_blob.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<title>createImageBitmap from Blob with invalid type</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<body>
+<script>
+
+function syncBlob() {
+  info("Let's create a small memory blob...");
+
+  // A 1x1 PNG image.
+  // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+  const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+                     "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+
+  let bytes = new Array(IMAGE.length);
+  for (let i = 0; i < IMAGE.length; i++) {
+    bytes[i] = IMAGE.charCodeAt(i);
+  }
+
+  let blob = new Blob([new Uint8Array(bytes)], { type: "text/html"});
+  window.createImageBitmap(blob)
+  .then(imageBitmap => {
+    ok(true, "Image created!");
+    is(imageBitmap.width, 1, "Image is 1x1");
+    is(imageBitmap.height, 1, "Image is 1x1");
+  })
+  .then(next);
+}
+
+function asyncBlob() {
+  info("Let's create a big memory blob...");
+
+  // A 1x1 PNG image.
+  // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+  const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+                     "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+
+  let bytes = new Array(IMAGE.length);
+  for (let i = 0; i < IMAGE.length; i++) {
+    bytes[i] = IMAGE.charCodeAt(i);
+  }
+
+  let array = [];
+  for (let i = 0; i < 20000; ++i) {
+    array.push(new Uint8Array(bytes));
+  }
+
+  let blob = new Blob(array, { type: "text/html"});
+  ok(blob.size > 1000000, "More than 1mb");
+
+  let bc = new BroadcastChannel('a');
+  bc.onmessage = e => {
+    window.createImageBitmap(e.data)
+    .then(imageBitmap => {
+      ok(true, "Image created!");
+      is(imageBitmap.width, 1, "Image is 1x1");
+      is(imageBitmap.height, 1, "Image is 1x1");
+    })
+    .then(next);
+  }
+
+  new BroadcastChannel('a').postMessage(blob);
+}
+
+let tests = [
+ syncBlob,
+ asyncBlob,
+];
+
+function next() {
+  if (tests.length == 0) {
+    SimpleTest.finish();
+    return;
+  }
+
+  let test = tests.shift();
+  test();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-blob-invalidtype.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<title>createImageBitmap: blob with wrong mime type</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/canvas-tests.js"></script>
+<script>
+promise_test(t => {
+  // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+  const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+                     "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+
+  let bytes = new Array(IMAGE.length);
+  for (let i = 0; i < IMAGE.length; i++) {
+    bytes[i] = IMAGE.charCodeAt(i);
+  }
+
+  let blob = new Blob([new Uint8Array(bytes)], { type: "text/html"});
+
+  return window.createImageBitmap(blob)
+    .then(imageBitmap => {
+      assert_true(true, "Image created!");
+      assert_equals(imageBitmap.width, 1, "Image is 1x1");
+      assert_equals(imageBitmap.height, 1, "Image is 1x1");
+    });
+});
+</script>