gecko-dev/toolkit/components/osfile/osfile_win_front.jsm
2012-08-13 21:54:43 -04:00

825 lines
30 KiB
JavaScript

/* 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 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.
*/
setPosition: function setPosition(pos, whence) {
if (whence === undefined) {
whence = Const.FILE_BEGIN;
}
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;
}
File.Win = exports.OS.Win.File;
File.Error = exports.OS.Shared.Win.Error;
exports.OS.File = File;
exports.OS.Path = exports.OS.Win.Path;
Object.defineProperty(File, "POS_START", { value: OS.Shared.POS_START });
Object.defineProperty(File, "POS_CURRENT", { value: OS.Shared.POS_CURRENT });
Object.defineProperty(File, "POS_END", { value: OS.Shared.POS_END });
})(this);
}