/* 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"); } importScripts("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); importScripts("resource://gre/modules/osfile/osfile_win_back.jsm"); importScripts("resource://gre/modules/osfile/ospath_win_back.jsm"); (function(exports) { "use strict"; // exports.OS.Win is created by osfile_win_back.jsm if (exports.OS.File) { return; // Avoid double-initialization } exports.OS.Win.File._init(); let Const = exports.OS.Constants.Win; let WinFile = exports.OS.Win.File; let LOG = OS.Shared.LOG.bind(OS.Shared, "Win front-end"); // Mutable thread-global data // In the Windows implementation, methods |read| and |write| // require passing a pointer to an int32 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 int32 and a C int32* 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.int32_t(-1); let gBytesReadPtr = gBytesRead.address(); let gBytesWritten = new ctypes.int32_t(-1); let gBytesWrittenPtr = gBytesWritten.address(); // Same story for GetFileInformationByHandle let gFileInfo = new OS.Shared.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. * @constructor */ let File = function File(fd) { this._fd = fd; }; File.prototype = { /** * If the file is open, this returns the file descriptor. * Otherwise, throw a |File.Error|. */ get fd() { return this._fd; }, // Placeholder getter, used to replace |get fd| once // the file is closed. _nofd: function nofd(operation) { operation = operation || this._nofd.caller.name || "unknown operation"; throw new File.Error(operation, Const.INVALID_HANDLE_VALUE); }, /** * 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. */ close: function close() { if (this._fd) { let fd = this._fd; this._fd = null; delete this.fd; Object.defineProperty(this, "fd", {get: File.prototype._nofd}); // Call CloseHandle(fd), detach finalizer if (fd.dispose() == 0) { this._closeResult = new File.Error("close", ctypes.errno); } } if (this._closeResult) { throw this._closeResult; } return; }, _closeResult: null, /** * Read some bytes from a file. * * @param {ArrayBuffer} 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. */ read: function read(buffer, nbytes, options) { // |gBytesReadPtr| is a pointer to |gBytesRead|. throw_on_zero("read", WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null) ); return gBytesRead.value; }, /** * Write some bytes to a file. * * @param {ArrayBuffer} 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. */ write: function write(buffer, nbytes, options) { // |gBytesWrittenPtr| is a pointer to |gBytesWritten|. throw_on_zero("write", WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null) ); return gBytesWritten.value; }, /** * Return the current position in the file. */ 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 taken from the start of the file. * If |OS.File.POS_CURRENT|, |pos| is taken from the current position * in the file. If |OS.File.POS_END|, |pos| is taken from the end of * the file. * * @return The new position in the file. */ setPosition: function setPosition(pos, whence) { // We are cheating to avoid one unnecessary conversion: // In this implementation, // OS.File.POS_START == OS.Constants.Win.FILE_BEGIN // OS.File.POS_CURRENT == OS.Constants.Win.FILE_CURRENT // OS.File.POS_END == OS.Constants.Win.FILE_END whence = (whence == undefined)?Const.FILE_BEGIN:whence; return throw_on_negative("setPosition", WinFile.SetFilePointer(this.fd, pos, null, whence)); }, /** * Fetch the information on the file. * * @return File.Info The information on |this| file. */ stat: function stat() { throw_on_zero("stat", WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr)); return new File.Info(gFileInfo); } }; // Constant used to normalize options. const noOptions = {}; // 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|. If neither |truncate| nor * |create| is specified, the file is opened for appending. * * 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) { options = options || noOptions; mode = mode || noOptions; let share = options.winShare || DEFAULT_SHARE; let security = options.winSecurity || null; let flags = options.winFlags || DEFAULT_FLAGS; let template = options.winTemplate?options.winTemplate._fd:null; let access; let disposition; 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 { mode = OS.Shared._aux.normalizeOpenMode(mode); 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 /*append*/ { 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)); 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)); return file; }; /** * Remove an existing file. * * @param {string} path The name of the file. * @throws {OS.File.Error} In case of I/O error. */ File.remove = function remove(path) { throw_on_zero("remove", WinFile.DeleteFile(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) { options = options || noOptions; throw_on_zero("copy", WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false) ); }; /** * 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. * * @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) { options = options || noOptions; let flags; if (options.noOverwrite) { flags = Const.MOVEFILE_COPY_ALLOWED; } else { flags = Const.MOVEFILE_COPY_ALLOWED | Const.MOVEFILE_REPLACE_EXISTING; } throw_on_zero("move", WinFile.MoveFileEx(sourcePath, destPath, flags) ); }; /** * A global value used to receive data during a * |FindFirstFile|/|FindNextFile|. */ let gFindData = new OS.Shared.Type.FindData.implementation(); let gFindDataPtr = gFindData.address(); /** * A global value used to receive data during time conversions. */ let gSystemTime = new OS.Shared.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) { if (fileTime == null) { throw new TypeError("Expecting a non-null filetime"); } throw_on_zero("FILETIME_to_Date", WinFile.FileTimeToSystemTime(fileTime.address(), gSystemTimePtr)); // 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); }; /** * 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) { if (options && options.winPattern) { this._pattern = path + "\\" + options.winPattern; } else { this._pattern = path + "\\*"; } this._handle = null; this._path = path; this._started = false; }; File.DirectoryIterator.prototype = { __iterator__: function __iterator__() { return this; }, /** * Fetch the next entry in the directory. * * @return null If we have reached the end of the directory. */ _next: function _next() { // Iterator is not fully initialized yet. Finish // initialization. if (!this._started) { this._started = true; this._handle = WinFile.FindFirstFile(this._pattern, gFindDataPtr); if (this._handle == null) { let error = ctypes.winLastError; if (error == Const.ERROR_FILE_NOT_FOUND) { this.close(); return null; } else { throw new File.Error("iter (FindFirstFile)", error); } } return gFindData; } // We have closed this iterator already. if (!this._handle) { return null; } if (WinFile.FindNextFile(this._handle, gFindDataPtr)) { return gFindData; } else { let error = ctypes.winLastError; this.close(); if (error == Const.ERROR_NO_MORE_FILES) { return null; } else { throw new File.Error("iter (FindNextFile)", error); } } }, /** * 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. */ 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; }, close: function close() { if (!this._handle) { return; } WinFile.FindClose(this._handle); this._handle = null; } }; File.DirectoryIterator.Entry = function Entry(win_entry, parent) { // Copy the relevant part of |win_entry| to ensure that // our data is not overwritten prematurely. if (!win_entry.dwFileAttributes) { throw new TypeError(); } this._dwFileAttributes = win_entry.dwFileAttributes; this._name = win_entry.cFileName.readString(); if (!this._name) { throw new TypeError("Empty name"); } this._ftCreationTime = win_entry.ftCreationTime; if (!win_entry.ftCreationTime) { throw new TypeError(); } this._ftLastAccessTime = win_entry.ftLastAccessTime; if (!win_entry.ftLastAccessTime) { throw new TypeError(); } this._ftLastWriteTime = win_entry.ftLastWriteTime; if (!win_entry.ftLastWriteTime) { throw new TypeError(); } if (!parent) { throw new TypeError("Empty parent"); } this._parent = parent; }; File.DirectoryIterator.Entry.prototype = { /** * |true| if the entry is a directory, |false| otherwise */ get isDir() { return !!(this._dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY); }, /** * |true| if the entry is a symbolic link, |false| otherwise */ get isSymLink() { return !!(this._dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT); }, /** * The name of the entry. * @type {string} */ get name() { return this._name; }, /** * The creation time of this file. * @type {Date} */ get winCreationTime() { let date = FILETIME_to_Date(this._ftCreationTime); delete this.winCreationTime; Object.defineProperty(this, "winCreationTime", {value: date}); return date; }, /** * The last modification time of this file. * @type {Date} */ get winLastWriteTime() { let date = FILETIME_to_Date(this._ftLastWriteTime); delete this.winLastWriteTime; Object.defineProperty(this, "winLastWriteTime", {value: date}); return date; }, /** * The last access time of this file. * @type {Date} */ get winLastAccessTime() { let date = FILETIME_to_Date(this._ftLastAccessTime); delete this.winLastAccessTime; Object.defineProperty(this, "winLastAccessTime", {value: date}); return date; }, /** * The full path to the entry. * @type {string} */ get path() { delete this.path; let path = OS.Win.Path.join(this._parent, this.name); Object.defineProperty(this, "path", {value: path}); return path; } }; /** * 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) { this._dwFileAttributes = stat.dwFileAttributes; this._ftCreationTime = stat.ftCreationTime; this._ftLastAccessTime = stat.ftLastAccessTime; this._ftLastWriteTime = stat.ftLastAccessTime; this._nFileSizeHigh = stat.nFileSizeHigh; this._nFileSizeLow = stat.nFileSizeLow; }; File.Info.prototype = { /** * |true| if this file is a directory, |false| otherwise */ get isDir() { return !!(this._dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY); }, /** * |true| if this file is a symbolink link, |false| otherwise */ get isSymLink() { return !!(this._dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT); }, /** * 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() { try { return OS.Shared.projectValue( ctypes.uint64_t("" + this._nFileSizeHigh + this._nFileSizeLow)); } catch (x) { return NaN; } }, /** * The date of creation of this file * * @type {Date} */ get creationDate() { delete this.creationDate; let date = FILETIME_to_Date(this._ftCreationTime); Object.defineProperty(this, "creationDate", { value: date }); return date; }, /** * 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() { delete this.lastAccess; let date = FILETIME_to_Date(this._ftLastAccessTime); Object.defineProperty(this, "lastAccessDate", { value: date }); return date; }, /** * Return 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() { delete this.lastModification; let date = FILETIME_to_Date(this._ftLastWriteTime); Object.defineProperty(this, "lastModificationDate", { value: date }); return date; } }; /** * 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: OS.Constants.Win.FILE_FLAG_BACKUP_SEMANTICS, winDisposition: OS.Constants.Win.OPEN_EXISTING }; /** * Get/set the current directory. */ Object.defineProperty(File, "curDir", { set: function(path) { throw_on_zero("set curDir", WinFile.SetCurrentDirectory(path)); }, get: function() { // 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 byt 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.jschar, buffer_size))(); let expected_size = throw_on_zero("get curDir", 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 // Note that, even in crazy scenarios, the loop will eventually // converge, as the length of the paths cannot increase infinitely. buffer_size = expected_size; } } } ); // Utility functions, used for error-handling function error_or_file(maybe) { if (maybe == exports.OS.Constants.Win.INVALID_HANDLE_VALUE) { throw new File.Error("open"); } return new File(maybe); } function throw_on_zero(operation, result) { if (result == 0) { throw new File.Error(operation); } return result; } function throw_on_negative(operation, result) { if (result < 0) { throw new File.Error(operation); } return result; } function throw_on_null(operation, result) { if (result == null || (result.isNull && result.isNull())) { throw new File.Error(operation); } return result; } // Constants // Constants for File.prototype.setPosition File.POS_START = Const.FILE_BEGIN; File.POS_CURRENT = Const.FILE_CURRENT; File.POS_END = Const.FILE_END; File.Win = exports.OS.Win.File; File.Error = exports.OS.Shared.Win.Error; exports.OS.File = File; exports.OS.Path = exports.OS.Win.Path; })(this); }