Bug 1642454: Implement IOUtils read and writeAtomic methods r=barret,smaug,Gijs

This patch introduces a minimal, asynchronous Web IDL interface for
reading/writing whole files in privileged chrome code (main-thread and web
workers). All I/O is performed on a background thread. Pending I/O blocks
Firefox shutdown.

Differential Revision: https://phabricator.services.mozilla.com/D78134
This commit is contained in:
Keefer Rourke 2020-07-02 22:32:03 +00:00
Родитель df2678e18f
Коммит 16fbac6f70
9 изменённых файлов: 850 добавлений и 2 удалений

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

@ -0,0 +1,54 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*/
[ChromeOnly, Exposed=(Window, Worker)]
namespace IOUtils {
/**
* Reads up to |maxBytes| of the file at |path|. If |maxBytes| is unspecified,
* the entire file is read.
*
* @param path A forward-slash separated path.
* @param maxBytes The max bytes to read from the file at path.
*/
Promise<Uint8Array> read(DOMString path, optional unsigned long maxBytes);
/**
* Atomically write |data| to a file at |path|. This operation attempts to
* ensure that until the data is entirely written to disk, the destination
* file is not modified.
*
* This is generally accomplished by writing to a temporary file, then
* performing an overwriting move.
*
* @param path A forward-slash separated path.
* @param data Data to write to the file at path.
*/
Promise<unsigned long long> writeAtomic(DOMString path, Uint8Array data, optional WriteAtomicOptions options = {});
};
/**
* Options to be passed to the |IOUtils.writeAtomic| method.
*/
dictionary WriteAtomicOptions {
/**
* If specified, backup the destination file to this path before writing.
*/
DOMString backupFile;
/**
* If specified, write the data to a file at |tmpPath|. Once the write is
* complete, the destination will be overwritten by a move.
*/
DOMString tmpPath;
/**
* If true, fail if the destination already exists.
*/
boolean noOverwrite = false;
/**
* If true, force the OS to write its internal buffers to the disk.
* This is considerably slower for the whole system, but safer in case of
* an improper system shutdown (e.g. due to a kernel panic) or device
* disconnection before the buffers are flushed.
*/
boolean flush = false;
};

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

@ -48,6 +48,7 @@ WEBIDL_FILES = [
'FrameLoader.webidl',
'HeapSnapshot.webidl',
'InspectorUtils.webidl',
'IOUtils.webidl',
'IteratorResult.webidl',
'JSActor.webidl',
'JSProcessActor.webidl',

433
dom/system/IOUtils.cpp Normal file
Просмотреть файл

@ -0,0 +1,433 @@
/* -*- 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 <algorithm>
#include "mozilla/dom/IOUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/Services.h"
#include "mozilla/Span.h"
#include "nspr/prio.h"
#include "nspr/private/pprio.h"
#include "nspr/prtypes.h"
#include "nsIGlobalObject.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsThreadManager.h"
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
# include <fcntl.h>
#endif
#define REJECT_IF_NULL_EVENT_TARGET(aEventTarget, aJSPromise) \
do { \
if (!(aEventTarget)) { \
(aJSPromise) \
->MaybeRejectWithAbortError( \
"Could not dispatch task to background thread"); \
return (aJSPromise).forget(); \
} \
} while (false)
#define REJECT_IF_SHUTTING_DOWN(aJSPromise) \
do { \
if (sShutdownStarted) { \
(aJSPromise) \
->MaybeRejectWithNotAllowedError( \
"Shutting down and refusing additional I/O tasks"); \
return (aJSPromise).forget(); \
} \
} while (false)
namespace mozilla {
namespace dom {
/* static */
StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
IOUtils::sBackgroundEventTarget("sBackgroundEventTarget");
/* static */
StaticRefPtr<nsIAsyncShutdownClient> IOUtils::sBarrier;
/* static */
Atomic<bool> IOUtils::sShutdownStarted = Atomic<bool>(false);
/* static */
already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
const nsAString& aPath,
const Optional<uint32_t>& aMaxBytes) {
RefPtr<Promise> promise = CreateJSPromise(aGlobal);
NS_ENSURE_TRUE(!!promise, nullptr);
REJECT_IF_SHUTTING_DOWN(promise);
// Process arguments.
uint32_t toRead = 0;
if (aMaxBytes.WasPassed()) {
toRead = aMaxBytes.Value();
if (toRead == 0) {
// Resolve with an empty buffer.
nsTArray<uint8_t> arr(0);
TypedArrayCreator<Uint8Array> arrCreator(arr);
promise->MaybeResolve(arrCreator);
return promise.forget();
}
}
NS_ConvertUTF16toUTF8 path(aPath);
// Do the IO on a background thread and return the result to this thread.
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
InvokeAsync(
bg, __func__,
[path, toRead]() {
MOZ_ASSERT(!NS_IsMainThread());
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(path.get(), PR_RDONLY);
if (!fd) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Could not open file"), __func__);
}
uint32_t bufSize;
if (toRead == 0) { // maxBytes was unspecified.
// 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.
PRFileInfo64 info;
if (PR_FAILURE == PR_GetOpenFileInfo64(fd.get(), &info)) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Could not get file info"), __func__);
}
uint32_t fileSize = info.size;
if (fileSize > UINT32_MAX) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("File is too large to read"), __func__);
}
bufSize = fileSize;
} else {
bufSize = toRead;
}
nsTArray<uint8_t> fileContents;
nsresult er = IOUtils::ReadSync(fd.get(), bufSize, fileContents);
if (NS_SUCCEEDED(er)) {
return IOReadMozPromise::CreateAndResolve(std::move(fileContents),
__func__);
}
if (er == NS_ERROR_OUT_OF_MEMORY) {
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Out of memory"), __func__);
}
return IOReadMozPromise::CreateAndReject(
nsLiteralCString("Unexpected error reading file"), __func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const nsTArray<uint8_t>& aBuf) {
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
promise->MaybeReject(NS_ERROR_UNEXPECTED);
return;
}
const TypedArrayCreator<Uint8Array> arrayCreator(aBuf);
promise->MaybeResolve(arrayCreator);
},
[promise = RefPtr(promise)](const nsACString& aMsg) {
promise->MaybeRejectWithOperationError(aMsg);
});
return promise.forget();
}
/* static */
already_AddRefed<Promise> IOUtils::WriteAtomic(
GlobalObject& aGlobal, const nsAString& aPath, const Uint8Array& aData,
const WriteAtomicOptions& aOptions) {
RefPtr<Promise> promise = CreateJSPromise(aGlobal);
NS_ENSURE_TRUE(!!promise, nullptr);
REJECT_IF_SHUTTING_DOWN(promise);
// Process arguments.
aData.ComputeState();
FallibleTArray<uint8_t> toWrite;
if (!toWrite.InsertElementsAt(0, aData.Data(), aData.Length(), fallible)) {
promise->MaybeRejectWithOperationError("Out of memory");
return promise.forget();
}
// TODO: Implement tmpPath and backupFile options.
// The data to be written to file might be larger than can be written in any
// single call, so we must truncate the file and set the write mode to append
// to the file.
int32_t flags = PR_WRONLY | PR_TRUNCATE | PR_APPEND;
bool noOverwrite = false;
if (aOptions.IsAnyMemberPresent()) {
if (aOptions.mBackupFile.WasPassed() || aOptions.mTmpPath.WasPassed()) {
promise->MaybeRejectWithNotSupportedError(
"Unsupported options were passed");
return promise.forget();
}
if (aOptions.mFlush) {
flags |= PR_SYNC;
}
noOverwrite = aOptions.mNoOverwrite;
}
NS_ConvertUTF16toUTF8 path(aPath);
// Do the IO on a background thread and return the result to this thread.
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
InvokeAsync(bg, __func__,
[path, flags, noOverwrite, toWrite = std::move(toWrite)]() {
MOZ_ASSERT(!NS_IsMainThread());
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
OpenExistingSync(path.get(), flags);
if (noOverwrite && fd) {
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Refusing to overwrite file"), __func__);
}
if (!fd) {
fd = CreateFileSync(path.get(), flags);
}
if (!fd) {
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Could not open file"), __func__);
}
uint32_t result;
nsresult er = IOUtils::WriteSync(fd.get(), toWrite, result);
if (NS_SUCCEEDED(er)) {
return IOWriteMozPromise::CreateAndResolve(result, __func__);
}
return IOWriteMozPromise::CreateAndReject(
nsLiteralCString("Unexpected error writing file"),
__func__);
})
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise = RefPtr(promise)](const uint32_t& aBytesWritten) {
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
promise->MaybeReject(NS_ERROR_UNEXPECTED);
return;
}
promise->MaybeResolve(aBytesWritten);
},
[promise = RefPtr(promise)](const nsACString& aMsg) {
promise->MaybeRejectWithOperationError(aMsg);
});
return promise.forget();
}
/* static */
already_AddRefed<nsISerialEventTarget> IOUtils::GetBackgroundEventTarget() {
if (sShutdownStarted) {
return nullptr;
}
auto lockedBackgroundEventTarget = sBackgroundEventTarget.Lock();
if (!lockedBackgroundEventTarget.ref()) {
RefPtr<nsISerialEventTarget> et;
MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
"IOUtils::BackgroundIOThread", getter_AddRefs(et)));
MOZ_ASSERT(et);
*lockedBackgroundEventTarget = et;
if (NS_IsMainThread()) {
IOUtils::SetShutdownHooks();
} else {
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
__func__, []() { IOUtils::SetShutdownHooks(); });
NS_DispatchToMainThread(runnable.forget());
}
}
return do_AddRef(*lockedBackgroundEventTarget);
}
/* static */
already_AddRefed<nsIAsyncShutdownClient> IOUtils::GetShutdownBarrier() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
if (!sBarrier) {
nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
MOZ_ASSERT(svc);
nsCOMPtr<nsIAsyncShutdownClient> barrier;
nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
NS_ENSURE_SUCCESS(rv, nullptr);
sBarrier = barrier;
}
return do_AddRef(sBarrier);
}
/* static */
void IOUtils::SetShutdownHooks() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new IOUtilsShutdownBlocker();
nsresult rv = barrier->AddBlocker(
blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
u"IOUtils: waiting for pending I/O to finish"_ns);
// Adding a new shutdown blocker should only fail if the current shutdown
// phase has completed. Ensure that we have set our shutdown flag to stop
// accepting new I/O tasks in this case.
if (NS_FAILED(rv)) {
sShutdownStarted = true;
}
NS_ENSURE_SUCCESS_VOID(rv);
}
/* static */
already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) {
ErrorResult er;
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<Promise> promise = Promise::Create(global, er);
if (er.Failed()) {
return nullptr;
}
MOZ_ASSERT(promise);
return do_AddRef(promise);
}
/* static */
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::OpenExistingSync(
const char* aPath, int32_t aFlags) {
// Ensure that CREATE_FILE and EXCL flags were not included, as we do not
// want to create a new file.
MOZ_ASSERT((aFlags & (PR_CREATE_FILE | PR_EXCL)) == 0);
return UniquePtr<PRFileDesc, PR_CloseDelete>(PR_Open(aPath, aFlags, 0));
}
/* static */
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::CreateFileSync(const char* aPath,
int32_t aFlags,
int32_t aMode) {
return UniquePtr<PRFileDesc, PR_CloseDelete>(
PR_Open(aPath, aFlags | PR_CREATE_FILE | PR_EXCL, aMode));
}
/* static */
nsresult IOUtils::ReadSync(PRFileDesc* aFd, const uint32_t aBufSize,
nsTArray<uint8_t>& aResult) {
MOZ_ASSERT(!NS_IsMainThread());
nsTArray<uint8_t> buffer;
if (!buffer.SetCapacity(aBufSize, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
// If possible, advise the operating system that we will be reading the file
// pointed to by |aFD| sequentially, in full. This advice is not binding, it
// informs the OS about our expectations as an application.
#if defined(HAVE_POSIX_FADVISE)
posix_fadvise(PR_FileDesc2NativeHandle(aFd), 0, 0, POSIX_FADV_SEQUENTIAL);
#endif
uint32_t totalRead = 0;
while (totalRead != aBufSize) {
int32_t nRead =
PR_Read(aFd, buffer.Elements() + totalRead, aBufSize - totalRead);
if (nRead == 0) {
break;
}
if (nRead < 0) {
return NS_ERROR_UNEXPECTED;
}
totalRead += nRead;
DebugOnly<bool> success = buffer.SetLength(totalRead, fallible);
MOZ_ASSERT(success);
}
aResult = std::move(buffer);
return NS_OK;
}
/* static */
nsresult IOUtils::WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
uint32_t& aResult) {
// aBytes comes from a JavaScript TypedArray, which has UINT32_MAX max length.
MOZ_ASSERT(aBytes.Length() <= UINT32_MAX);
MOZ_ASSERT(!NS_IsMainThread());
if (aBytes.Length() == 0) {
aResult = 0;
return NS_OK;
}
uint32_t bytesWritten = 0;
// PR_Write can only write up to PR_INT32_MAX bytes at a time, but the data
// source might be as large as UINT32_MAX bytes.
uint32_t chunkSize = PR_INT32_MAX;
uint32_t pendingBytes = aBytes.Length();
while (pendingBytes > 0) {
if (pendingBytes < chunkSize) {
chunkSize = pendingBytes;
}
int32_t rv = PR_Write(aFd, aBytes.Elements() + bytesWritten, chunkSize);
if (rv < 0) {
return NS_ERROR_FILE_CORRUPTED;
}
pendingBytes -= rv;
bytesWritten += rv;
}
aResult = bytesWritten;
return NS_OK;
}
NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker);
NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
aName = u"IOUtils Blocker"_ns;
return NS_OK;
}
NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
nsIAsyncShutdownClient* aBarrierClient) {
nsCOMPtr<nsISerialEventTarget> et = IOUtils::GetBackgroundEventTarget();
IOUtils::sShutdownStarted = true;
if (!IOUtils::sBarrier) {
return NS_ERROR_NULL_POINTER;
}
nsCOMPtr<nsIRunnable> backgroundRunnable =
NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() {
nsCOMPtr<nsIRunnable> mainThreadRunnable =
NS_NewRunnableFunction(__func__, [self = RefPtr(self)]() {
IOUtils::sBarrier->RemoveBlocker(self);
auto lockedBackgroundET = IOUtils::sBackgroundEventTarget.Lock();
*lockedBackgroundET = nullptr;
IOUtils::sBarrier = nullptr;
});
nsresult er = NS_DispatchToMainThread(mainThreadRunnable.forget());
NS_ENSURE_SUCCESS_VOID(er);
});
return et->Dispatch(backgroundRunnable.forget(),
nsIEventTarget::DISPATCH_NORMAL);
}
NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) {
return NS_OK;
}
} // namespace dom
} // namespace mozilla
#undef REJECT_IF_NULL_EVENT_TARGET
#undef REJECT_IF_SHUTTING_DOWN

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

@ -7,7 +7,16 @@
#ifndef mozilla_dom_IOUtils__
#define mozilla_dom_IOUtils__
#include <prio.h>
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/DataMutex.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/IOUtilsBinding.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/MozPromise.h"
#include "nspr/prio.h"
#include "nsIAsyncShutdown.h"
#include "nsISerialEventTarget.h"
namespace mozilla {
@ -30,6 +39,81 @@ class PR_CloseDelete {
void operator()(PRFileDesc* aPtr) const { PR_Close(aPtr); }
};
namespace dom {
class IOUtils final {
public:
static already_AddRefed<Promise> Read(GlobalObject& aGlobal,
const nsAString& aPath,
const Optional<uint32_t>& aMaxBytes);
static already_AddRefed<Promise> WriteAtomic(
GlobalObject& aGlobal, const nsAString& aPath, const Uint8Array& aData,
const WriteAtomicOptions& aOptions);
private:
~IOUtils() = default;
friend class IOUtilsShutdownBlocker;
static StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
sBackgroundEventTarget;
static StaticRefPtr<nsIAsyncShutdownClient> sBarrier;
static Atomic<bool> sShutdownStarted;
static already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier();
static already_AddRefed<nsISerialEventTarget> GetBackgroundEventTarget();
static void SetShutdownHooks();
static already_AddRefed<Promise> CreateJSPromise(GlobalObject& aGlobal);
/**
* Opens an existing file at |path|.
*
* @param path The location of the file as a unix-style UTF-8 path string.
* @param flags PRIO flags, excluding |PR_CREATE| and |PR_EXCL|.
*/
static UniquePtr<PRFileDesc, PR_CloseDelete> OpenExistingSync(
const char* aPath, int32_t aFlags);
/**
* Creates a new file at |path|.
*
* @param aPath The location of the file as a unix-style UTF-8 path string.
* @param aFlags PRIO flags to be used in addition to |PR_CREATE| and
* |PR_EXCL|.
* @param aMode Optional file mode. Defaults to 0666 to allow the system
* umask to compute the best mode for the new file.
*/
static UniquePtr<PRFileDesc, PR_CloseDelete> CreateFileSync(
const char* aPath, int32_t aFlags, int32_t aMode = 0666);
static nsresult ReadSync(PRFileDesc* aFd, const uint32_t aBufSize,
nsTArray<uint8_t>& aResult);
static nsresult WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
uint32_t& aResult);
using IOReadMozPromise =
mozilla::MozPromise<nsTArray<uint8_t>, const nsCString,
/* IsExclusive */ true>;
using IOWriteMozPromise =
mozilla::MozPromise<uint32_t, const nsCString, /* IsExclusive */ true>;
};
class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIASYNCSHUTDOWNBLOCKER
private:
virtual ~IOUtilsShutdownBlocker() = default;
};
} // namespace dom
} // namespace mozilla
#endif

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

@ -78,6 +78,7 @@ EXPORTS.mozilla.dom += [
]
UNIFIED_SOURCES += [
'IOUtils.cpp',
'nsDeviceSensors.cpp',
'nsOSPermissionRequestBase.cpp',
'OSFileConstants.cpp',

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

@ -1,4 +1,8 @@
[DEFAULT]
support-files = worker_constants.js
support-files =
worker_constants.js
file_ioutils_worker.js
[test_constants.xhtml]
[test_ioutils.html]
[test_ioutils_worker.xhtml]

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

@ -0,0 +1,66 @@
// Any copyright is dedicated to the Public Domain.
// - http://creativecommons.org/publicdomain/zero/1.0/
/* eslint-env mozilla/chrome-worker, node */
/* global finish, log*/
"use strict";
importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
importScripts("resource://gre/modules/ObjectUtils.jsm");
// TODO: Remove this import for OS.File. It is currently being used as a
// stop gap for missing IOUtils functionality.
importScripts("resource://gre/modules/osfile.jsm");
self.onmessage = async function(msg) {
// IOUtils functionality is the same when called from the main thread, or a
// web worker. These tests are a modified subset of the main thread tests, and
// serve as a sanity check that the implementation is thread-safe.
await test_api_is_available_on_worker();
await test_full_read_and_write();
finish();
info("test_ioutils_worker.xhtml: Test finished");
async function test_api_is_available_on_worker() {
ok(self.IOUtils, "IOUtils is present in web workers");
}
async function test_full_read_and_write() {
// Write a file.
const tmpFileName = "test_ioutils_numbers.tmp";
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await self.IOUtils.writeAtomic(tmpFileName, bytes);
is(
bytesWritten,
50,
"IOUtils::writeAtomic can write entire byte array to file"
);
// Read it back.
let fileContents = await self.IOUtils.read(tmpFileName);
ok(
ObjectUtils.deepEqual(bytes, fileContents) &&
bytes.length == fileContents.length,
"IOUtils::read can read back entire file"
);
const tooManyBytes = bytes.length + 1;
fileContents = await self.IOUtils.read(tmpFileName, tooManyBytes);
ok(
ObjectUtils.deepEqual(bytes, fileContents) &&
fileContents.length == bytes.length,
"IOUtils::read can read entire file when requested maxBytes is too large"
);
cleanup(tmpFileName);
}
};
function cleanup(...files) {
files.forEach(file => {
OS.File.remove(file);
ok(!OS.File.exists(file), `Removed temporary file: ${file}`);
});
}

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

@ -0,0 +1,165 @@
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test the IOUtils file I/O API</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<!---
This implementation is compared against an already well-tested reference
implementation of File I/0.
-->
<script src="resource://gre/modules/FileTestUtils.jsm"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script>
"use strict";
const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
// TODO: Remove this import for OS.File. It is currently being used as a
// stop gap for missing IOUtils functionality.
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
add_task(async function test_api_is_available_on_window() {
ok(window.IOUtils, "IOUtils is present on the window");
});
add_task(async function test_read_failure() {
await Assert.rejects(
window.IOUtils.read("does_not_exist.txt"),
/Could not open file/,
"IOUtils::read rejects when file does not exist"
);
});
add_task(async function test_write_no_overwrite() {
// Make a new file, and try to write to it with overwrites disabled.
const tmpFileName = "test_ioutils_overwrite.tmp";
const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
await window.IOUtils.writeAtomic(tmpFileName, untouchableContents);
const newContents = new TextEncoder().encode("Nah nah nah!\n");
await Assert.rejects(
window.IOUtils.writeAtomic(tmpFileName, newContents, {
noOverwrite: true,
}),
/Refusing to overwrite file/,
"IOUtils::writeAtomic rejects writing to existing file if overwrites are disabled"
);
const bytesWritten = await window.IOUtils.writeAtomic(
tmpFileName,
newContents,
{ noOverwrite: false /* Default. */ }
);
is(
bytesWritten,
newContents.length,
"IOUtils::writeAtomic can overwrite files if specified"
);
await cleanup(tmpFileName);
});
add_task(async function test_partial_read() {
const tmpFileName = "test_ioutils_partial_read.tmp";
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
is(
bytesWritten,
50,
"IOUtils::writeAtomic can write entire byte array to file"
);
// Read just the first 10 bytes.
const first10 = bytes.slice(0, 10);
const bytes10 = await window.IOUtils.read(tmpFileName, 10);
ok(
ObjectUtils.deepEqual(bytes10, first10),
"IOUtils::read can read part of a file, up to specified max bytes"
);
// Trying to explicitly read nothing isn't useful, but it should still
// succeed.
const bytes0 = await window.IOUtils.read(tmpFileName, 0);
is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
await cleanup(tmpFileName);
});
add_task(async function test_empty_read_and_write() {
// Trying to write an empty file isn't very useful, but it should still
// succeed.
const tmpFileName = "test_ioutils_empty.tmp";
const emptyByteArray = new Uint8Array(0);
const bytesWritten = await window.IOUtils.writeAtomic(
tmpFileName,
emptyByteArray
);
is(bytesWritten, 0, "IOUtils::writeAtomic can create an empty file");
// Trying to explicitly read nothing isn't useful, but it should still
// succeed.
const bytes0 = await window.IOUtils.read(tmpFileName, 0);
is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
// Implicitly try to read nothing.
const nothing = await window.IOUtils.read(tmpFileName);
is(nothing.length, 0, "IOUtils:: read can read empty files");
await cleanup(tmpFileName);
});
add_task(async function test_full_read_and_write() {
// Write a file.
const tmpFileName = "test_ioutils_numbers.tmp";
const bytes = Uint8Array.of(...new Array(50).keys());
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
is(
bytesWritten,
50,
"IOUtils::writeAtomic can write entire byte array to file"
);
// Read it back.
let fileContents = await window.IOUtils.read(tmpFileName);
ok(
ObjectUtils.deepEqual(bytes, fileContents) &&
bytes.length == fileContents.length,
"IOUtils::read can read back entire file"
);
const tooManyBytes = bytes.length + 1;
fileContents = await window.IOUtils.read(tmpFileName, tooManyBytes);
ok(
ObjectUtils.deepEqual(bytes, fileContents) &&
fileContents.length == bytes.length,
"IOUtils::read can read entire file when requested maxBytes is too large"
);
await cleanup(tmpFileName);
});
// Utility functions.
async function cleanup(...files) {
for (const file of files) {
await OS.File.remove(file);
const exists = await OS.File.exists(file);
ok(!exists, `Removed temporary file: ${file}`);
}
}
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>

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

@ -0,0 +1,40 @@
<?xml version="1.0"?>
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<window title="Testing IOUtils on a chrome worker thread"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="test();">
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/>
<script type="application/javascript">
<![CDATA[
// Test IOUtils in a chrome worker.
function test() {
// finish() will be called in the worker.
SimpleTest.waitForExplicitFinish();
info("test_ioutils_worker.xhtml: Starting test");
const worker = new ChromeWorker("file_ioutils_worker.js");
info("test_ioutils_worker.xhtml: Chrome worker created");
// Set up the worker with testing facilities, and start it.
listenForTests(worker, { verbose: false });
worker.postMessage(0);
info("test_ioutils_worker.xhtml: Test in progress");
};
]]>
</script>
<body xmlns="http://www.w3.org/1999/xhtml">
<p id="display"></p>
<div id="content" style="display:none;"></div>
<pre id="test"></pre>
</body>
<label id="test-result" />
</window>