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 "IOUtils.h"
8 
9 #include <cstdint>
10 
11 #include "ErrorList.h"
12 #include "js/ArrayBuffer.h"
13 #include "js/JSON.h"
14 #include "js/Utility.h"
15 #include "js/experimental/TypedData.h"
16 #include "jsfriendapi.h"
17 #include "mozilla/AutoRestore.h"
18 #include "mozilla/Compression.h"
19 #include "mozilla/Encoding.h"
20 #include "mozilla/EndianUtils.h"
21 #include "mozilla/ErrorNames.h"
22 #include "mozilla/Maybe.h"
23 #include "mozilla/ResultExtensions.h"
24 #include "mozilla/Services.h"
25 #include "mozilla/Span.h"
26 #include "mozilla/StaticPtr.h"
27 #include "mozilla/TextUtils.h"
28 #include "mozilla/Unused.h"
29 #include "mozilla/Utf8.h"
30 #include "mozilla/dom/IOUtilsBinding.h"
31 #include "mozilla/dom/Promise.h"
32 #include "nsCOMPtr.h"
33 #include "nsError.h"
34 #include "nsFileStreams.h"
35 #include "nsIDirectoryEnumerator.h"
36 #include "nsIFile.h"
37 #include "nsIGlobalObject.h"
38 #include "nsISupports.h"
39 #include "nsLocalFile.h"
40 #include "nsPrintfCString.h"
41 #include "nsReadableUtils.h"
42 #include "nsString.h"
43 #include "nsStringFwd.h"
44 #include "nsTArray.h"
45 #include "nsThreadManager.h"
46 #include "nsXULAppAPI.h"
47 #include "prerror.h"
48 #include "prio.h"
49 #include "prtime.h"
50 #include "prtypes.h"
51 
52 #ifndef ANDROID
53 #  include "nsSystemInfo.h"
54 #endif
55 
56 #define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise)               \
57   do {                                                                   \
58     if (nsresult _rv = (_file)->InitWithPath((_path)); NS_FAILED(_rv)) { \
59       (_promise)->MaybeRejectWithOperationError(                         \
60           FormatErrorMessage(_rv, "Could not parse path (%s)",           \
61                              NS_ConvertUTF16toUTF8(_path).get()));       \
62       return (_promise).forget();                                        \
63     }                                                                    \
64   } while (0)
65 
66 static constexpr auto SHUTDOWN_ERROR =
67     "IOUtils: Shutting down and refusing additional I/O tasks"_ns;
68 
69 namespace mozilla::dom {
70 
71 // static helper functions
72 
73 /**
74  * Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may
75  * report I/O errors inconsistently. For convenience, this function will attempt
76  * to match a |nsresult| against known results which imply a file cannot be
77  * found.
78  *
79  * @see nsLocalFileWin.cpp
80  * @see nsLocalFileUnix.cpp
81  */
IsFileNotFound(nsresult aResult)82 static bool IsFileNotFound(nsresult aResult) {
83   return aResult == NS_ERROR_FILE_NOT_FOUND ||
84          aResult == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
85 }
86 /**
87  * Like |IsFileNotFound|, but checks for known results that suggest a file
88  * is not a directory.
89  */
IsNotDirectory(nsresult aResult)90 static bool IsNotDirectory(nsresult aResult) {
91   return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR ||
92          aResult == NS_ERROR_FILE_NOT_DIRECTORY;
93 }
94 
95 /**
96  * Formats an error message and appends the error name to the end.
97  */
98 template <typename... Args>
FormatErrorMessage(nsresult aError,const char * const aMessage,Args...aArgs)99 static nsCString FormatErrorMessage(nsresult aError, const char* const aMessage,
100                                     Args... aArgs) {
101   nsPrintfCString msg(aMessage, aArgs...);
102 
103   if (const char* errName = GetStaticErrorName(aError)) {
104     msg.AppendPrintf(": %s", errName);
105   } else {
106     // In the exceptional case where there is no error name, print the literal
107     // integer value of the nsresult as an upper case hex value so it can be
108     // located easily in searchfox.
109     msg.AppendPrintf(": 0x%" PRIX32, static_cast<uint32_t>(aError));
110   }
111 
112   return std::move(msg);
113 }
114 
FormatErrorMessage(nsresult aError,const char * const aMessage)115 static nsCString FormatErrorMessage(nsresult aError,
116                                     const char* const aMessage) {
117   const char* errName = GetStaticErrorName(aError);
118   if (errName) {
119     return nsPrintfCString("%s: %s", aMessage, errName);
120   }
121   // In the exceptional case where there is no error name, print the literal
122   // integer value of the nsresult as an upper case hex value so it can be
123   // located easily in searchfox.
124   return nsPrintfCString("%s: 0x%" PRIX32, aMessage,
125                          static_cast<uint32_t>(aError));
126 }
127 
ToJSValue(JSContext * aCx,const IOUtils::InternalFileInfo & aInternalFileInfo,JS::MutableHandle<JS::Value> aValue)128 [[nodiscard]] inline bool ToJSValue(
129     JSContext* aCx, const IOUtils::InternalFileInfo& aInternalFileInfo,
130     JS::MutableHandle<JS::Value> aValue) {
131   FileInfo info;
132   info.mPath.Construct(aInternalFileInfo.mPath);
133   info.mType.Construct(aInternalFileInfo.mType);
134   info.mSize.Construct(aInternalFileInfo.mSize);
135   info.mLastModified.Construct(aInternalFileInfo.mLastModified);
136 
137   if (aInternalFileInfo.mCreationTime.isSome()) {
138     info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref());
139   }
140 
141   info.mPermissions.Construct(aInternalFileInfo.mPermissions);
142 
143   return ToJSValue(aCx, info, aValue);
144 }
145 
146 template <typename T>
ResolveJSPromise(Promise * aPromise,T && aValue)147 static void ResolveJSPromise(Promise* aPromise, T&& aValue) {
148   if constexpr (std::is_same_v<T, Ok>) {
149     aPromise->MaybeResolveWithUndefined();
150   } else {
151     aPromise->MaybeResolve(std::forward<T>(aValue));
152   }
153 }
154 
RejectJSPromise(Promise * aPromise,const IOUtils::IOError & aError)155 static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) {
156   const auto& errMsg = aError.Message();
157 
158   switch (aError.Code()) {
159     case NS_ERROR_FILE_UNRESOLVABLE_SYMLINK:
160       [[fallthrough]];  // to NS_ERROR_FILE_INVALID_PATH
161     case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
162       [[fallthrough]];  // to NS_ERROR_FILE_INVALID_PATH
163     case NS_ERROR_FILE_NOT_FOUND:
164       [[fallthrough]];  // to NS_ERROR_FILE_INVALID_PATH
165     case NS_ERROR_FILE_INVALID_PATH:
166       aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns));
167       break;
168     case NS_ERROR_FILE_IS_LOCKED:
169       [[fallthrough]];  // to NS_ERROR_FILE_ACCESS_DENIED
170     case NS_ERROR_FILE_ACCESS_DENIED:
171       aPromise->MaybeRejectWithNotAllowedError(
172           errMsg.refOr("Access was denied to the target file"_ns));
173       break;
174     case NS_ERROR_FILE_TOO_BIG:
175       aPromise->MaybeRejectWithNotReadableError(
176           errMsg.refOr("Target file is too big"_ns));
177       break;
178     case NS_ERROR_FILE_NO_DEVICE_SPACE:
179       aPromise->MaybeRejectWithNotReadableError(
180           errMsg.refOr("Target device is full"_ns));
181       break;
182     case NS_ERROR_FILE_ALREADY_EXISTS:
183       aPromise->MaybeRejectWithNoModificationAllowedError(
184           errMsg.refOr("Target file already exists"_ns));
185       break;
186     case NS_ERROR_FILE_COPY_OR_MOVE_FAILED:
187       aPromise->MaybeRejectWithOperationError(
188           errMsg.refOr("Failed to copy or move the target file"_ns));
189       break;
190     case NS_ERROR_FILE_READ_ONLY:
191       aPromise->MaybeRejectWithReadOnlyError(
192           errMsg.refOr("Target file is read only"_ns));
193       break;
194     case NS_ERROR_FILE_NOT_DIRECTORY:
195       [[fallthrough]];  // to NS_ERROR_FILE_DESTINATION_NOT_DIR
196     case NS_ERROR_FILE_DESTINATION_NOT_DIR:
197       aPromise->MaybeRejectWithInvalidAccessError(
198           errMsg.refOr("Target file is not a directory"_ns));
199       break;
200     case NS_ERROR_FILE_IS_DIRECTORY:
201       aPromise->MaybeRejectWithInvalidAccessError(
202           errMsg.refOr("Target file is a directory"_ns));
203       break;
204     case NS_ERROR_FILE_UNKNOWN_TYPE:
205       aPromise->MaybeRejectWithInvalidAccessError(
206           errMsg.refOr("Target file is of unknown type"_ns));
207       break;
208     case NS_ERROR_FILE_NAME_TOO_LONG:
209       aPromise->MaybeRejectWithOperationError(
210           errMsg.refOr("Target file path is too long"_ns));
211       break;
212     case NS_ERROR_FILE_UNRECOGNIZED_PATH:
213       aPromise->MaybeRejectWithOperationError(
214           errMsg.refOr("Target file path is not recognized"_ns));
215       break;
216     case NS_ERROR_FILE_DIR_NOT_EMPTY:
217       aPromise->MaybeRejectWithOperationError(
218           errMsg.refOr("Target directory is not empty"_ns));
219       break;
220     case NS_ERROR_FILE_DEVICE_FAILURE:
221       [[fallthrough]];  // to NS_ERROR_FILE_FS_CORRUPTED
222     case NS_ERROR_FILE_FS_CORRUPTED:
223       aPromise->MaybeRejectWithNotReadableError(
224           errMsg.refOr("Target file system may be corrupt or unavailable"_ns));
225       break;
226     case NS_ERROR_FILE_CORRUPTED:
227       aPromise->MaybeRejectWithNotReadableError(
228           errMsg.refOr("Target file could not be read and may be corrupt"_ns));
229       break;
230     case NS_ERROR_ILLEGAL_INPUT:
231       [[fallthrough]];  // NS_ERROR_ILLEGAL_VALUE
232     case NS_ERROR_ILLEGAL_VALUE:
233       aPromise->MaybeRejectWithDataError(
234           errMsg.refOr("Argument is not allowed"_ns));
235       break;
236     case NS_ERROR_ABORT:
237       aPromise->MaybeRejectWithAbortError(errMsg.refOr("Operation aborted"_ns));
238       break;
239     default:
240       aPromise->MaybeRejectWithUnknownError(
241           errMsg.refOr(FormatErrorMessage(aError.Code(), "Unexpected error")));
242   }
243 }
244 
RejectShuttingDown(Promise * aPromise)245 static void RejectShuttingDown(Promise* aPromise) {
246   RejectJSPromise(aPromise,
247                   IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR));
248 }
249 
250 // IOUtils implementation
251 /* static */
252 IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"};
253 
254 /* static */
255 template <typename OkT, typename Fn>
DispatchAndResolve(IOUtils::EventQueue * aQueue,Promise * aPromise,Fn aFunc)256 void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise,
257                                  Fn aFunc) {
258   if (RefPtr<IOPromise<OkT>> p = aQueue->Dispatch<OkT, Fn>(std::move(aFunc))) {
259     p->Then(
260         GetCurrentSerialEventTarget(), __func__,
261         [promise = RefPtr(aPromise)](OkT&& ok) {
262           ResolveJSPromise(promise, std::forward<OkT>(ok));
263         },
264         [promise = RefPtr(aPromise)](const IOError& err) {
265           RejectJSPromise(promise, err);
266         });
267   }
268 }
269 
270 /* static */
Read(GlobalObject & aGlobal,const nsAString & aPath,const ReadOptions & aOptions)271 already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
272                                         const nsAString& aPath,
273                                         const ReadOptions& aOptions) {
274   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
275   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
276   if (!promise) {
277     return nullptr;
278   }
279 
280   if (auto state = GetState()) {
281     nsCOMPtr<nsIFile> file = new nsLocalFile();
282     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
283 
284     Maybe<uint32_t> toRead = Nothing();
285     if (!aOptions.mMaxBytes.IsNull()) {
286       if (aOptions.mMaxBytes.Value() == 0) {
287         // Resolve with an empty buffer.
288         nsTArray<uint8_t> arr(0);
289         promise->MaybeResolve(TypedArrayCreator<Uint8Array>(arr));
290         return promise.forget();
291       }
292       toRead.emplace(aOptions.mMaxBytes.Value());
293     }
294 
295     DispatchAndResolve<JsBuffer>(
296         state.ref()->mEventQueue, promise,
297         [file = std::move(file), offset = aOptions.mOffset, toRead,
298          decompress = aOptions.mDecompress]() {
299           return ReadSync(file, offset, toRead, decompress,
300                           BufferKind::Uint8Array);
301         });
302   } else {
303     RejectShuttingDown(promise);
304   }
305   return promise.forget();
306 }
307 
308 /* static */
ReadUTF8(GlobalObject & aGlobal,const nsAString & aPath,const ReadUTF8Options & aOptions)309 already_AddRefed<Promise> IOUtils::ReadUTF8(GlobalObject& aGlobal,
310                                             const nsAString& aPath,
311                                             const ReadUTF8Options& aOptions) {
312   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
313   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
314   if (!promise) {
315     return nullptr;
316   }
317 
318   if (auto state = GetState()) {
319     nsCOMPtr<nsIFile> file = new nsLocalFile();
320     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
321 
322     DispatchAndResolve<JsBuffer>(
323         state.ref()->mEventQueue, promise,
324         [file = std::move(file), decompress = aOptions.mDecompress]() {
325           return ReadUTF8Sync(file, decompress);
326         });
327   } else {
328     RejectShuttingDown(promise);
329   }
330   return promise.forget();
331 }
332 
333 /* static */
ReadJSON(GlobalObject & aGlobal,const nsAString & aPath,const ReadUTF8Options & aOptions)334 already_AddRefed<Promise> IOUtils::ReadJSON(GlobalObject& aGlobal,
335                                             const nsAString& aPath,
336                                             const ReadUTF8Options& aOptions) {
337   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
338   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
339   if (!promise) {
340     return nullptr;
341   }
342 
343   if (auto state = GetState()) {
344     nsCOMPtr<nsIFile> file = new nsLocalFile();
345     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
346 
347     state.ref()
348         ->mEventQueue
349         ->Dispatch<JsBuffer>([file, decompress = aOptions.mDecompress]() {
350           return ReadUTF8Sync(file, decompress);
351         })
352         ->Then(
353             GetCurrentSerialEventTarget(), __func__,
354             [promise, file](JsBuffer&& aBuffer) {
355               AutoJSAPI jsapi;
356               if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
357                 promise->MaybeRejectWithUnknownError(
358                     "Could not initialize JS API");
359                 return;
360               }
361               JSContext* cx = jsapi.cx();
362 
363               JS::Rooted<JSString*> jsonStr(
364                   cx, IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer)));
365               if (!jsonStr) {
366                 RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY));
367                 return;
368               }
369 
370               JS::Rooted<JS::Value> val(cx);
371               if (!JS_ParseJSON(cx, jsonStr, &val)) {
372                 JS::Rooted<JS::Value> exn(cx);
373                 if (JS_GetPendingException(cx, &exn)) {
374                   JS_ClearPendingException(cx);
375                   promise->MaybeReject(exn);
376                 } else {
377                   RejectJSPromise(
378                       promise,
379                       IOError(NS_ERROR_DOM_UNKNOWN_ERR)
380                           .WithMessage(
381                               "ParseJSON threw an uncatchable exception "
382                               "while parsing file(%s)",
383                               file->HumanReadablePath().get()));
384                 }
385 
386                 return;
387               }
388 
389               promise->MaybeResolve(val);
390             },
391             [promise](const IOError& aErr) { RejectJSPromise(promise, aErr); });
392   } else {
393     RejectShuttingDown(promise);
394   }
395   return promise.forget();
396 }
397 
398 /* static */
Write(GlobalObject & aGlobal,const nsAString & aPath,const Uint8Array & aData,const WriteOptions & aOptions)399 already_AddRefed<Promise> IOUtils::Write(GlobalObject& aGlobal,
400                                          const nsAString& aPath,
401                                          const Uint8Array& aData,
402                                          const WriteOptions& aOptions) {
403   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
404   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
405   if (!promise) {
406     return nullptr;
407   }
408 
409   if (auto state = GetState()) {
410     nsCOMPtr<nsIFile> file = new nsLocalFile();
411     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
412 
413     aData.ComputeState();
414     auto buf = Buffer<uint8_t>::CopyFrom(Span(aData.Data(), aData.Length()));
415     if (buf.isNothing()) {
416       promise->MaybeRejectWithOperationError(
417           "Out of memory: Could not allocate buffer while writing to file");
418       return promise.forget();
419     }
420 
421     auto opts = InternalWriteOpts::FromBinding(aOptions);
422     if (opts.isErr()) {
423       RejectJSPromise(promise, opts.unwrapErr());
424       return promise.forget();
425     }
426 
427     DispatchAndResolve<uint32_t>(
428         state.ref()->mEventQueue, promise,
429         [file = std::move(file), buf = std::move(*buf),
430          opts = opts.unwrap()]() { return WriteSync(file, buf, opts); });
431   } else {
432     RejectShuttingDown(promise);
433   }
434   return promise.forget();
435 }
436 
437 /* static */
WriteUTF8(GlobalObject & aGlobal,const nsAString & aPath,const nsACString & aString,const WriteOptions & aOptions)438 already_AddRefed<Promise> IOUtils::WriteUTF8(GlobalObject& aGlobal,
439                                              const nsAString& aPath,
440                                              const nsACString& aString,
441                                              const WriteOptions& aOptions) {
442   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
443   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
444   if (!promise) {
445     return nullptr;
446   }
447 
448   if (auto state = GetState()) {
449     nsCOMPtr<nsIFile> file = new nsLocalFile();
450     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
451 
452     auto opts = InternalWriteOpts::FromBinding(aOptions);
453     if (opts.isErr()) {
454       RejectJSPromise(promise, opts.unwrapErr());
455       return promise.forget();
456     }
457 
458     DispatchAndResolve<uint32_t>(
459         state.ref()->mEventQueue, promise,
460         [file = std::move(file), str = nsCString(aString),
461          opts = opts.unwrap()]() {
462           return WriteSync(file, AsBytes(Span(str)), opts);
463         });
464   } else {
465     RejectShuttingDown(promise);
466   }
467   return promise.forget();
468 }
469 
AppendJsonAsUtf8(const char16_t * aData,uint32_t aLen,void * aStr)470 static bool AppendJsonAsUtf8(const char16_t* aData, uint32_t aLen, void* aStr) {
471   nsCString* str = static_cast<nsCString*>(aStr);
472   return AppendUTF16toUTF8(Span<const char16_t>(aData, aLen), *str, fallible);
473 }
474 
475 /* static */
WriteJSON(GlobalObject & aGlobal,const nsAString & aPath,JS::Handle<JS::Value> aValue,const WriteOptions & aOptions)476 already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal,
477                                              const nsAString& aPath,
478                                              JS::Handle<JS::Value> aValue,
479                                              const WriteOptions& aOptions) {
480   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
481   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
482   if (!promise) {
483     return nullptr;
484   }
485 
486   if (auto state = GetState()) {
487     nsCOMPtr<nsIFile> file = new nsLocalFile();
488     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
489 
490     auto opts = InternalWriteOpts::FromBinding(aOptions);
491     if (opts.isErr()) {
492       RejectJSPromise(promise, opts.unwrapErr());
493       return promise.forget();
494     }
495 
496     if (opts.inspect().mMode == WriteMode::Append) {
497       promise->MaybeRejectWithNotSupportedError(
498           "IOUtils.writeJSON does not support appending to files."_ns);
499       return promise.forget();
500     }
501 
502     JSContext* cx = aGlobal.Context();
503     JS::Rooted<JS::Value> rootedValue(cx, aValue);
504     nsCString utf8Str;
505 
506     if (!JS_Stringify(cx, &rootedValue, nullptr, JS::NullHandleValue,
507                       AppendJsonAsUtf8, &utf8Str)) {
508       JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue());
509       if (JS_GetPendingException(cx, &exn)) {
510         JS_ClearPendingException(cx);
511         promise->MaybeReject(exn);
512       } else {
513         RejectJSPromise(promise,
514                         IOError(NS_ERROR_DOM_UNKNOWN_ERR)
515                             .WithMessage("Could not serialize object to JSON"));
516       }
517       return promise.forget();
518     }
519 
520     DispatchAndResolve<uint32_t>(
521         state.ref()->mEventQueue, promise,
522         [file = std::move(file), utf8Str = std::move(utf8Str),
523          opts = opts.unwrap()]() {
524           return WriteSync(file, AsBytes(Span(utf8Str)), opts);
525         });
526   } else {
527     RejectShuttingDown(promise);
528   }
529   return promise.forget();
530 }
531 
532 /* static */
Move(GlobalObject & aGlobal,const nsAString & aSourcePath,const nsAString & aDestPath,const MoveOptions & aOptions)533 already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal,
534                                         const nsAString& aSourcePath,
535                                         const nsAString& aDestPath,
536                                         const MoveOptions& aOptions) {
537   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
538   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
539   if (!promise) {
540     return nullptr;
541   }
542 
543   if (auto state = GetState()) {
544     nsCOMPtr<nsIFile> sourceFile = new nsLocalFile();
545     REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise);
546 
547     nsCOMPtr<nsIFile> destFile = new nsLocalFile();
548     REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise);
549 
550     DispatchAndResolve<Ok>(
551         state.ref()->mEventQueue, promise,
552         [sourceFile = std::move(sourceFile), destFile = std::move(destFile),
553          noOverwrite = aOptions.mNoOverwrite]() {
554           return MoveSync(sourceFile, destFile, noOverwrite);
555         });
556   } else {
557     RejectShuttingDown(promise);
558   }
559   return promise.forget();
560 }
561 
562 /* static */
Remove(GlobalObject & aGlobal,const nsAString & aPath,const RemoveOptions & aOptions)563 already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal,
564                                           const nsAString& aPath,
565                                           const RemoveOptions& aOptions) {
566   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
567   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
568   if (!promise) {
569     return nullptr;
570   }
571 
572   if (auto state = GetState()) {
573     nsCOMPtr<nsIFile> file = new nsLocalFile();
574     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
575 
576     DispatchAndResolve<Ok>(
577         state.ref()->mEventQueue, promise,
578         [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent,
579          recursive = aOptions.mRecursive]() {
580           return RemoveSync(file, ignoreAbsent, recursive);
581         });
582   } else {
583     RejectShuttingDown(promise);
584   }
585   return promise.forget();
586 }
587 
588 /* static */
MakeDirectory(GlobalObject & aGlobal,const nsAString & aPath,const MakeDirectoryOptions & aOptions)589 already_AddRefed<Promise> IOUtils::MakeDirectory(
590     GlobalObject& aGlobal, const nsAString& aPath,
591     const MakeDirectoryOptions& aOptions) {
592   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
593   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
594   if (!promise) {
595     return nullptr;
596   }
597 
598   if (auto state = GetState()) {
599     nsCOMPtr<nsIFile> file = new nsLocalFile();
600     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
601 
602     DispatchAndResolve<Ok>(
603         state.ref()->mEventQueue, promise,
604         [file = std::move(file), createAncestors = aOptions.mCreateAncestors,
605          ignoreExisting = aOptions.mIgnoreExisting,
606          permissions = aOptions.mPermissions]() {
607           return MakeDirectorySync(file, createAncestors, ignoreExisting,
608                                    permissions);
609         });
610   } else {
611     RejectShuttingDown(promise);
612   }
613   return promise.forget();
614 }
615 
Stat(GlobalObject & aGlobal,const nsAString & aPath)616 already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
617                                         const nsAString& aPath) {
618   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
619   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
620   if (!promise) {
621     return nullptr;
622   }
623 
624   if (auto state = GetState()) {
625     nsCOMPtr<nsIFile> file = new nsLocalFile();
626     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
627 
628     DispatchAndResolve<InternalFileInfo>(
629         state.ref()->mEventQueue, promise,
630         [file = std::move(file)]() { return StatSync(file); });
631   } else {
632     RejectShuttingDown(promise);
633   }
634   return promise.forget();
635 }
636 
637 /* static */
Copy(GlobalObject & aGlobal,const nsAString & aSourcePath,const nsAString & aDestPath,const CopyOptions & aOptions)638 already_AddRefed<Promise> IOUtils::Copy(GlobalObject& aGlobal,
639                                         const nsAString& aSourcePath,
640                                         const nsAString& aDestPath,
641                                         const CopyOptions& aOptions) {
642   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
643   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
644   if (!promise) {
645     return nullptr;
646   }
647 
648   if (auto state = GetState()) {
649     nsCOMPtr<nsIFile> sourceFile = new nsLocalFile();
650     REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise);
651 
652     nsCOMPtr<nsIFile> destFile = new nsLocalFile();
653     REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise);
654 
655     DispatchAndResolve<Ok>(
656         state.ref()->mEventQueue, promise,
657         [sourceFile = std::move(sourceFile), destFile = std::move(destFile),
658          noOverwrite = aOptions.mNoOverwrite,
659          recursive = aOptions.mRecursive]() {
660           return CopySync(sourceFile, destFile, noOverwrite, recursive);
661         });
662   } else {
663     RejectShuttingDown(promise);
664   }
665   return promise.forget();
666 }
667 
668 /* static */
Touch(GlobalObject & aGlobal,const nsAString & aPath,const Optional<int64_t> & aModification)669 already_AddRefed<Promise> IOUtils::Touch(
670     GlobalObject& aGlobal, const nsAString& aPath,
671     const Optional<int64_t>& aModification) {
672   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
673   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
674   if (!promise) {
675     return nullptr;
676   }
677 
678   if (auto state = GetState()) {
679     nsCOMPtr<nsIFile> file = new nsLocalFile();
680     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
681 
682     Maybe<int64_t> newTime = Nothing();
683     if (aModification.WasPassed()) {
684       newTime = Some(aModification.Value());
685     }
686     DispatchAndResolve<int64_t>(state.ref()->mEventQueue, promise,
687                                 [file = std::move(file), newTime]() {
688                                   return TouchSync(file, newTime);
689                                 });
690   } else {
691     RejectShuttingDown(promise);
692   }
693   return promise.forget();
694 }
695 
696 /* static */
GetChildren(GlobalObject & aGlobal,const nsAString & aPath)697 already_AddRefed<Promise> IOUtils::GetChildren(GlobalObject& aGlobal,
698                                                const nsAString& aPath) {
699   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
700   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
701   if (!promise) {
702     return nullptr;
703   }
704 
705   if (auto state = GetState()) {
706     nsCOMPtr<nsIFile> file = new nsLocalFile();
707     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
708 
709     DispatchAndResolve<nsTArray<nsString>>(
710         state.ref()->mEventQueue, promise,
711         [file = std::move(file)]() { return GetChildrenSync(file); });
712   } else {
713     RejectShuttingDown(promise);
714   }
715   return promise.forget();
716 }
717 
718 /* static */
SetPermissions(GlobalObject & aGlobal,const nsAString & aPath,uint32_t aPermissions,const bool aHonorUmask)719 already_AddRefed<Promise> IOUtils::SetPermissions(GlobalObject& aGlobal,
720                                                   const nsAString& aPath,
721                                                   uint32_t aPermissions,
722                                                   const bool aHonorUmask) {
723   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
724   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
725   if (!promise) {
726     return nullptr;
727   }
728 
729 #if defined(XP_UNIX) && !defined(ANDROID)
730   if (aHonorUmask) {
731     aPermissions &= ~nsSystemInfo::gUserUmask;
732   }
733 #endif
734 
735   if (auto state = GetState()) {
736     nsCOMPtr<nsIFile> file = new nsLocalFile();
737     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
738 
739     DispatchAndResolve<Ok>(
740         state.ref()->mEventQueue, promise,
741         [file = std::move(file), permissions = aPermissions]() {
742           return SetPermissionsSync(file, permissions);
743         });
744   } else {
745     RejectShuttingDown(promise);
746   }
747   return promise.forget();
748 }
749 
750 /* static */
Exists(GlobalObject & aGlobal,const nsAString & aPath)751 already_AddRefed<Promise> IOUtils::Exists(GlobalObject& aGlobal,
752                                           const nsAString& aPath) {
753   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
754   RefPtr<Promise> promise = CreateJSPromise(aGlobal);
755   if (!promise) {
756     return nullptr;
757   }
758 
759   if (auto state = GetState()) {
760     nsCOMPtr<nsIFile> file = new nsLocalFile();
761     REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
762 
763     DispatchAndResolve<bool>(
764         state.ref()->mEventQueue, promise,
765         [file = std::move(file)]() { return ExistsSync(file); });
766   } else {
767     RejectShuttingDown(promise);
768   }
769   return promise.forget();
770 }
771 
772 /* static */
CreateJSPromise(GlobalObject & aGlobal)773 already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) {
774   ErrorResult er;
775   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
776   RefPtr<Promise> promise = Promise::Create(global, er);
777   if (er.Failed()) {
778     return nullptr;
779   }
780   MOZ_ASSERT(promise);
781   return do_AddRef(promise);
782 }
783 
784 /* static */
ReadSync(nsIFile * aFile,const uint32_t aOffset,const Maybe<uint32_t> aMaxBytes,const bool aDecompress,IOUtils::BufferKind aBufferKind)785 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadSync(
786     nsIFile* aFile, const uint32_t aOffset, const Maybe<uint32_t> aMaxBytes,
787     const bool aDecompress, IOUtils::BufferKind aBufferKind) {
788   MOZ_ASSERT(!NS_IsMainThread());
789 
790   if (aMaxBytes.isSome() && aDecompress) {
791     return Err(
792         IOError(NS_ERROR_ILLEGAL_INPUT)
793             .WithMessage(
794                 "The `maxBytes` and `decompress` options are not compatible"));
795   }
796 
797   RefPtr<nsFileStream> stream = new nsFileStream();
798   if (nsresult rv =
799           stream->Init(aFile, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0);
800       NS_FAILED(rv)) {
801     return Err(IOError(rv).WithMessage("Could not open the file at %s",
802                                        aFile->HumanReadablePath().get()));
803   }
804   int64_t bufSize = 0;
805 
806   if (aMaxBytes.isNothing()) {
807     // Limitation: We cannot read files that are larger than the max size of a
808     //             TypedArray (UINT32_MAX bytes). Reject if the file is too
809     //             big to be read.
810 
811     int64_t streamSize = -1;
812     if (nsresult rv = stream->GetSize(&streamSize); NS_FAILED(rv)) {
813       return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED)
814                      .WithMessage("Could not get info for the file at %s",
815                                   aFile->HumanReadablePath().get()));
816     }
817     MOZ_RELEASE_ASSERT(streamSize >= 0);
818 
819     if (streamSize > static_cast<int64_t>(UINT32_MAX)) {
820       return Err(
821           IOError(NS_ERROR_FILE_TOO_BIG)
822               .WithMessage("Could not read the file at %s because it is too "
823                            "large(size=%" PRId64 " bytes)",
824                            aFile->HumanReadablePath().get(), streamSize));
825     }
826     bufSize = static_cast<uint32_t>(streamSize);
827 
828     if (aOffset >= bufSize) {
829       bufSize = 0;
830     } else {
831       bufSize = bufSize - aOffset;
832     }
833   } else {
834     bufSize = aMaxBytes.value();
835   }
836 
837   if (aOffset > 0) {
838     if (nsresult rv = stream->Seek(PR_SEEK_SET, aOffset); NS_FAILED(rv)) {
839       return Err(IOError(rv).WithMessage(
840           "Could not seek to position %" PRId64 " in file %s", aOffset,
841           aFile->HumanReadablePath().get()));
842     }
843   }
844 
845   JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind);
846 
847   if (bufSize > 0) {
848     auto result = JsBuffer::Create(aBufferKind, bufSize);
849     if (result.isErr()) {
850       return result.propagateErr();
851     }
852     buffer = result.unwrap();
853     Span<char> toRead = buffer.BeginWriting();
854 
855     // Read the file from disk.
856     uint32_t totalRead = 0;
857     while (totalRead != bufSize) {
858       uint32_t bytesRead = 0;
859       if (nsresult rv =
860               stream->Read(toRead.Elements(), bufSize - totalRead, &bytesRead);
861           NS_FAILED(rv)) {
862         return Err(IOError(rv).WithMessage(
863             "Encountered an unexpected error while reading file(%s)",
864             aFile->HumanReadablePath().get()));
865       }
866       if (bytesRead == 0) {
867         break;
868       }
869       totalRead += bytesRead;
870       toRead = toRead.From(bytesRead);
871     }
872 
873     buffer.SetLength(totalRead);
874   }
875 
876   // Decompress the file contents, if required.
877   if (aDecompress) {
878     return MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind);
879   }
880 
881   return std::move(buffer);
882 }
883 
884 /* static */
ReadUTF8Sync(nsIFile * aFile,bool aDecompress)885 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadUTF8Sync(
886     nsIFile* aFile, bool aDecompress) {
887   auto result = ReadSync(aFile, 0, Nothing{}, aDecompress, BufferKind::String);
888   if (result.isErr()) {
889     return result.propagateErr();
890   }
891 
892   JsBuffer buffer = result.unwrap();
893   if (!IsUtf8(buffer.BeginReading())) {
894     return Err(
895         IOError(NS_ERROR_FILE_CORRUPTED)
896             .WithMessage(
897                 "Could not read file(%s) because it is not UTF-8 encoded",
898                 aFile->HumanReadablePath().get()));
899   }
900 
901   return buffer;
902 }
903 
904 /* static */
WriteSync(nsIFile * aFile,const Span<const uint8_t> & aByteArray,const IOUtils::InternalWriteOpts & aOptions)905 Result<uint32_t, IOUtils::IOError> IOUtils::WriteSync(
906     nsIFile* aFile, const Span<const uint8_t>& aByteArray,
907     const IOUtils::InternalWriteOpts& aOptions) {
908   MOZ_ASSERT(!NS_IsMainThread());
909 
910   nsIFile* backupFile = aOptions.mBackupFile;
911   nsIFile* tempFile = aOptions.mTmpFile;
912 
913   bool exists = false;
914   MOZ_TRY(aFile->Exists(&exists));
915 
916   if (exists && aOptions.mMode == WriteMode::Create) {
917     return Err(IOError(NS_ERROR_DOM_TYPE_MISMATCH_ERR)
918                    .WithMessage("Refusing to overwrite the file at %s\n"
919                                 "Specify `mode: \"overwrite\"` to allow "
920                                 "overwriting the destination",
921                                 aFile->HumanReadablePath().get()));
922   }
923 
924   // If backupFile was specified, perform the backup as a move.
925   if (exists && backupFile) {
926     // We copy `destFile` here to a new `nsIFile` because
927     // `nsIFile::MoveToFollowingLinks` will update the path of the file. If we
928     // did not do this, we would end up having `destFile` point to the same
929     // location as `backupFile`. Then, when we went to write to `destFile`, we
930     // would end up overwriting `backupFile` and never actually write to the
931     // file we were supposed to.
932     nsCOMPtr<nsIFile> toMove;
933     MOZ_ALWAYS_SUCCEEDS(aFile->Clone(getter_AddRefs(toMove)));
934 
935     bool noOverwrite = aOptions.mMode == WriteMode::Create;
936 
937     if (MoveSync(toMove, backupFile, noOverwrite).isErr()) {
938       return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED)
939                      .WithMessage("Failed to backup the source file(%s) to %s",
940                                   aFile->HumanReadablePath().get(),
941                                   backupFile->HumanReadablePath().get()));
942     }
943   }
944 
945   // If tempFile was specified, we will write to there first, then perform a
946   // move to ensure the file ends up at the final requested destination.
947   nsIFile* writeFile;
948 
949   if (tempFile) {
950     writeFile = tempFile;
951   } else {
952     writeFile = aFile;
953   }
954 
955   int32_t flags = PR_WRONLY;
956 
957   switch (aOptions.mMode) {
958     case WriteMode::Overwrite:
959       flags |= PR_TRUNCATE | PR_CREATE_FILE;
960       break;
961 
962     case WriteMode::Append:
963       flags |= PR_APPEND;
964       break;
965 
966     case WriteMode::Create:
967       flags |= PR_CREATE_FILE | PR_EXCL;
968       break;
969 
970     default:
971       MOZ_CRASH("IOUtils: unknown write mode");
972   }
973 
974   if (aOptions.mFlush) {
975     flags |= PR_SYNC;
976   }
977 
978   // Try to perform the write and ensure that the file is closed before
979   // continuing.
980   uint32_t totalWritten = 0;
981   {
982     // Compress the byte array if required.
983     nsTArray<uint8_t> compressed;
984     Span<const char> bytes;
985     if (aOptions.mCompress) {
986       auto rv = MozLZ4::Compress(aByteArray);
987       if (rv.isErr()) {
988         return rv.propagateErr();
989       }
990       compressed = rv.unwrap();
991       bytes = Span(reinterpret_cast<const char*>(compressed.Elements()),
992                    compressed.Length());
993     } else {
994       bytes = Span(reinterpret_cast<const char*>(aByteArray.Elements()),
995                    aByteArray.Length());
996     }
997 
998     RefPtr<nsFileOutputStream> stream = new nsFileOutputStream();
999     if (nsresult rv = stream->Init(writeFile, flags, 0666, 0); NS_FAILED(rv)) {
1000       // Normalize platform-specific errors for opening a directory to an access
1001       // denied error.
1002       if (rv == nsresult::NS_ERROR_FILE_IS_DIRECTORY) {
1003         rv = NS_ERROR_FILE_ACCESS_DENIED;
1004       }
1005       return Err(
1006           IOError(rv).WithMessage("Could not open the file at %s for writing",
1007                                   writeFile->HumanReadablePath().get()));
1008     }
1009 
1010     // nsFileStream::Write uses PR_Write under the hood, which accepts a
1011     // *int32_t* for the chunk size.
1012     uint32_t chunkSize = INT32_MAX;
1013     Span<const char> pendingBytes = bytes;
1014 
1015     while (pendingBytes.Length() > 0) {
1016       if (pendingBytes.Length() < chunkSize) {
1017         chunkSize = pendingBytes.Length();
1018       }
1019 
1020       uint32_t bytesWritten = 0;
1021       if (nsresult rv =
1022               stream->Write(pendingBytes.Elements(), chunkSize, &bytesWritten);
1023           NS_FAILED(rv)) {
1024         return Err(IOError(rv).WithMessage(
1025             "Could not write chunk (size = %" PRIu32
1026             ") to file %s. The file may be corrupt.",
1027             chunkSize, writeFile->HumanReadablePath().get()));
1028       }
1029       pendingBytes = pendingBytes.From(bytesWritten);
1030       totalWritten += bytesWritten;
1031     }
1032   }
1033 
1034   // If tempFile was passed, check destFile against writeFile and, if they
1035   // differ, the operation is finished by performing a move.
1036   if (tempFile) {
1037     nsAutoStringN<256> destPath;
1038     nsAutoStringN<256> writePath;
1039 
1040     MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath));
1041     MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath));
1042 
1043     // nsIFile::MoveToFollowingLinks will only update the path of the file if
1044     // the move succeeds.
1045     if (destPath != writePath) {
1046       if (aOptions.mTmpFile) {
1047         bool isDir = false;
1048         if (nsresult rv = aFile->IsDirectory(&isDir);
1049             NS_FAILED(rv) && !IsFileNotFound(rv)) {
1050           return Err(IOError(rv).WithMessage("Could not stat the file at %s",
1051                                              aFile->HumanReadablePath().get()));
1052         }
1053 
1054         // If we attempt to write to a directory *without* a temp file, we get a
1055         // permission error.
1056         //
1057         // However, if we are writing to a temp file first, when we copy the
1058         // temp file over the destination file, we actually end up copying it
1059         // inside the directory, which is not what we want. In this case, we are
1060         // just going to bail out early.
1061         if (isDir) {
1062           return Err(
1063               IOError(NS_ERROR_FILE_ACCESS_DENIED)
1064                   .WithMessage("Could not open the file at %s for writing",
1065                                aFile->HumanReadablePath().get()));
1066         }
1067       }
1068 
1069       if (MoveSync(writeFile, aFile, /* aNoOverwrite = */ false).isErr()) {
1070         return Err(
1071             IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED)
1072                 .WithMessage(
1073                     "Could not move temporary file(%s) to destination(%s)",
1074                     writeFile->HumanReadablePath().get(),
1075                     aFile->HumanReadablePath().get()));
1076       }
1077     }
1078   }
1079   return totalWritten;
1080 }
1081 
1082 /* static */
MoveSync(nsIFile * aSourceFile,nsIFile * aDestFile,bool aNoOverwrite)1083 Result<Ok, IOUtils::IOError> IOUtils::MoveSync(nsIFile* aSourceFile,
1084                                                nsIFile* aDestFile,
1085                                                bool aNoOverwrite) {
1086   MOZ_ASSERT(!NS_IsMainThread());
1087 
1088   // Ensure the source file exists before continuing. If it doesn't exist,
1089   // subsequent operations can fail in different ways on different platforms.
1090   bool srcExists = false;
1091   MOZ_TRY(aSourceFile->Exists(&srcExists));
1092   if (!srcExists) {
1093     return Err(
1094         IOError(NS_ERROR_FILE_NOT_FOUND)
1095             .WithMessage(
1096                 "Could not move source file(%s) because it does not exist",
1097                 aSourceFile->HumanReadablePath().get()));
1098   }
1099 
1100   return CopyOrMoveSync(&nsIFile::MoveToFollowingLinks, "move", aSourceFile,
1101                         aDestFile, aNoOverwrite);
1102 }
1103 
1104 /* static */
CopySync(nsIFile * aSourceFile,nsIFile * aDestFile,bool aNoOverwrite,bool aRecursive)1105 Result<Ok, IOUtils::IOError> IOUtils::CopySync(nsIFile* aSourceFile,
1106                                                nsIFile* aDestFile,
1107                                                bool aNoOverwrite,
1108                                                bool aRecursive) {
1109   MOZ_ASSERT(!NS_IsMainThread());
1110 
1111   // Ensure the source file exists before continuing. If it doesn't exist,
1112   // subsequent operations can fail in different ways on different platforms.
1113   bool srcExists;
1114   MOZ_TRY(aSourceFile->Exists(&srcExists));
1115   if (!srcExists) {
1116     return Err(
1117         IOError(NS_ERROR_FILE_NOT_FOUND)
1118             .WithMessage(
1119                 "Could not copy source file(%s) because it does not exist",
1120                 aSourceFile->HumanReadablePath().get()));
1121   }
1122 
1123   // If source is a directory, fail immediately unless the recursive option is
1124   // true.
1125   bool srcIsDir = false;
1126   MOZ_TRY(aSourceFile->IsDirectory(&srcIsDir));
1127   if (srcIsDir && !aRecursive) {
1128     return Err(
1129         IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED)
1130             .WithMessage(
1131                 "Refused to copy source directory(%s) to the destination(%s)\n"
1132                 "Specify the `recursive: true` option to allow copying "
1133                 "directories",
1134                 aSourceFile->HumanReadablePath().get(),
1135                 aDestFile->HumanReadablePath().get()));
1136   }
1137 
1138   return CopyOrMoveSync(&nsIFile::CopyToFollowingLinks, "copy", aSourceFile,
1139                         aDestFile, aNoOverwrite);
1140 }
1141 
1142 /* static */
1143 template <typename CopyOrMoveFn>
CopyOrMoveSync(CopyOrMoveFn aMethod,const char * aMethodName,nsIFile * aSource,nsIFile * aDest,bool aNoOverwrite)1144 Result<Ok, IOUtils::IOError> IOUtils::CopyOrMoveSync(CopyOrMoveFn aMethod,
1145                                                      const char* aMethodName,
1146                                                      nsIFile* aSource,
1147                                                      nsIFile* aDest,
1148                                                      bool aNoOverwrite) {
1149   MOZ_ASSERT(!NS_IsMainThread());
1150 
1151   // Case 1: Destination is an existing directory. Copy/move source into dest.
1152   bool destIsDir = false;
1153   bool destExists = true;
1154 
1155   nsresult rv = aDest->IsDirectory(&destIsDir);
1156   if (NS_SUCCEEDED(rv) && destIsDir) {
1157     rv = (aSource->*aMethod)(aDest, u""_ns);
1158     if (NS_FAILED(rv)) {
1159       return Err(IOError(rv).WithMessage(
1160           "Could not %s source file(%s) to destination directory(%s)",
1161           aMethodName, aSource->HumanReadablePath().get(),
1162           aDest->HumanReadablePath().get()));
1163     }
1164     return Ok();
1165   }
1166 
1167   if (NS_FAILED(rv)) {
1168     if (!IsFileNotFound(rv)) {
1169       // It's ok if the dest file doesn't exist. Case 2 handles this below.
1170       // Bail out early for any other kind of error though.
1171       return Err(IOError(rv));
1172     }
1173     destExists = false;
1174   }
1175 
1176   // Case 2: Destination is a file which may or may not exist.
1177   //         Try to copy or rename the source to the destination.
1178   //         If the destination exists and the source is not a regular file,
1179   //         then this may fail.
1180   if (aNoOverwrite && destExists) {
1181     return Err(
1182         IOError(NS_ERROR_FILE_ALREADY_EXISTS)
1183             .WithMessage(
1184                 "Could not %s source file(%s) to destination(%s) because the "
1185                 "destination already exists and overwrites are not allowed\n"
1186                 "Specify the `noOverwrite: false` option to mitigate this "
1187                 "error",
1188                 aMethodName, aSource->HumanReadablePath().get(),
1189                 aDest->HumanReadablePath().get()));
1190   }
1191   if (destExists && !destIsDir) {
1192     // If the source file is a directory, but the target is a file, abort early.
1193     // Different implementations of |CopyTo| and |MoveTo| seem to handle this
1194     // error case differently (or not at all), so we explicitly handle it here.
1195     bool srcIsDir = false;
1196     MOZ_TRY(aSource->IsDirectory(&srcIsDir));
1197     if (srcIsDir) {
1198       return Err(IOError(NS_ERROR_FILE_DESTINATION_NOT_DIR)
1199                      .WithMessage("Could not %s the source directory(%s) to "
1200                                   "the destination(%s) because the destination "
1201                                   "is not a directory",
1202                                   aMethodName,
1203                                   aSource->HumanReadablePath().get(),
1204                                   aDest->HumanReadablePath().get()));
1205     }
1206   }
1207 
1208   nsCOMPtr<nsIFile> destDir;
1209   nsAutoString destName;
1210   MOZ_TRY(aDest->GetLeafName(destName));
1211   MOZ_TRY(aDest->GetParent(getter_AddRefs(destDir)));
1212 
1213   // We know `destName` is a file and therefore must have a parent directory.
1214   MOZ_RELEASE_ASSERT(destDir);
1215 
1216   // NB: if destDir doesn't exist, then |CopyToFollowingLinks| or
1217   // |MoveToFollowingLinks| will create it.
1218   rv = (aSource->*aMethod)(destDir, destName);
1219   if (NS_FAILED(rv)) {
1220     return Err(IOError(rv).WithMessage(
1221         "Could not %s the source file(%s) to the destination(%s)", aMethodName,
1222         aSource->HumanReadablePath().get(), aDest->HumanReadablePath().get()));
1223   }
1224   return Ok();
1225 }
1226 
1227 /* static */
RemoveSync(nsIFile * aFile,bool aIgnoreAbsent,bool aRecursive)1228 Result<Ok, IOUtils::IOError> IOUtils::RemoveSync(nsIFile* aFile,
1229                                                  bool aIgnoreAbsent,
1230                                                  bool aRecursive) {
1231   MOZ_ASSERT(!NS_IsMainThread());
1232 
1233   nsresult rv = aFile->Remove(aRecursive);
1234   if (aIgnoreAbsent && IsFileNotFound(rv)) {
1235     return Ok();
1236   }
1237   if (NS_FAILED(rv)) {
1238     IOError err(rv);
1239     if (IsFileNotFound(rv)) {
1240       return Err(err.WithMessage(
1241           "Could not remove the file at %s because it does not exist.\n"
1242           "Specify the `ignoreAbsent: true` option to mitigate this error",
1243           aFile->HumanReadablePath().get()));
1244     }
1245     if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY) {
1246       return Err(err.WithMessage(
1247           "Could not remove the non-empty directory at %s.\n"
1248           "Specify the `recursive: true` option to mitigate this error",
1249           aFile->HumanReadablePath().get()));
1250     }
1251     return Err(err.WithMessage("Could not remove the file at %s",
1252                                aFile->HumanReadablePath().get()));
1253   }
1254   return Ok();
1255 }
1256 
1257 /* static */
MakeDirectorySync(nsIFile * aFile,bool aCreateAncestors,bool aIgnoreExisting,int32_t aMode)1258 Result<Ok, IOUtils::IOError> IOUtils::MakeDirectorySync(nsIFile* aFile,
1259                                                         bool aCreateAncestors,
1260                                                         bool aIgnoreExisting,
1261                                                         int32_t aMode) {
1262   MOZ_ASSERT(!NS_IsMainThread());
1263 
1264   nsCOMPtr<nsIFile> parent;
1265   MOZ_TRY(aFile->GetParent(getter_AddRefs(parent)));
1266   if (!parent) {
1267     // If we don't have a parent directory, we were called with a
1268     // root directory. If the directory doesn't already exist (e.g., asking
1269     // for a drive on Windows that does not exist), we will not be able to
1270     // create it.
1271     //
1272     // Calling `nsLocalFile::Create()` on Windows can fail with
1273     // `NS_ERROR_ACCESS_DENIED` trying to create a root directory, but we
1274     // would rather the call succeed, so return early if the directory exists.
1275     //
1276     // Otherwise, we fall through to `nsiFile::Create()` and let it fail there
1277     // instead.
1278     bool exists = false;
1279     MOZ_TRY(aFile->Exists(&exists));
1280     if (exists) {
1281       return Ok();
1282     }
1283   }
1284 
1285   nsresult rv =
1286       aFile->Create(nsIFile::DIRECTORY_TYPE, aMode, !aCreateAncestors);
1287   if (NS_FAILED(rv)) {
1288     if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
1289       // NB: We may report a success only if the target is an existing
1290       // directory. We don't want to silence errors that occur if the target is
1291       // an existing file, since trying to create a directory where a regular
1292       // file exists may be indicative of a logic error.
1293       bool isDirectory;
1294       MOZ_TRY(aFile->IsDirectory(&isDirectory));
1295       if (!isDirectory) {
1296         return Err(IOError(NS_ERROR_FILE_NOT_DIRECTORY)
1297                        .WithMessage("Could not create directory because the "
1298                                     "target file(%s) exists "
1299                                     "and is not a directory",
1300                                     aFile->HumanReadablePath().get()));
1301       }
1302       // The directory exists.
1303       // The caller may suppress this error.
1304       if (aIgnoreExisting) {
1305         return Ok();
1306       }
1307       // Otherwise, forward it.
1308       return Err(IOError(rv).WithMessage(
1309           "Could not create directory because it already exists at %s\n"
1310           "Specify the `ignoreExisting: true` option to mitigate this "
1311           "error",
1312           aFile->HumanReadablePath().get()));
1313     }
1314     return Err(IOError(rv).WithMessage("Could not create directory at %s",
1315                                        aFile->HumanReadablePath().get()));
1316   }
1317   return Ok();
1318 }
1319 
StatSync(nsIFile * aFile)1320 Result<IOUtils::InternalFileInfo, IOUtils::IOError> IOUtils::StatSync(
1321     nsIFile* aFile) {
1322   MOZ_ASSERT(!NS_IsMainThread());
1323 
1324   InternalFileInfo info;
1325   MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(info.mPath));
1326 
1327   bool isRegular = false;
1328   // IsFile will stat and cache info in the file object. If the file doesn't
1329   // exist, or there is an access error, we'll discover it here.
1330   // Any subsequent errors are unexpected and will just be forwarded.
1331   nsresult rv = aFile->IsFile(&isRegular);
1332   if (NS_FAILED(rv)) {
1333     IOError err(rv);
1334     if (IsFileNotFound(rv)) {
1335       return Err(
1336           err.WithMessage("Could not stat file(%s) because it does not exist",
1337                           aFile->HumanReadablePath().get()));
1338     }
1339     return Err(err);
1340   }
1341 
1342   // Now we can populate the info object by querying the file.
1343   info.mType = FileType::Regular;
1344   if (!isRegular) {
1345     bool isDir = false;
1346     MOZ_TRY(aFile->IsDirectory(&isDir));
1347     info.mType = isDir ? FileType::Directory : FileType::Other;
1348   }
1349 
1350   int64_t size = -1;
1351   if (info.mType == FileType::Regular) {
1352     MOZ_TRY(aFile->GetFileSize(&size));
1353   }
1354   info.mSize = size;
1355   PRTime lastModified = 0;
1356   MOZ_TRY(aFile->GetLastModifiedTime(&lastModified));
1357   info.mLastModified = static_cast<int64_t>(lastModified);
1358 
1359   PRTime creationTime = 0;
1360   if (nsresult rv = aFile->GetCreationTime(&creationTime); NS_SUCCEEDED(rv)) {
1361     info.mCreationTime.emplace(static_cast<int64_t>(creationTime));
1362   } else if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
1363     // This field is only supported on some platforms.
1364     return Err(IOError(rv));
1365   }
1366 
1367   MOZ_TRY(aFile->GetPermissions(&info.mPermissions));
1368 
1369   return info;
1370 }
1371 
1372 /* static */
TouchSync(nsIFile * aFile,const Maybe<int64_t> & aNewModTime)1373 Result<int64_t, IOUtils::IOError> IOUtils::TouchSync(
1374     nsIFile* aFile, const Maybe<int64_t>& aNewModTime) {
1375   MOZ_ASSERT(!NS_IsMainThread());
1376 
1377   int64_t now = aNewModTime.valueOrFrom([]() {
1378     // NB: PR_Now reports time in microseconds since the Unix epoch
1379     //     (1970-01-01T00:00:00Z). Both nsLocalFile's lastModifiedTime and
1380     //     JavaScript's Date primitive values are to be expressed in
1381     //     milliseconds since Epoch.
1382     int64_t nowMicros = PR_Now();
1383     int64_t nowMillis = nowMicros / PR_USEC_PER_MSEC;
1384     return nowMillis;
1385   });
1386 
1387   // nsIFile::SetLastModifiedTime will *not* do what is expected when passed 0
1388   // as an argument. Rather than setting the time to 0, it will recalculate the
1389   // system time and set it to that value instead. We explicit forbid this,
1390   // because this side effect is surprising.
1391   //
1392   // If it ever becomes possible to set a file time to 0, this check should be
1393   // removed, though this use case seems rare.
1394   if (now == 0) {
1395     return Err(
1396         IOError(NS_ERROR_ILLEGAL_VALUE)
1397             .WithMessage(
1398                 "Refusing to set the modification time of file(%s) to 0.\n"
1399                 "To use the current system time, call `touch` with no "
1400                 "arguments",
1401                 aFile->HumanReadablePath().get()));
1402   }
1403 
1404   nsresult rv = aFile->SetLastModifiedTime(now);
1405 
1406   if (NS_FAILED(rv)) {
1407     IOError err(rv);
1408     if (IsFileNotFound(rv)) {
1409       return Err(
1410           err.WithMessage("Could not touch file(%s) because it does not exist",
1411                           aFile->HumanReadablePath().get()));
1412     }
1413     return Err(err);
1414   }
1415   return now;
1416 }
1417 
1418 /* static */
GetChildrenSync(nsIFile * aFile)1419 Result<nsTArray<nsString>, IOUtils::IOError> IOUtils::GetChildrenSync(
1420     nsIFile* aFile) {
1421   MOZ_ASSERT(!NS_IsMainThread());
1422 
1423   nsCOMPtr<nsIDirectoryEnumerator> iter;
1424   nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(iter));
1425   if (NS_FAILED(rv)) {
1426     IOError err(rv);
1427     if (IsFileNotFound(rv)) {
1428       return Err(err.WithMessage(
1429           "Could not get children of file(%s) because it does not exist",
1430           aFile->HumanReadablePath().get()));
1431     }
1432     if (IsNotDirectory(rv)) {
1433       return Err(err.WithMessage(
1434           "Could not get children of file(%s) because it is not a directory",
1435           aFile->HumanReadablePath().get()));
1436     }
1437     return Err(err);
1438   }
1439   nsTArray<nsString> children;
1440 
1441   bool hasMoreElements = false;
1442   MOZ_TRY(iter->HasMoreElements(&hasMoreElements));
1443   while (hasMoreElements) {
1444     nsCOMPtr<nsIFile> child;
1445     MOZ_TRY(iter->GetNextFile(getter_AddRefs(child)));
1446     if (child) {
1447       nsString path;
1448       MOZ_TRY(child->GetPath(path));
1449       children.AppendElement(path);
1450     }
1451     MOZ_TRY(iter->HasMoreElements(&hasMoreElements));
1452   }
1453 
1454   return children;
1455 }
1456 
1457 /* static */
SetPermissionsSync(nsIFile * aFile,const uint32_t aPermissions)1458 Result<Ok, IOUtils::IOError> IOUtils::SetPermissionsSync(
1459     nsIFile* aFile, const uint32_t aPermissions) {
1460   MOZ_ASSERT(!NS_IsMainThread());
1461 
1462   MOZ_TRY(aFile->SetPermissions(aPermissions));
1463   return Ok{};
1464 }
1465 
1466 /* static */
ExistsSync(nsIFile * aFile)1467 Result<bool, IOUtils::IOError> IOUtils::ExistsSync(nsIFile* aFile) {
1468   MOZ_ASSERT(!NS_IsMainThread());
1469 
1470   bool exists = false;
1471   MOZ_TRY(aFile->Exists(&exists));
1472 
1473   return exists;
1474 }
1475 
1476 /* static */
GetProfileBeforeChange(GlobalObject & aGlobal,JS::MutableHandle<JS::Value> aClient,ErrorResult & aRv)1477 void IOUtils::GetProfileBeforeChange(GlobalObject& aGlobal,
1478                                      JS::MutableHandle<JS::Value> aClient,
1479                                      ErrorResult& aRv) {
1480   MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
1481   MOZ_RELEASE_ASSERT(NS_IsMainThread());
1482 
1483   if (auto state = GetState()) {
1484     MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus !=
1485                        ShutdownBlockerStatus::Uninitialized);
1486 
1487     if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) {
1488       aRv.ThrowAbortError("IOUtils: could not register shutdown blockers");
1489       return;
1490     }
1491 
1492     MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus ==
1493                        ShutdownBlockerStatus::Initialized);
1494     auto result = state.ref()->mEventQueue->GetProfileBeforeChangeClient();
1495     if (result.isErr()) {
1496       aRv.ThrowAbortError("IOUtils: could not get shutdown client");
1497       return;
1498     }
1499 
1500     RefPtr<nsIAsyncShutdownClient> client = result.unwrap();
1501     MOZ_RELEASE_ASSERT(client);
1502     if (nsresult rv = client->GetJsclient(aClient); NS_FAILED(rv)) {
1503       aRv.ThrowAbortError("IOUtils: Could not get shutdown jsclient");
1504     }
1505     return;
1506   }
1507 
1508   aRv.ThrowAbortError(
1509       "IOUtils: profileBeforeChange phase has already finished");
1510 }
1511 
1512 /* sstatic */
GetState()1513 Maybe<IOUtils::StateMutex::AutoLock> IOUtils::GetState() {
1514   auto state = sState.Lock();
1515   if (state->mQueueStatus == EventQueueStatus::Shutdown) {
1516     return Nothing{};
1517   }
1518 
1519   if (state->mQueueStatus == EventQueueStatus::Uninitialized) {
1520     MOZ_RELEASE_ASSERT(!state->mEventQueue);
1521     state->mEventQueue = new EventQueue();
1522     state->mQueueStatus = EventQueueStatus::Initialized;
1523 
1524     MOZ_RELEASE_ASSERT(state->mBlockerStatus ==
1525                        ShutdownBlockerStatus::Uninitialized);
1526   }
1527 
1528   if (NS_IsMainThread() &&
1529       state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) {
1530     state->SetShutdownHooks();
1531   }
1532 
1533   return Some(std::move(state));
1534 }
1535 
EventQueue()1536 IOUtils::EventQueue::EventQueue() {
1537   MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
1538       "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget)));
1539 
1540   MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
1541 }
1542 
SetShutdownHooks()1543 void IOUtils::State::SetShutdownHooks() {
1544   if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) {
1545     return;
1546   }
1547 
1548   if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) {
1549     mBlockerStatus = ShutdownBlockerStatus::Failed;
1550   } else {
1551     mBlockerStatus = ShutdownBlockerStatus::Initialized;
1552   }
1553 
1554   if (mBlockerStatus != ShutdownBlockerStatus::Initialized) {
1555     NS_WARNING("IOUtils: could not register shutdown blockers.");
1556   }
1557 }
1558 
SetShutdownHooks()1559 nsresult IOUtils::EventQueue::SetShutdownHooks() {
1560   MOZ_RELEASE_ASSERT(NS_IsMainThread());
1561 
1562   nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
1563   if (!svc) {
1564     return NS_ERROR_NOT_AVAILABLE;
1565   }
1566 
1567   nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new IOUtilsShutdownBlocker(
1568       IOUtilsShutdownBlocker::Phase::ProfileBeforeChange);
1569 
1570   nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
1571   MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(profileBeforeChange)));
1572   MOZ_RELEASE_ASSERT(profileBeforeChange);
1573 
1574   MOZ_TRY(profileBeforeChange->AddBlocker(
1575       blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
1576       u"IOUtils::EventQueue::SetShutdownHooks"_ns));
1577 
1578   nsCOMPtr<nsIAsyncShutdownClient> xpcomWillShutdown;
1579   MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(xpcomWillShutdown)));
1580   MOZ_RELEASE_ASSERT(xpcomWillShutdown);
1581 
1582   blocker = new IOUtilsShutdownBlocker(
1583       IOUtilsShutdownBlocker::Phase::XpcomWillShutdown);
1584   MOZ_TRY(xpcomWillShutdown->AddBlocker(
1585       blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
1586       u"IOUtils::EventQueue::SetShutdownHooks"_ns));
1587 
1588   MOZ_TRY(svc->MakeBarrier(
1589       u"IOUtils: waiting for profileBeforeChange IO to complete"_ns,
1590       getter_AddRefs(mProfileBeforeChangeBarrier)));
1591   MOZ_RELEASE_ASSERT(mProfileBeforeChangeBarrier);
1592 
1593   return NS_OK;
1594 }
1595 
1596 template <typename OkT, typename Fn>
Dispatch(Fn aFunc)1597 RefPtr<IOUtils::IOPromise<OkT>> IOUtils::EventQueue::Dispatch(Fn aFunc) {
1598   MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
1599 
1600   return InvokeAsync(
1601       mBackgroundEventTarget, __func__, [func = std::move(aFunc)]() {
1602         Result<OkT, IOError> result = func();
1603         if (result.isErr()) {
1604           return IOPromise<OkT>::CreateAndReject(result.unwrapErr(), __func__);
1605         }
1606         return IOPromise<OkT>::CreateAndResolve(result.unwrap(), __func__);
1607       });
1608 };
1609 
1610 Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult>
GetProfileBeforeChangeClient()1611 IOUtils::EventQueue::GetProfileBeforeChangeClient() {
1612   if (!mProfileBeforeChangeBarrier) {
1613     return Err(NS_ERROR_NOT_AVAILABLE);
1614   }
1615 
1616   nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
1617   MOZ_TRY(mProfileBeforeChangeBarrier->GetClient(
1618       getter_AddRefs(profileBeforeChange)));
1619   return profileBeforeChange.forget();
1620 }
1621 
1622 Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult>
GetProfileBeforeChangeBarrier()1623 IOUtils::EventQueue::GetProfileBeforeChangeBarrier() {
1624   if (!mProfileBeforeChangeBarrier) {
1625     return Err(NS_ERROR_NOT_AVAILABLE);
1626   }
1627 
1628   return do_AddRef(mProfileBeforeChangeBarrier);
1629 }
1630 
1631 /* static */
Compress(Span<const uint8_t> aUncompressed)1632 Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::MozLZ4::Compress(
1633     Span<const uint8_t> aUncompressed) {
1634   nsTArray<uint8_t> result;
1635   size_t worstCaseSize =
1636       Compression::LZ4::maxCompressedSize(aUncompressed.Length()) + HEADER_SIZE;
1637   if (!result.SetCapacity(worstCaseSize, fallible)) {
1638     return Err(IOError(NS_ERROR_OUT_OF_MEMORY)
1639                    .WithMessage("Could not allocate buffer to compress data"));
1640   }
1641   result.AppendElements(Span(MAGIC_NUMBER.data(), MAGIC_NUMBER.size()));
1642   std::array<uint8_t, sizeof(uint32_t)> contentSizeBytes{};
1643   LittleEndian::writeUint32(contentSizeBytes.data(), aUncompressed.Length());
1644   result.AppendElements(Span(contentSizeBytes.data(), contentSizeBytes.size()));
1645 
1646   if (aUncompressed.Length() == 0) {
1647     // Don't try to compress an empty buffer.
1648     // Just return the correctly formed header.
1649     result.SetLength(HEADER_SIZE);
1650     return result;
1651   }
1652 
1653   size_t compressed = Compression::LZ4::compress(
1654       reinterpret_cast<const char*>(aUncompressed.Elements()),
1655       aUncompressed.Length(),
1656       reinterpret_cast<char*>(result.Elements()) + HEADER_SIZE);
1657   if (!compressed) {
1658     return Err(
1659         IOError(NS_ERROR_UNEXPECTED).WithMessage("Could not compress data"));
1660   }
1661   result.SetLength(HEADER_SIZE + compressed);
1662   return result;
1663 }
1664 
1665 /* static */
Decompress(Span<const uint8_t> aFileContents,IOUtils::BufferKind aBufferKind)1666 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::MozLZ4::Decompress(
1667     Span<const uint8_t> aFileContents, IOUtils::BufferKind aBufferKind) {
1668   if (aFileContents.LengthBytes() < HEADER_SIZE) {
1669     return Err(
1670         IOError(NS_ERROR_FILE_CORRUPTED)
1671             .WithMessage(
1672                 "Could not decompress file because the buffer is too short"));
1673   }
1674   auto header = aFileContents.To(HEADER_SIZE);
1675   if (!std::equal(std::begin(MAGIC_NUMBER), std::end(MAGIC_NUMBER),
1676                   std::begin(header))) {
1677     nsCString magicStr;
1678     uint32_t i = 0;
1679     for (; i < header.Length() - 1; ++i) {
1680       magicStr.AppendPrintf("%02X ", header.at(i));
1681     }
1682     magicStr.AppendPrintf("%02X", header.at(i));
1683 
1684     return Err(IOError(NS_ERROR_FILE_CORRUPTED)
1685                    .WithMessage("Could not decompress file because it has an "
1686                                 "invalid LZ4 header (wrong magic number: '%s')",
1687                                 magicStr.get()));
1688   }
1689   size_t numBytes = sizeof(uint32_t);
1690   Span<const uint8_t> sizeBytes = header.Last(numBytes);
1691   uint32_t expectedDecompressedSize =
1692       LittleEndian::readUint32(sizeBytes.data());
1693   if (expectedDecompressedSize == 0) {
1694     return JsBuffer::CreateEmpty(aBufferKind);
1695   }
1696   auto contents = aFileContents.From(HEADER_SIZE);
1697   auto result = JsBuffer::Create(aBufferKind, expectedDecompressedSize);
1698   if (result.isErr()) {
1699     return result.propagateErr();
1700   }
1701 
1702   JsBuffer decompressed = result.unwrap();
1703   size_t actualSize = 0;
1704   if (!Compression::LZ4::decompress(
1705           reinterpret_cast<const char*>(contents.Elements()), contents.Length(),
1706           reinterpret_cast<char*>(decompressed.Elements()),
1707           expectedDecompressedSize, &actualSize)) {
1708     return Err(
1709         IOError(NS_ERROR_FILE_CORRUPTED)
1710             .WithMessage(
1711                 "Could not decompress file contents, the file may be corrupt"));
1712   }
1713   decompressed.SetLength(actualSize);
1714   return decompressed;
1715 }
1716 
1717 NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker,
1718                   nsIAsyncShutdownCompletionCallback);
1719 
GetName(nsAString & aName)1720 NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
1721   aName = u"IOUtils Blocker ("_ns;
1722 
1723   switch (mPhase) {
1724     case Phase::ProfileBeforeChange:
1725       aName.Append(u"profile-before-change"_ns);
1726       break;
1727 
1728     case Phase::XpcomWillShutdown:
1729       aName.Append(u"xpcom-will-shutdown"_ns);
1730       break;
1731 
1732     default:
1733       MOZ_CRASH("Unknown shutdown phase");
1734   }
1735 
1736   aName.Append(')');
1737   return NS_OK;
1738 }
1739 
BlockShutdown(nsIAsyncShutdownClient * aBarrierClient)1740 NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
1741     nsIAsyncShutdownClient* aBarrierClient) {
1742   using EventQueueStatus = IOUtils::EventQueueStatus;
1743 
1744   MOZ_RELEASE_ASSERT(NS_IsMainThread());
1745 
1746   nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
1747 
1748   {
1749     auto state = IOUtils::sState.Lock();
1750     if (state->mQueueStatus == EventQueueStatus::Shutdown) {
1751       // If the blocker for profile-before-change has already run, then the
1752       // event queue is already torn down and we have nothing to do.
1753 
1754       MOZ_RELEASE_ASSERT(mPhase == Phase::XpcomWillShutdown);
1755       MOZ_RELEASE_ASSERT(!state->mEventQueue);
1756 
1757       Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this)));
1758       mParentClient = nullptr;
1759 
1760       return NS_OK;
1761     }
1762 
1763     MOZ_RELEASE_ASSERT(state->mEventQueue);
1764 
1765     mParentClient = aBarrierClient;
1766 
1767     barrier =
1768         state->mEventQueue->GetProfileBeforeChangeBarrier().unwrapOr(nullptr);
1769   }
1770 
1771   // We cannot barrier->Wait() while holding the mutex because it will lead to
1772   // deadlock.
1773   if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) {
1774     // If we don't have a barrier, we still need to flush the IOUtils event
1775     // queue and disable task submission.
1776     //
1777     // Likewise, if waiting on the barrier failed, we are going to make our best
1778     // attempt to clean up.
1779     Unused << Done();
1780   }
1781 
1782   return NS_OK;
1783 }
1784 
Done()1785 NS_IMETHODIMP IOUtilsShutdownBlocker::Done() {
1786   using EventQueueStatus = IOUtils::EventQueueStatus;
1787 
1788   MOZ_RELEASE_ASSERT(NS_IsMainThread());
1789 
1790   auto state = IOUtils::sState.Lock();
1791   MOZ_RELEASE_ASSERT(state->mEventQueue);
1792 
1793   // This method is called once we have served all shutdown clients. Now we
1794   // flush the remaining IO queue and forbid additional IO requests.
1795   state->mEventQueue->Dispatch<Ok>([]() { return Ok{}; })
1796       ->Then(GetMainThreadSerialEventTarget(), __func__,
1797              [self = RefPtr(this)]() {
1798                if (self->mParentClient) {
1799                  Unused << NS_WARN_IF(
1800                      NS_FAILED(self->mParentClient->RemoveBlocker(self)));
1801                  self->mParentClient = nullptr;
1802 
1803                  auto state = IOUtils::sState.Lock();
1804                  MOZ_RELEASE_ASSERT(state->mEventQueue);
1805                  state->mEventQueue = nullptr;
1806                }
1807              });
1808 
1809   MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized);
1810   state->mQueueStatus = EventQueueStatus::Shutdown;
1811 
1812   return NS_OK;
1813 }
1814 
GetState(nsIPropertyBag ** aState)1815 NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) {
1816   return NS_OK;
1817 }
1818 
1819 Result<IOUtils::InternalWriteOpts, IOUtils::IOError>
FromBinding(const WriteOptions & aOptions)1820 IOUtils::InternalWriteOpts::FromBinding(const WriteOptions& aOptions) {
1821   InternalWriteOpts opts;
1822   opts.mFlush = aOptions.mFlush;
1823   opts.mMode = aOptions.mMode;
1824 
1825   if (aOptions.mBackupFile.WasPassed()) {
1826     opts.mBackupFile = new nsLocalFile();
1827     if (nsresult rv =
1828             opts.mBackupFile->InitWithPath(aOptions.mBackupFile.Value());
1829         NS_FAILED(rv)) {
1830       return Err(IOUtils::IOError(rv).WithMessage(
1831           "Could not parse path of backupFile (%s)",
1832           NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get()));
1833     }
1834   }
1835 
1836   if (aOptions.mTmpPath.WasPassed()) {
1837     opts.mTmpFile = new nsLocalFile();
1838     if (nsresult rv = opts.mTmpFile->InitWithPath(aOptions.mTmpPath.Value());
1839         NS_FAILED(rv)) {
1840       return Err(IOUtils::IOError(rv).WithMessage(
1841           "Could not parse path of temp file (%s)",
1842           NS_ConvertUTF16toUTF8(aOptions.mTmpPath.Value()).get()));
1843     }
1844   }
1845 
1846   opts.mCompress = aOptions.mCompress;
1847   return opts;
1848 }
1849 
1850 /* static */
Create(IOUtils::BufferKind aBufferKind,size_t aCapacity)1851 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create(
1852     IOUtils::BufferKind aBufferKind, size_t aCapacity) {
1853   JsBuffer buffer(aBufferKind, aCapacity);
1854   if (aCapacity != 0 && !buffer.mBuffer) {
1855     return Err(IOError(NS_ERROR_OUT_OF_MEMORY)
1856                    .WithMessage("Could not allocate buffer"));
1857   }
1858   return buffer;
1859 }
1860 
1861 /* static */
CreateEmpty(IOUtils::BufferKind aBufferKind)1862 IOUtils::JsBuffer IOUtils::JsBuffer::CreateEmpty(
1863     IOUtils::BufferKind aBufferKind) {
1864   JsBuffer buffer(aBufferKind, 0);
1865   MOZ_RELEASE_ASSERT(buffer.mBuffer == nullptr);
1866   return buffer;
1867 }
1868 
JsBuffer(IOUtils::BufferKind aBufferKind,size_t aCapacity)1869 IOUtils::JsBuffer::JsBuffer(IOUtils::BufferKind aBufferKind, size_t aCapacity)
1870     : mBufferKind(aBufferKind), mCapacity(aCapacity), mLength(0) {
1871   if (mCapacity) {
1872     if (aBufferKind == BufferKind::String) {
1873       mBuffer = JS::UniqueChars(
1874           js_pod_arena_malloc<char>(js::StringBufferArena, mCapacity));
1875     } else {
1876       MOZ_RELEASE_ASSERT(aBufferKind == BufferKind::Uint8Array);
1877       mBuffer = JS::UniqueChars(
1878           js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, mCapacity));
1879     }
1880   }
1881 }
1882 
JsBuffer(IOUtils::JsBuffer && aOther)1883 IOUtils::JsBuffer::JsBuffer(IOUtils::JsBuffer&& aOther) noexcept
1884     : mBufferKind(aOther.mBufferKind),
1885       mCapacity(aOther.mCapacity),
1886       mLength(aOther.mLength),
1887       mBuffer(std::move(aOther.mBuffer)) {
1888   aOther.mCapacity = 0;
1889   aOther.mLength = 0;
1890 }
1891 
operator =(IOUtils::JsBuffer && aOther)1892 IOUtils::JsBuffer& IOUtils::JsBuffer::operator=(
1893     IOUtils::JsBuffer&& aOther) noexcept {
1894   mBufferKind = aOther.mBufferKind;
1895   mCapacity = aOther.mCapacity;
1896   mLength = aOther.mLength;
1897   mBuffer = std::move(aOther.mBuffer);
1898 
1899   // Invalidate aOther.
1900   aOther.mCapacity = 0;
1901   aOther.mLength = 0;
1902 
1903   return *this;
1904 }
1905 
1906 /* static */
IntoString(JSContext * aCx,JsBuffer aBuffer)1907 JSString* IOUtils::JsBuffer::IntoString(JSContext* aCx, JsBuffer aBuffer) {
1908   MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::String);
1909 
1910   if (!aBuffer.mCapacity) {
1911     return JS_GetEmptyString(aCx);
1912   }
1913 
1914   if (IsAscii(aBuffer.BeginReading())) {
1915     // If the string is just plain ASCII, then we can hand the buffer off to
1916     // JavaScript as a Latin1 string (since ASCII is a subset of Latin1).
1917     JS::UniqueLatin1Chars asLatin1(
1918         reinterpret_cast<JS::Latin1Char*>(aBuffer.mBuffer.release()));
1919     return JS_NewLatin1String(aCx, std::move(asLatin1), aBuffer.mLength);
1920   }
1921 
1922   // If the string is encodable as Latin1, we need to deflate the string to a
1923   // Latin1 string to accoutn for UTF-8 characters that are encoded as more than
1924   // a single byte.
1925   //
1926   // Otherwise, the string contains characters outside Latin1 so we have to
1927   // inflate to UTF-16.
1928   return JS_NewStringCopyUTF8N(
1929       aCx, JS::UTF8Chars(aBuffer.mBuffer.get(), aBuffer.mLength));
1930 }
1931 
1932 /* static */
IntoUint8Array(JSContext * aCx,JsBuffer aBuffer)1933 JSObject* IOUtils::JsBuffer::IntoUint8Array(JSContext* aCx, JsBuffer aBuffer) {
1934   MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::Uint8Array);
1935 
1936   if (!aBuffer.mCapacity) {
1937     return JS_NewUint8Array(aCx, 0);
1938   }
1939 
1940   char* rawBuffer = aBuffer.mBuffer.release();
1941   MOZ_RELEASE_ASSERT(rawBuffer);
1942   JS::Rooted<JSObject*> arrayBuffer(
1943       aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength,
1944                                           reinterpret_cast<void*>(rawBuffer)));
1945 
1946   if (!arrayBuffer) {
1947     // The array buffer does not take ownership of the data pointer unless
1948     // creation succeeds. We are still on the hook to free it.
1949     //
1950     // aBuffer will be destructed at end of scope, but its destructor does not
1951     // take into account |mCapacity| or |mLength|, so it is OK for them to be
1952     // non-zero here with a null |mBuffer|.
1953     js_free(rawBuffer);
1954     return nullptr;
1955   }
1956 
1957   return JS_NewUint8ArrayWithBuffer(aCx, arrayBuffer, 0, aBuffer.mLength);
1958 }
1959 
ToJSValue(JSContext * aCx,IOUtils::JsBuffer && aBuffer,JS::MutableHandle<JS::Value> aValue)1960 [[nodiscard]] bool ToJSValue(JSContext* aCx, IOUtils::JsBuffer&& aBuffer,
1961                              JS::MutableHandle<JS::Value> aValue) {
1962   if (aBuffer.mBufferKind == IOUtils::BufferKind::String) {
1963     JSString* str = IOUtils::JsBuffer::IntoString(aCx, std::move(aBuffer));
1964     if (!str) {
1965       return false;
1966     }
1967 
1968     aValue.setString(str);
1969     return true;
1970   }
1971 
1972   JSObject* array = IOUtils::JsBuffer::IntoUint8Array(aCx, std::move(aBuffer));
1973   if (!array) {
1974     return false;
1975   }
1976 
1977   aValue.setObject(*array);
1978   return true;
1979 }
1980 
1981 }  // namespace mozilla::dom
1982 
1983 #undef REJECT_IF_INIT_PATH_FAILED
1984