diff --git a/src/filesystem/implementation.js b/src/filesystem/implementation.js index c1f5b3e..2ef72ca 100644 --- a/src/filesystem/implementation.js +++ b/src/filesystem/implementation.js @@ -13,6 +13,10 @@ var NODE_TYPE_DIRECTORY = Constants.NODE_TYPE_DIRECTORY; var NODE_TYPE_SYMBOLIC_LINK = Constants.NODE_TYPE_SYMBOLIC_LINK; var NODE_TYPE_META = Constants.NODE_TYPE_META; +var DEFAULT_FILE_PERMISSIONS = Constants.DEFAULT_FILE_PERMISSIONS; +var DEFAULT_DIR_PERMISSIONS = Constants.DEFAULT_DIR_PERMISSIONS; +var FULL_READ_WRITE_EXEC_PERMISSIONS = Constants.FULL_READ_WRITE_EXEC_PERMISSIONS; + var ROOT_DIRECTORY_NAME = Constants.ROOT_DIRECTORY_NAME; var SUPER_NODE_ID = Constants.SUPER_NODE_ID; var SYMLOOP_MAX = Constants.SYMLOOP_MAX; @@ -1587,8 +1591,22 @@ function pathCheck(path, callback) { function open(fs, context, path, flags, mode, callback) { - // NOTE: we support the same signature as node with a `mode` arg, - // but ignore it. + /** + * NOTE: we support the same signature as node with a `mode` arg, + * but ignore it. We need to add it. Here is what node.js does: + * function open(path, flags, mode, callback) { + * path = getPathFromURL(path); + * validatePath(path); + * const flagsNumber = stringToFlags(flags); + * if (arguments.length < 4) { + * callback = makeCallback(mode); + * mode = 0o666; + * } else { + * mode = validateAndMaskMode(mode, 'mode', 0o666); + * callback = makeCallback(callback); + * } + */ + callback = arguments[arguments.length - 1]; if(!pathCheck(path, callback)) return; @@ -1631,8 +1649,14 @@ function mknod(fs, context, path, type, callback) { } function mkdir(fs, context, path, mode, callback) { - // NOTE: we support passing a mode arg, but we ignore it internally for now. - callback = arguments[arguments.length - 1]; + if (arguments.length < 5) { + callback = mode; + mode = FULL_READ_WRITE_EXEC_PERMISSIONS; + } else { + mode = validateAndMaskMode(mode, FULL_READ_WRITE_EXEC_PERMISSIONS, callback); + if(!mode) return; + } + if(!pathCheck(path, callback)) return; make_directory(context, path, callback); } @@ -1863,6 +1887,121 @@ function exists(fs, context, path, callback) { stat(fs, context, path, cb); } +// Based on https://github.com/nodejs/node/blob/c700cc42da9cf73af9fec2098520a6c0a631d901/lib/internal/validators.js#L21 +var octalReg = /^[0-7]+$/; +var modeDesc = 'must be a 32-bit unsigned integer or an octal string'; +function isUint32(value) { + return value === (value >>> 0); +} +// Validator for mode_t (the S_* constants). Valid numbers or octal strings +// will be masked with 0o777 to be consistent with the behavior in POSIX APIs. +function validateAndMaskMode(value, def, callback) { + if(typeof def === 'function') { + callback = def; + def = undefined; + } + + if (isUint32(value)) { + return value & FULL_READ_WRITE_EXEC_PERMISSIONS; + } + + if (typeof value === 'number') { + if (!Number.isInteger(value)) { + callback(new Errors.EINVAL('mode not a valid an integer value', value)); + return false; + } else { + // 2 ** 32 === 4294967296 + callback(new Errors.EINVAL('mode not a valid an integer value', value)); + return false; + } + } + + if (typeof value === 'string') { + if (!octalReg.test(value)) { + callback(new Errors.EINVAL('mode not a valid octal string', value)); + return false; + } + var parsed = parseInt(value, 8); + return parsed & FULL_READ_WRITE_EXEC_PERMISSIONS; + } + + // TODO(BridgeAR): Only return `def` in case `value == null` + if (def !== undefined) { + return def; + } + + callback(new Errors.EINVAL('mode not valid', value)); + return false; +} + +function chmod_file(context, path, mode, callback) { + path = normalize(path); + + function update_mode(error, node) { + if (error) { + callback(error); + } else { + node.mode = mode; + update_node_times(context, path, node, { mtime: Date.now() }, callback); + } + } + + if (typeof mode != 'number') { + callback(new Errors.EINVAL('mode must be number', path)); + } + else { + find_node(context, path, update_mode); + } +} + +function fchmod_file(context, ofd, mode, callback) { + function update_mode(error, node) { + if (error) { + callback(error); + } else { + node.mode = mode; + update_node_times(context, ofd.path, node, { mtime: Date.now() }, callback); + } + } + + if (typeof mode != 'number') { + callback(new Errors.EINVAL('mode must be a number')); + } + else { + ofd.getNode(context, update_mode); + } +} + +function chown_file(context, path, uid, gid, callback) { + path = normalize(path); + + function update_owner(error, node) { + if (error) { + callback(error); + } else { + node.uid = uid; + node.gid = gid; + update_node_times(context, path, node, { mtime: Date.now() }, callback); + } + } + + find_node(context, path, update_owner); +} + +function fchown_file(context, ofd, uid, gid, callback) { + function update_owner(error, node) { + if (error) { + callback(error); + } else { + node.uid = uid; + node.gid = gid; + update_node_times(context, ofd.path, node, { mtime: Date.now() }, callback); + } + } + + ofd.getNode(context, update_owner); +} + function getxattr(fs, context, path, name, callback) { if (!pathCheck(path, callback)) return; getxattr_file(context, path, name, callback); @@ -1877,7 +2016,7 @@ function fgetxattr(fs, context, fd, name, callback) { fgetxattr_file(context, ofd, name, callback); } } - + function setxattr(fs, context, path, name, value, flag, callback) { if(typeof flag === 'function') { callback = flag; @@ -1994,6 +2133,58 @@ function futimes(fs, context, fd, atime, mtime, callback) { } } +function chmod(fs, context, path, mode, callback) { + if(!pathCheck(path, callback)) return; + mode = validateAndMaskMode(mode, 'mode'); + if(!mode) return; + + chmod_file(context, path, mode, callback); +} + +function fchmod(fs, context, fd, mode, callback) { + mode = validateAndMaskMode(mode, 'mode'); + if(!mode) return; + + var ofd = fs.openFiles[fd]; + if(!ofd) { + callback(new Errors.EBADF()); + } else if(!_(ofd.flags).contains(O_WRITE)) { + callback(new Errors.EBADF('descriptor does not permit writing')); + } else { + fchmod_file(context, ofd, mode, callback); + } +} + +function chown(fs, context, path, uid, gid, callback) { + if(!pathCheck(path, callback)) return; + if(!isUint32(uid)) { + return callback(new Errors.EINVAL('uid must be a valid integer', uid)); + } + if(!isUint32(gid)) { + return callback(new Errors.EINVAL('gid must be a valid integer', gid)); + } + + chown_file(context, path, uid, gid, callback); +} + +function fchown(fs, context, fd, uid, gid, callback) { + if(!isUint32(uid)) { + return callback(new Errors.EINVAL('uid must be a valid integer', uid)); + } + if(!isUint32(gid)) { + return callback(new Errors.EINVAL('gid must be a valid integer', gid)); + } + + var ofd = fs.openFiles[fd]; + if(!ofd) { + callback(new Errors.EBADF()); + } else if(!_(ofd.flags).contains(O_WRITE)) { + callback(new Errors.EBADF('descriptor does not permit writing')); + } else { + fchown_file(context, ofd, uid, gid, callback); + } +} + function rename(fs, context, oldpath, newpath, callback) { if(!pathCheck(oldpath, callback)) return; if(!pathCheck(newpath, callback)) return; @@ -2164,6 +2355,10 @@ function ftruncate(fs, context, fd, length, callback) { module.exports = { ensureRootDirectory: ensure_root_directory, open: open, + chmod: chmod, + fchmod: fchmod, + chown: chown, + fchown: fchown, close: close, mknod: mknod, mkdir: mkdir, diff --git a/src/filesystem/interface.js b/src/filesystem/interface.js index 7d8fa2e..719fd35 100644 --- a/src/filesystem/interface.js +++ b/src/filesystem/interface.js @@ -94,6 +94,9 @@ function FileSystem(options, callback) { fs.stdout = STDOUT; fs.stderr = STDERR; + // Expose Node's fs.constants to users + fs.constants = Constants.fsConstants; + // Expose Shell constructor this.Shell = Shell.bind(undefined, this); @@ -268,6 +271,10 @@ FileSystem.providers = providers; */ [ 'open', + 'chmod', + 'fchmod', + 'chown', + 'fchown', 'close', 'mknod', 'mkdir', diff --git a/tests/index.js b/tests/index.js index c9743ad..2a3f313 100644 --- a/tests/index.js +++ b/tests/index.js @@ -39,6 +39,8 @@ require("./spec/time-flags.spec"); require("./spec/fs.watch.spec"); require("./spec/errors.spec"); require("./spec/fs.shell.spec"); +require("./spec/fs.chmod.spec"); +require("./spec/fs.chown.spec") // Filer.FileSystem.providers.* require("./spec/providers/providers.spec"); diff --git a/tests/spec/fs.chmod.spec.js b/tests/spec/fs.chmod.spec.js new file mode 100644 index 0000000..54d5484 --- /dev/null +++ b/tests/spec/fs.chmod.spec.js @@ -0,0 +1,74 @@ +var Filer = require('../..'); +var util = require('../lib/test-utils.js'); +var expect = require('chai').expect; + +describe('fs.chmod, fs.fchmod', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be functions', function() { + var fs = util.fs(); + expect(typeof fs.chmod).to.equal('function'); + expect(typeof fs.fchmod).to.equal('function'); + }); + + it('should automatically set mode=755 for a directory', function(done) { + var fs = util.fs(); + + fs.mkdir('/dir', function(err) { + if(err) throw err; + + fs.stat('/dir', function(err, stats) { + if(err) throw err; + expect(stats.mode & 0o755).to.equal(0o755); + done(); + }); + }); + }); + + it('should automatically set mode=644 for a file', function(done) { + var fs = util.fs(); + + fs.open('/file', 'w', function(err, fd) { + if(err) throw err; + + fs.fstat(fd, function(err, stats) { + if(err) throw err; + expect(stats.mode & 0o644).to.equal(0o644); + fs.close(fd, done); + }); + }); + }); + + it('should allow for updating mode of a given file', function(done) { + var fs = util.fs(); + + fs.open('/file', 'w', function(err, fd) { + if(err) throw err; + + fs.fchmod(fd, 0o777, function(err) { + if(err) throw err; + + fs.fstat(fd, function(err, stats) { + if(err) throw err; + expect(stats.mode & 0o777).to.equal(0o777); + + fs.close(fd, function(err) { + if(err) throw err; + + fs.chmod('/file', 0o444, function(err) { + if(err) throw err; + + fs.stat('/file', function(err, stats) { + if(err) throw err; + + expect(stats.mode & 0o444).to.equal(0o444); + done(); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/tests/spec/fs.chown.spec.js b/tests/spec/fs.chown.spec.js new file mode 100644 index 0000000..aa72312 --- /dev/null +++ b/tests/spec/fs.chown.spec.js @@ -0,0 +1,65 @@ +var Filer = require('../..'); +var util = require('../lib/test-utils.js'); +var expect = require('chai').expect; + +describe('fs.chown, fs.fchown', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be functions', function() { + var fs = util.fs(); + expect(typeof fs.chown).to.equal('function'); + expect(typeof fs.fchown).to.equal('function'); + }); + + it('should automatically set a file\s uid and gid to 0 (i.e., root)', function(done) { + var fs = util.fs(); + + fs.open('/file', 'w', function(err, fd) { + if(err) throw err; + + fs.fstat(fd, function(err, stats) { + if(err) throw err; + + expect(stats.uid).to.equal(0); + expect(stats.gid).to.equal(0); + fs.close(fd, done); + }); + }); + }); + + it('should allow updating gid and uid for a file', function(done) { + var fs = util.fs(); + + fs.open('/file', 'w', function(err, fd) { + if(err) throw err; + + fs.fchown(fd, 1001, 1001, function(err) { + if(err) throw err; + + fs.fstat(fd, function(err, stats) { + if(err) throw err; + + expect(stats.uid).to.equal(1001); + expect(stats.gid).to.equal(1001); + + fs.close(fd, function(err) { + if(err) throw err; + + fs.chown('/file', 500, 500, function(err) { + if(err) throw err; + + fs.stat('/file', function(err, stats) { + if(err) throw err; + + expect(stats.uid).to.equal(500); + expect(stats.gid).to.equal(500); + done(); + }); + }); + }); + }); + }); + }); + }); +});