master ohmu/fastfetch-openbsd / src / common / windows / folders.c
ohmu  ·  2026-10-15
  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.
 36typedef enum FFFolderState : uint8_t {
 37    FF_FOLDER_UNRESOLVED,
 38    FF_FOLDER_FOUND,
 39    FF_FOLDER_MISSING,
 40} FFFolderState;
 41
 42static FFstrbuf folderPaths[FF_KNOWN_FOLDER_COUNT];
 43static 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.
 54static 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
 78static 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.
 83static 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.
101static 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
111static 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.
122bool 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.
144static 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.
161static 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
202static 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
223bool 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}