/* 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"; module.metadata = { "stability": "experimental" }; const { Cc, Ci, CC } = require("chrome"); const { setTimeout } = require("../timers"); const { Stream, InputStream, OutputStream } = require("./stream"); const { emit, on } = require("../event/core"); const { Buffer } = require("./buffer"); const { ns } = require("../core/namespace"); const { Class } = require("../core/heritage"); const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath"); const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1", "nsIFileOutputStream", "init"); const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", "nsIFileInputStream", "init"); const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"); const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream", "setOutputStream"); const StreamPump = CC("@mozilla.org/network/input-stream-pump;1", "nsIInputStreamPump", "init"); const { createOutputTransport, createInputTransport } = Cc["@mozilla.org/network/stream-transport-service;1"]. getService(Ci.nsIStreamTransportService); const { OPEN_UNBUFFERED } = Ci.nsITransport; const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream; const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile; const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream; const FILE_PERMISSION = 0o666; const PR_UINT32_MAX = 0xfffffff; // Values taken from: // http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615 const PR_RDONLY = 0x01; const PR_WRONLY = 0x02; const PR_RDWR = 0x04; const PR_CREATE_FILE = 0x08; const PR_APPEND = 0x10; const PR_TRUNCATE = 0x20; const PR_SYNC = 0x40; const PR_EXCL = 0x80; const FLAGS = { "r": PR_RDONLY, "r+": PR_RDWR, "w": PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY, "w+": PR_CREATE_FILE | PR_TRUNCATE | PR_RDWR, "a": PR_APPEND | PR_CREATE_FILE | PR_WRONLY, "a+": PR_APPEND | PR_CREATE_FILE | PR_RDWR }; function accessor() { let map = new WeakMap(); return function(fd, value) { if (value === null) map.delete(fd); if (value !== undefined) map.set(fd, value); return map.get(fd); } } var nsIFile = accessor(); var nsIFileInputStream = accessor(); var nsIFileOutputStream = accessor(); var nsIBinaryInputStream = accessor(); var nsIBinaryOutputStream = accessor(); // Just a contstant object used to signal that all of the file // needs to be read. const ALL = new String("Read all of the file"); function isWritable(mode) { return !!(mode & PR_WRONLY || mode & PR_RDWR); } function isReadable(mode) { return !!(mode & PR_RDONLY || mode & PR_RDWR); } function isString(value) { return typeof(value) === "string"; } function isFunction(value) { return typeof(value) === "function"; } function toArray(enumerator) { let value = []; while(enumerator.hasMoreElements()) value.push(enumerator.getNext()) return value } function getFileName(file) { return file.QueryInterface(Ci.nsIFile).leafName; } function remove(path, recursive) { let fd = new nsILocalFile(path) if (fd.exists()) { fd.remove(recursive || false); } else { throw FSError("remove", "ENOENT", 34, path); } } /** * Utility function to convert either an octal number or string * into an octal number * 0777 => 0o777 * "0644" => 0o644 */ function Mode(mode, fallback) { return isString(mode) ? parseInt(mode, 8) : mode || fallback; } function Flags(flag) { return !isString(flag) ? flag : FLAGS[flag] || Error("Unknown file open flag: " + flag); } function FSError(op, code, errno, path, file, line) { let error = Error(code + ", " + op + " " + path, file, line); error.code = code; error.path = path; error.errno = errno; return error; } const ReadStream = Class({ extends: InputStream, initialize: function initialize(path, options) { this.position = -1; this.length = -1; this.flags = "r"; this.mode = FILE_PERMISSION; this.bufferSize = 64 * 1024; options = options || {}; if ("flags" in options && options.flags) this.flags = options.flags; if ("bufferSize" in options && options.bufferSize) this.bufferSize = options.bufferSize; if ("length" in options && options.length) this.length = options.length; if ("position" in options && options.position !== undefined) this.position = options.position; let { flags, mode, position, length } = this; let fd = isString(path) ? openSync(path, flags, mode) : path; this.fd = fd; let input = nsIFileInputStream(fd); // Setting a stream position, unless it"s `-1` which means current position. if (position >= 0) input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file input stream into a fully asynchronous stream that can be written // without blocking the main thread. let transport = createInputTransport(input, position, length, false); // Open an input stream on a transport. We don"t pass flags to guarantee // non-blocking stream semantics. Also we use defaults for segment size & // count. InputStream.prototype.initialize.call(this, { asyncInputStream: transport.openInputStream(null, 0, 0) }); // Close file descriptor on end and destroy the stream. on(this, "end", _ => { this.destroy(); emit(this, "close"); }); this.read(); }, destroy: function() { closeSync(this.fd); InputStream.prototype.destroy.call(this); } }); exports.ReadStream = ReadStream; exports.createReadStream = function createReadStream(path, options) { return new ReadStream(path, options); }; const WriteStream = Class({ extends: OutputStream, initialize: function initialize(path, options) { this.drainable = true; this.flags = "w"; this.position = -1; this.mode = FILE_PERMISSION; options = options || {}; if ("flags" in options && options.flags) this.flags = options.flags; if ("mode" in options && options.mode) this.mode = options.mode; if ("position" in options && options.position !== undefined) this.position = options.position; let { position, flags, mode } = this; // If pass was passed we create a file descriptor out of it. Otherwise // we just use given file descriptor. let fd = isString(path) ? openSync(path, flags, mode) : path; this.fd = fd; let output = nsIFileOutputStream(fd); // Setting a stream position, unless it"s `-1` which means current position. if (position >= 0) output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file output stream into a fully asynchronous stream that can be written // without blocking the main thread. let transport = createOutputTransport(output, position, -1, false); // Open an output stream on a transport. We don"t pass flags to guarantee // non-blocking stream semantics. Also we use defaults for segment size & // count. OutputStream.prototype.initialize.call(this, { asyncOutputStream: transport.openOutputStream(OPEN_UNBUFFERED, 0, 0), output: output }); // For write streams "finish" basically means close. on(this, "finish", _ => { this.destroy(); emit(this, "close"); }); }, destroy: function() { OutputStream.prototype.destroy.call(this); closeSync(this.fd); } }); exports.WriteStream = WriteStream; exports.createWriteStream = function createWriteStream(path, options) { return new WriteStream(path, options); }; const Stats = Class({ initialize: function initialize(path) { let file = new nsILocalFile(path); if (!file.exists()) throw FSError("stat", "ENOENT", 34, path); nsIFile(this, file); }, isDirectory: function() { return nsIFile(this).isDirectory(); }, isFile: function() { return nsIFile(this).isFile(); }, isSymbolicLink: function() { return nsIFile(this).isSymlink(); }, get mode() { return nsIFile(this).permissions; }, get size() { return nsIFile(this).fileSize; }, get mtime() { return nsIFile(this).lastModifiedTime; }, isBlockDevice: function() { return nsIFile(this).isSpecial(); }, isCharacterDevice: function() { return nsIFile(this).isSpecial(); }, isFIFO: function() { return nsIFile(this).isSpecial(); }, isSocket: function() { return nsIFile(this).isSpecial(); }, // non standard get exists() { return nsIFile(this).exists(); }, get hidden() { return nsIFile(this).isHidden(); }, get writable() { return nsIFile(this).isWritable(); }, get readable() { return nsIFile(this).isReadable(); } }); exports.Stats = Stats; const LStats = Class({ extends: Stats, get size() { return this.isSymbolicLink() ? nsIFile(this).fileSizeOfLink : nsIFile(this).fileSize; }, get mtime() { return this.isSymbolicLink() ? nsIFile(this).lastModifiedTimeOfLink : nsIFile(this).lastModifiedTime; }, // non standard get permissions() { return this.isSymbolicLink() ? nsIFile(this).permissionsOfLink : nsIFile(this).permissions; } }); const FStat = Class({ extends: Stats, initialize: function initialize(fd) { nsIFile(this, nsIFile(fd)); } }); function noop() {} function Async(wrapped) { return function (path, callback) { let args = Array.slice(arguments); callback = args.pop(); // If node is not given a callback argument // it just does not calls it. if (typeof(callback) !== "function") { args.push(callback); callback = noop; } setTimeout(function() { try { var result = wrapped.apply(this, args); if (result === undefined) callback(null); else callback(null, result); } catch (error) { callback(error); } }, 0); } } /** * Synchronous rename(2) */ function renameSync(oldPath, newPath) { let source = new nsILocalFile(oldPath); let target = new nsILocalFile(newPath); if (!source.exists()) throw FSError("rename", "ENOENT", 34, oldPath); return source.moveTo(target.parent, target.leafName); }; exports.renameSync = renameSync; /** * Asynchronous rename(2). No arguments other than a possible exception are * given to the completion callback. */ var rename = Async(renameSync); exports.rename = rename; /** * Test whether or not the given path exists by checking with the file system. */ function existsSync(path) { return new nsILocalFile(path).exists(); } exports.existsSync = existsSync; var exists = Async(existsSync); exports.exists = exists; /** * Synchronous ftruncate(2). */ function truncateSync(path, length) { let fd = openSync(path, "w"); ftruncateSync(fd, length); closeSync(fd); } exports.truncateSync = truncateSync; /** * Asynchronous ftruncate(2). No arguments other than a possible exception are * given to the completion callback. */ function truncate(path, length, callback) { open(path, "w", function(error, fd) { if (error) return callback(error); ftruncate(fd, length, function(error) { if (error) { closeSync(fd); callback(error); } else { close(fd, callback); } }); }); } exports.truncate = truncate; function ftruncate(fd, length, callback) { write(fd, new Buffer(length), 0, length, 0, function(error) { callback(error); }); } exports.ftruncate = ftruncate; function ftruncateSync(fd, length = 0) { writeSync(fd, new Buffer(length), 0, length, 0); } exports.ftruncateSync = ftruncateSync; function chownSync(path, uid, gid) { throw Error("Not implemented yet!!"); } exports.chownSync = chownSync; var chown = Async(chownSync); exports.chown = chown; function lchownSync(path, uid, gid) { throw Error("Not implemented yet!!"); } exports.lchownSync = chownSync; var lchown = Async(lchown); exports.lchown = lchown; /** * Synchronous chmod(2). */ function chmodSync (path, mode) { let file; try { file = new nsILocalFile(path); } catch(e) { throw FSError("chmod", "ENOENT", 34, path); } file.permissions = Mode(mode); } exports.chmodSync = chmodSync; /** * Asynchronous chmod(2). No arguments other than a possible exception are * given to the completion callback. */ var chmod = Async(chmodSync); exports.chmod = chmod; /** * Synchronous chmod(2). */ function fchmodSync(fd, mode) { throw Error("Not implemented yet!!"); }; exports.fchmodSync = fchmodSync; /** * Asynchronous chmod(2). No arguments other than a possible exception are * given to the completion callback. */ var fchmod = Async(fchmodSync); exports.fchmod = fchmod; /** * Synchronous stat(2). Returns an instance of `fs.Stats` */ function statSync(path) { return new Stats(path); }; exports.statSync = statSync; /** * Asynchronous stat(2). The callback gets two arguments (err, stats) where * stats is a `fs.Stats` object. It looks like this: */ var stat = Async(statSync); exports.stat = stat; /** * Synchronous lstat(2). Returns an instance of `fs.Stats`. */ function lstatSync(path) { return new LStats(path); }; exports.lstatSync = lstatSync; /** * Asynchronous lstat(2). The callback gets two arguments (err, stats) where * stats is a fs.Stats object. lstat() is identical to stat(), except that if * path is a symbolic link, then the link itself is stat-ed, not the file that * it refers to. */ var lstat = Async(lstatSync); exports.lstat = lstat; /** * Synchronous fstat(2). Returns an instance of `fs.Stats`. */ function fstatSync(fd) { return new FStat(fd); }; exports.fstatSync = fstatSync; /** * Asynchronous fstat(2). The callback gets two arguments (err, stats) where * stats is a fs.Stats object. */ var fstat = Async(fstatSync); exports.fstat = fstat; /** * Synchronous link(2). */ function linkSync(source, target) { throw Error("Not implemented yet!!"); }; exports.linkSync = linkSync; /** * Asynchronous link(2). No arguments other than a possible exception are given * to the completion callback. */ var link = Async(linkSync); exports.link = link; /** * Synchronous symlink(2). */ function symlinkSync(source, target) { throw Error("Not implemented yet!!"); }; exports.symlinkSync = symlinkSync; /** * Asynchronous symlink(2). No arguments other than a possible exception are * given to the completion callback. */ var symlink = Async(symlinkSync); exports.symlink = symlink; /** * Synchronous readlink(2). Returns the resolved path. */ function readlinkSync(path) { return new nsILocalFile(path).target; }; exports.readlinkSync = readlinkSync; /** * Asynchronous readlink(2). The callback gets two arguments * `(error, resolvedPath)`. */ var readlink = Async(readlinkSync); exports.readlink = readlink; /** * Synchronous realpath(2). Returns the resolved path. */ function realpathSync(path) { return new nsILocalFile(path).path; }; exports.realpathSync = realpathSync; /** * Asynchronous realpath(2). The callback gets two arguments * `(err, resolvedPath)`. */ var realpath = Async(realpathSync); exports.realpath = realpath; /** * Synchronous unlink(2). */ var unlinkSync = remove; exports.unlinkSync = unlinkSync; /** * Asynchronous unlink(2). No arguments other than a possible exception are * given to the completion callback. */ var unlink = Async(remove); exports.unlink = unlink; /** * Synchronous rmdir(2). */ var rmdirSync = remove; exports.rmdirSync = rmdirSync; /** * Asynchronous rmdir(2). No arguments other than a possible exception are * given to the completion callback. */ var rmdir = Async(rmdirSync); exports.rmdir = rmdir; /** * Synchronous mkdir(2). */ function mkdirSync(path, mode) { try { return nsILocalFile(path).create(DIRECTORY_TYPE, Mode(mode)); } catch (error) { // Adjust exception thorw to match ones thrown by node. if (error.name === "NS_ERROR_FILE_ALREADY_EXISTS") { let { fileName, lineNumber } = error; error = FSError("mkdir", "EEXIST", 47, path, fileName, lineNumber); } throw error; } }; exports.mkdirSync = mkdirSync; /** * Asynchronous mkdir(2). No arguments other than a possible exception are * given to the completion callback. */ var mkdir = Async(mkdirSync); exports.mkdir = mkdir; /** * Synchronous readdir(3). Returns an array of filenames excluding `"."` and * `".."`. */ function readdirSync(path) { try { return toArray(new nsILocalFile(path).directoryEntries).map(getFileName); } catch (error) { // Adjust exception thorw to match ones thrown by node. if (error.name === "NS_ERROR_FILE_TARGET_DOES_NOT_EXIST" || error.name === "NS_ERROR_FILE_NOT_FOUND") { let { fileName, lineNumber } = error; error = FSError("readdir", "ENOENT", 34, path, fileName, lineNumber); } throw error; } }; exports.readdirSync = readdirSync; /** * Asynchronous readdir(3). Reads the contents of a directory. The callback * gets two arguments `(error, files)` where `files` is an array of the names * of the files in the directory excluding `"."` and `".."`. */ var readdir = Async(readdirSync); exports.readdir = readdir; /** * Synchronous close(2). */ function closeSync(fd) { let input = nsIFileInputStream(fd); let output = nsIFileOutputStream(fd); // Closing input stream and removing reference. if (input) input.close(); // Closing output stream and removing reference. if (output) output.close(); nsIFile(fd, null); nsIFileInputStream(fd, null); nsIFileOutputStream(fd, null); nsIBinaryInputStream(fd, null); nsIBinaryOutputStream(fd, null); }; exports.closeSync = closeSync; /** * Asynchronous close(2). No arguments other than a possible exception are * given to the completion callback. */ var close = Async(closeSync); exports.close = close; /** * Synchronous open(2). */ function openSync(aPath, aFlag, aMode) { let [ fd, flags, mode, file ] = [ { path: aPath }, Flags(aFlag), Mode(aMode), nsILocalFile(aPath) ]; nsIFile(fd, file); // If trying to open file for just read that does not exists // need to throw exception as node does. if (!file.exists() && !isWritable(flags)) throw FSError("open", "ENOENT", 34, aPath); // If we want to open file in read mode we initialize input stream. if (isReadable(flags)) { let input = FileInputStream(file, flags, mode, DEFER_OPEN); nsIFileInputStream(fd, input); } // If we want to open file in write mode we initialize output stream for it. if (isWritable(flags)) { let output = FileOutputStream(file, flags, mode, DEFER_OPEN); nsIFileOutputStream(fd, output); } return fd; } exports.openSync = openSync; /** * Asynchronous file open. See open(2). Flags can be * `"r", "r+", "w", "w+", "a"`, or `"a+"`. mode defaults to `0666`. * The callback gets two arguments `(error, fd). */ var open = Async(openSync); exports.open = open; /** * Synchronous version of buffer-based fs.write(). Returns the number of bytes * written. */ function writeSync(fd, buffer, offset, length, position) { if (length + offset > buffer.length) { throw Error("Length is extends beyond buffer"); } else if (length + offset !== buffer.length) { buffer = buffer.slice(offset, offset + length); } let output = BinaryOutputStream(nsIFileOutputStream(fd)); nsIBinaryOutputStream(fd, output); // We write content as a byte array as this will avoid any transcoding // if content was a buffer. output.writeByteArray(buffer.valueOf(), buffer.length); output.flush(); }; exports.writeSync = writeSync; /** * Write buffer to the file specified by fd. * * `offset` and `length` determine the part of the buffer to be written. * * `position` refers to the offset from the beginning of the file where this * data should be written. If `position` is `null`, the data will be written * at the current position. See pwrite(2). * * The callback will be given three arguments `(error, written, buffer)` where * written specifies how many bytes were written into buffer. * * Note that it is unsafe to use `fs.write` multiple times on the same file * without waiting for the callback. */ function write(fd, buffer, offset, length, position, callback) { if (!Buffer.isBuffer(buffer)) { // (fd, data, position, encoding, callback) let encoding = null; [ position, encoding, callback ] = Array.slice(arguments, 1); buffer = new Buffer(String(buffer), encoding); offset = 0; } else if (length + offset > buffer.length) { throw Error("Length is extends beyond buffer"); } else if (length + offset !== buffer.length) { buffer = buffer.slice(offset, offset + length); } let writeStream = new WriteStream(fd, { position: position, length: length }); writeStream.on("error", callback); writeStream.write(buffer, function onEnd() { writeStream.destroy(); if (callback) callback(null, buffer.length, buffer); }); }; exports.write = write; /** * Synchronous version of string-based fs.read. Returns the number of * bytes read. */ function readSync(fd, buffer, offset, length, position) { let input = nsIFileInputStream(fd); // Setting a stream position, unless it"s `-1` which means current position. if (position >= 0) input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file input stream into a fully asynchronous stream that can be written // without blocking the main thread. let binaryInputStream = BinaryInputStream(input); let count = length === ALL ? binaryInputStream.available() : length; if (offset === 0) binaryInputStream.readArrayBuffer(count, buffer.buffer); else { let chunk = new Buffer(count); binaryInputStream.readArrayBuffer(count, chunk.buffer); chunk.copy(buffer, offset); } return buffer.slice(offset, offset + count); }; exports.readSync = readSync; /** * Read data from the file specified by `fd`. * * `buffer` is the buffer that the data will be written to. * `offset` is offset within the buffer where writing will start. * * `length` is an integer specifying the number of bytes to read. * * `position` is an integer specifying where to begin reading from in the file. * If `position` is `null`, data will be read from the current file position. * * The callback is given the three arguments, `(error, bytesRead, buffer)`. */ function read(fd, buffer, offset, length, position, callback) { let bytesRead = 0; let readStream = new ReadStream(fd, { position: position, length: length }); readStream.on("data", function onData(data) { data.copy(buffer, offset + bytesRead); bytesRead += data.length; }); readStream.on("end", function onEnd() { callback(null, bytesRead, buffer); readStream.destroy(); }); }; exports.read = read; /** * Asynchronously reads the entire contents of a file. * The callback is passed two arguments `(error, data)`, where data is the * contents of the file. */ function readFile(path, encoding, callback) { if (isFunction(encoding)) { callback = encoding encoding = null } let buffer = null; try { let readStream = new ReadStream(path); readStream.on("data", function(data) { if (!buffer) buffer = data; else buffer = Buffer.concat([buffer, data], 2); }); readStream.on("error", function onError(error) { callback(error); }); readStream.on("end", function onEnd() { // Note: Need to destroy before invoking a callback // so that file descriptor is released. readStream.destroy(); callback(null, buffer); }); } catch (error) { setTimeout(callback, 0, error); } }; exports.readFile = readFile; /** * Synchronous version of `fs.readFile`. Returns the contents of the path. * If encoding is specified then this function returns a string. * Otherwise it returns a buffer. */ function readFileSync(path, encoding) { let fd = openSync(path, "r"); let size = fstatSync(fd).size; let buffer = new Buffer(size); try { readSync(fd, buffer, 0, ALL, 0); } finally { closeSync(fd); } return buffer; }; exports.readFileSync = readFileSync; /** * Asynchronously writes data to a file, replacing the file if it already * exists. data can be a string or a buffer. */ function writeFile(path, content, encoding, callback) { if (!isString(path)) throw new TypeError('path must be a string'); try { if (isFunction(encoding)) { callback = encoding encoding = null } if (isString(content)) content = new Buffer(content, encoding); let writeStream = new WriteStream(path); let error = null; writeStream.end(content, function() { writeStream.destroy(); callback(error); }); writeStream.on("error", function onError(reason) { error = reason; writeStream.destroy(); }); } catch (error) { callback(error); } }; exports.writeFile = writeFile; /** * The synchronous version of `fs.writeFile`. */ function writeFileSync(filename, data, encoding) { // TODO: Implement this in bug 1148209 https://bugzilla.mozilla.org/show_bug.cgi?id=1148209 throw Error("Not implemented"); }; exports.writeFileSync = writeFileSync; function utimesSync(path, atime, mtime) { throw Error("Not implemented"); } exports.utimesSync = utimesSync; var utimes = Async(utimesSync); exports.utimes = utimes; function futimesSync(fd, atime, mtime, callback) { throw Error("Not implemented"); } exports.futimesSync = futimesSync; var futimes = Async(futimesSync); exports.futimes = futimes; function fsyncSync(fd, atime, mtime, callback) { throw Error("Not implemented"); } exports.fsyncSync = fsyncSync; var fsync = Async(fsyncSync); exports.fsync = fsync; /** * Watch for changes on filename. The callback listener will be called each * time the file is accessed. * * The second argument is optional. The options if provided should be an object * containing two members a boolean, persistent, and interval, a polling value * in milliseconds. The default is { persistent: true, interval: 0 }. */ function watchFile(path, options, listener) { throw Error("Not implemented"); }; exports.watchFile = watchFile; function unwatchFile(path, listener) { throw Error("Not implemented"); } exports.unwatchFile = unwatchFile; function watch(path, options, listener) { throw Error("Not implemented"); } exports.watch = watch;