diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/defaultagent/Cache.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/mozapps/defaultagent/Cache.cpp')
-rw-r--r-- | toolkit/mozapps/defaultagent/Cache.cpp | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/toolkit/mozapps/defaultagent/Cache.cpp b/toolkit/mozapps/defaultagent/Cache.cpp new file mode 100644 index 0000000000..439fdf57f8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Cache.cpp @@ -0,0 +1,590 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Cache.h" + +#include <algorithm> + +#include "common.h" +#include "EventLog.h" +#include "mozilla/Unused.h" + +// Cache entry version documentation: +// Version 1: +// The version number is written explicitly when version 1 cache entries are +// migrated, but in their original location there is no version key. +// Required Keys: +// CacheEntryVersion: <DWORD> +// NotificationType: <string> +// NotificationShown: <string> +// NotificationAction: <string> +// Version 2: +// Required Keys: +// CacheEntryVersion: <DWORD> +// NotificationType: <string> +// NotificationShown: <string> +// NotificationAction: <string> +// PrevNotificationAction: <string> + +static std::wstring MakeVersionedRegSubKey(const wchar_t* baseKey) { + std::wstring key; + if (baseKey) { + key = baseKey; + } else { + key = Cache::kDefaultPingCacheRegKey; + } + key += L"\\version"; + key += std::to_wstring(Cache::kVersion); + return key; +} + +Cache::Cache(const wchar_t* cacheRegKey /* = nullptr */) + : mCacheRegKey(MakeVersionedRegSubKey(cacheRegKey)), + mInitializeResult(mozilla::Nothing()), + mCapacity(Cache::kDefaultCapacity), + mFront(0), + mSize(0) {} + +Cache::~Cache() {} + +VoidResult Cache::Init() { + if (mInitializeResult.isSome()) { + HRESULT hr = mInitializeResult.value(); + if (FAILED(hr)) { + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } else { + return mozilla::Ok(); + } + } + + VoidResult result = SetupCache(); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + mInitializeResult = mozilla::Some(hr); + return result; + } + + // At this point, the cache is ready to use, so mark the initialization as + // complete. This is important so that when we attempt migration, below, + // the migration's attempts to write to the cache don't try to initialize + // the cache again. + mInitializeResult = mozilla::Some(S_OK); + + // Ignore the result of the migration. If we failed to migrate, there may be + // some data loss. But that's better than failing to ever use the new cache + // just because there's something wrong with the old one. + mozilla::Unused << MaybeMigrateVersion1(); + + return mozilla::Ok(); +} + +// If the setting does not exist, the default value is written and returned. +DwordResult Cache::EnsureDwordSetting(const wchar_t* regName, + uint32_t defaultValue) { + MaybeDwordResult readResult = RegistryGetValueDword( + IsPrefixed::Unprefixed, regName, mCacheRegKey.c_str()); + if (readResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to read setting \"%s\": %#X", regName, hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::Maybe<uint32_t> maybeValue = readResult.unwrap(); + if (maybeValue.isSome()) { + return maybeValue.value(); + } + + VoidResult writeResult = RegistrySetValueDword( + IsPrefixed::Unprefixed, regName, defaultValue, mCacheRegKey.c_str()); + if (writeResult.isErr()) { + HRESULT hr = writeResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to write setting \"%s\": %#X", regName, hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + return defaultValue; +} + +// This function does two things: +// 1. It creates and sets the registry values used by the cache, if they don't +// already exist. +// 2. If the the values already existed, it reads the settings of the cache +// into their member variables. +VoidResult Cache::SetupCache() { + DwordResult result = + EnsureDwordSetting(Cache::kCapacityRegName, Cache::kDefaultCapacity); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mCapacity = std::min(result.unwrap(), Cache::kMaxCapacity); + + result = EnsureDwordSetting(Cache::kFrontRegName, 0); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mFront = std::min(result.unwrap(), Cache::kMaxCapacity - 1); + + result = EnsureDwordSetting(Cache::kSizeRegName, 0); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mSize = std::min(result.unwrap(), mCapacity); + + return mozilla::Ok(); +} + +static MaybeStringResult ReadVersion1CacheKey(const wchar_t* baseRegKeyName, + uint32_t index) { + std::wstring regName = Cache::kVersion1KeyPrefix; + regName += baseRegKeyName; + regName += std::to_wstring(index); + + MaybeStringResult result = + RegistryGetValueString(IsPrefixed::Unprefixed, regName.c_str()); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to read \"%s\": %#X", regName.c_str(), hr); + } + return result; +} + +static VoidResult DeleteVersion1CacheKey(const wchar_t* baseRegKeyName, + uint32_t index) { + std::wstring regName = Cache::kVersion1KeyPrefix; + regName += baseRegKeyName; + regName += std::to_wstring(index); + + VoidResult result = + RegistryDeleteValue(IsPrefixed::Unprefixed, regName.c_str()); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to delete \"%s\": %#X", regName.c_str(), hr); + } + return result; +} + +static VoidResult DeleteVersion1CacheEntry(uint32_t index) { + VoidResult typeResult = + DeleteVersion1CacheKey(Cache::kNotificationTypeKey, index); + VoidResult shownResult = + DeleteVersion1CacheKey(Cache::kNotificationShownKey, index); + VoidResult actionResult = + DeleteVersion1CacheKey(Cache::kNotificationActionKey, index); + + if (typeResult.isErr()) { + return typeResult; + } + if (shownResult.isErr()) { + return shownResult; + } + return actionResult; +} + +VoidResult Cache::MaybeMigrateVersion1() { + for (uint32_t index = 0; index < Cache::kVersion1MaxSize; ++index) { + MaybeStringResult typeResult = + ReadVersion1CacheKey(Cache::kNotificationTypeKey, index); + if (typeResult.isErr()) { + return mozilla::Err(typeResult.unwrapErr()); + } + MaybeString maybeType = typeResult.unwrap(); + + MaybeStringResult shownResult = + ReadVersion1CacheKey(Cache::kNotificationShownKey, index); + if (shownResult.isErr()) { + return mozilla::Err(shownResult.unwrapErr()); + } + MaybeString maybeShown = shownResult.unwrap(); + + MaybeStringResult actionResult = + ReadVersion1CacheKey(Cache::kNotificationActionKey, index); + if (actionResult.isErr()) { + return mozilla::Err(actionResult.unwrapErr()); + } + MaybeString maybeAction = actionResult.unwrap(); + + if (maybeType.isSome() && maybeShown.isSome() && maybeAction.isSome()) { + // If something goes wrong, we'd rather lose a little data than migrate + // over and over again. So delete the old entry before we add the new one. + VoidResult result = DeleteVersion1CacheEntry(index); + if (result.isErr()) { + return result; + } + + VersionedEntry entry = VersionedEntry{ + .entryVersion = 1, + .notificationType = maybeType.value(), + .notificationShown = maybeShown.value(), + .notificationAction = maybeAction.value(), + .prevNotificationAction = mozilla::Nothing(), + }; + result = VersionedEnqueue(entry); + if (result.isErr()) { + // We already deleted the version 1 cache entry. No real reason to abort + // now. May as well keep attempting to migrate. + LOG_ERROR_MESSAGE(L"Warning: Version 1 cache entry %u dropped: %#X", + index, result.unwrapErr().AsHResult()); + } + } else if (maybeType.isNothing() && maybeShown.isNothing() && + maybeAction.isNothing()) { + // Looks like we've reached the end of the version 1 cache. + break; + } else { + // This cache entry seems to be missing a key. Just drop it. + LOG_ERROR_MESSAGE( + L"Warning: Version 1 cache entry %u dropped due to missing keys", + index); + mozilla::Unused << DeleteVersion1CacheEntry(index); + } + } + return mozilla::Ok(); +} + +std::wstring Cache::MakeEntryRegKeyName(uint32_t index) { + std::wstring regName = mCacheRegKey; + regName += L'\\'; + regName += std::to_wstring(index); + return regName; +} + +VoidResult Cache::WriteEntryKeys(uint32_t index, const VersionedEntry& entry) { + std::wstring subKey = MakeEntryRegKeyName(index); + + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey, + entry.entryVersion, subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write entry version to index %u: %#X", index, + result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationTypeKey, + entry.notificationType.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationShownKey, + entry.notificationShown.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification shown to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationActionKey, + entry.notificationAction.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + if (entry.prevNotificationAction.isSome()) { + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kPrevNotificationActionKey, + entry.prevNotificationAction.value().c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE( + L"Unable to write prev notification type to index %u: %#X", index, + result.inspectErr().AsHResult()); + return result; + } + } + + return mozilla::Ok(); +} + +// Returns success on an attempt to delete a non-existent entry. +VoidResult Cache::DeleteEntry(uint32_t index) { + std::wstring key = AGENT_REGKEY_NAME; + key += L'\\'; + key += MakeEntryRegKeyName(index); + // We could probably just delete they key here, rather than use this function, + // which deletes keys recursively. But this mechanism allows future entry + // versions to contain sub-keys without causing problems for older versions. + LSTATUS ls = RegDeleteTreeW(HKEY_CURRENT_USER, key.c_str()); + if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) { + return mozilla::Err(mozilla::WindowsError::FromWin32Error(ls)); + } + return mozilla::Ok(); +} + +VoidResult Cache::SetFront(uint32_t newFront) { + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName, + newFront, mCacheRegKey.c_str()); + if (result.isOk()) { + mFront = newFront; + } + return result; +} + +VoidResult Cache::SetSize(uint32_t newSize) { + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName, + newSize, mCacheRegKey.c_str()); + if (result.isOk()) { + mSize = newSize; + } + return result; +} + +// The entry passed to this function MUST already be valid. This function does +// not do any validation internally. We must not, for example, pass an entry +// to it with a version of 2 and a prevNotificationAction of mozilla::Nothing() +// because a version 2 entry requires that key. +VoidResult Cache::VersionedEnqueue(const VersionedEntry& entry) { + VoidResult result = Init(); + if (result.isErr()) { + return result; + } + + if (mSize >= mCapacity) { + LOG_ERROR_MESSAGE(L"Attempted to add an entry to the cache, but it's full"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS)); + } + + uint32_t index = (mFront + mSize) % mCapacity; + + // We really don't want to write to a location that has stale cache entry data + // already lying around. + result = DeleteEntry(index); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to remove stale entry: %#X", + result.inspectErr().AsHResult()); + return result; + } + + result = WriteEntryKeys(index, entry); + if (result.isErr()) { + // We might have written a partial key. Attempt to clean up after ourself. + mozilla::Unused << DeleteEntry(index); + return result; + } + + result = SetSize(mSize + 1); + if (result.isErr()) { + // If we failed to write the size, the new entry was not added successfully. + // Attempt to clean up after ourself. + mozilla::Unused << DeleteEntry(index); + return result; + } + + return mozilla::Ok(); +} + +VoidResult Cache::Enqueue(const Cache::Entry& entry) { + Cache::VersionedEntry vEntry = Cache::VersionedEntry{ + .entryVersion = Cache::kEntryVersion, + .notificationType = entry.notificationType, + .notificationShown = entry.notificationShown, + .notificationAction = entry.notificationAction, + .prevNotificationAction = mozilla::Some(entry.prevNotificationAction), + }; + return VersionedEnqueue(vEntry); +} + +VoidResult Cache::DiscardFront() { + if (mSize < 1) { + LOG_ERROR_MESSAGE(L"Attempted to discard entry from an empty cache"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS)); + } + // It's not a huge deal if we can't delete this. Moving mFront will result in + // it being excluded from the cache anyways. We'll try to delete it again + // anyways if we try to write to this index again. + mozilla::Unused << DeleteEntry(mFront); + + VoidResult result = SetSize(mSize - 1); + // We don't really need to bother moving mFront to the next index if the cache + // is empty. + if (result.isErr() || mSize == 0) { + return result; + } + result = SetFront((mFront + 1) % mCapacity); + if (result.isErr()) { + // If we failed to set the front after we set the size, the cache is + // in an inconsistent state. + // But, even if the cache is inconsistent, we'll likely lose some data, but + // we should eventually be able to recover. Any expected entries with no + // data will be discarded and any unexpected entries with data will be + // cleared out before we write data there. + LOG_ERROR_MESSAGE(L"Cache inconsistent: Updated Size but not Front: %#X", + result.inspectErr().AsHResult()); + } + return result; +} + +/** + * This function reads a DWORD cache key's value and returns it. If the expected + * argument is true and the key is missing, this will delete the entire entry + * and return mozilla::Nothing(). + */ +MaybeDwordResult Cache::ReadEntryKeyDword(const std::wstring& regKey, + const wchar_t* regName, + bool expected) { + MaybeDwordResult result = + RegistryGetValueDword(IsPrefixed::Unprefixed, regName, regKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName, + regKey.c_str(), result.inspectErr().AsHResult()); + return mozilla::Err(result.unwrapErr()); + } + MaybeDword maybeValue = result.unwrap(); + if (expected && maybeValue.isNothing()) { + LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName, + regKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + } + return maybeValue; +} + +/** + * This function reads a string cache key's value and returns it. If the + * expected argument is true and the key is missing, this will delete the entire + * entry and return mozilla::Nothing(). + */ +MaybeStringResult Cache::ReadEntryKeyString(const std::wstring& regKey, + const wchar_t* regName, + bool expected) { + MaybeStringResult result = + RegistryGetValueString(IsPrefixed::Unprefixed, regName, regKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName, + regKey.c_str(), result.inspectErr().AsHResult()); + return mozilla::Err(result.unwrapErr()); + } + MaybeString maybeValue = result.unwrap(); + if (expected && maybeValue.isNothing()) { + LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName, + regKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + } + return maybeValue; +} + +Cache::MaybeEntryResult Cache::Dequeue() { + VoidResult result = Init(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + + std::wstring subKey = MakeEntryRegKeyName(mFront); + + // We are going to read within a loop so that if we find incomplete entries, + // we can just discard them and try to read the next entry. We'll put a limit + // on the maximum number of times this loop can possibly run so that if + // something goes horribly wrong, we don't loop forever. If we exit this loop + // without returning, it means that not only were we not able to read + // anything, but something very unexpected happened. + // We are going to potentially loop over this mCapacity + 1 times so that if + // we end up discarding every item in the cache, we return mozilla::Nothing() + // rather than an error. + for (uint32_t i = 0; i <= mCapacity; ++i) { + if (mSize == 0) { + return MaybeEntry(mozilla::Nothing()); + } + + Cache::VersionedEntry entry; + + // CacheEntryVersion + MaybeDwordResult dResult = + ReadEntryKeyDword(subKey, Cache::kEntryVersionKey, true); + if (dResult.isErr()) { + return mozilla::Err(dResult.unwrapErr()); + } + MaybeDword maybeDValue = dResult.unwrap(); + if (maybeDValue.isNothing()) { + // Note that we only call continue in this function after DiscardFront() + // has been called (either directly, or by one of the ReadEntryKey.* + // functions). So the continue call results in attempting to read the + // next entry in the cache. + continue; + } + entry.entryVersion = maybeDValue.value(); + if (entry.entryVersion < 1) { + LOG_ERROR_MESSAGE(L"Invalid entry version of %u in \"%s\"", + entry.entryVersion, subKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + continue; + } + + // NotificationType + MaybeStringResult sResult = + ReadEntryKeyString(subKey, Cache::kNotificationTypeKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + MaybeString maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationType = maybeSValue.value(); + + // NotificationShown + sResult = ReadEntryKeyString(subKey, Cache::kNotificationShownKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationShown = maybeSValue.value(); + + // NotificationAction + sResult = ReadEntryKeyString(subKey, Cache::kNotificationActionKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationAction = maybeSValue.value(); + + // PrevNotificationAction + bool expected = + entry.entryVersion >= Cache::kInitialVersionPrevNotificationActionKey; + sResult = + ReadEntryKeyString(subKey, Cache::kPrevNotificationActionKey, expected); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (expected && maybeSValue.isNothing()) { + continue; + } + entry.prevNotificationAction = maybeSValue; + + // We successfully read the entry. Now we need to remove it from the cache. + VoidResult result = DiscardFront(); + if (result.isErr()) { + // If we aren't able to remove the entry from the cache, don't return it. + // We don't want to return the same item over and over again if we get + // into a bad state. + return mozilla::Err(result.unwrapErr()); + } + + return mozilla::Some(entry); + } + + LOG_ERROR_MESSAGE(L"Unexpected: This line shouldn't be reached"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_FAIL)); +} |