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

win,fs: fix inconsistencies between platforms for rename #1941

Closed
wants to merge 1 commit into from

Conversation

Shivang44
Copy link

Renaming directories has odd inconsistencies between Unix and Win
platforms. For example, renaming dirA to dirB when dirB is empty fails
on Win while succeeds on Linux. This commit fixes that by removing dirB
if dirB is empty. Additionally, renaming dirA to dirB when dirB is NOT
empty, on Linux, returns a ENOTEMPTY error, while on Win, returns a
EPERM error. This fixes this consistency be returning a ENOTEMPTY error
on windows as well.

Related to nodejs/node#21957

Note to reviewers: I am very new to C/C++ and I am sure I made mistakes,
both in my method and my test case. I would appreciate any in-depth feedback
on my code. Thank you!

Note about tests: The tests pass, but on some rare occasions they will fail. I assuming there are just flaky tests?

Renaming directories has odd inconsistencies between Unix and Win
platforms. For example, renaming dirA to dirB when dirB is empty fails
on Win while succeeds on Linux. This commit fixes that by removing dirB
if dirB is empty. Additionally, renaming dirA to dirB when dirB is NOT
empty, on Linux, returns a ENOTEMPTY error, while on Win, returns a
EPERM error. This fixes this consistency be returning a ENOTEMPTY error
on windows as well.

Related to nodejs/node#21957

Note to reviewers: I am very new to C/C++ and I am sure I made mistakes,
both in my method and my test case. I would appreciate any in-depth feedback
on my code. Thank you!
@Shivang44
Copy link
Author

Also, I am not sure if that pragma is in the best spot (or if I should even be using it). I added it because it will not compile without that directive.

@Shivang44
Copy link
Author

I realize windows is not that popular of platform for nodejs, but I feel fixing these inconsistencies is important. Can somebody please review this PR?

Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

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

Note about tests: The tests pass, but on some rare occasions they will fail. I assuming there are just flaky tests?

It depends which test(s) you're talking about. Can you be more specific about individual tests and platforms?

Overall, I'm a little torn on this. If it aligns the behavior across platforms, that's good. However, it also introduces a bit of a potential race condition due to the extra file system calls. There is also the question of whether this would classify as a bug fix or breaking change. I'd like to know what other collaborators think.

@@ -37,6 +37,9 @@

#include <wincrypt.h>

#pragma comment(lib, "Shlwapi.lib")
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't really use this style in the codebase. There is currently #1890, which adds it, but it's unclear that it will get merged.

WCHAR* pathw = req->file.pathw;
WCHAR* new_pathw = req->fs.info.new_pathw;

// These checks allow us to mimic the rename behavior on Unix platforms when
Copy link
Contributor

Choose a reason for hiding this comment

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

The indentation on these comments is off. Also, please use /* */ style comments.

@@ -1301,6 +1304,34 @@ static void fs__fstat(uv_fs_t* req) {


static void fs__rename(uv_fs_t* req) {
WCHAR* pathw = req->file.pathw;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please separate the declaration and assignment. So:

WCHAR* pathw;

pathw = req->file.pathw;

// like on Unix, rather than a EPERM error that MoveFileXW will throw on
// a nonempty directory.
if (PathIsDirectoryW(new_pathw)) {

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 blank lines around the ifs and else here and below.

if (PathIsDirectoryEmptyW(new_pathw)) {

// Attempt deletion of empty directory so MoveFileExW will succeed on rename.
if(!RemoveDirectoryW(new_pathw)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Space after if.


// Attempt deletion of empty directory so MoveFileExW will succeed on rename.
if(!RemoveDirectoryW(new_pathw)) {
SET_REQ_WIN32_ERROR(req, GetLastError());
Copy link
Contributor

Choose a reason for hiding this comment

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

Indentation is off.

// If the new path is a directory, check if it is empty.
// If it is, remove the directory so MoveFileExW and rename will succeed.
// If the directory is not empty, return by throwing a ENOTEMPY error,
// like on Unix, rather than a EPERM error that MoveFileXW will throw on
Copy link
Contributor

Choose a reason for hiding this comment

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

MoveFileXW -> MoveFileExW ?


} else {
// Directory is not empty, so throw a non-empty directory error.
SET_REQ_WIN32_ERROR(req, ERROR_DIR_NOT_EMPTY);
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 you could just use SET_REQ_UV_ERROR() here.

@@ -2805,6 +2805,38 @@ TEST_IMPL(fs_rename_to_existing_file) {
return 0;
}

TEST_IMPL(fs_rename_to_existing_empty_directory) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there also be a test case for ENOTEMPY?

@@ -1301,6 +1304,34 @@ static void fs__fstat(uv_fs_t* req) {


static void fs__rename(uv_fs_t* req) {
WCHAR* pathw = req->file.pathw;
Copy link
Contributor

Choose a reason for hiding this comment

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

This variable appears to be unused.

@Shivang44
Copy link
Author

Shivang44 commented Aug 21, 2018

@cjihrig Thanks for reviewing the PR! I will revise the PR according to your comments, but want to discuss it before I do so time is not wasted if this change is deemed to be not right by other maintainers.

Overall, I'm a little torn on this. If it aligns the behavior across platforms, that's good. However, it also introduces a bit of a potential race condition due to the extra file system calls. There is also the question of whether this would classify as a bug fix or breaking change. I'd like to know what other collaborators think.

Which part of the code creates a race condition? It was my understanding that the calls like PathIsDirectory RemoveDirectory block until they are returned? So there wouldn't be a race condition right? Again, my understanding of these calls and C are limited.

It depends which test(s) you're talking about. Can you be more specific about individual tests and platforms?

I will rerun the tests when I get a chance and let you know!

Overall, I made this PR because I thought this inconsistency in fs was an important one to fix. If other maintainers think that this difference across platforms is not that big of a deal, then I am completely okay with skipping this PR all together!

@cjihrig
Copy link
Contributor

cjihrig commented Aug 21, 2018

Which part of the code creates a race condition? It was my understanding that the calls like PathIsDirectory RemoveDirectory block until they are returned?

I was referring to the window of time between the calls. It's possible for another process to modify the contents of the directory while this function is executing. For example, the directory could go from empty to not empty between checking if it's empty and trying to remove it. It's definitely an edge case, but that's why I wanted to see what others thought.

@refack
Copy link
Contributor

refack commented Aug 21, 2018

Hello @Shivang44,

I feel fixing these inconsistencies is important.

I'm sure you are aware there are numerous other inconsistencies, for example; case sensitive names, timestamp accuracy, no links on FAT, different ACL mechanisms.

For example, renaming dirA to dirB when dirB is empty fails on Win while succeeds on Linux. This commit fixes that by removing dirB if dirB is empty.

IMHO implementing the complex solution you suggest should be left for downstream projects (e.g. node like mkdirp, or a userland module)

As for returning ENOTEMPY I think that is a breaking change. Since you are already making breaking changes, I suggest returning ENOTEMPY for both cases (if dirB is empty or not). I would go as far as to suggest to return ENOTEMPY even on POSIX, but this requires buy-in from the Collaborators.

@refack
Copy link
Contributor

refack commented Aug 21, 2018

P.S. I just found what I believe is a bug on Windows:

> mkdir dirA
> echo " " > dirB
> node -e "fs.renameSync('dirA', 'dirB')"

>

This will overwrite dirB.

For reference, on Ubuntu 16.04:

$ mkdir dirA
$ echo " " > dirB
$ node -e "fs.renameSync('dirA', 'dirB')"
fs.js:119
    throw err;
    ^

Error: ENOTDIR: not a directory, rename 'dirA' -> 'dirB'

@Shivang44
Copy link
Author

Shivang44 commented Sep 1, 2018

Due to the lack of interest as well as the above mentioned (potential) issues, I will be closing this PR. If other maintainers think that this change would be a good addition to libuv, I would be happy to reopen it work on it!

Thanks for your comments and help @cjihrig @refack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants