Bug 1654295: Reduce boilerplate in IOUtils failure paths r=barret

This change completely overhauls the way errors are handled in `IOUtils`. With
the introduction of the new `IOError` type, a useful error message is paired
with an `nsresult` at the error site. This provides erroneous callers with more
information about what went wrong, while improving consistency with mapping
errors to `DOMExceptions`.

For error sites where it is not immediately clear what went wrong, messages can
be omitted. A default error message will be filled in corresponding with the
wrapped `nsresult` when the operation is rejected to the calling JavaScript.

Differential Revision: https://phabricator.services.mozilla.com/D84736
This commit is contained in:
Keefer Rourke 2020-07-30 14:02:50 +00:00
Родитель 970f76ccd9
Коммит 50a79e72d0
3 изменённых файлов: 343 добавлений и 240 удалений

Просмотреть файл

@ -9,22 +9,15 @@
#include "mozilla/dom/IOUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/Services.h"
#include "mozilla/Span.h"
#include "mozilla/TextUtils.h"
#include "nspr/prio.h"
#include "nspr/private/pprio.h"
#include "nspr/prtypes.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsNativeCharsetUtils.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsThreadManager.h"
#include "SpecialSystemDirectory.h"
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
# include <fcntl.h>
@ -218,7 +211,7 @@ already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
[path = nsAutoString(aPath), toRead]() {
MOZ_ASSERT(!NS_IsMainThread());
auto rv = IOUtils::ReadSync(path, toRead);
return ToMozPromise<IOReadMozPromise, nsTArray<uint8_t>, nsresult>(
return ToMozPromise<IOReadMozPromise, nsTArray<uint8_t>, IOError>(
rv, __func__);
})
->Then(
@ -227,26 +220,8 @@ already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
const TypedArrayCreator<Uint8Array> arrayCreator(aBuf);
promise->MaybeResolve(arrayCreator);
},
[promise = RefPtr(promise),
path = NS_ConvertUTF16toUTF8(aPath)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
nsPrintfCString("Could not open file at %s", path.get()));
break;
case NS_ERROR_FILE_ACCESS_DENIED:
promise->MaybeRejectWithNotReadableError(nsPrintfCString(
"Could not get info for file at %s", path.get()));
break;
case NS_ERROR_FILE_TOO_BIG:
promise->MaybeRejectWithNotReadableError(nsPrintfCString(
"File at %s is too large to be read", path.get()));
break;
default:
promise->MaybeRejectWithUnknownError(FormatErrorMessage(
aError, "Unexpected error reading file at %s", path.get()));
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
@ -278,49 +253,15 @@ already_AddRefed<Promise> IOUtils::WriteAtomic(
[destPath = nsString(aPath), toWrite = std::move(toWrite), aOptions]() {
MOZ_ASSERT(!NS_IsMainThread());
auto rv = WriteAtomicSync(destPath, toWrite, aOptions);
return ToMozPromise<IOWriteMozPromise, uint32_t, nsresult>(rv,
__func__);
return ToMozPromise<IOWriteMozPromise, uint32_t, IOError>(rv, __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const uint32_t& aBytesWritten) {
promise->MaybeResolve(aBytesWritten);
},
[promise = RefPtr(promise),
path = NS_ConvertUTF16toUTF8(aPath)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_ALREADY_EXISTS:
promise->MaybeRejectWithNoModificationAllowedError(
nsPrintfCString("Refusing to overwrite the file at %s",
path.get()));
break;
// TODO: It would be nice to be able to distinguish between an
// error with backing up the original file and moving the
// temp file to overwrite the dest.
case NS_ERROR_FILE_COPY_OR_MOVE_FAILED:
promise->MaybeRejectWithOperationError(
nsPrintfCString("Atomic write failed, but the destination "
"at %s should not have been changed",
path.get()));
break;
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
nsPrintfCString("Could not open file at %s", path.get()));
break;
case NS_ERROR_FILE_ACCESS_DENIED:
promise->MaybeRejectWithNotReadableError(nsPrintfCString(
"Could not open the file at %s", path.get()));
break;
case NS_ERROR_FILE_TOO_BIG:
promise->MaybeRejectWithNotReadableError(nsPrintfCString(
"File at %s is too large to be read", path.get()));
break;
default:
promise->MaybeRejectWithUnknownError(FormatErrorMessage(
aError, "Unexpected error writing to file at %s",
path.get()));
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
@ -352,44 +293,15 @@ already_AddRefed<Promise> IOUtils::Move(GlobalObject& aGlobal,
destPathString = nsAutoString(aDestPath), noOverwrite]() {
MOZ_ASSERT(!NS_IsMainThread());
auto rv = MoveSync(srcPathString, destPathString, noOverwrite);
return ToMozPromise<IOMozPromise, Ok, nsresult>(rv, __func__);
return ToMozPromise<IOMozPromise, Ok, IOError>(rv, __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const Ok&) {
promise->MaybeResolveWithUndefined();
},
[promise = RefPtr(promise)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_ACCESS_DENIED:
promise->MaybeRejectWithInvalidAccessError("Access denied");
break;
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
"Source file does not exist");
break;
case NS_ERROR_FILE_ALREADY_EXISTS:
promise->MaybeRejectWithNoModificationAllowedError(
"Destination file exists and overwrites are not allowed");
break;
case NS_ERROR_FILE_READ_ONLY:
promise->MaybeRejectWithNoModificationAllowedError(
"Destination is read only");
break;
case NS_ERROR_FILE_DESTINATION_NOT_DIR:
promise->MaybeRejectWithInvalidAccessError(
"Source is a directory but destination is not");
break;
case NS_ERROR_FILE_UNRECOGNIZED_PATH:
promise->MaybeRejectWithOperationError(
"Only absolute file paths are permitted");
break;
default: {
promise->MaybeRejectWithUnknownError(
FormatErrorMessage(aError, "Unexpected error moving file"));
}
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
@ -413,29 +325,15 @@ already_AddRefed<Promise> IOUtils::Remove(GlobalObject& aGlobal,
MOZ_ASSERT(!NS_IsMainThread());
auto rv = RemoveSync(path, aOptions.mIgnoreAbsent,
aOptions.mRecursive);
return ToMozPromise<IOMozPromise, Ok, nsresult>(rv, __func__);
return ToMozPromise<IOMozPromise, Ok, IOError>(rv, __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const Ok&) {
promise->MaybeResolveWithUndefined();
},
[promise = RefPtr(promise)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
"Target file does not exist");
break;
case NS_ERROR_FILE_DIR_NOT_EMPTY:
promise->MaybeRejectWithOperationError(
"Could not remove non-empty directory, specify the "
"`recursive: true` option to mitigate this error");
break;
default:
promise->MaybeRejectWithUnknownError(FormatErrorMessage(
aError, "Unexpected error removing file"));
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
@ -459,32 +357,15 @@ already_AddRefed<Promise> IOUtils::MakeDirectory(
MOZ_ASSERT(!NS_IsMainThread());
auto rv = CreateDirectorySync(path, aOptions.mCreateAncestors,
aOptions.mIgnoreExisting);
return ToMozPromise<IOMozPromise, Ok, nsresult>(rv, __func__);
return ToMozPromise<IOMozPromise, Ok, IOError>(rv, __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const Ok&) {
promise->MaybeResolveWithUndefined();
},
[promise = RefPtr(promise)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_ALREADY_EXISTS:
promise->MaybeRejectWithNoModificationAllowedError(
"Could not create directory because file already exists");
break;
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
"Target path has missing ancestors");
break;
case NS_ERROR_FILE_NOT_DIRECTORY:
promise->MaybeRejectWithOperationError(
"Target exists and is not a directory");
break;
default:
promise->MaybeRejectWithUnknownError(FormatErrorMessage(
aError, "Unexpected error creating directory"));
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
}
@ -506,7 +387,7 @@ already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
MOZ_ASSERT(!NS_IsMainThread());
auto rv = StatSync(path);
return ToMozPromise<IOStatMozPromise, InternalFileInfo, nsresult>(
return ToMozPromise<IOStatMozPromise, InternalFileInfo, IOError>(
rv, __func__);
})
->Then(
@ -514,17 +395,8 @@ already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
[promise = RefPtr(promise)](const InternalFileInfo& aInfo) {
promise->MaybeResolve(aInfo);
},
[promise = RefPtr(promise)](const nsresult& aError) {
switch (aError) {
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
promise->MaybeRejectWithNotFoundError(
"Target file does not exist");
break;
default:
promise->MaybeRejectWithUnknownError(FormatErrorMessage(
aError, "Unexpected error accessing file"));
}
[promise = RefPtr(promise)](const IOError& aError) {
RejectJSPromise(promise, aError);
});
return promise.forget();
@ -602,6 +474,55 @@ already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) {
return do_AddRef(promise);
}
/* static */
void IOUtils::RejectJSPromise(const RefPtr<Promise>& aPromise,
const IOError& aError) {
const auto& errMsg = aError.Message();
switch (aError.Code()) {
case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
case NS_ERROR_FILE_NOT_FOUND:
aPromise->MaybeRejectWithNotFoundError(errMsg.refOr("File not found"_ns));
break;
case NS_ERROR_FILE_ACCESS_DENIED:
aPromise->MaybeRejectWithNotAllowedError(
errMsg.refOr("Access was denied to the target file"_ns));
break;
case NS_ERROR_FILE_TOO_BIG:
aPromise->MaybeRejectWithNotReadableError(
errMsg.refOr("Target file is too big"_ns));
break;
case NS_ERROR_FILE_ALREADY_EXISTS:
aPromise->MaybeRejectWithNoModificationAllowedError(
errMsg.refOr("Target file already exists"_ns));
break;
case NS_ERROR_FILE_COPY_OR_MOVE_FAILED:
aPromise->MaybeRejectWithOperationError(
errMsg.refOr("Failed to copy or move the target file"_ns));
break;
case NS_ERROR_FILE_READ_ONLY:
aPromise->MaybeRejectWithReadOnlyError(
errMsg.refOr("Target file is read only"_ns));
break;
case NS_ERROR_FILE_NOT_DIRECTORY:
case NS_ERROR_FILE_DESTINATION_NOT_DIR:
aPromise->MaybeRejectWithInvalidAccessError(
errMsg.refOr("Target file is not a directory"_ns));
break;
case NS_ERROR_FILE_UNRECOGNIZED_PATH:
aPromise->MaybeRejectWithOperationError(
errMsg.refOr("Target file path is not recognized"_ns));
break;
case NS_ERROR_FILE_DIR_NOT_EMPTY:
aPromise->MaybeRejectWithOperationError(
errMsg.refOr("Target directory is not empty"_ns));
break;
default:
aPromise->MaybeRejectWithUnknownError(
errMsg.refOr(FormatErrorMessage(aError.Code(), "Unexpected error")));
}
}
/* static */
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::OpenExistingSync(
const nsAString& aPath, int32_t aFlags) {
@ -641,25 +562,34 @@ UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::CreateFileSync(
}
/* static */
Result<nsTArray<uint8_t>, nsresult> IOUtils::ReadSync(
Result<nsTArray<uint8_t>, IOUtils::IOError> IOUtils::ReadSync(
const nsAString& aPath, const Maybe<uint32_t>& aMaxBytes) {
MOZ_ASSERT(!NS_IsMainThread());
UniquePtr<PRFileDesc, PR_CloseDelete> fd = OpenExistingSync(aPath, PR_RDONLY);
if (!fd) {
return Err(NS_ERROR_FILE_NOT_FOUND);
return Err(IOError(NS_ERROR_FILE_NOT_FOUND)
.WithMessage("Could not open the file at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
}
uint32_t bufSize;
if (aMaxBytes.isNothing()) {
// Limitation: We cannot read files that are larger than the max size of a
// TypedArray (UINT32_MAX bytes). Reject if the file is too big
// to be read.
// TypedArray (UINT32_MAX bytes). Reject if the file is too
// big to be read.
PRFileInfo64 info;
if (PR_FAILURE == PR_GetOpenFileInfo64(fd.get(), &info)) {
return Err(NS_ERROR_FILE_ACCESS_DENIED);
return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED)
.WithMessage("Could not get info for the file at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
}
if (static_cast<uint64_t>(info.size) > UINT32_MAX) {
return Err(NS_ERROR_FILE_TOO_BIG);
return Err(
IOError(NS_ERROR_FILE_TOO_BIG)
.WithMessage("Could not read the file at %s because it is too "
"large(size=%" PRIu64 " bytes)",
NS_ConvertUTF16toUTF8(aPath).get(),
static_cast<uint64_t>(info.size)));
}
bufSize = static_cast<uint32_t>(info.size);
} else {
@ -668,7 +598,7 @@ Result<nsTArray<uint8_t>, nsresult> IOUtils::ReadSync(
nsTArray<uint8_t> buffer;
if (!buffer.SetCapacity(bufSize, fallible)) {
return Err(NS_ERROR_OUT_OF_MEMORY);
return Err(IOError(NS_ERROR_OUT_OF_MEMORY));
}
// If possible, advise the operating system that we will be reading the file
@ -687,7 +617,10 @@ Result<nsTArray<uint8_t>, nsresult> IOUtils::ReadSync(
break;
}
if (nRead < 0) {
return Err(NS_ERROR_UNEXPECTED);
return Err(
IOError(NS_ERROR_UNEXPECTED)
.WithMessage("Encountered an unexpected error while reading %s",
NS_ConvertUTF16toUTF8(aPath).get()));
}
totalRead += nRead;
DebugOnly<bool> success = buffer.SetLength(totalRead, fallible);
@ -697,7 +630,7 @@ Result<nsTArray<uint8_t>, nsresult> IOUtils::ReadSync(
}
/* static */
Result<uint32_t, nsresult> IOUtils::WriteAtomicSync(
Result<uint32_t, IOUtils::IOError> IOUtils::WriteAtomicSync(
const nsAString& aDestPath, const nsTArray<uint8_t>& aByteArray,
const WriteAtomicOptions& aOptions) {
MOZ_ASSERT(!NS_IsMainThread());
@ -712,13 +645,22 @@ Result<uint32_t, nsresult> IOUtils::WriteAtomicSync(
}
if (noOverwrite && exists) {
return Err(NS_ERROR_FILE_ALREADY_EXISTS);
return Err(IOError(NS_ERROR_FILE_ALREADY_EXISTS)
.WithMessage("Refusing to overwrite the file at %s\n"
"Specify `noOverwrite: false` to allow "
"overwriting the destination",
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
// If backupFile was specified, perform the backup as a move.
if (exists && aOptions.mBackupFile.WasPassed() &&
MoveSync(aDestPath, aOptions.mBackupFile.Value(), noOverwrite).isErr()) {
return Err(NS_ERROR_FILE_COPY_OR_MOVE_FAILED);
return Err(
IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED)
.WithMessage(
"Failed to backup the source file(%s) to %s",
NS_ConvertUTF16toUTF8(aDestPath).get(),
NS_ConvertUTF16toUTF8(aOptions.mBackupFile.Value()).get()));
}
// If tmpPath was specified, we will write to there first, then perform
@ -746,12 +688,14 @@ Result<uint32_t, nsresult> IOUtils::WriteAtomicSync(
fd = CreateFileSync(tmpPath, flags);
}
if (!fd) {
return Err(NS_ERROR_FILE_ACCESS_DENIED);
return Err(IOError(NS_ERROR_FILE_ACCESS_DENIED)
.WithMessage("Could not open the file at %s for writing",
NS_ConvertUTF16toUTF8(tmpPath).get()));
}
auto rv = WriteSync(fd.get(), aByteArray);
auto rv = WriteSync(fd.get(), NS_ConvertUTF16toUTF8(tmpPath), aByteArray);
if (rv.isErr()) {
return rv;
return rv.propagateErr();
}
result = rv.unwrap();
}
@ -759,15 +703,20 @@ Result<uint32_t, nsresult> IOUtils::WriteAtomicSync(
// If tmpPath was specified and different from the destPath, then the
// operation is finished by performing a move.
if (aDestPath != tmpPath && MoveSync(tmpPath, aDestPath, false).isErr()) {
return Err(NS_ERROR_FILE_COPY_OR_MOVE_FAILED);
return Err(
IOError(NS_ERROR_FILE_COPY_OR_MOVE_FAILED)
.WithMessage("Could not move temporary file(%s) to destination(%s)",
NS_ConvertUTF16toUTF8(tmpPath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
return result;
}
/* static */
Result<uint32_t, nsresult> IOUtils::WriteSync(PRFileDesc* aFd,
const nsTArray<uint8_t>& aBytes) {
// aBytes comes from a JavaScript TypedArray, which has UINT32_MAX max length.
Result<uint32_t, IOUtils::IOError> IOUtils::WriteSync(
PRFileDesc* aFd, const nsACString& aPath, const nsTArray<uint8_t>& aBytes) {
// aBytes comes from a JavaScript TypedArray, which has UINT32_MAX max
// length.
MOZ_ASSERT(aBytes.Length() <= UINT32_MAX);
MOZ_ASSERT(!NS_IsMainThread());
@ -788,7 +737,11 @@ Result<uint32_t, nsresult> IOUtils::WriteSync(PRFileDesc* aFd,
}
int32_t rv = PR_Write(aFd, aBytes.Elements() + bytesWritten, chunkSize);
if (rv < 0) {
return Err(NS_ERROR_FILE_CORRUPTED);
return Err(IOError(NS_ERROR_FILE_CORRUPTED)
.WithMessage("Could not write chunk(size=%" PRIu32
") to %s\n"
"The file may be corrupt",
chunkSize, aPath.BeginReading()));
}
pendingBytes -= rv;
bytesWritten += rv;
@ -798,25 +751,38 @@ Result<uint32_t, nsresult> IOUtils::WriteSync(PRFileDesc* aFd,
}
/* static */
Result<Ok, nsresult> IOUtils::MoveSync(const nsAString& aSourcePath,
const nsAString& aDestPath,
bool noOverwrite) {
Result<Ok, IOUtils::IOError> IOUtils::MoveSync(const nsAString& aSourcePath,
const nsAString& aDestPath,
bool noOverwrite) {
MOZ_ASSERT(!NS_IsMainThread());
nsresult rv = NS_OK;
// Assess the source file.
nsCOMPtr<nsIFile> srcFile = new nsLocalFile();
MOZ_TRY(srcFile->InitWithPath(aSourcePath)); // Fails if not absolute.
MOZ_TRY(srcFile->Normalize()); // Fails if path does not exist.
bool srcExists;
// Ensure the source file exists before continuing. If it doesn't exist, the
// following operations can fail in different ways on different platforms.
MOZ_TRY(srcFile->Exists(&srcExists));
if (!srcExists) {
return Err(
IOError(NS_ERROR_FILE_NOT_FOUND)
.WithMessage(
"Could not move source file(%s) because it does not exist",
NS_ConvertUTF16toUTF8(aSourcePath).get()));
}
MOZ_TRY(srcFile->Normalize());
// Prepare the destination file.
nsCOMPtr<nsIFile> destFile = new nsLocalFile();
MOZ_TRY(destFile->InitWithPath(aDestPath));
MOZ_TRY(destFile->InitWithPath(aDestPath)); // Fails if not absolute.
rv = destFile->Normalize();
// Normalize can fail for a number of reasons, including if the file doesn't
// exist. It is expected that the file might not exist for a number of calls
// (e.g. if we want to rename a file to a new location).
if (NS_FAILED(rv) && !IsFileNotFound(rv)) {
// Deliberately ignore "not found" errors, but propagate all others.
return Err(rv);
return Err(IOError(rv));
}
// Case 1: Destination is an existing directory. Move source into dest.
@ -825,21 +791,38 @@ Result<Ok, nsresult> IOUtils::MoveSync(const nsAString& aSourcePath,
rv = destFile->IsDirectory(&destIsDir);
if (NS_SUCCEEDED(rv) && destIsDir) {
return ToResult(srcFile->MoveTo(destFile, EmptyString()));
rv = srcFile->MoveTo(destFile, EmptyString());
if (NS_FAILED(rv)) {
return Err(IOError(rv).WithMessage(
"Could not move source file(%s) to destination(%s)",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
return Ok();
}
if (NS_FAILED(rv) && IsFileNotFound(rv)) {
// It's ok if the file doesn't exist. Case 2 handles this below.
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));
}
destExists = false;
} else if (NS_FAILED(rv)) {
// Bail out early for any other kind of error though.
return Err(rv);
}
// Case 2: Destination is a file which may or may not exist. Try to rename the
// source to the destination. This will fail if the source is a not a
// regular file.
// source to the destination. If the destination exists and the source
// is not a regular file, then this may fail.
if (noOverwrite && destExists) {
return Err(NS_ERROR_FILE_ALREADY_EXISTS);
return Err(
IOError(NS_ERROR_FILE_ALREADY_EXISTS)
.WithMessage(
"Could not move source file(%s) to destination(%s) because the "
"destination already exists and overwrites are not allowed\n"
"Specify the `noOverwrite: false` option to mitigate this "
"error",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
if (destExists && !destIsDir) {
// If the source file is a directory, but the target is a file, abort early.
@ -848,7 +831,12 @@ Result<Ok, nsresult> IOUtils::MoveSync(const nsAString& aSourcePath,
bool srcIsDir = false;
MOZ_TRY(srcFile->IsDirectory(&srcIsDir));
if (srcIsDir) {
return Err(NS_ERROR_FILE_DESTINATION_NOT_DIR);
return Err(IOError(NS_ERROR_FILE_DESTINATION_NOT_DIR)
.WithMessage("Could not move the source directory(%s) to "
"the destination(%s) "
"because the destination is not a directory",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
}
@ -858,12 +846,20 @@ Result<Ok, nsresult> IOUtils::MoveSync(const nsAString& aSourcePath,
MOZ_TRY(destFile->GetParent(getter_AddRefs(destDir)));
// NB: if destDir doesn't exist, then MoveTo will create it.
return ToResult(srcFile->MoveTo(destDir, destName));
rv = srcFile->MoveTo(destDir, destName);
if (NS_FAILED(rv)) {
return Err(IOError(rv).WithMessage(
"Could not move the source file(%s) to the destination(%s)",
NS_ConvertUTF16toUTF8(aSourcePath).get(),
NS_ConvertUTF16toUTF8(aDestPath).get()));
}
return Ok();
}
/* static */
Result<Ok, nsresult> IOUtils::RemoveSync(const nsAString& aPath,
bool aIgnoreAbsent, bool aRecursive) {
Result<Ok, IOUtils::IOError> IOUtils::RemoveSync(const nsAString& aPath,
bool aIgnoreAbsent,
bool aRecursive) {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<nsLocalFile> file = new nsLocalFile();
@ -873,51 +869,88 @@ Result<Ok, nsresult> IOUtils::RemoveSync(const nsAString& aPath,
if (aIgnoreAbsent && IsFileNotFound(rv)) {
return Ok();
}
return ToResult(rv);
if (NS_FAILED(rv)) {
IOError err(rv);
if (IsFileNotFound(rv)) {
return Err(err.WithMessage(
"Could not remove the file at %s because it does not exist.\n"
"Specify the `ignoreAbsent: true` option to mitigate this error",
NS_ConvertUTF16toUTF8(aPath).get()));
}
if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY) {
return Err(err.WithMessage(
"Could not remove the non-empty directory at %s.\n"
"Specify the `recursive: true` option to mitigate this error",
NS_ConvertUTF16toUTF8(aPath).get()));
}
return Err(err.WithMessage("Could not remove the file at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
}
return Ok();
}
/* static */
Result<Ok, nsresult> IOUtils::CreateDirectorySync(const nsAString& aPath,
bool aCreateAncestors,
bool aIgnoreExisting,
int32_t aMode) {
Result<Ok, IOUtils::IOError> IOUtils::CreateDirectorySync(
const nsAString& aPath, bool aCreateAncestors, bool aIgnoreExisting,
int32_t aMode) {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<nsLocalFile> targetFile = new nsLocalFile();
MOZ_TRY(targetFile->InitWithPath(aPath));
// nsIFile::Create will create ancestor directories by default.
// If the caller does not want this behaviour, then check and possibly return
// an error.
// If the caller does not want this behaviour, then check and possibly
// return an error.
if (!aCreateAncestors) {
nsCOMPtr<nsIFile> parent;
MOZ_TRY(targetFile->GetParent(getter_AddRefs(parent)));
bool parentExists;
MOZ_TRY(parent->Exists(&parentExists));
if (!parentExists) {
return Err(NS_ERROR_FILE_NOT_FOUND);
return Err(IOError(NS_ERROR_FILE_NOT_FOUND)
.WithMessage("Could not create directory at %s because "
"the path has missing "
"ancestor components",
NS_ConvertUTF16toUTF8(aPath).get()));
}
}
nsresult rv = targetFile->Create(nsIFile::DIRECTORY_TYPE, aMode);
if (NS_FAILED(rv) && rv == NS_ERROR_FILE_ALREADY_EXISTS) {
// NB: We may report a success only if the target is an existing directory.
// We don't want to silence errors that occur if the target is an existing
// file, since trying to create a directory where a regular file exists
// may be indicative of a logic error.
bool isDirectory;
MOZ_TRY(targetFile->IsDirectory(&isDirectory));
if (!isDirectory) {
return Err(NS_ERROR_FILE_NOT_DIRECTORY);
}
if (aIgnoreExisting) {
return Ok();
if (NS_FAILED(rv)) {
IOError err(rv);
if (rv == NS_ERROR_FILE_ALREADY_EXISTS) {
// NB: We may report a success only if the target is an existing
// directory. We don't want to silence errors that occur if the target is
// an existing file, since trying to create a directory where a regular
// file exists may be indicative of a logic error.
bool isDirectory;
MOZ_TRY(targetFile->IsDirectory(&isDirectory));
if (!isDirectory) {
return Err(IOError(NS_ERROR_FILE_NOT_DIRECTORY)
.WithMessage("Could not create directory because the "
"target file(%s) exists "
"and is not a directory",
NS_ConvertUTF16toUTF8(aPath).get()));
}
// The directory exists.
// The caller may suppress this error.
if (aIgnoreExisting) {
return Ok();
}
// Otherwise, forward it.
return Err(err.WithMessage(
"Could not create directory because it already exists at %s\n"
"Specify the `ignoreExisting: true` option to mitigate this "
"error",
NS_ConvertUTF16toUTF8(aPath).get()));
}
return Err(err.WithMessage("Could not create directory at %s",
NS_ConvertUTF16toUTF8(aPath).get()));
}
return ToResult(rv);
return Ok();
}
Result<IOUtils::InternalFileInfo, nsresult> IOUtils::StatSync(
Result<IOUtils::InternalFileInfo, IOUtils::IOError> IOUtils::StatSync(
const nsAString& aPath) {
MOZ_ASSERT(!NS_IsMainThread());
@ -927,9 +960,23 @@ Result<IOUtils::InternalFileInfo, nsresult> IOUtils::StatSync(
InternalFileInfo info;
info.mPath = nsString(aPath);
info.mType = FileType::Regular;
bool isRegular;
MOZ_TRY(file->IsFile(&isRegular));
// IsFile will stat and cache info in the file object. If the file doesn't
// exist, or there is an access error, we'll discover it here.
// Any subsequent errors are unexpected and will just be forwarded.
nsresult rv = file->IsFile(&isRegular);
if (NS_FAILED(rv)) {
IOError err(rv);
if (IsFileNotFound(rv)) {
return Err(
err.WithMessage("Could not stat file(%s) because it does not exist",
NS_ConvertUTF16toUTF8(aPath).get()));
}
return Err(err);
}
// Now we can populate the info object by querying the file.
info.mType = FileType::Regular;
if (!isRegular) {
bool isDir;
MOZ_TRY(file->IsDirectory(&isDir));

Просмотреть файл

@ -12,12 +12,14 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/IOUtilsBinding.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/MozPromise.h"
#include "mozilla/Result.h"
#include "nspr/prio.h"
#include "nsIAsyncShutdown.h"
#include "nsISerialEventTarget.h"
#include "nsLocalFile.h"
#include "nsPrintfCString.h"
#include "nsString.h"
namespace mozilla {
@ -76,6 +78,8 @@ class IOUtils final {
friend class IOUtilsShutdownBlocker;
struct InternalFileInfo;
class IOError;
static StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
sBackgroundEventTarget;
static StaticRefPtr<nsIAsyncShutdownClient> sBarrier;
@ -99,6 +103,12 @@ class IOUtils final {
const InternalFileInfo& aInternalFileInfo,
JS::MutableHandle<JS::Value> aValue);
/**
* Rejects |aPromise| with an appropriate |DOMException| describing |aError|.
*/
static void RejectJSPromise(const RefPtr<Promise>& aPromise,
const IOError& aError);
/**
* Opens an existing file at |aPath|.
*
@ -133,7 +143,7 @@ class IOUtils final {
*
* @return A byte array of the entire file contents, or an error.
*/
static Result<nsTArray<uint8_t>, nsresult> ReadSync(
static Result<nsTArray<uint8_t>, IOError> ReadSync(
const nsAString& aPath, const Maybe<uint32_t>& aMaxBytes);
/**
@ -148,7 +158,7 @@ class IOUtils final {
* @return The number of bytes written to the file, or an error if the write
* failed or was incomplete.
*/
static Result<uint32_t, nsresult> WriteAtomicSync(
static Result<uint32_t, IOError> WriteAtomicSync(
const nsAString& aDestPath, const nsTArray<uint8_t>& aByteArray,
const WriteAtomicOptions& aOptions);
@ -162,8 +172,9 @@ class IOUtils final {
* @return The number of bytes written to the file, or an error if the write
* failed or was incomplete.
*/
static Result<uint32_t, nsresult> WriteSync(PRFileDesc* aFd,
const nsTArray<uint8_t>& aBytes);
static Result<uint32_t, IOError> WriteSync(PRFileDesc* aFd,
const nsACString& aPath,
const nsTArray<uint8_t>& aBytes);
/**
* Attempts to move the file located at |aSource| to |aDest|.
@ -177,9 +188,8 @@ class IOUtils final {
*
* @return Ok if the file was moved successfully, or an error.
*/
static Result<Ok, nsresult> MoveSync(const nsAString& aSource,
const nsAString& aDest,
bool noOverwrite);
static Result<Ok, IOError> MoveSync(const nsAString& aSource,
const nsAString& aDest, bool noOverwrite);
/**
* Attempts to remove the file located at |aPath|.
@ -192,8 +202,8 @@ class IOUtils final {
*
* @return Ok if the file was removed successfully, or an error.
*/
static Result<Ok, nsresult> RemoveSync(const nsAString& aPath,
bool aIgnoreAbsent, bool aRecursive);
static Result<Ok, IOError> RemoveSync(const nsAString& aPath,
bool aIgnoreAbsent, bool aRecursive);
/**
* Attempts to create a new directory at |aPath|.
@ -212,10 +222,10 @@ class IOUtils final {
*
* @return Ok if the directory was created successfully, or an error.
*/
static Result<Ok, nsresult> CreateDirectorySync(const nsAString& aPath,
bool aCreateAncestors,
bool aIgnoreExisting,
int32_t aMode = 0777);
static Result<Ok, IOError> CreateDirectorySync(const nsAString& aPath,
bool aCreateAncestors,
bool aIgnoreExisting,
int32_t aMode = 0777);
/**
* Attempts to stat a file at |aPath|.
@ -224,24 +234,70 @@ class IOUtils final {
*
* @return An |InternalFileInfo| struct if successful, or an error.
*/
static Result<IOUtils::InternalFileInfo, nsresult> StatSync(
static Result<IOUtils::InternalFileInfo, IOError> StatSync(
const nsAString& aPath);
using IOReadMozPromise =
mozilla::MozPromise<nsTArray<uint8_t>, const nsresult,
/* IsExclusive */ true>;
using IOReadMozPromise = mozilla::MozPromise<nsTArray<uint8_t>, const IOError,
/* IsExclusive */ true>;
using IOWriteMozPromise =
mozilla::MozPromise<uint32_t, const nsresult, /* IsExclusive */ true>;
mozilla::MozPromise<uint32_t, const IOError, /* IsExclusive */ true>;
using IOStatMozPromise =
mozilla::MozPromise<struct InternalFileInfo, const nsresult,
mozilla::MozPromise<struct InternalFileInfo, const IOError,
/* IsExclusive */ true>;
using IOMozPromise = mozilla::MozPromise<Ok /* ignored */, const nsresult,
using IOMozPromise = mozilla::MozPromise<Ok /* ignored */, const IOError,
/* IsExclusive */ true>;
};
/**
* An error class used with the |Result| type returned by most private |IOUtils|
* methods.
*/
class IOUtils::IOError {
public:
MOZ_IMPLICIT IOError(nsresult aCode) : mCode(aCode), mMessage(Nothing()) {}
/**
* Replaces the message associated with this error.
*/
template <typename... Args>
IOError WithMessage(const char* const aMessage, Args... aArgs) {
mMessage.emplace(nsPrintfCString(aMessage, aArgs...));
return *this;
}
IOError WithMessage(const char* const aMessage) {
mMessage.emplace(nsCString(aMessage));
return *this;
}
IOError WithMessage(nsCString aMessage) {
mMessage.emplace(aMessage);
return *this;
}
/**
* Returns the |nsresult| associated with this error.
*/
nsresult Code() const { return mCode; }
/**
* Maybe returns a message associated with this error.
*/
const Maybe<nsCString>& Message() const { return mMessage; }
using IOStatMozPromise =
mozilla::MozPromise<struct InternalFileInfo, const IOError,
/* IsExclusive */ true>;
using IOMozPromise = mozilla::MozPromise<Ok /* ignored */, const IOError,
/* IsExclusive */ true>;
private:
nsresult mCode;
Maybe<nsCString> mMessage;
};
/**
* This is an easier to work with representation of a |mozilla::dom::FileInfo|
* for private use in the IOUtils implementation.

Просмотреть файл

@ -34,7 +34,7 @@
const doesNotExist = OS.Path.join(tmpDir, "does_not_exist.tmp");
await Assert.rejects(
window.IOUtils.read(doesNotExist),
/Could not open file/,
/Could not open the file at .*/,
"IOUtils::read rejects when file does not exist"
);
});
@ -56,7 +56,7 @@
);
await Assert.rejects(
window.IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: false }),
/Could not create directory because file already exists/,
/Could not create directory because it already exists at .*/,
"IOUtils::makeDirectory can throw if the target dir exists"
)
@ -68,7 +68,7 @@
);
await Assert.rejects(
window.IOUtils.makeDirectory(nestedDirName, { createAncestors: false }),
/Target path has missing ancestors/,
/Could not create directory at .* because the path has missing ancestor components/,
"IOUtils::makeDirectory can fail if the target is missing parents"
);
ok(!await OS.File.exists(nestedDirName), `Expected ${nestedDirName} not to exist`);
@ -88,14 +88,14 @@
await Assert.rejects(
window.IOUtils.makeDirectory(notADirFileName, { ignoreExisting: false }),
/Target exists and is not a directory/,
/Could not create directory because the target file\(.*\) exists and is not a directory/,
"IOUtils::makeDirectory [ignoreExisting: false] throws when the target is an existing file"
);
ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`);
await Assert.rejects(
window.IOUtils.makeDirectory(notADirFileName, { ignoreExisting: true }),
/Target exists and is not a directory/,
/Could not create directory because the target file\(.*\) exists and is not a directory/,
"IOUtils::makeDirectory [ignoreExisting: true] throws when the target is an existing file"
);
ok(await fileExists(notADirFileName), `Expected ${notADirFileName} to exist`);
@ -130,7 +130,7 @@
await Assert.rejects(
window.IOUtils.remove(tmpFileName, { ignoreAbsent: false }),
/Target file does not exist/,
/Could not remove the file at .* because it does not exist/,
"IOUtils::remove can throw an error when target file is missing"
);
ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`);
@ -156,7 +156,7 @@
await Assert.rejects(
window.IOUtils.remove(tmpParentDir, { recursive: false }),
/Could not remove non-empty directory.*/,
/Could not remove the non-empty directory at .*/,
"IOUtils::remove fails if non-recursively removing directory with contents"
);
@ -444,7 +444,7 @@
// Test.
await Assert.rejects(
window.IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }),
/Destination file exists and overwrites are not allowed/,
/Could not move source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/,
"IOUtils::move will refuse to move a file if overwrites are disabled"
);
ok(
@ -549,7 +549,7 @@
// Test.
await Assert.rejects(
window.IOUtils.move(notExistsSrc, notExistsDest),
/Source file does not exist/,
/Could not move source file\(.*\) because it does not exist/,
"IOUtils::move throws if source file does not exist"
);
ok(
@ -566,7 +566,7 @@
// Test.
await Assert.rejects(
window.IOUtils.move(srcDir, destFile),
/Source is a directory but destination is not/,
/Could not move the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/,
"IOUtils::move throws if try to move dir into an existing file"
);
@ -627,7 +627,7 @@
await Assert.rejects(
window.IOUtils.stat(notExistsFile),
/Target file does not exist/,
/Could not stat file\(.*\) because it does not exist/,
"IOUtils::stat throws if the target file does not exist"
);
});