|
| 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