Global (Windows): eliminates SHGetKnownFolderPath

This commit is contained in:
Carter Li
2026-09-28 23:02:34 +08:00
parent cdbe8f54c6
commit 4fb57bb39b
8 changed files with 353 additions and 83 deletions
+42 -42
View File
@@ -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 <stdalign.h>
#include <windows.h>
#include <shlobj.h>
#include <sddl.h>
#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);
+237
View File
@@ -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 <ntstatus.h>
// 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\<SID>\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;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "fastfetch.h"
#include "common/io.h"
#include <wchar.h> // 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 <shlobj.h> -- the call sites include it for reasons of
// their own, and a header that only works when <windows.h> 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);