diff --git a/CMakeLists.txt b/CMakeLists.txt index 449dc8322..47edda4cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -706,7 +706,8 @@ if(LIBUV_BUILD_TESTS) test/test-udp-reuseport.c test/test-uname.c test/test-walk-handles.c - test/test-watcher-cross-stop.c) + test/test-watcher-cross-stop.c + test/test-win32-namespaces.c) add_executable(uv_run_tests ${uv_test_sources} uv_win_longpath.manifest) target_compile_definitions(uv_run_tests diff --git a/Makefile.am b/Makefile.am index 797efc83e..321584f7b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -331,7 +331,8 @@ test_run_tests_SOURCES = test/blackhole-server.c \ test/test-udp-reuseport.c \ test/test-uname.c \ test/test-walk-handles.c \ - test/test-watcher-cross-stop.c + test/test-watcher-cross-stop.c \ + test/test-win32-namespaces.c test_run_tests_LDADD = libuv.la if WINNT diff --git a/src/win/util.c b/src/win/util.c index 9fb5694c9..f3a2a65b1 100644 --- a/src/win/util.c +++ b/src/win/util.c @@ -207,17 +207,157 @@ int uv_cwd(char* buffer, size_t* size) { } +/* Helper function to check if path contains forward slashes */ +static int uv__path_has_forward_slash(const WCHAR* path, size_t len) { + size_t i; + for (i = 0; i < len; i++) { + if (path[i] == L'/') + return 1; + } + return 0; +} + + +/* Helper function to check if path uses Win32 namespace prefix */ +static int uv__is_win32_namespace(const WCHAR* path, size_t len) { + return len >= 4 && + path[0] == L'\\' && + path[1] == L'\\' && + (path[2] == L'?' || path[2] == L'.') && + path[3] == L'\\'; +} + + +/* Helper function to check if path uses NT namespace prefix */ +static int uv__is_nt_namespace(const WCHAR* path, size_t len) { + return len >= 4 && + path[0] == L'\\' && + path[1] == L'?' && + path[2] == L'?' && + path[3] == L'\\'; +} + + +/* Helper function to check if path is UNC after namespace prefix */ +static int uv__is_unc_after_prefix(const WCHAR* path, size_t len) { + /* Need "UNC\" pattern: at least 4 chars (UNC + backslash) */ + if (len < 4) + return 0; + + /* Check if starts with "UNC" (case-insensitive) followed by backslash */ + return _wcsnicmp(path, L"UNC", 3) == 0 && path[3] == L'\\'; +} + + +/* Helper function to extract drive letter from various path formats */ +static WCHAR uv__extract_drive_letter(const WCHAR* path, size_t len) { + const WCHAR* scan = path; + size_t scan_len = len; + + /* Handle Win32 namespace: \\?\ or \\.\ */ + if (uv__is_win32_namespace(scan, scan_len)) { + scan += 4; + scan_len -= 4; + + /* Check for UNC path: \\?\UNC\server\share */ + if (uv__is_unc_after_prefix(scan, scan_len)) + return 0; /* No drive letter in UNC paths */ + + /* Check for device paths: \\.\COM1, \\.\PhysicalDrive0 */ + if (path[2] == L'.') { + /* Device namespace paths don't have drive letters we care about */ + if (scan_len < 2 || scan[1] != L':') + return 0; + } + + /* Check for Volume GUID: \\?\Volume{...} */ + if (scan_len >= 7 && + _wcsnicmp(scan, L"VOLUME", 6) == 0 && + scan[6] == L'{') + return 0; /* Volume GUIDs don't have traditional drive letters */ + + /* Check for GLOBALROOT and other special paths */ + if (scan_len >= 10 && + _wcsnicmp(scan, L"GLOBALROOT", 10) == 0) + return 0; + } + /* Handle NT namespace: \??\ */ + else if (uv__is_nt_namespace(scan, scan_len)) { + scan += 4; + scan_len -= 4; + + /* Check for UNC: \??\UNC\server\share */ + if (uv__is_unc_after_prefix(scan, scan_len)) + return 0; + } + /* Handle regular UNC: \\server\share */ + else if (scan_len >= 2 && + scan[0] == L'\\' && + scan[1] == L'\\') { + return 0; /* UNC path */ + } + + /* Now check for drive letter: X: */ + if (scan_len >= 2 && scan[1] == L':') { + WCHAR letter = scan[0]; + + /* Validate and normalize to uppercase */ + if (letter >= L'A' && letter <= L'Z') + return letter; + else if (letter >= L'a' && letter <= L'z') + return letter - L'a' + L'A'; + } + + return 0; /* No valid drive letter found */ +} + + int uv_chdir(const char* dir) { WCHAR *utf16_buffer; DWORD utf16_len; WCHAR drive_letter, env_var[4]; int r; + if (dir == NULL || dir[0] == '\0') + return UV_EINVAL; + /* Convert to UTF-16 */ r = uv__convert_utf8_to_utf16(dir, &utf16_buffer); if (r) return r; + /* Validate path length (32767 is the maximum for Win32 namespace) */ + utf16_len = wcslen(utf16_buffer); + if (utf16_len > 32767) { + uv__free(utf16_buffer); + return UV_ENAMETOOLONG; + } + + /* Win32 namespace paths (\\?\ and \\.\) do not support forward slashes. + * They require exact path strings and do not perform any normalization. + * Regular paths and UNC paths can handle forward slashes as Windows + * normalizes them to backslashes. + */ + if (uv__is_win32_namespace(utf16_buffer, utf16_len)) { + /* Skip prefix for further checks */ + const WCHAR* check_path = utf16_buffer + 4; + size_t check_len = utf16_len - 4; + + /* Explicitly reject GLOBALROOT paths */ + if (check_len >= 11 && + _wcsnicmp(check_path, L"GLOBALROOT", 10) == 0 && + check_path[10] == L'\\') { + uv__free(utf16_buffer); + return UV_EINVAL; + } + + /* Forward slash check */ + if (uv__path_has_forward_slash(utf16_buffer + 4, utf16_len - 4)) { + uv__free(utf16_buffer); + return UV_EINVAL; + } + } + if (!SetCurrentDirectoryW(utf16_buffer)) { uv__free(utf16_buffer); return uv_translate_sys_error(GetLastError()); @@ -241,19 +381,17 @@ int uv_chdir(const char* dir) { return r; } - if (utf16_len < 2 || utf16_buffer[1] != L':') { - /* Doesn't look like a drive letter could be there - probably an UNC path. - * TODO: Need to handle win32 namespaces like \\?\C:\ ? */ - drive_letter = 0; - } else if (utf16_buffer[0] >= L'A' && utf16_buffer[0] <= L'Z') { - drive_letter = utf16_buffer[0]; - } else if (utf16_buffer[0] >= L'a' && utf16_buffer[0] <= L'z') { - /* Convert to uppercase. */ - drive_letter = utf16_buffer[0] - L'a' + L'A'; - } else { - /* Not valid. */ - drive_letter = 0; - } + /* Extract drive letter from the current working directory path. + * This handles various Windows namespace formats: + * - Regular paths: C:\Windows + * - Win32 file namespace: \\?\C:\LongPath + * - Win32 device namespace: \\.\C: + * - NT namespace: \??\C:\Path + * - UNC paths: \\server\share (no drive letter) + * - Volume GUIDs: \\?\Volume{...} (no drive letter) + * - Special paths: \\?\GLOBALROOT\... (no drive letter) + */ + drive_letter = uv__extract_drive_letter(utf16_buffer, utf16_len); if (drive_letter != 0) { /* Construct the environment variable name and set it. */ diff --git a/test/test-list.h b/test/test-list.h index 954eff330..2ea028a63 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -593,6 +593,19 @@ TEST_DECLARE (metrics_idle_time) TEST_DECLARE (metrics_idle_time_thread) TEST_DECLARE (metrics_idle_time_zero) +#ifdef _WIN32 +TEST_DECLARE(chdir_win32_namespace) +TEST_DECLARE(chdir_unc_paths) +TEST_DECLARE(chdir_forward_slash_rejection) +TEST_DECLARE(chdir_path_too_long) +TEST_DECLARE(chdir_volume_guid_path) +TEST_DECLARE(chdir_globalroot_path) +TEST_DECLARE(chdir_nt_namespace) +TEST_DECLARE(chdir_case_insensitive) +TEST_DECLARE(chdir_drive_env_variable_update) +TEST_DECLARE(chdir_device_paths) +#endif + TASK_LIST_START TEST_ENTRY_CUSTOM (platform_output, 0, 1, 5000) @@ -1265,6 +1278,19 @@ TASK_LIST_START TEST_ENTRY (metrics_idle_time_thread) TEST_ENTRY (metrics_idle_time_zero) +#ifdef _WIN32 + TEST_ENTRY(chdir_win32_namespace) + TEST_ENTRY(chdir_unc_paths) + TEST_ENTRY(chdir_forward_slash_rejection) + TEST_ENTRY(chdir_path_too_long) + TEST_ENTRY(chdir_volume_guid_path) + TEST_ENTRY(chdir_globalroot_path) + TEST_ENTRY(chdir_nt_namespace) + TEST_ENTRY(chdir_case_insensitive) + TEST_ENTRY(chdir_drive_env_variable_update) + TEST_ENTRY(chdir_device_paths) +#endif + #if 0 /* These are for testing the test runner. */ TEST_ENTRY (fail_always) diff --git a/test/test-win32-namespaces.c b/test/test-win32-namespaces.c new file mode 100644 index 000000000..926a5dca9 --- /dev/null +++ b/test/test-win32-namespaces.c @@ -0,0 +1,430 @@ +#ifdef _WIN32 + +#include "uv.h" +#include "task.h" +#include +#include +#include + +/* Helper to get current drive letter */ +static char get_current_drive(void) { + char cwd[MAX_PATH]; + size_t cwd_size = sizeof(cwd); + if (uv_cwd(cwd, &cwd_size) != 0) + return 0; + if (cwd[0] >= 'A' && cwd[0] <= 'Z') + return cwd[0]; + if (cwd[0] >= 'a' && cwd[0] <= 'z') + return cwd[0] - 'a' + 'A'; + return 0; +} + + +/* Helper to check if a path exists and is accessible */ +static int path_exists(const char* path) { + WCHAR wpath[32768]; /* Max path length for Win32 namespace */ + DWORD attrs; + int len; + + /* Convert UTF-8 to UTF-16 using Windows API */ + len = MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, 32768); + if (len == 0) + return 0; + + attrs = GetFileAttributesW(wpath); + + return attrs != INVALID_FILE_ATTRIBUTES && + (attrs & FILE_ATTRIBUTE_DIRECTORY); +} + + +/* Helper to get environment variable value (drive-local path) */ +static int get_drive_env(char drive, char* buf, size_t buflen) { + char env_name[4]; + env_name[0] = '='; + env_name[1] = drive; + env_name[2] = ':'; + env_name[3] = '\0'; + + DWORD result = GetEnvironmentVariableA(env_name, buf, (DWORD)buflen); + return result > 0 && result < buflen; +} + + +TEST_IMPL(chdir_win32_namespace) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + char test_path[1024]; + char expected_path[1024]; + char env_value[1024]; + char drive; + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Test 1: Win32 namespace path \\?\C:\Windows */ + snprintf(test_path, sizeof(test_path), "\\\\?\\%c:\\Windows", drive); + if (path_exists(test_path)) { + r = uv_chdir(test_path); + ASSERT_EQ(r, 0); + + /* Verify the drive-local environment variable is set */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + + /* The env value should contain the current path (check substring) */ + snprintf(expected_path, sizeof(expected_path), "\\\\?\\%c:\\Windows", drive); + ASSERT(strstr(env_value, "Windows") != NULL); + } + + /* Test 2: Win32 namespace with lowercase drive letter */ + snprintf(test_path, sizeof(test_path), "\\\\?\\%c:\\Windows", drive + 32); + if (path_exists(test_path)) { + r = uv_chdir(test_path); + ASSERT_EQ(r, 0); + + /* Environment variable should use uppercase drive letter */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + } + + /* Test 3: Device namespace \\.\C: (if accessible) */ + snprintf(test_path, sizeof(test_path), "\\\\.\\%c:", drive); + r = uv_chdir(test_path); + /* May fail with access denied, which is OK */ + if (r == 0) { + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + } + + /* Restore original directory */ + r = uv_chdir(original_cwd); + ASSERT_EQ(r, 0); + + return 0; +} + + +TEST_IMPL(chdir_unc_paths) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + char test_path[1024]; + char drive; + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + char old_env[1024]; + int had_env = get_drive_env(drive, old_env, sizeof(old_env)); + + /* Test 1: UNC path with Win32 namespace \\?\UNC\localhost\C$ */ + snprintf(test_path, sizeof(test_path), "\\\\?\\UNC\\localhost\\%c$", drive); + r = uv_chdir(test_path); + + if (r == 0) { + char new_env[1024]; + int has_env = get_drive_env(drive, new_env, sizeof(new_env)); + + if (had_env && has_env) { + ASSERT_STR_EQ(old_env, new_env); + } + + char cwd[1024]; + size_t cwd_size = sizeof(cwd); + r = uv_cwd(cwd, &cwd_size); + ASSERT_EQ(r, 0); + + /* Verify we're actually in a UNC path */ + ASSERT(strncmp(cwd, "\\\\", 2) == 0 || strncmp(cwd, "//", 2) == 0); + } else { + /* Access denied or path not found is acceptable for UNC paths */ + ASSERT(r == UV_EACCES || r == UV_ENOENT || r == UV_EINVAL); + } + + /* Test 2: Regular UNC path \\localhost\C$ */ + snprintf(test_path, sizeof(test_path), "\\\\localhost\\%c$", drive); + r = uv_chdir(test_path); + /* May fail, which is acceptable */ + + /* Restore original directory */ + r = uv_chdir(original_cwd); + ASSERT_EQ(r, 0); + + return 0; +} + + +TEST_IMPL(chdir_forward_slash_rejection) { + char test_path[256]; + char drive; + int r; + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Test 1: Win32 namespace with forward slash should fail */ + snprintf(test_path, sizeof(test_path), "\\\\?\\%c:/Windows", drive); + r = uv_chdir(test_path); + ASSERT_EQ(r, UV_EINVAL); + + /* Test 2: Win32 namespace with mixed slashes should fail */ + snprintf(test_path, sizeof(test_path), "\\\\?\\%c:\\Windows/System32", drive); + r = uv_chdir(test_path); + ASSERT_EQ(r, UV_EINVAL); + + /* Test 3: Device namespace with forward slash should fail */ + snprintf(test_path, sizeof(test_path), "\\\\.\\%c:/", drive); + r = uv_chdir(test_path); + ASSERT_EQ(r, UV_EINVAL); + + /* Test 4: Regular path with forward slash should succeed (Windows normalizes) */ + snprintf(test_path, sizeof(test_path), "%c:/Windows", drive); + r = uv_chdir(test_path); + /* Should succeed if path exists, or fail with ENOENT, but NOT EINVAL */ + ASSERT_NE(r, UV_EINVAL); + + return 0; +} + + +TEST_IMPL(chdir_path_too_long) { + char* long_path; + int r; + size_t i; + char drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Create a path longer than 32767 characters */ + long_path = malloc(35000); + ASSERT(long_path != NULL); + + /* Build: \\?\C:\ + many repetitions of "LongDirectory\" */ + snprintf(long_path, 100, "\\\\?\\%c:\\", drive); + size_t pos = strlen(long_path); + + for (i = 0; i < 2500; i++) { + strcpy(long_path + pos, "LongDirectory\\"); + pos += 14; + if (pos > 33000) break; + } + long_path[pos] = '\0'; + + /* This should fail with UV_ENAMETOOLONG */ + r = uv_chdir(long_path); + ASSERT_EQ(r, UV_ENAMETOOLONG); + + free(long_path); + return 0; +} + + +TEST_IMPL(chdir_volume_guid_path) { + /* Note: This test may not work on all systems as volume GUIDs are dynamic. + * We're testing that the code handles them correctly without crashing. */ + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + /* Try a Volume GUID path (will likely fail with ENOENT, which is fine) */ + r = uv_chdir("\\\\?\\Volume{12345678-1234-1234-1234-123456789012}\\"); + + /* Should fail with ENOENT or EINVAL, but should NOT crash */ + ASSERT(r == UV_ENOENT || r == UV_EINVAL || r == UV_EACCES); + + /* Verify we're still in the original directory */ + char current_cwd[1024]; + size_t current_cwd_size = sizeof(current_cwd); + r = uv_cwd(current_cwd, ¤t_cwd_size); + ASSERT_EQ(r, 0); + ASSERT_STR_EQ(current_cwd, original_cwd); + + return 0; +} + + +TEST_IMPL(chdir_globalroot_path) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + /* Try a GLOBALROOT path (will likely fail, which is fine) */ + r = uv_chdir("\\\\?\\GLOBALROOT\\Device\\HarddiskVolume1"); + + /* Should fail with ENOENT or EINVAL or EACCES, but should NOT crash */ + ASSERT(r == UV_ENOENT || r == UV_EINVAL || r == UV_EACCES); + + /* Verify we're still in the original directory */ + char current_cwd[1024]; + size_t current_cwd_size = sizeof(current_cwd); + r = uv_cwd(current_cwd, ¤t_cwd_size); + ASSERT_EQ(r, 0); + ASSERT_STR_EQ(current_cwd, original_cwd); + + return 0; +} + + +TEST_IMPL(chdir_nt_namespace) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + char test_path[256]; + char env_value[1024]; + char drive; + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Test NT namespace path \??\C:\Windows */ + snprintf(test_path, sizeof(test_path), "\\??\\%c:\\Windows", drive); + r = uv_chdir(test_path); + + if (r == 0) { + /* Verify the drive-local environment variable is set */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + ASSERT(strstr(env_value, "Windows") != NULL); + } else { + /* May fail with access issues, which is acceptable */ + ASSERT(r == UV_ENOENT || r == UV_EINVAL || r == UV_EACCES); + } + + /* Restore original directory */ + r = uv_chdir(original_cwd); + ASSERT_EQ(r, 0); + + return 0; +} + + +TEST_IMPL(chdir_case_insensitive) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + char test_path_upper[256]; + char test_path_lower[256]; + char test_path_mixed[256]; + char env_value[1024]; + char drive; + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Test uppercase drive */ + snprintf(test_path_upper, sizeof(test_path_upper), "%c:\\Windows", drive); + if (path_exists(test_path_upper)) { + r = uv_chdir(test_path_upper); + ASSERT_EQ(r, 0); + + /* Environment variable should always use uppercase */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + } + + /* Test lowercase drive */ + snprintf(test_path_lower, sizeof(test_path_lower), "%c:\\Windows", drive + 32); + if (path_exists(test_path_lower)) { + r = uv_chdir(test_path_lower); + ASSERT_EQ(r, 0); + + /* Environment variable should STILL use uppercase */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + } + + /* Test with Win32 namespace and lowercase */ + snprintf(test_path_mixed, sizeof(test_path_mixed), "\\\\?\\%c:\\Windows", + drive + 32); + if (path_exists(test_path_mixed)) { + r = uv_chdir(test_path_mixed); + ASSERT_EQ(r, 0); + + /* Environment variable should use uppercase */ + ASSERT_EQ(get_drive_env(drive, env_value, sizeof(env_value)), 1); + } + + /* Restore original directory */ + r = uv_chdir(original_cwd); + ASSERT_EQ(r, 0); + + return 0; +} + + +TEST_IMPL(chdir_drive_env_variable_update) { + char original_cwd[1024]; + size_t original_cwd_size = sizeof(original_cwd); + char test_path_1[256]; + char test_path_2[256]; + char env_value_1[1024]; + char env_value_2[1024]; + char drive; + int r; + + /* Save original working directory */ + r = uv_cwd(original_cwd, &original_cwd_size); + ASSERT_EQ(r, 0); + + drive = get_current_drive(); + ASSERT_NE(drive, 0); + + /* Change to Windows directory */ + snprintf(test_path_1, sizeof(test_path_1), "%c:\\Windows", drive); + if (path_exists(test_path_1)) { + r = uv_chdir(test_path_1); + ASSERT_EQ(r, 0); + + /* Get environment variable value */ + ASSERT_EQ(get_drive_env(drive, env_value_1, sizeof(env_value_1)), 1); + ASSERT(strstr(env_value_1, "Windows") != NULL); + + /* Change to System32 subdirectory */ + snprintf(test_path_2, sizeof(test_path_2), "%c:\\Windows\\System32", drive); + if (path_exists(test_path_2)) { + r = uv_chdir(test_path_2); + ASSERT_EQ(r, 0); + + /* Environment variable should be updated */ + ASSERT_EQ(get_drive_env(drive, env_value_2, sizeof(env_value_2)), 1); + ASSERT(strstr(env_value_2, "System32") != NULL); + + /* Values should be different */ + ASSERT_STR_NE(env_value_1, env_value_2); + } + } + + /* Restore original directory */ + r = uv_chdir(original_cwd); + ASSERT_EQ(r, 0); + + return 0; +} + +TEST_IMPL(chdir_device_paths) { + /* These should fail gracefully, not crash */ + ASSERT_NE(uv_chdir("\\\\.\\COM1"), 0); + ASSERT_NE(uv_chdir("\\\\.\\PhysicalDrive0"), 0); + return 0; +} + +#endif