From 4e234c52a32d644279425f864be4fa9cd8f9a6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 17 Mar 2026 17:59:24 +0800 Subject: [PATCH] Platform (Windows): adds implementation of POSIX `readlink(2)` and `realpath(3)` --- src/common/impl/FFPlatform_windows.c | 2 +- src/common/impl/path.c | 261 ++++++++++++++++++++++++++- src/common/path.h | 7 + src/detection/editor/editor.c | 8 - src/logo/image/image.c | 21 +-- 5 files changed, 277 insertions(+), 22 deletions(-) diff --git a/src/common/impl/FFPlatform_windows.c b/src/common/impl/FFPlatform_windows.c index 5ca296afe..909a61638 100644 --- a/src/common/impl/FFPlatform_windows.c +++ b/src/common/impl/FFPlatform_windows.c @@ -28,7 +28,7 @@ static void getExePath(FFPlatform* platform) NULL); if (hPath != INVALID_HANDLE_VALUE) { - DWORD len = GetFinalPathNameByHandleW(hPath, exePathW, MAX_PATH, FILE_NAME_OPENED); + DWORD len = GetFinalPathNameByHandleW(hPath, exePathW, MAX_PATH, FILE_NAME_NORMALIZED); if (len > 0 && len < MAX_PATH) { ffStrbufSetNWS(&platform->exePath, len, exePathW); diff --git a/src/common/impl/path.c b/src/common/impl/path.c index 7a960c1ad..2c01d2330 100644 --- a/src/common/impl/path.c +++ b/src/common/impl/path.c @@ -1,6 +1,6 @@ #include "common/path.h" #include "common/io.h" -#include "common/stringUtils.h" +#include "common/arrayUtils.h" #if !_WIN32 const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) @@ -49,6 +49,9 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) } #else #include +#include +#include +#include const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) { @@ -62,4 +65,260 @@ const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) ffStrbufSetS(result, buffer); return NULL; } + +static inline int winerr2Errno(DWORD err) +{ + switch (err) + { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_NAME: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + return EACCES; + case ERROR_BUFFER_OVERFLOW: + case ERROR_INSUFFICIENT_BUFFER: + return ENAMETOOLONG; + case ERROR_INVALID_PARAMETER: + case ERROR_NOT_A_REPARSE_POINT: + return EINVAL; + default: + return EIO; + } +} + +char* frealpath(HANDLE hFile, char* resolved_name) +{ + if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !hFile, false)) + { + errno = EINVAL; + return NULL; + } + + wchar_t resolvedNameW[MAX_PATH + 4]; /* +4 for "\\\\?\\" prefix */ + DWORD lenW = GetFinalPathNameByHandleW(hFile, resolvedNameW, (DWORD)ARRAY_SIZE(resolvedNameW), FILE_NAME_NORMALIZED); + + if (lenW == 0) + { + errno = winerr2Errno(GetLastError()); + return NULL; + } + if (lenW >= ARRAY_SIZE(resolvedNameW)) + { + errno = E2BIG; + return NULL; + } + lenW++; // Include null terminator + + wchar_t* srcW = resolvedNameW; + DWORD srcLenW = lenW; + + if (srcLenW >= 8 && wcsncmp(resolvedNameW, L"\\\\?\\UNC\\", 8) == 0) + { + /* Convert "\\?\UNC\server\share" to "\\server\share" */ + srcW += 6; + srcLenW -= 6; + *srcW = L'\\'; + } + else if (srcLenW >= 4 && wcsncmp(resolvedNameW, L"\\\\?\\", 4) == 0) + { + srcW += 4; + srcLenW -= 4; + } + + if (resolved_name) + { + ULONG outBytes = 0; + if (!NT_SUCCESS(RtlUnicodeToUTF8N(resolved_name, MAX_PATH, &outBytes, srcW, (ULONG)(srcLenW * sizeof(wchar_t))))) + { + errno = E2BIG; + return NULL; + } + + if (outBytes > MAX_PATH) + { + errno = E2BIG; + return NULL; + } + + return resolved_name; + } + else + { + /* UTF-8 worst-case: up to 4 bytes per UTF-16 code unit */ + char tmp[(MAX_PATH + 4) * 4]; + ULONG outBytes = 0; + + if (!NT_SUCCESS(RtlUnicodeToUTF8N(tmp, (ULONG)sizeof(tmp), &outBytes, srcW, (ULONG)(srcLenW * sizeof(wchar_t))))) + { + errno = E2BIG; + return NULL; + } + + resolved_name = (char*)malloc(outBytes); + if (!resolved_name) + { + errno = ENOMEM; + return NULL; + } + + memcpy(resolved_name, tmp, outBytes); + return resolved_name; + } + + return resolved_name; +} + +char* realpath(const char* __restrict file_name, char* __restrict resolved_name) +{ + if (!file_name) + { + errno = EINVAL; + return NULL; + } + + wchar_t fileNameW[MAX_PATH]; + ULONG lenBytes = 0; + + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG)sizeof(fileNameW), &lenBytes, file_name, (ULONG)strlen(file_name) + 1))) + { + errno = EINVAL; + return NULL; + } + + FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW( + fileNameW, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + errno = winerr2Errno(GetLastError()); + return NULL; + } + + return frealpath(hFile, resolved_name); +} + +ssize_t freadlink(HANDLE hFile, char* buf, size_t bufsiz) +{ + if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !buf || bufsiz == 0, false)) + { + errno = EINVAL; + return -1; + } + + alignas(REPARSE_DATA_BUFFER) BYTE reparseBuf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, NULL, 0, reparseBuf, (DWORD) sizeof(reparseBuf), &bytesReturned, NULL)) + { + errno = winerr2Errno(GetLastError()); + return -1; + } + + REPARSE_DATA_BUFFER* rp = (REPARSE_DATA_BUFFER*) reparseBuf; + const wchar_t* targetW = NULL; + USHORT targetBytes = 0; + + if (rp->ReparseTag == IO_REPARSE_TAG_SYMLINK) + { + if (rp->SymbolicLinkReparseBuffer.PrintNameLength > 0) + { + targetW = rp->SymbolicLinkReparseBuffer.PathBuffer + + (rp->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(wchar_t)); + targetBytes = rp->SymbolicLinkReparseBuffer.PrintNameLength; + } + else + { + targetW = rp->SymbolicLinkReparseBuffer.PathBuffer + + (rp->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)); + targetBytes = rp->SymbolicLinkReparseBuffer.SubstituteNameLength; + + if (targetBytes >= 8 && + wcsncmp(targetW, L"\\??\\", 4) == 0) + { + targetW += 4; + targetBytes -= 8; + } + } + } + else if (rp->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { + if (rp->MountPointReparseBuffer.PrintNameLength > 0) + { + targetW = rp->MountPointReparseBuffer.PathBuffer + + (rp->MountPointReparseBuffer.PrintNameOffset / sizeof(wchar_t)); + targetBytes = rp->MountPointReparseBuffer.PrintNameLength; + } + else + { + targetW = rp->MountPointReparseBuffer.PathBuffer + + (rp->MountPointReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)); + targetBytes = rp->MountPointReparseBuffer.SubstituteNameLength; + + if (targetBytes >= 8 && + wcsncmp(targetW, L"\\??\\", 4) == 0) + { + targetW += 4; + targetBytes -= 8; + } + } + } + else + { + errno = EINVAL; + return -1; + } + + ULONG outBytes = 0; + if (!NT_SUCCESS(RtlUnicodeToUTF8N(buf, (ULONG) bufsiz, &outBytes, targetW, targetBytes))) + { + errno = E2BIG; + return -1; + } + + // Not null-terminated + return (ssize_t) outBytes; +} + +ssize_t readlink(const char* path, char* buf, size_t bufsiz) +{ + if (!path || !buf || bufsiz == 0) + { + errno = EINVAL; + return -1; + } + + wchar_t pathW[MAX_PATH]; + ULONG pathWBytes = 0; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), &pathWBytes, path, (ULONG) strlen(path) + 1))) + { + errno = EINVAL; + return -1; + } + + FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW( + pathW, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) + { + errno = winerr2Errno(GetLastError()); + return -1; + } + + return freadlink(hFile, buf, bufsiz); +} #endif diff --git a/src/common/path.h b/src/common/path.h index bec304150..73680034c 100644 --- a/src/common/path.h +++ b/src/common/path.h @@ -13,3 +13,10 @@ static inline bool ffIsAbsolutePath(const char* path) return path[0] == '/'; #endif } + +#if _WIN32 +char* frealpath(void* __restrict hFile, char* __restrict resolved_name /*MAX_PATH*/); +char* realpath(const char* __restrict file_name, char* __restrict resolved_name /*MAX_PATH*/); +ssize_t freadlink(void* hFile, char* buf, size_t bufsiz); +ssize_t readlink(const char* path, char* buf, size_t bufsiz); +#endif diff --git a/src/detection/editor/editor.c b/src/detection/editor/editor.c index 576938d47..9b0fe6070 100644 --- a/src/detection/editor/editor.c +++ b/src/detection/editor/editor.c @@ -7,14 +7,6 @@ #include -#ifdef _WIN32 -static inline char* realpath(const char* restrict file_name, char* restrict resolved_name) -{ - assert(resolved_name != NULL); - return _fullpath(resolved_name, file_name, _MAX_PATH); -} -#endif - static bool extractNvimVersionFromBinary(const char* str, FF_MAYBE_UNUSED uint32_t len, void* userdata) { if (!ffStrStartsWith(str, "NVIM v")) return true; diff --git a/src/logo/image/image.c b/src/logo/image/image.c index 0084144b3..ef7ed7a75 100644 --- a/src/logo/image/image.c +++ b/src/logo/image/image.c @@ -407,17 +407,7 @@ static bool printImageKittyDirect(bool printError) #include #else #include - -static inline char* realpath(const char* restrict file_name, char* restrict resolved_name) -{ - char* result = _fullpath(resolved_name, file_name, _MAX_PATH); - if(result) - { - resolved_name[1] = resolved_name[0]; // Drive Name - resolved_name[0] = '/'; - } - return result; -} +#include "common/path.h" #endif #ifdef FF_HAVE_ZLIB @@ -1002,7 +992,8 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images"); ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX); - if(realpath(instance.config.logo.source.chars, requestData.cacheDir.chars + requestData.cacheDir.length) == NULL) + char* filePath = requestData.cacheDir.chars + requestData.cacheDir.length; + if(realpath(instance.config.logo.source.chars, filePath) == NULL) { //We can safely return here, because if realpath failed, we surely won't be able to read the file ffStrbufDestroy(&requestData.cacheDir); @@ -1010,6 +1001,12 @@ static bool printImageIfExistsSlowPath(FFLogoType type, bool printError) fputs("Logo: Querying realpath of the image source failed\n", stderr); return false; } + + #ifdef _WIN32 + filePath[1] = filePath[0]; // Drive Name + filePath[0] = '/'; + #endif + ffStrbufRecalculateLength(&requestData.cacheDir); ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/'); ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight);