diff --git a/.eslintrc-test-paths.js b/.eslintrc-test-paths.js index 1eec01a1c6fd..9806aa4420ad 100644 --- a/.eslintrc-test-paths.js +++ b/.eslintrc-test-paths.js @@ -263,6 +263,7 @@ const extraChromeTestPaths = [ "layout/svg/tests/", "layout/xul/test/", "toolkit/components/aboutmemory/tests/", + "toolkit/components/osfile/tests/mochi/", "toolkit/components/printing/tests/", "toolkit/components/url-classifier/tests/mochitest/", "toolkit/components/viewsource/test/", diff --git a/.eslintrc.js b/.eslintrc.js index 999e82c4aaa1..6c4112c87a50 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -310,6 +310,7 @@ module.exports = { "toolkit/components/narrate/Narrator.jsm", "toolkit/components/nimbus/**", "toolkit/components/normandy/**", + "toolkit/components/osfile/**", "toolkit/components/passwordmgr/**", "toolkit/components/pdfjs/**", "toolkit/components/pictureinpicture/**", @@ -612,6 +613,13 @@ module.exports = { "devtools/**", ], }, + { + // Turn off the osfile rule for osfile. + files: ["toolkit/components/osfile/**"], + rules: { + "mozilla/reject-osfile": "off", + }, + }, { // Exempt files with these paths since they have to use http for full coverage files: httpTestingPaths.map(path => `${path}**`), diff --git a/browser/base/content/test/performance/browser_startup.js b/browser/base/content/test/performance/browser_startup.js index d83db7763b88..7b91aa99c191 100644 --- a/browser/base/content/test/performance/browser_startup.js +++ b/browser/base/content/test/performance/browser_startup.js @@ -102,6 +102,7 @@ const startupPhases = { modules: new Set([ "resource://gre/modules/AsyncPrefs.sys.mjs", "resource://gre/modules/LoginManagerContextMenu.sys.mjs", + "resource://gre/modules/osfile.jsm", "resource://pdf.js/PdfStreamConverter.sys.mjs", ]), }, diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index 203fb477bed2..2105de47f381 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -71,6 +71,12 @@ var gExceptionPaths = [ // Localization file added programatically in featureCallout.jsm "resource://app/localization/en-US/browser/featureCallout.ftl", + + // Will be removed in bug 1737308 + "resource://gre/modules/lz4.js", + "resource://gre/modules/lz4_internal.js", + "resource://gre/modules/osfile.jsm", + "resource://gre/modules/osfile/", ]; // These are not part of the omni.ja file, so we find them only when running diff --git a/docs/code-quality/lint/linters/eslint-plugin-mozilla.rst b/docs/code-quality/lint/linters/eslint-plugin-mozilla.rst index d1d60c963ce8..03d0d4ab8542 100644 --- a/docs/code-quality/lint/linters/eslint-plugin-mozilla.rst +++ b/docs/code-quality/lint/linters/eslint-plugin-mozilla.rst @@ -58,6 +58,7 @@ The plugin implements the following rules: eslint-plugin-mozilla/reject-lazy-imports-into-globals eslint-plugin-mozilla/reject-mixing-eager-and-lazy eslint-plugin-mozilla/reject-multiple-getters-calls + eslint-plugin-mozilla/reject-osfile eslint-plugin-mozilla/reject-relative-requires eslint-plugin-mozilla/reject-requires-await eslint-plugin-mozilla/reject-scriptableunicodeconverter diff --git a/docs/code-quality/lint/linters/eslint-plugin-mozilla/reject-osfile.rst b/docs/code-quality/lint/linters/eslint-plugin-mozilla/reject-osfile.rst new file mode 100644 index 000000000000..1d5496567952 --- /dev/null +++ b/docs/code-quality/lint/linters/eslint-plugin-mozilla/reject-osfile.rst @@ -0,0 +1,13 @@ +reject-osfile +============= + +Rejects calls into ``OS.File`` and ``OS.Path``. This is configured as a warning. +You should use |IOUtils|_ and |PathUtils|_ respectively for new code. If +modifying old code, please consider swapping it in if possible; if this is +tricky please ensure a bug is on file. + +.. |IOUtils| replace:: ``IOUtils`` +.. _IOUtils: https://searchfox.org/mozilla-central/source/dom/chrome-webidl/IOUtils.webidl + +.. |PathUtils| replace:: ``PathUtils`` +.. _PathUtils: https://searchfox.org/mozilla-central/source/dom/chrome-webidl/PathUtils.webidl diff --git a/dom/system/OSFileConstants.cpp b/dom/system/OSFileConstants.cpp index 5abc19dd993f..fa7e9532af5d 100644 --- a/dom/system/OSFileConstants.cpp +++ b/dom/system/OSFileConstants.cpp @@ -835,12 +835,97 @@ bool OSFileConstantsService::DefineOSFileConstants( return false; } + nsCOMPtr runtime = + do_GetService(XULRUNTIME_SERVICE_CONTRACTID); + if (runtime) { + nsAutoCString os; + DebugOnly rv = runtime->GetOS(os); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + JSString* strVersion = JS_NewStringCopyZ(aCx, os.get()); + if (!strVersion) { + return false; + } + + JS::Rooted valVersion(aCx, JS::StringValue(strVersion)); + if (!JS_SetProperty(aCx, objSys, "Name", valVersion)) { + return false; + } + } + +#if defined(DEBUG) + JS::Rooted valDebug(aCx, JS::TrueValue()); + if (!JS_SetProperty(aCx, objSys, "DEBUG", valDebug)) { + return false; + } +#endif + +#if defined(HAVE_64BIT_BUILD) + JS::Rooted valBits(aCx, JS::Int32Value(64)); +#else + JS::Rooted valBits(aCx, JS::Int32Value(32)); +#endif // defined (HAVE_64BIT_BUILD) + if (!JS_SetProperty(aCx, objSys, "bits", valBits)) { + return false; + } + if (!JS_DefineProperty( aCx, objSys, "umask", mUserUmask, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) { return false; } + // Build OS.Constants.Path + + JS::Rooted objPath(aCx); + if (!(objPath = GetOrCreateObjectProperty(aCx, objConstants, "Path"))) { + return false; + } + + // Locate libxul + // Note that we don't actually provide the full path, only the name of the + // library, which is sufficient to link to the library using js-ctypes. + +#if defined(XP_MACOSX) + // Under MacOS X, for some reason, libxul is called simply "XUL", + // and we need to provide the full path. + nsAutoString libxul; + libxul.Append(mPaths->libDir); + libxul.AppendLiteral("/XUL"); +#else + // On other platforms, libxul is a library "xul" with regular + // library prefix/suffix. + nsAutoString libxul; + libxul.AppendLiteral(MOZ_DLL_PREFIX); + libxul.AppendLiteral("xul"); + libxul.AppendLiteral(MOZ_DLL_SUFFIX); +#endif // defined(XP_MACOSX) + + if (!SetStringProperty(aCx, objPath, "libxul", libxul)) { + return false; + } + + if (!SetStringProperty(aCx, objPath, "libDir", mPaths->libDir)) { + return false; + } + + if (!SetStringProperty(aCx, objPath, "tmpDir", mPaths->tmpDir)) { + return false; + } + + // Configure profileDir only if it is available at this stage + if (!mPaths->profileDir.IsVoid() && + !SetStringProperty(aCx, objPath, "profileDir", mPaths->profileDir)) { + return false; + } + + // Configure localProfileDir only if it is available at this stage + if (!mPaths->localProfileDir.IsVoid() && + !SetStringProperty(aCx, objPath, "localProfileDir", + mPaths->localProfileDir)) { + return false; + } + return true; } diff --git a/dom/system/moz.build b/dom/system/moz.build index a156a6e4b482..240c56a3f293 100644 --- a/dom/system/moz.build +++ b/dom/system/moz.build @@ -8,6 +8,9 @@ with Files("**"): BUG_COMPONENT = ("Core", "DOM: Core & HTML") +with Files("*OSFile*"): + BUG_COMPONENT = ("Toolkit", "OS.File") + with Files("*ocationProvider*"): BUG_COMPONENT = ("Core", "DOM: Geolocation") diff --git a/dom/system/tests/chrome.ini b/dom/system/tests/chrome.ini index 2cee7b48296e..ffdd4393a499 100644 --- a/dom/system/tests/chrome.ini +++ b/dom/system/tests/chrome.ini @@ -1,8 +1,8 @@ [DEFAULT] - -[test_constants.xhtml] support-files = worker_constants.js + +[test_constants.xhtml] [test_pathutils.html] [test_pathutils_worker.xhtml] support-files = diff --git a/dom/system/tests/ioutils/test_ioutils_read_write.html b/dom/system/tests/ioutils/test_ioutils_read_write.html index f52115d26185..2b36f7de6fbb 100644 --- a/dom/system/tests/ioutils/test_ioutils_read_write.html +++ b/dom/system/tests/ioutils/test_ioutils_read_write.html @@ -15,6 +15,11 @@ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + // This is presently only used to test compatability between OS.File and + // IOUtils when it comes to writing compressed files. The import and the + // test `test_lz4_osfile_compat` can be removed with OS.File is removed. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + add_task(async function test_read_failure() { const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp"); await Assert.rejects( @@ -335,6 +340,31 @@ await cleanup(tmpFileName); }); + add_task(async function test_lz4_osfile_compat() { + const osfileTmpFile = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_compat_osfile.tmp"); + const ioutilsTmpFile = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_compat_ioutils.tmp"); + + info("Test OS.File and IOUtils write the same file with LZ4 compression enabled") + const repeatedBytes = Uint8Array.of(...new Array(50).fill(1)); + let expectedBytes = 23; + let ioutilsBytes = await IOUtils.write(ioutilsTmpFile, repeatedBytes, { compress: true }); + let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, repeatedBytes, { compression: "lz4" }); + is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression"); + is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression"); + + info("Test OS.File can read a file compressed by IOUtils"); + const osfileReadBytes = await OS.File.read(ioutilsTmpFile, { compression: "lz4" }); + ok(osfileReadBytes.every(byte => byte === 1), "OS.File can read a file compressed by IOUtils"); + is(osfileReadBytes.length, 50, "OS.File reads the right number of bytes from a file compressed by IOUtils") + + info("Test IOUtils can read a file compressed by OS.File"); + const ioutilsReadBytes = await IOUtils.read(osfileTmpFile, { decompress: true }); + ok(ioutilsReadBytes.every(byte => byte === 1), "IOUtils can read a file compressed by OS.File"); + is(ioutilsReadBytes.length, 50, "IOUtils reads the right number of bytes from a file compressed by OS.File") + + await cleanup(osfileTmpFile, ioutilsTmpFile); + }); + add_task(async function test_lz4_bad_call() { const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_bad_call.tmp"); diff --git a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html index 196f5d4862cd..9ce4ff615a45 100644 --- a/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html +++ b/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html @@ -15,6 +15,11 @@ const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm"); + // This is presently only used to test compatability between OS.File and + // IOUtils when it comes to writing compressed files. The import and the + // test `test_lz4_osfile_compat` can be removed with OS.File is removed. + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + // This is an impossible sequence of bytes in an UTF-8 encoded file. // See section 3.5.3 of this text: // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt @@ -285,6 +290,29 @@ await cleanup(tmpFileName); }); + add_task(async function test_utf8_lz4_osfile_compat() { + const osfileTmpFile = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_compat_osfile.tmp"); + const ioutilsTmpFile = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_compat_ioutils.tmp"); + + info("Test OS.File and IOUtils write the same UTF-8 file with LZ4 compression enabled") + const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐"; + let expectedBytes = 83; + let ioutilsBytes = await IOUtils.writeUTF8(ioutilsTmpFile, emoji, { compress: true }); + let osfileBytes = await OS.File.writeAtomic(osfileTmpFile, emoji, { compression: "lz4" }); + is(ioutilsBytes, expectedBytes, "IOUtils writes the expected number of bytes for compression"); + is(osfileBytes, ioutilsBytes, "OS.File and IOUtils write the same number of bytes for LZ4 compression"); + + info("Test OS.File can read an UTF-8 file compressed by IOUtils"); + const osfileReadStr = await OS.File.read(ioutilsTmpFile, { compression: "lz4", encoding: "utf-8" }); + is(osfileReadStr, emoji, "OS.File can read an UTF-8 file compressed by IOUtils") + + info("Test IOUtils can read an UTF-8 file compressed by OS.File"); + const ioutilsReadString = await IOUtils.readUTF8(ioutilsTmpFile, { decompress: true }); + is(ioutilsReadString, emoji, "IOUtils can read an UTF-8 file compressed by OS.File"); + + await cleanup(osfileTmpFile, ioutilsTmpFile); + }); + add_task(async function test_utf8_lz4_bad_call() { const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_utf8_lz4_bad_call.tmp"); diff --git a/dom/system/tests/test_constants.xhtml b/dom/system/tests/test_constants.xhtml index f7450e0a6c67..82c25ce1b0c9 100644 --- a/dom/system/tests/test_constants.xhtml +++ b/dom/system/tests/test_constants.xhtml @@ -16,6 +16,21 @@ let worker; +function test_xul() { + let lib; + isnot(null, OS.Constants.Path.libxul, "libxulpath is defined"); + try { + lib = ctypes.open(OS.Constants.Path.libxul); + lib.declare("DumpJSStack", ctypes.default_abi, ctypes.void_t); + } catch (x) { + ok(false, "Could not open libxul " + x); + } + if (lib) { + lib.close(); + } + ok(true, "test_xul: opened libxul successfully"); +} + // Test that OS.Constants.libc is defined function test_libc() { isnot(null, OS.Constants.libc, "OS.Constants.libc is defined"); @@ -43,6 +58,11 @@ function test_Win() { } } +// Test that OS.Constants.Sys.DEBUG is set properly on main thread +function test_debugBuildMainThread(isDebugBuild) { + is(isDebugBuild, !!OS.Constants.Sys.DEBUG, "OS.Constants.Sys.DEBUG is set properly on main thread"); +} + // Test that OS.Constants.Sys.umask is set properly on main thread function test_umaskMainThread(umask) { is(umask, OS.Constants.Sys.umask, @@ -59,9 +79,13 @@ function test() { getService(Ci.nsIOSFileConstantsService). init(); ({ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm")); + test_xul(); test_libc(); test_Win(); + let isDebugBuild = Cc["@mozilla.org/xpcom/debug;1"] + .getService(Ci.nsIDebug2).isDebugBuild; + test_debugBuildMainThread(isDebugBuild); let umask = Cc["@mozilla.org/system-info;1"]. getService(Ci.nsIPropertyBag2). @@ -99,6 +123,7 @@ function test() { // pass expected values that are unavailable off-main-thread // to the worker worker.postMessage({ + isDebugBuild: isDebugBuild, umask: umask }); ok(true, "test_constants.xhtml: Test in progress"); diff --git a/dom/system/tests/worker_constants.js b/dom/system/tests/worker_constants.js index 4212460abd5d..befc5e423969 100644 --- a/dom/system/tests/worker_constants.js +++ b/dom/system/tests/worker_constants.js @@ -15,9 +15,13 @@ self.onmessage = function(msg) { self.onmessage = function(msgInner) { log("ignored message " + JSON.stringify(msgInner.data)); }; - let { umask } = msg.data; + let { isDebugBuild, umask } = msg.data; try { + test_name(); + test_xul(); + test_debugBuildWorkerThread(isDebugBuild); test_umaskWorkerThread(umask); + test_bits(); } catch (x) { log("Catching error: " + x); log("Stack: " + x.stack); @@ -41,6 +45,20 @@ function isnot(a, b, description) { send({ kind: "isnot", a, b, description }); } +// Test that OS.Constants.Sys.Name is defined +function test_name() { + isnot(null, OS.Constants.Sys.Name, "OS.Constants.Sys.Name is defined"); +} + +// Test that OS.Constants.Sys.DEBUG is set properly in ChromeWorker thread +function test_debugBuildWorkerThread(isDebugBuild) { + is( + isDebugBuild, + !!OS.Constants.Sys.DEBUG, + "OS.Constants.Sys.DEBUG is set properly on worker thread" + ); +} + // Test that OS.Constants.Sys.umask is set properly in ChromeWorker thread function test_umaskWorkerThread(umask) { is( @@ -50,3 +68,28 @@ function test_umaskWorkerThread(umask) { ("0000" + umask.toString(8)).slice(-4) ); } + +// Test that OS.Constants.Path.libxul lets us open libxul +function test_xul() { + let lib; + isnot(null, OS.Constants.Path.libxul, "libxul is defined"); + try { + lib = ctypes.open(OS.Constants.Path.libxul); + lib.declare("DumpJSStack", ctypes.default_abi, ctypes.void_t); + } catch (x) { + ok(false, "test_xul: Could not open libxul: " + x); + } + if (lib) { + lib.close(); + } + ok(true, "test_xul: opened libxul successfully"); +} + +// Check if the value of OS.Constants.Sys.bits is 32 or 64 +function test_bits() { + is( + OS.Constants.Sys.bits, + ctypes.int.ptr.size * 8, + "OS.Constants.Sys.bits is either 32 or 64" + ); +} diff --git a/dom/webidl/NativeOSFileInternals.webidl b/dom/webidl/NativeOSFileInternals.webidl new file mode 100644 index 000000000000..27522a366cc5 --- /dev/null +++ b/dom/webidl/NativeOSFileInternals.webidl @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtaone at http://mozilla.org/MPL/2.0/. */ + +/** + * Options for nsINativeOSFileInternals::Read + */ +[GenerateInit] +dictionary NativeOSFileReadOptions +{ + /** + * If specified, convert the raw bytes to a String + * with the specified encoding. Otherwise, return + * the raw bytes as a TypedArray. + */ + DOMString? encoding; + + /** + * If specified, limit the number of bytes to read. + */ + unsigned long long? bytes; +}; + +/** + * Options for nsINativeOSFileInternals::WriteAtomic + */ +[GenerateInit] +dictionary NativeOSFileWriteAtomicOptions +{ + /** + * If specified, specify the number of bytes to write. + * NOTE: This takes (and should take) a uint64 here but the actual + * value is limited to int32. This needs to be fixed, see Bug 1063635. + */ + unsigned long long? bytes; + + /** + * If specified, write all data to a temporary file in the + * |tmpPath|. Else, write to the given path directly. + */ + DOMString? tmpPath = null; + + /** + * If specified and true, a failure will occur if the file + * already exists in the given path. + */ + boolean noOverwrite = false; + + /** + * If specified and true, this will sync any buffered data + * for the file to disk. This might be slower, but safer. + */ + boolean flush = false; + + /** + * If specified, this will backup the destination file as + * specified. + */ + DOMString? backupTo = null; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 52b0e0892420..818657671d01 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -229,6 +229,9 @@ with Files("Mouse*"): with Files("MutationEvent.webidl"): BUG_COMPONENT = ("Core", "DOM: Events") +with Files("NativeOSFileInternals.webidl"): + BUG_COMPONENT = ("Toolkit", "OS.File") + with Files("NavigationPreloadManager.webidl"): BUG_COMPONENT = ("Core", "DOM: Service Workers") @@ -743,6 +746,7 @@ WEBIDL_FILES = [ "MutationEvent.webidl", "MutationObserver.webidl", "NamedNodeMap.webidl", + "NativeOSFileInternals.webidl", "NavigationPreloadManager.webidl", "Navigator.webidl", "NetErrorInfo.webidl", diff --git a/js/xpconnect/tests/browser/browser_import_mapped_jsm.js b/js/xpconnect/tests/browser/browser_import_mapped_jsm.js index 385f4e33c037..85249f9d1f64 100644 --- a/js/xpconnect/tests/browser/browser_import_mapped_jsm.js +++ b/js/xpconnect/tests/browser/browser_import_mapped_jsm.js @@ -28,6 +28,7 @@ const JSMs = [ "resource://gre/modules/PrivateBrowsingUtils.jsm", "resource://gre/modules/Timer.jsm", "resource://gre/modules/XPCOMUtils.jsm", + "resource://gre/modules/osfile.jsm", "resource://gre/modules/addons/XPIDatabase.jsm", "resource://gre/modules/addons/XPIProvider.jsm", "resource://gre/modules/addons/XPIInstall.jsm", diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 372efd3599b3..1ed99de04e65 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -624,6 +624,9 @@ pref("gfx.use_text_smoothing_setting", false); // Number of characters to consider emphasizing for rich autocomplete results pref("toolkit.autocomplete.richBoundaryCutoff", 200); +// Variable controlling logging for osfile. +pref("toolkit.osfile.log", false); + pref("toolkit.scrollbox.smoothScroll", true); pref("toolkit.scrollbox.scrollIncrement", 20); pref("toolkit.scrollbox.clickToScroll.scrollDelay", 150); diff --git a/testing/runtimes/manifest-runtimes-android.json b/testing/runtimes/manifest-runtimes-android.json index 776c9a84c64d..d172e465341a 100644 --- a/testing/runtimes/manifest-runtimes-android.json +++ b/testing/runtimes/manifest-runtimes-android.json @@ -1564,6 +1564,7 @@ "toolkit/components/extensions/test/xpcshell/xpcshell.ini": 19.12, "toolkit/components/featuregates/test/unit/xpcshell.ini": 1.71, "toolkit/components/mozintl/test/xpcshell.ini": 1.82, + "toolkit/components/osfile/tests/xpcshell/xpcshell.ini": 27.1, "toolkit/components/telemetry/tests/unit/xpcshell.ini": 42.36, "toolkit/components/timermanager/tests/unit/xpcshell.ini": 9.07, "toolkit/components/utils/test/unit/xpcshell.ini": 2.22, diff --git a/testing/runtimes/manifest-runtimes-unix.json b/testing/runtimes/manifest-runtimes-unix.json index 81c97fdbdb4f..44a90173d98a 100644 --- a/testing/runtimes/manifest-runtimes-unix.json +++ b/testing/runtimes/manifest-runtimes-unix.json @@ -386,6 +386,7 @@ "toolkit/components/certviewer/tests/chrome/chrome.ini": 2.78, "toolkit/components/ctypes/tests/chrome/chrome.ini": 1.4, "toolkit/components/extensions/test/mochitest/chrome.ini": 11.29, + "toolkit/components/osfile/tests/mochi/chrome.ini": 9.12, "toolkit/components/places/tests/chrome/chrome.ini": 1.87, "toolkit/components/prompts/test/chrome.ini": 106.31, "toolkit/components/resistfingerprinting/tests/chrome.ini": 3.99, diff --git a/testing/runtimes/manifest-runtimes-windows.json b/testing/runtimes/manifest-runtimes-windows.json index 80176e1c4673..0bb7d25407b0 100644 --- a/testing/runtimes/manifest-runtimes-windows.json +++ b/testing/runtimes/manifest-runtimes-windows.json @@ -388,6 +388,7 @@ "toolkit/components/certviewer/tests/chrome/chrome.ini": 2.35, "toolkit/components/ctypes/tests/chrome/chrome.ini": 1.38, "toolkit/components/extensions/test/mochitest/chrome.ini": 8.63, + "toolkit/components/osfile/tests/mochi/chrome.ini": 7.73, "toolkit/components/places/tests/chrome/chrome.ini": 1.89, "toolkit/components/prompts/test/chrome.ini": 41.2, "toolkit/components/resistfingerprinting/tests/chrome.ini": 3.59, diff --git a/toolkit/components/build/components.conf b/toolkit/components/build/components.conf index 42346abf9131..ba5194d394d0 100644 --- a/toolkit/components/build/components.conf +++ b/toolkit/components/build/components.conf @@ -74,6 +74,12 @@ Classes = [ 'headers': ['/toolkit/components/reputationservice/LoginReputation.h'], 'constructor': 'mozilla::LoginReputationService::GetSingleton', }, + { + 'cid': '{63a69303-8a64-45a9-848c-d4e2792794e6}', + 'contract_ids': ['@mozilla.org/toolkit/osfile/native-internals;1'], + 'type': 'mozilla::NativeOSFileInternalsService', + 'headers': ['mozilla/NativeOSFileInternals.h'], + }, { 'name': 'Alerts', 'cid': '{a0ccaaf8-09da-44d8-b250-9ac3e93c8117}', diff --git a/toolkit/components/lz4/lz4.cpp b/toolkit/components/lz4/lz4.cpp new file mode 100644 index 000000000000..7e0ea13288cc --- /dev/null +++ b/toolkit/components/lz4/lz4.cpp @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Compression.h" + +/** + * LZ4 is a very fast byte-wise compression algorithm. + * + * Compared to Google's Snappy it is faster to compress and decompress and + * generally produces output of about the same size. + * + * Compared to zlib it compresses at about 10x the speed, decompresses at about + * 4x the speed and produces output of about 1.5x the size. + * + */ + +using namespace mozilla::Compression; + +/** + * Compresses 'inputSize' bytes from 'source' into 'dest'. + * Destination buffer must be already allocated, + * and must be sized to handle worst cases situations (input data not + * compressible) Worst case size evaluation is provided by function + * LZ4_compressBound() + * + * @param inputSize is the input size. Max supported value is ~1.9GB + * @param return the number of bytes written in buffer dest + */ +extern "C" MOZ_EXPORT size_t workerlz4_compress(const char* source, + size_t inputSize, char* dest) { + return LZ4::compress(source, inputSize, dest); +} + +/** + * If the source stream is malformed, the function will stop decoding + * and return a negative result, indicating the byte position of the + * faulty instruction + * + * This function never writes outside of provided buffers, and never + * modifies input buffer. + * + * note : destination buffer must be already allocated. + * its size must be a minimum of 'outputSize' bytes. + * @param outputSize is the output size, therefore the original size + * @return true/false + */ +extern "C" MOZ_EXPORT int workerlz4_decompress(const char* source, + size_t inputSize, char* dest, + size_t maxOutputSize, + size_t* bytesOutput) { + return LZ4::decompress(source, inputSize, dest, maxOutputSize, bytesOutput); +} + +/* + Provides the maximum size that LZ4 may output in a "worst case" + scenario (input data not compressible) primarily useful for memory + allocation of output buffer. + note : this function is limited by "int" range (2^31-1) + + @param inputSize is the input size. Max supported value is ~1.9GB + @return maximum output size in a "worst case" scenario +*/ +extern "C" MOZ_EXPORT size_t workerlz4_maxCompressedSize(size_t inputSize) { + return LZ4::maxCompressedSize(inputSize); +} diff --git a/toolkit/components/lz4/lz4.js b/toolkit/components/lz4/lz4.js new file mode 100644 index 000000000000..72debcd2da81 --- /dev/null +++ b/toolkit/components/lz4/lz4.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var SharedAll; +if (typeof Components != "undefined") { + SharedAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" + ); + var { Primitives } = ChromeUtils.import( + "resource://gre/modules/lz4_internal.js" + ); + var { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + this.EXPORTED_SYMBOLS = ["Lz4"]; + this.exports = {}; +} else if (typeof module != "undefined" && typeof require != "undefined") { + /* eslint-env commonjs */ + SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + Primitives = require("resource://gre/modules/lz4_internal.js"); + ctypes = self.ctypes; +} else { + throw new Error( + "Please load this module with Component.utils.import or with require()" + ); +} + +const MAGIC_NUMBER = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); // "mozLz40\0" + +const BYTES_IN_SIZE_HEADER = ctypes.uint32_t.size; + +const HEADER_SIZE = MAGIC_NUMBER.byteLength + BYTES_IN_SIZE_HEADER; + +/** + * An error during (de)compression + * + * @param {string} operation The name of the operation ("compress", "decompress") + * @param {string} reason A reason to be used when matching errors. Must start + * with "because", e.g. "becauseInvalidContent". + * @param {string} message A human-readable message. + */ +function LZError(operation, reason, message) { + SharedAll.OSError.call(this); + this.operation = operation; + this[reason] = true; + this.message = message; +} +LZError.prototype = Object.create(SharedAll.OSError); +LZError.prototype.toString = function toString() { + return this.message; +}; +exports.Error = LZError; + +/** + * Compress a block to a form suitable for writing to disk. + * + * Compatibility note: For the moment, we are basing our code on lz4 + * 1.3, which does not specify a *file* format. Therefore, we define + * our own format. Once lz4 defines a complete file format, we will + * migrate both |compressFileContent| and |decompressFileContent| to this file + * format. For backwards-compatibility, |decompressFileContent| will however + * keep the ability to decompress files provided with older versions of + * |compressFileContent|. + * + * Compressed files have the following layout: + * + * | MAGIC_NUMBER (8 bytes) | content size (uint32_t, little endian) | content, as obtained from lz4_compress | + * + * @param {TypedArray|void*} buffer The buffer to write to the disk. + * @param {object=} options An object that may contain the following fields: + * - {number} bytes The number of bytes to read from |buffer|. If |buffer| + * is an |ArrayBuffer|, |bytes| defaults to |buffer.byteLength|. If + * |buffer| is a |void*|, |bytes| MUST be provided. + * @return {Uint8Array} An array of bytes suitable for being written to the + * disk. + */ +function compressFileContent(array, options = {}) { + // Prepare the output array + let inputBytes; + if (SharedAll.isTypedArray(array) && !(options && "bytes" in options)) { + inputBytes = array.byteLength; + } else if (options && options.bytes) { + inputBytes = options.bytes; + } else { + throw new TypeError("compressFileContent requires a size"); + } + let maxCompressedSize = Primitives.maxCompressedSize(inputBytes); + let outputArray = new Uint8Array(HEADER_SIZE + maxCompressedSize); + + // Compress to output array + let payload = new Uint8Array( + outputArray.buffer, + outputArray.byteOffset + HEADER_SIZE + ); + let compressedSize = Primitives.compress(array, inputBytes, payload); + + // Add headers + outputArray.set(MAGIC_NUMBER); + let view = new DataView(outputArray.buffer); + view.setUint32(MAGIC_NUMBER.byteLength, inputBytes, true); + + return new Uint8Array(outputArray.buffer, 0, HEADER_SIZE + compressedSize); +} +exports.compressFileContent = compressFileContent; + +function decompressFileContent(array, options = {}) { + let bytes = SharedAll.normalizeBufferArgs(array, options.bytes || null); + if (bytes < HEADER_SIZE) { + throw new LZError( + "decompress", + "becauseLZNoHeader", + `Buffer is too short (no header) - Data: ${options.path || array}` + ); + } + + // Read headers + let expectMagicNumber = new DataView( + array.buffer, + 0, + MAGIC_NUMBER.byteLength + ); + for (let i = 0; i < MAGIC_NUMBER.byteLength; ++i) { + if (expectMagicNumber.getUint8(i) != MAGIC_NUMBER[i]) { + throw new LZError( + "decompress", + "becauseLZWrongMagicNumber", + `Invalid header (no magic number) - Data: ${options.path || array}` + ); + } + } + + let sizeBuf = new DataView( + array.buffer, + MAGIC_NUMBER.byteLength, + BYTES_IN_SIZE_HEADER + ); + let expectDecompressedSize = + sizeBuf.getUint8(0) + + (sizeBuf.getUint8(1) << 8) + + (sizeBuf.getUint8(2) << 16) + + (sizeBuf.getUint8(3) << 24); + if (expectDecompressedSize == 0) { + // The underlying algorithm cannot handle a size of 0 + return new Uint8Array(0); + } + + // Prepare the input buffer + let inputData = new DataView(array.buffer, HEADER_SIZE); + + // Prepare the output buffer + let outputBuffer = new Uint8Array(expectDecompressedSize); + let decompressedBytes = new SharedAll.Type.size_t.implementation(0); + + // Decompress + let success = Primitives.decompress( + inputData, + bytes - HEADER_SIZE, + outputBuffer, + outputBuffer.byteLength, + decompressedBytes.address() + ); + if (!success) { + throw new LZError( + "decompress", + "becauseLZInvalidContent", + `Invalid content: Decompression stopped at ${ + decompressedBytes.value + } - Data: ${options.path || array}` + ); + } + return new Uint8Array( + outputBuffer.buffer, + outputBuffer.byteOffset, + decompressedBytes.value + ); +} +exports.decompressFileContent = decompressFileContent; + +if (typeof Components != "undefined") { + this.Lz4 = { + compressFileContent, + decompressFileContent, + }; +} diff --git a/toolkit/components/lz4/lz4_internal.js b/toolkit/components/lz4/lz4_internal.js new file mode 100644 index 000000000000..5f6afe1304ca --- /dev/null +++ b/toolkit/components/lz4/lz4_internal.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env commonjs */ + +"use strict"; + +var Primitives = {}; + +var SharedAll; +if (typeof Components != "undefined") { + SharedAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" + ); + + this.EXPORTED_SYMBOLS = ["Primitives"]; + this.Primitives = Primitives; + this.exports = {}; +} else if (typeof module != "undefined" && typeof require != "undefined") { + SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); +} else { + throw new Error( + "Please load this module with Component.utils.import or with require()" + ); +} + +var libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul); +var Type = SharedAll.Type; + +libxul.declareLazyFFI( + Primitives, + "compress", + "workerlz4_compress", + null, + /* return*/ Type.size_t, + /* const source*/ Type.void_t.in_ptr, + /* inputSize*/ Type.size_t, + /* dest*/ Type.void_t.out_ptr +); + +libxul.declareLazyFFI( + Primitives, + "decompress", + "workerlz4_decompress", + null, + /* return*/ Type.int, + /* const source*/ Type.void_t.in_ptr, + /* inputSize*/ Type.size_t, + /* dest*/ Type.void_t.out_ptr, + /* maxOutputSize*/ Type.size_t, + /* actualOutputSize*/ Type.size_t.out_ptr +); + +libxul.declareLazyFFI( + Primitives, + "maxCompressedSize", + "workerlz4_maxCompressedSize", + null, + /* return*/ Type.size_t, + /* inputSize*/ Type.size_t +); + +if (typeof module != "undefined") { + module.exports = { + get compress() { + return Primitives.compress; + }, + get decompress() { + return Primitives.decompress; + }, + get maxCompressedSize() { + return Primitives.maxCompressedSize; + }, + }; +} diff --git a/toolkit/components/lz4/moz.build b/toolkit/components/lz4/moz.build new file mode 100644 index 000000000000..b6259ddaccec --- /dev/null +++ b/toolkit/components/lz4/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "OS.File") + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"] + +EXTRA_JS_MODULES += [ + "lz4.js", + "lz4_internal.js", +] + +SOURCES += [ + "lz4.cpp", +] + +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest b/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest new file mode 100644 index 000000000000..e2f9a9d8ef4f --- /dev/null +++ b/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest @@ -0,0 +1 @@ +content test_lz4 ./ diff --git a/toolkit/components/lz4/tests/xpcshell/data/compression.lz b/toolkit/components/lz4/tests/xpcshell/data/compression.lz new file mode 100644 index 000000000000..a354edc03673 Binary files /dev/null and b/toolkit/components/lz4/tests/xpcshell/data/compression.lz differ diff --git a/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js b/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js new file mode 100644 index 000000000000..e3b600e3c31b --- /dev/null +++ b/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js @@ -0,0 +1,166 @@ +/* eslint-env mozilla/chrome-worker */ + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); +/* import-globals-from /toolkit/components/osfile/osfile.jsm */ +importScripts("resource://gre/modules/osfile.jsm"); + +function info(x) { + // self.postMessage({kind: "do_print", args: [x]}); + dump("TEST-INFO: " + x + "\n"); +} + +const Assert = { + ok(x) { + self.postMessage({ kind: "assert_ok", args: [!!x] }); + if (x) { + dump("TEST-PASS: " + x + "\n"); + } else { + throw new Error("Assert.ok failed"); + } + }, + + equal(a, b) { + let result = a == b; + self.postMessage({ kind: "assert_ok", args: [result] }); + if (!result) { + throw new Error("Assert.equal failed " + a + " != " + b); + } + }, +}; + +function do_test_complete() { + self.postMessage({ kind: "do_test_complete", args: [] }); +} + +self.onmessage = function() { + try { + run_test(); + } catch (ex) { + let { message, moduleStack, moduleName, lineNumber } = ex; + let error = new Error(message, moduleName, lineNumber); + error.stack = moduleStack; + dump("Uncaught error: " + error + "\n"); + dump("Full stack: " + moduleStack + "\n"); + throw error; + } +}; + +var Lz4; +var Internals; +function test_import() { + Lz4 = require("resource://gre/modules/lz4.js"); + Internals = require("resource://gre/modules/lz4_internal.js"); +} + +function test_bound() { + for (let k of ["compress", "decompress", "maxCompressedSize"]) { + try { + info("Checking the existence of " + k + "\n"); + Assert.ok(!!Internals[k]); + info(k + " exists"); + } catch (ex) { + // Ignore errors + info(k + " doesn't exist!"); + } + } +} + +function test_reference_file() { + info("Decompress reference file"); + let path = OS.Path.join("data", "compression.lz"); + let data = OS.File.read(path); + let decompressed = Lz4.decompressFileContent(data); + let text = new TextDecoder().decode(decompressed); + Assert.equal(text, "Hello, lz4"); +} + +function compare_arrays(a, b) { + return Array.prototype.join.call(a) == Array.prototype.join.call(b); +} + +function run_rawcompression(name, array) { + info("Raw compression test " + name); + let length = array.byteLength; + let compressedArray = new Uint8Array(Internals.maxCompressedSize(length)); + let compressedBytes = Internals.compress(array, length, compressedArray); + compressedArray = new Uint8Array(compressedArray.buffer, 0, compressedBytes); + info("Raw compressed: " + length + " into " + compressedBytes); + + let decompressedArray = new Uint8Array(length); + let decompressedBytes = new ctypes.size_t(); + let success = Internals.decompress( + compressedArray, + compressedBytes, + decompressedArray, + length, + decompressedBytes.address() + ); + info("Raw decompression success? " + success); + info("Raw decompression size: " + decompressedBytes.value); + Assert.ok(compare_arrays(array, decompressedArray)); +} + +function run_filecompression(name, array) { + info("File compression test " + name); + let compressed = Lz4.compressFileContent(array); + info( + "Compressed " + array.byteLength + " bytes into " + compressed.byteLength + ); + + let decompressed = Lz4.decompressFileContent(compressed); + info( + "Decompressed " + + compressed.byteLength + + " bytes into " + + decompressed.byteLength + ); + Assert.ok(compare_arrays(array, decompressed)); +} + +function run_faileddecompression(name, array) { + info("invalid decompression test " + name); + + // Ensure that raw decompression doesn't segfault + let length = 1 << 14; + let decompressedArray = new Uint8Array(length); + let decompressedBytes = new ctypes.size_t(); + Internals.decompress( + array, + array.byteLength, + decompressedArray, + length, + decompressedBytes.address() + ); + + // File decompression should fail with an acceptable exception + let exn = null; + try { + Lz4.decompressFileContent(array); + } catch (ex) { + exn = ex; + } + Assert.ok(exn); + if (array.byteLength < 10) { + Assert.ok(exn.becauseLZNoHeader); + } else { + Assert.ok(exn.becauseLZWrongMagicNumber); + } +} + +function run_test() { + test_import(); + test_bound(); + test_reference_file(); + for (let length of [0, 1, 1024]) { + let array = new Uint8Array(length); + for (let i = 0; i < length; ++i) { + array[i] = i % 256; + } + let name = length + " bytes"; + run_rawcompression(name, array); + run_filecompression(name, array); + run_faileddecompression(name, array); + } + do_test_complete(); +} diff --git a/toolkit/components/lz4/tests/xpcshell/test_lz4.js b/toolkit/components/lz4/tests/xpcshell/test_lz4.js new file mode 100644 index 000000000000..d8da4457dc3e --- /dev/null +++ b/toolkit/components/lz4/tests/xpcshell/test_lz4.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var WORKER_SOURCE_URI = "chrome://test_lz4/content/worker_lz4.js"; +do_load_manifest("data/chrome.manifest"); + +add_task(function() { + let worker = new ChromeWorker(WORKER_SOURCE_URI); + return new Promise((resolve, reject) => { + worker.onmessage = function(event) { + let data = event.data; + switch (data.kind) { + case "assert_ok": + try { + Assert.ok(data.args[0]); + } catch (ex) { + // Ignore errors + } + return; + case "do_test_complete": + resolve(); + worker.terminate(); + break; + case "do_print": + info(data.args[0]); + } + }; + worker.onerror = function(event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + worker.postMessage("START"); + }); +}); diff --git a/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js b/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js new file mode 100644 index 000000000000..531da8a96685 --- /dev/null +++ b/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Lz4 } = ChromeUtils.import("resource://gre/modules/lz4.js"); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function compare_arrays(a, b) { + return Array.prototype.join.call(a) == Array.prototype.join.call(b); +} + +add_task(async function() { + let path = OS.Path.join("data", "compression.lz"); + let data = await OS.File.read(path); + let decompressed = Lz4.decompressFileContent(data); + let text = new TextDecoder().decode(decompressed); + Assert.equal(text, "Hello, lz4"); +}); + +add_task(async function() { + for (let length of [0, 1, 1024]) { + let array = new Uint8Array(length); + for (let i = 0; i < length; ++i) { + array[i] = i % 256; + } + + let compressed = Lz4.compressFileContent(array); + info( + "Compressed " + array.byteLength + " bytes into " + compressed.byteLength + ); + + let decompressed = Lz4.decompressFileContent(compressed); + info( + "Decompressed " + + compressed.byteLength + + " bytes into " + + decompressed.byteLength + ); + + Assert.ok(compare_arrays(array, decompressed)); + } +}); diff --git a/toolkit/components/lz4/tests/xpcshell/xpcshell.ini b/toolkit/components/lz4/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000000..92a6a99207bb --- /dev/null +++ b/toolkit/components/lz4/tests/xpcshell/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' +support-files = + data/worker_lz4.js + data/chrome.manifest + data/compression.lz + +[test_lz4.js] +[test_lz4_sync.js] diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 878dcea32396..b6cfcef8e769 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -46,9 +46,11 @@ DIRS += [ "httpsonlyerror", "jsoncpp/src/lib_json", "kvstore", + "lz4", "mediasniffer", "mozintl", "mozprotocol", + "osfile", "parentalcontrols", "passwordmgr", "pdfjs", diff --git a/toolkit/components/osfile/NativeOSFileInternals.cpp b/toolkit/components/osfile/NativeOSFileInternals.cpp new file mode 100644 index 000000000000..3ff8d46fb9f9 --- /dev/null +++ b/toolkit/components/osfile/NativeOSFileInternals.cpp @@ -0,0 +1,1263 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Native implementation of some OS.File operations. + */ + +#include "NativeOSFileInternals.h" + +#include "nsString.h" +#include "nsNetCID.h" +#include "nsThreadUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsServiceManagerUtils.h" +#include "nsProxyRelease.h" + +#include "mozilla/dom/NativeOSFileInternalsBinding.h" + +#include "mozilla/Encoding.h" +#include "nsIEventTarget.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/Scoped.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" + +#include "prio.h" +#include "prerror.h" +#include "private/pprio.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/ArrayBuffer.h" // JS::GetArrayBufferByteLength,IsArrayBufferObject,NewArrayBufferWithContents,StealArrayBufferContents +#include "js/experimental/TypedData.h" // JS_NewUint8ArrayWithBuffer +#include "js/Utility.h" +#include "xpcpublic.h" + +#include +#if defined(XP_UNIX) +# include +#endif // defined (XP_UNIX) + +#if defined(XP_WIN) +# include +#endif // defined (XP_WIN) + +namespace mozilla { + +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc, + PR_Close) + +namespace { + +// Utilities for safely manipulating ArrayBuffer contents even in the +// absence of a JSContext. + +/** + * The C buffer underlying to an ArrayBuffer. Throughout the code, we manipulate + * this instead of a void* buffer, as this lets us transfer data across threads + * and into JavaScript without copy. + */ +struct ArrayBufferContents { + /** + * The data of the ArrayBuffer. This is the pointer manipulated to + * read/write the contents of the buffer. + */ + uint8_t* data; + /** + * The number of bytes in the ArrayBuffer. + */ + size_t nbytes; +}; + +/** + * RAII for ArrayBufferContents. + */ +struct ScopedArrayBufferContentsTraits { + typedef ArrayBufferContents type; + const static type empty() { + type result = {0, 0}; + return result; + } + static void release(type ptr) { + js_free(ptr.data); + ptr.data = nullptr; + ptr.nbytes = 0; + } +}; + +struct MOZ_NON_TEMPORARY_CLASS ScopedArrayBufferContents + : public Scoped { + explicit ScopedArrayBufferContents() + : Scoped() {} + + ScopedArrayBufferContents& operator=(ArrayBufferContents ptr) { + Scoped::operator=(ptr); + return *this; + } + + /** + * Request memory for this ArrayBufferContent. This memory may later + * be used to create an ArrayBuffer object (possibly on another + * thread) without copy. + * + * @return true In case of success, false otherwise. + */ + bool Allocate(uint32_t length) { + dispose(); + ArrayBufferContents& value = rwget(); + void* ptr = js_calloc(1, length); + if (ptr) { + value.data = (uint8_t*)ptr; + value.nbytes = length; + return true; + } + return false; + } + + private: + explicit ScopedArrayBufferContents(ScopedArrayBufferContents& source) = + delete; + ScopedArrayBufferContents& operator=(ScopedArrayBufferContents& source) = + delete; +}; + +///////// Cross-platform issues + +// Platform specific constants. As OS.File always uses OS-level +// errors, we need to map a few high-level errors to OS-level +// constants. +#if defined(XP_UNIX) +# define OS_ERROR_FILE_EXISTS EEXIST +# define OS_ERROR_NOMEM ENOMEM +# define OS_ERROR_INVAL EINVAL +# define OS_ERROR_TOO_LARGE EFBIG +# define OS_ERROR_RACE EIO +#elif defined(XP_WIN) +# define OS_ERROR_FILE_EXISTS ERROR_ALREADY_EXISTS +# define OS_ERROR_NOMEM ERROR_NOT_ENOUGH_MEMORY +# define OS_ERROR_INVAL ERROR_BAD_ARGUMENTS +# define OS_ERROR_TOO_LARGE ERROR_FILE_TOO_LARGE +# define OS_ERROR_RACE ERROR_SHARING_VIOLATION +#else +# error "We do not have platform-specific constants for this platform" +#endif + +///////// Results of OS.File operations + +/** + * Base class for results passed to the callbacks. + * + * This base class implements caching of JS values returned to the client. + * We make use of this caching in derived classes e.g. to avoid accidents + * when we transfer data allocated on another thread into JS. Note that + * this caching can lead to cycles (e.g. if a client adds a back-reference + * in the JS value), so we implement all Cycle Collector primitives in + * AbstractResult. + */ +class AbstractResult : public nsINativeOSFileResult { + public: + NS_DECL_NSINATIVEOSFILERESULT + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbstractResult) + + /** + * Construct the result object. Must be called on the main thread + * as the AbstractResult is cycle-collected. + * + * @param aStartDate The instant at which the operation was + * requested. Used to collect Telemetry statistics. + */ + explicit AbstractResult(TimeStamp aStartDate) : mStartDate(aStartDate) { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::HoldJSObjects(this); + } + + /** + * Setup the AbstractResult once data is available. + * + * @param aDispatchDate The instant at which the IO thread received + * the operation request. Used to collect Telemetry statistics. + * @param aExecutionDuration The duration of the operation on the + * IO thread. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration) { + MOZ_ASSERT(!NS_IsMainThread()); + + mDispatchDuration = (aDispatchDate - mStartDate); + mExecutionDuration = aExecutionDuration; + } + + /** + * Drop any data that could lead to a cycle. + */ + void DropJSData() { mCachedResult = JS::UndefinedValue(); } + + protected: + virtual ~AbstractResult() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::DropJSObjects(this); + } + + virtual nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle aResult) = 0; + + private: + TimeStamp mStartDate; + TimeDuration mDispatchDuration; + TimeDuration mExecutionDuration; + JS::Heap mCachedResult; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AbstractResult) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AbstractResult) + +NS_IMPL_CYCLE_COLLECTION_CLASS(AbstractResult) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbstractResult) + NS_INTERFACE_MAP_ENTRY(nsINativeOSFileResult) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AbstractResult) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedResult) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractResult) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AbstractResult) + tmp->DropJSData(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMETHODIMP +AbstractResult::GetDispatchDurationMS(double* aDispatchDuration) { + *aDispatchDuration = mDispatchDuration.ToMilliseconds(); + return NS_OK; +} + +NS_IMETHODIMP +AbstractResult::GetExecutionDurationMS(double* aExecutionDuration) { + *aExecutionDuration = mExecutionDuration.ToMilliseconds(); + return NS_OK; +} + +NS_IMETHODIMP +AbstractResult::GetResult(JSContext* cx, JS::MutableHandle aResult) { + if (mCachedResult.isUndefined()) { + nsresult rv = GetCacheableResult(cx, aResult); + if (NS_FAILED(rv)) { + return rv; + } + mCachedResult = aResult; + return NS_OK; + } + aResult.set(mCachedResult); + return NS_OK; +} + +/** + * Return a result as a string. + * + * In this implementation, attribute |result| is a string. Strings are + * passed to JS without copy. + */ +class StringResult final : public AbstractResult { + public: + explicit StringResult(TimeStamp aStartDate) : AbstractResult(aStartDate) {} + + /** + * Initialize the object once the contents of the result as available. + * + * @param aContents The string to pass to JavaScript. Ownership of the + * string and its contents is passed to StringResult. The string must + * be valid UTF-16. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + nsString& aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle aResult) override; + + private: + nsString mContents; +}; + +nsresult StringResult::GetCacheableResult( + JSContext* cx, JS::MutableHandle aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mContents.get()); + + // Convert mContents to a js string without copy. Note that this + // may have the side-effect of stealing the contents of the string + // from XPCOM and into JS. + if (!xpc::StringToJsval(cx, mContents, aResult)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +/** + * Return a result as a Uint8Array. + * + * In this implementation, attribute |result| is a Uint8Array. The array + * is passed to JS without memory copy. + */ +class TypedArrayResult final : public AbstractResult { + public: + explicit TypedArrayResult(TimeStamp aStartDate) + : AbstractResult(aStartDate) {} + + /** + * @param aContents The contents to pass to JS. Calling this method. + * transmits ownership of the ArrayBufferContents to the TypedArrayResult. + * Do not reuse this value anywhere else. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + ArrayBufferContents aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle aResult) override; + + private: + ScopedArrayBufferContents mContents; +}; + +nsresult TypedArrayResult::GetCacheableResult( + JSContext* cx, JS::MutableHandle aResult) { + MOZ_ASSERT(NS_IsMainThread()); + // We cannot simply construct a typed array using contents.data as + // this would allow us to have several otherwise unrelated + // ArrayBuffers with the same underlying C buffer. As this would be + // very unsafe, we need to cache the result once we have it. + + const ArrayBufferContents& contents = mContents.get(); + MOZ_ASSERT(contents.data); + + // This takes ownership of the buffer and notes the memory allocation. + JS::Rooted arrayBuffer( + cx, JS::NewArrayBufferWithContents(cx, contents.nbytes, contents.data)); + if (!arrayBuffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted result( + cx, JS_NewUint8ArrayWithBuffer(cx, arrayBuffer, 0, contents.nbytes)); + if (!result) { + return NS_ERROR_OUT_OF_MEMORY; + } + mContents.forget(); + + aResult.setObject(*result); + return NS_OK; +} + +/** + * Return a result as an int32_t. + * + * In this implementation, attribute |result| is an int32_t. + */ +class Int32Result final : public AbstractResult { + public: + explicit Int32Result(TimeStamp aStartDate) + : AbstractResult(aStartDate), mContents(0) {} + + /** + * Initialize the object once the contents of the result are available. + * + * @param aContents The contents to pass to JS. This is an int32_t. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + int32_t aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle aResult) override; + + private: + int32_t mContents; +}; + +nsresult Int32Result::GetCacheableResult(JSContext* cx, + JS::MutableHandle aResult) { + MOZ_ASSERT(NS_IsMainThread()); + aResult.set(JS::NumberValue(mContents)); + return NS_OK; +} + +//////// Callback events + +/** + * An event used to notify asynchronously of an error. + */ +class OSFileErrorEvent final : public Runnable { + public: + /** + * @param aOnSuccess The success callback. + * @param aOnError The error callback. + * @param aDiscardedResult The discarded result. + * @param aOperation The name of the operation, used for error reporting. + * @param aOSError The OS error of the operation, as returned by errno/ + * GetLastError(). + * + * Note that we pass both the success callback and the error + * callback, as well as the discarded result to ensure that they are + * all released on the main thread, rather than on the IO thread + * (which would hopefully segfault). Also, we pass the callbacks as + * alread_AddRefed to ensure that we do not manipulate main-thread + * only refcounters off the main thread. + */ + OSFileErrorEvent( + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError, + already_AddRefed& aDiscardedResult, + const nsACString& aOperation, int32_t aOSError) + : Runnable("OSFileErrorEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError), + mDiscardedResult(aDiscardedResult), + mOSError(aOSError), + mOperation(aOperation) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + (void)mOnError->Complete(mOperation, mOSError); + + // Ensure that the callbacks are released on the main thread. + mOnSuccess = nullptr; + mOnError = nullptr; + mDiscardedResult = nullptr; + + return NS_OK; + } + + private: + // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally + // xpconnect values, which cannot be manipulated with nsCOMPtr off + // the main thread. We store both the success callback and the + // error callback to ensure that they are safely released on the + // main thread. + nsMainThreadPtrHandle mOnSuccess; + nsMainThreadPtrHandle mOnError; + RefPtr mDiscardedResult; + int32_t mOSError; + nsCString mOperation; +}; + +/** + * An event used to notify of a success. + */ +class SuccessEvent final : public Runnable { + public: + /** + * @param aOnSuccess The success callback. + * @param aOnError The error callback. + * + * Note that we pass both the success callback and the error + * callback to ensure that they are both released on the main + * thread, rather than on the IO thread (which would hopefully + * segfault). Also, we pass them as alread_AddRefed to ensure that + * we do not manipulate xpconnect refcounters off the main thread + * (which is illegal). + */ + SuccessEvent( + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError, + already_AddRefed& aResult) + : Runnable("SuccessEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError), + mResult(aResult) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + (void)mOnSuccess->Complete(mResult); + + // Ensure that the callbacks are released on the main thread. + mOnSuccess = nullptr; + mOnError = nullptr; + mResult = nullptr; + + return NS_OK; + } + + private: + // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally + // xpconnect values, which cannot be manipulated with nsCOMPtr off + // the main thread. We store both the success callback and the + // error callback to ensure that they are safely released on the + // main thread. + nsMainThreadPtrHandle mOnSuccess; + nsMainThreadPtrHandle mOnError; + RefPtr mResult; +}; + +//////// Action events + +/** + * Base class shared by actions. + */ +class AbstractDoEvent : public Runnable { + public: + AbstractDoEvent( + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError) + : Runnable("AbstractDoEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError) +#if defined(DEBUG) + , + mResolved(false) +#endif // defined(DEBUG) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + /** + * Fail, asynchronously. + */ + void Fail(const nsACString& aOperation, + already_AddRefed&& aDiscardedResult, + int32_t aOSError = 0) { + Resolve(); + + RefPtr event = new OSFileErrorEvent( + mOnSuccess, mOnError, aDiscardedResult, aOperation, aOSError); + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + // Last ditch attempt to release on the main thread - some of + // the members of event are not thread-safe, so letting the + // pointer go out of scope would cause a crash. + NS_ReleaseOnMainThread("AbstractDoEvent::OSFileErrorEvent", + event.forget()); + } + } + + /** + * Succeed, asynchronously. + */ + void Succeed(already_AddRefed&& aResult) { + Resolve(); + RefPtr event = + new SuccessEvent(mOnSuccess, mOnError, aResult); + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + // Last ditch attempt to release on the main thread - some of + // the members of event are not thread-safe, so letting the + // pointer go out of scope would cause a crash. + NS_ReleaseOnMainThread("AbstractDoEvent::SuccessEvent", event.forget()); + } + } + + private: + /** + * Mark the event as complete, for debugging purposes. + */ + void Resolve() { +#if defined(DEBUG) + MOZ_ASSERT(!mResolved); + mResolved = true; +#endif // defined(DEBUG) + } + + private: + nsMainThreadPtrHandle mOnSuccess; + nsMainThreadPtrHandle mOnError; +#if defined(DEBUG) + // |true| once the action is complete + bool mResolved; +#endif // defined(DEBUG) +}; + +/** + * An abstract event implementing reading from a file. + * + * Concrete subclasses are responsible for handling the + * data obtained from the file and possibly post-processing it. + */ +class AbstractReadEvent : public AbstractDoEvent { + public: + /** + * @param aPath The path of the file. + */ + AbstractReadEvent( + const nsAString& aPath, const uint64_t aBytes, + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError) + : AbstractDoEvent(aOnSuccess, aOnError), mPath(aPath), mBytes(aBytes) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + TimeStamp dispatchDate = TimeStamp::Now(); + + nsresult rv = BeforeRead(); + if (NS_FAILED(rv)) { + // Error reporting is handled by BeforeRead(); + return NS_OK; + } + + ScopedArrayBufferContents buffer; + rv = Read(buffer); + if (NS_FAILED(rv)) { + // Error reporting is handled by Read(); + return NS_OK; + } + + AfterRead(dispatchDate, buffer); + return NS_OK; + } + + private: + /** + * Read synchronously. + * + * Must be called off the main thread. + * + * @param aBuffer The destination buffer. + */ + nsresult Read(ScopedArrayBufferContents& aBuffer) { + MOZ_ASSERT(!NS_IsMainThread()); + + ScopedPRFileDesc file; +#if defined(XP_WIN) + // On Windows, we can't use PR_OpenFile because it doesn't + // handle UTF-16 encoding, which is pretty bad. In addition, + // PR_OpenFile opens files without sharing, which is not the + // general semantics of OS.File. + HANDLE handle = + ::CreateFileW(mPath.get(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, + /*Template file*/ nullptr); + + if (handle == INVALID_HANDLE_VALUE) { + Fail("open"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + + file = PR_ImportFile((PROsfd)handle); + if (!file) { + // |file| is closed by PR_ImportFile + Fail("ImportFile"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#else + // On other platforms, PR_OpenFile will do. + NS_ConvertUTF16toUTF8 path(mPath); + file = PR_OpenFile(path.get(), PR_RDONLY, 0); + if (!file) { + Fail("open"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#endif // defined(XP_XIN) + + PRFileInfo64 stat; + if (PR_GetOpenFileInfo64(file, &stat) != PR_SUCCESS) { + Fail("stat"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + + uint64_t bytes = std::min((uint64_t)stat.size, mBytes); + if (bytes > UINT32_MAX) { + Fail("Arithmetics"_ns, nullptr, OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + + if (!aBuffer.Allocate(bytes)) { + Fail("allocate"_ns, nullptr, OS_ERROR_NOMEM); + return NS_ERROR_FAILURE; + } + + uint64_t total_read = 0; + int32_t just_read = 0; + char* dest_chars = reinterpret_cast(aBuffer.rwget().data); + do { + just_read = PR_Read(file, dest_chars + total_read, + std::min(uint64_t(PR_INT32_MAX), bytes - total_read)); + if (just_read == -1) { + Fail("read"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + total_read += just_read; + } while (just_read != 0 && total_read < bytes); + if (total_read != bytes) { + // We seem to have a race condition here. + Fail("read"_ns, nullptr, OS_ERROR_RACE); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + protected: + /** + * Any steps that need to be taken before reading. + * + * In case of error, this method should call Fail() and return + * a failure code. + */ + virtual nsresult BeforeRead() { return NS_OK; } + + /** + * Proceed after reading. + */ + virtual void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) = 0; + + protected: + const nsString mPath; + const uint64_t mBytes; +}; + +/** + * An implementation of a Read event that provides the data + * as a TypedArray. + */ +class DoReadToTypedArrayEvent final : public AbstractReadEvent { + public: + DoReadToTypedArrayEvent( + const nsAString& aPath, const uint32_t aBytes, + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError) + : AbstractReadEvent(aPath, aBytes, aOnSuccess, aOnError), + mResult(new TypedArrayResult(TimeStamp::Now())) {} + + ~DoReadToTypedArrayEvent() override { + // If AbstractReadEvent::Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoReadToTypedArrayEvent::mResult", + mResult.forget()); + } + + protected: + void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) override { + MOZ_ASSERT(!NS_IsMainThread()); + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + aBuffer.forget()); + Succeed(mResult.forget()); + } + + private: + RefPtr mResult; +}; + +/** + * An implementation of a Read event that provides the data + * as a JavaScript string. + */ +class DoReadToStringEvent final : public AbstractReadEvent { + public: + DoReadToStringEvent( + const nsAString& aPath, const nsACString& aEncoding, + const uint32_t aBytes, + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError) + : AbstractReadEvent(aPath, aBytes, aOnSuccess, aOnError), + mEncoding(aEncoding), + mResult(new StringResult(TimeStamp::Now())) {} + + ~DoReadToStringEvent() override { + // If AbstraactReadEvent::Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoReadToStringEvent::mResult", mResult.forget()); + } + + protected: + nsresult BeforeRead() override { + // Obtain the decoder. We do this before reading to avoid doing + // any unnecessary I/O in case the name of the encoding is incorrect. + MOZ_ASSERT(!NS_IsMainThread()); + const Encoding* encoding = Encoding::ForLabel(mEncoding); + if (!encoding) { + Fail("Decode"_ns, mResult.forget(), OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + mDecoder = encoding->NewDecoderWithBOMRemoval(); + if (!mDecoder) { + Fail("DecoderForEncoding"_ns, mResult.forget(), OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) override { + MOZ_ASSERT(!NS_IsMainThread()); + + auto src = Span(aBuffer.get().data, aBuffer.get().nbytes); + + CheckedInt needed = mDecoder->MaxUTF16BufferLength(src.Length()); + if (!needed.isValid() || + needed.value() > std::numeric_limits::max()) { + Fail("arithmetics"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + nsString resultString; + auto resultSpan = resultString.GetMutableData(needed.value(), fallible); + if (!resultSpan) { + Fail("allocation"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + // Yoric said on IRC that this method is normally called for the entire + // file, but that's not guaranteed. Retaining the bug that EOF in conversion + // isn't handled anywhere. + uint32_t result; + size_t read; + size_t written; + std::tie(result, read, written, std::ignore) = + mDecoder->DecodeToUTF16(src, *resultSpan, false); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == src.Length()); + MOZ_ASSERT(written <= needed.value()); + bool ok = resultString.SetLength(written, fallible); + if (!ok) { + Fail("allocation"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + resultString); + Succeed(mResult.forget()); + } + + private: + nsCString mEncoding; + mozilla::UniquePtr mDecoder; + RefPtr mResult; +}; + +/** + * An event implenting writing atomically to a file. + */ +class DoWriteAtomicEvent : public AbstractDoEvent { + public: + /** + * @param aPath The path of the file. + */ + DoWriteAtomicEvent( + const nsAString& aPath, UniquePtr aBuffer, + const uint64_t aBytes, const nsAString& aTmpPath, + const nsAString& aBackupTo, const bool aFlush, const bool aNoOverwrite, + nsMainThreadPtrHandle& aOnSuccess, + nsMainThreadPtrHandle& aOnError) + : AbstractDoEvent(aOnSuccess, aOnError), + mPath(aPath), + mBuffer(std::move(aBuffer)), + mBytes(aBytes), + mTmpPath(aTmpPath), + mBackupTo(aBackupTo), + mFlush(aFlush), + mNoOverwrite(aNoOverwrite), + mResult(new Int32Result(TimeStamp::Now())) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~DoWriteAtomicEvent() override { + // If Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoWriteAtomicEvent::mResult", mResult.forget()); + } + + NS_IMETHODIMP Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + TimeStamp dispatchDate = TimeStamp::Now(); + int32_t bytesWritten; + + nsresult rv = WriteAtomic(&bytesWritten); + if (NS_FAILED(rv)) { + return NS_OK; + } + + AfterWriteAtomic(dispatchDate, bytesWritten); + return NS_OK; + } + + private: + /** + * Write atomically to a file. + * Must be called off the main thread. + * @param aBytesWritten will contain the total bytes written. + * This does not support compression in this implementation. + */ + nsresult WriteAtomic(int32_t* aBytesWritten) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Note: In Windows, many NSPR File I/O functions which act on pathnames + // do not handle UTF-16 encoding. Thus, we use the following functions + // to overcome this. + // PR_Access : GetFileAttributesW + // PR_Delete : DeleteFileW + // PR_OpenFile : CreateFileW followed by PR_ImportFile + // PR_Rename : MoveFileW + + ScopedPRFileDesc file; + NS_ConvertUTF16toUTF8 path(mPath); + NS_ConvertUTF16toUTF8 tmpPath(mTmpPath); + NS_ConvertUTF16toUTF8 backupTo(mBackupTo); + bool fileExists = false; + + if (!mTmpPath.IsVoid() || !mBackupTo.IsVoid() || mNoOverwrite) { + // fileExists needs to be computed in the case of tmpPath, since + // the rename behaves differently depending on whether the + // file already exists. It's also computed for backupTo since the + // backup can be skipped if the file does not exist in the first place. +#if defined(XP_WIN) + fileExists = ::GetFileAttributesW(mPath.get()) != INVALID_FILE_ATTRIBUTES; +#else + fileExists = PR_Access(path.get(), PR_ACCESS_EXISTS) == PR_SUCCESS; +#endif // defined(XP_WIN) + } + + // Check noOverwrite. + if (mNoOverwrite && fileExists) { + Fail("noOverwrite"_ns, nullptr, OS_ERROR_FILE_EXISTS); + return NS_ERROR_FAILURE; + } + + // Backup the original file if it exists. + if (!mBackupTo.IsVoid() && fileExists) { +#if defined(XP_WIN) + if (::GetFileAttributesW(mBackupTo.get()) != INVALID_FILE_ATTRIBUTES) { + // The file specified by mBackupTo exists, so we need to delete it + // first. + if (::DeleteFileW(mBackupTo.get()) == false) { + Fail("delete"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + } + + if (::MoveFileW(mPath.get(), mBackupTo.get()) == false) { + Fail("rename"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Access(backupTo.get(), PR_ACCESS_EXISTS) == PR_SUCCESS) { + // The file specified by mBackupTo exists, so we need to delete it + // first. + if (PR_Delete(backupTo.get()) == PR_FAILURE) { + Fail("delete"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + } + + if (PR_Rename(path.get(), backupTo.get()) == PR_FAILURE) { + Fail("rename"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + +#if defined(XP_WIN) + // In addition to not handling UTF-16 encoding in file paths, + // PR_OpenFile opens files without sharing, which is not the + // general semantics of OS.File. + HANDLE handle; + // if we're dealing with a tmpFile, we need to write there. + if (!mTmpPath.IsVoid()) { + handle = ::CreateFileW( + mTmpPath.get(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, + // CREATE_ALWAYS is used since since we need to create the temporary + // file, which we don't care about overwriting. + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, + /*Template file*/ nullptr); + } else { + handle = ::CreateFileW( + mPath.get(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, + // CREATE_ALWAYS is used since since have already checked the + // noOverwrite condition, and thus can overwrite safely. + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, + /*Template file*/ nullptr); + } + + if (handle == INVALID_HANDLE_VALUE) { + Fail("open"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + + file = PR_ImportFile((PROsfd)handle); + if (!file) { + // |file| is closed by PR_ImportFile + Fail("ImportFile"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#else + // if we're dealing with a tmpFile, we need to write there. + if (!mTmpPath.IsVoid()) { + file = + PR_OpenFile(tmpPath.get(), PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + PR_IRUSR | PR_IWUSR); + } else { + file = PR_OpenFile(path.get(), PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + PR_IRUSR | PR_IWUSR); + } + + if (!file) { + Fail("open"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + + int32_t bytesWrittenSuccess = + PR_Write(file, (void*)(mBuffer.get()), mBytes); + + if (bytesWrittenSuccess == -1) { + Fail("write"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + + // Apply any tmpPath renames. + if (!mTmpPath.IsVoid()) { + if (mBackupTo.IsVoid() && fileExists) { + // We need to delete the old file first, if it exists and we haven't + // already renamed it as a part of backing it up. +#if defined(XP_WIN) + if (::DeleteFileW(mPath.get()) == false) { + Fail("delete"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Delete(path.get()) == PR_FAILURE) { + Fail("delete"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + +#if defined(XP_WIN) + if (::MoveFileW(mTmpPath.get(), mPath.get()) == false) { + Fail("rename"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Rename(tmpPath.get(), path.get()) == PR_FAILURE) { + Fail("rename"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + + if (mFlush) { + if (PR_Sync(file) == PR_FAILURE) { + Fail("sync"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + } + + *aBytesWritten = bytesWrittenSuccess; + return NS_OK; + } + + protected: + nsresult AfterWriteAtomic(TimeStamp aDispatchDate, int32_t aBytesWritten) { + MOZ_ASSERT(!NS_IsMainThread()); + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + aBytesWritten); + Succeed(mResult.forget()); + return NS_OK; + } + + const nsString mPath; + const UniquePtr mBuffer; + const int32_t mBytes; + const nsString mTmpPath; + const nsString mBackupTo; + const bool mFlush; + const bool mNoOverwrite; + + private: + RefPtr mResult; +}; + +} // namespace + +// The OS.File service + +NS_IMPL_ISUPPORTS(NativeOSFileInternalsService, + nsINativeOSFileInternalsService); + +NS_IMETHODIMP +NativeOSFileInternalsService::Read(const nsAString& aPath, + JS::Handle aOptions, + nsINativeOSFileSuccessCallback* aOnSuccess, + nsINativeOSFileErrorCallback* aOnError, + JSContext* cx) { + // Extract options + nsCString encoding; + uint64_t bytes = UINT64_MAX; + + if (aOptions.isObject()) { + dom::NativeOSFileReadOptions dict; + if (!dict.Init(cx, aOptions)) { + return NS_ERROR_INVALID_ARG; + } + + if (dict.mEncoding.WasPassed()) { + CopyUTF16toUTF8(dict.mEncoding.Value(), encoding); + } + + if (dict.mBytes.WasPassed() && !dict.mBytes.Value().IsNull()) { + bytes = dict.mBytes.Value().Value(); + } + } + + // Prepare the off main thread event and dispatch it + nsCOMPtr onSuccess(aOnSuccess); + nsMainThreadPtrHandle onSuccessHandle( + new nsMainThreadPtrHolder( + "nsINativeOSFileSuccessCallback", onSuccess)); + nsCOMPtr onError(aOnError); + nsMainThreadPtrHandle onErrorHandle( + new nsMainThreadPtrHolder( + "nsINativeOSFileErrorCallback", onError)); + + RefPtr event; + if (encoding.IsEmpty()) { + event = new DoReadToTypedArrayEvent(aPath, bytes, onSuccessHandle, + onErrorHandle); + } else { + event = new DoReadToStringEvent(aPath, encoding, bytes, onSuccessHandle, + onErrorHandle); + } + + nsresult rv; + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + + if (NS_FAILED(rv)) { + return rv; + } + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +// Note: This method steals the contents of `aBuffer`. +NS_IMETHODIMP +NativeOSFileInternalsService::WriteAtomic( + const nsAString& aPath, JS::Handle aBuffer, + JS::Handle aOptions, nsINativeOSFileSuccessCallback* aOnSuccess, + nsINativeOSFileErrorCallback* aOnError, JSContext* cx) { + MOZ_ASSERT(NS_IsMainThread()); + // Extract typed-array/string into buffer. We also need to store the length + // of the buffer as that may be required if not provided in `aOptions`. + UniquePtr buffer; + int32_t bytes; + + // The incoming buffer must be an Object. + if (!aBuffer.isObject()) { + return NS_ERROR_INVALID_ARG; + } + + JS::Rooted bufferObject(cx, nullptr); + if (!JS_ValueToObject(cx, aBuffer, &bufferObject)) { + return NS_ERROR_FAILURE; + } + if (!JS::IsArrayBufferObject(bufferObject.get())) { + return NS_ERROR_INVALID_ARG; + } + + { + // Throw for large ArrayBuffers to prevent truncation. + size_t len = JS::GetArrayBufferByteLength(bufferObject.get()); + if (len > INT32_MAX) { + return NS_ERROR_INVALID_ARG; + } + bytes = len; + } + buffer.reset( + static_cast(JS::StealArrayBufferContents(cx, bufferObject))); + + if (!buffer) { + return NS_ERROR_FAILURE; + } + + // Extract options. + dom::NativeOSFileWriteAtomicOptions dict; + + if (aOptions.isObject()) { + if (!dict.Init(cx, aOptions)) { + return NS_ERROR_INVALID_ARG; + } + } else { + // If an options object is not provided, initializing with a `null` + // value, which will give a set of defaults defined in the WebIDL binding. + if (!dict.Init(cx, JS::NullHandleValue)) { + return NS_ERROR_FAILURE; + } + } + + if (dict.mBytes.WasPassed() && !dict.mBytes.Value().IsNull()) { + // We need to check size and cast because NSPR and WebIDL have different + // types. + if (dict.mBytes.Value().Value() > PR_INT32_MAX) { + return NS_ERROR_INVALID_ARG; + } + bytes = (int32_t)(dict.mBytes.Value().Value()); + } + + // Prepare the off main thread event and dispatch it + nsCOMPtr onSuccess(aOnSuccess); + nsMainThreadPtrHandle onSuccessHandle( + new nsMainThreadPtrHolder( + "nsINativeOSFileSuccessCallback", onSuccess)); + nsCOMPtr onError(aOnError); + nsMainThreadPtrHandle onErrorHandle( + new nsMainThreadPtrHolder( + "nsINativeOSFileErrorCallback", onError)); + + RefPtr event = new DoWriteAtomicEvent( + aPath, std::move(buffer), bytes, dict.mTmpPath, dict.mBackupTo, + dict.mFlush, dict.mNoOverwrite, onSuccessHandle, onErrorHandle); + nsresult rv; + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + + if (NS_FAILED(rv)) { + return rv; + } + + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +} // namespace mozilla diff --git a/toolkit/components/osfile/NativeOSFileInternals.h b/toolkit/components/osfile/NativeOSFileInternals.h new file mode 100644 index 000000000000..f7936a1ec456 --- /dev/null +++ b/toolkit/components/osfile/NativeOSFileInternals.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_nativeosfileinternalservice_h__ +#define mozilla_nativeosfileinternalservice_h__ + +#include "nsINativeOSFileInternals.h" + +#include "nsISupports.h" + +namespace mozilla { + +class NativeOSFileInternalsService final + : public nsINativeOSFileInternalsService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSINATIVEOSFILEINTERNALSSERVICE + private: + ~NativeOSFileInternalsService() = default; + // Avoid accidental use of built-in operator= + void operator=(const NativeOSFileInternalsService& other) = delete; +}; + +} // namespace mozilla + +#endif // mozilla_finalizationwitnessservice_h__ diff --git a/toolkit/components/osfile/modules/moz.build b/toolkit/components/osfile/modules/moz.build new file mode 100644 index 000000000000..ea14a82b76b4 --- /dev/null +++ b/toolkit/components/osfile/modules/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES.osfile += [ + "osfile_async_front.jsm", + "osfile_async_worker.js", + "osfile_native.jsm", + "osfile_shared_allthreads.jsm", + "osfile_shared_front.js", + "ospath.jsm", + "ospath_unix.jsm", + "ospath_win.jsm", +] + +if CONFIG["OS_TARGET"] == "WINNT": + EXTRA_JS_MODULES.osfile += [ + "osfile_win_allthreads.jsm", + "osfile_win_back.js", + "osfile_win_front.js", + ] +else: + EXTRA_JS_MODULES.osfile += [ + "osfile_unix_allthreads.jsm", + "osfile_unix_back.js", + "osfile_unix_front.js", + ] diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm new file mode 100644 index 000000000000..04100160eeba --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -0,0 +1,1570 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Asynchronous front-end for OS.File. + * + * This front-end is meant to be imported from the main thread. In turn, + * it spawns one worker (perhaps more in the future) and delegates all + * disk I/O to this worker. + * + * Documentation note: most of the functions and methods in this module + * return promises. For clarity, we denote as follows a promise that may resolve + * with type |A| and some value |value| or reject with type |B| and some + * reason |reason| + * @resolves {A} value + * @rejects {B} reason + */ + +"use strict"; + +// Scheduler is exported for test-only usage. +var EXPORTED_SYMBOLS = ["OS", "Scheduler"]; + +var SharedAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" +); +const { clearInterval, setInterval } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// Boilerplate, to simplify the transition to require() +var LOG = SharedAll.LOG.bind(SharedAll, "Controller"); +var isTypedArray = SharedAll.isTypedArray; + +// The constructor for file errors. +var SysAll; +if (SharedAll.Constants.Win) { + SysAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_win_allthreads.jsm" + ); +} else if (SharedAll.Constants.libc) { + SysAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_unix_allthreads.jsm" + ); +} else { + throw new Error("I am neither under Windows nor under a Posix system"); +} +var OSError = SysAll.Error; +var Type = SysAll.Type; + +var Path = ChromeUtils.import("resource://gre/modules/osfile/ospath.jsm"); + +const lazy = {}; + +// The library of promises. +ChromeUtils.defineESModuleGetters(lazy, { + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +// The implementation of communications +const { BasePromiseWorker } = ChromeUtils.import( + "resource://gre/modules/PromiseWorker.jsm" +); +const { AsyncShutdown } = ChromeUtils.importESModule( + "resource://gre/modules/AsyncShutdown.sys.mjs" +); +var Native = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_native.jsm" +); + +// It's possible for osfile.jsm to get imported before the profile is +// set up. In this case, some path constants aren't yet available. +// Here, we make them lazy loaders. + +function lazyPathGetter(constProp, dirKey) { + return function() { + let path; + try { + path = Services.dirsvc.get(dirKey, Ci.nsIFile).path; + delete SharedAll.Constants.Path[constProp]; + SharedAll.Constants.Path[constProp] = path; + } catch (ex) { + // Ignore errors if the value still isn't available. Hopefully + // the next access will return it. + } + + return path; + }; +} + +for (let [constProp, dirKey] of [ + ["localProfileDir", "ProfLD"], + ["profileDir", "ProfD"], + ["userApplicationDataDir", "UAppData"], + ["winAppDataDir", "AppData"], + ["winLocalAppDataDir", "LocalAppData"], + ["winStartMenuProgsDir", "Progs"], + ["tmpDir", "TmpD"], + ["homeDir", "Home"], + ["macUserLibDir", "ULibDir"], +]) { + if (constProp in SharedAll.Constants.Path) { + continue; + } + + LOG( + "Installing lazy getter for OS.Constants.Path." + + constProp + + " because it isn't defined and profile may not be loaded." + ); + Object.defineProperty(SharedAll.Constants.Path, constProp, { + get: lazyPathGetter(constProp, dirKey), + }); +} + +/** + * Return a shallow clone of the enumerable properties of an object. + */ +var clone = SharedAll.clone; + +/** + * Extract a shortened version of an object, fit for logging. + * + * This function returns a copy of the original object in which all + * long strings, Arrays, TypedArrays, ArrayBuffers are removed and + * replaced with placeholders. Use this function to sanitize objects + * if you wish to log them or to keep them in memory. + * + * @param {*} obj The obj to shorten. + * @return {*} array A shorter object, fit for logging. + */ +function summarizeObject(obj) { + if (!obj) { + return null; + } + if (typeof obj == "string") { + if (obj.length > 1024) { + return { "Long string": obj.length }; + } + return obj; + } + if (typeof obj == "object") { + if (Array.isArray(obj)) { + if (obj.length > 32) { + return { "Long array": obj.length }; + } + return obj.map(summarizeObject); + } + if ("byteLength" in obj) { + // Assume TypedArray or ArrayBuffer + return { "Binary Data": obj.byteLength }; + } + let result = {}; + for (let k of Object.keys(obj)) { + result[k] = summarizeObject(obj[k]); + } + return result; + } + return obj; +} + +var Scheduler = { + /** + * |true| once we have sent at least one message to the worker. + * This field is unaffected by resetting the worker. + */ + launched: false, + + /** + * |true| once shutdown has begun i.e. we should reject any + * message, including resets. + */ + shutdown: false, + + /** + * A promise resolved once all currently pending operations are complete. + * + * This promise is never rejected and the result is always undefined. + */ + queue: Promise.resolve(), + + /** + * A promise resolved once all currently pending `kill` operations + * are complete. + * + * This promise is never rejected and the result is always undefined. + */ + _killQueue: Promise.resolve(), + + /** + * Miscellaneous debugging information + */ + Debugging: { + /** + * The latest message sent and still waiting for a reply. + */ + latestSent: undefined, + + /** + * The latest reply received, or null if we are waiting for a reply. + */ + latestReceived: undefined, + + /** + * Number of messages sent to the worker. This includes the + * initial SET_DEBUG, if applicable. + */ + messagesSent: 0, + + /** + * Total number of messages ever queued, including the messages + * sent. + */ + messagesQueued: 0, + + /** + * Number of messages received from the worker. + */ + messagesReceived: 0, + }, + + /** + * A timer used to automatically shut down the worker after some time. + */ + resetTimer: null, + + /** + * A flag indicating whether we had some activities when waiting the + * timer and if it's not we can shut down the worker. + */ + hasRecentActivity: false, + + /** + * The worker to which to send requests. + * + * If the worker has never been created or has been reset, this is a + * fresh worker, initialized with osfile_async_worker.js. + * + * @type {PromiseWorker} + */ + get worker() { + if (!this._worker) { + // Either the worker has never been created or it has been + // reset. In either case, it is time to instantiate the worker. + this._worker = new BasePromiseWorker( + "resource://gre/modules/osfile/osfile_async_worker.js" + ); + this._worker.log = LOG; + this._worker.ExceptionHandlers["OS.File.Error"] = OSError.fromMsg; + + let delay = Services.prefs.getIntPref("osfile.reset_worker_delay", 0); + if (delay) { + this.resetTimer = setInterval(() => { + if (this.hasRecentActivity) { + this.hasRecentActivity = false; + return; + } + clearInterval(this.resetTimer); + Scheduler.kill({ reset: true, shutdown: false }); + }, delay); + } + } + return this._worker; + }, + + _worker: null, + + /** + * Restart the OS.File worker killer timer. + */ + restartTimer(arg) { + this.hasRecentActivity = true; + }, + + /** + * Shutdown OS.File. + * + * @param {*} options + * - {boolean} shutdown If |true|, reject any further request. Otherwise, + * further requests will resurrect the worker. + * - {boolean} reset If |true|, instruct the worker to shutdown if this + * would not cause leaks. Otherwise, assume that the worker will be shutdown + * through some other mean. + */ + kill({ shutdown, reset }) { + // Grab the kill queue to make sure that we + // cannot be interrupted by another call to `kill`. + let killQueue = this._killQueue; + + // Deactivate the queue, to ensure that no message is sent + // to an obsolete worker (we reactivate it in the `finally`). + // This needs to be done right now so that we maintain relative + // ordering with calls to post(), etc. + let deferred = lazy.PromiseUtils.defer(); + let savedQueue = this.queue; + this.queue = deferred.promise; + + return (this._killQueue = (async () => { + await killQueue; + // From this point, and until the end of the Task, we are the + // only call to `kill`, regardless of any `yield`. + + await savedQueue; + + try { + // Enter critical section: no yield in this block + // (we want to make sure that we remain the only + // request in the queue). + + if (!this.launched || this.shutdown || !this._worker) { + // Nothing to kill + this.shutdown = this.shutdown || shutdown; + this._worker = null; + return null; + } + + // Exit critical section + + let message = ["Meta_shutdown", [reset]]; + + Scheduler.latestReceived = []; + let stack = new Error().stack; + Scheduler.latestSent = [Date.now(), stack, ...message]; + + // Wait for result + let resources; + try { + resources = await this._worker.post(...message); + + Scheduler.latestReceived = [Date.now(), message]; + } catch (ex) { + LOG("Could not dispatch Meta_reset", ex); + // It's most likely a programmer error, but we'll assume that + // the worker has been shutdown, as it's less risky than the + // opposite stance. + resources = { + openedFiles: [], + openedDirectoryIterators: [], + killed: true, + }; + + Scheduler.latestReceived = [Date.now(), message, ex]; + } + + let { openedFiles, openedDirectoryIterators, killed } = resources; + if ( + !reset && + ((openedFiles && openedFiles.length) || + (openedDirectoryIterators && openedDirectoryIterators.length)) + ) { + // The worker still holds resources. Report them. + + let msg = ""; + if (openedFiles.length) { + msg += + "The following files are still open:\n" + openedFiles.join("\n"); + } + if (openedDirectoryIterators.length) { + msg += + "The following directory iterators are still open:\n" + + openedDirectoryIterators.join("\n"); + } + + LOG("WARNING: File descriptors leaks detected.\n" + msg); + } + + // Make sure that we do not leave an invalid |worker| around. + if (killed || shutdown) { + this._worker = null; + } + + this.shutdown = shutdown; + + return resources; + } finally { + // Resume accepting messages. If we have set |shutdown| to |true|, + // any pending/future request will be rejected. Otherwise, any + // pending/future request will spawn a new worker if necessary. + deferred.resolve(); + } + })()); + }, + + /** + * Push a task at the end of the queue. + * + * @param {function} code A function returning a Promise. + * This function will be executed once all the previously + * pushed tasks have completed. + * @return {Promise} A promise with the same behavior as + * the promise returned by |code|. + */ + push(code) { + let promise = this.queue.then(code); + // By definition, |this.queue| can never reject. + this.queue = promise.catch(() => undefined); + // Fork |promise| to ensure that uncaught errors are reported + return promise.then(); + }, + + /** + * Post a message to the worker thread. + * + * @param {string} method The name of the method to call. + * @param {...} args The arguments to pass to the method. These arguments + * must be clonable. + * The last argument by convention may be an object `options`, with some of + * the following fields: + * - {number|null} outSerializationDuration A parameter to be filled with + * duration of the `this.worker.post` method. + * @return {Promise} A promise conveying the result/error caused by + * calling |method| with arguments |args|. + */ + post: function post(method, args = undefined, closure = undefined) { + if (this.shutdown) { + LOG( + "OS.File is not available anymore. The following request has been rejected.", + method, + args + ); + return Promise.reject( + new Error("OS.File has been shut down. Rejecting post to " + method) + ); + } + let firstLaunch = !this.launched; + this.launched = true; + + if (firstLaunch && SharedAll.Config.DEBUG) { + // If we have delayed sending SET_DEBUG, do it now. + this.worker.post("SET_DEBUG", [true]); + Scheduler.Debugging.messagesSent++; + } + + Scheduler.Debugging.messagesQueued++; + return this.push(async () => { + if (this.shutdown) { + LOG( + "OS.File is not available anymore. The following request has been rejected.", + method, + args + ); + throw new Error( + "OS.File has been shut down. Rejecting request to " + method + ); + } + + // Update debugging information. As |args| may be quite + // expensive, we only keep a shortened version of it. + Scheduler.Debugging.latestReceived = null; + Scheduler.Debugging.latestSent = [ + Date.now(), + method, + summarizeObject(args), + ]; + + // Don't kill the worker just yet + Scheduler.restartTimer(); + + // The last object inside the args may be an options object. + let options = null; + if ( + args && + args.length >= 1 && + typeof args[args.length - 1] === "object" + ) { + options = args[args.length - 1]; + } + + let reply; + try { + try { + Scheduler.Debugging.messagesSent++; + Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice( + 0, + 2 + ); + let serializationStartTimeMs = Date.now(); + reply = await this.worker.post(method, args, closure); + let serializationEndTimeMs = Date.now(); + Scheduler.Debugging.latestReceived = [ + Date.now(), + summarizeObject(reply), + ]; + + // There were no options for recording the serialization duration. + if (options && "outSerializationDuration" in options) { + // The difference might be negative for very fast operations, since Date.now() may not be monotonic. + let serializationDurationMs = Math.max( + 0, + serializationEndTimeMs - serializationStartTimeMs + ); + + if (typeof options.outSerializationDuration === "number") { + options.outSerializationDuration += serializationDurationMs; + } else { + options.outSerializationDuration = serializationDurationMs; + } + } + return reply; + } finally { + Scheduler.Debugging.messagesReceived++; + } + } catch (error) { + Scheduler.Debugging.latestReceived = [ + Date.now(), + error.message, + error.fileName, + error.lineNumber, + ]; + throw error; + } finally { + if (firstLaunch) { + Scheduler._updateTelemetry(); + } + Scheduler.restartTimer(); + } + }); + }, + + /** + * Post Telemetry statistics. + * + * This is only useful on first launch. + */ + _updateTelemetry() { + let worker = this.worker; + let workerTimeStamps = worker.workerTimeStamps; + if (!workerTimeStamps) { + // If the first call to OS.File results in an uncaught errors, + // the timestamps are absent. As this case is a developer error, + // let's not waste time attempting to extract telemetry from it. + return; + } + let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById( + "OSFILE_WORKER_LAUNCH_MS" + ); + HISTOGRAM_LAUNCH.add( + worker.workerTimeStamps.entered - worker.launchTimeStamp + ); + + let HISTOGRAM_READY = Services.telemetry.getHistogramById( + "OSFILE_WORKER_READY_MS" + ); + HISTOGRAM_READY.add( + worker.workerTimeStamps.loaded - worker.launchTimeStamp + ); + }, +}; + +const PREF_OSFILE_LOG = "toolkit.osfile.log"; +const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect"; + +/** + * Safely read a PREF_OSFILE_LOG preference. + * Returns a value read or, in case of an error, oldPref or false. + * + * @param bool oldPref + * An optional value that the DEBUG flag was set to previously. + */ +function readDebugPref(prefName, oldPref = false) { + // If neither pref nor oldPref were set, default it to false. + return Services.prefs.getBoolPref(prefName, oldPref); +} + +/** + * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag + * appropriately. + */ +Services.prefs.addObserver(PREF_OSFILE_LOG, function prefObserver( + aSubject, + aTopic, + aData +) { + SharedAll.Config.DEBUG = readDebugPref( + PREF_OSFILE_LOG, + SharedAll.Config.DEBUG + ); + if (Scheduler.launched) { + // Don't start the worker just to set this preference. + Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]); + } +}); +SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false); + +Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT, function prefObserver( + aSubject, + aTopic, + aData +) { + SharedAll.Config.TEST = readDebugPref( + PREF_OSFILE_LOG_REDIRECT, + OS.Shared.TEST + ); +}); +SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false); + +/** + * If |true|, use the native implementaiton of OS.File methods + * whenever possible. Otherwise, force the use of the JS version. + */ +var nativeWheneverAvailable = true; +const PREF_OSFILE_NATIVE = "toolkit.osfile.native"; +Services.prefs.addObserver(PREF_OSFILE_NATIVE, function prefObserver( + aSubject, + aTopic, + aData +) { + nativeWheneverAvailable = readDebugPref( + PREF_OSFILE_NATIVE, + nativeWheneverAvailable + ); +}); + +// Update worker's DEBUG flag if it's true. +// Don't start the worker just for this, though. +if (SharedAll.Config.DEBUG && Scheduler.launched) { + Scheduler.post("SET_DEBUG", [true]); +} + +// Preference used to configure test shutdown observer. +const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER = + "toolkit.osfile.test.shutdown.observer"; + +AsyncShutdown.xpcomWillShutdown.addBlocker( + "OS.File: flush pending requests, warn about unclosed files, shut down service.", + async function() { + // Give clients a last chance to enqueue requests. + await Barriers.shutdown.wait({ crashAfterMS: null }); + + // Wait until all requests are complete and kill the worker. + await Scheduler.kill({ reset: false, shutdown: true }); + }, + () => { + let details = Barriers.getDetails(); + details.clients = Barriers.shutdown.state; + return details; + } +); + +// Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or +// disable the test shutdown event observer. +// Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset. +// Note: This is meant to be used for testing purposes only. +Services.prefs.addObserver( + PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, + function prefObserver() { + // The temporary phase topic used to trigger the unclosed + // phase warning. + let TOPIC = Services.prefs.getCharPref( + PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, + "" + ); + if (TOPIC) { + // Generate a phase, add a blocker. + // Note that this can work only if AsyncShutdown itself has been + // configured for testing by the testsuite. + let phase = AsyncShutdown._getPhase(TOPIC); + phase.addBlocker( + "(for testing purposes) OS.File: warn about unclosed files", + () => Scheduler.kill({ shutdown: false, reset: false }) + ); + } + } +); + +/** + * Representation of a file, with asynchronous methods. + * + * @param {*} fdmsg The _message_ representing the platform-specific file + * handle. + * + * @constructor + */ +var File = function File(fdmsg) { + // FIXME: At the moment, |File| does not close on finalize + // (see bug 777715) + this._fdmsg = fdmsg; + this._closeResult = null; + this._closed = null; +}; + +File.prototype = { + /** + * Close a file asynchronously. + * + * This method is idempotent. + * + * @return {promise} + * @resolves {null} + * @rejects {OS.File.Error} + */ + close: function close() { + if (this._fdmsg != null) { + let msg = this._fdmsg; + this._fdmsg = null; + return (this._closeResult = Scheduler.post( + "File_prototype_close", + [msg], + this + )); + } + return this._closeResult; + }, + + /** + * Fetch information about the file. + * + * @return {promise} + * @resolves {OS.File.Info} The latest information about the file. + * @rejects {OS.File.Error} + */ + stat: function stat() { + return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then( + File.Info.fromMsg + ); + }, + + /** + * Write bytes from a buffer to this file. + * + * Note that, by default, this function may perform several I/O + * operations to ensure that the buffer is fully written. + * + * @param {Typed array | C pointer} buffer The buffer in which the + * the bytes are stored. The buffer must be large enough to + * accomodate |bytes| bytes. Using the buffer before the operation + * is complete is a BAD IDEA. + * @param {*=} options Optionally, an object that may contain the + * following fields: + * - {number} bytes The number of |bytes| to write from the buffer. If + * unspecified, this is |buffer.byteLength|. Note that |bytes| is required + * if |buffer| is a C pointer. + * + * @return {number} The number of bytes actually written. + */ + write: function write(buffer, options = {}) { + // If |buffer| is a typed array and there is no |bytes| options, + // we need to extract the |byteLength| now, as it will be lost + // by communication. + // Options might be a nullish value, so better check for that before using + // the |in| operator. + if (isTypedArray(buffer) && !(options && "bytes" in options)) { + // Preserve reference to option |outExecutionDuration|, |outSerializationDuration|, if it is passed. + options = clone(options, [ + "outExecutionDuration", + "outSerializationDuration", + ]); + options.bytes = buffer.byteLength; + } + return Scheduler.post( + "File_prototype_write", + [this._fdmsg, Type.void_t.in_ptr.toMsg(buffer), options], + buffer /* Ensure that |buffer| is not gc-ed*/ + ); + }, + + /** + * Read bytes from this file to a new buffer. + * + * @param {number=} bytes If unspecified, read all the remaining bytes from + * this file. If specified, read |bytes| bytes, or less if the file does not + * contain that many bytes. + * @param {JSON} options + * @return {promise} + * @resolves {Uint8Array} An array containing the bytes read. + */ + read: function read(nbytes, options = {}) { + let promise = Scheduler.post("File_prototype_read", [ + this._fdmsg, + nbytes, + options, + ]); + return promise.then(function onSuccess(data) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + }); + }, + + /** + * Return the current position in the file, as bytes. + * + * @return {promise} + * @resolves {number} The current position in the file, + * as a number of bytes since the start of the file. + */ + getPosition: function getPosition() { + return Scheduler.post("File_prototype_getPosition", [this._fdmsg]); + }, + + /** + * Set the current position in the file, as bytes. + * + * @param {number} pos A number of bytes. + * @param {number} whence The reference position in the file, + * which may be either POS_START (from the start of the file), + * POS_END (from the end of the file) or POS_CUR (from the + * current position in the file). + * + * @return {promise} + */ + setPosition: function setPosition(pos, whence) { + return Scheduler.post("File_prototype_setPosition", [ + this._fdmsg, + pos, + whence, + ]); + }, + + /** + * Flushes the file's buffers and causes all buffered data + * to be written. + * Disk flushes are very expensive and therefore should be used carefully, + * sparingly and only in scenarios where it is vital that data survives + * system crashes. Even though the function will be executed off the + * main-thread, it might still affect the overall performance of any running + * application. + * + * @return {promise} + */ + flush: function flush() { + return Scheduler.post("File_prototype_flush", [this._fdmsg]); + }, + + /** + * Set the file's access permissions. This does nothing on Windows. + * + * This operation is likely to fail if applied to a file that was + * not created by the currently running program (more precisely, + * if it was created by a program running under a different OS-level + * user account). It may also fail, or silently do nothing, if the + * filesystem containing the file does not support access permissions. + * + * @param {*=} options Object specifying the requested permissions: + * + * - {number} unixMode The POSIX file mode to set on the file. If omitted, + * the POSIX file mode is reset to the default used by |OS.file.open|. If + * specified, the permissions will respect the process umask as if they + * had been specified as arguments of |OS.File.open|, unless the + * |unixHonorUmask| parameter tells otherwise. + * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is + * modified by the process umask, as |OS.File.open| would have done. If + * false, the exact value of |unixMode| will be applied. + */ + setPermissions: function setPermissions(options = {}) { + return Scheduler.post("File_prototype_setPermissions", [ + this._fdmsg, + options, + ]); + }, +}; + +if (SharedAll.Constants.Sys.Name != "Android") { + /** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * WARNING: This method is not implemented on Android/B2G. On Android/B2G, + * you should use File.setDates instead. + * + * @return {promise} + * @rejects {TypeError} + * @rejects {OS.File.Error} + */ + File.prototype.setDates = function(accessDate, modificationDate) { + return Scheduler.post( + "File_prototype_setDates", + [this._fdmsg, accessDate, modificationDate], + this + ); + }; +} + +/** + * Open a file asynchronously. + * + * @return {promise} + * @resolves {OS.File} + * @rejects {OS.Error} + */ +File.open = function open(path, mode, options) { + return Scheduler.post( + "open", + [Type.path.toMsg(path), mode, options], + path + ).then(function onSuccess(msg) { + return new File(msg); + }); +}; + +/** + * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name. + * + * @param {string} path The path to the file. + * @param {*=} options Additional options for file opening. This + * implementation interprets the following fields: + * + * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. + * If |false| use HEX numbers ie: filename-A65BC0.ext + * - {number} maxReadableNumber Used to limit the amount of tries after a failed + * file creation. Default is 20. + * + * @return {Object} contains A file object{file} and the path{path}. + * @throws {OS.File.Error} If the file could not be opened. + */ +File.openUnique = function openUnique(path, options) { + return Scheduler.post( + "openUnique", + [Type.path.toMsg(path), options], + path + ).then(function onSuccess(msg) { + return { + path: msg.path, + file: new File(msg.file), + }; + }); +}; + +/** + * Get the information on the file. + * + * @return {promise} + * @resolves {OS.File.Info} + * @rejects {OS.Error} + */ +File.stat = function stat(path, options) { + return Scheduler.post("stat", [Type.path.toMsg(path), options], path).then( + File.Info.fromMsg + ); +}; + +/** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * @return {promise} + * @rejects {TypeError} + * @rejects {OS.File.Error} + */ +File.setDates = function setDates(path, accessDate, modificationDate) { + return Scheduler.post( + "setDates", + [Type.path.toMsg(path), accessDate, modificationDate], + this + ); +}; + +/** + * Set the file's access permissions. This does nothing on Windows. + * + * This operation is likely to fail if applied to a file that was + * not created by the currently running program (more precisely, + * if it was created by a program running under a different OS-level + * user account). It may also fail, or silently do nothing, if the + * filesystem containing the file does not support access permissions. + * + * @param {string} path The path to the file. + * @param {*=} options Object specifying the requested permissions: + * + * - {number} unixMode The POSIX file mode to set on the file. If omitted, + * the POSIX file mode is reset to the default used by |OS.file.open|. If + * specified, the permissions will respect the process umask as if they + * had been specified as arguments of |OS.File.open|, unless the + * |unixHonorUmask| parameter tells otherwise. + * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is + * modified by the process umask, as |OS.File.open| would have done. If + * false, the exact value of |unixMode| will be applied. + */ +File.setPermissions = function setPermissions(path, options = {}) { + return Scheduler.post("setPermissions", [Type.path.toMsg(path), options]); +}; + +/** + * Fetch the current directory + * + * @return {promise} + * @resolves {string} The current directory, as a path usable with OS.Path + * @rejects {OS.Error} + */ +File.getCurrentDirectory = function getCurrentDirectory() { + return Scheduler.post("getCurrentDirectory").then(Type.path.fromMsg); +}; + +/** + * Copy a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be copied. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If true, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * + * @rejects {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be copied with the file. The + * behavior may not be the same across all platforms. + */ +File.copy = function copy(sourcePath, destPath, options) { + return Scheduler.post( + "copy", + [Type.path.toMsg(sourcePath), Type.path.toMsg(destPath), options], + [sourcePath, destPath] + ); +}; + +/** + * Move a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be moved. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If set, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * + * @returns {Promise} + * @rejects {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be moved with the file. The + * behavior may not be the same across all platforms. + */ +File.move = function move(sourcePath, destPath, options) { + return Scheduler.post( + "move", + [Type.path.toMsg(sourcePath), Type.path.toMsg(destPath), options], + [sourcePath, destPath] + ); +}; + +/** + * Create a symbolic link to a source. + * + * @param {string} sourcePath The platform-specific path to which + * the symbolic link should point. + * @param {string} destPath The platform-specific path at which the + * symbolic link should be created. + * + * @returns {Promise} + * @rejects {OS.File.Error} In case of any error. + */ +if (!SharedAll.Constants.Win) { + File.unixSymLink = function unixSymLink(sourcePath, destPath) { + return Scheduler.post( + "unixSymLink", + [Type.path.toMsg(sourcePath), Type.path.toMsg(destPath)], + [sourcePath, destPath] + ); + }; +} + +/** + * Remove an empty directory. + * + * @param {string} path The name of the directory to remove. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |true|, do not fail if the + * directory does not exist yet. + */ +File.removeEmptyDir = function removeEmptyDir(path, options) { + return Scheduler.post( + "removeEmptyDir", + [Type.path.toMsg(path), options], + path + ); +}; + +/** + * Remove an existing file. + * + * @param {string} path The name of the file. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the file does + * not exist. |true| by default. + * + * @throws {OS.File.Error} In case of I/O error. + */ +File.remove = function remove(path, options) { + return Scheduler.post("remove", [Type.path.toMsg(path), options], path); +}; + +/** + * Create a directory and, optionally, its parent directories. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. + * + * - {string} from If specified, the call to |makeDir| creates all the + * ancestors of |path| that are descendants of |from|. Note that |path| + * must be a descendant of |from|, and that |from| and its existing + * subdirectories present in |path| must be user-writeable. + * Example: + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); + * creates directories profileDir/foo, profileDir/foo/bar + * - {bool} ignoreExisting If |false|, throw an error if the directory + * already exists. |true| by default. Ignored if |from| is specified. + * - {number} unixMode Under Unix, if specified, a file creation mode, + * as per libc function |mkdir|. If unspecified, dirs are + * created with a default mode of 0700 (dir is private to + * the user, the user can read, write and execute). Ignored under Windows + * or if the file system does not support file creation modes. + * - {C pointer} winSecurity Under Windows, if specified, security + * attributes as per winapi function |CreateDirectory|. If + * unspecified, use the default security descriptor, inherited from + * the parent directory. Ignored under Unix or if the file system + * does not support security descriptors. + */ +File.makeDir = function makeDir(path, options) { + return Scheduler.post("makeDir", [Type.path.toMsg(path), options], path); +}; + +/** + * Return the contents of a file + * + * @param {string} path The path to the file. + * @param {number=} bytes Optionally, an upper bound to the number of bytes + * to read. DEPRECATED - please use options.bytes instead. + * @param {JSON} options Additional options. + * - {boolean} sequential A flag that triggers a population of the page cache + * with data from a file so that subsequent reads from that file would not + * block on disk I/O. If |true| or unspecified, inform the system that the + * contents of the file will be read in order. Otherwise, make no such + * assumption. |true| by default. + * - {number} bytes An upper bound to the number of bytes to read. + * - {string} compression If "lz4" and if the file is compressed using the lz4 + * compression algorithm, decompress the file contents on the fly. + * + * @resolves {Uint8Array} A buffer holding the bytes + * read from the file. + */ +File.read = function read(path, bytes, options = {}) { + if (typeof bytes == "object") { + // Passing |bytes| as an argument is deprecated. + // We should now be passing it as a field of |options|. + options = bytes || {}; + } else { + options = clone(options, [ + "outExecutionDuration", + "outSerializationDuration", + ]); + if (typeof bytes != "undefined") { + options.bytes = bytes; + } + } + + if (options.compression || !nativeWheneverAvailable) { + // We need to use the JS implementation. + let promise = Scheduler.post( + "read", + [Type.path.toMsg(path), bytes, options], + path + ); + return promise.then(function onSuccess(data) { + if (typeof data == "string") { + return data; + } + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + }); + } + + // Otherwise, use the native implementation. + return Scheduler.push(() => Native.read(path, options)); +}; + +/** + * Find outs if a file exists. + * + * @param {string} path The path to the file. + * + * @return {bool} true if the file exists, false otherwise. + */ +File.exists = function exists(path) { + return Scheduler.post("exists", [Type.path.toMsg(path)], path); +}; + +/** + * Write a file, atomically. + * + * By opposition to a regular |write|, this operation ensures that, + * until the contents are fully written, the destination file is + * not modified. + * + * Limitation: In a few extreme cases (hardware failure during the + * write, user unplugging disk during the write, etc.), data may be + * corrupted. If your data is user-critical (e.g. preferences, + * application data, etc.), you may wish to consider adding options + * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as + * detailed below. Note that no combination of options can be + * guaranteed to totally eliminate the risk of corruption. + * + * @param {string} path The path of the file to modify. + * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. + * @param {*=} options Optionally, an object determining the behavior + * of this function. This object may contain the following fields: + * - {number} bytes The number of bytes to write. If unspecified, + * |buffer.byteLength|. Required if |buffer| is a C pointer. + * - {string} tmpPath If |null| or unspecified, write all data directly + * to |path|. If specified, write all data to a temporary file called + * |tmpPath| and, once this write is complete, rename the file to + * replace |path|. Performing this additional operation is a little + * slower but also a little safer. + * - {bool} noOverwrite - If set, this function will fail if a file already + * exists at |path|. + * - {bool} flush - If |false| or unspecified, return immediately once the + * write is complete. If |true|, before writing, force the operating system + * to write its internal disk buffers to the disk. This is considerably slower + * (not just for the application but for the whole system) but also safer: + * if the system shuts down improperly (typically due to a kernel freeze + * or a power failure) or if the device is disconnected before the buffer + * is flushed, the file has more chances of not being corrupted. + * - {string} backupTo - If specified, backup the destination file as |backupTo|. + * Note that this function renames the destination file before overwriting it. + * If the process or the operating system freezes or crashes + * during the short window between these operations, + * the destination file will have been moved to its backup. + * + * @return {promise} + * @resolves {number} The number of bytes actually written. + */ +File.writeAtomic = function writeAtomic(path, buffer, options = {}) { + const useNativeImplementation = + nativeWheneverAvailable && + !options.compression && + !(isTypedArray(buffer) && "byteOffset" in buffer && buffer.byteOffset > 0); + // Copy |options| to avoid modifying the original object but preserve the + // reference to |outExecutionDuration|, |outSerializationDuration| option if it is passed. + options = clone(options, [ + "outExecutionDuration", + "outSerializationDuration", + ]); + // As options.tmpPath is a path, we need to encode it as |Type.path| message, but only + // if we are not using the native implementation. + if ("tmpPath" in options && !useNativeImplementation) { + options.tmpPath = Type.path.toMsg(options.tmpPath); + } + if (isTypedArray(buffer) && !("bytes" in options)) { + options.bytes = buffer.byteLength; + } + let refObj = {}; + let promise; + TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj); + if (useNativeImplementation) { + promise = Scheduler.push(() => Native.writeAtomic(path, buffer, options)); + } else { + promise = Scheduler.post( + "writeAtomic", + [Type.path.toMsg(path), Type.void_t.in_ptr.toMsg(buffer), options], + [options, buffer, path] + ); + } + TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj); + return promise; +}; + +File.removeDir = function(path, options = {}) { + return Scheduler.post("removeDir", [Type.path.toMsg(path), options], path); +}; + +/** + * Information on a file, as returned by OS.File.stat or + * OS.File.prototype.stat + * + * @constructor + */ +File.Info = function Info(value) { + // Note that we can't just do this[k] = value[k] because our + // prototype defines getters for all of these fields. + for (let k in value) { + Object.defineProperty(this, k, { value: value[k] }); + } +}; +File.Info.prototype = SysAll.AbstractInfo.prototype; + +File.Info.fromMsg = function fromMsg(value) { + return new File.Info(value); +}; + +/** + * Get worker's current DEBUG flag. + * Note: This is used for testing purposes. + */ +File.GET_DEBUG = function GET_DEBUG() { + return Scheduler.post("GET_DEBUG"); +}; + +/** + * Iterate asynchronously through a directory + * + * @constructor + */ +var DirectoryIterator = function DirectoryIterator(path, options) { + /** + * Open the iterator on the worker thread + * + * @type {Promise} + * @resolves {*} A message accepted by the methods of DirectoryIterator + * in the worker thread + */ + this._itmsg = Scheduler.post( + "new_DirectoryIterator", + [Type.path.toMsg(path), options], + path + ); + this._isClosed = false; +}; +DirectoryIterator.prototype = { + [Symbol.asyncIterator]() { + return this; + }, + + _itmsg: null, + + /** + * Determine whether the directory exists. + * + * @resolves {boolean} + */ + async exists() { + if (this._isClosed) { + return Promise.resolve(false); + } + let iterator = await this._itmsg; + return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]); + }, + /** + * Get the next entry in the directory. + * + * @return {Promise} + * @resolves By definition of the async iterator protocol, either + * `{value: {File.Entry}, done: false}` if there is an unvisited entry + * in the directory, or `{value: undefined, done: true}`, otherwise. + */ + async next() { + if (this._isClosed) { + return { value: undefined, done: true }; + } + return this._next(await this._itmsg); + }, + /** + * Get several entries at once. + * + * @param {number=} length If specified, the number of entries + * to return. If unspecified, return all remaining entries. + * @return {Promise} + * @resolves {Array} An array containing the |length| next entries. + */ + async nextBatch(size) { + if (this._isClosed) { + return []; + } + let iterator = await this._itmsg; + let array = await Scheduler.post("DirectoryIterator_prototype_nextBatch", [ + iterator, + size, + ]); + return array.map(DirectoryIterator.Entry.fromMsg); + }, + /** + * Apply a function to all elements of the directory sequentially. + * + * @param {Function} cb This function will be applied to all entries + * of the directory. It receives as arguments + * - the OS.File.Entry corresponding to the entry; + * - the index of the entry in the enumeration; + * - the iterator itself - return |iterator.close()| to stop the loop. + * + * If the callback returns a promise, iteration waits until the + * promise is resolved before proceeding. + * + * @return {Promise} A promise resolved once the loop has reached + * its end. + */ + async forEach(cb, options) { + if (this._isClosed) { + return undefined; + } + let position = 0; + let iterator = await this._itmsg; + while (true) { + if (this._isClosed) { + return undefined; + } + let { value, done } = await this._next(iterator); + if (done) { + return undefined; + } + await cb(value, position++, this); + } + }, + /** + * Auxiliary method: fetch the next item + * + * @resolves `{value: undefined, done: true}` If all entries have already + * been visited or the iterator has been closed. + */ + async _next(iterator) { + if (this._isClosed) { + return { value: undefined, done: true }; + } + let { + value, + done, + } = await Scheduler.post("DirectoryIterator_prototype_next", [iterator]); + if (done) { + this.close(); + return { value: undefined, done: true }; + } + return { value: DirectoryIterator.Entry.fromMsg(value), done: false }; + }, + /** + * Close the iterator + */ + async close() { + if (this._isClosed) { + return undefined; + } + this._isClosed = true; + let iterator = this._itmsg; + this._itmsg = null; + return Scheduler.post("DirectoryIterator_prototype_close", [iterator]); + }, +}; + +DirectoryIterator.Entry = function Entry(value) { + return value; +}; +DirectoryIterator.Entry.prototype = Object.create( + SysAll.AbstractEntry.prototype +); + +DirectoryIterator.Entry.fromMsg = function fromMsg(value) { + return new DirectoryIterator.Entry(value); +}; + +File.resetWorker = function() { + return (async function() { + let resources = await Scheduler.kill({ shutdown: false, reset: true }); + if (resources && !resources.killed) { + throw new Error( + "Could not reset worker, this would leak file descriptors: " + + JSON.stringify(resources) + ); + } + })(); +}; + +// Constants +File.POS_START = SysAll.POS_START; +File.POS_CURRENT = SysAll.POS_CURRENT; +File.POS_END = SysAll.POS_END; + +// Exports +File.Error = OSError; +File.DirectoryIterator = DirectoryIterator; + +var OS = {}; +OS.File = File; +OS.Constants = SharedAll.Constants; +OS.Shared = { + LOG: SharedAll.LOG, + Type: SysAll.Type, + get DEBUG() { + return SharedAll.Config.DEBUG; + }, + set DEBUG(x) { + SharedAll.Config.DEBUG = x; + }, +}; +Object.freeze(OS.Shared); +OS.Path = Path; + +// Returns a resolved promise when all the queued operation have been completed. +Object.defineProperty(OS.File, "queue", { + get() { + return Scheduler.queue; + }, +}); + +// `true` if this is a content process, `false` otherwise. +// It would be nicer to go through `Services.appinfo`, but some tests need to be +// able to replace that field with a custom implementation before it is first +// called. +const isContent = + // eslint-disable-next-line mozilla/use-services + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == + Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; + +/** + * Shutdown barriers, to let clients register to be informed during shutdown. + */ +var Barriers = { + shutdown: new AsyncShutdown.Barrier( + "OS.File: Waiting for clients before full shutdown" + ), + /** + * Return the shutdown state of OS.File + */ + getDetails() { + let result = { + launched: Scheduler.launched, + shutdown: Scheduler.shutdown, + worker: !!Scheduler._worker, + pendingReset: !!Scheduler.resetTimer, + latestSent: Scheduler.Debugging.latestSent, + latestReceived: Scheduler.Debugging.latestReceived, + messagesSent: Scheduler.Debugging.messagesSent, + messagesReceived: Scheduler.Debugging.messagesReceived, + messagesQueued: Scheduler.Debugging.messagesQueued, + DEBUG: SharedAll.Config.DEBUG, + }; + // Convert dates to strings for better readability + for (let key of ["latestSent", "latestReceived"]) { + if (result[key] && typeof result[key][0] == "number") { + result[key][0] = Date(result[key][0]); + } + } + return result; + }, +}; + +function setupShutdown(phaseName) { + Barriers[phaseName] = new AsyncShutdown.Barrier( + `OS.File: Waiting for clients before ${phaseName}` + ); + File[phaseName] = Barriers[phaseName].client; + + // Auto-flush OS.File during `phaseName`. This ensures that any I/O + // that has been queued *before* `phaseName` is properly completed. + // To ensure that I/O queued *during* `phaseName` change is completed, + // clients should register using AsyncShutdown.addBlocker. + AsyncShutdown[phaseName].addBlocker( + `OS.File: flush I/O queued before ${phaseName}`, + async function() { + // Give clients a last chance to enqueue requests. + await Barriers[phaseName].wait({ crashAfterMS: null }); + + // Wait until all currently enqueued requests are completed. + await Scheduler.queue; + }, + () => { + let details = Barriers.getDetails(); + details.clients = Barriers[phaseName].state; + return details; + } + ); +} + +// profile-before-change only exists in the parent, and OS.File should +// not be used in the child process anyways. +if (!isContent) { + setupShutdown("profileBeforeChange"); +} +File.shutdown = Barriers.shutdown.client; diff --git a/toolkit/components/osfile/modules/osfile_async_worker.js b/toolkit/components/osfile/modules/osfile_async_worker.js new file mode 100644 index 000000000000..8cf6fab81fb8 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_async_worker.js @@ -0,0 +1,450 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env worker */ + +if (this.Components) { + throw new Error("This worker can only be loaded from a worker thread"); +} + +// Worker thread for osfile asynchronous front-end + +(function(exports) { + "use strict"; + + // Timestamps, for use in Telemetry. + // The object is set to |null| once it has been sent + // to the main thread. + let timeStamps = { + entered: Date.now(), + loaded: null, + }; + + // NOTE: osfile.jsm imports require.js + /* import-globals-from /toolkit/components/workerloader/require.js */ + /* import-globals-from /toolkit/components/osfile/osfile.jsm */ + importScripts("resource://gre/modules/osfile.jsm"); + + let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); + let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + let LOG = SharedAll.LOG.bind(SharedAll, "Agent"); + + let worker = new PromiseWorker.AbstractWorker(); + worker.dispatch = function(method, args = []) { + let startTime = performance.now(); + try { + return Agent[method](...args); + } finally { + let text = method; + if (args.length && args[0] instanceof Object && args[0].string) { + // Including the path in the marker text here means it will be part of + // profiles. It's fine to include personally identifiable information + // in profiles, because when a profile is captured only the user will + // see it, and before uploading it a sanitization step will be offered. + // The 'OS.File' name will help the profiler know that these markers + // should be sanitized. + text += " — " + args[0].string; + } + ChromeUtils.addProfilerMarker("OS.File", startTime, text); + } + }; + worker.log = LOG; + worker.postMessage = function(message, ...transfers) { + if (timeStamps) { + message.timeStamps = timeStamps; + timeStamps = null; + } + self.postMessage(message, ...transfers); + }; + worker.close = function() { + self.close(); + }; + let Meta = PromiseWorker.Meta; + + self.addEventListener("message", msg => worker.handleMessage(msg)); + self.addEventListener("unhandledrejection", function(error) { + throw error.reason; + }); + + /** + * A data structure used to track opened resources + */ + let ResourceTracker = function ResourceTracker() { + // A number used to generate ids + this._idgen = 0; + // A map from id to resource + this._map = new Map(); + }; + ResourceTracker.prototype = { + /** + * Get a resource from its unique identifier. + */ + get(id) { + let result = this._map.get(id); + if (result == null) { + return result; + } + return result.resource; + }, + /** + * Remove a resource from its unique identifier. + */ + remove(id) { + if (!this._map.has(id)) { + throw new Error("Cannot find resource id " + id); + } + this._map.delete(id); + }, + /** + * Add a resource, return a new unique identifier + * + * @param {*} resource A resource. + * @param {*=} info Optional information. For debugging purposes. + * + * @return {*} A unique identifier. For the moment, this is a number, + * but this might not remain the case forever. + */ + add(resource, info) { + let id = this._idgen++; + this._map.set(id, { resource, info }); + return id; + }, + /** + * Return a list of all open resources i.e. the ones still present in + * ResourceTracker's _map. + */ + listOpenedResources: function listOpenedResources() { + return Array.from(this._map, ([id, resource]) => resource.info.path); + }, + }; + + /** + * A map of unique identifiers to opened files. + */ + let OpenedFiles = new ResourceTracker(); + + /** + * Execute a function in the context of a given file. + * + * @param {*} id A unique identifier, as used by |OpenFiles|. + * @param {Function} f A function to call. + * @param {boolean} ignoreAbsent If |true|, the error is ignored. Otherwise, the error causes an exception. + * @return The return value of |f()| + * + * This function attempts to get the file matching |id|. If + * the file exists, it executes |f| within the |this| set + * to the corresponding file. Otherwise, it throws an error. + */ + let withFile = function withFile(id, f, ignoreAbsent) { + let file = OpenedFiles.get(id); + if (file == null) { + if (!ignoreAbsent) { + throw OS.File.Error.closed("accessing file"); + } + return undefined; + } + return f.call(file); + }; + + let OpenedDirectoryIterators = new ResourceTracker(); + let withDir = function withDir(fd, f, ignoreAbsent) { + let file = OpenedDirectoryIterators.get(fd); + if (file == null) { + if (!ignoreAbsent) { + throw OS.File.Error.closed("accessing directory"); + } + return undefined; + } + if (!(file instanceof File.DirectoryIterator)) { + throw new Error( + "file is not a directory iterator " + + Object.getPrototypeOf(file).toSource() + ); + } + return f.call(file); + }; + + let Type = exports.OS.Shared.Type; + + let File = exports.OS.File; + + /** + * The agent. + * + * It is in charge of performing method-specific deserialization + * of messages, calling the function/method of OS.File and serializing + * back the results. + */ + let Agent = { + // Update worker's OS.Shared.DEBUG flag message from controller. + SET_DEBUG(aDEBUG) { + SharedAll.Config.DEBUG = aDEBUG; + }, + // Return worker's current OS.Shared.DEBUG value to controller. + // Note: This is used for testing purposes. + GET_DEBUG() { + return SharedAll.Config.DEBUG; + }, + /** + * Execute shutdown sequence, returning data on leaked file descriptors. + * + * @param {bool} If |true|, kill the worker if this would not cause + * leaks. + */ + Meta_shutdown(kill) { + let result = { + openedFiles: OpenedFiles.listOpenedResources(), + openedDirectoryIterators: OpenedDirectoryIterators.listOpenedResources(), + killed: false, // Placeholder + }; + + // Is it safe to kill the worker? + let safe = + !result.openedFiles.length && !result.openedDirectoryIterators.length; + result.killed = safe && kill; + + return new Meta(result, { shutdown: result.killed }); + }, + // Functions of OS.File + stat: function stat(path, options) { + return exports.OS.File.Info.toMsg( + exports.OS.File.stat(Type.path.fromMsg(path), options) + ); + }, + setPermissions: function setPermissions(path, options = {}) { + return exports.OS.File.setPermissions(Type.path.fromMsg(path), options); + }, + setDates: function setDates(path, accessDate, modificationDate) { + return exports.OS.File.setDates( + Type.path.fromMsg(path), + accessDate, + modificationDate + ); + }, + getCurrentDirectory: function getCurrentDirectory() { + return exports.OS.Shared.Type.path.toMsg(File.getCurrentDirectory()); + }, + copy: function copy(sourcePath, destPath, options) { + return File.copy( + Type.path.fromMsg(sourcePath), + Type.path.fromMsg(destPath), + options + ); + }, + move: function move(sourcePath, destPath, options) { + return File.move( + Type.path.fromMsg(sourcePath), + Type.path.fromMsg(destPath), + options + ); + }, + makeDir: function makeDir(path, options) { + return File.makeDir(Type.path.fromMsg(path), options); + }, + removeEmptyDir: function removeEmptyDir(path, options) { + return File.removeEmptyDir(Type.path.fromMsg(path), options); + }, + remove: function remove(path, options) { + return File.remove(Type.path.fromMsg(path), options); + }, + open: function open(path, mode, options) { + let filePath = Type.path.fromMsg(path); + let file = File.open(filePath, mode, options); + return OpenedFiles.add(file, { + // Adding path information to keep track of opened files + // to report leaks when debugging. + path: filePath, + }); + }, + openUnique: function openUnique(path, options) { + let filePath = Type.path.fromMsg(path); + let openedFile = OS.Shared.AbstractFile.openUnique(filePath, options); + let resourceId = OpenedFiles.add(openedFile.file, { + // Adding path information to keep track of opened files + // to report leaks when debugging. + path: openedFile.path, + }); + + return { + path: openedFile.path, + file: resourceId, + }; + }, + read: function read(path, bytes, options) { + let data = File.read(Type.path.fromMsg(path), bytes, options); + if (typeof data == "string") { + return data; + } + return new Meta( + { + buffer: data.buffer, + byteOffset: data.byteOffset, + byteLength: data.byteLength, + }, + { + transfers: [data.buffer], + } + ); + }, + exists: function exists(path) { + return File.exists(Type.path.fromMsg(path)); + }, + writeAtomic: function writeAtomic(path, buffer, options) { + if (options.tmpPath) { + options.tmpPath = Type.path.fromMsg(options.tmpPath); + } + return File.writeAtomic( + Type.path.fromMsg(path), + Type.voidptr_t.fromMsg(buffer), + options + ); + }, + removeDir(path, options) { + return File.removeDir(Type.path.fromMsg(path), options); + }, + new_DirectoryIterator: function new_DirectoryIterator(path, options) { + let directoryPath = Type.path.fromMsg(path); + let iterator = new File.DirectoryIterator(directoryPath, options); + return OpenedDirectoryIterators.add(iterator, { + // Adding path information to keep track of opened directory + // iterators to report leaks when debugging. + path: directoryPath, + }); + }, + // Methods of OS.File + File_prototype_close: function close(fd) { + return withFile(fd, function do_close() { + try { + return this.close(); + } finally { + OpenedFiles.remove(fd); + } + }); + }, + File_prototype_stat: function stat(fd) { + return withFile(fd, function do_stat() { + return exports.OS.File.Info.toMsg(this.stat()); + }); + }, + File_prototype_setPermissions: function setPermissions(fd, options = {}) { + return withFile(fd, function do_setPermissions() { + return this.setPermissions(options); + }); + }, + File_prototype_setDates: function setDates( + fd, + accessTime, + modificationTime + ) { + return withFile(fd, function do_setDates() { + return this.setDates(accessTime, modificationTime); + }); + }, + File_prototype_read: function read(fd, nbytes, options) { + return withFile(fd, function do_read() { + let data = this.read(nbytes, options); + return new Meta( + { + buffer: data.buffer, + byteOffset: data.byteOffset, + byteLength: data.byteLength, + }, + { + transfers: [data.buffer], + } + ); + }); + }, + File_prototype_readTo: function readTo(fd, buffer, options) { + return withFile(fd, function do_readTo() { + return this.readTo( + exports.OS.Shared.Type.voidptr_t.fromMsg(buffer), + options + ); + }); + }, + File_prototype_write: function write(fd, buffer, options) { + return withFile(fd, function do_write() { + return this.write( + exports.OS.Shared.Type.voidptr_t.fromMsg(buffer), + options + ); + }); + }, + File_prototype_setPosition: function setPosition(fd, pos, whence) { + return withFile(fd, function do_setPosition() { + return this.setPosition(pos, whence); + }); + }, + File_prototype_getPosition: function getPosition(fd) { + return withFile(fd, function do_getPosition() { + return this.getPosition(); + }); + }, + File_prototype_flush: function flush(fd) { + return withFile(fd, function do_flush() { + return this.flush(); + }); + }, + // Methods of OS.File.DirectoryIterator + DirectoryIterator_prototype_next: function next(dir) { + return withDir( + dir, + function do_next() { + let { value, done } = this.next(); + if (done) { + OpenedDirectoryIterators.remove(dir); + return { value: undefined, done: true }; + } + return { + value: File.DirectoryIterator.Entry.toMsg(value), + done: false, + }; + }, + false + ); + }, + DirectoryIterator_prototype_nextBatch: function nextBatch(dir, size) { + return withDir( + dir, + function do_nextBatch() { + let result; + try { + result = this.nextBatch(size); + } catch (x) { + OpenedDirectoryIterators.remove(dir); + throw x; + } + return result.map(File.DirectoryIterator.Entry.toMsg); + }, + false + ); + }, + DirectoryIterator_prototype_close: function close(dir) { + return withDir( + dir, + function do_close() { + this.close(); + OpenedDirectoryIterators.remove(dir); + }, + true + ); // ignore error to support double-closing |DirectoryIterator| + }, + DirectoryIterator_prototype_exists: function exists(dir) { + return withDir(dir, function do_exists() { + return this.exists(); + }); + }, + }; + if (!SharedAll.Constants.Win) { + Agent.unixSymLink = function unixSymLink(sourcePath, destPath) { + return File.unixSymLink( + Type.path.fromMsg(sourcePath), + Type.path.fromMsg(destPath) + ); + }; + } + + timeStamps.loaded = Date.now(); +})(this); diff --git a/toolkit/components/osfile/modules/osfile_native.jsm b/toolkit/components/osfile/modules/osfile_native.jsm new file mode 100644 index 000000000000..4002e106f59c --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_native.jsm @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Native (xpcom) implementation of key OS.File functions + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["read", "writeAtomic"]; + +var { Constants } = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" +); + +var SysAll; +if (Constants.Win) { + SysAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_win_allthreads.jsm" + ); +} else if (Constants.libc) { + SysAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_unix_allthreads.jsm" + ); +} else { + throw new Error("I am neither under Windows nor under a Posix system"); +} +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +/** + * The native service holding the implementation of the functions. + */ +XPCOMUtils.defineLazyServiceGetter( + lazy, + "Internals", + "@mozilla.org/toolkit/osfile/native-internals;1", + "nsINativeOSFileInternalsService" +); + +/** + * Native implementation of OS.File.read + * + * This implementation does not handle option |compression|. + */ +var read = function(path, options = {}) { + // Sanity check on types of options + if ("encoding" in options && typeof options.encoding != "string") { + return Promise.reject(new TypeError("Invalid type for option encoding")); + } + if ("compression" in options && typeof options.compression != "string") { + return Promise.reject(new TypeError("Invalid type for option compression")); + } + if ("bytes" in options && typeof options.bytes != "number") { + return Promise.reject(new TypeError("Invalid type for option bytes")); + } + + return new Promise((resolve, reject) => { + lazy.Internals.read( + path, + options, + function onSuccess(success) { + success.QueryInterface(Ci.nsINativeOSFileResult); + if ("outExecutionDuration" in options) { + options.outExecutionDuration = + success.executionDurationMS + (options.outExecutionDuration || 0); + } + resolve(success.result); + }, + function onError(operation, oserror) { + reject(new SysAll.Error(operation, oserror, path)); + } + ); + }); +}; + +/** + * Native implementation of OS.File.writeAtomic. + * This should not be called when |buffer| is a view with some non-zero byte offset. + * Does not handle option |compression|. + */ +var writeAtomic = function(path, buffer, options = {}) { + // Sanity check on types of options - we check only the encoding, since + // the others are checked inside Internals.writeAtomic. + if ("encoding" in options && typeof options.encoding !== "string") { + return Promise.reject(new TypeError("Invalid type for option encoding")); + } + + if (typeof buffer == "string") { + // Normalize buffer to a C buffer by encoding it + buffer = new TextEncoder().encode(buffer); + } + + if (ArrayBuffer.isView(buffer)) { + // We need to throw an error if it's a buffer with some byte offset. + if ("byteOffset" in buffer && buffer.byteOffset > 0) { + return Promise.reject( + new Error("Invalid non-zero value of Typed Array byte offset") + ); + } + buffer = buffer.buffer; + } + + return new Promise((resolve, reject) => { + lazy.Internals.writeAtomic( + path, + buffer, + options, + function onSuccess(success) { + success.QueryInterface(Ci.nsINativeOSFileResult); + if ("outExecutionDuration" in options) { + options.outExecutionDuration = + success.executionDurationMS + (options.outExecutionDuration || 0); + } + resolve(success.result); + }, + function onError(operation, oserror) { + reject(new SysAll.Error(operation, oserror, path)); + } + ); + }); +}; diff --git a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm new file mode 100644 index 000000000000..dce6f3613e94 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm @@ -0,0 +1,1371 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * OS.File utilities used by all threads. + * + * This module defines: + * - logging; + * - the base constants; + * - base types and primitives for declaring new types; + * - primitives for importing C functions; + * - primitives for dealing with integers, pointers, typed arrays; + * - the base class OSError; + * - a few additional utilities. + */ + +/* eslint-env worker */ + +// Boilerplate used to be able to import this module both from the main +// thread and from worker threads. + +/** + * A constructor for messages that require transfers instead of copies. + * + * See BasePromiseWorker.Meta. + * + * @constructor + */ +var Meta; +if (typeof Components != "undefined") { + // Global definition of |exports|, to keep everybody happy. + // In non-main thread, |exports| is provided by the module + // loader. + // eslint-disable-next-line mozilla/reject-global-this + this.exports = {}; + Meta = ChromeUtils.import("resource://gre/modules/PromiseWorker.jsm") + .BasePromiseWorker.Meta; +} else { + /* import-globals-from /toolkit/components/workerloader/require.js */ + importScripts("resource://gre/modules/workers/require.js"); + Meta = require("resource://gre/modules/workers/PromiseWorker.js").Meta; +} + +var EXPORTED_SYMBOLS = [ + "LOG", + "clone", + "Config", + "Constants", + "Type", + "HollowStructure", + "OSError", + "Library", + "declareFFI", + "declareLazy", + "declareLazyFFI", + "normalizeBufferArgs", + "projectValue", + "isArrayBuffer", + "isTypedArray", + "defineLazyGetter", + "OS", // Warning: this exported symbol will disappear +]; + +// //////////////////// Configuration of OS.File + +var Config = { + /** + * If |true|, calls to |LOG| are shown. Otherwise, they are hidden. + * + * This configuration option is controlled by preference "toolkit.osfile.log". + */ + DEBUG: false, + + /** + * TEST + */ + TEST: false, +}; +exports.Config = Config; + +// //////////////////// OS Constants + +if (typeof Components != "undefined") { + // On the main thread, OS.Constants is defined by a xpcom + // component. On other threads, it is available automatically + /* global OS */ + var { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + Cc["@mozilla.org/net/osfileconstantsservice;1"] + .getService(Ci.nsIOSFileConstantsService) + .init(); +} else { + ctypes = self.ctypes; +} + +exports.Constants = OS.Constants; + +// /////////////////// Utilities + +// Define a lazy getter for a property +var defineLazyGetter = function defineLazyGetter(object, name, getter) { + Object.defineProperty(object, name, { + configurable: true, + get: function lazy() { + delete this[name]; + let value = getter.call(this); + Object.defineProperty(object, name, { + value, + }); + return value; + }, + }); +}; +exports.defineLazyGetter = defineLazyGetter; + +// /////////////////// Logging + +/** + * The default implementation of the logger. + * + * The choice of logger can be overridden with Config.TEST. + */ +var gLogger; +// eslint-disable-next-line no-undef +if (typeof window != "undefined" && window.console && console.log) { + gLogger = console.log.bind(console, "OS"); +} else { + gLogger = function(...args) { + dump("OS " + args.join(" ") + "\n"); + }; +} + +/** + * Attempt to stringify an argument into something useful for + * debugging purposes, by using |.toString()| or |JSON.stringify| + * if available. + * + * @param {*} arg An argument to be stringified if possible. + * @return {string} A stringified version of |arg|. + */ +var stringifyArg = function stringifyArg(arg) { + if (typeof arg === "string") { + return arg; + } + if (arg && typeof arg === "object") { + let argToString = "" + arg; + + /** + * The only way to detect whether this object has a non-default + * implementation of |toString| is to check whether it returns + * '[object Object]'. Unfortunately, we cannot simply compare |arg.toString| + * and |Object.prototype.toString| as |arg| typically comes from another + * compartment. + */ + if (argToString === "[object Object]") { + return JSON.stringify(arg, function(key, value) { + if (isTypedArray(value)) { + return ( + "[" + + value.constructor.name + + " " + + value.byteOffset + + " " + + value.byteLength + + "]" + ); + } + if (isArrayBuffer(arg)) { + return "[" + value.constructor.name + " " + value.byteLength + "]"; + } + return value; + }); + } + return argToString; + } + return arg; +}; + +var LOG = function(...args) { + if (!Config.DEBUG) { + // If logging is deactivated, don't log + return; + } + + let logFunc = gLogger; + if (Config.TEST && typeof Components != "undefined") { + // If _TESTING_LOGGING is set, and if we are on the main thread, + // redirect logs to Services.console, for testing purposes + logFunc = function logFunc(...args) { + let message = ["TEST", "OS"].concat(args).join(" "); + Services.console.logStringMessage(message + "\n"); + }; + } + logFunc.apply(null, args.map(stringifyArg)); +}; + +exports.LOG = LOG; + +/** + * Return a shallow clone of the enumerable properties of an object. + * + * Utility used whenever normalizing options requires making (shallow) + * changes to an option object. The copy ensures that we do not modify + * a client-provided object by accident. + * + * Note: to reference and not copy specific fields, provide an optional + * |refs| argument containing their names. + * + * @param {JSON} object Options to be cloned. + * @param {Array} refs An optional array of field names to be passed by + * reference instead of copying. + */ +var clone = function(object, refs = []) { + let result = {}; + // Make a reference between result[key] and object[key]. + let refer = function refer(result, key, object) { + Object.defineProperty(result, key, { + enumerable: true, + get() { + return object[key]; + }, + set(value) { + object[key] = value; + }, + }); + }; + for (let k in object) { + if (!refs.includes(k)) { + result[k] = object[k]; + } else { + refer(result, k, object); + } + } + return result; +}; + +exports.clone = clone; + +// /////////////////// Abstractions above js-ctypes + +/** + * Abstraction above js-ctypes types. + * + * Use values of this type to register FFI functions. In addition to the + * usual features of js-ctypes, values of this type perform the necessary + * transformations to ensure that C errors are handled nicely, to connect + * resources with their finalizer, etc. + * + * @param {string} name The name of the type. Must be unique. + * @param {CType} implementation The js-ctypes implementation of the type. + * + * @constructor + */ +function Type(name, implementation) { + if (!(typeof name == "string")) { + throw new TypeError("Type expects as first argument a name, got: " + name); + } + if (!(implementation instanceof ctypes.CType)) { + throw new TypeError( + "Type expects as second argument a ctypes.CType" + + ", got: " + + implementation + ); + } + Object.defineProperty(this, "name", { value: name }); + Object.defineProperty(this, "implementation", { value: implementation }); +} +Type.prototype = { + /** + * Serialize a value of |this| |Type| into a format that can + * be transmitted as a message (not necessarily a string). + * + * In the default implementation, the method returns the + * value unchanged. + */ + toMsg: function default_toMsg(value) { + return value; + }, + /** + * Deserialize a message to a value of |this| |Type|. + * + * In the default implementation, the method returns the + * message unchanged. + */ + fromMsg: function default_fromMsg(msg) { + return msg; + }, + /** + * Import a value from C. + * + * In this default implementation, return the value + * unchanged. + */ + importFromC: function default_importFromC(value) { + return value; + }, + + /** + * A pointer/array used to pass data to the foreign function. + */ + get in_ptr() { + delete this.in_ptr; + let ptr_t = new PtrType( + "[in] " + this.name + "*", + this.implementation.ptr, + this + ); + Object.defineProperty(this, "in_ptr", { + get() { + return ptr_t; + }, + }); + return ptr_t; + }, + + /** + * A pointer/array used to receive data from the foreign function. + */ + get out_ptr() { + delete this.out_ptr; + let ptr_t = new PtrType( + "[out] " + this.name + "*", + this.implementation.ptr, + this + ); + Object.defineProperty(this, "out_ptr", { + get() { + return ptr_t; + }, + }); + return ptr_t; + }, + + /** + * A pointer/array used to both pass data to the foreign function + * and receive data from the foreign function. + * + * Whenever possible, prefer using |in_ptr| or |out_ptr|, which + * are generally faster. + */ + get inout_ptr() { + delete this.inout_ptr; + let ptr_t = new PtrType( + "[inout] " + this.name + "*", + this.implementation.ptr, + this + ); + Object.defineProperty(this, "inout_ptr", { + get() { + return ptr_t; + }, + }); + return ptr_t; + }, + + /** + * Attach a finalizer to a type. + */ + releaseWith: function releaseWith(finalizer) { + let parent = this; + let type = this.withName("[auto " + this.name + ", " + finalizer + "] "); + type.importFromC = function importFromC(value, operation) { + return ctypes.CDataFinalizer( + parent.importFromC(value, operation), + finalizer + ); + }; + return type; + }, + + /** + * Lazy variant of releaseWith. + * Attach a finalizer lazily to a type. + * + * @param {function} getFinalizer The function that + * returns finalizer lazily. + */ + releaseWithLazy: function releaseWithLazy(getFinalizer) { + let parent = this; + let type = this.withName("[auto " + this.name + ", (lazy)] "); + type.importFromC = function importFromC(value, operation) { + return ctypes.CDataFinalizer( + parent.importFromC(value, operation), + getFinalizer() + ); + }; + return type; + }, + + /** + * Return an alias to a type with a different name. + */ + withName: function withName(name) { + return Object.create(this, { name: { value: name } }); + }, + + /** + * Cast a C value to |this| type. + * + * Throw an error if the value cannot be casted. + */ + cast: function cast(value) { + return ctypes.cast(value, this.implementation); + }, + + /** + * Return the number of bytes in a value of |this| type. + * + * This may not be defined, e.g. for |void_t|, array types + * without length, etc. + */ + get size() { + return this.implementation.size; + }, +}; + +/** + * Utility function used to determine whether an object is a typed array + */ +var isTypedArray = function isTypedArray(obj) { + return obj != null && typeof obj == "object" && "byteOffset" in obj; +}; +exports.isTypedArray = isTypedArray; + +/** + * Utility function used to determine whether an object is an ArrayBuffer. + */ +var isArrayBuffer = function(obj) { + return ( + obj != null && + typeof obj == "object" && + obj.constructor.name == "ArrayBuffer" + ); +}; +exports.isArrayBuffer = isArrayBuffer; + +/** + * A |Type| of pointers. + * + * @param {string} name The name of this type. + * @param {CType} implementation The type of this pointer. + * @param {Type} targetType The target type. + */ +function PtrType(name, implementation, targetType) { + Type.call(this, name, implementation); + if (targetType == null || !(targetType instanceof Type)) { + throw new TypeError("targetType must be an instance of Type"); + } + /** + * The type of values targeted by this pointer type. + */ + Object.defineProperty(this, "targetType", { + value: targetType, + }); +} +PtrType.prototype = Object.create(Type.prototype); + +/** + * Convert a value to a pointer. + * + * Protocol: + * - |null| returns |null| + * - a string returns |{string: value}| + * - a typed array returns |{ptr: address_of_buffer}| + * - a C array returns |{ptr: address_of_buffer}| + * everything else raises an error + */ +PtrType.prototype.toMsg = function ptr_toMsg(value) { + if (value == null) { + return null; + } + if (typeof value == "string") { + return { string: value }; + } + if (isTypedArray(value)) { + // Automatically transfer typed arrays + return new Meta({ data: value }, { transfers: [value.buffer] }); + } + if (isArrayBuffer(value)) { + // Automatically transfer array buffers + return new Meta({ data: value }, { transfers: [value] }); + } + let normalized; + if ("addressOfElement" in value) { + // C array + normalized = value.addressOfElement(0); + } else if ("isNull" in value) { + // C pointer + normalized = value; + } else { + throw new TypeError("Value " + value + " cannot be converted to a pointer"); + } + let cast = Type.uintptr_t.cast(normalized); + return { ptr: cast.value.toString() }; +}; + +/** + * Convert a message back to a pointer. + */ +PtrType.prototype.fromMsg = function ptr_fromMsg(msg) { + if (msg == null) { + return null; + } + if ("string" in msg) { + return msg.string; + } + if ("data" in msg) { + return msg.data; + } + if ("ptr" in msg) { + let address = ctypes.uintptr_t(msg.ptr); + return this.cast(address); + } + throw new TypeError( + "Message " + msg.toSource() + " does not represent a pointer" + ); +}; + +exports.Type = Type; + +/* + * Some values are large integers on 64 bit platforms. Unfortunately, + * in practice, 64 bit integers cannot be manipulated in JS. We + * therefore project them to regular numbers whenever possible. + */ + +var projectLargeInt = function projectLargeInt(x) { + let str = x.toString(); + let rv = parseInt(str, 10); + if (rv.toString() !== str) { + throw new TypeError("Number " + str + " cannot be projected to a double"); + } + return rv; +}; +var projectLargeUInt = function projectLargeUInt(x) { + return projectLargeInt(x); +}; +var projectValue = function projectValue(x) { + if (!(x instanceof ctypes.CData)) { + return x; + } + if (!("value" in x)) { + // Sanity check + throw new TypeError("Number " + x.toSource() + " has no field |value|"); + } + return x.value; +}; + +function projector(type, signed) { + LOG( + "Determining best projection for", + type, + "(size: ", + type.size, + ")", + signed ? "signed" : "unsigned" + ); + if (type instanceof Type) { + type = type.implementation; + } + if (!type.size) { + throw new TypeError("Argument is not a proper C type"); + } + // Determine if type is projected to Int64/Uint64 + if ( + type.size == 8 || // Usual case + // The following cases have special treatment in js-ctypes + // Regardless of their size, the value getter returns + // a Int64/Uint64 + type == ctypes.size_t || // Special cases + type == ctypes.ssize_t || + type == ctypes.intptr_t || + type == ctypes.uintptr_t || + type == ctypes.off_t + ) { + if (signed) { + LOG("Projected as a large signed integer"); + return projectLargeInt; + } + LOG("Projected as a large unsigned integer"); + return projectLargeUInt; + } + LOG("Projected as a regular number"); + return projectValue; +} +exports.projectValue = projectValue; + +/** + * Get the appropriate type for an unsigned int of the given size. + * + * This function is useful to define types such as |mode_t| whose + * actual width depends on the OS/platform. + * + * @param {number} size The number of bytes requested. + */ +Type.uintn_t = function uintn_t(size) { + switch (size) { + case 1: + return Type.uint8_t; + case 2: + return Type.uint16_t; + case 4: + return Type.uint32_t; + case 8: + return Type.uint64_t; + default: + throw new Error( + "Cannot represent unsigned integers of " + size + " bytes" + ); + } +}; + +/** + * Get the appropriate type for an signed int of the given size. + * + * This function is useful to define types such as |mode_t| whose + * actual width depends on the OS/platform. + * + * @param {number} size The number of bytes requested. + */ +Type.intn_t = function intn_t(size) { + switch (size) { + case 1: + return Type.int8_t; + case 2: + return Type.int16_t; + case 4: + return Type.int32_t; + case 8: + return Type.int64_t; + default: + throw new Error("Cannot represent integers of " + size + " bytes"); + } +}; + +/** + * Actual implementation of common C types. + */ + +/** + * The void value. + */ +Type.void_t = new Type("void", ctypes.void_t); + +/** + * Shortcut for |void*|. + */ +Type.voidptr_t = new PtrType("void*", ctypes.voidptr_t, Type.void_t); + +// void* is a special case as we can cast any pointer to/from it +// so we have to shortcut |in_ptr|/|out_ptr|/|inout_ptr| and +// ensure that js-ctypes' casting mechanism is invoked directly +["in_ptr", "out_ptr", "inout_ptr"].forEach(function(key) { + Object.defineProperty(Type.void_t, key, { + value: Type.voidptr_t, + }); +}); + +/** + * A Type of integers. + * + * @param {string} name The name of this type. + * @param {CType} implementation The underlying js-ctypes implementation. + * @param {bool} signed |true| if this is a type of signed integers, + * |false| otherwise. + * + * @constructor + */ +function IntType(name, implementation, signed) { + Type.call(this, name, implementation); + this.importFromC = projector(implementation, signed); + this.project = this.importFromC; +} +IntType.prototype = Object.create(Type.prototype); +IntType.prototype.toMsg = function toMsg(value) { + if (typeof value == "number") { + return value; + } + return this.project(value); +}; + +/** + * A C char (one byte) + */ +Type.char = new Type("char", ctypes.char); + +/** + * A C wide char (two bytes) + */ +Type.char16_t = new Type("char16_t", ctypes.char16_t); + +/** + * Base string types. + */ +Type.cstring = Type.char.in_ptr.withName("[in] C string"); +Type.wstring = Type.char16_t.in_ptr.withName("[in] wide string"); +Type.out_cstring = Type.char.out_ptr.withName("[out] C string"); +Type.out_wstring = Type.char16_t.out_ptr.withName("[out] wide string"); + +/** + * A C integer (8-bits). + */ +Type.int8_t = new IntType("int8_t", ctypes.int8_t, true); + +Type.uint8_t = new IntType("uint8_t", ctypes.uint8_t, false); + +/** + * A C integer (16-bits). + * + * Also known as WORD under Windows. + */ +Type.int16_t = new IntType("int16_t", ctypes.int16_t, true); + +Type.uint16_t = new IntType("uint16_t", ctypes.uint16_t, false); + +/** + * A C integer (32-bits). + * + * Also known as DWORD under Windows. + */ +Type.int32_t = new IntType("int32_t", ctypes.int32_t, true); + +Type.uint32_t = new IntType("uint32_t", ctypes.uint32_t, false); + +/** + * A C integer (64-bits). + */ +Type.int64_t = new IntType("int64_t", ctypes.int64_t, true); + +Type.uint64_t = new IntType("uint64_t", ctypes.uint64_t, false); + +/** + * A C integer + * + * Size depends on the platform. + */ +Type.int = Type.intn_t(ctypes.int.size).withName("int"); + +Type.unsigned_int = Type.intn_t(ctypes.unsigned_int.size).withName( + "unsigned int" +); + +/** + * A C long integer. + * + * Size depends on the platform. + */ +Type.long = Type.intn_t(ctypes.long.size).withName("long"); + +Type.unsigned_long = Type.intn_t(ctypes.unsigned_long.size).withName( + "unsigned long" +); + +/** + * An unsigned integer with the same size as a pointer. + * + * Used to cast a pointer to an integer, whenever necessary. + */ +Type.uintptr_t = Type.uintn_t(ctypes.uintptr_t.size).withName("uintptr_t"); + +/** + * A boolean. + * Implemented as a C integer. + */ +Type.bool = Type.int.withName("bool"); +Type.bool.importFromC = function projectBool(x) { + return !!x.value; +}; + +/** + * A user identifier. + * + * Implemented as a C integer. + */ +Type.uid_t = Type.int.withName("uid_t"); + +/** + * A group identifier. + * + * Implemented as a C integer. + */ +Type.gid_t = Type.int.withName("gid_t"); + +/** + * An offset (positive or negative). + * + * Implemented as a C integer. + */ +Type.off_t = new IntType("off_t", ctypes.off_t, true); + +/** + * A size (positive). + * + * Implemented as a C size_t. + */ +Type.size_t = new IntType("size_t", ctypes.size_t, false); + +/** + * An offset (positive or negative). + * Implemented as a C integer. + */ +Type.ssize_t = new IntType("ssize_t", ctypes.ssize_t, true); + +/** + * Encoding/decoding strings + */ +Type.uencoder = new Type("uencoder", ctypes.StructType("uencoder")); +Type.udecoder = new Type("udecoder", ctypes.StructType("udecoder")); + +/** + * Utility class, used to build a |struct| type + * from a set of field names, types and offsets. + * + * @param {string} name The name of the |struct| type. + * @param {number} size The total size of the |struct| type in bytes. + */ +function HollowStructure(name, size) { + if (!name) { + throw new TypeError("HollowStructure expects a name"); + } + if (!size || size < 0) { + throw new TypeError("HollowStructure expects a (positive) size"); + } + + // A mapping from offsets in the struct to name/type pairs + // (or nothing if no field starts at that offset). + this.offset_to_field_info = []; + + // The name of the struct + this.name = name; + + // The size of the struct, in bytes + this.size = size; + + // The number of paddings inserted so far. + // Used to give distinct names to padding fields. + this._paddings = 0; +} +HollowStructure.prototype = { + /** + * Add a field at a given offset. + * + * @param {number} offset The offset at which to insert the field. + * @param {string} name The name of the field. + * @param {CType|Type} type The type of the field. + */ + add_field_at: function add_field_at(offset, name, type) { + if (offset == null) { + throw new TypeError("add_field_at requires a non-null offset"); + } + if (!name) { + throw new TypeError("add_field_at requires a non-null name"); + } + if (!type) { + throw new TypeError("add_field_at requires a non-null type"); + } + if (type instanceof Type) { + type = type.implementation; + } + if (this.offset_to_field_info[offset]) { + throw new Error( + "HollowStructure " + + this.name + + " already has a field at offset " + + offset + ); + } + if (offset + type.size > this.size) { + throw new Error( + "HollowStructure " + + this.name + + " cannot place a value of type " + + type + + " at offset " + + offset + + " without exceeding its size of " + + this.size + ); + } + let field = { name, type }; + this.offset_to_field_info[offset] = field; + }, + + /** + * Create a pseudo-field that will only serve as padding. + * + * @param {number} size The number of bytes in the field. + * @return {Object} An association field-name => field-type, + * as expected by |ctypes.StructType|. + */ + _makePaddingField: function makePaddingField(size) { + let field = {}; + field["padding_" + this._paddings] = ctypes.ArrayType(ctypes.uint8_t, size); + this._paddings++; + return field; + }, + + /** + * Convert this |HollowStructure| into a |Type|. + */ + getType: function getType() { + // Contents of the structure, in the format expected + // by ctypes.StructType. + let struct = []; + + let i = 0; + while (i < this.size) { + let currentField = this.offset_to_field_info[i]; + if (!currentField) { + // No field was specified at this offset, we need to + // introduce some padding. + + // Firstly, determine how many bytes of padding + let padding_length = 1; + while ( + i + padding_length < this.size && + !this.offset_to_field_info[i + padding_length] + ) { + ++padding_length; + } + + // Then add the padding + struct.push(this._makePaddingField(padding_length)); + + // And proceed + i += padding_length; + } else { + // We have a field at this offset. + + // Firstly, ensure that we do not have two overlapping fields + for (let j = 1; j < currentField.type.size; ++j) { + let candidateField = this.offset_to_field_info[i + j]; + if (candidateField) { + throw new Error( + "Fields " + + currentField.name + + " and " + + candidateField.name + + " overlap at position " + + (i + j) + ); + } + } + + // Then add the field + let field = {}; + field[currentField.name] = currentField.type; + struct.push(field); + + // And proceed + i += currentField.type.size; + } + } + let result = new Type(this.name, ctypes.StructType(this.name, struct)); + if (result.implementation.size != this.size) { + throw new Error( + "Wrong size for type " + + this.name + + ": expected " + + this.size + + ", found " + + result.implementation.size + + " (" + + result.implementation.toSource() + + ")" + ); + } + return result; + }, +}; +exports.HollowStructure = HollowStructure; + +/** + * Representation of a native library. + * + * The native library is opened lazily, during the first call to its + * field |library| or whenever accessing one of the methods imported + * with declareLazyFFI. + * + * @param {string} name A human-readable name for the library. Used + * for debugging and error reporting. + * @param {string...} candidates A list of system libraries that may + * represent this library. Used e.g. to try different library names + * on distinct operating systems ("libxul", "XUL", etc.). + * + * @constructor + */ +function Library(name, ...candidates) { + this.name = name; + this._candidates = candidates; +} +Library.prototype = Object.freeze({ + /** + * The native library as a js-ctypes object. + * + * @throws {Error} If none of the candidate libraries could be opened. + */ + get library() { + let library; + delete this.library; + for (let candidate of this._candidates) { + try { + library = ctypes.open(candidate); + break; + } catch (ex) { + LOG("Could not open library", candidate, ex); + } + } + this._candidates = null; + if (library) { + Object.defineProperty(this, "library", { + value: library, + }); + Object.freeze(this); + return library; + } + let error = new Error("Could not open library " + this.name); + Object.defineProperty(this, "library", { + get() { + throw error; + }, + }); + Object.freeze(this); + throw error; + }, + + /** + * Declare a function, lazily. + * + * @param {object} The object containing the function as a field. + * @param {string} The name of the field containing the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {Type} returnType The type of values returned by the function. + * @param {...Type} argTypes The type of arguments to the function. + */ + declareLazyFFI(object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get() { + delete this[field]; + let ffi = declareFFI(lib.library, ...args); + if (ffi) { + return (this[field] = ffi); + } + return undefined; + }, + configurable: true, + enumerable: true, + }); + }, + + /** + * Define a js-ctypes function lazily using ctypes method declare. + * + * @param {object} The object containing the function as a field. + * @param {string} The name of the field containing the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {ctypes.CType} returnType The type of values returned by the function. + * @param {...ctypes.CType} argTypes The type of arguments to the function. + */ + declareLazy(object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get() { + delete this[field]; + let ffi = lib.library.declare(...args); + if (ffi) { + return (this[field] = ffi); + } + return undefined; + }, + configurable: true, + enumerable: true, + }); + }, + + /** + * Define a js-ctypes function lazily using ctypes method declare, + * with a fallback library to use if this library can't be opened + * or the function cannot be declared. + * + * @param {fallbacklibrary} The fallback Library object. + * @param {object} The object containing the function as a field. + * @param {string} The name of the field containing the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {ctypes.CType} returnType The type of values returned by the function. + * @param {...ctypes.CType} argTypes The type of arguments to the function. + */ + declareLazyWithFallback(fallbacklibrary, object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get() { + delete this[field]; + try { + let ffi = lib.library.declare(...args); + if (ffi) { + return (this[field] = ffi); + } + } catch (ex) { + // Use the fallback library and get the symbol from there. + fallbacklibrary.declareLazy(object, field, ...args); + return object[field]; + } + return undefined; + }, + configurable: true, + enumerable: true, + }); + }, + + toString() { + return "[Library " + this.name + "]"; + }, +}); +exports.Library = Library; + +/** + * Declare a function through js-ctypes + * + * @param {ctypes.library} lib The ctypes library holding the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {Type} returnType The type of values returned by the function. + * @param {...Type} argTypes The type of arguments to the function. + * + * @return null if the function could not be defined (generally because + * it does not exist), or a JavaScript wrapper performing the call to C + * and any type conversion required. + */ +var declareFFI = function declareFFI( + lib, + symbol, + abi, + returnType /* , argTypes ...*/ +) { + LOG("Attempting to declare FFI ", symbol); + // We guard agressively, to avoid any late surprise + if (typeof symbol != "string") { + throw new TypeError("declareFFI expects as first argument a string"); + } + abi = abi || ctypes.default_abi; + if (Object.prototype.toString.call(abi) != "[object CABI]") { + // Note: This is the only known manner of checking whether an object + // is an abi. + throw new TypeError("declareFFI expects as second argument an abi or null"); + } + if (!returnType.importFromC) { + throw new TypeError( + "declareFFI expects as third argument an instance of Type" + ); + } + let signature = [symbol, abi]; + for (let i = 3; i < arguments.length; ++i) { + let current = arguments[i]; + if (!current) { + throw new TypeError( + "Missing type for argument " + (i - 3) + " of symbol " + symbol + ); + } + // Ellipsis for variadic arguments. + if (current == "...") { + if (i != arguments.length - 1) { + throw new TypeError("Variadic ellipsis must be the last argument"); + } + signature.push(current); + continue; + } + if (!current.implementation) { + throw new TypeError( + "Missing implementation for argument " + + (i - 3) + + " of symbol " + + symbol + + " ( " + + current.name + + " )" + ); + } + signature.push(current.implementation); + } + try { + let fun = lib.declare.apply(lib, signature); + let result = function ffi(...args) { + for (let i = 0; i < args.length; i++) { + if (typeof args[i] == "undefined") { + throw new TypeError( + "Argument " + i + " of " + symbol + " is undefined" + ); + } + } + let result = fun.apply(fun, args); + return returnType.importFromC(result, symbol); + }; + LOG("Function", symbol, "declared"); + return result; + } catch (x) { + // Note: Not being able to declare a function is normal. + // Some functions are OS (or OS version)-specific. + LOG("Could not declare function ", symbol, x); + return null; + } +}; +exports.declareFFI = declareFFI; + +/** + * Define a lazy getter to a js-ctypes function using declareFFI. + * + * @param {object} The object containing the function as a field. + * @param {string} The name of the field containing the function. + * @param {ctypes.library} lib The ctypes library holding the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {Type} returnType The type of values returned by the function. + * @param {...Type} argTypes The type of arguments to the function. + */ +function declareLazyFFI(object, field, ...declareFFIArgs) { + Object.defineProperty(object, field, { + get() { + delete this[field]; + let ffi = declareFFI(...declareFFIArgs); + if (ffi) { + return (this[field] = ffi); + } + return undefined; + }, + configurable: true, + enumerable: true, + }); +} +exports.declareLazyFFI = declareLazyFFI; + +/** + * Define a lazy getter to a js-ctypes function using ctypes method declare. + * + * @param {object} The object containing the function as a field. + * @param {string} The name of the field containing the function. + * @param {ctypes.library} lib The ctypes library holding the function. + * @param {string} symbol The name of the function, as defined in the + * library. + * @param {ctypes.abi} abi The abi to use, or |null| for default. + * @param {ctypes.CType} returnType The type of values returned by the function. + * @param {...ctypes.CType} argTypes The type of arguments to the function. + */ +function declareLazy(object, field, lib, ...declareArgs) { + Object.defineProperty(object, field, { + get() { + delete this[field]; + try { + let ffi = lib.declare(...declareArgs); + return (this[field] = ffi); + } catch (ex) { + // The symbol doesn't exist + return undefined; + } + }, + configurable: true, + }); +} +exports.declareLazy = declareLazy; + +/** + * Utility function used to sanity check buffer and length arguments. The + * buffer must be a Typed Array. + * + * @param {Typed array} candidate The buffer. + * @param {number} bytes The number of bytes that |candidate| should contain. + * + * @return number The bytes argument clamped to the length of the buffer. + */ +function normalizeBufferArgs(candidate, bytes) { + if (!candidate) { + throw new TypeError("Expecting a Typed Array"); + } + if (!isTypedArray(candidate)) { + throw new TypeError("Expecting a Typed Array"); + } + if (bytes == null) { + bytes = candidate.byteLength; + } else if (candidate.byteLength < bytes) { + throw new TypeError( + "Buffer is too short. I need at least " + + bytes + + " bytes but I have only " + + candidate.byteLength + + "bytes" + ); + } + return bytes; +} +exports.normalizeBufferArgs = normalizeBufferArgs; + +// /////////////////// OS interactions + +/** + * An OS error. + * + * This class is provided mostly for type-matching. If you need more + * details about an error, you should use the platform-specific error + * codes provided by subclasses of |OS.Shared.Error|. + * + * @param {string} operation The operation that failed. + * @param {string=} path The path of the file on which the operation failed, + * or nothing if there was no file involved in the failure. + * + * @constructor + */ +function OSError(operation, path = "") { + Error.call(this); + this.operation = operation; + this.path = path; +} +OSError.prototype = Object.create(Error.prototype); +exports.OSError = OSError; + +// /////////////////// Temporary boilerplate +// Boilerplate, to simplify the transition to require() +// Do not rely upon this symbol, it will disappear with +// bug 883050. +exports.OS = { + Constants: exports.Constants, + Shared: { + LOG, + clone, + Type, + HollowStructure, + Error: OSError, + declareFFI, + projectValue, + isTypedArray, + defineLazyGetter, + }, +}; + +Object.defineProperty(exports.OS.Shared, "DEBUG", { + get() { + return Config.DEBUG; + }, + set(x) { + Config.DEBUG = x; + }, +}); +Object.defineProperty(exports.OS.Shared, "TEST", { + get() { + return Config.TEST; + }, + set(x) { + Config.TEST = x; + }, +}); + +// /////////////////// Permanent boilerplate +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + // eslint-disable-next-line mozilla/reject-global-this + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_shared_front.js b/toolkit/components/osfile/modules/osfile_shared_front.js new file mode 100644 index 000000000000..203e23352704 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_front.js @@ -0,0 +1,607 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Code shared by OS.File front-ends. + * + * This code is meant to be included by another library. It is also meant to + * be executed only on a worker thread. + */ + +/* eslint-env node */ +/* global OS */ + +if (typeof Components != "undefined") { + throw new Error("osfile_shared_front.js cannot be used from the main thread"); +} +(function(exports) { + var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + var Path = require("resource://gre/modules/osfile/ospath.jsm"); + var Lz4 = require("resource://gre/modules/lz4.js"); + SharedAll.LOG.bind(SharedAll, "Shared front-end"); + var clone = SharedAll.clone; + + /** + * Code shared by implementations of File. + * + * @param {*} fd An OS-specific file handle. + * @param {string} path File path of the file handle, used for error-reporting. + * @constructor + */ + var AbstractFile = function AbstractFile(fd, path) { + this._fd = fd; + if (!path) { + throw new TypeError("path is expected"); + } + this._path = path; + }; + + AbstractFile.prototype = { + /** + * Return the file handle. + * + * @throw OS.File.Error if the file has been closed. + */ + get fd() { + if (this._fd) { + return this._fd; + } + throw OS.File.Error.closed("accessing file", this._path); + }, + /** + * Read bytes from this file to a new buffer. + * + * @param {number=} maybeBytes (deprecated, please use options.bytes) + * @param {JSON} options + * @return {Uint8Array} An array containing the bytes read. + */ + read: function read(maybeBytes, options = {}) { + if (typeof maybeBytes === "object") { + // Caller has skipped `maybeBytes` and provided an options object. + options = clone(maybeBytes); + maybeBytes = null; + } else { + options = options || {}; + } + let bytes = options.bytes || undefined; + if (bytes === undefined) { + bytes = maybeBytes == null ? this.stat().size : maybeBytes; + } + let buffer = new Uint8Array(bytes); + let pos = 0; + while (pos < bytes) { + let length = bytes - pos; + let view = new DataView(buffer.buffer, pos, length); + let chunkSize = this._read(view, length, options); + if (chunkSize == 0) { + break; + } + pos += chunkSize; + } + if (pos == bytes) { + return buffer; + } + return buffer.subarray(0, pos); + }, + + /** + * Write bytes from a buffer to this file. + * + * Note that, by default, this function may perform several I/O + * operations to ensure that the buffer is fully written. + * + * @param {Typed array} buffer The buffer in which the the bytes are + * stored. The buffer must be large enough to accomodate |bytes| bytes. + * @param {*=} options Optionally, an object that may contain the + * following fields: + * - {number} bytes The number of |bytes| to write from the buffer. If + * unspecified, this is |buffer.byteLength|. + * + * @return {number} The number of bytes actually written. + */ + write: function write(buffer, options = {}) { + let bytes = SharedAll.normalizeBufferArgs( + buffer, + "bytes" in options ? options.bytes : undefined + ); + let pos = 0; + while (pos < bytes) { + let length = bytes - pos; + let view = new DataView(buffer.buffer, buffer.byteOffset + pos, length); + let chunkSize = this._write(view, length, options); + pos += chunkSize; + } + return pos; + }, + }; + + /** + * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name. + * + * @param {string} path The path to the file. + * @param {*=} options Additional options for file opening. This + * implementation interprets the following fields: + * + * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. + * If |false| use HEX numbers ie: filename-A65BC0.ext + * - {number} maxReadableNumber Used to limit the amount of tries after a failed + * file creation. Default is 20. + * + * @return {Object} contains A file object{file} and the path{path}. + * @throws {OS.File.Error} If the file could not be opened. + */ + AbstractFile.openUnique = function openUnique(path, options = {}) { + let mode = { + create: true, + }; + + let dirName = Path.dirname(path); + let leafName = Path.basename(path); + let lastDotCharacter = leafName.lastIndexOf("."); + let fileName = leafName.substring( + 0, + lastDotCharacter != -1 ? lastDotCharacter : leafName.length + ); + let suffix = + lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : ""; + let uniquePath = ""; + let maxAttempts = options.maxAttempts || 99; + let humanReadable = !!options.humanReadable; + const HEX_RADIX = 16; + // We produce HEX numbers between 0 and 2^24 - 1. + const MAX_HEX_NUMBER = 16777215; + + try { + return { + path, + file: OS.File.open(path, mode), + }; + } catch (ex) { + if (ex instanceof OS.File.Error && ex.becauseExists) { + for (let i = 0; i < maxAttempts; ++i) { + try { + if (humanReadable) { + uniquePath = Path.join( + dirName, + fileName + "-" + (i + 1) + suffix + ); + } else { + let hexNumber = Math.floor( + Math.random() * MAX_HEX_NUMBER + ).toString(HEX_RADIX); + uniquePath = Path.join( + dirName, + fileName + "-" + hexNumber + suffix + ); + } + return { + path: uniquePath, + file: OS.File.open(uniquePath, mode), + }; + } catch (ex) { + if (ex instanceof OS.File.Error && ex.becauseExists) { + // keep trying ... + } else { + throw ex; + } + } + } + throw OS.File.Error.exists("could not find an unused file name.", path); + } + throw ex; + } + }; + + /** + * Code shared by iterators. + */ + AbstractFile.AbstractIterator = function AbstractIterator() {}; + AbstractFile.AbstractIterator.prototype = { + /** + * Allow iterating with |for-of| + */ + [Symbol.iterator]() { + return this; + }, + /** + * Apply a function to all elements of the directory sequentially. + * + * @param {Function} cb This function will be applied to all entries + * of the directory. It receives as arguments + * - the OS.File.Entry corresponding to the entry; + * - the index of the entry in the enumeration; + * - the iterator itself - calling |close| on the iterator stops + * the loop. + */ + forEach: function forEach(cb) { + let index = 0; + for (let entry of this) { + cb(entry, index++, this); + } + }, + /** + * Return several entries at once. + * + * Entries are returned in the same order as a walk with |forEach| or + * |for(...)|. + * + * @param {number=} length If specified, the number of entries + * to return. If unspecified, return all remaining entries. + * @return {Array} An array containing the next |length| entries, or + * less if the iteration contains less than |length| entries left. + */ + nextBatch: function nextBatch(length) { + let array = []; + let i = 0; + for (let entry of this) { + array.push(entry); + if (++i >= length) { + return array; + } + } + return array; + }, + }; + + /** + * Utility function shared by implementations of |OS.File.open|: + * extract read/write/trunc/create/existing flags from a |mode| + * object. + * + * @param {*=} mode An object that may contain fields |read|, + * |write|, |truncate|, |create|, |existing|. These fields + * are interpreted only if true-ish. + * @return {{read:bool, write:bool, trunc:bool, create:bool, + * existing:bool}} an object recapitulating the options set + * by |mode|. + * @throws {TypeError} If |mode| contains other fields, or + * if it contains both |create| and |truncate|, or |create| + * and |existing|. + */ + AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) { + let result = { + read: false, + write: false, + trunc: false, + create: false, + existing: false, + append: true, + }; + for (let key in mode) { + let val = !!mode[key]; // bool cast. + switch (key) { + case "read": + result.read = val; + break; + case "write": + result.write = val; + break; + case "truncate": // fallthrough + case "trunc": + result.trunc = val; + result.write |= val; + break; + case "create": + result.create = val; + result.write |= val; + break; + case "existing": // fallthrough + case "exist": + result.existing = val; + break; + case "append": + result.append = val; + break; + default: + throw new TypeError("Mode " + key + " not understood"); + } + } + // Reject opposite modes + if (result.existing && result.create) { + throw new TypeError("Cannot specify both existing:true and create:true"); + } + if (result.trunc && result.create) { + throw new TypeError("Cannot specify both trunc:true and create:true"); + } + // Handle read/write + if (!result.write) { + result.read = true; + } + return result; + }; + + /** + * Return the contents of a file. + * + * @param {string} path The path to the file. + * @param {number=} bytes Optionally, an upper bound to the number of bytes + * to read. DEPRECATED - please use options.bytes instead. + * @param {object=} options Optionally, an object with some of the following + * fields: + * - {number} bytes An upper bound to the number of bytes to read. + * - {string} compression If "lz4" and if the file is compressed using the lz4 + * compression algorithm, decompress the file contents on the fly. + * + * @return {Uint8Array} A buffer holding the bytes + * and the number of bytes read from the file. + */ + AbstractFile.read = function read(path, bytes, options = {}) { + if (bytes && typeof bytes == "object") { + options = bytes; + bytes = options.bytes || null; + } + if ("encoding" in options && typeof options.encoding != "string") { + throw new TypeError("Invalid type for option encoding"); + } + if ("compression" in options && typeof options.compression != "string") { + throw new TypeError( + "Invalid type for option compression: " + options.compression + ); + } + if ("bytes" in options && typeof options.bytes != "number") { + throw new TypeError("Invalid type for option bytes"); + } + let file = exports.OS.File.open(path); + try { + let buffer = file.read(bytes, options); + if ("compression" in options) { + if (options.compression == "lz4") { + options = Object.create(options); + options.path = path; + buffer = Lz4.decompressFileContent(buffer, options); + } else { + throw OS.File.Error.invalidArgument("Compression"); + } + } + if (!("encoding" in options)) { + return buffer; + } + let decoder; + try { + decoder = new TextDecoder(options.encoding); + } catch (ex) { + if (ex instanceof RangeError) { + throw OS.File.Error.invalidArgument("Decode"); + } else { + throw ex; + } + } + return decoder.decode(buffer); + } finally { + file.close(); + } + }; + + /** + * Write a file, atomically. + * + * By opposition to a regular |write|, this operation ensures that, + * until the contents are fully written, the destination file is + * not modified. + * + * Limitation: In a few extreme cases (hardware failure during the + * write, user unplugging disk during the write, etc.), data may be + * corrupted. If your data is user-critical (e.g. preferences, + * application data, etc.), you may wish to consider adding options + * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as + * detailed below. Note that no combination of options can be + * guaranteed to totally eliminate the risk of corruption. + * + * @param {string} path The path of the file to modify. + * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. + * @param {*=} options Optionally, an object determining the behavior + * of this function. This object may contain the following fields: + * - {number} bytes The number of bytes to write. If unspecified, + * |buffer.byteLength|. Required if |buffer| is a C pointer. + * - {string} tmpPath If |null| or unspecified, write all data directly + * to |path|. If specified, write all data to a temporary file called + * |tmpPath| and, once this write is complete, rename the file to + * replace |path|. Performing this additional operation is a little + * slower but also a little safer. + * - {bool} noOverwrite - If set, this function will fail if a file already + * exists at |path|. + * - {bool} flush - If |false| or unspecified, return immediately once the + * write is complete. If |true|, before writing, force the operating system + * to write its internal disk buffers to the disk. This is considerably slower + * (not just for the application but for the whole system) but also safer: + * if the system shuts down improperly (typically due to a kernel freeze + * or a power failure) or if the device is disconnected before the buffer + * is flushed, the file has more chances of not being corrupted. + * - {string} compression - If empty or unspecified, do not compress the file. + * If "lz4", compress the contents of the file atomically using lz4. For the + * time being, the container format is specific to Mozilla and cannot be read + * by means other than OS.File.read(..., { compression: "lz4"}) + * - {string} backupTo - If specified, backup the destination file as |backupTo|. + * Note that this function renames the destination file before overwriting it. + * If the process or the operating system freezes or crashes + * during the short window between these operations, + * the destination file will have been moved to its backup. + * + * @return {number} The number of bytes actually written. + */ + AbstractFile.writeAtomic = function writeAtomic(path, buffer, options = {}) { + // Verify that path is defined and of the correct type + if (typeof path != "string" || path == "") { + throw new TypeError("File path should be a (non-empty) string"); + } + let noOverwrite = options.noOverwrite; + if (noOverwrite && OS.File.exists(path)) { + throw OS.File.Error.exists("writeAtomic", path); + } + + if (typeof buffer == "string") { + // Normalize buffer to a C buffer by encoding it + buffer = new TextEncoder().encode(buffer); + } + + if ("compression" in options && options.compression == "lz4") { + buffer = Lz4.compressFileContent(buffer, options); + options = Object.create(options); + options.bytes = buffer.byteLength; + } + + let bytesWritten = 0; + + if (!options.tmpPath) { + if (options.backupTo) { + try { + OS.File.move(path, options.backupTo, { noCopy: true }); + } catch (ex) { + if (ex.becauseNoSuchFile) { + // The file doesn't exist, nothing to backup. + } else { + throw ex; + } + } + } + // Just write, without any renaming trick + let dest = OS.File.open(path, { write: true, truncate: true }); + try { + bytesWritten = dest.write(buffer, options); + if (options.flush) { + dest.flush(); + } + } finally { + dest.close(); + } + return bytesWritten; + } + + let tmpFile = OS.File.open(options.tmpPath, { + write: true, + truncate: true, + }); + try { + bytesWritten = tmpFile.write(buffer, options); + if (options.flush) { + tmpFile.flush(); + } + } catch (x) { + OS.File.remove(options.tmpPath); + throw x; + } finally { + tmpFile.close(); + } + + if (options.backupTo) { + try { + OS.File.move(path, options.backupTo, { noCopy: true }); + } catch (ex) { + if (ex.becauseNoSuchFile) { + // The file doesn't exist, nothing to backup. + } else { + throw ex; + } + } + } + + OS.File.move(options.tmpPath, path, { noCopy: true }); + return bytesWritten; + }; + + /** + * This function is used by removeDir to avoid calling lstat for each + * files under the specified directory. External callers should not call + * this function directly. + */ + AbstractFile.removeRecursive = function(path, options = {}) { + let iterator = new OS.File.DirectoryIterator(path); + if (!iterator.exists()) { + if (!("ignoreAbsent" in options) || options.ignoreAbsent) { + return; + } + } + + try { + for (let entry of iterator) { + if (entry.isDir) { + if (entry.isLink) { + // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to + // directories are directories themselves. OS.File.remove() + // will not work for them. + OS.File.removeEmptyDir(entry.path, options); + } else { + // Normal directories. + AbstractFile.removeRecursive(entry.path, options); + } + } else { + // NTFS symlinks to files, Unix symlinks, or regular files. + OS.File.remove(entry.path, options); + } + } + } finally { + iterator.close(); + } + + OS.File.removeEmptyDir(path); + }; + + /** + * Create a directory and, optionally, its parent directories. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. + * + * - {string} from If specified, the call to |makeDir| creates all the + * ancestors of |path| that are descendants of |from|. Note that |path| + * must be a descendant of |from|, and that |from| and its existing + * subdirectories present in |path| must be user-writeable. + * Example: + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); + * creates directories profileDir/foo, profileDir/foo/bar + * - {bool} ignoreExisting If |false|, throw an error if the directory + * already exists. |true| by default. Ignored if |from| is specified. + * - {number} unixMode Under Unix, if specified, a file creation mode, + * as per libc function |mkdir|. If unspecified, dirs are + * created with a default mode of 0700 (dir is private to + * the user, the user can read, write and execute). Ignored under Windows + * or if the file system does not support file creation modes. + * - {C pointer} winSecurity Under Windows, if specified, security + * attributes as per winapi function |CreateDirectory|. If + * unspecified, use the default security descriptor, inherited from + * the parent directory. Ignored under Unix or if the file system + * does not support security descriptors. + */ + AbstractFile.makeDir = function(path, options = {}) { + let from = options.from; + if (!from) { + OS.File._makeDir(path, options); + return; + } + if (!path.startsWith(from)) { + // Apparently, `from` is not a parent of `path`. However, we may + // have false negatives due to non-normalized paths, e.g. + // "foo//bar" is a parent of "foo/bar/sna". + path = Path.normalize(path); + from = Path.normalize(from); + if (!path.startsWith(from)) { + throw new Error( + "Incorrect use of option |from|: " + + path + + " is not a descendant of " + + from + ); + } + } + let innerOptions = Object.create(options, { + ignoreExisting: { + value: true, + }, + }); + // Compute the elements that appear in |path| but not in |from|. + let items = Path.split(path).components.slice( + Path.split(from).components.length + ); + let current = from; + for (let item of items) { + current = Path.join(current, item); + OS.File._makeDir(current, innerOptions); + } + }; + + if (!exports.OS.Shared) { + exports.OS.Shared = {}; + } + exports.OS.Shared.AbstractFile = AbstractFile; +})(this); diff --git a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm new file mode 100644 index 000000000000..f40d3ee1d176 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm @@ -0,0 +1,412 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This module defines the thread-agnostic components of the Unix version + * of OS.File. It depends on the thread-agnostic cross-platform components + * of OS.File. + * + * It serves the following purposes: + * - open libc; + * - define OS.Unix.Error; + * - define a few constants and types that need to be defined on all platforms. + * + * This module can be: + * - opened from the main thread as a jsm module; + * - opened from a chrome worker through require(). + */ + +/* eslint-env node */ + +"use strict"; + +var SharedAll; +if (typeof Components != "undefined") { + // Module is opened as a jsm module + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + // eslint-disable-next-line mozilla/reject-global-this + this.ctypes = ctypes; + + SharedAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" + ); + // eslint-disable-next-line mozilla/reject-global-this + this.exports = {}; +} else if (typeof module != "undefined" && typeof require != "undefined") { + // Module is loaded with require() + SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); +} else { + throw new Error( + "Please open this module with Component.utils.import or with require()" + ); +} + +SharedAll.LOG.bind(SharedAll, "Unix", "allthreads"); +var Const = SharedAll.Constants.libc; + +// Open libc +var libc = new SharedAll.Library( + "libc", + "libc.so", + "libSystem.B.dylib", + "a.out" +); +exports.libc = libc; + +// Define declareFFI +var declareFFI = SharedAll.declareFFI.bind(null, libc); +exports.declareFFI = declareFFI; + +// Define lazy binding +var LazyBindings = {}; +libc.declareLazy( + LazyBindings, + "strerror", + "strerror", + ctypes.default_abi, + /* return*/ ctypes.char.ptr, + /* errnum*/ ctypes.int +); + +/** + * A File-related error. + * + * To obtain a human-readable error message, use method |toString|. + * To determine the cause of the error, use the various |becauseX| + * getters. To determine the operation that failed, use field + * |operation|. + * + * Additionally, this implementation offers a field + * |unixErrno|, which holds the OS-specific error + * constant. If you need this level of detail, you may match the value + * of this field against the error constants of |OS.Constants.libc|. + * + * @param {string=} operation The operation that failed. If unspecified, + * the name of the calling function is taken to be the operation that + * failed. + * @param {number=} lastError The OS-specific constant detailing the + * reason of the error. If unspecified, this is fetched from the system + * status. + * @param {string=} path The file path that manipulated. If unspecified, + * assign the empty string. + * + * @constructor + * @extends {OS.Shared.Error} + */ +var OSError = function OSError( + operation = "unknown operation", + errno = ctypes.errno, + path = "" +) { + SharedAll.OSError.call(this, operation, path); + this.unixErrno = errno; +}; +OSError.prototype = Object.create(SharedAll.OSError.prototype); +OSError.prototype.toString = function toString() { + return ( + "Unix error " + + this.unixErrno + + " during operation " + + this.operation + + (this.path ? " on file " + this.path : "") + + " (" + + LazyBindings.strerror(this.unixErrno).readStringReplaceMalformed() + + ")" + ); +}; +OSError.prototype.toMsg = function toMsg() { + return OSError.toMsg(this); +}; + +/** + * |true| if the error was raised because a file or directory + * already exists, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseExists", { + get: function becauseExists() { + return this.unixErrno == Const.EEXIST; + }, +}); +/** + * |true| if the error was raised because a file or directory + * does not exist, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseNoSuchFile", { + get: function becauseNoSuchFile() { + return this.unixErrno == Const.ENOENT; + }, +}); + +/** + * |true| if the error was raised because a directory is not empty + * does not exist, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseNotEmpty", { + get: function becauseNotEmpty() { + return this.unixErrno == Const.ENOTEMPTY; + }, +}); +/** + * |true| if the error was raised because a file or directory + * is closed, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseClosed", { + get: function becauseClosed() { + return this.unixErrno == Const.EBADF; + }, +}); +/** + * |true| if the error was raised because permission is denied to + * access a file or directory, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseAccessDenied", { + get: function becauseAccessDenied() { + return this.unixErrno == Const.EACCES; + }, +}); +/** + * |true| if the error was raised because some invalid argument was passed, + * |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseInvalidArgument", { + get: function becauseInvalidArgument() { + return this.unixErrno == Const.EINVAL; + }, +}); + +/** + * Serialize an instance of OSError to something that can be + * transmitted across threads (not necessarily a string). + */ +OSError.toMsg = function toMsg(error) { + return { + exn: "OS.File.Error", + fileName: error.moduleName, + lineNumber: error.lineNumber, + stack: error.moduleStack, + operation: error.operation, + unixErrno: error.unixErrno, + path: error.path, + }; +}; + +/** + * Deserialize a message back to an instance of OSError + */ +OSError.fromMsg = function fromMsg(msg) { + let error = new OSError(msg.operation, msg.unixErrno, msg.path); + error.stack = msg.stack; + error.fileName = msg.fileName; + error.lineNumber = msg.lineNumber; + return error; +}; +exports.Error = OSError; + +/** + * Code shared by implementations of File.Info on Unix + * + * @constructor + */ +var AbstractInfo = function AbstractInfo( + path, + isDir, + isSymLink, + size, + lastAccessDate, + lastModificationDate, + unixLastStatusChangeDate, + unixOwner, + unixGroup, + unixMode +) { + this._path = path; + this._isDir = isDir; + this._isSymlLink = isSymLink; + this._size = size; + this._lastAccessDate = lastAccessDate; + this._lastModificationDate = lastModificationDate; + this._unixLastStatusChangeDate = unixLastStatusChangeDate; + this._unixOwner = unixOwner; + this._unixGroup = unixGroup; + this._unixMode = unixMode; +}; + +AbstractInfo.prototype = { + /** + * The path of the file, used for error-reporting. + * + * @type {string} + */ + get path() { + return this._path; + }, + /** + * |true| if this file is a directory, |false| otherwise + */ + get isDir() { + return this._isDir; + }, + /** + * |true| if this file is a symbolink link, |false| otherwise + */ + get isSymLink() { + return this._isSymlLink; + }, + /** + * The size of the file, in bytes. + * + * Note that the result may be |NaN| if the size of the file cannot be + * represented in JavaScript. + * + * @type {number} + */ + get size() { + return this._size; + }, + /** + * The date of last access to this file. + * + * Note that the definition of last access may depend on the + * underlying operating system and file system. + * + * @type {Date} + */ + get lastAccessDate() { + return this._lastAccessDate; + }, + /** + * Return the date of last modification of this file. + */ + get lastModificationDate() { + return this._lastModificationDate; + }, + /** + * Return the date at which the status of this file was last modified + * (this is the date of the latest write/renaming/mode change/... + * of the file) + */ + get unixLastStatusChangeDate() { + return this._unixLastStatusChangeDate; + }, + /* + * Return the Unix owner of this file + */ + get unixOwner() { + return this._unixOwner; + }, + /* + * Return the Unix group of this file + */ + get unixGroup() { + return this._unixGroup; + }, + /* + * Return the Unix group of this file + */ + get unixMode() { + return this._unixMode; + }, +}; +exports.AbstractInfo = AbstractInfo; + +/** + * Code shared by implementations of File.DirectoryIterator.Entry on Unix + * + * @constructor + */ +var AbstractEntry = function AbstractEntry(isDir, isSymLink, name, path) { + this._isDir = isDir; + this._isSymlLink = isSymLink; + this._name = name; + this._path = path; +}; + +AbstractEntry.prototype = { + /** + * |true| if the entry is a directory, |false| otherwise + */ + get isDir() { + return this._isDir; + }, + /** + * |true| if the entry is a directory, |false| otherwise + */ + get isSymLink() { + return this._isSymlLink; + }, + /** + * The name of the entry + * @type {string} + */ + get name() { + return this._name; + }, + /** + * The full path to the entry + */ + get path() { + return this._path; + }, +}; +exports.AbstractEntry = AbstractEntry; + +// Special constants that need to be defined on all platforms + +exports.POS_START = Const.SEEK_SET; +exports.POS_CURRENT = Const.SEEK_CUR; +exports.POS_END = Const.SEEK_END; + +// Special types that need to be defined for communication +// between threads +var Type = Object.create(SharedAll.Type); +exports.Type = Type; + +/** + * Native paths + * + * Under Unix, expressed as C strings + */ +Type.path = Type.cstring.withName("[in] path"); +Type.out_path = Type.out_cstring.withName("[out] path"); + +// Special constructors that need to be defined on all threads +OSError.closed = function closed(operation, path) { + return new OSError(operation, Const.EBADF, path); +}; + +OSError.exists = function exists(operation, path) { + return new OSError(operation, Const.EEXIST, path); +}; + +OSError.noSuchFile = function noSuchFile(operation, path) { + return new OSError(operation, Const.ENOENT, path); +}; + +OSError.invalidArgument = function invalidArgument(operation) { + return new OSError(operation, Const.EINVAL); +}; + +var EXPORTED_SYMBOLS = [ + "declareFFI", + "libc", + "Error", + "AbstractInfo", + "AbstractEntry", + "Type", + "POS_START", + "POS_CURRENT", + "POS_END", +]; + +// ////////// Boilerplate +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + // eslint-disable-next-line mozilla/reject-global-this + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_unix_back.js b/toolkit/components/osfile/modules/osfile_unix_back.js new file mode 100644 index 000000000000..89600e1abca7 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_back.js @@ -0,0 +1,1051 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/chrome-worker, node */ +/* global OS */ + +// eslint-disable-next-line no-lone-blocks +{ + if (typeof Components != "undefined") { + // We do not wish osfile_unix_back.js to be used directly as a main thread + // module yet. When time comes, it will be loaded by a combination of + // a main thread front-end/worker thread implementation that makes sure + // that we are not executing synchronous IO code in the main thread. + + throw new Error( + "osfile_unix_back.js cannot be used from the main thread yet" + ); + } + (function(exports) { + "use strict"; + if (exports.OS && exports.OS.Unix && exports.OS.Unix.File) { + return; // Avoid double initialization + } + + let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + let SysAll = require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm"); + SharedAll.LOG.bind(SharedAll, "Unix", "back"); + let libc = SysAll.libc; + let Const = SharedAll.Constants.libc; + + /** + * Initialize the Unix module. + * + * @param {function=} declareFFI + */ + // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them + let init = function init(aDeclareFFI) { + if (aDeclareFFI) { + aDeclareFFI.bind(null, libc); + } else { + SysAll.declareFFI; + } + SharedAll.declareLazyFFI; + + // Initialize types that require additional OS-specific + // support - either finalization or matching against + // OS-specific constants. + let Type = Object.create(SysAll.Type); + let SysFile = (exports.OS.Unix.File = { Type }); + + /** + * A file descriptor. + */ + Type.fd = Type.int.withName("fd"); + Type.fd.importFromC = function importFromC(fd_int) { + return ctypes.CDataFinalizer(fd_int, SysFile._close); + }; + + /** + * A C integer holding -1 in case of error or a file descriptor + * in case of success. + */ + Type.negativeone_or_fd = Type.fd.withName("negativeone_or_fd"); + Type.negativeone_or_fd.importFromC = function importFromC(fd_int) { + if (fd_int == -1) { + return -1; + } + return ctypes.CDataFinalizer(fd_int, SysFile._close); + }; + + /** + * A C integer holding -1 in case of error or a meaningless value + * in case of success. + */ + Type.negativeone_or_nothing = Type.int.withName("negativeone_or_nothing"); + + /** + * A C integer holding -1 in case of error or a positive integer + * in case of success. + */ + Type.negativeone_or_ssize_t = Type.ssize_t.withName( + "negativeone_or_ssize_t" + ); + + /** + * Various libc integer types + */ + Type.mode_t = Type.intn_t(Const.OSFILE_SIZEOF_MODE_T).withName("mode_t"); + Type.uid_t = Type.intn_t(Const.OSFILE_SIZEOF_UID_T).withName("uid_t"); + Type.gid_t = Type.intn_t(Const.OSFILE_SIZEOF_GID_T).withName("gid_t"); + + /** + * Type |time_t| + */ + Type.time_t = Type.intn_t(Const.OSFILE_SIZEOF_TIME_T).withName("time_t"); + + // Structure |dirent| + // Building this type is rather complicated, as its layout varies between + // variants of Unix. For this reason, we rely on a number of constants + // (computed in C from the C data structures) that give us the layout. + // The structure we compute looks like + // { int8_t[...] before_d_type; // ignored content + // int8_t d_type ; + // int8_t[...] before_d_name; // ignored content + // char[...] d_name; + // }; + { + let d_name_extra_size = 0; + if (Const.OSFILE_SIZEOF_DIRENT_D_NAME < 8) { + // d_name is defined like "char d_name[1];" on some platforms + // (e.g. Solaris), we need to give it more size for our structure. + d_name_extra_size = 256; + } + + let dirent = new SharedAll.HollowStructure( + "dirent", + Const.OSFILE_SIZEOF_DIRENT + d_name_extra_size + ); + if (Const.OSFILE_OFFSETOF_DIRENT_D_TYPE != undefined) { + // |dirent| doesn't have d_type on some platforms (e.g. Solaris). + dirent.add_field_at( + Const.OSFILE_OFFSETOF_DIRENT_D_TYPE, + "d_type", + ctypes.uint8_t + ); + } + dirent.add_field_at( + Const.OSFILE_OFFSETOF_DIRENT_D_NAME, + "d_name", + ctypes.ArrayType( + ctypes.char, + Const.OSFILE_SIZEOF_DIRENT_D_NAME + d_name_extra_size + ) + ); + + // We now have built |dirent|. + Type.dirent = dirent.getType(); + } + Type.null_or_dirent_ptr = new SharedAll.Type( + "null_of_dirent", + Type.dirent.out_ptr.implementation + ); + + // Structure |stat| + // Same technique + { + let stat = new SharedAll.HollowStructure( + "stat", + Const.OSFILE_SIZEOF_STAT + ); + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_MODE, + "st_mode", + Type.mode_t.implementation + ); + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_UID, + "st_uid", + Type.uid_t.implementation + ); + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_GID, + "st_gid", + Type.gid_t.implementation + ); + + // Here, things get complicated with different data structures. + // Some platforms have |time_t st_atime| and some platforms have + // |timespec st_atimespec|. However, since |timespec| starts with + // a |time_t|, followed by nanoseconds, we just cheat and pretend + // that everybody has |time_t st_atime|, possibly followed by padding + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_ATIME, + "st_atime", + Type.time_t.implementation + ); + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_MTIME, + "st_mtime", + Type.time_t.implementation + ); + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_CTIME, + "st_ctime", + Type.time_t.implementation + ); + + stat.add_field_at( + Const.OSFILE_OFFSETOF_STAT_ST_SIZE, + "st_size", + Type.off_t.implementation + ); + Type.stat = stat.getType(); + } + + // Structure |DIR| + if ("OSFILE_SIZEOF_DIR" in Const) { + // On platforms for which we need to access the fields of DIR + // directly (e.g. because certain functions are implemented + // as macros), we need to define DIR as a hollow structure. + let DIR = new SharedAll.HollowStructure("DIR", Const.OSFILE_SIZEOF_DIR); + + DIR.add_field_at( + Const.OSFILE_OFFSETOF_DIR_DD_FD, + "dd_fd", + Type.fd.implementation + ); + + Type.DIR = DIR.getType(); + } else { + // On other platforms, we keep DIR as a blackbox + Type.DIR = new SharedAll.Type("DIR", ctypes.StructType("DIR")); + } + + Type.null_or_DIR_ptr = Type.DIR.out_ptr.withName("null_or_DIR*"); + Type.null_or_DIR_ptr.importFromC = function importFromC(dir) { + if (dir == null || dir.isNull()) { + return null; + } + return ctypes.CDataFinalizer(dir, SysFile._close_dir); + }; + + // Structure |timeval| + { + let timeval = new SharedAll.HollowStructure( + "timeval", + Const.OSFILE_SIZEOF_TIMEVAL + ); + timeval.add_field_at( + Const.OSFILE_OFFSETOF_TIMEVAL_TV_SEC, + "tv_sec", + Type.long.implementation + ); + timeval.add_field_at( + Const.OSFILE_OFFSETOF_TIMEVAL_TV_USEC, + "tv_usec", + Type.long.implementation + ); + Type.timeval = timeval.getType(); + Type.timevals = new SharedAll.Type( + "two timevals", + ctypes.ArrayType(Type.timeval.implementation, 2) + ); + } + + // Types fsblkcnt_t and fsfilcnt_t, used by structure |statvfs| + Type.fsblkcnt_t = Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName( + "fsblkcnt_t" + ); + // There is no guarantee of the size or order of members in sys-header structs + // It mostly is "unsigned long", but can be "unsigned int" as well. + // So it has its own "type". + // NOTE: This is still only partially correct, as signedness is also not guaranteed, + // so assuming an unsigned int might still be wrong here. + // But unsigned seems to have worked all those years, even though its signed + // on various platforms. + Type.statvfs_f_frsize = Type.uintn_t( + Const.OSFILE_SIZEOF_STATVFS_F_FRSIZE + ).withName("statvfs_f_rsize"); + + // Structure |statvfs| + // Use an hollow structure + { + let statvfs = new SharedAll.HollowStructure( + "statvfs", + Const.OSFILE_SIZEOF_STATVFS + ); + + statvfs.add_field_at( + Const.OSFILE_OFFSETOF_STATVFS_F_FRSIZE, + "f_frsize", + Type.statvfs_f_frsize.implementation + ); + statvfs.add_field_at( + Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL, + "f_bavail", + Type.fsblkcnt_t.implementation + ); + + Type.statvfs = statvfs.getType(); + } + + // Declare libc functions as functions of |OS.Unix.File| + + // Finalizer-related functions + libc.declareLazy( + SysFile, + "_close", + "close", + ctypes.default_abi, + /* return */ ctypes.int, + ctypes.int + ); + + SysFile.close = function close(fd) { + // Detach the finalizer and call |_close|. + return fd.dispose(); + }; + + libc.declareLazy( + SysFile, + "_close_dir", + "closedir", + ctypes.default_abi, + /* return */ ctypes.int, + Type.DIR.in_ptr.implementation + ); + + SysFile.closedir = function closedir(fd) { + // Detach the finalizer and call |_close_dir|. + return fd.dispose(); + }; + + { + // Symbol free() is special. + // We override the definition of free() on several platforms. + let default_lib = new SharedAll.Library("default_lib", "a.out"); + + // On platforms for which we override free(), nspr defines + // a special library name "a.out" that will resolve to the + // correct implementation free(). + // If it turns out we don't have an a.out library or a.out + // doesn't contain free, use the ordinary libc free. + + default_lib.declareLazyWithFallback( + libc, + SysFile, + "free", + "free", + ctypes.default_abi, + /* return*/ ctypes.void_t, + ctypes.voidptr_t + ); + } + + // Other functions + libc.declareLazyFFI( + SysFile, + "access", + "access", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.int + ); + + libc.declareLazyFFI( + SysFile, + "chmod", + "chmod", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.mode_t + ); + + libc.declareLazyFFI( + SysFile, + "chown", + "chown", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.uid_t, + Type.gid_t + ); + + libc.declareLazyFFI( + SysFile, + "copyfile", + "copyfile", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + /* source*/ Type.path, + Type.path, + Type.void_t.in_ptr, + Type.uint32_t + ); + + libc.declareLazyFFI( + SysFile, + "dup", + "dup", + ctypes.default_abi, + /* return*/ Type.negativeone_or_fd, + Type.fd + ); + + if ("OSFILE_SIZEOF_DIR" in Const) { + // On platforms for which |dirfd| is a macro + SysFile.dirfd = function dirfd(DIRp) { + return Type.DIR.in_ptr.implementation(DIRp).contents.dd_fd; + }; + } else { + // On platforms for which |dirfd| is a function + libc.declareLazyFFI( + SysFile, + "dirfd", + "dirfd", + ctypes.default_abi, + /* return*/ Type.negativeone_or_fd, + Type.DIR.in_ptr + ); + } + + libc.declareLazyFFI( + SysFile, + "chdir", + "chdir", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "fchdir", + "fchdir", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd + ); + + libc.declareLazyFFI( + SysFile, + "fchmod", + "fchmod", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd, + Type.mode_t + ); + + libc.declareLazyFFI( + SysFile, + "fchown", + "fchown", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd, + Type.uid_t, + Type.gid_t + ); + + libc.declareLazyFFI( + SysFile, + "fsync", + "fsync", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd + ); + + libc.declareLazyFFI( + SysFile, + "getcwd", + "getcwd", + ctypes.default_abi, + /* return*/ Type.out_path, + Type.out_path, + Type.size_t + ); + + libc.declareLazyFFI( + SysFile, + "getwd", + "getwd", + ctypes.default_abi, + /* return*/ Type.out_path, + Type.out_path + ); + + // Two variants of |getwd| which allocate the memory + // dynamically. + + // Linux/Android version + libc.declareLazyFFI( + SysFile, + "get_current_dir_name", + "get_current_dir_name", + ctypes.default_abi, + /* return*/ Type.out_path.releaseWithLazy(() => SysFile.free) + ); + + // MacOS/BSD version (will return NULL on Linux/Android) + libc.declareLazyFFI( + SysFile, + "getwd_auto", + "getwd", + ctypes.default_abi, + /* return*/ Type.out_path.releaseWithLazy(() => SysFile.free), + Type.void_t.out_ptr + ); + + if (OS.Constants.Sys.Name == "Darwin") { + // At the time of writing we only need this on MacOS. If we generalize + // this, be sure to do so with the other xattr functions also. + libc.declareLazyFFI( + SysFile, + "getxattr", + "getxattr", + ctypes.default_abi, + /* return*/ Type.int, + Type.path, + Type.cstring, + Type.void_t.out_ptr, + Type.size_t, + Type.uint32_t, + Type.int + ); + } + + libc.declareLazyFFI( + SysFile, + "fdatasync", + "fdatasync", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd + ); // Note: MacOS/BSD-specific + + libc.declareLazyFFI( + SysFile, + "ftruncate", + "ftruncate", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.fd, + /* length*/ Type.off_t + ); + + libc.declareLazyFFI( + SysFile, + "lchown", + "lchown", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.uid_t, + Type.gid_t + ); + + libc.declareLazyFFI( + SysFile, + "link", + "link", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + /* source*/ Type.path, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "lseek", + "lseek", + ctypes.default_abi, + /* return*/ Type.off_t, + Type.fd, + /* offset*/ Type.off_t, + /* whence*/ Type.int + ); + + libc.declareLazyFFI( + SysFile, + "mkdir", + "mkdir", + ctypes.default_abi, + /* return*/ Type.int, + /* path*/ Type.path, + /* mode*/ Type.int + ); + + libc.declareLazyFFI( + SysFile, + "mkstemp", + "mkstemp", + ctypes.default_abi, + Type.fd, + /* template*/ Type.out_path + ); + + libc.declareLazyFFI( + SysFile, + "open", + "open", + ctypes.default_abi, + /* return*/ Type.negativeone_or_fd, + Type.path, + /* oflags*/ Type.int, + "..." + ); + + if (OS.Constants.Sys.Name == "NetBSD") { + libc.declareLazyFFI( + SysFile, + "opendir", + "__opendir30", + ctypes.default_abi, + /* return*/ Type.null_or_DIR_ptr, + Type.path + ); + } else { + libc.declareLazyFFI( + SysFile, + "opendir", + "opendir", + ctypes.default_abi, + /* return*/ Type.null_or_DIR_ptr, + Type.path + ); + } + + libc.declareLazyFFI( + SysFile, + "pread", + "pread", + ctypes.default_abi, + /* return*/ Type.negativeone_or_ssize_t, + Type.fd, + Type.void_t.out_ptr, + /* nbytes*/ Type.size_t, + /* offset*/ Type.off_t + ); + + libc.declareLazyFFI( + SysFile, + "pwrite", + "pwrite", + ctypes.default_abi, + /* return*/ Type.negativeone_or_ssize_t, + Type.fd, + Type.void_t.in_ptr, + /* nbytes*/ Type.size_t, + /* offset*/ Type.off_t + ); + + libc.declareLazyFFI( + SysFile, + "read", + "read", + ctypes.default_abi, + /* return*/ Type.negativeone_or_ssize_t, + Type.fd, + Type.void_t.out_ptr, + /* nbytes*/ Type.size_t + ); + + libc.declareLazyFFI( + SysFile, + "posix_fadvise", + "posix_fadvise", + ctypes.default_abi, + /* return*/ Type.int, + Type.fd, + /* offset*/ Type.off_t, + Type.off_t, + /* advise*/ Type.int + ); + + if (Const._DARWIN_INODE64_SYMBOLS) { + // Special case for MacOS X 10.5+ + // Symbol name "readdir" still exists but is used for a + // deprecated function that does not match the + // constants of |Const|. + libc.declareLazyFFI( + SysFile, + "readdir", + "readdir$INODE64", + ctypes.default_abi, + /* return*/ Type.null_or_dirent_ptr, + Type.DIR.in_ptr + ); // For MacOS X + } else if (OS.Constants.Sys.Name == "NetBSD") { + libc.declareLazyFFI( + SysFile, + "readdir", + "__readdir30", + ctypes.default_abi, + /* return*/ Type.null_or_dirent_ptr, + Type.DIR.in_ptr + ); // Other Unices + } else { + libc.declareLazyFFI( + SysFile, + "readdir", + "readdir", + ctypes.default_abi, + /* return*/ Type.null_or_dirent_ptr, + Type.DIR.in_ptr + ); // Other Unices + } + + if (OS.Constants.Sys.Name == "Darwin") { + // At the time of writing we only need this on MacOS. If we generalize + // this, be sure to do so with the other xattr functions also. + libc.declareLazyFFI( + SysFile, + "removexattr", + "removexattr", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.cstring, + Type.int + ); + } + + libc.declareLazyFFI( + SysFile, + "rename", + "rename", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "rmdir", + "rmdir", + ctypes.default_abi, + /* return*/ Type.int, + Type.path + ); + + if (OS.Constants.Sys.Name == "Darwin") { + // At the time of writing we only need this on MacOS. If we generalize + // this, be sure to do so with the other xattr functions also. + libc.declareLazyFFI( + SysFile, + "setxattr", + "setxattr", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.cstring, + Type.void_t.in_ptr, + Type.size_t, + Type.uint32_t, + Type.int + ); + } + + libc.declareLazyFFI( + SysFile, + "splice", + "splice", + ctypes.default_abi, + /* return*/ Type.long, + Type.fd, + /* off_in*/ Type.off_t.in_ptr, + /* fd_out*/ Type.fd, + /* off_out*/ Type.off_t.in_ptr, + Type.size_t, + Type.unsigned_int + ); // Linux/Android-specific + + libc.declareLazyFFI( + SysFile, + "statfs", + "statfs", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.statvfs.out_ptr + ); // Android,B2G + + libc.declareLazyFFI( + SysFile, + "statvfs", + "statvfs", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + Type.statvfs.out_ptr + ); // Other platforms + + libc.declareLazyFFI( + SysFile, + "symlink", + "symlink", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + /* source*/ Type.path, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "truncate", + "truncate", + ctypes.default_abi, + /* return*/ Type.negativeone_or_nothing, + Type.path, + /* length*/ Type.off_t + ); + + libc.declareLazyFFI( + SysFile, + "unlink", + "unlink", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path + ); + + libc.declareLazyFFI( + SysFile, + "write", + "write", + ctypes.default_abi, + /* return */ Type.negativeone_or_ssize_t, + /* fd */ Type.fd, + /* buf */ Type.void_t.in_ptr, + /* nbytes */ Type.size_t + ); + + // Weird cases that require special treatment + + // OSes use a variety of hacks to differentiate between + // 32-bits and 64-bits versions of |stat|, |lstat|, |fstat|. + if (Const._DARWIN_INODE64_SYMBOLS) { + // MacOS X 64-bits + libc.declareLazyFFI( + SysFile, + "stat", + "stat$INODE64", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "lstat", + "lstat$INODE64", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "fstat", + "fstat$INODE64", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.fd, + /* buf */ Type.stat.out_ptr + ); + } else if (Const._STAT_VER != undefined) { + const ver = Const._STAT_VER; + let xstat_name, lxstat_name, fxstat_name; + if (OS.Constants.Sys.Name == "SunOS") { + // Solaris + xstat_name = "_xstat"; + lxstat_name = "_lxstat"; + fxstat_name = "_fxstat"; + } else { + // Linux, all widths + xstat_name = "__xstat"; + lxstat_name = "__lxstat"; + fxstat_name = "__fxstat"; + } + + let Stat = {}; + libc.declareLazyFFI( + Stat, + "xstat", + xstat_name, + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* _stat_ver */ Type.int, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + Stat, + "lxstat", + lxstat_name, + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* _stat_ver */ Type.int, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + Stat, + "fxstat", + fxstat_name, + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* _stat_ver */ Type.int, + /* fd */ Type.fd, + /* buf */ Type.stat.out_ptr + ); + + SysFile.stat = function stat(path, buf) { + return Stat.xstat(ver, path, buf); + }; + + SysFile.lstat = function lstat(path, buf) { + return Stat.lxstat(ver, path, buf); + }; + + SysFile.fstat = function fstat(fd, buf) { + return Stat.fxstat(ver, fd, buf); + }; + } else if (OS.Constants.Sys.Name == "NetBSD") { + // NetBSD 5.0 and newer + libc.declareLazyFFI( + SysFile, + "stat", + "__stat50", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "lstat", + "__lstat50", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "fstat", + "__fstat50", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* fd */ Type.fd, + /* buf */ Type.stat.out_ptr + ); + } else { + // Mac OS X 32-bits, other Unix + libc.declareLazyFFI( + SysFile, + "stat", + "stat", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "lstat", + "lstat", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* buf */ Type.stat.out_ptr + ); + libc.declareLazyFFI( + SysFile, + "fstat", + "fstat", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* fd */ Type.fd, + /* buf */ Type.stat.out_ptr + ); + } + + // We cannot make a C array of CDataFinalizer, so + // pipe cannot be directly defined as a C function. + + let Pipe = {}; + libc.declareLazyFFI( + Pipe, + "_pipe", + "pipe", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* fds */ new SharedAll.Type( + "two file descriptors", + ctypes.ArrayType(ctypes.int, 2) + ) + ); + + // A shared per-thread buffer used to communicate with |pipe| + let _pipebuf = new (ctypes.ArrayType(ctypes.int, 2))(); + + SysFile.pipe = function pipe(array) { + let result = Pipe._pipe(_pipebuf); + if (result == -1) { + return result; + } + array[0] = ctypes.CDataFinalizer(_pipebuf[0], SysFile._close); + array[1] = ctypes.CDataFinalizer(_pipebuf[1], SysFile._close); + return result; + }; + + if (OS.Constants.Sys.Name == "NetBSD") { + libc.declareLazyFFI( + SysFile, + "utimes", + "__utimes50", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* timeval[2] */ Type.timevals.out_ptr + ); + } else { + libc.declareLazyFFI( + SysFile, + "utimes", + "utimes", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* path */ Type.path, + /* timeval[2] */ Type.timevals.out_ptr + ); + } + if (OS.Constants.Sys.Name == "NetBSD") { + libc.declareLazyFFI( + SysFile, + "futimes", + "__futimes50", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* fd */ Type.fd, + /* timeval[2] */ Type.timevals.out_ptr + ); + } else { + libc.declareLazyFFI( + SysFile, + "futimes", + "futimes", + ctypes.default_abi, + /* return */ Type.negativeone_or_nothing, + /* fd */ Type.fd, + /* timeval[2] */ Type.timevals.out_ptr + ); + } + }; + + exports.OS.Unix = { + File: { + _init: init, + }, + }; + })(this); +} diff --git a/toolkit/components/osfile/modules/osfile_unix_front.js b/toolkit/components/osfile/modules/osfile_unix_front.js new file mode 100644 index 000000000000..a2f6c3ce66dc --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_front.js @@ -0,0 +1,1243 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Synchronous front-end for the JavaScript OS.File library. + * Unix implementation. + * + * This front-end is meant to be imported by a worker thread. + */ + +/* eslint-env mozilla/chrome-worker, node */ +/* global OS */ + +// eslint-disable-next-line no-lone-blocks +{ + if (typeof Components != "undefined") { + // We do not wish osfile_unix_front.js to be used directly as a main thread + // module yet. + + throw new Error( + "osfile_unix_front.js cannot be used from the main thread yet" + ); + } + (function(exports) { + "use strict"; + + // exports.OS.Unix is created by osfile_unix_back.js + if (exports.OS && exports.OS.File) { + return; // Avoid double-initialization + } + + let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + let Path = require("resource://gre/modules/osfile/ospath.jsm"); + let SysAll = require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm"); + exports.OS.Unix.File._init(); + SharedAll.LOG.bind(SharedAll, "Unix front-end"); + let Const = SharedAll.Constants.libc; + let UnixFile = exports.OS.Unix.File; + let Type = UnixFile.Type; + + /** + * Representation of a file. + * + * You generally do not need to call this constructor yourself. Rather, + * to open a file, use function |OS.File.open|. + * + * @param fd A OS-specific file descriptor. + * @param {string} path File path of the file handle, used for error-reporting. + * @constructor + */ + let File = function File(fd, path) { + exports.OS.Shared.AbstractFile.call(this, fd, path); + this._closeResult = null; + }; + File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype); + + /** + * Close the file. + * + * This method has no effect if the file is already closed. However, + * if the first call to |close| has thrown an error, further calls + * will throw the same error. + * + * @throws File.Error If closing the file revealed an error that could + * not be reported earlier. + */ + File.prototype.close = function close() { + if (this._fd) { + let fd = this._fd; + this._fd = null; + // Call |close(fd)|, detach finalizer if any + // (|fd| may not be a CDataFinalizer if it has been + // instantiated from a controller thread). + let result = UnixFile._close(fd); + if (typeof fd == "object" && "forget" in fd) { + fd.forget(); + } + if (result == -1) { + this._closeResult = new File.Error("close", ctypes.errno, this._path); + } + } + if (this._closeResult) { + throw this._closeResult; + } + }; + + /** + * Read some bytes from a file. + * + * @param {C pointer} buffer A buffer for holding the data + * once it is read. + * @param {number} nbytes The number of bytes to read. It must not + * exceed the size of |buffer| in bytes but it may exceed the number + * of bytes unread in the file. + * @param {*=} options Additional options for reading. Ignored in + * this implementation. + * + * @return {number} The number of bytes effectively read. If zero, + * the end of the file has been reached. + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype._read = function _read(buffer, nbytes, options = {}) { + // Populate the page cache with data from a file so the subsequent reads + // from that file will not block on disk I/O. + if ( + typeof UnixFile.posix_fadvise === "function" && + (options.sequential || !("sequential" in options)) + ) { + UnixFile.posix_fadvise( + this.fd, + 0, + nbytes, + OS.Constants.libc.POSIX_FADV_SEQUENTIAL + ); + } + return throw_on_negative( + "read", + UnixFile.read(this.fd, buffer, nbytes), + this._path + ); + }; + + /** + * Write some bytes to a file. + * + * @param {Typed array} buffer A buffer holding the data that must be + * written. + * @param {number} nbytes The number of bytes to write. It must not + * exceed the size of |buffer| in bytes. + * @param {*=} options Additional options for writing. Ignored in + * this implementation. + * + * @return {number} The number of bytes effectively written. + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype._write = function _write(buffer, nbytes, options = {}) { + return throw_on_negative( + "write", + UnixFile.write(this.fd, buffer, nbytes), + this._path + ); + }; + + /** + * Return the current position in the file. + */ + File.prototype.getPosition = function getPosition(pos) { + return this.setPosition(0, File.POS_CURRENT); + }; + + /** + * Change the current position in the file. + * + * @param {number} pos The new position. Whether this position + * is considered from the current position, from the start of + * the file or from the end of the file is determined by + * argument |whence|. Note that |pos| may exceed the length of + * the file. + * @param {number=} whence The reference position. If omitted + * or |OS.File.POS_START|, |pos| is relative to the start of the + * file. If |OS.File.POS_CURRENT|, |pos| is relative to the + * current position in the file. If |OS.File.POS_END|, |pos| is + * relative to the end of the file. + * + * @return The new position in the file. + */ + File.prototype.setPosition = function setPosition(pos, whence) { + if (whence === undefined) { + whence = Const.SEEK_SET; + } + return throw_on_negative( + "setPosition", + UnixFile.lseek(this.fd, pos, whence), + this._path + ); + }; + + /** + * Fetch the information on the file. + * + * @return File.Info The information on |this| file. + */ + File.prototype.stat = function stat() { + throw_on_negative( + "stat", + UnixFile.fstat(this.fd, gStatDataPtr), + this._path + ); + return new File.Info(gStatData, this._path); + }; + + /** + * Set the file's access permissions. + * + * This operation is likely to fail if applied to a file that was + * not created by the currently running program (more precisely, + * if it was created by a program running under a different OS-level + * user account). It may also fail, or silently do nothing, if the + * filesystem containing the file does not support access permissions. + * + * @param {*=} options Object specifying the requested permissions: + * + * - {number} unixMode The POSIX file mode to set on the file. If omitted, + * the POSIX file mode is reset to the default used by |OS.file.open|. If + * specified, the permissions will respect the process umask as if they + * had been specified as arguments of |OS.File.open|, unless the + * |unixHonorUmask| parameter tells otherwise. + * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is + * modified by the process umask, as |OS.File.open| would have done. If + * false, the exact value of |unixMode| will be applied. + */ + File.prototype.setPermissions = function setPermissions(options = {}) { + throw_on_negative( + "setPermissions", + UnixFile.fchmod(this.fd, unixMode(options)), + this._path + ); + }; + + /** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * WARNING: This method is not implemented on Android/B2G. On Android/B2G, + * you should use File.setDates instead. + * + * @param {Date,number=} accessDate The last access date. If numeric, + * milliseconds since epoch. If omitted or null, then the current date + * will be used. + * @param {Date,number=} modificationDate The last modification date. If + * numeric, milliseconds since epoch. If omitted or null, then the current + * date will be used. + * + * @throws {TypeError} In case of invalid parameters. + * @throws {OS.File.Error} In case of I/O error. + */ + if (SharedAll.Constants.Sys.Name != "Android") { + File.prototype.setDates = function(accessDate, modificationDate) { + let { /* value, */ ptr } = datesToTimevals( + accessDate, + modificationDate + ); + throw_on_negative( + "setDates", + UnixFile.futimes(this.fd, ptr), + this._path + ); + }; + } + + /** + * Flushes the file's buffers and causes all buffered data + * to be written. + * Disk flushes are very expensive and therefore should be used carefully, + * sparingly and only in scenarios where it is vital that data survives + * system crashes. Even though the function will be executed off the + * main-thread, it might still affect the overall performance of any + * running application. + * + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype.flush = function flush() { + throw_on_negative("flush", UnixFile.fsync(this.fd), this._path); + }; + + // The default unix mode for opening (0600) + const DEFAULT_UNIX_MODE = 384; + + /** + * Open a file + * + * @param {string} path The path to the file. + * @param {*=} mode The opening mode for the file, as + * an object that may contain the following fields: + * + * - {bool} truncate If |true|, the file will be opened + * for writing. If the file does not exist, it will be + * created. If the file exists, its contents will be + * erased. Cannot be specified with |create|. + * - {bool} create If |true|, the file will be opened + * for writing. If the file exists, this function fails. + * If the file does not exist, it will be created. Cannot + * be specified with |truncate| or |existing|. + * - {bool} existing. If the file does not exist, this function + * fails. Cannot be specified with |create|. + * - {bool} read If |true|, the file will be opened for + * reading. The file may also be opened for writing, depending + * on the other fields of |mode|. + * - {bool} write If |true|, the file will be opened for + * writing. The file may also be opened for reading, depending + * on the other fields of |mode|. + * - {bool} append If |true|, the file will be opened for appending, + * meaning the equivalent of |.setPosition(0, POS_END)| is executed + * before each write. The default is |true|, i.e. opening a file for + * appending. Specify |append: false| to open the file in regular mode. + * + * If neither |truncate|, |create| or |write| is specified, the file + * is opened for reading. + * + * Note that |false|, |null| or |undefined| flags are simply ignored. + * + * @param {*=} options Additional options for file opening. This + * implementation interprets the following fields: + * + * - {number} unixFlags If specified, file opening flags, as + * per libc function |open|. Replaces |mode|. + * - {number} unixMode If specified, a file creation mode, + * as per libc function |open|. If unspecified, files are + * created with a default mode of 0600 (file is private to the + * user, the user can read and write). + * + * @return {File} A file object. + * @throws {OS.File.Error} If the file could not be opened. + */ + File.open = function Unix_open(path, mode, options = {}) { + // We don't need to filter for the umask because "open" does this for us. + let omode = + options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE; + let flags; + if (options.unixFlags !== undefined) { + flags = options.unixFlags; + } else { + mode = OS.Shared.AbstractFile.normalizeOpenMode(mode); + // Handle read/write + if (!mode.write) { + flags = Const.O_RDONLY; + } else if (mode.read) { + flags = Const.O_RDWR; + } else { + flags = Const.O_WRONLY; + } + // Finally, handle create/existing/trunc + if (mode.trunc) { + if (mode.existing) { + flags |= Const.O_TRUNC; + } else { + flags |= Const.O_CREAT | Const.O_TRUNC; + } + } else if (mode.create) { + flags |= Const.O_CREAT | Const.O_EXCL; + } else if (mode.read && !mode.write) { + // flags are sufficient + } else if (!mode.existing) { + flags |= Const.O_CREAT; + } + if (mode.append) { + flags |= Const.O_APPEND; + } + } + return error_or_file(UnixFile.open(path, flags, ctypes.int(omode)), path); + }; + + /** + * Checks if a file exists + * + * @param {string} path The path to the file. + * + * @return {bool} true if the file exists, false otherwise. + */ + File.exists = function Unix_exists(path) { + if (UnixFile.access(path, Const.F_OK) == -1) { + return false; + } + return true; + }; + + /** + * Remove an existing file. + * + * @param {string} path The name of the file. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the file does + * not exist. |true| by default. + * + * @throws {OS.File.Error} In case of I/O error. + */ + File.remove = function remove(path, options = {}) { + let result = UnixFile.unlink(path); + if (result == -1) { + if ( + (!("ignoreAbsent" in options) || options.ignoreAbsent) && + ctypes.errno == Const.ENOENT + ) { + return; + } + throw new File.Error("remove", ctypes.errno, path); + } + }; + + /** + * Remove an empty directory. + * + * @param {string} path The name of the directory to remove. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the directory + * does not exist. |true| by default + */ + File.removeEmptyDir = function removeEmptyDir(path, options = {}) { + let result = UnixFile.rmdir(path); + if (result == -1) { + if ( + (!("ignoreAbsent" in options) || options.ignoreAbsent) && + ctypes.errno == Const.ENOENT + ) { + return; + } + throw new File.Error("removeEmptyDir", ctypes.errno, path); + } + }; + + /** + * Default mode for opening directories. + */ + const DEFAULT_UNIX_MODE_DIR = Const.S_IRWXU; + + /** + * Create a directory. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. This + * implementation interprets the following fields: + * + * - {number} unixMode If specified, a file creation mode, + * as per libc function |mkdir|. If unspecified, dirs are + * created with a default mode of 0700 (dir is private to + * the user, the user can read, write and execute). + * - {bool} ignoreExisting If |false|, throw error if the directory + * already exists. |true| by default + * - {string} from If specified, the call to |makeDir| creates all the + * ancestors of |path| that are descendants of |from|. Note that |from| + * and its existing descendants must be user-writeable and that |path| + * must be a descendant of |from|. + * Example: + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); + * creates directories profileDir/foo, profileDir/foo/bar + */ + File._makeDir = function makeDir(path, options = {}) { + let omode = + options.unixMode !== undefined + ? options.unixMode + : DEFAULT_UNIX_MODE_DIR; + let result = UnixFile.mkdir(path, omode); + if (result == -1) { + if ( + (!("ignoreExisting" in options) || options.ignoreExisting) && + (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR) + ) { + return; + } + throw new File.Error("makeDir", ctypes.errno, path); + } + }; + + /** + * Copy a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be copied. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If set, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * + * @throws {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be copied with the file. The + * behavior may not be the same across all platforms. + */ + File.copy = null; + + /** + * Move a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be moved. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If set, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * @option {bool} noCopy - If set, this function will fail if the + * operation is more sophisticated than a simple renaming, i.e. if + * |sourcePath| and |destPath| are not situated on the same device. + * + * @throws {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be moved with the file. The + * behavior may not be the same across all platforms. + */ + File.move = null; + + if (UnixFile.copyfile) { + // This implementation uses |copyfile(3)|, from the BSD library. + // Adding copying of hierarchies and/or attributes is just a flag + // away. + File.copy = function copyfile(sourcePath, destPath, options = {}) { + let flags = Const.COPYFILE_DATA; + if (options.noOverwrite) { + flags |= Const.COPYFILE_EXCL; + } + throw_on_negative( + "copy", + UnixFile.copyfile(sourcePath, destPath, null, flags), + sourcePath + ); + }; + } else { + // If the OS does not implement file copying for us, we need to + // implement it ourselves. For this purpose, we need to define + // a pumping function. + + /** + * Copy bytes from one file to another one. + * + * @param {File} source The file containing the data to be copied. It + * should be opened for reading. + * @param {File} dest The file to which the data should be written. It + * should be opened for writing. + * @param {*=} options An object which may contain the following fields: + * + * @option {number} nbytes The maximal number of bytes to + * copy. If unspecified, copy everything from the current + * position. + * @option {number} bufSize A hint regarding the size of the + * buffer to use for copying. The implementation may decide to + * ignore this hint. + * @option {bool} unixUserland Will force the copy operation to be + * caried out in user land, instead of using optimized syscalls such + * as splice(2). + * + * @throws {OS.File.Error} In case of error. + */ + let pump; + + // A buffer used by |pump_userland| + let pump_buffer = null; + + // An implementation of |pump| using |read|/|write| + let pump_userland = function pump_userland(source, dest, options = {}) { + let bufSize = options.bufSize > 0 ? options.bufSize : 4096; + let nbytes = options.nbytes > 0 ? options.nbytes : Infinity; + if (!pump_buffer || pump_buffer.length < bufSize) { + pump_buffer = new (ctypes.ArrayType(ctypes.char))(bufSize); + } + let read = source._read.bind(source); + let write = dest._write.bind(dest); + // Perform actual copy + let total_read = 0; + while (true) { + let bytes_just_read = read(pump_buffer, bufSize); + if (bytes_just_read == 0) { + return total_read; + } + total_read += bytes_just_read; + let bytes_written = 0; + do { + bytes_written += write( + pump_buffer.addressOfElement(bytes_written), + bytes_just_read - bytes_written + ); + } while (bytes_written < bytes_just_read); + nbytes -= bytes_written; + if (nbytes <= 0) { + return total_read; + } + } + }; + + // Fortunately, under Linux, that pumping function can be optimized. + if (UnixFile.splice) { + const BUFSIZE = 1 << 17; + + // An implementation of |pump| using |splice| (for Linux/Android) + pump = function pump_splice(source, dest, options = {}) { + let nbytes = options.nbytes > 0 ? options.nbytes : Infinity; + let pipe = []; + throw_on_negative("pump", UnixFile.pipe(pipe)); + let pipe_read = pipe[0]; + let pipe_write = pipe[1]; + let source_fd = source.fd; + let dest_fd = dest.fd; + let total_read = 0; + let total_written = 0; + try { + while (true) { + let chunk_size = Math.min(nbytes, BUFSIZE); + let bytes_read = throw_on_negative( + "pump", + UnixFile.splice( + source_fd, + null, + pipe_write, + null, + chunk_size, + 0 + ) + ); + if (!bytes_read) { + break; + } + total_read += bytes_read; + let bytes_written = throw_on_negative( + "pump", + UnixFile.splice( + pipe_read, + null, + dest_fd, + null, + bytes_read, + bytes_read == chunk_size ? Const.SPLICE_F_MORE : 0 + ) + ); + if (!bytes_written) { + // This should never happen + throw new Error("Internal error: pipe disconnected"); + } + total_written += bytes_written; + nbytes -= bytes_read; + if (!nbytes) { + break; + } + } + return total_written; + } catch (x) { + if (x.unixErrno == Const.EINVAL) { + // We *might* be on a file system that does not support splice. + // Try again with a fallback pump. + if (total_read) { + source.setPosition(-total_read, File.POS_CURRENT); + } + if (total_written) { + dest.setPosition(-total_written, File.POS_CURRENT); + } + return pump_userland(source, dest, options); + } + throw x; + } finally { + pipe_read.dispose(); + pipe_write.dispose(); + } + }; + } else { + // Fallback implementation of pump for other Unix platforms. + pump = pump_userland; + } + + // Implement |copy| using |pump|. + // This implementation would require some work before being able to + // copy directories + File.copy = function copy(sourcePath, destPath, options = {}) { + let source, dest; + try { + source = File.open(sourcePath); + // Need to open the output file with |append:false|, or else |splice| + // won't work. + if (options.noOverwrite) { + dest = File.open(destPath, { create: true, append: false }); + } else { + dest = File.open(destPath, { trunc: true, append: false }); + } + if (options.unixUserland) { + pump_userland(source, dest, options); + } else { + pump(source, dest, options); + } + } catch (x) { + if (dest) { + dest.close(); + } + if (source) { + source.close(); + } + throw x; + } + }; + } // End of definition of copy + + // Implement |move| using |rename| (wherever possible) or |copy| + // (if files are on distinct devices). + File.move = function move(sourcePath, destPath, options = {}) { + // An implementation using |rename| whenever possible or + // |File.pump| when required, for other Unices. + // It can move directories on one file system, not + // across file systems + + // If necessary, fail if the destination file exists + if (options.noOverwrite) { + let fd = UnixFile.open(destPath, Const.O_RDONLY); + if (fd != -1) { + fd.dispose(); + // The file exists and we have access + throw new File.Error("move", Const.EEXIST, sourcePath); + } else if (ctypes.errno == Const.EACCESS) { + // The file exists and we don't have access + throw new File.Error("move", Const.EEXIST, sourcePath); + } + } + + // If we can, rename the file. + let result = UnixFile.rename(sourcePath, destPath); + if (result != -1) { + // Succeeded. + return; + } + + // In some cases, we cannot rename, e.g. because we're crossing + // devices. In such cases, if permitted, we'll need to copy then + // erase the original. + if (options.noCopy) { + throw new File.Error("move", ctypes.errno, sourcePath); + } + + File.copy(sourcePath, destPath, options); + // Note that we do not attempt to clean-up in case of copy error. + // I'm sure that there are edge cases in which this could end up + // removing an important file by accident. I'd rather leave + // a file lying around by error than removing a critical file. + + File.remove(sourcePath); + }; + + File.unixSymLink = function unixSymLink(sourcePath, destPath) { + throw_on_negative( + "symlink", + UnixFile.symlink(sourcePath, destPath), + sourcePath + ); + }; + + /** + * Iterate on one directory. + * + * This iterator will not enter subdirectories. + * + * @param {string} path The directory upon which to iterate. + * @param {*=} options Ignored in this implementation. + * + * @throws {File.Error} If |path| does not represent a directory or + * if the directory cannot be iterated. + * @constructor + */ + File.DirectoryIterator = function DirectoryIterator(path, options) { + exports.OS.Shared.AbstractFile.AbstractIterator.call(this); + this._path = path; + this._dir = UnixFile.opendir(this._path); + if (this._dir == null) { + let error = ctypes.errno; + if (error != Const.ENOENT) { + throw new File.Error("DirectoryIterator", error, path); + } + this._exists = false; + this._closed = true; + } else { + this._exists = true; + this._closed = false; + } + }; + File.DirectoryIterator.prototype = Object.create( + exports.OS.Shared.AbstractFile.AbstractIterator.prototype + ); + + /** + * Return the next entry in the directory, if any such entry is + * available. + * + * Skip special directories "." and "..". + * + * @return By definition of the iterator protocol, either + * `{value: {File.Entry}, done: false}` if there is an unvisited entry + * in the directory, or `{value: undefined, done: true}`, otherwise. + */ + File.DirectoryIterator.prototype.next = function next() { + if (!this._exists) { + throw File.Error.noSuchFile( + "DirectoryIterator.prototype.next", + this._path + ); + } + if (this._closed) { + return { value: undefined, done: true }; + } + for ( + let entry = UnixFile.readdir(this._dir); + entry != null && !entry.isNull(); + entry = UnixFile.readdir(this._dir) + ) { + let contents = entry.contents; + let name = contents.d_name.readString(); + if (name == "." || name == "..") { + continue; + } + + let isDir, isSymLink; + if ( + !("d_type" in contents) || + !("DT_UNKNOWN" in Const) || + contents.d_type == Const.DT_UNKNOWN + ) { + // File type information is not available in d_type. The cases are: + // 1. |dirent| doesn't have d_type on some platforms (e.g. Solaris). + // 2. DT_UNKNOWN and other DT_ constants are not defined. + // 3. d_type is set to unknown (e.g. not supported by the + // filesystem). + let path = Path.join(this._path, name); + throw_on_negative( + "lstat", + UnixFile.lstat(path, gStatDataPtr), + this._path + ); + isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR; + isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK; + } else { + isDir = contents.d_type == Const.DT_DIR; + isSymLink = contents.d_type == Const.DT_LNK; + } + + return { + value: new File.DirectoryIterator.Entry( + isDir, + isSymLink, + name, + this._path + ), + done: false, + }; + } + this.close(); + return { value: undefined, done: true }; + }; + + /** + * Close the iterator and recover all resources. + * You should call this once you have finished iterating on a directory. + */ + File.DirectoryIterator.prototype.close = function close() { + if (this._closed) { + return; + } + this._closed = true; + UnixFile.closedir(this._dir); + this._dir = null; + }; + + /** + * Determine whether the directory exists. + * + * @return {boolean} + */ + File.DirectoryIterator.prototype.exists = function exists() { + return this._exists; + }; + + /** + * Return directory as |File| + */ + File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() { + if (!this._dir) { + throw File.Error.closed("unixAsFile", this._path); + } + return error_or_file(UnixFile.dirfd(this._dir), this._path); + }; + + /** + * An entry in a directory. + */ + File.DirectoryIterator.Entry = function Entry( + isDir, + isSymLink, + name, + parent + ) { + // Copy the relevant part of |unix_entry| to ensure that + // our data is not overwritten prematurely. + this._parent = parent; + let path = Path.join(this._parent, name); + + SysAll.AbstractEntry.call(this, isDir, isSymLink, name, path); + }; + File.DirectoryIterator.Entry.prototype = Object.create( + SysAll.AbstractEntry.prototype + ); + + /** + * Return a version of an instance of + * File.DirectoryIterator.Entry that can be sent from a worker + * thread to the main thread. Note that deserialization is + * asymmetric and returns an object with a different + * implementation. + */ + File.DirectoryIterator.Entry.toMsg = function toMsg(value) { + if (!(value instanceof File.DirectoryIterator.Entry)) { + throw new TypeError( + "parameter of " + + "File.DirectoryIterator.Entry.toMsg must be a " + + "File.DirectoryIterator.Entry" + ); + } + let serialized = {}; + for (let key in File.DirectoryIterator.Entry.prototype) { + serialized[key] = value[key]; + } + return serialized; + }; + + let gStatData = new Type.stat.implementation(); + let gStatDataPtr = gStatData.address(); + + let MODE_MASK = 4095; /* = 07777*/ + File.Info = function Info(stat, path) { + let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR; + let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK; + let size = Type.off_t.importFromC(stat.st_size); + + let lastAccessDate = new Date(stat.st_atime * 1000); + let lastModificationDate = new Date(stat.st_mtime * 1000); + let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000); + + let unixOwner = Type.uid_t.importFromC(stat.st_uid); + let unixGroup = Type.gid_t.importFromC(stat.st_gid); + let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK); + + SysAll.AbstractInfo.call( + this, + path, + isDir, + isSymLink, + size, + lastAccessDate, + lastModificationDate, + unixLastStatusChangeDate, + unixOwner, + unixGroup, + unixMode + ); + }; + File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype); + + /** + * Return a version of an instance of File.Info that can be sent + * from a worker thread to the main thread. Note that deserialization + * is asymmetric and returns an object with a different implementation. + */ + File.Info.toMsg = function toMsg(stat) { + if (!(stat instanceof File.Info)) { + throw new TypeError("parameter of File.Info.toMsg must be a File.Info"); + } + let serialized = {}; + for (let key in File.Info.prototype) { + serialized[key] = stat[key]; + } + return serialized; + }; + + /** + * Fetch the information on a file. + * + * @param {string} path The full name of the file to open. + * @param {*=} options Additional options. In this implementation: + * + * - {bool} unixNoFollowingLinks If set and |true|, if |path| + * represents a symbolic link, the call will return the information + * of the link itself, rather than that of the target file. + * + * @return {File.Information} + */ + File.stat = function stat(path, options = {}) { + if (options.unixNoFollowingLinks) { + throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path); + } else { + throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path); + } + return new File.Info(gStatData, path); + }; + + /** + * Set the file's access permissions. + * + * This operation is likely to fail if applied to a file that was + * not created by the currently running program (more precisely, + * if it was created by a program running under a different OS-level + * user account). It may also fail, or silently do nothing, if the + * filesystem containing the file does not support access permissions. + * + * @param {string} path The name of the file to reset the permissions of. + * @param {*=} options Object specifying the requested permissions: + * + * - {number} unixMode The POSIX file mode to set on the file. If omitted, + * the POSIX file mode is reset to the default used by |OS.file.open|. If + * specified, the permissions will respect the process umask as if they + * had been specified as arguments of |OS.File.open|, unless the + * |unixHonorUmask| parameter tells otherwise. + * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is + * modified by the process umask, as |OS.File.open| would have done. If + * false, the exact value of |unixMode| will be applied. + */ + File.setPermissions = function setPermissions(path, options = {}) { + throw_on_negative( + "setPermissions", + UnixFile.chmod(path, unixMode(options)), + path + ); + }; + + /** + * Convert an access date and a modification date to an array + * of two |timeval|. + */ + function datesToTimevals(accessDate, modificationDate) { + accessDate = normalizeDate("File.setDates", accessDate); + modificationDate = normalizeDate("File.setDates", modificationDate); + + let timevals = new Type.timevals.implementation(); + let timevalsPtr = timevals.address(); + + // JavaScript date values are expressed in milliseconds since epoch. + // Split this up into second and microsecond components. + timevals[0].tv_sec = (accessDate / 1000) | 0; + timevals[0].tv_usec = ((accessDate % 1000) * 1000) | 0; + timevals[1].tv_sec = (modificationDate / 1000) | 0; + timevals[1].tv_usec = ((modificationDate % 1000) * 1000) | 0; + + return { value: timevals, ptr: timevalsPtr }; + } + + /** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * @param {string} path The full name of the file to set the dates for. + * @param {Date,number=} accessDate The last access date. If numeric, + * milliseconds since epoch. If omitted or null, then the current date + * will be used. + * @param {Date,number=} modificationDate The last modification date. If + * numeric, milliseconds since epoch. If omitted or null, then the current + * date will be used. + * + * @throws {TypeError} In case of invalid paramters. + * @throws {OS.File.Error} In case of I/O error. + */ + File.setDates = function setDates(path, accessDate, modificationDate) { + let { /* value, */ ptr } = datesToTimevals(accessDate, modificationDate); + throw_on_negative("setDates", UnixFile.utimes(path, ptr), path); + }; + + File.read = exports.OS.Shared.AbstractFile.read; + File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic; + File.openUnique = exports.OS.Shared.AbstractFile.openUnique; + File.makeDir = exports.OS.Shared.AbstractFile.makeDir; + + /** + * Remove an existing directory and its contents. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't + * exist. |true| by default. + * - {boolean} ignorePermissions If |true|, remove the file even when lacking write + * permission. + * + * @throws {OS.File.Error} In case of I/O error, in particular if |path| is + * not a directory. + * + * Note: This function will remove a symlink even if it points a directory. + */ + File.removeDir = function(path, options = {}) { + let isSymLink; + try { + let info = File.stat(path, { unixNoFollowingLinks: true }); + isSymLink = info.isSymLink; + } catch (e) { + if ( + (!("ignoreAbsent" in options) || options.ignoreAbsent) && + ctypes.errno == Const.ENOENT + ) { + return; + } + throw e; + } + if (isSymLink) { + // A Unix symlink itself is not a directory even if it points + // a directory. + File.remove(path, options); + return; + } + exports.OS.Shared.AbstractFile.removeRecursive(path, options); + }; + + /** + * Get the current directory by getCurrentDirectory. + */ + File.getCurrentDirectory = function getCurrentDirectory() { + let path, buf; + if (UnixFile.get_current_dir_name) { + path = UnixFile.get_current_dir_name(); + } else if (UnixFile.getwd_auto) { + path = UnixFile.getwd_auto(null); + } else { + for (let length = Const.PATH_MAX; !path; length *= 2) { + buf = new (ctypes.char.array(length))(); + path = UnixFile.getcwd(buf, length); + } + } + throw_on_null("getCurrentDirectory", path); + return path.readString(); + }; + + /** + * Get/set the current directory. + */ + Object.defineProperty(File, "curDir", { + set(path) { + this.setCurrentDirectory(path); + }, + get() { + return this.getCurrentDirectory(); + }, + }); + + // Utility functions + + /** + * Turn the result of |open| into an Error or a File + * @param {number} maybe The result of the |open| operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.errno, otherwise it returns the opened file. + * @param {string=} path The path of the file. + */ + function error_or_file(maybe, path) { + if (maybe == -1) { + throw new File.Error("open", ctypes.errno, path); + } + return new File(maybe, path); + } + + /** + * Utility function to sort errors represented as "-1" from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {number} result The result of the operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.errno, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_negative(operation, result, path) { + if (result < 0) { + throw new File.Error(operation, ctypes.errno, path); + } + return result; + } + + /** + * Utility function to sort errors represented as |null| from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {pointer} result The result of the operation that may + * represent either an error or a success. If |null|, this function raises + * an error holding ctypes.errno, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_null(operation, result, path) { + if (result == null || (result.isNull && result.isNull())) { + throw new File.Error(operation, ctypes.errno, path); + } + return result; + } + + /** + * Normalize and verify a Date or numeric date value. + * + * @param {string} fn Function name of the calling function. + * @param {Date,number} date The date to normalize. If omitted or null, + * then the current date will be used. + * + * @throws {TypeError} Invalid date provided. + * + * @return {number} Sanitized, numeric date in milliseconds since epoch. + */ + function normalizeDate(fn, date) { + if (typeof date !== "number" && !date) { + // |date| was Omitted or null. + date = Date.now(); + } else if (typeof date.getTime === "function") { + // Input might be a date or date-like object. + date = date.getTime(); + } + + if (typeof date !== "number" || Number.isNaN(date)) { + throw new TypeError( + "|date| parameter of " + + fn + + " must be a " + + "|Date| instance or number" + ); + } + return date; + } + + /** + * Helper used by both versions of setPermissions. + */ + function unixMode(options) { + let mode = + options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE; + let unixHonorUmask = true; + if ("unixHonorUmask" in options) { + unixHonorUmask = options.unixHonorUmask; + } + if (unixHonorUmask) { + mode &= ~SharedAll.Constants.Sys.umask; + } + return mode; + } + + File.Unix = exports.OS.Unix.File; + File.Error = SysAll.Error; + exports.OS.File = File; + exports.OS.Shared.Type = Type; + + Object.defineProperty(File, "POS_START", { value: SysAll.POS_START }); + Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT }); + Object.defineProperty(File, "POS_END", { value: SysAll.POS_END }); + })(this); +} diff --git a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm new file mode 100644 index 000000000000..3695c28c03f2 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm @@ -0,0 +1,444 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This module defines the thread-agnostic components of the Win version + * of OS.File. It depends on the thread-agnostic cross-platform components + * of OS.File. + * + * It serves the following purposes: + * - open kernel32; + * - define OS.Shared.Win.Error; + * - define a few constants and types that need to be defined on all platforms. + * + * This module can be: + * - opened from the main thread as a jsm module; + * - opened from a chrome worker through require(). + */ + +/* eslint-env node */ + +"use strict"; + +var SharedAll; +if (typeof Components != "undefined") { + // Module is opened as a jsm module + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + // eslint-disable-next-line mozilla/reject-global-this + this.ctypes = ctypes; + + SharedAll = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" + ); + // eslint-disable-next-line mozilla/reject-global-this + this.exports = {}; +} else if (typeof module != "undefined" && typeof require != "undefined") { + // Module is loaded with require() + SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); +} else { + throw new Error( + "Please open this module with Component.utils.import or with require()" + ); +} + +SharedAll.LOG.bind(SharedAll, "Win", "allthreads"); +var Const = SharedAll.Constants.Win; + +// Open libc +var libc = new SharedAll.Library("libc", "kernel32.dll"); +exports.libc = libc; + +// Define declareFFI +var declareFFI = SharedAll.declareFFI.bind(null, libc); +exports.declareFFI = declareFFI; + +var Scope = {}; + +// Define Error +libc.declareLazy( + Scope, + "FormatMessage", + "FormatMessageW", + ctypes.winapi_abi, + /* return*/ ctypes.uint32_t, + ctypes.uint32_t, + /* source*/ ctypes.voidptr_t, + ctypes.uint32_t, + /* langid*/ ctypes.uint32_t, + ctypes.char16_t.ptr, + ctypes.uint32_t, + /* Arguments*/ ctypes.voidptr_t +); + +/** + * A File-related error. + * + * To obtain a human-readable error message, use method |toString|. + * To determine the cause of the error, use the various |becauseX| + * getters. To determine the operation that failed, use field + * |operation|. + * + * Additionally, this implementation offers a field + * |winLastError|, which holds the OS-specific error + * constant. If you need this level of detail, you may match the value + * of this field against the error constants of |OS.Constants.Win|. + * + * @param {string=} operation The operation that failed. If unspecified, + * the name of the calling function is taken to be the operation that + * failed. + * @param {number=} lastError The OS-specific constant detailing the + * reason of the error. If unspecified, this is fetched from the system + * status. + * @param {string=} path The file path that manipulated. If unspecified, + * assign the empty string. + * + * @constructor + * @extends {OS.Shared.Error} + */ +var OSError = function OSError( + operation = "unknown operation", + lastError = ctypes.winLastError, + path = "" +) { + SharedAll.OSError.call(this, operation, path); + this.winLastError = lastError; +}; +OSError.prototype = Object.create(SharedAll.OSError.prototype); +OSError.prototype.toString = function toString() { + let buf = new (ctypes.ArrayType(ctypes.char16_t, 1024))(); + let result = Scope.FormatMessage( + Const.FORMAT_MESSAGE_FROM_SYSTEM | Const.FORMAT_MESSAGE_IGNORE_INSERTS, + null, + /* The error number */ this.winLastError, + /* Default language */ 0, + buf, + /* Minimum size of buffer */ 1024, + null + ); + if (!result) { + buf = + "additional error " + + ctypes.winLastError + + " while fetching system error message"; + } + return ( + "Win error " + + this.winLastError + + " during operation " + + this.operation + + (this.path ? " on file " + this.path : "") + + " (" + + buf.readString() + + ")" + ); +}; +OSError.prototype.toMsg = function toMsg() { + return OSError.toMsg(this); +}; + +/** + * |true| if the error was raised because a file or directory + * already exists, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseExists", { + get: function becauseExists() { + return ( + this.winLastError == Const.ERROR_FILE_EXISTS || + this.winLastError == Const.ERROR_ALREADY_EXISTS + ); + }, +}); +/** + * |true| if the error was raised because a file or directory + * does not exist, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseNoSuchFile", { + get: function becauseNoSuchFile() { + return ( + this.winLastError == Const.ERROR_FILE_NOT_FOUND || + this.winLastError == Const.ERROR_PATH_NOT_FOUND + ); + }, +}); +/** + * |true| if the error was raised because a directory is not empty + * does not exist, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseNotEmpty", { + get: function becauseNotEmpty() { + return this.winLastError == Const.ERROR_DIR_NOT_EMPTY; + }, +}); +/** + * |true| if the error was raised because a file or directory + * is closed, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseClosed", { + get: function becauseClosed() { + return this.winLastError == Const.ERROR_INVALID_HANDLE; + }, +}); +/** + * |true| if the error was raised because permission is denied to + * access a file or directory, |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseAccessDenied", { + get: function becauseAccessDenied() { + return this.winLastError == Const.ERROR_ACCESS_DENIED; + }, +}); +/** + * |true| if the error was raised because some invalid argument was passed, + * |false| otherwise. + */ +Object.defineProperty(OSError.prototype, "becauseInvalidArgument", { + get: function becauseInvalidArgument() { + return ( + this.winLastError == Const.ERROR_NOT_SUPPORTED || + this.winLastError == Const.ERROR_BAD_ARGUMENTS + ); + }, +}); + +/** + * Serialize an instance of OSError to something that can be + * transmitted across threads (not necessarily a string). + */ +OSError.toMsg = function toMsg(error) { + return { + exn: "OS.File.Error", + fileName: error.moduleName, + lineNumber: error.lineNumber, + stack: error.moduleStack, + operation: error.operation, + winLastError: error.winLastError, + path: error.path, + }; +}; + +/** + * Deserialize a message back to an instance of OSError + */ +OSError.fromMsg = function fromMsg(msg) { + let error = new OSError(msg.operation, msg.winLastError, msg.path); + error.stack = msg.stack; + error.fileName = msg.fileName; + error.lineNumber = msg.lineNumber; + return error; +}; +exports.Error = OSError; + +/** + * Code shared by implementation of File.Info on Windows + * + * @constructor + */ +var AbstractInfo = function AbstractInfo( + path, + isDir, + isSymLink, + size, + lastAccessDate, + lastWriteDate, + winAttributes +) { + this._path = path; + this._isDir = isDir; + this._isSymLink = isSymLink; + this._size = size; + this._lastAccessDate = lastAccessDate; + this._lastModificationDate = lastWriteDate; + this._winAttributes = winAttributes; +}; + +AbstractInfo.prototype = { + /** + * The path of the file, used for error-reporting. + * + * @type {string} + */ + get path() { + return this._path; + }, + /** + * |true| if this file is a directory, |false| otherwise + */ + get isDir() { + return this._isDir; + }, + /** + * |true| if this file is a symbolic link, |false| otherwise + */ + get isSymLink() { + return this._isSymLink; + }, + /** + * The size of the file, in bytes. + * + * Note that the result may be |NaN| if the size of the file cannot be + * represented in JavaScript. + * + * @type {number} + */ + get size() { + return this._size; + }, + /** + * The date of last access to this file. + * + * Note that the definition of last access may depend on the underlying + * operating system and file system. + * + * @type {Date} + */ + get lastAccessDate() { + return this._lastAccessDate; + }, + /** + * The date of last modification of this file. + * + * Note that the definition of last access may depend on the underlying + * operating system and file system. + * + * @type {Date} + */ + get lastModificationDate() { + return this._lastModificationDate; + }, + /** + * The Object with following boolean properties of this file. + * {readOnly, system, hidden} + * + * @type {object} + */ + get winAttributes() { + return this._winAttributes; + }, +}; +exports.AbstractInfo = AbstractInfo; + +/** + * Code shared by implementation of File.DirectoryIterator.Entry on Windows + * + * @constructor + */ +var AbstractEntry = function AbstractEntry( + isDir, + isSymLink, + name, + winLastWriteDate, + winLastAccessDate, + path +) { + this._isDir = isDir; + this._isSymLink = isSymLink; + this._name = name; + this._winLastWriteDate = winLastWriteDate; + this._winLastAccessDate = winLastAccessDate; + this._path = path; +}; + +AbstractEntry.prototype = { + /** + * |true| if the entry is a directory, |false| otherwise + */ + get isDir() { + return this._isDir; + }, + /** + * |true| if the entry is a symbolic link, |false| otherwise + */ + get isSymLink() { + return this._isSymLink; + }, + /** + * The name of the entry. + * @type {string} + */ + get name() { + return this._name; + }, + /** + * The last modification time of this file. + * @type {Date} + */ + get winLastWriteDate() { + return this._winLastWriteDate; + }, + /** + * The last access time of this file. + * @type {Date} + */ + get winLastAccessDate() { + return this._winLastAccessDate; + }, + /** + * The full path of the entry + * @type {string} + */ + get path() { + return this._path; + }, +}; +exports.AbstractEntry = AbstractEntry; + +// Special constants that need to be defined on all platforms + +exports.POS_START = Const.FILE_BEGIN; +exports.POS_CURRENT = Const.FILE_CURRENT; +exports.POS_END = Const.FILE_END; + +// Special types that need to be defined for communication +// between threads +var Type = Object.create(SharedAll.Type); +exports.Type = Type; + +/** + * Native paths + * + * Under Windows, expressed as wide strings + */ +Type.path = Type.wstring.withName("[in] path"); +Type.out_path = Type.out_wstring.withName("[out] path"); + +// Special constructors that need to be defined on all threads +OSError.closed = function closed(operation, path) { + return new OSError(operation, Const.ERROR_INVALID_HANDLE, path); +}; + +OSError.exists = function exists(operation, path) { + return new OSError(operation, Const.ERROR_FILE_EXISTS, path); +}; + +OSError.noSuchFile = function noSuchFile(operation, path) { + return new OSError(operation, Const.ERROR_FILE_NOT_FOUND, path); +}; + +OSError.invalidArgument = function invalidArgument(operation) { + return new OSError(operation, Const.ERROR_NOT_SUPPORTED); +}; + +var EXPORTED_SYMBOLS = [ + "declareFFI", + "libc", + "Error", + "AbstractInfo", + "AbstractEntry", + "Type", + "POS_START", + "POS_CURRENT", + "POS_END", +]; + +// ////////// Boilerplate +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + // eslint-disable-next-line mozilla/reject-global-this + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_win_back.js b/toolkit/components/osfile/modules/osfile_win_back.js new file mode 100644 index 000000000000..5460ef783040 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_back.js @@ -0,0 +1,542 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file can be used in the following contexts: + * + * 1. included from a non-osfile worker thread using importScript + * (it serves to define a synchronous API for that worker thread) + * (bug 707681) + * + * 2. included from the main thread using Components.utils.import + * (it serves to define the asynchronous API, whose implementation + * resides in the worker thread) + * (bug 729057) + * + * 3. included from the osfile worker thread using importScript + * (it serves to define the implementation of the asynchronous API) + * (bug 729057) + */ + +/* eslint-env mozilla/chrome-worker, node */ + +// eslint-disable-next-line no-lone-blocks +{ + if (typeof Components != "undefined") { + // We do not wish osfile_win_back.js to be used directly as a main thread + // module yet. When time comes, it will be loaded by a combination of + // a main thread front-end/worker thread implementation that makes sure + // that we are not executing synchronous IO code in the main thread. + + throw new Error( + "osfile_win_back.js cannot be used from the main thread yet" + ); + } + + (function(exports) { + "use strict"; + if (exports.OS && exports.OS.Win && exports.OS.Win.File) { + return; // Avoid double initialization + } + + let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm"); + SharedAll.LOG.bind(SharedAll, "Unix", "back"); + let libc = SysAll.libc; + let advapi32 = new SharedAll.Library("advapi32", "advapi32.dll"); + let Const = SharedAll.Constants.Win; + + /** + * Initialize the Windows module. + * + * @param {function=} declareFFI + */ + // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them + let init = function init(aDeclareFFI) { + let declareFFI; + if (aDeclareFFI) { + declareFFI = aDeclareFFI.bind(null, libc); + } else { + declareFFI = SysAll.declareFFI; // eslint-disable-line no-unused-vars + } + let declareLazyFFI = SharedAll.declareLazyFFI; // eslint-disable-line no-unused-vars + + // Initialize types that require additional OS-specific + // support - either finalization or matching against + // OS-specific constants. + let Type = Object.create(SysAll.Type); + let SysFile = (exports.OS.Win.File = { Type }); + + // Initialize types + + /** + * A C integer holding INVALID_HANDLE_VALUE in case of error or + * a file descriptor in case of success. + */ + Type.HANDLE = Type.voidptr_t.withName("HANDLE"); + Type.HANDLE.importFromC = function importFromC(maybe) { + if (Type.int.cast(maybe).value == INVALID_HANDLE) { + // Ensure that API clients can effectively compare against + // Const.INVALID_HANDLE_VALUE. Without this cast, + // == would always return |false|. + return INVALID_HANDLE; + } + return ctypes.CDataFinalizer(maybe, this.finalizeHANDLE); + }; + Type.HANDLE.finalizeHANDLE = function placeholder() { + throw new Error("finalizeHANDLE should be implemented"); + }; + let INVALID_HANDLE = Const.INVALID_HANDLE_VALUE; + + Type.file_HANDLE = Type.HANDLE.withName("file HANDLE"); + SharedAll.defineLazyGetter( + Type.file_HANDLE, + "finalizeHANDLE", + function() { + return SysFile._CloseHandle; + } + ); + + Type.find_HANDLE = Type.HANDLE.withName("find HANDLE"); + SharedAll.defineLazyGetter( + Type.find_HANDLE, + "finalizeHANDLE", + function() { + return SysFile._FindClose; + } + ); + + Type.DWORD = Type.uint32_t.withName("DWORD"); + + /* A special type used to represent flags passed as DWORDs to a function. + * In JavaScript, bitwise manipulation of numbers, such as or-ing flags, + * can produce negative numbers. Since DWORD is unsigned, these negative + * numbers simply cannot be converted to DWORD. For this reason, whenever + * bit manipulation is called for, you should rather use DWORD_FLAGS, + * which is represented as a signed integer, hence has the correct + * semantics. + */ + Type.DWORD_FLAGS = Type.int32_t.withName("DWORD_FLAGS"); + + /** + * A C integer holding 0 in case of error or a positive integer + * in case of success. + */ + Type.zero_or_DWORD = Type.DWORD.withName("zero_or_DWORD"); + + /** + * A C integer holding 0 in case of error, any other value in + * case of success. + */ + Type.zero_or_nothing = Type.int.withName("zero_or_nothing"); + + /** + * A C integer holding flags related to NTFS security. + */ + Type.SECURITY_ATTRIBUTES = Type.void_t.withName("SECURITY_ATTRIBUTES"); + + /** + * A C integer holding pointers related to NTFS security. + */ + Type.PSID = Type.voidptr_t.withName("PSID"); + + Type.PACL = Type.voidptr_t.withName("PACL"); + + Type.PSECURITY_DESCRIPTOR = Type.voidptr_t.withName( + "PSECURITY_DESCRIPTOR" + ); + + /** + * A C integer holding Win32 local memory handle. + */ + Type.HLOCAL = Type.voidptr_t.withName("HLOCAL"); + + Type.FILETIME = new SharedAll.Type( + "FILETIME", + ctypes.StructType("FILETIME", [ + { lo: Type.DWORD.implementation }, + { hi: Type.DWORD.implementation }, + ]) + ); + + Type.FindData = new SharedAll.Type( + "FIND_DATA", + ctypes.StructType("FIND_DATA", [ + { dwFileAttributes: ctypes.uint32_t }, + { ftCreationTime: Type.FILETIME.implementation }, + { ftLastAccessTime: Type.FILETIME.implementation }, + { ftLastWriteTime: Type.FILETIME.implementation }, + { nFileSizeHigh: Type.DWORD.implementation }, + { nFileSizeLow: Type.DWORD.implementation }, + { dwReserved0: Type.DWORD.implementation }, + { dwReserved1: Type.DWORD.implementation }, + { cFileName: ctypes.ArrayType(ctypes.char16_t, Const.MAX_PATH) }, + { cAlternateFileName: ctypes.ArrayType(ctypes.char16_t, 14) }, + ]) + ); + + Type.FILE_INFORMATION = new SharedAll.Type( + "FILE_INFORMATION", + ctypes.StructType("FILE_INFORMATION", [ + { dwFileAttributes: ctypes.uint32_t }, + { ftCreationTime: Type.FILETIME.implementation }, + { ftLastAccessTime: Type.FILETIME.implementation }, + { ftLastWriteTime: Type.FILETIME.implementation }, + { dwVolumeSerialNumber: ctypes.uint32_t }, + { nFileSizeHigh: Type.DWORD.implementation }, + { nFileSizeLow: Type.DWORD.implementation }, + { nNumberOfLinks: ctypes.uint32_t }, + { nFileIndex: ctypes.uint64_t }, + ]) + ); + + Type.SystemTime = new SharedAll.Type( + "SystemTime", + ctypes.StructType("SystemTime", [ + { wYear: ctypes.int16_t }, + { wMonth: ctypes.int16_t }, + { wDayOfWeek: ctypes.int16_t }, + { wDay: ctypes.int16_t }, + { wHour: ctypes.int16_t }, + { wMinute: ctypes.int16_t }, + { wSecond: ctypes.int16_t }, + { wMilliSeconds: ctypes.int16_t }, + ]) + ); + + // Special case: these functions are used by the + // finalizer + libc.declareLazy( + SysFile, + "_CloseHandle", + "CloseHandle", + ctypes.winapi_abi, + /* return */ ctypes.bool, + /* handle*/ ctypes.voidptr_t + ); + + SysFile.CloseHandle = function(fd) { + if (fd == INVALID_HANDLE) { + return true; + } + return fd.dispose(); // Returns the value of |CloseHandle|. + }; + + libc.declareLazy( + SysFile, + "_FindClose", + "FindClose", + ctypes.winapi_abi, + /* return */ ctypes.bool, + /* handle*/ ctypes.voidptr_t + ); + + SysFile.FindClose = function(handle) { + if (handle == INVALID_HANDLE) { + return true; + } + return handle.dispose(); // Returns the value of |FindClose|. + }; + + // Declare libc functions as functions of |OS.Win.File| + + libc.declareLazyFFI( + SysFile, + "CopyFile", + "CopyFileW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + /* sourcePath*/ Type.path, + Type.path, + /* bailIfExist*/ Type.bool + ); + + libc.declareLazyFFI( + SysFile, + "CreateDirectory", + "CreateDirectoryW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.char16_t.in_ptr, + /* security*/ Type.SECURITY_ATTRIBUTES.in_ptr + ); + + libc.declareLazyFFI( + SysFile, + "CreateFile", + "CreateFileW", + ctypes.winapi_abi, + Type.file_HANDLE, + Type.path, + Type.DWORD_FLAGS, + Type.DWORD_FLAGS, + /* security*/ Type.SECURITY_ATTRIBUTES.in_ptr, + /* creation*/ Type.DWORD_FLAGS, + Type.DWORD_FLAGS, + /* template*/ Type.HANDLE + ); + + libc.declareLazyFFI( + SysFile, + "DeleteFile", + "DeleteFileW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "FileTimeToSystemTime", + "FileTimeToSystemTime", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + /* filetime*/ Type.FILETIME.in_ptr, + /* systime*/ Type.SystemTime.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "SystemTimeToFileTime", + "SystemTimeToFileTime", + ctypes.winapi_abi, + Type.zero_or_nothing, + Type.SystemTime.in_ptr, + /* filetime*/ Type.FILETIME.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "FindFirstFile", + "FindFirstFileW", + ctypes.winapi_abi, + /* return*/ Type.find_HANDLE, + /* pattern*/ Type.path, + Type.FindData.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "FindNextFile", + "FindNextFileW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.find_HANDLE, + Type.FindData.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "FormatMessage", + "FormatMessageW", + ctypes.winapi_abi, + /* return*/ Type.DWORD, + Type.DWORD_FLAGS, + /* source*/ Type.void_t.in_ptr, + Type.DWORD_FLAGS, + /* langid*/ Type.DWORD_FLAGS, + Type.out_wstring, + Type.DWORD, + /* Arguments*/ Type.void_t.in_ptr + ); + + libc.declareLazyFFI( + SysFile, + "GetCurrentDirectory", + "GetCurrentDirectoryW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_DWORD, + /* length*/ Type.DWORD, + Type.out_path + ); + + libc.declareLazyFFI( + SysFile, + "GetFullPathName", + "GetFullPathNameW", + ctypes.winapi_abi, + Type.zero_or_DWORD, + /* fileName*/ Type.path, + Type.DWORD, + Type.out_path, + /* filePart*/ Type.DWORD + ); + + libc.declareLazyFFI( + SysFile, + "GetDiskFreeSpaceEx", + "GetDiskFreeSpaceExW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + /* directoryName*/ Type.path, + /* freeBytesForUser*/ Type.uint64_t.out_ptr, + /* totalBytesForUser*/ Type.uint64_t.out_ptr, + /* freeTotalBytesOnDrive*/ Type.uint64_t.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "GetFileInformationByHandle", + "GetFileInformationByHandle", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + /* handle*/ Type.HANDLE, + Type.FILE_INFORMATION.out_ptr + ); + + libc.declareLazyFFI( + SysFile, + "MoveFileEx", + "MoveFileExW", + ctypes.winapi_abi, + Type.zero_or_nothing, + /* sourcePath*/ Type.path, + /* destPath*/ Type.path, + Type.DWORD + ); + + libc.declareLazyFFI( + SysFile, + "ReadFile", + "ReadFile", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.HANDLE, + /* buffer*/ Type.voidptr_t, + /* nbytes*/ Type.DWORD, + /* nbytes_read*/ Type.DWORD.out_ptr, + /* overlapped*/ Type.void_t.inout_ptr // FIXME: Implement? + ); + + libc.declareLazyFFI( + SysFile, + "RemoveDirectory", + "RemoveDirectoryW", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.path + ); + + libc.declareLazyFFI( + SysFile, + "SetEndOfFile", + "SetEndOfFile", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.HANDLE + ); + + libc.declareLazyFFI( + SysFile, + "SetFilePointer", + "SetFilePointer", + ctypes.winapi_abi, + /* return*/ Type.DWORD, + Type.HANDLE, + /* distlow*/ Type.long, + /* disthi*/ Type.long.in_ptr, + /* method*/ Type.DWORD + ); + + libc.declareLazyFFI( + SysFile, + "SetFileTime", + "SetFileTime", + ctypes.winapi_abi, + Type.zero_or_nothing, + Type.HANDLE, + /* creation*/ Type.FILETIME.in_ptr, + Type.FILETIME.in_ptr, + Type.FILETIME.in_ptr + ); + + libc.declareLazyFFI( + SysFile, + "WriteFile", + "WriteFile", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.HANDLE, + /* buffer*/ Type.voidptr_t, + /* nbytes*/ Type.DWORD, + /* nbytes_wr*/ Type.DWORD.out_ptr, + /* overlapped*/ Type.void_t.inout_ptr // FIXME: Implement? + ); + + libc.declareLazyFFI( + SysFile, + "FlushFileBuffers", + "FlushFileBuffers", + ctypes.winapi_abi, + /* return*/ Type.zero_or_nothing, + Type.HANDLE + ); + + libc.declareLazyFFI( + SysFile, + "GetFileAttributes", + "GetFileAttributesW", + ctypes.winapi_abi, + Type.DWORD_FLAGS, + /* fileName*/ Type.path + ); + + libc.declareLazyFFI( + SysFile, + "SetFileAttributes", + "SetFileAttributesW", + ctypes.winapi_abi, + Type.zero_or_nothing, + Type.path, + /* fileAttributes*/ Type.DWORD_FLAGS + ); + + advapi32.declareLazyFFI( + SysFile, + "GetNamedSecurityInfo", + "GetNamedSecurityInfoW", + ctypes.winapi_abi, + Type.DWORD, + Type.path, + Type.DWORD, + /* securityInfo*/ Type.DWORD, + Type.PSID.out_ptr, + Type.PSID.out_ptr, + Type.PACL.out_ptr, + Type.PACL.out_ptr, + /* securityDesc*/ Type.PSECURITY_DESCRIPTOR.out_ptr + ); + + advapi32.declareLazyFFI( + SysFile, + "SetNamedSecurityInfo", + "SetNamedSecurityInfoW", + ctypes.winapi_abi, + Type.DWORD, + Type.path, + Type.DWORD, + /* securityInfo*/ Type.DWORD, + Type.PSID, + Type.PSID, + Type.PACL, + Type.PACL + ); + + libc.declareLazyFFI( + SysFile, + "LocalFree", + "LocalFree", + ctypes.winapi_abi, + Type.HLOCAL, + Type.HLOCAL + ); + }; + + exports.OS.Win = { + File: { + _init: init, + }, + }; + })(this); +} diff --git a/toolkit/components/osfile/modules/osfile_win_front.js b/toolkit/components/osfile/modules/osfile_win_front.js new file mode 100644 index 000000000000..056464574e72 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_front.js @@ -0,0 +1,1322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Synchronous front-end for the JavaScript OS.File library. + * Windows implementation. + * + * This front-end is meant to be imported by a worker thread. + */ + +/* eslint-env mozilla/chrome-worker, node */ +/* global OS */ + +// eslint-disable-next-line no-lone-blocks +{ + if (typeof Components != "undefined") { + // We do not wish osfile_win_front.js to be used directly as a main thread + // module yet. + throw new Error( + "osfile_win_front.js cannot be used from the main thread yet" + ); + } + + (function(exports) { + "use strict"; + + // exports.OS.Win is created by osfile_win_back.js + if (exports.OS && exports.OS.File) { + return; // Avoid double-initialization + } + + let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + let Path = require("resource://gre/modules/osfile/ospath.jsm"); + let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm"); + exports.OS.Win.File._init(); + let Const = exports.OS.Constants.Win; + let WinFile = exports.OS.Win.File; + let Type = WinFile.Type; + + // Mutable thread-global data + // In the Windows implementation, methods |read| and |write| + // require passing a pointer to an uint32 to determine how many + // bytes have been read/written. In C, this is a benigne operation, + // but in js-ctypes, this has a cost. Rather than re-allocating a + // C uint32 and a C uint32* for each |read|/|write|, we take advantage + // of the fact that the state is thread-private -- hence that two + // |read|/|write| operations cannot take place at the same time -- + // and we use the following global mutable values: + let gBytesRead = new ctypes.uint32_t(0); + let gBytesReadPtr = gBytesRead.address(); + let gBytesWritten = new ctypes.uint32_t(0); + let gBytesWrittenPtr = gBytesWritten.address(); + + // Same story for GetFileInformationByHandle + let gFileInfo = new Type.FILE_INFORMATION.implementation(); + let gFileInfoPtr = gFileInfo.address(); + + /** + * Representation of a file. + * + * You generally do not need to call this constructor yourself. Rather, + * to open a file, use function |OS.File.open|. + * + * @param fd A OS-specific file descriptor. + * @param {string} path File path of the file handle, used for error-reporting. + * @constructor + */ + let File = function File(fd, path) { + exports.OS.Shared.AbstractFile.call(this, fd, path); + this._closeResult = null; + }; + File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype); + + /** + * Close the file. + * + * This method has no effect if the file is already closed. However, + * if the first call to |close| has thrown an error, further calls + * will throw the same error. + * + * @throws File.Error If closing the file revealed an error that could + * not be reported earlier. + */ + File.prototype.close = function close() { + if (this._fd) { + let fd = this._fd; + this._fd = null; + // Call |close(fd)|, detach finalizer if any + // (|fd| may not be a CDataFinalizer if it has been + // instantiated from a controller thread). + let result = WinFile._CloseHandle(fd); + if (typeof fd == "object" && "forget" in fd) { + fd.forget(); + } + if (result == -1) { + this._closeResult = new File.Error( + "close", + ctypes.winLastError, + this._path + ); + } + } + if (this._closeResult) { + throw this._closeResult; + } + }; + + /** + * Read some bytes from a file. + * + * @param {C pointer} buffer A buffer for holding the data + * once it is read. + * @param {number} nbytes The number of bytes to read. It must not + * exceed the size of |buffer| in bytes but it may exceed the number + * of bytes unread in the file. + * @param {*=} options Additional options for reading. Ignored in + * this implementation. + * + * @return {number} The number of bytes effectively read. If zero, + * the end of the file has been reached. + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype._read = function _read(buffer, nbytes, options) { + // |gBytesReadPtr| is a pointer to |gBytesRead|. + throw_on_zero( + "read", + WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null), + this._path + ); + return gBytesRead.value; + }; + + /** + * Write some bytes to a file. + * + * @param {Typed array} buffer A buffer holding the data that must be + * written. + * @param {number} nbytes The number of bytes to write. It must not + * exceed the size of |buffer| in bytes. + * @param {*=} options Additional options for writing. Ignored in + * this implementation. + * + * @return {number} The number of bytes effectively written. + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype._write = function _write(buffer, nbytes, options) { + if (this._appendMode) { + // Need to manually seek on Windows, as O_APPEND is not supported. + // This is, of course, a race, but there is no real way around this. + this.setPosition(0, File.POS_END); + } + // |gBytesWrittenPtr| is a pointer to |gBytesWritten|. + throw_on_zero( + "write", + WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null), + this._path + ); + return gBytesWritten.value; + }; + + /** + * Return the current position in the file. + */ + File.prototype.getPosition = function getPosition(pos) { + return this.setPosition(0, File.POS_CURRENT); + }; + + /** + * Change the current position in the file. + * + * @param {number} pos The new position. Whether this position + * is considered from the current position, from the start of + * the file or from the end of the file is determined by + * argument |whence|. Note that |pos| may exceed the length of + * the file. + * @param {number=} whence The reference position. If omitted + * or |OS.File.POS_START|, |pos| is relative to the start of the + * file. If |OS.File.POS_CURRENT|, |pos| is relative to the + * current position in the file. If |OS.File.POS_END|, |pos| is + * relative to the end of the file. + * + * @return The new position in the file. + */ + File.prototype.setPosition = function setPosition(pos, whence) { + if (whence === undefined) { + whence = Const.FILE_BEGIN; + } + let pos64 = ctypes.Int64(pos); + // Per MSDN, while |lDistanceToMove| (low) is declared as int32_t, when + // providing |lDistanceToMoveHigh| as well, it should countain the + // bottom 32 bits of the 64-bit integer. Hence the following |posLo| + // cast is OK. + let posLo = new ctypes.uint32_t(ctypes.Int64.lo(pos64)); + posLo = ctypes.cast(posLo, ctypes.int32_t); + let posHi = new ctypes.int32_t(ctypes.Int64.hi(pos64)); + let result = WinFile.SetFilePointer( + this.fd, + posLo.value, + posHi.address(), + whence + ); + // INVALID_SET_FILE_POINTER might be still a valid result, as it + // represents the lower 32 bit of the int64 result. MSDN says to check + // both, INVALID_SET_FILE_POINTER and a non-zero winLastError. + if (result == Const.INVALID_SET_FILE_POINTER && ctypes.winLastError) { + throw new File.Error("setPosition", ctypes.winLastError, this._path); + } + pos64 = ctypes.Int64.join(posHi.value, result); + return Type.int64_t.project(pos64); + }; + + /** + * Fetch the information on the file. + * + * @return File.Info The information on |this| file. + */ + File.prototype.stat = function stat() { + throw_on_zero( + "stat", + WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr), + this._path + ); + return new File.Info(gFileInfo, this._path); + }; + + /** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * @param {Date,number=} accessDate The last access date. If numeric, + * milliseconds since epoch. If omitted or null, then the current date + * will be used. + * @param {Date,number=} modificationDate The last modification date. If + * numeric, milliseconds since epoch. If omitted or null, then the current + * date will be used. + * + * @throws {TypeError} In case of invalid parameters. + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype.setDates = function setDates(accessDate, modificationDate) { + accessDate = Date_to_FILETIME( + "File.prototype.setDates", + accessDate, + this._path + ); + modificationDate = Date_to_FILETIME( + "File.prototype.setDates", + modificationDate, + this._path + ); + throw_on_zero( + "setDates", + WinFile.SetFileTime( + this.fd, + null, + accessDate.address(), + modificationDate.address() + ), + this._path + ); + }; + + /** + * Set the file's access permission bits. + */ + File.prototype.setPermissions = function setPermissions(options = {}) { + if (!("winAttributes" in options)) { + return; + } + let oldAttributes = WinFile.GetFileAttributes(this._path); + if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) { + throw new File.Error("setPermissions", ctypes.winLastError, this._path); + } + let newAttributes = toFileAttributes( + options.winAttributes, + oldAttributes + ); + throw_on_zero( + "setPermissions", + WinFile.SetFileAttributes(this._path, newAttributes), + this._path + ); + }; + + /** + * Flushes the file's buffers and causes all buffered data + * to be written. + * Disk flushes are very expensive and therefore should be used carefully, + * sparingly and only in scenarios where it is vital that data survives + * system crashes. Even though the function will be executed off the + * main-thread, it might still affect the overall performance of any + * running application. + * + * @throws {OS.File.Error} In case of I/O error. + */ + File.prototype.flush = function flush() { + throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd), this._path); + }; + + // The default sharing mode for opening files: files are not + // locked against being reopened for reading/writing or against + // being deleted by the same process or another process. + // This is consistent with the default Unix policy. + const DEFAULT_SHARE = + Const.FILE_SHARE_READ | Const.FILE_SHARE_WRITE | Const.FILE_SHARE_DELETE; + + // The default flags for opening files. + const DEFAULT_FLAGS = Const.FILE_ATTRIBUTE_NORMAL; + + /** + * Open a file + * + * @param {string} path The path to the file. + * @param {*=} mode The opening mode for the file, as + * an object that may contain the following fields: + * + * - {bool} truncate If |true|, the file will be opened + * for writing. If the file does not exist, it will be + * created. If the file exists, its contents will be + * erased. Cannot be specified with |create|. + * - {bool} create If |true|, the file will be opened + * for writing. If the file exists, this function fails. + * If the file does not exist, it will be created. Cannot + * be specified with |truncate| or |existing|. + * - {bool} existing. If the file does not exist, this function + * fails. Cannot be specified with |create|. + * - {bool} read If |true|, the file will be opened for + * reading. The file may also be opened for writing, depending + * on the other fields of |mode|. + * - {bool} write If |true|, the file will be opened for + * writing. The file may also be opened for reading, depending + * on the other fields of |mode|. + * - {bool} append If |true|, the file will be opened for appending, + * meaning the equivalent of |.setPosition(0, POS_END)| is executed + * before each write. The default is |true|, i.e. opening a file for + * appending. Specify |append: false| to open the file in regular mode. + * + * If neither |truncate|, |create| or |write| is specified, the file + * is opened for reading. + * + * Note that |false|, |null| or |undefined| flags are simply ignored. + * + * @param {*=} options Additional options for file opening. This + * implementation interprets the following fields: + * + * - {number} winShare If specified, a share mode, as per + * Windows function |CreateFile|. You can build it from + * constants |OS.Constants.Win.FILE_SHARE_*|. If unspecified, + * the file uses the default sharing policy: it can be opened + * for reading and/or writing and it can be removed by other + * processes and by the same process. + * - {number} winSecurity If specified, Windows security + * attributes, as per Windows function |CreateFile|. If unspecified, + * no security attributes. + * - {number} winAccess If specified, Windows access mode, as + * per Windows function |CreateFile|. This also requires option + * |winDisposition| and this replaces argument |mode|. If unspecified, + * uses the string |mode|. + * - {number} winDisposition If specified, Windows disposition mode, + * as per Windows function |CreateFile|. This also requires option + * |winAccess| and this replaces argument |mode|. If unspecified, + * uses the string |mode|. + * + * @return {File} A file object. + * @throws {OS.File.Error} If the file could not be opened. + */ + File.open = function Win_open(path, mode = {}, options = {}) { + let share = + options.winShare !== undefined ? options.winShare : DEFAULT_SHARE; + let security = options.winSecurity || null; + let flags = + options.winFlags !== undefined ? options.winFlags : DEFAULT_FLAGS; + let template = options.winTemplate ? options.winTemplate._fd : null; + let access; + let disposition; + + mode = OS.Shared.AbstractFile.normalizeOpenMode(mode); + + // The following option isn't a generic implementation of access to paths + // of arbitrary lengths. It allows for the specific case of writing to an + // Alternate Data Stream on a file whose path length is already close to + // MAX_PATH. This implementation is safe with a full path as input, if + // the first part of the path comes from local configuration and the + // file without the ADS was successfully opened before, so we know the + // path is valid. + if (options.winAllowLengthBeyondMaxPathWithCaveats) { + // Use the \\?\ syntax to allow lengths beyond MAX_PATH. This limited + // implementation only supports a DOS local path or UNC path as input. + let isUNC = + path.length >= 2 && + (path[0] == "\\" || path[0] == "/") && + (path[1] == "\\" || path[1] == "/"); + let pathToUse = "\\\\?\\" + (isUNC ? "UNC\\" + path.slice(2) : path); + // Use GetFullPathName to normalize slashes into backslashes. This is + // required because CreateFile won't do this for the \\?\ syntax. + let buffer_size = 512; + let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))(); + let expected_size = throw_on_zero( + "open", + WinFile.GetFullPathName(pathToUse, buffer_size, array, 0) + ); + if (expected_size > buffer_size) { + // We don't need to allow an arbitrary path length for now. + throw new File.Error("open", ctypes.winLastError, path); + } + path = array.readString(); + } + + if ("winAccess" in options && "winDisposition" in options) { + access = options.winAccess; + disposition = options.winDisposition; + } else if ( + ("winAccess" in options && !("winDisposition" in options)) || + (!("winAccess" in options) && "winDisposition" in options) + ) { + throw new TypeError( + "OS.File.open requires either both options " + + "winAccess and winDisposition or neither" + ); + } else { + if (mode.read) { + access |= Const.GENERIC_READ; + } + if (mode.write) { + access |= Const.GENERIC_WRITE; + } + // Finally, handle create/existing/trunc + if (mode.trunc) { + if (mode.existing) { + // It seems that Const.TRUNCATE_EXISTING is broken + // in presence of links (source, anyone?). We need + // to open normally, then perform truncation manually. + disposition = Const.OPEN_EXISTING; + } else { + disposition = Const.CREATE_ALWAYS; + } + } else if (mode.create) { + disposition = Const.CREATE_NEW; + } else if (mode.read && !mode.write) { + disposition = Const.OPEN_EXISTING; + } else if (mode.existing) { + disposition = Const.OPEN_EXISTING; + } else { + disposition = Const.OPEN_ALWAYS; + } + } + + let file = error_or_file( + WinFile.CreateFile( + path, + access, + share, + security, + disposition, + flags, + template + ), + path + ); + + file._appendMode = !!mode.append; + + if (!(mode.trunc && mode.existing)) { + return file; + } + // Now, perform manual truncation + file.setPosition(0, File.POS_START); + throw_on_zero("open", WinFile.SetEndOfFile(file.fd), path); + return file; + }; + + /** + * Checks if a file or directory exists + * + * @param {string} path The path to the file. + * + * @return {bool} true if the file exists, false otherwise. + */ + File.exists = function Win_exists(path) { + try { + let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS); + file.close(); + return true; + } catch (x) { + return false; + } + }; + + /** + * Remove an existing file. + * + * @param {string} path The name of the file. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the file does + * not exist. |true| by default. + * + * @throws {OS.File.Error} In case of I/O error. + */ + File.remove = function remove(path, options = {}) { + if (WinFile.DeleteFile(path)) { + return; + } + + if ( + ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND || + ctypes.winLastError == Const.ERROR_PATH_NOT_FOUND + ) { + if (!("ignoreAbsent" in options) || options.ignoreAbsent) { + return; + } + } else if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED) { + // Save winLastError before another ctypes call. + let lastError = ctypes.winLastError; + let attributes = WinFile.GetFileAttributes(path); + if (attributes != Const.INVALID_FILE_ATTRIBUTES) { + if (!(attributes & Const.FILE_ATTRIBUTE_READONLY)) { + throw new File.Error("remove", lastError, path); + } + let newAttributes = attributes & ~Const.FILE_ATTRIBUTE_READONLY; + if ( + WinFile.SetFileAttributes(path, newAttributes) && + WinFile.DeleteFile(path) + ) { + return; + } + } + } + + throw new File.Error("remove", ctypes.winLastError, path); + }; + + /** + * Remove an empty directory. + * + * @param {string} path The name of the directory to remove. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the directory + * does not exist. |true| by default + */ + File.removeEmptyDir = function removeEmptyDir(path, options = {}) { + let result = WinFile.RemoveDirectory(path); + if (!result) { + if ( + (!("ignoreAbsent" in options) || options.ignoreAbsent) && + ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND + ) { + return; + } + throw new File.Error("removeEmptyDir", ctypes.winLastError, path); + } + }; + + /** + * Create a directory and, optionally, its parent directories. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. This + * implementation interprets the following fields: + * + * - {C pointer} winSecurity If specified, security attributes + * as per winapi function |CreateDirectory|. If unspecified, + * use the default security descriptor, inherited from the + * parent directory. + * - {bool} ignoreExisting If |false|, throw an error if the directory + * already exists. |true| by default + * - {string} from If specified, the call to |makeDir| creates all the + * ancestors of |path| that are descendants of |from|. Note that |from| + * and its existing descendants must be user-writeable and that |path| + * must be a descendant of |from|. + * Example: + * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); + * creates directories profileDir/foo, profileDir/foo/bar + */ + File._makeDir = function makeDir(path, options = {}) { + let security = options.winSecurity || null; + let result = WinFile.CreateDirectory(path, security); + + if (result) { + return; + } + + if ("ignoreExisting" in options && !options.ignoreExisting) { + throw new File.Error("makeDir", ctypes.winLastError, path); + } + + if (ctypes.winLastError == Const.ERROR_ALREADY_EXISTS) { + return; + } + + // If the user has no access, but it's a root directory, no error should be thrown + let splitPath = OS.Path.split(path); + // Removing last component if it's empty + // An empty last component is caused by trailing slashes in path + // This is always the case with root directories + if (splitPath.components[splitPath.components.length - 1].length === 0) { + splitPath.components.pop(); + } + // One component consisting of a drive letter implies a directory root. + if ( + ctypes.winLastError == Const.ERROR_ACCESS_DENIED && + splitPath.winDrive && + splitPath.components.length === 1 + ) { + return; + } + + throw new File.Error("makeDir", ctypes.winLastError, path); + }; + + /** + * Copy a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be copied. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If true, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * + * @throws {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be copied with the file. The + * behavior may not be the same across all platforms. + */ + File.copy = function copy(sourcePath, destPath, options = {}) { + throw_on_zero( + "copy", + WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false), + sourcePath + ); + }; + + /** + * Move a file to a destination. + * + * @param {string} sourcePath The platform-specific path at which + * the file may currently be found. + * @param {string} destPath The platform-specific path at which the + * file should be moved. + * @param {*=} options An object which may contain the following fields: + * + * @option {bool} noOverwrite - If set, this function will fail if + * a file already exists at |destPath|. Otherwise, if this file exists, + * it will be erased silently. + * @option {bool} noCopy - If set, this function will fail if the + * operation is more sophisticated than a simple renaming, i.e. if + * |sourcePath| and |destPath| are not situated on the same drive. + * + * @throws {OS.File.Error} In case of any error. + * + * General note: The behavior of this function is defined only when + * it is called on a single file. If it is called on a directory, the + * behavior is undefined and may not be the same across all platforms. + * + * General note: The behavior of this function with respect to metadata + * is unspecified. Metadata may or may not be moved with the file. The + * behavior may not be the same across all platforms. + */ + File.move = function move(sourcePath, destPath, options = {}) { + let flags = 0; + if (!options.noCopy) { + flags = Const.MOVEFILE_COPY_ALLOWED; + } + if (!options.noOverwrite) { + flags = flags | Const.MOVEFILE_REPLACE_EXISTING; + } + throw_on_zero( + "move", + WinFile.MoveFileEx(sourcePath, destPath, flags), + sourcePath + ); + + // Inherit NTFS permissions from the destination directory + // if possible. + if (Path.dirname(sourcePath) === Path.dirname(destPath)) { + // Skip if the move operation was the simple rename, + return; + } + // The function may fail for various reasons (e.g. not all + // filesystems support NTFS permissions or the user may not + // have the enough rights to read/write permissions). + // However we can safely ignore errors. The file was already + // moved. Setting permissions is not mandatory. + let dacl = new ctypes.voidptr_t(); + let sd = new ctypes.voidptr_t(); + WinFile.GetNamedSecurityInfo( + destPath, + Const.SE_FILE_OBJECT, + Const.DACL_SECURITY_INFORMATION, + null /* sidOwner*/, + null /* sidGroup*/, + dacl.address(), + null /* sacl*/, + sd.address() + ); + // dacl will be set only if the function succeeds. + if (!dacl.isNull()) { + WinFile.SetNamedSecurityInfo( + destPath, + Const.SE_FILE_OBJECT, + Const.DACL_SECURITY_INFORMATION | + Const.UNPROTECTED_DACL_SECURITY_INFORMATION, + null /* sidOwner*/, + null /* sidGroup*/, + dacl, + null /* sacl*/ + ); + } + // sd will be set only if the function succeeds. + if (!sd.isNull()) { + WinFile.LocalFree(Type.HLOCAL.cast(sd)); + } + }; + + /** + * A global value used to receive data during time conversions. + */ + let gSystemTime = new Type.SystemTime.implementation(); + let gSystemTimePtr = gSystemTime.address(); + + /** + * Utility function: convert a FILETIME to a JavaScript Date. + */ + let FILETIME_to_Date = function FILETIME_to_Date(fileTime, path) { + if (fileTime == null) { + throw new TypeError("Expecting a non-null filetime"); + } + throw_on_zero( + "FILETIME_to_Date", + WinFile.FileTimeToSystemTime(fileTime.address(), gSystemTimePtr), + path + ); + // Windows counts hours, minutes, seconds from UTC, + // JS counts from local time, so we need to go through UTC. + let utc = Date.UTC( + gSystemTime.wYear, + gSystemTime.wMonth - 1, + /* Windows counts months from 1, JS from 0*/ gSystemTime.wDay, + gSystemTime.wHour, + gSystemTime.wMinute, + gSystemTime.wSecond, + gSystemTime.wMilliSeconds + ); + return new Date(utc); + }; + + /** + * Utility function: convert Javascript Date to FileTime. + * + * @param {string} fn Name of the calling function. + * @param {Date,number} date The date to be converted. If omitted or null, + * then the current date will be used. If numeric, assumed to be the date + * in milliseconds since epoch. + */ + let Date_to_FILETIME = function Date_to_FILETIME(fn, date, path) { + if (typeof date === "number") { + date = new Date(date); + } else if (!date) { + date = new Date(); + } else if (typeof date.getUTCFullYear !== "function") { + throw new TypeError( + "|date| parameter of " + + fn + + " must be a " + + "|Date| instance or number" + ); + } + gSystemTime.wYear = date.getUTCFullYear(); + // Windows counts months from 1, JS from 0. + gSystemTime.wMonth = date.getUTCMonth() + 1; + gSystemTime.wDay = date.getUTCDate(); + gSystemTime.wHour = date.getUTCHours(); + gSystemTime.wMinute = date.getUTCMinutes(); + gSystemTime.wSecond = date.getUTCSeconds(); + gSystemTime.wMilliseconds = date.getUTCMilliseconds(); + let result = new OS.Shared.Type.FILETIME.implementation(); + throw_on_zero( + "Date_to_FILETIME", + WinFile.SystemTimeToFileTime(gSystemTimePtr, result.address()), + path + ); + return result; + }; + + /** + * Iterate on one directory. + * + * This iterator will not enter subdirectories. + * + * @param {string} path The directory upon which to iterate. + * @param {*=} options An object that may contain the following field: + * @option {string} winPattern Windows file name pattern; if set, + * only files matching this pattern are returned. + * + * @throws {File.Error} If |path| does not represent a directory or + * if the directory cannot be iterated. + * @constructor + */ + File.DirectoryIterator = function DirectoryIterator(path, options) { + exports.OS.Shared.AbstractFile.AbstractIterator.call(this); + if (options && options.winPattern) { + this._pattern = path + "\\" + options.winPattern; + } else { + this._pattern = path + "\\*"; + } + this._path = path; + + // Pre-open the first item. + this._first = true; + this._findData = new Type.FindData.implementation(); + this._findDataPtr = this._findData.address(); + this._handle = WinFile.FindFirstFile(this._pattern, this._findDataPtr); + if (this._handle == Const.INVALID_HANDLE_VALUE) { + let error = ctypes.winLastError; + this._findData = null; + this._findDataPtr = null; + if (error == Const.ERROR_FILE_NOT_FOUND) { + // Directory is empty, let's behave as if it were closed + SharedAll.LOG("Directory is empty"); + this._closed = true; + this._exists = true; + } else if (error == Const.ERROR_PATH_NOT_FOUND) { + // Directory does not exist, let's throw if we attempt to walk it + SharedAll.LOG("Directory does not exist"); + this._closed = true; + this._exists = false; + } else { + throw new File.Error("DirectoryIterator", error, this._path); + } + } else { + this._closed = false; + this._exists = true; + } + }; + + File.DirectoryIterator.prototype = Object.create( + exports.OS.Shared.AbstractFile.AbstractIterator.prototype + ); + + /** + * Fetch the next entry in the directory. + * + * @return null If we have reached the end of the directory. + */ + File.DirectoryIterator.prototype._next = function _next() { + // Bailout if the directory does not exist + if (!this._exists) { + throw File.Error.noSuchFile( + "DirectoryIterator.prototype.next", + this._path + ); + } + // Bailout if the iterator is closed. + if (this._closed) { + return null; + } + // If this is the first entry, we have obtained it already + // during construction. + if (this._first) { + this._first = false; + return this._findData; + } + + if (WinFile.FindNextFile(this._handle, this._findDataPtr)) { + return this._findData; + } + let error = ctypes.winLastError; + this.close(); + if (error == Const.ERROR_NO_MORE_FILES) { + return null; + } + throw new File.Error("iter (FindNextFile)", error, this._path); + }; + + /** + * Return the next entry in the directory, if any such entry is + * available. + * + * Skip special directories "." and "..". + * + * @return By definition of the iterator protocol, either + * `{value: {File.Entry}, done: false}` if there is an unvisited entry + * in the directory, or `{value: undefined, done: true}`, otherwise. + */ + File.DirectoryIterator.prototype.next = function next() { + // FIXME: If we start supporting "\\?\"-prefixed paths, do not forget + // that "." and ".." are absolutely normal file names if _path starts + // with such prefix + for (let entry = this._next(); entry != null; entry = this._next()) { + let name = entry.cFileName.readString(); + if (name == "." || name == "..") { + continue; + } + return { + value: new File.DirectoryIterator.Entry(entry, this._path), + done: false, + }; + } + return { value: undefined, done: true }; + }; + + File.DirectoryIterator.prototype.close = function close() { + if (this._closed) { + return; + } + this._closed = true; + if (this._handle) { + // We might not have a handle if the iterator is closed + // before being used. + throw_on_zero("FindClose", WinFile.FindClose(this._handle), this._path); + this._handle = null; + } + }; + + /** + * Determine whether the directory exists. + * + * @return {boolean} + */ + File.DirectoryIterator.prototype.exists = function exists() { + return this._exists; + }; + + File.DirectoryIterator.Entry = function Entry(win_entry, parent) { + if ( + !win_entry.dwFileAttributes || + !win_entry.ftLastAccessTime || + !win_entry.ftLastWriteTime + ) { + throw new TypeError(); + } + + // Copy the relevant part of |win_entry| to ensure that + // our data is not overwritten prematurely. + let isDir = !!( + win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY + ); + let isSymLink = !!( + win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT + ); + + let winLastWriteDate = FILETIME_to_Date( + win_entry.ftLastWriteTime, + this._path + ); + let winLastAccessDate = FILETIME_to_Date( + win_entry.ftLastAccessTime, + this._path + ); + + let name = win_entry.cFileName.readString(); + if (!name) { + throw new TypeError("Empty name"); + } + + if (!parent) { + throw new TypeError("Empty parent"); + } + this._parent = parent; + + let path = Path.join(this._parent, name); + + SysAll.AbstractEntry.call( + this, + isDir, + isSymLink, + name, + winLastWriteDate, + winLastAccessDate, + path + ); + }; + File.DirectoryIterator.Entry.prototype = Object.create( + SysAll.AbstractEntry.prototype + ); + + /** + * Return a version of an instance of + * File.DirectoryIterator.Entry that can be sent from a worker + * thread to the main thread. Note that deserialization is + * asymmetric and returns an object with a different + * implementation. + */ + File.DirectoryIterator.Entry.toMsg = function toMsg(value) { + if (!(value instanceof File.DirectoryIterator.Entry)) { + throw new TypeError( + "parameter of " + + "File.DirectoryIterator.Entry.toMsg must be a " + + "File.DirectoryIterator.Entry" + ); + } + let serialized = {}; + for (let key in File.DirectoryIterator.Entry.prototype) { + serialized[key] = value[key]; + } + return serialized; + }; + + /** + * Information on a file. + * + * To obtain the latest information on a file, use |File.stat| + * (for an unopened file) or |File.prototype.stat| (for an + * already opened file). + * + * @constructor + */ + File.Info = function Info(stat, path) { + let isDir = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY); + let isSymLink = !!( + stat.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT + ); + + let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime, this._path); + let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime, this._path); + + let value = ctypes.UInt64.join(stat.nFileSizeHigh, stat.nFileSizeLow); + let size = Type.uint64_t.importFromC(value); + let winAttributes = { + readOnly: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_READONLY), + system: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_SYSTEM), + hidden: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_HIDDEN), + }; + + SysAll.AbstractInfo.call( + this, + path, + isDir, + isSymLink, + size, + lastAccessDate, + lastWriteDate, + winAttributes + ); + }; + File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype); + + /** + * Return a version of an instance of File.Info that can be sent + * from a worker thread to the main thread. Note that deserialization + * is asymmetric and returns an object with a different implementation. + */ + File.Info.toMsg = function toMsg(stat) { + if (!(stat instanceof File.Info)) { + throw new TypeError("parameter of File.Info.toMsg must be a File.Info"); + } + let serialized = {}; + for (let key in File.Info.prototype) { + serialized[key] = stat[key]; + } + return serialized; + }; + + /** + * Fetch the information on a file. + * + * Performance note: if you have opened the file already, + * method |File.prototype.stat| is generally much faster + * than method |File.stat|. + * + * Platform-specific note: under Windows, if the file is + * already opened without sharing of the read capability, + * this function will fail. + * + * @return {File.Information} + */ + File.stat = function stat(path) { + let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS); + try { + return file.stat(); + } finally { + file.close(); + } + }; + // All of the following is required to ensure that File.stat + // also works on directories. + const FILE_STAT_MODE = { + read: true, + }; + const FILE_STAT_OPTIONS = { + // Directories can be opened neither for reading(!) nor for writing + winAccess: 0, + // Directories can only be opened with backup semantics(!) + winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS, + winDisposition: Const.OPEN_EXISTING, + }; + + /** + * Set the file's access permission bits. + */ + File.setPermissions = function setPermissions(path, options = {}) { + if (!("winAttributes" in options)) { + return; + } + let oldAttributes = WinFile.GetFileAttributes(path); + if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) { + throw new File.Error("setPermissions", ctypes.winLastError, path); + } + let newAttributes = toFileAttributes( + options.winAttributes, + oldAttributes + ); + throw_on_zero( + "setPermissions", + WinFile.SetFileAttributes(path, newAttributes), + path + ); + }; + + /** + * Set the last access and modification date of the file. + * The time stamp resolution is 1 second at best, but might be worse + * depending on the platform. + * + * Performance note: if you have opened the file already in write mode, + * method |File.prototype.stat| is generally much faster + * than method |File.stat|. + * + * Platform-specific note: under Windows, if the file is + * already opened without sharing of the write capability, + * this function will fail. + * + * @param {string} path The full name of the file to set the dates for. + * @param {Date,number=} accessDate The last access date. If numeric, + * milliseconds since epoch. If omitted or null, then the current date + * will be used. + * @param {Date,number=} modificationDate The last modification date. If + * numeric, milliseconds since epoch. If omitted or null, then the current + * date will be used. + * + * @throws {TypeError} In case of invalid paramters. + * @throws {OS.File.Error} In case of I/O error. + */ + File.setDates = function setDates(path, accessDate, modificationDate) { + let file = File.open(path, FILE_SETDATES_MODE, FILE_SETDATES_OPTIONS); + try { + return file.setDates(accessDate, modificationDate); + } finally { + file.close(); + } + }; + // All of the following is required to ensure that File.setDates + // also works on directories. + const FILE_SETDATES_MODE = { + write: true, + }; + const FILE_SETDATES_OPTIONS = { + winAccess: Const.GENERIC_WRITE, + // Directories can only be opened with backup semantics(!) + winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS, + winDisposition: Const.OPEN_EXISTING, + }; + + File.read = exports.OS.Shared.AbstractFile.read; + File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic; + File.openUnique = exports.OS.Shared.AbstractFile.openUnique; + File.makeDir = exports.OS.Shared.AbstractFile.makeDir; + + /** + * Remove an existing directory and its contents. + * + * @param {string} path The name of the directory. + * @param {*=} options Additional options. + * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't + * exist. |true| by default. + * - {boolean} ignorePermissions If |true|, remove the file even when lacking write + * permission. + * + * @throws {OS.File.Error} In case of I/O error, in particular if |path| is + * not a directory. + */ + File.removeDir = function(path, options = {}) { + // We can't use File.stat here because it will follow the symlink. + let attributes = WinFile.GetFileAttributes(path); + if (attributes == Const.INVALID_FILE_ATTRIBUTES) { + if ( + (!("ignoreAbsent" in options) || options.ignoreAbsent) && + ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND + ) { + return; + } + throw new File.Error("removeEmptyDir", ctypes.winLastError, path); + } + if (attributes & Const.FILE_ATTRIBUTE_REPARSE_POINT) { + // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to + // directories are directories themselves. OS.File.remove() + // will not work for them. + OS.File.removeEmptyDir(path, options); + return; + } + exports.OS.Shared.AbstractFile.removeRecursive(path, options); + }; + + /** + * Get the current directory by getCurrentDirectory. + */ + File.getCurrentDirectory = function getCurrentDirectory() { + // This function is more complicated than one could hope. + // + // This is due to two facts: + // - the maximal length of a path under Windows is not completely + // specified (there is a constant MAX_PATH, but it is quite possible + // to create paths that are much larger, see bug 744413); + // - if we attempt to call |GetCurrentDirectory| with a buffer that + // is too short, it returns the length of the current directory, but + // this length might be insufficient by the time we can call again + // the function with a larger buffer, in the (unlikely but possible) + // case in which the process changes directory to a directory with + // a longer name between both calls. + // + let buffer_size = 4096; + while (true) { + let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))(); + let expected_size = throw_on_zero( + "getCurrentDirectory", + WinFile.GetCurrentDirectory(buffer_size, array) + ); + if (expected_size <= buffer_size) { + return array.readString(); + } + // At this point, we are in a case in which our buffer was not + // large enough to hold the name of the current directory. + // Consequently, we need to increase the size of the buffer. + // Note that, even in crazy scenarios, the loop will eventually + // converge, as the length of the paths cannot increase infinitely. + buffer_size = expected_size + 1 /* to store \0 */; + } + }; + + /** + * Get/set the current directory by |curDir|. + */ + Object.defineProperty(File, "curDir", { + set(path) { + this.setCurrentDirectory(path); + }, + get() { + return this.getCurrentDirectory(); + }, + }); + + // Utility functions, used for error-handling + + /** + * Turn the result of |open| into an Error or a File + * @param {number} maybe The result of the |open| operation that may + * represent either an error or a success. If -1, this function raises + * an error holding ctypes.winLastError, otherwise it returns the opened file. + * @param {string=} path The path of the file. + */ + function error_or_file(maybe, path) { + if (maybe == Const.INVALID_HANDLE_VALUE) { + throw new File.Error("open", ctypes.winLastError, path); + } + return new File(maybe, path); + } + + /** + * Utility function to sort errors represented as "0" from successes. + * + * @param {string=} operation The name of the operation. If unspecified, + * the name of the caller function. + * @param {number} result The result of the operation that may + * represent either an error or a success. If 0, this function raises + * an error holding ctypes.winLastError, otherwise it returns |result|. + * @param {string=} path The path of the file. + */ + function throw_on_zero(operation, result, path) { + if (result == 0) { + throw new File.Error(operation, ctypes.winLastError, path); + } + return result; + } + + /** + * Helper used by both versions of setPermissions + */ + function toFileAttributes(winAttributes, oldDwAttrs) { + if ("readOnly" in winAttributes) { + if (winAttributes.readOnly) { + oldDwAttrs |= Const.FILE_ATTRIBUTE_READONLY; + } else { + oldDwAttrs &= ~Const.FILE_ATTRIBUTE_READONLY; + } + } + if ("system" in winAttributes) { + if (winAttributes.system) { + oldDwAttrs |= Const.FILE_ATTRIBUTE_SYSTEM; + } else { + oldDwAttrs &= ~Const.FILE_ATTRIBUTE_SYSTEM; + } + } + if ("hidden" in winAttributes) { + if (winAttributes.hidden) { + oldDwAttrs |= Const.FILE_ATTRIBUTE_HIDDEN; + } else { + oldDwAttrs &= ~Const.FILE_ATTRIBUTE_HIDDEN; + } + } + return oldDwAttrs; + } + + File.Win = exports.OS.Win.File; + File.Error = SysAll.Error; + exports.OS.File = File; + exports.OS.Shared.Type = Type; + + Object.defineProperty(File, "POS_START", { value: SysAll.POS_START }); + Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT }); + Object.defineProperty(File, "POS_END", { value: SysAll.POS_END }); + })(this); +} diff --git a/toolkit/components/osfile/modules/ospath.jsm b/toolkit/components/osfile/modules/ospath.jsm new file mode 100644 index 000000000000..3e06f9705df8 --- /dev/null +++ b/toolkit/components/osfile/modules/ospath.jsm @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Handling native paths. + * + * This module contains a number of functions destined to simplify + * working with native paths through a cross-platform API. Functions + * of this module will only work with the following assumptions: + * + * - paths are valid; + * - paths are defined with one of the grammars that this module can + * parse (see later); + * - all path concatenations go through function |join|. + */ + +/* global OS */ +/* eslint-env node */ + +"use strict"; + +if (typeof Components == "undefined") { + let Path; + if (OS.Constants.Win) { + Path = require("resource://gre/modules/osfile/ospath_win.jsm"); + } else { + Path = require("resource://gre/modules/osfile/ospath_unix.jsm"); + } + module.exports = Path; +} else { + let Scope = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm" + ); + + let Path; + if (Scope.OS.Constants.Win) { + Path = ChromeUtils.import("resource://gre/modules/osfile/ospath_win.jsm"); + } else { + Path = ChromeUtils.import("resource://gre/modules/osfile/ospath_unix.jsm"); + } + + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = []; + for (let k in Path) { + EXPORTED_SYMBOLS.push(k); + // eslint-disable-next-line mozilla/reject-global-this + this[k] = Path[k]; + } +} diff --git a/toolkit/components/osfile/modules/ospath_unix.jsm b/toolkit/components/osfile/modules/ospath_unix.jsm new file mode 100644 index 000000000000..065c2ee8ea1d --- /dev/null +++ b/toolkit/components/osfile/modules/ospath_unix.jsm @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Handling native paths. + * + * This module contains a number of functions destined to simplify + * working with native paths through a cross-platform API. Functions + * of this module will only work with the following assumptions: + * + * - paths are valid; + * - paths are defined with one of the grammars that this module can + * parse (see later); + * - all path concatenations go through function |join|. + */ + +"use strict"; + +// Boilerplate used to be able to import this module both from the main +// thread and from worker threads. +if (typeof Components != "undefined") { + // Global definition of |exports|, to keep everybody happy. + // In non-main thread, |exports| is provided by the module + // loader. + // eslint-disable-next-line mozilla/reject-global-this + this.exports = {}; +} else if (typeof module == "undefined" || typeof exports == "undefined") { + throw new Error("Please load this module using require()"); +} + +var EXPORTED_SYMBOLS = [ + "basename", + "dirname", + "join", + "normalize", + "split", + "toFileURI", + "fromFileURI", +]; + +/** + * Return the final part of the path. + * The final part of the path is everything after the last "/". + */ +var basename = function(path) { + return path.slice(path.lastIndexOf("/") + 1); +}; +exports.basename = basename; + +/** + * Return the directory part of the path. + * The directory part of the path is everything before the last + * "/". If the last few characters of this part are also "/", + * they are ignored. + * + * If the path contains no directory, return ".". + */ +var dirname = function(path) { + let index = path.lastIndexOf("/"); + if (index == -1) { + return "."; + } + while (index >= 0 && path[index] == "/") { + --index; + } + return path.slice(0, index + 1); +}; +exports.dirname = dirname; + +/** + * Join path components. + * This is the recommended manner of getting the path of a file/subdirectory + * in a directory. + * + * Example: Obtaining $TMP/foo/bar in an OS-independent manner + * var tmpDir = OS.Constants.Path.tmpDir; + * var path = OS.Path.join(tmpDir, "foo", "bar"); + * + * Under Unix, this will return "/tmp/foo/bar". + * + * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the + * same as `OS.Path.join("foo", "bar")`. + */ +var join = function(...path) { + // If there is a path that starts with a "/", eliminate everything before + let paths = []; + for (let subpath of path) { + if (subpath == null) { + throw new TypeError("invalid path component"); + } + if (!subpath.length) { + continue; + } else if (subpath[0] == "/") { + paths = [subpath]; + } else { + paths.push(subpath); + } + } + return paths.join("/"); +}; +exports.join = join; + +/** + * Normalize a path by removing any unneeded ".", "..", "//". + */ +var normalize = function(path) { + let stack = []; + let absolute; + if (path.length >= 0 && path[0] == "/") { + absolute = true; + } else { + absolute = false; + } + path.split("/").forEach(function(v) { + switch (v) { + case "": + case ".": // fallthrough + break; + case "..": + if (!stack.length) { + if (absolute) { + throw new Error("Path is ill-formed: attempting to go past root"); + } else { + stack.push(".."); + } + } else if (stack[stack.length - 1] == "..") { + stack.push(".."); + } else { + stack.pop(); + } + break; + default: + stack.push(v); + } + }); + let string = stack.join("/"); + return absolute ? "/" + string : string; +}; +exports.normalize = normalize; + +/** + * Return the components of a path. + * You should generally apply this function to a normalized path. + * + * @return {{ + * {bool} absolute |true| if the path is absolute, |false| otherwise + * {array} components the string components of the path + * }} + * + * Other implementations may add additional OS-specific informations. + */ +var split = function(path) { + return { + absolute: path.length && path[0] == "/", + components: path.split("/"), + }; +}; +exports.split = split; + +/** + * Returns the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +var toFileURIExtraEncodings = { ";": "%3b", "?": "%3F", "#": "%23" }; +var toFileURI = function toFileURI(path) { + // Per https://url.spec.whatwg.org we should not encode [] in the path + let dontNeedEscaping = { "%5B": "[", "%5D": "]" }; + let uri = encodeURI(this.normalize(path)).replace( + /%(5B|5D)/gi, + match => dontNeedEscaping[match] + ); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file://"; + uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]); + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +var fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != "file:") { + throw new Error("fromFileURI expects a file URI"); + } + let path = this.normalize(decodeURIComponent(url.pathname)); + return path; +}; +exports.fromFileURI = fromFileURI; + +// ////////// Boilerplate +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + // eslint-disable-next-line mozilla/reject-global-this + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/ospath_win.jsm b/toolkit/components/osfile/modules/ospath_win.jsm new file mode 100644 index 000000000000..57fb7429eeb4 --- /dev/null +++ b/toolkit/components/osfile/modules/ospath_win.jsm @@ -0,0 +1,382 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Handling native paths. + * + * This module contains a number of functions destined to simplify + * working with native paths through a cross-platform API. Functions + * of this module will only work with the following assumptions: + * + * - paths are valid; + * - paths are defined with one of the grammars that this module can + * parse (see later); + * - all path concatenations go through function |join|. + * + * Limitations of this implementation. + * + * Windows supports 6 distinct grammars for paths. For the moment, this + * implementation supports the following subset: + * + * - drivename:backslash-separated components + * - backslash-separated components + * - \\drivename\ followed by backslash-separated components + * + * Additionally, |normalize| can convert a path containing slash- + * separated components to a path containing backslash-separated + * components. + */ + +"use strict"; + +// Boilerplate used to be able to import this module both from the main +// thread and from worker threads. +if (typeof Components != "undefined") { + // Global definition of |exports|, to keep everybody happy. + // In non-main thread, |exports| is provided by the module + // loader. + // eslint-disable-next-line mozilla/reject-global-this + this.exports = {}; +} else if (typeof module == "undefined" || typeof exports == "undefined") { + throw new Error("Please load this module using require()"); +} + +var EXPORTED_SYMBOLS = [ + "basename", + "dirname", + "join", + "normalize", + "split", + "winGetDrive", + "winIsAbsolute", + "toFileURI", + "fromFileURI", +]; + +/** + * Return the final part of the path. + * The final part of the path is everything after the last "\\". + */ +var basename = function(path) { + if (path.startsWith("\\\\")) { + // UNC-style path + let index = path.lastIndexOf("\\"); + if (index != 1) { + return path.slice(index + 1); + } + return ""; // Degenerate case + } + return path.slice( + Math.max(path.lastIndexOf("\\"), path.lastIndexOf(":")) + 1 + ); +}; +exports.basename = basename; + +/** + * Return the directory part of the path. + * + * If the path contains no directory, return the drive letter, + * or "." if the path contains no drive letter or if option + * |winNoDrive| is set. + * + * Otherwise, return everything before the last backslash, + * including the drive/server name. + * + * + * @param {string} path The path. + * @param {*=} options Platform-specific options controlling the behavior + * of this function. This implementation supports the following options: + * - |winNoDrive| If |true|, also remove the letter from the path name. + */ +var dirname = function(path, options) { + let noDrive = options && options.winNoDrive; + + // Find the last occurrence of "\\" + let index = path.lastIndexOf("\\"); + if (index == -1) { + // If there is no directory component... + if (!noDrive) { + // Return the drive path if possible, falling back to "." + return this.winGetDrive(path) || "."; + } + // Or just "." + return "."; + } + + if (index == 1 && path.charAt(0) == "\\") { + // The path is reduced to a UNC drive + if (noDrive) { + return "."; + } + return path; + } + + // Ignore any occurrence of "\\: immediately before that one + while (index >= 0 && path[index] == "\\") { + --index; + } + + // Compute what is left, removing the drive name if necessary + let start; + if (noDrive) { + start = (this.winGetDrive(path) || "").length; + } else { + start = 0; + } + return path.slice(start, index + 1); +}; +exports.dirname = dirname; + +/** + * Join path components. + * This is the recommended manner of getting the path of a file/subdirectory + * in a directory. + * + * Example: Obtaining $TMP/foo/bar in an OS-independent manner + * var tmpDir = OS.Constants.Path.tmpDir; + * var path = OS.Path.join(tmpDir, "foo", "bar"); + * + * Under Windows, this will return "$TMP\foo\bar". + * + * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the + * same as `OS.Path.join("foo", "bar")`. + */ +var join = function(...path) { + let paths = []; + let root; + let absolute = false; + for (let subpath of path) { + if (subpath == null) { + throw new TypeError("invalid path component"); + } + if (subpath == "") { + continue; + } + let drive = this.winGetDrive(subpath); + if (drive) { + root = drive; + let component = trimBackslashes(subpath.slice(drive.length)); + if (component) { + paths = [component]; + } else { + paths = []; + } + absolute = true; + } else if (this.winIsAbsolute(subpath)) { + paths = [trimBackslashes(subpath)]; + absolute = true; + } else { + paths.push(trimBackslashes(subpath)); + } + } + let result = ""; + if (root) { + result += root; + } + if (absolute) { + result += "\\"; + } + result += paths.join("\\"); + return result; +}; +exports.join = join; + +/** + * Return the drive name of a path, or |null| if the path does + * not contain a drive name. + * + * Drive name appear either as "DriveName:..." (the return drive + * name includes the ":") or "\\\\DriveName..." (the returned drive name + * includes "\\\\"). + */ +var winGetDrive = function(path) { + if (path == null) { + throw new TypeError("path is invalid"); + } + + if (path.startsWith("\\\\")) { + // UNC path + if (path.length == 2) { + return null; + } + let index = path.indexOf("\\", 2); + if (index == -1) { + return path; + } + return path.slice(0, index); + } + // Non-UNC path + let index = path.indexOf(":"); + if (index <= 0) { + return null; + } + return path.slice(0, index + 1); +}; +exports.winGetDrive = winGetDrive; + +/** + * Return |true| if the path is absolute, |false| otherwise. + * + * We consider that a path is absolute if it starts with "\\" + * or "driveletter:\\". + */ +var winIsAbsolute = function(path) { + let index = path.indexOf(":"); + return path.length > index + 1 && path[index + 1] == "\\"; +}; +exports.winIsAbsolute = winIsAbsolute; + +/** + * Normalize a path by removing any unneeded ".", "..", "\\". + * Also convert any "/" to a "\\". + */ +var normalize = function(path) { + let stack = []; + + if (!path.startsWith("\\\\")) { + // Normalize "/" to "\\" + path = path.replace(/\//g, "\\"); + } + + // Remove the drive (we will put it back at the end) + let root = this.winGetDrive(path); + if (root) { + path = path.slice(root.length); + } + + // Remember whether we need to restore a leading "\\" or drive name. + let absolute = this.winIsAbsolute(path); + + // And now, fill |stack| from the components, + // popping whenever there is a ".." + path.split("\\").forEach(function loop(v) { + switch (v) { + case "": + case ".": // Ignore + break; + case "..": + if (!stack.length) { + if (absolute) { + throw new Error("Path is ill-formed: attempting to go past root"); + } else { + stack.push(".."); + } + } else if (stack[stack.length - 1] == "..") { + stack.push(".."); + } else { + stack.pop(); + } + break; + default: + stack.push(v); + } + }); + + // Put everything back together + let result = stack.join("\\"); + if (absolute || root) { + result = "\\" + result; + } + if (root) { + result = root + result; + } + return result; +}; +exports.normalize = normalize; + +/** + * Return the components of a path. + * You should generally apply this function to a normalized path. + * + * @return {{ + * {bool} absolute |true| if the path is absolute, |false| otherwise + * {array} components the string components of the path + * {string?} winDrive the drive or server for this path + * }} + * + * Other implementations may add additional OS-specific informations. + */ +var split = function(path) { + return { + absolute: this.winIsAbsolute(path), + winDrive: this.winGetDrive(path), + components: path.split("\\"), + }; +}; +exports.split = split; + +/** + * Return the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +var toFileURIExtraEncodings = { ";": "%3b", "?": "%3F", "#": "%23" }; +var toFileURI = function toFileURI(path) { + // URI-escape forward slashes and convert backward slashes to forward + path = this.normalize(path).replace(/[\\\/]/g, m => + m == "\\" ? "/" : "%2F" + ); + // Per https://url.spec.whatwg.org we should not encode [] in the path + let dontNeedEscaping = { "%5B": "[", "%5D": "]" }; + let uri = encodeURI(path).replace( + /%(5B|5D)/gi, + match => dontNeedEscaping[match] + ); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file:///"; + uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]); + + // turn e.g., file:///C: into file:///C:/ + if (uri.charAt(uri.length - 1) === ":") { + uri += "/"; + } + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +var fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != "file:") { + throw new Error("fromFileURI expects a file URI"); + } + + // strip leading slash, since Windows paths don't start with one + uri = url.pathname.substr(1); + + let path = decodeURI(uri); + // decode a few characters where URL's parsing is overzealous + path = path.replace(/%(3b|3f|23)/gi, match => decodeURIComponent(match)); + path = this.normalize(path); + + // this.normalize() does not remove the trailing slash if the path + // component is a drive letter. eg. 'C:\'' will not get normalized. + if (path.endsWith(":\\")) { + path = path.substr(0, path.length - 1); + } + return this.normalize(path); +}; +exports.fromFileURI = fromFileURI; + +/** + * Utility function: Remove any leading/trailing backslashes + * from a string. + */ +var trimBackslashes = function trimBackslashes(string) { + return string.replace(/^\\+|\\+$/g, ""); +}; + +// ////////// Boilerplate +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + // eslint-disable-next-line mozilla/reject-global-this + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/moz.build b/toolkit/components/osfile/moz.build new file mode 100644 index 000000000000..81504c63be5c --- /dev/null +++ b/toolkit/components/osfile/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "modules", +] + +MOCHITEST_CHROME_MANIFESTS += ["tests/mochi/chrome.ini"] +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"] + +SOURCES += [ + "NativeOSFileInternals.cpp", +] + +XPIDL_MODULE = "toolkit_osfile" + +XPIDL_SOURCES += [ + "nsINativeOSFileInternals.idl", +] + +EXPORTS.mozilla += [ + "NativeOSFileInternals.h", +] + +EXTRA_JS_MODULES += [ + "osfile.jsm", +] + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "OS.File") diff --git a/toolkit/components/osfile/nsINativeOSFileInternals.idl b/toolkit/components/osfile/nsINativeOSFileInternals.idl new file mode 100644 index 000000000000..721540024fa2 --- /dev/null +++ b/toolkit/components/osfile/nsINativeOSFileInternals.idl @@ -0,0 +1,120 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * The result of a successful asynchronous operation. + */ +[scriptable, builtinclass, uuid(08B4CF29-3D65-4E79-B522-A694C322ED07)] +interface nsINativeOSFileResult: nsISupports +{ + /** + * The actual value produced by the operation. + * + * Actual type of this value depends on the options passed to the + * operation. + */ + [implicit_jscontext] + readonly attribute jsval result; + + /** + * Delay between when the operation was requested on the main thread and + * when the operation was started off main thread. + */ + readonly attribute double dispatchDurationMS; + + /** + * Duration of the off main thread execution. + */ + readonly attribute double executionDurationMS; +}; + +/** + * A callback invoked in case of success. + */ +[scriptable, function, uuid(2C1922CA-CA1B-4099-8B61-EC23CFF49412)] +interface nsINativeOSFileSuccessCallback: nsISupports +{ + void complete(in nsINativeOSFileResult result); +}; + +/** + * A callback invoked in case of error. + */ +[scriptable, function, uuid(F612E0FC-6736-4D24-AA50-FD661B3B40B6)] +interface nsINativeOSFileErrorCallback: nsISupports +{ + /** + * @param operation The name of the failed operation. Provided to aid + * debugging only, may change without notice. + * @param OSstatus The OS status of the operation (errno under Unix, + * GetLastError under Windows). + */ + void complete(in ACString operation, in long OSstatus); +}; + +/** + * A service providing native implementations of some of the features + * of OS.File. + */ +[scriptable, builtinclass, uuid(913362AD-1526-4623-9E6B-A2EB08AFBBB9)] +interface nsINativeOSFileInternalsService: nsISupports +{ + /** + * Implementation of OS.File.read + * + * @param path The absolute path to the file to read. + * @param options An object that may contain some of the following fields + * - {number} bytes The maximal number of bytes to read. + * - {string} encoding If provided, return the result as a string, decoded + * using this encoding. Otherwise, pass the result as an ArrayBuffer. + * Invalid encodings cause onError to be called with the platform-specific + * "invalid argument" constant. + * - {string} compression Unimplemented at the moment. + * @param onSuccess The success callback. + * @param onError The error callback. + */ + [implicit_jscontext] + void read(in AString path, in jsval options, + in nsINativeOSFileSuccessCallback onSuccess, + in nsINativeOSFileErrorCallback onError); + + /** + * Implementation of OS.File.writeAtomic + * + * @param path the absolute path of the file to write to. + * @param buffer the data as an array buffer to be written to the file. + * @param options An object that may contain the following fields + * - {number} bytes If provided, the number of bytes written is equal to this. + * The default value is the size of the |buffer|. + * - {string} tmpPath If provided and not null, first write to this path, and + * move to |path| after writing. + * - {string} backupPath if provided, backup file at |path| to this path + * before overwriting it. + * - {bool} flush if provided and true, flush the contents of the buffer after + * writing. This is slower, but safer. + * - {bool} noOverwrite if provided and true, do not write if a file already + * exists at |path|. + * @param onSuccess The success callback. + * @param onError The error callback. + */ + [implicit_jscontext] + void writeAtomic(in AString path, + in jsval buffer, + in jsval options, + in nsINativeOSFileSuccessCallback onSuccess, + in nsINativeOSFileErrorCallback onError); + +}; + + +%{ C++ + +#define NATIVE_OSFILE_INTERNALS_SERVICE_CID {0x63A69303,0x8A64,0x45A9,{0x84, 0x8C, 0xD4, 0xE2, 0x79, 0x27, 0x94, 0xE6}} +#define NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID "@mozilla.org/toolkit/osfile/native-internals;1" + +%} diff --git a/toolkit/components/osfile/osfile.jsm b/toolkit/components/osfile/osfile.jsm new file mode 100644 index 000000000000..2d9e096e63d9 --- /dev/null +++ b/toolkit/components/osfile/osfile.jsm @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Common front for various implementations of OS.File + */ + +if (typeof Components != "undefined") { + // eslint-disable-next-line mozilla/reject-global-this + this.EXPORTED_SYMBOLS = ["OS"]; + const { OS } = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_async_front.jsm" + ); + // eslint-disable-next-line mozilla/reject-global-this + this.OS = OS; +} else { + /* eslint-env worker */ + /* import-globals-from /toolkit/components/workerloader/require.js */ + importScripts("resource://gre/modules/workers/require.js"); + + var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); + + /* eslint-disable no-unused-vars */ + + // At this stage, we need to import all sources at once to avoid + // a unique failure on tbpl + talos that seems caused by a + // what looks like a nested event loop bug (see bug 794091). + if (SharedAll.Constants.Win) { + importScripts( + "resource://gre/modules/osfile/osfile_win_back.js", + "resource://gre/modules/osfile/osfile_shared_front.js", + "resource://gre/modules/osfile/osfile_win_front.js" + ); + } else { + importScripts( + "resource://gre/modules/osfile/osfile_unix_back.js", + "resource://gre/modules/osfile/osfile_shared_front.js", + "resource://gre/modules/osfile/osfile_unix_front.js" + ); + } + + /* eslint-enable no-unused-vars */ + + OS.Path = require("resource://gre/modules/osfile/ospath.jsm"); +} diff --git a/toolkit/components/osfile/tests/mochi/chrome.ini b/toolkit/components/osfile/tests/mochi/chrome.ini new file mode 100644 index 000000000000..c36cf2c045d7 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/chrome.ini @@ -0,0 +1,15 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + main_test_osfile_async.js + worker_test_osfile_comms.js + worker_test_osfile_front.js + worker_test_osfile_unix.js + worker_test_osfile_win.js + +[test_osfile_async.xhtml] +[test_osfile_back.xhtml] +[test_osfile_comms.xhtml] +[test_osfile_front.xhtml] +skip-if = + win11_2009 && bits == 32 # Bug 1809355 diff --git a/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js new file mode 100644 index 000000000000..0a1fe938d480 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js @@ -0,0 +1,501 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// The following are used to compare against a well-tested reference +// implementation of file I/O. +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); + +var myok = ok; +var myis = is; +var myinfo = info; +var myisnot = isnot; + +var isPromise = function ispromise(value) { + return value != null && typeof value == "object" && "then" in value; +}; + +var maketest = function(prefix, test) { + let utils = { + ok: function ok(t, m) { + myok(t, prefix + ": " + m); + }, + is: function is(l, r, m) { + myis(l, r, prefix + ": " + m); + }, + isnot: function isnot(l, r, m) { + myisnot(l, r, prefix + ": " + m); + }, + info: function info(m) { + myinfo(prefix + ": " + m); + }, + fail: function fail(m) { + utils.ok(false, m); + }, + okpromise: function okpromise(t, m) { + return t.then( + function onSuccess() { + utils.ok(true, m); + }, + function onFailure() { + utils.ok(false, m); + } + ); + }, + }; + return function runtest() { + utils.info("Entering"); + try { + let result = test.call(this, utils); + if (!isPromise(result)) { + throw new TypeError("The test did not return a promise"); + } + utils.info("This was a promise"); + // The test returns a promise + result = result.then( + function test_complete() { + utils.info("Complete"); + }, + function catch_uncaught_errors(err) { + utils.fail("Uncaught error " + err); + if (err && typeof err == "object" && "message" in err) { + utils.fail("(" + err.message + ")"); + } + if (err && typeof err == "object" && "stack" in err) { + utils.fail("at " + err.stack); + } + } + ); + return result; + } catch (x) { + utils.fail("Error " + x + " at " + x.stack); + return null; + } + }; +}; + +/** + * Fetch asynchronously the contents of a file using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} path The _absolute_ path to the file. + * @return {promise} + * @resolves {string} The contents of the file. + */ +var reference_fetch_file = function reference_fetch_file(path, test) { + test.info("Fetching file " + path); + return new Promise((resolve, reject) => { + let file = new FileUtils.File(path); + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(stream, status) { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + let result, reject; + try { + result = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (x) { + reject = x; + } + stream.close(); + if (reject) { + reject(reject); + } else { + resolve(result); + } + } + ); + }); +}; + +var reference_dir_contents = function reference_dir_contents(path) { + let result = []; + let entries = new FileUtils.File(path).directoryEntries; + while (entries.hasMoreElements()) { + let entry = entries.nextFile; + result.push(entry.path); + } + return result; +}; + +// Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener. +function toggleDebugTest(pref, consoleListener) { + Services.prefs.setBoolPref("toolkit.osfile.log", pref); + Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref); + Services.console[pref ? "registerListener" : "unregisterListener"]( + consoleListener + ); +} + +var test = maketest("Main", function main(test) { + return (async function() { + SimpleTest.waitForExplicitFinish(); + await test_stat(); + await test_debug(); + await test_info_features_detect(); + await test_position(); + await test_iter(); + await test_exists(); + await test_debug_test(); + info("Test is over"); + SimpleTest.finish(); + })(); +}); + +/** + * A file that we know exists and that can be used for reading. + */ +var EXISTING_FILE = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi", + "main_test_osfile_async.js" +); + +/** + * Test OS.File.stat and OS.File.prototype.stat + */ +var test_stat = maketest("stat", function stat(test) { + return (async function() { + // Open a file and stat it + let file = await OS.File.open(EXISTING_FILE); + let stat1; + + try { + test.info("Stating file"); + stat1 = await file.stat(); + test.ok(true, "stat has worked " + stat1); + test.ok(stat1, "stat is not empty"); + } finally { + await file.close(); + } + + // Stat the same file without opening it + test.info("Stating a file without opening it"); + let stat2 = await OS.File.stat(EXISTING_FILE); + test.ok(true, "stat 2 has worked " + stat2); + test.ok(stat2, "stat 2 is not empty"); + for (let key in stat2) { + test.is( + "" + stat1[key], + "" + stat2[key], + "Stat field " + key + "is the same" + ); + } + })(); +}); + +/** + * Test feature detection using OS.File.Info.prototype on main thread + */ +var test_info_features_detect = maketest( + "features_detect", + function features_detect(test) { + return (async function() { + if (!OS.Constants.Win && OS.Constants.libc) { + // see if unixGroup is defined + if ("unixGroup" in OS.File.Info.prototype) { + test.ok(true, "unixGroup is defined"); + } else { + test.fail("unixGroup is not defined though we are under Unix"); + } + } + })(); + } +); + +/** + * Test file.{getPosition, setPosition} + */ +var test_position = maketest("position", function position(test) { + return (async function() { + let file = await OS.File.open(EXISTING_FILE); + + try { + let view = await file.read(); + test.info("First batch of content read"); + let CHUNK_SIZE = 178; // An arbitrary number of bytes to read from the file + let pos = await file.getPosition(); + test.info("Obtained position"); + test.is(pos, view.byteLength, "getPosition returned the end of the file"); + pos = await file.setPosition(-CHUNK_SIZE, OS.File.POS_END); + test.info("Changed position"); + test.is( + pos, + view.byteLength - CHUNK_SIZE, + "setPosition returned the correct position" + ); + + let view2 = await file.read(); + test.info("Read the end of the file"); + for (let i = 0; i < CHUNK_SIZE; ++i) { + if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) { + test.is( + view2[i], + view[i], + "setPosition put us in the right position" + ); + } + } + } finally { + await file.close(); + } + })(); +}); + +/** + * Test OS.File.prototype.{DirectoryIterator} + */ +var test_iter = maketest("iter", function iter(test) { + return (async function() { + let currentDir = await OS.File.getCurrentDirectory(); + + // Trivial walks through the directory + test.info("Preparing iteration"); + let iterator = new OS.File.DirectoryIterator(currentDir); + let temporary_file_name = OS.Path.join( + currentDir, + "empty-temporary-file.tmp" + ); + try { + await OS.File.remove(temporary_file_name); + } catch (err) { + // Ignore errors removing file + } + let allFiles1 = await iterator.nextBatch(); + test.info("Obtained all files through nextBatch"); + test.isnot(allFiles1.length, 0, "There is at least one file"); + test.isnot(allFiles1[0].path, null, "Files have a path"); + + // Ensure that we have the same entries with |reference_dir_contents| + let referenceEntries = new Set(); + for (let entry of reference_dir_contents(currentDir)) { + referenceEntries.add(entry); + } + test.is( + referenceEntries.size, + allFiles1.length, + "All the entries in the directory have been listed" + ); + for (let entry of allFiles1) { + test.ok( + referenceEntries.has(entry.path), + "File " + entry.path + " effectively exists" + ); + // Ensure that we have correct isDir and isSymLink + // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks. + var f = new FileUtils.File(entry.path); + test.is( + entry.isDir, + f.isDirectory(), + "Get file " + entry.path + " isDir correctly" + ); + test.is( + entry.isSymLink, + f.isSymlink(), + "Get file " + entry.path + " isSymLink correctly" + ); + } + + await iterator.close(); + test.info("Closed iterator"); + + test.info("Double closing DirectoryIterator"); + iterator = new OS.File.DirectoryIterator(currentDir); + await iterator.close(); + await iterator.close(); // double closing |DirectoryIterator| + test.ok(true, "|DirectoryIterator| was closed twice successfully"); + + let allFiles2 = []; + let i = 0; + iterator = new OS.File.DirectoryIterator(currentDir); + await iterator.forEach(function(entry, index) { + test.is(i++, index, "Getting the correct index"); + allFiles2.push(entry); + }); + test.info("Obtained all files through forEach"); + is( + allFiles1.length, + allFiles2.length, + "Both runs returned the same number of files" + ); + for (let i = 0; i < allFiles1.length; ++i) { + if (allFiles1[i].path != allFiles2[i].path) { + test.is( + allFiles1[i].path, + allFiles2[i].path, + "Both runs return the same files" + ); + break; + } + } + + // Testing batch iteration + whether an iteration can be stopped early + let BATCH_LENGTH = 10; + test.info("Getting some files through nextBatch"); + await iterator.close(); + + iterator = new OS.File.DirectoryIterator(currentDir); + let someFiles1 = await iterator.nextBatch(BATCH_LENGTH); + let someFiles2 = await iterator.nextBatch(BATCH_LENGTH); + await iterator.close(); + + iterator = new OS.File.DirectoryIterator(currentDir); + await iterator.forEach(function cb(entry, index, iterator) { + if (index < BATCH_LENGTH) { + test.is( + entry.path, + someFiles1[index].path, + "Both runs return the same files (part 1)" + ); + } else if (index < 2 * BATCH_LENGTH) { + test.is( + entry.path, + someFiles2[index - BATCH_LENGTH].path, + "Both runs return the same files (part 2)" + ); + } else if (index == 2 * BATCH_LENGTH) { + test.info("Attempting to stop asynchronous forEach"); + return iterator.close(); + } else { + test.fail("Can we stop an asynchronous forEach? " + index); + } + return null; + }); + await iterator.close(); + + // Ensuring that we find new files if they appear + let file = await OS.File.open(temporary_file_name, { write: true }); + file.close(); + iterator = new OS.File.DirectoryIterator(currentDir); + try { + let files = await iterator.nextBatch(); + is( + files.length, + allFiles1.length + 1, + "The directory iterator has noticed the new file" + ); + let exists = await iterator.exists(); + test.ok( + exists, + "After nextBatch, iterator detects that the directory exists" + ); + } finally { + await iterator.close(); + } + + // Ensuring that opening a non-existing directory fails consistently + // once iteration starts. + try { + iterator = null; + iterator = new OS.File.DirectoryIterator("/I do not exist"); + let exists = await iterator.exists(); + test.ok( + !exists, + "Before any iteration, iterator detects that the directory doesn't exist" + ); + let exn = null; + try { + await iterator.next(); + } catch (ex) { + if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) { + exn = ex; + let exists = await iterator.exists(); + test.ok( + !exists, + "After one iteration, iterator detects that the directory doesn't exist" + ); + } else { + throw ex; + } + } + test.ok( + exn, + "Iterating through a directory that does not exist has failed with becauseNoSuchFile" + ); + } finally { + if (iterator) { + iterator.close(); + } + } + test.ok( + !!iterator, + "The directory iterator for a non-existing directory was correctly created" + ); + })(); +}); + +/** + * Test OS.File.prototype.{exists} + */ +var test_exists = maketest("exists", function exists(test) { + return (async function() { + let fileExists = await OS.File.exists(EXISTING_FILE); + test.ok(fileExists, "file exists"); + fileExists = await OS.File.exists(EXISTING_FILE + ".tmp"); + test.ok(!fileExists, "file does not exists"); + })(); +}); + +/** + * Test changes to OS.Shared.DEBUG flag. + */ +var test_debug = maketest("debug", function debug(test) { + return (async function() { + function testSetDebugPref(pref) { + try { + Services.prefs.setBoolPref("toolkit.osfile.log", pref); + } catch (x) { + test.fail( + "Setting OS.Shared.DEBUG to " + pref + " should not cause error." + ); + } finally { + test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly."); + } + } + testSetDebugPref(true); + let workerDEBUG = await OS.File.GET_DEBUG(); + test.is(workerDEBUG, true, "Worker's DEBUG is set."); + testSetDebugPref(false); + workerDEBUG = await OS.File.GET_DEBUG(); + test.is(workerDEBUG, false, "Worker's DEBUG is unset."); + })(); +}); + +/** + * Test logging in the main thread with set OS.Shared.DEBUG and + * OS.Shared.TEST flags. + */ +var test_debug_test = maketest("debug_test", function debug_test(test) { + return (async function() { + // Create a console listener. + let consoleListener = { + observe(aMessage) { + // Ignore unexpected messages. + if (!(aMessage instanceof Ci.nsIConsoleMessage)) { + return; + } + if (!aMessage.message.includes("TEST OS")) { + return; + } + test.ok(true, "DEBUG TEST messages are logged correctly."); + }, + }; + toggleDebugTest(true, consoleListener); + // Execution of OS.File.exist method will trigger OS.File.LOG several times. + await OS.File.exists(EXISTING_FILE); + toggleDebugTest(false, consoleListener); + })(); +}); diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml new file mode 100644 index 000000000000..89d8eba47353 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/test_osfile_async.xhtml @@ -0,0 +1,21 @@ + + + + + + + +

+ +

+  
+  
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml new file mode 100644 index 000000000000..fd31e7bee877 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xhtml @@ -0,0 +1,88 @@ + + + + + + + +

+ +

+  
+  
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml b/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml new file mode 100644 index 000000000000..0a13e2d8c821 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/test_osfile_front.xhtml @@ -0,0 +1,42 @@ + + + + + + + +

+ +

+  
+  
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js new file mode 100644 index 000000000000..0cc1c5933953 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker, node */ + +"use strict"; + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +// The set of samples for communications test. Declare as a global +// variable to prevent this from being garbage-collected too early. +var samples; + +self.onmessage = function(msg) { + info("Initializing"); + self.onmessage = function on_unexpected_message(msg) { + throw new Error("Unexpected message " + JSON.stringify(msg.data)); + }; + /* import-globals-from /toolkit/components/osfile/osfile.jsm */ + importScripts("resource://gre/modules/osfile.jsm"); + info("Initialization complete"); + + samples = [ + { + typename: "OS.Shared.Type.char.in_ptr", + valuedescr: "String", + value: "This is a test", + type: OS.Shared.Type.char.in_ptr, + check: function check_string(candidate, prefix) { + is(candidate, "This is a test", prefix); + }, + }, + { + typename: "OS.Shared.Type.char.in_ptr", + valuedescr: "Typed array", + value: (function() { + let view = new Uint8Array(15); + for (let i = 0; i < 15; ++i) { + view[i] = i; + } + return view; + })(), + type: OS.Shared.Type.char.in_ptr, + check: function check_ArrayBuffer(candidate, prefix) { + for (let i = 0; i < 15; ++i) { + is( + candidate[i], + i % 256, + prefix + + "Checking that the contents of the ArrayBuffer were preserved" + ); + } + }, + }, + { + typename: "OS.Shared.Type.char.in_ptr", + valuedescr: "Pointer", + value: new OS.Shared.Type.char.in_ptr.implementation(1), + type: OS.Shared.Type.char.in_ptr, + check: function check_ptr(candidate, prefix) { + let address = ctypes.cast(candidate, ctypes.uintptr_t).value.toString(); + is( + address, + "1", + prefix + "Checking that the pointer address was preserved" + ); + }, + }, + { + typename: "OS.Shared.Type.char.in_ptr", + valuedescr: "C array", + value: (function() { + let buf = new (ctypes.ArrayType(ctypes.uint8_t, 15))(); + for (let i = 0; i < 15; ++i) { + buf[i] = i % 256; + } + return buf; + })(), + type: OS.Shared.Type.char.in_ptr, + check: function check_array(candidate, prefix) { + let cast = ctypes.cast(candidate, ctypes.uint8_t.ptr); + for (let i = 0; i < 15; ++i) { + is( + cast.contents, + i % 256, + prefix + + "Checking that the contents of the C array were preserved, index " + + i + ); + cast = cast.increment(); + } + }, + }, + { + typename: "OS.File.Error", + valuedescr: "OS Error", + type: OS.File.Error, + value: new OS.File.Error("foo", 1), + check: function check_error(candidate, prefix) { + ok( + candidate instanceof OS.File.Error, + prefix + "Error is an OS.File.Error" + ); + ok( + candidate.unixErrno == 1 || candidate.winLastError == 1, + prefix + "Error code is correct" + ); + try { + let string = candidate.toString(); + info(prefix + ".toString() works " + string); + } catch (x) { + ok(false, prefix + ".toString() fails " + x); + } + }, + }, + ]; + samples.forEach(function test(sample) { + let type = sample.type; + let value = sample.value; + let check = sample.check; + info( + "Testing handling of type " + + sample.typename + + " communicating " + + sample.valuedescr + ); + + // 1. Test serialization + let serialized; + let exn; + try { + serialized = type.toMsg(value); + } catch (ex) { + exn = ex; + } + is( + exn, + null, + "Can I serialize the following value? " + + value + + " aka " + + JSON.stringify(value) + ); + if (exn) { + return; + } + + if ("data" in serialized) { + // Unwrap from `Meta` + serialized = serialized.data; + } + + // 2. Test deserialization + let deserialized; + try { + deserialized = type.fromMsg(serialized); + } catch (ex) { + exn = ex; + } + is( + exn, + null, + "Can I deserialize the following message? " + + serialized + + " aka " + + JSON.stringify(serialized) + ); + if (exn) { + return; + } + + // 3. Local test deserialized value + info( + "Running test on deserialized value " + + deserialized + + " aka " + + JSON.stringify(deserialized) + ); + check(deserialized, "Local test: "); + + // 4. Test sending serialized + info("Attempting to send message"); + try { + self.postMessage({ + kind: "value", + typename: sample.typename, + value: serialized, + check: check.toSource(), + }); + } catch (ex) { + exn = ex; + } + is( + exn, + null, + "Can I send the following message? " + + serialized + + " aka " + + JSON.stringify(serialized) + ); + }); + + finish(); +}; diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js new file mode 100644 index 000000000000..4f7e5901021c --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js @@ -0,0 +1,696 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker, node */ + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); + +var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); +SharedAll.Config.DEBUG = true; + +function should_throw(f) { + try { + f(); + } catch (x) { + return x; + } + return null; +} + +self.onmessage = function onmessage_start(msg) { + self.onmessage = function onmessage_ignored(msg) { + log("ignored message " + JSON.stringify(msg.data)); + }; + try { + test_init(); + test_open_existing_file(); + test_open_non_existing_file(); + test_flush_open_file(); + test_copy_existing_file(); + test_position(); + test_move_file(); + test_iter_dir(); + test_info(); + test_path(); + test_exists_file(); + test_remove_file(); + } catch (x) { + log("Catching error: " + x); + log("Stack: " + x.stack); + log("Source: " + x.toSource()); + ok(false, x.toString() + "\n" + x.stack); + } + finish(); +}; + +function test_init() { + info("Starting test_init"); + /* import-globals-from /toolkit/components/osfile/osfile.jsm */ + importScripts("resource://gre/modules/osfile.jsm"); +} + +/** + * Test that we can open an existing file. + */ +function test_open_existing_file() { + info("Starting test_open_existing"); + let file = OS.File.open( + "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js" + ); + file.close(); +} + +/** + * Test that opening a file that does not exist fails with the right error. + */ +function test_open_non_existing_file() { + info("Starting test_open_non_existing"); + let exn; + try { + OS.File.open("/I do not exist"); + } catch (x) { + exn = x; + info("test_open_non_existing_file: Exception detail " + exn); + } + ok(!!exn, "test_open_non_existing_file: Exception was raised "); + ok( + exn instanceof OS.File.Error, + "test_open_non_existing_file: Exception was a OS.File.Error" + ); + ok( + exn.becauseNoSuchFile, + "test_open_non_existing_file: Exception confirms that the file does not exist" + ); +} + +/** + * Test that to ensure that |foo.flush()| does not + * cause an error, where |foo| is an open file. + */ +function test_flush_open_file() { + info("Starting test_flush_open_file"); + let tmp = "test_flush.tmp"; + let file = OS.File.open(tmp, { create: true, write: true }); + file.flush(); + file.close(); + OS.File.remove(tmp); +} + +/** + * Utility function for comparing two files (or a prefix of two files). + * + * This function returns nothing but fails of both files (or prefixes) + * are not identical. + * + * @param {string} test The name of the test (used for logging). + * @param {string} sourcePath The name of the first file. + * @param {string} destPath The name of the second file. + * @param {number=} prefix If specified, only compare the |prefix| + * first bytes of |sourcePath| and |destPath|. + */ +function compare_files(test, sourcePath, destPath, prefix) { + info(test + ": Comparing " + sourcePath + " and " + destPath); + let source = OS.File.open(sourcePath); + let dest = OS.File.open(destPath); + info("Files are open"); + let sourceResult, destResult; + try { + if (prefix != undefined) { + sourceResult = source.read(prefix); + destResult = dest.read(prefix); + } else { + sourceResult = source.read(); + destResult = dest.read(); + } + is( + sourceResult.length, + destResult.length, + test + ": Both files have the same size" + ); + for (let i = 0; i < sourceResult.length; ++i) { + if (sourceResult[i] != destResult[i]) { + is(sourceResult[i] != destResult[i], test + ": Comparing char " + i); + break; + } + } + } finally { + source.close(); + dest.close(); + } + info(test + ": Comparison complete"); +} + +/** + * Test that copying a file using |copy| works. + */ +function test_copy_existing_file() { + let src_file_name = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi", + "worker_test_osfile_front.js" + ); + let tmp_file_name = "test_osfile_front.tmp"; + info("Starting test_copy_existing"); + OS.File.copy(src_file_name, tmp_file_name); + + info("test_copy_existing: Copy complete"); + compare_files("test_copy_existing", src_file_name, tmp_file_name); + + // Create a bogus file with arbitrary content, then attempt to overwrite + // it with |copy|. + let dest = OS.File.open(tmp_file_name, { trunc: true }); + let buf = new Uint8Array(50); + dest.write(buf); + dest.close(); + + OS.File.copy(src_file_name, tmp_file_name); + + compare_files("test_copy_existing 2", src_file_name, tmp_file_name); + + // Attempt to overwrite with noOverwrite + let exn; + try { + OS.File.copy(src_file_name, tmp_file_name, { noOverwrite: true }); + } catch (x) { + exn = x; + } + ok( + !!exn, + "test_copy_existing: noOverwrite prevents overwriting existing files" + ); + + info("test_copy_existing: Cleaning up"); + OS.File.remove(tmp_file_name); +} + +/** + * Test that moving a file works. + */ +function test_move_file() { + info("test_move_file: Starting"); + // 1. Copy file into a temporary file + let src_file_name = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi", + "worker_test_osfile_front.js" + ); + let tmp_file_name = "test_osfile_front.tmp"; + let tmp2_file_name = "test_osfile_front.tmp2"; + OS.File.copy(src_file_name, tmp_file_name); + + info("test_move_file: Copy complete"); + + // 2. Move + OS.File.move(tmp_file_name, tmp2_file_name); + + info("test_move_file: Move complete"); + + // 3. Check that destination exists + compare_files("test_move_file", src_file_name, tmp2_file_name); + + // 4. Check that original file does not exist anymore + let exn; + try { + OS.File.open(tmp_file_name); + } catch (x) { + exn = x; + } + ok(!!exn, "test_move_file: Original file has been removed"); + + info("test_move_file: Cleaning up"); + OS.File.remove(tmp2_file_name); +} + +function test_iter_dir() { + info("test_iter_dir: Starting"); + + // Create a file, to be sure that it exists + let tmp_file_name = "test_osfile_front.tmp"; + let tmp_file = OS.File.open(tmp_file_name, { write: true, trunc: true }); + tmp_file.close(); + + let parent = OS.File.getCurrentDirectory(); + info("test_iter_dir: directory " + parent); + let iterator = new OS.File.DirectoryIterator(parent); + info("test_iter_dir: iterator created"); + let encountered_tmp_file = false; + for (let entry of iterator) { + // Checking that |name| can be decoded properly + info("test_iter_dir: encountering entry " + entry.name); + + if (entry.name == tmp_file_name) { + encountered_tmp_file = true; + isnot( + entry.isDir, + "test_iter_dir: The temporary file is not a directory" + ); + isnot(entry.isSymLink, "test_iter_dir: The temporary file is not a link"); + } + + let file; + let success = true; + try { + file = OS.File.open(entry.path); + } catch (x) { + if (x.becauseNoSuchFile) { + success = false; + } + } + if (file) { + file.close(); + } + ok(success, "test_iter_dir: Entry " + entry.path + " exists"); + + if (OS.Win) { + // We assume that the files are at least as recent as 2009. + // Since this test was written in 2011 and some of our packaging + // sets dates arbitrarily to 2010, this should be safe. + let year = new Date().getFullYear(); + + let lastWrite = entry.winLastWriteDate; + ok( + lastWrite, + "test_iter_dir: Windows lastWrite date exists: " + lastWrite + ); + ok( + lastWrite.getFullYear() >= 2009 && lastWrite.getFullYear() <= year, + "test_iter_dir: consistent lastWrite date" + ); + + let lastAccess = entry.winLastAccessDate; + ok( + lastAccess, + "test_iter_dir: Windows lastAccess date exists: " + lastAccess + ); + ok( + lastAccess.getFullYear() >= 2009 && lastAccess.getFullYear() <= year, + "test_iter_dir: consistent lastAccess date" + ); + } + } + ok(encountered_tmp_file, "test_iter_dir: We have found the temporary file"); + + info("test_iter_dir: Cleaning up"); + iterator.close(); + + // Testing nextBatch() + iterator = new OS.File.DirectoryIterator(parent); + let allentries = []; + for (let x of iterator) { + allentries.push(x); + } + iterator.close(); + + ok( + allentries.length >= 14, + "test_iter_dir: Meta-check: the test directory should contain at least 14 items" + ); + + iterator = new OS.File.DirectoryIterator(parent); + let firstten = iterator.nextBatch(10); + is(firstten.length, 10, "test_iter_dir: nextBatch(10) returns 10 items"); + for (let i = 0; i < firstten.length; ++i) { + is( + allentries[i].path, + firstten[i].path, + "test_iter_dir: Checking that batch returns the correct entries" + ); + } + let nextthree = iterator.nextBatch(3); + is(nextthree.length, 3, "test_iter_dir: nextBatch(3) returns 3 items"); + for (let i = 0; i < nextthree.length; ++i) { + is( + allentries[i + firstten.length].path, + nextthree[i].path, + "test_iter_dir: Checking that batch 2 returns the correct entries" + ); + } + let everythingelse = iterator.nextBatch(); + ok( + everythingelse.length >= 1, + "test_iter_dir: nextBatch() returns at least one item" + ); + for (let i = 0; i < everythingelse.length; ++i) { + is( + allentries[i + firstten.length + nextthree.length].path, + everythingelse[i].path, + "test_iter_dir: Checking that batch 3 returns the correct entries" + ); + } + is( + iterator.nextBatch().length, + 0, + "test_iter_dir: Once there is nothing left, nextBatch returns an empty array" + ); + iterator.close(); + + iterator = new OS.File.DirectoryIterator(parent); + iterator.close(); + is( + iterator.nextBatch().length, + 0, + "test_iter_dir: nextBatch on closed iterator returns an empty array" + ); + + iterator = new OS.File.DirectoryIterator(parent); + let allentries2 = iterator.nextBatch(); + is( + allentries.length, + allentries2.length, + "test_iter_dir: Checking that getBatch(null) returns the right number of entries" + ); + for (let i = 0; i < allentries.length; ++i) { + is( + allentries[i].path, + allentries2[i].path, + "test_iter_dir: Checking that getBatch(null) returns everything in the right order" + ); + } + iterator.close(); + + // Test forEach + iterator = new OS.File.DirectoryIterator(parent); + let index = 0; + iterator.forEach(function cb(entry, aIndex, aIterator) { + is(index, aIndex, "test_iter_dir: Checking that forEach index is correct"); + ok( + iterator == aIterator, + "test_iter_dir: Checking that right iterator is passed" + ); + if (index < 10) { + is( + allentries[index].path, + entry.path, + "test_iter_dir: Checking that forEach entry is correct" + ); + } else if (index == 10) { + iterator.close(); + } else { + ok(false, "test_iter_dir: Checking that forEach can be stopped early"); + } + ++index; + }); + iterator.close(); + + // test for prototype |OS.File.DirectoryIterator.unixAsFile| + if ("unixAsFile" in OS.File.DirectoryIterator.prototype) { + info("testing property unixAsFile"); + let path = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi" + ); + iterator = new OS.File.DirectoryIterator(path); + + let dir_file = iterator.unixAsFile(); // return |File| + let stat0 = dir_file.stat(); + let stat1 = OS.File.stat(path); + + let unix_info_to_string = function unix_info_to_string(info) { + return ( + "| " + + info.unixMode + + " | " + + info.unixOwner + + " | " + + info.unixGroup + + " | " + + info.lastModificationDate + + " | " + + info.lastAccessDate + + " | " + + info.size + + " |" + ); + }; + + let s0_string = unix_info_to_string(stat0); + let s1_string = unix_info_to_string(stat1); + + ok(stat0.isDir, "unixAsFile returned a directory"); + is(s0_string, s1_string, "unixAsFile returned the correct file"); + dir_file.close(); + iterator.close(); + } + info("test_iter_dir: Complete"); +} + +function test_position() { + info("test_position: Starting"); + + ok("POS_START" in OS.File, "test_position: POS_START exists"); + ok("POS_CURRENT" in OS.File, "test_position: POS_CURRENT exists"); + ok("POS_END" in OS.File, "test_position: POS_END exists"); + + let ARBITRARY_POSITION = 321; + let src_file_name = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi", + "worker_test_osfile_front.js" + ); + + let file = OS.File.open(src_file_name); + is(file.getPosition(), 0, "test_position: Initial position is 0"); + + let size = 0 + file.stat().size; // Hack: We can remove this 0 + once 776259 has landed + + file.setPosition(ARBITRARY_POSITION, OS.File.POS_START); + is( + file.getPosition(), + ARBITRARY_POSITION, + "test_position: Setting position from start" + ); + + file.setPosition(0, OS.File.POS_START); + is( + file.getPosition(), + 0, + "test_position: Setting position from start back to 0" + ); + + file.setPosition(ARBITRARY_POSITION); + is( + file.getPosition(), + ARBITRARY_POSITION, + "test_position: Setting position without argument" + ); + + file.setPosition(-ARBITRARY_POSITION, OS.File.POS_END); + is( + file.getPosition(), + size - ARBITRARY_POSITION, + "test_position: Setting position from end" + ); + + file.setPosition(ARBITRARY_POSITION, OS.File.POS_CURRENT); + is(file.getPosition(), size, "test_position: Setting position from current"); + + file.close(); + info("test_position: Complete"); +} + +function test_info() { + info("test_info: Starting"); + + let filename = "test_info.tmp"; + let size = 261; // An arbitrary file length + let start = new Date(); + + // Cleanup any leftover from previous tests + try { + OS.File.remove(filename); + info("test_info: Cleaned up previous garbage"); + } catch (x) { + if (!x.becauseNoSuchFile) { + throw x; + } + info("test_info: No previous garbage"); + } + + let file = OS.File.open(filename, { trunc: true }); + let buf = new ArrayBuffer(size); + file._write(buf, size); + file.close(); + + // Test OS.File.stat on new file + let stat = OS.File.stat(filename); + ok(!!stat, "test_info: info acquired"); + ok(!stat.isDir, "test_info: file is not a directory"); + is(stat.isSymLink, false, "test_info: file is not a link"); + is(stat.size.toString(), size, "test_info: correct size"); + + let stop = new Date(); + + // We round down/up by 1s as file system precision is lower than + // Date precision (no clear specifications about that, but it seems + // that this can be a little over 1 second under ext3 and 2 seconds + // under FAT). + let SLOPPY_FILE_SYSTEM_ADJUSTMENT = 3000; + let startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT; + let stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT; + info("Testing stat with bounds [ " + startMs + ", " + stopMs + " ]"); + + let change = stat.lastModificationDate; + info("Testing lastModificationDate: " + change); + ok( + change.getTime() >= startMs && change.getTime() <= stopMs, + "test_info: lastModificationDate is consistent" + ); + + // Test OS.File.prototype.stat on new file + file = OS.File.open(filename); + try { + stat = file.stat(); + } finally { + file.close(); + } + + ok(!!stat, "test_info: info acquired 2"); + ok(!stat.isDir, "test_info: file is not a directory 2"); + ok(!stat.isSymLink, "test_info: file is not a link 2"); + is(stat.size.toString(), size, "test_info: correct size 2"); + + stop = new Date(); + + // Round up/down as above + startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT; + stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT; + info("Testing stat 2 with bounds [ " + startMs + ", " + stopMs + " ]"); + + let access = stat.lastAccessDate; + info("Testing lastAccessDate: " + access); + ok( + access.getTime() >= startMs && access.getTime() <= stopMs, + "test_info: lastAccessDate is consistent" + ); + + change = stat.lastModificationDate; + info("Testing lastModificationDate 2: " + change); + ok( + change.getTime() >= startMs && change.getTime() <= stopMs, + "test_info: lastModificationDate 2 is consistent" + ); + + // Test OS.File.stat on directory + stat = OS.File.stat(OS.File.getCurrentDirectory()); + ok(!!stat, "test_info: info on directory acquired"); + ok(stat.isDir, "test_info: directory is a directory"); + + info("test_info: Complete"); +} + +// Note that most of the features of path are tested in +// worker_test_osfile_{unix, win}.js +function test_path() { + info("test_path: starting"); + let abcd = OS.Path.join("a", "b", "c", "d"); + is(OS.Path.basename(abcd), "d", "basename of a/b/c/d"); + + let abc = OS.Path.join("a", "b", "c"); + is(OS.Path.dirname(abcd), abc, "dirname of a/b/c/d"); + + let abdotsc = OS.Path.join("a", "b", "..", "c"); + is(OS.Path.normalize(abdotsc), OS.Path.join("a", "c"), "normalize a/b/../c"); + + let adotsdotsdots = OS.Path.join("a", "..", "..", ".."); + is( + OS.Path.normalize(adotsdotsdots), + OS.Path.join("..", ".."), + "normalize a/../../.." + ); + + info("test_path: Complete"); +} + +/** + * Test the file |exists| method. + */ +function test_exists_file() { + let file_name = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi", + "test_osfile_front.xhtml" + ); + info("test_exists_file: starting"); + ok( + OS.File.exists(file_name), + "test_exists_file: file exists (OS.File.exists)" + ); + ok( + !OS.File.exists(file_name + ".tmp"), + "test_exists_file: file does not exists (OS.File.exists)" + ); + + let dir_name = OS.Path.join( + "chrome", + "toolkit", + "components", + "osfile", + "tests", + "mochi" + ); + ok(OS.File.exists(dir_name), "test_exists_file: directory exists"); + ok( + !OS.File.exists(dir_name) + ".tmp", + "test_exists_file: directory does not exist" + ); + + info("test_exists_file: complete"); +} + +/** + * Test the file |remove| method. + */ +function test_remove_file() { + let absent_file_name = "test_osfile_front_absent.tmp"; + + // Check that removing absent files is handled correctly + let exn = should_throw(function() { + OS.File.remove(absent_file_name, { ignoreAbsent: false }); + }); + ok(!!exn, "test_remove_file: throws if there is no such file"); + + exn = should_throw(function() { + OS.File.remove(absent_file_name, { ignoreAbsent: true }); + OS.File.remove(absent_file_name); + }); + ok(!exn, "test_remove_file: ignoreAbsent works"); + + if (OS.Win) { + let file_name = "test_osfile_front_file_to_remove.tmp"; + let file = OS.File.open(file_name, { write: true }); + file.close(); + ok(OS.File.exists(file_name), "test_remove_file: test file exists"); + OS.Win.File.SetFileAttributes( + file_name, + OS.Constants.Win.FILE_ATTRIBUTE_READONLY + ); + OS.File.remove(file_name); + ok( + !OS.File.exists(file_name), + "test_remove_file: test file has been removed" + ); + } +} diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js new file mode 100644 index 000000000000..aa456da030fe --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker, node */ + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +self.onmessage = function(msg) { + log("received message " + JSON.stringify(msg.data)); + self.onmessage = function(msg) { + log("ignored message " + JSON.stringify(msg.data)); + }; + test_init(); + test_getcwd(); + test_open_close(); + test_create_file(); + test_access(); + test_read_write(); + test_passing_undefined(); + finish(); +}; + +function test_init() { + info("Starting test_init"); + /* import-globals-from /toolkit/components/osfile/osfile.jsm */ + importScripts("resource://gre/modules/osfile.jsm"); +} + +function test_open_close() { + info("Starting test_open_close"); + is(typeof OS.Unix.File.open, "function", "OS.Unix.File.open is a function"); + let file = OS.Unix.File.open( + "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js", + OS.Constants.libc.O_RDONLY + ); + isnot(file, -1, "test_open_close: opening succeeded"); + info("Close: " + OS.Unix.File.close.toSource()); + let result = OS.Unix.File.close(file); + is(result, 0, "test_open_close: close succeeded"); + + file = OS.Unix.File.open("/i do not exist", OS.Constants.libc.O_RDONLY); + is(file, -1, "test_open_close: opening of non-existing file failed"); + is( + ctypes.errno, + OS.Constants.libc.ENOENT, + "test_open_close: error is ENOENT" + ); +} + +function test_create_file() { + info("Starting test_create_file"); + let file = OS.Unix.File.open( + "test.tmp", + OS.Constants.libc.O_RDWR | + OS.Constants.libc.O_CREAT | + OS.Constants.libc.O_TRUNC, + ctypes.int(OS.Constants.libc.S_IRWXU) + ); + isnot(file, -1, "test_create_file: file created"); + OS.Unix.File.close(file); +} + +function test_access() { + info("Starting test_access"); + let file = OS.Unix.File.open( + "test1.tmp", + OS.Constants.libc.O_RDWR | + OS.Constants.libc.O_CREAT | + OS.Constants.libc.O_TRUNC, + ctypes.int(OS.Constants.libc.S_IRWXU) + ); + let result = OS.Unix.File.access( + "test1.tmp", + OS.Constants.libc.R_OK | + OS.Constants.libc.W_OK | + OS.Constants.libc.X_OK | + OS.Constants.libc.F_OK + ); + is(result, 0, "first call to access() succeeded"); + OS.Unix.File.close(file); + + file = OS.Unix.File.open( + "test1.tmp", + OS.Constants.libc.O_WRONLY | + OS.Constants.libc.O_CREAT | + OS.Constants.libc.O_TRUNC, + ctypes.int(OS.Constants.libc.S_IWUSR) + ); + + info("test_access: preparing second call to access()"); + result = OS.Unix.File.access( + "test2.tmp", + OS.Constants.libc.R_OK | + OS.Constants.libc.W_OK | + OS.Constants.libc.X_OK | + OS.Constants.libc.F_OK + ); + is(result, -1, "test_access: second call to access() failed as expected"); + is(ctypes.errno, OS.Constants.libc.ENOENT, "This is the correct error"); + OS.Unix.File.close(file); +} + +function test_getcwd() { + let array = new (ctypes.ArrayType(ctypes.char, 32768))(); + let path = OS.Unix.File.getcwd(array, array.length); + if (ctypes.char.ptr(path).isNull()) { + ok(false, "test_get_cwd: getcwd returned null, errno: " + ctypes.errno); + } + let path2; + if (OS.Unix.File.get_current_dir_name) { + path2 = OS.Unix.File.get_current_dir_name(); + } else { + path2 = OS.Unix.File.getwd_auto(null); + } + if (ctypes.char.ptr(path2).isNull()) { + ok( + false, + "test_get_cwd: getwd_auto/get_current_dir_name returned null, errno: " + + ctypes.errno + ); + } + is( + path.readString(), + path2.readString(), + "test_get_cwd: getcwd and getwd return the same path" + ); +} + +function test_read_write() { + let output_name = "osfile_copy.tmp"; + // Copy file + let input = OS.Unix.File.open( + "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js", + OS.Constants.libc.O_RDONLY + ); + isnot(input, -1, "test_read_write: input file opened"); + let output = OS.Unix.File.open( + "osfile_copy.tmp", + OS.Constants.libc.O_RDWR | + OS.Constants.libc.O_CREAT | + OS.Constants.libc.O_TRUNC, + ctypes.int(OS.Constants.libc.S_IRWXU) + ); + isnot(output, -1, "test_read_write: output file opened"); + + let array = new (ctypes.ArrayType(ctypes.char, 4096))(); + let bytes = -1; + let total = 0; + while (true) { + bytes = OS.Unix.File.read(input, array, 4096); + ok(bytes != undefined, "test_read_write: bytes is defined"); + isnot(bytes, -1, "test_read_write: no read error"); + let write_from = 0; + if (bytes == 0) { + break; + } + while (bytes > 0) { + array.addressOfElement(write_from); + // Note: |write| launches an exception in case of error + let written = OS.Unix.File.write(output, array, bytes); + isnot(written, -1, "test_read_write: no write error"); + write_from += written; + bytes -= written; + } + total += write_from; + } + info("test_read_write: copy complete " + total); + + // Compare files + let result; + info("SEEK_SET: " + OS.Constants.libc.SEEK_SET); + info("Input: " + input + "(" + input.toSource() + ")"); + info("Output: " + output + "(" + output.toSource() + ")"); + result = OS.Unix.File.lseek(input, 0, OS.Constants.libc.SEEK_SET); + info("Result of lseek: " + result); + isnot(result, -1, "test_read_write: input seek succeeded " + ctypes.errno); + result = OS.Unix.File.lseek(output, 0, OS.Constants.libc.SEEK_SET); + isnot(result, -1, "test_read_write: output seek succeeded " + ctypes.errno); + + let array2 = new (ctypes.ArrayType(ctypes.char, 4096))(); + let bytes2 = -1; + let pos = 0; + while (true) { + bytes = OS.Unix.File.read(input, array, 4096); + isnot(bytes, -1, "test_read_write: input read succeeded"); + bytes2 = OS.Unix.File.read(output, array2, 4096); + isnot(bytes, -1, "test_read_write: output read succeeded"); + is( + bytes > 0, + bytes2 > 0, + "Both files contain data or neither does " + bytes + ", " + bytes2 + ); + if (bytes == 0) { + break; + } + if (bytes != bytes2) { + // This would be surprising, but theoretically possible with a + // remote file system, I believe. + bytes = Math.min(bytes, bytes2); + pos += bytes; + result = OS.Unix.File.lseek(input, pos, OS.Constants.libc.SEEK_SET); + isnot(result, -1, "test_read_write: input seek succeeded"); + result = OS.Unix.File.lseek(output, pos, OS.Constants.libc.SEEK_SET); + isnot(result, -1, "test_read_write: output seek succeeded"); + } else { + pos += bytes; + } + for (let i = 0; i < bytes; ++i) { + if (array[i] != array2[i]) { + ok( + false, + "Files do not match at position " + + i + + " (" + + array[i] + + "/" + + array2[i] + + ")" + ); + } + } + } + info("test_read_write test complete"); + result = OS.Unix.File.close(input); + isnot(result, -1, "test_read_write: input close succeeded"); + result = OS.Unix.File.close(output); + isnot(result, -1, "test_read_write: output close succeeded"); + result = OS.Unix.File.unlink(output_name); + isnot(result, -1, "test_read_write: input remove succeeded"); + info("test_read_write cleanup complete"); +} + +function test_passing_undefined() { + info( + "Testing that an exception gets thrown when an FFI function is passed undefined" + ); + let exceptionRaised = false; + + try { + OS.Unix.File.open( + undefined, + OS.Constants.libc.O_RDWR | + OS.Constants.libc.O_CREAT | + OS.Constants.libc.O_TRUNC, + ctypes.int(OS.Constants.libc.S_IRWXU) + ); + } catch (e) { + if (e instanceof TypeError && e.message.indexOf("open") > -1) { + exceptionRaised = true; + } else { + throw e; + } + } + + ok(exceptionRaised, "test_passing_undefined: exception gets thrown"); +} diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js new file mode 100644 index 000000000000..5e02c0399847 --- /dev/null +++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js @@ -0,0 +1,310 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker, node */ + +/* import-globals-from /testing/mochitest/tests/SimpleTest/WorkerSimpleTest.js */ +importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js"); + +self.onmessage = function(msg) { + self.onmessage = function(msg) { + log("ignored message " + JSON.stringify(msg.data)); + }; + + test_init(); + test_GetCurrentDirectory(); + test_OpenClose(); + test_CreateFile(); + test_ReadWrite(); + test_passing_undefined(); + finish(); +}; + +function test_init() { + info("Starting test_init"); + /* import-globals-from /toolkit/components/osfile/osfile.jsm */ + importScripts("resource://gre/modules/osfile.jsm"); +} + +function test_OpenClose() { + info("Starting test_OpenClose"); + is( + typeof OS.Win.File.CreateFile, + "function", + "OS.Win.File.CreateFile is a function" + ); + is( + OS.Win.File.CloseHandle(OS.Constants.Win.INVALID_HANDLE_VALUE), + true, + "CloseHandle returns true given the invalid handle" + ); + is( + OS.Win.File.FindClose(OS.Constants.Win.INVALID_HANDLE_VALUE), + true, + "FindClose returns true given the invalid handle" + ); + isnot(OS.Constants.Win.GENERIC_READ, undefined, "GENERIC_READ exists"); + isnot(OS.Constants.Win.FILE_SHARE_READ, undefined, "FILE_SHARE_READ exists"); + isnot( + OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, + undefined, + "FILE_ATTRIBUTE_NORMAL exists" + ); + let file = OS.Win.File.CreateFile( + "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js", + OS.Constants.Win.GENERIC_READ, + 0, + null, + OS.Constants.Win.OPEN_EXISTING, + 0, + null + ); + info("test_OpenClose: Passed open"); + isnot( + file, + OS.Constants.Win.INVALID_HANDLE_VALUE, + "test_OpenClose: file opened" + ); + let result = OS.Win.File.CloseHandle(file); + isnot(result, 0, "test_OpenClose: close succeeded"); + + file = OS.Win.File.CreateFile( + "\\I do not exist", + OS.Constants.Win.GENERIC_READ, + OS.Constants.Win.FILE_SHARE_READ, + null, + OS.Constants.Win.OPEN_EXISTING, + OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, + null + ); + is( + file, + OS.Constants.Win.INVALID_HANDLE_VALUE, + "test_OpenClose: cannot open non-existing file" + ); + is( + ctypes.winLastError, + OS.Constants.Win.ERROR_FILE_NOT_FOUND, + "test_OpenClose: error is ERROR_FILE_NOT_FOUND" + ); +} + +function test_CreateFile() { + info("Starting test_CreateFile"); + let file = OS.Win.File.CreateFile( + "test.tmp", + OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE, + OS.Constants.Win.FILE_SHARE_READ | OS.Constants.FILE_SHARE_WRITE, + null, + OS.Constants.Win.CREATE_ALWAYS, + OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, + null + ); + isnot( + file, + OS.Constants.Win.INVALID_HANDLE_VALUE, + "test_CreateFile: opening succeeded" + ); + let result = OS.Win.File.CloseHandle(file); + isnot(result, 0, "test_CreateFile: close succeeded"); +} + +function test_GetCurrentDirectory() { + let array = new (ctypes.ArrayType(ctypes.char16_t, 4096))(); + let result = OS.Win.File.GetCurrentDirectory(4096, array); + ok(result < array.length, "test_GetCurrentDirectory: length sufficient"); + ok(result > 0, "test_GetCurrentDirectory: length != 0"); +} + +function test_ReadWrite() { + info("Starting test_ReadWrite"); + let output_name = "osfile_copy.tmp"; + // Copy file + let input = OS.Win.File.CreateFile( + "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js", + OS.Constants.Win.GENERIC_READ, + 0, + null, + OS.Constants.Win.OPEN_EXISTING, + 0, + null + ); + isnot( + input, + OS.Constants.Win.INVALID_HANDLE_VALUE, + "test_ReadWrite: input file opened" + ); + let output = OS.Win.File.CreateFile( + "osfile_copy.tmp", + OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE, + 0, + null, + OS.Constants.Win.CREATE_ALWAYS, + OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, + null + ); + isnot( + output, + OS.Constants.Win.INVALID_HANDLE_VALUE, + "test_ReadWrite: output file opened" + ); + let array = new (ctypes.ArrayType(ctypes.char, 4096))(); + let bytes_read = new ctypes.uint32_t(0); + let bytes_read_ptr = bytes_read.address(); + log("We have a pointer for bytes read: " + bytes_read_ptr); + let bytes_written = new ctypes.uint32_t(0); + let bytes_written_ptr = bytes_written.address(); + log("We have a pointer for bytes written: " + bytes_written_ptr); + log("test_ReadWrite: buffer and pointers ready"); + let result; + while (true) { + log("test_ReadWrite: reading"); + result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null); + isnot(result, 0, "test_ReadWrite: read success"); + let write_from = 0; + let bytes_left = bytes_read; + log("test_ReadWrite: read chunk complete " + bytes_left.value); + if (bytes_left.value == 0) { + break; + } + while (bytes_left.value > 0) { + log("test_ReadWrite: writing " + bytes_left.value); + array.addressOfElement(write_from); + // Note: |WriteFile| launches an exception in case of error + result = OS.Win.File.WriteFile( + output, + array, + bytes_left, + bytes_written_ptr, + null + ); + isnot(result, 0, "test_ReadWrite: write success"); + write_from += bytes_written; + bytes_left -= bytes_written; + } + } + info("test_ReadWrite: copy complete"); + + // Compare files + result = OS.Win.File.SetFilePointer( + input, + 0, + null, + OS.Constants.Win.FILE_BEGIN + ); + isnot( + result, + OS.Constants.Win.INVALID_SET_FILE_POINTER, + "test_ReadWrite: input reset" + ); + + result = OS.Win.File.SetFilePointer( + output, + 0, + null, + OS.Constants.Win.FILE_BEGIN + ); + isnot( + result, + OS.Constants.Win.INVALID_SET_FILE_POINTER, + "test_ReadWrite: output reset" + ); + + let array2 = new (ctypes.ArrayType(ctypes.char, 4096))(); + let bytes_read2 = new ctypes.uint32_t(0); + let bytes_read2_ptr = bytes_read2.address(); + let pos = 0; + while (true) { + result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null); + isnot(result, 0, "test_ReadWrite: input read succeeded"); + + result = OS.Win.File.ReadFile(output, array2, 4096, bytes_read2_ptr, null); + isnot(result, 0, "test_ReadWrite: output read succeeded"); + + is( + bytes_read.value > 0, + bytes_read2.value > 0, + "Both files contain data or neither does " + + bytes_read.value + + ", " + + bytes_read2.value + ); + if (bytes_read.value == 0) { + break; + } + let bytes; + if (bytes_read.value != bytes_read2.value) { + // This would be surprising, but theoretically possible with a + // remote file system, I believe. + bytes = Math.min(bytes_read.value, bytes_read2.value); + pos += bytes; + result = OS.Win.File.SetFilePointer( + input, + pos, + null, + OS.Constants.Win.FILE_BEGIN + ); + isnot(result, 0, "test_ReadWrite: input seek succeeded"); + + result = OS.Win.File.SetFilePointer( + output, + pos, + null, + OS.Constants.Win.FILE_BEGIN + ); + isnot(result, 0, "test_ReadWrite: output seek succeeded"); + } else { + bytes = bytes_read.value; + pos += bytes; + } + for (let i = 0; i < bytes; ++i) { + if (array[i] != array2[i]) { + ok( + false, + "Files do not match at position " + + i + + " (" + + array[i] + + "/" + + array2[i] + + ")" + ); + } + } + } + info("test_ReadWrite test complete"); + result = OS.Win.File.CloseHandle(input); + isnot(result, 0, "test_ReadWrite: inpout close succeeded"); + result = OS.Win.File.CloseHandle(output); + isnot(result, 0, "test_ReadWrite: outpout close succeeded"); + result = OS.Win.File.DeleteFile(output_name); + isnot(result, 0, "test_ReadWrite: output remove succeeded"); + info("test_ReadWrite cleanup complete"); +} + +function test_passing_undefined() { + info( + "Testing that an exception gets thrown when an FFI function is passed undefined" + ); + let exceptionRaised = false; + + try { + OS.Win.File.CreateFile( + undefined, + OS.Constants.Win.GENERIC_READ, + 0, + null, + OS.Constants.Win.OPEN_EXISTING, + 0, + null + ); + } catch (e) { + if (e instanceof TypeError && e.message.indexOf("CreateFile") > -1) { + exceptionRaised = true; + } else { + throw e; + } + } + + ok(exceptionRaised, "test_passing_undefined: exception gets thrown"); +} diff --git a/toolkit/components/osfile/tests/xpcshell/.eslintrc.js b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js new file mode 100644 index 000000000000..4cb383ff7aa0 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + rules: { + "no-shadow": "off", + }, +}; diff --git a/toolkit/components/osfile/tests/xpcshell/head.js b/toolkit/components/osfile/tests/xpcshell/head.js new file mode 100644 index 000000000000..8d162b976794 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/head.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// Bug 1014484 can only be reproduced by loading OS.File first from the +// CommonJS loader, so we do not want OS.File to be loaded eagerly for +// all the tests in this directory. +ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); + +Services.prefs.setBoolPref("toolkit.osfile.log", true); + +/** + * As add_task, but execute the test both with native operations and + * without. + */ +function add_test_pair(generator) { + add_task(async function() { + info("Executing test " + generator.name + " with native operations"); + Services.prefs.setBoolPref("toolkit.osfile.native", true); + return generator(); + }); + add_task(async function() { + info("Executing test " + generator.name + " without native operations"); + Services.prefs.setBoolPref("toolkit.osfile.native", false); + return generator(); + }); +} + +/** + * Fetch asynchronously the contents of a file using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} path The _absolute_ path to the file. + * @return {promise} + * @resolves {string} The contents of the file. + */ +function reference_fetch_file(path, test) { + info("Fetching file " + path); + return new Promise((resolve, reject) => { + let file = new FileUtils.File(path); + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(stream, status) { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + let result, reject; + try { + result = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (x) { + reject = x; + } + stream.close(); + if (reject) { + reject(reject); + } else { + resolve(result); + } + } + ); + }); +} + +/** + * Compare asynchronously the contents two files using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} a The _absolute_ path to the first file. + * @param {string} b The _absolute_ path to the second file. + * + * @resolves {null} + */ +function reference_compare_files(a, b, test) { + return (async function() { + info("Comparing files " + a + " and " + b); + let a_contents = await reference_fetch_file(a, test); + let b_contents = await reference_fetch_file(b, test); + Assert.equal(a_contents, b_contents); + })(); +} + +async function removeTestFile(filePath, ignoreNoSuchFile = true) { + try { + await OS.File.remove(filePath); + } catch (ex) { + if (!ignoreNoSuchFile || !ex.becauseNoSuchFile) { + do_throw(ex); + } + } +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_compression.js b/toolkit/components/osfile/tests/xpcshell/test_compression.js new file mode 100644 index 000000000000..2daa4c78915a --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_compression.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +add_task(async function test_compress_lz4() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "compression.lz"); + let length = 1024; + let array = new Uint8Array(length); + for (let i = 0; i < array.byteLength; ++i) { + array[i] = i; + } + let arrayAsString = Array.prototype.join.call(array); + + info("Writing data with lz4 compression"); + let bytes = await OS.File.writeAtomic(path, array, { compression: "lz4" }); + info("Compressed " + length + " bytes into " + bytes); + + info("Reading back with lz4 decompression"); + let decompressed = await OS.File.read(path, { compression: "lz4" }); + info("Decompressed into " + decompressed.byteLength + " bytes"); + Assert.equal(arrayAsString, Array.prototype.join.call(decompressed)); +}); + +add_task(async function test_uncompressed() { + info("Writing data without compression"); + let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_compression.tmp"); + let array = new Uint8Array(1024); + for (let i = 0; i < array.byteLength; ++i) { + array[i] = i; + } + await OS.File.writeAtomic(path, array); // No compression + + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes(`Invalid header (no magic number) - Data: ${path}`) + ); +}); + +add_task(async function test_no_header() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_header.tmp"); + let array = new Uint8Array(8).fill(0, 0); // Small array with no header + + info("Writing data with no header"); + + await OS.File.writeAtomic(path, array); // No compression + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes(`Buffer is too short (no header) - Data: ${path}`) + ); +}); + +add_task(async function test_invalid_content() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, "invalid_content.tmp"); + let arr1 = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); + let arr2 = new Uint8Array(248).fill(1, 0); + + let array = new Uint8Array(arr1.length + arr2.length); + array.set(arr1); + array.set(arr2, arr1.length); + + info("Writing invalid data (with a valid header and only ones after that)"); + + await OS.File.writeAtomic(path, array); // No compression + let exn; + // Force decompression, reading should fail + try { + await OS.File.read(path, { compression: "lz4" }); + } catch (ex) { + exn = ex; + } + Assert.ok(!!exn); + // Check the exception message (and that it contains the file name) + Assert.ok( + exn.message.includes( + `Invalid content: Decompression stopped at 0 - Data: ${path}` + ) + ); +}); + +add_task(function() { + do_test_finished(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_constants.js b/toolkit/components/osfile/tests/xpcshell/test_constants.js new file mode 100644 index 000000000000..7bc2e72a075e --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_constants.js @@ -0,0 +1,20 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Test that OS.Constants is defined correctly. +add_task(async function check_definition() { + Assert.ok(OS.Constants != null); + Assert.ok(!!OS.Constants.Win || !!OS.Constants.libc); + Assert.ok(OS.Constants.Path != null); + Assert.ok(OS.Constants.Sys != null); + // check system name + Assert.equal(Services.appinfo.OS, OS.Constants.Sys.Name); + + // check if using DEBUG build + if (Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2).isDebugBuild) { + Assert.ok(OS.Constants.Sys.DEBUG); + } else { + Assert.ok(typeof OS.Constants.Sys.DEBUG == "undefined"); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_duration.js b/toolkit/components/osfile/tests/xpcshell/test_duration.js new file mode 100644 index 000000000000..9c2b54a4b6db --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_duration.js @@ -0,0 +1,127 @@ +var { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Test optional duration reporting that can be used for telemetry. + */ +add_task(async function duration() { + const availableDurations = [ + "outSerializationDuration", + "outExecutionDuration", + ]; + Services.prefs.setBoolPref("toolkit.osfile.log", true); + // Options structure passed to a OS.File copy method. + let copyOptions = { + // These fields should be overwritten with the actual duration + // measurements. + outSerializationDuration: null, + outExecutionDuration: null, + }; + let currentDir = await OS.File.getCurrentDirectory(); + let pathSource = OS.Path.join(currentDir, "test_duration.js"); + let copyFile = pathSource + ".bak"; + function testOptions(options, name, durations = availableDurations) { + for (let duration of durations) { + info(`Checking ${duration} for operation: ${name}`); + info(`${name}: Gathered method duration time: ${options[duration]} ms`); + // Making sure that duration was updated. + Assert.equal(typeof options[duration], "number"); + Assert.ok(options[duration] >= 0); + } + } + + function testOptionIncrements( + options, + name, + backupDuration, + durations = availableDurations + ) { + for (let duration of durations) { + info(`Checking ${duration} increment for operation: ${name}`); + info(`${name}: Gathered method duration time: ${options[duration]} ms`); + info(`${name}: Previous duration: ${backupDuration[duration]} ms`); + // Making sure that duration was incremented. + Assert.ok(options[duration] >= backupDuration[duration]); + } + } + + // Testing duration of OS.File.copy. + await OS.File.copy(pathSource, copyFile, copyOptions); + testOptions(copyOptions, "OS.File.copy"); + await OS.File.remove(copyFile); + + // Trying an operation where options are cloned. + let pathDest = OS.Path.join( + OS.Constants.Path.tmpDir, + "osfile async test read writeAtomic.tmp" + ); + let tmpPath = pathDest + ".tmp"; + let readOptions = { + // We do not check for |outSerializationDuration| since |Scheduler.post| + // may not be called whenever |read| is called. + outExecutionDuration: null, + }; + let contents = await OS.File.read(pathSource, undefined, readOptions); + testOptions(readOptions, "OS.File.read", ["outExecutionDuration"]); + // Options structure passed to a OS.File writeAtomic method. + let writeAtomicOptions = { + // This field should be first initialized with the actual + // duration measurement then progressively incremented. + outExecutionDuration: null, + tmpPath, + }; + // Note that |contents| cannot be reused after this call since it is detached. + await OS.File.writeAtomic(pathDest, contents, writeAtomicOptions); + testOptions(writeAtomicOptions, "OS.File.writeAtomic", [ + "outExecutionDuration", + ]); + await OS.File.remove(pathDest); + + info( + `Ensuring that we can use ${availableDurations.join( + ", " + )} to accumulate durations` + ); + + let ARBITRARY_BASE_DURATION = 5; + copyOptions = { + // This field should now be incremented with the actual duration + // measurement. + outSerializationDuration: ARBITRARY_BASE_DURATION, + outExecutionDuration: ARBITRARY_BASE_DURATION, + }; + + // We need to copy the object, since having a reference would make this pointless. + let backupDuration = Object.assign({}, copyOptions); + + // Testing duration of OS.File.copy. + await OS.File.copy(pathSource, copyFile, copyOptions); + testOptionIncrements(copyOptions, "copy", backupDuration); + + backupDuration = Object.assign({}, copyOptions); + await OS.File.remove(copyFile, copyOptions); + testOptionIncrements(copyOptions, "remove", backupDuration); + + // Trying an operation where options are cloned. + // Options structure passed to a OS.File writeAtomic method. + writeAtomicOptions = { + // We do not check for |outSerializationDuration| since |Scheduler.post| + // may not be called whenever |writeAtomic| is called. + outExecutionDuration: ARBITRARY_BASE_DURATION, + }; + writeAtomicOptions.tmpPath = tmpPath; + backupDuration = Object.assign({}, writeAtomicOptions); + contents = await OS.File.read(pathSource, undefined, readOptions); + await OS.File.writeAtomic(pathDest, contents, writeAtomicOptions); + testOptionIncrements( + writeAtomicOptions, + "writeAtomicOptions", + backupDuration, + ["outExecutionDuration"] + ); + OS.File.remove(pathDest); + + // Testing an operation that doesn't take arguments at all + let file = await OS.File.open(pathSource); + await file.stat(); + await file.close(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_exception.js b/toolkit/components/osfile/tests/xpcshell/test_exception.js new file mode 100644 index 000000000000..5a4ffc8441a1 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_exception.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that functions throw the appropriate exceptions. + */ + +"use strict"; + +var EXISTING_FILE = do_get_file("xpcshell.ini").path; + +// Tests on |open| + +add_test_pair(async function test_typeerror() { + let exn; + try { + let fd = await OS.File.open("/tmp", { no_such_key: 1 }); + info("Fd: " + fd); + } catch (ex) { + exn = ex; + } + info("Exception: " + exn); + Assert.ok(exn.constructor.name == "TypeError"); +}); + +// Tests on |read| + +add_test_pair(async function test_bad_encoding() { + info("Testing with a wrong encoding"); + try { + await OS.File.read(EXISTING_FILE, { encoding: "baby-speak-encoded" }); + do_throw("Should have thrown with an ex.becauseInvalidArgument"); + } catch (ex) { + if (ex.becauseInvalidArgument) { + info("Wrong encoding caused the correct exception"); + } else { + throw ex; + } + } + + try { + await OS.File.read(EXISTING_FILE, { encoding: 4 }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-string encoding caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function test_bad_compression() { + info("Testing with a non-existing compression"); + try { + await OS.File.read(EXISTING_FILE, { compression: "mmmh-crunchy" }); + do_throw("Should have thrown with an ex.becauseInvalidArgument"); + } catch (ex) { + if (ex.becauseInvalidArgument) { + info("Wrong encoding caused the correct exception"); + } else { + throw ex; + } + } + + info("Testing with a bad type for option compression"); + try { + await OS.File.read(EXISTING_FILE, { compression: 5 }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-string encoding caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function test_bad_bytes() { + info("Testing with a bad type for option bytes"); + try { + await OS.File.read(EXISTING_FILE, { bytes: "five" }); + do_throw("Should have thrown a TypeError"); + } catch (ex) { + if (ex.constructor.name == "TypeError") { + // Note that TypeError doesn't carry across compartments + info("Non-number bytes caused the correct exception"); + } else { + throw ex; + } + } +}); + +add_test_pair(async function read_non_existent() { + info("Testing with a non-existent file"); + try { + await OS.File.read("I/do/not/exist"); + do_throw("Should have thrown with an ex.becauseNoSuchFile"); + } catch (ex) { + if (ex.becauseNoSuchFile) { + info("Correct exceptions"); + } else { + throw ex; + } + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js new file mode 100644 index 000000000000..41b57414e042 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + let isWindows = "@mozilla.org/windows-registry-key;1" in Cc; + + // Test cases for filePathToURI + let paths = isWindows + ? [ + "C:\\", + "C:\\test", + "C:\\test\\", + "C:\\test%2f", + "C:\\test\\test\\test", + "C:\\test;+%", + "C:\\test?action=index\\", + "C:\\test test", + "\\\\C:\\a\\b\\c", + "\\\\Server\\a\\b\\c", + + // note that per http://support.microsoft.com/kb/177506 (under more info), + // the following characters are allowed on Windows: + "C:\\char^", + "C:\\char&", + "C:\\char'", + "C:\\char@", + "C:\\char{", + "C:\\char}", + "C:\\char[", + "C:\\char]", + "C:\\char,", + "C:\\char$", + "C:\\char=", + "C:\\char!", + "C:\\char-", + "C:\\char#", + "C:\\char(", + "C:\\char)", + "C:\\char%", + "C:\\char.", + "C:\\char+", + "C:\\char~", + "C:\\char_", + ] + : [ + "/", + "/test", + "/test/", + "/test%2f", + "/test/test/test", + "/test;+%", + "/test?action=index/", + "/test test", + "/punctuation/;,/?:@&=+$-_.!~*'()[]\"#", + "/CasePreserving", + ]; + + // some additional URIs to test, beyond those generated from paths + let uris = isWindows + ? [ + "file:///C:/test/", + "file://localhost/C:/test", + "file:///c:/test/test.txt", + // 'file:///C:/foo%2f', // trailing, encoded slash + "file:///C:/%3f%3F", + "file:///C:/%3b%3B", + "file:///C:/%3c%3C", // not one of the special-cased ? or ; + "file:///C:/%78", // 'x', not usually uri encoded + "file:///C:/test#frag", // a fragment identifier + "file:///C:/test?action=index", // an actual query component + ] + : [ + "file:///test/", + "file://localhost/test", + "file:///test/test.txt", + "file:///foo%2f", // trailing, encoded slash + "file:///%3f%3F", + "file:///%3b%3B", + "file:///%3c%3C", // not one of the special-cased ? or ; + "file:///%78", // 'x', not usually uri encoded + "file:///test#frag", // a fragment identifier + "file:///test?action=index", // an actual query component + ]; + + for (let path of paths) { + // convert that to a uri using FileUtils and Services, which toFileURI is trying to model + let file = FileUtils.File(path); + let uri = Services.io.newFileURI(file).spec; + Assert.equal(uri, OS.Path.toFileURI(path)); + + // keep the resulting URI to try the reverse, except for "C:\" for which the + // behavior of nsIFileURL and OS.File is inconsistent + if (path != "C:\\") { + uris.push(uri); + } + } + + for (let uri of uris) { + // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model + let path = Services.io.newURI(uri).QueryInterface(Ci.nsIFileURL).file.path; + Assert.equal(path, OS.Path.fromFileURI(uri)); + } + + // check that non-file URLs aren't allowed + let thrown = false; + try { + OS.Path.fromFileURI("http://test.com"); + } catch (e) { + Assert.equal(e.message, "fromFileURI expects a file URI"); + thrown = true; + } + Assert.ok(thrown); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_logging.js b/toolkit/components/osfile/tests/xpcshell/test_logging.js new file mode 100644 index 000000000000..2fa8f9dbec80 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_logging.js @@ -0,0 +1,73 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Tests logging by passing OS.Shared.LOG both an object with its own + * toString method, and one with the default. + */ +function run_test() { + do_test_pending(); + let messageCount = 0; + + info("Test starting"); + + // Create a console listener. + let consoleListener = { + observe(aMessage) { + // Ignore unexpected messages. + if (!(aMessage instanceof Ci.nsIConsoleMessage)) { + return; + } + // This is required, as printing to the |Services.console| + // while in the observe function causes an exception. + executeSoon(function() { + info("Observing message " + aMessage.message); + if (!aMessage.message.includes("TEST OS")) { + return; + } + + ++messageCount; + if (messageCount == 1) { + Assert.equal(aMessage.message, 'TEST OS {"name":"test"}\n'); + } + if (messageCount == 2) { + Assert.equal(aMessage.message, "TEST OS name is test\n"); + toggleConsoleListener(false); + do_test_finished(); + } + }); + }, + }; + + // Set/Unset the console listener. + function toggleConsoleListener(pref) { + info("Setting console listener: " + pref); + Services.prefs.setBoolPref("toolkit.osfile.log", pref); + Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref); + Services.console[pref ? "registerListener" : "unregisterListener"]( + consoleListener + ); + } + + toggleConsoleListener(true); + + let objectDefault = { name: "test" }; + let CustomToString = function() { + this.name = "test"; + }; + CustomToString.prototype.toString = function() { + return "name is " + this.name; + }; + let objectCustom = new CustomToString(); + + info(OS.Shared.LOG.toSource()); + + info("Logging 1"); + OS.Shared.LOG(objectDefault); + + info("Logging 2"); + OS.Shared.LOG(objectCustom); + // Once both messages are observed OS.Shared.DEBUG, and OS.Shared.TEST + // are reset to false. +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_makeDir.js b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js new file mode 100644 index 000000000000..686bff2f2a12 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +var Path = OS.Path; +var profileDir; + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +/** + * Test OS.File.makeDir + */ + +add_task(function init() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + profileDir = OS.Constants.Path.profileDir; + Services.prefs.setBoolPref("toolkit.osfile.log", true); +}); + +/** + * Basic use + */ + +add_task(async function test_basic() { + let dir = Path.join(profileDir, "directory"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Make a directory + await OS.File.makeDir(dir); + + // check if the directory exists + await OS.File.stat(dir); + + // Make a directory that already exists, this should succeed + await OS.File.makeDir(dir); + + // Make a directory with ignoreExisting + await OS.File.makeDir(dir, { ignoreExisting: true }); + + // Make a directory with ignoreExisting false + let exception = null; + try { + await OS.File.makeDir(dir, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseExists); +}); + +// Make a root directory that already exists +add_task(async function test_root() { + if (OS.Constants.Win) { + await OS.File.makeDir("C:"); + await OS.File.makeDir("C:\\"); + } else { + await OS.File.makeDir("/"); + } +}); + +/** + * Creating subdirectories + */ +add_task(async function test_option_from() { + let dir = Path.join(profileDir, "a", "b", "c"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Make a directory + await OS.File.makeDir(dir, { from: profileDir }); + + // check if the directory exists + await OS.File.stat(dir); + + // Make a directory that already exists, this should succeed + await OS.File.makeDir(dir); + + // Make a directory with ignoreExisting + await OS.File.makeDir(dir, { ignoreExisting: true }); + + // Make a directory with ignoreExisting false + let exception = null; + try { + await OS.File.makeDir(dir, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseExists); + + // Make a directory without |from| and fail + let dir2 = Path.join(profileDir, "g", "h", "i"); + exception = null; + try { + await OS.File.makeDir(dir2); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseNoSuchFile); + + // Test edge cases on paths + + let dir3 = Path.join(profileDir, "d", "", "e", "f"); + Assert.equal(false, await OS.File.exists(dir3)); + await OS.File.makeDir(dir3, { from: profileDir }); + Assert.ok(await OS.File.exists(dir3)); + + let dir4; + if (OS.Constants.Win) { + // Test that we can create a directory recursively even + // if we have too many "\\". + dir4 = profileDir + "\\\\g"; + } else { + dir4 = profileDir + "////g"; + } + Assert.equal(false, await OS.File.exists(dir4)); + await OS.File.makeDir(dir4, { from: profileDir }); + Assert.ok(await OS.File.exists(dir4)); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_open.js b/toolkit/components/osfile/tests/xpcshell/test_open.js new file mode 100644 index 000000000000..6b0c6d8b9046 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_open.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Test OS.File.open for reading: + * - with an existing file (should succeed); + * - with a non-existing file (should fail); + * - with inconsistent arguments (should fail). + */ +add_task(async function() { + // Attempt to open a file that does not exist, ensure that it yields the + // appropriate error. + try { + await OS.File.open(OS.Path.join(".", "This file does not exist")); + Assert.ok(false, "File opening 1 succeeded (it should fail)"); + } catch (err) { + if (err instanceof OS.File.Error && err.becauseNoSuchFile) { + info("File opening 1 failed " + err); + } else { + throw err; + } + } + // Attempt to open a file with the wrong args, so that it fails before + // serialization, ensure that it yields the appropriate error. + info("Attempting to open a file with wrong arguments"); + try { + let fd = await OS.File.open(1, 2, 3); + Assert.ok(false, "File opening 2 succeeded (it should fail)" + fd); + } catch (err) { + info("File opening 2 failed " + err); + Assert.equal( + false, + err instanceof OS.File.Error, + "File opening 2 returned something that is not a file error" + ); + Assert.ok( + err.constructor.name == "TypeError", + "File opening 2 returned a TypeError" + ); + } + + // Attempt to open a file correctly + info("Attempting to open a file correctly"); + let openedFile = await OS.File.open( + OS.Path.join(do_get_cwd().path, "test_open.js") + ); + info("File opened correctly"); + + info("Attempting to close a file correctly"); + await openedFile.close(); + + info("Attempting to close a file again"); + await openedFile.close(); +}); + +/** + * Test the error thrown by OS.File.open when attempting to open a directory + * that does not exist. + */ +add_task(async function test_error_attributes() { + let dir = OS.Path.join(do_get_profile().path, "test_osfileErrorAttrs"); + let fpath = OS.Path.join(dir, "test_error_attributes.txt"); + + try { + await OS.File.open(fpath, { truncate: true }, {}); + Assert.ok(false, "Opening path suceeded (it should fail) " + fpath); + } catch (err) { + Assert.ok(err instanceof OS.File.Error); + Assert.ok(err.becauseNoSuchFile); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js new file mode 100644 index 000000000000..7c2e2db06b32 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js @@ -0,0 +1,13 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A trivial test ensuring that we can call osfile from xpcshell. + * (see bug 808161) + */ + +function run_test() { + do_test_pending(); + OS.File.getCurrentDirectory().then(do_test_finished, do_test_finished); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js new file mode 100644 index 000000000000..8fabbad1edd1 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js @@ -0,0 +1,105 @@ +"use strict"; + +info("starting tests"); + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to check that the |append| mode flag is correctly implemented. + * (see bug 925865) + */ + +function setup_mode(mode) { + // Complete mode. + let realMode = { + read: true, + write: true, + }; + for (let k in mode) { + realMode[k] = mode[k]; + } + return realMode; +} + +// Test append mode. +async function test_append(mode) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_append.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + mode = setup_mode(mode); + mode.append = true; + if (mode.trunc) { + // Pre-fill file with some data to see if |trunc| actually works. + await OS.File.writeAtomic(path, new Uint8Array(500)); + } + let file = await OS.File.open(path, mode); + try { + await file.write(new Uint8Array(1000)); + await file.setPosition(0, OS.File.POS_START); + await file.read(100); + // Should be at offset 100, length 1000 now. + await file.write(new Uint8Array(100)); + // Should be at offset 1100, length 1100 now. + let stat = await file.stat(); + Assert.equal(1100, stat.size); + } finally { + await file.close(); + } + } catch (ex) { + await removeTestFile(path); + } +} + +// Test no-append mode. +async function test_no_append(mode) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_noappend.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + mode = setup_mode(mode); + mode.append = false; + if (mode.trunc) { + // Pre-fill file with some data to see if |trunc| actually works. + await OS.File.writeAtomic(path, new Uint8Array(500)); + } + let file = await OS.File.open(path, mode); + try { + await file.write(new Uint8Array(1000)); + await file.setPosition(0, OS.File.POS_START); + await file.read(100); + // Should be at offset 100, length 1000 now. + await file.write(new Uint8Array(100)); + // Should be at offset 200, length 1000 now. + let stat = await file.stat(); + Assert.equal(1000, stat.size); + } finally { + await file.close(); + } + } finally { + await removeTestFile(path); + } +} + +var test_flags = [{}, { create: true }, { trunc: true }]; +function run_test() { + do_test_pending(); + + for (let t of test_flags) { + add_task(test_append.bind(null, t)); + add_task(test_no_append.bind(null, t)); + } + add_task(do_test_finished); + + run_next_test(); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js new file mode 100644 index 000000000000..6441c88112d5 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js @@ -0,0 +1,40 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * Test to ensure that {bytes:} in options to |write| is correctly + * preserved. + */ +add_task(async function test_bytes() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_bytes.tmp" + ); + let file = await OS.File.open(path, { trunc: true, read: true, write: true }); + try { + try { + // 1. Test write, by supplying {bytes:} options smaller than the actual + // buffer. + await file.write(new Uint8Array(2048), { bytes: 1024 }); + Assert.equal((await file.stat()).size, 1024); + + // 2. Test that passing nullish values for |options| still works. + await file.setPosition(0, OS.File.POS_END); + await file.write(new Uint8Array(1024), null); + await file.write(new Uint8Array(1024), undefined); + Assert.equal((await file.stat()).size, 3072); + } finally { + await file.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js new file mode 100644 index 000000000000..0c82e542f664 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js @@ -0,0 +1,109 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * A file that we know exists and that can be used for reading. + */ +var EXISTING_FILE = "test_osfile_async_copy.js"; + +/** + * Fetch asynchronously the contents of a file using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} path The _absolute_ path to the file. + * @return {promise} + * @resolves {string} The contents of the file. + */ +var reference_fetch_file = function reference_fetch_file(path) { + return new Promise((resolve, reject) => { + let file = new FileUtils.File(path); + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(stream, status) { + if (!Components.isSuccessCode(status)) { + reject(status); + return; + } + let result, reject; + try { + result = NetUtil.readInputStreamToString(stream, stream.available()); + } catch (x) { + reject = x; + } + stream.close(); + if (reject) { + reject(reject); + } else { + resolve(result); + } + } + ); + }); +}; + +/** + * Compare asynchronously the contents two files using xpcom. + * + * Used for comparing xpcom-based results to os.file-based results. + * + * @param {string} a The _absolute_ path to the first file. + * @param {string} b The _absolute_ path to the second file. + * + * @resolves {null} + */ +var reference_compare_files = async function reference_compare_files(a, b) { + let a_contents = await reference_fetch_file(a); + let b_contents = await reference_fetch_file(b); + // Not using do_check_eq to avoid dumping the whole file to the log. + // It is OK to === compare here, as both variables contain a string. + Assert.ok(a_contents === b_contents); +}; + +/** + * Test to ensure that OS.File.copy works. + */ +async function test_copymove(options = {}) { + let source = OS.Path.join(await OS.File.getCurrentDirectory(), EXISTING_FILE); + let dest = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_copy_dest.tmp" + ); + let dest2 = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_copy_dest2.tmp" + ); + try { + // 1. Test copy. + await OS.File.copy(source, dest, options); + await reference_compare_files(source, dest); + // 2. Test subsequent move. + await OS.File.move(dest, dest2); + await reference_compare_files(source, dest2); + // 3. Check that the moved file was really moved. + Assert.equal(await OS.File.exists(dest), false); + } finally { + await removeTestFile(dest); + await removeTestFile(dest2); + } +} + +// Regular copy test. +add_task(test_copymove); +// Userland copy test. +add_task(test_copymove.bind(null, { unixUserland: true })); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js new file mode 100644 index 000000000000..e1b377f3c72b --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js @@ -0,0 +1,31 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * Test to ensure that |File.prototype.flush| is available in the async API. + */ + +add_task(async function test_flush() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_flush.tmp" + ); + let file = await OS.File.open(path, { trunc: true, write: true }); + try { + try { + await file.flush(); + } finally { + await file.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js new file mode 100644 index 000000000000..c9cf2c2fc082 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to check that .getPosition/.setPosition work with large files. + * (see bug 952997) + */ + +// Test setPosition/getPosition. +async function test_setPosition(forward, current, backward) { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + let file = await OS.File.open(path, { write: true, append: false }); + try { + let pos = 0; + + // 1. seek forward from start + info("Moving forward: " + forward); + await file.setPosition(forward, OS.File.POS_START); + pos += forward; + Assert.equal(await file.getPosition(), pos); + + // 2. seek forward from current position + info("Moving current: " + current); + await file.setPosition(current, OS.File.POS_CURRENT); + pos += current; + Assert.equal(await file.getPosition(), pos); + + // 3. seek backward from current position + info("Moving current backward: " + backward); + await file.setPosition(-backward, OS.File.POS_CURRENT); + pos -= backward; + Assert.equal(await file.getPosition(), pos); + } finally { + await file.setPosition(0, OS.File.POS_START); + await file.close(); + } + } catch (ex) { + await removeTestFile(path); + } +} + +// Test setPosition/getPosition expected failures. +async function test_setPosition_failures() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp" + ); + + // Clear any left-over files from previous runs. + await removeTestFile(path); + + try { + let file = await OS.File.open(path, { write: true, append: false }); + try { + // 1. Use an invalid position value + try { + await file.setPosition(0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + Assert.ok(ex.toString().includes("can't pass")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + Assert.equal(await file.getPosition(), 0); + + // 2. Use an invalid position value + try { + await file.setPosition(0xffffffff + 0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + Assert.ok(ex.toString().includes("can't pass")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + Assert.equal(await file.getPosition(), 0); + + // 3. Use a position that cannot be represented as a double + try { + // Not all numbers after 9007199254740992 can be represented as a + // double. E.g. in js 9007199254740992 + 1 == 9007199254740992 + await file.setPosition(9007199254740992, OS.File.POS_START); + await file.setPosition(1, OS.File.POS_CURRENT); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + info(ex.toString()); + Assert.ok(!!ex); + } + } finally { + await file.setPosition(0, OS.File.POS_START); + await file.close(); + await removeTestFile(path); + } + } catch (ex) { + do_throw(ex); + } +} + +function run_test() { + // First verify stuff works for small values. + add_task(test_setPosition.bind(null, 0, 100, 50)); + add_task(test_setPosition.bind(null, 1000, 100, 50)); + add_task(test_setPosition.bind(null, 1000, -100, -50)); + + if (OS.Constants.Win || ctypes.off_t.size >= 8) { + // Now verify stuff still works for large values. + // 1. Multiple small seeks, which add up to > MAXINT32 + add_task(test_setPosition.bind(null, 0x7fffffff, 0x7fffffff, 0)); + // 2. Plain large seek, that should end up at 0 again. + // 0xffffffff also happens to be the INVALID_SET_FILE_POINTER value on + // Windows, so this also tests the error handling + add_task(test_setPosition.bind(null, 0, 0xffffffff, 0xffffffff)); + // 3. Multiple large seeks that should end up > MAXINT32. + add_task(test_setPosition.bind(null, 0xffffffff, 0xffffffff, 0xffffffff)); + // 5. Multiple large seeks with negative offsets. + add_task(test_setPosition.bind(null, 0xffffffff, -0x7fffffff, 0x7fffffff)); + + // 6. Check failures + add_task(test_setPosition_failures); + } + + run_next_test(); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js new file mode 100644 index 000000000000..b60c448ceedd --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js @@ -0,0 +1,214 @@ +"use strict"; + +/* eslint-disable no-lone-blocks */ + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * A test to ensure that OS.File.setDates and OS.File.prototype.setDates are + * working correctly. + * (see bug 924916) + */ + +// Non-prototypical tests, operating on path names. +add_task(async function test_nonproto() { + // First, create a file we can mess with. + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_nonproto.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await OS.File.setDates(path, accDate, modDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 2.1 Try to omit modificationDate (which should then default to + // |Date.now()|, expect for resolution differences). + { + await OS.File.setDates(path, accDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 2.2 Try to omit accessDate as well (which should then default to + // |Date.now()|, expect for resolution differences). + { + await OS.File.setDates(path); + let stat = await OS.File.stat(path); + Assert.notEqual(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 3. Repeat 1., but with Date objects this time + { + await OS.File.setDates(path, new Date(accDate), new Date(modDate)); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 4. Check that invalid params will cause an exception/rejection. + { + for (let p of ["invalid", new Uint8Array(1), NaN]) { + try { + await OS.File.setDates(path, p, modDate); + do_throw("Invalid access date should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await OS.File.setDates(path, accDate, p); + do_throw("Invalid modification date should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await OS.File.setDates(path, p, p); + do_throw("Invalid dates should have thrown for: " + p); + } catch (ex) { + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } + } + } finally { + // Remove the temp file again + await OS.File.remove(path); + } +}); + +// Prototypical tests, operating on |File| handles. +add_task(async function test_proto() { + if (OS.Constants.Sys.Name == "Android") { + info("File.prototype.setDates is not implemented for Android"); + Assert.equal(OS.File.prototype.setDates, undefined); + return; + } + + // First, create a file we can mess with. + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_proto.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await fd.setDates(accDate, modDate); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 2.1 Try to omit modificationDate (which should then default to + // |Date.now()|, expect for resolution differences). + { + await fd.setDates(accDate); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 2.2 Try to omit accessDate as well (which should then default to + // |Date.now()|, expect for resolution differences). + { + await fd.setDates(); + let stat = await fd.stat(); + Assert.notEqual(accDate, stat.lastAccessDate.getTime()); + Assert.notEqual(modDate, stat.lastModificationDate.getTime()); + } + + // 3. Repeat 1., but with Date objects this time + { + await fd.setDates(new Date(accDate), new Date(modDate)); + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + + // 4. Check that invalid params will cause an exception/rejection. + { + for (let p of ["invalid", new Uint8Array(1), NaN]) { + try { + await fd.setDates(p, modDate); + do_throw("Invalid access date should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await fd.setDates(accDate, p); + do_throw("Invalid modification date should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + try { + await fd.setDates(p, p); + do_throw("Invalid dates should have thrown for: " + p); + } catch (ex) { + let stat = await fd.stat(); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } + } + } finally { + await fd.close(); + } + } finally { + // Remove the temp file again + await OS.File.remove(path); + } +}); + +// Tests setting dates on directories. +add_task(async function test_dirs() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setDates_dir" + ); + await OS.File.makeDir(path); + + try { + // 1. Try to set some well known dates. + // We choose multiples of 2000ms, because the time stamp resolution of + // the underlying OS might not support something more precise. + const accDate = 2000; + const modDate = 4000; + { + await OS.File.setDates(path, accDate, modDate); + let stat = await OS.File.stat(path); + Assert.equal(accDate, stat.lastAccessDate.getTime()); + Assert.equal(modDate, stat.lastModificationDate.getTime()); + } + } finally { + await OS.File.removeEmptyDir(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js new file mode 100644 index 000000000000..97a33633ac63 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * A test to ensure that OS.File.setPermissions and + * OS.File.prototype.setPermissions are all working correctly. + * (see bug 1001849) + * These functions are currently Unix-specific. The manifest skips + * the test on Windows. + */ + +/** + * Helper function for test logging: prints a POSIX file permission mode as an + * octal number, with a leading '0' per C (not JS) convention. When the + * numeric value is 0o777 or lower, it is padded on the left with zeroes to + * four digits wide. + * Sample outputs: 0022, 0644, 04755. + */ +function format_mode(mode) { + if (mode <= 0o777) { + return ("0000" + mode.toString(8)).slice(-4); + } + return "0" + mode.toString(8); +} + +const _umask = OS.Constants.Sys.umask; +info("umask: " + format_mode(_umask)); + +/** + * Compute the mode that a file should have after applying the umask, + * whatever it happens to be. + */ +function apply_umask(mode) { + return mode & ~_umask; +} + +// Sequence of setPermission parameters and expected file mode. The first test +// checks the permissions when the file is first created. +var testSequence = [ + [null, apply_umask(0o600)], + [{ unixMode: 0o4777 }, apply_umask(0o4777)], + [{ unixMode: 0o4777, unixHonorUmask: false }, 0o4777], + [{ unixMode: 0o4777, unixHonorUmask: true }, apply_umask(0o4777)], + [undefined, apply_umask(0o600)], + [{ unixMode: 0o666 }, apply_umask(0o666)], + [{ unixMode: 0o600 }, apply_umask(0o600)], + [{ unixMode: 0 }, 0], + [{}, apply_umask(0o600)], +]; + +// Test application to paths. +add_task(async function test_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + for (let [options, expectedMode] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await OS.File.setPermissions(path, options); + } + + let stat = await OS.File.stat(path); + Assert.equal(format_mode(stat.unixMode), format_mode(expectedMode)); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to open files. +add_task(async function test_file_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_async_setPermissions_file.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + try { + for (let [options, expectedMode] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await fd.setPermissions(options); + } + + let stat = await fd.stat(); + Assert.equal(format_mode(stat.unixMode), format_mode(expectedMode)); + } + } finally { + await fd.close(); + } + } finally { + await OS.File.remove(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js new file mode 100644 index 000000000000..f4a1fe845594 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js @@ -0,0 +1,46 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +add_task(async function test_closed() { + OS.Shared.DEBUG = true; + let currentDir = await OS.File.getCurrentDirectory(); + info("Open a file, ensure that we can call stat()"); + let path = OS.Path.join(currentDir, "test_osfile_closed.js"); + let file = await OS.File.open(path); + await file.stat(); + Assert.ok(true); + + await file.close(); + + info("Ensure that we cannot stat() on closed file"); + let exn; + try { + await file.stat(); + } catch (ex) { + exn = ex; + } + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseClosed); + + info("Ensure that we cannot read() on closed file"); + exn = null; + try { + await file.read(); + } catch (ex) { + exn = ex; + } + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseClosed); +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js new file mode 100644 index 000000000000..0bf8cd4cb73f --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +add_task(async function testFileError_with_writeAtomic() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let path = Path.join(Constants.Path.tmpDir, "testFileError.tmp"); + await File.remove(path); + await File.writeAtomic(path, DEFAULT_CONTENTS); + let exception; + try { + await File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == path); +}); + +add_task(async function testFileError_with_makeDir() { + let path = Path.join(Constants.Path.tmpDir, "directory"); + await File.removeDir(path); + await File.makeDir(path); + let exception; + try { + await File.makeDir(path, { ignoreExisting: false }); + } catch (ex) { + exception = ex; + } + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == path); +}); + +add_task(async function testFileError_with_move() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let sourcePath = Path.join(Constants.Path.tmpDir, "src.tmp"); + let destPath = Path.join(Constants.Path.tmpDir, "dest.tmp"); + await File.remove(sourcePath); + await File.remove(destPath); + await File.writeAtomic(sourcePath, DEFAULT_CONTENTS); + await File.writeAtomic(destPath, DEFAULT_CONTENTS); + let exception; + try { + await File.move(sourcePath, destPath, { noOverwrite: true }); + } catch (ex) { + exception = ex; + } + info(exception); + Assert.ok(exception instanceof File.Error); + Assert.ok(exception.path == sourcePath); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js new file mode 100644 index 000000000000..376b515f7685 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js @@ -0,0 +1,97 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// We want the actual global to get at the internals since Scheduler is not +// exported. +var { Scheduler } = ChromeUtils.import( + "resource://gre/modules/osfile/osfile_async_front.jsm" +); + +/** + * Verify that Scheduler.kill() interacts with other OS.File requests correctly, + * and that no requests are lost. This is relevant because on B2G we + * auto-kill the worker periodically, making it very possible for valid requests + * to be interleaved with the automatic kill(). + * + * This test is being created with the fix for Bug 1125989 where `kill` queue + * management was found to be buggy. It is a glass-box test that explicitly + * re-creates the observed failure situation; it is not guaranteed to prevent + * all future regressions. The following is a detailed explanation of the test + * for your benefit if this test ever breaks or you are wondering what was the + * point of all this. You might want to skim the code below first. + * + * OS.File maintains a `queue` of operations to be performed. This queue is + * nominally implemented as a chain of promises. Every time a new job is + * OS.File.push()ed, it effectively becomes the new `queue` promise. (An + * extra promise is interposed with a rejection handler to avoid the rejection + * cascading, but that does not matter for our purposes.) + * + * The flaw in `kill` was that it would wait for the `queue` to complete before + * replacing `queue`. As a result, another OS.File operation could use `push` + * (by way of OS.File.post()) to also use .then() on the same `queue` promise. + * Accordingly, assuming that promise was not yet resolved (due to a pending + * OS.File request), when it was resolved, both the task scheduled in `kill` + * and in `post` would be triggered. Both of those tasks would run until + * encountering a call to worker.post(). + * + * Re-creating this race is not entirely trivial because of the large number of + * promises used by the code causing control flow to repeatedly be deferred. In + * a slightly simpler world we could run the follwing in the same turn of the + * event loop and trigger the problem. + * - any OS.File request + * - Scheduler.kill() + * - any OS.File request + * + * However, we need the Scheduler.kill task to reach the point where it is + * waiting on the same `queue` that another task has been scheduled against. + * Since the `kill` task yields on the `killQueue` promise prior to yielding + * on `queue`, however, some turns of the event loop are required. Happily, + * for us, as discussed above, the problem triggers when we have two promises + * scheduled on the `queue`, so we can just wait to schedule the second OS.File + * request on the queue. (Note that because of the additional then() added to + * eat rejections, there is an important difference between the value of + * `queue` and the value returned by the first OS.File request.) + */ +add_task(async function test_kill_race() { + // Ensure the worker has been created and that SET_DEBUG has taken effect. + // We have chosen OS.File.exists for our tests because it does not trigger + // a rejection and we absolutely do not care what the operation is other + // than it does not invoke a native fast-path. + await OS.File.exists("foo.foo"); + + info("issuing first request"); + let firstRequest = OS.File.exists("foo.bar"); // eslint-disable-line no-unused-vars + let secondRequest; + let secondResolved = false; + + // As noted in our big block comment, we want to wait to schedule the + // second request so that it races `kill`'s call to `worker.post`. Having + // ourselves wait on the same promise, `queue`, and registering ourselves + // before we issue the kill request means we will get run before the `kill` + // task resumes and allow us to precisely create the desired race. + Scheduler.queue.then(function() { + info("issuing second request"); + secondRequest = OS.File.exists("foo.baz"); + secondRequest.then(function() { + secondResolved = true; + }); + }); + + info("issuing kill request"); + let killRequest = Scheduler.kill({ reset: true, shutdown: false }); + + // Wait on the killRequest so that we can schedule a new OS.File request + // after it completes... + await killRequest; + // ...because our ordering guarantee ensures that there is at most one + // worker (and this usage here should not be vulnerable even with the + // bug present), so when this completes the secondRequest has either been + // resolved or lost. + await OS.File.exists("foo.goz"); + + ok( + secondResolved, + "The second request was resolved so we avoided the bug. Victory!" + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js new file mode 100644 index 000000000000..b2708274c240 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * A test to ensure that OS.File.setPermissions and + * OS.File.prototype.setPermissions are all working correctly. + * (see bug 1022816) + * The manifest tests on Windows. + */ + +// Sequence of setPermission parameters. +var testSequence = [ + [ + { winAttributes: { readOnly: true, system: true, hidden: true } }, + { readOnly: true, system: true, hidden: true }, + ], + [ + { winAttributes: { readOnly: false } }, + { readOnly: false, system: true, hidden: true }, + ], + [ + { winAttributes: { system: false } }, + { readOnly: false, system: false, hidden: true }, + ], + [ + { winAttributes: { hidden: false } }, + { readOnly: false, system: false, hidden: false }, + ], + [ + { winAttributes: { readOnly: true, system: false, hidden: false } }, + { readOnly: true, system: false, hidden: false }, + ], + [ + { winAttributes: { readOnly: false, system: true, hidden: false } }, + { readOnly: false, system: true, hidden: false }, + ], + [ + { winAttributes: { readOnly: false, system: false, hidden: true } }, + { readOnly: false, system: false, hidden: true }, + ], +]; + +// Test application to paths. +add_task(async function test_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + for (let [options, attributesExpected] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await OS.File.setPermissions(path, options); + } + + let stat = await OS.File.stat(path); + info("Got stat winAttributes: " + JSON.stringify(stat.winAttributes)); + + Assert.equal(stat.winAttributes.readOnly, attributesExpected.readOnly); + Assert.equal(stat.winAttributes.system, attributesExpected.system); + Assert.equal(stat.winAttributes.hidden, attributesExpected.hidden); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to open files. +add_task(async function test_file_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_file.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + try { + for (let [options, attributesExpected] of testSequence) { + if (options !== null) { + info("Setting permissions to " + JSON.stringify(options)); + await fd.setPermissions(options); + } + + let stat = await fd.stat(); + info("Got stat winAttributes: " + JSON.stringify(stat.winAttributes)); + Assert.equal(stat.winAttributes.readOnly, attributesExpected.readOnly); + Assert.equal(stat.winAttributes.system, attributesExpected.system); + Assert.equal(stat.winAttributes.hidden, attributesExpected.hidden); + } + } finally { + await fd.close(); + } + } finally { + await OS.File.remove(path); + } +}); + +// Test application to Check setPermissions on a non-existant file path. +add_task(async function test_non_existant_file_path_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await Assert.rejects( + OS.File.setPermissions(path, { winAttributes: { readOnly: true } }), + /The system cannot find the file specified/, + "setPermissions failed as expected on a non-existant file path" + ); +}); + +// Test application to Check setPermissions on a invalid file handle. +add_task(async function test_closed_file_handle_setPermissions() { + let path = OS.Path.join( + OS.Constants.Path.tmpDir, + "test_osfile_win_async_setPermissions_path.tmp" + ); + await OS.File.writeAtomic(path, new Uint8Array(1)); + + try { + let fd = await OS.File.open(path, { write: true }); + await fd.close(); + await Assert.rejects( + fd.setPermissions(path, { winAttributes: { readOnly: true } }), + /The handle is invalid/, + "setPermissions failed as expected on a invalid file handle" + ); + } finally { + await OS.File.remove(path); + } +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js new file mode 100644 index 000000000000..e3f510a2c207 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +/** + * Remove all temporary files and back up files, including + * test_backupTo_option_with_tmpPath.tmp + * test_backupTo_option_with_tmpPath.tmp.backup + * test_backupTo_option_without_tmpPath.tmp + * test_backupTo_option_without_tmpPath.tmp.backup + * test_non_backupTo_option.tmp + * test_non_backupTo_option.tmp.backup + * test_backupTo_option_without_destination_file.tmp + * test_backupTo_option_without_destination_file.tmp.backup + * test_backupTo_option_with_backup_file.tmp + * test_backupTo_option_with_backup_file.tmp.backup + */ +async function clearFiles() { + let files = [ + "test_backupTo_option_with_tmpPath.tmp", + "test_backupTo_option_without_tmpPath.tmp", + "test_non_backupTo_option.tmp", + "test_backupTo_option_without_destination_file.tmp", + "test_backupTo_option_with_backup_file.tmp", + ]; + for (let file of files) { + let path = Path.join(Constants.Path.tmpDir, file); + await File.remove(path); + await File.remove(path + ".backup"); + } +} + +add_task(async function init() { + await clearFiles(); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| specified + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_with_tmpPath() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_with_tmpPath.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS, { + tmpPath: path + ".tmp", + backupTo: path + ".backup", + }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_without_tmpPath() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_without_tmpPath.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +/** + * test when + * |backupTo| not specified + * |tmpPath| not specified + * destination file exists + * @result destination file will not be backed up + */ +add_task(async function test_non_backupTo_option() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join(Constants.Path.tmpDir, "test_non_backupTo_option.tmp"); + await File.writeAtomic(path, DEFAULT_CONTENTS); + await File.writeAtomic(path, WRITE_CONTENTS); + Assert.equal(false, await File.exists(path + ".backup")); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * destination file not exists + * @result no back up file exists + */ +add_task(async function test_backupTo_option_without_destination_file() { + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_without_destination_file.tmp" + ); + await File.remove(path); + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.equal(false, await File.exists(path + ".backup")); +}); + +/** + * test when + * |backupTo| specified + * |tmpPath| not specified + * backup file exists + * destination file exists + * @result destination file will be backed up + */ +add_task(async function test_backupTo_option_with_backup_file() { + let DEFAULT_CONTENTS = "default contents" + Math.random(); + let WRITE_CONTENTS = "abc" + Math.random(); + let path = Path.join( + Constants.Path.tmpDir, + "test_backupTo_option_with_backup_file.tmp" + ); + await File.writeAtomic(path, DEFAULT_CONTENTS); + + await File.writeAtomic(path + ".backup", new Uint8Array(1000)); + + await File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" }); + Assert.ok(await File.exists(path + ".backup")); + let contents = await File.read(path + ".backup"); + Assert.equal(DEFAULT_CONTENTS, new TextDecoder().decode(contents)); +}); + +add_task(async function cleanup() { + await clearFiles(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js new file mode 100644 index 000000000000..72d23e690936 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_unicode_filename.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks against failures that may occur while creating and/or + * renaming files with Unicode paths on Windows. + * See bug 1063635#c89 for a failure due to a Unicode filename being renamed. + */ + +"use strict"; +var profileDir; + +async function writeAndCheck(path, tmpPath) { + const encoder = new TextEncoder(); + const content = "tmpContent"; + const outBin = encoder.encode(content); + await OS.File.writeAtomic(path, outBin, { tmpPath }); + + const decoder = new TextDecoder(); + const writtenBin = await OS.File.read(path); + const written = decoder.decode(writtenBin); + + // Clean up + await OS.File.remove(path); + Assert.equal( + written, + content, + `Expected correct write/read for ${path} with tmpPath ${tmpPath}` + ); +} + +add_task(async function init() { + do_get_profile(); + profileDir = OS.Constants.Path.profileDir; +}); + +add_test_pair(async function test_osfile_writeAtomic_unicode_filename() { + await writeAndCheck(OS.Path.join(profileDir, "☕") + ".tmp", undefined); + await writeAndCheck(OS.Path.join(profileDir, "☕"), undefined); + await writeAndCheck( + OS.Path.join(profileDir, "☕") + ".tmp", + OS.Path.join(profileDir, "☕") + ); + await writeAndCheck( + OS.Path.join(profileDir, "☕"), + OS.Path.join(profileDir, "☕") + ".tmp" + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js new file mode 100644 index 000000000000..eeaac803063b --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +var SHARED_PATH; + +add_task(async function init() { + do_get_profile(); + SHARED_PATH = OS.Path.join( + OS.Constants.Path.profileDir, + "test_osfile_write_zerobytes.tmp" + ); +}); + +add_test_pair(async function test_osfile_writeAtomic_zerobytes() { + let encoder = new TextEncoder(); + let string1 = ""; + let outbin = encoder.encode(string1); + await OS.File.writeAtomic(SHARED_PATH, outbin); + + let decoder = new TextDecoder(); + let bin = await OS.File.read(SHARED_PATH); + let string2 = decoder.decode(bin); + // Checking if writeAtomic supports writing encoded zero-byte strings + Assert.equal(string2, string1, "Read the expected (empty) string."); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_path.js b/toolkit/components/osfile/tests/xpcshell/test_path.js new file mode 100644 index 000000000000..8a945f67640b --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_path.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false); +// We don't need libc/kernel32.dll for this test + +const Win = ChromeUtils.import("resource://gre/modules/osfile/ospath_win.jsm"); +const Unix = ChromeUtils.import( + "resource://gre/modules/osfile/ospath_unix.jsm" +); + +function do_check_fail(f) { + try { + let result = f(); + info("Failed do_check_fail: " + result); + Assert.ok(false); + } catch (ex) { + Assert.ok(true); + } +} + +function run_test() { + info("Testing Windows paths"); + + info("Backslash-separated, no drive"); + Assert.equal(Win.basename("a\\b"), "b"); + Assert.equal(Win.basename("a\\b\\"), ""); + Assert.equal(Win.basename("abc"), "abc"); + Assert.equal(Win.dirname("a\\b"), "a"); + Assert.equal(Win.dirname("a\\b\\"), "a\\b"); + Assert.equal(Win.dirname("a\\\\\\\\b"), "a"); + Assert.equal(Win.dirname("abc"), "."); + Assert.equal(Win.normalize("\\a\\b\\c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c"); + Assert.equal(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f"); + Assert.equal(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f"); + do_check_fail(() => Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f")); + + Assert.equal( + Win.join("\\tmp", "foo", "bar"), + "\\tmp\\foo\\bar", + "join \\tmp,foo,bar" + ); + Assert.equal( + Win.join("\\tmp", "\\foo", "bar"), + "\\foo\\bar", + "join \\tmp,\\foo,bar" + ); + Assert.equal(Win.winGetDrive("\\tmp"), null); + Assert.equal(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null); + Assert.equal(Win.winGetDrive("\\"), null); + + info("Backslash-separated, with a drive"); + Assert.equal(Win.basename("c:a\\b"), "b"); + Assert.equal(Win.basename("c:a\\b\\"), ""); + Assert.equal(Win.basename("c:abc"), "abc"); + Assert.equal(Win.dirname("c:a\\b"), "c:a"); + Assert.equal(Win.dirname("c:a\\b\\"), "c:a\\b"); + Assert.equal(Win.dirname("c:a\\\\\\\\b"), "c:a"); + Assert.equal(Win.dirname("c:abc"), "c:"); + let options = { + winNoDrive: true, + }; + Assert.equal(Win.dirname("c:a\\b", options), "a"); + Assert.equal(Win.dirname("c:a\\b\\", options), "a\\b"); + Assert.equal(Win.dirname("c:a\\\\\\\\b", options), "a"); + Assert.equal(Win.dirname("c:abc", options), "."); + Assert.equal(Win.join("c:", "abc"), "c:\\abc", "join c:,abc"); + + Assert.equal(Win.normalize("c:"), "c:\\"); + Assert.equal(Win.normalize("c:\\"), "c:\\"); + Assert.equal(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c"); + Assert.equal( + Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), + "c:\\d\\e\\f" + ); + Assert.equal(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f"); + do_check_fail(() => Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f")); + + Assert.equal(Win.join("c:\\", "foo"), "c:\\foo", "join c:,foo"); + Assert.equal( + Win.join("c:\\tmp", "foo", "bar"), + "c:\\tmp\\foo\\bar", + "join c:\\tmp,foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "\\foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,\\foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "c:\\foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,c:\\foo,bar" + ); + Assert.equal( + Win.join("c:\\tmp", "c:foo", "bar"), + "c:\\foo\\bar", + "join c:\\tmp,c:foo,bar" + ); + Assert.equal(Win.winGetDrive("c:"), "c:"); + Assert.equal(Win.winGetDrive("c:\\"), "c:"); + Assert.equal(Win.winGetDrive("c:abc"), "c:"); + Assert.equal(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:"); + Assert.equal(Win.winGetDrive("c:\\abc"), "c:"); + Assert.equal(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:"); + + info("Forwardslash-separated, no drive"); + Assert.equal(Win.normalize("/a/b/c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b////c"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b/c///"), "\\a\\b\\c"); + Assert.equal(Win.normalize("/a/b/c/../../../d/e/f"), "\\d\\e\\f"); + Assert.equal(Win.normalize("a/b/c/../../../d/e/f"), "d\\e\\f"); + + info("Forwardslash-separated, with a drive"); + Assert.equal(Win.normalize("c:/"), "c:\\"); + Assert.equal(Win.normalize("c:/a/b/c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b////c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:////a/b/c"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b/c///"), "c:\\a\\b\\c"); + Assert.equal(Win.normalize("c:/a/b/c/../../../d/e/f"), "c:\\d\\e\\f"); + Assert.equal(Win.normalize("c:a/b/c/../../../d/e/f"), "c:\\d\\e\\f"); + + info("Backslash-separated, UNC-style"); + Assert.equal(Win.basename("\\\\a\\b"), "b"); + Assert.equal(Win.basename("\\\\a\\b\\"), ""); + Assert.equal(Win.basename("\\\\abc"), ""); + Assert.equal(Win.dirname("\\\\a\\b"), "\\\\a"); + Assert.equal(Win.dirname("\\\\a\\b\\"), "\\\\a\\b"); + Assert.equal(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a"); + Assert.equal(Win.dirname("\\\\abc"), "\\\\abc"); + Assert.equal(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c"); + Assert.equal(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f"); + do_check_fail(() => Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f")); + + Assert.equal(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar"); + Assert.equal(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar"); + Assert.equal(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar"); + Assert.equal(Win.winGetDrive("\\\\"), null); + Assert.equal(Win.winGetDrive("\\\\c"), "\\\\c"); + Assert.equal(Win.winGetDrive("\\\\c\\abc"), "\\\\c"); + + info("Testing unix paths"); + Assert.equal(Unix.basename("a/b"), "b"); + Assert.equal(Unix.basename("a/b/"), ""); + Assert.equal(Unix.basename("abc"), "abc"); + Assert.equal(Unix.dirname("a/b"), "a"); + Assert.equal(Unix.dirname("a/b/"), "a/b"); + Assert.equal(Unix.dirname("a////b"), "a"); + Assert.equal(Unix.dirname("abc"), "."); + Assert.equal(Unix.normalize("/a/b/c"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b////c"), "/a/b/c"); + Assert.equal(Unix.normalize("////a/b/c"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b/c///"), "/a/b/c"); + Assert.equal(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f"); + Assert.equal(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f"); + do_check_fail(() => Unix.normalize("/a/b/c/../../../../d/e/f")); + + Assert.equal( + Unix.join("/tmp", "foo", "bar"), + "/tmp/foo/bar", + "join /tmp,foo,bar" + ); + Assert.equal( + Unix.join("/tmp", "/foo", "bar"), + "/foo/bar", + "join /tmp,/foo,bar" + ); + + info("Testing the presence of ospath.jsm"); + let scope; + try { + scope = ChromeUtils.import("resource://gre/modules/osfile/ospath.jsm"); + } catch (ex) { + // Can't load ospath + } + Assert.ok(!!scope.basename); +} diff --git a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js new file mode 100644 index 000000000000..4ca21a6645c5 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); +const { makeFakeAppDir } = ChromeUtils.importESModule( + "resource://testing-common/AppData.sys.mjs" +); + +function compare_paths(ospath, key) { + let file; + try { + file = Services.dirsvc.get(key, Ci.nsIFile); + } catch (ex) {} + + if (file) { + Assert.ok(!!ospath); + Assert.equal(ospath, file.path); + } else { + info( + "WARNING: " + key + " is not defined. Test may not be testing anything!" + ); + Assert.ok(!ospath); + } +} + +// Test simple paths +add_task(async function test_simple_paths() { + Assert.ok(!!OS.Constants.Path.tmpDir); + compare_paths(OS.Constants.Path.tmpDir, "TmpD"); +}); + +// Some path constants aren't set up until the profile is available. This +// test verifies that behavior. +add_task(async function test_before_after_profile() { + // On Android the profile is initialized during xpcshell init, so this test + // will fail. + if (AppConstants.platform != "android") { + Assert.equal(null, OS.Constants.Path.profileDir); + Assert.equal(null, OS.Constants.Path.localProfileDir); + Assert.equal(null, OS.Constants.Path.userApplicationDataDir); + } + + do_get_profile(); + Assert.ok(!!OS.Constants.Path.profileDir); + Assert.ok(!!OS.Constants.Path.localProfileDir); + + // UAppData is still null because the xpcshell profile doesn't set it up. + // This test is mostly here to fail in case behavior of do_get_profile() ever + // changes. We want to know if our assumptions no longer hold! + Assert.equal(null, OS.Constants.Path.userApplicationDataDir); + + await makeFakeAppDir(); + Assert.ok(!!OS.Constants.Path.userApplicationDataDir); + + // FUTURE: verify AppData too (bug 964291). +}); + +// Test presence of paths that only exist on Desktop platforms +add_task(async function test_desktop_paths() { + if (OS.Constants.Sys.Name == "Android") { + return; + } + Assert.ok(!!OS.Constants.Path.homeDir); + + compare_paths(OS.Constants.Path.homeDir, "Home"); + compare_paths(OS.Constants.Path.userApplicationDataDir, "UAppData"); + + compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir"); +}); + +// Open libxul +add_task(async function test_libxul() { + ctypes.open(OS.Constants.Path.libxul); + info("Linked to libxul"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_queue.js b/toolkit/components/osfile/tests/xpcshell/test_queue.js new file mode 100644 index 000000000000..e6e6f841c3eb --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_queue.js @@ -0,0 +1,34 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Check if Scheduler.queue returned by OS.File.queue is resolved initially. +add_task(async function check_init() { + await OS.File.queue; + info("Function resolved"); +}); + +// Check if Scheduler.queue returned by OS.File.queue is resolved +// after an operation is successful. +add_task(async function check_success() { + info("Attempting to open a file correctly"); + await OS.File.open(OS.Path.join(do_get_cwd().path, "test_queue.js")); + info("File opened correctly"); + await OS.File.queue; + info("Function resolved"); +}); + +// Check if Scheduler.queue returned by OS.File.queue is resolved +// after an operation fails. +add_task(async function check_failure() { + let exception; + try { + info("Attempting to open a non existing file"); + await OS.File.open(OS.Path.join(".", "Bigfoot")); + } catch (err) { + exception = err; + await OS.File.queue; + } + Assert.ok(exception != null); + info("Function resolved"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_read_write.js b/toolkit/components/osfile/tests/xpcshell/test_read_write.js new file mode 100644 index 000000000000..6fe554c9220f --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_read_write.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var SHARED_PATH; + +var EXISTING_FILE = do_get_file("xpcshell.ini").path; + +add_task(async function init() { + do_get_profile(); + SHARED_PATH = OS.Path.join( + OS.Constants.Path.profileDir, + "test_osfile_read.tmp" + ); +}); + +// Check that OS.File.read() is executed after the previous operation +add_test_pair(async function ordering() { + let string1 = "Initial state " + Math.random(); + let string2 = "After writing " + Math.random(); + await OS.File.writeAtomic(SHARED_PATH, string1); + OS.File.writeAtomic(SHARED_PATH, string2); + let string3 = await OS.File.read(SHARED_PATH, { encoding: "utf-8" }); + Assert.equal(string3, string2); +}); + +add_test_pair(async function read_write_all() { + let DEST_PATH = SHARED_PATH + Math.random(); + let TMP_PATH = DEST_PATH + ".tmp"; + + let test_with_options = function(options, suffix) { + return (async function() { + info( + "Running test read_write_all with options " + JSON.stringify(options) + ); + let TEST = "read_write_all " + suffix; + + let optionsBackup = JSON.parse(JSON.stringify(options)); + + // Check that read + writeAtomic performs a correct copy + let currentDir = await OS.File.getCurrentDirectory(); + let pathSource = OS.Path.join(currentDir, EXISTING_FILE); + let contents = await OS.File.read(pathSource); + Assert.ok(!!contents); // Content is not empty + let bytesRead = contents.byteLength; + + let bytesWritten = await OS.File.writeAtomic( + DEST_PATH, + contents, + options + ); + Assert.equal(bytesRead, bytesWritten); // Correct number of bytes written + + // Check that options are not altered + Assert.equal(JSON.stringify(options), JSON.stringify(optionsBackup)); + await reference_compare_files(pathSource, DEST_PATH, TEST); + + // Check that temporary file was removed or never created exist + Assert.ok(!new FileUtils.File(TMP_PATH).exists()); + + // Check that writeAtomic fails if noOverwrite is true and the destination + // file already exists! + contents = new Uint8Array(300); + let view = new Uint8Array(contents.buffer, 10, 200); + try { + let opt = JSON.parse(JSON.stringify(options)); + opt.noOverwrite = true; + await OS.File.writeAtomic(DEST_PATH, view, opt); + do_throw( + "With noOverwrite, writeAtomic should have refused to overwrite file (" + + suffix + + ")" + ); + } catch (err) { + if (err instanceof OS.File.Error && err.becauseExists) { + info( + "With noOverwrite, writeAtomic correctly failed (" + suffix + ")" + ); + } else { + throw err; + } + } + await reference_compare_files(pathSource, DEST_PATH, TEST); + + // Check that temporary file was removed or never created + Assert.ok(!new FileUtils.File(TMP_PATH).exists()); + + // Now write a subset + let START = 10; + let LENGTH = 100; + contents = new Uint8Array(300); + for (let i = 0; i < contents.byteLength; i++) { + contents[i] = i % 256; + } + view = new Uint8Array(contents.buffer, START, LENGTH); + bytesWritten = await OS.File.writeAtomic(DEST_PATH, view, options); + Assert.equal(bytesWritten, LENGTH); + + let array2 = await OS.File.read(DEST_PATH); + Assert.equal(LENGTH, array2.length); + for (let j = 0; j < LENGTH; j++) { + Assert.equal(array2[j], (j + START) % 256); + } + + // Cleanup. + await OS.File.remove(DEST_PATH); + await OS.File.remove(TMP_PATH); + })(); + }; + + await test_with_options({ tmpPath: TMP_PATH }, "Renaming, not flushing"); + await test_with_options( + { tmpPath: TMP_PATH, flush: true }, + "Renaming, flushing" + ); + await test_with_options({}, "Not renaming, not flushing"); + await test_with_options({ flush: true }, "Not renaming, flushing"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_remove.js b/toolkit/components/osfile/tests/xpcshell/test_remove.js new file mode 100644 index 000000000000..f638d99000be --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_remove.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + run_next_test(); +} + +add_task(async function test_ignoreAbsent() { + let absent_file_name = "test_osfile_front_absent.tmp"; + + // Removing absent files should throw if "ignoreAbsent" is true. + await Assert.rejects( + OS.File.remove(absent_file_name, { ignoreAbsent: false }), + err => err.operation == "remove", + "OS.File.remove throws if there is no such file." + ); + + // Removing absent files should not throw if "ignoreAbsent" is true or not + // defined. + let exception = null; + try { + await OS.File.remove(absent_file_name, { ignoreAbsent: true }); + await OS.File.remove(absent_file_name); + } catch (ex) { + exception = ex; + } + Assert.ok(!exception, "OS.File.remove should not throw when not requested."); +}); + +add_task(async function test_ignoreAbsent_directory_missing() { + let absent_file_name = OS.Path.join("absent_parent", "test.tmp"); + + // Removing absent files should throw if "ignoreAbsent" is true. + await Assert.rejects( + OS.File.remove(absent_file_name, { ignoreAbsent: false }), + err => err.operation == "remove", + "OS.File.remove throws if there is no such file." + ); + + // Removing files from absent directories should not throw if "ignoreAbsent" + // is true or not defined. + let exception = null; + try { + await OS.File.remove(absent_file_name, { ignoreAbsent: true }); + await OS.File.remove(absent_file_name); + } catch (ex) { + exception = ex; + } + Assert.ok(!exception, "OS.File.remove should not throw when not requested."); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js new file mode 100644 index 000000000000..a246afa86f97 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + + run_next_test(); +} + +add_task(async function() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + + let file = OS.Path.join(OS.Constants.Path.profileDir, "file"); + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + let file1 = OS.Path.join(dir, "file1"); + let file2 = OS.Path.join(dir, "file2"); + let subDir = OS.Path.join(dir, "subdir"); + let fileInSubDir = OS.Path.join(subDir, "file"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Remove non-existent directory + let exception = null; + try { + await OS.File.removeDir(dir, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + + // Remove non-existent directory with ignoreAbsent + await OS.File.removeDir(dir, { ignoreAbsent: true }); + await OS.File.removeDir(dir); + + // Remove file with ignoreAbsent: false + await OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" }); + exception = null; + try { + await OS.File.removeDir(file, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + + // Remove empty directory + await OS.File.makeDir(dir); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains one file + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains multiple files + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.writeAtomic(file2, "content", { tmpPath: file2 + ".tmp" }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + + // Remove directory that contains a file and a directory + await OS.File.makeDir(dir); + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + await OS.File.makeDir(subDir); + await OS.File.writeAtomic(fileInSubDir, "content", { + tmpPath: fileInSubDir + ".tmp", + }); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); +}); + +add_task(async function test_unix_symlink() { + // Windows does not implement OS.File.unixSymLink() + if (OS.Constants.Win) { + return; + } + + // Android / B2G file systems typically don't support symlinks. + if (OS.Constants.Sys.Name == "Android") { + return; + } + + let file = OS.Path.join(OS.Constants.Path.profileDir, "file"); + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + let file1 = OS.Path.join(dir, "file1"); + + // This test will create the following directory structure: + // /file (regular file) + // /file.link => file (symlink) + // /directory (directory) + // /linkdir => directory (directory) + // /directory/file1 (regular file) + // /directory3 (directory) + // /directory3/file3 (directory) + // /directory/link2 => ../directory3 (regular file) + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + await OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" }); + Assert.ok(await OS.File.exists(file)); + let info = await OS.File.stat(file, { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(!info.isSymLink); + + await OS.File.unixSymLink(file, file + ".link"); + Assert.ok(await OS.File.exists(file + ".link")); + info = await OS.File.stat(file + ".link", { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(info.isSymLink); + info = await OS.File.stat(file + ".link"); + Assert.ok(!info.isDir); + Assert.ok(!info.isSymLink); + await OS.File.remove(file + ".link"); + Assert.equal(false, await OS.File.exists(file + ".link")); + + await OS.File.makeDir(dir); + Assert.ok(await OS.File.exists(dir)); + info = await OS.File.stat(dir, { unixNoFollowingLinks: true }); + Assert.ok(info.isDir); + Assert.ok(!info.isSymLink); + + let link = OS.Path.join(OS.Constants.Path.profileDir, "linkdir"); + + await OS.File.unixSymLink(dir, link); + Assert.ok(await OS.File.exists(link)); + info = await OS.File.stat(link, { unixNoFollowingLinks: true }); + Assert.ok(!info.isDir); + Assert.ok(info.isSymLink); + info = await OS.File.stat(link); + Assert.ok(info.isDir); + Assert.ok(!info.isSymLink); + + let dir3 = OS.Path.join(OS.Constants.Path.profileDir, "directory3"); + let file3 = OS.Path.join(dir3, "file3"); + let link2 = OS.Path.join(dir, "link2"); + + await OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" }); + Assert.ok(await OS.File.exists(file1)); + await OS.File.makeDir(dir3); + Assert.ok(await OS.File.exists(dir3)); + await OS.File.writeAtomic(file3, "content", { tmpPath: file3 + ".tmp" }); + Assert.ok(await OS.File.exists(file3)); + await OS.File.unixSymLink("../directory3", link2); + Assert.ok(await OS.File.exists(link2)); + + await OS.File.removeDir(link); + Assert.equal(false, await OS.File.exists(link)); + Assert.ok(await OS.File.exists(file1)); + await OS.File.removeDir(dir); + Assert.equal(false, await OS.File.exists(dir)); + Assert.ok(await OS.File.exists(file3)); + await OS.File.removeDir(dir3); + Assert.equal(false, await OS.File.exists(dir3)); + + // This task will be executed only on Unix-like systems. + // Please do not add tests independent to operating systems here + // or implement symlink() on Windows. +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js new file mode 100644 index 000000000000..a81463bc7f7b --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("toolkit.osfile.log", false); +}); + +function run_test() { + Services.prefs.setBoolPref("toolkit.osfile.log", true); + + run_next_test(); +} + +/** + * Test OS.File.removeEmptyDir + */ +add_task(async function() { + // Set up profile. We create the directory in the profile, because the profile + // is removed after every test run. + do_get_profile(); + + let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory"); + + // Sanity checking for the test + Assert.equal(false, await OS.File.exists(dir)); + + // Remove non-existent directory + await OS.File.removeEmptyDir(dir); + + // Remove non-existent directory with ignoreAbsent + await OS.File.removeEmptyDir(dir, { ignoreAbsent: true }); + + // Remove non-existent directory with ignoreAbsent false + let exception = null; + try { + await OS.File.removeEmptyDir(dir, { ignoreAbsent: false }); + } catch (ex) { + exception = ex; + } + + Assert.ok(!!exception); + Assert.ok(exception instanceof OS.File.Error); + Assert.ok(exception.becauseNoSuchFile); + + // Remove empty directory + await OS.File.makeDir(dir); + await OS.File.removeEmptyDir(dir); + Assert.equal(false, await OS.File.exists(dir)); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_reset.js b/toolkit/components/osfile/tests/xpcshell/test_reset.js new file mode 100644 index 000000000000..41dea1e9dd1c --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_reset.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Path = OS.Constants.Path; + +add_task(async function init() { + do_get_profile(); +}); + +add_task(async function reset_before_launching() { + info("Reset without launching OS.File, it shouldn't break"); + await OS.File.resetWorker(); +}); + +add_task(async function transparent_reset() { + for (let i = 1; i < 3; ++i) { + info( + "Do stome stuff before and after " + + i + + " reset(s), " + + "it shouldn't break" + ); + let CONTENT = "some content " + i; + let path = OS.Path.join(Path.profileDir, "tmp"); + await OS.File.writeAtomic(path, CONTENT); + for (let j = 0; j < i; ++j) { + await OS.File.resetWorker(); + } + let data = await OS.File.read(path); + let string = new TextDecoder().decode(data); + Assert.equal(string, CONTENT); + } +}); + +add_task(async function file_open_cannot_reset() { + let TEST_FILE = OS.Path.join(Path.profileDir, "tmp-" + Math.random()); + info( + "Leaking file descriptor " + TEST_FILE + ", we shouldn't be able to reset" + ); + let openedFile = await OS.File.open(TEST_FILE, { create: true }); + let thrown = false; + try { + await OS.File.resetWorker(); + } catch (ex) { + if (ex.message.includes(OS.Path.basename(TEST_FILE))) { + thrown = true; + } else { + throw ex; + } + } + Assert.ok(thrown); + + info("Closing the file, we should now be able to reset"); + await openedFile.close(); + await OS.File.resetWorker(); +}); + +add_task(async function dir_open_cannot_reset() { + let TEST_DIR = await OS.File.getCurrentDirectory(); + info("Leaking directory " + TEST_DIR + ", we shouldn't be able to reset"); + let iterator = new OS.File.DirectoryIterator(TEST_DIR); + let thrown = false; + try { + await OS.File.resetWorker(); + } catch (ex) { + if (ex.message.includes(OS.Path.basename(TEST_DIR))) { + thrown = true; + } else { + throw ex; + } + } + Assert.ok(thrown); + + info("Closing the directory, we should now be able to reset"); + await iterator.close(); + await OS.File.resetWorker(); +}); + +add_task(async function race_against_itself() { + info("Attempt to get resetWorker() to race against itself"); + // Arbitrary operation, just to wake up the worker + try { + await OS.File.read("/foo"); + } catch (ex) {} + + let all = []; + for (let i = 0; i < 100; ++i) { + all.push(OS.File.resetWorker()); + } + + await Promise.all(all); +}); + +add_task(async function finish_with_a_reset() { + info("Reset without waiting for the result"); + // Arbitrary operation, just to wake up the worker + try { + await OS.File.read("/foo"); + } catch (ex) {} + // Now reset + /* don't yield*/ OS.File.resetWorker(); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_shutdown.js b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js new file mode 100644 index 000000000000..9e8c696481b2 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js @@ -0,0 +1,103 @@ +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +add_task(function init() { + do_get_profile(); +}); + +/** + * Test logging of file descriptors leaks. + */ +add_task(async function system_shutdown() { + // Test that unclosed files cause warnings + // Test that unclosed directories cause warnings + // Test that closed files do not cause warnings + // Test that closed directories do not cause warnings + function testLeaksOf(resource, topic) { + return (async function() { + let deferred = PromiseUtils.defer(); + + // Register observer + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + Services.prefs.setBoolPref("toolkit.osfile.log", true); + Services.prefs.setBoolPref("toolkit.osfile.log.redirect", true); + Services.prefs.setCharPref( + "toolkit.osfile.test.shutdown.observer", + topic + ); + + let observer = function(aMessage) { + try { + info("Got message: " + aMessage); + if (!(aMessage instanceof Ci.nsIConsoleMessage)) { + return; + } + let message = aMessage.message; + info("Got message: " + message); + if (!message.includes("TEST OS Controller WARNING")) { + return; + } + info( + "Got message: " + message + ", looking for resource " + resource + ); + if (!message.includes(resource)) { + return; + } + info("Resource: " + resource + " found"); + executeSoon(deferred.resolve); + } catch (ex) { + executeSoon(function() { + deferred.reject(ex); + }); + } + }; + Services.console.registerListener(observer); + Services.obs.notifyObservers(null, topic); + do_timeout(1000, function() { + info("Timeout while waiting for resource: " + resource); + deferred.reject("timeout"); + }); + + let resolved = false; + try { + await deferred.promise; + resolved = true; + } catch (ex) { + if (ex == "timeout") { + resolved = false; + } else { + throw ex; + } + } + Services.console.unregisterListener(observer); + Services.prefs.clearUserPref("toolkit.osfile.log"); + Services.prefs.clearUserPref("toolkit.osfile.log.redirect"); + Services.prefs.clearUserPref("toolkit.osfile.test.shutdown.observer"); + Services.prefs.clearUserPref("toolkit.async_shutdown.testing"); + + return resolved; + })(); + } + + let TEST_DIR = OS.Path.join(await OS.File.getCurrentDirectory(), ".."); + info("Testing for leaks of directory iterator " + TEST_DIR); + let iterator = new OS.File.DirectoryIterator(TEST_DIR); + info("At this stage, we leak the directory"); + Assert.ok(await testLeaksOf(TEST_DIR, "test.shutdown.dir.leak")); + await iterator.close(); + info("At this stage, we don't leak the directory anymore"); + Assert.equal(false, await testLeaksOf(TEST_DIR, "test.shutdown.dir.noleak")); + + let TEST_FILE = OS.Path.join(OS.Constants.Path.profileDir, "test"); + info("Testing for leaks of file descriptor: " + TEST_FILE); + let openedFile = await OS.File.open(TEST_FILE, { create: true }); + info("At this stage, we leak the file"); + Assert.ok(await testLeaksOf(TEST_FILE, "test.shutdown.file.leak")); + await openedFile.close(); + info("At this stage, we don't leak the file anymore"); + Assert.equal( + false, + await testLeaksOf(TEST_FILE, "test.shutdown.file.leak.2") + ); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_telemetry.js b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js new file mode 100644 index 000000000000..178a27b3d807 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js @@ -0,0 +1,61 @@ +"use strict"; + +var { + OS: { File, Path, Constants }, +} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +// Ensure that we have a profile but that the OS.File worker is not launched +add_task(async function init() { + do_get_profile(); + await File.resetWorker(); +}); + +function getCount(histogram) { + if (histogram == null) { + return 0; + } + + let total = 0; + for (let i of Object.values(histogram.values)) { + total += i; + } + return total; +} + +// Ensure that launching the OS.File worker adds data to the relevant +// histograms +add_task(async function test_startup() { + let LAUNCH = "OSFILE_WORKER_LAUNCH_MS"; + let READY = "OSFILE_WORKER_READY_MS"; + + let before = Services.telemetry.getSnapshotForHistograms("main", false) + .parent; + + // Launch the OS.File worker + await File.getCurrentDirectory(); + + let after = Services.telemetry.getSnapshotForHistograms("main", false).parent; + + info("Ensuring that we have recorded measures for histograms"); + Assert.equal(getCount(after[LAUNCH]), getCount(before[LAUNCH]) + 1); + Assert.equal(getCount(after[READY]), getCount(before[READY]) + 1); + + info("Ensuring that launh <= ready"); + Assert.ok(after[LAUNCH].sum <= after[READY].sum); +}); + +// Ensure that calling writeAtomic adds data to the relevant histograms +add_task(async function test_writeAtomic() { + let LABEL = "OSFILE_WRITEATOMIC_JANK_MS"; + + let before = Services.telemetry.getSnapshotForHistograms("main", false) + .parent; + + // Perform a write. + let path = Path.join(Constants.Path.profileDir, "test_osfile_telemetry.tmp"); + await File.writeAtomic(path, LABEL, { tmpPath: path + ".tmp" }); + + let after = Services.telemetry.getSnapshotForHistograms("main", false).parent; + + Assert.equal(getCount(after[LABEL]), getCount(before[LABEL]) + 1); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/test_unique.js b/toolkit/components/osfile/tests/xpcshell/test_unique.js new file mode 100644 index 000000000000..740d84a2d655 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_unique.js @@ -0,0 +1,87 @@ +"use strict"; + +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +function run_test() { + do_get_profile(); + run_next_test(); +} + +function testFiles(filename) { + return (async function() { + const MAX_TRIES = 10; + let profileDir = OS.Constants.Path.profileDir; + let path = OS.Path.join(profileDir, filename); + + // Ensure that openUnique() uses the file name if there is no file with that name already. + let openedFile = await OS.File.openUnique(path); + info("\nCreate new file: " + openedFile.path); + await openedFile.file.close(); + let exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + Assert.equal(path, openedFile.path); + let fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() creates a new file name using a HEX number, as the original name is already taken. + openedFile = await OS.File.openUnique(path); + info("\nCreate unique HEX file: " + openedFile.path); + await openedFile.file.close(); + exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() generates different file names each time, using the HEX number algorithm + let filenames = new Set(); + for (let i = 0; i < MAX_TRIES; i++) { + openedFile = await OS.File.openUnique(path); + await openedFile.file.close(); + filenames.add(openedFile.path); + } + + Assert.equal(filenames.size, MAX_TRIES); + + // Ensure that openUnique() creates a new human readable file name using, as the original name is already taken. + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + info("\nCreate unique Human Readable file: " + openedFile.path); + await openedFile.file.close(); + exists = await OS.File.exists(openedFile.path); + Assert.ok(exists); + fileInfo = await OS.File.stat(openedFile.path); + Assert.ok(fileInfo.size == 0); + + // Ensure that openUnique() generates different human readable file names each time + filenames = new Set(); + for (let i = 0; i < MAX_TRIES; i++) { + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + await openedFile.file.close(); + filenames.add(openedFile.path); + } + + Assert.equal(filenames.size, MAX_TRIES); + + let exn; + try { + for (let i = 0; i < 100; i++) { + openedFile = await OS.File.openUnique(path, { humanReadable: true }); + await openedFile.file.close(); + } + } catch (ex) { + exn = ex; + } + + info("Ensure that this raises the correct error"); + Assert.ok(!!exn); + Assert.ok(exn instanceof OS.File.Error); + Assert.ok(exn.becauseExists); + })(); +} + +add_task(async function test_unique() { + OS.Shared.DEBUG = true; + // Tests files with extension + await testFiles("dummy_unique_file.txt"); + // Tests files with no extension + await testFiles("dummy_unique_file_no_ext"); +}); diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000000..02b7345a3c53 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -0,0 +1,48 @@ +[DEFAULT] +head = head.js + +[test_compression.js] +[test_constants.js] +[test_duration.js] +[test_exception.js] +[test_file_URL_conversion.js] +[test_logging.js] +[test_makeDir.js] +[test_open.js] +[test_osfile_async.js] +[test_osfile_async_append.js] +[test_osfile_async_bytes.js] +[test_osfile_async_copy.js] +[test_osfile_async_flush.js] +[test_osfile_async_largefiles.js] +[test_osfile_async_setDates.js] +# Unimplemented on Windows (bug 1022816). +# Spurious failure on Android test farm due to non-POSIX behavior of +# filesystem backing /mnt/sdcard (not worth trying to fix). +[test_osfile_async_setPermissions.js] +skip-if = os == "win" || os == "android" +[test_osfile_closed.js] +[test_osfile_error.js] +[test_osfile_kill.js] +# Windows test +[test_osfile_win_async_setPermissions.js] +skip-if = os != "win" +[test_osfile_writeAtomic_backupTo_option.js] +[test_osfile_writeAtomic_zerobytes.js] +[test_osfile_writeAtomic_unicode_filename.js] +[test_path.js] +[test_path_constants.js] +[test_queue.js] +[test_read_write.js] +requesttimeoutfactor = 4 +[test_remove.js] +[test_removeDir.js] +requesttimeoutfactor = 4 +[test_removeEmptyDir.js] +[test_reset.js] +[test_shutdown.js] +[test_telemetry.js] +# On Android, we use OS.File during xpcshell initialization, so the expected +# telemetry cannot be observed. +skip-if = toolkit == "android" +[test_unique.js] diff --git a/tools/esmify/map.json b/tools/esmify/map.json index 1a8aaad7bb24..f51d2ce28a60 100644 --- a/tools/esmify/map.json +++ b/tools/esmify/map.json @@ -857,6 +857,15 @@ "resource://gre/modules/nsAsyncShutdown.jsm": "toolkit/components/asyncshutdown/nsAsyncShutdown.jsm", "resource://gre/modules/nsCrashMonitor.jsm": "toolkit/components/crashmonitor/nsCrashMonitor.jsm", "resource://gre/modules/nsFormAutoCompleteResult.jsm": "toolkit/components/satchel/nsFormAutoCompleteResult.jsm", + "resource://gre/modules/osfile.jsm": "toolkit/components/osfile/osfile.jsm", + "resource://gre/modules/osfile/osfile_async_front.jsm": "toolkit/components/osfile/modules/osfile_async_front.jsm", + "resource://gre/modules/osfile/osfile_native.jsm": "toolkit/components/osfile/modules/osfile_native.jsm", + "resource://gre/modules/osfile/osfile_shared_allthreads.jsm": "toolkit/components/osfile/modules/osfile_shared_allthreads.jsm", + "resource://gre/modules/osfile/osfile_unix_allthreads.jsm": "toolkit/components/osfile/modules/osfile_unix_allthreads.jsm", + "resource://gre/modules/osfile/osfile_win_allthreads.jsm": "toolkit/components/osfile/modules/osfile_win_allthreads.jsm", + "resource://gre/modules/osfile/ospath.jsm": "toolkit/components/osfile/modules/ospath.jsm", + "resource://gre/modules/osfile/ospath_unix.jsm": "toolkit/components/osfile/modules/ospath_unix.jsm", + "resource://gre/modules/osfile/ospath_win.jsm": "toolkit/components/osfile/modules/ospath_win.jsm", "resource://gre/modules/pdfjs.js": "toolkit/components/pdfjs/pdfjs.js", "resource://gre/modules/policies/WindowsGPOParser.jsm": "toolkit/components/enterprisepolicies/WindowsGPOParser.jsm", "resource://gre/modules/policies/macOSPoliciesParser.jsm": "toolkit/components/enterprisepolicies/macOSPoliciesParser.jsm", diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js index a7d8c067f0c1..a01d19ab3dbd 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js @@ -169,6 +169,7 @@ module.exports = { "mozilla/reject-chromeutils-import-params": "error", "mozilla/reject-importGlobalProperties": ["error", "allownonwebidl"], "mozilla/reject-multiple-getters-calls": "error", + "mozilla/reject-osfile": "warn", "mozilla/reject-scriptableunicodeconverter": "warn", "mozilla/rejects-requires-await": "error", "mozilla/use-cc-etc": "error", diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js index aad6d8bbbdd3..c8f83c3a07b9 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js @@ -72,6 +72,7 @@ module.exports = { "reject-lazy-imports-into-globals": require("../lib/rules/reject-lazy-imports-into-globals"), "reject-mixing-eager-and-lazy": require("../lib/rules/reject-mixing-eager-and-lazy"), "reject-multiple-getters-calls": require("../lib/rules/reject-multiple-getters-calls"), + "reject-osfile": require("../lib/rules/reject-osfile"), "reject-scriptableunicodeconverter": require("../lib/rules/reject-scriptableunicodeconverter"), "reject-relative-requires": require("../lib/rules/reject-relative-requires"), "reject-some-requires": require("../lib/rules/reject-some-requires"), diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-osfile.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-osfile.js new file mode 100644 index 000000000000..708ba11bf570 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-osfile.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Reject calls into OS.File. We're phasing this out in + * favour of IOUtils. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const { maybeGetMemberPropertyName } = require("../helpers"); + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isOSProp(expr, prop) { + return ( + maybeGetMemberPropertyName(expr.object) === "OS" && + isIdentifier(expr.property, prop) + ); +} + +module.exports = { + meta: { + docs: { + url: + "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/reject-osfile.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + MemberExpression(node) { + if (isOSProp(node, "File")) { + context.report({ + node, + message: "OS.File is deprecated. You should use IOUtils instead.", + }); + } else if (isOSProp(node, "Path")) { + context.report({ + node, + message: "OS.Path is deprecated. You should use PathUtils instead.", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-osfile.js b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-osfile.js new file mode 100644 index 000000000000..a1f26e6aa8e3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/tests/reject-osfile.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require("../lib/rules/reject-osfile"); +var RuleTester = require("eslint").RuleTester; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: "latest" } }); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +function invalidError(os, util) { + let message = `${os} is deprecated. You should use ${util} instead.`; + return [{ message, type: "MemberExpression" }]; +} + +ruleTester.run("reject-osfile", rule, { + valid: ["IOUtils.write()"], + invalid: [ + { + code: "OS.File.write()", + errors: invalidError("OS.File", "IOUtils"), + }, + { + code: "lazy.OS.File.write()", + errors: invalidError("OS.File", "IOUtils"), + }, + ], +}); + +ruleTester.run("reject-osfile", rule, { + valid: ["PathUtils.join()"], + invalid: [ + { + code: "OS.Path.join()", + errors: invalidError("OS.Path", "PathUtils"), + }, + { + code: "lazy.OS.Path.join()", + errors: invalidError("OS.Path", "PathUtils"), + }, + ], +});