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/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 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/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/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/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/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/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index e2d439a275..babb057f0f 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -284,6 +284,7 @@ + @@ -1073,6 +1074,18 @@ true + + true + + + true + + + true + + + true + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 643382697d..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 @@ -1140,5 +1143,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/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/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/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/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/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/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/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) 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/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/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_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/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/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/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; } 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. 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.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/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/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. /// 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 + + + + $TotalTotal + $PassedPass + $WarningsWarning + $FailedFail + $ErrorsError + + + + + + All statuses + Pass + Warning + Fail + Error + + + + + + + + Path + Status + Output + + + +$rowsHtml + + + + + + + +"@ + + $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" +}