winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 8f2747f04a99ae45c9210d1ef9d2478d48309ec8
parent 2d6f8badda43275bb23f9a76765fa702085d8ea2
Author: KEINOS <github+fork-qiita-news@keinos.com>
Date:   Mon, 22 Sep 2025 16:33:53 +0000

Merge remote-tracking branch 'upstream/master'

Diffstat:
M.github/actions/spelling/expect.txt | 1+
Msrc/AppInstallerCLITests/PackageTrackingCatalog.cpp | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp | 40++++++++++++++++++++++++++++++++++------
Msrc/AppInstallerRepositoryCore/Public/winget/PackageTrackingCatalog.h | 10+++++++++-
Msrc/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h | 6++++++
Msrc/AppInstallerSharedLib/SQLiteStorageBase.cpp | 45+++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 141 insertions(+), 7 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -582,6 +582,7 @@ vns vsconfig vstest waitable +wal wcex WDAG webpages diff --git a/src/AppInstallerCLITests/PackageTrackingCatalog.cpp b/src/AppInstallerCLITests/PackageTrackingCatalog.cpp @@ -233,3 +233,49 @@ TEST_CASE("TrackingCatalog_Overlapping_ARP_Range", "[tracking_catalog]") REQUIRE(resultAfter.Matches[0].Package->GetAvailable()[0]->GetLatestVersion()->GetProperty(PackageVersionProperty::Version) == manifest.Version); } + +TEST_CASE("TrackingCatalog_Corrupt", "[tracking_catalog]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SourceDetails details; + Manifest manifest; + std::string relativePath; + auto source = SimpleTestSetup(tempFile, details, manifest, relativePath); + + SearchRequest request; + request.Filters.emplace_back(PackageMatchField::Id, MatchType::Exact, manifest.Id); + + std::filesystem::path catalogFile; + + { + // Add data to initial database + PackageTrackingCatalog catalog = CreatePackageTrackingCatalogForSource(source); + + SearchResult resultBefore = catalog.Search(request); + REQUIRE(resultBefore.Matches.size() == 0); + + catalog.RecordInstall(manifest, manifest.Installers[0], false); + + SearchResult resultAfter = catalog.Search(request); + REQUIRE(resultAfter.Matches.size() == 1); + REQUIRE(resultAfter.Matches[0].Package->GetAvailable().size() == 1); + + catalogFile = catalog.GetFilePath(); + } + + { + std::ofstream file{ catalogFile, std::ios_base::trunc }; + file << "Corrupted!"; + } + + { + // Open database again after "corruption" + PackageTrackingCatalog catalog = CreatePackageTrackingCatalogForSource(source); + + // Should not find anything in new database + SearchResult resultBefore = catalog.Search(request); + REQUIRE(resultBefore.Matches.size() == 0); + } +} diff --git a/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp b/src/AppInstallerRepositoryCore/PackageTrackingCatalog.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::Repository namespace { constexpr std::string_view c_PackageTrackingFileName = "installed.db"; + constexpr std::string_view c_PackageTrackingCorruptedFileName = "installed-corrupted.db"; std::string CreateNameForCPL(const std::string& pathName) { @@ -32,18 +33,38 @@ namespace AppInstaller::Repository } // Call while holding the CrossProcessLock + SQLiteIndex CreateOnlyTrackingIndex(const std::filesystem::path& trackingDB) + { + return SQLiteIndex::CreateNew(trackingDB.u8string(), SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless | SQLiteIndex::CreateOptions::DisableDependenciesSupport); + } + + // Call while holding the CrossProcessLock SQLiteIndex CreateOrOpenTrackingIndex(const std::filesystem::path& trackingDB) { if (!std::filesystem::exists(trackingDB)) { std::filesystem::create_directories(trackingDB.parent_path()); - return SQLiteIndex::CreateNew(trackingDB.u8string(), SQLite::Version::Latest(), SQLiteIndex::CreateOptions::SupportPathless | SQLiteIndex::CreateOptions::DisableDependenciesSupport); + return CreateOnlyTrackingIndex(trackingDB); } else { - // TODO: Check schema version and upgrade as necessary when there is a relevant new schema. - // Could write this all now but it will be better tested when there is a new schema. - return SQLiteIndex::Open(trackingDB.u8string(), SQLiteIndex::OpenDisposition::ReadWrite); + try + { + // TODO: Check schema version and upgrade as necessary when there is a relevant new schema. + // Could write this all now but it will be better tested when there is a new schema. + return SQLiteIndex::Open(trackingDB.u8string(), SQLiteIndex::OpenDisposition::ReadWrite); + } + catch(...) + { + LOG_CAUGHT_EXCEPTION_MSG("Exception opening tracking catalog"); + } + + // Move existing database and create a new one + std::filesystem::path destination{ trackingDB }; + destination.replace_filename(c_PackageTrackingCorruptedFileName); + SQLite::SQLiteStorageBase::RenameSQLiteDatabase(trackingDB, destination, true); + + return CreateOnlyTrackingIndex(trackingDB); } } @@ -205,11 +226,11 @@ namespace AppInstaller::Repository PackageTrackingCatalog::Version::~Version() = default; PackageTrackingCatalog::Version::Version(PackageTrackingCatalog& catalog, std::shared_ptr<implementation>&& value) : - m_catalog(catalog), m_implementation(std::move(value)) {} + m_catalog(&catalog), m_implementation(std::move(value)) {} void PackageTrackingCatalog::Version::SetMetadata(PackageVersionMetadata metadata, const Utility::NormalizedString& value) { - auto& index = m_catalog.m_implementation->Source->GetIndex(); + auto& index = m_catalog->m_implementation->Source->GetIndex(); index.SetMetadataByManifestId(m_implementation->Id, metadata, value); } @@ -287,6 +308,13 @@ namespace AppInstaller::Repository } } +#ifndef AICLI_DISABLE_TEST_HOOKS + std::filesystem::path PackageTrackingCatalog::GetFilePath() const + { + return m_implementation->Source->GetIndex().GetContextData().Get<Schema::Property::DatabaseFilePath>(); + } +#endif + std::unique_ptr<ISourceFactory> PackageTrackingCatalogSourceFactory::Create() { return std::make_unique<PackageTrackingCatalogSourceFactoryImpl>(); diff --git a/src/AppInstallerRepositoryCore/Public/winget/PackageTrackingCatalog.h b/src/AppInstallerRepositoryCore/Public/winget/PackageTrackingCatalog.h @@ -6,6 +6,9 @@ #include <memory> +#ifndef AICLI_DISABLE_TEST_HOOKS +#include <filesystem> +#endif namespace AppInstaller::Repository { @@ -55,7 +58,7 @@ namespace AppInstaller::Repository struct implementation; Version(PackageTrackingCatalog& catalog, std::shared_ptr<implementation>&& value); std::shared_ptr<implementation> m_implementation; - PackageTrackingCatalog& m_catalog; + PackageTrackingCatalog* m_catalog; }; // Records an installation of the given package. @@ -64,6 +67,11 @@ namespace AppInstaller::Repository // Records an uninstall of the given package. void RecordUninstall(const Utility::LocIndString& packageIdentifier); +#ifndef AICLI_DISABLE_TEST_HOOKS + // Gets the path to the database file. + std::filesystem::path GetFilePath() const; +#endif + protected: // Creates or opens the tracking catalog for the given source. static PackageTrackingCatalog CreateForSource(const Source& source); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h @@ -5,6 +5,7 @@ #include <winget/SQLiteVersion.h> #include <winget/ManagedFile.h> +#include <filesystem> #include <mutex> namespace AppInstaller::SQLite @@ -32,6 +33,11 @@ namespace AppInstaller::SQLite // Gets the schema version of the database. const Version& GetVersion() const { return m_version; } + // Renames the database file and any auxiliary files given the inputs. + // Should only be used on an inactive database. + // If overwrite is given, existing destination files will be removed first. + static void RenameSQLiteDatabase(const std::filesystem::path& source, const std::filesystem::path& destination, bool overwrite = false); + protected: SQLiteStorageBase(const std::string& target, const Version& version); diff --git a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp @@ -23,6 +23,20 @@ namespace AppInstaller::SQLite return "Unknown"; } } + + std::filesystem::path AddSuffix(const std::filesystem::path& source, std::wstring_view suffix) + { + std::filesystem::path result{ source }; + + if (!suffix.empty()) + { + std::wstring filename = result.filename().wstring(); + filename += suffix; + result.replace_filename(std::move(filename)); + } + + return result; + } } // One method for converting open disposition to proper open disposition @@ -45,6 +59,37 @@ namespace AppInstaller::SQLite return MetadataTable::TryGetNamedValue<std::string>(m_dbconn, s_MetadataValueName_DatabaseIdentifier).value_or(std::string{}); } + void SQLiteStorageBase::RenameSQLiteDatabase(const std::filesystem::path& source, const std::filesystem::path& destination, bool overwrite) + { + auto fileSuffixes = { L"", L"-journal", L"-wal" }; + + THROW_WIN32_IF(ERROR_FILE_NOT_FOUND, !std::filesystem::exists(source)); + THROW_WIN32_IF(ERROR_DIRECTORY, std::filesystem::is_directory(source)); + + if (overwrite) + { + for (const auto& suffix : fileSuffixes) + { + std::filesystem::path target = AddSuffix(destination, suffix); + + if (std::filesystem::exists(target)) + { + std::filesystem::remove_all(target); + } + } + } + + for (const auto& suffix : fileSuffixes) + { + std::filesystem::path target = AddSuffix(source, suffix); + + if (std::filesystem::exists(target)) + { + std::filesystem::rename(target, AddSuffix(destination, suffix)); + } + } + } + SQLiteStorageBase::SQLiteStorageBase(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& file) : m_indexFile(std::move(file)) {