From 588ea9b91359d9b6bb8ae02cd9dfc565107056cf Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:38:18 -0500 Subject: [PATCH] win: readlink support for IO_REPARSE_TAG_LX_SYMLINK (#4994) This adds support for "Linux"-style Windows symbolic links, reparse tag 0xA000001D (IO_REPARSE_TAG_LX_SYMLINK). --- src/win/fs.c | 26 ++++++++++++ src/win/winapi.h | 7 ++++ test/test-fs.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++ test/test-list.h | 2 + 4 files changed, 135 insertions(+) diff --git a/src/win/fs.c b/src/win/fs.c index ca09ad46c..cb498608e 100644 --- a/src/win/fs.c +++ b/src/win/fs.c @@ -247,6 +247,32 @@ static int fs__readlink_handle(HANDLE handle, } } + } else if (reparse_data->ReparseTag == IO_REPARSE_TAG_LX_SYMLINK) { + /* Real (Linux) symlink */ + char* buffer; + char* target; + size_t target_len; + + target_len = (reparse_data->ReparseDataLength - + sizeof(ULONG)); /* Version field */ + buffer = (char*) reparse_data->LinuxSymbolicLinkReparseBuffer.PathBuffer; + + if (target_len_ptr != NULL) { + *target_len_ptr = target_len; + } + + if (target_ptr != NULL) { + assert(*target_ptr == NULL); + target = uv__malloc(target_len + 1); + if (target == NULL) { + return UV_ENOMEM; + } + memcpy(target, buffer, target_len); + target[target_len] = '\0'; + *target_ptr = target; + } + return 0; + } else if (reparse_data->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { /* Junction. */ w_target = reparse_data->MountPointReparseBuffer.PathBuffer + diff --git a/src/win/winapi.h b/src/win/winapi.h index a7e1b179f..2f25dc55f 100644 --- a/src/win/winapi.h +++ b/src/win/winapi.h @@ -4163,6 +4163,10 @@ typedef struct _REPARSE_DATA_BUFFER { ULONG Flags; WCHAR PathBuffer[1]; } SymbolicLinkReparseBuffer; + struct { + ULONG Version; + UCHAR PathBuffer[1]; + } LinuxSymbolicLinkReparseBuffer; struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; @@ -4582,6 +4586,9 @@ typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { #ifndef IO_REPARSE_TAG_SYMLINK # define IO_REPARSE_TAG_SYMLINK (0xA000000CL) #endif +#ifndef IO_REPARSE_TAG_LX_SYMLINK +# define IO_REPARSE_TAG_LX_SYMLINK (0xA000001DL) +#endif #ifndef IO_REPARSE_TAG_APPEXECLINK # define IO_REPARSE_TAG_APPEXECLINK (0x8000001BL) #endif diff --git a/test/test-fs.c b/test/test-fs.c index 44788dc26..5f3265fe3 100644 --- a/test/test-fs.c +++ b/test/test-fs.c @@ -23,6 +23,7 @@ #include "task.h" #include +#include /* offsetof */ #include /* memset */ #include #include @@ -37,6 +38,13 @@ # ifndef ERROR_SYMLINK_NOT_SUPPORTED # define ERROR_SYMLINK_NOT_SUPPORTED 1464 # endif +# ifndef REPARSE_DATA_BUFFER_HEADER_SIZE +# define REPARSE_DATA_BUFFER_HEADER_SIZE \ + offsetof(REPARSE_DATA_BUFFER, GenericReparseBuffer) +# endif +# ifndef IO_REPARSE_TAG_LX_SYMLINK +# define IO_REPARSE_TAG_LX_SYMLINK (0xA000001DL) +# endif # ifndef S_IFIFO # define S_IFIFO _S_IFIFO # endif @@ -77,6 +85,24 @@ typedef struct { double mtime; } utime_check_t; +#ifdef _WIN32 +# ifndef REPARSE_DATA_BUFFER +typedef struct _REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + ULONG Version; + UCHAR PathBuffer[1]; + } LinuxSymbolicLinkReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + } DUMMYUNIONNAME; +} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER; +# endif +#endif static int dummy_cb_count; static int close_cb_count; @@ -2694,6 +2720,80 @@ TEST_FS_IMPL(fs_non_symlink_reparse_point) { return 0; } +TEST_FS_IMPL(fs_readlink_lx_symlink) { + uv_fs_t req; + int r; + HANDLE file_handle; + REPARSE_DATA_BUFFER* reparse_buffer; + DWORD bytes_returned; + const char* target_path = "target_file"; + size_t target_len = strlen(target_path); + size_t buffer_size; + + /* set-up */ + unlink("test_dir/lx_symlink"); + rmdir("test_dir"); + + loop = uv_default_loop(); + + uv_fs_mkdir(NULL, &req, "test_dir", 0777, NULL); + uv_fs_req_cleanup(&req); + + file_handle = CreateFile("test_dir/lx_symlink", + GENERIC_WRITE | FILE_WRITE_ATTRIBUTES, + 0, + NULL, + CREATE_ALWAYS, + FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + ASSERT_PTR_NE(file_handle, INVALID_HANDLE_VALUE); + + /* Allocate buffer for reparse data */ + buffer_size = REPARSE_DATA_BUFFER_HEADER_SIZE + + sizeof(ULONG) + /* Version field */ + target_len; + reparse_buffer = malloc(buffer_size); + ASSERT_NOT_NULL(reparse_buffer); + + /* Set up Linux symlink reparse buffer */ + memset(reparse_buffer, 0, buffer_size); + reparse_buffer->ReparseTag = IO_REPARSE_TAG_LX_SYMLINK; + reparse_buffer->ReparseDataLength = sizeof(ULONG) + target_len; + reparse_buffer->Reserved = 0; + reparse_buffer->LinuxSymbolicLinkReparseBuffer.Version = 2; + memcpy(reparse_buffer->LinuxSymbolicLinkReparseBuffer.PathBuffer, + target_path, + target_len); + + r = DeviceIoControl(file_handle, + FSCTL_SET_REPARSE_POINT, + reparse_buffer, + buffer_size, + NULL, + 0, + &bytes_returned, + NULL); + ASSERT(r); + + CloseHandle(file_handle); + + /* Test that readlink works on the Linux symlink */ + r = uv_fs_readlink(NULL, &req, "test_dir/lx_symlink", NULL); + ASSERT_OK(r); + ASSERT_NOT_NULL(req.ptr); + ASSERT_OK(strcmp(req.ptr, target_path)); + uv_fs_req_cleanup(&req); + + /* clean-up */ + free(reparse_buffer); + unlink("test_dir/lx_symlink"); + rmdir("test_dir"); + + MAKE_VALGRIND_HAPPY(loop); + return 0; +} + TEST_FS_IMPL(fs_lstat_windows_store_apps) { uv_loop_t* loop; char localappdata[MAX_PATH]; diff --git a/test/test-list.h b/test/test-list.h index f1dd7ef51..e28de37d5 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -385,6 +385,7 @@ TEST_FS_DECLARE (fs_symlink_dir) #ifdef _WIN32 TEST_FS_DECLARE (fs_symlink_junction) TEST_FS_DECLARE (fs_non_symlink_reparse_point) +TEST_FS_DECLARE (fs_readlink_lx_symlink) TEST_FS_DECLARE (fs_lstat_windows_store_apps) TEST_FS_DECLARE (fs_open_flags) #endif @@ -1113,6 +1114,7 @@ TASK_LIST_START #ifdef _WIN32 TEST_FS_ENTRY (fs_symlink_junction) TEST_FS_ENTRY (fs_non_symlink_reparse_point) + TEST_FS_ENTRY (fs_readlink_lx_symlink) TEST_FS_ENTRY (fs_lstat_windows_store_apps) TEST_FS_ENTRY (fs_open_flags) #endif