| 198 | } |
| 199 | |
| 200 | std::unordered_map<std::string, std::string> LoadFileTokens(uint32_t accountId, uint32_t appId, |
| 201 | const std::unordered_set<std::string>& validRootTokens) { |
| 202 | std::shared_lock<std::shared_mutex> lock(g_metadataMutex); |
| 203 | std::string appPath = GetAppPathInternal(accountId, appId); |
| 204 | std::string path = appPath + kFileTokensFilename; |
| 205 | std::string legacyPath = appPath + kLegacyFileTokensFilename; |
| 206 | std::unordered_map<std::string, std::string> result; |
| 207 | bool fromLegacy = false; |
| 208 | |
| 209 | std::ifstream f(FileUtil::Utf8ToPath(path)); |
| 210 | if (!f) { |
| 211 | f.open(FileUtil::Utf8ToPath(legacyPath)); |
| 212 | if (f) fromLegacy = true; |
| 213 | } |
| 214 | if (f) { |
| 215 | std::string line; |
| 216 | while (std::getline(f, line)) { |
| 217 | while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) |
| 218 | line.pop_back(); |
| 219 | if (line.empty()) continue; |
| 220 | auto tab = line.find('\t'); |
| 221 | if (tab == std::string::npos) continue; |
| 222 | std::string cleanName = line.substr(0, tab); |
| 223 | std::string token = line.substr(tab + 1); |
| 224 | if (!cleanName.empty()) { |
| 225 | result[cleanName] = token; |
| 226 | } |
| 227 | } |
| 228 | f.close(); |
| 229 | if (!result.empty()) { |
| 230 | LOG("LoadFileTokens: loaded %zu entries from %s for app %u", |
| 231 | result.size(), fromLegacy ? "legacy path" : "disk", appId); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | // Migrate from legacy if needed. Skip if canonical file already |
| 236 | // exists (prior session or concurrent thread may have migrated). |
| 237 | if (fromLegacy && !result.empty()) { |
| 238 | std::string canonicalPath = appPath + kFileTokensFilename; |
| 239 | if (!std::filesystem::exists(FileUtil::Utf8ToPath(canonicalPath))) { |
| 240 | // Re-check under exclusive lock. |
| 241 | lock.unlock(); |
| 242 | // Re-check existence to mitigate TOCTOU with concurrent migration. |
| 243 | if (!std::filesystem::exists(FileUtil::Utf8ToPath(canonicalPath))) { |
| 244 | LOG("LoadFileTokens: migrating file tokens for app %u", appId); |
| 245 | SaveFileTokens(accountId, appId, result, validRootTokens); |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | // Filter against valid root tokens and metadata files. |
| 251 | if (validRootTokens.empty()) return result; |
| 252 | std::unordered_map<std::string, std::string> filtered; |
| 253 | filtered.reserve(result.size()); |
| 254 | for (auto& [cleanName, token] : result) { |
| 255 | if (token.empty() || validRootTokens.count(token) || CloudIntercept::IsInternalMetadataFile(cleanName)) { |
| 256 | filtered[cleanName] = token; |
| 257 | } |
nothing calls this directly
no test coverage detected