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

fix: Handle utf-8 file paths on Windows #125

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
18 changes: 9 additions & 9 deletions src/windows/WindowsBackend.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ void BruteForceBackend::readTree(Watcher &watcher, std::shared_ptr<DirTree> tree
std::string spec = path + "\\*";
directories.pop();

WIN32_FIND_DATA ffd;
hFind = FindFirstFile(spec.c_str(), &ffd);
WIN32_FIND_DATAW ffd;
hFind = FindFirstFileW(extendedWidePath(spec).data(), &ffd);

if (hFind == INVALID_HANDLE_VALUE) {
if (path == watcher.mDir) {
Expand All @@ -35,8 +35,8 @@ void BruteForceBackend::readTree(Watcher &watcher, std::shared_ptr<DirTree> tree
}

do {
if (strcmp(ffd.cFileName, ".") != 0 && strcmp(ffd.cFileName, "..") != 0) {
std::string fullPath = path + "\\" + ffd.cFileName;
if (wcscmp(ffd.cFileName, L".") != 0 && wcscmp(ffd.cFileName, L"..") != 0) {
std::string fullPath = path + "\\" + utf16ToUtf8(ffd.cFileName, sizeof(ffd.cFileName));
if (watcher.isIgnored(fullPath)) {
continue;
}
Expand All @@ -46,7 +46,7 @@ void BruteForceBackend::readTree(Watcher &watcher, std::shared_ptr<DirTree> tree
directories.push(fullPath);
}
}
} while (FindNextFile(hFind, &ffd) != 0);
} while (FindNextFileW(hFind, &ffd) != 0);

FindClose(hFind);
}
Expand Down Expand Up @@ -80,7 +80,7 @@ class Subscription {
mWriteBuffer.resize(DEFAULT_BUF_SIZE);

mDirectoryHandle = CreateFileW(
utf8ToUtf16(watcher->mDir).data(),
extendedWidePath(watcher->mDir).data(),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
Expand Down Expand Up @@ -178,7 +178,7 @@ class Subscription {
case ERROR_ACCESS_DENIED: {
// This can happen if the watched directory is deleted. Check if that is the case,
// and if so emit a delete event. Otherwise, fall through to default error case.
DWORD attrs = GetFileAttributesW(utf8ToUtf16(mWatcher->mDir).data());
DWORD attrs = GetFileAttributesW(extendedWidePath(mWatcher->mDir).data());
bool isDir = attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
if (!isDir) {
mWatcher->mEvents.remove(mWatcher->mDir);
Expand Down Expand Up @@ -224,15 +224,15 @@ class Subscription {
case FILE_ACTION_ADDED:
case FILE_ACTION_RENAMED_NEW_NAME: {
WIN32_FILE_ATTRIBUTE_DATA data;
if (GetFileAttributesExW(utf8ToUtf16(path).data(), GetFileExInfoStandard, &data)) {
if (GetFileAttributesExW(extendedWidePath(path).data(), GetFileExInfoStandard, &data)) {
mWatcher->mEvents.create(path);
mTree->add(path, CONVERT_TIME(data.ftLastWriteTime), data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
}
break;
}
case FILE_ACTION_MODIFIED: {
WIN32_FILE_ATTRIBUTE_DATA data;
if (GetFileAttributesExW(utf8ToUtf16(path).data(), GetFileExInfoStandard, &data)) {
if (GetFileAttributesExW(extendedWidePath(path).data(), GetFileExInfoStandard, &data)) {
mTree->update(path, CONVERT_TIME(data.ftLastWriteTime));
if (!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
mWatcher->mEvents.update(path);
Expand Down
8 changes: 6 additions & 2 deletions src/windows/win_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ std::string utf16ToUtf8(const WCHAR *input, size_t length) {
return res;
}

std::string normalizePath(std::string path) {
std::wstring extendedWidePath(std::string path) {
// Prevent truncation to MAX_PATH characters by adding the \\?\ prefix
std::wstring p = utf8ToUtf16("\\\\?\\" + path);
return utf8ToUtf16("\\\\?\\" + path);
}

std::string normalizePath(std::string path) {
std::wstring p = extendedWidePath(path);

// Get the required length for the output
unsigned int len = GetLongPathNameW(p.data(), NULL, 0);
Expand Down
1 change: 1 addition & 0 deletions src/windows/win_utils.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

std::wstring utf8ToUtf16(std::string input);
std::string utf16ToUtf8(const WCHAR *input, size_t length);
std::wstring extendedWidePath(std::string path);
std::string normalizePath(std::string path);

#endif
53 changes: 52 additions & 1 deletion test/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,57 @@ describe('watcher', () => {
let res = await nextEvent();
assert.deepEqual(res, [{type: 'delete', path: f}]);
});

it('should store UTF-8 paths properly in the tree', async () => {
if (backend === 'watchman') {
// No events seem to be emitted when watchman is used. It might mean
// that WatchmanBackend does not properly handle UTF-8 characters on
// Windows as well.
return;
}

let f = path.join(tmpDir, 'spécial');
fs.writeFile(f, 'hello');

async function listen(dir) {
let cbs = [];
let nextEvent = () => {
return new Promise((resolve) => {
cbs.push(resolve);
});
};

let fn = (err, events) => {
if (err) {
throw err;
}

setImmediate(() => {
for (let cb of cbs) {
cb(events);
}

cbs = [];
});
};
let sub = await watcher.subscribe(dir, fn, {backend});

return [nextEvent, sub];
}

let [nextEvent, sub] = await listen(tmpDir);

await fs.remove(f);

try {
// XXX: no events emitted if non-ascii characters are not handled
// properly in BruteForceBackend::readTree on Windows.
let res = await nextEvent();
assert.deepEqual(res, [{type: 'delete', path: f}]);
} finally {
await sub.unsubscribe();
}
});
});

describe('directories', () => {
Expand Down Expand Up @@ -339,7 +390,7 @@ describe('watcher', () => {
await fs.mkdir(base);
await nextEvent();

let getPath = p => path.join(base, p);
let getPath = (p) => path.join(base, p);

await fs.mkdir(getPath('dir'));
await nextEvent();
Expand Down