From 248f5feb58667223132350f662d9c9cf451836b5 Mon Sep 17 00:00:00 2001 From: Mikko Rasa Date: Mon, 4 Sep 2023 01:44:31 +0300 Subject: [PATCH] Implement symlink support on Windows Also avoid lossage when statting reparse points of the appexeclink type. --- source/core/windows/winapi.h | 2 +- source/fs/stat_private.h | 4 +- source/fs/windows/stat.cpp | 84 ++++++++++++++++++++++++++++-------- source/fs/windows/utils.cpp | 28 +++++++++++- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/source/core/windows/winapi.h b/source/core/windows/winapi.h index dca0880..624de07 100644 --- a/source/core/windows/winapi.h +++ b/source/core/windows/winapi.h @@ -1,7 +1,7 @@ #ifndef MSP_CORE_WINAPI_H_ #define MSP_CORE_WINAPI_H_ -#ifndef WIN32_LEAN_AND_MEAN +#if !defined(WIN32_LEAN_AND_MEAN) && !defined(MSP_FULL_WINAPI) #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX diff --git a/source/fs/stat_private.h b/source/fs/stat_private.h index 23019a5..5806f62 100644 --- a/source/fs/stat_private.h +++ b/source/fs/stat_private.h @@ -16,7 +16,9 @@ struct Stat::Private Private(const Private &); ~Private(); -#ifndef _WIN32 +#ifdef _WIN32 + static Stat sys_stat(const Path &, bool); +#else /* This is here because it needs access to private members of Stat, but we can't expose the system stat struct in the public header */ static Stat from_struct_stat(const struct stat &); diff --git a/source/fs/windows/stat.cpp b/source/fs/windows/stat.cpp index ee79476..58c946d 100644 --- a/source/fs/windows/stat.cpp +++ b/source/fs/windows/stat.cpp @@ -1,3 +1,4 @@ +#define MSP_FULL_WINAPI #include #include #include @@ -37,6 +38,16 @@ string get_account_name(PSID sid) return Msp::format("%s/%s", name, domain); } +DWORD get_reparse_point_tag(HANDLE handle) +{ + char data[1024]; + DWORD size; + if(DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data, sizeof(data), &size, nullptr)) + return *reinterpret_cast(data); + else + return 0; +} + } @@ -56,30 +67,39 @@ Stat::Private::~Private() HeapFree(GetProcessHeap(), 0, group_id); } -void Stat::Private::fill_owner_info(Stat::OwnerInfo &result) -{ - if(owner_id!=INVALID_UID) - result.owner = get_account_name(owner_id); - else - result.owner = "None"; - - if(group_id!=INVALID_GID) - result.group = get_account_name(group_id); - else - result.group = "None"; -} - - -Stat Stat::stat(const Path &path) +Stat Stat::Private::sys_stat(const Path &path, bool follow_symlinks) { HANDLE handle; - handle = CreateFile(path.str().c_str(), READ_CONTROL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS, nullptr); + DWORD reparse_tag = 0; + DWORD flags = FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS; + if(!follow_symlinks) + flags |= FILE_FLAG_OPEN_REPARSE_POINT; + handle = CreateFile(path.str().c_str(), READ_CONTROL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, flags, nullptr); if(handle==INVALID_HANDLE_VALUE) { DWORD err = GetLastError(); if(err==ERROR_FILE_NOT_FOUND || err==ERROR_PATH_NOT_FOUND) return Stat(); - else + else if(err==ERROR_CANT_ACCESS_FILE && follow_symlinks) + { + flags |= FILE_FLAG_OPEN_REPARSE_POINT; + handle = CreateFile(path.str().c_str(), READ_CONTROL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, flags, nullptr); + if(handle!=INVALID_HANDLE_VALUE) + { + reparse_tag = get_reparse_point_tag(handle); + /* App exec links are used for apps installed from the Microsoft + Store, but the apps themselves are stored in a location regular + users aren't permitted to access. Get information for the reparse + point itself even if following symlinks was requested. */ + if(reparse_tag!=IO_REPARSE_TAG_APPEXECLINK) + { + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } + } + } + + if(handle==INVALID_HANDLE_VALUE) throw system_error(format("CreateFile(%s)", path), err); } @@ -95,7 +115,14 @@ Stat Stat::stat(const Path &path) if(info.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) result.type = DIRECTORY; else - result.type = REGULAR; + { + if((info.dwFileAttributes&FILE_ATTRIBUTE_REPARSE_POINT) && !reparse_tag) + reparse_tag = get_reparse_point_tag(handle); + if(reparse_tag==IO_REPARSE_TAG_SYMLINK) + result.type = SYMLINK; + else + result.type = REGULAR; + } result.size = FileSize(info.nFileSizeHigh)<<32 | info.nFileSizeLow; result.alloc_size = (result.size+511)&~511; @@ -123,9 +150,28 @@ Stat Stat::stat(const Path &path) return result; } +void Stat::Private::fill_owner_info(Stat::OwnerInfo &result) +{ + if(owner_id!=INVALID_UID) + result.owner = get_account_name(owner_id); + else + result.owner = "None"; + + if(group_id!=INVALID_GID) + result.group = get_account_name(group_id); + else + result.group = "None"; +} + + +Stat Stat::stat(const Path &path) +{ + return Private::sys_stat(path, true); +} + Stat Stat::lstat(const Path &path) { - return stat(path); + return Private::sys_stat(path, false); } bool exists(const Path &path) diff --git a/source/fs/windows/utils.cpp b/source/fs/windows/utils.cpp index a1c2513..53e21f7 100644 --- a/source/fs/windows/utils.cpp +++ b/source/fs/windows/utils.cpp @@ -1,6 +1,9 @@ +#define MSP_FULL_WINAPI #include #include #include +#include +#include #include "dir.h" #include "utils.h" @@ -11,8 +14,29 @@ namespace FS { Path readlink(const Path &link) { - (void)link; - throw unsupported("readlink"); + DWORD attrs = GetFileAttributes(link.str().c_str()); + if(!(attrs&FILE_ATTRIBUTE_REPARSE_POINT)) + throw system_error("readlink", ERROR_NOT_A_REPARSE_POINT); + + HANDLE handle = CreateFile(link.str().c_str(), READ_CONTROL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if(handle==INVALID_HANDLE_VALUE) + throw system_error("CreateFile"); + + char data[1024]; + DWORD length; + bool success = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data, sizeof(data), &length, nullptr); + CloseHandle(handle); + if(!success) + throw system_error("DeviceIoControl(FSCTL_GET_REPARSE_POINT)"); + + DWORD reparse_tag = *reinterpret_cast(data); + if(reparse_tag!=IO_REPARSE_TAG_SYMLINK) + throw runtime_error("reparse point is not a symlink"); + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b41f1cbf-10df-4a47-98d4-1c52a833d913 + WORD start = 20+*reinterpret_cast(data+8); + WORD end = start+*reinterpret_cast(data+10); + return StringCodec::transcode(string(data+start, data+end)); } Path realpath(const Path &path) -- 2.45.2