[PATCH v2 0/2] MR9595: cmd: Reparse support.
-- v2: cmd: Implement mklink /j. https://gitlab.winehq.org/wine/wine/-/merge_requests/9595
From: Elizabeth Figura <zfigura(a)codeweavers.com> Based on patches by Erich E. Hoover. --- programs/cmd/Makefile.in | 2 +- programs/cmd/directory.c | 75 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/programs/cmd/Makefile.in b/programs/cmd/Makefile.in index cca40b84011..cc913733c8b 100644 --- a/programs/cmd/Makefile.in +++ b/programs/cmd/Makefile.in @@ -1,5 +1,5 @@ MODULE = cmd.exe -IMPORTS = shell32 user32 advapi32 +IMPORTS = shell32 user32 advapi32 kernelbase EXTRADLLFLAGS = -mconsole -municode diff --git a/programs/cmd/directory.c b/programs/cmd/directory.c index 985a9899cec..e95d57e9d19 100644 --- a/programs/cmd/directory.c +++ b/programs/cmd/directory.c @@ -22,6 +22,10 @@ #define WIN32_LEAN_AND_MEAN #include "wcmd.h" +#define WIN32_NO_STATUS +#include <pathcch.h> +#include <winioctl.h> +#include <ddk/ntifs.h> #include "wine/debug.h" WINE_DEFAULT_DEBUG_CHANNEL(cmd); @@ -232,6 +236,56 @@ static void WCMD_getfileowner(WCHAR *filename, WCHAR *owner, int ownerlen) { return; } +static void output_reparse_target(const WCHAR *dir, const WCHAR *filename) +{ + REPARSE_DATA_BUFFER *data; + HANDLE file; + WCHAR *path; + DWORD size; + BOOL ret; + + PathAllocCombine(dir, filename, PATHCCH_ALLOW_LONG_PATHS, &path); + file = CreateFileW(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (file == INVALID_HANDLE_VALUE) + { + WARN("failed to open %s, error %lu\n", debugstr_w(path), GetLastError()); + LocalFree(path); + return; + } + + data = malloc(1024); + ret = DeviceIoControl(file, FSCTL_GET_REPARSE_POINT, NULL, 0, data, 1024, &size, NULL); + if (!ret && GetLastError() == ERROR_MORE_DATA) + { + size = sizeof(REPARSE_DATA_BUFFER) + data->ReparseDataLength; + data = realloc(data, size); + ret = DeviceIoControl(file, FSCTL_GET_REPARSE_POINT, NULL, 0, data, size, &size, NULL); + } + + if (ret) + { + if (data->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { + size_t offset = data->MountPointReparseBuffer.PrintNameOffset / sizeof(WCHAR); + WCMD_output(L" [%1]", data->MountPointReparseBuffer.PathBuffer + offset); + } + else if (data->ReparseTag == IO_REPARSE_TAG_SYMLINK) + { + size_t offset = data->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(WCHAR); + WCMD_output(L" [%1]", data->SymbolicLinkReparseBuffer.PathBuffer + offset); + } + } + else + { + WARN("failed to get reparse point from %s, error %lu\n", debugstr_w(path), GetLastError()); + } + + free(data); + LocalFree(path); + CloseHandle(file); +} + /***************************************************************************** * WCMD_list_directory * @@ -404,10 +458,19 @@ static RETURN_CODE WCMD_list_directory (DIRECTORY_STACK *inputparms, int level, dir_count++; if (!bare) { - WCMD_output (L"%1 %2 <DIR> ", datestring, timestring); + if ((fd[i].dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + && (fd[i].dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT)) + WCMD_output(L"%1 %2 <JUNCTION> ", datestring, timestring); + else if ((fd[i].dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + && (fd[i].dwReserved0 == IO_REPARSE_TAG_SYMLINK)) + WCMD_output(L"%1 %2 <SYMLINKD> ", datestring, timestring); + else + WCMD_output(L"%1 %2 <DIR> ", datestring, timestring); if (shortname) WCMD_output(L"%1!-13s!", fd[i].cAlternateFileName); if (usernames) WCMD_output(L"%1!-23s!", username); WCMD_output(L"%1",fd[i].cFileName); + if (fd[i].dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + output_reparse_target(inputparms->dirName, fd[i].cFileName); } else { if (!((lstrcmpW(fd[i].cFileName, L".") == 0) || (lstrcmpW(fd[i].cFileName, L"..") == 0))) { @@ -423,11 +486,17 @@ static RETURN_CODE WCMD_list_directory (DIRECTORY_STACK *inputparms, int level, file_size.u.HighPart = fd[i].nFileSizeHigh; byte_count.QuadPart += file_size.QuadPart; if (!bare) { - WCMD_output (L"%1 %2 %3!14s! ", datestring, timestring, - WCMD_filesize64(file_size.QuadPart)); + if ((fd[i].dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + && (fd[i].dwReserved0 == IO_REPARSE_TAG_SYMLINK)) + WCMD_output(L"%1 %2 <SYMLINK> ", datestring, timestring); + else + WCMD_output(L"%1 %2 %3!14s! ", datestring, timestring, + WCMD_filesize64(file_size.QuadPart)); if (shortname) WCMD_output(L"%1!-13s!", fd[i].cAlternateFileName); if (usernames) WCMD_output(L"%1!-23s!", username); WCMD_output(L"%1",fd[i].cFileName); + if (fd[i].dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + output_reparse_target(inputparms->dirName, fd[i].cFileName); } else { WCMD_output(L"%1%2", recurse ? inputparms->dirName : L"", fd[i].cFileName); } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9595
From: Elizabeth Figura <zfigura(a)codeweavers.com> --- programs/cmd/builtins.c | 61 +++++++++++++++++++++++- programs/cmd/tests/test_builtins.bat | 3 ++ programs/cmd/tests/test_builtins.bat.exp | 2 + programs/cmd/tests/test_builtins.cmd | 3 ++ programs/cmd/tests/test_builtins.cmd.exp | 2 + 5 files changed, 70 insertions(+), 1 deletion(-) diff --git a/programs/cmd/builtins.c b/programs/cmd/builtins.c index aebd1136563..5d177851a6f 100644 --- a/programs/cmd/builtins.c +++ b/programs/cmd/builtins.c @@ -30,6 +30,10 @@ #include "wcmd.h" #include <shellapi.h> +#define WIN32_NO_STATUS +#include "winternl.h" +#include "winioctl.h" +#include "ddk/ntifs.h" #include "wine/debug.h" WINE_DEFAULT_DEBUG_CHANNEL(cmd); @@ -3961,6 +3965,61 @@ RETURN_CODE WCMD_color(void) return errorlevel = return_code; } +/* We cannot use SetVolumeMountPoint(), because that function forbids setting + * arbitrary directories as mount points, whereas mklink /j allows it. */ +BOOL create_mount_point(const WCHAR *link, const WCHAR *target) { + char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + REPARSE_DATA_BUFFER *data = (void *)buffer; + UNICODE_STRING nt_link, nt_target; + OBJECT_ATTRIBUTES attr; + IO_STATUS_BLOCK io; + NTSTATUS status; + HANDLE file; + DWORD size; + BOOL ret; + + TRACE( "link %s, target %s\n", debugstr_w(link), debugstr_w(target) ); + + status = RtlDosPathNameToNtPathName_U_WithStatus( link, &nt_link, NULL, NULL ); + if (status) return FALSE; + + status = RtlDosPathNameToNtPathName_U_WithStatus( target, &nt_target, NULL, NULL ); + if (status) + { + RtlFreeUnicodeString( &nt_link ); + return FALSE; + } + + size = offsetof( REPARSE_DATA_BUFFER, MountPointReparseBuffer.PathBuffer ); + size += (nt_target.Length + sizeof(WCHAR)) * 2; + if (size > sizeof(buffer)) + return FALSE; + + data->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + data->ReparseDataLength = size - offsetof( REPARSE_DATA_BUFFER, MountPointReparseBuffer ); + data->Reserved = 0; + data->MountPointReparseBuffer.SubstituteNameOffset = 0; + data->MountPointReparseBuffer.SubstituteNameLength = nt_target.Length; + data->MountPointReparseBuffer.PrintNameOffset = nt_target.Length + sizeof(WCHAR); + data->MountPointReparseBuffer.PrintNameLength = nt_target.Length; + memcpy( data->MountPointReparseBuffer.PathBuffer, + nt_target.Buffer, nt_target.Length + sizeof(WCHAR) ); + memcpy( data->MountPointReparseBuffer.PathBuffer + (nt_target.Length / sizeof(WCHAR)) + 1, + nt_target.Buffer, nt_target.Length + sizeof(WCHAR) ); + RtlFreeUnicodeString( &nt_target ); + + InitializeObjectAttributes( &attr, &nt_link, OBJ_CASE_INSENSITIVE, 0, NULL ); + status = NtCreateFile( &file, GENERIC_WRITE, &attr, &io, NULL, 0, 0, FILE_CREATE, + FILE_OPEN_REPARSE_POINT | FILE_DIRECTORY_FILE, NULL, 0 ); + RtlFreeUnicodeString( &nt_link ); + if (status) + return FALSE; + + ret = DeviceIoControl( file, FSCTL_SET_REPARSE_POINT, data, size, NULL, 0, &size, NULL ); + CloseHandle( file ); + return ret; +} + /**************************************************************************** * WCMD_mklink */ @@ -4011,7 +4070,7 @@ RETURN_CODE WCMD_mklink(WCHAR *args) else if(!junction) ret = CreateSymbolicLinkW(file1, file2, isdir); else - TRACE("Junction links currently not supported.\n"); + ret = create_mount_point(file1, file2); } if (ret) return errorlevel = NO_ERROR; diff --git a/programs/cmd/tests/test_builtins.bat b/programs/cmd/tests/test_builtins.bat index e36cc742053..1bd3de55292 100644 --- a/programs/cmd/tests/test_builtins.bat +++ b/programs/cmd/tests/test_builtins.bat @@ -236,6 +236,9 @@ call :setError 666 & (mklink &&echo SUCCESS !errorlevel!||echo FAILURE !errorlev call :setError 666 & (mklink /h foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /h foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /z foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +call :setError 666 & (mklink /j foo foo >NUL &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +call :setError 666 & (mklink /j foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +rmdir foo echo bar > foo call :setError 666 & (mklink /h foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /h bar foo >NUL &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) diff --git a/programs/cmd/tests/test_builtins.bat.exp b/programs/cmd/tests/test_builtins.bat.exp index 220ba55c857..397b720c3bf 100644 --- a/programs/cmd/tests/test_builtins.bat.exp +++ b/programs/cmd/tests/test_builtins.bat.exp @@ -149,6 +149,8 @@ FAILURE 1 FAILURE 1 FAILURE 1 FAILURE 1 +SUCCESS 0 +FAILURE 1 FAILURE 1 SUCCESS 0 FAILURE 1 diff --git a/programs/cmd/tests/test_builtins.cmd b/programs/cmd/tests/test_builtins.cmd index a4134fe32c1..36b36699772 100644 --- a/programs/cmd/tests/test_builtins.cmd +++ b/programs/cmd/tests/test_builtins.cmd @@ -819,6 +819,9 @@ call :setError 666 & (mklink &&echo SUCCESS !errorlevel!||echo FAILURE !errorlev call :setError 666 & (mklink /h foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /h foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /z foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +call :setError 666 & (mklink /j foo foo >NUL &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +call :setError 666 & (mklink /j foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) +rmdir foo echo bar > foo call :setError 666 & (mklink /h foo foo &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) call :setError 666 & (mklink /h bar foo >NUL &&echo SUCCESS !errorlevel!||echo FAILURE !errorlevel!) diff --git a/programs/cmd/tests/test_builtins.cmd.exp b/programs/cmd/tests/test_builtins.cmd.exp index 5409c13f662..d169066a167 100644 --- a/programs/cmd/tests/test_builtins.cmd.exp +++ b/programs/cmd/tests/test_builtins.cmd.exp @@ -668,6 +668,8 @@ FAILURE 1 FAILURE 1 FAILURE 1 FAILURE 1 +SUCCESS 0 +FAILURE 1 FAILURE 1 SUCCESS 0 FAILURE 1 -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/9595
I'm afraid this shall require some tests, esp. for the various return codes in case of failures (setting aside DIR output)
Added. I don't think there's a meaningful error here other than the file already existing. (Which, to be fair, was an error in my original patch. I've changed FILE_OPEN_IF to FILE_CREATE.) mklink /j, unlike the other variants, doesn't validate the destination at all.
also, native cmd doesn't handle path:s longer than MAX_PATH characters, so this shall be detected and failed upon
According to my testing it does here, actually. I've changed the NtDeviceIoControlFile() to DeviceIoControl().
This could also use a MAXIMUM_REPARSE_DATA_BUFFER_SIZE buffer.
Doh, yet again. Fixed. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9595#note_124637
Added. I don't think there's a meaningful error here other than the file already existing. (Which, to be fair, was an error in my original patch. I've changed FILE_OPEN_IF to FILE_CREATE.) mklink /j, unlike the other variants, doesn't validate the destination at all.
thanks for the update... looks good (yes the non existing target is the one I had in mind)
also, native cmd doesn't handle path:s longer than MAX_PATH characters, so this shall be detected and failed upon
According to my testing it does here, actually.
retested with: [mklink-cmd.patch](/uploads/fc70ee6d2b13278afe842605c9622590/mklink-cmd.patch) here native (Win10) fails when target is larger than MAX_PATH (didn't try to check that the very value of the limit though) (and your MR crashes because of a buffer overflow when setting file2) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9595#note_124676
On Thu Dec 4 10:04:17 2025 +0000, eric pouech wrote:
Added. I don't think there's a meaningful error here other than the file already existing. (Which, to be fair, was an error in my original patch. I've changed FILE_OPEN_IF to FILE_CREATE.) mklink /j, unlike the other variants, doesn't validate the destination at all. thanks for the update... looks good (yes the non existing target is the one I had in mind)
also, native cmd doesn't handle path:s longer than MAX_PATH characters, so this shall be detected and failed upon
According to my testing it does here, actually. retested with: [mklink-cmd.patch](/uploads/fc70ee6d2b13278afe842605c9622590/mklink-cmd.patch) here native (Win10) fails when target is larger than MAX_PATH (didn't try to check that the very value of the limit though) (and your MR crashes because of a buffer overflow when setting file2) replying to and correcting myself
updated version of the test (looks like long target in first version of the test was failing for other reasons) [mklink-cmd.patch](/uploads/6fc5eadd257bb5fb23121a2c42907806/mklink-cmd.patch) what seems to happen: * native accepts and supports target path larger than MAX_PATH (that's likely what you were referring to) * but the absolute path of the created entry by mklink shall not be larger than MAX_PATH (and in fact even lower than that, cf the 'cd..' in tests, didn't investigate further (\*)). IMO we want to ensure that if 'mklink /j whatever foobar' succeeds, then 'rmdir whatever' will also succeed (and not fail because of a too long path) * added also a test for checking that even with a target path longer than MAX_PATH 'cd' to that target works (here the absolute destination path is smaller than MAX_PATH, I would have liked to test also a larger one, didn't went further) (\*) one hyp would be that since target length is longer than MAX_PATH, native moves from DOS paths to NT paths still keeping the MAX_PATH limit (but that's just wild thinking) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/9595#note_124678
participants (3)
-
Elizabeth Figura -
Elizabeth Figura (@zfigura) -
eric pouech (@epo)