Bug 1732219 - Add API for fetching the preview image. r=geckoview-reviewers,agi,mconley
[gecko.git] / dom / system / IOUtils.cpp
blob07a98705210b16509479e7999bc3eaefa436c6a8
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/. */
7 #include "IOUtils.h"
9 #include <cstdint>
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"
52 #ifndef ANDROID
53 # include "nsSystemInfo.h"
54 #endif
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)
66 static constexpr auto SHUTDOWN_ERROR =
67 "IOUtils: Shutting down and refusing additional I/O tasks"_ns;
69 namespace mozilla::dom {
71 // static helper functions
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.
79 * @see nsLocalFileWin.cpp
80 * @see nsLocalFileUnix.cpp
82 static bool IsFileNotFound(nsresult aResult) {
83 return aResult == NS_ERROR_FILE_NOT_FOUND ||
84 aResult == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
86 /**
87 * Like |IsFileNotFound|, but checks for known results that suggest a file
88 * is not a directory.
90 static bool IsNotDirectory(nsresult aResult) {
91 return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR ||
92 aResult == NS_ERROR_FILE_NOT_DIRECTORY;
95 /**
96 * Formats an error message and appends the error name to the end.
98 template <typename... Args>
99 static nsCString FormatErrorMessage(nsresult aError, const char* const aMessage,
100 Args... aArgs) {
101 nsPrintfCString msg(aMessage, aArgs...);
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));
112 return std::move(msg);
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);
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));
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);
137 if (aInternalFileInfo.mCreationTime.isSome()) {
138 info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref());
141 info.mPermissions.Construct(aInternalFileInfo.mPermissions);
143 return ToJSValue(aCx, info, aValue);
146 template <typename T>
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));
155 static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) {
156 const auto& errMsg = aError.Message();
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")));
245 static void RejectShuttingDown(Promise* aPromise) {
246 RejectJSPromise(aPromise,
247 IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR));
250 // IOUtils implementation
251 /* static */
252 IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"};
254 /* static */
255 template <typename OkT, typename Fn>
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));
264 [promise = RefPtr(aPromise)](const IOError& err) {
265 RejectJSPromise(promise, err);
270 /* static */
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;
280 if (auto state = GetState()) {
281 nsCOMPtr<nsIFile> file = new nsLocalFile();
282 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
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();
292 toRead.emplace(aOptions.mMaxBytes.Value());
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);
302 } else {
303 RejectShuttingDown(promise);
305 return promise.forget();
308 /* static */
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;
318 if (auto state = GetState()) {
319 nsCOMPtr<nsIFile> file = new nsLocalFile();
320 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
322 DispatchAndResolve<JsBuffer>(
323 state.ref()->mEventQueue, promise,
324 [file = std::move(file), decompress = aOptions.mDecompress]() {
325 return ReadUTF8Sync(file, decompress);
327 } else {
328 RejectShuttingDown(promise);
330 return promise.forget();
333 /* static */
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;
343 if (auto state = GetState()) {
344 nsCOMPtr<nsIFile> file = new nsLocalFile();
345 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
347 state.ref()
348 ->mEventQueue
349 ->Dispatch<JsBuffer>([file, decompress = aOptions.mDecompress]() {
350 return ReadUTF8Sync(file, decompress);
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;
361 JSContext* cx = jsapi.cx();
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;
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()));
386 return;
389 promise->MaybeResolve(val);
391 [promise](const IOError& aErr) { RejectJSPromise(promise, aErr); });
392 } else {
393 RejectShuttingDown(promise);
395 return promise.forget();
398 /* static */
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;
409 if (auto state = GetState()) {
410 nsCOMPtr<nsIFile> file = new nsLocalFile();
411 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
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();
421 auto opts = InternalWriteOpts::FromBinding(aOptions);
422 if (opts.isErr()) {
423 RejectJSPromise(promise, opts.unwrapErr());
424 return promise.forget();
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);
434 return promise.forget();
437 /* static */
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;
448 if (auto state = GetState()) {
449 nsCOMPtr<nsIFile> file = new nsLocalFile();
450 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
452 auto opts = InternalWriteOpts::FromBinding(aOptions);
453 if (opts.isErr()) {
454 RejectJSPromise(promise, opts.unwrapErr());
455 return promise.forget();
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);
464 } else {
465 RejectShuttingDown(promise);
467 return promise.forget();
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);
475 /* static */
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;
486 if (auto state = GetState()) {
487 nsCOMPtr<nsIFile> file = new nsLocalFile();
488 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
490 auto opts = InternalWriteOpts::FromBinding(aOptions);
491 if (opts.isErr()) {
492 RejectJSPromise(promise, opts.unwrapErr());
493 return promise.forget();
496 if (opts.inspect().mMode == WriteMode::Append) {
497 promise->MaybeRejectWithNotSupportedError(
498 "IOUtils.writeJSON does not support appending to files."_ns);
499 return promise.forget();
502 JSContext* cx = aGlobal.Context();
503 JS::Rooted<JS::Value> rootedValue(cx, aValue);
504 nsCString utf8Str;
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"));
517 return promise.forget();
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);
526 } else {
527 RejectShuttingDown(promise);
529 return promise.forget();
532 /* static */
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;
543 if (auto state = GetState()) {
544 nsCOMPtr<nsIFile> sourceFile = new nsLocalFile();
545 REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise);
547 nsCOMPtr<nsIFile> destFile = new nsLocalFile();
548 REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise);
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);
556 } else {
557 RejectShuttingDown(promise);
559 return promise.forget();
562 /* static */
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;
572 if (auto state = GetState()) {
573 nsCOMPtr<nsIFile> file = new nsLocalFile();
574 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
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);
582 } else {
583 RejectShuttingDown(promise);
585 return promise.forget();
588 /* static */
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;
598 if (auto state = GetState()) {
599 nsCOMPtr<nsIFile> file = new nsLocalFile();
600 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
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);
610 } else {
611 RejectShuttingDown(promise);
613 return promise.forget();
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;
624 if (auto state = GetState()) {
625 nsCOMPtr<nsIFile> file = new nsLocalFile();
626 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
628 DispatchAndResolve<InternalFileInfo>(
629 state.ref()->mEventQueue, promise,
630 [file = std::move(file)]() { return StatSync(file); });
631 } else {
632 RejectShuttingDown(promise);
634 return promise.forget();
637 /* static */
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;
648 if (auto state = GetState()) {
649 nsCOMPtr<nsIFile> sourceFile = new nsLocalFile();
650 REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise);
652 nsCOMPtr<nsIFile> destFile = new nsLocalFile();
653 REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise);
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);
662 } else {
663 RejectShuttingDown(promise);
665 return promise.forget();
668 /* static */
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;
678 if (auto state = GetState()) {
679 nsCOMPtr<nsIFile> file = new nsLocalFile();
680 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
682 Maybe<int64_t> newTime = Nothing();
683 if (aModification.WasPassed()) {
684 newTime = Some(aModification.Value());
686 DispatchAndResolve<int64_t>(state.ref()->mEventQueue, promise,
687 [file = std::move(file), newTime]() {
688 return TouchSync(file, newTime);
690 } else {
691 RejectShuttingDown(promise);
693 return promise.forget();
696 /* static */
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;
705 if (auto state = GetState()) {
706 nsCOMPtr<nsIFile> file = new nsLocalFile();
707 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
709 DispatchAndResolve<nsTArray<nsString>>(
710 state.ref()->mEventQueue, promise,
711 [file = std::move(file)]() { return GetChildrenSync(file); });
712 } else {
713 RejectShuttingDown(promise);
715 return promise.forget();
718 /* static */
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;
729 #if defined(XP_UNIX) && !defined(ANDROID)
730 if (aHonorUmask) {
731 aPermissions &= ~nsSystemInfo::gUserUmask;
733 #endif
735 if (auto state = GetState()) {
736 nsCOMPtr<nsIFile> file = new nsLocalFile();
737 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
739 DispatchAndResolve<Ok>(
740 state.ref()->mEventQueue, promise,
741 [file = std::move(file), permissions = aPermissions]() {
742 return SetPermissionsSync(file, permissions);
744 } else {
745 RejectShuttingDown(promise);
747 return promise.forget();
750 /* static */
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;
759 if (auto state = GetState()) {
760 nsCOMPtr<nsIFile> file = new nsLocalFile();
761 REJECT_IF_INIT_PATH_FAILED(file, aPath, promise);
763 DispatchAndResolve<bool>(
764 state.ref()->mEventQueue, promise,
765 [file = std::move(file)]() { return ExistsSync(file); });
766 } else {
767 RejectShuttingDown(promise);
769 return promise.forget();
772 /* static */
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;
780 MOZ_ASSERT(promise);
781 return do_AddRef(promise);
784 /* static */
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());
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"));
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()));
804 int64_t bufSize = 0;
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.
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()));
817 MOZ_RELEASE_ASSERT(streamSize >= 0);
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));
826 bufSize = static_cast<uint32_t>(streamSize);
828 if (aOffset >= bufSize) {
829 bufSize = 0;
830 } else {
831 bufSize = bufSize - aOffset;
833 } else {
834 bufSize = aMaxBytes.value();
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()));
845 JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind);
847 if (bufSize > 0) {
848 auto result = JsBuffer::Create(aBufferKind, bufSize);
849 if (result.isErr()) {
850 return result.propagateErr();
852 buffer = result.unwrap();
853 Span<char> toRead = buffer.BeginWriting();
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()));
866 if (bytesRead == 0) {
867 break;
869 totalRead += bytesRead;
870 toRead = toRead.From(bytesRead);
873 buffer.SetLength(totalRead);
876 // Decompress the file contents, if required.
877 if (aDecompress) {
878 return MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind);
881 return std::move(buffer);
884 /* static */
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();
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()));
901 return buffer;
904 /* static */
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());
910 nsIFile* backupFile = aOptions.mBackupFile;
911 nsIFile* tempFile = aOptions.mTmpFile;
913 bool exists = false;
914 MOZ_TRY(aFile->Exists(&exists));
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()));
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)));
935 bool noOverwrite = aOptions.mMode == WriteMode::Create;
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()));
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;
949 if (tempFile) {
950 writeFile = tempFile;
951 } else {
952 writeFile = aFile;
955 int32_t flags = PR_WRONLY;
957 switch (aOptions.mMode) {
958 case WriteMode::Overwrite:
959 flags |= PR_TRUNCATE | PR_CREATE_FILE;
960 break;
962 case WriteMode::Append:
963 flags |= PR_APPEND;
964 break;
966 case WriteMode::Create:
967 flags |= PR_CREATE_FILE | PR_EXCL;
968 break;
970 default:
971 MOZ_CRASH("IOUtils: unknown write mode");
974 if (aOptions.mFlush) {
975 flags |= PR_SYNC;
978 // Try to perform the write and ensure that the file is closed before
979 // continuing.
980 uint32_t totalWritten = 0;
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();
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());
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;
1005 return Err(
1006 IOError(rv).WithMessage("Could not open the file at %s for writing",
1007 writeFile->HumanReadablePath().get()));
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;
1015 while (pendingBytes.Length() > 0) {
1016 if (pendingBytes.Length() < chunkSize) {
1017 chunkSize = pendingBytes.Length();
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()));
1029 pendingBytes = pendingBytes.From(bytesWritten);
1030 totalWritten += bytesWritten;
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;
1040 MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath));
1041 MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath));
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()));
1054 // If we attempt to write to a directory *without* a temp file, we get a
1055 // permission error.
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()));
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()));
1079 return totalWritten;
1082 /* static */
1083 Result<Ok, IOUtils::IOError> IOUtils::MoveSync(nsIFile* aSourceFile,
1084 nsIFile* aDestFile,
1085 bool aNoOverwrite) {
1086 MOZ_ASSERT(!NS_IsMainThread());
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()));
1100 return CopyOrMoveSync(&nsIFile::MoveToFollowingLinks, "move", aSourceFile,
1101 aDestFile, aNoOverwrite);
1104 /* static */
1105 Result<Ok, IOUtils::IOError> IOUtils::CopySync(nsIFile* aSourceFile,
1106 nsIFile* aDestFile,
1107 bool aNoOverwrite,
1108 bool aRecursive) {
1109 MOZ_ASSERT(!NS_IsMainThread());
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()));
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()));
1138 return CopyOrMoveSync(&nsIFile::CopyToFollowingLinks, "copy", aSourceFile,
1139 aDestFile, aNoOverwrite);
1142 /* static */
1143 template <typename CopyOrMoveFn>
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());
1151 // Case 1: Destination is an existing directory. Copy/move source into dest.
1152 bool destIsDir = false;
1153 bool destExists = true;
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()));
1164 return Ok();
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));
1173 destExists = false;
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()));
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()));
1208 nsCOMPtr<nsIFile> destDir;
1209 nsAutoString destName;
1210 MOZ_TRY(aDest->GetLeafName(destName));
1211 MOZ_TRY(aDest->GetParent(getter_AddRefs(destDir)));
1213 // We know `destName` is a file and therefore must have a parent directory.
1214 MOZ_RELEASE_ASSERT(destDir);
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()));
1224 return Ok();
1227 /* static */
1228 Result<Ok, IOUtils::IOError> IOUtils::RemoveSync(nsIFile* aFile,
1229 bool aIgnoreAbsent,
1230 bool aRecursive) {
1231 MOZ_ASSERT(!NS_IsMainThread());
1233 nsresult rv = aFile->Remove(aRecursive);
1234 if (aIgnoreAbsent && IsFileNotFound(rv)) {
1235 return Ok();
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()));
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()));
1251 return Err(err.WithMessage("Could not remove the file at %s",
1252 aFile->HumanReadablePath().get()));
1254 return Ok();
1257 /* static */
1258 Result<Ok, IOUtils::IOError> IOUtils::MakeDirectorySync(nsIFile* aFile,
1259 bool aCreateAncestors,
1260 bool aIgnoreExisting,
1261 int32_t aMode) {
1262 MOZ_ASSERT(!NS_IsMainThread());
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.
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.
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();
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()));
1302 // The directory exists.
1303 // The caller may suppress this error.
1304 if (aIgnoreExisting) {
1305 return Ok();
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()));
1314 return Err(IOError(rv).WithMessage("Could not create directory at %s",
1315 aFile->HumanReadablePath().get()));
1317 return Ok();
1320 Result<IOUtils::InternalFileInfo, IOUtils::IOError> IOUtils::StatSync(
1321 nsIFile* aFile) {
1322 MOZ_ASSERT(!NS_IsMainThread());
1324 InternalFileInfo info;
1325 MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(info.mPath));
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()));
1339 return Err(err);
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;
1350 int64_t size = -1;
1351 if (info.mType == FileType::Regular) {
1352 MOZ_TRY(aFile->GetFileSize(&size));
1354 info.mSize = size;
1355 PRTime lastModified = 0;
1356 MOZ_TRY(aFile->GetLastModifiedTime(&lastModified));
1357 info.mLastModified = static_cast<int64_t>(lastModified);
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));
1367 MOZ_TRY(aFile->GetPermissions(&info.mPermissions));
1369 return info;
1372 /* static */
1373 Result<int64_t, IOUtils::IOError> IOUtils::TouchSync(
1374 nsIFile* aFile, const Maybe<int64_t>& aNewModTime) {
1375 MOZ_ASSERT(!NS_IsMainThread());
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;
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.
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()));
1404 nsresult rv = aFile->SetLastModifiedTime(now);
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()));
1413 return Err(err);
1415 return now;
1418 /* static */
1419 Result<nsTArray<nsString>, IOUtils::IOError> IOUtils::GetChildrenSync(
1420 nsIFile* aFile) {
1421 MOZ_ASSERT(!NS_IsMainThread());
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()));
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()));
1437 return Err(err);
1439 nsTArray<nsString> children;
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);
1451 MOZ_TRY(iter->HasMoreElements(&hasMoreElements));
1454 return children;
1457 /* static */
1458 Result<Ok, IOUtils::IOError> IOUtils::SetPermissionsSync(
1459 nsIFile* aFile, const uint32_t aPermissions) {
1460 MOZ_ASSERT(!NS_IsMainThread());
1462 MOZ_TRY(aFile->SetPermissions(aPermissions));
1463 return Ok{};
1466 /* static */
1467 Result<bool, IOUtils::IOError> IOUtils::ExistsSync(nsIFile* aFile) {
1468 MOZ_ASSERT(!NS_IsMainThread());
1470 bool exists = false;
1471 MOZ_TRY(aFile->Exists(&exists));
1473 return exists;
1476 /* static */
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());
1483 if (auto state = GetState()) {
1484 MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus !=
1485 ShutdownBlockerStatus::Uninitialized);
1487 if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) {
1488 aRv.ThrowAbortError("IOUtils: could not register shutdown blockers");
1489 return;
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;
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");
1505 return;
1508 aRv.ThrowAbortError(
1509 "IOUtils: profileBeforeChange phase has already finished");
1512 /* sstatic */
1513 Maybe<IOUtils::StateMutex::AutoLock> IOUtils::GetState() {
1514 auto state = sState.Lock();
1515 if (state->mQueueStatus == EventQueueStatus::Shutdown) {
1516 return Nothing{};
1519 if (state->mQueueStatus == EventQueueStatus::Uninitialized) {
1520 MOZ_RELEASE_ASSERT(!state->mEventQueue);
1521 state->mEventQueue = new EventQueue();
1522 state->mQueueStatus = EventQueueStatus::Initialized;
1524 MOZ_RELEASE_ASSERT(state->mBlockerStatus ==
1525 ShutdownBlockerStatus::Uninitialized);
1528 if (NS_IsMainThread() &&
1529 state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) {
1530 state->SetShutdownHooks();
1533 return Some(std::move(state));
1536 IOUtils::EventQueue::EventQueue() {
1537 MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
1538 "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget)));
1540 MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
1543 void IOUtils::State::SetShutdownHooks() {
1544 if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) {
1545 return;
1548 if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) {
1549 mBlockerStatus = ShutdownBlockerStatus::Failed;
1550 } else {
1551 mBlockerStatus = ShutdownBlockerStatus::Initialized;
1554 if (mBlockerStatus != ShutdownBlockerStatus::Initialized) {
1555 NS_WARNING("IOUtils: could not register shutdown blockers.");
1559 nsresult IOUtils::EventQueue::SetShutdownHooks() {
1560 MOZ_RELEASE_ASSERT(NS_IsMainThread());
1562 nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
1563 if (!svc) {
1564 return NS_ERROR_NOT_AVAILABLE;
1567 nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new IOUtilsShutdownBlocker(
1568 IOUtilsShutdownBlocker::Phase::ProfileBeforeChange);
1570 nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
1571 MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(profileBeforeChange)));
1572 MOZ_RELEASE_ASSERT(profileBeforeChange);
1574 MOZ_TRY(profileBeforeChange->AddBlocker(
1575 blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
1576 u"IOUtils::EventQueue::SetShutdownHooks"_ns));
1578 nsCOMPtr<nsIAsyncShutdownClient> xpcomWillShutdown;
1579 MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(xpcomWillShutdown)));
1580 MOZ_RELEASE_ASSERT(xpcomWillShutdown);
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));
1588 MOZ_TRY(svc->MakeBarrier(
1589 u"IOUtils: waiting for profileBeforeChange IO to complete"_ns,
1590 getter_AddRefs(mProfileBeforeChangeBarrier)));
1591 MOZ_RELEASE_ASSERT(mProfileBeforeChangeBarrier);
1593 return NS_OK;
1596 template <typename OkT, typename Fn>
1597 RefPtr<IOUtils::IOPromise<OkT>> IOUtils::EventQueue::Dispatch(Fn aFunc) {
1598 MOZ_RELEASE_ASSERT(mBackgroundEventTarget);
1600 auto promise =
1601 MakeRefPtr<typename IOUtils::IOPromise<OkT>::Private>(__func__);
1602 mBackgroundEventTarget->Dispatch(
1603 NS_NewRunnableFunction("IOUtils::EventQueue::Dispatch",
1604 [promise, func = std::move(aFunc)] {
1605 Result<OkT, IOError> result = func();
1606 if (result.isErr()) {
1607 promise->Reject(result.unwrapErr(), __func__);
1608 } else {
1609 promise->Resolve(result.unwrap(), __func__);
1612 NS_DISPATCH_EVENT_MAY_BLOCK);
1613 return promise;
1616 Result<already_AddRefed<nsIAsyncShutdownClient>, nsresult>
1617 IOUtils::EventQueue::GetProfileBeforeChangeClient() {
1618 if (!mProfileBeforeChangeBarrier) {
1619 return Err(NS_ERROR_NOT_AVAILABLE);
1622 nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
1623 MOZ_TRY(mProfileBeforeChangeBarrier->GetClient(
1624 getter_AddRefs(profileBeforeChange)));
1625 return profileBeforeChange.forget();
1628 Result<already_AddRefed<nsIAsyncShutdownBarrier>, nsresult>
1629 IOUtils::EventQueue::GetProfileBeforeChangeBarrier() {
1630 if (!mProfileBeforeChangeBarrier) {
1631 return Err(NS_ERROR_NOT_AVAILABLE);
1634 return do_AddRef(mProfileBeforeChangeBarrier);
1637 /* static */
1638 Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::MozLZ4::Compress(
1639 Span<const uint8_t> aUncompressed) {
1640 nsTArray<uint8_t> result;
1641 size_t worstCaseSize =
1642 Compression::LZ4::maxCompressedSize(aUncompressed.Length()) + HEADER_SIZE;
1643 if (!result.SetCapacity(worstCaseSize, fallible)) {
1644 return Err(IOError(NS_ERROR_OUT_OF_MEMORY)
1645 .WithMessage("Could not allocate buffer to compress data"));
1647 result.AppendElements(Span(MAGIC_NUMBER.data(), MAGIC_NUMBER.size()));
1648 std::array<uint8_t, sizeof(uint32_t)> contentSizeBytes{};
1649 LittleEndian::writeUint32(contentSizeBytes.data(), aUncompressed.Length());
1650 result.AppendElements(Span(contentSizeBytes.data(), contentSizeBytes.size()));
1652 if (aUncompressed.Length() == 0) {
1653 // Don't try to compress an empty buffer.
1654 // Just return the correctly formed header.
1655 result.SetLength(HEADER_SIZE);
1656 return result;
1659 size_t compressed = Compression::LZ4::compress(
1660 reinterpret_cast<const char*>(aUncompressed.Elements()),
1661 aUncompressed.Length(),
1662 reinterpret_cast<char*>(result.Elements()) + HEADER_SIZE);
1663 if (!compressed) {
1664 return Err(
1665 IOError(NS_ERROR_UNEXPECTED).WithMessage("Could not compress data"));
1667 result.SetLength(HEADER_SIZE + compressed);
1668 return result;
1671 /* static */
1672 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::MozLZ4::Decompress(
1673 Span<const uint8_t> aFileContents, IOUtils::BufferKind aBufferKind) {
1674 if (aFileContents.LengthBytes() < HEADER_SIZE) {
1675 return Err(
1676 IOError(NS_ERROR_FILE_CORRUPTED)
1677 .WithMessage(
1678 "Could not decompress file because the buffer is too short"));
1680 auto header = aFileContents.To(HEADER_SIZE);
1681 if (!std::equal(std::begin(MAGIC_NUMBER), std::end(MAGIC_NUMBER),
1682 std::begin(header))) {
1683 nsCString magicStr;
1684 uint32_t i = 0;
1685 for (; i < header.Length() - 1; ++i) {
1686 magicStr.AppendPrintf("%02X ", header.at(i));
1688 magicStr.AppendPrintf("%02X", header.at(i));
1690 return Err(IOError(NS_ERROR_FILE_CORRUPTED)
1691 .WithMessage("Could not decompress file because it has an "
1692 "invalid LZ4 header (wrong magic number: '%s')",
1693 magicStr.get()));
1695 size_t numBytes = sizeof(uint32_t);
1696 Span<const uint8_t> sizeBytes = header.Last(numBytes);
1697 uint32_t expectedDecompressedSize =
1698 LittleEndian::readUint32(sizeBytes.data());
1699 if (expectedDecompressedSize == 0) {
1700 return JsBuffer::CreateEmpty(aBufferKind);
1702 auto contents = aFileContents.From(HEADER_SIZE);
1703 auto result = JsBuffer::Create(aBufferKind, expectedDecompressedSize);
1704 if (result.isErr()) {
1705 return result.propagateErr();
1708 JsBuffer decompressed = result.unwrap();
1709 size_t actualSize = 0;
1710 if (!Compression::LZ4::decompress(
1711 reinterpret_cast<const char*>(contents.Elements()), contents.Length(),
1712 reinterpret_cast<char*>(decompressed.Elements()),
1713 expectedDecompressedSize, &actualSize)) {
1714 return Err(
1715 IOError(NS_ERROR_FILE_CORRUPTED)
1716 .WithMessage(
1717 "Could not decompress file contents, the file may be corrupt"));
1719 decompressed.SetLength(actualSize);
1720 return decompressed;
1723 NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker,
1724 nsIAsyncShutdownCompletionCallback);
1726 NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
1727 aName = u"IOUtils Blocker ("_ns;
1729 switch (mPhase) {
1730 case Phase::ProfileBeforeChange:
1731 aName.Append(u"profile-before-change"_ns);
1732 break;
1734 case Phase::XpcomWillShutdown:
1735 aName.Append(u"xpcom-will-shutdown"_ns);
1736 break;
1738 default:
1739 MOZ_CRASH("Unknown shutdown phase");
1742 aName.Append(')');
1743 return NS_OK;
1746 NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
1747 nsIAsyncShutdownClient* aBarrierClient) {
1748 using EventQueueStatus = IOUtils::EventQueueStatus;
1750 MOZ_RELEASE_ASSERT(NS_IsMainThread());
1752 nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
1755 auto state = IOUtils::sState.Lock();
1756 if (state->mQueueStatus == EventQueueStatus::Shutdown) {
1757 // If the blocker for profile-before-change has already run, then the
1758 // event queue is already torn down and we have nothing to do.
1760 MOZ_RELEASE_ASSERT(mPhase == Phase::XpcomWillShutdown);
1761 MOZ_RELEASE_ASSERT(!state->mEventQueue);
1763 Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this)));
1764 mParentClient = nullptr;
1766 return NS_OK;
1769 MOZ_RELEASE_ASSERT(state->mEventQueue);
1771 mParentClient = aBarrierClient;
1773 barrier =
1774 state->mEventQueue->GetProfileBeforeChangeBarrier().unwrapOr(nullptr);
1777 // We cannot barrier->Wait() while holding the mutex because it will lead to
1778 // deadlock.
1779 if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) {
1780 // If we don't have a barrier, we still need to flush the IOUtils event
1781 // queue and disable task submission.
1783 // Likewise, if waiting on the barrier failed, we are going to make our best
1784 // attempt to clean up.
1785 Unused << Done();
1788 return NS_OK;
1791 NS_IMETHODIMP IOUtilsShutdownBlocker::Done() {
1792 using EventQueueStatus = IOUtils::EventQueueStatus;
1794 MOZ_RELEASE_ASSERT(NS_IsMainThread());
1796 auto state = IOUtils::sState.Lock();
1797 MOZ_RELEASE_ASSERT(state->mEventQueue);
1799 // This method is called once we have served all shutdown clients. Now we
1800 // flush the remaining IO queue and forbid additional IO requests.
1801 state->mEventQueue->Dispatch<Ok>([]() { return Ok{}; })
1802 ->Then(GetMainThreadSerialEventTarget(), __func__,
1803 [self = RefPtr(this)]() {
1804 if (self->mParentClient) {
1805 Unused << NS_WARN_IF(
1806 NS_FAILED(self->mParentClient->RemoveBlocker(self)));
1807 self->mParentClient = nullptr;
1809 auto state = IOUtils::sState.Lock();
1810 MOZ_RELEASE_ASSERT(state->mEventQueue);
1811 state->mEventQueue = nullptr;
1815 MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized);
1816 state->mQueueStatus = EventQueueStatus::Shutdown;
1818 return NS_OK;
1821 NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) {
1822 return NS_OK;
1825 Result<IOUtils::InternalWriteOpts, IOUtils::IOError>
1826 IOUtils::InternalWriteOpts::FromBinding(const WriteOptions& aOptions) {
1827 InternalWriteOpts opts;
1828 opts.mFlush = aOptions.mFlush;
1829 opts.mMode = aOptions.mMode;
1831 if (aOptions.mBackupFile.WasPassed()) {
1832 opts.mBackupFile = new nsLocalFile();
1833 if (nsresult rv =
1834 opts.mBackupFile->InitWithPath(aOptions.mBackupFile.Value());
1835 NS_FAILED(rv)) {
1836 return Err(IOUtils::IOError(rv).WithMessage(
1837 "Could not parse path of backupFile (%s)",
1838 NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get()));
1842 if (aOptions.mTmpPath.WasPassed()) {
1843 opts.mTmpFile = new nsLocalFile();
1844 if (nsresult rv = opts.mTmpFile->InitWithPath(aOptions.mTmpPath.Value());
1845 NS_FAILED(rv)) {
1846 return Err(IOUtils::IOError(rv).WithMessage(
1847 "Could not parse path of temp file (%s)",
1848 NS_ConvertUTF16toUTF8(aOptions.mTmpPath.Value()).get()));
1852 opts.mCompress = aOptions.mCompress;
1853 return opts;
1856 /* static */
1857 Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::JsBuffer::Create(
1858 IOUtils::BufferKind aBufferKind, size_t aCapacity) {
1859 JsBuffer buffer(aBufferKind, aCapacity);
1860 if (aCapacity != 0 && !buffer.mBuffer) {
1861 return Err(IOError(NS_ERROR_OUT_OF_MEMORY)
1862 .WithMessage("Could not allocate buffer"));
1864 return buffer;
1867 /* static */
1868 IOUtils::JsBuffer IOUtils::JsBuffer::CreateEmpty(
1869 IOUtils::BufferKind aBufferKind) {
1870 JsBuffer buffer(aBufferKind, 0);
1871 MOZ_RELEASE_ASSERT(buffer.mBuffer == nullptr);
1872 return buffer;
1875 IOUtils::JsBuffer::JsBuffer(IOUtils::BufferKind aBufferKind, size_t aCapacity)
1876 : mBufferKind(aBufferKind), mCapacity(aCapacity), mLength(0) {
1877 if (mCapacity) {
1878 if (aBufferKind == BufferKind::String) {
1879 mBuffer = JS::UniqueChars(
1880 js_pod_arena_malloc<char>(js::StringBufferArena, mCapacity));
1881 } else {
1882 MOZ_RELEASE_ASSERT(aBufferKind == BufferKind::Uint8Array);
1883 mBuffer = JS::UniqueChars(
1884 js_pod_arena_malloc<char>(js::ArrayBufferContentsArena, mCapacity));
1889 IOUtils::JsBuffer::JsBuffer(IOUtils::JsBuffer&& aOther) noexcept
1890 : mBufferKind(aOther.mBufferKind),
1891 mCapacity(aOther.mCapacity),
1892 mLength(aOther.mLength),
1893 mBuffer(std::move(aOther.mBuffer)) {
1894 aOther.mCapacity = 0;
1895 aOther.mLength = 0;
1898 IOUtils::JsBuffer& IOUtils::JsBuffer::operator=(
1899 IOUtils::JsBuffer&& aOther) noexcept {
1900 mBufferKind = aOther.mBufferKind;
1901 mCapacity = aOther.mCapacity;
1902 mLength = aOther.mLength;
1903 mBuffer = std::move(aOther.mBuffer);
1905 // Invalidate aOther.
1906 aOther.mCapacity = 0;
1907 aOther.mLength = 0;
1909 return *this;
1912 /* static */
1913 JSString* IOUtils::JsBuffer::IntoString(JSContext* aCx, JsBuffer aBuffer) {
1914 MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::String);
1916 if (!aBuffer.mCapacity) {
1917 return JS_GetEmptyString(aCx);
1920 if (IsAscii(aBuffer.BeginReading())) {
1921 // If the string is just plain ASCII, then we can hand the buffer off to
1922 // JavaScript as a Latin1 string (since ASCII is a subset of Latin1).
1923 JS::UniqueLatin1Chars asLatin1(
1924 reinterpret_cast<JS::Latin1Char*>(aBuffer.mBuffer.release()));
1925 return JS_NewLatin1String(aCx, std::move(asLatin1), aBuffer.mLength);
1928 // If the string is encodable as Latin1, we need to deflate the string to a
1929 // Latin1 string to accoutn for UTF-8 characters that are encoded as more than
1930 // a single byte.
1932 // Otherwise, the string contains characters outside Latin1 so we have to
1933 // inflate to UTF-16.
1934 return JS_NewStringCopyUTF8N(
1935 aCx, JS::UTF8Chars(aBuffer.mBuffer.get(), aBuffer.mLength));
1938 /* static */
1939 JSObject* IOUtils::JsBuffer::IntoUint8Array(JSContext* aCx, JsBuffer aBuffer) {
1940 MOZ_RELEASE_ASSERT(aBuffer.mBufferKind == IOUtils::BufferKind::Uint8Array);
1942 if (!aBuffer.mCapacity) {
1943 return JS_NewUint8Array(aCx, 0);
1946 char* rawBuffer = aBuffer.mBuffer.release();
1947 MOZ_RELEASE_ASSERT(rawBuffer);
1948 JS::Rooted<JSObject*> arrayBuffer(
1949 aCx, JS::NewArrayBufferWithContents(aCx, aBuffer.mLength,
1950 reinterpret_cast<void*>(rawBuffer)));
1952 if (!arrayBuffer) {
1953 // The array buffer does not take ownership of the data pointer unless
1954 // creation succeeds. We are still on the hook to free it.
1956 // aBuffer will be destructed at end of scope, but its destructor does not
1957 // take into account |mCapacity| or |mLength|, so it is OK for them to be
1958 // non-zero here with a null |mBuffer|.
1959 js_free(rawBuffer);
1960 return nullptr;
1963 return JS_NewUint8ArrayWithBuffer(aCx, arrayBuffer, 0, aBuffer.mLength);
1966 [[nodiscard]] bool ToJSValue(JSContext* aCx, IOUtils::JsBuffer&& aBuffer,
1967 JS::MutableHandle<JS::Value> aValue) {
1968 if (aBuffer.mBufferKind == IOUtils::BufferKind::String) {
1969 JSString* str = IOUtils::JsBuffer::IntoString(aCx, std::move(aBuffer));
1970 if (!str) {
1971 return false;
1974 aValue.setString(str);
1975 return true;
1978 JSObject* array = IOUtils::JsBuffer::IntoUint8Array(aCx, std::move(aBuffer));
1979 if (!array) {
1980 return false;
1983 aValue.setObject(*array);
1984 return true;
1987 } // namespace mozilla::dom
1989 #undef REJECT_IF_INIT_PATH_FAILED