summaryrefslogtreecommitdiffstats
path: root/dom/system/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/system/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/system/tests')
-rw-r--r--dom/system/tests/chrome.toml6
-rw-r--r--dom/system/tests/file_bug1197901.html16
-rw-r--r--dom/system/tests/ioutils/chrome.toml39
-rw-r--r--dom/system/tests/ioutils/file_ioutils_test_fixtures.js78
-rw-r--r--dom/system/tests/ioutils/file_ioutils_worker.js219
-rw-r--r--dom/system/tests/ioutils/test_ioutils.html26
-rw-r--r--dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html55
-rw-r--r--dom/system/tests/ioutils/test_ioutils_copy_move.html360
-rw-r--r--dom/system/tests/ioutils/test_ioutils_create_unique.html86
-rw-r--r--dom/system/tests/ioutils/test_ioutils_dir_iteration.html96
-rw-r--r--dom/system/tests/ioutils/test_ioutils_getfile.html84
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mac_xattr.html91
-rw-r--r--dom/system/tests/ioutils/test_ioutils_mkdir.html135
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write.html524
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_json.html193
-rw-r--r--dom/system/tests/ioutils/test_ioutils_read_write_utf8.html384
-rw-r--r--dom/system/tests/ioutils/test_ioutils_remove.html118
-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.html242
-rw-r--r--dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html137
-rw-r--r--dom/system/tests/ioutils/test_ioutils_worker.xhtml40
-rw-r--r--dom/system/tests/mochitest.toml11
-rw-r--r--dom/system/tests/pathutils_worker.js37
-rw-r--r--dom/system/tests/test_bug1197901.html96
-rw-r--r--dom/system/tests/test_pathutils.html602
-rw-r--r--dom/system/tests/test_pathutils_worker.xhtml38
26 files changed, 3797 insertions, 0 deletions
diff --git a/dom/system/tests/chrome.toml b/dom/system/tests/chrome.toml
new file mode 100644
index 0000000000..87f782b465
--- /dev/null
+++ b/dom/system/tests/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+["test_pathutils.html"]
+
+["test_pathutils_worker.xhtml"]
+support-files = ["pathutils_worker.js"]
diff --git a/dom/system/tests/file_bug1197901.html b/dom/system/tests/file_bug1197901.html
new file mode 100644
index 0000000000..e4ca9fd380
--- /dev/null
+++ b/dom/system/tests/file_bug1197901.html
@@ -0,0 +1,16 @@
+<pre>Sensor events testing</pre>
+<script>
+
+window.onmessage = function(event) {
+ if (event.data.command == "addEventListener") {
+ window.addEventListener(
+ "devicemotion", function() {
+ event.source.postMessage({ result: event.data.expected,
+ message: event.data.message },
+ "*");
+ }
+ );
+ }
+};
+
+</script>
diff --git a/dom/system/tests/ioutils/chrome.toml b/dom/system/tests/ioutils/chrome.toml
new file mode 100644
index 0000000000..16434b5bb6
--- /dev/null
+++ b/dom/system/tests/ioutils/chrome.toml
@@ -0,0 +1,39 @@
+[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_set_permissions.html"]
+
+["test_ioutils_stat_set_modification_time.html"]
+
+["test_ioutils_windows_file_attributes.html"]
+skip-if = ["(os != 'win')"]
+
+["test_ioutils_worker.xhtml"]
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..e367eb4d99
--- /dev/null
+++ b/dom/system/tests/ioutils/file_ioutils_worker.js
@@ -0,0 +1,219 @@
+// Any copyright is dedicated to the Public Domain.
+// - http://creativecommons.org/publicdomain/zero/1.0/
+
+// Portions of this file are originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+/* eslint-env worker */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+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(
+ _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(
+ _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);
+ }
+};
+
+// This is copied from the ObjectUtils module, as it is difficult to translate
+// file_ioutils_test_fixtures.js into a ES module and have it used in non-module
+// contexts.
+
+// ... Start of previously MIT-licensed code.
+// This deepEqual implementation is originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+function _deepEqual(a, b) {
+ // The numbering below refers to sections in the CommonJS spec.
+
+ // 7.1 All identical values are equivalent, as determined by ===.
+ if (a === b) {
+ return true;
+ // 7.2 If the b value is a Date object, the a value is
+ // equivalent if it is also a Date object that refers to the same time.
+ }
+ let aIsDate = instanceOf(a, "Date");
+ let bIsDate = instanceOf(b, "Date");
+ if (aIsDate || bIsDate) {
+ if (!aIsDate || !bIsDate) {
+ return false;
+ }
+ if (isNaN(a.getTime()) && isNaN(b.getTime())) {
+ return true;
+ }
+ return a.getTime() === b.getTime();
+ // 7.3 If the b value is a RegExp object, the a value is
+ // equivalent if it is also a RegExp object with the same source and
+ // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).
+ }
+ let aIsRegExp = instanceOf(a, "RegExp");
+ let bIsRegExp = instanceOf(b, "RegExp");
+ if (aIsRegExp || bIsRegExp) {
+ return (
+ aIsRegExp &&
+ bIsRegExp &&
+ a.source === b.source &&
+ a.global === b.global &&
+ a.multiline === b.multiline &&
+ a.lastIndex === b.lastIndex &&
+ a.ignoreCase === b.ignoreCase
+ );
+ // 7.4 Other pairs that do not both pass typeof value == "object",
+ // equivalence is determined by ==.
+ }
+ if (typeof a != "object" || typeof b != "object") {
+ return a == b;
+ }
+ // 7.5 For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical 'prototype' property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ return objEquiv(a, b);
+}
+
+function instanceOf(object, type) {
+ return Object.prototype.toString.call(object) == "[object " + type + "]";
+}
+
+function isUndefinedOrNull(value) {
+ return value === null || value === undefined;
+}
+
+function isArguments(object) {
+ return instanceOf(object, "Arguments");
+}
+
+function objEquiv(a, b) {
+ if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
+ return false;
+ }
+ // An identical 'prototype' property.
+ if ((a.prototype || undefined) != (b.prototype || undefined)) {
+ return false;
+ }
+ // Object.keys may be broken through screwy arguments passing. Converting to
+ // an array solves the problem.
+ if (isArguments(a)) {
+ if (!isArguments(b)) {
+ return false;
+ }
+ a = Array.prototype.slice.call(a);
+ b = Array.prototype.slice.call(b);
+ return _deepEqual(a, b);
+ }
+ let ka, kb;
+ try {
+ ka = Object.keys(a);
+ kb = Object.keys(b);
+ } catch (e) {
+ // Happens when one is a string literal and the other isn't
+ return false;
+ }
+ // Having the same number of owned properties (keys incorporates
+ // hasOwnProperty)
+ if (ka.length != kb.length) {
+ return false;
+ }
+ // The same set of keys (although not necessarily the same order),
+ ka.sort();
+ kb.sort();
+ // Equivalent values for every corresponding key, and possibly expensive deep
+ // test
+ for (let key of ka) {
+ if (!_deepEqual(a[key], b[key])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// ... End of previously MIT-licensed code.
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..7a98f83a1f
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_compute_hex_digest.html
@@ -0,0 +1,55 @@
+
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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 = [
+ "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
+ "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086b343d7a1366838083e4b8f3ca6fd3c80",
+ "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de84eacdc8c62fe34ee4e12b4b1428817f09b6a2760c3f8a664ceae94d2434a593",
+ ];
+ const ALGORITHMS = ["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..408bb82f39
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_copy_move.html
@@ -0,0 +1,360 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..be7ab23697
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_create_unique.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 src="file_ioutils_test_fixtures.js"></script>
+ <script>
+ "use strict";
+
+ 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..54168235b0
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_dir_iteration.html
@@ -0,0 +1,96 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..077f0b5c1c
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_getfile.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>
+ "use strict";
+
+ 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..6af9b2e6f8
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mac_xattr.html
@@ -0,0 +1,91 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..6827b24cc6
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_mkdir.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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..2243eb1eda
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write.html
@@ -0,0 +1,524 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+ const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+ );
+
+ 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..0acb191e1b
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_json.html
@@ -0,0 +1,193 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+ const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+ );
+
+ 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);
+ });
+
+ add_task(async function test_read_json_bom() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_json_bom.tmp");
+ const raw = `\uFEFF${JSON.stringify({hello: "world"})}`;
+ await IOUtils.writeUTF8(tmpFileName, raw);
+
+ ok(
+ ObjectUtils.deepEqual(
+ await IOUtils.readJSON(tmpFileName),
+ { hello: "world" },
+ ),
+ "IOUtils.readJSON should skip BOM"
+ );
+
+ await IOUtils.writeUTF8(tmpFileName, raw, { compress: true });
+
+ ok(
+ ObjectUtils.deepEqual(
+ await IOUtils.readJSON(tmpFileName, { decompress: true }),
+ { hello: "world" },
+ ),
+ "IOUtils.readJSON should skip BOM for compressed 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_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
new file mode 100644
index 0000000000..cdea016732
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
@@ -0,0 +1,384 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ // 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);
+ });
+
+ add_task(async function test_skipBOM() {
+ const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_readutf8_bom.tmp");
+
+ const raw = `\uFEFFstring`;
+
+ await IOUtils.writeUTF8(tmpFileName, raw);
+
+ is(
+ await IOUtils.readUTF8(tmpFileName),
+ "string",
+ "IOUtils.readUTF8 should skip BOM by default"
+ );
+
+ await IOUtils.writeUTF8(tmpFileName, raw, { compress: true });
+
+ is(
+ await IOUtils.readUTF8(tmpFileName, { decompress: true }),
+ "string",
+ "IOUtils.readUTF8 should skip BOM by default for compressed 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..f368fc09d3
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_remove.html
@@ -0,0 +1,118 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..e508817a41
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_stat_set_modification_time.html
@@ -0,0 +1,242 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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..a5b72bd078
--- /dev/null
+++ b/dom/system/tests/ioutils/test_ioutils_windows_file_attributes.html
@@ -0,0 +1,137 @@
+<!-- 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.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ 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>
diff --git a/dom/system/tests/mochitest.toml b/dom/system/tests/mochitest.toml
new file mode 100644
index 0000000000..b607853077
--- /dev/null
+++ b/dom/system/tests/mochitest.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = "condprof"
+scheme = "https"
+
+support-files = ["file_bug1197901.html"]
+
+["test_bug1197901.html"]
+skip-if = [
+ "os == 'android'",
+ "condprof", #: "Only focused window should get the sensor events"
+]
diff --git a/dom/system/tests/pathutils_worker.js b/dom/system/tests/pathutils_worker.js
new file mode 100644
index 0000000000..9e4742b764
--- /dev/null
+++ b/dom/system/tests/pathutils_worker.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env worker */
+
+"use strict";
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */
+importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
+
+self.onmessage = async function (message) {
+ let expected = message.data;
+ info("ON message");
+ info(JSON.stringify(expected));
+ const profileDir = await PathUtils.getProfileDir();
+ is(
+ profileDir,
+ expected.profileDir,
+ "PathUtils.profileDir() in a worker should match PathUtils.profileDir on main thread"
+ );
+
+ const localProfileDir = await PathUtils.getLocalProfileDir();
+ is(
+ localProfileDir,
+ expected.localProfileDir,
+ "PathUtils.getLocalProfileDir() in a worker should match PathUtils.localProfileDir on main thread"
+ );
+
+ const tempDir = await PathUtils.getTempDir();
+ is(
+ tempDir,
+ expected.tempDir,
+ "PathUtils.getTempDir() in a worker should match PathUtils.tempDir on main thread"
+ );
+
+ finish();
+};
diff --git a/dom/system/tests/test_bug1197901.html b/dom/system/tests/test_bug1197901.html
new file mode 100644
index 0000000000..7e1866ffa3
--- /dev/null
+++ b/dom/system/tests/test_bug1197901.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1197901
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1197901</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1197901 **/
+ SimpleTest.requestFlakyTimeout("requestFlakyTimeout is silly");
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ SimpleTest.waitForFocus(function() {
+ SpecialPowers.pushPrefEnv({"set": [["device.sensors.test.events", true]]},
+ doTest);
+ }, window);
+ };
+
+ function doTest() {
+ window.onmessage = function(event) {
+ ok(event.data.result, event.data.message);
+ };
+
+ // Only same-origin iframe should get the events.
+ var xo = document.getElementById("cross-origin");
+ xo.contentWindow.postMessage(
+ { command: "addEventListener",
+ expected: false,
+ message: "Cross-origin iframe shouldn't get the sensor events."},
+ "*");
+
+ var so = document.getElementById("same-origin");
+ so.contentWindow.postMessage(
+ { command: "addEventListener",
+ expected: true,
+ message: "Same-origin iframe should get the sensor events." },
+ "*");
+
+ // We need a timeout here to check that something does not happen.
+ setTimeout(function() {
+ so.remove();
+ xo.remove();
+ doWindowTest();
+ }, 500);
+ }
+
+ function doWindowTest() {
+ var win = window.open("file_bug1197901.html", "w1", "height=100,width=100");
+ win.onload = function() {
+ win.focus();
+ SimpleTest.waitForFocus(function() {
+ var win2 = window.open("file_bug1197901.html", "w2", "height=100,width=100,left=100");
+ win2.onload = function() {
+ win2.focus();
+ SimpleTest.waitForFocus(function() {
+ // Only focused window should get the events.
+ win.postMessage(
+ { command: "addEventListener",
+ expected: false,
+ message: "Only focused window should get the sensor events." },
+ "*");
+ win2.postMessage(
+ { command: "addEventListener",
+ expected: true,
+ message: "Focused window should get the sensor events." },
+ "*");
+ setTimeout(function() {
+ window.onmessage = null;
+ win.close();
+ win2.close();
+ SimpleTest.finish();
+ }, 500);
+ }, win2);
+ };
+ }, win);
+ };
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<iframe src="file_bug1197901.html" id="same-origin"></iframe>
+<iframe src="http://example.com/tests/dom/system/tests/file_bug1197901.html" id="cross-origin"></iframe>
+</body>
+</html>
diff --git a/dom/system/tests/test_pathutils.html b/dom/system/tests/test_pathutils.html
new file mode 100644
index 0000000000..4391de77fd
--- /dev/null
+++ b/dom/system/tests/test_pathutils.html
@@ -0,0 +1,602 @@
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>PathUtils tests</title>
+</head>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script>
+<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+<script>
+ "use strict";
+
+ const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ const UNRECOGNIZED_PATH = /Could not initialize path: NS_ERROR_FILE_UNRECOGNIZED_PATH/;
+ const SPLIT_RELATIVE_ABSOLUTE = /PathUtils.splitRelative requires a relative path/;
+ const SPLIT_RELATIVE_EMPTY = /PathUtils.splitRelative: Empty directory components \(""\) not allowed by options/;
+ const SPLIT_RELATIVE_PARENT = /PathUtils.splitRelative: Parent directory components \("\.\."\) not allowed by options/;
+ const SPLIT_RELATIVE_CURRENT = /PathUtils.splitRelative: Current directory components \("\."\) not allowed by options/;
+ const EMPTY_PATH = /PathUtils does not support empty paths/;
+ const JOIN = /Could not append to path/;
+
+ add_task(function test_filename() {
+ Assert.throws(
+ () => PathUtils.filename(""),
+ EMPTY_PATH,
+ "PathUtils.filename() does not support empty paths"
+ );
+ Assert.throws(
+ () => PathUtils.filename("foo.txt"),
+ UNRECOGNIZED_PATH,
+ "PathUtils.filename() does not support relative paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.filename("C:"),
+ "C:",
+ "PathUtils.filename() with a drive path"
+ );
+ is(
+ PathUtils.filename("C:\\"),
+ "C:",
+ "PathUtils.filename() with a drive path"
+ );
+ is(
+ PathUtils.filename("C:\\Windows"),
+ "Windows",
+ "PathUtils.filename() with a path with 2 components"
+ );
+ is(
+ PathUtils.filename("C:\\Windows\\"),
+ "Windows",
+ "PathUtils.filename() with a path with 2 components and a trailing slash"
+ );
+ is(
+ PathUtils.filename("C:\\Windows\\System32"),
+ "System32",
+ "PathUtils.filename() with a path with 3 components"
+ );
+ is(
+ PathUtils.filename("\\\\server"),
+ "\\\\server",
+ "PathUtils.filename() with a UNC server path"
+ );
+ is(
+ PathUtils.filename("C:\\file.dat"),
+ "file.dat",
+ "PathUtils.filename() with a file path"
+ );
+ } else {
+ is(
+ PathUtils.filename("/"),
+ "/",
+ "PathUtils.filename() with a root path"
+ );
+ is(
+ PathUtils.filename("/usr/"),
+ "usr",
+ "PathUtils.filename() with a non-root path"
+ );
+ is(
+ PathUtils.filename("/usr/lib/libfoo.so"),
+ "libfoo.so",
+ "PathUtils.filename() with a path with 3 components"
+ );
+ }
+ });
+
+ add_task(function test_parent() {
+ Assert.throws(
+ () => PathUtils.parent("."),
+ UNRECOGNIZED_PATH,
+ "PathUtils.parent() does not support relative paths"
+ );
+ Assert.throws(
+ () => PathUtils.parent(""),
+ EMPTY_PATH,
+ "PathUtils.parent() does not support empty paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.parent("C:"),
+ null,
+ "PathUtils.parent() with a drive path"
+ );
+ is(
+ PathUtils.parent("\\\\server"),
+ null,
+ "PathUtils.parent() with a UNC server path"
+ );
+ is(
+ PathUtils.parent("\\\\server\\foo"),
+ "\\\\server",
+ "PathUtils.parent() with a UNC server path and child component"
+ );
+
+ Assert.throws(
+ () => PathUtils.parent("C:", -1),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a negative depth throws"
+ );
+ Assert.throws(
+ () => PathUtils.parent("C:", 0),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a zero depth throws"
+ );
+
+ {
+ const path = "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles\\foo.default";
+
+ const expected = [
+ "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox\\Profiles",
+ "C:\\Users\\User\\AppData\\Local\\Mozilla\\Firefox",
+ "C:\\Users\\User\\AppData\\Local\\Mozilla",
+ "C:\\Users\\User\\AppData\\Local",
+ "C:\\Users\\User\\AppData",
+ "C:\\Users\\User",
+ "C:\\Users",
+ "C:",
+ null,
+ ];
+
+ for (const [i, parent] of expected.entries()) {
+ is(PathUtils.parent(path, i + 1), parent, `PathUtils.parent() with depth=${i + 1}`)
+ }
+ }
+ } else {
+ is(
+ PathUtils.parent("/"),
+ null,
+ "PathUtils.parent() with a root path"
+ );
+ is(
+ PathUtils.parent("/var"),
+ "/",
+ "PathUtils.parent() with a 2 component path"
+ );
+ is(
+ PathUtils.parent("/var/run"),
+ "/var",
+ "PathUtils.parent() with a 3 component path"
+ );
+
+ Assert.throws(
+ () => PathUtils.parent("/", -1),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a negative depth throws"
+ );
+ Assert.throws(
+ () => PathUtils.parent("/", 0),
+ /^NotSupportedError: PathUtils.parent: A depth of at least 1 is required/,
+ "PathUtils.parent() with a zero depth throws"
+ );
+
+ {
+ const path = "/home/user/.mozilla/firefox/foo.default";
+ const expected = [
+ "/home/user/.mozilla/firefox",
+ "/home/user/.mozilla",
+ "/home/user",
+ "/home",
+ "/",
+ null,
+ ];
+
+ for (const [i, parent] of expected.entries()) {
+ is(
+ PathUtils.parent(path, i + 1),
+ parent,
+ `PathUtils.parent() with depth=${i + 1}`
+ );
+ }
+ }
+ }
+ });
+
+ add_task(function test_join() {
+ Assert.throws(
+ () => PathUtils.join(),
+ EMPTY_PATH,
+ "PathUtils.join() does not support empty paths"
+ );
+ Assert.throws(
+ () => PathUtils.join(""),
+ EMPTY_PATH,
+ "PathUtils.join() does not support empty paths"
+ );
+ Assert.throws(
+ () => PathUtils.join("foo", "bar"),
+ UNRECOGNIZED_PATH,
+ "PathUtils.join() does not support relative paths"
+ );
+ Assert.throws(
+ () => PathUtils.join("."),
+ UNRECOGNIZED_PATH,
+ "PathUtils.join() does not support relative paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.join("C:"),
+ "C:",
+ "PathUtils.join() with a single path"
+ );
+ is(
+ PathUtils.join("C:\\Windows", "System32"),
+ "C:\\Windows\\System32",
+ "PathUtils.join() with a 2 component path and an additional component"
+ );
+ is(
+ PathUtils.join("C:", "Users", "Example"),
+ "C:\\Users\\Example",
+ "PathUtils.join() with a root path and two additional components"
+ );
+ is(
+ PathUtils.join("\\\\server", "Files", "Example.dat"),
+ "\\\\server\\Files\\Example.dat",
+ "PathUtils.join() with a server path"
+ );
+ } else {
+ is(
+ PathUtils.join("/"),
+ "/",
+ "PathUtils.join() with a root path"
+ );
+ is(
+ PathUtils.join("/usr", "lib"),
+ "/usr/lib",
+ "PathUtils.join() with a 2 component path and an additional component"
+ );
+ is(
+ PathUtils.join("/", "home", "example"),
+ "/home/example",
+ "PathUtils.join() with a root path and two additional components"
+ );
+ }
+ });
+
+ add_task(function test_join_relative() {
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.joinRelative("C:", ""),
+ "C:",
+ "PathUtils.joinRelative() with an empty relative path"
+ );
+
+ is(
+ PathUtils.joinRelative("C:", "foo\\bar\\baz"),
+ "C:\\foo\\bar\\baz",
+ "PathUtils.joinRelative() with a relative path containing path separators"
+ );
+ } else {
+ is(
+ PathUtils.joinRelative("/", ""),
+ "/",
+ "PathUtils.joinRelative() with an empty relative path"
+ );
+
+ is(
+ PathUtils.joinRelative("/", "foo/bar/baz"),
+ "/foo/bar/baz",
+ "PathUtils.joinRelative() with a relative path containing path separators"
+ );
+ }
+ });
+
+ add_task(async function test_normalize() {
+ Assert.throws(
+ () => PathUtils.normalize(""),
+ EMPTY_PATH,
+ "PathUtils.normalize() does not support empty paths"
+ );
+ Assert.throws(
+ () => PathUtils.normalize("."),
+ UNRECOGNIZED_PATH,
+ "PathUtils.normalize() does not support relative paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.normalize("C:\\\\Windows\\\\..\\\\\\.\\Users\\..\\Windows"),
+ "C:\\Windows",
+ "PathUtils.normalize() with a non-normalized path"
+ );
+ } else {
+ // nsLocalFileUnix::Normalize() calls realpath, which resolves symlinks
+ // and requires the file to exist.
+ //
+ // On Darwin, the temp directory is located in `/private/var`, which is a
+ // symlink to `/var`, so we need to pre-normalize our temporary directory
+ // or expected paths won't match.
+ const tmpDir = PathUtils.join(
+ PathUtils.normalize(PathUtils.tempDir),
+ "pathutils_test"
+ );
+
+ await IOUtils.makeDirectory(tmpDir, { ignoreExisting: true });
+ info(`created tmpDir ${tmpDir}`);
+ SimpleTest.registerCleanupFunction(async () => {
+ await IOUtils.remove(tmpDir, {
+ recursive: true,
+ });
+ });
+
+ await IOUtils.makeDirectory(PathUtils.join(tmpDir, "foo", "bar"), {
+ createAncestors: true,
+ });
+
+ is(
+ PathUtils.normalize("/"),
+ "/",
+ "PathUtils.normalize() with a normalized path"
+ );
+
+ is(
+ PathUtils.normalize(
+ PathUtils.join(
+ tmpDir,
+ "foo",
+ ".",
+ "bar",
+ ".",
+ )
+ ),
+ PathUtils.join(tmpDir, "foo", "bar"),
+ "PathUtils.normalize() with a non-normalized path"
+ );
+ }
+ });
+
+ add_task(function test_split() {
+ Assert.throws(
+ () => PathUtils.split("foo"),
+ UNRECOGNIZED_PATH,
+ "PathUtils.split() does not support relative paths"
+ );
+ Assert.throws(
+ () => PathUtils.split(""),
+ EMPTY_PATH,
+ "PathUtils.split() does not support empty paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ Assert.deepEqual(
+ PathUtils.split("C:\\Users\\Example"),
+ ["C:", "Users", "Example"],
+ "PathUtils.split() on an absolute path"
+ );
+
+ Assert.deepEqual(
+ PathUtils.split("C:\\Users\\Example\\"),
+ ["C:", "Users", "Example"],
+ "PathUtils.split() on an absolute path with a trailing slash"
+ );
+
+ Assert.deepEqual(
+ PathUtils.split("\\\\server\\Files\\Example.dat"),
+ ["\\\\server", "Files", "Example.dat"],
+ "PathUtils.split() with a server as the root"
+ );
+ } else {
+ Assert.deepEqual(
+ PathUtils.split("/home/foo"),
+ ["/", "home", "foo"],
+ "PathUtils.split() on absolute path"
+ );
+
+ Assert.deepEqual(
+ PathUtils.split("/home/foo/"),
+ ["/", "home", "foo"],
+ "PathUtils.split() on absolute path with trailing slash"
+ );
+ }
+ });
+
+ add_task(function test_splitRelative() {
+ Assert.throws(
+ () => PathUtils.splitRelative(""),
+ EMPTY_PATH,
+ "PathUtils.splitRelative() should throw with empty path"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ Assert.throws(
+ () => PathUtils.splitRelative("C:\\"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with a drive path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("\\\\server\\share\\"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with a UNC path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("foo\\\\bar"),
+ SPLIT_RELATIVE_EMPTY,
+ "PathUtils.splitRelative() should throw with empty component by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\\\bar", { allowEmpty: true }),
+ ["foo", "", "bar"],
+ "PathUtils.splitRelative() with an empty component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component and a trailing slash"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo\\bar"),
+ ["foo", "bar"],
+ "PathUtils.splitRelative() on a relative path with two components"
+ );
+ } else {
+ Assert.throws(
+ () => PathUtils.splitRelative("/foo/bar"),
+ SPLIT_RELATIVE_ABSOLUTE,
+ "PathUtils.splitRelative() should throw with an absolute path"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative("foo//bar"),
+ SPLIT_RELATIVE_EMPTY,
+ "PathUtils.splitRelative() should throw with empty component by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo//bar", { allowEmpty: true }),
+ ["foo", "", "bar"],
+ "PathUtils.splitRelative() with an empty component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo/"),
+ ["foo"],
+ "PathUtils.splitRelative() on a relative path with one component and a trailing slash"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("foo/bar"),
+ ["foo", "bar"],
+ "PathUtils.splitRelative() on a relative path with two components",
+ );
+ }
+
+ Assert.throws(
+ () => PathUtils.splitRelative("."),
+ SPLIT_RELATIVE_CURRENT,
+ "PathUtils.splitRelative() with a current dir component is disallowed by default"
+ );
+
+ Assert.throws(
+ () => PathUtils.splitRelative(".."),
+ SPLIT_RELATIVE_PARENT,
+ "PathUtils.splitRelative() with a parent dir component is disallowed by default"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative(".", { allowCurrentDir: true }),
+ ["."],
+ "PathUtils.splitRelative() with a current dir component is allowed with option"
+ );
+
+ Assert.deepEqual(
+ PathUtils.splitRelative("..", { allowParentDir: true }),
+ [".."],
+ "PathUtils.splitRelative() with a parent dir component is allowed with option"
+ );
+ });
+
+ add_task(function test_toFileURI() {
+ Assert.throws(
+ () => PathUtils.toFileURI("."),
+ UNRECOGNIZED_PATH,
+ "PathUtils.toFileURI() does not support relative paths"
+ );
+ Assert.throws(
+ () => PathUtils.toFileURI(""),
+ EMPTY_PATH,
+ "PathUtils.toFileURI() does not support empty paths"
+ );
+
+ if (Services.appinfo.OS === "WINNT") {
+ is(
+ PathUtils.toFileURI("C:\\"),
+ "file:///C:",
+ "PathUtils.toFileURI() with a root path"
+ );
+
+ is(
+ PathUtils.toFileURI("C:\\Windows\\"),
+ "file:///C:/Windows",
+ "PathUtils.toFileURI() with a non-root directory path"
+ );
+
+ is(
+ PathUtils.toFileURI("C:\\Windows\\system32\\notepad.exe"),
+ "file:///C:/Windows/system32/notepad.exe",
+ "PathUtils.toFileURI() with a file path"
+ );
+ } else {
+ is(
+ PathUtils.toFileURI("/"),
+ "file:///",
+ "PathUtils.toFileURI() with a root path"
+ );
+
+ is(
+ PathUtils.toFileURI("/bin"),
+ "file:///bin",
+ "PathUtils.toFileURI() with a non-root directory path"
+ );
+
+ is(
+ PathUtils.toFileURI("/bin/ls"),
+ "file:///bin/ls",
+ "PathUtils.toFileURI() with a file path"
+ );
+ }
+ });
+
+ add_task(async function test_isAbsolute() {
+ if (Services.appinfo.OS === "WINNT") {
+ ok(PathUtils.isAbsolute("C:"), "Drive paths are absolute paths on Windows");
+ ok(PathUtils.isAbsolute("C:\\Windows"), "Paths from the root are absolute paths on Windows");
+ ok(!PathUtils.isAbsolute("foo"), "Paths containing a single item are not absolute paths on Windows");
+ ok(!PathUtils.isAbsolute(".\\foo"), "Paths relative to the current working directory are not absolute paths on Windows");
+ ok(!PathUtils.isAbsolute("..\\foo"), "Paths relative to the parent directory are not absolute paths on Windows");
+ } else {
+ ok(PathUtils.isAbsolute("/"), "Root paths are absolute paths");
+ ok(PathUtils.isAbsolute("/home"), "Paths with a root stem are absolute paths");
+ ok(!PathUtils.isAbsolute("foo"), "Paths containing a single non-root item are not absolute paths");
+ ok(!PathUtils.isAbsolute("./foo"), "Paths relative to the current working directory are not absolute paths");
+ ok(!PathUtils.isAbsolute("../foo"), "Paths relative to the parent directory are not absolute paths");
+ }
+ });
+
+ add_task(async function test_getDirectories() {
+ // See: nsAppDirectoryServiceDefs.h
+ const tests = [
+ ["profileDir", "ProfD"],
+ ["localProfileDir", "ProfLD"],
+ ["tempDir", "TmpD"],
+ ];
+
+ for (const [attrName, dirConstant] of tests) {
+ const expected = Services.dirsvc.get(dirConstant, Ci.nsIFile).path;
+
+ const attrValue = PathUtils[attrName];
+ is(attrValue, expected, `PathUtils.${attrName} == Services.dirsvc.get("${dirConstant}", Ci.nsIFile).path`);
+ }
+ });
+</script>
+
+<body>
+</body>
+
+</html>
diff --git a/dom/system/tests/test_pathutils_worker.xhtml b/dom/system/tests/test_pathutils_worker.xhtml
new file mode 100644
index 0000000000..dc181ce07d
--- /dev/null
+++ b/dom/system/tests/test_pathutils_worker.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!-- Any copyright is dedicated to the Public Domain.
+- http://creativecommons.org/publicdomain/zero/1.0/ -->
+<window title="Testing PathUtils 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>
+ <script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function test() {
+ SimpleTest.waitForExplicitFinish();
+
+ info("test_pathtuils_worker.xhtml: Starting test");
+
+ const worker = new ChromeWorker("pathutils_worker.js");
+ info("test_pathtuils_worker.xhtml: ChromeWorker created");
+
+ listenForTests(worker, { verbose: false });
+ worker.postMessage({
+ profileDir: PathUtils.profileDir,
+ localProfileDir: PathUtils.localProfileDir,
+ tempDir: PathUtils.tempDir,
+ });
+
+ info("test_pathtuils_worker.xhtml: Test running...");
+ }
+ ]]>
+ </script>
+ <body xmlns="https://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>