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