/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "IOUtils.h"
#include <cstdint>
#include "ErrorList.h"
#include "TypedArray.h"
#include "js/ArrayBuffer.h"
#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin
#include "js/JSON.h"
#include "js/Utility.h"
#include "js/experimental/TypedData.h"
#include "jsfriendapi.h"
#include "mozilla/Assertions.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/Compression.h"
#include "mozilla/Encoding.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/FileUtils.h"
#include "mozilla/Maybe.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/Services.h"
#include "mozilla/Span.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TextUtils.h"
#include "mozilla/Try.h"
#include "mozilla/Unused.h"
#include "mozilla/Utf8.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/IOUtilsBinding.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/ipc/LaunchError.h"
#include "PathUtils.h"
#include "nsCOMPtr.h"
#include "nsError.h"
#include "nsFileStreams.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsIInputStream.h"
#include "nsISupports.h"
#include "nsLocalFile.h"
#include "nsNetUtil.h"
#include "nsNSSComponent.h"
#include "nsPrintfCString.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsThreadManager.h"
#include "nsXULAppAPI.h"
#include "prerror.h"
#include "prio.h"
#include "prtime.h"
#include "prtypes.h"
#include "ScopedNSSTypes.h"
#include "secoidt.h"
#if defined(XP_UNIX) && !
defined(ANDROID)
# include
"nsSystemInfo.h"
#endif
#if defined(XP_WIN)
# include
"nsILocalFileWin.h"
#elif defined(XP_MACOSX)
# include
"nsILocalFileMac.h"
#endif
#ifdef XP_UNIX
# include
"base/process_util.h"
#endif
#define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise, _msg, ...) \
do { \
if (nsresult _rv = PathUtils::InitFileWithPath((_file), (_path)); \
NS_FAILED(_rv)) { \
(_promise)->MaybeRejectWithOperationError(FormatErrorMessage( \
_rv, _msg
": could not parse path",
##__VA_ARGS__)); \
return; \
} \
}
while (0)
#define IOUTILS_TRY_WITH_CONTEXT(_expr, _fmt, ...) \
do { \
if (nsresult _rv = (_expr); NS_FAILED(_rv)) { \
return Err(IOUtils::IOError(_rv, _fmt,
##__VA_ARGS__)); \
} \
}
while (0)
static constexpr
auto SHUTDOWN_ERROR =
"IOUtils: Shutting down and refusing additional I/O tasks"_ns;
namespace mozilla::dom {
// static helper functions
/**
* Platform-specific (e.g. Windows, Unix) implementations of XPCOM APIs may
* report I/O errors inconsistently. For convenience, this function will attempt
* to match a |nsresult| against known results which imply a file cannot be
* found.
*
* @see nsLocalFileWin.cpp
* @see nsLocalFileUnix.cpp
*/
static bool IsFileNotFound(nsresult aResult) {
return aResult == NS_ERROR_FILE_NOT_FOUND;
}
/**
* Like |IsFileNotFound|, but checks for known results that suggest a file
* is not a directory.
*/
static bool IsNotDirectory(nsresult aResult) {
return aResult == NS_ERROR_FILE_DESTINATION_NOT_DIR ||
aResult == NS_ERROR_FILE_NOT_DIRECTORY;
}
/**
* Formats an error message and appends the error name to the end.
*/
static nsCString MOZ_FORMAT_PRINTF(2, 3)
FormatErrorMessage(nsresult aError,
const char*
const aFmt, ...) {
nsAutoCString errorName;
GetErrorName(aError, errorName);
nsCString msg;
va_list ap;
va_start(ap, aFmt);
msg.AppendVprintf(aFmt, ap);
va_end(ap);
msg.AppendPrintf(
" (%s)", errorName.get());
return msg;
}
static nsCString FormatErrorMessage(nsresult aError,
const nsCString& aMessage) {
nsAutoCString errorName;
GetErrorName(aError, errorName);
nsCString msg(aMessage);
msg.AppendPrintf(
" (%s)", errorName.get());
return msg;
}
[[nodiscard]]
inline bool ToJSValue(
JSContext* aCx,
const IOUtils::InternalFileInfo& aInternalFileInfo,
JS::MutableHandle<JS::Value> aValue) {
FileInfo info;
info.mPath.Construct(aInternalFileInfo.mPath);
info.mType.Construct(aInternalFileInfo.mType);
info.mSize.Construct(aInternalFileInfo.mSize);
if (aInternalFileInfo.mCreationTime.isSome()) {
info.mCreationTime.Construct(aInternalFileInfo.mCreationTime.ref());
}
info.mLastAccessed.Construct(aInternalFileInfo.mLastAccessed);
info.mLastModified.Construct(aInternalFileInfo.mLastModified);
info.mPermissions.Construct(aInternalFileInfo.mPermissions);
return ToJSValue(aCx, info, aValue);
}
template <
typename T>
static void ResolveJSPromise(Promise* aPromise, T&& aValue) {
if constexpr (std::is_same_v<T, Ok>) {
aPromise->MaybeResolveWithUndefined();
}
else if constexpr (std::is_same_v<T, nsTArray<uint8_t>>) {
TypedArrayCreator<Uint8Array> array(aValue);
aPromise->MaybeResolve(array);
}
else {
aPromise->MaybeResolve(std::forward<T>(aValue));
}
}
static void RejectJSPromise(Promise* aPromise,
const IOUtils::IOError& aError) {
const auto errMsg = FormatErrorMessage(aError.Code(), aError.Message());
switch (aError.Code()) {
case NS_ERROR_FILE_UNRESOLVABLE_SYMLINK:
[[fallthrough]];
case NS_ERROR_FILE_NOT_FOUND:
[[fallthrough]];
case NS_ERROR_FILE_INVALID_PATH:
[[fallthrough]];
case NS_ERROR_NOT_AVAILABLE:
aPromise->MaybeRejectWithNotFoundError(errMsg);
break;
case NS_ERROR_FILE_IS_LOCKED:
[[fallthrough]];
case NS_ERROR_FILE_ACCESS_DENIED:
aPromise->MaybeRejectWithNotAllowedError(errMsg);
break;
case NS_ERROR_FILE_TOO_BIG:
[[fallthrough]];
case NS_ERROR_FILE_NO_DEVICE_SPACE:
[[fallthrough]];
case NS_ERROR_FILE_DEVICE_FAILURE:
[[fallthrough]];
case NS_ERROR_FILE_FS_CORRUPTED:
[[fallthrough]];
case NS_ERROR_FILE_CORRUPTED:
aPromise->MaybeRejectWithNotReadableError(errMsg);
break;
case NS_ERROR_FILE_ALREADY_EXISTS:
aPromise->MaybeRejectWithNoModificationAllowedError(errMsg);
break;
case NS_ERROR_FILE_COPY_OR_MOVE_FAILED:
[[fallthrough]];
case NS_ERROR_FILE_NAME_TOO_LONG:
[[fallthrough]];
case NS_ERROR_FILE_UNRECOGNIZED_PATH:
[[fallthrough]];
case NS_ERROR_FILE_DIR_NOT_EMPTY:
aPromise->MaybeRejectWithOperationError(errMsg);
break;
case NS_ERROR_FILE_READ_ONLY:
aPromise->MaybeRejectWithReadOnlyError(errMsg);
break;
case NS_ERROR_FILE_NOT_DIRECTORY:
[[fallthrough]];
case NS_ERROR_FILE_DESTINATION_NOT_DIR:
[[fallthrough]];
case NS_ERROR_FILE_IS_DIRECTORY:
[[fallthrough]];
case NS_ERROR_FILE_UNKNOWN_TYPE:
aPromise->MaybeRejectWithInvalidAccessError(errMsg);
break;
case NS_ERROR_ILLEGAL_INPUT:
[[fallthrough]];
case NS_ERROR_ILLEGAL_VALUE:
aPromise->MaybeRejectWithDataError(errMsg);
break;
case NS_ERROR_ABORT:
aPromise->MaybeRejectWithAbortError(errMsg);
break;
default:
aPromise->MaybeRejectWithUnknownError(errMsg);
}
}
static void RejectShuttingDown(Promise* aPromise) {
RejectJSPromise(aPromise, IOUtils::IOError(NS_ERROR_ABORT, SHUTDOWN_ERROR));
}
static bool AssertParentProcessWithCallerLocationImpl(GlobalObject& aGlobal,
nsCString& reason) {
if (MOZ_LIKELY(XRE_IsParentProcess())) {
return true;
}
AutoJSAPI jsapi;
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
MOZ_ALWAYS_TRUE(global);
MOZ_ALWAYS_TRUE(jsapi.Init(global));
JSContext* cx = jsapi.cx();
JS::AutoFilename scriptFilename;
uint32_t lineNo = 0;
JS::ColumnNumberOneOrigin colNo;
NS_ENSURE_TRUE(
JS::DescribeScriptedCaller(&scriptFilename, cx, &lineNo, &colNo),
false);
NS_ENSURE_TRUE(scriptFilename.get(),
false);
reason.AppendPrintf(
" Called from %s:%d:%d.", scriptFilename.get(), lineNo,
colNo.oneOriginValue());
return false;
}
static void AssertParentProcessWithCallerLocation(GlobalObject& aGlobal) {
nsCString reason =
"IOUtils can only be used in the parent process."_ns;
if (!AssertParentProcessWithCallerLocationImpl(aGlobal, reason)) {
MOZ_CRASH_UNSAFE_PRINTF(
"%s", reason.get());
}
}
// IOUtils implementation
/* static */
MOZ_RUNINIT IOUtils::StateMutex IOUtils::sState{
"IOUtils::sState"};
/* static */
template <
typename Fn>
already_AddRefed<Promise> IOUtils::WithPromiseAndState(GlobalObject& aGlobal,
ErrorResult& aError,
Fn aFn) {
AssertParentProcessWithCallerLocation(aGlobal);
RefPtr<Promise> promise = CreateJSPromise(aGlobal, aError);
if (!promise) {
return nullptr;
}
if (
auto state = GetState()) {
aFn(promise, state.ref());
}
else {
RejectShuttingDown(promise);
}
return promise.forget();
}
/* static */
template <
typename OkT,
typename Fn>
void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise,
Fn aFunc) {
RefPtr<StrongWorkerRef> workerRef;
if (!NS_IsMainThread()) {
// We need to manually keep the worker alive until the promise returned by
// Dispatch() resolves or rejects.
workerRef = StrongWorkerRef::CreateForcibly(GetCurrentThreadWorkerPrivate(),
__func__);
}
if (RefPtr<IOPromise<OkT>> p = aQueue->Dispatch<OkT, Fn>(std::move(aFunc))) {
p->Then(
GetCurrentSerialEventTarget(), __func__,
[workerRef, promise = RefPtr(aPromise)](OkT&& ok) {
ResolveJSPromise(promise, std::forward<OkT>(ok));
},
[workerRef, promise = RefPtr(aPromise)](
const IOError& err) {
RejectJSPromise(promise, err);
});
}
}
/* static */
already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
const nsAString& aPath,
const ReadOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not read `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
Maybe<uint32_t> toRead = Nothing();
if (!aOptions.mMaxBytes.IsNull()) {
if (aOptions.mDecompress) {
RejectJSPromise(
promise, IOError(NS_ERROR_ILLEGAL_INPUT,
"Could not read `%s': the `maxBytes' and "
"`decompress' options are mutually exclusive",
file->HumanReadablePath().get()));
return;
}
if (aOptions.mMaxBytes.Value() == 0) {
// Resolve with an empty buffer.
nsTArray<uint8_t> arr(0);
promise->MaybeResolve(TypedArrayCreator<Uint8Array>(arr));
return;
}
toRead.emplace(aOptions.mMaxBytes.Value());
}
DispatchAndResolve<JsBuffer>(
state->mEventQueue, promise,
[file = std::move(file), offset = aOptions.mOffset, toRead,
decompress = aOptions.mDecompress]() {
return ReadSync(file, offset, toRead, decompress,
BufferKind::Uint8Array);
});
});
}
/* static */
RefPtr<SyncReadFile> IOUtils::OpenFileForSyncReading(GlobalObject& aGlobal,
const nsAString& aPath,
ErrorResult& aRv) {
MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
// This API is only exposed to workers, so we should not be on the main
// thread here.
MOZ_RELEASE_ASSERT(!NS_IsMainThread());
nsCOMPtr<nsIFile> file =
new nsLocalFile();
if (nsresult rv = PathUtils::InitFileWithPath(file, aPath); NS_FAILED(rv)) {
aRv.ThrowOperationError(FormatErrorMessage(
rv,
"Could not parse path (%s)", NS_ConvertUTF16toUTF8(aPath).get()));
return nullptr;
}
RefPtr<nsFileRandomAccessStream> stream =
new nsFileRandomAccessStream();
if (nsresult rv =
stream->Init(file, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0);
NS_FAILED(rv)) {
aRv.ThrowOperationError(
FormatErrorMessage(rv,
"Could not open the file at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
return nullptr;
}
int64_t size = 0;
if (nsresult rv = stream->GetSize(&size); NS_FAILED(rv)) {
aRv.ThrowOperationError(FormatErrorMessage(
rv,
"Could not get the stream size for the file at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
return nullptr;
}
return new SyncReadFile(aGlobal.GetAsSupports(), std::move(stream), size);
}
/* static */
already_AddRefed<Promise> IOUtils::ReadUTF8(GlobalObject& aGlobal,
const nsAString& aPath,
const ReadUTF8Options& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not read `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<JsBuffer>(
state->mEventQueue, promise,
[file = std::move(file), decompress = aOptions.mDecompress]() {
return ReadUTF8Sync(file, decompress);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::ReadJSON(GlobalObject& aGlobal,
const nsAString& aPath,
const ReadUTF8Options& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not read `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
RefPtr<StrongWorkerRef> workerRef;
if (!NS_IsMainThread()) {
// We need to manually keep the worker alive until the promise
// returned by Dispatch() resolves or rejects.
workerRef = StrongWorkerRef::CreateForcibly(
GetCurrentThreadWorkerPrivate(), __func__);
}
state->mEventQueue
->
template Dispatch<JsBuffer>(
[file, decompress = aOptions.mDecompress]() {
return ReadUTF8Sync(file, decompress);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[workerRef, promise = RefPtr{promise},
file](JsBuffer&& aBuffer) {
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
RejectJSPromise(
promise,
IOError(
NS_ERROR_DOM_UNKNOWN_ERR,
"Could not read `%s': could not initialize JS API",
file->HumanReadablePath().get()));
return;
}
JSContext* cx = jsapi.cx();
JS::Rooted<JSString*> jsonStr(
cx,
IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer)));
if (!jsonStr) {
RejectJSPromise(
promise,
IOError(
NS_ERROR_OUT_OF_MEMORY,
"Could not read `%s': failed to allocate buffer",
file->HumanReadablePath().get()));
return;
}
JS::Rooted<JS::Value> val(cx);
if (!JS_ParseJSON(cx, jsonStr, &val)) {
JS::Rooted<JS::Value> exn(cx);
if (JS_GetPendingException(cx, &exn)) {
JS_ClearPendingException(cx);
promise->MaybeReject(exn);
}
else {
RejectJSPromise(promise,
IOError(NS_ERROR_DOM_UNKNOWN_ERR,
"Could not read `%s': ParseJSON "
"threw an uncatchable exception",
file->HumanReadablePath().get()));
}
return;
}
promise->MaybeResolve(val);
},
[workerRef, promise = RefPtr{promise}](
const IOError& aErr) {
RejectJSPromise(promise, aErr);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::Write(GlobalObject& aGlobal,
const nsAString& aPath,
const Uint8Array& aData,
const WriteOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not write to `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
Maybe<Buffer<uint8_t>> buf = aData.CreateFromData<Buffer<uint8_t>>();
if (buf.isNothing()) {
promise->MaybeRejectWithOperationError(nsPrintfCString(
"Could not write to `%s': could not allocate buffer",
file->HumanReadablePath().get()));
return;
}
auto result = InternalWriteOpts::FromBinding(aOptions);
if (result.isErr()) {
RejectJSPromise(
promise,
IOError::WithCause(result.unwrapErr(),
"Could not write to `%s'",
file->HumanReadablePath().get()));
return;
}
DispatchAndResolve<uint32_t>(
state->mEventQueue, promise,
[file = std::move(file), buf = buf.extract(),
opts = result.unwrap()]() {
return WriteSync(file, buf, opts); });
});
}
/* static */
already_AddRefed<Promise> IOUtils::WriteUTF8(GlobalObject& aGlobal,
const nsAString& aPath,
const nsACString& aString,
const WriteOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not write to `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
auto result = InternalWriteOpts::FromBinding(aOptions);
if (result.isErr()) {
RejectJSPromise(
promise,
IOError::WithCause(result.unwrapErr(),
"Could not write to `%s'",
file->HumanReadablePath().get()));
return;
}
DispatchAndResolve<uint32_t>(
state->mEventQueue, promise,
[file = std::move(file), str = nsCString(aString),
opts = result.unwrap()]() {
return WriteSync(file, AsBytes(Span(str)), opts);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::WriteJSON(GlobalObject& aGlobal,
const nsAString& aPath,
JS::Handle<JS::Value> aValue,
const WriteOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not write to `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
auto result = InternalWriteOpts::FromBinding(aOptions);
if (result.isErr()) {
RejectJSPromise(
promise,
IOError::WithCause(result.unwrapErr(),
"Could not write to `%s'",
file->HumanReadablePath().get()));
return;
}
auto opts = result.unwrap();
if (opts.mMode == WriteMode::Append ||
opts.mMode == WriteMode::AppendOrCreate) {
promise->MaybeRejectWithNotSupportedError(
nsPrintfCString(
"Could not write to `%s': IOUtils.writeJSON does "
"not support appending to files.",
file->HumanReadablePath().get()));
return;
}
JSContext* cx = aGlobal.Context();
JS::Rooted<JS::Value> rootedValue(cx, aValue);
nsString string;
if (!nsContentUtils::StringifyJSON(cx, aValue, string,
UndefinedIsNullStringLiteral)) {
JS::Rooted<JS::Value> exn(cx, JS::UndefinedValue());
if (JS_GetPendingException(cx, &exn)) {
JS_ClearPendingException(cx);
promise->MaybeReject(exn);
}
else {
RejectJSPromise(promise,
IOError(NS_ERROR_DOM_UNKNOWN_ERR,
"Could not serialize object to JSON"_ns));
}
return;
}
DispatchAndResolve<uint32_t>(
state->mEventQueue, promise,
[file = std::move(file), string = std::move(string),
opts = std::move(opts)]() -> Result<uint32_t, IOError> {
nsAutoCString utf8Str;
if (!CopyUTF16toUTF8(string, utf8Str, fallible)) {
return Err(IOError(
NS_ERROR_OUT_OF_MEMORY,
"Failed to write to `%s': could not allocate buffer",
file->HumanReadablePath().get()));
}
return WriteSync(file, AsBytes(Span(utf8Str)), opts);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal,
const nsAString& aSourcePath,
const nsAString& aDestPath,
const MoveOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> sourceFile =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise,
"Could not move `%s' to `%s'",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get());
nsCOMPtr<nsIFile> destFile =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise,
"Could not move `%s' to `%s'",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get());
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[sourceFile = std::move(sourceFile), destFile = std::move(destFile),
noOverwrite = aOptions.mNoOverwrite]() {
return MoveSync(sourceFile, destFile, noOverwrite);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal,
const nsAString& aPath,
const RemoveOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not remove `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent,
recursive = aOptions.mRecursive,
retryReadonly = aOptions.mRetryReadonly]() {
return RemoveSync(file, ignoreAbsent, recursive, retryReadonly);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::MakeDirectory(
GlobalObject& aGlobal,
const nsAString& aPath,
const MakeDirectoryOptions& aOptions, ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not make directory `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<Ok>(state->mEventQueue, promise,
[file = std::move(file),
createAncestors = aOptions.mCreateAncestors,
ignoreExisting = aOptions.mIgnoreExisting,
permissions = aOptions.mPermissions]() {
return MakeDirectorySync(file, createAncestors,
ignoreExisting,
permissions);
});
});
}
already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
const nsAString& aPath,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not stat `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<InternalFileInfo>(
state->mEventQueue, promise,
[file = std::move(file)]() {
return StatSync(file); });
});
}
/* static */
already_AddRefed<Promise> IOUtils::Copy(GlobalObject& aGlobal,
const nsAString& aSourcePath,
const nsAString& aDestPath,
const CopyOptions& aOptions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> sourceFile =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise,
"Could not copy `%s' to `%s'",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get());
nsCOMPtr<nsIFile> destFile =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise,
"Could not copy `%s' to `%s'",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get());
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[sourceFile = std::move(sourceFile), destFile = std::move(destFile),
noOverwrite = aOptions.mNoOverwrite,
recursive = aOptions.mRecursive]() {
return CopySync(sourceFile, destFile, noOverwrite, recursive);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::SetAccessTime(
GlobalObject& aGlobal,
const nsAString& aPath,
const Optional<int64_t>& aAccess, ErrorResult& aError) {
return SetTime(aGlobal, aPath, aAccess, &nsIFile::SetLastAccessedTime,
"access", aError);
}
/* static */
already_AddRefed<Promise> IOUtils::SetModificationTime(
GlobalObject& aGlobal,
const nsAString& aPath,
const Optional<int64_t>& aModification, ErrorResult& aError) {
return SetTime(aGlobal, aPath, aModification, &nsIFile::SetLastModifiedTime,
"modification", aError);
}
/* static */
already_AddRefed<Promise> IOUtils::SetTime(GlobalObject& aGlobal,
const nsAString& aPath,
const Optional<int64_t>& aNewTime,
IOUtils::SetTimeFn aSetTimeFn,
const char*
const aTimeKind,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not set %s time on `%s'", aTimeKind,
NS_ConvertUTF16toUTF8(aPath).get());
int64_t newTime = aNewTime.WasPassed() ? aNewTime.Value()
: PR_Now() / PR_USEC_PER_MSEC;
DispatchAndResolve<int64_t>(
state->mEventQueue, promise,
[file = std::move(file), aSetTimeFn, newTime]() {
return SetTimeSync(file, aSetTimeFn, newTime);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::HasChildren(
GlobalObject& aGlobal,
const nsAString& aPath,
const HasChildrenOptions& aOptions, ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not check children of `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<
bool>(
state->mEventQueue, promise,
[file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent]() {
return HasChildrenSync(file, ignoreAbsent);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::GetChildren(
GlobalObject& aGlobal,
const nsAString& aPath,
const GetChildrenOptions& aOptions, ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not get children of `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<nsTArray<nsString>>(
state->mEventQueue, promise,
[file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent]() {
return GetChildrenSync(file, ignoreAbsent);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::SetPermissions(GlobalObject& aGlobal,
const nsAString& aPath,
uint32_t aPermissions,
const bool aHonorUmask,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
#if defined(XP_UNIX) && !
defined(ANDROID)
if (aHonorUmask) {
aPermissions &= ~nsSystemInfo::gUserUmask;
}
#endif
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not set permissions on `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[file = std::move(file), permissions = aPermissions]() {
return SetPermissionsSync(file, permissions);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::Exists(GlobalObject& aGlobal,
const nsAString& aPath,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not determine if `%s' exists",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<
bool>(
state->mEventQueue, promise,
[file = std::move(file)]() {
return ExistsSync(file); });
});
}
/* static */
already_AddRefed<Promise> IOUtils::CreateUniqueFile(GlobalObject& aGlobal,
const nsAString& aParent,
const nsAString& aPrefix,
const uint32_t aPermissions,
ErrorResult& aError) {
return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::NORMAL_FILE_TYPE,
aPermissions, aError);
}
/* static */
already_AddRefed<Promise> IOUtils::CreateUniqueDirectory(
GlobalObject& aGlobal,
const nsAString& aParent,
const nsAString& aPrefix,
const uint32_t aPermissions, ErrorResult& aError) {
return CreateUnique(aGlobal, aParent, aPrefix, nsIFile::DIRECTORY_TYPE,
aPermissions, aError);
}
/* static */
already_AddRefed<Promise> IOUtils::CreateUnique(GlobalObject& aGlobal,
const nsAString& aParent,
const nsAString& aPrefix,
const uint32_t aFileType,
const uint32_t aPermissions,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aParent, promise,
"Could not create unique %s in `%s'",
aFileType == nsIFile::NORMAL_FILE_TYPE ?
"file" :
"directory",
NS_ConvertUTF16toUTF8(aParent).get());
if (nsresult rv = file->Append(aPrefix); NS_FAILED(rv)) {
RejectJSPromise(
promise,
IOError(
rv,
"Could not create unique %s: could not append prefix `%s' to "
"parent `%s'",
aFileType == nsIFile::NORMAL_FILE_TYPE ?
"file" :
"directory",
NS_ConvertUTF16toUTF8(aPrefix).get(),
file->HumanReadablePath().get()));
return;
}
DispatchAndResolve<nsString>(
state->mEventQueue, promise,
[file = std::move(file), aPermissions, aFileType]() {
return CreateUniqueSync(file, aFileType, aPermissions);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::ComputeHexDigest(
GlobalObject& aGlobal,
const nsAString& aPath,
const HashAlgorithm aAlgorithm, ErrorResult& aError) {
const bool nssInitialized = EnsureNSSInitializedChromeOrContent();
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
if (!nssInitialized) {
RejectJSPromise(promise, IOError(NS_ERROR_UNEXPECTED,
"Could not initialize NSS"_ns));
return;
}
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not hash `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<nsCString>(state->mEventQueue, promise,
[file = std::move(file), aAlgorithm]() {
return ComputeHexDigestSync(file,
aAlgorithm);
});
});
}
#if defined(XP_WIN)
/* static */
already_AddRefed<Promise> IOUtils::GetWindowsAttributes(GlobalObject& aGlobal,
const nsAString& aPath,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(file, aPath, promise,
"Could not get Windows file attributes of "
"`%s'",
NS_ConvertUTF16toUTF8(aPath).get());
RefPtr<StrongWorkerRef> workerRef;
if (!NS_IsMainThread()) {
// We need to manually keep the worker alive until the promise
// returned by Dispatch() resolves or rejects.
workerRef = StrongWorkerRef::CreateForcibly(
GetCurrentThreadWorkerPrivate(), __func__);
}
state->mEventQueue
->
template Dispatch<uint32_t>([file = std::move(file)]() {
return GetWindowsAttributesSync(file);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[workerRef, promise = RefPtr{promise}](
const uint32_t aAttrs) {
WindowsFileAttributes attrs;
attrs.mReadOnly.Construct(aAttrs & FILE_ATTRIBUTE_READONLY);
attrs.mHidden.Construct(aAttrs & FILE_ATTRIBUTE_HIDDEN);
attrs.mSystem.Construct(aAttrs & FILE_ATTRIBUTE_SYSTEM);
promise->MaybeResolve(attrs);
},
[workerRef, promise = RefPtr{promise}](
const IOError& aErr) {
RejectJSPromise(promise, aErr);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::SetWindowsAttributes(
GlobalObject& aGlobal,
const nsAString& aPath,
const WindowsFileAttributes& aAttrs, ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aPath, promise,
"Could not set Windows file attributes on `%s'",
NS_ConvertUTF16toUTF8(aPath).get());
uint32_t setAttrs = 0;
uint32_t clearAttrs = 0;
if (aAttrs.mReadOnly.WasPassed()) {
if (aAttrs.mReadOnly.Value()) {
setAttrs |= FILE_ATTRIBUTE_READONLY;
}
else {
clearAttrs |= FILE_ATTRIBUTE_READONLY;
}
}
if (aAttrs.mHidden.WasPassed()) {
if (aAttrs.mHidden.Value()) {
setAttrs |= FILE_ATTRIBUTE_HIDDEN;
}
else {
clearAttrs |= FILE_ATTRIBUTE_HIDDEN;
}
}
if (aAttrs.mSystem.WasPassed()) {
if (aAttrs.mSystem.Value()) {
setAttrs |= FILE_ATTRIBUTE_SYSTEM;
}
else {
clearAttrs |= FILE_ATTRIBUTE_SYSTEM;
}
}
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[file = std::move(file), setAttrs, clearAttrs]() {
return SetWindowsAttributesSync(file, setAttrs, clearAttrs);
});
});
}
#elif defined(XP_MACOSX)
/* static */
already_AddRefed<Promise> IOUtils::HasMacXAttr(GlobalObject& aGlobal,
const nsAString& aPath,
const nsACString& aAttr,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aPath, promise,
"Could not read the extended attribute `%s' from `%s'",
PromiseFlatCString(aAttr).get(),
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<
bool>(
state->mEventQueue, promise,
[file = std::move(file), attr = nsCString(aAttr)]() {
return HasMacXAttrSync(file, attr);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::GetMacXAttr(GlobalObject& aGlobal,
const nsAString& aPath,
const nsACString& aAttr,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aPath, promise,
"Could not read extended attribute `%s' from `%s'",
PromiseFlatCString(aAttr).get(),
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<nsTArray<uint8_t>>(
state->mEventQueue, promise,
[file = std::move(file), attr = nsCString(aAttr)]() {
return GetMacXAttrSync(file, attr);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::SetMacXAttr(GlobalObject& aGlobal,
const nsAString& aPath,
const nsACString& aAttr,
const Uint8Array& aValue,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aPath, promise,
"Could not set the extended attribute `%s' on `%s'",
PromiseFlatCString(aAttr).get(),
NS_ConvertUTF16toUTF8(aPath).get());
nsTArray<uint8_t> value;
if (!aValue.AppendDataTo(value)) {
RejectJSPromise(
promise, IOError(NS_ERROR_OUT_OF_MEMORY,
"Could not set extended attribute `%s' on `%s': "
"could not allocate buffer",
PromiseFlatCString(aAttr).get(),
file->HumanReadablePath().get()));
return;
}
DispatchAndResolve<Ok>(state->mEventQueue, promise,
[file = std::move(file), attr = nsCString(aAttr),
value = std::move(value)] {
return SetMacXAttrSync(file, attr, value);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::DelMacXAttr(GlobalObject& aGlobal,
const nsAString& aPath,
const nsACString& aAttr,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
nsCOMPtr<nsIFile> file =
new nsLocalFile();
REJECT_IF_INIT_PATH_FAILED(
file, aPath, promise,
"Could not delete extended attribute `%s' on `%s'",
PromiseFlatCString(aAttr).get(),
NS_ConvertUTF16toUTF8(aPath).get());
DispatchAndResolve<Ok>(
state->mEventQueue, promise,
[file = std::move(file), attr = nsCString(aAttr)] {
return DelMacXAttrSync(file, attr);
});
});
}
#endif
/* static */
already_AddRefed<Promise> IOUtils::GetFile(
GlobalObject& aGlobal,
const Sequence<nsString>& aComponents,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
ErrorResult joinErr;
nsCOMPtr<nsIFile> file = PathUtils::Join(aComponents, joinErr);
if (joinErr.Failed()) {
promise->MaybeReject(std::move(joinErr));
return;
}
nsCOMPtr<nsIFile> parent;
if (nsresult rv = file->GetParent(getter_AddRefs(parent));
NS_FAILED(rv)) {
RejectJSPromise(promise, IOError(rv,
"Could not get nsIFile for `%s': "
"could not get parent directory",
file->HumanReadablePath().get()));
return;
}
state->mEventQueue
->
template Dispatch<Ok>([parent = std::move(parent)]() {
return MakeDirectorySync(parent,
/* aCreateAncestors = */ true,
/* aIgnoreExisting = */ true, 0755);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[file = std::move(file), promise = RefPtr(promise)](
const Ok&) {
promise->MaybeResolve(file);
},
[promise = RefPtr(promise)](
const IOError& err) {
RejectJSPromise(promise, err);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::GetDirectory(
GlobalObject& aGlobal,
const Sequence<nsString>& aComponents,
ErrorResult& aError) {
return WithPromiseAndState(
aGlobal, aError, [&](Promise* promise,
auto& state) {
ErrorResult joinErr;
nsCOMPtr<nsIFile> dir = PathUtils::Join(aComponents, joinErr);
if (joinErr.Failed()) {
promise->MaybeReject(std::move(joinErr));
return;
}
state->mEventQueue
->
template Dispatch<Ok>([dir]() {
return MakeDirectorySync(dir,
/* aCreateAncestors = */ true,
/* aIgnoreExisting = */ true, 0755);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[dir, promise = RefPtr(promise)](
const Ok&) {
promise->MaybeResolve(dir);
},
[promise = RefPtr(promise)](
const IOError& err) {
RejectJSPromise(promise, err);
});
});
}
/* static */
already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal,
ErrorResult& aError) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<Promise> promise = Promise::Create(global, aError);
if (aError.Failed()) {
return nullptr;
}
MOZ_ASSERT(promise);
return do_AddRef(promise);
}
/* static */
Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadSync(
nsIFile* aFile,
const uint64_t aOffset,
const Maybe<uint32_t> aMaxBytes,
const bool aDecompress, IOUtils::BufferKind aBufferKind) {
MOZ_ASSERT(!NS_IsMainThread());
// This is checked in IOUtils::Read.
MOZ_ASSERT(aMaxBytes.isNothing() || !aDecompress,
"maxBytes and decompress are mutually exclusive");
if (aOffset >
static_cast<uint64_t>(INT64_MAX)) {
return Err(
IOError(NS_ERROR_ILLEGAL_INPUT,
"Could not read `%s': requested offset is too large (%" PRIu64
" > %" PRId64
")",
aFile->HumanReadablePath().get(), aOffset, INT64_MAX));
}
const int64_t offset =
static_cast<int64_t>(aOffset);
RefPtr<nsFileRandomAccessStream> stream =
new nsFileRandomAccessStream();
if (nsresult rv =
stream->Init(aFile, PR_RDONLY | nsIFile::OS_READAHEAD, 0666, 0);
NS_FAILED(rv)) {
if (IsFileNotFound(rv)) {
return Err(IOError(rv,
"Could not open `%s': file does not exist",
aFile->HumanReadablePath().get()));
}
return Err(
IOError(rv,
"Could not open `%s'", aFile->HumanReadablePath().get()));
}
uint32_t bufSize = 0;
if (aMaxBytes.isNothing()) {
// Limitation: We cannot read more than the maximum size of a TypedArray
// (UINT32_MAX bytes). Reject if we have been requested to
// perform too large of a read.
int64_t rawStreamSize = -1;
if (nsresult rv = stream->GetSize(&rawStreamSize); NS_FAILED(rv)) {
return Err(
IOError(NS_ERROR_FILE_ACCESS_DENIED,
"Could not open `%s': could not stat file or directory",
aFile->HumanReadablePath().get()));
}
MOZ_RELEASE_ASSERT(rawStreamSize >= 0);
uint64_t streamSize =
static_cast<uint64_t>(rawStreamSize);
if (aOffset >= streamSize) {
bufSize = 0;
}
else {
if (streamSize - offset >
static_cast<int64_t>(UINT32_MAX)) {
return Err(IOError(NS_ERROR_FILE_TOO_BIG,
"Could not read `%s' with offset %" PRIu64
": file is too large (%" PRIu64
" bytes)",
aFile->HumanReadablePath().get(), offset,
streamSize));
}
bufSize =
static_cast<uint32_t>(streamSize - offset);
}
}
else {
bufSize = aMaxBytes.value();
}
if (offset > 0) {
if (nsresult rv = stream->Seek(PR_SEEK_SET, offset); NS_FAILED(rv)) {
return Err(IOError(
rv,
"Could not read `%s': could not seek to position %" PRId64,
aFile->HumanReadablePath().get(), offset));
}
}
JsBuffer buffer = JsBuffer::CreateEmpty(aBufferKind);
if (bufSize > 0) {
auto result = JsBuffer::Create(aBufferKind, bufSize);
if (result.isErr()) {
return Err(IOError::WithCause(result.unwrapErr(),
"Could not read `%s'",
aFile->HumanReadablePath().get()));
}
buffer = result.unwrap();
Span<
char> toRead = buffer.BeginWriting();
// Read the file from disk.
uint32_t totalRead = 0;
while (totalRead != bufSize) {
// Read no more than INT32_MAX on each call to stream->Read, otherwise it
// returns an error.
uint32_t bytesToReadThisChunk =
std::min<uint32_t>(bufSize - totalRead, INT32_MAX);
uint32_t bytesRead = 0;
if (nsresult rv =
stream->Read(toRead.Elements(), bytesToReadThisChunk, &bytesRead);
NS_FAILED(rv)) {
return Err(
IOError(rv,
"Could not read `%s': encountered an unexpected error",
aFile->HumanReadablePath().get()));
}
if (bytesRead == 0) {
break;
}
totalRead += bytesRead;
toRead = toRead.From(bytesRead);
}
buffer.SetLength(totalRead);
}
// Decompress the file contents, if required.
if (aDecompress) {
auto result =
MozLZ4::Decompress(AsBytes(buffer.BeginReading()), aBufferKind);
if (result.isErr()) {
return Err(IOError::WithCause(result.unwrapErr(),
"Could not read `%s'",
aFile->HumanReadablePath().get()));
}
return result;
}
return std::move(buffer);
}
/* static */
Result<IOUtils::JsBuffer, IOUtils::IOError> IOUtils::ReadUTF8Sync(
nsIFile* aFile,
bool aDecompress) {
auto result = ReadSync(aFile, 0, Nothing{}, aDecompress, BufferKind::String);
if (result.isErr()) {
return result.propagateErr();
}
JsBuffer buffer = result.unwrap();
if (!IsUtf8(buffer.BeginReading())) {
return Err(IOError(NS_ERROR_FILE_CORRUPTED,
"Could not read `%s': file is not UTF-8 encoded",
aFile->HumanReadablePath().get()));
}
return buffer;
}
/* static */
Result<uint32_t, IOUtils::IOError> IOUtils::WriteSync(
nsIFile* aFile,
const Span<
const uint8_t>& aByteArray,
const IOUtils::InternalWriteOpts& aOptions) {
MOZ_ASSERT(!NS_IsMainThread());
nsIFile* backupFile = aOptions.mBackupFile;
nsIFile* tempFile = aOptions.mTmpFile;
bool exists =
false;
IOUTILS_TRY_WITH_CONTEXT(
aFile->Exists(&exists),
"Could not write to `%s': could not stat file or directory",
aFile->HumanReadablePath().get());
if (exists && aOptions.mMode == WriteMode::Create) {
return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS,
"Could not write to `%s': refusing to overwrite file, "
"`mode' is not \"overwrite\
"",
aFile->HumanReadablePath().get()));
}
// If backupFile was specified, perform the backup as a move.
if (exists && backupFile) {
// We copy `destFile` here to a new `nsIFile` because
// `nsIFile::MoveToFollowingLinks` will update the path of the file. If we
// did not do this, we would end up having `destFile` point to the same
// location as `backupFile`. Then, when we went to write to `destFile`, we
// would end up overwriting `backupFile` and never actually write to the
// file we were supposed to.
nsCOMPtr<nsIFile> toMove;
MOZ_ALWAYS_SUCCEEDS(aFile->Clone(getter_AddRefs(toMove)));
bool noOverwrite = aOptions.mMode == WriteMode::Create;
if (
auto result = MoveSync(toMove, backupFile, noOverwrite);
result.isErr()) {
return Err(IOError::WithCause(
result.unwrapErr(),
"Could not write to `%s': failed to back up source file",
aFile->HumanReadablePath().get()));
}
}
// If tempFile was specified, we will write to there first, then perform a
// move to ensure the file ends up at the final requested destination.
nsIFile* writeFile;
if (tempFile) {
writeFile = tempFile;
}
else {
writeFile = aFile;
}
int32_t flags = PR_WRONLY;
switch (aOptions.mMode) {
case WriteMode::Overwrite:
flags |= PR_TRUNCATE | PR_CREATE_FILE;
break;
case WriteMode::Append:
flags |= PR_APPEND;
break;
case WriteMode::AppendOrCreate:
flags |= PR_APPEND | PR_CREATE_FILE;
break;
case WriteMode::Create:
flags |= PR_CREATE_FILE | PR_EXCL;
break;
default:
MOZ_CRASH(
"IOUtils: unknown write mode");
}
if (aOptions.mFlush) {
flags |= PR_SYNC;
}
// Try to perform the write and ensure that the file is closed before
// continuing.
uint32_t totalWritten = 0;
{
// Compress the byte array if required.
nsTArray<uint8_t> compressed;
Span<
const char> bytes;
if (aOptions.mCompress) {
auto result = MozLZ4::Compress(aByteArray);
if (result.isErr()) {
return Err(IOError::WithCause(result.unwrapErr(),
"Could not write to `%s'",
writeFile->HumanReadablePath().get()));
}
compressed = result.unwrap();
bytes = Span(
reinterpret_cast<
const char*>(compressed.Elements()),
compressed.Length());
}
else {
bytes = Span(
reinterpret_cast<
const char*>(aByteArray.Elements()),
aByteArray.Length());
}
RefPtr<nsFileOutputStream> stream =
new nsFileOutputStream();
if (nsresult rv = stream->Init(writeFile, flags, 0666, 0); NS_FAILED(rv)) {
// Normalize platform-specific errors for opening a directory to an access
// denied error.
if (rv == nsresult::NS_ERROR_FILE_IS_DIRECTORY) {
rv = NS_ERROR_FILE_ACCESS_DENIED;
}
return Err(IOError(
rv,
"Could not write to `%s': failed to open file for writing",
writeFile->HumanReadablePath().get()));
}
// nsFileRandomAccessStream::Write uses PR_Write under the hood, which
// accepts a *int32_t* for the chunk size.
uint32_t chunkSize = INT32_MAX;
Span<
const char> pendingBytes = bytes;
while (pendingBytes.Length() > 0) {
if (pendingBytes.Length() < chunkSize) {
chunkSize = pendingBytes.Length();
}
uint32_t bytesWritten = 0;
if (nsresult rv =
stream->Write(pendingBytes.Elements(), chunkSize, &bytesWritten);
NS_FAILED(rv)) {
return Err(IOError(rv,
"Could not write to `%s': failed to write chunk; "
"the file may be corrupt",
writeFile->HumanReadablePath().get()));
}
pendingBytes = pendingBytes.From(bytesWritten);
totalWritten += bytesWritten;
}
}
// If tempFile was passed, check destFile against writeFile and, if they
// differ, the operation is finished by performing a move.
if (tempFile) {
nsAutoStringN<256> destPath;
nsAutoStringN<256> writePath;
MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(destPath));
MOZ_ALWAYS_SUCCEEDS(writeFile->GetPath(writePath));
// nsIFile::MoveToFollowingLinks will only update the path of the file if
// the move succeeds.
if (destPath != writePath) {
if (aOptions.mTmpFile) {
bool isDir =
false;
if (nsresult rv = aFile->IsDirectory(&isDir);
NS_FAILED(rv) && !IsFileNotFound(rv)) {
return Err(IOError(
rv,
"Could not write to `%s': could not stat file or directory",
aFile->HumanReadablePath().get()));
}
// If we attempt to write to a directory *without* a temp file, we get a
// permission error.
//
// However, if we are writing to a temp file first, when we copy the
// temp file over the destination file, we actually end up copying it
// inside the directory, which is not what we want. In this case, we are
// just going to bail out early.
if (isDir) {
return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED,
"Could not write to `%s': file is a directory",
aFile->HumanReadablePath().get()));
}
}
if (
auto result = MoveSync(writeFile, aFile,
/* aNoOverwrite = */ false);
result.isErr()) {
return Err(IOError::WithCause(
result.unwrapErr(),
"Could not write to `%s': could not move overwite with temporary "
"file",
aFile->HumanReadablePath().get()));
}
}
}
return totalWritten;
}
/* static */
Result<Ok, IOUtils::IOError> IOUtils::MoveSync(nsIFile* aSourceFile,
nsIFile* aDestFile,
bool aNoOverwrite) {
MOZ_ASSERT(!NS_IsMainThread());
// Ensure the source file exists before continuing. If it doesn't exist,
// subsequent operations can fail in different ways on different platforms.
bool srcExists =
false;
IOUTILS_TRY_WITH_CONTEXT(
aSourceFile->Exists(&srcExists),
"Could not move `%s' to `%s': could not stat source file or directory",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get());
if (!srcExists) {
return Err(
IOError(NS_ERROR_FILE_NOT_FOUND,
"Could not move `%s' to `%s': source file does not exist",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get()));
}
return CopyOrMoveSync(&nsIFile::MoveToFollowingLinks,
"move", aSourceFile,
aDestFile, aNoOverwrite);
}
/* static */
Result<Ok, IOUtils::IOError> IOUtils::CopySync(nsIFile* aSourceFile,
nsIFile* aDestFile,
bool aNoOverwrite,
bool aRecursive) {
MOZ_ASSERT(!NS_IsMainThread());
// Ensure the source file exists before continuing. If it doesn't exist,
// subsequent operations can fail in different ways on different platforms.
bool srcExists;
IOUTILS_TRY_WITH_CONTEXT(
aSourceFile->Exists(&srcExists),
"Could not copy `%s' to `%s': could not stat source file or directory",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get());
if (!srcExists) {
return Err(
IOError(NS_ERROR_FILE_NOT_FOUND,
"Could not copy `%s' to `%s': source file does not exist",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get()));
}
// If source is a directory, fail immediately unless the recursive option is
// true.
bool srcIsDir =
false;
IOUTILS_TRY_WITH_CONTEXT(
aSourceFile->IsDirectory(&srcIsDir),
"Could not copy `%s' to `%s': could not stat source file or directory",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get());
if (srcIsDir && !aRecursive) {
return Err(IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED,
"Refused to copy directory `%s' to `%s': `recursive' is "
"false\n",
aSourceFile->HumanReadablePath().get(),
aDestFile->HumanReadablePath().get()));
}
return CopyOrMoveSync(&nsIFile::CopyToFollowingLinks,
"copy", aSourceFile,
aDestFile, aNoOverwrite);
}
/* static */
template <
typename CopyOrMoveFn>
Result<Ok, IOUtils::IOError> IOUtils::CopyOrMoveSync(CopyOrMoveFn aMethod,
const char* aMethodName,
nsIFile* aSource,
nsIFile* aDest,
bool aNoOverwrite) {
MOZ_ASSERT(!NS_IsMainThread());
// Case 1: Destination is an existing directory. Copy/move source into dest.
bool destIsDir =
false;
bool destExists =
true;
nsresult rv = aDest->IsDirectory(&destIsDir);
if (NS_SUCCEEDED(rv) && destIsDir) {
rv = (aSource->*aMethod)(aDest, u
""_ns);
if (NS_FAILED(rv)) {
return Err(IOError(rv,
"Could not %s `%s' to `%s'", aMethodName,
aSource->HumanReadablePath().get(),
aDest->HumanReadablePath().get()));
}
return Ok();
}
if (NS_FAILED(rv)) {
if (!IsFileNotFound(rv)) {
// It's ok if the dest file doesn't exist. Case 2 handles this below.
// Bail out early for any other kind of error though.
return Err(IOError(rv,
"Could not %s `%s' to `%s'", aMethodName,
aSource->HumanReadablePath().get(),
aDest->HumanReadablePath().get()));
}
destExists =
false;
}
// Case 2: Destination is a file which may or may not exist.
// Try to copy or rename the source to the destination.
// If the destination exists and the source is not a regular file,
// then this may fail.
if (aNoOverwrite && destExists) {
return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS,
"Could not %s `%s' to `%s': destination file exists and "
"`noOverwrite' is true",
aMethodName, aSource->HumanReadablePath().get(),
aDest->HumanReadablePath().get()));
}
if (destExists && !destIsDir) {
// If the source file is a directory, but the target is a file, abort early.
// Different implementations of |CopyTo| and |MoveTo| seem to handle this
// error case differently (or not at all), so we explicitly handle it here.
bool srcIsDir =
false;
IOUTILS_TRY_WITH_CONTEXT(
aSource->IsDirectory(&srcIsDir),
"Could not %s `%s' to `%s': could not stat source file or directory",
aMethodName, aSource->HumanReadablePath().get(),
aDest->HumanReadablePath().get());
if (srcIsDir) {
return Err(IOError(
NS_ERROR_FILE_DESTINATION_NOT_DIR,
"Could not %s directory `%s' to `%s': destination is not a directory",
aMethodName, aSource->HumanReadablePath().get(),
aDest->HumanReadablePath().get()));
}
}
// We would have already thrown if the path was zero-length.
nsAutoString destName;
MOZ_ALWAYS_SUCCEEDS(aDest->GetLeafName(destName));
--> --------------------
--> maximum size reached
--> --------------------