1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "FileReaderSync.h"
8 
9 #include "js/ArrayBuffer.h"  // JS::NewArrayBufferWithContents
10 #include "js/RootingAPI.h"   // JS::{,Mutable}Handle
11 #include "js/Utility.h"  // js::ArrayBufferContentsArena, JS::FreePolicy, js_pod_arena_malloc
12 #include "mozilla/Unused.h"
13 #include "mozilla/Base64.h"
14 #include "mozilla/dom/File.h"
15 #include "mozilla/Encoding.h"
16 #include "mozilla/dom/FileReaderSyncBinding.h"
17 #include "nsCExternalHandlerService.h"
18 #include "nsComponentManagerUtils.h"
19 #include "nsCOMPtr.h"
20 #include "nsError.h"
21 #include "nsIConverterInputStream.h"
22 #include "nsIInputStream.h"
23 #include "nsIMultiplexInputStream.h"
24 #include "nsStreamUtils.h"
25 #include "nsStringStream.h"
26 #include "nsISupportsImpl.h"
27 #include "nsNetUtil.h"
28 #include "nsServiceManagerUtils.h"
29 #include "nsIAsyncInputStream.h"
30 #include "mozilla/dom/WorkerPrivate.h"
31 #include "mozilla/dom/WorkerRunnable.h"
32 
33 using namespace mozilla;
34 using namespace mozilla::dom;
35 using mozilla::dom::GlobalObject;
36 using mozilla::dom::Optional;
37 
38 // static
Constructor(const GlobalObject & aGlobal)39 already_AddRefed<FileReaderSync> FileReaderSync::Constructor(
40     const GlobalObject& aGlobal) {
41   RefPtr<FileReaderSync> frs = new FileReaderSync();
42 
43   return frs.forget();
44 }
45 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto,JS::MutableHandle<JSObject * > aReflector)46 bool FileReaderSync::WrapObject(JSContext* aCx,
47                                 JS::Handle<JSObject*> aGivenProto,
48                                 JS::MutableHandle<JSObject*> aReflector) {
49   return FileReaderSync_Binding::Wrap(aCx, this, aGivenProto, aReflector);
50 }
51 
ReadAsArrayBuffer(JSContext * aCx,JS::Handle<JSObject * > aScopeObj,Blob & aBlob,JS::MutableHandle<JSObject * > aRetval,ErrorResult & aRv)52 void FileReaderSync::ReadAsArrayBuffer(JSContext* aCx,
53                                        JS::Handle<JSObject*> aScopeObj,
54                                        Blob& aBlob,
55                                        JS::MutableHandle<JSObject*> aRetval,
56                                        ErrorResult& aRv) {
57   uint64_t blobSize = aBlob.GetSize(aRv);
58   if (NS_WARN_IF(aRv.Failed())) {
59     return;
60   }
61 
62   UniquePtr<char[], JS::FreePolicy> bufferData(
63       js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, blobSize));
64   if (!bufferData) {
65     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
66     return;
67   }
68 
69   nsCOMPtr<nsIInputStream> stream;
70   aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
71   if (NS_WARN_IF(aRv.Failed())) {
72     return;
73   }
74 
75   uint32_t numRead;
76   aRv = SyncRead(stream, bufferData.get(), blobSize, &numRead);
77   if (NS_WARN_IF(aRv.Failed())) {
78     return;
79   }
80 
81   // The file is changed in the meantime?
82   if (numRead != blobSize) {
83     aRv.Throw(NS_ERROR_FAILURE);
84     return;
85   }
86 
87   JSObject* arrayBuffer =
88       JS::NewArrayBufferWithContents(aCx, blobSize, bufferData.get());
89   if (!arrayBuffer) {
90     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
91     return;
92   }
93   // arrayBuffer takes the ownership when it is not null. Otherwise we
94   // need to release it explicitly.
95   mozilla::Unused << bufferData.release();
96 
97   aRetval.set(arrayBuffer);
98 }
99 
ReadAsBinaryString(Blob & aBlob,nsAString & aResult,ErrorResult & aRv)100 void FileReaderSync::ReadAsBinaryString(Blob& aBlob, nsAString& aResult,
101                                         ErrorResult& aRv) {
102   nsCOMPtr<nsIInputStream> stream;
103   aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
104   if (NS_WARN_IF(aRv.Failed())) {
105     return;
106   }
107 
108   uint32_t numRead;
109   do {
110     char readBuf[4096];
111     aRv = SyncRead(stream, readBuf, sizeof(readBuf), &numRead);
112     if (NS_WARN_IF(aRv.Failed())) {
113       return;
114     }
115 
116     uint32_t oldLength = aResult.Length();
117     AppendASCIItoUTF16(Substring(readBuf, readBuf + numRead), aResult);
118     if (aResult.Length() - oldLength != numRead) {
119       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
120       return;
121     }
122   } while (numRead > 0);
123 }
124 
ReadAsText(Blob & aBlob,const Optional<nsAString> & aEncoding,nsAString & aResult,ErrorResult & aRv)125 void FileReaderSync::ReadAsText(Blob& aBlob,
126                                 const Optional<nsAString>& aEncoding,
127                                 nsAString& aResult, ErrorResult& aRv) {
128   nsCOMPtr<nsIInputStream> stream;
129   aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
130   if (NS_WARN_IF(aRv.Failed())) {
131     return;
132   }
133 
134   nsCString sniffBuf;
135   if (!sniffBuf.SetLength(3, fallible)) {
136     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
137     return;
138   }
139 
140   uint32_t numRead = 0;
141   aRv = SyncRead(stream, sniffBuf.BeginWriting(), sniffBuf.Length(), &numRead);
142   if (NS_WARN_IF(aRv.Failed())) {
143     return;
144   }
145 
146   // No data, we don't need to continue.
147   if (numRead == 0) {
148     aResult.Truncate();
149     return;
150   }
151 
152   // Try the API argument.
153   const Encoding* encoding =
154       aEncoding.WasPassed() ? Encoding::ForLabel(aEncoding.Value()) : nullptr;
155   if (!encoding) {
156     // API argument failed. Try the type property of the blob.
157     nsAutoString type16;
158     aBlob.GetType(type16);
159     NS_ConvertUTF16toUTF8 type(type16);
160     nsAutoCString specifiedCharset;
161     bool haveCharset;
162     int32_t charsetStart, charsetEnd;
163     NS_ExtractCharsetFromContentType(type, specifiedCharset, &haveCharset,
164                                      &charsetStart, &charsetEnd);
165     encoding = Encoding::ForLabel(specifiedCharset);
166     if (!encoding) {
167       // Type property failed. Use UTF-8.
168       encoding = UTF_8_ENCODING;
169     }
170   }
171 
172   if (numRead < sniffBuf.Length()) {
173     sniffBuf.Truncate(numRead);
174   }
175 
176   // Let's recreate the full stream using a:
177   // multiplexStream(syncStream + original stream)
178   // In theory, we could try to see if the inputStream is a nsISeekableStream,
179   // but this doesn't work correctly for nsPipe3 - See bug 1349570.
180 
181   nsCOMPtr<nsIMultiplexInputStream> multiplexStream =
182       do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1");
183   if (NS_WARN_IF(!multiplexStream)) {
184     aRv.Throw(NS_ERROR_FAILURE);
185     return;
186   }
187 
188   nsCOMPtr<nsIInputStream> sniffStringStream;
189   aRv = NS_NewCStringInputStream(getter_AddRefs(sniffStringStream), sniffBuf);
190   if (NS_WARN_IF(aRv.Failed())) {
191     return;
192   }
193 
194   aRv = multiplexStream->AppendStream(sniffStringStream);
195   if (NS_WARN_IF(aRv.Failed())) {
196     return;
197   }
198 
199   uint64_t blobSize = aBlob.GetSize(aRv);
200   if (NS_WARN_IF(aRv.Failed())) {
201     return;
202   }
203 
204   nsCOMPtr<nsIInputStream> syncStream;
205   aRv = ConvertAsyncToSyncStream(blobSize - sniffBuf.Length(), stream.forget(),
206                                  getter_AddRefs(syncStream));
207   if (NS_WARN_IF(aRv.Failed())) {
208     return;
209   }
210 
211   // ConvertAsyncToSyncStream returns a null syncStream if the stream has been
212   // already closed or there is nothing to read.
213   if (syncStream) {
214     aRv = multiplexStream->AppendStream(syncStream);
215     if (NS_WARN_IF(aRv.Failed())) {
216       return;
217     }
218   }
219 
220   nsAutoCString charset;
221   encoding->Name(charset);
222 
223   nsCOMPtr<nsIInputStream> multiplex(do_QueryInterface(multiplexStream));
224   aRv = ConvertStream(multiplex, charset.get(), aResult);
225   if (NS_WARN_IF(aRv.Failed())) {
226     return;
227   }
228 }
229 
ReadAsDataURL(Blob & aBlob,nsAString & aResult,ErrorResult & aRv)230 void FileReaderSync::ReadAsDataURL(Blob& aBlob, nsAString& aResult,
231                                    ErrorResult& aRv) {
232   nsAutoString scratchResult;
233   scratchResult.AssignLiteral("data:");
234 
235   nsString contentType;
236   aBlob.GetType(contentType);
237 
238   if (contentType.IsEmpty()) {
239     scratchResult.AppendLiteral("application/octet-stream");
240   } else {
241     scratchResult.Append(contentType);
242   }
243   scratchResult.AppendLiteral(";base64,");
244 
245   nsCOMPtr<nsIInputStream> stream;
246   aBlob.CreateInputStream(getter_AddRefs(stream), aRv);
247   if (NS_WARN_IF(aRv.Failed())) {
248     return;
249   }
250 
251   uint64_t blobSize = aBlob.GetSize(aRv);
252   if (NS_WARN_IF(aRv.Failed())) {
253     return;
254   }
255 
256   nsCOMPtr<nsIInputStream> syncStream;
257   aRv = ConvertAsyncToSyncStream(blobSize, stream.forget(),
258                                  getter_AddRefs(syncStream));
259   if (NS_WARN_IF(aRv.Failed())) {
260     return;
261   }
262 
263   MOZ_ASSERT(syncStream);
264 
265   uint64_t size;
266   aRv = syncStream->Available(&size);
267   if (NS_WARN_IF(aRv.Failed())) {
268     return;
269   }
270 
271   // The file is changed in the meantime?
272   if (blobSize != size) {
273     return;
274   }
275 
276   nsAutoString encodedData;
277   aRv = Base64EncodeInputStream(syncStream, encodedData, size);
278   if (NS_WARN_IF(aRv.Failed())) {
279     return;
280   }
281 
282   scratchResult.Append(encodedData);
283 
284   aResult = scratchResult;
285 }
286 
ConvertStream(nsIInputStream * aStream,const char * aCharset,nsAString & aResult)287 nsresult FileReaderSync::ConvertStream(nsIInputStream* aStream,
288                                        const char* aCharset,
289                                        nsAString& aResult) {
290   nsCOMPtr<nsIConverterInputStream> converterStream =
291       do_CreateInstance("@mozilla.org/intl/converter-input-stream;1");
292   NS_ENSURE_TRUE(converterStream, NS_ERROR_FAILURE);
293 
294   nsresult rv = converterStream->Init(
295       aStream, aCharset, 8192,
296       nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER);
297   NS_ENSURE_SUCCESS(rv, rv);
298 
299   nsCOMPtr<nsIUnicharInputStream> unicharStream = converterStream;
300   NS_ENSURE_TRUE(unicharStream, NS_ERROR_FAILURE);
301 
302   uint32_t numChars;
303   nsString result;
304   while (NS_SUCCEEDED(unicharStream->ReadString(8192, result, &numChars)) &&
305          numChars > 0) {
306     uint32_t oldLength = aResult.Length();
307     aResult.Append(result);
308     if (aResult.Length() - oldLength != result.Length()) {
309       return NS_ERROR_OUT_OF_MEMORY;
310     }
311   }
312 
313   return rv;
314 }
315 
316 namespace {
317 
318 // This runnable is used to terminate the sync event loop.
319 class ReadReadyRunnable final : public WorkerSyncRunnable {
320  public:
ReadReadyRunnable(WorkerPrivate * aWorkerPrivate,nsIEventTarget * aSyncLoopTarget)321   ReadReadyRunnable(WorkerPrivate* aWorkerPrivate,
322                     nsIEventTarget* aSyncLoopTarget)
323       : WorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget) {}
324 
WorkerRun(JSContext * aCx,WorkerPrivate * aWorkerPrivate)325   bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
326     aWorkerPrivate->AssertIsOnWorkerThread();
327     MOZ_ASSERT(mSyncLoopTarget);
328 
329     nsCOMPtr<nsIEventTarget> syncLoopTarget;
330     mSyncLoopTarget.swap(syncLoopTarget);
331 
332     aWorkerPrivate->StopSyncLoop(syncLoopTarget, true);
333     return true;
334   }
335 
336  private:
337   ~ReadReadyRunnable() = default;
338 };
339 
340 // This class implements nsIInputStreamCallback and it will be called when the
341 // stream is ready to be read.
342 class ReadCallback final : public nsIInputStreamCallback {
343  public:
344   NS_DECL_THREADSAFE_ISUPPORTS
345 
ReadCallback(WorkerPrivate * aWorkerPrivate,nsIEventTarget * aEventTarget)346   ReadCallback(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aEventTarget)
347       : mWorkerPrivate(aWorkerPrivate), mEventTarget(aEventTarget) {}
348 
349   NS_IMETHOD
OnInputStreamReady(nsIAsyncInputStream * aStream)350   OnInputStreamReady(nsIAsyncInputStream* aStream) override {
351     // I/O Thread. Now we need to block the sync event loop.
352     RefPtr<ReadReadyRunnable> runnable =
353         new ReadReadyRunnable(mWorkerPrivate, mEventTarget);
354     return mEventTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
355   }
356 
357  private:
358   ~ReadCallback() = default;
359 
360   // The worker is kept alive because of the sync event loop.
361   WorkerPrivate* mWorkerPrivate;
362   nsCOMPtr<nsIEventTarget> mEventTarget;
363 };
364 
365 NS_IMPL_ADDREF(ReadCallback);
366 NS_IMPL_RELEASE(ReadCallback);
367 
368 NS_INTERFACE_MAP_BEGIN(ReadCallback)
369   NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
370   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback)
371 NS_INTERFACE_MAP_END
372 
373 }  // namespace
374 
SyncRead(nsIInputStream * aStream,char * aBuffer,uint32_t aBufferSize,uint32_t * aTotalBytesRead)375 nsresult FileReaderSync::SyncRead(nsIInputStream* aStream, char* aBuffer,
376                                   uint32_t aBufferSize,
377                                   uint32_t* aTotalBytesRead) {
378   MOZ_ASSERT(aStream);
379   MOZ_ASSERT(aBuffer);
380   MOZ_ASSERT(aTotalBytesRead);
381 
382   WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
383   MOZ_ASSERT(workerPrivate);
384 
385   *aTotalBytesRead = 0;
386 
387   nsCOMPtr<nsIAsyncInputStream> asyncStream;
388   nsCOMPtr<nsIEventTarget> target;
389 
390   while (*aTotalBytesRead < aBufferSize) {
391     uint32_t currentBytesRead = 0;
392 
393     // Let's read something.
394     nsresult rv =
395         aStream->Read(aBuffer + *aTotalBytesRead,
396                       aBufferSize - *aTotalBytesRead, &currentBytesRead);
397 
398     // Nothing else to read.
399     if (rv == NS_BASE_STREAM_CLOSED ||
400         (NS_SUCCEEDED(rv) && currentBytesRead == 0)) {
401       return NS_OK;
402     }
403 
404     // An error.
405     if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
406       return rv;
407     }
408 
409     // All good.
410     if (NS_SUCCEEDED(rv)) {
411       *aTotalBytesRead += currentBytesRead;
412       continue;
413     }
414 
415     // We need to proceed async.
416     if (!asyncStream) {
417       asyncStream = do_QueryInterface(aStream);
418       if (!asyncStream) {
419         return rv;
420       }
421     }
422 
423     AutoSyncLoopHolder syncLoop(workerPrivate, Canceling);
424 
425     nsCOMPtr<nsIEventTarget> syncLoopTarget = syncLoop.GetEventTarget();
426     if (!syncLoopTarget) {
427       // SyncLoop creation can fail if the worker is shutting down.
428       return NS_ERROR_DOM_INVALID_STATE_ERR;
429     }
430 
431     RefPtr<ReadCallback> callback =
432         new ReadCallback(workerPrivate, syncLoopTarget);
433 
434     if (!target) {
435       target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
436       MOZ_ASSERT(target);
437     }
438 
439     rv = asyncStream->AsyncWait(callback, 0, aBufferSize - *aTotalBytesRead,
440                                 target);
441     if (NS_WARN_IF(NS_FAILED(rv))) {
442       return rv;
443     }
444 
445     if (!syncLoop.Run()) {
446       return NS_ERROR_DOM_INVALID_STATE_ERR;
447     }
448   }
449 
450   return NS_OK;
451 }
452 
ConvertAsyncToSyncStream(uint64_t aStreamSize,already_AddRefed<nsIInputStream> aAsyncStream,nsIInputStream ** aSyncStream)453 nsresult FileReaderSync::ConvertAsyncToSyncStream(
454     uint64_t aStreamSize, already_AddRefed<nsIInputStream> aAsyncStream,
455     nsIInputStream** aSyncStream) {
456   nsCOMPtr<nsIInputStream> asyncInputStream = std::move(aAsyncStream);
457 
458   // If the stream is not async, we just need it to be bufferable.
459   nsCOMPtr<nsIAsyncInputStream> asyncStream =
460       do_QueryInterface(asyncInputStream);
461   if (!asyncStream) {
462     return NS_NewBufferedInputStream(aSyncStream, asyncInputStream.forget(),
463                                      4096);
464   }
465 
466   nsAutoCString buffer;
467   if (!buffer.SetLength(aStreamSize, fallible)) {
468     return NS_ERROR_OUT_OF_MEMORY;
469   }
470 
471   uint32_t read;
472   nsresult rv =
473       SyncRead(asyncInputStream, buffer.BeginWriting(), aStreamSize, &read);
474   if (NS_WARN_IF(NS_FAILED(rv))) {
475     return rv;
476   }
477 
478   if (read != aStreamSize) {
479     return NS_ERROR_FAILURE;
480   }
481 
482   rv = NS_NewCStringInputStream(aSyncStream, std::move(buffer));
483   if (NS_WARN_IF(NS_FAILED(rv))) {
484     return rv;
485   }
486 
487   return NS_OK;
488 }
489