diff options
Diffstat (limited to 'toolkit/components/osfile/modules')
15 files changed, 8965 insertions, 0 deletions
diff --git a/toolkit/components/osfile/modules/moz.build b/toolkit/components/osfile/modules/moz.build new file mode 100644 index 000000000..7a0580ca3 --- /dev/null +++ b/toolkit/components/osfile/modules/moz.build @@ -0,0 +1,22 @@ +# -*- 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.jsm', + 'osfile_unix_allthreads.jsm', + 'osfile_unix_back.jsm', + 'osfile_unix_front.jsm', + 'osfile_win_allthreads.jsm', + 'osfile_win_back.jsm', + 'osfile_win_front.jsm', + 'ospath.jsm', + 'ospath_unix.jsm', + 'ospath_win.jsm', +] 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 000000000..181471cd8 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -0,0 +1,1533 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["OS"]; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +var SharedAll = {}; +Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); + + +// 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) { + Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll); +} else if (SharedAll.Constants.libc) { + Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll); +} else { + throw new Error("I am neither under Windows nor under a Posix system"); +} +var OSError = SysAll.Error; +var Type = SysAll.Type; + +var Path = {}; +Cu.import("resource://gre/modules/osfile/ospath.jsm", Path); + +// The library of promises. +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); + +// The implementation of communications +Cu.import("resource://gre/modules/PromiseWorker.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); +Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); +var Native = Cu.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"], + ["winStartMenuProgsDir", "Progs"], + ]) { + + 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; +} + +// In order to expose Scheduler to the unfiltered Cu.import return value variant +// on B2G we need to save it to `this`. This does not make it public; +// EXPORTED_SYMBOLS still controls that in all cases. +var Scheduler = this.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, + + /** + * 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; + } + return this._worker; + }, + + _worker: null, + + /** + * Prepare to kill the OS.File worker after a few seconds. + */ + restartTimer: function(arg) { + let delay; + try { + delay = Services.prefs.getIntPref("osfile.reset_worker_delay"); + } catch(e) { + // Don't auto-shutdown if we don't have a delay preference set. + return; + } + + if (this.resetTimer) { + clearTimeout(this.resetTimer); + } + this.resetTimer = setTimeout( + () => Scheduler.kill({reset: true, shutdown: false}), + delay + ); + }, + + /** + * 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: function({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 = Promise.defer(); + let savedQueue = this.queue; + this.queue = deferred.promise; + + return this._killQueue = Task.spawn(function*() { + + yield killQueue; + // From this point, and until the end of the Task, we are the + // only call to `kill`, regardless of any `yield`. + + yield 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 = []; + Scheduler.latestSent = [Date.now(), + Task.Debugging.generateReadableStack(new Error().stack), + ...message]; + + // Wait for result + let resources; + try { + resources = yield 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 > 0) { + msg += "The following files are still open:\n" + + openedFiles.join("\n"); + } + if (openedDirectoryIterators.length > 0) { + 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(); + } + + }.bind(this)); + }, + + /** + * 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: function(code) { + let promise = this.queue.then(code); + // By definition, |this.queue| can never reject. + this.queue = promise.then(null, () => undefined); + // Fork |promise| to ensure that uncaught errors are reported + return promise.then(null, null); + }, + + /** + * 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. + * @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(Task.async(function*() { + 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(); + + + let reply; + try { + try { + Scheduler.Debugging.messagesSent++; + Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2); + reply = yield this.worker.post(method, args, closure); + Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)]; + 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(); + } + }.bind(this))); + }, + + /** + * Post Telemetry statistics. + * + * This is only useful on first launch. + */ + _updateTelemetry: function() { + 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) { + let pref = oldPref; + try { + pref = Services.prefs.getBoolPref(prefName); + } catch (x) { + // In case of an error when reading a pref keep it as is. + } + // If neither pref nor oldPref were set, default it to false. + return pref; +}; + +/** + * 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]); + } + }, false); +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); + }, false); +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); + }, false); + + +// 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]); +} + +// Observer topics used for monitoring shutdown +const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown"; + +// Preference used to configure test shutdown observer. +const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER = + "toolkit.osfile.test.shutdown.observer"; + +AsyncShutdown.webWorkersShutdown.addBlocker( + "OS.File: flush pending requests, warn about unclosed files, shut down service.", + Task.async(function*() { + // Give clients a last chance to enqueue requests. + yield Barriers.shutdown.wait({crashAfterMS: null}); + + // Wait until all requests are complete and kill the worker. + yield 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 = null; + try { + TOPIC = Services.prefs.getCharPref( + PREF_OSFILE_TEST_SHUTDOWN_OBSERVER); + } catch (x) { + } + 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}) + ); + } + }, 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|, if it is passed. + options = clone(options, ["outExecutionDuration"]); + 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" && SharedAll.Constants.Sys.Name != "Gonk") { + /** + * 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); +}; + +/** + * Change the current directory + * + * @param {string} path The OS-specific path to the current directory. + * You should use the methods of OS.Path and the constants of OS.Constants.Path + * to build OS-specific paths in a portable manner. + * + * @return {promise} + * @resolves {null} + * @rejects {OS.Error} + */ +File.setCurrentDirectory = function setCurrentDirectory(path) { + return Scheduler.post( + "setCurrentDirectory", [Type.path.toMsg(path)], 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. + * + * @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]); + }; +} + +/** + * Gets the number of bytes available on disk to the current user. + * + * @param {string} Platform-specific path to a directory on the disk to + * query for free available bytes. + * + * @return {number} The number of bytes available for the current user. + * @throws {OS.File.Error} In case of any error. + */ +File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) { + return Scheduler.post("getAvailableFreeSpace", + [Type.path.toMsg(sourcePath)], sourcePath + ).then(Type.uint64_t.fromMsg); +}; + +/** + * 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"]); + 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 = {}) { + // Copy |options| to avoid modifying the original object but preserve the + // reference to |outExecutionDuration| option if it is passed. + options = clone(options, ["outExecutionDuration"]); + // As options.tmpPath is a path, we need to encode it as |Type.path| message + if ("tmpPath" in options) { + options.tmpPath = Type.path.toMsg(options.tmpPath); + }; + if (isTypedArray(buffer) && (!("bytes" in options))) { + options.bytes = buffer.byteLength; + }; + let refObj = {}; + TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj); + let 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) { + if (k != "creationDate") { + Object.defineProperty(this, k, {value: value[k]}); + } + } + Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]}); +}; +File.Info.prototype = SysAll.AbstractInfo.prototype; + +// Deprecated +Object.defineProperty(File.Info.prototype, "creationDate", { + get: function creationDate() { + let {Deprecated} = Cu.import("resource://gre/modules/Deprecated.jsm", {}); + Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes"); + return this._deprecatedCreationDate; + } +}); + +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 + * @rejects {StopIteration} If all entries have already been visited + * or the iterator has been closed. + */ + this.__itmsg = Scheduler.post( + "new_DirectoryIterator", [Type.path.toMsg(path), options], + path + ); + this._isClosed = false; +}; +DirectoryIterator.prototype = { + iterator: function () { + return this; + }, + __iterator__: function () { + return this; + }, + + // Once close() is called, _itmsg should reject with a + // StopIteration. However, we don't want to create the promise until + // it's needed because it might never be used. In that case, we + // would get a warning on the console. + get _itmsg() { + if (!this.__itmsg) { + this.__itmsg = Promise.reject(StopIteration); + } + return this.__itmsg; + }, + + /** + * Determine whether the directory exists. + * + * @resolves {boolean} + */ + exists: function exists() { + return this._itmsg.then( + function onSuccess(iterator) { + return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]); + } + ); + }, + /** + * Get the next entry in the directory. + * + * @return {Promise} + * @resolves {OS.File.Entry} + * @rejects {StopIteration} If all entries have already been visited. + */ + next: function next() { + let self = this; + let promise = this._itmsg; + + // Get the iterator, call _next + promise = promise.then( + function withIterator(iterator) { + return self._next(iterator); + }); + + return promise; + }, + /** + * 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. + */ + nextBatch: function nextBatch(size) { + if (this._isClosed) { + return Promise.resolve([]); + } + let promise = this._itmsg; + promise = promise.then( + function withIterator(iterator) { + return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]); + }); + promise = promise.then( + function withEntries(array) { + return array.map(DirectoryIterator.Entry.fromMsg); + }); + return promise; + }, + /** + * 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. + */ + forEach: function forEach(cb, options) { + if (this._isClosed) { + return Promise.resolve(); + } + + let self = this; + let position = 0; + let iterator; + + // Grab iterator + let promise = this._itmsg.then( + function(aIterator) { + iterator = aIterator; + } + ); + + // Then iterate + let loop = function loop() { + if (self._isClosed) { + return Promise.resolve(); + } + return self._next(iterator).then( + function onSuccess(value) { + return Promise.resolve(cb(value, position++, self)).then(loop); + }, + function onFailure(reason) { + if (reason == StopIteration) { + return; + } + throw reason; + } + ); + }; + + return promise.then(loop); + }, + /** + * Auxiliary method: fetch the next item + * + * @rejects {StopIteration} If all entries have already been visited + * or the iterator has been closed. + */ + _next: function _next(iterator) { + if (this._isClosed) { + return this._itmsg; + } + let self = this; + let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]); + promise = promise.then( + DirectoryIterator.Entry.fromMsg, + function onReject(reason) { + if (reason == StopIteration) { + self.close(); + throw StopIteration; + } + throw reason; + }); + return promise; + }, + /** + * Close the iterator + */ + close: function close() { + if (this._isClosed) { + return Promise.resolve(); + } + this._isClosed = true; + let self = this; + return this._itmsg.then( + function withIterator(iterator) { + // Set __itmsg to null so that the _itmsg getter returns a + // rejected StopIteration promise if it's ever used. + self.__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 Task.spawn(function*() { + let resources = yield 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; + +this.OS = {}; +this.OS.File = File; +this.OS.Constants = SharedAll.Constants; +this.OS.Shared = { + LOG: SharedAll.LOG, + Type: SysAll.Type, + get DEBUG() { + return SharedAll.Config.DEBUG; + }, + set DEBUG(x) { + return SharedAll.Config.DEBUG = x; + } +}; +Object.freeze(this.OS.Shared); +this.OS.Path = Path; + +// Returns a resolved promise when all the queued operation have been completed. +Object.defineProperty(OS.File, "queue", { + get: function() { + 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 = Components.classes["@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: function() { + 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}`, + Task.async(function*() { + // Give clients a last chance to enqueue requests. + yield Barriers[phaseName].wait({crashAfterMS: null}); + + // Wait until all currently enqueued requests are completed. + yield 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 000000000..84287c75e --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_async_worker.js @@ -0,0 +1,407 @@ +/* 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/. */ + + +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 + }; + + 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 = []) { + return Agent[method](...args); + }, + 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)); + + /** + * 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: function(id) { + let result = this._map.get(id); + if (result == null) { + return result; + } + return result.resource; + }, + /** + * Remove a resource from its unique identifier. + */ + remove: function(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: function(resource, info) { + let id = this._idgen++; + this._map.set(id, {resource:resource, info: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 " + file.__proto__.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: function(aDEBUG) { + SharedAll.Config.DEBUG = aDEBUG; + }, + // Return worker's current OS.Shared.DEBUG value to controller. + // Note: This is used for testing purposes. + GET_DEBUG: function() { + 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: function(kill) { + let result = { + openedFiles: OpenedFiles.listOpenedResources(), + openedDirectoryIterators: OpenedDirectoryIterators.listOpenedResources(), + killed: false // Placeholder + }; + + // Is it safe to kill the worker? + let safe = result.openedFiles.length == 0 + && result.openedDirectoryIterators.length == 0; + 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()); + }, + setCurrentDirectory: function setCurrentDirectory(path) { + File.setCurrentDirectory(exports.OS.Shared.Type.path.fromMsg(path)); + }, + 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); + }, + getAvailableFreeSpace: function getAvailableFreeSpace(sourcePath) { + return Type.uint64_t.toMsg( + File.getAvailableFreeSpace(Type.path.fromMsg(sourcePath))); + }, + 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: function(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() { + try { + return File.DirectoryIterator.Entry.toMsg(this.next()); + } catch (x) { + if (x == StopIteration) { + OpenedDirectoryIterators.remove(dir); + } + throw x; + } + }, 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 000000000..16cd3c92a --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_native.jsm @@ -0,0 +1,70 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["read"]; + +var {results: Cr, utils: Cu, interfaces: Ci} = Components; + +var SharedAll = Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", {}); + +var SysAll = {}; +if (SharedAll.Constants.Win) { + Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll); +} else if (SharedAll.Constants.libc) { + Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll); +} else { + throw new Error("I am neither under Windows nor under a Posix system"); +} +var {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +/** + * The native service holding the implementation of the functions. + */ +XPCOMUtils.defineLazyServiceGetter(this, + "Internals", + "@mozilla.org/toolkit/osfile/native-internals;1", + "nsINativeOSFileInternalsService"); + +/** + * Native implementation of OS.File.read + * + * This implementation does not handle option |compression|. + */ +this.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")); + } + + let deferred = Promise.defer(); + Internals.read(path, + options, + function onSuccess(success) { + success.QueryInterface(Ci.nsINativeOSFileResult); + if ("outExecutionDuration" in options) { + options.outExecutionDuration = + success.executionDurationMS + + (options.outExecutionDuration || 0); + } + deferred.resolve(success.result); + }, + function onError(operation, oserror) { + deferred.reject(new SysAll.Error(operation, oserror, path)); + } + ); + return deferred.promise; +}; 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 000000000..c5c505102 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm @@ -0,0 +1,1315 @@ +/* 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. + */ + +// Boilerplate used to be able to import this module both from the main +// thread and from worker threads. + +// Since const is lexically scoped, hoist the +// conditionally-useful definition ourselves. +const Cu = typeof Components != "undefined" ? Components.utils : undefined; +const Ci = typeof Components != "undefined" ? Components.interfaces : undefined; +const Cc = typeof Components != "undefined" ? Components.classes : undefined; + +/** + * 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. + this.exports = {}; + + Cu.import("resource://gre/modules/Services.jsm", this); + Meta = Cu.import("resource://gre/modules/PromiseWorker.jsm", {}).BasePromiseWorker.Meta; +} else { + 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 + Cu.import("resource://gre/modules/ctypes.jsm"); + Cc["@mozilla.org/net/osfileconstantsservice;1"]. + getService(Ci.nsIOSFileConstantsService).init(); +} + +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: 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; +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; + }); + } else { + 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: function() { + return object[key]; + }, + set: function(value) { + object[key] = value; + } + }); + }; + for (let k in object) { + if (refs.indexOf(k) < 0) { + 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: function() { + 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: function() { + 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: function() { + 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; + } else { + 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: name, type: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: function() { + 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: function(object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get: function() { + 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: function(object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get: function() { + 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: function(fallbacklibrary, object, field, ...args) { + let lib = this; + Object.defineProperty(object, field, { + get: function() { + 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: function() { + 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]; + let argtypes = []; + 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); + } + 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: function() { + 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: function() { + 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: LOG, + clone: clone, + Type: Type, + HollowStructure: HollowStructure, + Error: OSError, + declareFFI: declareFFI, + projectValue: projectValue, + isTypedArray: isTypedArray, + defineLazyGetter: defineLazyGetter + } +}; + +Object.defineProperty(exports.OS.Shared, "DEBUG", { + get: function() { + return Config.DEBUG; + }, + set: function(x) { + return Config.DEBUG = x; + } +}); +Object.defineProperty(exports.OS.Shared, "TEST", { + get: function() { + return Config.TEST; + }, + set: function(x) { + return Config.TEST = x; + } +}); + + +///////////////////// Permanent boilerplate +if (typeof Components != "undefined") { + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_shared_front.jsm b/toolkit/components/osfile/modules/osfile_shared_front.jsm new file mode 100644 index 000000000..a2971991d --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm @@ -0,0 +1,567 @@ +/* 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. + */ + +if (typeof Components != "undefined") { + throw new Error("osfile_shared_front.jsm 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"); +var LOG = 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; + } else { + 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: 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 ... + } + } + throw OS.File.Error.exists("could not find an unused file name.", path); + } +}; + +/** + * Code shared by iterators. + */ +AbstractFile.AbstractIterator = function AbstractIterator() { +}; +AbstractFile.AbstractIterator.prototype = { + /** + * Allow iterating with |for| + */ + __iterator__: function __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 in 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 in 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"); + } + 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. + } + } + // 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. + } + } + + 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 in 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 000000000..632f9c40b --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm @@ -0,0 +1,375 @@ +/* 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(). + */ + +"use strict"; + +var SharedAll; +if (typeof Components != "undefined") { + let Cu = Components.utils; + // Module is opened as a jsm module + Cu.import("resource://gre/modules/ctypes.jsm", this); + + SharedAll = {}; + Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); + 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()"); +} + +var LOG = 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).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.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") { + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_unix_back.jsm b/toolkit/components/osfile/modules/osfile_unix_back.jsm new file mode 100644 index 000000000..a028dda7d --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_back.jsm @@ -0,0 +1,735 @@ +/* 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/. */ + +{ + if (typeof Components != "undefined") { + // We do not wish osfile_unix_back.jsm 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.jsm 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"); + let LOG = 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) { + let declareFFI; + if (aDeclareFFI) { + declareFFI = aDeclareFFI.bind(null, libc); + } else { + declareFFI = SysAll.declareFFI; + } + let declareLazyFFI = 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: 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); + + // To complicate further, MacOS and some BSDs have a field |birthtime| + if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) { + stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_BIRTHTIME, + "st_birthtime", 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"); + + // 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_BSIZE, + "f_bsize", Type.unsigned_long.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, + /*fd*/ 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, + /*dirp*/ 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, + /*ptr*/ ctypes.voidptr_t); + } + + + // Other functions + libc.declareLazyFFI(SysFile, "access", + "access", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*mode*/ Type.int); + + libc.declareLazyFFI(SysFile, "chdir", + "chdir", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path); + + libc.declareLazyFFI(SysFile, "chmod", + "chmod", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*mode*/ Type.mode_t); + + libc.declareLazyFFI(SysFile, "chown", + "chown", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*uid*/ Type.uid_t, + /*gid*/ Type.gid_t); + + libc.declareLazyFFI(SysFile, "copyfile", + "copyfile", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*source*/ Type.path, + /*dest*/ Type.path, + /*state*/ Type.void_t.in_ptr, // Ignored atm + /*flags*/ Type.uint32_t); + + libc.declareLazyFFI(SysFile, "dup", + "dup", ctypes.default_abi, + /*return*/ Type.negativeone_or_fd, + /*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, + /*dir*/ Type.DIR.in_ptr); + } + + libc.declareLazyFFI(SysFile, "chdir", + "chdir", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path); + + libc.declareLazyFFI(SysFile, "fchdir", + "fchdir", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd); + + libc.declareLazyFFI(SysFile, "fchmod", + "fchmod", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd, + /*mode*/ Type.mode_t); + + libc.declareLazyFFI(SysFile, "fchown", + "fchown", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd, + /*uid_t*/ Type.uid_t, + /*gid_t*/ Type.gid_t); + + libc.declareLazyFFI(SysFile, "fsync", + "fsync", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd); + + libc.declareLazyFFI(SysFile, "getcwd", + "getcwd", ctypes.default_abi, + /*return*/ Type.out_path, + /*buf*/ Type.out_path, + /*size*/ Type.size_t); + + libc.declareLazyFFI(SysFile, "getwd", + "getwd", ctypes.default_abi, + /*return*/ Type.out_path, + /*buf*/ 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 + ), + /*buf*/ Type.void_t.out_ptr); + + libc.declareLazyFFI(SysFile, "fdatasync", + "fdatasync", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd); // Note: MacOS/BSD-specific + + libc.declareLazyFFI(SysFile, "ftruncate", + "ftruncate", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*fd*/ Type.fd, + /*length*/ Type.off_t); + + + libc.declareLazyFFI(SysFile, "lchown", + "lchown", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*uid_t*/ Type.uid_t, + /*gid_t*/ Type.gid_t); + + libc.declareLazyFFI(SysFile, "link", + "link", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*source*/ Type.path, + /*dest*/ Type.path); + + libc.declareLazyFFI(SysFile, "lseek", + "lseek", ctypes.default_abi, + /*return*/ Type.off_t, + /*fd*/ 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, + /*return*/ Type.fd, + /*template*/ Type.out_path); + + libc.declareLazyFFI(SysFile, "open", + "open", ctypes.default_abi, + /*return*/ Type.negativeone_or_fd, + /*path*/ Type.path, + /*oflags*/ Type.int, + /*mode*/ Type.int); + + if (OS.Constants.Sys.Name == "NetBSD") { + libc.declareLazyFFI(SysFile, "opendir", + "__opendir30", ctypes.default_abi, + /*return*/ Type.null_or_DIR_ptr, + /*path*/ Type.path); + } else { + libc.declareLazyFFI(SysFile, "opendir", + "opendir", ctypes.default_abi, + /*return*/ Type.null_or_DIR_ptr, + /*path*/ Type.path); + } + + libc.declareLazyFFI(SysFile, "pread", + "pread", ctypes.default_abi, + /*return*/ Type.negativeone_or_ssize_t, + /*fd*/ Type.fd, + /*buf*/ 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, + /*fd*/ Type.fd, + /*buf*/ 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, + /*fd*/ Type.fd, + /*buf*/ Type.void_t.out_ptr, + /*nbytes*/Type.size_t); + + libc.declareLazyFFI(SysFile, "posix_fadvise", + "posix_fadvise", ctypes.default_abi, + /*return*/ Type.int, + /*fd*/ Type.fd, + /*offset*/ Type.off_t, + /*len*/ Type.off_t, + /*advise*/ Type.int); + + if (Const._DARWIN_FEATURE_64_BIT_INODE) { + // 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, + /*dir*/ 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, + /*dir*/ Type.DIR.in_ptr); // Other Unices + } else { + libc.declareLazyFFI(SysFile, "readdir", + "readdir", ctypes.default_abi, + /*return*/Type.null_or_dirent_ptr, + /*dir*/ Type.DIR.in_ptr); // Other Unices + } + + libc.declareLazyFFI(SysFile, "rename", + "rename", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*old*/ Type.path, + /*new*/ Type.path); + + libc.declareLazyFFI(SysFile, "rmdir", + "rmdir", ctypes.default_abi, + /*return*/ Type.int, + /*path*/ Type.path); + + libc.declareLazyFFI(SysFile, "splice", + "splice", ctypes.default_abi, + /*return*/ Type.long, + /*fd_in*/ Type.fd, + /*off_in*/ Type.off_t.in_ptr, + /*fd_out*/ Type.fd, + /*off_out*/Type.off_t.in_ptr, + /*len*/ Type.size_t, + /*flags*/ Type.unsigned_int); // Linux/Android-specific + + libc.declareLazyFFI(SysFile, "statfs", + "statfs", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*buf*/ Type.statvfs.out_ptr); // Android,B2G + + libc.declareLazyFFI(SysFile, "statvfs", + "statvfs", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*path*/ Type.path, + /*buf*/ Type.statvfs.out_ptr); // Other platforms + + libc.declareLazyFFI(SysFile, "symlink", + "symlink", ctypes.default_abi, + /*return*/ Type.negativeone_or_nothing, + /*source*/ Type.path, + /*dest*/ Type.path); + + libc.declareLazyFFI(SysFile, "truncate", + "truncate", ctypes.default_abi, + /*return*/Type.negativeone_or_nothing, + /*path*/ 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_FEATURE_64_BIT_INODE) { + // 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.jsm b/toolkit/components/osfile/modules/osfile_unix_front.jsm new file mode 100644 index 000000000..19a27ae1a --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm @@ -0,0 +1,1193 @@ +/* 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. + */ + +{ + if (typeof Components != "undefined") { + // We do not wish osfile_unix_front.jsm to be used directly as a main thread + // module yet. + + throw new Error("osfile_unix_front.jsm cannot be used from the main thread yet"); + } + (function(exports) { + "use strict"; + + // exports.OS.Unix is created by osfile_unix_back.jsm + 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(); + let LOG = 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; + } + return; + }; + + /** + * 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, 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; + } else { + 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); + } + }; + + /** + * Gets the number of bytes available on disk to the current user. + * + * @param {string} sourcePath Platform-specific path to a directory on + * the disk to query for free available bytes. + * + * @return {number} The number of bytes available for the current user. + * @throws {OS.File.Error} In case of any error. + */ + File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) { + let fileSystemInfo = new Type.statvfs.implementation(); + let fileSystemInfoPtr = fileSystemInfo.address(); + + throw_on_negative("statvfs", (UnixFile.statvfs || UnixFile.statfs)(sourcePath, fileSystemInfoPtr)); + + let bytes = new Type.uint64_t.implementation( + fileSystemInfo.f_bsize * fileSystemInfo.f_bavail); + + return bytes.value; + }; + + /** + * 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 chunk_size = Math.min(nbytes, bufSize); + 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; + let result; + 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) { + result = pump_userland(source, dest, options); + } else { + result = 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, 0); + 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) + return; + + // If the error is not EXDEV ("not on the same device"), + // or if the error is EXDEV and we have passed an option + // that prevents us from crossing devices, throw the + // error. + if (ctypes.errno != Const.EXDEV || options.noCopy) { + throw new File.Error("move", ctypes.errno, sourcePath); + } + + // Otherwise, copy and remove. + File.copy(sourcePath, destPath, options); + // FIXME: Clean-up in case of copy error? + 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 {File.Entry} The next entry in the directory. + * @throws {StopIteration} Once all files in the directory have been + * encountered. + */ + File.DirectoryIterator.prototype.next = function next() { + if (!this._exists) { + throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path); + } + if (this._closed) { + throw StopIteration; + } + 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)) { + // |dirent| doesn't have d_type on some platforms (e.g. Solaris). + 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 new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path); + } + this.close(); + throw StopIteration; + }; + + /** + * 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); + + // Some platforms (e.g. MacOS X, some BSDs) store a file creation date + if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) { + let date = new Date(stat.st_birthtime * 1000); + + /** + * The date of creation of this file. + * + * Note that the date returned by this method is not always + * reliable. Not all file systems are able to provide this + * information. + * + * @type {Date} + */ + this.macBirthDate = date; + } + }; + File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype); + + // Deprecated, use macBirthDate/winBirthDate instead + Object.defineProperty(File.Info.prototype, "creationDate", { + get: function creationDate() { + // On the Macintosh, returns the birth date if available. + // On other Unix, as the birth date is not available, + // returns the epoch. + return this.macBirthDate || new Date(0); + } + }); + + /** + * 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(); + + timevals[0].tv_sec = (accessDate / 1000) | 0; + timevals[0].tv_usec = 0; + timevals[1].tv_sec = (modificationDate / 1000) | 0; + timevals[1].tv_usec = 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(); + }; + + /** + * Set the current directory by setCurrentDirectory. + */ + File.setCurrentDirectory = function setCurrentDirectory(path) { + throw_on_negative("setCurrentDirectory", + UnixFile.chdir(path), + path + ); + }; + + /** + * Get/set the current directory. + */ + Object.defineProperty(File, "curDir", { + set: function(path) { + this.setCurrentDirectory(path); + }, + get: function() { + 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 000000000..b059d4e12 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm @@ -0,0 +1,425 @@ +/* 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(). + */ + +"use strict"; + +var SharedAll; +if (typeof Components != "undefined") { + let Cu = Components.utils; + // Module is opened as a jsm module + Cu.import("resource://gre/modules/ctypes.jsm", this); + + SharedAll = {}; + Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); + 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()"); +} + +var LOG = 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, + /*flags*/ ctypes.uint32_t, + /*source*/ ctypes.voidptr_t, + /*msgid*/ ctypes.uint32_t, + /*langid*/ ctypes.uint32_t, + /*buf*/ ctypes.char16_t.ptr, + /*size*/ 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 = "") { + operation = operation; + 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, + /* Output buffer*/ buf, + /* Minimum size of buffer */ 1024, + /* Format args*/ 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, + winBirthDate, + lastAccessDate, lastWriteDate, + winAttributes) { + this._path = path; + this._isDir = isDir; + this._isSymLink = isSymLink; + this._size = size; + this._winBirthDate = winBirthDate; + 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; + }, + // Deprecated + get creationDate() { + return this._winBirthDate; + }, + /** + * The date of creation of this file. + * + * @type {Date} + */ + get winBirthDate() { + return this._winBirthDate; + }, + /** + * 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, + winCreationDate, winLastWriteDate, + winLastAccessDate, path) { + this._isDir = isDir; + this._isSymLink = isSymLink; + this._name = name; + this._winCreationDate = winCreationDate; + 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 creation time of this file. + * @type {Date} + */ + get winCreationDate() { + return this._winCreationDate; + }, + /** + * 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") { + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + this[symbol] = exports[symbol]; + } +} diff --git a/toolkit/components/osfile/modules/osfile_win_back.jsm b/toolkit/components/osfile/modules/osfile_win_back.jsm new file mode 100644 index 000000000..21172b6bf --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_back.jsm @@ -0,0 +1,437 @@ +/* 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) + */ + +{ + if (typeof Components != "undefined") { + // We do not wish osfile_win.jsm 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.jsm 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"); + let LOG = 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; + } + let declareLazyFFI = 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.Win.File = { Type: 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; + } else { + 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; + } else { + 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, + /*destPath*/ Type.path, + /*bailIfExist*/Type.bool); + + libc.declareLazyFFI(SysFile, "CreateDirectory", + "CreateDirectoryW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*name*/ Type.char16_t.in_ptr, + /*security*/Type.SECURITY_ATTRIBUTES.in_ptr); + + libc.declareLazyFFI(SysFile, "CreateFile", + "CreateFileW", ctypes.winapi_abi, + /*return*/ Type.file_HANDLE, + /*name*/ Type.path, + /*access*/ Type.DWORD_FLAGS, + /*share*/ Type.DWORD_FLAGS, + /*security*/Type.SECURITY_ATTRIBUTES.in_ptr, + /*creation*/Type.DWORD_FLAGS, + /*flags*/ Type.DWORD_FLAGS, + /*template*/Type.HANDLE); + + libc.declareLazyFFI(SysFile, "DeleteFile", + "DeleteFileW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*path*/ 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, + /*return*/ Type.zero_or_nothing, + /*systime*/ Type.SystemTime.in_ptr, + /*filetime*/ Type.FILETIME.out_ptr); + + libc.declareLazyFFI(SysFile, "FindFirstFile", + "FindFirstFileW", ctypes.winapi_abi, + /*return*/ Type.find_HANDLE, + /*pattern*/Type.path, + /*data*/ Type.FindData.out_ptr); + + libc.declareLazyFFI(SysFile, "FindNextFile", + "FindNextFileW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*prev*/ Type.find_HANDLE, + /*data*/ Type.FindData.out_ptr); + + libc.declareLazyFFI(SysFile, "FormatMessage", + "FormatMessageW", ctypes.winapi_abi, + /*return*/ Type.DWORD, + /*flags*/ Type.DWORD_FLAGS, + /*source*/ Type.void_t.in_ptr, + /*msgid*/ Type.DWORD_FLAGS, + /*langid*/ Type.DWORD_FLAGS, + /*buf*/ Type.out_wstring, + /*size*/ Type.DWORD, + /*Arguments*/Type.void_t.in_ptr + ); + + libc.declareLazyFFI(SysFile, "GetCurrentDirectory", + "GetCurrentDirectoryW", ctypes.winapi_abi, + /*return*/ Type.zero_or_DWORD, + /*length*/ Type.DWORD, + /*buf*/ Type.out_path + ); + + libc.declareLazyFFI(SysFile, "GetFullPathName", + "GetFullPathNameW", ctypes.winapi_abi, + /*return*/ Type.zero_or_DWORD, + /*fileName*/ Type.path, + /*length*/ Type.DWORD, + /*buf*/ 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, + /*info*/ Type.FILE_INFORMATION.out_ptr); + + libc.declareLazyFFI(SysFile, "MoveFileEx", + "MoveFileExW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*sourcePath*/ Type.path, + /*destPath*/ Type.path, + /*flags*/ Type.DWORD + ); + + libc.declareLazyFFI(SysFile, "ReadFile", + "ReadFile", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*file*/ 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, + /*path*/ Type.path); + + libc.declareLazyFFI(SysFile, "SetCurrentDirectory", + "SetCurrentDirectoryW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*path*/ Type.path + ); + + libc.declareLazyFFI(SysFile, "SetEndOfFile", + "SetEndOfFile", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*file*/ Type.HANDLE); + + libc.declareLazyFFI(SysFile, "SetFilePointer", + "SetFilePointer", ctypes.winapi_abi, + /*return*/ Type.DWORD, + /*file*/ Type.HANDLE, + /*distlow*/Type.long, + /*disthi*/ Type.long.in_ptr, + /*method*/ Type.DWORD); + + libc.declareLazyFFI(SysFile, "SetFileTime", + "SetFileTime", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*file*/ Type.HANDLE, + /*creation*/ Type.FILETIME.in_ptr, + /*access*/ Type.FILETIME.in_ptr, + /*write*/ Type.FILETIME.in_ptr); + + + libc.declareLazyFFI(SysFile, "WriteFile", + "WriteFile", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*file*/ 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, + /*file*/ Type.HANDLE); + + libc.declareLazyFFI(SysFile, "GetFileAttributes", + "GetFileAttributesW", ctypes.winapi_abi, + /*return*/ Type.DWORD_FLAGS, + /*fileName*/ Type.path); + + libc.declareLazyFFI(SysFile, "SetFileAttributes", + "SetFileAttributesW", ctypes.winapi_abi, + /*return*/ Type.zero_or_nothing, + /*fileName*/ Type.path, + /*fileAttributes*/ Type.DWORD_FLAGS); + + advapi32.declareLazyFFI(SysFile, "GetNamedSecurityInfo", + "GetNamedSecurityInfoW", ctypes.winapi_abi, + /*return*/ Type.DWORD, + /*objectName*/ Type.path, + /*objectType*/ Type.DWORD, + /*securityInfo*/ Type.DWORD, + /*sidOwner*/ Type.PSID.out_ptr, + /*sidGroup*/ Type.PSID.out_ptr, + /*dacl*/ Type.PACL.out_ptr, + /*sacl*/ Type.PACL.out_ptr, + /*securityDesc*/ Type.PSECURITY_DESCRIPTOR.out_ptr); + + advapi32.declareLazyFFI(SysFile, "SetNamedSecurityInfo", + "SetNamedSecurityInfoW", ctypes.winapi_abi, + /*return*/ Type.DWORD, + /*objectName*/ Type.path, + /*objectType*/ Type.DWORD, + /*securityInfo*/ Type.DWORD, + /*sidOwner*/ Type.PSID, + /*sidGroup*/ Type.PSID, + /*dacl*/ Type.PACL, + /*sacl*/ Type.PACL); + + libc.declareLazyFFI(SysFile, "LocalFree", + "LocalFree", ctypes.winapi_abi, + /*return*/ Type.HLOCAL, + /*mem*/ Type.HLOCAL); + }; + + exports.OS.Win = { + File: { + _init: init + } + }; + })(this); +} diff --git a/toolkit/components/osfile/modules/osfile_win_front.jsm b/toolkit/components/osfile/modules/osfile_win_front.jsm new file mode 100644 index 000000000..387dd08b5 --- /dev/null +++ b/toolkit/components/osfile/modules/osfile_win_front.jsm @@ -0,0 +1,1266 @@ +/* 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. + */ + +{ + if (typeof Components != "undefined") { + // We do not wish osfile_win_front.jsm to be used directly as a main thread + // module yet. + throw new Error("osfile_win_front.jsm cannot be used from the main thread yet"); + } + + (function(exports) { + "use strict"; + + + // exports.OS.Win is created by osfile_win_back.jsm + 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; + } + return; + }; + + /** + * 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)); + } + }; + + /** + * Gets the number of bytes available on disk to the current user. + * + * @param {string} sourcePath Platform-specific path to a directory on + * the disk to query for free available bytes. + * + * @return {number} The number of bytes available for the current user. + * @throws {OS.File.Error} In case of any error. + */ + File.getAvailableFreeSpace = function Win_getAvailableFreeSpace(sourcePath) { + let freeBytesAvailableToUser = new Type.uint64_t.implementation(0); + let freeBytesAvailableToUserPtr = freeBytesAvailableToUser.address(); + + throw_on_zero("getAvailableFreeSpace", + WinFile.GetDiskFreeSpaceEx(sourcePath, freeBytesAvailableToUserPtr, null, null) + ); + + return freeBytesAvailableToUser.value; + }; + + /** + * 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; + } else { + let error = ctypes.winLastError; + this.close(); + if (error == Const.ERROR_NO_MORE_FILES) { + return null; + } else { + 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 {File.Entry} The next entry in the directory. + * @throws {StopIteration} Once all files in the directory have been + * encountered. + */ + 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 new File.DirectoryIterator.Entry(entry, this._path); + } + throw StopIteration; + }; + + 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.ftCreationTime || + !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 winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime, this._path); + 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, + winCreationDate, 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 winBirthDate = FILETIME_to_Date(stat.ftCreationTime, this._path); + 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, + winBirthDate, 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 */; + } + }; + + /** + * Set the current directory by setCurrentDirectory. + */ + File.setCurrentDirectory = function setCurrentDirectory(path) { + throw_on_zero("setCurrentDirectory", + WinFile.SetCurrentDirectory(path), + path); + }; + + /** + * Get/set the current directory by |curDir|. + */ + Object.defineProperty(File, "curDir", { + set: function(path) { + this.setCurrentDirectory(path); + }, + get: function() { + 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; + } + + /** + * 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.winLastError, 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.winLastError, 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.winLastError, 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.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 000000000..68bbe4345 --- /dev/null +++ b/toolkit/components/osfile/modules/ospath.jsm @@ -0,0 +1,45 @@ +/* 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"; + +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 Cu = Components.utils; + let Scope = {}; + Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", Scope); + + let Path = {}; + if (Scope.OS.Constants.Win) { + Cu.import("resource://gre/modules/osfile/ospath_win.jsm", Path); + } else { + Cu.import("resource://gre/modules/osfile/ospath_unix.jsm", Path); + } + + this.EXPORTED_SYMBOLS = []; + for (let k in Path) { + this.EXPORTED_SYMBOLS.push(k); + 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 000000000..1d574baed --- /dev/null +++ b/toolkit/components/osfile/modules/ospath_unix.jsm @@ -0,0 +1,202 @@ +/* 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") { + Components.utils.importGlobalProperties(["URL"]); + // Global definition of |exports|, to keep everybody happy. + // In non-main thread, |exports| is provided by the module + // loader. + 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 == 0) { + 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 == 0) { + 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") { + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + 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 000000000..31a87b115 --- /dev/null +++ b/toolkit/components/osfile/modules/ospath_win.jsm @@ -0,0 +1,373 @@ +/** + * 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") { + Components.utils.importGlobalProperties(["URL"]); + // Global definition of |exports|, to keep everybody happy. + // In non-main thread, |exports| is provided by the module + // loader. + 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) || "."; + } else { + // Or just "." + return "."; + } + } + + if (index == 1 && path.charAt(0) == "\\") { + // The path is reduced to a UNC drive + if (noDrive) { + return "."; + } else { + 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 == 0) { + 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") { + this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; + for (let symbol of EXPORTED_SYMBOLS) { + this[symbol] = exports[symbol]; + } +} |