Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: mask mode_t type of arguments with 0o777 #20636

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,10 @@ For example, the octal value `0o765` means:
* The group may read and write the file.
* Others may read and execute the file.

Caveats: on Windows only the write permission can be changed, and the
distinction among the permissions of group, owner or others is not
implemented.

## fs.chmodSync(path, mode)
<!-- YAML
added: v0.6.7
Expand Down Expand Up @@ -1992,7 +1996,7 @@ changes:
-->

* `path` {string|Buffer|URL}
* `mode` {integer} **Default:** `0o777`
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.
* `callback` {Function}
* `err` {Error}

Expand All @@ -2012,7 +2016,7 @@ changes:
-->

* `path` {string|Buffer|URL}
* `mode` {integer} **Default:** `0o777`
* `mode` {integer} Not supported on Windows. **Default:** `0o777`.

Synchronously creates a directory. Returns `undefined`.
This is the synchronous version of [`fs.mkdir()`][].
Expand Down Expand Up @@ -2132,7 +2136,8 @@ changes:
Asynchronous file open. See open(2).

`mode` sets the file mode (permission and sticky bits), but only if the file was
created.
created. Note that on Windows only the write permission can be manipulated,
see [`fs.chmod()`][].

The callback gets two arguments `(err, fd)`.

Expand Down
63 changes: 31 additions & 32 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const internalUtil = require('internal/util');
const {
copyObject,
getOptions,
modeNum,
nullCheck,
preprocessSymlinkDestination,
Stats,
Expand All @@ -71,6 +70,7 @@ const {
} = require('internal/constants');
const {
isUint32,
validateAndMaskMode,
validateInt32,
validateUint32
} = require('internal/validators');
Expand Down Expand Up @@ -531,32 +531,36 @@ fs.closeSync = function(fd) {
handleErrorFromBinding(ctx);
};

fs.open = function(path, flags, mode, callback_) {
var callback = makeCallback(arguments[arguments.length - 1]);
mode = modeNum(mode, 0o666);

fs.open = function(path, flags, mode, callback) {
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
const flagsNumber = stringToFlags(flags);
if (arguments.length < 4) {
callback = makeCallback(mode);
mode = 0o666;
} else {
mode = validateAndMaskMode(mode, 'mode', 0o666);
callback = makeCallback(callback);
}

const req = new FSReqWrap();
req.oncomplete = callback;

binding.open(pathModule.toNamespacedPath(path),
stringToFlags(flags),
flagsNumber,
mode,
req);
};

fs.openSync = function(path, flags, mode) {
mode = modeNum(mode, 0o666);
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
const flagsNumber = stringToFlags(flags);
mode = validateAndMaskMode(mode, 'mode', 0o666);

const ctx = { path };
const result = binding.open(pathModule.toNamespacedPath(path),
stringToFlags(flags), mode,
flagsNumber, mode,
undefined, ctx);
handleErrorFromBinding(ctx);
return result;
Expand Down Expand Up @@ -834,12 +838,16 @@ fs.fsyncSync = function(fd) {
};

fs.mkdir = function(path, mode, callback) {
if (typeof mode === 'function') callback = mode;
callback = makeCallback(callback);
path = getPathFromURL(path);
validatePath(path);
mode = modeNum(mode, 0o777);
validateUint32(mode, 'mode');

if (arguments.length < 3) {
callback = makeCallback(mode);
mode = 0o777;
} else {
callback = makeCallback(callback);
mode = validateAndMaskMode(mode, 'mode', 0o777);
}

const req = new FSReqWrap();
req.oncomplete = callback;
Expand All @@ -849,8 +857,7 @@ fs.mkdir = function(path, mode, callback) {
fs.mkdirSync = function(path, mode) {
path = getPathFromURL(path);
validatePath(path);
mode = modeNum(mode, 0o777);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode', 0o777);
const ctx = { path };
binding.mkdir(pathModule.toNamespacedPath(path), mode, undefined, ctx);
handleErrorFromBinding(ctx);
Expand Down Expand Up @@ -1032,25 +1039,18 @@ fs.unlinkSync = function(path) {
};

fs.fchmod = function(fd, mode, callback) {
mode = modeNum(mode);
validateUint32(fd, 'fd');
validateUint32(mode, 'mode');
// Values for mode < 0 are already checked via the validateUint32 function
if (mode > 0o777)
throw new ERR_OUT_OF_RANGE('mode', undefined, mode);
mode = validateAndMaskMode(mode, 'mode');
callback = makeCallback(callback);

const req = new FSReqWrap();
req.oncomplete = makeCallback(callback);
req.oncomplete = callback;
binding.fchmod(fd, mode, req);
};

fs.fchmodSync = function(fd, mode) {
mode = modeNum(mode);
validateUint32(fd, 'fd');
validateUint32(mode, 'mode');
// Values for mode < 0 are already checked via the validateUint32 function
if (mode > 0o777)
throw new ERR_OUT_OF_RANGE('mode', undefined, mode);
mode = validateAndMaskMode(mode, 'mode');
const ctx = {};
binding.fchmod(fd, mode, undefined, ctx);
handleErrorFromBinding(ctx);
Expand Down Expand Up @@ -1091,11 +1091,10 @@ if (constants.O_SYMLINK !== undefined) {


fs.chmod = function(path, mode, callback) {
callback = makeCallback(callback);
path = getPathFromURL(path);
validatePath(path);
mode = modeNum(mode);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode');
callback = makeCallback(callback);

const req = new FSReqWrap();
req.oncomplete = callback;
Expand All @@ -1105,8 +1104,8 @@ fs.chmod = function(path, mode, callback) {
fs.chmodSync = function(path, mode) {
path = getPathFromURL(path);
validatePath(path);
mode = modeNum(mode);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode');

const ctx = { path };
binding.chmod(pathModule.toNamespacedPath(path), mode, undefined, ctx);
handleErrorFromBinding(ctx);
Expand Down
19 changes: 6 additions & 13 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ const { Buffer, kMaxLength } = require('buffer');
const {
ERR_FS_FILE_TOO_LARGE,
ERR_INVALID_ARG_TYPE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_OUT_OF_RANGE
ERR_METHOD_NOT_IMPLEMENTED
} = require('internal/errors').codes;
const { getPathFromURL } = require('internal/url');
const { isUint8Array } = require('internal/util/types');
const {
copyObject,
getOptions,
getStatsFromBinding,
modeNum,
nullCheck,
preprocessSymlinkDestination,
stringToFlags,
Expand All @@ -34,6 +32,7 @@ const {
} = require('internal/fs/utils');
const {
isUint32,
validateAndMaskMode,
validateInt32,
validateUint32
} = require('internal/validators');
Expand Down Expand Up @@ -191,10 +190,9 @@ async function copyFile(src, dest, flags) {
// Note that unlike fs.open() which uses numeric file descriptors,
// fsPromises.open() uses the fs.FileHandle class.
async function open(path, flags, mode) {
mode = modeNum(mode, 0o666);
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode', 0o666);
return new FileHandle(
await binding.openFileHandle(pathModule.toNamespacedPath(path),
stringToFlags(flags),
Expand Down Expand Up @@ -287,10 +285,9 @@ async function fsync(handle) {
}

async function mkdir(path, mode) {
mode = modeNum(mode, 0o777);
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode', 0o777);
return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises);
}

Expand Down Expand Up @@ -361,19 +358,15 @@ async function unlink(path) {
}

async function fchmod(handle, mode) {
mode = modeNum(mode);
validateFileHandle(handle);
validateUint32(mode, 'mode');
if (mode > 0o777)
throw new ERR_OUT_OF_RANGE('mode', undefined, mode);
mode = validateAndMaskMode(mode, 'mode');
return binding.fchmod(handle.fd, mode, kUsePromises);
}

async function chmod(path, mode) {
path = getPathFromURL(path);
validatePath(path);
mode = modeNum(mode);
validateUint32(mode, 'mode');
mode = validateAndMaskMode(mode, 'mode');
return binding.chmod(pathModule.toNamespacedPath(path), mode, kUsePromises);
}

Expand Down
16 changes: 0 additions & 16 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,6 @@ function getOptions(options, defaultOptions) {
return options;
}

function modeNum(m, def) {
if (typeof m === 'number')
return m;
if (typeof m === 'string') {
const parsed = parseInt(m, 8);
if (Number.isNaN(parsed))
return m;
return parsed;
}
// TODO(BridgeAR): Only return `def` in case `m == null`
if (def !== undefined)
return def;
return m;
}

// Check if the path contains null types if it is a string nor Uint8Array,
// otherwise return silently.
function nullCheck(path, propName, throwError = true) {
Expand Down Expand Up @@ -391,7 +376,6 @@ module.exports = {
assertEncoding,
copyObject,
getOptions,
modeNum,
nullCheck,
preprocessSymlinkDestination,
realpathCacheKey: Symbol('realpathCacheKey'),
Expand Down
26 changes: 5 additions & 21 deletions lib/internal/process/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_UNKNOWN_CREDENTIAL
} = require('internal/errors').codes;
const {
validateAndMaskMode,
validateUint32
} = require('internal/validators');

Expand All @@ -30,29 +30,13 @@ function setupProcessMethods() {
return _chdir(directory);
}

const octalReg = /^[0-7]+$/;
function umask(mask) {
if (typeof mask === 'undefined') {
if (mask === undefined) {
// Get the mask
return _umask(mask);
}

if (typeof mask === 'number') {
validateUint32(mask, 'mask');
return _umask(mask);
}

if (typeof mask === 'string') {
if (!octalReg.test(mask)) {
throw new ERR_INVALID_ARG_VALUE('mask', mask,
'must be an octal string');
}
const octal = Number.parseInt(mask, 8);
validateUint32(octal, 'mask');
return _umask(octal);
}

throw new ERR_INVALID_ARG_TYPE('mask', ['number', 'string', 'undefined'],
mask);
mask = validateAndMaskMode(mask, 'mask');
return _umask(mask);
}
}

Expand Down
36 changes: 36 additions & 0 deletions lib/internal/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;

Expand All @@ -13,6 +14,40 @@ function isUint32(value) {
return value === (value >>> 0);
}

const octalReg = /^[0-7]+$/;
const modeDesc = 'must be a 32-bit unsigned integer or an octal string';
// 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, name, def) {
if (isUint32(value)) {
return value & 0o777;
}

if (typeof value === 'number') {
if (!Number.isInteger(value)) {
throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we really be using an out of range error type when it's a value type issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex JavaScript only has one number type (at least for now) so it could be seen both ways. I went with ERR_OUT_OF_RANGE so this can still be semver-patch.

} else {
// 2 ** 32 === 4294967296
throw new ERR_OUT_OF_RANGE(name, '>= 0 && < 4294967296', value);
}
}

if (typeof value === 'string') {
if (!octalReg.test(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? If the number is not a parseable octal value, NaN will be returned, which we check for anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseInt will not return NaN for strings like '666abc'.
But I agree there seems to be redundancy. Could there be cases where the string passes the regex and the value cannot be parsed? I don't think so.

Copy link
Member Author

@joyeecheung joyeecheung May 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex @targos Yes there are existing test cases ('123x') in our code base, it is currently only enforced in process.umask though. It does not seem quite right to remove the test IMO. Passing a number followed by invalid characters looks like a mistake that the user may want to know.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, https://github.com/tc39/proposal-number-fromstring (currently in Stage 1) is designed for this situation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joyeecheung what I meant is that the Number.isNaN(parsed) check can be removed. Unless I'm missing an edge case, it can never be true after the regex check.

throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc);
}
const parsed = parseInt(value, 8);
return parsed & 0o777;
}

// TODO(BridgeAR): Only return `def` in case `value == null`
if (def !== undefined) {
return def;
}

throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc);
}

function validateInt32(value, name) {
if (!isInt32(value)) {
let err;
Expand Down Expand Up @@ -53,6 +88,7 @@ function validateUint32(value, name, positive) {
module.exports = {
isInt32,
isUint32,
validateAndMaskMode,
validateInt32,
validateUint32
};
Loading