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

fs: create consistent behavior for fs module across platforms #17927

Closed
wants to merge 1 commit into from

Conversation

timotew
Copy link
Contributor

@timotew timotew commented Dec 31, 2017

In Windows, if a file name ends with forward slash /, fs interprets it as regular file (like there is no slash at all) and does not throw any errors. In Linux it throws error but not in Windows.

Fixes: #17801

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

fs

@nodejs-github-bot nodejs-github-bot added errors Issues and PRs related to JavaScript errors originated in Node.js core. fs Issues and PRs related to the fs subsystem / file system. labels Dec 31, 2017
lib/fs.js Outdated
@@ -385,6 +386,11 @@ fs.readFile = function(path, options, callback) {
if (!nullCheck(path, callback))
return;

//doing justice between linux and windows for `/` char at the end of file path
Copy link
Member

Choose a reason for hiding this comment

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

space after comment please

Copy link
Contributor Author

@timotew timotew Jan 1, 2018

Choose a reason for hiding this comment

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

@devsnek Space added thanks.

Copy link
Member

@Trott Trott Jan 1, 2018

Choose a reason for hiding this comment

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

I think what @devsnek meant was a space after the comment indicator (//) so:

// doing justice...

There are some lint errors you're going to want to correct (mostly around line lengths). These should have been flagged if you ran make test or vcbuild test. To just run the JS linting (since running the full tests can be time-consuming), you can use make lint-js or vcbuild lint-js.

Lastly, welcome @timotew and thanks for the pull request!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@devsnek sorry I misunderstood, @Trott Thanks a lot for having me and time for this.

lib/fs.js Outdated
@@ -343,6 +343,7 @@ fs.exists = function(path, callback) {
if (typeof path !== 'string' && !(path instanceof Buffer)) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'path',
['string', 'Buffer', 'URL']);

Copy link
Contributor

Choose a reason for hiding this comment

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

This empty line is redundant :-)

lib/fs.js Outdated
// doing justice between linux and windows
// for `/` char at the end of file path
if (isWindows && (path.slice(-1) === '/')) {
throw new errors.Error('ERR_INVALID_FILE_URL_PATH', path);
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Using errors.TypeError might be more appropriate.
  2. The second parameter (here is path) of ERR_INVALID_FILE_URL_PATH is wrong. It should be a string contains an error description about the invalid path.
    Here is some example:

    node/lib/internal/url.js

    Lines 1362 to 1363 in abe14ab

    return new errors.TypeError('ERR_INVALID_FILE_URL_PATH',
    'must not include encoded / characters');

    node/lib/internal/url.js

    Lines 1346 to 1347 in abe14ab

    return new errors.TypeError('ERR_INVALID_FILE_URL_PATH',
    'must be absolute');

Copy link
Member

Choose a reason for hiding this comment

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

I don't think errors.TypeError is appropriate here. A TypeError would be if we were expecting a string but received something else. In this case, we are expecting a string and receiving a string, but it has a value that is not valid. (By all means, please correct me if I'm wrong or misunderstanding something!)

The second point (about the second parameter) is 👍.

Copy link
Contributor

Choose a reason for hiding this comment

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

Now the similar ERR_INVALID_FILE_URL_PATH errors in internal/url.js are using TypeError (some examples showed above).

I do agree with your point. For semantics, using Error is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the correction it's effected. I am still not clear about the error type @Trott .
From what @starkwang showed above, Please I might also be wrong but I think TypeError is appropriate for the case.
errors in internal/url.js are expecting characters that should not include some special characters, The type there might be referring to character type not data type.

lib/fs.js Outdated
// for `/` char at the end of file path
if (isWindows && (path.slice(-1) === '/')) {
throw new errors.Error('ERR_INVALID_FILE_URL_PATH',
'file path must not end with / character');
Copy link
Contributor

Choose a reason for hiding this comment

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

This string should be reduced into 'must not end with / character'

The first two words 'file path' is redundant because these words were already contained in template:

E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s');

fs.readFile(new URL(`file:///c:/tmp/${i}/`), common.expectsError({
code: 'ERR_INVALID_FILE_URL_PATH',
type: TypeError,
message: 'File URL path can not end with / characters'
Copy link
Contributor

Choose a reason for hiding this comment

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

Umm... If I'm not mistaken, the message option will not match the actual error message.

It should be 'File URL path must not end with / character'.

By the way, you can use ./configure && make -j4 test on Unix/MacOS or vcbuild test on Windows to run the tests on your computer (For more guides, you can refer to here). PR should always pass all the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@starkwang Thanks a lot.

doc/api/fs.md Outdated
@@ -202,6 +202,10 @@ fs.readFileSync(new URL('file:///C:/tmp/hello'));
fs.readFileSync(new URL('file:///notdriveletter/p/a/t/h/file'));
fs.readFileSync(new URL('file:///c/p/a/t/h/file'));
// TypeError [ERR_INVALID_FILE_URL_PATH]: File URL path must be absolute

// - WHATWG file URLs must not end with forward slash `/`
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove the leading -

lib/fs.js Outdated
@@ -385,6 +385,14 @@ fs.readFile = function(path, options, callback) {
if (!nullCheck(path, callback))
return;

// doing justice between linux and windows
// for `/` char at the end of file path
if (isWindows && (typeof path === 'string')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the parens around the typeof check are unnecessary.

lib/fs.js Outdated
// doing justice between linux and windows
// for `/` char at the end of file path
if (isWindows && (typeof path === 'string')) {
if (path.slice(-1) === '/') {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no need for a nested if.

lib/fs.js Outdated
// doing justice between linux and windows
// for `/` char at the end of file path
if (isWindows && (typeof path === 'string')) {
if (path.slice(-1) === '/') {
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth testing if slice() is faster than charAt().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cjihrig charAt() is faster from my test, Thanks.

@starkwang
Copy link
Contributor

@nodejs/fs

@Trott
Copy link
Member

Trott commented Jan 3, 2018

Might be worth a look from @nodejs/url too, especially @TimothyGu?

lib/fs.js Outdated
// doing justice between linux and windows
// for `/` char at the end of file path
if (isWindows && typeof path === 'string' &&
path.charAt(path.length - 1) === '/') {
Copy link
Contributor

Choose a reason for hiding this comment

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

path.charAt() on this line should be aligned with isWindows on the previous line.

lib/fs.js Outdated
@@ -385,6 +385,13 @@ fs.readFile = function(path, options, callback) {
if (!nullCheck(path, callback))
return;

// doing justice between linux and windows
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure that "doing justice" makes a lot of sense here.

@joyeecheung
Copy link
Member

If #17801 is about inconsistent behavior on different platforms, this still does not resolves that since different errors are thrown in different platforms. Also this just fixes readFile and writeFile ad-hoc, a better approach would be allowing getPathFromURL to take an option to check the trailing slash and specify that in the fs APIs that are supposed to operate on files.

Copy link
Member

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

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

Making it explicit.

@timotew
Copy link
Contributor Author

timotew commented Jan 4, 2018

@joyeecheung Hi there! hope you have time to review these change.

@@ -72,3 +72,10 @@ if (common.isWindows) {
message: `File URL host must be "localhost" or empty on ${os.platform()}`
}));
}

// path ending with forward slash are not permitted
fs.readFile(new URL('file:///c:/tmp/test/'), common.expectsError({
Copy link
Member

Choose a reason for hiding this comment

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

Can you add tests for other APIs with the validation changed as well? Also it would be more robust if we test paths like file:///tmp/test/ on non-windows systems because file:///c:/tmp/test/ is obviously invalid there for a different reason(drives).

Copy link
Member

Choose a reason for hiding this comment

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

You can test different type of URLs on different systems by reusing the tested URL object like this:

const urlWithSlash = common.isWindows ?
  new URL('file:///c:/tmp/test/') : new URL('file:///tmp/test/');

fs.readFile(urlWithSlash, common.expectsError(...));

if (path == null || !path[searchParams] ||
!path[searchParams][searchParams]) {
return path;
}
if (path.protocol !== 'file:')
return new errors.TypeError('ERR_INVALID_URL_SCHEME', 'file');
if (strictPath && typeof path.href === 'string' &&
Copy link
Member

Choose a reason for hiding this comment

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

The typeof path.href === 'string' should be unnecessary because a valid WHATWG URL should always have a string as href

@@ -1367,13 +1367,18 @@ function getPathFromURLPosix(url) {
return decodeURIComponent(pathname);
}

function getPathFromURL(path) {
function getPathFromURL(path, strictPath) {
Copy link
Member

Choose a reason for hiding this comment

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

I think it's more accurate to name this assertFilename or something similar.

if (strictPath && typeof path.href === 'string' &&
path.href.charAt(path.href.length - 1) === '/') {
return new errors.Error('ERR_INVALID_FILE_URL_PATH',
'must not end with / character');
Copy link
Member

Choose a reason for hiding this comment

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

The message Illegal operation on a directory is actually more accurate here.

@joyeecheung
Copy link
Member

Nice work, although I think there are still some APIs that need to be updated:

  • fs.appendFile / fs.appendFileSync
  • fs.createReadStream
  • fs.createWriteStream
  • fs.truncate/fs.truncateSync
  • fs.writeFile/fs.writeFileSync

Can you update those APIs as well?

@timotew
Copy link
Contributor Author

timotew commented Jan 5, 2018

ping @joyeecheung .

@joyeecheung
Copy link
Member

@timotew Can you add tests for the changed APIs? Thanks!

@timotew
Copy link
Contributor Author

timotew commented Jan 9, 2018

@joyeecheung please review these test cases. Thanks!

message: 'File URL path illegal operation on a directory'
});

//fs.open
Copy link
Member

Choose a reason for hiding this comment

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

Does the linter pass on this? I think it should complain on comments without a space after //?

//creatReadStream
common.expectsError(
() => {
fs.createReadStream(new URL('file:///c:/tmp/test/'), 'test data');
Copy link
Member

@joyeecheung joyeecheung Jan 9, 2018

Choose a reason for hiding this comment

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

The second argument of fs.createReadStream is an option object or an encoding, I am pretty sure this would result in an error. We can leave it as fs.createReadStream(urlWithSlash) here. The same goes to fs.createWriteStream as well

@@ -72,3 +72,10 @@ if (common.isWindows) {
message: `File URL host must be "localhost" or empty on ${os.platform()}`
}));
}

// path ending with forward slash are not permitted
fs.readFile(new URL('file:///c:/tmp/test/'), common.expectsError({
Copy link
Member

Choose a reason for hiding this comment

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

You can test different type of URLs on different systems by reusing the tested URL object like this:

const urlWithSlash = common.isWindows ?
  new URL('file:///c:/tmp/test/') : new URL('file:///tmp/test/');

fs.readFile(urlWithSlash, common.expectsError(...));

@timotew
Copy link
Contributor Author

timotew commented Jan 9, 2018

@joyeecheung Please let me know if you notice any outstanding API.

@joyeecheung
Copy link
Member

Just realized this PR only normalizes the behavior for URL objects, but it does not validate the string paths. One way to do this is to update every path validations now, or wait until #17682 lands then update the validatePath function there to reduce code duplication.

lib/fs.js Outdated
@@ -1818,7 +1826,7 @@ StatWatcher.prototype.stop = function() {
const statWatchers = new Map();

fs.watchFile = function(filename, options, listener) {
handleError((filename = getPathFromURL(filename)));
handleError((filename = getPathFromURL(filename, true)));
Copy link
Member

Choose a reason for hiding this comment

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

We actually support fs.watchFile and fs.unwatchFile on directories. There are even tests in the async-hook test suites.

lib/fs.js Outdated
@@ -297,7 +297,7 @@ fs.access = function(path, mode, callback) {
throw new errors.TypeError('ERR_INVALID_CALLBACK');
}

if (handleError((path = getPathFromURL(path)), callback))
Copy link
Member

Choose a reason for hiding this comment

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

fs.access is supported on directories.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, did you mean to change fs.append?

lib/fs.js Outdated
@@ -1112,7 +1116,7 @@ fs.lstat = function(path, callback) {

fs.stat = function(path, callback) {
callback = makeStatsCallback(callback);
if (handleError((path = getPathFromURL(path)), callback))
if (handleError((path = getPathFromURL(path, true)), callback))
Copy link
Member

Choose a reason for hiding this comment

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

fs.stat is supported on directories.

lib/fs.js Outdated
@@ -1312,7 +1316,7 @@ fs.linkSync = function(existingPath, newPath) {

fs.unlink = function(path, callback) {
callback = makeCallback(callback);
if (handleError((path = getPathFromURL(path)), callback))
if (handleError((path = getPathFromURL(path, true)), callback))
Copy link
Member

Choose a reason for hiding this comment

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

This supports directories as well.

Copy link
Contributor

@starkwang starkwang left a comment

Choose a reason for hiding this comment

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

Thanks for your patience on this PR!
Just some small nits.

fs.readFile(urlWithSlash, common.expectsError({
code: 'ERR_INVALID_FILE_URL_PATH',
type: Error,
message: 'File URL path illegal operation on a directory'
Copy link
Contributor

Choose a reason for hiding this comment

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

This message seems to contain some syntax errors. Maybe 'File URL path cannot be a directory' ?

@timotew
Copy link
Contributor Author

timotew commented Jan 11, 2018

I try to pull #17682 and this happened! I guess my branch was out of date.
Here is the last commit 3f11d7c

@jasnell
Copy link
Member

jasnell commented Jan 11, 2018

Just for future reference, we typically use git rebase rather than git pull or git merge in order to prevent merge commits. We can get this PR cleaned up no problem :-)

@joyeecheung
Copy link
Member

joyeecheung commented Jan 11, 2018

@timotew You can rebase your branch by doing something like this:

# upstream is the remote that points to nodejs/node
git fetch upstream master
git checkout your-pr-branch
git rebase upstream/master

Looking at the commit history of this branch I think you have mixed commits from the upstream with your own commits. One way to get those back:

git checkout your-pr-branch
git checkout -b backup-branch  # back up where we are right now
git fetch upstream master
git checkout -b branch-for-cleanup  # create a new branch to clean things up
git reset --hard upstream/master  # bring the cleanup branch up-to-date with upstream
git log --reverse --pretty=format:%H --author="Your User Name" your-pr-branch | xargs -n 1 git cherry-pick
git checkout your-pr-branch
git reset --hard branch-for-cleanup # reset your pr branch to the same state as the cleanup branch

Where Your User Name is what git config --get user.name returns. This filters out all your commits and cherry-picks them onto the current master.

If you have trouble doing the above, let me know and I can help you restore the commits back (I can push to this PR branch since you allowed edits from maintainers in this PR).

@timotew timotew force-pushed the master branch 2 times, most recently from 27d7d4c to 49ef465 Compare January 11, 2018 21:48
@timotew
Copy link
Contributor Author

timotew commented Jan 11, 2018

@joyeecheung Thanks a lot!

const urlWithoutSlash = new URL(stringWithoutSlash);

// Uint8Array Buffer
const U8ABufferWithSlash = common.isWindows ? new Uint8Array([
Copy link
Member

Choose a reason for hiding this comment

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

You can use new Uint8Array(Buffer.from('....')) here to make it more readable.

<a id="ERR_INVALID_PATH_FORMAT"></a>
### ERR_INVALID_PATH_FORMAT

Provided path ended with an unexpected character i.e `/` for string and
Copy link
Member

Choose a reason for hiding this comment

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

There is no need to differentiate between strings and buffers here, it's just about the path ending with /.
Also we should just make the error code exclusively about "it is a directory" otherwise IMO we should not invent a new error code instead of using ERR_INVALID_ARG_VALUE. Maybe change this to ERR_PATH_IS_DIRECTORY (Path %s cannot end with '/' in operations invalid on directories)?

lib/fs.js Outdated
@@ -204,22 +204,52 @@ function validateOffsetLengthWrite(offset, length, byteLength) {
}
}

function validatePath(path, propName) {
let err;
function validatePath(path, propName, assertFilename, callback) {
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to pass a callback here or use handleError, this kind of errors should be synchronous, just like the type-checking errors (which reminds me the pattern handleError((path = getPathFromURL(path)), callback)) also look weird...why aren't they synchronous? @jasnell ).

This can just be function validatePath(path, propName = 'path', assertFilename = false), to hit the assertions you can just go with validatePath(path, 'path', true) since it does not hurt to explicitly pass in the name of the argument ('path'), then all the argument-overloading code below would be unnecessary.

lib/fs.js Outdated
return handle(new errors.Error('ERR_INVALID_PATH_FORMAT', path));
}
if (isUint8Array(path) && path[path.length - 1] === 47) {
return handle(new errors.Error('ERR_INVALID_PATH_FORMAT', path));
Copy link
Member

Choose a reason for hiding this comment

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

Note that this would be displayed as Path 102,105,108,101,58,47,47,47,116,109,112,47,116,101,115,116,47 ended with an invalid suffix character, we can either

  1. Do not display the content in the path if it is a Uint8Array
  2. Decode it as UTF8 and display it

Personally I prefer 1 even though we currently assume the buffer is encoded in UTF8 in the C++ layer. You can tweak the formatter in internal/errors.js and use a custom function instead of a string passed to util.format to do this.

@timotew
Copy link
Contributor Author

timotew commented Jan 23, 2018

@joyeecheung I think because fs.readFile is async it's throwing an error instead of passing it to the callback. How can can this be handled?

@timotew
Copy link
Contributor Author

timotew commented Jan 29, 2018

ping @joyeecheung

@Fishrock123
Copy link
Contributor

Is this not semver-major?

@Fishrock123 Fishrock123 added the semver-major PRs that contain breaking changes and should be released in the next major version. label Feb 2, 2018
@joyeecheung
Copy link
Member

joyeecheung commented Feb 3, 2018

@timotew Personally I believe these errors should be thrown synchronously like the type check errors, since they are not caused by any actual system calls and indicate that there is probably a bug in the user's code instead of the user's environment. The user either need to fix the code to trim the trailing /, or rethrow the error - that's not the error callbacks are for, IMO, they are for proper error handling like cleanup code that try to recover the state according to the syscall , not a known obvious bug in the argument, hence the errors should be thrown asap so the user can handle them asap. See #18308. But before that lands, I don't have a better opinion about this...@nodejs/tsc ?

@joyeecheung
Copy link
Member

joyeecheung commented Feb 8, 2018

@timotew #18308 has landed so I think we are good with throwing those errors in the validation helpers. Can you rebase against master? Thanks!

@joyeecheung
Copy link
Member

BTW, since there are plenty of commits in this PR, you might want to squash them on your PR branch before rebasing against master so there will be less conflicts to resolve.

@BridgeAR
Copy link
Member

BridgeAR commented Feb 9, 2018

Ping @timotew

@timotew
Copy link
Contributor Author

timotew commented Feb 9, 2018

@joyeecheung , @BridgeAR I'am on it, Thanks.

@timotew timotew force-pushed the master branch 2 times, most recently from 89f24cd to 8dc1c99 Compare February 10, 2018 18:32
@timotew
Copy link
Contributor Author

timotew commented Feb 10, 2018

ping @joyeecheung , @BridgeAR

@BridgeAR
Copy link
Member

Copy link
Contributor

@starkwang starkwang left a comment

Choose a reason for hiding this comment

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

It's quite near. Just some nits. 😉

lib/fs.js Outdated
@@ -81,7 +81,7 @@ function showTruncateDeprecation() {

function getOptions(options, defaultOptions) {
if (options === null || options === undefined ||
typeof options === 'function') {
typeof options === 'function') {
Copy link
Contributor

Choose a reason for hiding this comment

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

This line is an unnecessary change. Could you be so kind to revert it?

lib/fs.js Outdated
// force append behavior when using a supplied file descriptor
if (!options.flag || isFd(path))
options.flag = 'a';

Copy link
Contributor

Choose a reason for hiding this comment

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

There also some unnecessary blank line changes above. Could you revert it?

@@ -358,6 +358,9 @@ function message(key, args) {
fmt = util.format;
if (args === undefined || args.length === 0)
return msg;
else if (require('internal/util/types').isUint8Array(args[0]))
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure if this is necessary, this would interfere with other formatters that might want their first arguments formatted differently. Also why are we coercing Uint8Arrays to empty strings? The message Path cannot end with / in operations invalid on directories looks rather confusing, whereas util.inspect would print the Uint8Array properly if we just use %s in the formatter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this becuase you suggested it.

if (isUint8Array(path) && path[path.length - 1] === 47) {
return handle(new errors.Error('ERR_INVALID_PATH_FORMAT', path));

Note that this would be displayed as Path 102,105,108,101,58,47,47,47,116,109,112,47,116,101,115,116,47 ended with an invalid suffix character, we can either
1.Do not display the content in the path if it is a Uint8Array
2. Decode it as UTF8 and display it
Personally I prefer 1 even though we currently assume the buffer is encoded in UTF8 in the C++ layer.

Copy link
Member

Choose a reason for hiding this comment

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

@timotew Sorry, totally forgot about this, I think there is a third option: display the content as util.inspect displays it...still, come to think of it, Path Uint8Array [1, 2, 3...] cannot end with / in operations invalid on directories does look confusing as well. So the current message looks fine to me, but we should only change the formatter for this error instead of the message function.

Copy link
Member

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

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

I think there are some APIs missing the error after the rebase? Does the test pass locally?

@@ -737,6 +740,8 @@ E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported');
E('ERR_OUT_OF_RANGE', outOfRange);
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
E('ERR_PATH_IS_DIRECTORY',
'Path %s cannot end with / in operations invalid on directories');
Copy link
Member

Choose a reason for hiding this comment

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

Can you use a custom function here for not displaying Uint8Arrays?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the test pass locally?

Yes. it does.

Copy link
Member

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

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

Eh, wrong button...did not intend to approve, this still needs some work..but very close!

lib/fs.js Outdated
@@ -2392,6 +2403,7 @@ ReadStream.prototype.close = function(cb) {
};

fs.createWriteStream = function(path, options) {
path = getPathFromURL(path, true);
Copy link
Member

Choose a reason for hiding this comment

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

That should actually be the wrong spot to test for this. It should be tested in WriteStream (the same applies to ReadStream).

lib/fs.js Outdated
let err;
const isString = typeof path === 'string',
isU8Array = isUint8Array(path);
Copy link
Member

Choose a reason for hiding this comment

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

Please write things like these as in:

const var1 = 123;
const var2 = 321;

lib/fs.js Outdated
err = new errors.Error('ERR_PATH_IS_DIRECTORY', path);
if (isU8Array && lastChar === 47)
err = new errors.Error('ERR_PATH_IS_DIRECTORY', path);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Checking for an UInt8Array is not cheap. Therefore it would be good to rewrite this to:

if (typeof path === 'string') {
  err = nullCheck(path, propName, false);
  if (assertFilename && err === undefined && path[path.length - 1] === '/')
    err = new errors.Errors(...);
} else if (isUint8Array(path)) {
  err = nullCheck(path, propName, false);
  if (assertFilename && err === undefined && path[path.length - 1] === 47)
    err = new errors.Error(...);
} else {
  err = new errors.TypeError(...);
}

@@ -737,6 +737,11 @@ E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported');
E('ERR_OUT_OF_RANGE', outOfRange);
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
E('ERR_PATH_IS_DIRECTORY', (path) => {
if (require('internal/util/types').isUint8Array(path))
Copy link
Member

Choose a reason for hiding this comment

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

Since this error will only be thrown in case it is either a string or Uint8Array it would be cheaper to test for the string case.

urlWithSlash = new URL(stringWithSlash),
urlWithoutSlash = new URL(stringWithoutSlash),
U8ABufferWithSlash = new Uint8Array(Buffer.from(stringWithSlash)),
U8ABufferWithoutSlash = new Uint8Array(Buffer.from(stringWithoutSlash));
Copy link
Member

Choose a reason for hiding this comment

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

As above: please write this as:

const var1 = 123;
const var2 = 321;

test({ NODE_DEBUG: '' }, common.mustCall((data) => {
assert(/EISDIR/.test(data));
assert(!/test-fs-readfile-error/.test(data));
}));
Copy link
Member

Choose a reason for hiding this comment

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

As question: what was the reason that this was removed?

Copy link
Contributor Author

@timotew timotew Feb 17, 2018

Choose a reason for hiding this comment

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

This was to be controlled by rethrow() but because of Error.captureStackTrace(err, validatePath); in validatePath() . This assert(!/test-fs-readfile-error/.test(data)); will always be false.

@@ -737,6 +737,11 @@ E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported');
E('ERR_OUT_OF_RANGE', outOfRange);
E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
E('ERR_PATH_IS_DIRECTORY', (path) => {
if (require('internal/util/types').isUint8Array(path))
path = '';
Copy link
Member

Choose a reason for hiding this comment

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

I am not sold on showing an empty path. Instead it could just be a placeholder as [Uint8Array path].

E('ERR_PATH_IS_DIRECTORY', (path) => {
if (require('internal/util/types').isUint8Array(path))
path = '';
return `Path ${path} cannot end with / in operations invalid on directories`;
Copy link
Member

Choose a reason for hiding this comment

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

I try to understand the message but after reading it multiple times, I still can not fully understand it. Can you please rephrase this?

Copy link
Contributor Author

@timotew timotew Feb 17, 2018

Choose a reason for hiding this comment

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

It felt that way at first but I left it because English is not my native language and it was suggested by @joyeecheung.

Copy link
Member

@joyeecheung joyeecheung Feb 17, 2018

Choose a reason for hiding this comment

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

One way to make this more debuggable and consistent with other error messages is:

The argument "${name}" must not end with '/'. Received ${util.inspect(value)}

That requires us to pass in the name of the argument into the formatter, similar to ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE and the like.

@BridgeAR
Copy link
Member

BridgeAR commented Mar 2, 2018

Ping @timotew

@timotew
Copy link
Contributor Author

timotew commented Mar 4, 2018

ping! @BridgeAR , @joyeecheung , @starkwang .

if (path == null || !path[searchParams] ||
!path[searchParams][searchParams]) {
return path;
}
if (path.protocol !== 'file:')
throw new errors.TypeError('ERR_INVALID_URL_SCHEME', 'file');

if (assertFilename && path.href[path.href.length - 1] === '/')
throw new errors.Error('ERR_PATH_IS_DIRECTORY', 'path', path.pathname);
Copy link
Member

Choose a reason for hiding this comment

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

The string being checked and reported here should be the result of isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path) for consistency, also because errors like ERR_INVALID_FILE_URL_HOST should be thrown first.

Copy link
Member

Choose a reason for hiding this comment

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

Or, maybe getPathFromURL does not even need to be changed since the path extracted from URLs will be eventually passed to validatePath anyway..from what I can tell if we update WriteStream and ReadStream to validatePath(path, true) then we don't need to a ssert the file name in getPathFromURL

@@ -2095,6 +2096,7 @@ function WriteStream(path, options) {
if (!(this instanceof WriteStream))
return new WriteStream(path, options);

path = getPathFromURL(path, true);
Copy link
Member

@joyeecheung joyeecheung Mar 4, 2018

Choose a reason for hiding this comment

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

There is already one this.path = getPathFromURL(path); below. Also path is ignored if options.fd !== undefined so maybe we just need to delete this and change the line below to:

if (options.fd !== undefined) {
  this.path = getPathFromURL(path);
  validatePath(this.path, true);
}

Same about ReadStream

Copy link
Contributor Author

@timotew timotew Mar 4, 2018

Choose a reason for hiding this comment

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

validatePath only accepts Uint8Array this will work if Stream path is always Uint8Array or string.

Copy link
Member

Choose a reason for hiding this comment

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

@timotew The path in the fs stream classes can be falsy if options.fd is provdided, e.g. fs.createReadStream(null, { fd: fd, encoding: 'utf8' }) is valid. From what I can tell, validatePath accepts both string and Uint8Array.

@timotew
Copy link
Contributor Author

timotew commented Apr 9, 2018

I ran out of steam on this, if someone else wants to take it over please do, but I'm going to close it for now, kthxbai

@timotew timotew closed this Apr 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
errors Issues and PRs related to JavaScript errors originated in Node.js core. fs Issues and PRs related to the fs subsystem / file system. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Inconsistent behavior with fs module accross platforms