From 4fb57bb39b2ee496a27dc648a301460df6f3d2c3 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Mon, 28 Sep 2026 23:02:34 +0800 Subject: [PATCH] Global (Windows): eliminates `SHGetKnownFolderPath` --- CMakeLists.txt | 1 + src/common/impl/FFPlatform_windows.c | 84 +++---- src/common/windows/folders.c | 237 ++++++++++++++++++ src/common/windows/folders.h | 47 ++++ src/detection/packages/packages_windows.c | 10 +- .../terminalfont/terminalfont_windows.c | 18 +- .../terminalshell/terminalshell_windows.c | 9 +- src/detection/wm/wm_windows.c | 30 ++- 8 files changed, 353 insertions(+), 83 deletions(-) create mode 100644 src/common/windows/folders.c create mode 100644 src/common/windows/folders.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2acd937c2..1a86879fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1098,6 +1098,7 @@ elseif(WIN32) src/common/impl/kmod_windows.c src/common/windows/getline.c src/common/windows/com.c + src/common/windows/folders.c src/common/windows/registry.c src/common/windows/unicode.c src/common/windows/variant.cpp diff --git a/src/common/impl/FFPlatform_windows.c b/src/common/impl/FFPlatform_windows.c index 5f937ca2a..7568664c9 100644 --- a/src/common/impl/FFPlatform_windows.c +++ b/src/common/impl/FFPlatform_windows.c @@ -2,13 +2,13 @@ #include "common/io.h" #include "common/library.h" #include "common/strutil.h" +#include "common/windows/folders.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" #include "common/windows/nt.h" #include #include -#include #include #define SECURITY_WIN32 1 // For secext.h @@ -43,43 +43,27 @@ static void getExePath(FFPlatform* platform) { ffStrbufReplaceAllC(&platform->exePath, '\\', '/'); } -static void getHomeDir(FFPlatform* platform) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, nullptr, &pPath))) { - ffStrbufSetWS(&platform->homeDir, pPath); - ffStrbufReplaceAllC(&platform->homeDir, '\\', '/'); - ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); - } else { - ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); - ffStrbufReplaceAllC(&platform->homeDir, '\\', '/'); - ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); - } - CoTaskMemFree(pPath); -} - static void getCacheDir(FFPlatform* platform) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &pPath))) { - ffStrbufSetWS(&platform->cacheDir, pPath); - ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/'); - ffStrbufEnsureEndsWithC(&platform->cacheDir, '/'); - } else { + if (!ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &platform->cacheDir)) { ffStrbufAppend(&platform->cacheDir, &platform->homeDir); ffStrbufAppendS(&platform->cacheDir, "AppData/Local/"); + return; } - CoTaskMemFree(pPath); + + ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->cacheDir, '/'); } -static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, nullptr, &pPath))) { - FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath); - CoTaskMemFree(pPath); - ffStrbufReplaceAllC(&buffer, '\\', '/'); - ffStrbufEnsureEndsWithC(&buffer, '/'); - if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { - ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); - } +static void platformPathAddKnownFolder(FFlist* dirs, FFKnownFolder folder) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (!ffGetKnownFolderPath(folder, &buffer)) { + return; + } + + ffStrbufReplaceAllC(&buffer, '\\', '/'); + ffStrbufEnsureEndsWithC(&buffer, '/'); + if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { + ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); } } @@ -112,9 +96,9 @@ static void getConfigDirs(FFPlatform* platform) { } ffPlatformPathAddHome(&platform->configDirs, platform, ".config/"); - platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData); - platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData); - platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData); + platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_PROGRAM_DATA); + platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA); + platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA); ffPlatformPathAddHome(&platform->configDirs, platform, ""); } @@ -126,9 +110,9 @@ static void getDataDirs(FFPlatform* platform) { platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share"); } ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/"); - platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData); - platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData); - platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData); + platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_PROGRAM_DATA); + platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA); + platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA); ffPlatformPathAddHome(&platform->dataDirs, platform, ""); } @@ -156,9 +140,25 @@ static void getUserName(FFPlatform* platform) { TOKEN_USER* tokenUser = (TOKEN_USER*) buf; UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) }; if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) { - ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer); + uint32_t sidLength = sidString.Length / sizeof(wchar_t); + ffStrbufSetNWS(&platform->sid, sidLength, sidString.Buffer); + + // The SID is already in hand, so the profile directory costs one registry read rather + // than a second NtQueryInformationToken(). ffGetProfilePath() needs a null-terminated + // string, which RtlConvertSidToUnicodeString() does not produce. + if (sidLength < ARRAY_SIZE(buffer)) { + buffer[sidLength] = L'\0'; + ffGetProfilePath(buffer, &platform->homeDir); + } } } + + if (platform->homeDir.length == 0) { + ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); // Last resort; ffGetProfilePath() covers the normal case + } + + ffStrbufReplaceAllC(&platform->homeDir, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); } static void getHostName(FFPlatform* platform) { @@ -280,12 +280,12 @@ void ffPlatformInitImpl(FFPlatform* platform) { platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess; getExePath(platform); getCwd(platform); - getHomeDir(platform); + + getUserName(platform); + getCacheDir(platform); getConfigDirs(platform); getDataDirs(platform); - - getUserName(platform); getHostName(platform); getUserShell(platform); diff --git a/src/common/windows/folders.c b/src/common/windows/folders.c new file mode 100644 index 000000000..b42afed95 --- /dev/null +++ b/src/common/windows/folders.c @@ -0,0 +1,237 @@ +#include "folders.h" +#include "common/debug.h" +#include "common/mallocHelper.h" +#include "common/windows/nt.h" +#include "common/windows/registry.h" +#include "common/windows/unicode.h" + +#include + +// Where the shell keeps the values SHGetKnownFolderPath() would compute. +// +// `User Shell Folders` is the store folder redirection writes to -- HKCU for the user's own +// folders, HKLM for the machine-wide ones -- and it holds the *unexpanded* form, which is why the +// environment expansion below is not optional. `ProfileList\\ProfileImagePath` is what the +// logon session recorded as the profile directory, and is read by ffGetProfilePath(). +#define FF_FOLDERS_HKCU_SHELL_FOLDERS L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" +#define FF_FOLDERS_HKLM_SHELL_FOLDERS L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" +#define FF_FOLDERS_HKLM_CURRENT_VERSION L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion" +#define FF_FOLDERS_HKLM_PROFILE_LIST L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" + +// The environment fallbacks. Two of the registry values above are self-referential -- `Common +// AppData` is literally `%ProgramData%` -- so these are the same answer, not a different one. +#define FF_FOLDERS_ENV_LOCAL_APP_DATA L"%LOCALAPPDATA%" +#define FF_FOLDERS_ENV_ROAMING_APP_DATA L"%APPDATA%" +#define FF_FOLDERS_ENV_PROGRAM_DATA L"%ProgramData%" +#define FF_FOLDERS_ENV_PROGRAM_FILES L"%ProgramFiles%" + +// Every folder is resolved once and then kept. ffGetKnownFolderPath() is called about a dozen times +// per process -- getCacheDir, getConfigDirs and getDataDirs all ask for the same folders -- and +// each answer is a pure function of the registry and this process' environment. `states` separates +// "not looked up yet" from "looked up and not there", which the path itself cannot: an empty +// FFstrbuf is a valid answer. +// +// No lock guards this: the only thread fastfetch ever creates is the networking module's +// asynchronous connect, and no module that resolves a known folder runs on it. +typedef enum FFFolderState : uint8_t { + FF_FOLDER_UNRESOLVED, + FF_FOLDER_FOUND, + FF_FOLDER_MISSING, +} FFFolderState; + +static FFstrbuf folderPaths[FF_KNOWN_FOLDER_COUNT]; +static FFFolderState folderStates[FF_KNOWN_FOLDER_COUNT]; + +// Expands `%NAME%` placeholders from this process' environment block. +// +// The work is done in UTF-16 so that a user name outside the ANSI code page survives: getenv() +// goes through the CRT's narrow copy of the environment and would mangle it. +// +// RtlExpandEnvironmentStrings() substitutes nothing and still reports success for a variable it +// cannot find (measured), so a leftover '%' is treated as a failure. A directory name that +// legitimately contains one is a false negative, and the only thing that costs is the caller's +// fallback. +static bool expandEnv(const wchar_t* source, uint32_t sourceLen, FFstrbuf* result) { + // Asking for the required length is done with a null destination, which the call reports as + // STATUS_BUFFER_TOO_SMALL -- a *failure* status that still fills in `size`. The status is + // therefore deliberately not checked here; a call that writes nothing leaves `size` at zero. + SIZE_T size = 0; // in characters, including the null terminator + (void) RtlExpandEnvironmentStrings(nullptr, source, sourceLen, nullptr, 0, &size); + if (size == 0) { + return false; + } + + FF_AUTO_FREE wchar_t* expanded = (wchar_t*) malloc(size * sizeof(wchar_t)); + SIZE_T written = 0; + if (!NT_SUCCESS(RtlExpandEnvironmentStrings(nullptr, source, sourceLen, expanded, size, &written)) || written == 0) { + return false; + } + + if (wcschr(expanded, L'%')) { + return false; + } + + ffStrbufSetNWS(result, (uint32_t) (written - 1), expanded); + return true; +} + +static bool expandEnvZ(const wchar_t* source, FFstrbuf* result) { + return expandEnv(source, (uint32_t) wcslen(source), result); +} + +// `raw` is the payload of a REG_SZ / REG_EXPAND_SZ value: UTF-16, usually null terminated. +static bool expandBuffer(const FFArgBuffer* raw, FFstrbuf* result) { + uint32_t length = raw->length / sizeof(wchar_t); + if (length == 0) { + return false; + } + + const wchar_t* valueW = (const wchar_t*) raw->data; + if (valueW[length - 1] == L'\0') { + --length; + } + + return length > 0 && expandEnv(valueW, length, result); +} + +// FF_ARG_TYPE_BUFFER rather than FF_ARG_TYPE_STRBUF, because the value has to stay UTF-16 for +// expandEnv(); the strbuf flavour converts it to UTF-8 on the way out. The buffer flavour does not +// report the registry type either, so REG_SZ and REG_EXPAND_SZ are not told apart -- expanding a +// string without placeholders is a no-op, which is what makes that acceptable. +static bool readExpandedValue(HANDLE hKey, const wchar_t* valueNameW, FFstrbuf* result) { + FFArgBuffer raw = {}; + if (!ffRegReadData(hKey, valueNameW, &raw, nullptr)) { + return false; + } + FF_AUTO_FREE void* data = raw.data; + + return expandBuffer(&raw, result); +} + +static bool readExpandedValueFrom(HKEY hRootKey, const wchar_t* subKeyW, const wchar_t* valueNameW, FFstrbuf* result) { + FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; + if (!ffRegOpenKeyForRead(hRootKey, subKeyW, &hKey, nullptr)) { + return false; + } + + return readExpandedValue(hKey, valueNameW, result); +} + +// The SID comes from the caller rather than from a token query here: FFPlatform_windows.c already +// resolves it for `platform->sid`, and the two are the same SID by definition. +bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result) { + assert(sidW); + + FF_AUTO_CLOSE_FD HANDLE hProfileList = nullptr; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_PROFILE_LIST, &hProfileList, nullptr)) { + FF_DEBUG("Cannot open %ls", FF_FOLDERS_HKLM_PROFILE_LIST); + return false; + } + + FF_AUTO_CLOSE_FD HANDLE hProfile = nullptr; + if (!ffRegOpenSubkeyForRead(hProfileList, sidW, &hProfile, nullptr)) { + FF_DEBUG("No profile is registered for %ls", sidW); + return false; + } + + return readExpandedValue(hProfile, L"ProfileImagePath", result); +} + +// `Local AppData` and `AppData` share a key, so they are read together. +// +// The names have to be exact, spaces and all: `Local AppData` is `Local`, one space, `AppData` -- +// there is no `Local App Data`, and asking for it fails the whole batch below. +static const struct { + FFKnownFolder folder; + const wchar_t* valueNameW; + const wchar_t* envW; +} shellFolderEntries[] = { + { FF_KNOWN_FOLDER_LOCAL_APP_DATA, L"Local AppData", FF_FOLDERS_ENV_LOCAL_APP_DATA }, + { FF_KNOWN_FOLDER_ROAMING_APP_DATA, L"AppData", FF_FOLDERS_ENV_ROAMING_APP_DATA }, +}; + +// One open plus one NtQueryMultipleValueKey resolves both -- and whichever of the two is asked for +// first caches the pair, which is what the callers want anyway. +// +// NtQueryMultipleValueKey is all-or-nothing: one absent name fails the batch, which would push +// *both* folders to the environment fallback. That is survivable, because these values are +// themselves just `%LOCALAPPDATA%` and `%APPDATA%` expanded -- but it is also invisible, and an +// invisible fallback is how a wrong value name went unnoticed here. So a failed batch is retried +// one value at a time, and the extra queries are only paid when the batch has already failed. +static void resolveUserShellFolders(void) { + FFArgBuffer buffers[ARRAY_SIZE(shellFolderEntries)] = {}; + FFRegValueArg args[ARRAY_SIZE(shellFolderEntries)]; + for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) { + args[i] = (FFRegValueArg) { .type = FF_ARG_TYPE_BUFFER, .value = &buffers[i], .name = shellFolderEntries[i].valueNameW }; + } + + FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, FF_FOLDERS_HKCU_SHELL_FOLDERS, &hKey, nullptr)) { + if (ffRegReadValues(hKey, ARRAY_SIZE(shellFolderEntries), args, nullptr)) { + for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) { + if (expandBuffer(&buffers[i], &folderPaths[shellFolderEntries[i].folder])) { + folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND; + } + } + } else { + FF_DEBUG("Batched read of %ls failed; retrying one value at a time", FF_FOLDERS_HKCU_SHELL_FOLDERS); + for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) { + if (readExpandedValue(hKey, shellFolderEntries[i].valueNameW, &folderPaths[shellFolderEntries[i].folder])) { + folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND; + } + } + } + } + + for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) { + // Every buffer has to be freed even when the read failed part way through, which is why the + // free is not tied to the success path above. + free(buffers[i].data); + + FFKnownFolder folder = shellFolderEntries[i].folder; + if (folderStates[folder] == FF_FOLDER_UNRESOLVED && + expandEnvZ(shellFolderEntries[i].envW, &folderPaths[folder])) { + folderStates[folder] = FF_FOLDER_FOUND; + } + if (folderStates[folder] == FF_FOLDER_UNRESOLVED) { + folderStates[folder] = FF_FOLDER_MISSING; + } + } +} + +static bool resolveFolder(FFKnownFolder folder) { + switch (folder) { + case FF_KNOWN_FOLDER_LOCAL_APP_DATA: + case FF_KNOWN_FOLDER_ROAMING_APP_DATA: + resolveUserShellFolders(); // Resolves both of them + return folderStates[folder] == FF_FOLDER_FOUND; + + case FF_KNOWN_FOLDER_PROGRAM_DATA: + return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_SHELL_FOLDERS, L"Common AppData", &folderPaths[folder]) || + expandEnvZ(FF_FOLDERS_ENV_PROGRAM_DATA, &folderPaths[folder]); + + case FF_KNOWN_FOLDER_PROGRAM_FILES: + return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_CURRENT_VERSION, L"ProgramFilesDir", &folderPaths[folder]) || + expandEnvZ(FF_FOLDERS_ENV_PROGRAM_FILES, &folderPaths[folder]); + + default: + assert(false); + return false; + } +} + +bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result) { + assert(result); + assert(folder < FF_KNOWN_FOLDER_COUNT); + + if (folderStates[folder] == FF_FOLDER_UNRESOLVED) { + folderStates[folder] = resolveFolder(folder) ? FF_FOLDER_FOUND : FF_FOLDER_MISSING; + } + + if (folderStates[folder] != FF_FOLDER_FOUND) { + return false; + } + + ffStrbufSet(result, &folderPaths[folder]); + return true; +} diff --git a/src/common/windows/folders.h b/src/common/windows/folders.h new file mode 100644 index 000000000..683041e41 --- /dev/null +++ b/src/common/windows/folders.h @@ -0,0 +1,47 @@ +#pragma once + +#include "fastfetch.h" +#include "common/io.h" + +#include // ffGetProfilePath() takes a SID as a wide string + +// The known folders fastfetch needs. They are spelled out instead of taking a REFKNOWNFOLDERID so +// that this header does not have to drag in -- the call sites include it for reasons of +// their own, and a header that only works when came first is a trap. +typedef enum FFKnownFolder : uint8_t { + FF_KNOWN_FOLDER_LOCAL_APP_DATA, + FF_KNOWN_FOLDER_ROAMING_APP_DATA, + FF_KNOWN_FOLDER_PROGRAM_DATA, + FF_KNOWN_FOLDER_PROGRAM_FILES, + FF_KNOWN_FOLDER_COUNT, +} FFKnownFolder; + +// Resolves a known folder without initializing the shell. +// +// The first SHGetKnownFolderPath() call in a process costs ~2.4 ms (measured on Windows 10 22H2): +// the shell's known-folder machinery is built on demand, and whichever folder is asked for first +// pays for it. Every later call is free, so a process that needs a handful of folders pays that +// price once and can never amortize it. Each folder below is instead one registry value or one +// environment variable away -- ~0.03 ms for all of them together -- so the shell is never touched. +// +// The answer is cached: these five folders are asked for about a dozen times per process, and each +// answer is a pure function of the registry and this process' environment. That is safe without a +// lock, because nothing that resolves a known folder ever runs on the one thread fastfetch creates. +// +// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the +// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes, +// exactly like SHGetKnownFolderPath(). +bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result); + +// Resolves the profile directory recorded for `sidW`, a SID string as produced by +// RtlConvertSidToUnicodeString(). +// +// There is no FF_KNOWN_FOLDER_PROFILE because the caller already has the SID: +// FFPlatform_windows.c resolves it once for `platform->sid`, and deriving it a second time here +// would mean a second NtQueryInformationToken(). The answer is deliberately not cached -- this is +// called once per process, and from one place. +// +// Returns false, leaving `result` untouched, when the value is missing or unusable, so that the +// caller keeps whatever fallback it had for the shell API failing. The path uses backslashes, +// exactly like SHGetKnownFolderPath(). +bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result); diff --git a/src/detection/packages/packages_windows.c b/src/detection/packages/packages_windows.c index 3325c15a2..2c9e1ed54 100644 --- a/src/detection/packages/packages_windows.c +++ b/src/detection/packages/packages_windows.c @@ -2,6 +2,7 @@ #include "common/processing.h" #include "common/strutil.h" #include "common/path.h" +#include "common/windows/folders.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" #include "common/mallocHelper.h" @@ -11,7 +12,6 @@ #include #include "common/windows/nt.h" #include -#include static uint32_t getNumElements(const char* searchPath, DWORD type, const wchar_t* ignore) { FF_AUTO_CLOSE_FD HANDLE dfd = CreateFileA(searchPath, FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); @@ -120,11 +120,9 @@ static void detectScoop(FFPackagesResult* result) { ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "global_path")); } if (scoopPath.length == 0) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramData, KF_FLAG_DEFAULT, nullptr, &pPath))) { - ffStrbufSetWS(&scoopPath, pPath); - CoTaskMemFree(pPath); - } + // Leaving `scoopPath` empty when the lookup fails makes `getNumElements` report 0, which + // is what the shell API failing used to do as well. + ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_DATA, &scoopPath); ffStrbufAppendS(&scoopPath, "/scoop"); } ffStrbufAppendS(&scoopPath, "/apps/"); diff --git a/src/detection/terminalfont/terminalfont_windows.c b/src/detection/terminalfont/terminalfont_windows.c index 1bd765d2b..75636eaac 100644 --- a/src/detection/terminalfont/terminalfont_windows.c +++ b/src/detection/terminalfont/terminalfont_windows.c @@ -3,13 +3,13 @@ #include "common/path.h" #include "common/processing.h" #include "common/properties.h" +#include "common/windows/folders.h" #include "common/windows/unicode.h" #include "common/windows/registry.h" #include "common/strutil.h" #include "detection/terminalshell/terminalshell.h" #include "terminalfont.h" -#include #include #include @@ -225,13 +225,11 @@ static void detectWTProfileFromFragmentsIn(const FFstrbuf* fragmentDir, const FF static void detectFromWTFragments(const FFstrbuf* wtProfileId, FFTerminalFontWT* result) { // Windows Terminal merges the user scoped fragments before the machine scoped ones, // so the user scoped fragments are more important - static const KNOWNFOLDERID* const fragmentFolderIds[] = { &FOLDERID_LocalAppData, &FOLDERID_ProgramData }; + static const FFKnownFolder fragmentFolders[] = { FF_KNOWN_FOLDER_LOCAL_APP_DATA, FF_KNOWN_FOLDER_PROGRAM_DATA }; - for (uint32_t i = 0; i < ARRAY_SIZE(fragmentFolderIds); ++i) { - PWSTR folderW = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(fragmentFolderIds[i], KF_FLAG_DEFAULT, nullptr, &folderW))) { - FF_STRBUF_AUTO_DESTROY fragmentDir = ffStrbufCreateWS(folderW); - CoTaskMemFree(folderW); + for (uint32_t i = 0; i < ARRAY_SIZE(fragmentFolders); ++i) { + FF_STRBUF_AUTO_DESTROY fragmentDir = ffStrbufCreate(); + if (ffGetKnownFolderPath(fragmentFolders[i], &fragmentDir)) { ffStrbufAppendS(&fragmentDir, "\\Microsoft\\Windows Terminal\\Fragments\\"); if (ffPathExists(fragmentDir.chars, FF_PATHTYPE_DIRECTORY)) { @@ -258,11 +256,7 @@ static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFon error = "Error reading Windows Terminal portable settings JSON file"; } } else { - PWSTR localAppDataW = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &localAppDataW))) { - ffStrbufSetWS(&jsonPath, localAppDataW); - CoTaskMemFree(localAppDataW); - + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &jsonPath)) { if (ffStrbufContainIgnCaseS(terminalExe, "_8wekyb3d8bbwe\\")) { // Microsoft Store version if (ffStrbufContainIgnCaseS(terminalExe, ".WindowsTerminalPreview_")) { diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c index 7dc9caf68..b241721ff 100644 --- a/src/detection/terminalshell/terminalshell_windows.c +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -4,6 +4,7 @@ #include "common/processing.h" #include "common/thread.h" #include "common/mallocHelper.h" +#include "common/windows/folders.h" #include "common/windows/registry.h" #include "common/windows/unicode.h" #include "common/windows/version.h" @@ -16,7 +17,6 @@ #include #include #include -#include static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) { uint32_t ppid = 0; @@ -182,12 +182,7 @@ static bool detectDefaultTerminal(FFTerminalResult* result) { ffStrbufSetS(&result->processName, "WindowsTerminal.exe"); ffStrbufSetS(&result->prettyName, "WindowsTerminal"); - PWSTR programFiles = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &programFiles))) { - ffStrbufSetWS(&result->exe, programFiles); - CoTaskMemFree(programFiles); - programFiles = nullptr; - + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &result->exe)) { ffStrbufAppendS(&result->exe, "\\WindowsApps\\"); ffStrbufAppend(&result->exe, &path); ffStrbufAppendS(&result->exe, "\\WindowsTerminal.exe"); diff --git a/src/detection/wm/wm_windows.c b/src/detection/wm/wm_windows.c index 21e073dd2..fbe74544d 100644 --- a/src/detection/wm/wm_windows.c +++ b/src/detection/wm/wm_windows.c @@ -3,6 +3,7 @@ #include "common/io.h" #include "common/library.h" #include "common/processing.h" +#include "common/windows/folders.h" #include "common/windows/nt.h" #include "common/windows/unicode.h" #include "common/windows/version.h" @@ -10,7 +11,6 @@ #include #include #include -#include #include typedef enum { @@ -69,16 +69,18 @@ static bool isProcessTrusted(DWORD processId, FFProcessType processType, UNICODE static wchar_t windowsAppsPath[MAX_PATH]; static uint32_t windowsAppsPathLen; if (windowsAppsPathLen == 0) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, nullptr, &pPath))) { - windowsAppsPathLen = (uint32_t) wcslen(pPath); - memcpy(windowsAppsPath, pPath, windowsAppsPathLen * sizeof(wchar_t)); - memcpy(windowsAppsPath + windowsAppsPathLen, L"\\WindowsApps\\", sizeof(L"\\WindowsApps\\")); - windowsAppsPathLen += strlen("\\WindowsApps\\"); + FF_STRBUF_AUTO_DESTROY programFiles = ffStrbufCreate(); + ULONG length = 0; // in bytes, including the null terminator + if (ffGetKnownFolderPath(FF_KNOWN_FOLDER_PROGRAM_FILES, &programFiles)) { + ffStrbufAppendS(&programFiles, "\\WindowsApps\\"); + if (NT_SUCCESS(RtlUTF8ToUnicodeN(windowsAppsPath, (ULONG) sizeof(windowsAppsPath), &length, programFiles.chars, (ULONG) programFiles.length + 1))) { + windowsAppsPathLen = (uint32_t) (length / sizeof(wchar_t) - 1); + } else { + windowsAppsPathLen = -1u; + } } else { windowsAppsPathLen = -1u; } - CoTaskMemFree(pPath); } if (windowsAppsPathLen != -1u && (buffer->Length <= windowsAppsPathLen * sizeof(wchar_t) || // Path is too short to be in WindowsApps @@ -230,14 +232,10 @@ const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, [[maybe_ } if (ffStrbufEqualS(wmName, "dwm.exe")) { - PWSTR pPath = nullptr; - if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_System, KF_FLAG_DEFAULT, nullptr, &pPath))) { - wchar_t fullPath[MAX_PATH]; - wcscpy(fullPath, pPath); - wcscat(fullPath, L"\\dwm.exe"); - ffGetFileVersion(fullPath, nullptr, result); - } - CoTaskMemFree(pPath); + wchar_t exePath[MAX_PATH]; + _snwprintf(exePath, ARRAY_SIZE(exePath), L"%ls\\system32\\%ls", (const wchar_t*) SharedUserData->NtSystemRoot, L"dwm.exe"); + ffGetFileVersion(exePath, nullptr, result); + return nullptr; } return "Not supported on this platform";