From 469d3385b08bcd751189ef6145f7afa703a66e29 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Tue, 3 Feb 2026 11:12:54 -0800 Subject: [PATCH 1/6] Make list details stable (1.28) (#6021) For release. --- doc/Settings.md | 27 ------------------- .../JSON/settings/settings.schema.0.2.json | 5 ---- src/AppInstallerCLICore/Argument.cpp | 2 +- .../ExperimentalFeature.cpp | 4 --- .../Public/winget/ExperimentalFeature.h | 1 - .../Public/winget/UserSettings.h | 2 -- src/AppInstallerCommonCore/UserSettings.cpp | 1 - 7 files changed, 1 insertion(+), 41 deletions(-) diff --git a/doc/Settings.md b/doc/Settings.md index 139a6079e4..5eb1ff765e 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -393,30 +393,3 @@ This feature enables support for fonts via `winget settings`. The `winget font l "fonts": true }, ``` - -### listDetails - -This feature enables support for displaying detailed output from the `list` command. Rather than a table view of the results, when the `--details` option is provided -to the `list` command, it will output information similar to how `show` would. Most of the data presented is directly from the local installation. - -Example output: -```PowerShell -> winget list Microsoft.VisualStudio.2022.Enterprise --details -Visual Studio Enterprise 2022 [Microsoft.VisualStudio.2022.Enterprise] -Version: 17.14.21 (November 2025) -Publisher: Microsoft Corporation -Local Identifier: ARP\Machine\X86\875fed29 -Product Code: 875fed29 -Installer Category: exe -Installed Scope: Machine -Installed Location: C:\Program Files\Microsoft Visual Studio\2022\Enterprise -Available Upgrades: - winget [17.14.23] -``` - -To enable: -```json - "experimentalFeatures": { - "listDetails": true - }, -``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 43b26ed2a2..daa289b75c 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -328,11 +328,6 @@ "description": "Enable support for managing fonts", "type": "boolean", "default": false - }, - "listDetails": { - "description": "Enable detailed output option for list command", - "type": "boolean", - "default": false } } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 12289a3888..acaaf0c022 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -485,7 +485,7 @@ namespace AppInstaller::CLI case Args::Type::Correlation: return Argument{ type, Resource::String::CorrelationArgumentDescription, ArgumentType::Standard, Argument::Visibility::Hidden }; case Args::Type::ListDetails: - return Argument{ type, Resource::String::ListDetailsArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help, ExperimentalFeature::Feature::ListDetails }; + return Argument{ type, Resource::String::ListDetailsArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 09bbb0f661..d417a6645e 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,8 +44,6 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Font: return userSettings.Get(); - case ExperimentalFeature::Feature::ListDetails: - return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -79,8 +77,6 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Font: return ExperimentalFeature{ "Font", "fonts", "https://aka.ms/winget-settings", Feature::Font }; - case Feature::ListDetails: - return ExperimentalFeature{ "List Details", "listDetails", "https://aka.ms/winget-settings", Feature::ListDetails }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 33571c48aa..2dc097f548 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,7 +25,6 @@ namespace AppInstaller::Settings DirectMSI = 0x1, Resume = 0x2, Font = 0x4, - ListDetails = 0x8, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index e5e12c0b61..10b0c8f36b 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,7 +76,6 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFFonts, - EFListDetails, // Telemetry TelemetryDisable, // Install behavior @@ -164,7 +163,6 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFListDetails, bool, bool, false, ".experimentalFeatures.listDetails"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index b0f87fd625..f220ec3c2c 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -267,7 +267,6 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFFonts) - WINGET_VALIDATE_PASS_THROUGH(EFListDetails) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) From 50b07972d70c7e9192b19d0974a2a686693b4705 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Tue, 3 Feb 2026 15:14:33 -0800 Subject: [PATCH 2/6] Move to IReference rather than custom enum for optional bool (1.28) (#6024) CP of #6022 ## Change Use an `IReference` rather than a custom enum to represent an optional bool value. The projection is much cleaner and scales better to other value types. --- .../Interop/PackageCatalogInterop.cs | 8 +++-- .../Public/winget/RepositorySource.h | 4 +-- .../RepositorySource.cpp | 2 -- .../Converters.cpp | 33 ------------------- .../Converters.h | 2 -- .../EditPackageCatalogOptions.cpp | 13 +++++--- .../EditPackageCatalogOptions.h | 9 ++--- .../PackageManager.cpp | 3 +- .../PackageManager.idl | 23 ++++--------- 9 files changed, 29 insertions(+), 68 deletions(-) diff --git a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs index 5493f951ae..7b7df12ed0 100644 --- a/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs @@ -291,13 +291,14 @@ public async Task AddEditRemovePackageCatalog() options.SourceUri = Constants.TestSourceUrl; options.Name = Constants.TestSourceName; options.TrustLevel = PackageCatalogTrustLevel.Trusted; + options.Explicit = true; await this.AddAndValidatePackageCatalogAsync(options, AddPackageCatalogStatus.Ok); // Edit EditPackageCatalogOptions editOptions = this.TestFactory.CreateEditPackageCatalogOptions(); editOptions.Name = Constants.TestSourceName; - editOptions.Explicit = OptionalBoolean.False; + editOptions.Explicit = false; this.EditAndValidatePackageCatalog(editOptions, EditPackageCatalogStatus.Ok); // Remove @@ -340,6 +341,7 @@ private PackageCatalogReference GetAndValidatePackageCatalog(AddPackageCatalogOp Assert.IsNotNull(packageCatalog); Assert.AreEqual(addPackageCatalogOptions.Name, packageCatalog.Info.Name); Assert.AreEqual(addPackageCatalogOptions.SourceUri, packageCatalog.Info.Argument); + Assert.AreEqual(addPackageCatalogOptions.Explicit, packageCatalog.Info.Explicit); return packageCatalog; } @@ -396,9 +398,9 @@ private void EditAndValidatePackageCatalog(EditPackageCatalogOptions editPackage // Verify edits are correct. var packageCatalog = this.packageManager.GetPackageCatalogByName(editPackageCatalogOptions.Name); - if (editPackageCatalogOptions.Explicit != OptionalBoolean.Unspecified) + if (editPackageCatalogOptions.Explicit != null) { - Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit == OptionalBoolean.True); + Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit); } } } diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index 78e726bd93..d0177558a3 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -201,9 +201,9 @@ namespace AppInstaller::Repository // Contains information about edits to a source. struct SourceEdit { - SourceEdit(std::optional isExplicit); + SourceEdit() = default; - // The explicit property of a source. + // The Explicit property of a source. std::optional Explicit; }; diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index 35209e28c2..7450c16f09 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -432,8 +432,6 @@ namespace AppInstaller::Repository return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type); } - SourceEdit::SourceEdit(std::optional isExplicit) : Explicit(isExplicit) {} - Source::Source() {} Source::Source(std::string_view name) diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 80def812a2..3c3b4f238b 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -556,37 +556,4 @@ namespace winrt::Microsoft::Management::Deployment::implementation default: return AppInstaller::Manifest::PlatformEnum::Unknown; } } - - std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean) - { - switch (optionalBoolean) - { - case OptionalBoolean::True: - return std::optional { true }; - case OptionalBoolean::False: - return std::optional { false }; - default: - return std::nullopt; - } - } - - winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean) - { - if (optionalBoolean.has_value()) - { - if (optionalBoolean.value()) - { - return OptionalBoolean::True; - } - else - { - return OptionalBoolean::False; - } - } - else - { - return OptionalBoolean::Unspecified; - } - } - } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index f2535704ab..460fe4e21c 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -35,8 +35,6 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); - std::optional GetOptionalBoolean(winrt::Microsoft::Management::Deployment::OptionalBoolean optionalBoolean); - winrt::Microsoft::Management::Deployment::OptionalBoolean GetOptionalBoolean(std::optional optionalBoolean); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ if constexpr (std::is_same_v) \ diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp index 906106fa54..589893aef3 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.cpp @@ -17,16 +17,19 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring EditPackageCatalogOptions::Name() { return hstring(m_name); - } + } + void EditPackageCatalogOptions::Name(hstring const& value) { m_name = value; - } - OptionalBoolean EditPackageCatalogOptions::Explicit() + } + + Windows::Foundation::IReference EditPackageCatalogOptions::Explicit() { return m_explicit; - } - void EditPackageCatalogOptions::Explicit(OptionalBoolean const& value) + } + + void EditPackageCatalogOptions::Explicit(Windows::Foundation::IReference value) { m_explicit = value; } diff --git a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h index 68e3372f0d..754ab3a370 100644 --- a/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h +++ b/src/Microsoft.Management.Deployment/EditPackageCatalogOptions.h @@ -3,7 +3,8 @@ #pragma once #include "EditPackageCatalogOptions.g.h" #include "public/ComClsids.h" -#include +#include +#include namespace winrt::Microsoft::Management::Deployment::implementation { @@ -15,13 +16,13 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring Name(); void Name(hstring const& value); - OptionalBoolean Explicit(); - void Explicit(OptionalBoolean const& value); + Windows::Foundation::IReference Explicit(); + void Explicit(Windows::Foundation::IReference value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: hstring m_name = L""; - OptionalBoolean m_explicit = OptionalBoolean::Unspecified; + std::optional m_explicit; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index fd08134c93..ae991b1dea 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1467,7 +1467,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, !matchingSource.has_value()); ::AppInstaller::Repository::Source sourceToEdit = ::AppInstaller::Repository::Source{ matchingSource.value().Name }; - ::AppInstaller::Repository::SourceEdit edits{ GetOptionalBoolean(options.Explicit())}; + ::AppInstaller::Repository::SourceEdit edits; + edits.Explicit = options.Explicit(); if (sourceToEdit.RequiresChanges(edits)) { sourceToEdit.Edit(edits); diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index da4976c3ac..c2cc718609 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1541,15 +1541,6 @@ namespace Microsoft.Management.Deployment HRESULT ExtendedErrorCode { get; }; }; - /// IMPLEMENTATION NOTE: OptionalBoolean - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] - enum OptionalBoolean - { - Unspecified, - False, - True, - }; - /// IMPLEMENTATION NOTE: EditPackageCatalogOptions [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] runtimeclass EditPackageCatalogOptions @@ -1562,7 +1553,7 @@ namespace Microsoft.Management.Deployment String Name; /// Editing the Explicit property has three states: true, false, and not specified (no changes). - OptionalBoolean Explicit; + Windows.Foundation.IReference Explicit; }; /// IMPLEMENTATION NOTE: RemovePackageCatalogStatus @@ -1620,12 +1611,6 @@ namespace Microsoft.Management.Deployment Windows.Foundation.IAsyncOperationWithProgress RemovePackageCatalogAsync(RemovePackageCatalogOptions options); } - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] - { - /// Edit an existing Windows Package Catalog. - EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); - } - /// Install the specified package Windows.Foundation.IAsyncOperationWithProgress InstallPackageAsync(CatalogPackage package, InstallOptions options); @@ -1667,6 +1652,12 @@ namespace Microsoft.Management.Deployment // The version of the Windows Package Manager that is running. String Version{ get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 28)] + { + /// Edit an existing Windows Package Catalog. + EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); + } } /// Global settings for PackageManager operations. From 35663a9c632fccab4ee94c6e82ba519e84a63ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flor=20Chac=C3=B3n?= <14323496+florelis@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:28:49 -0800 Subject: [PATCH 3/6] Apply latest localization patch (1.28) (#6025) Cherry-pick of #6023 ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/6025) --- Localization/Resources/de-DE/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/es-ES/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/fr-FR/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/it-IT/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/ja-JP/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/ko-KR/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/pt-BR/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/ru-RU/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/zh-CN/winget.resw | 77 ++++++++++++++++++++++++ Localization/Resources/zh-TW/winget.resw | 77 ++++++++++++++++++++++++ 10 files changed, 770 insertions(+) diff --git a/Localization/Resources/de-DE/winget.resw b/Localization/Resources/de-DE/winget.resw index b55e441f9a..335284e0f5 100644 --- a/Localization/Resources/de-DE/winget.resw +++ b/Localization/Resources/de-DE/winget.resw @@ -545,6 +545,20 @@ Sie können über die Einstellungsdatei „winget settings“ konfiguriert werde Verwalten von Paketquellen + + Eigenschaften einer vorhandenen Quelle bearbeiten. Eine Quelle stellt die Daten bereit, mit denen Sie Pakete ermitteln und installieren können. + + + Eigenschaften einer Quelle bearbeiten + + + Quelle wird bearbeitet: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + Die Quelle mit dem Namen „{0}“ befindet sich bereits im gewünschten Zustand. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argument Value given to source. @@ -2890,6 +2904,9 @@ Geben Sie eine Option für --source an, um den Vorgang fortzusetzen. Schließt eine Quelle aus der Ermittlung aus, sofern keine Angabe erfolgt. + + Schließt eine Quelle aus der Ermittlung aus (TRUE oder FALSE). + Anstößig @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. Das Schriftartpaket ist bereits installiert. + + Detaillierte Informationen zu Paketen anzeigen + Providing this argument causes the CLI to output additional details about installed application packages. + + + Kanal: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Lokaler Bezeichner: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Paketfamilienname: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Produktcode: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Upgrade-Code: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Installierter Bereich: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Installierte Architektur: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Installiertes Gebietsschema: + Precedes a value that is the locale of the installed package. + + + Installationsspeicherort: + Precedes a value that is the directory path to the installed package. + + + Ursprungsquelle: + Precedes a value that names the package source where the installed package originated from. + + + Verfügbare Upgrades: + Precedes a list of package upgrades available for the installed package. + + + Installer-Kategorie: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Alter Wert + Column title for listing edit changes. + + + Neuer Wert + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/es-ES/winget.resw b/Localization/Resources/es-ES/winget.resw index 9858b4c91f..c23b6387c4 100644 --- a/Localization/Resources/es-ES/winget.resw +++ b/Localization/Resources/es-ES/winget.resw @@ -545,6 +545,20 @@ Se pueden configurar mediante el archivo de configuración "winget settings". Administrar orígenes de paquetes + + Edite las propiedades de un origen existente. Un origen proporciona los datos para que pueda detectar e instalar paquetes. + + + Editar propiedades de un origen + + + Editando origen: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + El origen denominado '{0}' ya está en el estado deseado. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argumento Value given to source. @@ -2890,6 +2904,9 @@ Especifique uno de ellos con la opción --source para continuar. Excluye un origen de la detección a menos que se especifique + + Excluye un origen de la detección (true o false) + Explícito @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. El paquete de fuentes ya está instalado. + + Mostrar información detallada sobre los paquetes + Providing this argument causes the CLI to output additional details about installed application packages. + + + Canal: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Identificador local: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Nombre de familia de paquete: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Código de producto: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Código de actualización: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Ámbito de la instalación: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Arquitectura instalada: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Configuración regional instalada: + Precedes a value that is the locale of the installed package. + + + Ubicación instalada: + Precedes a value that is the directory path to the installed package. + + + Fuente de origen: + Precedes a value that names the package source where the installed package originated from. + + + Actualizaciones disponibles: + Precedes a list of package upgrades available for the installed package. + + + Categoría del instalador: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Valor antiguo + Column title for listing edit changes. + + + Nuevo valor + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/fr-FR/winget.resw b/Localization/Resources/fr-FR/winget.resw index 997a55cf0c..9e2aede069 100644 --- a/Localization/Resources/fr-FR/winget.resw +++ b/Localization/Resources/fr-FR/winget.resw @@ -545,6 +545,20 @@ Elles peuvent être configurées par le biais du fichier de paramètres « wing Gérer les sources des packages + + Modifiez les propriétés d’une source existante. Une source fournit les données pour vous permettre de découvrir et d’installer des packages. + + + Modifier les propriétés d’une source + + + Source de modification : {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + La source nommée '{0}' est déjà dans l’état souhaité. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argument Value given to source. @@ -2890,6 +2904,9 @@ Spécifiez l’un d’entre eux à l’aide de l’option --source pour continue Exclut une source de la recherche, sauf indication contraire + + Exclut une source de la découverte (true ou false) + Contenu explicite @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. Le package de polices est déjà installé. + + Afficher des informations détaillées sur les packages + Providing this argument causes the CLI to output additional details about installed application packages. + + + Canal : + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Identifiant local : + Precedes a value that is the unique identifier for the installed package on the local system. + + + Nom de la famille de packages : + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Code du produit : + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Code de mise à niveau : + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Étendue de l’installation : + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Architecture installée : + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Paramètres régionaux de l’installation : + Precedes a value that is the locale of the installed package. + + + Emplacement de l’installation : + Precedes a value that is the directory path to the installed package. + + + Source d’origine : + Precedes a value that names the package source where the installed package originated from. + + + Mises à niveau disponibles : + Precedes a list of package upgrades available for the installed package. + + + Catégorie du programme d’installation : + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Ancienne valeur + Column title for listing edit changes. + + + Nouvelle valeur + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/it-IT/winget.resw b/Localization/Resources/it-IT/winget.resw index b27d5acb21..82cb960cac 100644 --- a/Localization/Resources/it-IT/winget.resw +++ b/Localization/Resources/it-IT/winget.resw @@ -545,6 +545,20 @@ Possono essere configurati tramite il file di impostazioni ' winget settings '.< Gestisci le origini dei pacchetti + + Modifica le proprietà di un'origine esistente. Un'origine fornisce i dati per l'individuazione e l'installazione dei pacchetti. + + + Modifica proprietà di un'origine + + + Modifica origine: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + L'origine denominata '{0}' si trova già nello stato desiderato. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argomento Value given to source. @@ -2890,6 +2904,9 @@ Specificarne uno utilizzando l'opzione --source per continuare. Esclude un'origine dall'individuazione se non specificata + + Esclude un'origine dall'individuazione (vero o falso) + Contenuti espliciti @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. Il pacchetto di tipi di carattere è già installato. + + Visualizza informazioni dettagliate sui pacchetti + Providing this argument causes the CLI to output additional details about installed application packages. + + + Canale: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Identificatore locale: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Nome della famiglia di pacchetti: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Codice prodotto: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Codice aggiornamento: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Ambito installazione: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Architettura installata: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Impostazioni locali installate: + Precedes a value that is the locale of the installed package. + + + Posizione installata: + Precedes a value that is the directory path to the installed package. + + + Origine: + Precedes a value that names the package source where the installed package originated from. + + + Aggiornamenti disponibili: + Precedes a list of package upgrades available for the installed package. + + + Categoria programma di installazione: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Valore precedente + Column title for listing edit changes. + + + Nuovo valore + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/ja-JP/winget.resw b/Localization/Resources/ja-JP/winget.resw index 9218079ace..3e56d55f3b 100644 --- a/Localization/Resources/ja-JP/winget.resw +++ b/Localization/Resources/ja-JP/winget.resw @@ -545,6 +545,20 @@ パッケージのソースの管理 + + 既存のソースのプロパティを編集します。ソースは、パッケージを検出してインストールするためのデータを提供します。 + + + ソースのプロパティを編集します + + + ソースの編集中: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + '{0}' という名前のソースは既に適切な状態です。 + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + 引数 Value given to source. @@ -2890,6 +2904,9 @@ 指定しない限り、ソースを検出から除外します + + ソースを検出から除外します (true または false) + 成人指定 @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. フォント パッケージは既にインストールされています。 + + パッケージに関する詳細情報を表示します + Providing this argument causes the CLI to output additional details about installed application packages. + + + チャネル: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + ローカル識別子: + Precedes a value that is the unique identifier for the installed package on the local system. + + + パッケージ ファミリ名: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + 製品コード: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + アップグレード コード: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + インストール スコープ: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + インストールされたアーキテクチャ: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + インストール ロケール: + Precedes a value that is the locale of the installed package. + + + インストール場所: + Precedes a value that is the directory path to the installed package. + + + ソース提供元: + Precedes a value that names the package source where the installed package originated from. + + + 利用可能なアップグレード: + Precedes a list of package upgrades available for the installed package. + + + インストーラー カテゴリ: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + 古い値 + Column title for listing edit changes. + + + 新しい値 + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/ko-KR/winget.resw b/Localization/Resources/ko-KR/winget.resw index 5db9b1e06e..6818b455f1 100644 --- a/Localization/Resources/ko-KR/winget.resw +++ b/Localization/Resources/ko-KR/winget.resw @@ -545,6 +545,20 @@ 패키지 원본 관리 + + 기존 원본의 속성을 편집합니다. 원본은 패키지를 검색하고 설치하는 데 사용할 데이터를 제공합니다. + + + 소스 속성 편집 + + + 소스 편집 중: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + 이름이 '{0}' 원본이 이미 원하는 상태에 있습니다. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + 인수 Value given to source. @@ -2890,6 +2904,9 @@ 지정하지 않는 한 검색에서 원본 제외 + + 검색에서 원본 제외(true 또는 false) + 유해 콘텐츠 @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. 글꼴 패키지가 이미 설치되어 있습니다. + + 패키지에 대한 자세한 정보 표시 + Providing this argument causes the CLI to output additional details about installed application packages. + + + 채널: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + 로컬 식별자 + Precedes a value that is the unique identifier for the installed package on the local system. + + + 패키지 패밀리 이름: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + 제품 코드: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + 업그레이드 코드: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + 설치된 범위: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + 설치된 아키텍처: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + 설치된 지역: + Precedes a value that is the locale of the installed package. + + + 설치된 위치: + Precedes a value that is the directory path to the installed package. + + + 원본 소스: + Precedes a value that names the package source where the installed package originated from. + + + 사용 가능한 업그레이드: + Precedes a list of package upgrades available for the installed package. + + + 설치 관리자 범주: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + 이전 값 + Column title for listing edit changes. + + + 새 값 + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/pt-BR/winget.resw b/Localization/Resources/pt-BR/winget.resw index f7f8f96b94..543bb86b1a 100644 --- a/Localization/Resources/pt-BR/winget.resw +++ b/Localization/Resources/pt-BR/winget.resw @@ -545,6 +545,20 @@ Eles podem ser configurados por meio do arquivo de configurações ' winget sett Gerenciar fontes de pacotes + + Editar propriedades de uma fonte existente. Uma fonte fornece os dados para você descobrir e instalar pacotes. + + + Editar propriedades da fonte + + + Editando fonte: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + A fonte chamada '{0}' já está no estado desejado. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Argumento Value given to source. @@ -2890,6 +2904,9 @@ Especifique um deles usando a opção --source para continuar. Exclui uma fonte da descoberta, a menos que especificado + + Exclui uma origem da descoberta (true ou false) + Explícito @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. O pacote de fontes já está instalado. + + Mostrar informações detalhadas sobre os pacotes instalados + Providing this argument causes the CLI to output additional details about installed application packages. + + + Canal: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Identificador Local: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Nome da Família de Pacotes: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Código do Produto: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Código de Atualização: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Escopo da Instalação: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Arquitetura Instalada: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Localidade da Instalação: + Precedes a value that is the locale of the installed package. + + + Local de Instalação: + Precedes a value that is the directory path to the installed package. + + + Fonte de Origem: + Precedes a value that names the package source where the installed package originated from. + + + Atualizações Disponíveis: + Precedes a list of package upgrades available for the installed package. + + + Categoria do Instalador: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Valor Antigo + Column title for listing edit changes. + + + Novo Valor + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/ru-RU/winget.resw b/Localization/Resources/ru-RU/winget.resw index afe53ac950..990913d8e2 100644 --- a/Localization/Resources/ru-RU/winget.resw +++ b/Localization/Resources/ru-RU/winget.resw @@ -545,6 +545,20 @@ Управление источниками пакетов + + Изменение свойств существующего источника. Источник предоставляет данные для обнаружения и установки пакетов. + + + Изменить свойства источника + + + Редактирование источника: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + Источник с '{0}' уже находится в нужном состоянии. + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + Аргумент Value given to source. @@ -2890,6 +2904,9 @@ Исключает источник из обнаружения, если не указано + + Исключить источник из обнаружения (true или false) + Возрастные ограничения @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. Пакет шрифтов уже установлен. + + Показать подробную информацию о пакетах + Providing this argument causes the CLI to output additional details about installed application packages. + + + Канал: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Локальный идентификатор: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Имя семейства пакетов + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Код продукта: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Код обновления: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Установленная область: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Установленная архитектура: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Установленный языковой стандарт: + Precedes a value that is the locale of the installed package. + + + Установленное расположение: + Precedes a value that is the directory path to the installed package. + + + Источник пакета: + Precedes a value that names the package source where the installed package originated from. + + + Доступные обновления: + Precedes a list of package upgrades available for the installed package. + + + Категория установщика: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + Старое значение + Column title for listing edit changes. + + + Новое значение + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/zh-CN/winget.resw b/Localization/Resources/zh-CN/winget.resw index 4fc879d2e3..daac4613f8 100644 --- a/Localization/Resources/zh-CN/winget.resw +++ b/Localization/Resources/zh-CN/winget.resw @@ -545,6 +545,20 @@ 管理程序包的来源 + + 编辑现有源的属性。源提供用于发现和安装包的数据。 + + + 编辑源的属性 + + + 正在编辑源: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + 名为 '{0}' 的源已处于所需状态。 + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + 参数 Value given to source. @@ -2890,6 +2904,9 @@ 除非指定,否则从发现中排除源 + + 从发现中排除源(true 或 false) + 显式 @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. 已安装字体包。 + + 显示有关包的详细信息 + Providing this argument causes the CLI to output additional details about installed application packages. + + + 通道: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + 本地标识符: + Precedes a value that is the unique identifier for the installed package on the local system. + + + 包系列名称: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + 产品代码: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + 升级代码: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + 已安装的范围: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + 已安装的体系结构: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + 已安装的区域设置: + Precedes a value that is the locale of the installed package. + + + 安装位置: + Precedes a value that is the directory path to the installed package. + + + 源位置: + Precedes a value that names the package source where the installed package originated from. + + + 可用升级: + Precedes a list of package upgrades available for the installed package. + + + 安装程序类别: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + 旧值 + Column title for listing edit changes. + + + 新值 + Column title for listing the new value. + \ No newline at end of file diff --git a/Localization/Resources/zh-TW/winget.resw b/Localization/Resources/zh-TW/winget.resw index b5d65441c9..70678b97db 100644 --- a/Localization/Resources/zh-TW/winget.resw +++ b/Localization/Resources/zh-TW/winget.resw @@ -545,6 +545,20 @@ 管理套件來源 + + 編輯現有來源的屬性。來源提供資料讓你發現並安裝套件。 + + + 編輯來源屬性 + + + 正在編輯來源: {0} + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + + + 名為 '{0}' 的來源已經處於所需的狀態。 + {Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. + 引數 Value given to source. @@ -2890,6 +2904,9 @@ 除非指定,否則將來源排除在探索之外 + + 從探索中排除來源 (true 或 false) + 偏激 @@ -3458,4 +3475,64 @@ An unlocalized JSON fragment will follow on another line. 已安裝字型套件。 + + 顯示套件的詳細資訊 + Providing this argument causes the CLI to output additional details about installed application packages. + + + 管道: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + 本機識別碼: + Precedes a value that is the unique identifier for the installed package on the local system. + + + 套件系列名稱: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + 產品碼: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + 升級代碼: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + 已安裝範圍: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + 安裝結構: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + 安裝地區設定: + Precedes a value that is the locale of the installed package. + + + 安裝位置: + Precedes a value that is the directory path to the installed package. + + + 原始來源: + Precedes a value that names the package source where the installed package originated from. + + + 可用的升級: + Precedes a list of package upgrades available for the installed package. + + + 安裝程式類別: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + + + 舊值 + Column title for listing edit changes. + + + 新值 + Column title for listing the new value. + \ No newline at end of file From c25b6629d79060698a14f538ab9065276158ab23 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Wed, 22 Apr 2026 11:58:15 -0700 Subject: [PATCH 4/6] Improved manifest validations for MSI and Windows Feature names (#6169) ## Change Adds some additional validation for MSI switches and Windows Feature names: 1. MSI switches are checked during full manifest validation in the same way that they are checked when we attempt to use the MSI APIs rather than msiexec. The API is used for fully all silent installs of MSIs, so we now ensure that those will work (6 total manifests in winget-pkgs were found that needed fixes). 2. Windows Feature names in dependencies are validated to be { alphanumeric, `-`, `_` }. This is done both during full manifest validation and at runtime. A PowerShell script (tools/ManifestValidation/Invoke-ManifestValidation.ps1) is added that will run `wingetdev validate` against a directory recursively and generate a report. There is support for resuming in the middle of the run. This would probably perform much better if it were updated to use the WinGetUtil API, but that can be a future project. --- .github/actions/spelling/expect.txt | 5 + .../Workflows/DependenciesFlow.cpp | 6 + .../Workflows/InstallFlow.cpp | 10 +- .../ShellExecuteInstallerHandler.cpp | 6 + .../AppInstallerCLITests.vcxproj | 12 + .../AppInstallerCLITests.vcxproj.filters | 12 + .../InstallDependenciesFlow.cpp | 37 ++ src/AppInstallerCLITests/Strings.cpp | 25 + .../Manifest-Bad-BlockedMsiProperty.yaml | 19 + .../Manifest-Bad-InvalidMsiSwitches.yaml | 19 + ...anifest-Bad-InvalidWindowsFeatureName.yaml | 22 + ...Manifest-Bad-NetworkAddressInSwitches.yaml | 20 + src/AppInstallerCLITests/YamlManifest.cpp | 67 +++ .../Manifest/ManifestCommon.cpp | 7 + .../Manifest/ManifestValidation.cpp | 80 +++- .../Manifest/YamlParser.cpp | 2 +- .../MsiExecArguments.cpp | 42 +- .../Public/winget/ManifestCommon.h | 7 + .../Public/winget/ManifestValidation.h | 6 +- .../Public/winget/MsiExecArguments.h | 8 + .../Rest/Schema/1_0/RestInterface_1_0.cpp | 2 +- .../AppInstallerStrings.cpp | 18 + .../Public/AppInstallerStrings.h | 3 + .../WinGetServerManualActivation_Client.cpp | 2 +- src/WinGetUtil/Exports.cpp | 2 + src/WinGetUtil/WinGetUtil.h | 4 + src/WinGetUtilInterop/Common/Enums.cs | 9 +- .../Invoke-ManifestValidation.ps1 | 434 ++++++++++++++++++ 28 files changed, 856 insertions(+), 30 deletions(-) create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-BlockedMsiProperty.yaml create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidMsiSwitches.yaml create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidWindowsFeatureName.yaml create mode 100644 src/AppInstallerCLITests/TestData/Manifest-Bad-NetworkAddressInSwitches.yaml create mode 100644 tools/ManifestValidation/Invoke-ManifestValidation.ps1 diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2fb073f88a..437d21d946 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -7,6 +7,7 @@ ACCESSDENIED ACCESSTOKEN acl adjacents +adminproperties adml admx AFAIK @@ -98,6 +99,7 @@ COMGLB commandline compressapi concurrencysal +Consolas constexpr contactsupport contentfiles @@ -279,6 +281,7 @@ Kaido KNOWNFOLDERID kool ktf +LASTEXITCODE LCID learnxinyminutes LEBOM @@ -347,6 +350,7 @@ msdownload msft msftrubengu MSIHASH +msinewinstance MSIXHASH MSIXSTRM msstore @@ -510,6 +514,7 @@ sddl secureobject securestring seekp +Segoe seof servercert servercertificate diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp index 948be1585b..0581d48d07 100644 --- a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -175,6 +175,12 @@ namespace AppInstaller::CLI::Workflow { AICLI_LOG(Core, Info, << "Successfully enabled [" << featureName << "]"); } + else if (result == E_INVALIDARG) + { + AICLI_LOG(Core, Warning, << "Invalid Windows Feature name [" << featureName << "]"); + enableFeaturesFailed = true; + featureContext.Reporter.Warn() << Resource::String::WindowsFeatureNotFound(locIndFeatureName) << std::endl; + } else if (result == 0x800f080c) // DISMAPI_E_UNKNOWN_FEATURE { AICLI_LOG(Core, Warning, << "Windows Feature [" << featureName << "] does not exist"); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 130cddb2b2..15ff0bf57b 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -61,14 +61,8 @@ namespace AppInstaller::CLI::Workflow bool ShouldUseDirectMSIInstall(InstallerTypeEnum type, bool isSilentInstall) { - switch (type) - { - case InstallerTypeEnum::Msi: - case InstallerTypeEnum::Wix: - return isSilentInstall || ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::DirectMSI); - default: - return false; - } + return DoesInstallerTypeUseMsiProperties(type) && + (isSilentInstall || ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::DirectMSI)); } bool ShouldErrorForUnsupportedArgument(UnsupportedArgumentEnum arg) diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp index 726a547f85..37405faf51 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -509,6 +509,12 @@ namespace AppInstaller::CLI::Workflow void ShellExecuteEnableWindowsFeature::operator()(Execution::Context& context) const { + if (!Utility::IsValidWindowsFeaturePattern(m_featureName)) + { + context.Add(static_cast(E_INVALIDARG)); + return; + } + Utility::LocIndView locIndFeatureName{ m_featureName }; std::optional doesFeatureExistResult = DoesWindowsFeatureExist(context, m_featureName); diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index e2d439a275..0d3a09a72c 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -1073,6 +1073,18 @@ true + + true + + + true + + + true + + + true + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 643382697d..779b1d2490 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -1140,5 +1140,17 @@ TestData + + TestData + + + TestData + + + TestData + + + TestData + \ No newline at end of file diff --git a/src/AppInstallerCLITests/InstallDependenciesFlow.cpp b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp index c3f81912de..b021d3d366 100644 --- a/src/AppInstallerCLITests/InstallDependenciesFlow.cpp +++ b/src/AppInstallerCLITests/InstallDependenciesFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "WorkflowCommon.h" #include "DependenciesTestSource.h" +#include #include #include #include @@ -306,6 +307,42 @@ TEST_CASE("InstallFlow_Dependencies_COM", "[InstallFlow][workflow][dependencies] REQUIRE(installationOrder.at(2) == "AppInstallerCliTest.TestExeInstaller.MultipleDependencies"); } +void InstallFlow_Dependencies_WindowsFeaturesArgument_Generic(std::string_view featureName) +{ + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + + context << ShellExecuteEnableWindowsFeature(featureName); + + INFO(installOutput.str()); + + REQUIRE(context.Contains(Execution::Data::OperationReturnCode)); + REQUIRE(context.Get() == E_INVALIDARG); +} + +TEST_CASE("InstallFlow_Dependencies_WindowsFeaturesArgument_Extras", "[InstallFlow][workflow][dependencies][111981]") +{ + TempFile potentialLogFile("dism-log", ".log"); + std::string featureName = "MediaPlayback /LogPath:"; + featureName.append(potentialLogFile.GetPath().u8string()); + + InstallFlow_Dependencies_WindowsFeaturesArgument_Generic(featureName); + + REQUIRE(!std::filesystem::exists(potentialLogFile)); +} + +TEST_CASE("InstallFlow_Dependencies_WindowsFeaturesArgument_Quoted", "[InstallFlow][workflow][dependencies][111981]") +{ + TempFile potentialLogFile("dism-log", ".log"); + std::string featureName = "\"MediaPlayback /LogPath:"; + featureName.append(potentialLogFile.GetPath().u8string()); + featureName.append("\""); + + InstallFlow_Dependencies_WindowsFeaturesArgument_Generic(featureName); + + REQUIRE(!std::filesystem::exists(potentialLogFile)); +} + // TODO: // add dependencies for installer tests to DependenciesTestSource (or a new one) // add tests for min version dependency solving diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index dfe4e97f76..5a8a0d0fda 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -354,3 +354,28 @@ TEST_CASE("ConvertControlCodesToPictures", "[strings]") REQUIRE(ConvertControlCodesToPictures(allCodes) == ConvertToUTF8(allPictures)); } + +TEST_CASE("IsValidWindowsFeaturePattern_AllFound_True", "[strings][111981]") +{ + for (const auto& name : { + "IIS-ODBCLogging", + "NetFx3", + "SMB1Protocol", + }) + { + INFO(name); + REQUIRE(IsValidWindowsFeaturePattern(name)); + } +} + +TEST_CASE("IsValidWindowsFeaturePattern_Bad_False", "[strings][111981]") +{ + for (const auto& name : { + "MediaPlayback /LogPath:C:\\file.txt", + "\"MediaPlayback /LogPath:C:\\file.txt\"", + }) + { + INFO(name); + REQUIRE(!IsValidWindowsFeaturePattern(name)); + } +} diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-BlockedMsiProperty.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-BlockedMsiProperty.yaml new file mode 100644 index 0000000000..cf29f69845 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-BlockedMsiProperty.yaml @@ -0,0 +1,19 @@ +# Installer with a blocked MSI property in a switch value +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json + +PackageIdentifier: AppInstallerCliTest.TestMsiInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSI Installer +ShortDescription: AppInstaller Test MSI Installer +Publisher: Microsoft Corporation +License: Test +InstallerType: msi +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + InstallerSwitches: + Silent: TRANSFORMS=evil.mst +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidMsiSwitches.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidMsiSwitches.yaml new file mode 100644 index 0000000000..6cf116e431 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidMsiSwitches.yaml @@ -0,0 +1,19 @@ +# Installer with unparseable MSI switch arguments +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json + +PackageIdentifier: AppInstallerCliTest.TestMsiInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSI Installer +ShortDescription: AppInstaller Test MSI Installer +Publisher: Microsoft Corporation +License: Test +InstallerType: msi +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + InstallerSwitches: + Silent: '@INVALID' +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidWindowsFeatureName.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidWindowsFeatureName.yaml new file mode 100644 index 0000000000..16a4fe190f --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-InvalidWindowsFeatureName.yaml @@ -0,0 +1,22 @@ +# Installer with an invalid Windows Feature name in dependencies +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json + +PackageIdentifier: AppInstallerCliTest.TestMsixInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test MSIX Installer +ShortDescription: AppInstaller Test MSIX Installer +Publisher: Microsoft Corporation +Moniker: AICLITestMsix +License: Test +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true + InstallerType: msix + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe + Dependencies: + WindowsFeatures: + - Invalid@Feature +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-NetworkAddressInSwitches.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-NetworkAddressInSwitches.yaml new file mode 100644 index 0000000000..9cb27a6488 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-NetworkAddressInSwitches.yaml @@ -0,0 +1,20 @@ +# Installer with a network address in a switch value +# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.0.0.schema.json + +PackageIdentifier: AppInstallerCliTest.TestExeInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Exe Installer +ShortDescription: AppInstaller Test Exe Installer +Publisher: Microsoft Corporation +License: Test +InstallerType: exe +Installers: + - Architecture: x64 + InstallerUrl: https://ThisIsNotUsed + InstallerSha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + InstallerSwitches: + Silent: http://evil.example.com + SilentWithProgress: /normal +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index 4c5021887e..dda1c1c9e4 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "TestSettings.h" #include #include #include @@ -54,6 +55,11 @@ namespace ValidateError(error, level, message, std::string(), std::string()); } + std::vector ValidateManifest(const Manifest& manifest, bool fullValidation) + { + return ValidateManifest(manifest, ManifestValidateOption{ fullValidation }); + } + struct ManifestExceptionMatcher : public Catch::Matchers::MatcherBase { ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly = false) : @@ -1360,6 +1366,67 @@ TEST_CASE("PortableFileTypeValidation", "[ManifestValidation]") REQUIRE(errors.size() == 0); } +TEST_CASE("WindowsFeatureNameValidation", "[ManifestValidation][111981]") +{ + // An invalid Windows Feature name should produce an error regardless of the fullValidation flag + Manifest invalidManifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Bad-InvalidWindowsFeatureName.yaml")); + + auto errors = ValidateManifest(invalidManifest, true); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Error, ManifestError::InvalidWindowsFeatureName, "Invalid@Feature", ""); + + errors = ValidateManifest(invalidManifest, false); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Error, ManifestError::InvalidWindowsFeatureName, "Invalid@Feature", ""); +} + +TEST_CASE("NetworkAddressInSwitchesValidation", "[ManifestValidation][111981]") +{ + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Bad-NetworkAddressInSwitches.yaml")); + + auto errors = ValidateManifest(manifest, true); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Warning, ManifestError::ContainsNetworkAddress, "http://evil.example.com", ""); + + ManifestValidateOption options{ true }; + options.ErrorOnNetworkAddressInSwitches = true; + errors = ValidateManifest(manifest, options); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Error, ManifestError::ContainsNetworkAddress, "http://evil.example.com", ""); + + errors = ValidateManifest(manifest, false); + REQUIRE(errors.size() == 0); +} + +TEST_CASE("BlockedMsiPropertyValidation", "[ManifestValidation][111981]") +{ + SECTION("Blocked property is detected under full validation") + { + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Bad-BlockedMsiProperty.yaml")); + + auto errors = ValidateManifest(manifest, true); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Error, ManifestError::BlockedMsiProperty, "TRANSFORMS", ""); + + // Not checked when fullValidation is false + errors = ValidateManifest(manifest, false); + REQUIRE(errors.size() == 0); + } + + SECTION("Invalid MSI switches are detected under full validation") + { + Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Bad-InvalidMsiSwitches.yaml")); + + auto errors = ValidateManifest(manifest, true); + REQUIRE(errors.size() == 1); + ValidateError(errors[0], ValidationError::Level::Error, ManifestError::InvalidMsiSwitches); + + // Not checked when fullValidation is false + errors = ValidateManifest(manifest, false); + REQUIRE(errors.size() == 0); + } +} + TEST_CASE("ReadManifestAndValidateMsixInstallers_Success", "[ManifestValidation]") { TestDataFile testFile("Manifest-Good-MsixInstaller.yaml"); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp index 82454b1847..eca420ea00 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -964,6 +964,13 @@ namespace AppInstaller::Manifest installerType == InstallerTypeEnum::Exe; } + bool DoesInstallerTypeUseMsiProperties(InstallerTypeEnum installerType) + { + return + installerType == InstallerTypeEnum::Msi || + installerType == InstallerTypeEnum::Wix; + } + bool IsArchiveType(InstallerTypeEnum installerType) { return (installerType == InstallerTypeEnum::Zip); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 50ef2ad2a5..2f877ceb60 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -8,6 +8,7 @@ #include "winget/MsixManifestValidation.h" #include "winget/Locale.h" #include "winget/Filesystem.h" +#include "winget/MsiExecArguments.h" namespace AppInstaller::Manifest { @@ -85,13 +86,29 @@ namespace AppInstaller::Manifest { AppInstaller::Manifest::ManifestError::SchemaHeaderUrlPatternMismatch, "The schema header URL does not match the expected pattern."sv }, { AppInstaller::Manifest::ManifestError::InvalidPortableFiletype, "The file type of the referenced file is not allowed."sv }, { AppInstaller::Manifest::ManifestError::InvalidFontFiletype, "The file type of the referenced file is not a supported font file type."sv }, + { AppInstaller::Manifest::ManifestError::InvalidWindowsFeatureName, "The provided value is not a valid Windows feature name."sv }, + { AppInstaller::Manifest::ManifestError::BlockedMsiProperty, "Contains a blocked MSI property."sv }, + { AppInstaller::Manifest::ManifestError::InvalidMsiSwitches, "Contains invalid MSI switches."sv }, + { AppInstaller::Manifest::ManifestError::ContainsNetworkAddress, "Installer switch contains network address."sv }, }; return ErrorIdToMessageMap; } + + bool ContainsSharePathSignifier(std::string_view input) + { + return Utility::CaseInsensitiveContainsSubstring(input, "\\\\"); + } + + bool ContainsNetworkAddressSignifier(std::string_view input) + { + return Utility::CaseInsensitiveContainsSubstring(input, "http://") || + Utility::CaseInsensitiveContainsSubstring(input, "https://") || + Utility::CaseInsensitiveContainsSubstring(input, "ftp://"); + } } - std::vector ValidateManifest(const Manifest& manifest, bool fullValidation) + std::vector ValidateManifest(const Manifest& manifest, const ManifestValidateOption& options) { std::vector resultErrors; @@ -115,7 +132,7 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "PackageVersion", manifest.Version); } - auto defaultLocErrors = ValidateManifestLocalization(manifest.DefaultLocalization, !fullValidation); + auto defaultLocErrors = ValidateManifestLocalization(manifest.DefaultLocalization, !options.FullValidation); std::move(defaultLocErrors.begin(), defaultLocErrors.end(), std::inserter(resultErrors, resultErrors.end())); // Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. @@ -166,7 +183,7 @@ namespace AppInstaller::Manifest for (auto const& installer : manifest.Installers) { // If not full validation, for future compatibility, skip validating unknown installers. - if (installer.EffectiveInstallerType() == InstallerTypeEnum::Unknown && !fullValidation) + if (installer.EffectiveInstallerType() == InstallerTypeEnum::Unknown && !options.FullValidation) { continue; } @@ -215,7 +232,7 @@ namespace AppInstaller::Manifest if (installer.EffectiveInstallerType() == InstallerTypeEnum::MSStore) { - if (fullValidation) + if (options.FullValidation) { // MSStore type is not supported in community repo resultErrors.emplace_back( @@ -247,7 +264,7 @@ namespace AppInstaller::Manifest // Ensure that each URL has a one to one mapping with a Sha256 and // warn if a Sha256 has a one to many mapping with a URL - if (fullValidation && !installer.Url.empty() && !installer.Sha256.empty()) + if (options.FullValidation && !installer.Url.empty() && !installer.Sha256.empty()) { std::string checksum = Utility::SHA256::ConvertToString(installer.Sha256); std::string url = installer.Url; @@ -351,7 +368,7 @@ namespace AppInstaller::Manifest } // If running full validation, check filetype - if (fullValidation) + if (options.FullValidation) { if (isPortable) { @@ -425,7 +442,7 @@ namespace AppInstaller::Manifest // Check AuthInfo validity. For full validation (community repo), authentication type must be none. if (installer.AuthInfo.Type != Authentication::AuthenticationType::None) { - if (fullValidation) + if (options.FullValidation) { // Authentication is not supported (must be none) in community repo. resultErrors.emplace_back(ManifestError::FieldNotSupported, "Authentication"); @@ -437,7 +454,15 @@ namespace AppInstaller::Manifest } } - if (fullValidation) + installer.Dependencies.ApplyToType(DependencyType::WindowsFeature, [&](const Dependency& dependency) + { + if (!IsValidWindowsFeaturePattern(dependency.Id())) + { + resultErrors.emplace_back(ManifestError::InvalidWindowsFeatureName, dependency.Id()); + } + }); + + if (options.FullValidation) { for (const auto& container : installer.DesiredStateConfiguration) { @@ -448,13 +473,50 @@ namespace AppInstaller::Manifest break; } } + + if (DoesInstallerTypeUseMsiProperties(installer.EffectiveInstallerType())) + { + try + { + for (const auto& item : installer.Switches) + { + if (!item.second.empty()) + { + auto blocked = Msi::ParseMSIArguments(item.second).GetFirstBlockedProperty(); + if (blocked) + { + resultErrors.emplace_back(ManifestError::BlockedMsiProperty, blocked.value()); + } + } + } + } + catch (...) + { + resultErrors.emplace_back(ManifestError::InvalidMsiSwitches); + } + } + + for (const auto& item : installer.Switches) + { + if (!item.second.empty()) + { + if (ContainsSharePathSignifier(item.second)) + { + resultErrors.emplace_back(ManifestError::ContainsNetworkAddress, item.second); + } + else if (ContainsNetworkAddressSignifier(item.second)) + { + resultErrors.emplace_back(ManifestError::ContainsNetworkAddress, item.second, options.ErrorOnNetworkAddressInSwitches ? ValidationError::Level::Error : ValidationError::Level::Warning); + } + } + } } } // Validate localizations for (auto const& localization : manifest.Localizations) { - auto locErrors = ValidateManifestLocalization(localization, !fullValidation); + auto locErrors = ValidateManifestLocalization(localization, !options.FullValidation); std::move(locErrors.begin(), locErrors.end(), std::inserter(resultErrors, resultErrors.end())); } diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp index 0e034c469a..d61d9efb78 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -477,7 +477,7 @@ namespace AppInstaller::Manifest::YamlParser // Extra semantic validations after basic validation and field population if (validateOption.FullValidation) { - errors = ValidateManifest(manifest); + errors = ValidateManifest(manifest, validateOption); std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); // Validate the schema header for manifest version 1.7 and above diff --git a/src/AppInstallerCommonCore/MsiExecArguments.cpp b/src/AppInstallerCommonCore/MsiExecArguments.cpp index e5c955b6a3..cc845f89dc 100644 --- a/src/AppInstallerCommonCore/MsiExecArguments.cpp +++ b/src/AppInstallerCommonCore/MsiExecArguments.cpp @@ -355,14 +355,14 @@ namespace AppInstaller::Msi // Validates that a token represents a property. // This checks that the property has the form PropertyName=Value, // with the value optionally quoted. - bool IsValidPropertyToken(std::string_view token) + std::optional ParsePropertyToken(std::string_view token) { THROW_HR_IF(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR, token.empty()); if (token[0] != '%' && !IsCharAlphaNumericA(token[0])) { AICLI_LOG(Core, Error, << "Bad property for msiexec: " << token); - return false; + return std::nullopt; } // Find the = separator at the end of the property name @@ -375,9 +375,11 @@ namespace AppInstaller::Msi if (pos == token.size() || token[pos] != '=') { AICLI_LOG(Core, Error, << "Expected property for call to msiexec, but couldn't find separator: " << token); - return false; + return std::nullopt; } + size_t nameLength = pos; + // Validate the property value. // It should be completely enclosed in quotes, or not contain white space. // If quoted, there can be pairs of consecutive quotes that work as escape sequences. @@ -386,7 +388,7 @@ namespace AppInstaller::Msi if (pos == token.size()) { // Empty value - return true; + return MsiParsedArguments::ParsedProperty{ std::string{ token.substr(0, nameLength) }, {} }; } // If quoted, we will only inspect the values between the quotes. @@ -438,7 +440,7 @@ namespace AppInstaller::Msi ++pos; } - return true; + return MsiParsedArguments::ParsedProperty{ std::string{ token.substr(0, nameLength) }, std::string{ token.substr(nameLength + 1) } }; } // Replaces long options in the arguments (e.g. /quiet), by their short equivalents @@ -502,9 +504,11 @@ namespace AppInstaller::Msi tokens.pop_front(); if (!IsSwitch(token)) { + auto propertyToken = ParsePropertyToken(token); // Token is a property, i.e. NAME=value. Add it to the parsed args. - THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT, !IsValidPropertyToken(token)); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT, !propertyToken); parsedArgs.Properties += L" " + Utility::ConvertToUTF16(token); + parsedArgs.ParsedProperties.emplace_back(std::move(propertyToken).value()); return; } @@ -550,6 +554,30 @@ namespace AppInstaller::Msi } } + std::optional MsiParsedArguments::GetFirstBlockedProperty() const + { + for (const auto& property : ParsedProperties) + { + auto lowerName = Utility::ToLower(property.first); + + for (const auto& blockedName : { + "transforms", + "patch", + "msinewinstance", + "adminproperties", + }) + { + if (blockedName == lowerName) + { + AICLI_LOG(Core, Warning, << "MSI arguments contain blocked property: " << lowerName); + return property.first; + } + } + } + + return std::nullopt; + } + MsiParsedArguments ParseMSIArguments(std::string_view arguments) { // Split the arguments into tokens, which we will process one by one. @@ -567,4 +595,4 @@ namespace AppInstaller::Msi return result; } -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index 96b7affb01..50a2fa2902 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -64,9 +64,13 @@ namespace AppInstaller::Manifest struct ManifestValidateOption { + ManifestValidateOption() = default; + explicit ManifestValidateOption(bool fullValidation) : FullValidation(fullValidation) {} + bool SchemaValidationOnly = false; bool ErrorOnVerifiedPublisherFields = false; bool InstallerValidation = false; + bool ErrorOnNetworkAddressInSwitches = false; // Options not exposed in winget util bool FullValidation = false; @@ -496,6 +500,9 @@ namespace AppInstaller::Manifest // Gets a value indicating whether the given installer requires RepairBehavior for repair. bool DoesInstallerTypeRequireRepairBehaviorForRepair(InstallerTypeEnum installerType); + // Gets a value indicating whether the given installer type uses MSI properties in its command line. + bool DoesInstallerTypeUseMsiProperties(InstallerTypeEnum installerType); + // Gets a value indicating whether the given installer type is an archive. bool IsArchiveType(InstallerTypeEnum installerType); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index 8fdbd6c3a8..b6595571f9 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -23,7 +23,9 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(ArpValidationError); WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionOverlapWithIndex); WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionValidationInternalError); + WINGET_DEFINE_RESOURCE_STRINGID(BlockedMsiProperty); WINGET_DEFINE_RESOURCE_STRINGID(BothAllowedAndExcludedMarketsDefined); + WINGET_DEFINE_RESOURCE_STRINGID(ContainsNetworkAddress); WINGET_DEFINE_RESOURCE_STRINGID(DuplicatePortableCommandAlias); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateRelativeFilePath); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale); @@ -54,7 +56,9 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(InstallerTypeDoesNotWriteAppsAndFeaturesEntry); WINGET_DEFINE_RESOURCE_STRINGID(InvalidBcp47Value); WINGET_DEFINE_RESOURCE_STRINGID(InvalidFieldValue); + WINGET_DEFINE_RESOURCE_STRINGID(InvalidMsiSwitches); WINGET_DEFINE_RESOURCE_STRINGID(InvalidRootNode); + WINGET_DEFINE_RESOURCE_STRINGID(InvalidWindowsFeatureName); WINGET_DEFINE_RESOURCE_STRINGID(MissingManifestDependenciesNode); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); WINGET_DEFINE_RESOURCE_STRINGID(MultiManifestPackageHasDependencies); @@ -253,7 +257,7 @@ namespace AppInstaller::Manifest }; // fullValidation: bool to set if manifest validation should perform extra validation that is not required for reading a manifest. - std::vector ValidateManifest(const Manifest& manifest, bool fullValidation = true); + std::vector ValidateManifest(const Manifest& manifest, const ManifestValidateOption& options); std::vector ValidateManifestLocalization(const ManifestLocalization& localization, bool treatErrorAsWarning = false); std::vector ValidateManifestInstallers(const Manifest& manifest, bool treatErrorAsWarning = false); } diff --git a/src/AppInstallerCommonCore/Public/winget/MsiExecArguments.h b/src/AppInstallerCommonCore/Public/winget/MsiExecArguments.h index 02f1233f63..07ea66d8ad 100644 --- a/src/AppInstallerCommonCore/Public/winget/MsiExecArguments.h +++ b/src/AppInstallerCommonCore/Public/winget/MsiExecArguments.h @@ -51,6 +51,14 @@ namespace AppInstaller::Msi // Properties string std::wstring Properties; + + using ParsedProperty = std::pair; + + // Contains the properties as split into name and value portions. + std::vector ParsedProperties; + + // Checks for properties that are blocked in some cases. + std::optional GetFirstBlockedProperty() const; }; // Parses a command line string for msiexec. diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp index 1095dc4b50..33aa8b10ea 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp @@ -256,7 +256,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 for (auto& manifestItem : manifests) { std::vector validationErrors = - AppInstaller::Manifest::ValidateManifest(manifestItem, false); + AppInstaller::Manifest::ValidateManifest(manifestItem, AppInstaller::Manifest::ManifestValidateOption{ false }); int errors = 0; for (auto& error : validationErrors) diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 99be199fda..6bee93edce 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -1118,4 +1118,22 @@ namespace AppInstaller::Utility return result; } + + bool IsValidWindowsFeaturePattern(std::string_view value) + { + if (value.empty()) + { + return false; + } + + for (char c : value) + { + if (!std::isalnum(static_cast(c)) && c != '-' && c != '_') + { + return false; + } + } + + return true; + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index ea2e1af88a..7b60542fe8 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -320,4 +320,7 @@ namespace AppInstaller::Utility // Generates a random alpha numeric string. std::string GetRandomString(size_t size = 8); + + // Checks whether a given string is a valid potential Windows feature name. + bool IsValidWindowsFeaturePattern(std::string_view value); } diff --git a/src/WinGetServer/WinGetServerManualActivation_Client.cpp b/src/WinGetServer/WinGetServerManualActivation_Client.cpp index c5b00b4495..b6dde2c96a 100644 --- a/src/WinGetServer/WinGetServerManualActivation_Client.cpp +++ b/src/WinGetServer/WinGetServerManualActivation_Client.cpp @@ -71,7 +71,7 @@ struct ServerProcessLauncher #ifndef USE_PROD_WINGET_SERVER // The feature that allows directly launching a packaged process as long as it has a matching alias - // requires a failure to trigger, and the dev package is not ACL'd to force this to happen. Attempting + // requires a failure to trigger, and the dev package ACL does not force this to happen. Attempting // to use the other code path results in an unpackaged server, causing other issues. // We run the product code above to ensure that it is functioning properly, but then replace it with // the path of the alias. diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp index f713a13544..d62de90b43 100644 --- a/src/WinGetUtil/Exports.cpp +++ b/src/WinGetUtil/Exports.cpp @@ -303,6 +303,7 @@ extern "C" validateOption.SchemaValidationOnly = WI_IsFlagSet(option, WinGetValidateManifestOption::SchemaValidationOnly); validateOption.ErrorOnVerifiedPublisherFields = WI_IsFlagSet(option, WinGetValidateManifestOption::ErrorOnVerifiedPublisherFields); validateOption.InstallerValidation = WI_IsFlagSet(option, WinGetValidateManifestOption::InstallerValidations); + validateOption.ErrorOnNetworkAddressInSwitches = WI_IsFlagSet(option, WinGetValidateManifestOption::ErrorOnNetworkAddressInSwitches); (void)YamlParser::CreateFromPath(inputPath, validateOption, mergedManifestPath ? mergedManifestPath : L""); @@ -348,6 +349,7 @@ extern "C" validateOption.ThrowOnWarning = true; validateOption.SchemaValidationOnly = WI_IsFlagClear(option, WinGetCreateManifestOption::SchemaAndSemanticValidation); validateOption.ErrorOnVerifiedPublisherFields = WI_IsFlagSet(option, WinGetCreateManifestOption::ReturnErrorOnVerifiedPublisherFields); + validateOption.ErrorOnNetworkAddressInSwitches = WI_IsFlagSet(option, WinGetCreateManifestOption::ReturnErrorOnNetworkAddressInSwitches); } if (WI_IsFlagSet(option, WinGetCreateManifestOption::AllowShadowManifest)) diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h index fe60ca77ed..bc3d34e2d1 100644 --- a/src/WinGetUtil/WinGetUtil.h +++ b/src/WinGetUtil/WinGetUtil.h @@ -26,6 +26,7 @@ extern "C" SchemaValidationOnly = 0x1, ErrorOnVerifiedPublisherFields = 0x2, InstallerValidations = 0x4, + ErrorOnNetworkAddressInSwitches = 0x8, }; DEFINE_ENUM_FLAG_OPERATORS(WinGetValidateManifestOption); @@ -45,6 +46,9 @@ extern "C" // Return error on manifest fields that require verified publishers, used during semantic validation ReturnErrorOnVerifiedPublisherFields = 0x1000, + + // Return error if a network address is present in installer switches. + ReturnErrorOnNetworkAddressInSwitches = 0x2000, }; DEFINE_ENUM_FLAG_OPERATORS(WinGetCreateManifestOption); diff --git a/src/WinGetUtilInterop/Common/Enums.cs b/src/WinGetUtilInterop/Common/Enums.cs index 77e90880a0..ad309a793a 100644 --- a/src/WinGetUtilInterop/Common/Enums.cs +++ b/src/WinGetUtilInterop/Common/Enums.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -41,7 +41,12 @@ public enum WinGetCreateManifestOption /// /// Return error on manifest fields that require verified publishers, used during semantic validation /// - ReturnErrorOnVerifiedPublisherFields = 0x1000, + ReturnErrorOnVerifiedPublisherFields = 0x1000, + + /// + /// Return error if a network address is present in installer switches. + /// + ReturnErrorOnNetworkAddressInSwitches = 0x2000, } /// diff --git a/tools/ManifestValidation/Invoke-ManifestValidation.ps1 b/tools/ManifestValidation/Invoke-ManifestValidation.ps1 new file mode 100644 index 0000000000..95fd75b40c --- /dev/null +++ b/tools/ManifestValidation/Invoke-ManifestValidation.ps1 @@ -0,0 +1,434 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Runs manifest validation on every manifest under a given path and produces an HTML report. + +.DESCRIPTION + Discovers all manifest directories under the specified path (the leaf directories + containing YAML files, as found in a winget-pkgs clone), runs 'wingetdev validate' + on each, shows progress, and writes a self-contained HTML report with no external + script or style references. + + wingetdev is resolved from PATH. An explicit path may be provided via -WingetDevPath + if wingetdev is not on PATH. + +.PARAMETER ManifestsPath + Path to search for manifests. May be the root of a winget-pkgs clone (the script will + descend into its 'manifests' subdirectory if present) or a manifests directory directly. + +.PARAMETER WingetDevPath + Optional explicit path to wingetdev.exe. If not provided, wingetdev is resolved from PATH. + +.PARAMETER OutputPath + Optional path for the HTML report file. Defaults to 'manifest-validation-report.html' + in the current working directory. + +.EXAMPLE + .\Invoke-ManifestValidation.ps1 -ManifestsPath C:\repos\winget-pkgs + +.EXAMPLE + .\Invoke-ManifestValidation.ps1 -ManifestsPath C:\repos\winget-pkgs\manifests ` + -WingetDevPath C:\tools\wingetdev.exe -OutputPath C:\reports\results.html +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Path to search for manifests (winget-pkgs root or manifests directory).")] + [string] $ManifestsPath, + + [Parameter(HelpMessage = "Path to wingetdev.exe. Resolved from PATH if not provided.")] + [string] $WingetDevPath, + + [Parameter(HelpMessage = "Output path for the HTML report.")] + [string] $OutputPath = (Join-Path (Get-Location) "manifest-validation-report.html"), + + [Parameter(HelpMessage = "Launch the HTML report in the default browser when complete.")] + [switch] $Launch, + + [Parameter(HelpMessage = "Exclude warnings from the report results table.")] + [switch] $SuppressWarnings, + + [Parameter(HelpMessage = "Resume from an existing report file, skipping already-completed top-level directories.")] + [switch] $Resume +) + +$ErrorActionPreference = "Stop" + +# --------------------------------------------------------------------------- +# HTML encoding helper (avoids requiring System.Web) +# --------------------------------------------------------------------------- +function ConvertTo-HtmlEncoded([string] $text) +{ + $text.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace('"', '"') +} + +# --------------------------------------------------------------------------- +# Resolve wingetdev +# --------------------------------------------------------------------------- +if ($WingetDevPath) +{ + if (-not (Test-Path $WingetDevPath -PathType Leaf)) + { + Write-Error -Category InvalidArgument -Message "wingetdev.exe not found at: $WingetDevPath" + } + $wingetDev = $WingetDevPath +} +else +{ + $wingetDevCmd = Get-Command "wingetdev" -ErrorAction SilentlyContinue + if (-not $wingetDevCmd) + { + Write-Error -Category ObjectNotFound -Message @" +wingetdev was not found on PATH. +Either add wingetdev to your PATH, or provide its location with -WingetDevPath. +"@ + } + $wingetDev = $wingetDevCmd.Source +} + +Write-Host "Using wingetdev: $wingetDev" + +$wingetDevVersion = (& $wingetDev --version 2>&1 | Out-String).Trim() + +# --------------------------------------------------------------------------- +# Resolve manifests search root +# --------------------------------------------------------------------------- +$ManifestsPath = [System.IO.Path]::GetFullPath($ManifestsPath) +if (-not (Test-Path $ManifestsPath -PathType Container)) +{ + Write-Error -Category InvalidArgument -Message "ManifestsPath does not exist or is not a directory: $ManifestsPath" +} + +# Support passing either the repo root (which contains a 'manifests' subdirectory) +# or the manifests directory itself. +$manifestsSubdir = Join-Path $ManifestsPath "manifests" +$searchRoot = if (Test-Path $manifestsSubdir -PathType Container) { $manifestsSubdir } else { $ManifestsPath } + +Write-Host "Discovering top-level directories under: $searchRoot" + +$tier1Dirs = Get-ChildItem $searchRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name +if (-not $tier1Dirs) +{ + Write-Error -Category ObjectNotFound -Message "No subdirectories found under: $searchRoot" +} +$tier1Total = $tier1Dirs.Count +$tier1Current = 0 + +Write-Host "Found $tier1Total top-level directories." + +# --------------------------------------------------------------------------- +# Initialize script-level state (shared with Write-ValidationReport) +# --------------------------------------------------------------------------- +$script:existingRowsHtml = '' +$script:completedTier1Dirs = [System.Collections.Generic.List[string]]::new() + +$passed = 0 +$warnings = 0 +$failed = 0 +$errors = 0 +$total = 0 + +if ($Resume) +{ + if (-not (Test-Path $OutputPath -PathType Leaf)) + { + Write-Error -Category ObjectNotFound -Message "Resume requested but no report file found at: $OutputPath" + } + + Write-Host "Reading resume state from: $OutputPath" + $content = Get-Content $OutputPath -Raw -Encoding utf8 + + if ($content -notmatch '(?s)') + { + Write-Error -Category InvalidData -Message "Could not find embedded resume state in: $OutputPath" + } + $state = $Matches[1].Trim() | ConvertFrom-Json + + foreach ($d in $state.completedTier1Dirs) { $script:completedTier1Dirs.Add($d) } + $passed = [int]$state.passed + $warnings = [int]$state.warnings + $failed = [int]$state.failed + $errors = [int]$state.errors + $total = [int]$state.total + + if ($content -match '(?s)(.*?)') + { + $script:existingRowsHtml = $Matches[1].Trim() + } + + Write-Host ("Resuming: {0} top-level directories already complete, {1} manifests already processed." -f $script:completedTier1Dirs.Count, $total) +} + +# --------------------------------------------------------------------------- +# Report writing helper +# --------------------------------------------------------------------------- +function Write-ValidationReport +{ + param( + [System.Collections.Generic.List[PSCustomObject]] $Results, + [int] $Total, + [int] $Passed, + [int] $Warnings, + [int] $Failed, + [int] $Errors + ) + + # Always back up the existing report before overwriting - guards against data loss + # during a long-running write. State is already in memory at this point. + if (Test-Path $OutputPath -PathType Leaf) + { + $dir = [System.IO.Path]::GetDirectoryName($OutputPath) + $base = [System.IO.Path]::GetFileNameWithoutExtension($OutputPath) + $ext = [System.IO.Path]::GetExtension($OutputPath) + $backup = Join-Path $dir "$base.backup$ext" + Move-Item $OutputPath $backup -Force + } + + $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + $escapedRoot = ConvertTo-HtmlEncoded $searchRoot + $escapedWingetVer = ConvertTo-HtmlEncoded $wingetDevVersion + + # Rows from the previous (resumed) run come first; new rows from this run follow. + $newRowsHtml = ($Results | ForEach-Object { + $statusClass = $_.Status.ToLower() + $escapedPath = ConvertTo-HtmlEncoded $_.RelativePath + $escapedOutput = (ConvertTo-HtmlEncoded $_.Output) -replace "`r?`n", '
' + " $escapedPath$($_.Status)$escapedOutput" + }) -join "`n" + + $rowsHtml = if ($script:existingRowsHtml -and $newRowsHtml) { "$($script:existingRowsHtml)`n$newRowsHtml" } + elseif ($script:existingRowsHtml) { $script:existingRowsHtml } + else { $newRowsHtml } + + # Embed progress state so the run can be resumed later. + $stateJson = [PSCustomObject]@{ + completedTier1Dirs = @($script:completedTier1Dirs) + tier1Total = $tier1Total + total = $Total + passed = $Passed + warnings = $Warnings + failed = $Failed + errors = $Errors + } | ConvertTo-Json -Compress + + $completed = $script:completedTier1Dirs.Count + $bannerHtml = if ($completed -lt $tier1Total) { + "
Validation in progress — results are partial$completed of $tier1Total top-level directories complete.
" + } else { '' } + + $html = @" + + + + + + Manifest Validation Report + + + +

Manifest Validation Report

+$bannerHtml +
+ Generated: $timestamp  •  + wingetdev: $escapedWingetVer  •  + Path: $escapedRoot +
+ +
+
$Total
Total
+
$Passed
Pass
+
$Warnings
Warning
+
$Failed
Fail
+
$Errors
Error
+
+ +
+ + + +
+ + + + + + + + + + +$rowsHtml + +
PathStatusOutput
+ + + + + +"@ + + $html | Out-File -FilePath $OutputPath -Encoding utf8 -Force +} + +# --------------------------------------------------------------------------- +# Validate manifests with two-tier progress +# --------------------------------------------------------------------------- +$results = [System.Collections.Generic.List[PSCustomObject]]::new() + +foreach ($tier1 in $tier1Dirs) +{ + $tier1Current++ + Write-Progress -Id 1 -Activity "Processing top-level directories" ` + -Status "($tier1Current / $tier1Total) $($tier1.Name)" ` + -PercentComplete (($tier1Current / $tier1Total) * 100) + + if ($script:completedTier1Dirs.Contains($tier1.Name)) + { + Write-Host "Skipping (already complete): $($tier1.Name)" + continue + } + + # Discover manifest directories (leaf dirs with .yaml files) under this tier-1 dir. + $yamlFiles = Get-ChildItem $tier1.FullName -Recurse -File -Filter "*.yaml" -ErrorAction SilentlyContinue + $manifestDirs = if ($yamlFiles) { $yamlFiles | Select-Object -ExpandProperty DirectoryName | Sort-Object -Unique } else { @() } + $tier2Total = $manifestDirs.Count + $tier2Current = 0 + $total += $tier2Total + + foreach ($dir in $manifestDirs) + { + $tier2Current++ + $relativePath = $dir.Substring($searchRoot.Length).TrimStart([char]'\', [char]'/') + + Write-Progress -Id 2 -ParentId 1 -Activity "Validating manifests" ` + -Status "($tier2Current / $tier2Total) $relativePath" ` + -PercentComplete (($tier2Current / $tier2Total) * 100) + + $output = & $wingetDev validate $dir 2>&1 | Out-String + $exitCode = $LASTEXITCODE + + $status = if ($output -match 'Manifest validation succeeded with warnings') { 'Warning' } + elseif ($output -match 'Manifest validation succeeded') { 'Pass' } + elseif ($output -match 'Manifest validation failed') { 'Fail' } + else { 'Error' } + + switch ($status) + { + 'Pass' { $passed++; break } + 'Warning' { $warnings++; break } + 'Fail' { $failed++; break } + 'Error' { $errors++; break } + } + + $keepInReport = $status -ne 'Pass' -and (-not $SuppressWarnings -or $status -ne 'Warning') + if ($keepInReport) + { + $results.Add([PSCustomObject]@{ + RelativePath = $relativePath + AbsolutePath = $dir + Status = $status + ExitCode = $exitCode + Output = $output.Trim() + }) + } + } + + Write-Progress -Id 2 -Completed + + $script:completedTier1Dirs.Add($tier1.Name) + Write-ValidationReport -Results $results -Total $total -Passed $passed ` + -Warnings $warnings -Failed $failed -Errors $errors +} + +Write-Progress -Id 1 -Completed + +Write-Host "" +Write-Host ("Results: Total={0} Pass={1} Warning={2} Fail={3} Error={4}" -f $total, $passed, $warnings, $failed, $errors) + +if ($Launch) +{ + Start-Process $OutputPath +} +else +{ + Write-Host "Report created at $OutputPath" +} From f5b00a2cdb0907c8612e96e5604fc0d356122c9c Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Thu, 14 May 2026 08:52:08 -0700 Subject: [PATCH 5/6] Update `configure export --all` for recent DSC changes (#6226) CP #6222 to 1.28 Updates our `ExportAll` test to use `_` instead of `.` in the name of the subdirectory resource as DSC 3.2 is now strict about this. Improves our DSC resource filtering mechanism to target some well-known resources and then exclude all resources that reside in that same location. This will prevent new DSC provided resources from being included by default. --- .../Workflows/ConfigurationFlow.cpp | 160 ++++++++++++++---- .../ConfigureExportCommand.cs | 2 +- src/AppInstallerTestExeInstaller/main.cpp | 2 +- 3 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 17d6fcfd99..a93c065956 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -101,16 +101,29 @@ namespace AppInstaller::CLI::Workflow return { L"Microsoft.WinGet/", L"Microsoft.WinGet.Dev/", - L"Microsoft.DSC.Debug/", + }; + }; + + // Returns unit type prefixes that identify resources known to be shipped with DSC. + // These are used both to exclude the resources themselves and to discover the DSC + // installation location, so that any co-located resources are also excluded. + std::vector DscShippedResourcePrefixes() + { + return { L"Microsoft.DSC/", + L"Microsoft.DSC.Debug/", L"Microsoft.DSC.Transitional/", - L"Microsoft.Windows/RebootPending", - L"Microsoft.Windows/Registry", - L"Microsoft.Windows/WMI", - L"Microsoft.Windows/WindowsPowerShell", - L"Microsoft/OSInfo" + L"Microsoft/OSInfo", }; - }; + } + + // Returns unit type prefixes for DSC-shipped resources that are still allowed to + // appear in the exported configuration. All other DSC-shipped resources are excluded by default. + std::vector DscShippedResourcesAllowList() + { + // Currently empty: all DSC-shipped resources are excluded from export by default. + return {}; + } Logging::Level ConvertLevel(DiagnosticLevel level) { @@ -1582,10 +1595,10 @@ namespace AppInstaller::CLI::Workflow } } - std::vector GetAllUnitProcessors(Execution::Context& context) + std::vector GetAllUnitProcessors3(Execution::Context& context) { ConfigurationContext& configContext = context.Get(); - std::vector result; + std::vector result; // Only supported by dsc v3 processor. if (ConfigurationRemoting::ProcessorEngine::DSCv3 == ConfigurationRemoting::DetermineProcessorEngine(configContext.Set())) @@ -1601,7 +1614,11 @@ namespace AppInstaller::CLI::Workflow auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { findAction.Cancel(); }); for (auto unitProcessor : findAction.get()) { - result.emplace_back(std::move(unitProcessor)); + IConfigurationUnitProcessorDetails3 processor3; + if (unitProcessor.try_as(processor3)) + { + result.emplace_back(std::move(processor3)); + } } } @@ -1683,19 +1700,13 @@ namespace AppInstaller::CLI::Workflow struct UnitProcessorTree { private: - struct SourceAndPackage - { - PackageCollection::Source Source; - PackageCollection::Package Package; - }; - struct Node { // Packages whose installed location is at this node - std::vector Packages; + std::vector Packages; // Units whose location is at this node. - std::vector Units; + std::vector Units; }; Filesystem::PathTree m_pathTree; @@ -1707,33 +1718,29 @@ namespace AppInstaller::CLI::Workflow } public: - UnitProcessorTree(std::vector&& unitProcessors) + UnitProcessorTree(std::vector&& unitProcessors) { for (auto&& unit : unitProcessors) { - IConfigurationUnitProcessorDetails3 unitProcessor3; - if (unit.try_as(unitProcessor3)) - { - winrt::hstring unitPath = unitProcessor3.Path(); - AICLI_LOG(Config, Verbose, << "Found unit `" << Utility::ConvertToUTF8(unit.UnitType()) << "` at: " << Utility::ConvertToUTF8(unitPath)); - Node& node = FindNodeForFilePath(unitPath); - node.Units.emplace_back(std::move(unit)); - } + winrt::hstring unitPath = unit.Path(); + AICLI_LOG(Config, Verbose, << "Found unit `" << Utility::ConvertToUTF8(unit.UnitType()) << "` at: " << Utility::ConvertToUTF8(unitPath)); + Node& node = FindNodeForFilePath(unitPath); + node.Units.emplace_back(std::move(unit)); } } - void PlacePackage(const PackageCollection::Source& source, const PackageCollection::Package& package) + void PlacePackage(const PackageCollection::Package& package) { Node* node = m_pathTree.Find(package.InstalledLocation); if (node) { - node->Packages.emplace_back(SourceAndPackage{ source, package }); + node->Packages.emplace_back(package); } } - std::vector GetResourcesForPackage(const PackageCollection::Package& package) const + std::vector GetResourcesForPackage(const PackageCollection::Package& package) const { - std::vector result; + std::vector result; m_pathTree.VisitIf( package.InstalledLocation, @@ -1760,10 +1767,10 @@ namespace AppInstaller::CLI::Workflow std::wstring packageUnitType = GetWinGetPackageUnitType(configContext); // This will be later used by per package settings export. - std::vector unitProcessors; + std::vector unitProcessors; try { - unitProcessors = GetAllUnitProcessors(context); + unitProcessors = GetAllUnitProcessors3(context); } catch (...) { @@ -1776,11 +1783,13 @@ namespace AppInstaller::CLI::Workflow // Filter out processors in exclusion list. for (auto itr = unitProcessors.begin(); itr != unitProcessors.end(); /* itr incremented in the logic */) { + std::wstring unitType{ itr->UnitType() }; bool processorRemoved = false; for (const auto& exclusionItem : exclusionList) { - if (Utility::CaseInsensitiveStartsWith(itr->UnitType(), exclusionItem)) + if (Utility::CaseInsensitiveStartsWith(unitType, exclusionItem)) { + AICLI_LOG(Config, Verbose, << "Filtering excluded resource `" << Utility::ConvertToUTF8(itr->UnitType()) << "` from export"); itr = unitProcessors.erase(itr); processorRemoved = true; break; @@ -1793,6 +1802,87 @@ namespace AppInstaller::CLI::Workflow } } + // Filter out DSC-shipped resources and any resources co-located with them. + // First pass: find the parent directory of each known DSC resource to identify the DSC + // installation location(s), handling both packaged and unpackaged (PATH-based) DSC installs. + // Second pass: remove any resource that either matches a known DSC prefix or resides in one + // of those locations, unless the resource type appears in DscShippedResourcesAllowList(). + { + const auto dscPrefixes = DscShippedResourcePrefixes(); + const auto dscAllowList = DscShippedResourcesAllowList(); + + std::set dscLocations; + for (const auto& processor : unitProcessors) + { + std::wstring unitType{ processor.UnitType() }; + for (const auto& prefix : dscPrefixes) + { + if (Utility::CaseInsensitiveStartsWith(unitType, prefix)) + { + std::filesystem::path location = std::filesystem::weakly_canonical( + std::filesystem::path{ std::wstring{ processor.Path() } }.parent_path()); + dscLocations.emplace(std::move(location)); + break; + } + } + } + + for (auto itr = unitProcessors.begin(); itr != unitProcessors.end(); /* itr incremented in the logic */) + { + std::wstring unitType{ itr->UnitType() }; + + // Check the allow list first. + bool inAllowList = false; + for (const auto& allowedPrefix : dscAllowList) + { + if (Utility::CaseInsensitiveStartsWith(unitType, allowedPrefix)) + { + inAllowList = true; + break; + } + } + + if (inAllowList) + { + ++itr; + continue; + } + + // Check if the resource type is a known DSC-shipped prefix. + bool isDscResource = false; + for (const auto& prefix : dscPrefixes) + { + if (Utility::CaseInsensitiveStartsWith(unitType, prefix)) + { + isDscResource = true; + break; + } + } + + // If not matched by prefix, check whether it shares a location with a known DSC resource. + if (!isDscResource && !dscLocations.empty()) + { + std::filesystem::path location = std::filesystem::weakly_canonical( + std::filesystem::path{ std::wstring{ itr->Path() } }.parent_path()); + + if (dscLocations.find(location) != dscLocations.end()) + { + isDscResource = true; + } + } + + if (isDscResource) + { + AICLI_LOG(Config, Verbose, << "Filtering DSC-shipped resource `" << Utility::ConvertToUTF8(itr->UnitType()) << "` from export"); + itr = unitProcessors.erase(itr); + } + else + { + ++itr; + } + } + } + // Build a tree of the unit processors and place packages onto it to indicate nearest ownership. UnitProcessorTree unitProcessorTree{ std::move(unitProcessors) }; @@ -1800,7 +1890,7 @@ namespace AppInstaller::CLI::Workflow { for (const auto& package : source.Packages) { - unitProcessorTree.PlacePackage(source, package); + unitProcessorTree.PlacePackage(package); } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs index cfc30554b8..bf68f03a28 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureExportCommand.cs @@ -178,7 +178,7 @@ public void ExportAll() Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); Assert.True(showResult.StdOut.Contains("data: TestData")); - Assert.True(showResult.StdOut.Contains("AppInstallerTest/TestResource.SubDirectory")); + Assert.True(showResult.StdOut.Contains("AppInstallerTest/TestResource_SubDirectory")); Assert.True(showResult.StdOut.Contains($"Dependencies: {Constants.TestSourceName}_AppInstallerTest.TestPackageExport")); Assert.True(showResult.StdOut.Contains("data: TestData")); } diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index f0e86644cd..cd93e3618f 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -218,7 +218,7 @@ void GenerateDSCv3ProviderFiles(const path& installDirectory, const std::wstring if (!subDirectory.empty()) { - DscResourceJsonContent += '.'; + DscResourceJsonContent += '_'; DscResourceJsonContent += subDirectory; } From 21b633b5eab694719c56943ebfccbaeb3c18e93a Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Wed, 20 May 2026 15:13:59 -0700 Subject: [PATCH 6/6] =?UTF-8?q?Update=20rest=20source=20and=20wingetutil?= =?UTF-8?q?=20interop=20to=20include=201.28=20manifest=20(#=E2=80=A6=20(#6?= =?UTF-8?q?237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppInstallerCLITests.vcxproj | 1 + .../AppInstallerCLITests.vcxproj.filters | 3 + src/AppInstallerCLITests/RestClient.cpp | 2 +- .../RestInterface_1_28.cpp | 440 ++++++++++++++++++ .../AppInstallerRepositoryCore.vcxproj | 4 + ...AppInstallerRepositoryCore.vcxproj.filters | 18 + .../ManifestJSONParser.cpp | 7 +- .../Rest/RestClient.cpp | 6 + .../Rest/Schema/1_28/Interface.h | 21 + .../Schema/1_28/Json/ManifestDeserializer.h | 17 + .../1_28/Json/ManifestDeserializer_1_28.cpp | 116 +++++ .../Rest/Schema/1_28/RestInterface_1_28.cpp | 26 ++ .../Rest/Schema/CommonRestConstants.h | 1 + .../ManifestUnitTest/V1ManifestReadTest.cs | 36 ++ .../TestCollateral/V1_28ManifestMerged.yaml | 296 ++++++++++++ .../WinGetUtilInterop.UnitTests.csproj | 3 + .../V1/InstallerDSCPowerShellModule.cs | 31 ++ .../V1/InstallerDSCPowerShellResource.cs | 19 + .../Manifest/V1/InstallerDSCv3.cs | 21 + .../Manifest/V1/InstallerDSCv3Resource.cs | 19 + .../V1/InstallerDesiredStateConfiguration.cs | 26 ++ src/WinGetUtilInterop/Manifest/V1/Manifest.cs | 25 +- .../Manifest/V1/ManifestInstaller.cs | 31 +- 23 files changed, 1144 insertions(+), 25 deletions(-) create mode 100644 src/AppInstallerCLITests/RestInterface_1_28.cpp create mode 100644 src/AppInstallerRepositoryCore/Rest/Schema/1_28/Interface.h create mode 100644 src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer.h create mode 100644 src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer_1_28.cpp create mode 100644 src/AppInstallerRepositoryCore/Rest/Schema/1_28/RestInterface_1_28.cpp create mode 100644 src/WinGetUtilInterop.UnitTests/TestCollateral/V1_28ManifestMerged.yaml create mode 100644 src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellModule.cs create mode 100644 src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellResource.cs create mode 100644 src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3.cs create mode 100644 src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3Resource.cs create mode 100644 src/WinGetUtilInterop/Manifest/V1/InstallerDesiredStateConfiguration.cs diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 0d3a09a72c..babb057f0f 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -284,6 +284,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 779b1d2490..faffd5d8dd 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -383,6 +383,9 @@ Source Files\Repository + + Source Files\Repository + Source Files\Common diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp index 97e1d54c91..46de28a7df 100644 --- a/src/AppInstallerCLITests/RestClient.cpp +++ b/src/AppInstallerCLITests/RestClient.cpp @@ -63,7 +63,7 @@ TEST_CASE("GetSupportedInterface", "[RestSource]") REQUIRE(RestClient::GetSupportedInterface(TestRestUri, {}, info, {}, version, {})->GetVersion() == version); // Update this test to next version so that we don't forget to add to supported versions before rest e2e tests are available. - Version invalid{ "1.13.0" }; + Version invalid{ "1.29.0" }; REQUIRE_THROWS_HR(RestClient::GetSupportedInterface(TestRestUri, {}, info, {}, invalid, {}), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); Authentication::AuthenticationArguments authArgs; diff --git a/src/AppInstallerCLITests/RestInterface_1_28.cpp b/src/AppInstallerCLITests/RestInterface_1_28.cpp new file mode 100644 index 0000000000..4286430f00 --- /dev/null +++ b/src/AppInstallerCLITests/RestInterface_1_28.cpp @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestRestRequestHandler.h" +#include +#include +#include +#include + +using namespace TestCommon; +using namespace AppInstaller::Http; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_28; + +namespace +{ + const std::string TestRestUriString = "http://restsource.com/api"; + + struct GoodManifest_AllFields + { + utility::string_t GetSampleManifest_AllFields() + { + return _XPLATSTR( + R"delimiter( + { + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "3.0.0abc", + "DefaultLocale": { + "PackageLocale": "en-US", + "Publisher": "Foo", + "PublisherUrl": "http://publisher.net", + "PublisherSupportUrl": "http://publisherSupport.net", + "PrivacyUrl": "http://packagePrivacyUrl.net", + "Author": "FooBar", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl.net", + "ShortDescription": "Foo bar is a foo bar.", + "Description": "Foo bar is a placeholder.", + "Tags": [ + "FooBar", + "Foo", + "Bar" + ], + "Moniker": "FooBarMoniker", + "ReleaseNotes": "Default release notes", + "ReleaseNotesUrl": "https://DefaultReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "DefaultLabel", + "Agreement": "DefaultText", + "AgreementUrl": "https://DefaultAgreementUrl.net" + }], + "PurchaseUrl": "http://DefaultPurchaseUrl.net", + "InstallationNotes": "Default Installation Notes", + "Documentations": [{ + "DocumentLabel": "Default Document Label", + "DocumentUrl": "http://DefaultDocumentUrl.net" + }], + "Icons": [{ + "IconUrl": "https://DefaultTestIcon", + "IconFileType": "ico", + "IconResolution": "custom", + "IconTheme": "default", + "IconSha256": "69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123" + }] + }, + "Channel": "", + "Locales": [ + { + "PackageLocale": "fr-Fr", + "Publisher": "Foo French", + "PublisherUrl": "http://publisher-fr.net", + "PublisherSupportUrl": "http://publisherSupport-fr.net", + "PrivacyUrl": "http://packagePrivacyUrl-fr.net", + "Author": "FooBar French", + "PackageName": "Bar", + "PackageUrl": "http://packageUrl-fr.net", + "License": "Foo Bar License", + "LicenseUrl": "http://licenseUrl-fr.net", + "Copyright": "Foo Bar Copyright", + "CopyrightUrl": "http://copyrightUrl-fr.net", + "ShortDescription": "Foo bar is a foo bar French.", + "Description": "Foo bar is a placeholder French.", + "Tags": [ + "FooBarFr", + "FooFr", + "BarFr" + ], + "ReleaseNotes": "Release notes", + "ReleaseNotesUrl": "https://ReleaseNotes.net", + "Agreements": [{ + "AgreementLabel": "Label", + "Agreement": "Text", + "AgreementUrl": "https://AgreementUrl.net" + }], + "PurchaseUrl": "http://purchaseUrl.net", + "InstallationNotes": "Installation Notes", + "Documentations": [{ + "DocumentLabel": "Document Label", + "DocumentUrl": "http://documentUrl.net" + }], + "Icons": [{ + "IconUrl": "https://testIcon", + "IconFileType": "png", + "IconResolution": "32x32", + "IconTheme": "light", + "IconSha256": "69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321" + }] + } + ],)delimiter") _XPLATSTR(R"delimiter( + "Installers": [ + { + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerUrl": "http://foobar.zip", + "Architecture": "x86", + "InstallerLocale": "en-US", + "Platform": [ + "Windows.Desktop" + ], + "MinimumOSVersion": "1078", + "InstallerType": "zip", + "Scope": "user", + "InstallModes": [ + "interactive" + ], + "InstallerSwitches": { + "Silent": "/s", + "SilentWithProgress": "/s", + "Interactive": "/i", + "InstallLocation": "C:\\Users\\User1", + "Log": "/l", + "Upgrade": "/u", + "Custom": "/custom", + "Repair": "/repair" + }, + "InstallerSuccessCodes": [ + 0 + ], + "UpgradeBehavior": "deny", + "Commands": [ + "command1" + ], + "Protocols": [ + "protocol1" + ], + "FileExtensions": [ + ".file-extension" + ], + "Dependencies": { + "WindowsFeatures": [ + "feature1" + ], + "WindowsLibraries": [ + "library1" + ], + "PackageDependencies": [ + { + "PackageIdentifier": "Foo.Baz", + "MinimumVersion": "2.0.0" + } + ], + "ExternalDependencies": [ + "FooBarBaz" + ] + }, + "ProductCode": "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d", + "ReleaseDate": "2021-01-01", + "InstallerAbortsTerminal": true, + "InstallLocationRequired": true, + "RequireExplicitUpgrade": true, + "UnsupportedOSArchitectures": [ "arm" ], + "ElevationRequirement": "elevatesSelf", + "AppsAndFeaturesEntries": [{ + "DisplayName": "DisplayName", + "DisplayVersion": "DisplayVersion", + "Publisher": "Publisher", + "ProductCode": "ProductCode", + "UpgradeCode": "UpgradeCode", + "InstallerType": "exe" + }], + "Markets" : { + "AllowedMarkets": [ "US" ] + }, + "ExpectedReturnCodes": [{ + "InstallerReturnCode": 3, + "ReturnResponse": "custom", + "ReturnResponseUrl": "http://returnResponseUrl.net" + }], + "NestedInstallerType": "portable", + "DisplayInstallWarnings": true, + "UnsupportedArguments": [ "log" ], + "NestedInstallerFiles": [{ + "RelativeFilePath": "test\\app.exe", + "PortableCommandAlias": "test.exe" + }], + "InstallationMetadata": { + "DefaultInstallLocation": "%TEMP%\\DefaultInstallLocation", + "Files": [{ + "RelativeFilePath": "test\\app.exe", + "FileSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "FileType": "launch", + "InvocationParameter": "/parameter", + "DisplayName": "test" + }] + }, + "DownloadCommandProhibited": true, + "RepairBehavior": "uninstaller", + "ArchiveBinariesDependOnPath": true, + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Resource": "TestResource", + "Scope" : "TestScope" + } + }, + "DesiredStateConfiguration": { + "PowerShell": [{ + "RepositoryUrl": "https://www.powershellgallery.com/api/v2", + "ModuleName": "TestModule", + "Resources": [{ + "Name": "TestResource" + }] + }], + "DSCv3": { + "Resources": [{ + "Type": "TestPublisher.TestProduct/TestResource" + }] + } + } + } + ] + } + ] + }, + "ContinuationToken": "abcd" + })delimiter"); + } + + void VerifyLocalizations_AllFields(const AppInstaller::Manifest::Manifest& manifest) + { + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get() == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://publisher.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://publisherSupport.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://packagePrivacyUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get() == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://packageUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "Foo Bar License"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://licenseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "Foo Bar Copyright"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://copyrightUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "Foo bar is a foo bar."); + REQUIRE(manifest.DefaultLocalization.Get() == "Foo bar is a placeholder."); + REQUIRE(manifest.DefaultLocalization.Get().size() == 3); + REQUIRE(manifest.DefaultLocalization.Get().at(0) == "FooBar"); + REQUIRE(manifest.DefaultLocalization.Get().at(1) == "Foo"); + REQUIRE(manifest.DefaultLocalization.Get().at(2) == "Bar"); + REQUIRE(manifest.DefaultLocalization.Get() == "Default release notes"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://DefaultReleaseNotes.net"); + REQUIRE(manifest.DefaultLocalization.Get().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get().at(0).Label == "DefaultLabel"); + REQUIRE(manifest.DefaultLocalization.Get().at(0).AgreementText == "DefaultText"); + REQUIRE(manifest.DefaultLocalization.Get().at(0).AgreementUrl == "https://DefaultAgreementUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "http://DefaultPurchaseUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get() == "Default Installation Notes"); + REQUIRE(manifest.DefaultLocalization.Get().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get().at(0).DocumentLabel == "Default Document Label"); + REQUIRE(manifest.DefaultLocalization.Get().at(0).DocumentUrl == "http://DefaultDocumentUrl.net"); + REQUIRE(manifest.DefaultLocalization.Get().size() == 1); + REQUIRE(manifest.DefaultLocalization.Get().at(0).Url == "https://DefaultTestIcon"); + REQUIRE(manifest.DefaultLocalization.Get().at(0).FileType == IconFileTypeEnum::Ico); + REQUIRE(manifest.DefaultLocalization.Get().at(0).Resolution == IconResolutionEnum::Custom); + REQUIRE(manifest.DefaultLocalization.Get().at(0).Theme == IconThemeEnum::Default); + REQUIRE(manifest.DefaultLocalization.Get().at(0).Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123")); + + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization frenchLocalization = manifest.Localizations.at(0); + REQUIRE(frenchLocalization.Locale == "fr-Fr"); + REQUIRE(frenchLocalization.Get() == "Foo French"); + REQUIRE(frenchLocalization.Get() == "http://publisher-fr.net"); + REQUIRE(frenchLocalization.Get() == "http://publisherSupport-fr.net"); + REQUIRE(frenchLocalization.Get() == "http://packagePrivacyUrl-fr.net"); + REQUIRE(frenchLocalization.Get() == "FooBar French"); + REQUIRE(frenchLocalization.Get() == "Bar"); + REQUIRE(frenchLocalization.Get() == "http://packageUrl-fr.net"); + REQUIRE(frenchLocalization.Get() == "Foo Bar License"); + REQUIRE(frenchLocalization.Get() == "http://licenseUrl-fr.net"); + REQUIRE(frenchLocalization.Get() == "Foo Bar Copyright"); + REQUIRE(frenchLocalization.Get() == "http://copyrightUrl-fr.net"); + REQUIRE(frenchLocalization.Get() == "Foo bar is a foo bar French."); + REQUIRE(frenchLocalization.Get() == "Foo bar is a placeholder French."); + REQUIRE(frenchLocalization.Get().size() == 3); + REQUIRE(frenchLocalization.Get().at(0) == "FooBarFr"); + REQUIRE(frenchLocalization.Get().at(1) == "FooFr"); + REQUIRE(frenchLocalization.Get().at(2) == "BarFr"); + REQUIRE(frenchLocalization.Get() == "Release notes"); + REQUIRE(frenchLocalization.Get() == "https://ReleaseNotes.net"); + REQUIRE(frenchLocalization.Get().size() == 1); + REQUIRE(frenchLocalization.Get().at(0).Label == "Label"); + REQUIRE(frenchLocalization.Get().at(0).AgreementText == "Text"); + REQUIRE(frenchLocalization.Get().at(0).AgreementUrl == "https://AgreementUrl.net"); + REQUIRE(frenchLocalization.Get() == "http://purchaseUrl.net"); + REQUIRE(frenchLocalization.Get() == "Installation Notes"); + REQUIRE(frenchLocalization.Get().size() == 1); + REQUIRE(frenchLocalization.Get().at(0).DocumentLabel == "Document Label"); + REQUIRE(frenchLocalization.Get().at(0).DocumentUrl == "http://documentUrl.net"); + REQUIRE(frenchLocalization.Get().size() == 1); + REQUIRE(frenchLocalization.Get().at(0).Url == "https://testIcon"); + REQUIRE(frenchLocalization.Get().at(0).FileType == IconFileTypeEnum::Png); + REQUIRE(frenchLocalization.Get().at(0).Resolution == IconResolutionEnum::Square32); + REQUIRE(frenchLocalization.Get().at(0).Theme == IconThemeEnum::Light); + REQUIRE(frenchLocalization.Get().at(0).Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321")); + } + + void VerifyInstallers_AllFields(const AppInstaller::Manifest::Manifest& manifest) + { + REQUIRE(manifest.Installers.size() == 1); + + ManifestInstaller actualInstaller = manifest.Installers.at(0); + REQUIRE(actualInstaller.Sha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.Url == "http://foobar.zip"); + REQUIRE(actualInstaller.Arch == Architecture::X86); + REQUIRE(actualInstaller.Locale == "en-US"); + REQUIRE(actualInstaller.Platform.size() == 1); + REQUIRE(actualInstaller.Platform[0] == PlatformEnum::Desktop); + REQUIRE(actualInstaller.MinOSVersion == "1078"); + REQUIRE(actualInstaller.BaseInstallerType == InstallerTypeEnum::Zip); + REQUIRE(actualInstaller.Scope == ScopeEnum::User); + REQUIRE(actualInstaller.InstallModes.size() == 1); + REQUIRE(actualInstaller.InstallModes.at(0) == InstallModeEnum::Interactive); + REQUIRE(actualInstaller.Switches.size() == 8); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::SilentWithProgress) == "/s"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::InstallLocation) == "C:\\Users\\User1"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Log) == "/l"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Update) == "/u"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(actualInstaller.Switches.at(InstallerSwitchType::Repair) == "/repair"); + REQUIRE(actualInstaller.InstallerSuccessCodes.size() == 1); + REQUIRE(actualInstaller.InstallerSuccessCodes.at(0) == 0); + REQUIRE(actualInstaller.UpdateBehavior == UpdateBehaviorEnum::Deny); + REQUIRE(actualInstaller.Commands.at(0) == "command1"); + REQUIRE(actualInstaller.Protocols.at(0) == "protocol1"); + REQUIRE(actualInstaller.FileExtensions.at(0) == ".file-extension"); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsFeature, "feature1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::WindowsLibrary, "library1")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::Package, "Foo.Baz", "2.0.0")); + REQUIRE(actualInstaller.Dependencies.HasExactDependency(DependencyType::External, "FooBarBaz")); + REQUIRE(actualInstaller.PackageFamilyName == ""); + REQUIRE(actualInstaller.ProductCode == "5b6e0f8a-3bbf-4a17-aefd-024c2b3e075d"); + REQUIRE(actualInstaller.ReleaseDate == "2021-01-01"); + REQUIRE(actualInstaller.InstallerAbortsTerminal); + REQUIRE(actualInstaller.InstallLocationRequired); + REQUIRE(actualInstaller.RequireExplicitUpgrade); + REQUIRE(actualInstaller.ElevationRequirement == ElevationRequirementEnum::ElevatesSelf); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.size() == 1); + REQUIRE(actualInstaller.UnsupportedOSArchitectures.at(0) == Architecture::Arm); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.size() == 1); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayName == "DisplayName"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).DisplayVersion == "DisplayVersion"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).Publisher == "Publisher"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).ProductCode == "ProductCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).UpgradeCode == "UpgradeCode"); + REQUIRE(actualInstaller.AppsAndFeaturesEntries.at(0).InstallerType == InstallerTypeEnum::Exe); + REQUIRE(actualInstaller.Markets.AllowedMarkets.size() == 1); + REQUIRE(actualInstaller.Markets.AllowedMarkets.at(0) == "US"); + REQUIRE(actualInstaller.ExpectedReturnCodes.at(3).ReturnResponseEnum == ExpectedReturnCodeEnum::Custom); + REQUIRE(actualInstaller.ExpectedReturnCodes.at(3).ReturnResponseUrl == "http://returnResponseUrl.net"); + REQUIRE(actualInstaller.NestedInstallerType == InstallerTypeEnum::Portable); + REQUIRE(actualInstaller.DisplayInstallWarnings); + REQUIRE(actualInstaller.UnsupportedArguments.size() == 1); + REQUIRE(actualInstaller.UnsupportedArguments.at(0) == UnsupportedArgumentEnum::Log); + REQUIRE(actualInstaller.NestedInstallerFiles.size() == 1); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.NestedInstallerFiles.at(0).PortableCommandAlias == "test.exe"); + REQUIRE(actualInstaller.InstallationMetadata.DefaultInstallLocation == "%TEMP%\\DefaultInstallLocation"); + REQUIRE(actualInstaller.InstallationMetadata.Files.size() == 1); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).RelativeFilePath == "test\\app.exe"); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileType == InstalledFileTypeEnum::Launch); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).FileSha256 == AppInstaller::Utility::SHA256::ConvertToBytes("011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6")); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).InvocationParameter == "/parameter"); + REQUIRE(actualInstaller.InstallationMetadata.Files.at(0).DisplayName == "test"); + REQUIRE(actualInstaller.DownloadCommandProhibited); + REQUIRE(actualInstaller.RepairBehavior == RepairBehaviorEnum::Uninstaller); + REQUIRE(actualInstaller.ArchiveBinariesDependOnPath); + REQUIRE(actualInstaller.AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId); + REQUIRE(actualInstaller.AuthInfo.MicrosoftEntraIdInfo.has_value()); + REQUIRE(actualInstaller.AuthInfo.MicrosoftEntraIdInfo->Resource == "TestResource"); + REQUIRE(actualInstaller.AuthInfo.MicrosoftEntraIdInfo->Scope == "TestScope"); + + // DesiredStateConfiguration + REQUIRE(actualInstaller.DesiredStateConfiguration.size() == 2); + + // PowerShell entry + REQUIRE(actualInstaller.DesiredStateConfiguration[0].Type == DesiredStateConfigurationContainerType::PowerShell); + REQUIRE(actualInstaller.DesiredStateConfiguration[0].RepositoryURL == "https://www.powershellgallery.com/api/v2"); + REQUIRE(actualInstaller.DesiredStateConfiguration[0].ModuleName == "TestModule"); + REQUIRE(actualInstaller.DesiredStateConfiguration[0].Resources.size() == 1); + REQUIRE(actualInstaller.DesiredStateConfiguration[0].Resources[0].Name == "TestResource"); + + // DSCv3 entry + REQUIRE(actualInstaller.DesiredStateConfiguration[1].Type == DesiredStateConfigurationContainerType::DSCv3); + REQUIRE(actualInstaller.DesiredStateConfiguration[1].Resources.size() == 1); + REQUIRE(actualInstaller.DesiredStateConfiguration[1].Resources[0].Name == "TestPublisher.TestProduct/TestResource"); + } + }; +} + +TEST_CASE("GetManifests_GoodResponse_V1_28", "[RestSource][Interface_1_28]") +{ + GoodManifest_AllFields sampleManifest; + utility::string_t sample = sampleManifest.GetSampleManifest_AllFields(); + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, std::move(sample)) }; + Interface v1_28{ TestRestUriString, std::move(helper), {} }; + std::vector manifests = v1_28.GetManifests("Foo.Bar"); + REQUIRE(manifests.size() == 1); + + // Verify manifest is populated + Manifest& manifest = manifests[0]; + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "3.0.0abc"); + REQUIRE(manifest.Moniker == "FooBarMoniker"); + REQUIRE(manifest.Channel == ""); + REQUIRE(manifest.ManifestVersion == AppInstaller::Manifest::ManifestVer{ "1.28.0" }); + sampleManifest.VerifyLocalizations_AllFields(manifest); + sampleManifest.VerifyInstallers_AllFields(manifest); +} diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index d02ada420b..85e5d0a014 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -377,6 +377,8 @@ + + @@ -478,6 +480,8 @@ + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 7e89c4bf56..3263dcc7c6 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -121,6 +121,12 @@ {2075b51e-aa11-473a-bae0-e0d4366f926b} + + {a3b7c8d1-e2f4-5a6b-9c0d-1e2f3a4b5c6d} + + + {b4c8d9e2-f3a5-6b7c-0d1e-2f3a4b5c6d7e} + @@ -501,6 +507,12 @@ Rest\Schema\1_12\Json + + Rest\Schema\1_28 + + + Rest\Schema\1_28\Json + Rest @@ -785,6 +797,12 @@ Rest\Schema\1_12\Json + + Rest\Schema\1_28 + + + Rest\Schema\1_28\Json + Rest diff --git a/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp b/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp index 17386bbf54..1043dc83f5 100644 --- a/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp +++ b/src/AppInstallerRepositoryCore/ManifestJSONParser.cpp @@ -11,6 +11,7 @@ #include "Rest/Schema/1_9/Json/ManifestDeserializer.h" #include "Rest/Schema/1_10/Json/ManifestDeserializer.h" #include "Rest/Schema/1_12/Json/ManifestDeserializer.h" +#include "Rest/Schema/1_28/Json/ManifestDeserializer.h" namespace AppInstaller::Repository::JSON { @@ -61,10 +62,14 @@ namespace AppInstaller::Repository::JSON { m_pImpl->m_deserializer = std::make_unique(); } - else + else if (parts.size() > 1 && parts[1].Integer < 28) { m_pImpl->m_deserializer = std::make_unique(); } + else + { + m_pImpl->m_deserializer = std::make_unique(); + } } else { diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp index ffc9609a2f..49e6458739 100644 --- a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp +++ b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -12,6 +12,7 @@ #include "Rest/Schema/1_9/Interface.h" #include "Rest/Schema/1_10/Interface.h" #include "Rest/Schema/1_12/Interface.h" +#include "Rest/Schema/1_28/Interface.h" #include "Rest/Schema/InformationResponseDeserializer.h" #include "Rest/Schema/CommonRestConstants.h" #include @@ -37,6 +38,7 @@ namespace AppInstaller::Repository::Rest Version_1_9_0, Version_1_10_0, Version_1_12_0, + Version_1_28_0, }; constexpr std::string_view WindowsPackageManagerHeader = "Windows-Package-Manager"sv; @@ -218,6 +220,10 @@ namespace AppInstaller::Repository::Rest { return std::make_unique(api, helper, information, additionalHeaders, authArgs); } + else if (version == Version_1_28_0) + { + return std::make_unique(api, helper, information, additionalHeaders, authArgs); + } THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Interface.h new file mode 100644 index 0000000000..0fcc0a4fcd --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Interface.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_12/Interface.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_28 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public V1_12::Interface + { + Interface(const std::string& restApi, const Http::HttpClientHelper& helper, IRestClient::Information information, const Http::HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, Authentication::AuthenticationArguments authArgs = {}); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + Utility::Version GetVersion() const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer.h new file mode 100644 index 0000000000..6466d6fe7a --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_12/Json/ManifestDeserializer.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_28::Json +{ + // Manifest Deserializer. + struct ManifestDeserializer : public V1_12::Json::ManifestDeserializer + { + protected: + + std::optional DeserializeInstaller(const web::json::value& installerJsonObject) const override; + + Manifest::ManifestVer GetManifestVersion() const override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer_1_28.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer_1_28.cpp new file mode 100644 index 0000000000..4224c0a98d --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/Json/ManifestDeserializer_1_28.cpp @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ManifestDeserializer.h" +#include + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::V1_28::Json +{ + namespace + { + constexpr std::string_view DesiredStateConfiguration = "DesiredStateConfiguration"sv; + constexpr std::string_view PowerShell = "PowerShell"sv; + constexpr std::string_view DSCv3 = "DSCv3"sv; + constexpr std::string_view RepositoryUrl = "RepositoryUrl"sv; + constexpr std::string_view ModuleName = "ModuleName"sv; + constexpr std::string_view Resources = "Resources"sv; + constexpr std::string_view Name = "Name"sv; + constexpr std::string_view Type = "Type"sv; + } + + std::optional ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + auto result = V1_12::Json::ManifestDeserializer::DeserializeInstaller(installerJsonObject); + + if (result) + { + auto& installer = result.value(); + + // DesiredStateConfiguration + auto dscNode = JSON::GetJsonValueFromNode(installerJsonObject, JSON::GetUtilityString(DesiredStateConfiguration)); + if (dscNode) + { + const auto& dscObject = dscNode->get(); + if (!dscObject.is_null() && dscObject.is_object()) + { + // PowerShell DSC modules + auto powerShellNode = JSON::GetRawJsonArrayFromJsonNode(dscObject, JSON::GetUtilityString(PowerShell)); + if (powerShellNode) + { + for (auto const& moduleNode : powerShellNode->get()) + { + std::optional repositoryUrl = JSON::GetRawStringValueFromJsonNode(moduleNode, JSON::GetUtilityString(RepositoryUrl)); + std::optional moduleName = JSON::GetRawStringValueFromJsonNode(moduleNode, JSON::GetUtilityString(ModuleName)); + + if (!JSON::IsValidNonEmptyStringValue(repositoryUrl) || !JSON::IsValidNonEmptyStringValue(moduleName)) + { + AICLI_LOG(Repo, Error, << "Missing required fields in DesiredStateConfiguration PowerShell entry."); + continue; + } + + std::vector resources; + auto resourcesNode = JSON::GetRawJsonArrayFromJsonNode(moduleNode, JSON::GetUtilityString(Resources)); + if (resourcesNode) + { + for (auto const& resourceNode : resourcesNode->get()) + { + std::optional name = JSON::GetRawStringValueFromJsonNode(resourceNode, JSON::GetUtilityString(Name)); + if (JSON::IsValidNonEmptyStringValue(name)) + { + DesiredStateConfigurationResourceInfo resourceInfo; + resourceInfo.Name = std::move(*name); + resources.emplace_back(std::move(resourceInfo)); + } + } + } + + if (!resources.empty()) + { + installer.DesiredStateConfiguration.emplace_back(std::move(*repositoryUrl), std::move(*moduleName), std::move(resources)); + } + } + } + + // DSCv3 resources + auto dscv3Node = JSON::GetJsonValueFromNode(dscObject, JSON::GetUtilityString(DSCv3)); + if (dscv3Node) + { + const auto& dscv3Object = dscv3Node->get(); + if (!dscv3Object.is_null() && dscv3Object.is_object()) + { + std::vector resources; + auto resourcesNode = JSON::GetRawJsonArrayFromJsonNode(dscv3Object, JSON::GetUtilityString(Resources)); + if (resourcesNode) + { + for (auto const& resourceNode : resourcesNode->get()) + { + std::optional type = JSON::GetRawStringValueFromJsonNode(resourceNode, JSON::GetUtilityString(Type)); + if (JSON::IsValidNonEmptyStringValue(type)) + { + DesiredStateConfigurationResourceInfo resourceInfo; + resourceInfo.Name = std::move(*type); + resources.emplace_back(std::move(resourceInfo)); + } + } + } + + if (!resources.empty()) + { + installer.DesiredStateConfiguration.emplace_back(std::move(resources)); + } + } + } + } + } + } + + return result; + } + + Manifest::ManifestVer ManifestDeserializer::GetManifestVersion() const + { + return Manifest::s_ManifestVersionV1_28; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_28/RestInterface_1_28.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/RestInterface_1_28.cpp new file mode 100644 index 0000000000..d2381e85f5 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_28/RestInterface_1_28.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_28/Interface.h" +#include "Rest/Schema/CommonRestConstants.h" +#include "Rest/Schema/IRestClient.h" +#include +#include + +namespace AppInstaller::Repository::Rest::Schema::V1_28 +{ + Interface::Interface( + const std::string& restApi, + const Http::HttpClientHelper& httpClientHelper, + IRestClient::Information information, + const Http::HttpClientHelper::HttpRequestHeaders& additionalHeaders, + Authentication::AuthenticationArguments authArgs) : V1_12::Interface(restApi, httpClientHelper, std::move(information), additionalHeaders, std::move(authArgs)) + { + m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_28_0.ToString()); + } + + Utility::Version Interface::GetVersion() const + { + return Version_1_28_0; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h index 97a3806eff..2454e33e3e 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h +++ b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h @@ -15,6 +15,7 @@ namespace AppInstaller::Repository::Rest::Schema const Utility::Version Version_1_9_0{ "1.9.0" }; const Utility::Version Version_1_10_0{ "1.10.0" }; const Utility::Version Version_1_12_0{ "1.12.0" }; + const Utility::Version Version_1_28_0{ "1.28.0" }; // General API response constants constexpr std::string_view Data = "Data"sv; diff --git a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs index 1c3eefaca4..d01b309dad 100644 --- a/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs +++ b/src/WinGetUtilInterop.UnitTests/ManifestUnitTest/V1ManifestReadTest.cs @@ -38,6 +38,7 @@ private enum TestManifestVersion V1_9_0, V1_10_0, V1_12_0, + V1_28_0, } /// @@ -81,6 +82,11 @@ public void ReadV1ManifestsAndVerifyContents() Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestCollateral", ManifestStrings.V1_12_0ManifestMerged)); this.ValidateManifestFields(v1_12_0manifest, TestManifestVersion.V1_12_0); + + Manifest v1_28_0manifest = Manifest.CreateManifestFromPath( + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestCollateral", ManifestStrings.V1_28_0ManifestMerged)); + + this.ValidateManifestFields(v1_28_0manifest, TestManifestVersion.V1_28_0); } /// @@ -500,6 +506,31 @@ private void ValidateManifestFields(Manifest manifest, TestManifestVersion manif Assert.Equal("https://www.microsoft.com/msixsdk/msixsdkx64.exe", installer4.Url); Assert.Equal("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82", installer4.Sha256); } + + if (manifestVersion >= TestManifestVersion.V1_28_0) + { + // Root level DesiredStateConfiguration + Assert.NotNull(manifest.DesiredStateConfiguration); + Assert.Single(manifest.DesiredStateConfiguration.PowerShell); + Assert.Equal("https://www.powershellgallery.com/api/v2", manifest.DesiredStateConfiguration.PowerShell[0].RepositoryUrl); + Assert.Equal("DefaultTestModule", manifest.DesiredStateConfiguration.PowerShell[0].ModuleName); + Assert.Single(manifest.DesiredStateConfiguration.PowerShell[0].Resources); + Assert.Equal("DefaultTestResource", manifest.DesiredStateConfiguration.PowerShell[0].Resources[0].Name); + Assert.NotNull(manifest.DesiredStateConfiguration.DSCv3); + Assert.Single(manifest.DesiredStateConfiguration.DSCv3.Resources); + Assert.Equal("DefaultPublisher.DefaultProduct/DefaultResource", manifest.DesiredStateConfiguration.DSCv3.Resources[0].Type); + + // Installer level DesiredStateConfiguration + Assert.NotNull(installer1.DesiredStateConfiguration); + Assert.Single(installer1.DesiredStateConfiguration.PowerShell); + Assert.Equal("https://www.powershellgallery.com/api/v2", installer1.DesiredStateConfiguration.PowerShell[0].RepositoryUrl); + Assert.Equal("TestModule", installer1.DesiredStateConfiguration.PowerShell[0].ModuleName); + Assert.Single(installer1.DesiredStateConfiguration.PowerShell[0].Resources); + Assert.Equal("TestResource", installer1.DesiredStateConfiguration.PowerShell[0].Resources[0].Name); + Assert.NotNull(installer1.DesiredStateConfiguration.DSCv3); + Assert.Single(installer1.DesiredStateConfiguration.DSCv3.Resources); + Assert.Equal("TestPublisher.TestProduct/TestResource", installer1.DesiredStateConfiguration.DSCv3.Resources[0].Type); + } } /// @@ -542,6 +573,11 @@ internal class ManifestStrings /// Merged v1.12 manifest. /// public const string V1_12_0ManifestMerged = "V1_12ManifestMerged.yaml"; + + /// + /// Merged v1.28 manifest. + /// + public const string V1_28_0ManifestMerged = "V1_28ManifestMerged.yaml"; #pragma warning restore SA1310 // FieldNamesMustNotContainUnderscore /// diff --git a/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_28ManifestMerged.yaml b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_28ManifestMerged.yaml new file mode 100644 index 0000000000..06d16a1152 --- /dev/null +++ b/src/WinGetUtilInterop.UnitTests/TestCollateral/V1_28ManifestMerged.yaml @@ -0,0 +1,296 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-US +Publisher: Microsoft +PublisherUrl: https://www.microsoft.com +PublisherSupportUrl: https://www.microsoft.com/support +PrivacyUrl: https://www.microsoft.com/privacy +Author: Microsoft +PackageName: MSIX SDK +PackageUrl: https://www.microsoft.com/msixsdk/home +License: MIT License +LicenseUrl: https://www.microsoft.com/msixsdk/license +Copyright: Copyright Microsoft Corporation +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright +ShortDescription: This is MSIX SDK +Description: The MSIX SDK project is an effort to enable developers +Moniker: msixsdk +Tags: + - "appxsdk" + - "msixsdk" +ReleaseNotes: Default release notes +ReleaseNotesUrl: https://DefaultReleaseNotes.net +PurchaseUrl: https://DefaultPurchaseUrl.com +InstallationNotes: Default installation notes +Documentations: + - DocumentLabel: Default document label + DocumentUrl: https://DefaultDocumentUrl.com +Icons: + - IconUrl: https://testIcon + IconFileType: ico + IconResolution: custom + IconTheme: default + IconSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8123 +Agreements: + - AgreementLabel: DefaultLabel + Agreement: DefaultText + AgreementUrl: https://DefaultAgreementUrl.net +InstallerLocale: en-US +Platform: + - Windows.Desktop + - Windows.Universal +MinimumOSVersion: 10.0.0.0 +InstallerType: zip +Scope: machine +InstallModes: + - interactive + - silent + - silentWithProgress +InstallerSwitches: + Custom: /custom + SilentWithProgress: /silentwithprogress + Silent: /silence + Interactive: /interactive + Log: /log= + InstallLocation: /dir= + Upgrade: /upgrade + Repair: /repair +InstallerSuccessCodes: + - 1 + - 0x80070005 +UpgradeBehavior: uninstallPrevious +Commands: + - makemsix + - makeappx +Protocols: + - protocol1 + - protocol2 +FileExtensions: + - appx + - msix + - appxbundle + - msixbundle +Dependencies: + WindowsFeatures: + - IIS + WindowsLibraries: + - VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDep + MinimumVersion: 1.0.0 + ExternalDependencies: + - Outside dependencies +Capabilities: + - internetClient +RestrictedCapabilities: + - runFullTrust +PackageFamilyName: Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +ProductCode: "{Foo}" +ReleaseDate: 2021-01-01 +InstallerAbortsTerminal: true +InstallLocationRequired: true +RequireExplicitUpgrade: true +DisplayInstallWarnings: true +ElevationRequirement: elevatesSelf +UnsupportedOSArchitectures: + - arm +AppsAndFeaturesEntries: + - DisplayName: DisplayName + DisplayVersion: DisplayVersion + Publisher: Publisher + ProductCode: ProductCode + UpgradeCode: UpgradeCode + InstallerType: exe +Markets: + AllowedMarkets: + - US +ExpectedReturnCodes: + - InstallerReturnCode: 2 + ReturnResponse: contactSupport + ReturnResponseUrl: https://defaultReturnResponseUrl.com + - InstallerReturnCode: 3 + ReturnResponse: custom +UnsupportedArguments: + - log +NestedInstallerType: msi +NestedInstallerFiles: + - RelativeFilePath: RelativeFilePath + PortableCommandAlias: PortableCommandAlias +InstallationMetadata: + DefaultInstallLocation: "%ProgramFiles%\\TestApp" + Files: + - RelativeFilePath: "main.exe" + FileSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + FileType: launch + InvocationParameter: "/arg" + DisplayName: "DisplayName" +DownloadCommandProhibited: true +ArchiveBinariesDependOnPath: true +RepairBehavior: uninstaller +Authentication: + AuthenticationType: microsoftEntraId + MicrosoftEntraIdAuthenticationInfo: + Resource: DefaultResource + Scope: DefaultScope +DesiredStateConfiguration: + PowerShell: + - RepositoryUrl: https://www.powershellgallery.com/api/v2 + ModuleName: DefaultTestModule + Resources: + - Name: DefaultTestResource + DSCv3: + Resources: + - Type: DefaultPublisher.DefaultProduct/DefaultResource +Localization: +- Agreements: + - Agreement: Text + AgreementLabel: Label + AgreementUrl: https://AgreementUrl.net + Author: Microsoft UK + Copyright: Copyright Microsoft Corporation UK + CopyrightUrl: https://www.microsoft.com/msixsdk/copyright/UK + Description: The MSIX SDK project is an effort to enable developers UK + License: MIT License UK + LicenseUrl: https://www.microsoft.com/msixsdk/license/UK + PackageLocale: en-GB + PackageName: MSIX SDK UK + PackageUrl: https://www.microsoft.com/msixsdk/home/UK + PrivacyUrl: https://www.microsoft.com/privacy/UK + Publisher: Microsoft UK + PublisherSupportUrl: https://www.microsoft.com/support/UK + PublisherUrl: https://www.microsoft.com/UK + ReleaseNotes: Release notes + ReleaseNotesUrl: https://ReleaseNotes.net + ShortDescription: This is MSIX SDK UK + Tags: + - appxsdkUK + - msixsdkUK + PurchaseUrl: https://PurchaseUrl.com + InstallationNotes: Installation notes + Documentations: + - DocumentLabel: Document label + DocumentUrl: https://DocumentUrl.com + Icons: + - IconUrl: https://testIcon2 + IconFileType: png + IconResolution: 32x32 + IconTheme: dark + IconSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8321 +Installers: + - Architecture: x86 + InstallerLocale: en-GB + Platform: + - Windows.Desktop + MinimumOSVersion: 10.0.1.0 + InstallerType: msix + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx86.msix + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + SignatureSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + Scope: user + InstallModes: + - interactive + InstallerSwitches: + Custom: /c + SilentWithProgress: /sp + Silent: /s + Interactive: /i + Log: /l= + InstallLocation: /d= + Upgrade: /u + Repair: /r + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + MinimumVersion: 1.0.0 + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + ReleaseDate: 2021-02-02 + InstallerAbortsTerminal: false + InstallLocationRequired: false + NestedInstallerFiles: + - RelativeFilePath: RelativeFilePath2 + PortableCommandAlias: PortableCommandAlias2 + RequireExplicitUpgrade: false + DisplayInstallWarnings: true + ElevationRequirement: elevationRequired + NestedInstallerType: msi + UnsupportedArguments: + - location + UnsupportedOSArchitectures: + - arm64 + Markets: + ExcludedMarkets: + - "US" + ExpectedReturnCodes: + - InstallerReturnCode: 2 + ReturnResponse: contactSupport + ReturnResponseUrl: https://returnResponseUrl.com + DownloadCommandProhibited: true + ArchiveBinariesDependOnPath: false + RepairBehavior: modify + InstallationMetadata: + DefaultInstallLocation: "%ProgramFiles%\\TestApp" + Files: + - RelativeFilePath: "main2.exe" + FileSha256: 79D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + FileType: launch2 + InvocationParameter: "/arg2" + DisplayName: "DisplayName2" + Authentication: + AuthenticationType: microsoftEntraId + MicrosoftEntraIdAuthenticationInfo: + Resource: Resource + Scope: Scope + DesiredStateConfiguration: + PowerShell: + - RepositoryUrl: https://www.powershellgallery.com/api/v2 + ModuleName: TestModule + Resources: + - Name: TestResource + DSCv3: + Resources: + - Type: TestPublisher.TestProduct/TestResource + - Architecture: x64 + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerType: exe + ProductCode: '{Bar}' + MSStoreProductIdentifier: fakeIdentifier + - Architecture: neutral + InstallerType: zip + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + NestedInstallerType: font + NestedInstallerFiles: + - RelativeFilePath: relativeFilePath1.otf + - RelativeFilePath: relativeFilePath2.ttf + - RelativeFilePath: relativeFilePath3.fnt + - RelativeFilePath: relativeFilePath4.ttc + - RelativeFilePath: relativeFilePath5.otc + - Architecture: neutral + InstallerType: font + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 +ManifestType: merged +ManifestVersion: 1.28.0 diff --git a/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj b/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj index afdc951f23..f8b3507bad 100644 --- a/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj +++ b/src/WinGetUtilInterop.UnitTests/WinGetUtilInterop.UnitTests.csproj @@ -105,6 +105,9 @@ Always + + Always + PreserveNewest diff --git a/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellModule.cs b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellModule.cs new file mode 100644 index 0000000000..6bd0fadae2 --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellModule.cs @@ -0,0 +1,31 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + using System.Collections.Generic; + + /// + /// PowerShell DSC module item. + /// + public class InstallerDSCPowerShellModule + { + /// + /// Gets or sets the repository URL. + /// + public string RepositoryUrl { get; set; } + + /// + /// Gets or sets the module name. + /// + public string ModuleName { get; set; } + + /// + /// Gets or sets the list of resources contained in the module. + /// + public List Resources { get; set; } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellResource.cs b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellResource.cs new file mode 100644 index 0000000000..9883b61822 --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCPowerShellResource.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + /// + /// PowerShell DSC resource item. + /// + public class InstallerDSCPowerShellResource + { + /// + /// Gets or sets the name of the resource. + /// + public string Name { get; set; } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3.cs b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3.cs new file mode 100644 index 0000000000..3ce552f81f --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + using System.Collections.Generic; + + /// + /// DSCv3 resource info. + /// + public class InstallerDSCv3 + { + /// + /// Gets or sets the list of DSCv3 resources. + /// + public List Resources { get; set; } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3Resource.cs b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3Resource.cs new file mode 100644 index 0000000000..d831ef29fa --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/InstallerDSCv3Resource.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + /// + /// DSCv3 resource item. + /// + public class InstallerDSCv3Resource + { + /// + /// Gets or sets the type of the resource. + /// + public string Type { get; set; } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/InstallerDesiredStateConfiguration.cs b/src/WinGetUtilInterop/Manifest/V1/InstallerDesiredStateConfiguration.cs new file mode 100644 index 0000000000..24fe213ecb --- /dev/null +++ b/src/WinGetUtilInterop/Manifest/V1/InstallerDesiredStateConfiguration.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGetUtil.Models.V1 +{ + using System.Collections.Generic; + + /// + /// Desired state configuration info for an installer. + /// + public class InstallerDesiredStateConfiguration + { + /// + /// Gets or sets the list of PowerShell DSC module items. + /// + public List PowerShell { get; set; } + + /// + /// Gets or sets the DSCv3 resource info. + /// + public InstallerDSCv3 DSCv3 { get; set; } + } +} diff --git a/src/WinGetUtilInterop/Manifest/V1/Manifest.cs b/src/WinGetUtilInterop/Manifest/V1/Manifest.cs index 9df3343f84..9c5908bd9a 100644 --- a/src/WinGetUtilInterop/Manifest/V1/Manifest.cs +++ b/src/WinGetUtilInterop/Manifest/V1/Manifest.cs @@ -317,28 +317,33 @@ public class Manifest /// /// Gets or sets the default list of installer expected return codes. /// - public List ExpectedReturnCodes { get; set; } - + public List ExpectedReturnCodes { get; set; } + /// /// Gets or sets a value indicating whether the installer is prohibited from being downloaded for offline installation. /// - public bool? DownloadCommandProhibited { get; set; } - - /// - /// Gets or sets a value indicating whether the install location should be added directly to the PATH environment variable. - /// + public bool? DownloadCommandProhibited { get; set; } + + /// + /// Gets or sets a value indicating whether the install location should be added directly to the PATH environment variable. + /// public bool? ArchiveBinariesDependOnPath { get; set; } /// /// Gets or sets the default repair behavior. /// - public string RepairBehavior { get; set; } - + public string RepairBehavior { get; set; } + /// /// Gets or sets the default installer authentication info. - /// + /// public InstallerAuthentication Authentication { get; set; } + /// + /// Gets or sets the default desired state configuration info. + /// + public InstallerDesiredStateConfiguration DesiredStateConfiguration { get; set; } + /// /// Gets or sets collection of ManifestInstaller. At least one is required. /// diff --git a/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs b/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs index 172bb0de58..d38b10e1ad 100644 --- a/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs +++ b/src/WinGetUtilInterop/Manifest/V1/ManifestInstaller.cs @@ -35,15 +35,15 @@ public class ManifestInstaller /// /// Gets or sets the signature SHA256 for an appx/msix. Only used by appx/msix type. /// - public string SignatureSha256 { get; set; } - + public string SignatureSha256 { get; set; } + /// /// Gets or sets the Store ProductId. Only used when InstallerType is MSStore. - /// + /// [YamlMember(Alias = "MSStoreProductIdentifier")] public string ProductId { get; set; } - - // Common installer fields that may have defaults in manifest root level. + + // Common installer fields that may have defaults in manifest root level. /// /// Gets or sets the installer locale. @@ -204,23 +204,28 @@ public class ManifestInstaller /// /// Gets or sets a value indicating whether the installer is prohibited from being downloaded for offline installation. /// - public bool? DownloadCommandProhibited { get; set; } - - /// - /// Gets or sets a value indicating whether the install location should be added directly to the PATH environment variable. - /// + public bool? DownloadCommandProhibited { get; set; } + + /// + /// Gets or sets a value indicating whether the install location should be added directly to the PATH environment variable. + /// public bool? ArchiveBinariesDependOnPath { get; set; } /// /// Gets or sets the repair behavior. /// - public string RepairBehavior { get; set; } - + public string RepairBehavior { get; set; } + /// /// Gets or sets the installer authentication info. - /// + /// public InstallerAuthentication Authentication { get; set; } + /// + /// Gets or sets the desired state configuration info. + /// + public InstallerDesiredStateConfiguration DesiredStateConfiguration { get; set; } + /// /// Returns a List of strings containing the URIs contained within this installer. ///