Skip to content

Commit 4fb57bb

Browse files
committed
Global (Windows): eliminates SHGetKnownFolderPath
1 parent cdbe8f5 commit 4fb57bb

8 files changed

Lines changed: 353 additions & 83 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,7 @@ elseif(WIN32)
10981098
src/common/impl/kmod_windows.c
10991099
src/common/windows/getline.c
11001100
src/common/windows/com.c
1101+
src/common/windows/folders.c
11011102
src/common/windows/registry.c
11021103
src/common/windows/unicode.c
11031104
src/common/windows/variant.cpp

‎src/common/impl/FFPlatform_windows.c‎

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
#include "common/io.h"
33
#include "common/library.h"
44
#include "common/strutil.h"
5+
#include "common/windows/folders.h"
56
#include "common/windows/unicode.h"
67
#include "common/windows/registry.h"
78
#include "common/windows/nt.h"
89

910
#include <stdalign.h>
1011
#include <windows.h>
11-
#include <shlobj.h>
1212
#include <sddl.h>
1313

1414
#define SECURITY_WIN32 1 // For secext.h
@@ -43,43 +43,27 @@ static void getExePath(FFPlatform* platform) {
4343
ffStrbufReplaceAllC(&platform->exePath, '\\', '/');
4444
}
4545

46-
static void getHomeDir(FFPlatform* platform) {
47-
PWSTR pPath = nullptr;
48-
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, nullptr, &pPath))) {
49-
ffStrbufSetWS(&platform->homeDir, pPath);
50-
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
51-
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
52-
} else {
53-
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE"));
54-
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
55-
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
56-
}
57-
CoTaskMemFree(pPath);
58-
}
59-
6046
static void getCacheDir(FFPlatform* platform) {
61-
PWSTR pPath = nullptr;
62-
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &pPath))) {
63-
ffStrbufSetWS(&platform->cacheDir, pPath);
64-
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
65-
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
66-
} else {
47+
if (!ffGetKnownFolderPath(FF_KNOWN_FOLDER_LOCAL_APP_DATA, &platform->cacheDir)) {
6748
ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
6849
ffStrbufAppendS(&platform->cacheDir, "AppData/Local/");
50+
return;
6951
}
70-
CoTaskMemFree(pPath);
52+
53+
ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
54+
ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
7155
}
7256

73-
static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) {
74-
PWSTR pPath = nullptr;
75-
if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, nullptr, &pPath))) {
76-
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath);
77-
CoTaskMemFree(pPath);
78-
ffStrbufReplaceAllC(&buffer, '\\', '/');
79-
ffStrbufEnsureEndsWithC(&buffer, '/');
80-
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
81-
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
82-
}
57+
static void platformPathAddKnownFolder(FFlist* dirs, FFKnownFolder folder) {
58+
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
59+
if (!ffGetKnownFolderPath(folder, &buffer)) {
60+
return;
61+
}
62+
63+
ffStrbufReplaceAllC(&buffer, '\\', '/');
64+
ffStrbufEnsureEndsWithC(&buffer, '/');
65+
if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
66+
ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
8367
}
8468
}
8569

@@ -112,9 +96,9 @@ static void getConfigDirs(FFPlatform* platform) {
11296
}
11397

11498
ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
115-
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData);
116-
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData);
117-
platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData);
99+
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
100+
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
101+
platformPathAddKnownFolder(&platform->configDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
118102
ffPlatformPathAddHome(&platform->configDirs, platform, "");
119103
}
120104

@@ -126,9 +110,9 @@ static void getDataDirs(FFPlatform* platform) {
126110
platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share");
127111
}
128112
ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
129-
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData);
130-
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData);
131-
platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData);
113+
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_PROGRAM_DATA);
114+
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_ROAMING_APP_DATA);
115+
platformPathAddKnownFolder(&platform->dataDirs, FF_KNOWN_FOLDER_LOCAL_APP_DATA);
132116
ffPlatformPathAddHome(&platform->dataDirs, platform, "");
133117
}
134118

@@ -156,9 +140,25 @@ static void getUserName(FFPlatform* platform) {
156140
TOKEN_USER* tokenUser = (TOKEN_USER*) buf;
157141
UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) };
158142
if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) {
159-
ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer);
143+
uint32_t sidLength = sidString.Length / sizeof(wchar_t);
144+
ffStrbufSetNWS(&platform->sid, sidLength, sidString.Buffer);
145+
146+
// The SID is already in hand, so the profile directory costs one registry read rather
147+
// than a second NtQueryInformationToken(). ffGetProfilePath() needs a null-terminated
148+
// string, which RtlConvertSidToUnicodeString() does not produce.
149+
if (sidLength < ARRAY_SIZE(buffer)) {
150+
buffer[sidLength] = L'\0';
151+
ffGetProfilePath(buffer, &platform->homeDir);
152+
}
160153
}
161154
}
155+
156+
if (platform->homeDir.length == 0) {
157+
ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); // Last resort; ffGetProfilePath() covers the normal case
158+
}
159+
160+
ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
161+
ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
162162
}
163163

164164
static void getHostName(FFPlatform* platform) {
@@ -280,12 +280,12 @@ void ffPlatformInitImpl(FFPlatform* platform) {
280280
platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess;
281281
getExePath(platform);
282282
getCwd(platform);
283-
getHomeDir(platform);
283+
284+
getUserName(platform);
285+
284286
getCacheDir(platform);
285287
getConfigDirs(platform);
286288
getDataDirs(platform);
287-
288-
getUserName(platform);
289289
getHostName(platform);
290290
getUserShell(platform);
291291

‎src/common/windows/folders.c‎

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#include "folders.h"
2+
#include "common/debug.h"
3+
#include "common/mallocHelper.h"
4+
#include "common/windows/nt.h"
5+
#include "common/windows/registry.h"
6+
#include "common/windows/unicode.h"
7+
8+
#include <ntstatus.h>
9+
10+
// Where the shell keeps the values SHGetKnownFolderPath() would compute.
11+
//
12+
// `User Shell Folders` is the store folder redirection writes to -- HKCU for the user's own
13+
// folders, HKLM for the machine-wide ones -- and it holds the *unexpanded* form, which is why the
14+
// environment expansion below is not optional. `ProfileList\<SID>\ProfileImagePath` is what the
15+
// logon session recorded as the profile directory, and is read by ffGetProfilePath().
16+
#define FF_FOLDERS_HKCU_SHELL_FOLDERS L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
17+
#define FF_FOLDERS_HKLM_SHELL_FOLDERS L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
18+
#define FF_FOLDERS_HKLM_CURRENT_VERSION L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion"
19+
#define FF_FOLDERS_HKLM_PROFILE_LIST L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
20+
21+
// The environment fallbacks. Two of the registry values above are self-referential -- `Common
22+
// AppData` is literally `%ProgramData%` -- so these are the same answer, not a different one.
23+
#define FF_FOLDERS_ENV_LOCAL_APP_DATA L"%LOCALAPPDATA%"
24+
#define FF_FOLDERS_ENV_ROAMING_APP_DATA L"%APPDATA%"
25+
#define FF_FOLDERS_ENV_PROGRAM_DATA L"%ProgramData%"
26+
#define FF_FOLDERS_ENV_PROGRAM_FILES L"%ProgramFiles%"
27+
28+
// Every folder is resolved once and then kept. ffGetKnownFolderPath() is called about a dozen times
29+
// per process -- getCacheDir, getConfigDirs and getDataDirs all ask for the same folders -- and
30+
// each answer is a pure function of the registry and this process' environment. `states` separates
31+
// "not looked up yet" from "looked up and not there", which the path itself cannot: an empty
32+
// FFstrbuf is a valid answer.
33+
//
34+
// No lock guards this: the only thread fastfetch ever creates is the networking module's
35+
// asynchronous connect, and no module that resolves a known folder runs on it.
36+
typedef enum FFFolderState : uint8_t {
37+
FF_FOLDER_UNRESOLVED,
38+
FF_FOLDER_FOUND,
39+
FF_FOLDER_MISSING,
40+
} FFFolderState;
41+
42+
static FFstrbuf folderPaths[FF_KNOWN_FOLDER_COUNT];
43+
static FFFolderState folderStates[FF_KNOWN_FOLDER_COUNT];
44+
45+
// Expands `%NAME%` placeholders from this process' environment block.
46+
//
47+
// The work is done in UTF-16 so that a user name outside the ANSI code page survives: getenv()
48+
// goes through the CRT's narrow copy of the environment and would mangle it.
49+
//
50+
// RtlExpandEnvironmentStrings() substitutes nothing and still reports success for a variable it
51+
// cannot find (measured), so a leftover '%' is treated as a failure. A directory name that
52+
// legitimately contains one is a false negative, and the only thing that costs is the caller's
53+
// fallback.
54+
static bool expandEnv(const wchar_t* source, uint32_t sourceLen, FFstrbuf* result) {
55+
// Asking for the required length is done with a null destination, which the call reports as
56+
// STATUS_BUFFER_TOO_SMALL -- a *failure* status that still fills in `size`. The status is
57+
// therefore deliberately not checked here; a call that writes nothing leaves `size` at zero.
58+
SIZE_T size = 0; // in characters, including the null terminator
59+
(void) RtlExpandEnvironmentStrings(nullptr, source, sourceLen, nullptr, 0, &size);
60+
if (size == 0) {
61+
return false;
62+
}
63+
64+
FF_AUTO_FREE wchar_t* expanded = (wchar_t*) malloc(size * sizeof(wchar_t));
65+
SIZE_T written = 0;
66+
if (!NT_SUCCESS(RtlExpandEnvironmentStrings(nullptr, source, sourceLen, expanded, size, &written)) || written == 0) {
67+
return false;
68+
}
69+
70+
if (wcschr(expanded, L'%')) {
71+
return false;
72+
}
73+
74+
ffStrbufSetNWS(result, (uint32_t) (written - 1), expanded);
75+
return true;
76+
}
77+
78+
static bool expandEnvZ(const wchar_t* source, FFstrbuf* result) {
79+
return expandEnv(source, (uint32_t) wcslen(source), result);
80+
}
81+
82+
// `raw` is the payload of a REG_SZ / REG_EXPAND_SZ value: UTF-16, usually null terminated.
83+
static bool expandBuffer(const FFArgBuffer* raw, FFstrbuf* result) {
84+
uint32_t length = raw->length / sizeof(wchar_t);
85+
if (length == 0) {
86+
return false;
87+
}
88+
89+
const wchar_t* valueW = (const wchar_t*) raw->data;
90+
if (valueW[length - 1] == L'\0') {
91+
--length;
92+
}
93+
94+
return length > 0 && expandEnv(valueW, length, result);
95+
}
96+
97+
// FF_ARG_TYPE_BUFFER rather than FF_ARG_TYPE_STRBUF, because the value has to stay UTF-16 for
98+
// expandEnv(); the strbuf flavour converts it to UTF-8 on the way out. The buffer flavour does not
99+
// report the registry type either, so REG_SZ and REG_EXPAND_SZ are not told apart -- expanding a
100+
// string without placeholders is a no-op, which is what makes that acceptable.
101+
static bool readExpandedValue(HANDLE hKey, const wchar_t* valueNameW, FFstrbuf* result) {
102+
FFArgBuffer raw = {};
103+
if (!ffRegReadData(hKey, valueNameW, &raw, nullptr)) {
104+
return false;
105+
}
106+
FF_AUTO_FREE void* data = raw.data;
107+
108+
return expandBuffer(&raw, result);
109+
}
110+
111+
static bool readExpandedValueFrom(HKEY hRootKey, const wchar_t* subKeyW, const wchar_t* valueNameW, FFstrbuf* result) {
112+
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
113+
if (!ffRegOpenKeyForRead(hRootKey, subKeyW, &hKey, nullptr)) {
114+
return false;
115+
}
116+
117+
return readExpandedValue(hKey, valueNameW, result);
118+
}
119+
120+
// The SID comes from the caller rather than from a token query here: FFPlatform_windows.c already
121+
// resolves it for `platform->sid`, and the two are the same SID by definition.
122+
bool ffGetProfilePath(const wchar_t* sidW, FFstrbuf* result) {
123+
assert(sidW);
124+
125+
FF_AUTO_CLOSE_FD HANDLE hProfileList = nullptr;
126+
if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_PROFILE_LIST, &hProfileList, nullptr)) {
127+
FF_DEBUG("Cannot open %ls", FF_FOLDERS_HKLM_PROFILE_LIST);
128+
return false;
129+
}
130+
131+
FF_AUTO_CLOSE_FD HANDLE hProfile = nullptr;
132+
if (!ffRegOpenSubkeyForRead(hProfileList, sidW, &hProfile, nullptr)) {
133+
FF_DEBUG("No profile is registered for %ls", sidW);
134+
return false;
135+
}
136+
137+
return readExpandedValue(hProfile, L"ProfileImagePath", result);
138+
}
139+
140+
// `Local AppData` and `AppData` share a key, so they are read together.
141+
//
142+
// The names have to be exact, spaces and all: `Local AppData` is `Local`, one space, `AppData` --
143+
// there is no `Local App Data`, and asking for it fails the whole batch below.
144+
static const struct {
145+
FFKnownFolder folder;
146+
const wchar_t* valueNameW;
147+
const wchar_t* envW;
148+
} shellFolderEntries[] = {
149+
{ FF_KNOWN_FOLDER_LOCAL_APP_DATA, L"Local AppData", FF_FOLDERS_ENV_LOCAL_APP_DATA },
150+
{ FF_KNOWN_FOLDER_ROAMING_APP_DATA, L"AppData", FF_FOLDERS_ENV_ROAMING_APP_DATA },
151+
};
152+
153+
// One open plus one NtQueryMultipleValueKey resolves both -- and whichever of the two is asked for
154+
// first caches the pair, which is what the callers want anyway.
155+
//
156+
// NtQueryMultipleValueKey is all-or-nothing: one absent name fails the batch, which would push
157+
// *both* folders to the environment fallback. That is survivable, because these values are
158+
// themselves just `%LOCALAPPDATA%` and `%APPDATA%` expanded -- but it is also invisible, and an
159+
// invisible fallback is how a wrong value name went unnoticed here. So a failed batch is retried
160+
// one value at a time, and the extra queries are only paid when the batch has already failed.
161+
static void resolveUserShellFolders(void) {
162+
FFArgBuffer buffers[ARRAY_SIZE(shellFolderEntries)] = {};
163+
FFRegValueArg args[ARRAY_SIZE(shellFolderEntries)];
164+
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
165+
args[i] = (FFRegValueArg) { .type = FF_ARG_TYPE_BUFFER, .value = &buffers[i], .name = shellFolderEntries[i].valueNameW };
166+
}
167+
168+
FF_AUTO_CLOSE_FD HANDLE hKey = nullptr;
169+
if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, FF_FOLDERS_HKCU_SHELL_FOLDERS, &hKey, nullptr)) {
170+
if (ffRegReadValues(hKey, ARRAY_SIZE(shellFolderEntries), args, nullptr)) {
171+
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
172+
if (expandBuffer(&buffers[i], &folderPaths[shellFolderEntries[i].folder])) {
173+
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
174+
}
175+
}
176+
} else {
177+
FF_DEBUG("Batched read of %ls failed; retrying one value at a time", FF_FOLDERS_HKCU_SHELL_FOLDERS);
178+
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
179+
if (readExpandedValue(hKey, shellFolderEntries[i].valueNameW, &folderPaths[shellFolderEntries[i].folder])) {
180+
folderStates[shellFolderEntries[i].folder] = FF_FOLDER_FOUND;
181+
}
182+
}
183+
}
184+
}
185+
186+
for (uint32_t i = 0; i < ARRAY_SIZE(shellFolderEntries); ++i) {
187+
// Every buffer has to be freed even when the read failed part way through, which is why the
188+
// free is not tied to the success path above.
189+
free(buffers[i].data);
190+
191+
FFKnownFolder folder = shellFolderEntries[i].folder;
192+
if (folderStates[folder] == FF_FOLDER_UNRESOLVED &&
193+
expandEnvZ(shellFolderEntries[i].envW, &folderPaths[folder])) {
194+
folderStates[folder] = FF_FOLDER_FOUND;
195+
}
196+
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
197+
folderStates[folder] = FF_FOLDER_MISSING;
198+
}
199+
}
200+
}
201+
202+
static bool resolveFolder(FFKnownFolder folder) {
203+
switch (folder) {
204+
case FF_KNOWN_FOLDER_LOCAL_APP_DATA:
205+
case FF_KNOWN_FOLDER_ROAMING_APP_DATA:
206+
resolveUserShellFolders(); // Resolves both of them
207+
return folderStates[folder] == FF_FOLDER_FOUND;
208+
209+
case FF_KNOWN_FOLDER_PROGRAM_DATA:
210+
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_SHELL_FOLDERS, L"Common AppData", &folderPaths[folder]) ||
211+
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_DATA, &folderPaths[folder]);
212+
213+
case FF_KNOWN_FOLDER_PROGRAM_FILES:
214+
return readExpandedValueFrom(HKEY_LOCAL_MACHINE, FF_FOLDERS_HKLM_CURRENT_VERSION, L"ProgramFilesDir", &folderPaths[folder]) ||
215+
expandEnvZ(FF_FOLDERS_ENV_PROGRAM_FILES, &folderPaths[folder]);
216+
217+
default:
218+
assert(false);
219+
return false;
220+
}
221+
}
222+
223+
bool ffGetKnownFolderPath(FFKnownFolder folder, FFstrbuf* result) {
224+
assert(result);
225+
assert(folder < FF_KNOWN_FOLDER_COUNT);
226+
227+
if (folderStates[folder] == FF_FOLDER_UNRESOLVED) {
228+
folderStates[folder] = resolveFolder(folder) ? FF_FOLDER_FOUND : FF_FOLDER_MISSING;
229+
}
230+
231+
if (folderStates[folder] != FF_FOLDER_FOUND) {
232+
return false;
233+
}
234+
235+
ffStrbufSet(result, &folderPaths[folder]);
236+
return true;
237+
}

0 commit comments

Comments
 (0)