summaryrefslogtreecommitdiffstats
path: root/dom/system/tests/ioutils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/system/tests/ioutils
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/system/tests/ioutils')
-rw-r--r--dom/system/tests/ioutils/chrome.ini23
-rw-r--r--dom/system/tests/ioutils/file_ioutils_test_fixtures.js78
-rw-r--r--dom/system/tests/ioutils/file_ioutils_worker.js102
-rw-r--r--dom/system/tests/ioutils/test_ioutils.html26
-rw-r--r--dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html54
-rw-r--r--dom/system/tests/ioutils/test_ioutils_copy_move.html359
-rw-r--r--dom/system/tests/ioutils/test_ioutils_create_unique.html88
-rw-r--r--dom/system/tests/ioutils/test_ioutils_dir_iteration.html94
-rw-r--r--dom/system/tests/ioutils/test_ioutils_getfile.html86
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mac_xattr.html90
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mkdir.html133
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write.html520
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_json.html165
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_utf8.html359
-rw-r--r--dom/system/tests/ioutils/test_ioutils_remove.html117
-rw-r--r--dom/system/tests/ioutils/test_ioutils_set_permissions.html84
-rw-r--r--dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html241
-rw-r--r--dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html135
-rw-r--r--dom/system/tests/ioutils/test_ioutils_worker.xhtml40
19 files changed, 2794 insertions, 0 deletions
diff --git a/dom/system/tests/ioutils/chrome.ini b/dom/system/tests/ioutils/chrome.ini
new file mode 100644
index 0000000000..a961f45631
--- /dev/null
+++ b/dom/system/tests/ioutils/chrome.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ file_ioutils_test_fixtures.js
+ file_ioutils_worker.js
+
+[test_ioutils.html]
+[test_ioutils_compute_hex_digest.html]
+[test_ioutils_copy_move.html]
+[test_ioutils_create_unique.html]
+[test_ioutils_dir_iteration.html]
+[test_ioutils_getfile.html]
+[test_ioutils_mac_xattr.html]
+skip-if = (os != "mac")
+[test_ioutils_mkdir.html]
+[test_ioutils_read_write.html]
+[test_ioutils_read_write_json.html]
+[test_ioutils_read_write_utf8.html]
+[test_ioutils_remove.html]
+[test_ioutils_stat_set_modification_time.html]
+[test_ioutils_worker.xhtml]
+[test_ioutils_set_permissions.html]
+[test_ioutils_windows_file_attributes.html]
+skip-if = (os != 'win')
diff --git a/dom/system/tests/ioutils/file_ioutils_test_fixtures.js b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js
new file mode 100644
index 0000000000..5d2e5011c9
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_test_fixtures.js
@@ -0,0 +1,78 @@
+// Utility functions.
+
+Uint8Array.prototype.equals = function equals(other) {
+ if (this.byteLength !== other.byteLength) {
+ return false;
+ }
+ return this.every((val, i) => val === other[i]);
+};
+
+async function createFile(location, contents = "") {
+ if (typeof contents === "string") {
+ contents = new TextEncoder().encode(contents);
+ }
+ await IOUtils.write(location, contents);
+ const exists = await fileExists(location);
+ ok(exists, `Created temporary file at: ${location}`);
+}
+
+async function createDir(location) {
+ await IOUtils.makeDirectory(location, {
+ ignoreExisting: true,
+ createAncestors: true,
+ });
+ const exists = await dirExists(location);
+ ok(exists, `Created temporary directory at: ${location}`);
+}
+
+async function fileHasBinaryContents(location, expectedContents) {
+ if (!(expectedContents instanceof Uint8Array)) {
+ throw new TypeError("expectedContents must be a byte array");
+ }
+ info(`Opening ${location} for reading`);
+ const bytes = await IOUtils.read(location);
+ return bytes.equals(expectedContents);
+}
+
+async function fileHasTextContents(location, expectedContents) {
+ if (typeof expectedContents !== "string") {
+ throw new TypeError("expectedContents must be a string");
+ }
+ info(`Opening ${location} for reading`);
+ const bytes = await IOUtils.read(location);
+ const contents = new TextDecoder().decode(bytes);
+ return contents === expectedContents;
+}
+
+async function fileExists(file) {
+ try {
+ let { type } = await IOUtils.stat(file);
+ return type === "regular";
+ } catch (ex) {
+ return false;
+ }
+}
+
+async function dirExists(dir) {
+ try {
+ let { type } = await IOUtils.stat(dir);
+ return type === "directory";
+ } catch (ex) {
+ return false;
+ }
+}
+
+async function cleanup(...files) {
+ for (const file of files) {
+ await IOUtils.remove(file, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+ const exists = await IOUtils.exists(file);
+ ok(!exists, `Removed temporary file: ${file}`);
+ }
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/dom/system/tests/ioutils/file_ioutils_worker.js b/dom/system/tests/ioutils/file_ioutils_worker.js
new file mode 100644
index 0000000000..9f9dfc899b
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_worker.js
@@ -0,0 +1,102 @@
+// Any copyright is dedicated to the Public Domain.
+// - http://creativecommons.org/publicdomain/zero/1.0/
+
+/* eslint-env mozilla/chrome-worker, node */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+/* import-globals-from /toolkit/modules/ObjectUtils.jsm */
+importScripts("resource://gre/modules/ObjectUtils.jsm");
+
+importScripts("file_ioutils_test_fixtures.js");
+
+self.onmessage = async function (msg) {
+ const tmpDir = await PathUtils.getTempDir();
+
+ // 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 confidence check that the implementation is thread-safe.
+ await test_api_is_available_on_worker();
+ await test_full_read_and_write();
+ await test_move_file();
+ await test_copy_file();
+ await test_make_directory();
+
+ 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 = PathUtils.join(tmpDir, "test_ioutils_numbers.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(bytesWritten, 50, "IOUtils::write can write entire byte array to file");
+
+ // Read it back.
+ let fileContents = await 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 IOUtils.read(tmpFileName, { maxBytes: 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);
+ }
+
+ async function test_move_file() {
+ const src = PathUtils.join(tmpDir, "test_move_file_src.tmp");
+ const dest = PathUtils.join(tmpDir, "test_move_file_dest.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ await IOUtils.write(src, bytes);
+
+ await IOUtils.move(src, dest);
+ ok(
+ !(await fileExists(src)) && (await fileExists(dest)),
+ "IOUtils::move can move files from a worker"
+ );
+
+ await cleanup(dest);
+ }
+
+ async function test_copy_file() {
+ const tmpFileName = PathUtils.join(tmpDir, "test_ioutils_orig.tmp");
+ const destFileName = PathUtils.join(tmpDir, "test_ioutils_copy.tmp");
+ await createFile(tmpFileName, "original");
+
+ await IOUtils.copy(tmpFileName, destFileName);
+ ok(
+ (await fileExists(tmpFileName)) &&
+ (await fileHasTextContents(destFileName, "original")),
+ "IOUtils::copy can copy source to dest in same directory"
+ );
+
+ await cleanup(tmpFileName, destFileName);
+ }
+
+ async function test_make_directory() {
+ const dir = PathUtils.join(tmpDir, "test_make_dir.tmp.d");
+ await IOUtils.makeDirectory(dir);
+ const stat = await IOUtils.stat(dir);
+ is(
+ stat.type,
+ "directory",
+ "IOUtils::makeDirectory can make a new directory from a worker"
+ );
+
+ await cleanup(dir);
+ }
+};
diff --git a/dom/system/tests/ioutils/test_ioutils.html b/dom/system/tests/ioutils/test_ioutils.html
new file mode 100644
index 0000000000..cf62c4c388
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils.html
@@ -0,0 +1,26 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script>
+ "use strict";
+
+ add_task(async function test_api_is_available_on_window() {
+ ok(window.IOUtils, "IOUtils is present on the window");
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
new file mode 100644
index 0000000000..1c27a28822
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
@@ -0,0 +1,54 @@
+
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ add_task(async function test_computeHexDigest() {
+ const tempDir = PathUtils.join(PathUtils.tempDir, "ioutils-test-compute-hex-digest.tmp.d");
+ await createDir(tempDir);
+
+ const path = PathUtils.join(tempDir, "file");
+ await IOUtils.writeUTF8(path, "hello world\n");
+
+ const DIGESTS = [
+ "22596363b3de40b06f981fb85d82312e8c0ed511",
+ "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
+ "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80",
+ "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593",
+ ];
+ const ALGORITHMS = ["sha1", "sha256", "sha384", "sha512"];
+
+ for (let i = 0; i < ALGORITHMS.length; i++) {
+ const alg = ALGORITHMS[i];
+ const expected = DIGESTS[i];
+
+ Assert.equal(
+ await IOUtils.computeHexDigest(path, alg),
+ expected,
+ `IOUtils.hashFile() has expected value for ${alg}`);
+ }
+
+ await cleanup(tempDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_copy_move.html b/dom/system/tests/ioutils/test_ioutils_copy_move.html
new file mode 100644
index 0000000000..ba55eb8463
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html
@@ -0,0 +1,359 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ add_task(async function test_move_relative_path() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_relative_path.tmp");
+ const dest = "relative_to_cwd.tmp";
+ await createFile(tmpFileName, "source");
+
+ info("Test moving a file to a relative destination");
+ await Assert.rejects(
+ IOUtils.move(tmpFileName, dest),
+ /Could not parse path/,
+ "IOUtils::move only works with absolute paths"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, "source"),
+ "IOUtils::move doesn't change source file when move fails"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_move_rename() {
+ // Set up.
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_src.tmp");
+ const destFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_move_dest.tmp");
+ await createFile(tmpFileName, "dest");
+ // Test.
+ info("Test move to new file in same directory");
+ await IOUtils.move(tmpFileName, destFileName);
+ info(`Moved ${tmpFileName} to ${destFileName}`);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "dest"),
+ "IOUtils::move can move source to dest in same directory"
+ )
+
+ // Set up.
+ info("Test move to existing file with no overwrite");
+ await createFile(tmpFileName, "source");
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(tmpFileName, destFileName, { noOverwrite: true }),
+ /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(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "dest"),
+ "Failed IOUtils::move doesn't move the source file"
+ );
+
+ // Test.
+ info("Test move to existing file with overwrite");
+ await IOUtils.move(tmpFileName, destFileName, { noOverwrite: false });
+ ok(!await fileExists(tmpFileName), "IOUtils::move moved source");
+ ok(
+ await fileHasTextContents(destFileName, "source"),
+ "IOUtils::move overwrote the destination with the source"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destFileName);
+ });
+
+ add_task(async function test_move_to_dir() {
+ // Set up.
+ info("Test move and rename to non-existing directory");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_move_to_dir.tmp.d");
+ const dest = PathUtils.join(destDir, "dest.tmp");
+ await createFile(tmpFileName);
+ // Test.
+ ok(!await IOUtils.exists(destDir), "Expected path not to exist");
+ await IOUtils.move(tmpFileName, dest);
+ ok(
+ !await fileExists(tmpFileName) && await fileExists(dest),
+ "IOUtils::move creates non-existing parents if needed"
+ );
+
+ // Set up.
+ info("Test move and rename to existing directory.")
+ await createFile(tmpFileName);
+ // Test.
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ await IOUtils.move(tmpFileName, dest);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileExists(dest),
+ "IOUtils::move can move/rename a file into an existing dir"
+ );
+
+ // Set up.
+ info("Test move to existing directory without specifying leaf name.")
+ await createFile(tmpFileName);
+ // Test.
+ await IOUtils.move(tmpFileName, destDir);
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ ok(
+ !await fileExists(tmpFileName)
+ && await fileExists(PathUtils.join(destDir, PathUtils.filename(tmpFileName))),
+ "IOUtils::move can move a file into an existing dir"
+ );
+
+ // Clean up.
+ await cleanup(destDir);
+ });
+
+ add_task(async function test_move_dir() {
+ // Set up.
+ info("Test rename an empty directory");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "test_move_dir.tmp.d");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_move_dir_dest.tmp.d");
+ await createDir(srcDir);
+ // Test.
+ await IOUtils.move(srcDir, destDir);
+ ok(
+ !await IOUtils.exists(srcDir) && await dirExists(destDir),
+ "IOUtils::move can rename directories"
+ );
+
+ // Set up.
+ info("Test move directory and its content into another directory");
+ await createDir(srcDir);
+ await createFile(PathUtils.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.move(srcDir, destDir);
+ const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp");
+ ok(
+ !await IOUtils.exists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(PathUtils.join(destDir, PathUtils.filename(srcDir)))
+ && await fileHasTextContents(destFile, "foo"),
+ "IOUtils::move can move a directory and its contents into another one"
+ )
+
+ // Clean up.
+ await cleanup(srcDir, destDir);
+ });
+
+ add_task(async function test_move_failures() {
+ // Set up.
+ info("Test attempt to rename a non-existent source file");
+ const notExistsSrc = PathUtils.join(PathUtils.tempDir, "not_exists_src.tmp");
+ const notExistsDest = PathUtils.join(PathUtils.tempDir, "not_exists_dest.tmp");
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(notExistsSrc, notExistsDest),
+ /Could not move source file\(.*\) because it does not exist/,
+ "IOUtils::move throws if source file does not exist"
+ );
+ ok(
+ !await fileExists(notExistsSrc) && !await fileExists(notExistsDest),
+ "IOUtils::move fails if source file does not exist"
+ );
+
+ // Set up.
+ info("Test attempt to move a directory to a file");
+ const destFile = PathUtils.join(PathUtils.tempDir, "test_move_failures_file_dest.tmp");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "test_move_failure_src.tmp.d");
+ await createFile(destFile);
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.move(srcDir, destFile),
+ /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"
+ );
+
+ // Clean up.
+ await cleanup(destFile, srcDir);
+ });
+
+ add_task(async function test_copy() {
+ // Set up.
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_orig.tmp");
+ const destFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_copy.tmp");
+ await createFile(tmpFileName, "original");
+ // Test.
+ info("Test copy to new file in same directory");
+ await IOUtils.copy(tmpFileName, destFileName);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "original"),
+ "IOUtils::copy can copy source to dest in same directory"
+ );
+
+ // Set up.
+ info("Test copy to existing file with no overwrite");
+ await createFile(tmpFileName, "new contents");
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(tmpFileName, destFileName, { noOverwrite: true }),
+ /Could not copy source file\(.*\) to destination\(.*\) because the destination already exists and overwrites are not allowed/,
+ "IOUtils::copy will refuse to copy to existing destination if overwrites are disabled"
+ );
+ ok(
+ await fileExists(tmpFileName)
+ && await fileHasTextContents(destFileName, "original"),
+ "Failed IOUtils::move doesn't move the source file"
+ );
+
+ // Test.
+ info("Test copy to existing file with overwrite");
+ await IOUtils.copy(tmpFileName, destFileName, { noOverwrite: false });
+ ok(await fileExists(tmpFileName), "IOUtils::copy retains source");
+ ok(
+ await fileHasTextContents(destFileName, "new contents"),
+ "IOUtils::copy overwrote the destination with the source"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destFileName);
+ });
+
+ add_task(async function test_copy_file_to_dir() {
+ // Set up.
+ info("Test copy file to non-existing directory");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_file_to_dir.tmp.d");
+ const dest = PathUtils.join(destDir, "dest.tmp");
+ await createFile(tmpFileName);
+ // Test.
+ ok(!await IOUtils.exists(destDir), "Expected path not to exist");
+ await IOUtils.copy(tmpFileName, dest);
+ ok(
+ await fileExists(tmpFileName) && await fileExists(dest),
+ "IOUtils::copy creates non-existing parents if needed"
+ );
+
+ // Set up.
+ info("Test copy file to existing directory")
+ await createFile(tmpFileName);
+ // Test.
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ await IOUtils.copy(tmpFileName, dest);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileExists(dest),
+ "IOUtils::copy can copy a file into an existing dir"
+ );
+
+ // Set up.
+ info("Test copy file to existing directory without specifying leaf name")
+ await createFile(tmpFileName);
+ // Test.
+ await IOUtils.copy(tmpFileName, destDir);
+ ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
+ ok(
+ await fileExists(tmpFileName)
+ && await fileExists(PathUtils.join(destDir, PathUtils.filename(tmpFileName))),
+ "IOUtils::copy can copy a file into an existing dir"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName, destDir);
+ });
+
+ add_task(async function test_copy_dir_recursive() {
+ // Set up.
+ info("Test rename an empty directory");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "test_copy_dir.tmp.d");
+ const destDir = PathUtils.join(PathUtils.tempDir, "test_copy_dir_dest.tmp.d");
+ await createDir(srcDir);
+ // Test.
+ await IOUtils.copy(srcDir, destDir, { recursive: true });
+ ok(
+ await dirExists(srcDir) && await dirExists(destDir),
+ "IOUtils::copy can recursively copy entire directories"
+ );
+
+ // Set up.
+ info("Test copy directory and its content into another directory");
+ await createDir(srcDir);
+ await createFile(PathUtils.join(srcDir, "file.tmp"), "foo");
+ // Test.
+ await IOUtils.copy(srcDir, destDir, { recursive: true });
+ const destFile = PathUtils.join(destDir, PathUtils.filename(srcDir), "file.tmp");
+ ok(
+ await dirExists(srcDir)
+ && await dirExists(destDir)
+ && await dirExists(PathUtils.join(destDir, PathUtils.filename(srcDir)))
+ && await fileHasTextContents(destFile, "foo"),
+ "IOUtils::copy can move a directory and its contents into another one"
+ )
+
+ // Clean up.
+ await cleanup(srcDir, destDir);
+ });
+
+ add_task(async function test_copy_failures() {
+ // Set up.
+ info("Test attempt to copy a non-existent source file");
+ const notExistsSrc = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_src.tmp");
+ const notExistsDest = PathUtils.join(PathUtils.tempDir, "test_copy_not_exists_dest.tmp");
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(notExistsSrc, notExistsDest),
+ /Could not copy source file\(.*\) because it does not exist/,
+ "IOUtils::copy throws if source file does not exist"
+ );
+ ok(
+ !await fileExists(notExistsSrc) && !await fileExists(notExistsDest),
+ "IOUtils::copy failure due to missing source file does not affect destination"
+ );
+
+ // Set up.
+ info("Test attempt to copy a directory to a file");
+ const destFile = PathUtils.join(PathUtils.tempDir, "test_copy_failures_file_dest.tmp");
+ const srcDir = PathUtils.join(PathUtils.tempDir, "test_copy_failure_src.tmp.d");
+ await createFile(destFile);
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(srcDir, destFile, { recursive: true }),
+ /Could not copy the source directory\(.*\) to the destination\(.*\) because the destination is not a directory/,
+ "IOUtils::copy throws if try to move dir into an existing file"
+ );
+ ok(await fileHasTextContents(destFile, ""), "IOUtils::copy failure does not affect destination");
+
+ // Set up.
+ info("Test copy directory without recursive option");
+ await createDir(srcDir);
+ // Test.
+ await Assert.rejects(
+ IOUtils.copy(srcDir, notExistsDest, { recursive: false }),
+ /Refused to copy source directory\(.*\) to the destination\(.*\)/,
+ "IOUtils::copy throws if try to copy a directory with { recursive: false }"
+ );
+ console.log(`${notExistsDest} exists?`, await IOUtils.exists(notExistsDest))
+ ok(!await IOUtils.exists(notExistsDest), "IOUtils::copy failure does not affect destination");
+
+ // Clean up.
+ await cleanup(destFile, srcDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_create_unique.html b/dom/system/tests/ioutils/test_ioutils_create_unique.html
new file mode 100644
index 0000000000..e447964343
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_create_unique.html
@@ -0,0 +1,88 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ function octalFormat(n) {
+ let s = n.toString(8);
+ while (s.length < 3) {
+ s = `0${s}`;
+ }
+ return `0o${s}`;
+ }
+
+ async function check(method, path, prefix, type, perms) {
+ const filename = PathUtils.filename(path);
+
+ ok(filename.startsWith(prefix), `IOUtils.${method} uses the prefix`);
+ ok(await IOUtils.exists(path), `IOUtils.${method} creates a file`);
+
+ const stat = await IOUtils.stat(path);
+ is(stat.type, type, `IOUtils.${method} creates a "${type}" file`);
+
+ is(
+ octalFormat(stat.permissions),
+ octalFormat(perms),
+ `IOUtils.${method} creates a file with the correct permissions`
+ );
+ }
+
+ add_task(async function test_createUnique() {
+ const tempDir = PathUtils.join(
+ PathUtils.tempDir,
+ "test_createUnique.tmp.d"
+ );
+
+ const filesToChmod = [];
+
+ SimpleTest.registerCleanupFunction(async function test_createUnique_cleanup() {
+ for (const file of filesToChmod) {
+ if (await IOUtils.exists(file)) {
+ await IOUtils.setPermissions(file, 0o666);
+ }
+ }
+
+ await IOUtils.remove(tempDir, { recursive: true });
+ });
+
+ const isWindows = Services.appinfo.OS === "WINNT";
+
+ info("Creating a unique directory")
+ const dir = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o600);
+ await check("createUniqueDirectory", dir, "unique-dir", "directory", isWindows ? 0o666 : 0o600);
+
+ info("Creating a unique directory with the same prefix")
+ const dir2 = await IOUtils.createUniqueDirectory(tempDir, "unique-dir", 0o700);
+ await check("createUniqueDirectory", dir2, "unique-dir", "directory", isWindows ? 0o666 : 0o700);
+ ok(dir !== dir2, "IOUtils.createUniqueDirectory creates unique paths");
+
+ info("Creating a unique file");
+ const file = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o641);
+ await check("createUniqueFile", file, "unique-file", "regular", isWindows ? 0o666 : 0o641);
+
+ info("Creating a unique file with the same prefix");
+ const file2 = await IOUtils.createUniqueFile(tempDir, "unique-file", 0o400);
+ filesToChmod.push(file2);
+ await check("createUniqueFile", file2, "unique-file", "regular", isWindows ? 0o444 : 0o400);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_dir_iteration.html b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
new file mode 100644
index 0000000000..93e2c60039
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
@@ -0,0 +1,94 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ add_task(async function iterate_dir_failure() {
+ let notExists = PathUtils.join(PathUtils.tempDir, 'does_not_exist_dir.tmp.d');
+
+ await Assert.rejects(
+ IOUtils.getChildren(notExists),
+ /Could not get children of file\(.*\) because it does not exist/,
+ "IOUtils::getChildren rejects if the file does not exist"
+ );
+ ok(!await fileExists(notExists), `Expected ${notExists} not to exist`);
+
+ info('Try to get the children of a regular file');
+
+ let tmpFileName = PathUtils.join(PathUtils.tempDir, 'iterator_file.tmp');
+ await createFile(tmpFileName)
+ await Assert.rejects(IOUtils.getChildren(tmpFileName),
+ /Could not get children of file\(.*\) because it is not a directory/,
+ "IOUtils::getChildren rejects if the file is not a dir"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function iterate_dir() {
+ info('Try to get the children of a multi-level directory hierarchy');
+
+ let root = PathUtils.join(PathUtils.tempDir, 'iterator.tmp.d');
+ let child1 = PathUtils.join(root, 'child1.tmp');
+ let child2 = PathUtils.join(root, 'child2.tmp');
+ let grandchild = PathUtils.join(child1, 'grandchild.tmp');
+
+ await createDir(grandchild); // Ancestors will be created.
+ await createDir(child2);
+
+ let entries = await IOUtils.getChildren(root);
+
+ is(entries.length, 2, `Expected 2 entries below the path at ${root}`);
+ ok(!entries.includes(grandchild), "IOUtils::getChildren does not enter subdirectories");
+
+ await cleanup(root);
+ });
+
+ add_task(async function iterate_empty_dir() {
+ info('Try to get the children of an empty directory');
+
+ let emptyDir = PathUtils.join(PathUtils.tempDir, 'iterator_empty_dir.tmp.d');
+ await createDir(emptyDir);
+
+ is(
+ (await IOUtils.getChildren(emptyDir)).length,
+ 0,
+ "IOUtils::getChildren return an empty array when called on an empty dir"
+ );
+
+ await cleanup(emptyDir);
+ });
+
+ add_task(async function iterate_ignore_missing_dir() {
+ info("Try to get the children of a missing file with ignoreAbsent");
+
+ const notExists = PathUtils.join(PathUtils.tempDir, "does_not_exist_dir.tmp.d");
+
+ is(
+ (await IOUtils.getChildren(notExists, { ignoreAbsent: true })).length,
+ 0,
+ "IOUtils::getChildren returns an empty array when called with ignoreAbsent on a missing file"
+ );
+ ok(!await fileExists(notExists), `Expected ${notExists} not to exist`);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_getfile.html b/dom/system/tests/ioutils/test_ioutils_getfile.html
new file mode 100644
index 0000000000..d5295f469f
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_getfile.html
@@ -0,0 +1,86 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script>
+ "use strict";
+
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ const TEST_PATH = PathUtils.join(PathUtils.tempDir, "test-ioutils-getfile");
+
+ add_task(async function test_getFile() {
+ const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file.txt");
+ const parentPath = PathUtils.parent(expectedPath);
+
+ ok(!(await IOUtils.exists(parentPath)), "Parent directory should not exist");
+
+ const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file.txt");
+ const path = file.path;
+
+ is(path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(parentPath), "Parent directory should be created");
+ ok(!(await IOUtils.exists(path)), "File should not be created");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getFile_exists() {
+ const expectedPath = PathUtils.join(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt");
+
+ await IOUtils.makeDirectory(PathUtils.parent(expectedPath));
+ await IOUtils.writeUTF8(expectedPath, "hello world");
+
+ const file = await IOUtils.getFile(TEST_PATH, "foo", "bar", "baz", "get-file-exists.txt");
+ is(file.path, expectedPath, "Should have the correct path");
+ is(await IOUtils.readUTF8(file.path), "hello world", "Contents should be unchanged");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getDirectory() {
+ const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge");
+
+ ok(!(await IOUtils.exists(PathUtils.parent(expectedPath))), "Parent directory should not exist");
+
+ const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge");
+
+ is(file.path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(expectedPath), "Directory should be created");
+
+ const info = await IOUtils.stat(expectedPath);
+ is(info.type, "directory", "Should create a directory");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+
+ add_task(async function test_getDirectory_exists() {
+ const expectedPath = PathUtils.join(TEST_PATH, "qux", "quux", "corge");
+
+ await IOUtils.makeDirectory(expectedPath);
+
+ const file = await IOUtils.getDirectory(TEST_PATH, "qux", "quux", "corge");
+ is(file.path, expectedPath, "Should have the correct path");
+ ok(await IOUtils.exists(expectedPath), "Directory should still exist");
+
+ const info = await IOUtils.stat(expectedPath);
+ is(info.type, "directory", "Should still be a directory");
+
+ await IOUtils.remove(TEST_PATH, { recursive: true });
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_mac_xattr.html b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
new file mode 100644
index 0000000000..cd6e7aeb5d
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
@@ -0,0 +1,90 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+
+ const ATTR = "bogus.attr";
+ const VALUE = new TextEncoder().encode("bogus");
+
+ add_task(async function test_macXAttr() {
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-macos-xattr.tmp.d");
+
+ await createDir(tmpDir);
+
+ const path = PathUtils.join(tmpDir, "file.tmp");
+ ok(!await IOUtils.exists(path), "File should not exist");
+ await IOUtils.writeUTF8(path, "");
+
+ ok(
+ !await IOUtils.hasMacXAttr(path, ATTR),
+ "File does not have an extended attribute at creation"
+ );
+
+ info("Testing getting an attribute that does not exist");
+ await Assert.rejects(
+ IOUtils.getMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::getMacXAttr rejects when the attribute does not exist"
+ );
+
+ info("Testing setting an attribute");
+ await IOUtils.setMacXAttr(path, ATTR, VALUE);
+ ok(
+ await IOUtils.hasMacXAttr(path, ATTR),
+ "File has extended attribute after setting"
+ );
+
+ {
+ info("Testing getting an attribute")
+ const value = await IOUtils.getMacXAttr(path, ATTR);
+ Assert.deepEqual(
+ Array.from(value),
+ Array.from(VALUE),
+ "Attribute value should match"
+ );
+ }
+
+ info("Testing removing an attribute");
+ await IOUtils.delMacXAttr(path, ATTR);
+ await Assert.rejects(
+ IOUtils.getMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::delMacXAttr removes the attribute"
+ );
+
+ ok(
+ !await IOUtils.hasMacXAttr(path, ATTR),
+ "File does not have extended attribute after removing"
+ );
+
+ info("Testing removing an attribute that does not exist");
+ await Assert.rejects(
+ IOUtils.delMacXAttr(path, ATTR),
+ /NotFoundError: The file `.+' does not have an extended attribute/,
+ "IOUtils::delMacXAttr rejects when the attribute does not exist"
+ );
+
+ await cleanup(tmpDir);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_mkdir.html b/dom/system/tests/ioutils/test_ioutils_mkdir.html
new file mode 100644
index 0000000000..c1a073dea6
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mkdir.html
@@ -0,0 +1,133 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ add_task(async function test_make_directory() {
+ info("Test creating a new directory");
+ const newDirectoryName = PathUtils.join(PathUtils.tempDir, "test_ioutils_new_dir.tmp.d");
+ await IOUtils.makeDirectory(newDirectoryName);
+ ok(
+ await IOUtils.exists(newDirectoryName),
+ "IOUtils::makeDirectory can create a new directory"
+ );
+
+ info("Test creating an existing directory");
+ await IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: true });
+ ok(
+ await IOUtils.exists(newDirectoryName),
+ "IOUtils::makeDirectory can ignore existing directories"
+ );
+ await Assert.rejects(
+ IOUtils.makeDirectory(newDirectoryName, { ignoreExisting: false }),
+ /Could not create directory because it already exists at .*/,
+ "IOUtils::makeDirectory can throw if the target dir exists"
+ )
+
+ info("Test creating a nested directory");
+ const parentDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_mkdir_parent.tmp.d");
+ const nestedDirName = PathUtils.join(
+ parentDirName,
+ "test_ioutils_mkdir_child.tmp.d"
+ );
+ await Assert.rejects(
+ IOUtils.makeDirectory(nestedDirName, { createAncestors: false }),
+ /Could not create directory at .*/,
+ "IOUtils::makeDirectory can fail if the target is missing parents"
+ );
+ ok(!await IOUtils.exists(nestedDirName), `Expected ${nestedDirName} not to exist`);
+ await IOUtils.makeDirectory(nestedDirName, { createAncestors: true });
+ ok(
+ await IOUtils.exists(nestedDirName),
+ "IOUtils::makeDirectory can create ancestors of the target directory"
+ );
+
+ await cleanup(newDirectoryName, parentDirName);
+ });
+
+ add_task(async function test_make_directory_failure() {
+ info("Try to create a directory where a file already exists");
+ const notADirFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_not_a_dir.tmp");
+ await createFile(notADirFileName);
+
+ await Assert.rejects(
+ IOUtils.makeDirectory(notADirFileName, { ignoreExisting: false }),
+ /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(
+ IOUtils.makeDirectory(notADirFileName, { ignoreExisting: true }),
+ /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`);
+
+ await cleanup(notADirFileName);
+ });
+
+ add_task(async function test_make_directory_permissions() {
+ if (Services.appinfo.OS === "WINNT") {
+ ok(true, "Skipping test on unsupported platform (Windows)");
+ return;
+ }
+
+ const newDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_mkdir_perms.tmp.d");
+
+ ok(!await IOUtils.exists(newDir), "Directory does not exist before creation");
+ await IOUtils.makeDirectory(newDir, { permissions: 0o751 });
+ ok(await IOUtils.exists(newDir), "Directory created");
+
+ const stat = await IOUtils.stat(newDir);
+ is(stat.type, "directory", "Directory stat() as directory");
+ is(stat.permissions, 0o751, "Directory created with expected permissions");
+
+ await cleanup(newDir);
+ });
+
+ add_task(async function test_make_directory_root() {
+ if (Services.appinfo.OS === "WINNT") {
+ // We don't actually know the root drive, but we can find the root drive
+ // of the profile directory.
+ let current = PathUtils.profileDir;
+ let parent = PathUtils.parent(current);
+ while (parent !== null) {
+ current = parent;
+ parent = PathUtils.parent(current);
+ }
+ // `current` will now be a valid root directory.
+ ok(await IOUtils.exists(current), "Root directory should exist");
+
+ const DRIVE_RE = /^[A-Za-z]:$/;
+ ok(
+ current.startsWith("\\\\") || DRIVE_RE.test(current),
+ `Root directory (${current}) should be a UNC path or drive`,
+ );
+ await IOUtils.makeDirectory(current, {createAncestors: false});
+ } else {
+ ok(await IOUtils.exists("/"), "Root directory should exist");
+ await IOUtils.makeDirectory("/", {createAncestors: false});
+ }
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write.html b/dom/system/tests/ioutils/test_ioutils_read_write.html
new file mode 100644
index 0000000000..f52115d261
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write.html
@@ -0,0 +1,520 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ add_task(async function test_read_failure() {
+ const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp");
+ await Assert.rejects(
+ IOUtils.read(doesNotExist),
+ /Could not open the file at .*/,
+ "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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_overwrite.tmp");
+ const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
+
+ let exists = await IOUtils.exists(tmpFileName);
+ ok(!exists, `File ${tmpFileName} should not exist before writing`);
+
+ await IOUtils.write(tmpFileName, untouchableContents);
+
+ exists = await IOUtils.exists(tmpFileName);
+ ok(exists, `File ${tmpFileName} should exist after writing`);
+
+ const newContents = new TextEncoder().encode("Nah nah nah!\n");
+ await Assert.rejects(
+ IOUtils.write(tmpFileName, newContents, {
+ mode: "create",
+ }),
+ /Refusing to overwrite the file at */,
+ "IOUtils::write rejects writing to existing file if overwrites are disabled"
+ );
+ ok(
+ await fileHasBinaryContents(tmpFileName, untouchableContents),
+ "IOUtils::write doesn't change target file when overwrite is refused"
+ );
+
+ const bytesWritten = await IOUtils.write(
+ tmpFileName,
+ newContents,
+ { mode: "overwrite" }
+ );
+ is(
+ bytesWritten,
+ newContents.length,
+ "IOUtils::write can overwrite files if specified"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_with_backup() {
+ info("Test backup file option with non-existing file");
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_option.tmp");
+ let backupFileName = destFileName + ".backup";
+ let bytesWritten =
+ await IOUtils.write(destFileName, fileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::write creates a new file with the correct contents"
+ );
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::write does not create a backup if the target file does not exist"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write correctly writes to a new file without performing a backup"
+ );
+
+ info("Test backup file option with existing destination");
+ let newFileContents = new TextEncoder().encode("New file contents");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::write can backup an existing file before writing"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::write can create the target with the correct contents"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::write correctly writes to the target after taking a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_write_with_backup_and_tmp() {
+ info("Test backup with tmp and backup file options, non-existing destination");
+
+ let fileContents = new TextEncoder().encode("Original file contents");
+ let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp");
+ let bytesWritten =
+ await IOUtils.write(destFileName, fileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::write does not create a backup if the target file does not exist"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::write can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write can copy tmp file to destination without performing a backup"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination");
+ let newFileContents = new TextEncoder().encode("New file contents");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::write can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::write can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination and backup");
+ newFileContents = new TextEncoder().encode("Updated new file contents");
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ ok(await fileExists(backupFileName), `Expected ${backupFileName} to exist`);
+ bytesWritten =
+ await IOUtils.write(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "New file contents"),
+ "IOUtils::write can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Updated new file contents"),
+ "IOUtils::write can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_partial_read() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_partial_read.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(
+ bytesWritten,
+ 50,
+ "IOUtils::write can write entire byte array to file"
+ );
+
+ // Read just the first 10 bytes.
+ const first10 = bytes.slice(0, 10);
+ const bytes10 = await IOUtils.read(tmpFileName, { maxBytes: 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 IOUtils.read(tmpFileName, { maxBytes: 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 = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty.tmp");
+ const emptyByteArray = new Uint8Array(0);
+ const bytesWritten = await IOUtils.write(
+ tmpFileName,
+ emptyByteArray
+ );
+ is(bytesWritten, 0, "IOUtils::write can create an empty file");
+
+ // Trying to explicitly read nothing isn't useful, but it should still
+ // succeed.
+ const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 });
+ is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
+
+ // Implicitly try to read nothing.
+ const nothing = await 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.
+
+ info("Test writing to a new binary file");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_numbers.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ const bytesWritten = await IOUtils.write(tmpFileName, bytes);
+ is(
+ bytesWritten,
+ 50,
+ "IOUtils::write can write entire byte array to file"
+ );
+
+ // Read it back.
+ info("Test reading a binary file");
+ let fileContents = await 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 IOUtils.read(tmpFileName, { maxBytes: tooManyBytes });
+ ok(
+ ObjectUtils.deepEqual(bytes, fileContents) &&
+ fileContents.length == bytes.length,
+ "IOUtils::read can read entire file when requested maxBytes is too large"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_relative_path() {
+ const tmpFileName = "test_ioutils_write_relative_path.tmp";
+ const bytes = Uint8Array.of(...new Array(50).keys());
+
+ info("Test writing a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.write(tmpFileName, bytes),
+ /Could not parse path/,
+ "IOUtils::write only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_read_relative_path() {
+ const tmpFileName = "test_ioutils_read_relative_path.tmp";
+
+ info("Test reading a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.read(tmpFileName),
+ /Could not parse path/,
+ "IOUtils::write only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_lz4() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4.tmp");
+
+ info("Test writing lz4 encoded data");
+ const varyingBytes = Uint8Array.of(...new Array(50).keys());
+ let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
+ is(bytesWritten, 64, "Expected to write 64 bytes");
+
+ info("Test reading lz4 encoded data");
+ let readData = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readData.equals(varyingBytes), "IOUtils can write and read back LZ4 encoded data");
+
+ info("Test writing lz4 compressed data");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ bytesWritten = await IOUtils.write(tmpFileName, repeatedBytes, { compress: true });
+ is(bytesWritten, 23, "Expected 50 bytes to compress to 23 bytes");
+
+ info("Test reading lz4 encoded data");
+ readData = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readData.equals(repeatedBytes), "IOUtils can write and read back LZ4 compressed data");
+
+ info("Test writing empty lz4 compressed data")
+ const empty = new Uint8Array();
+ bytesWritten = await IOUtils.write(tmpFileName, empty, { compress: true });
+ is(bytesWritten, 12, "Expected to write just the LZ4 header, with a content length of 0");
+
+
+ info("Test reading empty lz4 compressed data")
+ const readEmpty = await IOUtils.read(tmpFileName, { decompress: true });
+ ok(readEmpty.equals(empty), "IOUtils can write and read back empty buffers with LZ4");
+ const readEmptyRaw = await IOUtils.read(tmpFileName, { decompress: false });
+ is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");
+ const expectedHeader = Uint8Array.of(109, 111, 122, 76, 122, 52, 48, 0, 0, 0, 0, 0); // "mozLz40\0\0\0\0"
+ ok(readEmptyRaw.equals(expectedHeader), "Expected to read header with content length of 0");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_lz4_bad_call() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_bad_call.tmp");
+
+ info("Test decompression with invalid options");
+ const varyingBytes = Uint8Array.of(...new Array(50).keys());
+ let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
+ is(bytesWritten, 64, "Expected to write 64 bytes");
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { maxBytes: 4, decompress: true }),
+ /The `maxBytes` and `decompress` options are not compatible/,
+ "IOUtils::read rejects when maxBytes and decompress options are both used"
+ );
+
+ await cleanup(tmpFileName)
+ });
+
+ add_task(async function test_lz4_failure() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_fail.tmp");
+
+ info("Test decompression of non-lz4 data");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ await IOUtils.write(tmpFileName, repeatedBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ (actual) => {
+ is(actual.constructor, DOMException,
+ "rejection reason constructor for decompress with bad header");
+ is(actual.name, "NotReadableError",
+ "rejection error name for decompress with bad header");
+ ok(/Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/
+ .test(actual.message),
+ "rejection error message for decompress with bad header. Got "
+ + actual.message);
+ return true;
+ },
+ "IOUtils::read fails to decompress LZ4 data with a bad header"
+ );
+
+ info("Test decompression of short byte buffer");
+ const elevenBytes = Uint8Array.of(...new Array(11).fill(1));
+ await IOUtils.write(tmpFileName, elevenBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::read fails to decompress LZ4 data with missing header"
+ );
+
+ info("Test decompression of valid header, but corrupt contents");
+ const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length
+ const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream.
+ const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents);
+ await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.read(tmpFileName, { decompress: true }),
+ /Could not decompress file contents, the file may be corrupt/,
+ "IOUtils::read fails to read corrupt LZ4 contents with a correct header"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_directory() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_directory.tmp");
+ const tmpPath = `${fileName}.tmp`;
+ const bytes = Uint8Array.of(1, 2, 3, 4);
+
+ await IOUtils.makeDirectory(fileName);
+ await Assert.rejects(
+ IOUtils.write(fileName, bytes),
+ /NotAllowedError: Could not open the file at .+ for writing/);
+
+ await Assert.rejects(
+ IOUtils.write(fileName, bytes, { tmpPath }),
+ /NotAllowedError: Could not open the file at .+ for writing/);
+
+ ok(!await IOUtils.exists(PathUtils.join(fileName, PathUtils.filename(tmpPath))));
+ });
+
+ add_task(async function test_read_offset() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_offset.tmp");
+
+ const bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+ const byteArray = Uint8Array.of(...bytes);
+
+ await IOUtils.write(tmpFileName, byteArray);
+
+ for (const offset of [0, 5]) {
+ info(`Reading bytes from offset ${offset}`);
+
+ const readBytes = await IOUtils.read(tmpFileName, { offset });
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(offset),
+ `should have read bytes from offset ${offset}`
+ );
+ }
+
+ for (const offset of [0, 5]) {
+ info(`Reading up to 5 bytes from offset ${offset}`);
+
+ const readBytes = await IOUtils.read(tmpFileName, {offset, maxBytes: 5});
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(offset, offset + 5),
+ `should have read 5 bytes from offset ${offset}`
+ );
+ }
+
+ {
+ info(`Reading bytes from offset 10`);
+ const readBytes = await IOUtils.read(tmpFileName, {offset: 10});
+ is(readBytes.length, 0, "should have read 0 bytes");
+ }
+
+ {
+ info(`Reading up to 10 bytes from offset 5`);
+ const readBytes = await IOUtils.read(tmpFileName, {offset: 5, maxBytes: 10});
+ is(readBytes.length, 5, "should have read 5 bytes");
+ Assert.deepEqual(
+ Array.from(readBytes),
+ bytes.slice(5, 10),
+ "should have read last 5 bytes"
+ );
+ }
+ });
+
+ add_task(async function test_write_appendOrCreate() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_appendOrCreate.tmp");
+
+ await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4), { mode: "appendOrCreate" });
+
+ {
+ const contents = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4], "read bytes should be equal");
+ }
+
+ await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "appendOrCreate" });
+
+ {
+ const contents = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appendOrCreateing");
+ }
+
+ await cleanup(fileName);
+ });
+
+ add_task(async function test_write_append() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append.tmp");
+
+ await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4));
+
+ const beforeAppend = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(beforeAppend), [0, 1, 2, 3, 4], "read bytes should be equal");
+
+ await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" });
+
+ const afterAppend = await IOUtils.read(fileName);
+ Assert.deepEqual(Array.from(afterAppend), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appending");
+
+ await cleanup(fileName);
+ });
+
+ add_task(async function test_write_append_no_create() {
+ const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append_no_create.tmp");
+
+ await Assert.rejects(
+ IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }),
+ /NotFoundError: Could not open the file at .*/
+ );
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_json.html b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
new file mode 100644
index 0000000000..e356e50c47
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
@@ -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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import(
+ "resource://gre/modules/ObjectUtils.jsm"
+ );
+
+ const OBJECT = {
+ "foo": [
+ "bar",
+ 123,
+ 456.789,
+ true,
+ false,
+ null,
+ ],
+ "bar": {
+ "baz": {},
+ },
+ };
+
+ const ARRAY = [1, 2.3, true, false, null, { "foo": "bar" }];
+
+ const PRIMITIVES = [123, true, false, "hello, world", null];
+
+ add_task(async function read_json() {
+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json.tmp");
+
+ info("Testing IOUtils.readJSON() with a serialized object...");
+ await IOUtils.writeUTF8(filename, JSON.stringify(OBJECT));
+ const readObject = await IOUtils.readJSON(filename);
+ const parsedObject = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip");
+ ok(
+ ObjectUtils.deepEqual(parsedObject, readObject),
+ "IOUtils.readJSON() equivalent to JSON.parse() for objects"
+ );
+
+ info("Testing IOUtils.readJSON() with a serialized array...");
+ await IOUtils.writeUTF8(filename, JSON.stringify(ARRAY));
+ const readArray = await IOUtils.readJSON(filename);
+ const parsedArray = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip");
+ ok(
+ ObjectUtils.deepEqual(parsedArray, readArray),
+ "IOUtils.readJSON() equivalent to JSON.parse(IOUtils.readUTF8()) for arrays"
+ );
+
+ info("Testing IOUtils.readJSON() with serialized primitives...");
+ for (const primitive of PRIMITIVES) {
+ await IOUtils.writeUTF8(filename, JSON.stringify(primitive));
+ const readPrimitive = await IOUtils.readJSON(filename);
+ const parsedPrimitive = JSON.parse(await IOUtils.readUTF8(filename));
+ ok(primitive === readPrimitive, `JSON primitive ${primitive} should round trip`);
+ ok(
+ readPrimitive === parsedPrimitive,
+ `${readPrimitive} === ${parsedPrimitive} -- IOUtils.readJSON() equivalent to JSON.parse() for primitive`
+ );
+ }
+
+ info("Testing IOUtils.readJSON() with a file that does not exist...");
+ const notExistsFilename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_not_exists.tmp");
+ ok(!await IOUtils.exists(notExistsFilename), `${notExistsFilename} should not exist`);
+ await Assert.rejects(
+ IOUtils.readJSON(notExistsFilename),
+ /NotFoundError: Could not open the file at/,
+ "IOUtils::readJSON rejects when file does not exist"
+ );
+
+ info("Testing IOUtils.readJSON() with a file that does not contain JSON");
+ const invalidFilename = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_invalid.tmp");
+ await IOUtils.writeUTF8(invalidFilename, ":)");
+
+ await Assert.rejects(
+ IOUtils.readJSON(invalidFilename),
+ /SyntaxError: JSON\.parse/,
+ "IOUTils::readJSON rejects when the file contains invalid JSON"
+ );
+
+ await cleanup(filename, invalidFilename);
+ });
+
+ add_task(async function write_json() {
+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_json.tmp");
+
+ info("Testing IOUtils.writeJSON() with an object...");
+ await IOUtils.writeJSON(filename, OBJECT);
+ const readObject = await IOUtils.readJSON(filename);
+ const readObjectStr = await IOUtils.readUTF8(filename);
+ ok(ObjectUtils.deepEqual(OBJECT, readObject), "JSON objects should round-trip");
+ ok(
+ readObjectStr === JSON.stringify(OBJECT),
+ "IOUtils.writeJSON() eqvuialent to JSON.stringify() for an object"
+ );
+
+ info("Testing IOUtils.writeJSON() with an array...");
+ await IOUtils.writeJSON(filename, ARRAY);
+ const readArray = await IOUtils.readJSON(filename);
+ const readArrayStr = await IOUtils.readUTF8(filename);
+ ok(ObjectUtils.deepEqual(ARRAY, readArray), "JSON arrays should round-trip");
+ ok(
+ readArrayStr === JSON.stringify(ARRAY),
+ "IOUtils.writeJSON() equivalent to JSON.stringify() for an array"
+ );
+
+ info("Testing IOUtils.writeJSON() with primitives...");
+ for (const primitive of PRIMITIVES) {
+ await IOUtils.writeJSON(filename, primitive);
+ const readPrimitive = await IOUtils.readJSON(filename);
+ const readPrimitiveStr = await IOUtils.readUTF8(filename);
+ ok(
+ primitive === readPrimitive,
+ `${primitive} === ${readPrimitive} -- IOUtils.writeJSON() should round trip primitive`
+ );
+ ok(
+ readPrimitiveStr === JSON.stringify(primitive),
+ `${readPrimitiveStr} === ${JSON.stringify(primitive)} -- IOUtils.writeJSON() equivalent to JSON.stringify for primitive`
+ );
+ }
+
+ info("Testing IOUtils.writeJSON() with unserializable objects...");
+ await Assert.rejects(
+ IOUtils.writeJSON(filename, window),
+ /TypeError: cyclic object value/,
+ "IOUtils.writeJSON() cannot write cyclic objects"
+ );
+
+ await cleanup(filename);
+ });
+
+ add_task(async function test_append_json() {
+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp");
+
+ await IOUtils.writeJSON(filename, OBJECT);
+
+ await Assert.rejects(
+ IOUtils.writeJSON(filename, OBJECT, {mode: "append"}),
+ /NotSupportedError: IOUtils.writeJSON does not support appending to files/,
+ "IOUtils.writeJSON() cannot append"
+ );
+
+ await cleanup(filename);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
new file mode 100644
index 0000000000..196f5d4862
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
@@ -0,0 +1,359 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ // This is an impossible sequence of bytes in an UTF-8 encoded file.
+ // See section 3.5.3 of this text:
+ // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ const invalidUTF8 = Uint8Array.of(0xfe, 0xfe, 0xff, 0xff);
+
+ add_task(async function test_read_utf8_failure() {
+ info("Test attempt to read non-existent file (UTF8)");
+ const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp");
+ await Assert.rejects(
+ IOUtils.readUTF8(doesNotExist),
+ /Could not open the file at .*/,
+ "IOUtils::readUTF8 rejects when file does not exist"
+ );
+
+ info("Test attempt to read invalid UTF-8");
+ const invalidUTF8File = PathUtils.join(PathUtils.tempDir, "invalid_utf8.tmp");
+
+ // Deliberately write the invalid byte sequence to file.
+ await IOUtils.write(invalidUTF8File, invalidUTF8);
+
+ await Assert.rejects(
+ IOUtils.readUTF8(invalidUTF8File),
+ /Could not read file\(.*\) because it is not UTF-8 encoded/,
+ "IOUtils::readUTF8 will reject when reading a file that is not valid UTF-8"
+ );
+
+ await cleanup(invalidUTF8File);
+ });
+
+ add_task(async function test_write_utf8_no_overwrite() {
+ // Make a new file, and try to write to it with overwrites disabled.
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_utf8_overwrite.tmp");
+ const untouchableContents = "Can't touch this!\n";
+ await IOUtils.writeUTF8(tmpFileName, untouchableContents);
+
+ const newContents = "Nah nah nah!\n";
+ await Assert.rejects(
+ IOUtils.writeUTF8(tmpFileName, newContents, {
+ mode: "create",
+ }),
+ /Refusing to overwrite the file at */,
+ "IOUtils::writeUTF8 rejects writing to existing file if overwrites are disabled"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, untouchableContents),
+ "IOUtils::writeUTF8 doesn't change target file when overwrite is refused"
+ );
+
+ const bytesWritten = await IOUtils.writeUTF8(
+ tmpFileName,
+ newContents,
+ { mode: "overwrite" }
+ );
+ is(
+ bytesWritten,
+ newContents.length,
+ "IOUtils::writeUTF8 can overwrite files if specified"
+ );
+ ok(
+ await fileHasTextContents(tmpFileName, newContents),
+ "IOUtils::writeUTF8 overwrites with the expected contents"
+ );
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_with_backup() {
+ info("Test backup file option with non-existing file");
+ let fileContents = "Original file contents";
+ let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_option.tmp");
+ let backupFileName = destFileName + ".backup";
+ let bytesWritten =
+ await IOUtils.writeUTF8(destFileName, fileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::writeUTF8 creates a new file with the correct contents"
+ );
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::writeUTF8 does not create a backup if the target file does not exist"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::write correctly writes to a new file without performing a backup"
+ );
+
+ info("Test backup file option with existing destination");
+ let newFileContents = "New file contents";
+ ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
+ bytesWritten =
+ await IOUtils.writeUTF8(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ });
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can backup an existing file before writing"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::writeUTF8 can create the target with the correct contents"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::writeUTF8 correctly writes to the target after taking a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_write_with_backup_and_tmp() {
+ info("Test backup with tmp and backup file options, non-existing destination");
+ let fileContents = "Original file contents";
+ let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_and_tmp_options.tmp");
+ let backupFileName = destFileName + ".backup";
+ let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp");
+ let bytesWritten =
+ await IOUtils.writeUTF8(destFileName, fileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+ ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile");
+ ok(
+ !await fileExists(backupFileName),
+ "IOUtils::writeUTF8 does not create a backup if the target file does not exist"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ fileContents.length,
+ "IOUtils::writeUTF8 can copy tmp file to destination without performing a backup"
+ );
+
+ info("Test backup with tmp and backup file options, existing destination");
+ let newFileContents = "New file contents";
+ bytesWritten =
+ await IOUtils.writeUTF8(destFileName, newFileContents, {
+ backupFile: backupFileName,
+ tmpPath: tmpFileName,
+ });
+
+ ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile");
+ ok(
+ await fileHasTextContents(backupFileName, "Original file contents"),
+ "IOUtils::writeUTF8 can create a backup if the target file exists"
+ );
+ ok(
+ await fileHasTextContents(destFileName, "New file contents"),
+ "IOUtils::writeUTF8 can write to the destination when a temporary file is used"
+ );
+ is(
+ bytesWritten,
+ newFileContents.length,
+ "IOUtils::writeUTF8 can move tmp file to destination after performing a backup"
+ );
+
+ await cleanup(destFileName, backupFileName);
+ });
+
+ add_task(async function test_empty_read_and_write_utf8() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty_utf8.tmp");
+ const emptyString = ""
+ const bytesWritten = await IOUtils.writeUTF8(
+ tmpFileName,
+ emptyString
+ );
+ is(bytesWritten, 0, "IOUtils::writeUTF8 can create an empty file");
+
+ const nothing = await IOUtils.readUTF8(tmpFileName);
+ is(nothing.length, 0, "IOUtils::readUTF8 can read empty files");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_full_read_and_write_utf8() {
+ // Write a file.
+ info("Test writing emoji file");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_emoji.tmp");
+
+ // Make sure non-ASCII text is supported for writing and reading back.
+ // For fun, a sampling of space-separated emoji characters from different
+ // Unicode versions, including multi-byte glyphs that are rendered using
+ // ZWJ sequences.
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ const expectedBytes = 71;
+ const bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji);
+ is(
+ bytesWritten,
+ expectedBytes,
+ "IOUtils::writeUTF8 can write emoji to file"
+ );
+
+ // Read it back.
+ info("Test reading emoji from file");
+ let fileContents = await IOUtils.readUTF8(tmpFileName);
+ ok(
+ emoji == fileContents &&
+ emoji.length == fileContents.length,
+ "IOUtils::readUTF8 can read back entire file"
+ );
+
+ // Clean up.
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_write_utf8_relative_path() {
+ const tmpFileName = "test_ioutils_write_utf8_relative_path.tmp";
+
+ info("Test writing a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.writeUTF8(tmpFileName, "foo"),
+ /Could not parse path/,
+ "IOUtils::writeUTF8 only works with absolute paths"
+ );
+ });
+
+ add_task(async function test_read_utf8_relative_path() {
+ const tmpFileName = "test_ioutils_read_utf8_relative_path.tmp";
+
+ info("Test reading a file at a relative destination");
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName),
+ /Could not parse path/,
+ "IOUtils::readUTF8 only works with absolute paths"
+ );
+ });
+
+
+ add_task(async function test_utf8_lz4() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4.tmp");
+
+ info("Test writing lz4 encoded UTF-8 string");
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
+ is(bytesWritten, 83, "Expected to write 64 bytes");
+
+ info("Test reading lz4 encoded UTF-8 string");
+ let readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");
+
+ info("Test writing lz4 compressed UTF-8 string");
+ const lotsOfCoffee = new Array(24).fill("☕️").join(""); // ☕️ is 3 bytes in UTF-8: \0xe2 \0x98 \0x95
+ bytesWritten = await IOUtils.writeUTF8(tmpFileName, lotsOfCoffee, { compress: true });
+ console.log(bytesWritten);
+ is(bytesWritten, 28, "Expected 72 bytes to compress to 28 bytes");
+
+ info("Test reading lz4 encoded UTF-8 string");
+ readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readData, lotsOfCoffee, "IOUtils can write and read back UTF-8 LZ4 compressed data");
+
+ info("Test writing empty lz4 compressed UTF-8 string")
+ const empty = "";
+ bytesWritten = await IOUtils.writeUTF8(tmpFileName, empty, { compress: true });
+ is(bytesWritten, 12, "Expected to write just the LZ4 header");
+
+ info("Test reading empty lz4 compressed UTF-8 string")
+ const readEmpty = await IOUtils.readUTF8(tmpFileName, { decompress: true });
+ is(readEmpty, empty, "IOUtils can write and read back empty buffers with LZ4");
+ const readEmptyRaw = await IOUtils.readUTF8(tmpFileName, { decompress: false });
+ is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");
+
+ await cleanup(tmpFileName);
+ });
+
+ add_task(async function test_utf8_lz4_bad_call() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_bad_call.tmp");
+
+ info("readUTF8 ignores the maxBytes option if provided");
+ const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
+ let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
+ is(bytesWritten, 83, "Expected to write 83 bytes");
+
+ let readData = await IOUtils.readUTF8(tmpFileName, { maxBytes: 4, decompress: true });
+ is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");
+
+ await cleanup(tmpFileName)
+ });
+
+ add_task(async function test_utf8_lz4_failure() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_fail.tmp");
+
+ info("Test decompression of non-lz4 UTF-8 string");
+ const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
+ await IOUtils.write(tmpFileName, repeatedBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/,
+ "IOUtils::readUTF8 fails to decompress LZ4 data with a bad header"
+ );
+
+ info("Test UTF-8 decompression of short byte buffer");
+ const elevenBytes = Uint8Array.of(...new Array(11).fill(1));
+ await IOUtils.write(tmpFileName, elevenBytes, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::readUTF8 fails to decompress LZ4 data with missing header"
+ );
+
+ info("Test UTF-8 decompression of valid header, but corrupt contents");
+ const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length
+ const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream.
+ const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents);
+ await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false });
+
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file contents, the file may be corrupt/,
+ "IOUtils::readUTF8 fails to read corrupt LZ4 contents with a correct header"
+ );
+
+ info("Testing decompression of an empty file (no header)");
+ {
+ const n = await IOUtils.writeUTF8(tmpFileName, "");
+ ok(n === 0, "Overwrote with empty file");
+ }
+ await Assert.rejects(
+ IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ /Could not decompress file because the buffer is too short/,
+ "IOUtils::readUTF8 fails to decompress empty files"
+ );
+
+ await cleanup(tmpFileName);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_remove.html b/dom/system/tests/ioutils/test_ioutils_remove.html
new file mode 100644
index 0000000000..85ffa481a4
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_remove.html
@@ -0,0 +1,117 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ add_task(async function test_create_and_remove_file() {
+ info("Test creating and removing a single file");
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp");
+ await IOUtils.write(tmpFileName, new Uint8Array(0));
+ ok(await fileExists(tmpFileName), `Expected file ${tmpFileName} to exist`);
+
+ await IOUtils.remove(tmpFileName);
+ ok(!await fileExists(tmpFileName), "IOUtils::remove can remove files");
+
+ info("Test creating and removing an empty directory");
+ const tempDirName = PathUtils.join(PathUtils.tempDir, "test_ioutils_create_and_remove.tmp.d");
+ await IOUtils.makeDirectory(tempDirName);
+ ok(await dirExists(tempDirName), `Expected directory ${tempDirName} to exist`);
+
+ await IOUtils.remove(tempDirName);
+ ok(!await dirExists(tempDirName), "IOUtils::remove can remove empty directories");
+ });
+
+ add_task(async function test_remove_non_existing() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutil_remove_non_existing.tmp");
+ ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`);
+
+ await IOUtils.remove(tmpFileName, { ignoreAbsent: true });
+ ok(!await fileExists(tmpFileName), "IOUtils::remove can ignore missing files without error");
+
+ await Assert.rejects(
+ IOUtils.remove(tmpFileName, { ignoreAbsent: false }),
+ /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`);
+ });
+
+ add_task(async function test_remove_recursive() {
+ const tmpParentDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove.tmp.d");
+ const tmpChildDir = PathUtils.join(tmpParentDir, "child.tmp.d");
+ const tmpTopLevelFileName = PathUtils.join(tmpParentDir, "top.tmp");
+ const tmpNestedFileName = PathUtils.join(tmpChildDir, "nested.tmp");
+ await createDir(tmpChildDir);
+ await createFile(tmpTopLevelFileName, "");
+ await createFile(tmpNestedFileName, "");
+
+ ok(
+ await fileExists(tmpTopLevelFileName),
+ `Expected file ${tmpTopLevelFileName} to exist`
+ );
+ ok(
+ await fileExists(tmpNestedFileName),
+ `Expected file ${tmpNestedFileName} to exist`
+ );
+
+ await Assert.rejects(
+ IOUtils.remove(tmpParentDir, { recursive: false }),
+ /Could not remove the non-empty directory at .*/,
+ "IOUtils::remove fails if non-recursively removing directory with contents"
+ );
+
+ await IOUtils.remove(tmpParentDir, { recursive: true });
+ ok(
+ !await dirExists(tmpParentDir),
+ "IOUtils::remove can recursively remove a directory"
+ );
+ });
+
+ if (Services.appinfo.OS === "WINNT") {
+ add_task(async function test_remove_retry_readonly() {
+
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "test_ioutils_remove_retry_readonly.tmp.d");
+ const path = PathUtils.join(tmpDir, "file.txt");
+
+ await createDir(tmpDir);
+ await createFile(path, "");
+
+ await IOUtils.setWindowsAttributes(path, { readOnly: true });
+
+ await Assert.rejects(
+ IOUtils.remove(path),
+ /NotAllowedError/,
+ "Cannot remove a readonly file by default"
+ );
+
+ Assert.ok(await fileExists(path), "File should still exist");
+
+ await IOUtils.remove(path, { retryReadonly: true });
+
+ Assert.ok(!await fileExists(path), "File should not exist");
+
+ await IOUtils.remove(tmpDir);
+ });
+ }
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_set_permissions.html b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
new file mode 100644
index 0000000000..36f7dab72a
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_set_permissions.html
@@ -0,0 +1,84 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ add_task(async function test_setPermissions() {
+ const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp");
+
+ await IOUtils.writeUTF8(tempFile, "");
+ await IOUtils.setPermissions(tempFile, 0o421);
+
+ let stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ // setPermissions ignores the x bit on Windows.
+ is(stat.permissions, 0o666, "Permissions munged on Windows");
+ } else {
+ let umask = Services.sysinfo.getProperty("umask");
+ is(stat.permissions, 0o421 & ~umask, "Permissions match");
+ }
+
+ await IOUtils.setPermissions(tempFile, 0o400);
+ stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(stat.permissions, 0o444, "Permissions munged on Windows");
+
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(tempFile, 0o600);
+ } else {
+ is(stat.permissions, 0o400, "Permissions match");
+ }
+
+ await cleanup(tempFile);
+ });
+
+ add_task(async function test_setPermissionsWithoutHonoringUmask() {
+ const tempFile = PathUtils.join(PathUtils.tempDir, "setPermissions.tmp");
+
+ await IOUtils.writeUTF8(tempFile, "");
+ await IOUtils.setPermissions(tempFile, 0o421, false);
+
+ let stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ // setPermissions ignores the x bit on Windows.
+ is(stat.permissions, 0o666, "Permissions munged on Windows");
+ } else {
+ is(stat.permissions, 0o421, "Permissions match");
+ }
+
+ await IOUtils.setPermissions(tempFile, 0o400);
+ stat = await IOUtils.stat(tempFile);
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(stat.permissions, 0o444, "Permissions munged on Windows");
+
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(tempFile, 0o600);
+ } else {
+ is(stat.permissions, 0o400, "Permissions match");
+ }
+
+ await cleanup(tempFile);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
new file mode 100644
index 0000000000..8f8328bd81
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
@@ -0,0 +1,241 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
+
+ add_task(async function test_stat() {
+ info("Test attempt to stat a regular empty file");
+
+ const emptyFileName = PathUtils.join(PathUtils.tempDir, "test_stat_empty.tmp");
+ await createFile(emptyFileName);
+
+ const emptyFileInfo = await IOUtils.stat(emptyFileName);
+ is(emptyFileInfo.size, 0, "IOUtils::stat can get correct (empty) file size");
+ is(emptyFileInfo.path, emptyFileName, "IOUtils::stat result contains the path");
+ is(emptyFileInfo.type, "regular", "IOUtils::stat can stat regular (empty) files");
+ Assert.less(
+ (emptyFileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+
+ info("Test attempt to stat a regular binary file");
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_binary.tmp");
+ const bytes = Uint8Array.of(...new Array(50).keys());
+ await createFile(tempFileName, bytes);
+
+ const fileInfo = await IOUtils.stat(tempFileName);
+ is(fileInfo.size, 50, "IOUtils::stat can get correct file size");
+ is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+ is(fileInfo.type, "regular", "IOUtils::stat can stat regular files");
+ Assert.less(
+ (fileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+
+ info("Test attempt to stat a directory");
+ const tempDirName = PathUtils.join(PathUtils.tempDir, "test_stat_dir.tmp.d");
+ await IOUtils.makeDirectory(tempDirName);
+
+ const dirInfo = await IOUtils.stat(tempDirName);
+ is(dirInfo.size, -1, "IOUtils::stat reports -1 size for directories")
+ is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+ is(fileInfo.type, "regular", "IOUtils::stat can stat directories");
+ Assert.less(
+ (fileInfo.lastModified - new Date().valueOf()),
+ 1000, // Allow for 1 second deviation in case of slow tests.
+ "IOUtils::stat can get the last modification date for a regular file"
+ );
+ Assert.less(
+ (fileInfo.lastAccessed - new Date().valueOf()),
+ 1000,
+ "IOUtils::stat can get the last access date for a regular file"
+ );
+
+ await cleanup(emptyFileName, tempFileName, tempDirName)
+ });
+
+ add_task(async function test_stat_failures() {
+ info("Test attempt to stat a non-existing file");
+
+ const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_stat_not_exists.tmp");
+
+ await Assert.rejects(
+ IOUtils.stat(notExistsFile),
+ /Could not stat file\(.*\) because it does not exist/,
+ "IOUtils::stat throws if the target file does not exist"
+ );
+ });
+
+ add_task(async function test_setModificationTime_and_stat() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp");
+ {
+ info("Test attempt to setModificationTime a file");
+ await createFile(tmpFileName);
+
+ const oldFileInfo = await IOUtils.stat(tmpFileName);
+ await sleep(500);
+
+ // Now update the time stamp.
+ const stamp = await IOUtils.setModificationTime(tmpFileName);
+ const newFileInfo = await IOUtils.stat(tmpFileName);
+
+ ok(
+ newFileInfo.lastModified > oldFileInfo.lastModified,
+ "IOUtils::setModificationTime can update the lastModified time stamp on the file system"
+ );
+ is(
+ stamp,
+ newFileInfo.lastModified,
+ "IOUtils::setModificationTime returns the updated time stamp."
+ );
+ is(
+ newFileInfo.lastAccessed,
+ oldFileInfo.lastAccessed,
+ "IOUtils::setModificationTime does not change lastAccessed"
+ );
+
+ await sleep(500);
+
+ const newerStamp = await IOUtils.setAccessTime(tmpFileName);
+ const newerFileInfo = await IOUtils.stat(tmpFileName);
+
+ ok(
+ newerFileInfo.lastAccessed > newFileInfo.lastAccessed,
+ "IOUtils::setAccessTime can update the lastAccessed time stamp on the file system"
+ );
+ is(
+ newerStamp,
+ newerFileInfo.lastAccessed,
+ "IOUtils::setAccessTime returns the updated time stamp."
+ );
+ is(
+ newerFileInfo.lastModified,
+ newFileInfo.lastModified,
+ "IOUtils::setAccessTime does not change lastModified"
+ );
+ }
+
+ const tmpDirName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_and_stat.tmp.d");
+ {
+ info("Test attempt to setModificationTime a directory");
+ await createDir(tmpDirName);
+
+ const oldFileInfo = await IOUtils.stat(tmpDirName);
+ await sleep(500);
+
+ const stamp = await IOUtils.setModificationTime(tmpDirName);
+ const newFileInfo = await IOUtils.stat(tmpDirName);
+
+ ok(
+ newFileInfo.lastModified > oldFileInfo.lastModified,
+ "IOUtils::setModificationTime can update the lastModified time stamp on a directory"
+ );
+ is(
+ stamp,
+ newFileInfo.lastModified,
+ "IOUtils::setModificationTime returns the updated time stamp on a directory"
+ );
+ }
+
+ await cleanup(tmpFileName, tmpDirName);
+ });
+
+ add_task(async function test_setModificationTime_custom_mod_time() {
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_custom_mod_time.tmp");
+ await createFile(tempFileName);
+ const originalInfo = await IOUtils.stat(tempFileName);
+ const now = originalInfo.lastModified;
+
+ const oneMinute = 60 * 1000; // milliseconds
+
+ info("Test attempt to set modification time to the future");
+ const future = now + oneMinute;
+ let newModTime = await IOUtils.setModificationTime(tempFileName, future);
+ const futureInfo = await IOUtils.stat(tempFileName);
+ Assert.less(originalInfo.lastModified, futureInfo.lastModified, "IOUtils::setModificationTime can set a future modification time for the file");
+
+ is(newModTime, futureInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp");
+ is(newModTime, future, "IOUtils::setModificationTime return value matches the argument value exactly");
+
+ info("Test attempt to set modification time to the past");
+ const past = now - 2 * oneMinute;
+ newModTime = await IOUtils.setModificationTime(tempFileName, past);
+ const pastInfo = await IOUtils.stat(tempFileName);
+ Assert.greater(originalInfo.lastModified, pastInfo.lastModified, "IOUtils::setModificationTime can set a past modification time for the file");
+
+ is(newModTime, pastInfo.lastModified, "IOUtils::setModificationTime returns the updated time stamp");
+ is(newModTime, past, "IOUtils::setModificationTime return value matches the argument value exactly");
+
+ await cleanup(tempFileName);
+ });
+
+ add_task(async function test_stat_btime() {
+ if (["Darwin", "WINNT"].includes(Services.appinfo.OS)) {
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_stat_btime.tmp");
+ await createFile(tempFileName);
+ const originalInfo = await IOUtils.stat(tempFileName);
+
+ const future = originalInfo.lastModified + 6000;
+ await IOUtils.setModificationTime(tempFileName, future);
+ const futureInfo = await IOUtils.stat(tempFileName);
+
+ ok(originalInfo.hasOwnProperty("creationTime"), "originalInfo has creationTime field");
+ ok(originalInfo.creationTime !== undefined && originalInfo.creationTime !== null, "originalInfo has non-null creationTime");
+
+ ok(futureInfo.hasOwnProperty("creationTime"), "futureInfo has creationTime field");
+ ok(futureInfo.creationTime !== undefined && futureInfo.creationTime !== null, "futureInfo has non-null creationTime");
+
+ is(originalInfo.creationTime, futureInfo.creationTime, "creationTime matches");
+
+ await cleanup(tempFileName);
+ } else {
+ ok(true, `skipping test_stat_btime() on unsupported platform ${Services.appinfo.OS}`);
+ }
+ });
+
+ add_task(async function test_setModificationTime_failures() {
+ info("Test attempt to setModificationTime a non-existing file");
+ const notExistsFile = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_not_exists.tmp");
+
+ await Assert.rejects(
+ IOUtils.setModificationTime(notExistsFile),
+ /Could not set modification time of file\(.*\) because it does not exist/,
+ "IOUtils::setModificationTime throws if the target file does not exist"
+ );
+
+ info("Test attempt to set modification time to Epoch");
+ const tempFileName = PathUtils.join(PathUtils.tempDir, "test_setModificationTime_epoch.tmp");
+ await createFile(tempFileName);
+
+ await Assert.rejects(
+ IOUtils.setModificationTime(tempFileName, 0),
+ /Refusing to set the modification time of file\(.*\) to 0/,
+ "IOUtils::setModificationTime cannot set the file modification time to Epoch"
+ );
+
+ await cleanup(tempFileName);
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
new file mode 100644
index 0000000000..b452b4f6db
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
@@ -0,0 +1,135 @@
+<!-- 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>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+ add_task(async function getSetWindowsAttributes() {
+ const tmpDir = PathUtils.join(PathUtils.tempDir, "ioutils-windows-attributes.tmp.d");
+ await createDir(tmpDir);
+ ok(await dirExists(tmpDir), `Expected ${tmpDir} to be a directory`);
+
+ const filePath = PathUtils.join(tmpDir, "file.tmp");
+ await createFile(filePath);
+ ok(await fileExists(filePath), `Expected ${filePath} to exist`);
+
+ {
+ info("Getting attributes for newly created file.");
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Newly created file ${filePath} is not a read-only file`);
+ ok(attrs.hidden === false, `Newly created file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Newly created file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting read-only on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ info("Attempting to write to a read-only file.");
+
+ await Assert.rejects(
+ IOUtils.writeUTF8(filePath, "hello, world"),
+ /NotAllowedError: Could not open the file at .+ for writing/,
+ "IOUtils::writeUTF8 on a read-only file fails."
+ );
+
+ {
+ info("Setting hidden on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { hidden: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting system on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { system: true })
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is still a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is a system file`);
+ }
+
+ {
+ info("Clearing all Windows attributes on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: false, hidden: false, system: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is not a system file`);
+ }
+
+ {
+ info("Setting all Windows attributes on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: true, hidden: true, system: true });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === true, `Updated file ${filePath} is a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is a system file`);
+ }
+
+ {
+ info("Clearing read-only on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { readOnly: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is no longer a read-only file`);
+ ok(attrs.hidden === true, `Updated file ${filePath} is still a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is still a system file`);
+ }
+
+ {
+ info("Clearing hidden on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { hidden: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is no longer a hidden file`);
+ ok(attrs.system === true, `Updated file ${filePath} is still a system file`);
+ }
+
+ {
+ info("Clearing system on an existing file.");
+ await IOUtils.setWindowsAttributes(filePath, { system: false });
+ const attrs = await IOUtils.getWindowsAttributes(filePath);
+
+ ok(attrs.readOnly === false, `Updated file ${filePath} is still not a read-only file`);
+ ok(attrs.hidden === false, `Updated file ${filePath} is sitll not a hidden file`);
+ ok(attrs.system === false, `Updated file ${filePath} is no longer a system file`);
+ }
+
+ await cleanup(tmpDir);
+ });
+
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
diff --git a/dom/system/tests/ioutils/test_ioutils_worker.xhtml b/dom/system/tests/ioutils/test_ioutils_worker.xhtml
new file mode 100644
index 0000000000..df67d48676
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_worker.xhtml
@@ -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>