diff --git a/contrib/win32/win32compat/fileio.c b/contrib/win32/win32compat/fileio.c index 92f0a65c3..43c533bcf 100644 --- a/contrib/win32/win32compat/fileio.c +++ b/contrib/win32/win32compat/fileio.c @@ -87,6 +87,7 @@ int errno_from_Win32Error(int win32_error) { switch (win32_error) { + case ERROR_PRIVILEGE_NOT_HELD: case ERROR_ACCESS_DENIED: return EACCES; case ERROR_OUTOFMEMORY: @@ -752,124 +753,12 @@ fileio_fstat(struct w32_io* pio, struct _stat64 *buf) return _fstat64(fd, buf); } -wchar_t * -fileio_readlink_internal(wchar_t * wpath) -{ - /* note: there are two approaches for resolving a symlink in Windows: - * - * 1) Use CreateFile() to obtain a file handle to the reparse point and - * send using the DeviceIoControl() call to retrieve the link data from the - * reparse point. - * 2) Use CreateFile() to obtain a file handle to the target file followed - * by a call to GetFinalPathNameByHandle() to get the real path on the - * file system. - * - * This approach uses the first method because the second method does not - * work on broken link since the target file cannot be opened. It also - * requires additional I/O to read both the symlink and its target. - */ - - - /* abbreviated REPARSE_DATA_BUFFER data structure for decoding symlinks; - * the full definition can be found in ntifs.h within the Windows DDK. - * we include it here so the DDK does not become prereq to the build. - * for more info: https://msdn.microsoft.com/en-us/library/cc232006.aspx - */ - typedef struct _REPARSE_DATA_BUFFER_SYMLINK { - ULONG ReparseTag; - USHORT ReparseDataLength; - USHORT Reserved; - USHORT SubstituteNameOffset; - USHORT SubstituteNameLength; - USHORT PrintNameOffset; - USHORT PrintNameLength; - ULONG Flags; - WCHAR PathBuffer[1]; - } REPARSE_DATA_BUFFER_SYMLINK, *PREPARSE_DATA_BUFFER_SYMLINK; - - /* early declarations for cleanup */ - wchar_t *linkpath = NULL; - HANDLE handle = INVALID_HANDLE_VALUE; - PREPARSE_DATA_BUFFER_SYMLINK reparse_buffer = NULL; - - /* obtain a handle to send to deviceioctl */ - handle = CreateFileW(wpath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0); - if (handle == INVALID_HANDLE_VALUE) { - errno = errno_from_Win32LastError(); - goto cleanup; - } - - /* send a request to the file system to get the real path */ - reparse_buffer = (PREPARSE_DATA_BUFFER_SYMLINK) malloc(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); - DWORD dwBytesReturned = 0; - if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, - (LPVOID)reparse_buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dwBytesReturned, 0) == 0) { - errno = errno_from_Win32LastError(); - goto cleanup; - } - - /* ensure file is actually symlink */ - if (reparse_buffer->ReparseTag != IO_REPARSE_TAG_SYMLINK) { - errno = ENOLINK; - goto cleanup; - } - - /* the symlink structure has a 'Print Name' value that is displayed to the - * user which is different from the actual value it uses for redirection - * called the 'Substitute Name'; since the Substitute Name has an odd format - * that begins with \??\ and it appears that CreateSymbolicLink() always - * formats the PrintName value consistently we will just use that - */ - int symlink_nonnull_size = reparse_buffer->PrintNameLength; - wchar_t * symlink_nonnull = &reparse_buffer->PathBuffer[reparse_buffer->PrintNameOffset / sizeof(WCHAR)]; - - /* since readlink allows a return string larger than MAX_PATH by specifying - * the bufsiz parameter and windows can have paths larger than MAX_PATH, - * dynamically allocate a string to hold the resultant symbolic link path. - * this string could be as large as parent path plus the reparse buffer - * data plus a null terminator. - */ - const int wpath_len = (int)wcslen(wpath); - int linkpath_len = wpath_len + symlink_nonnull_size / sizeof(wchar_t) + 1; - linkpath = calloc(linkpath_len, sizeof(wchar_t)); - if (linkpath == NULL) { - errno = ENOMEM; - goto cleanup; - } - - /* if symlink is relative, copy the truncated parent as the base path*/ - if (reparse_buffer->Flags != 0) { - - /* copy the parent path, convert forward slashes to backslashes, and - * trim off the last entry in the path */ - wcscpy_s(linkpath, linkpath_len, wpath); - convertToBackslashW(linkpath); - for (int i = wpath_len; i >= 0; i--) { - if (linkpath[i] == L'\\') { - linkpath[i+1] = L'\0'; - break; - } - } - } - - /* append the symbolic link data to the output string*/ - wcsncat_s(linkpath, linkpath_len, symlink_nonnull, symlink_nonnull_size / sizeof(wchar_t)); - -cleanup: - - if (reparse_buffer) - free(reparse_buffer); - if (handle != INVALID_HANDLE_VALUE) - CloseHandle(handle); - return linkpath; -} - int fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lstat) { wchar_t *wpath = NULL; - wchar_t *resolved_link = NULL; + char link_test = L'\0'; + HANDLE link_handle = INVALID_HANDLE_VALUE; WIN32_FILE_ATTRIBUTE_DATA attributes = { 0 }; int ret = -1; int is_link = 0; @@ -897,15 +786,29 @@ fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lsta /* try to see if it is a symlink */ is_link = (attributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && - (resolved_link = fileio_readlink_internal(wpath)) != NULL); + fileio_readlink(path, &link_test, 1) == 1); /* if doing a stat() on a link, then lookup attributes on the target of the link */ if (!do_lstat && is_link) { - is_link = 0; - if (GetFileAttributesExW(resolved_link, GetFileExInfoStandard, &attributes) == FALSE) { + + /* obtain a file handle to the destination file (not the source link) */ + BY_HANDLE_FILE_INFORMATION link_attributes; + if ((link_handle = CreateFileW(wpath, 0, 0, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL)) == INVALID_HANDLE_VALUE + || GetFileInformationByHandle(link_handle, &link_attributes) == 0) + { errno = errno_from_Win32LastError(); goto cleanup; } + + /* copy attributes from handle structure to normal structure */ + attributes.ftCreationTime = link_attributes.ftCreationTime; + attributes.ftLastAccessTime = link_attributes.ftLastAccessTime; + attributes.ftLastWriteTime = link_attributes.ftLastWriteTime; + attributes.nFileSizeHigh = link_attributes.nFileSizeHigh; + attributes.nFileSizeLow = link_attributes.nFileSizeLow; + attributes.dwFileAttributes = link_attributes.dwFileAttributes; + is_link = 0; } buf->st_ino = 0; /* Has no meaning in the FAT, HPFS, or NTFS file systems*/ @@ -931,8 +834,8 @@ fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lsta ret = 0; cleanup: - if (resolved_link) - free(resolved_link); + if (link_handle != INVALID_HANDLE_VALUE) + CloseHandle(link_handle); if (wpath) free(wpath); return ret; @@ -1085,19 +988,54 @@ fileio_is_io_available(struct w32_io* pio, BOOL rd) } } -ssize_t +ssize_t fileio_readlink(const char *path, char *buf, size_t bufsiz) { + /* note: there are two approaches for resolving a symlink in Windows: + * + * 1) Use CreateFile() to obtain a file handle to the reparse point and + * send using the DeviceIoControl() call to retrieve the link data from the + * reparse point. + * 2) Use CreateFile() to obtain a file handle to the target file followed + * by a call to GetFinalPathNameByHandle() to get the real path on the + * file system. + * + * This approach uses the first method because the second method does not + * work on broken link since the target file cannot be opened. It also + * requires additional I/O to read both the symlink and its target. + */ + + /* abbreviated REPARSE_DATA_BUFFER data structure for decoding symlinks; + * the full definition can be found in ntifs.h within the Windows DDK. + * we include it here so the DDK does not become prereq to the build. + * for more info: https://msdn.microsoft.com/en-us/library/cc232006.aspx + */ + debug4("readlink - io:%p", pio); - wchar_t *wpath = NULL; - wchar_t *resolved_link = NULL; - char* output = NULL; + typedef struct _REPARSE_DATA_BUFFER_SYMLINK { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } REPARSE_DATA_BUFFER_SYMLINK, *PREPARSE_DATA_BUFFER_SYMLINK; + + /* early declarations for cleanup */ ssize_t ret = -1; + wchar_t *wpath = NULL; + wchar_t *linkpath = NULL; + char *output = NULL; + HANDLE handle = INVALID_HANDLE_VALUE; + PREPARSE_DATA_BUFFER_SYMLINK reparse_buffer = NULL; /* sanity check */ - if (buf == NULL) { - errno = EFAULT; + if (path == NULL || buf == NULL || bufsiz == 0) { + errno = EINVAL; goto cleanup; } @@ -1106,39 +1044,161 @@ fileio_readlink(const char *path, char *buf, size_t bufsiz) goto cleanup; } - /* read the link data from the passed symlink */ - resolved_link = fileio_readlink_internal(wpath); - if (resolved_link == NULL) { - /* errorno will have been set by called function */ + /* obtain a handle to send to deviceioctl */ + handle = CreateFileW(wpath, 0, 0, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0); + if (handle == INVALID_HANDLE_VALUE) { + errno = errno_from_Win32LastError(); goto cleanup; } - if ((output = utf16_to_utf8(resolved_link)) == NULL) { + /* send a request to the file system to get the real path */ + reparse_buffer = (PREPARSE_DATA_BUFFER_SYMLINK) malloc(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + DWORD dwBytesReturned = 0; + if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, + (LPVOID) reparse_buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dwBytesReturned, 0) == 0) { + errno = errno_from_Win32LastError(); + goto cleanup; + } + + /* ensure file is actually symlink */ + if (reparse_buffer->ReparseTag != IO_REPARSE_TAG_SYMLINK) { + errno = EINVAL; + goto cleanup; + } + + /* the symlink structure has a 'Print Name' value that is displayed to the + * user which is different from the actual value it uses for redirection + * called the 'Substitute Name'; since the Substitute Name has an odd format + * that begins with \??\ and it appears that CreateSymbolicLink() always + * formats the PrintName value consistently we will just use that + */ + int symlink_nonnull_size = reparse_buffer->PrintNameLength; + wchar_t * symlink_nonnull = &reparse_buffer->PathBuffer[reparse_buffer->PrintNameOffset / sizeof(WCHAR)]; + + /* allocate area to hold a null terminated version of the string */ + if ((linkpath = malloc(symlink_nonnull_size + sizeof(wchar_t))) == NULL) { + goto cleanup; + } + + /* copy the data out of the reparse buffer and add null terminator */ + memcpy_s(linkpath, symlink_nonnull_size + sizeof(wchar_t), symlink_nonnull, symlink_nonnull_size); + linkpath[symlink_nonnull_size / sizeof(wchar_t)] = L'\0'; + + /* convert link path to utf8 */ + if ((output = utf16_to_utf8(linkpath)) == NULL) { errno = ENOMEM; goto cleanup; } - /* ensure output buffer is large enough forward slash and the string */ - ssize_t out_size = (ssize_t) strlen(output); - if (1 + out_size > bufsiz) { - errno = ENAMETOOLONG; - goto cleanup; - } + /* determine if we need to prepend a forward slash to make this look like + * an absolute path C:\Path\Target --> /C:/Path/Target + */ + int abs_chars = is_absolute_path(output) ? 1 : 0; + if (abs_chars) + buf[0] = '/'; - /* copy to output buffer in the forward-slash format: /C:/Path/Target */ + /* copy link data to output buffer; per specification, truncation is okay */ convertToForwardslash(output); - buf[0] = '/'; - memcpy(buf + 1, output, out_size); - ret = out_size + 1; + size_t out_size = strlen(output); + memcpy(buf + abs_chars, output, min(out_size, bufsiz - abs_chars)); + ret = (ssize_t) min(out_size + abs_chars, bufsiz); cleanup: + if (linkpath) + free(linkpath); + if (reparse_buffer) + free(reparse_buffer); + if (handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); if (wpath) free(wpath); if (output) free(output); - if (resolved_link) - free(resolved_link); - return (ssize_t) ret; -} \ No newline at end of file + return (ssize_t)ret; +} + +int +fileio_symlink(const char *target, const char *linkpath) +{ + if (target == NULL || linkpath == NULL) { + errno = EFAULT; + return -1; + } + + DWORD ret = 0; + wchar_t *target_utf16 = utf8_to_utf16(resolved_path(target)); + wchar_t *linkpath_utf16 = utf8_to_utf16(resolved_path(linkpath)); + wchar_t *resolved_utf16 = _wcsdup(target_utf16); + if (target_utf16 == NULL || linkpath_utf16 == NULL || resolved_utf16 == NULL) { + errno = ENOMEM; + ret = -1; + goto cleanup; + } + + /* Relative targets are relative to the link and not our current directory + * so attempt to calculate a resolvable path by removing the link file name + * leaving only the parent path and then append the relative link: + * C:\Path\Link with Link->SubDir\Target to C:\Path\SubDir\Target + */ + if (!is_absolute_path(target)) { + + /* allocate area to hold the total possible path */ + free(resolved_utf16); + size_t resolved_len = (wcslen(target_utf16) + wcslen(linkpath_utf16) + 1); + resolved_utf16 = malloc(resolved_len * sizeof(wchar_t)); + if (resolved_utf16 == NULL) { + errno = ENOMEM; + ret = -1; + goto cleanup; + } + + /* copy the relative target to the end of the link's parent */ + wcscpy_s(resolved_utf16, resolved_len, linkpath_utf16); + convertToBackslashW(resolved_utf16); + wchar_t * ptr = wcsrchr(resolved_utf16, L'\\'); + if (ptr == NULL) wcscpy_s(resolved_utf16, resolved_len, target_utf16); + else wcscpy_s(ptr + 1, resolved_len - (ptr + 1 - resolved_utf16), target_utf16); + } + + /* unlike other platforms, we need to know whether the symbolic link target is + * a file or a directory. the only way we can confidently do this is to + * get the attributes of the target. therefore, our symlink() has the + * limitation of only creating symlink with valid targets + */ + WIN32_FILE_ATTRIBUTE_DATA attributes = { 0 }; + if (GetFileAttributesExW(resolved_utf16, GetFileExInfoStandard, &attributes) == FALSE) { + errno = errno_from_Win32LastError(); + ret = -1; + goto cleanup; + } + + /* use the attribute of the file to determine the proper flag to send */ + DWORD create_flags = (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? + SYMBOLIC_LINK_FLAG_DIRECTORY : 0; + + /* symlink creation on earlier versions of windows were a privileged op + * and then an option was added to create symlink using from an unprivileged + * context so we try both operations, attempting privileged version first. + * note: 0x2 = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + */ + if (CreateSymbolicLinkW(linkpath_utf16, target_utf16, create_flags) == 0) { + if (CreateSymbolicLinkW(linkpath_utf16, target_utf16, create_flags | 0x2) == 0) { + errno = errno_from_Win32LastError(); + ret = -1; + goto cleanup; + } + } + +cleanup: + + if (target_utf16) + free(target_utf16); + if (linkpath_utf16) + free(linkpath_utf16); + if (resolved_utf16) + free(resolved_utf16); + return ret; +} diff --git a/contrib/win32/win32compat/inc/stdio.h b/contrib/win32/win32compat/inc/stdio.h index 1702b3589..269ece845 100644 --- a/contrib/win32/win32compat/inc/stdio.h +++ b/contrib/win32/win32compat/inc/stdio.h @@ -20,5 +20,3 @@ FILE* w32_fdopen(int fd, const char *mode); int w32_rename(const char *old_name, const char *new_name); #define rename w32_rename - -int is_absolute_path(char *); \ No newline at end of file diff --git a/contrib/win32/win32compat/misc.c b/contrib/win32/win32compat/misc.c index e64b62688..667e73c7b 100644 --- a/contrib/win32/win32compat/misc.c +++ b/contrib/win32/win32compat/misc.c @@ -644,9 +644,7 @@ w32_utimes(const char *filename, struct timeval *tvp) int w32_symlink(const char *target, const char *linkpath) { - /* Not supported in windows */ - errno = EOPNOTSUPP; - return -1; + return fileio_symlink(target, linkpath); } int @@ -1443,7 +1441,7 @@ get_program_data_path() /* Windows absolute paths - \abc, /abc, c:\abc, c:/abc, __PROGRAMDATA__\openssh\sshd_config */ int -is_absolute_path(char *path) +is_absolute_path(const char *path) { int retVal = 0; if(*path == '\"') /* skip double quote if path is "c:\abc" */ diff --git a/contrib/win32/win32compat/misc_internal.h b/contrib/win32/win32compat/misc_internal.h index f66bfff08..d3d1ef881 100644 --- a/contrib/win32/win32compat/misc_internal.h +++ b/contrib/win32/win32compat/misc_internal.h @@ -41,4 +41,5 @@ char* get_program_data_path(); HANDLE get_user_token(char* user); int load_user_profile(HANDLE user_token, char* user); int copy_file(char *source, char *destination); -int create_directory_withsddl(char *path, char *sddl); \ No newline at end of file +int create_directory_withsddl(char *path, char *sddl); +int is_absolute_path(const char *); \ No newline at end of file diff --git a/contrib/win32/win32compat/w32fd.h b/contrib/win32/win32compat/w32fd.h index d2fdca689..9a5158945 100644 --- a/contrib/win32/win32compat/w32fd.h +++ b/contrib/win32/win32compat/w32fd.h @@ -164,4 +164,5 @@ int fileio_stat(const char *path, struct _stat64 *buf); int fileio_lstat(const char *path, struct _stat64 *buf); long fileio_lseek(struct w32_io* pio, unsigned __int64 offset, int origin); FILE* fileio_fdopen(struct w32_io* pio, const char *mode); -ssize_t fileio_readlink(const char *path, char *buf, size_t bufsiz); \ No newline at end of file +ssize_t fileio_readlink(const char *path, char *buf, size_t bufsiz); +int fileio_symlink(const char *target, const char *linkpath); \ No newline at end of file diff --git a/regress/unittests/win32compat/file_tests.c b/regress/unittests/win32compat/file_tests.c index 59c75c833..50912537a 100644 --- a/regress/unittests/win32compat/file_tests.c +++ b/regress/unittests/win32compat/file_tests.c @@ -1,5 +1,8 @@ /* * Author: Manoj Ampalam +* +* Author: Bryan Berns +* Added tests for symlink(), readlink(), lstat() */ #include "includes.h" @@ -487,6 +490,117 @@ file_miscellaneous_tests() TEST_DONE(); } +void +file_symlink_tests() +{ + /* skip these unit tests if we cannot create symbolic links at all + * note: 0x2 = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE + */ + DeleteFileW(L"admin_check"); + if (CreateSymbolicLinkW(L"admin_check", L"admin_check", 0) == 0 && + CreateSymbolicLinkW(L"admin_check", L"admin_check", 0x2 == 0)) { + return; + } + DeleteFileW(L"admin_check"); + + wchar_t curdir[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, curdir); + + /* perform a variety of symlink tests using unicode, directory targets, + * file targets, absolute/relative links, absolute/relative targets + */ + for (int do_unicode = 0; do_unicode <= 1; do_unicode++) + for (int do_dir = 0; do_dir <= 1; do_dir++) + for (int do_absolute_lnk = 0; do_absolute_lnk <= 1; do_absolute_lnk++) + for (int do_absolute_tgt = 0; do_absolute_tgt <= 1; do_absolute_tgt++) + { + char test_name[128]; + sprintf(test_name, "Symlink: %s link, %s %s target, %s", + (do_absolute_lnk) ? "relative" : "absolute", + (do_absolute_tgt) ? "relative" : "absolute", + (do_dir) ? "directory" : "file", + (do_unicode) ? "unicode" : "ansi"); + TEST_START(test_name); + + /* cleanup / setup basic test structure */ + _wsystem(L"RD /S /Q win32compat-tmp >NUL 2>&1"); + _wsystem(L"MKDIR win32compat-tmp >NUL 2>&1"); + + wchar_t tgt_path[MAX_PATH] = L""; + wchar_t lnk_path[MAX_PATH] = L""; + + /* prepend absolute path if doing absolute test */ + if (do_absolute_tgt) { + wcscat(tgt_path, L"/"); + wcscat(tgt_path, curdir); + wcscat(tgt_path, L"/"); + } + if (do_absolute_lnk) { + wcscat(lnk_path, L"/"); + wcscat(lnk_path, curdir); + wcscat(lnk_path, L"/"); + } + + /* append the test paths */ + wcscat(tgt_path, L"win32compat-tmp/tgt"); + wcscat(lnk_path, L"win32compat-tmp/lnk"); + + /* append unicode char if doing unicode test */ + if (do_unicode) { + wcscat(tgt_path, L"Δ"); + wcscat(lnk_path, L"Δ"); + } + + /* ensure target is in forward slash format since this is + * required for the readlink test output later * + */ + for (wchar_t * t = tgt_path; *t; t++) if (*t == '\\') *t = L'/'; + + /* create directory or file as target --- we have to offset + * the first forward slash so the windows functions operate + */ + if (do_dir) + CreateDirectoryW(&tgt_path[do_absolute_tgt], NULL); + else + CloseHandle(CreateFileW(&tgt_path[do_absolute_tgt], + GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL)); + + /* convert to utf8 for test */ + char * tgt_utf8 = utf16_to_utf8(tgt_path); + char * lnk_utf8 = utf16_to_utf8(lnk_path); + + /* for relative link, the target is relative to the link */ + char * tgt_name_utf8 = tgt_utf8; + if (!do_absolute_tgt) tgt_name_utf8 = strrchr(tgt_utf8, '/') + 1; + + /* create symlink */ + int symlink_ret = symlink(tgt_name_utf8, lnk_utf8); + ASSERT_INT_EQ(symlink_ret, 0); + + /* verify readlink() output against symlink() input */ + char readlink_buf[MAX_PATH] = ""; + int readlink_ret = readlink(lnk_utf8, readlink_buf, MAX_PATH); + ASSERT_INT_EQ(readlink_ret, strlen(tgt_name_utf8)); + ASSERT_INT_EQ(0, memcmp(readlink_buf, tgt_name_utf8, readlink_ret)); + + /* verify lstat() gets the reference to the link */ + struct w32_stat statbuf; + int lstat_ret = lstat(lnk_utf8, &statbuf); + ASSERT_INT_EQ(lstat_ret, 0); + ASSERT_INT_EQ(1, S_ISLNK(statbuf.st_mode)); + + /* verify stat() gets a reference to the dir or file */ + int stat_ret = stat(lnk_utf8, &statbuf); + ASSERT_INT_EQ(stat_ret, 0); + ASSERT_INT_EQ(0, S_ISLNK(statbuf.st_mode)); + ASSERT_INT_EQ(do_dir, S_ISDIR(statbuf.st_mode)); + + TEST_DONE(); + } + + _wsystem(L"RD /S /Q win32compat-tmp >NUL 2>&1"); +} + void file_tests() { @@ -496,4 +610,5 @@ file_tests() file_nonblocking_io_tests(); file_select_tests(); file_miscellaneous_tests(); + file_symlink_tests(); }