diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 2b75ac9a91..7231a00be5 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -4,6 +4,7 @@ ACTIONDATA ACTIONSTART addfile addmanifest +addpin addportablefile addstore admins @@ -94,6 +95,7 @@ cpprestsdk cppwinrt CPRWL createnew +createpintable createportabletable createtables cref @@ -253,6 +255,7 @@ INVALIDARG INVALIDSID iomanip iostream +IPinning IPortable ipmo ISAPPROVEDFOROUTPUT @@ -389,6 +392,7 @@ pfp PGP php PII +pinningindex pipssource PKCS placeholders @@ -441,10 +445,12 @@ regex regexp removefile removemanifest +removepin removeportablefile repolibtest requeue rescap +resetpins resheader resmimetype RESOLVESOURCE @@ -623,6 +629,7 @@ Unregisters untimes updatefile updatemanifest +updatepin updateportablefile UPLEVEL upvote diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index effaa5d8b7..e43c58fb68 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -341,6 +341,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 53e912fc82..f2983e3f44 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -349,8 +349,11 @@ Source Files + + Workflows + - Source Files + Commands diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp index 207e636133..81a94755d1 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.cpp +++ b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -76,15 +76,26 @@ namespace AppInstaller::CLI void PinAddCommand::Complete(Execution::Context& context, Args::Type valueType) const { + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + switch (valueType) { - case Args::Type::Query: - case Args::Type::Id: - case Args::Type::Name: - case Args::Type::Moniker: - case Args::Type::Source: + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Source: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: context << - Workflow::CompleteWithSingleSemanticsForValue(valueType); + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); break; } } @@ -105,8 +116,17 @@ namespace AppInstaller::CLI void PinAddCommand::ExecuteInternal(Execution::Context& context) const { - // TODO - Command::ExecuteInternal(context); + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::SearchSourceForSingle << + Workflow::HandleSearchResultFailures << + Workflow::EnsureOneMatchFromSearchResult(false) << + Workflow::GetInstalledPackageVersion << + Workflow::ReportPackageIdentity << + Workflow::OpenPinningIndex() << + Workflow::SearchPin << + Workflow::AddPin; } std::vector PinRemoveCommand::GetArguments() const @@ -137,15 +157,26 @@ namespace AppInstaller::CLI void PinRemoveCommand::Complete(Execution::Context& context, Args::Type valueType) const { + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + switch (valueType) { - case Args::Type::Query: - case Args::Type::Id: - case Args::Type::Name: - case Args::Type::Moniker: - case Args::Type::Source: + case Execution::Args::Type::Query: context << - Workflow::CompleteWithSingleSemanticsForValue(valueType); + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Source: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); break; } } @@ -157,8 +188,17 @@ namespace AppInstaller::CLI void PinRemoveCommand::ExecuteInternal(Execution::Context& context) const { - // TODO - Command::ExecuteInternal(context); + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::SearchSourceForSingle << + Workflow::HandleSearchResultFailures << + Workflow::EnsureOneMatchFromSearchResult(false) << + Workflow::GetInstalledPackageVersion << + Workflow::ReportPackageIdentity << + Workflow::OpenPinningIndex() << + Workflow::SearchPin << + Workflow::RemovePin; } std::vector PinListCommand::GetArguments() const @@ -189,15 +229,26 @@ namespace AppInstaller::CLI void PinListCommand::Complete(Execution::Context& context, Args::Type valueType) const { + context << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + switch (valueType) { - case Args::Type::Query: - case Args::Type::Id: - case Args::Type::Name: - case Args::Type::Moniker: - case Args::Type::Source: + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Source: + case Execution::Args::Type::Tag: + case Execution::Args::Type::Command: context << - Workflow::CompleteWithSingleSemanticsForValue(valueType); + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); break; } } @@ -209,14 +260,20 @@ namespace AppInstaller::CLI void PinListCommand::ExecuteInternal(Execution::Context& context) const { - // TODO - Command::ExecuteInternal(context); + context << + Workflow::OpenPinningIndex(/* readOnly */ true) << + Workflow::GetAllPins << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::CrossReferencePinsWithSource << + Workflow::ReportPins; } std::vector PinResetCommand::GetArguments() const { return { Argument::ForType(Args::Type::Force), + Argument::ForType(Args::Type::Source), }; } @@ -237,7 +294,25 @@ namespace AppInstaller::CLI void PinResetCommand::ExecuteInternal(Execution::Context& context) const { - // TODO - Command::ExecuteInternal(context); + + if (context.Args.Contains(Execution::Args::Type::Force)) + { + context << + Workflow::OpenPinningIndex() << + Workflow::ResetAllPins; + } + else + { + AICLI_LOG(CLI, Info, << "--force argument is not present"); + context.Reporter.Info() << Resource::String::PinResetUseForceArg << std::endl; + + context << + Workflow::OpenPinningIndex(/* readOnly */ true) << + Workflow::GetAllPins << + Workflow::OpenSource() << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::CrossReferencePinsWithSource << + Workflow::ReportPins; + } } } diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index 9ab78e1873..6011e4b397 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "CompletionData.h" #include "PackageCollection.h" #include "PortableInstaller.h" @@ -16,6 +17,10 @@ #include #include +namespace AppInstaller::Repository::Microsoft +{ + struct PinningIndex; +} namespace AppInstaller::CLI::Execution { @@ -55,6 +60,8 @@ namespace AppInstaller::CLI::Execution AllowedArchitectures, AllowUnknownScope, PortableInstaller, + PinningIndex, + Pins, Max }; @@ -229,5 +236,17 @@ namespace AppInstaller::CLI::Execution { using value_t = CLI::Portable::PortableInstaller; }; + + template <> + struct DataMapping + { + using value_t = std::shared_ptr; + }; + + template <> + struct DataMapping + { + using value_t = std::vector; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 66204f4735..432c77c270 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -244,14 +244,25 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinAddBlockingArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinAdded); + WINGET_DEFINE_RESOURCE_STRINGID(PinAlreadyExists); + WINGET_DEFINE_RESOURCE_STRINGID(PinCannotOpenIndex); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist); + WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting); + WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful); + WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll); + WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); + WINGET_DEFINE_RESOURCE_STRINGID(PinType); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy); diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp new file mode 100644 index 0000000000..55f2294348 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Resources.h" +#include "PinFlow.h" +#include "TableOutput.h" +#include "Microsoft/PinningIndex.h" +#include "Microsoft/SQLiteStorageBase.h" +#include "winget/RepositorySearch.h" + +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Microsoft; + +namespace AppInstaller::CLI::Workflow +{ + namespace + { + // Creates a Pin appropriate for the context based on the arguments provided + Pinning::Pin CreatePin(Execution::Context& context, std::string_view packageId, std::string_view sourceId) + { + if (context.Args.Contains(Execution::Args::Type::GatedVersion)) + { + return Pinning::Pin::CreateGatingPin({ packageId, sourceId }, context.Args.GetArg(Execution::Args::Type::GatedVersion)); + } + else if (context.Args.Contains(Execution::Args::Type::BlockingPin)) + { + return Pinning::Pin::CreateBlockingPin({ packageId, sourceId }); + } + else + { + return Pinning::Pin::CreatePinningPin({ packageId, sourceId }); + } + } + } + + void OpenPinningIndex::operator()(Execution::Context& context) const + { + auto openDisposition = m_readOnly ? SQLiteStorageBase::OpenDisposition::Read : SQLiteStorageBase::OpenDisposition::ReadWrite; + auto pinningIndex = PinningIndex::OpenOrCreateDefault(openDisposition); + if (!pinningIndex) + { + AICLI_LOG(CLI, Error, << "Unable to open pinning index."); + context.Reporter.Error() << Resource::String::PinCannotOpenIndex << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX); + } + + context.Add(std::move(pinningIndex)); + } + + void GetAllPins(Execution::Context& context) + { + AICLI_LOG(CLI, Info, << "Getting all existing pins"); + context.Add(context.Get()->GetAllPins()); + } + + void SearchPin(Execution::Context& context) + { + auto package = context.Get(); + std::vector pins; + + // TODO: We should support querying the multiple sources for a package, instead of just one + auto availableVersion = package->GetLatestAvailableVersion(); + + auto pinningIndex = context.Get(); + auto pin = pinningIndex->GetPin({ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }); + if (pin) + { + pins.emplace_back(std::move(pin.value())); + } + + context.Add(std::move(pins)); + } + + void AddPin(Execution::Context& context) + { + auto package = context.Get(); + auto installedVersion = context.Get(); + + std::vector pins; + + // TODO: We should support querying the multiple sources for a package, instead of just one + auto availableVersion = package->GetLatestAvailableVersion(); + + Pinning::PinKey pinKey{ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; + auto pin = CreatePin(context, pinKey.PackageId, pinKey.SourceId); + AICLI_LOG(CLI, Info, << "Adding pin with type " << ToString(pin.GetType()) << " for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "]"); + + + auto pinningIndex = context.Get(); + auto existingPin = pinningIndex->GetPin(pinKey); + + if (existingPin) + { + // Pin already exists. + // If it is the same, we do nothing. If it is different, check for the --force arg + if (pin == existingPin) + { + AICLI_LOG(CLI, Info, << "Pin already exists"); + context.Reporter.Info() << Resource::String::PinAlreadyExists << std::endl; + return; + } + + AICLI_LOG(CLI, Info, << "Another pin already exists for the package"); + if (context.Args.Contains(Execution::Args::Type::Force)) + { + AICLI_LOG(CLI, Info, << "Overwriting pin due to --force argument"); + context.Reporter.Warn() << Resource::String::PinExistsOverwriting << std::endl; + pinningIndex->UpdatePin(pin); + } + else + { + context.Reporter.Error() << Resource::String::PinExistsUseForceArg << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS); + } + } + else + { + pinningIndex->AddPin(pin); + AICLI_LOG(CLI, Info, << "Finished adding pin"); + context.Reporter.Info() << Resource::String::PinAdded << std::endl; + } + } + + void RemovePin(Execution::Context& context) + { + auto package = context.Get(); + std::vector pins; + + // TODO: We should support querying the multiple sources for a package, instead of just one + auto availableVersion = package->GetLatestAvailableVersion(); + + auto pinningIndex = context.Get(); + Pinning::PinKey pinKey{ + availableVersion->GetProperty(PackageVersionProperty::Id).get(), + availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() }; + AICLI_LOG(CLI, Info, << "Removing pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + if (!pinningIndex->GetPin(pinKey)) + { + AICLI_LOG(CLI, Warning, << "Pin does not exist"); + context.Reporter.Warn() << Resource::String::PinDoesNotExist(pinKey.PackageId) << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + } + + pinningIndex->RemovePin(pinKey); + } + + void ReportPins(Execution::Context& context) + { + const auto& pins = context.Get(); + if (pins.empty()) + { + context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; + return; + } + + // TODO: Use package and source names + Execution::TableOutput<4> table(context.Reporter, + { + Resource::String::SearchId, + Resource::String::SearchSource, + Resource::String::SearchVersion, + Resource::String::PinType, + }); + + for (const auto& pin : pins) + { + // TODO: Avoid these conversions to string + table.OutputLine({ + pin.GetPackageId(), + std::string{ pin.GetSourceId() }, + pin.GetGatedVersion().ToString(), + std::string{ ToString(pin.GetType()) }, + }); + } + + table.Complete(); + } + + void ResetAllPins(Execution::Context& context) + { + AICLI_LOG(CLI, Info, << "Resetting all pins"); + context.Reporter.Info() << Resource::String::PinResettingAll << std::endl; + + std::string sourceId; + if (context.Args.Contains(Execution::Args::Type::Source)) + { + auto sourceName = context.Args.GetArg(Execution::Args::Type::Source); + auto sources = Source::GetCurrentSources(); + for (const auto& source : sources) + { + if (Utility::CaseInsensitiveEquals(source.Name, sourceName)) + { + sourceId = source.Identifier; + break; + } + } + } + + if (context.Get()->ResetAllPins(sourceId)) + { + context.Reporter.Info() << Resource::String::PinResetSuccessful << std::endl; + } + else + { + context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; + } + } + + void CrossReferencePinsWithSource(Execution::Context& context) + { + const auto& pins = context.Get(); + const auto& source = context.Get(); + + std::vector matchingPins; + std::copy_if(pins.begin(), pins.end(), std::back_inserter(matchingPins), [&](Pinning::Pin pin) { + // TODO: Filter to source + SearchRequest searchRequest; + searchRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, pin.GetPackageId()); + auto searchResult = source.Search(searchRequest); + + return !searchResult.Matches.empty(); + }); + + context.Add(std::move(matchingPins)); + } + +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h index 504ed5782e..fd4a5fce2c 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.h +++ b/src/AppInstallerCLICore/Workflows/PinFlow.h @@ -5,4 +5,61 @@ namespace AppInstaller::CLI::Workflow { + // Opens the pinning index for use in future operations. + // Required Args: None + // Inputs: None + // Outputs: PinningIndex + struct OpenPinningIndex : public WorkflowTask + { + OpenPinningIndex(bool readOnly = false) : WorkflowTask("OpenPinningIndex"), m_readOnly(readOnly) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_readOnly; + }; + + // Gets all the pins from the index. + // Required Args: None + // Inputs: PinningIndex + // Outputs: Pins + void GetAllPins(Execution::Context& context); + + // Searches for all the pins associated with a package. + // There may be several if a package is available from multiple sources. + // Required Args: None + // Inputs: PinningIndex, Package + // Outputs Pins + void SearchPin(Execution::Context& context); + + // Adds a pin for the current package. + // A separate pin will be added for each source. + // Required Args: None + // Inputs: PinningIndex, Package + // Outputs: None + void AddPin(Execution::Context& context); + + // Removes all the pins associated with a package. + // Required Args: None + // Inputs: PinningIndex, Package, InstalledPackageVersion + // Outputs: None + void RemovePin(Execution::Context& context); + + // Report the pins in a table. + // Required Args: None + // Inputs: Pins + // Outputs: None + void ReportPins(Execution::Context& context); + + // Resets all the existing pins. + // Required Args: None + // Inputs: None + // Outputs: None + void ResetAllPins(Execution::Context& context); + + // Updates the list of pins to include only those matching the current open source + // Required Args: None + // Inputs: Pins, Source + // Outputs: None + void CrossReferencePinsWithSource(Execution::Context& context); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 23bc85bf23..182670813d 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1605,4 +1605,44 @@ Please specify one of them using the --source option to proceed. Select installed package scope filter (user or machine) This argument allows the user to select installed packages for just the user or for the entire machine. - + + Pin added successfully + + + The pin already exists + + + A pin already exists for this package. Overwriting due to the --force argument. + {Locked="--force"} + + + A pin already exists for this package. Use the --force argument to overwrite it. + {Locked="--force"} + + + Resetting all current pins. + + + Use the --force argument to reset all pins. The following pins would be removed: + {Locked="--force"} + + + Pin type + + + There is no pin for package {0} + {Locked="{0}"} {0} is a placeholder that will be replaced by a package name. The message is shown when attempting to delete a pin for a package that is not pinned. + + + There are no pins configured. + Shown when listing or modifying existing pins if there are none. + + + Pins reset successfully + Shown after resetting (deleting) all the pins + + + Unable to open pin database. + Error message for when we cannot open the database containing package pins. + + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index ca1a736e72..f6c35bb3a9 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -219,6 +219,8 @@ + + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index f98d82be41..21072d3f32 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -275,6 +275,12 @@ Source Files\CLI + + Source Files\Repository + + + Source Files\CLI + diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp new file mode 100644 index 0000000000..42989fc5da --- /dev/null +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "WorkflowCommon.h" +#include +#include +#include +#include + +using namespace TestCommon; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Workflow; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Pinning; + +void OverrideForOpenPinningIndex(TestContext& context, const std::filesystem::path& indexPath) +{ + context.Override({ "OpenPinningIndex", [=](TestContext& context) + { + auto pinningIndex = std::filesystem::exists(indexPath) ? + PinningIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) : + PinningIndex::CreateNew(indexPath.u8string()); + context.Add(std::make_shared(std::move(pinningIndex))); + } }); +} + +TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForOpenPinningIndex(addContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR:: TestInstaller_Exe })); + addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + addContext.Args.AddArg(Execution::Args::Type::BlockingPin); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + SECTION("Pin is saved") + { + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetType() == PinType::Blocking); + REQUIRE(pins[0].GetPackageId() == "AppInstallerCliTest.TestExeInstaller"); + REQUIRE(pins[0].GetSourceId() == "*TestSource"); + REQUIRE(pins[0].GetGatedVersion().ToString() == ""); + + std::ostringstream pinListOutput; + TestContext listContext{ pinListOutput, std::cin }; + OverrideForOpenPinningIndex(listContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(listContext, CreateTestSource({ TSR::TestInstaller_Exe })); + listContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + + PinListCommand pinList({}); + pinList.Execute(listContext); + + INFO(pinListOutput.str()); + REQUIRE(pinListOutput.str().find("AppInstallerCliTest.TestExeInstaller")); + REQUIRE(pinListOutput.str().find("Blocking")); + } + SECTION("Remove pin") + { + std::ostringstream pinRemoveOutput; + TestContext removeContext{ pinRemoveOutput, std::cin }; + OverrideForOpenPinningIndex(removeContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(removeContext, CreateTestSource({ TSR::TestInstaller_Exe })); + removeContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + + PinRemoveCommand pinRemove({}); + pinRemove.Execute(removeContext); + INFO(pinRemoveOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.empty()); + } + SECTION("Reset pins") + { + std::ostringstream pinResetOutput; + TestContext resetContext{ pinResetOutput, std::cin }; + OverrideForOpenPinningIndex(resetContext, indexFile.GetPath()); + + SECTION("Without --force") + { + OverrideForCompositeInstalledSource(resetContext, CreateTestSource({ TSR::TestInstaller_Exe })); + PinResetCommand pinReset({}); + pinReset.Execute(resetContext); + INFO(pinResetOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + } + SECTION("With --force") + { + resetContext.Args.AddArg(Execution::Args::Type::Force); + + PinResetCommand pinReset({}); + pinReset.Execute(resetContext); + INFO(pinResetOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.empty()); + } + } + SECTION("Update pin") + { + std::ostringstream pinUpdateOutput; + TestContext updateContext{ pinUpdateOutput, std::cin }; + OverrideForOpenPinningIndex(updateContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(updateContext, CreateTestSource({ TSR::TestInstaller_Exe })); + updateContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + + SECTION("Without --force") + { + PinAddCommand pinUpdate({}); + pinUpdate.Execute(updateContext); + INFO(pinUpdateOutput.str()); + REQUIRE_TERMINATED_WITH(updateContext, APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetType() == PinType::Blocking); + } + SECTION("With --force") + { + updateContext.Args.AddArg(Execution::Args::Type::Force); + + PinAddCommand pinUpdate({}); + pinUpdate.Execute(updateContext); + INFO(pinUpdateOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetType() == PinType::Pinning); + } + } +} + +TEST_CASE("PinFlow_Add_NotFound", "[PinFlow][workflow]") +{ + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForCompositeInstalledSource(addContext, CreateTestSource({})); + addContext.Args.AddArg(Execution::Args::Type::Query, "This package doesn't exist"sv); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + REQUIRE_TERMINATED_WITH(addContext, APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); +} + +TEST_CASE("PinFlow_ListEmpty", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + + std::ostringstream pinListOutput; + TestContext listContext{ pinListOutput, std::cin }; + OverrideForOpenPinningIndex(listContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(listContext, CreateTestSource({})); + + PinListCommand pinList({}); + pinList.Execute(listContext); + INFO(pinListOutput.str()); + + REQUIRE(pinListOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos); +} + +TEST_CASE("PinFlow_RemoveNonExisting", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + + std::ostringstream pinRemoveOutput; + TestContext removeContext{ pinRemoveOutput, std::cin }; + OverrideForOpenPinningIndex(removeContext, indexFile.GetPath()); + OverrideForCompositeInstalledSource(removeContext, CreateTestSource({ TSR::TestInstaller_Exe })); + removeContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + + PinRemoveCommand pinRemove({}); + pinRemove.Execute(removeContext); + INFO(pinRemoveOutput.str()); + + REQUIRE_TERMINATED_WITH(removeContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); +} + +TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + + std::ostringstream pinResetOutput; + TestContext resetContext{ pinResetOutput, std::cin }; + OverrideForOpenPinningIndex(resetContext, indexFile.GetPath()); + resetContext.Args.AddArg(Execution::Args::Type::Force); + + PinResetCommand pinReset({}); + pinReset.Execute(resetContext); + INFO(pinResetOutput.str()); + + REQUIRE(pinResetOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos); +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp new file mode 100644 index 0000000000..b22e97c762 --- /dev/null +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace TestCommon; +using namespace AppInstaller::Pinning; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Repository::SQLite; +using namespace AppInstaller::Repository::Microsoft::Schema; + +TEST_CASE("PinningIndexCreateLatestAndReopen", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Schema::Version versionCreated; + + // Create the index + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Schema::Version::Latest()); + versionCreated = index.GetVersion(); + } + + // Reopen the index for read only + { + INFO("Trying with Read"); + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read); + Schema::Version versionRead = index.GetVersion(); + REQUIRE(versionRead == versionCreated); + } + + // Reopen the index for read/write + { + INFO("Trying with ReadWrite"); + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + Schema::Version versionRead = index.GetVersion(); + REQUIRE(versionRead == versionCreated); + } + + // Reopen the index for immutable read + { + INFO("Trying with Immutable"); + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Immutable); + Schema::Version versionRead = index.GetVersion(); + REQUIRE(versionRead == versionCreated); + } +} + +TEST_CASE("PinningIndexAddEntryToTable", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "pkgId", "sourceId" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin); + } + + { + // Open it directly to directly test table state + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + + auto pins = Pinning_V1_0::PinTable::GetAllPins(connection); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0] == pin); + + auto pinFromIndex = Pinning_V1_0::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex.value() == pin); + + REQUIRE(pinFromIndex->GetType() == pin.GetType()); + REQUIRE(pinFromIndex->GetPackageId() == pin.GetPackageId()); + REQUIRE(pinFromIndex->GetSourceId() == pin.GetSourceId()); + } + + { + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemovePin(pin.GetKey()); + } + + { + // Open it directly to directly test table state + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + REQUIRE(Pinning_V1_0::PinTable::GetAllPins(connection).empty()); + REQUIRE(!Pinning_V1_0::PinTable::GetPinById(connection, 1)); + } +} + +TEST_CASE("PinningIndex_AddUpdateRemove", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*" }); + Pin updatedPin = Pin::CreatePinningPin({ "pkgId", "srcId" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin); + REQUIRE(index.UpdatePin(updatedPin)); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + auto pinFromIndex = Pinning_V1_0::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex.value() == updatedPin); + } + + { + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemovePin(updatedPin.GetKey()); + } + + { + // Open it directly to directly test table state + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + REQUIRE(Pinning_V1_0::PinTable::GetAllPins(connection).empty()); + REQUIRE(!Pinning_V1_0::PinTable::GetPinById(connection, 1)); + } +} + +TEST_CASE("PinningIndex_ResetAll", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin1 = Pin::CreateBlockingPin({ "pkg1", "src1" }); + Pin pin2 = Pin::CreatePinningPin({ "pkg2", "src2" }); + + // Add two pins to the index, then check that they show up when queried + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin1); + index.AddPin(pin2); + + REQUIRE(index.GetAllPins().size() == 2); + REQUIRE(index.GetPin(pin1.GetKey()).has_value()); + REQUIRE(index.GetPin(pin2.GetKey()).has_value()); + REQUIRE(!index.GetPin({ "pkg", "src" }).has_value()); + + // Reset the index, then check that there are no pins + index.ResetAllPins(); + REQUIRE(index.GetAllPins().empty()); + REQUIRE(!index.GetPin(pin1.GetKey()).has_value()); + REQUIRE(!index.GetPin(pin2.GetKey()).has_value()); +} + +TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateGatingPin({ "pkg", "src" }, { "1.*" }); + + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin); + + REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 3b6d5a2f66..88ed0f5aa7 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -326,6 +326,7 @@ + @@ -394,6 +395,7 @@ + diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 3f4da4c727..5a7a358352 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -222,6 +222,9 @@ Public\winget + + Public\winget + @@ -395,6 +398,9 @@ Source Files + + Source Files + diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index 0dce355f0e..524920f0fe 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -208,6 +208,12 @@ namespace AppInstaller return "Archive malware scan failed."; case APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED: return "Found at least one version of the package installed."; + case APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS: + return "A pin already exists for the package."; + case APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST: + return "There is no pin for the package."; + case APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX: + return "Unable to open the pin database."; // Install errors case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: diff --git a/src/AppInstallerCommonCore/Pin.cpp b/src/AppInstallerCommonCore/Pin.cpp new file mode 100644 index 0000000000..d76d0f4ea4 --- /dev/null +++ b/src/AppInstallerCommonCore/Pin.cpp @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/Pin.h" + +using namespace AppInstaller::Utility; + +namespace AppInstaller::Pinning +{ + using namespace std::string_view_literals; + + std::string_view ToString(PinType type) + { + switch (type) + { + case PinType::Blocking: + return "Blocking"sv; + case PinType::Pinning: + return "Pinning"sv; + case PinType::Gating: + return "Gating"sv; + case PinType::Unknown: + default: + return "Unknown"; + } + } + + Pin Pin::CreateBlockingPin(PinKey&& pinKey) + { + return { PinType::Blocking, std::move(pinKey) }; + } + + Pin Pin::CreatePinningPin(PinKey&& pinKey) + { + return { PinType::Pinning, std::move(pinKey) }; + } + + Pin Pin::CreateGatingPin(PinKey&& pinKey, GatedVersion&& gatedVersion) + { + return { PinType::Gating, std::move(pinKey), std::move(gatedVersion) }; + } + + PinType Pin::GetType() const + { + return m_type; + } + + const PinKey& Pin::GetKey() const + { + return m_key; + } + + const AppInstaller::Manifest::Manifest::string_t& Pin::GetPackageId() const + { + return m_key.PackageId; + } + + std::string_view Pin::GetSourceId() const + { + return m_key.SourceId; + } + + Utility::GatedVersion Pin::GetGatedVersion() const + { + return m_gatedVersion; + } + + bool Pin::operator==(const Pin& other) const + { + return m_type == other.m_type && + m_key == other.m_key && + m_gatedVersion == other.m_gatedVersion; + } +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 130a556b0b..f7864f751a 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -110,6 +110,9 @@ #define APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED ((HRESULT)0x8A15005F) #define APPINSTALLER_CLI_ERROR_ARCHIVE_SCAN_FAILED ((HRESULT)0x8A150060) #define APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED ((HRESULT)0x8A150061) +#define APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS ((HRESULT)0x8A150062) +#define APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST ((HRESULT)0x8A150063) +#define APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX ((HRESULT)0x8A150064) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h index 954c76a12b..6b4a01cd0c 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h @@ -169,6 +169,21 @@ namespace AppInstaller::Utility bool m_isEmpty = false; }; + // A range of versions indicated by a version and optionally a wildcard at the end. + struct GatedVersion + { + // TODO + // For now, using dummy implementation that just holds a string + GatedVersion() {} + GatedVersion(std::string_view s) : m_tmp(s) {} + std::string ToString() const { return m_tmp; } + + bool operator==(const GatedVersion& other) const { return m_tmp == other.m_tmp; } + + private: + std::string m_tmp; + }; + // A channel string; existing solely to give a type. // // Compared lexicographically. diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h new file mode 100644 index 0000000000..3a617e18ba --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winget/Manifest.h" +#include "AppInstallerVersions.h" + +namespace AppInstaller::Pinning +{ + enum class PinType + { + Unknown, + // The package is blocked from 'upgrade --all' and 'upgrade '. + // User has to unblock to allow update. + Blocking, + // The package is excluded from 'upgrade --all', unless '--include-pinned' is added. + // 'upgrade ' is not blocked. + Pinning, + // The package is pinned to a specific version. + Gating, + }; + + std::string_view ToString(PinType type); + + // The set of values needed to uniquely identify a Pin + struct PinKey + { + PinKey() {} + PinKey(const Manifest::Manifest::string_t& packageId, std::string_view sourceId) + : PackageId(packageId), SourceId(sourceId) {} + + bool operator==(const PinKey& other) const + { + return PackageId == other.PackageId && SourceId == other.SourceId; + } + bool operator!=(const PinKey& other) const + { + return !(*this == other); + } + + Manifest::Manifest::string_t PackageId; + std::string SourceId; + }; + + struct Pin + { + static Pin CreateBlockingPin(PinKey&& pinKey); + static Pin CreatePinningPin(PinKey&& pinKey); + static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion); + + PinType GetType() const; + const PinKey& GetKey() const; + const Manifest::Manifest::string_t& GetPackageId() const; + std::string_view GetSourceId() const; + Utility::GatedVersion GetGatedVersion() const; + + bool operator==(const Pin& other) const; + + private: + Pin(PinType type, PinKey&& pinKey, Utility::GatedVersion&& gatedVersion = {}) + : m_type(type), m_key(std::move(pinKey)), m_gatedVersion(std::move(gatedVersion)) {} + + PinType m_type = PinType::Unknown; + PinKey m_key; + Utility::GatedVersion m_gatedVersion; + }; +} diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 2f97d0b62e..fad805b911 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -237,6 +237,7 @@ + @@ -273,8 +274,11 @@ + + + @@ -339,6 +343,7 @@ + @@ -361,6 +366,8 @@ + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 07bfc49bc3..8ffbdc73ca 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -76,6 +76,9 @@ {d9d70cf5-ce04-4db2-a0ec-970dd0ad22b6} + + {f05e19bb-2161-4ab0-9d04-2dfa2d3eb3c6} + @@ -339,6 +342,18 @@ Rest\Schema + + Microsoft\Schema + + + Microsoft + + + Microsoft\Schema\Pinning_1_0 + + + Microsoft\Schema\Pinning_1_0 + @@ -539,6 +554,15 @@ Source Files + + Source Files + + + Source Files + + + Microsoft\Schema\Pinning_1_0 + diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp new file mode 100644 index 0000000000..aa4688dc12 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinningIndex.h" +#include "SQLiteStorageBase.h" +#include "Schema/Pinning_1_0/PinningIndexInterface.h" + +namespace AppInstaller::Repository::Microsoft +{ + PinningIndex PinningIndex::CreateNew(const std::string& filePath, Schema::Version version) + { + AICLI_LOG(Repo, Info, << "Creating new Pinning Index [" << version << "] at '" << filePath << "'"); + PinningIndex result{ filePath, version }; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "pinningindex_createnew"); + + // Use calculated version, as incoming version could be 'latest' + result.m_version.SetSchemaVersion(result.m_dbconn); + + result.m_interface->CreateTables(result.m_dbconn); + + result.SetLastWriteTime(); + + savepoint.Commit(); + + return result; + } + + std::shared_ptr PinningIndex::OpenOrCreateDefault(OpenDisposition openDisposition) + { + auto indexPath = Runtime::GetPathTo(Runtime::PathName::LocalState) / "pinning.db"; + + try + { + if (std::filesystem::exists(indexPath)) + { + if (std::filesystem::is_regular_file(indexPath)) + { + try + { + AICLI_LOG(Repo, Info, << "Opening existing pinning index"); + return std::make_shared(PinningIndex::Open(indexPath.u8string(), openDisposition)); + } + CATCH_LOG(); + } + + AICLI_LOG(Repo, Info, << "Attempting to delete bad index file"); + std::filesystem::remove_all(indexPath); + } + + return std::make_shared(PinningIndex::CreateNew(indexPath.u8string())); + } + CATCH_LOG(); + + return {}; + } + + PinningIndex::IdType PinningIndex::AddPin(const Pinning::Pin& pin) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Adding Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType())); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_addpin"); + + IdType result = m_interface->AddPin(m_dbconn, pin); + + SetLastWriteTime(); + + savepoint.Commit(); + + return result; + } + + bool PinningIndex::UpdatePin(const Pinning::Pin& pin) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Updating Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType())); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_updatepin"); + + bool result = m_interface->UpdatePin(m_dbconn, pin).first; + + if (result) + { + SetLastWriteTime(); + savepoint.Commit(); + } + + return result; + } + + void PinningIndex::RemovePin(const Pinning::PinKey& pinKey) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Removing Pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningIndex_removePin"); + + m_interface->RemovePin(m_dbconn, pinKey); + + SetLastWriteTime(); + + savepoint.Commit(); + } + + std::optional PinningIndex::GetPin(const Pinning::PinKey& pinKey) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + return m_interface->GetPin(m_dbconn, pinKey); + } + + std::vector PinningIndex::GetAllPins() + { + std::lock_guard lockInterface{ *m_interfaceLock }; + return m_interface->GetAllPins(m_dbconn); + } + + bool PinningIndex::ResetAllPins(std::string_view sourceId) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + return m_interface->ResetAllPins(m_dbconn, sourceId); + } + + std::unique_ptr PinningIndex::CreateIPinningIndex() const + { + if (m_version == Schema::Version{ 1, 0 } || + m_version.MajorVersion == 1 || + m_version.IsLatest()) + { + return std::make_unique(); + } + + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + + PinningIndex::PinningIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) : + SQLiteStorageBase(target, disposition, std::move(indexFile)) + { + AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); + m_interface = CreateIPinningIndex(); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); + } + + PinningIndex::PinningIndex(const std::string& target, Schema::Version version) : SQLiteStorageBase(target, version) + { + m_interface = CreateIPinningIndex(); + m_version = m_interface->GetVersion(); + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h new file mode 100644 index 0000000000..77180331b5 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include "Microsoft/Schema/IPinningIndex.h" +#include "Microsoft/SQLiteStorageBase.h" +#include "winget/Pin.h" +#include + +namespace AppInstaller::Repository::Microsoft +{ + struct PinningIndex : SQLiteStorageBase + { + // An id that refers to a specific Pinning file. + using IdType = SQLite::rowid_t; + + PinningIndex(const PinningIndex&) = delete; + PinningIndex& operator=(const PinningIndex&) = delete; + + PinningIndex(PinningIndex&&) = default; + PinningIndex& operator=(PinningIndex&&) = default; + + // Creates a new PinningIndex database of the given version. + static PinningIndex CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest()); + + // Opens an existing PinningIndex database. + static PinningIndex Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile = {}) + { + return { filePath, disposition, std::move(indexFile) }; + } + + // Opens or creates a PinningIndex database on the default path. + // openDisposition is only used when opening an existing database. + // Returns nullptr in case of error. + static std::shared_ptr OpenOrCreateDefault(OpenDisposition openDisposition = OpenDisposition::ReadWrite); + + // Adds a pin to the index. + IdType AddPin(const Pinning::Pin& pin); + + // Updates a pin type, and gated version if needed + bool UpdatePin(const Pinning::Pin& pin); + + // Removes a pin from the index. + void RemovePin(const Pinning::PinKey& pinKey); + + // Returns the current pin for a given package if it exists. + std::optional GetPin(const Pinning::PinKey& pinKey); + + // Returns a vector containing all the existing pins. + std::vector GetAllPins(); + + // Deletes all pins from a given source, or from all sources if none is specified + bool ResetAllPins(std::string_view sourceId = {}); + + private: + // Constructor used to open an existing index. + PinningIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); + + // Constructor used to create a new index. + PinningIndex(const std::string& target, Schema::Version version); + + // Creates the IPinningIndex interface object for this version. + std::unique_ptr CreateIPinningIndex() const; + + std::unique_ptr m_interface; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h new file mode 100644 index 0000000000..c4c96a73aa --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include "Microsoft/Schema/Version.h" +#include "winget/Pin.h" + +namespace AppInstaller::Repository::Microsoft::Schema +{ + struct IPinningIndex + { + virtual ~IPinningIndex() = default; + + // Gets the schema version that this index interface is built for. + virtual Schema::Version GetVersion() const = 0; + + // Creates all of the version dependent tables within the database. + virtual void CreateTables(SQLite::Connection& connection) = 0; + + // Version 1.0 + // Adds a pin to the index. + virtual SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0; + + // Updates an existing pin in the index. + // The return value indicates whether the index was modified by the function. + virtual std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0; + + // Removes a pin from the index. + virtual SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0; + + // Returns the current pin for a given package if it exists. + virtual std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0; + + // Returns a vector containing all the existing pins. + virtual std::vector GetAllPins(SQLite::Connection& connection) = 0; + + // Removes all the pins from a given source, or from all sources if none is specified. + // Returns a value indicating whether any pin was deleted. + virtual bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) = 0; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp new file mode 100644 index 0000000000..5425556d5e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinTable.h" +#include "SQLiteStatementBuilder.h" +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 +{ + namespace + { + std::optional GetPinFromRow(std::string_view packageId, std::string_view sourceId, Pinning::PinType type, std::string_view version) + + { + switch (type) + { + case Pinning::PinType::Blocking: + return Pinning::Pin::CreateBlockingPin({ packageId, sourceId }); + case Pinning::PinType::Pinning: + return Pinning::Pin::CreatePinningPin({ packageId, sourceId }); + case Pinning::PinType::Gating: + return Pinning::Pin::CreateGatingPin({ packageId, sourceId }, Utility::GatedVersion{ version }); + default: + return {}; + } + } + } + + using namespace std::string_view_literals; + static constexpr std::string_view s_PinTable_Table_Name = "pin"sv; + static constexpr std::string_view s_PinTable_PackageId_Column = "package_id"sv; + static constexpr std::string_view s_PinTable_SourceId_Column = "source_id"sv; + static constexpr std::string_view s_PinTable_Type_Column = "type"sv; + static constexpr std::string_view s_PinTable_Version_Column = "version"sv; + static constexpr std::string_view s_PinTable_Index = "pin_index"sv; + + std::string_view PinTable::TableName() + { + return s_PinTable_Table_Name; + } + + void PinTable::Create(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_0"); + + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_PinTable_Table_Name).BeginColumns(); + + createTableBuilder.Column(ColumnBuilder(s_PinTable_PackageId_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_SourceId_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Type_Column, Type::Int64).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Version_Column, Type::Text)); + + createTableBuilder.EndColumns(); + createTableBuilder.Execute(connection); + + // Create an index over the pairs package,source + StatementBuilder createIndexBuilder; + createIndexBuilder.CreateUniqueIndex(s_PinTable_Index).On(s_PinTable_Table_Name).Columns({ s_PinTable_PackageId_Column, s_PinTable_SourceId_Column }); + createIndexBuilder.Execute(connection); + + savepoint.Commit(); + } + + std::optional PinTable::GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::RowIDName).From(s_PinTable_Table_Name) + .Where(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) + .And(s_PinTable_SourceId_Column).Equals((std::string_view)pinKey.SourceId); + + SQLite::Statement select = builder.Prepare(connection); + + if (select.Step()) + { + return select.GetColumn(0); + } + else + { + return {}; + } + } + + SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_PinTable_Table_Name) + .Columns({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column }) + .Values( + (std::string_view)pin.GetPackageId(), + pin.GetSourceId(), + pin.GetType(), + pin.GetGatedVersion().ToString()); + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } + + bool PinTable::UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + builder.Update(s_PinTable_Table_Name).Set() + .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pin.GetPackageId()) + .Column(s_PinTable_SourceId_Column).Equals(pin.GetSourceId()) + .Column(s_PinTable_Type_Column).Equals(pin.GetType()) + .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Where(SQLite::RowIDName).Equals(pinId); + + builder.Execute(connection); + return connection.GetChanges() != 0; + } + + void PinTable::RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId) + { + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); + builder.Execute(connection); + } + + std::optional PinTable::GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column }) + .From(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); + + SQLite::Statement select = builder.Prepare(connection); + + if (!select.Step()) + { + return {}; + } + + auto [packageId, sourceId, type, gatedVersion] = select.GetRow(); + return GetPinFromRow(packageId, sourceId, type, gatedVersion); + } + + std::vector PinTable::GetAllPins(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column }) + .From(s_PinTable_Table_Name); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector pins; + while (select.Step()) + { + auto [packageId, sourceId, type, gatedVersion] = select.GetRow(); + auto pin = GetPinFromRow(packageId, sourceId, type, gatedVersion); + if (pin) + { + pins.push_back(std::move(pin.value())); + } + } + + return pins; + } + + bool PinTable::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) + { + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_PinTable_Table_Name); + + if (!sourceId.empty()) + { + builder.Where(s_PinTable_SourceId_Column).Equals(sourceId); + } + + builder.Execute(connection); + + return connection.GetChanges() != 0; + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h new file mode 100644 index 0000000000..357ca2319b --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include "SQLiteStatementBuilder.h" +#include "Microsoft/Schema/IPinningIndex.h" +#include + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 +{ + struct PinTable + { + // Get the table name. + static std::string_view TableName(); + + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + // Gets the row ID for the pin, if it exists. + static std::optional GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey); + + // Adds a new pin. Returns the row ID of the added pin. + static SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin); + + // Updates an existing pin. + // Returns a value indicating whether there were any changes. + static bool UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); + + // Removes a pin given its row ID. + static void RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId); + + // Gets a pin by its row ID if it exists. + // Used for testing + static std::optional GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId); + + // Gets all the currently existing pins. + static std::vector GetAllPins(SQLite::Connection& connection); + + // Resets all pins from a given source, or from all sources if none is specified. + // Returns a value indicating whether there were any changes. + static bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId = {}); + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h new file mode 100644 index 0000000000..cfe049ebad --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 +{ + struct PinningIndexInterface : public IPinningIndex + { + // Version 1.0 + Schema::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + + private: + SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + std::vector GetAllPins(SQLite::Connection& connection) override; + bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp new file mode 100644 index 0000000000..354dca15b2 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h" +#include "Microsoft/Schema/Pinning_1_0/PinTable.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 +{ + namespace + { + std::optional GetExistingPinId(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto result = PinTable::GetIdByPinKey(connection, pinKey); + + if (!result) + { + AICLI_LOG(Repo, Verbose, << "Did not find a pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]"); + } + + return result; + } + + } + + // Version 1.0 + Schema::Version PinningIndexInterface::GetVersion() const + { + return { 1, 0 }; + } + + void PinningIndexInterface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_0"); + Pinning_V1_0::PinTable::Create(connection); + savepoint.Commit(); + } + + SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + auto existingPin = GetExistingPinId(connection, pin.GetKey()); + + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), existingPin.has_value()); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_0"); + SQLite::rowid_t pinId = PinTable::AddPin(connection, pin); + + savepoint.Commit(); + return pinId; + } + + std::pair PinningIndexInterface::UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + auto existingPinId = GetExistingPinId(connection, pin.GetKey()); + + // If the pin doesn't exist, fail the update + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); + + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_0"); + bool status = PinTable::UpdatePinById(connection, existingPinId.value(), pin); + + savepoint.Commit(); + return { status, existingPinId.value() }; + } + + SQLite::rowid_t PinningIndexInterface::RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = GetExistingPinId(connection, pinKey); + + // If the pin doesn't exist, fail the remove + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); + + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "removepin_v1_0"); + PinTable::RemovePinById(connection, existingPinId.value()); + + savepoint.Commit(); + return existingPinId.value(); + } + + std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = GetExistingPinId(connection, pinKey); + + if (!existingPinId) + { + return {}; + } + + return PinTable::GetPinById(connection, existingPinId.value()); + } + + std::vector PinningIndexInterface::GetAllPins(SQLite::Connection& connection) + { + return PinTable::GetAllPins(connection); + } + + bool PinningIndexInterface::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "resetpins_v1_0"); + bool result = PinTable::ResetAllPins(connection, sourceId); + savepoint.Commit(); + + return result; + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 2efca323fd..247ec33689 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -172,7 +172,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 portableFile.SymlinkTarget = std::move(symlinkTarget); result.emplace_back(std::move(portableFile)); } - + return result; } } \ No newline at end of file