diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/osfile/modules | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/osfile/modules')
15 files changed, 9799 insertions, 0 deletions
diff --git a/toolkit/components/osfile/modules/moz.build b/toolkit/components/osfile/modules/moz.build new file mode 100644 index 0000000000..ea14a82b76 --- /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 0000000000..04100160ee --- /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 0000000000..8cf6fab81f --- /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 0000000000..5534887510 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_native.jsm @@ -0,0 +1,127 @@ +/* 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 + let encoding = options.encoding || "utf-8"; + buffer = new TextEncoder(encoding).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 0000000000..d3b952beeb --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm @@ -0,0 +1,1369 @@ +/* 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.import("resource://gre/modules/ctypes.jsm"); + 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 0000000000..2917b9663f --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_front.js @@ -0,0 +1,608 @@ +/* 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 + let encoding = options.encoding || "utf-8"; + buffer = new TextEncoder(encoding).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 0000000000..189d80e29f --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm @@ -0,0 +1,410 @@ +/* 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.import("resource://gre/modules/ctypes.jsm"); + // 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 0000000000..89600e1abc --- /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 0000000000..a2f6c3ce66 --- /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 0000000000..0788570a6f --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm @@ -0,0 +1,442 @@ +/* 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.import("resource://gre/modules/ctypes.jsm"); + // 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 0000000000..5460ef7830 --- /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 0000000000..056464574e --- /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 0000000000..3e06f9705d --- /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 0000000000..065c2ee8ea --- /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 0000000000..57fb7429ee --- /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]; + } +} |