-
-
Notifications
You must be signed in to change notification settings - Fork 33.9k
fs: create consistent behavior for fs module across platforms #17927
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
Conversation
lib/fs.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
space after comment please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@devsnek Space added thanks.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lib/fs.js
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Using
errors.TypeErrormight be more appropriate. - The second parameter (here is
path) ofERR_INVALID_FILE_URL_PATHis wrong. It should be a string contains an error description about the invalid path.
Here is some example:
Lines 1362 to 1363 in abe14ab
return new errors.TypeError('ERR_INVALID_FILE_URL_PATH', 'must not include encoded / characters');
Lines 1346 to 1347 in abe14ab
return new errors.TypeError('ERR_INVALID_FILE_URL_PATH', 'must be absolute');
There was a problem hiding this comment.
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 👍.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
Line 401 in a82b1b7
| E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s'); |
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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.
|
@nodejs/fs |
|
Might be worth a look from @nodejs/url too, especially @TimothyGu? |
lib/fs.js
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
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 |
joyeecheung
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making it explicit.
|
@joyeecheung Hi there! hope you have time to review these change. |
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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(...));
lib/internal/url.js
Outdated
There was a problem hiding this comment.
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
lib/internal/url.js
Outdated
There was a problem hiding this comment.
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.
lib/internal/url.js
Outdated
There was a problem hiding this comment.
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.
|
Nice work, although I think there are still some APIs that need to be updated:
Can you update those APIs as well? |
|
ping @joyeecheung . |
|
@timotew Can you add tests for the changed APIs? Thanks! |
|
@joyeecheung please review these test cases. Thanks! |
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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 //?
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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(...));|
@joyeecheung Please let me know if you notice any outstanding API. |
|
Just realized this PR only normalizes the behavior for |
lib/fs.js
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
starkwang
left a comment
There was a problem hiding this 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.
test/parallel/test-fs-whatwg-url.js
Outdated
There was a problem hiding this comment.
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' ?
|
Just for future reference, we typically use |
|
@timotew You can rebase your branch by doing something like this: 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 branchWhere 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). |
27d7d4c to
49ef465
Compare
|
@joyeecheung Thanks a lot! |
test/parallel/test-fs-path-err.js
Outdated
There was a problem hiding this comment.
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.
doc/api/errors.md
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
- Do not display the content in the path if it is a Uint8Array
- 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.
|
@joyeecheung I think because |
|
ping @joyeecheung |
|
Is this not |
|
@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 |
|
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. |
|
Ping @timotew |
|
@joyeecheung , @BridgeAR I'am on it, Thanks. |
89f24cd to
8dc1c99
Compare
|
ping @joyeecheung , @BridgeAR |
starkwang
left a comment
There was a problem hiding this 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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
lib/internal/errors.js
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
joyeecheung
left a comment
There was a problem hiding this 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?
lib/internal/errors.js
Outdated
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
joyeecheung
left a comment
There was a problem hiding this 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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(...);
}
lib/internal/errors.js
Outdated
There was a problem hiding this comment.
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.
test/parallel/test-fs-path-err.js
Outdated
There was a problem hiding this comment.
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;There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/internal/errors.js
Outdated
There was a problem hiding this comment.
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].
lib/internal/errors.js
Outdated
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Ping @timotew |
|
ping! @BridgeAR , @joyeecheung , @starkwang . |
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| if (!(this instanceof WriteStream)) | ||
| return new WriteStream(path, options); | ||
|
|
||
| path = getPathFromURL(path, true); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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 |
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), orvcbuild test(Windows) passesAffected core subsystem(s)
fs