diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 14a3c7a7c4..01386be152 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -13,6 +13,7 @@ apps appx appxbundle appxmanifest +appxsdk APSTUDIO argc args @@ -55,6 +56,8 @@ cotaskmem cout cpp cppreference +cpprest +cpprestsdk cppwinrt CPRWL createnew @@ -68,10 +71,14 @@ dbconn DBId declspec decltype +defaultlocale delstore Demitrius denelon depersist +Deserialize +deserializer +deserializing dest devblogs differentpath @@ -93,6 +100,7 @@ ENU enum errorlevel errstr +esrp etag ETW EVENTTAG @@ -127,29 +135,36 @@ getline github githubusercontent hfile +HGLOBAL hinternet HKEY hmac +HMODULE +homepage Homepage hostname hpp HRESULT hresult +HRSRC hstring html http https +Hyperlink IApplication IAppx IAsync IBuffer icu IDisposable +IDX ifdef ifndef ifstream IInput IInspectable +IIS ILogger impl Inet @@ -166,6 +181,7 @@ iostream IOutput IProgress IRandom +IRest ISAPPROVEDFOROUTPUT ISource isspace @@ -180,6 +196,7 @@ Linux LOCALAPPDATA localtime LOGPATH +logsql logto LONGLONG LPCGUID @@ -187,12 +204,16 @@ LPVOID mailto MAJORVERSION makeappx +MAKEINTRESOURCE makemsix +MANIFESTSCHEMA MANIFESTVERSION +MBs mday metadata microsoft mimetype +Minimatch MINORVERSION monostate motw @@ -208,6 +229,7 @@ msixsdk msixsdkx msixtest msrc +Multifile Multimatch mutex namespace @@ -232,6 +254,8 @@ NTSTATUS nuget nullptr nullsoft +nupkg +nuspec nunit OAuth ofstream @@ -242,11 +266,16 @@ ostringstream OSVERSIONINFOEXW outfile OUTOFMEMORY +OWC +PACKAGESSCHEMA +Params +params parentidx pathpart Pathto PBYTE pch +PCWSTR pdb PEVENT pfp @@ -258,6 +287,7 @@ png posix powershell PPV +pplx pragma PRECONFIG preindexed @@ -271,6 +301,7 @@ ptr publiccontainer PUCHAR PVOID +pwa QCol RAII rdbuf @@ -287,6 +318,7 @@ repolibtest rescap resheader resmimetype +RESTSOURCE resw resx roadmap @@ -295,6 +327,7 @@ rowcount rowid rubengustorage runsettings +runtimes safecast savepoint SCROLLER @@ -353,6 +386,7 @@ strcoll streamoff streampos stricmp +strikethrough STRINGID STRINGIFY STRINGIZE @@ -392,6 +426,7 @@ triaged trunc TRUSTEDPEOPLE tt +ttl typedef typename uap @@ -414,6 +449,7 @@ und undef unicode uninstall +uninstalling Unregister updatemanifest UPLEVEL @@ -428,6 +464,8 @@ usersources utext utf uuidof +validator +valijson vcxitems vcxproj vdproj @@ -442,6 +480,7 @@ VOS vso wapproj wchar +wcout wcsicmp webpage wekyb @@ -457,11 +496,14 @@ winsqlite wix wmain woah +wofstream workflow +wostream wpfn wrl WStr wstring +wstringstream www xamarin xlang diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index e3172d48f4..1846569567 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -1,4 +1,5 @@ (?:^|/)\.gitignore$ +(?:^|/)(?i)LICENSE /shared\.manifest$ \.cer$ \.msix$ @@ -13,6 +14,11 @@ ^NOTICE$ ^src/AppInstallerCLICore/Commands/ExperimentalCommand\.cpp$ ^src/catch2/ -^src/YamlCppLib/ ^src/JsonCppLib/ +^src/Valijson/ +^src/YamlCppLib/ +^src/cpprestsdk/ ^\.github/ +^src/AppInstallerCLITests/TestData/InputNames.txt$ +^src/AppInstallerCLITests/TestData/InputPublishers.txt$ +^src/AppInstallerCLITests/TestData/NormalizationInitialIds.txt$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 722897a8f3..60f9fd1b83 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,22 +1,17 @@ abcd -AComment -addressof AFX agg aicli AICLIC ajor -ALIGNAS -allescaped -alse amrutha anonymized APARTMENTTHREADED apfn apiset appinstallertest +Archs argumentlist -ARMEL ARMNT arp arphelper @@ -25,169 +20,126 @@ Atest AType AUrl Aysnc -backend -bbwe Beigi bfd bght -bitfield bitmask bkup blargle -blep blogs -BORLAND +bomgar +BOMs brk Buf +casemap casemappings -cassert cch -cctype -cdunn +CDEF +cend centralus -cerr certmgr +certs Cfg cgmanifest chcp ci cinq CLIE -clocale cloudapp CLSID -CMake -cmath -cname -codepoint COINIT commandline Concat contosa contosainstaller count'th -cplusplus -CPPLIB createmanifestmetadata -cstddef cstdint -cstdio -cstdlib -cstr -cstring -CStyle ctc -ctor Ctx curated -czstring +CYRL deigh deleteifnotneeded -deque -deref -dllimport +dirs dnld -dnp dvinns dw -elif -emark -endcode endian -endverbatim enr -EOL +enums ERANGE errno etest -eturn execustom EXEHASH experimentalfeatures -eyc fd fintimes +Fixfor flargle foldc foldcase FOLDERID -fpclassify +ftp FULLWIDTH fuzzer -gcc gcpi +GES +GESMBH +GHS gity -GNUC +Globals google hhx HINSTANCE hkey hlocal -hpux hre -HREF HRESULTs -hrow IAttachment IConfiguration -ieeefp IHost IID IInstalled incosistencies -inl +IName +INET inor -iosfwd IPackage IPersist IService ISettings -isfinite -Isfinitef -isnan -isprint ISQ -IString itr IVector IWeb IZone -javascript JObject jp jsoncpp -jsonvalue kayone -keey Keivan -keylength KF KNOWNFOLDERID ktf -lconv +Langs +LATN ldcase -len -Lepilleur lhs libyaml Linq liv llvm -localeconv localhost -lossfree LPBYTE LPWSTR LSTATUS -malloc -maxsize +LTDA +MBH megamorf -memcmp memcpy -memset -MINGW MMmmbbbb -modf monicka msdownload MSFT @@ -195,10 +147,8 @@ msftrubengu MSIHASH MSIXHASH msstore -msvc -Multiline multimap -mutators +mx mycustom myinstalldir mylog @@ -206,42 +156,43 @@ mysilent mysilentwithprogress mytool Newtonsoft -nfinity -noreturn +NOEXPAND +normer nuffing nullopt -NULLREF -nvcc -NVIDIAs objbase ofile oss -OString -ote Packagedx +pathparts pathpaths pfn pfxpath Pherson pkindex PMS +positionals productcode -ptrdiff pvk pvm pwabuilder PWAs PWSTR -py pz +qb qword readonly +regexes REGSAM REINSTALLMODE rhs +roblox rosoft +rowids +RRF rrr rzkzqaqjwj +SARL schematab sddl seof @@ -251,29 +202,20 @@ Shlobj sid SIGNATUREHASH Sku -Solaris sortof -sourceforge -sout -SOVERSION spamming SPAPI Srinivasan +SRL srs -ssin -stackoverflow Standalone startswith -stdarg -strchr -strcmp -strdup -strlen -strncmp strtoull subdir subkey +superstring suppy +swervy sysrefcomp Tagit temppath @@ -284,45 +226,50 @@ tombstoned transitioning UCase ucasemap +UChars +uec uild uintptr -unindent Uninitialize +uninstallation uninstaller uninstallprevious uninstalls unparsable UNSCOPED -Unserialize +UParse UPSERT +URIs URLZONE userfilesetting usersettingstest -usf USHORT Utils UWP +validator +valijson valueiterator vamus +VERSI +VERSIE vns -vscprintf -vsnprintf -walkaround +vy wcslen +webpages Webserver website +WERSJA wesome -wiki -wikipedia +windir windowsdeveloper winerror wingetdev winreg withstarts -Workaround -wstringstream +wn +wsv +Workflows wto Wunused WZDNCRFJ -xf -xl +zy diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index fd8c8599e2..74ab869cb3 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -13,5 +13,23 @@ data:[a-zA-Z=;,/0-9+-]+ "[0-9a-f]{64}" # sha-1 \b[0-9a-f]{40}\b -\b([A-Za-z])\1{3,}\b +# ignore long runs of a single character: +\b([A-Za-z])\g{-1}{3,}\b El proyecto .* diferentes +# Package family names +\b[-.A-Za-z0-9]+_[a-z0-9]{13}\b +# Locales for name normalization +\b\p{Lu}{2,3}(?:-(?:CANS|CYRL|LATN|MONG))?-\p{Lu}{2}(?![A-Z])(?:-VALENCIA)?\b +# Azure pipeline tasks +- task: .* + +# Slash-prefixed patterns +\\native(?![a-z]) +/NPH(?![a-z]) +/td(?![a-z]) + +# URLs -- Added here instead of allow.txt to facilitate wildcarding them as more are added +http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer + +# schema regex +"pattern": .*$ diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index e3f36985c0..5da5daef54 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -1,13 +1,11 @@ name: Spell checking on: + pull_request_target: push: branches: - "**" tags-ignore: - "**" - schedule: - # * is a special character in YAML so you have to quote this string - - cron: '15 * * * *' jobs: build: @@ -16,9 +14,6 @@ jobs: steps: - uses: actions/checkout@v2.0.0 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 5 - - uses: check-spelling/check-spelling@0.0.16-alpha - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - bucket: .github/actions - project: spelling + - uses: check-spelling/check-spelling@0.0.17-alpha diff --git a/Localization/Resources/af-ZA/winget.resw b/Localization/Resources/af-ZA/winget.resw index 8008fba544..9796103044 100644 --- a/Localization/Resources/af-ZA/winget.resw +++ b/Localization/Resources/af-ZA/winget.resw @@ -389,7 +389,7 @@ Dit kan deur die instellinglêer ‘winget settings’ gekonfigureer word.Geen installeerders is van toepassing op die huidige stelsel nie. - Daar is tans geen eksperimentele kenmerke beskikbaar nie. + Daar is huidiglik geen eksperimentele kenmerke beskikbaar nie. Geen geïnstalleerde pakket gevind wat by toevoerkriteria pas nie. @@ -476,6 +476,9 @@ Dit kan deur die instellinglêer ‘winget settings’ gekonfigureer word. Kanaal + + Wys inligting oor ’n spesifieke pakket. By verstek moet die navraag nie-kassensitief by die ID, naam of bynaam van die pakket pas. Ander velde kan gebruik word deur hulle geskikte opsie te slaag. + Wys inligting oor ’n pakket @@ -650,7 +653,7 @@ Dit kan deur die instellinglêer ‘winget settings’ gekonfigureer word.Onerkende bevel - Werk alle geïnstalleerde pakkette by na nuutste as beskikbaar + Dateer alle geïnstalleerde pakkette op na die nuutste indien beskikbaar Geen toepaslike bywerking gevind nie. @@ -719,4 +722,96 @@ Dit kan deur die instellinglêer ‘winget settings’ gekonfigureer word.Logs Diagnostic files containing information about application use. + + Die installeerder word deur die beleid geblokkeer + + + Die installeerder se sekuriteitskontrole het misluk + + + 'n Anti-virus produk rapporteer 'n infeksie in die installeerder + + + Poging om die bron op te dateer het misluk: + + + Verwyder die geselekteerde pakket, of gevind deur die geïnstalleerde pakket lys te soek, of direk vanaf 'n manifes. By verstek, moet die navraag onbeduidend pas by die id, naam of moniker van die pakket. Ander velde kan gebruik word deur hulle toepaslike opsie te slaag. + + + Verwyder die gegewe pakket + + + Begin pakket verwydering... + + + Suksesvol verwyder + + + winget kan nie die verwyderingsopdrag vir hierdie pakket vind nie. Kontak die pakketuitgewer vir ondersteuning. + {Locked="winget"} + + + Verwydering is laat vaar + + + Verwydering het misluk met verlaat kode: + + + Voer 'n lys van geïnstalleerde pakkette uit + + + Installeer al die pakkette wat in 'n lêer gelys is. + + + Installeer al die pakkette in 'n lêer. + + + Lêer waar die resultaat geskryf moet word + + + Lêer wat die pakkette beskryf om te installeer + + + Voer pakkette van die gespesifiseerde bron uit + + + Skryf 'n lys van die geïnstalleerde pakkette na 'n lêer. Die pakkette kan dan geïnstalleer word met die import bevel. + {Locked="import"} + + + Een of meer ingevoerde pakkette kon nie geïnstalleer word nie + + + Pakket nie gevind vir invoer nie: + + + Bron word vereis vir invoer is nie geïnstalleer nie: + + + Geïnstalleerde pakket is nie beskikbaar nie van enige bron af nie: + + + Geïnstalleerde weergawe van die pakket is nie beskikbaar nie van enige bron af nie: + + + Geen pakkette in invoer lêer gevind nie + + + JSON-lêer is ongeldig + + + Pakket is reeds geïnstalleer: + + + Ignoreer onbeskikbare pakkette + + + Sluit pakketweergawes in geproduseerde lêer in + + + Ignoreer pakket weergawes van invoer lêer + + + Pad bestaan nie: + \ No newline at end of file diff --git a/Localization/Resources/am-ET/winget.resw b/Localization/Resources/am-ET/winget.resw index b387ff3b67..b1d5b3c2c2 100644 --- a/Localization/Resources/am-ET/winget.resw +++ b/Localization/Resources/am-ET/winget.resw @@ -476,6 +476,9 @@ ጣቢያ + + በአንድ የተወሰነ ጥቅል ላይ መረጃ ያሳዩ። በነባሪነት ፣መጠይቁ በጥልቀት ከጥቅሉ መታወቂያ ፣ ስም ወይም ሞኒከር ጋር መዛመድ አለበት። ሌሎች መስኮች ተገቢውን አማራጭ በማለፍ ሊያገለግሉ ይችላሉ። + ስለ አንድ ጥቅል መረጃ ያሳያል @@ -650,7 +653,7 @@ ያልታወቀ ትእዛዝ - የሚገኙ ከሆነ ሁሉንም የተጫኑ ጥቅሎችን አዘምን + ሁሉንም የተጫኑ ጥቅሎችን ካሉ በቅርብ ጊዜ ያዘምኑ አግባብ ዝመና አልተገኘም። @@ -719,4 +722,96 @@ ምዝግቦች Diagnostic files containing information about application use. + + ጫኝው በፖሊሲ ታግዷል + + + ጫኝው የደህንነት ፍተሻውን አልተሳካም + + + የፀረ-ቫይረስ ምርት በመጫኛው ውስጥ ስለ ኢንፌክሽን ሪፖርት ያደርጋል + + + ምንጩን ለማዘመን በመሞከር አልተሳካም: + + + የተጫኑ የጥቅሎች ዝርዝር በመፈለግ ወይም በቀጥታ ከአንድ ዝርዝር መግለጫ ውስጥ የተገኘውን የተመረጠውን ጥቅል ማራገፍ። በነባሪነት መጠይቁ case-insensitive በሆነ መልኩ ከጥቅሉ መታወቂያ፣ ስም፣ ወይም ቅጽል ስም ጋር መዛመድ አለበት።፡ሌሎች መስኮች ተገቢውን አማራጭ በማለፍ ሊያገለግሉ ይችላሉ ። + + + የተሰጠውን ጥቅል አለመጫን + + + ጥቅል ማራገፍን በመጀመር ላይ... + + + በተሳካ ሁኔታ ተራግፏል + + + winget ለዚህ ጥቅል የማራገፊያ ትእዛዝን ማግኘት አይችልም ። እባክዎን ከጥቅሉ አሳታሚ ጋር ድጋፍ ያድርጉ ። + {Locked="winget"} + + + የማራገፍ ስራ ቀርቷል + + + ማራገፍ በመውጫ ኮድ አልተሳካም: + + + የተጫኑትን ፓኬጆች ዝርዝር ይላኩ + + + በአንድ ፋይል ውስጥ የተዘረዘሩትን ሁሉንም ጥቅሎች ይጫናል። + + + ሁሉንም ጥቅሎች በፋይል ውስጥ ይጫናል + + + ውጤቱ በሚጻፍበት ቦታ ፋይል ያድርጉ + + + ፋይል ለመጫን ጥቅሎች እየገለጸ ነው + + + ከተጠቀሰው ምንጭ ጥቅሎችን ይላኩ + + + የተጫኑትን ጥቅሎች ዝርዝር ወደ ፋይል ይጽፋሉ። ጥቅሎቹ ከዚያ import ትእዛዝ ሊጫኑ ይችላሉ። + {Locked="import"} + + + አንድ ወይም ከዚያ በላይ ከውጭ የመጡ ፓኬጆች መጫን አልተሳካም + + + ጥቅል ከውጭ ለማስገባት አልተገኘም፦ + + + ከውጭ ለማስገባት የሚያስፈልገው ምንጭ አልተጫነም፦ + + + የተጫነ ጥቅል ከየትም ምንጭ አይገኝም፦ + + + የተጫነ የጥቅል ስሪት ከማንኛውም ምንጭ አይገኝም፦ + + + ከውጭ በሚመጣ ፋይል ውስጥ ምንም ፓኬጆች አልተገኙም + + + JSON ፋይል ልክ ያልሆነ ነው + + + ጥቅል ቀድሞ ተጭኗል፦ + + + ያልተገኙ ጥቅሎችን ችላ በሏቸው + + + በተመረተ ፋይል ውስጥ የጥቅል ስሪቶችን ያካትቱ + + + ከአስመጪ ፋይል የጥቅል ስሪቶችን ችላ በል + + + ዱካ የለም፦ + \ No newline at end of file diff --git a/Localization/Resources/ar-SA/winget.resw b/Localization/Resources/ar-SA/winget.resw index 00fce28fad..5b20342308 100644 --- a/Localization/Resources/ar-SA/winget.resw +++ b/Localization/Resources/ar-SA/winget.resw @@ -389,7 +389,7 @@ لا توجد أي مثبتات قابلة للتطبيق على النظام الحالي. - لا تتوفر حالياً أي ميزات تجريبية. + لا توجد حاليا أي ميزات تجريبية متاحة. لم يتم العثور على حزمة مثبتة متطابقة مع معايير الإدخال. @@ -476,6 +476,9 @@ القناة + + إظهار معلومات حول حزمه معينه. بشكل افتراضي ، يجب ان يطابق الاستعلام إينسينسيتيفيلي المعرف أو الاسم أو لقب الحزمة. يمكن استخدام الحقول الأخرى بواسطة تمرير الخيار المناسب. + عرض معلومات حول إحدى الحزم @@ -650,7 +653,7 @@ الأمر غير معروف - تحديث جميع الحزم المثبتة إلى الأحدث في حالة توفره + قم بتحديث كافة الحزم المثبتة إلى الأحدث إذا كانت متوفرة لم يتم العثور علي تحديث قابل للتطبيق. @@ -719,4 +722,96 @@ السجلات Diagnostic files containing information about application use. + + تم حظر المثبت بواسطة السياسة + + + فشل المثبت في اختبار الأمان + + + أبلغ منتج مضاد للفيروسات عن إصابة في المثبت + + + فشلت محاولة تحديث المصدر: + + + إلغاء تثبيت الحزمة المحددة ، اما ان يتم العثور عليها من خلال البحث في قائمه الحزم المثبتة أو مباشره من بيان. بشكل افتراضي ، يجب ان يطابق الاستعلام إينسينسيتيفيلي المعرف أو الاسم أو لقب الحزمة. يمكن استخدام الحقول الأخرى بواسطة تمرير الخيار المناسب. + + + يلغي تثبيت الحزمة المحددة + + + جارٍ بدء إلغاء تثبيت الحزمة... + + + تم الإزالة بنجاح + + + يتعذر على winget تحديد موقع أمر إلغاء التثبيت لهذه الحزمة. يرجى التواصل مع ناشر الحزمة للحصول على الدعم. + {Locked="winget"} + + + تم التخلي عن إلغاء التثبيت + + + فشل إلغاء التثبيت مع رمز الخروج: + + + تصدير قائمة بالحزم المثبتة + + + تثبيت كافة الحزم المدرجة في ملف. + + + تثبيت كافة الحزم الموجودة في ملف + + + الملف الذي تتم كتابه النتيجة فيه + + + ملف يصف الحزم المطلوب تثبيتها + + + تصدير الحزم من المصدر المحدد + + + كتابة قائمة الحزم المثبتة إلى ملف. يمكن بعد ذلك تثبيت الحزم باستخدام الأمر import. + {Locked="import"} + + + فشل تثبيت واحدة أو أكثر من الحزم المستوردة + + + لم يتم العثور على حزمة لاستيرادها: + + + المصدر المطلوب للاستيراد غير مثبت: + + + لا تتوفر حزمة مثبتة من أي مصدر: + + + لا يتوفر إصدار حزمة مثبت من أي مصدر: + + + لم يتم العثور على أي حزم في ملف الاستيراد + + + ملف JSON غير صالح + + + ‏‏اللغة مثبتة بالفعل: + + + تجاهل الحزم غير المتوفرة + + + تضمين إصدارات الحزمة في الملف المنتج + + + تجاهل إصدارات الحزمة من ملف الاستيراد + + + ‏‏المسار غير موجود: + \ No newline at end of file diff --git a/Localization/Resources/az-Latn-AZ/winget.resw b/Localization/Resources/az-Latn-AZ/winget.resw index c2a20c3bbd..b321cded74 100644 --- a/Localization/Resources/az-Latn-AZ/winget.resw +++ b/Localization/Resources/az-Latn-AZ/winget.resw @@ -389,7 +389,7 @@ Onlar parametrlər faylı olan 'winget settings' vasitəsilə konfiqurasiya edil Cari sistem üçün heç bir quraşdırıcı tətbiq edilmir. - Hal-hazırda heç bir eksperimental xüsusiyyətlər mövcud deyil. + Hal-hazırda heç bir eksperimental xüsusiyyətlər mövcud deyil. Daxiletmə meyarına uyğun gələn quraşdırılmış paket tapılmadı. @@ -476,6 +476,9 @@ Onlar parametrlər faylı olan 'winget settings' vasitəsilə konfiqurasiya edil Kanal + + Xüsusi paketlə bağlı məlumatı göstərir. Standart şəkildə sorğu böyük-kiçik hərfə həssas olmadan paketin identifikatoru, adı və ya ləqəbi ilə uyğun gəlməlidir. Başqa sahələrdən onların müvafiq seçimini ötürməklə istifadə oluna bilər. + Paket haqqında məlumatı göstərir @@ -719,4 +722,96 @@ Onlar parametrlər faylı olan 'winget settings' vasitəsilə konfiqurasiya edil Jurnallar Diagnostic files containing information about application use. + + Quraşdırıcı siyasətə əsasən bloklanıb + + + Quraşdırıcı təhlükəsizlik yoxlamasından keçə bilmədi + + + Antivirus məhsulu quraşdırıcıda virus yoluxması olduğunu bildirir + + + Mənbəni yeniləmək cəhdi uğursuz oldu: + + + Ya quraşdırılmış paketlər siyahısında axtarmaqla, ya da birbaşa manifestdən tapılan seçilmiş paketin quraşdırılmasını ləğv edir. Standart olaraq sorğu böyük-kiçik hərfə həssas olmadan paketin identifikatoru, adı və ya ayaması ilə uyğun gəlməlidir. Başqa sahələrdən onların müvafiq seçimini ötürməklə istifadə oluna bilər. + + + Verilmiş paketin quraşdırılmışını ləğv edir + + + Paketin quraşdırılmışının ləğvi başladılır... + + + Quraşdırılmış uğurla ləğv olundu + + + winget bu paket üçün quraşdırılmışın ləğvi əmrini tapa bilmir. Dəstək üçün paketin naşiri ilə əlaqə saxlayın. + {Locked="winget"} + + + Quraşdırılmışın ləğvi dayandırıldı + + + Quraşdırılmışın ləğvi verilən çıxış kodu ilə alınmadı: + + + Quraşdırılmış paketlərin siyahısını ixrac et + + + Faylda sadalanan bütün paketləri quraşdırır. + + + Fayldakı bütün paketləri quraşdırır + + + Nəticənin yazılacağı fayl + + + Paketlərini quraşdırmaq üçün təsvir edilən fayl + + + Göstərilən mənbədən paketləri ixrac et + + + Qurulmuş paketlərin siyahısını bir fayla yazır. Paketlər daha sonra “import” əmri ilə quraşdırıla bilər. + {Locked="import"} + + + Bir və ya daha çox idxal edilmiş paketi quraşdırmaq uğursuz oldu + + + İdxal üçün paket tapılmadı: + + + Idxal üçün lazım olan mənbə quraşdırılmayıb: + + + Qurulmuş paket heç bir mənbədən əlçatan deyil: + + + Paketin quraşdırılmış versiyası heç bir mənbədən əlçatan deyil: + + + İdxal sənədində paket tapılmadı + + + JSON faylı düzgün deyil + + + Paket artıq quraşdırılıb: + + + Mövcud paketləri rədd et + + + Paket versiyalarını istehsal edilmiş fayla daxil edin + + + Paketin quraşdırılmış versiyası heç bir mənbədən əlcatan deyil: + + + Yol mövcud deyil: + \ No newline at end of file diff --git a/Localization/Resources/bg-BG/winget.resw b/Localization/Resources/bg-BG/winget.resw index 5d161537f3..0c4398b13f 100644 --- a/Localization/Resources/bg-BG/winget.resw +++ b/Localization/Resources/bg-BG/winget.resw @@ -476,6 +476,9 @@ Канал + + Показва информация за конкретен пакет. По подразбиране заявката трябва да съвпада нечувствително с ИД, името или псевдонима на пакета. Други полета могат да се използват, като се премине съответната опция. + Показва информация за пакет @@ -650,7 +653,7 @@ Неразпозната команда - Актуализиране на всички инсталирани пакети до най-новите, ако са налични + Актуализирай всички инсталирани пакети до най-новите, ако има такива Не са намерени приложими актуализации. @@ -719,4 +722,96 @@ Регистрационни файлове Diagnostic files containing information about application use. + + Инсталиращата програма е блокирана от правила + + + Неуспешна проверка на защитата за инсталиращата програма + + + Антивирусен продукт съобщава за инфекция в инсталиращата програма + + + Неуспешен опит за актуализиране на източника: + + + Деинсталира избрания пакет или е открит чрез търсене в списъка с инсталирани пакети, или директно от манифест. По подразбиране заявката трябва да съвпада нечувствително с ИД, името или псевдонима на пакета. Други полета могат да се използват, като се премине съответната опция. + + + Деинсталира дадения пакет + + + Стартиране на деинсталирането на пакета... + + + Успешно деинсталиран + + + winget не може да намери командата за деинсталиране за този пакет. Моля, свържете се с издателя на пакета за поддръжка. + {Locked="winget"} + + + Деинсталирането е прекратено + + + Неуспешно деинсталиране с код за излизане: + + + Експортиране на списък с инсталираните пакети + + + Инсталира всички пакети, изброени във файл. + + + Инсталира всички пакети във файл + + + Файлът, където трябва да бъде записан резултатът + + + Файл, описващ пакетите за инсталиране + + + Експортиране на пакети от указания източник + + + Записва списък на инсталираните пакети във файл. Пакетите могат да бъдат инсталирани с командата import. + {Locked="import"} + + + Неуспешно инсталиране на един или повече импортирани пакети + + + Пакет, който не е намерен за импортиране: + + + Източникът, необходим за импортирането, не е инсталиран: + + + Инсталираният пакет не е достъпен от никой източник: + + + Инсталираната версия на пакета не е достъпна от никой източник: + + + Не са намерени пакети във файла за импортиране + + + JSON файлът не е валиден + + + Пакетът вече е инсталиран: + + + Игнориране на недостъпните пакети + + + Включване на пакетни версии в произведени файлове + + + Игнориране на версиите на пакета от файла за импортиране + + + Пътят не съществува: + \ No newline at end of file diff --git a/Localization/Resources/ca-ES/winget.resw b/Localization/Resources/ca-ES/winget.resw index 3d2c4a7c73..e84d887422 100644 --- a/Localization/Resources/ca-ES/winget.resw +++ b/Localization/Resources/ca-ES/winget.resw @@ -476,6 +476,9 @@ Es poden configurar mitjançant el fitxer de configuració "winget settings". Canal + + Mostra informació d'un paquet específic. Per defecte, la consulta ha de coincidir amb l'identificador, el nom o el sobrenom del paquet. Es poden utilitzar altres camps passant la seva opció apropiada. + Mostra informació sobre un paquet @@ -719,4 +722,96 @@ Es poden configurar mitjançant el fitxer de configuració "winget settings".Registres Diagnostic files containing information about application use. + + L'instal·lador està bloquejat per la norma + + + L'instal·lador ha tingut un error en la revisió de seguretat + + + Un producte antivirus informa d'una infecció a l'instal·lador + + + S'ha produït un error en intentar actualitzar l'origen: + + + Desinstal·la el paquet seleccionat, ja es troba cercant la llista de paquets instal·lats o directament des d'un manifest. Per defecte, la consulta ha de coincidir amb l'identificador, el nom o el sobrenom del paquet. Es poden utilitzar altres camps passant la seva opció apropiada. + + + Desinstal·la el paquet especificat + + + S'està iniciant la desinstal·lació del paquet... + + + S'ha desinstal·lat correctament + + + El winget no pot localitzar l'ordre desinstal·la per a aquest paquet. Poseu-vos en contacte amb l'editor de paquets per obtenir assistència tècnica. + {Locked="winget"} + + + S'ha abandonat la desinstal·lació + + + No s'ha pogut desinstal·lar amb el codi de sortida: + + + Exporta una llista dels paquets instal·lats + + + Instal·la tots els paquets que es mostren en un fitxer. + + + Instal·la tots els paquets d'un fitxer + + + Fitxer on s'escriurà el resultat + + + Fitxer que descriu els paquets que s'han d'instal·lar + + + Exporta els paquets de l'origen especificat + + + Escriu una llista dels paquets instal·lats a un fitxer. Els paquets es poden instal·lar amb l'ordre import. + {Locked="import"} + + + No s'han pogut instal·lar un o més paquets importats + + + No s'ha trobat el paquet per a la importació: + + + L'origen necessari per a la importació no està instal·lat: + + + El paquet instal·lat no està disponible de cap origen: + + + La versió instal·lada del paquet no està disponible de cap origen: + + + No s'ha trobat cap paquet al fitxer d'importació + + + El fitxer JSON no és vàlid + + + El paquet ja està instal·lat: + + + Ignora els paquets no disponibles + + + Inclou les versions del paquet en un fitxer produït + + + Ignora les versions del paquet del fitxer d'importació + + + El camí no existeix: + \ No newline at end of file diff --git a/Localization/Resources/cs-CZ/winget.resw b/Localization/Resources/cs-CZ/winget.resw index 618f9451dc..9de42ccefb 100644 --- a/Localization/Resources/cs-CZ/winget.resw +++ b/Localization/Resources/cs-CZ/winget.resw @@ -476,6 +476,9 @@ Lze je nakonfigurovat pomocí souboru nastavení „winget settings“. Kanál + + Zobrazí informace o konkrétním balíčku. Ve výchozím nastavení se musí dotaz bez rozlišování malých a velkých písmen shodovat s ID, názvem nebo monikerem balíčku. Další pole lze použít předáním příslušné možnosti. + Zobrazuje podrobnosti o balíčku @@ -650,7 +653,7 @@ Lze je nakonfigurovat pomocí souboru nastavení „winget settings“. Nerozpoznaný příkaz - Aktualizovat všechny nainstalované balíčky na nejnovější, pokud k dispozici + Aktualizovat všechny nainstalované balíčky na nejnovější, pokud jsou k dispozici Žádná vhodná aktualizace nebyla nalezena. @@ -719,4 +722,96 @@ Lze je nakonfigurovat pomocí souboru nastavení „winget settings“. Protokoly Diagnostic files containing information about application use. + + Instalační program je blokován zásadami + + + Instalační program nedokázal provést kontrolu zabezpečení. + + + Antivirový produkt hlásí napadený instalační program + + + Pokus o aktualizaci zdroje selhal: + + + Odinstaluje vybraný balíček, který najdete buď vyhledáním v seznamu nainstalovaných balíčků, nebo přímo z manifestu aplikace. Ve výchozím nastavení se dotaz musí shodovat s ID balíčku, jeho názvem a cestou odkazu (nerozlišují se malá a velká písmena). Zadáním vhodných možností se dají použít i další pole. + + + Odinstaluje daný balíček + + + Spouští se odinstalace balíčku... + + + Odinstalace byla úspěšně dokončena + + + winget nemůže najít příkaz k odinstalaci pro tento balíček. Pokud potřebujete pomoc, obraťte se na vydavatele balíčku. + {Locked="winget"} + + + Odinstalace se neprovedla + + + Odinstalace selhala s ukončovacím kódem: + + + Exportuje seznam nainstalovaných balíčků + + + Nainstaluje všechny balíčky uvedené v souboru. + + + Nainstaluje všechny balíčky do souboru + + + Soubor, do kterého se má výsledek zapsat + + + Soubor popisující balíčky k instalaci + + + Exportuje balíčky ze zadaného zdroje + + + Zapíše seznam nainstalovaných balíčků do souboru. Balíčky pak lze nainstalovat příkazem import. + {Locked="import"} + + + Nepodařilo se nainstalovat minimálně jeden importovaný balíček. + + + Balíček pro import nebyl nalezen: + + + Není nainstalován zdroj vyžadovaný pro import: + + + Nainstalovaný balíček není k dispozici z žádného zdroje: + + + Nainstalovaná verze balíčku není k dispozici z žádného zdroje: + + + V souboru importu nebyly nalezeny žádné balíčky + + + Soubor JSON je neplatný + + + Balíček je již nainstalován: + + + Ignorovat nedostupné balíčky + + + Zahrnout verze balíčků do vytvořeného souboru + + + Ignorovat verze balíčků z importovaného souboru + + + Cesta neexistuje: + \ No newline at end of file diff --git a/Localization/Resources/da-DK/winget.resw b/Localization/Resources/da-DK/winget.resw index 53f7f2cdfb..6fb98e51f6 100644 --- a/Localization/Resources/da-DK/winget.resw +++ b/Localization/Resources/da-DK/winget.resw @@ -389,7 +389,7 @@ De kan konfigureres ved hjælp af indstillingsfilen ' winget settings '. Intet installationsprogram er relevant for det aktuelle system. - Der er i øjeblikket ingen tilgængelige eksperimentale-funktioner. + Der er i øjeblikket ingen tilgængelige eksperimentale funktioner. Ingen installeret pakke blev fundet, der matcher inputkriterierne. @@ -476,6 +476,9 @@ De kan konfigureres ved hjælp af indstillingsfilen ' winget settings '. Kanal + + Viser oplysninger om en bestemt pakke. Som standard skal forespørgslen have en forskel på store og små bogstaver, der svarer til id'et, navnet eller moniker for pakken. Andre felter kan bruges ved at videregive deres relevante indstilling. + Viser oplysninger om en pakke @@ -719,4 +722,96 @@ De kan konfigureres ved hjælp af indstillingsfilen ' winget settings '. Logfiler Diagnostic files containing information about application use. + + Installationsprogrammet er blokeret af politik + + + Sikkerhedskontrollen for installationsprogrammet mislykkedes + + + Et antivirusprogram rapporterer en infektion i installationsprogrammet + + + Der opstod fejl under forsøg på at opdatere kilden: + + + Fjerner den valgte pakke, enten ved at søge på listen over installerede pakker eller direkte fra et manifest. Forespørgslen skal som standard skelne mellem store og små bogstaver i pakkens id, navn eller moniker. Andre felter kan bruges ved at videregive de ønskede indstillinger. + + + Fjerner den pågældende pakke + + + Starter fjernelse af pakke... + + + Pakken er fjernet + + + winget kan ikke finde fjernelses kommandoen til denne pakke. Kontakt pakke udgiveren for at få support. + {Locked="winget"} + + + Fjernelse blev afbrudt + + + Fjernelsen mislykkedes med returkoden: + + + Eksporterer en liste med installerede pakker + + + Installerer alle pakker, der er angivet i en fil. + + + Installerer alle pakkerne i en fil + + + Fil, som resultatet skal skrives til + + + Fil, der beskriver de pakker, der skal installeres + + + Eksportér pakker fra den angivne kilde + + + Skriver en liste over de installerede pakker til en fil. Pakkerne kan derefter installeres sammen med kommandoen import. + {Locked="import"} + + + En eller flere importerede pakker blev ikke installeret + + + Pakken blev ikke fundet til import: + + + Kilde, der kræves til import, er ikke installeret: + + + Den installerede version af pakken er ikke tilgængelig fra nogen kilde: + + + Den installerede version af pakken er ikke tilgængelig fra nogen kilde: + + + Der blev ikke fundet nogen pakker i importfilen + + + JSON-filen er ugyldig + + + Pakken er allerede installeret: + + + Ignorer utilgængelige pakker + + + Medtag pakkeversioner i den producerede fil + + + Ignorer pakkeversioner fra importfilen + + + Stien findes ikke: + \ No newline at end of file diff --git a/Localization/Resources/de-DE/winget.resw b/Localization/Resources/de-DE/winget.resw index eb96f1b248..bd9e5692dd 100644 --- a/Localization/Resources/de-DE/winget.resw +++ b/Localization/Resources/de-DE/winget.resw @@ -389,7 +389,7 @@ Sie können über die Einstellungsdatei „winget settings“ konfiguriert werde Für das aktuelle System sind keine Installationsprogramme verfügbar. - Zurzeit sind keine Exprimentalfunktionen verfügbar. + Derzeit sind keine experimentellen Funktionen verfügbar. Es wurde kein installiertes Paket gefunden, das den Eingabekriterien entspricht. @@ -476,6 +476,9 @@ Sie können über die Einstellungsdatei „winget settings“ konfiguriert werde Kanal + + Zeigt Informationen zu einem bestimmten Paket an. Standard mäßig muss die Abfrage Groß-insensitively mit der ID, dem Namen oder dem Moniker des Pakets überein stimmen. Andere Felder können verwendet werden, indem Sie Ihre entsprechende Option übergeben. + Zeigt Informationen zu einem Paket an @@ -650,7 +653,7 @@ Sie können über die Einstellungsdatei „winget settings“ konfiguriert werde Nicht erkannter Befehl - Alle installierten Pakete in die neuesten Updates aktualisieren, wenn diese verfügbar sind + Aktualisieren aller installierten Pakete auf den neuesten Stand, falls verfügbar Es wurden keine anwendbaren Aktualisierungen gefunden. @@ -719,4 +722,96 @@ Sie können über die Einstellungsdatei „winget settings“ konfiguriert werde Protokolle Diagnostic files containing information about application use. + + Der Installer wird von einer Richtlinie blockiert. + + + Fehler bei der Sicherheitsprüfung des Installer + + + Ein Antivirenprodukt meldet ein Infektion im Installer + + + Fehler beim Versuch, die Quelle zu aktualisieren: + + + Deinstalliert das ausgewählte Paket, das entweder durch Suchen der Liste der installierten Pakete oder direkt aus einem Manifest gefunden wurde. Standard mäßig muss die Abfrage Groß-insensitively mit der ID, dem Namen oder dem Moniker des Pakets überein stimmen. Andere Felder können verwendet werden, indem Sie Ihre entsprechende Option übergeben. + + + Deinstalliert das angegebene Paket + + + Paket-Deinstallation wird gestartet... + + + Erfolgreich deinstalliert + + + Das „winget“-Tool kann den Deinstallationsbefehl für dieses Paket nicht finden. Bitte wenden Sie sich an den Herausgeber des Pakets, um Unterstützung zu erhalten. + {Locked="winget"} + + + Deinstallation abgebrochen + + + Deinstallation fehlgeschlagen mit Exitcode: + + + Exportiert eine Liste der installierten Pakete + + + Installiert alle in einer Datei aufgeführten Pakete. + + + Installiert alle Pakete in einer Datei + + + Datei, in der das Ergebnis geschrieben werden soll + + + Datei, die die zu installierenden Pakete beschreibt + + + Pakete aus der angegebenen Quelle exportieren + + + Schreibt eine Liste der installierten Pakete in eine Datei. Die Pakete können dann mit dem Befehl "import" installiert werden. + {Locked="import"} + + + Eine oder mehrere importierte Pakete konnten nicht installiert werden + + + Das Paket wurde für den Import nicht gefunden: + + + Die für den Import erforderliche Quelle ist nicht installiert: + + + Das installierte Paket ist in keiner Quelle verfügbar: + + + Die installierte Version des Pakets ist in keiner Quelle verfügbar: + + + In der Importdatei wurden keine Pakete gefunden + + + Die JSON-Datei ist nicht gültig + + + Das Paket ist bereits installiert: + + + Nicht verfügbare Pakete ignorieren + + + Paketversionen in die erstellte Datei einschließen + + + Paketversionen aus Importdatei ignorieren + + + Der Pfad ist nicht vorhanden: + \ No newline at end of file diff --git a/Localization/Resources/el-GR/winget.resw b/Localization/Resources/el-GR/winget.resw index 3a5501db75..866e485501 100644 --- a/Localization/Resources/el-GR/winget.resw +++ b/Localization/Resources/el-GR/winget.resw @@ -476,6 +476,9 @@ Κανάλι + + Εμφανίζει πληροφορίες για ένα συγκεκριμένο πακέτο. Από προεπιλογή, το ερώτημα πρέπει να insensitively να συμφωνεί με το Αναγνωριστικό, το όνομα ή το αναγνωριστικό του πακέτου. Τα άλλα πεδία μπορούν να χρησιμοποιηθούν μέσω της κατάλληλης επιλογής τους. + Εμφανίζει πληροφορίες σχετικά με ένα πακέτο @@ -650,7 +653,7 @@ Η εντολή δεν αναγνωρίστηκε - Ενημέρωση όλων των εγκατεστημένων πακέτων στην πιο πρόσφατη έκδοση, εάν απαιτείται + Ενημέρωση όλων των εγκατεστημένων πακέτων στην πιο πρόσφατη έκδοση, εάν είναι διαθέσιμη Δεν βρέθηκαν ισχύουσες ενημερώσεις. @@ -719,4 +722,96 @@ Αρχεία καταγραφής Diagnostic files containing information about application use. + + Το πρόγραμμα εγκατάστασης αποκλείστηκε από την πολιτική + + + Το πρόγραμμα εγκατάστασης δεν πραγματοποίησε έλεγχο ασφαλείας + + + Προϊόν προστασίας από ιούς αναφέρει προσβολή από ιό στο πρόγραμμα εγκατάστασης + + + Η απόπειρα ενημέρωσης της προέλευσης απέτυχε: + + + Καταργεί την εγκατάσταση το επιλεγμένο πακέτο, που εντοπίζεται με αναζήτηση στη λίστα εγκατεστημένων πακέτων ή απευθείας από μια διακήρυξη. Από προεπιλογή, το ερώτημα πρέπει να insensitively να συμφωνεί με το Αναγνωριστικό, το όνομα ή το αναγνωριστικό του πακέτου. Τα άλλα πεδία μπορούν να χρησιμοποιηθούν μέσω της κατάλληλης επιλογής τους. + + + Κατάργηση εγκατάστασης του συγκεκριμένου πακέτου + + + Έναρξη κατάργησης εγκατάστασης πακέτου... + + + Η εγκατάσταση καταργήθηκε επιτυχώς + + + winget δεν είναι δυνατό να εντοπίσει την εντολή κατάργησης εγκατάστασης για αυτό το πακέτο. Επικοινωνήστε με τον εκδότη του πακέτου για υποστήριξη. + {Locked="winget"} + + + Η κατάργηση εγκατάστασης ματαιώθηκε + + + Η κατάργηση εγκατάστασης με κωδικό εξόδου απέτυχε: + + + Εξαγάγει λίστα με τα εγκατεστημένα πακέτα + + + Εγκαθιστά όλα τα πακέτα που αναφέρονται σε ένα αρχείο. + + + Εγκαθιστά όλα τα πακέτα σε ένα αρχείο + + + Αρχείο όπου θα καταχωρηθεί το αποτέλεσμα + + + Αρχείο που περιγράφει τα πακέτα προς εγκατάσταση + + + Εξαγωγή πακέτων από την καθορισμένη προέλευση + + + Εγγράφει μια λίστα με τα εγκατεστημένα πακέτα σε ένα αρχείο. Τα πακέτα μπορούν να εγκατασταθούν στη συνέχεια με την εντολή import. + {Locked="import"} + + + Απέτυχε η εγκατάσταση ενός ή περισσότερων πακέτων που έχουν εισαχθεί + + + Το πακέτο δεν βρέθηκε για εισαγωγή: + + + Η προέλευση που απαιτείται για την εισαγωγή δεν έχει εγκατασταθεί: + + + Το εγκατεστημένο πακέτο δεν είναι διαθέσιμο από καμία προέλευση: + + + Η εγκατεστημένη έκδοση του πακέτου δεν είναι διαθέσιμη από καμία προέλευση: + + + Δεν βρέθηκαν πακέτα στο αρχείο εισαγωγής + + + Το αρχείο JSON δεν είναι έγκυρο + + + Το πακέτο είναι ήδη εγκαταστημένο: + + + Παράβλεψη μη διαθέσιμων πακέτων + + + Συμπερίληψη εκδόσεων πακέτου στο παραγόμενο αρχείο + + + Παράβλεψη εκδόσεων πακέτου από αρχείο εισαγωγής + + + Η διαδρομή δεν υπάρχει: + \ No newline at end of file diff --git a/Localization/Resources/en-GB/winget.resw b/Localization/Resources/en-GB/winget.resw index e41a3a8a42..339e314e06 100644 --- a/Localization/Resources/en-GB/winget.resw +++ b/Localization/Resources/en-GB/winget.resw @@ -476,6 +476,9 @@ They can be configured through the settings file 'winget settings'. Channel + + Shows information on a specific package. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. + Shows info about an package @@ -719,4 +722,96 @@ They can be configured through the settings file 'winget settings'. Logs Diagnostic files containing information about application use. + + The installer is blocked by policy + + + The installer failed security check + + + An anti-virus product reports an infection in the installer + + + Failed in attempting to update the source: + + + Uninstalls the selected package, either found by searching the installed packages list or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. + + + Uninstalls the given package + + + Starting package uninstall... + + + Successfully uninstalled + + + winget cannot locate the uninstall command for this package. Please reach out to the package publisher for support. + {Locked="winget"} + + + Uninstallation abandoned + + + Uninstall failed with exit code: + + + Exports a list of the installed packages + + + Installs all the packages listed in a file. + + + Installs all the packages in a file + + + File where the result is to be written + + + File describing the packages to install + + + Export packages from the specified source + + + Writes a list of the installed packages to a file. The packages can then be installed with the import command. + {Locked="import"} + + + One or more imported packages failed to install + + + Package not found for import: + + + Source required for import is not installed: + + + Installed package is not available from any source: + + + Installed version of package is not available from any source: + + + No packages found in import file + + + JSON file is not valid + + + Package is already installed: + + + Ignore unavailable packages + + + Include package versions in produced file + + + Ignore package versions from import file + + + Path does not exist: + \ No newline at end of file diff --git a/Localization/Resources/es-ES/winget.resw b/Localization/Resources/es-ES/winget.resw index cc89270490..34f3a31a59 100644 --- a/Localization/Resources/es-ES/winget.resw +++ b/Localization/Resources/es-ES/winget.resw @@ -476,6 +476,9 @@ Se pueden configurar mediante el archivo de configuración "winget settings". Canal + + Muestra información sobre un paquete específico. De forma predeterminada, la consulta no debe distinguir entre mayúsculas y minúsculas y no distingue entre el identificador, el nombre o el moniker del paquete. Puede usar otros campos si pasa su opción adecuada. + Muestra información sobre un paquete @@ -650,7 +653,7 @@ Se pueden configurar mediante el archivo de configuración "winget settings".Comando no reconocido - Actualizar todos los paquetes instalados a la versión más reciente si está disponible + Actualiza todos los paquetes instalados a la versión más reciente si está disponible No se encontraron actualizaciones aplicables. @@ -719,4 +722,96 @@ Se pueden configurar mediante el archivo de configuración "winget settings".Registros Diagnostic files containing information about application use. + + El instalador está bloqueado debido a una directiva + + + El instalador no pudo realizar la comprobación de seguridad + + + Un producto antivirus informa de una infección en el instalador + + + Error al intentar actualizar el origen: + + + Desinstala el paquete seleccionado, encontrado al buscar en una lista de paquetes instalados o bien, directamente desde un manifiesto. De forma predeterminada, la consulta debe coincidir con el id, el nombre o el moniker distinguiendo entre mayúsculas y minúsculas. Se pueden usar otros campos usando la opción apropiada. + + + Desinstala el paquete proporcionado + + + Iniciando la desinstalación de paquete... + + + Desinstalación realizada con éxito + + + winget no encuentra el comando de desinstalación de este paquete. Póngase en contacto con el anunciante del paquete para obtener ayuda. + {Locked="winget"} + + + Desinstalación abandonada + + + Error de desinstalación con el código de salida: + + + Exporta una lista de los paquetes instalados + + + Instala todos los paquetes enumerados en un archivo. + + + Instala todos los paquetes en un archivo. + + + Archivo en el que se va a escribir el resultado + + + Archivo que describe los paquetes que se instalarán + + + Exportar paquetes del origen especificado + + + Escribe una lista de los paquetes instalados en un archivo. Los paquetes se pueden instalar con el comando import. + {Locked="import"} + + + No se pudieron instalar uno o más paquetes importados + + + Paquete no encontrado para la importación: + + + El origen necesario para la importación no está instalado: + + + El paquete instalado no está disponible desde ningún origen: + + + La versión instalada del paquete no está disponible de ningún origen: + + + No se encontraron paquetes en importar archivo + + + El archivo JSON no es válido. + + + El paquete ya está instalado: + + + Omitir los paquetes no disponibles + + + Incluir versiones del paquete en un archivo producido + + + Omitir las versiones del paquete de la importación del archivo + + + Ruta de acceso no existe: + \ No newline at end of file diff --git a/Localization/Resources/es-MX/winget.resw b/Localization/Resources/es-MX/winget.resw index 153a523ef6..aae7638b02 100644 --- a/Localization/Resources/es-MX/winget.resw +++ b/Localization/Resources/es-MX/winget.resw @@ -389,7 +389,7 @@ Se pueden configurar con el archivo de configuración "winget settings". No hay instaladores aplicables al sistema actual. - Actualmente no hay características experimentales disponibles. + Actualmente no hay características experimentales disponibles. No se encontró ningún paquete instalado que coincida con los criterios de entrada. @@ -476,6 +476,9 @@ Se pueden configurar con el archivo de configuración "winget settings". Canal + + Muestra información sobre un paquete específico. De forma predeterminada, la consulta debe coincidir, sin distinción entre mayúsculas y minúsculas, con el ID, el nombre o el apodo del paquete. Se pueden usar otros campos si se pasa la opción correspondiente. + Muestra información sobre un paquete. @@ -719,4 +722,96 @@ Se pueden configurar con el archivo de configuración "winget settings". Registros Diagnostic files containing information about application use. + + El instalador está bloqueado por directiva + + + El instalador ha producido un error de comprobación de seguridad + + + Un producto antivirus informa de una infección en el instalador + + + Error al intentar actualizar el origen: + + + Desinstala el paquete seleccionado, ya sea que se encuentre buscando en la lista de paquetes instalados o directamente desde un manifiesto. De forma predeterminada, la consulta debe coincidir sin distinción entre mayúsculas y minúsculas con el ID, el nombre o el apodo del paquete. Se pueden usar otros campos pasando su opción apropiada. + + + Desinstala el paquete determinado + + + Iniciando la desinstalación del paquete... + + + Se desinstaló correctamente + + + winget no se puede encontrar el comando de desinstalación de este paquete. Comunícate con el editor del paquete para obtener asistencia. + {Locked="winget"} + + + Desinstalación abandonada + + + Error de desinstalación con el código de salida: + + + Exporta una lista de los paquetes instalados + + + Instala todos los paquetes enumerados en un archivo. + + + Instala todos los paquetes en un archivo + + + Archivo en donde se escribirá el resultado + + + Archivo que describe los paquetes a instalar + + + Exportar paquetes desde el origen especificado + + + Escribe una lista de los paquetes instalados en un archivo. Después, los paquetes se pueden instalar con el comando de importación. + {Locked="import"} + + + Error en la instalación de uno o más paquetes importados + + + No se encontró el paquete para la importación: + + + El origen necesario para la importación no está instalado: + + + El paquete instalado no está disponible desde ningún origen: + + + La versión instalada del paquete no está disponible desde ningún origen: + + + No se encontraron paquetes en el archivo de importación + + + El archivo JSON no es válido + + + El paquete ya está instalado: + + + Ignorar paquetes no disponibles + + + Incluir las versiones del paquete en el archivo generado + + + Ignorar las versiones del paquete desde el archivo de importación + + + La ruta de acceso no existe: + \ No newline at end of file diff --git a/Localization/Resources/et-EE/winget.resw b/Localization/Resources/et-EE/winget.resw index df1ac8725d..6ea54b9e8b 100644 --- a/Localization/Resources/et-EE/winget.resw +++ b/Localization/Resources/et-EE/winget.resw @@ -389,7 +389,7 @@ Sätteid saab konfigureerida sätete faili winget settings kaudu. Praeguse süsteemi jaoks asjakohaseid installereid ei ole. - Hetkel pole saadaval ühtegi katsetatavat funktsiooni. + Praegu pole saadaval ühtegi katsetatavat funktsiooni. Ühtegi sisestuskriteeriumitele vastavat installitud paketti ei leitud. @@ -476,6 +476,9 @@ Sätteid saab konfigureerida sätete faili winget settings kaudu. Kanal + + Kuvab kindlas paketis sisalduva teabe. Vaikimisi peab päring ilma tõstutundlikkuseta ühtima ID, nime või paketi hüüdnimega. Teisi välju saab kasutada nende vastava valiku edastamisel. + Kuvab paketi kohta teavet @@ -719,4 +722,96 @@ Sätteid saab konfigureerida sätete faili winget settings kaudu. Logid Diagnostic files containing information about application use. + + Poliitika on installeri blokeerinud + + + Installeri turbekontroll nurjus + + + Viirusetõrjetoode annab teada, et installeris on viirus + + + Allika värskendamise katse nurjus: + + + Desinstallib valitud paketi, mis leitakse kas installitud pakettide loendist või otse manifestist otsides. Vaikimisi peab päring ilma tõstutundlikkuseta ühtima ID, nime või paketi hüüdnimega. Teisi välju saab kasutada nende vastava valiku edastamisel. + + + Antud pakett desinstallitakse + + + Paketi deinstallimise käivitamine... + + + Desinstallitud + + + winget ei leia selle paketi koha desinstallimise käsku. Pöörduge abi saamiseks paketi väljaandja poole. + {Locked="winget"} + + + Desinstallimine on hüljatud + + + Desinstallimine nurjus, väljumiskood: + + + Installitud pakettide loendi installimine + + + Kõikide failis loetletud pakettide installimine. + + + Kõikide faili pakettide installimine + + + Fail, kuhu tuleb kirjutada tulem + + + Fail, mis kirjeldab installimiseks valitud pakette + + + Pakettide eksportimine määratud allikast + + + Kirjutab faili installitud pakettide loendi. Seejärel saab pakette installida käsuga import. + {Locked="import"} + + + Ühe või mitme imporditud paketi installimine nurjus + + + Ei leitud paketti importimiseks: + + + Importimiseks vajalik allikas on installimata: + + + Installitud pakett pole ühestki allikast saadaval: + + + Installitud paketi versioon pole saadaval ühestki allikast: + + + Impordifailist ei leitud ühtegi paketti + + + JSON-i fail on sobimatu + + + Keel on juba installitud: + + + Ignoreeri mittekättesaadavaid pakette + + + Paketiversioonide kaasamine loodud failile + + + Ignoreeri imporditava faili paketi versioone + + + Rada puudub: + \ No newline at end of file diff --git a/Localization/Resources/eu-ES/winget.resw b/Localization/Resources/eu-ES/winget.resw index 8924f37ace..40648f504f 100644 --- a/Localization/Resources/eu-ES/winget.resw +++ b/Localization/Resources/eu-ES/winget.resw @@ -389,7 +389,7 @@ Uneko sisteman aplika daitekeen instalatzailerik ez dago. - Une honetan ez dago erabilgarri eginbide esperimentalik. + Une honetan ez dago erabilgarri eginbide esperimentalik. Ez da aurkitu sartutako irizpideekin bat datorren paketerik instalatuta. @@ -476,6 +476,9 @@ Kanala + + Pakete espezifiko bati buruzko informazioa erakusten du. Lehenespenez, kontsultak bat etorri behar du paketearen IDarekin, izenarekin edo monikerrarekin (ez dira bereizten maiuskulak eta minuskulak). Beste eremu batzuk erabil daitezke dagokien aukera pasatuta. + Pakete bati buruzko informazioa erakusten du @@ -719,4 +722,96 @@ Erregistroak Diagnostic files containing information about application use. + + Gidalerroek instalatzailea blokeatu dute + + + Instalatzaileak huts egin du segurtasun-kontrolean + + + Birusen kontrako produktu batek instalatzailearen infekzio baten berri ematen du + + + Iturburua eguneratzeko saiakeran huts egin du: + + + Hautatutako paketea desinstalatzen du, dela instalatutako paketeen zerrenda bilatzean aurkitutakoa, dela zuzenean manifestuan aurkitutakoa. Modu lehenetsian, kontsultako maiuskulak eta minuskulak bat etorri behar dira paketearen ID, izen edo monikerreko maiuskulekin eta minuskulekin. Beste eremuak dagokion aukera pasatuta erabil daitezke. + + + Emandako paketea desinstalatzen du + + + Desinstalazio paketea abiarazten... + + + Desinstalatu da + + + wingetek ezin du aurkitu pakete hau desinstalatzeko komandoa. Mesedez, jarri harremanetan paketearen editorearekin laguntza lortzeko. + {Locked="winget"} + + + Desinstalazioa bertan behera utzi da + + + Desinstalazioak huts egin du irteera-kode honekin: + + + Instalatutako paketeen zerrenda esportatzen du + + + Instalatu zerrendatutako pakete guztiak fitxategi batean. + + + Instalatu pakete guztiak fitxategi batean + + + Emaitza idazteko fitxategia + + + Instalatu beharreko paketeak deskribatzen dituen fitxategia + + + Esportatu paketeak zehaztutako iturritik + + + Fitxategi batean instalatutako paketeen zerrenda idazten du. Paketeak “import” komandoarekin instala daitezke. + {Locked="import"} + + + Inportatutako pakete bat edo gehiago ezin izan dira instalatu + + + Ez da aurkitu inportatzeko paketea: + + + Inportaziorako behar den iturria ez dago instalatuta: + + + Instalatutako paketea ez dago erabilgarri inolako iturritik: + + + Instalatutako paketearen bertsioa ez dago erabilgarri inolako iturritik: + + + Ez da paketerik aurkitu inportazio-fitxategian + + + JSON fitxategia ez da baliozkoa + + + Paketea lehendik dago instalatuta: + + + Ez ikusi erabilgarri ez dauden paketeak + + + Sartu paketeen bertsioak sortutako fitxategian + + + Ez ikusi inportazio-artxiboko paketeen bertsioak + + + Bidea ez dago: + \ No newline at end of file diff --git a/Localization/Resources/fa-IR/winget.resw b/Localization/Resources/fa-IR/winget.resw index 47d8db2d04..824cfa69a0 100644 --- a/Localization/Resources/fa-IR/winget.resw +++ b/Localization/Resources/fa-IR/winget.resw @@ -389,7 +389,7 @@ هیچ نصب‌کننده‌ای برای سیستم فعلی قابل اجرا نیست. - در حال حاضر هیچ ویژگی تجربی در دسترس نیست. + در حال حاضر هیچ ویژگی آزمایشی در دسترس نیست. هیچ بسته نصب شده با معیارهای ورودی مطابقت ندارد. @@ -476,6 +476,9 @@ کانال + + اطلاعات مربوط به یک بسته خاص را نشان می‌دهد. به‌طور پیش‌فرض، جستار باید از لحاظ بزرگی یا کوچکی حروف با شناسه، نام یا اسم بسته مطابقت داشته باشد. فیلدهای دیگر را می‌توان با قبول کردن گزینه مناسب آنها استفاده کرد. + اطلاعات مربوط به یک بسته را نشان می‌دهد @@ -650,7 +653,7 @@ فرمان تشخیص‌داده‌نشده - در صورت موجود بودن، همه بسته‌های نصب شده را به جدیدترین نسخه آن به‌روز کنید + در صورت موجود بودن، همه بسته‌های نصب شده را به جدیدترین نسخه خود به‌روز کنید هیچ به روزرسانی کاربردی یافت نشد. @@ -719,4 +722,96 @@ گزارش‌ها Diagnostic files containing information about application use. + + نصب توسط خط‌ مشی مسدود شده است + + + نصب‌کننده نتوانست بررسی امنیتی را انجام دهد + + + یک محصول ضد ویروس، آلودگی در نصب را گزارش می کند + + + تلاش برای به‌روز‌رسانی منبع انجام نشد: + + + بسته انتخاب شده را حذف نصب می کند، یا با جستجو در فهرست بسته‌های نصب شده یا مستقیم از یک بیانیه پیدا می شود. به طور پیش‌فرض، پرس‌و‌جو باید از نظر کوچک و بزرگ با شناسه، نام یا مانیتور بسته مطابقت داشته باشد. با عبور از گزینه مناسب می توان از سایر زمینه‌ها استفاده کرد. + + + بسته داده شده را حذف نصب می کند + + + در حال شروع حذف نصب بسته... + + + با موفقیت حذف نصب شد + + + winget نمی تواند دستور حذف نصب این بسته را پیدا کند. لطفاً برای پشتیبانی با ناشر بسته تماس بگیرید. + {Locked="winget"} + + + حذف نصب متوقف شد + + + حذف نصب با کد خروج انجام نشد: + + + فهرستی از بسته‌های نصب‌شده را صادر می‌کند + + + تمام بسته‌های فهرست‌شده در یک فایل را نصب می‌کند. + + + تمام بسته‌های یک فایل را نصب می‌کند + + + فایلی که قرار است نتیجه در آن نوشته شود + + + فایل توصیح بسته‌هایی که قرار است نصب شوند + + + صدور بسته‌ها از منبع مشخص‌شده + + + فهرستی از بسته‌های نصب‌شده را در یک فایل می‌نویسد. سپس می‌توان بسته‌ها را با فرمان import نصب کرد. + {Locked="import"} + + + نصب یک یا چند بستۀ واردشده ناموفق بود + + + بسته برای وارد کردن پیدا نشد: + + + منبع مورد نیاز برای وارد کردن نصب نشده است: + + + بستۀ نصب‌شده از هیچ منبعی در دسترس نیست: + + + نسخۀ نصب‌شدۀ بسته از هیچ منبعی در دسترس نیست: + + + در فایل واردات هیچ بسته‌ای پیدا نشد + + + فایل JSON معتبر نیست + + + بسته پیش از این نصب شده است: + + + نادیده گرفتن بسته‌های خارج از دسترس + + + گنجاندن نسخه‌های بسته در فایل ایجادشده + + + نادیده گرفتن نسخه‌های بسته از فایل واردات + + + مسیر وجود ندارد: + \ No newline at end of file diff --git a/Localization/Resources/fi-FI/winget.resw b/Localization/Resources/fi-FI/winget.resw index 87d17d2213..c2eca79099 100644 --- a/Localization/Resources/fi-FI/winget.resw +++ b/Localization/Resources/fi-FI/winget.resw @@ -476,6 +476,9 @@ Ne voidaan määrittää asetus tiedoston 'winget settings'. Kanava + + Näyttää tietoja tietystä paketista. Kyselyn on oletusarvoisesti vastattava pakkauksen tunnusta, nimeä tai monikeria. Isojen ja pienten kirjainten välillä ei tehdä erottelua. Muita kenttiä voidaan käyttää ohittamalla sopiva vaihtoehto. + Näyttää tietoja paketista @@ -719,4 +722,96 @@ Ne voidaan määrittää asetus tiedoston 'winget settings'. Lokit Diagnostic files containing information about application use. + + Käytäntö estää asennusohjelman + + + Asennusohjelman turvallisuustarkistus epäonnistui + + + Virustentorjuntaohjelma ilmoittaa tartunnasta asennusohjelmassa + + + Lähteen päivittäminen epäonnistui: + + + Poistaa valitun paketin uudelleen. tästä löytyy etsimällä asennettujen pakettien luettelosta tai suoraan luettelo tiedostosta. Oletus arvon mukaan kyselyn on insensitively vastattava paketin tunnusta, nimeä tai Monikeri. Muita kenttiä voidaan käyttää muuttamalla niiden vaihto ehtoa. + + + Poistaa määritetyn paketin asennuksen + + + Aloitetaan paketin asennuksen poistoa... + + + Asennuksen poisto onnistui + + + winget ei löydä tämän paketin asennuksen poistokomentoa. Ota yhteyttä paketin julkaisijaan, jos tarvitset tukea. + {Locked="winget"} + + + Asennuksen poistaminen hylättiin + + + Asennuksen poistaminen epäonnistui, lopetuskoodi: + + + Vie asennettujen pakettien luettelon + + + Asentaa kaikki tiedostossa olevat paketit. + + + Asentaa kaikki tiedoston paketit + + + Tiedosto, johon tulos kirjoitetaan + + + Asennettavia paketteja kuvaava tiedosto + + + Vie paketit määritetystä lähteestä + + + Kirjoittaa asennettujen pakettien luettelon tiedostoon. Paketit voidaan asentaa import komennolla. + {Locked="import"} + + + Yhden tai useamman tuodun paketin asentaminen epäonnistui + + + Tuotavaa pakettia ei löydy: + + + Tuontia varten tarvittavaa lähdettä ei ole asennettu: + + + Asennettua pakettia ei ole käytettävissä mistään lähteestä: + + + Asennettua paketin versiota ei ole käytettävissä mistään lähteestä: + + + Tuontitiedostosta ei löydy paketteja + + + JSON-tiedosto ei kelpaa + + + Paketti on jo asennettu: + + + Ohita ei käytettävissä olevat paketit + + + Sisällytä pakettiversiot tuotettuun tiedostoon + + + Ohita tuontitiedoston pakettiversiot + + + Polkua ei ole: + \ No newline at end of file diff --git a/Localization/Resources/fil-PH/winget.resw b/Localization/Resources/fil-PH/winget.resw index bce3fe1ebb..f6fb3d28f1 100644 --- a/Localization/Resources/fil-PH/winget.resw +++ b/Localization/Resources/fil-PH/winget.resw @@ -389,7 +389,7 @@ Maaari silang i-configure sa pamamagitan ng settings file na 'winget settings'.< Walang pang-install na angkop sa kasalukuyang sistema. - Kasalukuyang walang mga experimental na feature na magagamit. + Kasalukuyang walang magagamit na mga pang-eksperimentong tampok. Walang nakitang naka-install na package na tumutugma sa pamantayan ng input. @@ -476,6 +476,9 @@ Maaari silang i-configure sa pamamagitan ng settings file na 'winget settings'.< Channel + + Nagpapakita ng impormasyon sa isang parikular na package. Bilang default, ang query ay dapat na case-insensitive na tumutugma sa id, pangalan, o moniker ng package. Maaaring magamit ang iba pang mga field sa pamamagitan ng pagpasa sa kanilang naaangkop na opsyon. + Nagpapakita ng impormasyon tungkol sa package @@ -650,7 +653,7 @@ Maaari silang i-configure sa pamamagitan ng settings file na 'winget settings'.< Hindi makilalang utos - I-update ang lahat ng naka-install na package sa pinakabago kung available + I-update ang lahat ng naka-install na mga pakete sa pinakabago kung available Walang nakitang naaangkop na update. @@ -719,4 +722,96 @@ Maaari silang i-configure sa pamamagitan ng settings file na 'winget settings'.< Mga Log Diagnostic files containing information about application use. + + Na-block ng patakaran ang pang-install + + + Pumalya sa pagsusuri ng seguridad ang pang-install + + + Isang produktong anti-virus ang nag-uulat ng infection sa pang-install + + + Pumalya sa pagtatangkang i-update ang pinagmulan: + + + Ina-uninstall ang napiling pakete, maaaring makita sa pamamagitan ng paghahanap sa listahan ng mga naka-install na pakete o direkta mula sa isang manifest. Bilang default, ang query ay dapat na case-insensitive na tumutugma sa id, pangalan, o moniker ng pakete. Maaaring magamit ang iba pang mga field sa pamamagitan ng pagpasa sa kanilang naaangkop na opsyon. + + + Ina-uninstall ang ibinigay na pakete + + + Sinisimulan ang pag-uninstall ng pakete... + + + Matagumpay na na-uninstall + + + hindi mahanap ng winget ang uninstall na utos para sa pakete na ito. Mangyaring makipag-ugnayan sa publisher ng pakete para sa suporta. + {Locked="winget"} + + + Inabandona ang pag-uninstall + + + Pumalya ang pag-uninstall na may exit code na: + + + Nag-eexport ng listahan ng naka-install na mga pakete + + + Ini-install ang lahat ng pakete na nakalista sa file. + + + Ini-install ang lahat ng pakete sa file. + + + File kung saan maisusulat ang resulta + + + File na naglalarawan ng mga pakete para i-install + + + I-export ang mga pakete mula sa tinukoy na pinagmulan + + + Nagsusulat ng listahan ng mga naka-install na mga pakete sa file. Pagkatapos ay maaaring mag-install ng mga pakete gamit ang import command. + {Locked="import"} + + + Nabigong ma-install ang isa o higit pang na-import na mga pakete + + + Hindi natagpuan ang pakete para sa pag-import: + + + Hindi naka-install ang pinagmulan na kailangan para sa pag-import: + + + Hindi available ang naka-install na pakete mula sa anumang pinagmulan: + + + Hindi available ang naka-install na bersiyon ng pakete sa anumang pinagmulan: + + + Walang natagpuang mga pakete sa file ng import + + + Hindi tama ang file na JSON + + + Naka-install na ang pakete: + + + Balewalain ang mga hindi available na mga pakete + + + Magsama ng mga bersiyon ng pakete sa ginawang file + + + Balewalain ang mga bersyon ng pakete mula sa pag-import ng file + + + Hindi umiiral ang landas: + \ No newline at end of file diff --git a/Localization/Resources/fr-CA/winget.resw b/Localization/Resources/fr-CA/winget.resw index 5cc29b8d60..c67b4d7687 100644 --- a/Localization/Resources/fr-CA/winget.resw +++ b/Localization/Resources/fr-CA/winget.resw @@ -476,6 +476,9 @@ Elles peuvent être configurées par le biais du fichier de paramètres "winget Canal + + Affiche des informations sur un paquet précis. Par défaut, la requête doit correspondre, sans respect de la casse, à l’identification, au nom ou au nom du paquet. D’autres champs peuvent être utilisés en passant leur option appropriée. + Affiche des informations sur un paquet @@ -719,4 +722,96 @@ Elles peuvent être configurées par le biais du fichier de paramètres "winget Journaux Diagnostic files containing information about application use. + + L’installeur est bloqué par une politique + + + Le programme d’installation a échoué lors de la vérification de sécurité. + + + Un produit antivirus signale une infection dans l’installateur + + + Échec de la tentative de mise à jour de la source : + + + Désinstallation du paquet sélectionné, trouvé en recherchant les paquets installés ou directement à partir d'un manifeste. Par défaut, la requête doit correspondre, sans respect de la casse, à l’identification, au nom ou au moniker du paquet. D’autres champs peuvent être utilisés en passant leur option appropriée. + + + Désinstallation du paquet donné + + + Début de la désinstallation du paquet... + + + Désinstallé avec succès + + + winget n’a pas pu localiser la commande de désinstallation pour ce paquet. Veuillez communiquer avec l’éditeur du paquet pour obtenir de l’assistance. + {Locked="winget"} + + + Désinstallation abandonnée + + + Échec de la désinstallation avec le code de sortie : + + + Exporte une liste des paquets installés + + + Installe tous les paquets répertoriés dans un fichier + + + Installe tous les paquets dans un fichier + + + Fichier dans lequel le résultat doit être écrit + + + Fichier décrivant les paquets à installer + + + Exporter les paquets à partir de la source spécifiée + + + Écrit une liste des paquets installés dans un fichier. Les paquets peuvent ensuite être installés avec la commande d’importation. + {Locked="import"} + + + Un ou plusieurs paquets importés n’ont pas pu être installés. + + + Paquet introuvable pour l’importation : + + + La source requise pour l’importation n’est pas installée : + + + Le paquet installé n’est pas disponible à partir d’aucune source : + + + La version installée du paquet n’est pas disponible à partir d’aucune source : + + + Aucun paquet n’a été trouvé dans le fichier d’importation. + + + Le fichier JSON n'est pas valide. + + + Le paquet est déjà installé : + + + Ignorer les paquets non disponibles + + + Inclure les versions de paquets dans le fichier généré + + + Ignorer les versions de paquets du fichier d’importation + + + Le chemin d’accès n’existe pas : + \ No newline at end of file diff --git a/Localization/Resources/fr-FR/winget.resw b/Localization/Resources/fr-FR/winget.resw index 0ceb997349..7dce376cc3 100644 --- a/Localization/Resources/fr-FR/winget.resw +++ b/Localization/Resources/fr-FR/winget.resw @@ -476,6 +476,9 @@ Elles peuvent être configurées par le biais du fichier de paramètres « wing Canal + + Affiche des informations sur un package spécifique. Par défaut, la requête doit correspondre de façon non sensible à la casse à l’ID, au nom ou au moniker du package. D’autres champs peuvent être utilisés en passant l’option appropriée. + Affiche des informations sur un package @@ -650,7 +653,7 @@ Elles peuvent être configurées par le biais du fichier de paramètres « wing Commande non reconnue - Mettre à jour tous les packages installés vers la dernière version si disponible + Mettre à jour tous les paquets installés vers la dernière version si disponible Aucune mise à jour applicable trouvée. @@ -719,4 +722,96 @@ Elles peuvent être configurées par le biais du fichier de paramètres « wing Journaux Diagnostic files containing information about application use. + + Le programme d’installation est bloqué par une stratégie + + + Le programme d’installation a échoué vérification de sécurité + + + Un produit antivirus signale une infection dans le programme d’installation + + + Désolé...Nous n’avons pas pu tenter la mise à jour de la source : + + + Désinstalle le package sélectionné, que vous avez trouvé en effectuant une recherche dans la liste des packages installés ou directement à partir d’un manifeste. Par défaut, la requête doit correspondre de façon non sensible à la casse à l’ID, au nom ou au moniker du package. D’autres champs peuvent être utilisés en passant l’option appropriée. + + + Désinstallation du paquet donné + + + Début de la désinstallation du paquet... + + + Désinstallé avec succès + + + winget ne peut pas trouver la commande de désinstallation pour ce package. Veuillez contacter l’éditeur du package pour obtenir de l’aide. + {Locked="winget"} + + + Désinstallation abandonnée + + + Désolé... Nous n’avons pas pu effectuer la désinstallation avec le code de sortie : + + + Exporte une liste des packages installés + + + Installe tous les packages listés dans un fichier. + + + Installe tous les packages dans un fichier + + + Fichier dans lequel le résultat doit être écrit + + + Fichier décrivant les packages à installer + + + Exporter les packages à partir de la source spécifiée + + + Écrit une liste des packages installés dans un fichier. Les packages peuvent ensuite être installés à l’aide de la commande import. + {Locked="import"} + + + L’installation d’un ou plusieurs packages importés a échoué + + + Package introuvable pour l’importation : + + + La source requise pour l’importation n’est pas installée : + + + Le package installé n’est pas disponible à partir d’une source : + + + La version installée du package n’est pas disponible à partir d’une source : + + + Aucun package n’a été trouvé dans le fichier d’importation + + + Le fichier JSON n'est pas valide + + + Le package est déjà installé : + + + Ignorer les packages non disponibles + + + Inclure les versions de package dans le fichier généré + + + Ignorer les versions de package du fichier d’importation + + + Le chemin d’accès n’existe pas : + \ No newline at end of file diff --git a/Localization/Resources/gl-ES/winget.resw b/Localization/Resources/gl-ES/winget.resw index 52b373a822..821c1f3679 100644 --- a/Localization/Resources/gl-ES/winget.resw +++ b/Localization/Resources/gl-ES/winget.resw @@ -389,7 +389,7 @@ Pódense configurar mediante o ficheiro de configuración “winget settings”. Non hai instaladores aplicables ao sistema actual. - Actualmente non hai funcionalidades exprimentais dispoñibles. + Non hai funcionalidades exprimentais dispoñibles actualmente. Non se encontrou ningún paquete instalado que cumpra os criterios de entrada. @@ -476,6 +476,9 @@ Pódense configurar mediante o ficheiro de configuración “winget settings”. Canle + + Mostra información sobre un paquete específico. Por defecto, a consulta debe coincidir sen diferenciar maiúsculas e minúsculas co ID, o nome ou co moniker do paquete. Poden usarse outros campos pasando as opcións apropiadas. + Mostra información acerca dun paquete @@ -650,7 +653,7 @@ Pódense configurar mediante o ficheiro de configuración “winget settings”. Comando non recoñecido - Actualizar todos os paquetes instalados á versión máis recente que estea dispoñible + Actualiza todos os paquetes instalados á versión máis recente que estea dispoñible Non se atoparon actualizacións aplicables. @@ -719,4 +722,96 @@ Pódense configurar mediante o ficheiro de configuración “winget settings”. Informes Diagnostic files containing information about application use. + + A política está a bloquear o instalador + + + O instalador rexistrou un erro na comprobación de seguranza + + + Un produto antivirus rexistra unha infección no instalador + + + Produciuse un erro ao tentar actualizar a orixe: + + + Desinstala o paquete seleccionado, que se atopa buscando na lista de paquetes instalados ou directamente desde un manifesto. Por defecto, a consulta debe coincidir coa identificación, nome ou moniker do paquete sen diferenciar maiúsculas e minúsculas. Poden usarse outros campos transmitindo as opcións apropiadas. + + + Desinstala o paquete fornecido + + + Iniciando a desinstalación do paquete... + + + A desinstalación concluíu correctamente + + + winget non atopa o comando de desinstalación deste paquete. Ponte en contacto co editor do paquete para recibir asistencia. + {Locked="winget"} + + + Saíuse da desinstalación + + + Produciuse un erro na desinstalación co seguinte código de saída: + + + Exporta unha lista dos paquetes instalados + + + Instala todos os paquetes incluídos nun ficheiro. + + + Instala todos os paquetes incluídos nun ficheiro + + + Ficheiro no que se vai escribir o resultado + + + Ficheiro no que se describen os paquetes que se van instalar + + + Exporta os paquetes da orixe especificada + + + Escribe unha lista dos paquetes instalados nun ficheiro. A continuación, os paquetes pódense instalar cun comando de importación. + {Locked="import"} + + + Non se poden instalar un ou máis paquetes importados + + + Non se atopou ningún paquete para importalo: + + + A orixe necesaria para a importación non está instalada: + + + O paquete instalado non está dispoñible desde ningunha orixe: + + + A versión instalada do paquete non está dispoñible desde ningunha orixe: + + + Non se encontrou ningún paquete no ficheiro de importación + + + O ficheiro de JSON non é válido + + + O paquete xa está instalado: + + + Ignora os paquetes desactivados + + + Inclúe as versións do paquete no ficheiro xerado + + + Ignora as versións do paquete do ficheiro de importación + + + O camiño non existe: + \ No newline at end of file diff --git a/Localization/Resources/he-IL/winget.resw b/Localization/Resources/he-IL/winget.resw index 6a9ce254e2..1844ba2d7f 100644 --- a/Localization/Resources/he-IL/winget.resw +++ b/Localization/Resources/he-IL/winget.resw @@ -476,6 +476,9 @@ ערוץ + + הצגת מידע על חבילה ספציפית. כברירת מחדל, השאילתה צריכה להתאים, ללא תלות באותיות רישיות, למזהה, לשם או לכינוי של החבילה. ניתן להשתמש בשדות אחרים על-ידי העברת האפשרות המתאימה להם. + הצגת מידע אודות חבילה @@ -719,4 +722,96 @@ יומנים Diagnostic files containing information about application use. + + המתקין חסום על-ידי מדיניות + + + המתקין נכשל בבדיקת אבטחה + + + מוצר למניעת וירוס מדווח על הידבקות במתקין + + + הניסיון לעדכן את המקור נכשל: + + + הסרת ההתקנה של החבילה שנבחרה, נמצאה על-ידי חיפוש ברשימת החבילות המותקנות או ישירות ממניפסט. כברירת מחדל, השאילתה חייבת להיות מתאימה לרישיות בהתאם למזהה, לשם או לכינוי של החבילה. ניתן להשתמש בשדות אחרים על-ידי העברת האפשרות המתאימה שלהם. + + + מסיר התקנה לחבילה נתונה + + + מתחיל הסרת התקנה של חבילה... + + + ההתקנה הוסרה בהצלחה + + + winget אין אפשרות לאתר את פקודת הסרת ההתקנה עבור חבילה זו. פנה אל מפרסם החבילה לקבלת תמיכה. + {Locked="winget"} + + + הסרת התקנה ננטשה + + + הסרת ההתקנה נכשלה עם קוד יציאה: + + + מייצא רשימה של החבילות המותקנות + + + מתקין את כל החבילות המפורטות בקובץ. + + + מתקין את כל החבילות בקובץ + + + הקובץ שבו יש לכתוב את התוצאה + + + קובץ המתאר את החבילות שיש להתקין + + + יצא חבילות מהמקור שצוין + + + כותב רשימה של החבילות המותקנות לקובץ. ניתן להתקין את החבילות באמצעות הפקודה import. + {Locked="import"} + + + התקנת חבילות מיובאת אחת או יותר נכשלה + + + לא נמצאה חבילה לייבוא: + + + המקור הנדרש לצורך ייבוא אינו מותקן: + + + החבילה המותקנת אינה זמינה בשום מקור: + + + הגירסה המותקנת של החבילה אינה זמינה בשום מקור: + + + לא נמצאו חבילות בקובץ הייבוא + + + קובץ JSON אינו חוקי + + + החבילה כבר מותקנת: + + + התעלם מחבילות לא זמינות + + + כלול גירסאות חבילות בקובץ המופק + + + התעלם מגירסאות החבילות בקובץ הייבוא + + + ‏‏הנתיב אינו קיים: + \ No newline at end of file diff --git a/Localization/Resources/hi-IN/winget.resw b/Localization/Resources/hi-IN/winget.resw index f8047fcd8c..2b1d4c44fd 100644 --- a/Localization/Resources/hi-IN/winget.resw +++ b/Localization/Resources/hi-IN/winget.resw @@ -389,7 +389,7 @@ वर्तमान सिस्टम पर कोई भी installer लागू नहीं है. - वर्तमान में कोई प्रयोगात्मक सुविधाएँ उपलब्ध नहीं हैं. + इस समय कोई प्रयोगात्मक सुविधाएँ उपलब्ध नहीं हैं. इनपुट मानदंड से मेल खाता कोई स्थापित पैकेज नहीं मिला. @@ -476,6 +476,9 @@ चैनल + + किसी विशिष्ट पैकेज पर जानकारी दिखाता है. डिफ़ॉल्ट रूप से, क्वेरी को केस-संवेदी रूप से पैकेज के id, नाम, या moniker से मेल खाना आवश्यक है. अन्य फ़ील्ड्स का उचित विकल्प पास करके उपयोग किया जा सकता है. + किसी पैकेज के बारे में जानकारी दिखाता है @@ -650,7 +653,7 @@ अपरिचित आदेश - सभी स्थापित पैकेज को नवीनतम के लिए अद्यतन करें यदि उपलब्ध हैं + उपलब्ध होने पर सभी स्थापित पैकेज को नवीनतम के लिए अद्यतन करें कोई लागू अद्यतन नहीं मिला. @@ -719,4 +722,96 @@ लॉग Diagnostic files containing information about application use. + + इंस्टॉलर को नीति के कारण अवरोधित किया गया + + + इंस्टॉलर सुरक्षा जाँच में विफल रहा + + + एक एंटी-वायरस उत्पाद इंस्टॉलर में संक्रमण की रिपोर्ट करता है + + + स्रोत को अद्यतन करने के प्रयास में विफल रहा: + + + चयनित पैकेज की स्थापना रद्द करें, या तो स्थापित पैकेज सूची में खोज कर या सीधे किसी मैनिफ़ेस्ट से मिले. डिफ़ॉल्ट रूप से, क्वेरी को केस-संवेदी रूप से पैकेज के id, नाम, या moniker से मेल खाना आवश्यक है. अन्य फ़ील्ड्स का उचित विकल्प पास करके उपयोग किया जा सकता है. + + + दिए गए पैकेज की स्थापना रद्द करता है + + + पैकेज स्थापना रद्द करना प्रारंभ हो रहा है... + + + स्थापना सफलतापूर्वक रद्द की गई + + + winget इस पैकेज के लिए स्थापना रद्द करें आदेश का पता नहीं लगा सकता. कृपया सहायता के लिए पैकेज प्रकाशक से संपर्क करें. + {Locked="winget"} + + + स्थापना रद्द करना छोड़ा गया + + + निकास कोड के साथ स्थापना रद्द करना विफल रहा: + + + स्थापित पैकेज की सूची निर्यात करता है + + + किसी फ़ाइल में सूचीबद्ध सभी पैकेज स्थापित करता है. + + + किसी फ़ाइल में सभी पैकेज स्थापित करता है + + + फ़ाइल जहाँ परिणाम लिखा जाना है + + + स्थापित करने के लिए पैकेज का वर्णन करने वाली फ़ाइल + + + निर्दिष्ट स्रोत से पैकेज का निर्यात करें + + + किसी फ़ाइल में स्थापित पैकेज की सूची लिखता है. इसके बाद import आदेश के साथ पैकेज स्थापित किए जा सकते हैं. + {Locked="import"} + + + एक या अधिक आयातित पैकेज स्थापित करने में विफल + + + आयात के लिए पैकेज नहीं मिला: + + + आयात के लिए आवश्यक स्रोत स्थापित नहीं है: + + + स्थापित पैकेज किसी भी स्रोत से उपलब्ध नहीं है: + + + पैकेज का स्थापित संस्करण किसी भी स्रोत से उपलब्ध नहीं है: + + + आयात फ़ाइल में कोई पैकेज नहीं मिला + + + JSON फ़ाइल मान्य नहीं है + + + पैकेज पहले से ही स्थापित है: + + + अनुपलब्ध पैकेज पर ध्यान न दें + + + उत्पादित फ़ाइल में पैकेज संस्करण को शामिल करें + + + आयात फ़ाइल से मिले पैकेज संस्करणों पर ध्यान न दें + + + पथ मौजूद नहीं है: + \ No newline at end of file diff --git a/Localization/Resources/hr-HR/winget.resw b/Localization/Resources/hr-HR/winget.resw index b018990413..542011a90e 100644 --- a/Localization/Resources/hr-HR/winget.resw +++ b/Localization/Resources/hr-HR/winget.resw @@ -476,6 +476,9 @@ Mogu se konfigurirati u datoteci postavki „winget settings“. Kanal + + Prikazuje informacije o određenom paketu. Prema zadanim postavkama, upit se mora podudarati s ID-jem, imenom ili nadimkom paketa, bez razlikovanja velikih i malih slova. Druga se polja mogu upotrebljavati prosljeđivanjem odgovarajuće mogućnosti. + Pokazuje informacije o paketu @@ -719,4 +722,96 @@ Mogu se konfigurirati u datoteci postavki „winget settings“. Zapisnici Diagnostic files containing information about application use. + + Pravilnik blokira igru + + + Instalacijski program nije prošao sigurnosnu provjeru + + + Antivirusni proizvod prijavljuje zarazu u instalacijskom programu + + + Pokušaj ažuriranja izvora nije uspio: + + + Deinstalira odabrani paket koji se pronalazi pretraživanjem popisa instaliranih paketa ili izravno iz manifesta. Prema zadanim postavkama, upit se mora podudarati s ID-om, nazivom ili nadimkom paketa, bez razlikovanja velikih i malih slova. Druga se polja mogu koristiti prosljeđivanjem odgovarajuće mogućnosti. + + + Deinstalira se zadani paket + + + Pokretanje instalacije paketa... + + + Uspješno deinstalirano + + + winget ne može pronaći naredbu deinstalacije za ovaj paket. Za podršku se obratite izdavaču paketa. + {Locked="winget"} + + + Deinstalacija je odbačena + + + Deinstalacija nije uspjela uz izlazni kod: + + + Izvozi popis instaliranih paketa + + + Instalira sve pakete navedene u datoteci. + + + Instalira sve pakete u datoteku + + + Datoteka u kojoj se rezultat treba napisati + + + Datoteka s opisom paketa za instalaciju + + + Izvoz paketa iz navedenog izvora + + + Zapisuje popis instaliranih paketa u datoteku. Pakete zatim možete instalirati s pomoću naredbe za import. + {Locked="import"} + + + Instalacija jednog ili više uvezenih paketa nije uspjela + + + Paket nije pronađen za uvoz: + + + Izvor potreban za uvoz nije instaliran: + + + Instalirani paket nije dostupan ni iz kojeg izvora: + + + Instalirana verzija paketa nije dostupna ni iz kojeg izvora: + + + U datoteci za uvoz nije pronađen paket + + + JSON datoteka nije valjana + + + Paket je već instaliran: + + + Zanemarivanje nedostupnih paketa + + + Uvrsti verzije paketa u proizvedenu datoteku + + + Zanemarivanje verzija paketa iz datoteke za uvoz + + + Put ne postoji: + \ No newline at end of file diff --git a/Localization/Resources/hu-HU/winget.resw b/Localization/Resources/hu-HU/winget.resw index cd035be7b9..b99536ac5f 100644 --- a/Localization/Resources/hu-HU/winget.resw +++ b/Localization/Resources/hu-HU/winget.resw @@ -476,6 +476,9 @@ A „winget settings” nevű beállításfájl használatával konfigurálható Csatorna + + Egy adott csomagra vonatkozó információt jelenít meg. Alapértelmezés szerint a lekérdezésnek kis- és nagybetűk megkülönböztetése nélkül kell egyezést keresnie a csomag azonosítójával, nevével vagy monikerével. Más mezők is használhatók a megfelelő opciók átadásával. + Csomagra vonatkozó adatok megjelenítése @@ -719,4 +722,96 @@ A „winget settings” nevű beállításfájl használatával konfigurálható Naplók Diagnostic files containing information about application use. + + A telepítőt a házirend letiltotta + + + A telepítő nem tudta a biztonsági ellenőrzést elvégezni + + + A víruskereső fertőzést észlelt a telepítőben + + + A forrás frissítése sikertelen: + + + A kijelölt csomag eltávolításával a telepített csomagok listában vagy közvetlenül egy jegyzékből. Alapértelmezés szerint a lekérdezésnek meg kell insensitively, hogy megfeleljen a csomag azonosítója, neve vagy a moniker között. A megfelelő beállítás alapján más mezők is használhatók. + + + Az adott csomag eltávolítása + + + A csomag eltávolításának indítása... + + + Sikeresen eltávolítva + + + A winget nem találja a csomag eltávolítási parancsát. Kérjen segítséget a csomag közzétevőjával. + {Locked="winget"} + + + Eltávolítás megszakítva + + + Az eltávolítás sikertelen a következő kilépési kóddal: + + + A telepített csomagok listájának exportálása + + + A fájlban felsorolt összes csomag telepítése. + + + Egy fájlban szereplő összes csomag telepítése + + + Az eredmény kiírására használandó fájl + + + A telepítendő csomagokat leíró fájl + + + Csomagok exportálása a megadott forrásból + + + Egy fájlban felsorolja a telepített csomagokat. Ezt követően a csomagok az „import” paranccsal telepíthetők. + {Locked="import"} + + + Egy vagy több importált csomag telepítése nem sikerült + + + Nem található csomag az importáláshoz: + + + Az importáláshoz szükséges forrás nincs telepítve: + + + A telepített csomag egyetlen forrásból sem érhető el: + + + A csomag telepített verziója egyetlen forrásból sem érhető el: + + + Nem található csomag az importált fájlban + + + A JSON-fájl érvénytelen + + + A csomag már telepítve van: + + + Nem elérhető csomagok figyelmen kívül hagyása + + + Csomagverziók belefoglalása a létrehozott fájlba + + + Az importált fájlban szereplő csomagverziók figyelmen kívül hagyása + + + Az elérési út nem létezik: + \ No newline at end of file diff --git a/Localization/Resources/id-ID/winget.resw b/Localization/Resources/id-ID/winget.resw index 09d82efe20..b81e2a8ccc 100644 --- a/Localization/Resources/id-ID/winget.resw +++ b/Localization/Resources/id-ID/winget.resw @@ -476,6 +476,9 @@ Mereka dapat dikonfigurasi melalui ' winget settings ' file pengaturan. Saluran + + Menampilkan informasi pada paket tertentu. Secara default, kueri harus huruf-insensitively cocok dengan id, nama, atau moniker paket. Bidang lain dapat digunakan dengan cara melewati opsi yang tepat. + Tampilkan info tentang paket @@ -719,4 +722,96 @@ Mereka dapat dikonfigurasi melalui ' winget settings ' file pengaturan. Log Diagnostic files containing information about application use. + + Penginstal diblokir oleh kebijakan + + + Penginstal gagal dalam pemeriksaan keamanan + + + Produk anti-virus melaporkan infeksi pada penginstal + + + Gagal mencoba memperbarui sumber: + + + Menghapus paket yang dipilih, ditemukan dengan mencari daftar paket yang diinstal atau secara langsung dari manifes. Secara default, kueri harus huruf-insensitively cocok dengan id, nama, atau moniker paket. Bidang lain dapat digunakan dengan cara melewati opsi yang tepat. + + + Menghapus instalan paket tertentu + + + Memulai penghapusan instalan paket... + + + Berhasil menghapus instalan + + + winget tidak dapat menemukan perintah penghapusan instalasi untuk paket ini. Silakan hubungi penerbit paket untuk dukungan. + {Locked="winget"} + + + Penghapusan instalan diabaikan + + + Penghapusan instalan gagal dengan kode keluar: + + + Mengekspor daftar paket yang diinstal + + + Menginstal semua paket yang tercantum dalam file. + + + Menginstal semua paket dalam file + + + File tempat hasil akan ditulis + + + File yang menjelaskan paket yang akan diinstal + + + Ekspor paket dari sumber yang ditentukan + + + Menulis daftar paket yang diinstal ke file. Paket kemudian dapat diinstal dengan perintah import. + {Locked="import"} + + + Satu atau beberapa paket yang diimpor gagal diinstal + + + Paket tidak ditemukan untuk diimpor: + + + Sumber yang diperlukan untuk impor tidak diinstal: + + + Paket yang diinstal tidak tersedia dari sumber mana pun: + + + Versi paket yang diinstal tidak tersedia dari sumber mana pun: + + + Tidak ada paket yang ditemukan dalam file impor + + + File JSON tidak valid + + + Paket sudah diinstal: + + + Abaikan paket yang tidak tersedia + + + Sertakan versi paket dalam file yang dihasilkan + + + Abaikan versi paket dari file impor + + + Jalur tidak ada: + \ No newline at end of file diff --git a/Localization/Resources/is-IS/winget.resw b/Localization/Resources/is-IS/winget.resw index 363c63428f..8a38b64dde 100644 --- a/Localization/Resources/is-IS/winget.resw +++ b/Localization/Resources/is-IS/winget.resw @@ -476,6 +476,9 @@ Hægt er að grinnstilla þá í stillingaskránni „winget settings“. Rás + + Sýnir upplýsingar um tiltekinn pakka. Fyrirspurnin verður sjálfgefið að samsvara auðkenni, heiti eða viðurnefni pakkans. Hægt er að nota aðra reiti með því að senda viðeigandi valkosti þeirra. + Sýnir upplýsingar um pakka @@ -719,4 +722,96 @@ Hægt er að grinnstilla þá í stillingaskránni „winget settings“.Annálar Diagnostic files containing information about application use. + + Regla útilokar uppsetningarforritið + + + Uppsetningarforritið stóðst ekki öryggisathugun + + + Vírusvarnarvara tilkynnir uppsetningarforritinu um sýkingu + + + Mistókst að uppfæra upprunann: + + + Fjarlægir valinn pakka, sem annað hvort fannst með því að leita að uppsettum pakkalistum eða beint úr farmskrá. Fyrirspurnin verður sjálfgefið að samsvara auðkenni, heiti eða viðurnefni pakkans án þess að greina á milli há- og lágstafa. Hægt er að nota aðra reiti með því að senda viðeigandi valkosti. + + + Fjarlægir pakkann + + + Byrjar fjarlægingu á pakka... + + + Fjarlægt + + + winget finnur ekki skipun um að fjarlægja þennan pakka. Hafðu samband við útgefanda pakkans til að fá aðstoð. + {Locked="winget"} + + + Hætt við fjarlægingu + + + Fjarlæging mistókst með lokunarkóða: + + + Flytur út lista yfir uppsetta pakka + + + Setur upp alla pakka sem gefnir eru upp í skrá. + + + Setur upp alla pakkana í skrá. + + + Skrá þar sem skrifa á niðurstöðuna + + + Skrá sem lýsir pökkunum til að setja upp + + + Flytja út pakka frá tilgreindum uppruna + + + Skrifar lista yfir uppsetta pakka í skrá. Hægt er að setja pakkana upp með import skipuninni. + {Locked="import"} + + + Ekki tókst að setja upp einn eða fleiri innflutta pakka + + + Pakkinn fannst ekki fyrir innflutning: + + + Uppruni sem þarf fyrir að flytja inn er ekki uppsettur: + + + Uppsettur pakki er ekki tiltækur úr neinum uppruna: + + + Uppsett útgáfa af pakka er ekki tiltæk úr neinum uppruna: + + + Engir pakkar fundust í innfluttu skránni + + + JSON-skrá er ekki gild + + + Pakki er þegar uppsettur: + + + Hunsa ótiltæka pakka + + + Taka útgáfur pakka með í framleidda skrá + + + Hunsa útgáfur pakka úr innfluttri skrá + + + Slóð er ekki til: + \ No newline at end of file diff --git a/Localization/Resources/it-IT/winget.resw b/Localization/Resources/it-IT/winget.resw index c4ab646e35..eab5f51bde 100644 --- a/Localization/Resources/it-IT/winget.resw +++ b/Localization/Resources/it-IT/winget.resw @@ -389,7 +389,7 @@ Possono essere configurati tramite il file di impostazioni ' winget settings '.< Nessun programmi di installazione è applicabile al sistema corrente. - Al momento non sono disponibili funzionalità sperimentali. + Al momento non sono disponibili funzionalità sperimentali. Non è stato trovato alcun pacchetto installato corrispondente ai criteri di input. @@ -476,6 +476,9 @@ Possono essere configurati tramite il file di impostazioni ' winget settings '.< Canale + + Visualizza informazioni su un pacchetto specifico. Per impostazione predefinita, la query deve essere insensitively con l'ID, il nome o il moniker del pacchetto. Altri campi possono essere utilizzati passando l'opzione appropriata. + Mostra le informazioni relative a un pacchetto @@ -719,4 +722,96 @@ Possono essere configurati tramite il file di impostazioni ' winget settings '.< Log Diagnostic files containing information about application use. + + Programma di installazione bloccato dai criteri + + + Non è stato possibile eseguire il controllo di sicurezza per il programma di installazione + + + Un antivirus ha rilevato una infezione nel programma di installazione + + + Errore durante il tentativo di aggiornare l'origine: + + + Disinstalla il pacchetto selezionato, trovato mediante la ricerca dell'elenco dei pacchetti installati o direttamente da un manifesto. Per impostazione predefinita, la query deve essere insensitively con l'ID, il nome o il moniker del pacchetto. Altri campi possono essere utilizzati passando l'opzione appropriata. + + + Disinstalla il pacchetto specificato + + + Avvio disinstallazione pacchetto in corso... + + + Installazione completata + + + winget non riesce a individuare il comando di disinstallazione per questo pacchetto. Contattare l'autore del pacchetto per chiedere supporto. + {Locked="winget"} + + + Disinstallazione interrotta + + + Disinstallazione non riuscita con codice di uscita: + + + Esporta un elenco dei pacchetti installati + + + Installa tutti i pacchetti elencati in un file. + + + Installa tutti i pacchetti in un file + + + File in cui deve essere scritto il risultato + + + File che descrive i pacchetti da installare + + + Esporta pacchetti dall'origine specificata + + + Scrive un elenco dei pacchetti installati in un file. I pacchetti possono quindi essere installati con il comando import. + {Locked="import"} + + + Impossibile installare uno o più pacchetti importati + + + Pacchetto non trovato per l'importazione: + + + L'origine necessaria per l'importazione non è installata: + + + Il pacchetto installato non è disponibile in alcuna origine: + + + La versione installata del pacchetto non è disponibile in alcuna origine: + + + Nessun pacchetto trovato nel file di importazione + + + Il file JSON non è valido + + + Il pacchetto è già installato: + + + Ignora pacchetti non disponibili + + + Includi versioni del pacchetto nel file prodotto + + + Ignora versioni del pacchetto dal file di importazione + + + Il percorso non esiste: + \ No newline at end of file diff --git a/Localization/Resources/ja-JP/winget.resw b/Localization/Resources/ja-JP/winget.resw index 5139caf4f2..0856acb5d3 100644 --- a/Localization/Resources/ja-JP/winget.resw +++ b/Localization/Resources/ja-JP/winget.resw @@ -389,7 +389,7 @@ 現在のシステムに適用できるインストーラーはありません。 - 現在、試験的機能はありません。 + 現在、試験的な機能はありません。 入力条件に一致するインストール済みのパッケージが見つかりませんでした。 @@ -476,6 +476,9 @@ チャネル + + 特定のパッケージの情報を表示します。既定では、クエリはパッケージの ID、名前、モニカーに大文字小文字の区別なく一致する必要があります。その他のフィールドは、適切なオプションを渡すことで使用することができます。 + パッケージに関する情報の表示 @@ -650,7 +653,7 @@ 認識されないコマンド - インストールされているすべてのパッケージを利用可能であれば最新のに更新する + インストールされているすべてのパッケージが利用可能であれば最新のものに更新する 適用可能な更新は見つかりませんでした。 @@ -719,4 +722,96 @@ ログ Diagnostic files containing information about application use. + + インストーラーはポリシーによってブロックされています + + + インストーラーのセキュリティチェックに失敗しました + + + ウイルス対策製品がインストーラーに感染を報告します + + + ソースを更新中に失敗しました: + + + インストールされているパッケージ リストを検索するか、マニフェストから直接検索して、選択したパッケージをアンインストールします。既定では、クエリはパッケージの ID、名前、モニカーと大文字と小文字の区別なく一致する必要があります。その他のフィールドは、適切なオプションを渡すことで使用することができます。 + + + 指定されたパッケージをアンインストール + + + パッケージのアンインストールを開始しています... + + + 正常にアンインストールされました + + + winget は、このパッケージのアンインストールコマンドを見つけることができません。サポートについては、パッケージの発行元に問い合わせてください。 + {Locked="winget"} + + + アンインストールが中止されました + + + 終了コードでアンインストールに失敗しました: + + + インストールされているパッケージのリストをエクスポートする + + + ファイルに一覧表示されているすべてのパッケージをインストールします。 + + + すべてのパッケージをファイルにインストールします + + + 結果が書き込まれるファイル + + + インストールするパッケージを説明するファイル + + + 指定したソースからパッケージをエクスポートする + + + インストールされているパッケージの一覧をファイルに書き込みます。その後、パッケージを import コマンドを使用してインストールできます。 + {Locked="import"} + + + 1つまたは複数のインポートされたパッケージをインストールできませんでした + + + インポートするパッケージが見つかりません: + + + インポートに必要なソースがインストールされていません: + + + インストールされているパッケージのバージョンは、どのソースからも利用できません: + + + インストールされているパッケージのバージョンは、どのソースからも利用できません: + + + インポート ファイルにパッケージが見つかりません + + + JSON ファイルが無効です + + + パッケージは既にインストールされています: + + + 使用できないパッケージを無視する + + + 作成されたファイルにパッケージ バージョンを含める + + + インポート ファイルからパッケージ バージョンを無視する + + + パスが存在しません: + \ No newline at end of file diff --git a/Localization/Resources/kk-KZ/winget.resw b/Localization/Resources/kk-KZ/winget.resw index 5aee6e9b40..b64fa97a43 100644 --- a/Localization/Resources/kk-KZ/winget.resw +++ b/Localization/Resources/kk-KZ/winget.resw @@ -389,7 +389,7 @@ Ағымдағы жүйе үшін жарамды орнату құралдары жоқ. - Қазіргі уақытта қолжетімді эксперименттік мүмкіндіктер жоқ. + Қазіргі уақытта қолжетімді эксперименттік мүмкіндіктер жоқ. Енгізілген шарттарға сәйкес келетін ешқандай орнатылған бума табылмады. @@ -476,6 +476,9 @@ Арна + + Белгілі бір бума туралы мәліметтерді көрсетеді. Әдепкі бойынша сұрау буманың идентификаторын, атын немесе моникерін (регистрді ескермей) салыстыруы қажет. Тиісті параметрді көрсету арқылы басқа өрістерді пайдалануға болады. + Бума туралы ақпаратты көрсетеді @@ -650,7 +653,7 @@ Пәрмен анықталмады - Қолжетімді болса, барлық орнатылған бумаларды жаңарту + Қолжетімді болса, барлық орнатылған бумаларды соңғы нұсқасына жаңарту Қолжетімді жаңарту табылмады. @@ -719,4 +722,96 @@ Журналдар Diagnostic files containing information about application use. + + Орнатушы саясат бойынша блокталған + + + Орнату құралы қауіпсіздік тексерісінен өте алмады + + + Антивирус орнату құралында зиянды бағдарлама бар болғанын анықтады + + + Дереккөзді жаңарту әрекеті орындалмады: + + + Орнатылған бумалар тізімінен іздеу арқылы немесе манифесттен тікелей табылған, таңдалған буманы жояды. Әдепкі бойынша сұрау буманың идентификаторын, атын немесе моникерін (регистрді ескермей) салыстыруы қажет. Тиісті параметрді көрсету арқылы басқа өрістерді пайдалануға болады. + + + Берілген буманы жояды + + + Буманы жою басталды... + + + Жойылды + + + winget осы бумаға арналған жою пәрменінің орналасқан жерін таба алмады. Бума жариялаушысына хабарласып, көмек сұраңыз. + {Locked="winget"} + + + Жою әрекеті қараусыз қалды + + + Жою әрекеті сәтсіз аяқталды, шығу коды: + + + Орнатылған бумалар тізімін экспорттайды + + + Файлдағы барлық бумаларды орнатады. + + + Файлдағы барлық бумаларды орнатады + + + Нәтиже жазылатын файл + + + Орнатылатын бумаларды сипаттайтын файл + + + Көрсетілген дереккөзден бумаларды экспорттау + + + Орнатылған бумалардың тізімін файлға жазады. Осыдан кейін бумаларды import пәрменімен орнатуға болады. + {Locked="import"} + + + Бір немесе бірнеше импортталған буманы орнату мүмкін болмады + + + Импортталатын бума табылмады: + + + Импорттау үшін қажетті дереккөз орнатылмаған: + + + Орнатылған бума кез келген дереккөзден қолжетімді емес: + + + Буманың орнатылған нұсқасы кез келген дереккөзден қолжетімді емес: + + + Импорттау файлында бумалар табылмады + + + JSON файлы жарамсыз + + + Бума орнатылып қойған: + + + Қол жеткізгісіз бумаларды елемеу + + + Жасалған файлға бума нұсқаларын қосу + + + Импорттау файлындағы бума нұсқаларын елемеу + + + Мұндай жол жоқ: + \ No newline at end of file diff --git a/Localization/Resources/km-KH/winget.resw b/Localization/Resources/km-KH/winget.resw index 747b60c4b2..0765372158 100644 --- a/Localization/Resources/km-KH/winget.resw +++ b/Localization/Resources/km-KH/winget.resw @@ -389,7 +389,7 @@ គ្មាន​កម្មវិធី​ដំឡើង​ដែល​អាច​អនុវត្ត​បាន​ចំពោះ​ប្រព័ន្ធ​បច្ចុប្បន្ន​ទេ។ - បច្ចុប្បន្ននេះមិនមានលក្ខណៈពិសោធន៍សាកល្បងទេ។ + បច្ចុប្បន្ននេះ មិនមាន​​មុខងារ​​ពិសោធន៍​ទេ។ រកមិនឃើញកញ្ចប់ដែលបានដំឡើងត្រូវនឹងលក្ខខណ្ឌបញ្ចូលដែលត្រូវគ្នា។ @@ -476,6 +476,9 @@ កាណាល់ + + បង្ហាញ​ព័ត៌មាន​អំពី​កញ្ចប់​ជាក់លាក់។ តាម​លំនាំដើម សំណួរ​ត្រូវ​តែ​ប្រកាន់អក្សរ​តូ​ចធំ លេខ​សម្គាល់ ឈ្មោះ ឬ​ឈ្មោះ​ក្រៅ​របស់​កញ្ចប់។ កន្លែង​បញ្ចូល​ផ្សេង​ទៀត​អាច​ត្រូវ​បានប្រើ​ដោយ​បញ្ជូន​ជម្រើស​សមស្រប​របស់​ពួកវា។ + បង្ហាញ​ព័ត៌មាន​អំពីកញ្ចប់ @@ -650,7 +653,7 @@ ពាក្យបញ្ជាដែលមិនស្គាល់ - ធ្វើបច្ចុប្បន្នភាពកញ្ចប់ដែលបានដំឡើងទាំងអស់ទៅថ្មីបំផុតប្រសិនបើមាន + អាប់ដេតកញ្ចប់ដែលបានដំឡើងទាំងអស់ទៅកំណែចុងក្រោយបំផុត ប្រសិនបើមាន រកមិនឃើញបច្ចុប្បន្នភាពដែលអាចអនុវត្តបានទេ។ @@ -719,4 +722,96 @@ កំណត់ហេតុ Diagnostic files containing information about application use. + + កម្មវិធីដំឡើង​ត្រូវបាន​ប្លុក​ដោយគោលការណ៍ + + + កម្មវិធី​ដំឡើង​មិន​អាច​ត្រួតពិនិត្យសន្តិសុខ​បាន​ទេ + + + ផលិតផល​ប្រឆាំងមេរោគ​រាយការណ៍អំពី​ការឆ្លងមេរោគ​នៅក្នុងកម្មវិធីដំឡើង + + + បានបរាជ័យ​ក្នុងការព្យាយាម​អាប់ដេត​ប្រភព៖ + + + លុបកញ្ចប់ដែលបានជ្រើសរើស ទោះបីជារកឃើញតាមការស្វែងរកបញ្ជីកញ្ចប់ដែលបានដំឡើង ឬដោយផ្ទាល់ពីមេនីហ្វេសថ៍ក៏ដោយ។ តាមលំនាំដើម សំណួរត្រូវតែត្រូវគ្នានឹងលេខសម្គាល់ ឈ្មោះ ឬឈ្មោះហៅក្រៅរបស់​កញ្ចប់​ដោយមិនបែងចែកអក្សរធំតូច។ កន្លែងបញ្ចូល​ផ្សេងទៀតអាចត្រូវបានប្រើដោយឆ្លងកាត់ជម្រើសដែលសមស្របរបស់កន្លែងបញ្ចូលទាំងនោះ។ + + + លុបកញ្ចប់ដែលបានផ្ដល់ + + + កំពុងចាប់ផ្ដើម​លុបកញ្ចប់... + + + បានលុប​​ដោយជោគជ័យ + + + winget មិនអាចកំណត់ទីតាំង​ការបញ្ជាដែលលុបសម្រាប់​កញ្ចប់នេះបានទេ។ សូមទាក់ទងទៅ​អ្នកបោះពុម្ពផ្សាយ​កញ្ចប់​ ដើម្បីទទួលបានជំនួយ។ + {Locked="winget"} + + + បានបោះបង់​ការលុប + + + មិនអាចលុបបានទេ ហើយកូដចេញគឺ៖ + + + នាំចេញបញ្ជីនៃកញ្ចប់ដែលបានដំឡើង + + + ដំឡើងកញ្ចប់ទាំងអស់ដែលបានដាក់ជាបញ្ជីនៅក្នុងឯកសារ។ + + + ដំឡើងកញ្ចប់ទាំងអស់នៅក្នុងឯកសារ + + + ឯកសារដែលលទ្ធផលត្រូវបានកត់ត្រា + + + ឯកសារដែលពណ៌នាអំពីកញ្ចប់សម្រាប់ដំឡើង + + + នាំចេញកញ្ចប់ពីប្រភពដែលបានបញ្ជាក់ + + + សរសេរបញ្ជីនៃកញ្ចប់ដែលបានដំឡើងទៅឯកសារ។ បន្ទាប់មក កញ្ចប់អាចត្រូវបានដំឡើងជាមួយការបញ្ជា import។ + {Locked="import"} + + + មិនអាចដំឡើងកញ្ចប់ដែលនាំចូលមួយ ឬច្រើនបានទេ + + + រកមិនឃើញកញ្ចប់សម្រាប់នាំចូលទេ៖ + + + ប្រភពដែលតម្រូវឱ្យមានសម្រាប់ការនាំចូលមិនត្រូវបានដំឡើងទេ៖ + + + មិនមានកំណែកញ្ចប់ដែលបានដំឡើងពីប្រភពណាមួយទេ៖ + + + មិនមានកំណែកញ្ចប់ដែលបានដំឡើងពីប្រភពណាមួយទេ៖ + + + រកមិនឃើញកញ្ចប់នៅក្នុងឯកសារនាំចូលទេ + + + ឯកសារ JSON មិនត្រឹមត្រូវទេ + + + កញ្ចប់ត្រូវបានដំឡើងរួចហើយ៖ + + + មិនអើពើកញ្ចប់ដែលមិនអាចប្រើបាន + + + រួមបញ្ចូលកំណែកញ្ចប់នៅក្នុងឯកសារដែលបានផលិត + + + មិនអើពើកំណែកញ្ចប់ពីឯកសារនាំចូល + + + មិនមានផ្លូវទេ៖ + \ No newline at end of file diff --git a/Localization/Resources/kn-IN/winget.resw b/Localization/Resources/kn-IN/winget.resw index 14209ef0d2..755bf1e90f 100644 --- a/Localization/Resources/kn-IN/winget.resw +++ b/Localization/Resources/kn-IN/winget.resw @@ -389,7 +389,7 @@ ಪ್ರಸ್ತುತ ವ್ಯವಸ್ಥೆಗೆ ಯಾವುದೇ ಸ್ಥಾಪಕಗಳು ಅನ್ವಯಿಸುವುದಿಲ್ಲ. - ಯಾವುದೇ ಪ್ರಾಯೋಗಿಕ ವೈಶಿಷ್ಟ್ಯಗಳು ಪ್ರಸ್ತುತ ಲಭ್ಯವಿಲ್ಲ. + ಪ್ರಸ್ತುತ ಯಾವುದೇ ಪ್ರಾಯೋಗಿಕ ವೈಶಿಷ್ಟ್ಯಗಳು ಲಭ್ಯವಿಲ್ಲ. ಇನ್‍ಪುಟ್ ಮಾನದಂಡಗಳಿಗೆ ಯಾವುದೇ ಸ್ಥಾಪಿತ ಪ್ಯಾಕೇಜ್ ಕಂಡುಬಂದಿಲ್ಲ. @@ -476,6 +476,9 @@ ಚಾನಲ್ + + ನಿರ್ದಿಷ್ಟ ಪ್ಯಾಕೇಜ್‍ನಲ್ಲಿ ಮಾಹಿತಿಯನ್ನು ತೋರಿಸುತ್ತದೆ. ಡೀಫಾಲ್ಟ್ ಆಗಿ, ಪ್ರಶ್ನಾವಳಿಯು ಪ್ಯಾಕೇಜ್‍ನ ಐಡಿ, ಹೆಸರು ಅಥವಾ ಮೊನಿಕರ್‌‍ಗೆ ಹೋಲಿಕೆಯಾಗಬೇಕು. ಇತರ ಕ್ಷೇತ್ರಗಳನ್ನ ಅವುಗಳ ಸೂಕ್ತ ಆಯ್ಕೆಯನ್ನು ಪಾಸ್ ಮಾಡುವ ಮೂಲಕ ಬಳಸಬಹುದು. + ಒಂದು ಪ್ಯಾಕೇಜ್ ಬಗ್ಗೆ ಮಾಹಿತಿಯನ್ನು ತೋರಿಸುತ್ತದೆ @@ -650,7 +653,7 @@ ಗುರುತಿಸಲಾಗದ ಕಮಾಂಡ್ - ಲಭ್ಯವಿದ್ದರೆ ಎಲ್ಲಾ ಸ್ಥಾಪಿತ ಪ್ಯಾಕೇಜುಗಳನ್ನು ಇತ್ತೀಚಿನ ವರೆಗೆ ನವೀಕರಿಸಿ + ಲಭ್ಯವಿದ್ದರೆ ಎಲ್ಲಾ ಸ್ಥಾಪಿತ ಪ್ಯಾಕೇಜ್‌ಗಳನ್ನು ಇತ್ತೀಚಿನವರೆಗೆ ನವೀಕರಿಸಿ ಯಾವುದೇ ಅನ್ವಯವಾಗುವ ನವೀಕರಣ ವು ಕಂಡುಬಂದಿಲ್ಲ. @@ -719,4 +722,96 @@ ಲಾಗ್‌ಗಳು Diagnostic files containing information about application use. + + ಸ್ಥಾಪಕವನ್ನು ನೀತಿಯ ಮೂಲಕ ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ + + + ಭದ್ರತಾ ಪರಿಶೀಲನೆಯಲ್ಲಿ ಸ್ಥಾಪಕವು ವಿಫಲವಾಗಿದೆ + + + ಸ್ಥಾಪಕದಲ್ಲಿ ಸೋಂಕು ಇರುವುದನ್ನು ಒಂದು ಆ್ಯಂಟಿ-ವೈರಸ್ ಉತ್ಪನ್ನವು ವರದಿ ಮಾಡಿದೆ + + + ಮೂಲವನ್ನು ನವೀಕರಿಸುವ ಪ್ರಯತ್ನದಲ್ಲಿ ವಿಫಲವಾಗಿದೆ: + + + ಸ್ಥಾಪಿತ ಪ್ಯಾಕೇಜ್‍ಗಳ ಪಟ್ಟಿಯಿಂದ ಅಥವಾ ನೇರವಾಗಿ ಮ್ಯಾನಿಫೆಸ್ಟ್‌ನಿಂದ ಶೋಧಿಸುವ ಮೂಲಕ ಆಯ್ಕೆ ಮಾಡಿದ ಪ್ಯಾಕೇಜ್ ಅನ್ನು ಅಸ್ಥಾಪನೆಗೊಳಿಸುತ್ತದೆ. ಡೀಫಾಲ್ಟ್ ಆಗಿ, ಪ್ರಶ್ನಾವಳಿಯು ಪ್ಯಾಕೇಜ್‍ನ ID, ಹೆಸರು ಅಥವಾ ಮೋನಿಕರ್‌ನೊಂದಿಗೆ ಕೇಸ್-ಸೆನ್ಸೆಟಿವ್ ಆಗಿ ಹೋಲಿಕೆಯಾಗಬೇಕು. ಇತರ ಕ್ಷೇತ್ರಗಳನ್ನು ಅವುಗಳ ಸೂಕ್ತ ಆಯ್ಕೆಯನ್ನು ಪಾಸ್ ಮಾಡುವ ಮೂಲಕ ಬಳಸಬಹುದು. + + + ನೀಡಲಾದ ಪ್ಯಾಕೇಜ್ ಅನ್ನು ಅಸ್ಥಾಪನೆಗೊಳಿಸುತ್ತದೆ + + + ಪ್ಯಾಕೇಜ್ ಅಸ್ಥಾಪನೆಯನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ... + + + ಯಶಸ್ವಿಯಾಗಿ ಅಸ್ಥಾಪನೆಗೊಳಿಸಲಾಗಿದೆ + + + ಈ ಪ್ಯಾಕೇಜ್‌ಗಾಗಿ ಅಸ್ಥಾಪನೆ ಆದೇಶವನ್ನು ಕಂಡುಹಿಡಿಯಲು winget ಗೆ ಸಾಧ್ಯವಿಲ್ಲ. ಬೆಂಬಲಕ್ಕಾಗಿ ದಯವಿಟ್ಟು ಪ್ಯಾಕೇಜ್ ಪ್ರಕಾಶಕರನ್ನು ಸಂಪರ್ಕಿಸಿ. + {Locked="winget"} + + + ಅಸ್ಥಾಪನೆಯನ್ನು ಕೈಬಿಡಲಾಗಿದೆ + + + ಅಸ್ಥಾಪಿಸುವುದು ಈ ನಿರ್ಗಮನದ ಕೋಡ್‌ನೊಂದಿಗೆ ವಿಫಲವಾಗಿದೆ: + + + ಸ್ಥಾಪನೆಗೊಳಿಸಿದ ಪ್ಯಾಕೇಜ್‍ಗಳ ಪಟ್ಟಿಯನ್ನು ರಫ್ತು ಮಾಡುತ್ತದೆ + + + ಫೈಲ್‍ನಲ್ಲಿ ಪಟ್ಟಿ ಮಾಡಲಾಗಿರುವ ಎಲ್ಲ ಪ್ಯಾಕೇಜ್‍ಗಳನ್ನು ಸ್ಥಾಪಿಸುತ್ತದೆ. + + + ಫೈಲ್‍ನಲ್ಲಿ ಎಲ್ಲ ಪ್ಯಾಕೇಜ್‍ಗಳನ್ನು ಸ್ಥಾಪಿಸುತ್ತದೆ. + + + ಫಲಿತಾಂಶವನ್ನು ಎಲ್ಲಿ ಬರೆಯಬೇಕೊ ಆ ಫೈಲ್ + + + ಸ್ಥಾಪನೆಗೊಳಿಸುವ ಪ್ಯಾಕೇಜ್‍ಗಳನ್ನು ವಿವರಿಸುವ ಫೈಲ್ + + + ನಿರ್ದಿಷ್ಟಪಡಿಸಿದ ಮೂಲದಿಂದ ರಫ್ತು ಪ್ಯಾಕೇಜ್‍ಗಳು + + + ಫೈಲ್‍ಗೆ ಸ್ಥಾಪನೆಗೊಳಿಸಿದ ಪ್ಯಾಕೇಜ್‍ಗಳ ಪಟ್ಟಿಯನ್ನು ಬರೆಯುತ್ತದೆ. ಪ್ಯಾಕೇಜ್‍ಗಳನ್ನು ಆಗಲೂ import ಆದೇಶದೊಂದಿಗೆ ಸ್ಥಾಪನೆಗೊಳಿಸಬಹುದು. + {Locked="import"} + + + ಆಮದು ಮಾಡಿದ ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಪ್ಯಾಕೇಜ್ ಸ್ಥಾಪನೆಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ + + + ಆಮದಿಗೆ ಪ್ಯಾಕೇಜ್ ಕಂಡುಬಂದಿಲ್ಲ: + + + ಆಮದಿಗೆ ಅಗತ್ಯವಿರುವ ಮೂಲವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ: + + + ಯಾವುದೇ ಮೂಲದಿಂದ ಸ್ಥಾಪನೆಗೊಳಿಸಿದ ಪ್ಯಾಕೇಜ್ ಲಭ್ಯವಿಲ್ಲ: + + + ಯಾವುದೇ ಮೂಲದಿಂದ ಪ್ಯಾಕೇಜ್‍ನ ಸ್ಥಾಪಿತ ಆವೃತ್ತಿ ಲಭ್ಯವಿಲ್ಲ: + + + ಆಮದು ಫೈಲ್‍ನಲ್ಲಿ ಯಾವುದೇ ಪ್ಯಾಕೇಜ್‍ಗಳು ಕಂಡುಬಂದಿಲ್ಲ + + + JSON ಫೈಲ್ ಮಾನ್ಯವಾಗಿಲ್ಲ + + + ಪ್ಯಾಕೇಜ್ ಅನ್ನು ಈಗಾಗಲೇ ಸ್ಥಾಪಿಸಲಾಗಿದೆ: + + + ಅಲಭ್ಯ ಪ್ಯಾಕೇಜ್‍ಗಳನ್ನು ಕಡೆಗಣಿಸು + + + ಪ್ಯಾಕೇಜ್ ಆವೃತ್ತಿಗಳನ್ನು ಉತ್ಪಾದಿಸಿದ ಫೈಲ್‍ನಲ್ಲಿ ಸೇರಿಸಿ + + + ಆಮದು ಫೈಲ್‍ನಿಂದ ಪ್ಯಾಕೇಜ್ ಆವೃತ್ತಿಗಳನ್ನು ಕಡೆಗಣಿಸು + + + ಹಾದಿ ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲ: + \ No newline at end of file diff --git a/Localization/Resources/ko-KR/winget.resw b/Localization/Resources/ko-KR/winget.resw index 3b80db4858..830c768c12 100644 --- a/Localization/Resources/ko-KR/winget.resw +++ b/Localization/Resources/ko-KR/winget.resw @@ -389,7 +389,7 @@ 현재 시스템에 적용할 수 있는 설치 관리자가 없습니다. - 현재 사용 가능한 추가 기능이 없습니다. + 현재 사용 가능한 실험적 기능이 없습니다. 입력 조건과 일치하는 설치된 패키지를 찾을 수 없습니다. @@ -476,6 +476,9 @@ 채널 + + 특정 패키지에 대한 정보를 표시합니다. 기본적으로 쿼리는 패키지의 ID, 이름 또는 모양과 대/소문자를 구분하지 않는 다섯 번이 어려져야 합니다. 적절한 옵션을 전달하여 다른 필드를 사용할 수 있습니다. + 패키지에 대한 정보 표시 @@ -719,4 +722,96 @@ 로그 Diagnostic files containing information about application use. + + 설치 프로그램이 정책에 의해 차단됨 + + + 설치 관리자가 보안 검사를 하지 못했습니다 + + + 바이러스 백신 제품이 설치 관리자의 감염을 보고합니다. + + + 원본을 업데이트하지 못함: + + + 설치된 패키지 목록을 검색하거나 매니페스트에서 직접 찾은 선택한 패키지를 제거합니다. 기본적으로 쿼리는 패키지의 ID, 이름 또는 모양과 대/소문자를 구분하지 않는 것이 어떻게 되아야 합니다. 적절한 옵션을 전달하여 다른 필드를 사용할 수 있습니다. + + + 지정된 패키지를 제거 + + + 패키지 제거를 시작하는 중... + + + 성공적으로 제거됨 + + + winget 이 패키지에 대한 제거 명령을 찾을 수 없습니다. 지원을 위해 패키지 게시자에게 문의하세요. + {Locked="winget"} + + + 제거가 중단됨 + + + 설치 종료 코드로 인해 제거하지 못함: + + + 설치된 패키지 목록 내보내기 + + + 파일에 나열된 모든 패키지를 설치합니다. + + + 파일에 있는 모든 패키지를 설치합니다. + + + 결과를 기록할 파일 + + + 설치할 패키지를 설명하는 파일 + + + 지정한 원본에서 패키지 내보내기 + + + 설치된 패키지 목록을 파일에 기록합니다. 그런 다음 패키지를 import 명령과 함께 설치할 수 있습니다. + {Locked="import"} + + + 가져온 패키지를 하나 이상 설치하지 못했습니다. + + + 가져올 패키지를 찾을 수 없습니다. + + + 가져오기에 필요한 원본이 설치되지 않았습니다. + + + 어떤 원본에서도 설치된 패키지를 사용할 수 없습니다. + + + 어떤 원본에서도 설치된 패키지 버전을 사용할 수 없습니다. + + + 가져오기 파일에 패키지가 없습니다. + + + JSON 파일이 잘못되었습니다. + + + 패키지가 이미 설치되어 있습니다. + + + 사용할 수 없는 패키지 무시 + + + 생성된 파일에 패키지 버전 포함 + + + 가져오기 파일에서 패키지 버전 무시 + + + 경로가 존재하지 않습니다. + \ No newline at end of file diff --git a/Localization/Resources/lo-LA/winget.resw b/Localization/Resources/lo-LA/winget.resw index e6a126483c..41d4c20fad 100644 --- a/Localization/Resources/lo-LA/winget.resw +++ b/Localization/Resources/lo-LA/winget.resw @@ -389,7 +389,7 @@ ບໍ່ມີຕົວຕິດຕັ້ງທີ່ສາມາດໃຊ້ໄດ້ກັບລະບົບປັດຈຸບັນ. - ບໍ່ມີຄຸນສົມບັດແບບທົດລອງ. + ດຽວນີ້ບໍ່ມີຄຸນສົມບັດໃຊ້ງານທົດລອງທີ່ມີຢູ່. ບໍ່ພົບແພັກເກດຕິດຕັ້ງແລ້ວທີ່ກົງກັບເກນເງື່ອນໄຂການປ້ອນຂໍ້ມູນ. @@ -476,6 +476,9 @@ ຊ່ອງ + + ສະແດງຂໍ້ມູນກ່ຽວກັບແພັກເກດສະເພາະ. ຕາມຄ່າເລີ່ມຕົ້ນ, ຄຳຖາມຕ້ອງກົງກັບໄອດີ, ຊື່ ຫຼື ຊື່ຫຼິ້ນຂອງແພັກເກດຕາມຕົວອັກສອນນ້ອຍໃຫຍ່. ສາມາດໃຊ້ຊ່ອງຂໍ້ມູນອື່ນໂດຍການຜ່ານຕົວເລືອກທີ່ເໝາະສົມຂອງພວກມັນ. + ສະແດງຂໍ້ມູນກ່ຽວກັບແພັກເກດ @@ -719,4 +722,96 @@ ບັນທຶກ Diagnostic files containing information about application use. + + ຕົວຕິດຕັ້ງຖືກປິດກັ້ນໂດຍນະໂຍບາຍ + + + ຕົວຕິດຕັ້ງບໍ່ຜ່ານການກວດກາຄວາມປອດໄພ + + + ຜະລິດຕະພັນປ້ອງກັນໄວຣັສລາຍງານການຕິດໄວຣັສໃນຕົວຕິດຕັ້ງ + + + ລົ້ມເຫລວໃນການພະຍາຍາມອັບເດດແຫຼ່ງຂໍ້ມູນ: + + + ຖອນການຕິດຕັ້ງແພັກເກດທີ່ເລືອກ, ບໍ່ວ່າຈະພົບໂດຍການຊອກຫາລາຍການແພັກເກດທີ່ຕິດຕັ້ງແລ້ວ ຫຼື ໂດຍກົງຈາກລາຍການສະແດງ. ຕາມຄ່າເລີ່ມຕົ້ນ, ຄຳຖາມຕ້ອງກົງກັບໄອດີ, ຊື່ ຫຼື ຊື່ຫຼິ້ນຂອງແພັກເກດຕາມຕົວອັກສອນນ້ອຍໃຫຍ່. ສາມາດໃຊ້ຊ່ອງຂໍ້ມູນອື່ນໂດຍການຜ່ານຕົວເລືອກທີ່ເໝາະສົມຂອງພວກມັນ. + + + ຖອນການຕິດຕັ້ງແພັກເກດທີ່ໃຫ້ມາ + + + ກຳລັງເລີ່ມການຖອນຕິດຕັ້ງແພັກເກດ... + + + ຖອນການຕິດຕັ້ງສຳເລັດແລ້ວ + + + winget ບໍ່ສາມາດຊອກຫາຄຳສັ່ງທີ່ຖອນການຕິດຕັ້ງສຳລັບແພັກເກດນີ້. ກະລຸນາຕິດຕໍ່ຫາຜູ້​ເຜີຍ​ແຜ່ແພັກເກດສຳລັບການສະໜັບສະໜູນ. + {Locked="winget"} + + + ຍົກເລີກການຖອນການຕິດຕັ້ງ + + + ການຖອນການຕິດຕັ້ງລົ້ມເຫລວດ້ວຍລະຫັດອອກ: + + + ສົ່ງອອກລາຍການແພັກແກັດທີ່ຕິດຕັ້ງແລ້ວ + + + ຕິດຕັ້ງແພັກແກັດທັງໝົດທີ່ສະແດງຢູ່ໃນໄຟລ໌. + + + ຕິດຕັ້ງແພັກແກັດທັງໝົດໃນໄຟລ໌ + + + ໄຟລ໌ທີ່ຈະຂຽນຜົນ​ໄດ້​ຮັບ + + + ໄຟລ໌ອະທິບາຍແພັກແກັດທີ່ຕ້ອງຕິດຕັ້ງ + + + ສົ່ງອອກແພັກແກັດຈາກແຫຼ່ງທີ່ລະບຸ + + + ຂຽນລາຍການຂອງແພັກເກດທີ່ຕິດຕັ້ງໃສ່ໃນໄຟລ໌. ຫຼັງຈາກນັ້ນສາມາດຕິດຕັ້ງແພັກແກັດດ້ວຍຄຳສັ່ງນຳ import. + {Locked="import"} + + + ບໍ່ສາມາດຕິດຕັ້ງແພັກແກັດທີ່ນຳເຂົ້າຢ່າງໜ້ອຍໜຶ່ງ ຫຼື ຫຼາຍແພັກແກັດ + + + ບໍ່ພົບແພັກແກັດສຳລັບການນຳເຂົ້າ: + + + ບໍ່ໄດ້ຕິດຕັ້ງແຫຼ່ງຂໍ້ມູນທີ່ຕ້ອງການສຳລັບການນຳເຂົ້າ: + + + ແພັກເກດທີ່ຕິດຕັ້ງບໍ່ສາມາດໃຊ້ໄດ້ຈາກແຫຼ່ງຂໍ້ມູນໃດໆ: + + + ເວີຊັນທີ່ຕິດຕັ້ງແລ້ວຂອງແພັກແກັດບໍ່ສາມາດໃຊ້ໄດ້ຈາກແຫຼ່ງຂໍ້ມູນໃດໆ: + + + ບໍ່ພົບແພັກເກດໃນການນຳເຂົ້າໄຟລ໌ + + + ໄຟລ໌ JSON ແມ່ນບໍ່ຖືກຕ້ອງ + + + ແພັກເກດທີ່ຖຶກຕິດຕັ້ງແລ້ວ: + + + ເພີກເສີຍແພັກແກັດທີ່ບໍ່ສາມາດໃຊ້ໄດ້ + + + ຮວມເອົາເວີຊັນແພັກເກດໃນໄຟລ໌ທີ່ຜະລິດ + + + ເພີກເສີຍເວີຊັນແພັກແກັດຈາກໄຟລ໌ນຳເຂົ້າ + + + ບໍ່ມີເສັ້ນທາງຢູ່: + \ No newline at end of file diff --git a/Localization/Resources/lt-LT/winget.resw b/Localization/Resources/lt-LT/winget.resw index 344a8bf308..b8e18ddbd4 100644 --- a/Localization/Resources/lt-LT/winget.resw +++ b/Localization/Resources/lt-LT/winget.resw @@ -389,7 +389,7 @@ Jas galima sukonfigūruoti naudojant parametrų failą „winget settings“.Nėra diegimo programų, kurios taikomos esamai sistemai. - Šiuo metu pasiekiamų eksperimentinių funkcijų nėra. + Šiuo metu pasiekiamų eksperimentinių funkcijų nėra. Įvesties kriterijus atitinkančių įdiegtų paketų nerasta. @@ -476,6 +476,9 @@ Jas galima sukonfigūruoti naudojant parametrų failą „winget settings“. Kanalas + + Rodoma informacija apie konkretų paketą. Pagal numatytuosius nustatymus užklausa turi insensitively atitikti ID, pavadinimą arba iškvietimo komponentas paketą. Kiti laukai gali būti naudojami perduodant savo atitinkamą parinktį. + Rodoma informacija apie paketą @@ -650,7 +653,7 @@ Jas galima sukonfigūruoti naudojant parametrų failą „winget settings“.Neatpažinta komanda - Atnaujinkite visus įdiegtus paketus į naujausius, jei yra + Atnaujinkite visus įdiegtus paketus į naujausius, jei yra pasiekiamų Taikomų naujinimų nerasta. @@ -719,4 +722,96 @@ Jas galima sukonfigūruoti naudojant parametrų failą „winget settings“.Žurnalai Diagnostic files containing information about application use. + + Diegimo programą užblokavo strategija + + + Diegimo programos saugos patikrinimas nepavyko + + + Antivirusinis produktas praneša apie diegimo programos užkrėtimą + + + Nepavyko atnaujinti šaltinio: + + + Pašalina pasirinktą paketą, rasti įdiegtus paketų sąrašą arba tiesiogiai iš deklaracijos. Pagal numatytuosius nustatymus užklausa turi insensitively atitikti ID, pavadinimą arba iškvietimo komponentas paketą. Kiti laukai gali būti naudojami perduodant savo atitinkamą parinktį. + + + Pašalina nurodytą paketą + + + Pradedamas paketo pašalinimas... + + + Sėkmingai pašalinta + + + „winget“ nepavyksta rasti šio paketo šalinimo komandos. Kreipkitės į paketo leidėją, kad gautumėte pagalbos. + {Locked="winget"} + + + Šalinimas nutrauktas + + + Pašalinti nepavyko, išėjimo kodas: + + + Eksportuojamas įdiegtų paketų sąrašas + + + Įdiegia visus faile esančius paketus. + + + Įdiegia visus faile esančius paketus + + + Failas, kuriame bus įrašytas rezultatas + + + Failas, kuriame aprašomi įdiegiami paketai + + + Eksportuoti paketus iš nurodyto šaltinio + + + Į failą įrašo įdiegtų paketų sąrašą. Paketai gali būti įdiegti su importavimo komanda. + {Locked="import"} + + + Nepavyko įdiegti vieno ar kelių importuotus paketų + + + Paketas nerastas importuoti: + + + Importuoti reikalingas šaltinis nėra įdiegtas: + + + Įdiegtas paketas nepasiekiamas iš bet kurio šaltinio: + + + Įdiegta paketo versija nepasiekiama iš bet kurio šaltinio: + + + Importavimo faile nerasta paketų + + + „JSON“ failas neleistinas + + + Paketas jau įdiegtas: + + + Nepaisyti nepasiekiamų paketų + + + Įtraukti paketo versijas į sukurtą failą + + + Nepaisyti paketo versijų importavimo faile + + + Kelio nėra: + \ No newline at end of file diff --git a/Localization/Resources/lv-LV/winget.resw b/Localization/Resources/lv-LV/winget.resw index 0e84523746..bbc2f27af7 100644 --- a/Localization/Resources/lv-LV/winget.resw +++ b/Localization/Resources/lv-LV/winget.resw @@ -476,6 +476,9 @@ Tos var konfigurēt, izmantojot iestatījumu failu 'winget settings'. Kanāls + + Parāda informāciju noteiktā pakotnē. Pēc noklusējuma vaicājumā ir jābūt insensitively, kas atbilst pakotnes ID, nosaukumam vai Pavārds neaptver. Citus laukus var izmantot, nodot to atbilstošu opciju. + Rāda informāciju par pakotni @@ -719,4 +722,96 @@ Tos var konfigurēt, izmantojot iestatījumu failu 'winget settings'. Žurnāli Diagnostic files containing information about application use. + + Politika ir bloķējusi instalētāju + + + Instalētāja drošības pārbaude neizdevās + + + Pretvīrusu produkts ziņo par infekciju instalētājā + + + Neizdevās mēģinājums atjaunināt avotu: + + + Atinstalē atlasīto pakotni, konstatējām, meklējot instalētos pakotņu sarakstu vai tieši no manifesta. Pēc noklusējuma vaicājumā ir jābūt insensitively, kas atbilst pakotnes ID, nosaukumam vai Pavārds neaptver. Citus laukus var izmantot, nodot to atbilstošu opciju. + + + Atinstalē norādīto pakotni + + + Sāk pakotnes atinstalēšanu... + + + Sekmīgi atinstalēts + + + winget nevar atrast šīs pakotnes atinstalēšanas komandu. Lūdzu, sazinieties ar pakotnes izdevēju, lai saņemtu atbalstu. + {Locked="winget"} + + + Atinstalēšana pārtraukta + + + Atinstalēšana neizdevās ar izejas kodu: + + + Eksportē instalēto pakotņu sarakstu + + + Instalē visas failā norādītās pakotnes. + + + Instalē visas pakotnes failā + + + Fails, kurā jāraksta rezultāts + + + Fails, kas apraksta instalējamās pakotnes + + + Eksportēt pakotnes no norādītā avota + + + Uzskaita instalēto pakotņu sarakstu failā. Pēc tam pakotnes var instalēt ar importēšanas komandu. + {Locked="import"} + + + Neizdevās instalēt vienu vai vairākas importētās pakotnes + + + Importējama pakotne nav atrasta: + + + Importēšanai nepieciešamais avots nav instalēts: + + + Instalētā pakotne nav pieejama ne no neviena avota: + + + Instalētā pakotnes versija nav pieejama ne no neviena avota: + + + Importēšanas failā nav atrasta neviena pakotne + + + JSON fails nav derīgs + + + Pakotne jau ir instalēta: + + + Ignorēt nepieejamas pakotnes + + + Iekļaut pakotņu versijas izveidotajā failā + + + Ignorēt pakotnes versijas no importēšanas faila + + + Ceļš nepastāv: + \ No newline at end of file diff --git a/Localization/Resources/mk-MK/winget.resw b/Localization/Resources/mk-MK/winget.resw index 72b1574c3c..3a27fc310f 100644 --- a/Localization/Resources/mk-MK/winget.resw +++ b/Localization/Resources/mk-MK/winget.resw @@ -389,7 +389,7 @@ Нема програми за инсталирање што се применливи на тековниот систем. - Во моментов не се достапни пробни функции. + Во моментов нема достапни пробни функции. Не е најден инсталиран пакет што се совпаѓа со внесените критериуми. @@ -476,6 +476,9 @@ Канал + + Прикажува информации за одреден пакет. Стандардно, прашалникот не мора да разликува големи и мали букви и мора да се совпаѓа со идентификаторот, името или моникерот на пакетот. Другите полиња може да се користат со пренесување на нивната соодветна опција. + Прикажува информации за пакетот @@ -650,7 +653,7 @@ Непрепознаена наредба - Ажурирајте ги сите инсталирани пакети со најновите верзии ако се достапни + Ажурирајте ги сите инсталирани пакети со најновите верзии доколку се достапни Не е најдено применливо ажурирање. @@ -719,4 +722,96 @@ Дневници Diagnostic files containing information about application use. + + Инсталаторот е блокиран од правилата + + + Инсталаторот не успеа да ја помине безбедносната проверка + + + Антивирусниот производ известува за инфекција во инсталаторот + + + Не успеа обидот за ажурирање на изворот: + + + Го деинсталира избраниот пакет, кој е најден или со пребарување на списокот со инсталирани пакети или директно од манифестот. Стандардно, прашалникот не мора да разликува големи и мали букви, но мора да се совпаѓа со идентитетот, името или прекарот на пакетот. Другите полиња може да се користат со пренесување на нивната соодветна опција. + + + Го деинсталира дадениот пакет + + + Се започнува деинсталацијата на пакетот... + + + Успешно се деинсталира + + + winget не може да ја лоцира наредбата за деинсталација за овој пакет. Стапете во контакт со издавачот на пакетот за поддршка. + {Locked="winget"} + + + Деинсталацијата е напуштена + + + Деинсталирањето не успеа со излезниот код: + + + Извезува список на инсталирани пакети + + + Ги инсталира сите пакети наведени во датотеката. + + + Ги инсталира сите пакети во датотеката + + + Датотека каде што ќе се запише резултатот + + + Датотека што ги опишува пакетите за инсталирање + + + Извези ги пакетите од одредениот извор + + + Пишува список на инсталираните пакети во датотека. Потоа пакетите може да се инсталираат со наредбата import. + {Locked="import"} + + + Еден или повеќе увезени пакети не успеаја да се инсталираат + + + Пакетот не е најден за увоз: + + + Не е инсталиран изворот потребен за увоз: + + + Инсталираниот пакет не е достапен од кој било извор: + + + Инсталираната верзија на пакетот не е достапна од кој било извор: + + + Не се најдени пакети во увезената датотека + + + JSON-датотеката не е важечка + + + Пакетот е веќе инсталиран: + + + Игнорирај ги недостапните пакети + + + Вклучи ги верзиите на пакетот во произведената датотека + + + Игнорирај ги верзиите на пакетот од увезената датотека + + + Патеката не постои: + \ No newline at end of file diff --git a/Localization/Resources/ml-IN/winget.resw b/Localization/Resources/ml-IN/winget.resw index 6807e16391..ac35618c17 100644 --- a/Localization/Resources/ml-IN/winget.resw +++ b/Localization/Resources/ml-IN/winget.resw @@ -389,7 +389,7 @@ നിലവിലുള്ള സിസ്റ്റത്തിന് ഇൻസ്റ്റാളറുകൾ ഉചിതമായത് ഒന്നും ലഭ്യമല്ല. - പരീക്ഷണ സവിശേഷതകളൊന്നും ഇപ്പോൾ നിലവിലില്ല. + പരീക്ഷണ സവിശേഷതകളൊന്നും ഇപ്പോൾ ലഭ്യമല്ല. ഇൻപുട്ട് മാനദണ്ഡങ്ങളുമായി പൊരുത്തപ്പെടുന്ന ഇൻസ്റ്റാൾ ചെയ്‌ത പാക്കേജുകളൊന്നും കണ്ടെത്തിയില്ല. @@ -476,6 +476,9 @@ ചാനൽ + + ഒരു നിർദ്ദിഷ്ട പാക്കേജിലെ വിവരങ്ങൾ കാണിക്കുന്നു. ഡിഫോൾട്ടായി, ചോദ്യ id, പേര് അല്ലെങ്കിൽ പാക്കേജിന്റെ മോണിക്കർ എന്നിവ കേസ്-നിർവ്വികാരമായി പൊരുത്തപ്പെട്ടിരിക്കണം. മറ്റ് ഫീൽഡുകൾക്ക് അവരുടെ ഉചിതമായ ഐച്ഛികം കൈമാറുന്നതിലൂടെ ഉപയോഗിക്കാവുന്നതാണ്. + ഒരു പാക്കേജിനെക്കുറിച്ചുള്ള വിവരങ്ങൾ കാണിക്കുന്നു @@ -719,4 +722,96 @@ ലോഗുകള്‍ Diagnostic files containing information about application use. + + നയം ഇൻസ്റ്റാളരെ തടഞ്ഞിരിക്കുന്നു + + + ഇന്‍സ്റ്റാളര്‍ സുരക്ഷാ പരിശോധനയിൽ പരാജയപ്പെട്ടു + + + ഒരു ആന്റി വൈറസ് ഉൽപ്പന്നം ഇൻസ്റ്റാളറിൽ അപകടസാധ്യത റിപ്പോർട്ടുചെയ്യുന്നു + + + ഉറവിടം കാലികമാക്കാൻ ശ്രമിക്കുമ്പോൾ പരാജയപ്പെട്ടു: + + + ഇൻസ്റ്റാൾചെയ്ത പാക്കേജുകളുടെ ലിസ്റ്റ് തിരയുന്നതിലൂടെ അല്ലെങ്കിൽ ഒരു മാനിഫെസ്റ്റിൽ നിന്ന് നേരിട്ട് കണ്ടെത്തുന്നത് വഴി തിരഞ്ഞെടുത്ത പാക്കേജ് അൺഇൻസ്റ്റാൾ ചെയ്യുന്നു. ഡിഫോൾട്ടായി, ചോദ്യം ചെറിയക്ഷര - വലിയക്ഷ വ്യത്യാസം ഇല്ലാതെ id, പേര് അല്ലെങ്കിൽ പാക്കേജിന്റെ മോണിക്കർ എന്നിവയുമായി പൊരുത്തപ്പെട്ടിരിക്കണം. മറ്റ് ഫീൽഡുകൾ ഉചിതമായ ഓപ്ഷൻ നിറവേറ്റുന്നതിലൂടെ ഉപയോഗിക്കാവുന്നതാണ്. + + + നിർദിഷ്ട പാക്കേജ് അൺഇൻസ്റ്റാൾ ചെയ്യുന്നു + + + പാക്കേജ് അൺഇൻസ്റ്റാൾ ആരംഭിക്കുന്നു... + + + വിജയകരമായി അൺഇൻസ്റ്റാൾ ചെയ്തു + + + winget-ന് ഈ പാക്കേജിന്റെ അൺഇൻസ്റ്റാൾ കമാൻഡ് കണ്ടെത്താനാകുന്നില്ല. ദയവായി പിന്തുണയ്ക്കായി പാക്കേജ് പ്രസാധകരേ സമീപിക്കുക. + {Locked="winget"} + + + അൺഇൻസ്റ്റാൾ ചെയ്യൽ ഉപേക്ഷിച്ചു + + + അൺഇൻസ്റ്റാൾ പരാജയപെട്ടു, ഈ പുറത്തുകടക്കൽ കോഡോടെ: + + + പാക്കേജുകളുടെ ഒരു ലിസ്റ്റ് എക്സ്‌പോർട്ട് ചെയ്യുന്നു + + + ഒരു ഫയലിൽ ലിസ്റ്റുചെയ്തിരിക്കുന്ന എല്ലാ പാക്കേജുകളും ഇൻസ്റ്റാൾ ചെയ്യുന്നു. + + + എല്ലാ പാക്കേജുകളും ഒരു ഫയലിൽ ഇൻസ്റ്റാൾ ചെയ്യുന്നു + + + ഫലം എഴുതേണ്ട ഫയൽ + + + ഇൻസ്റ്റാൾ ചെയ്യുന്നതിനുള്ള പാക്കേജുകൾ വിവരിക്കുന്ന ഫയൽ + + + നിർദ്ദിഷ്ട ഉറവിടത്തിൽ നിന്ന് പാക്കേജുകൾ എക്സ്പോർട്ട് ചെയ്യുക + + + ഇൻസ്റ്റാൾ ചെയ്ത പാക്കേജുകളുടെ ഒരു ലിസ്റ്റ് ഒരു ഫയലിൽ എഴുതുന്നു. import കമാൻഡ് ഉപയോഗിച്ച് പാക്കേജുകൾ ഇൻസ്റ്റാൾ ചെയ്യാൻ കഴിയും. + {Locked="import"} + + + ഒന്നോ അതിലധികമോ ഇംപോർട്ട് ചെയ്ത പാക്കേജുകൾ ഇൻസ്റ്റാൾ ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു + + + ഇംപോർട്ട് ചെയ്യുന്നതിനായി പാക്കേജ് കണ്ടെത്തിയില്ല: + + + ഇംപോർട്ട് ചെയ്യുന്നതിന് ആവശ്യമായ ഉറവിടം ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല: + + + ഇൻസ്റ്റാൾ ചെയ്ത പാക്കേജ് ഒരു ഉറവിടത്തിൽ നിന്നും ലഭ്യമാകില്ല: + + + പാക്കേജിന്റെ ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പ് ഒരു ഉറവിടത്തിൽ നിന്നും ലഭ്യമല്ല: + + + ഇംപോർട്ട് ചെയ്ത ഫയലിൽ പാക്കേജുകളൊന്നും കണ്ടെത്തിയില്ല + + + JSON ഫയൽ അസാധുവാണ് + + + പാക്കേജ് ഇതിനകം ഇൻസ്റ്റാൾ ചെയ്തിട്ടുണ്ട്: + + + ലഭ്യമല്ലാത്ത പാക്കേജുകൾ അവഗണിക്കുക + + + നിർമ്മിച്ച ഫയലിൽ പാക്കേജ് പതിപ്പുകൾ ഉൾപ്പെടുത്തുക + + + ഇംപോർട്ട് ചെയ്ത ഫയലിൽ നിന്ന് പാക്കേജ് പതിപ്പുകൾ അവഗണിക്കുക + + + പാത്ത് നിലവിലില്ല: + \ No newline at end of file diff --git a/Localization/Resources/ms-MY/winget.resw b/Localization/Resources/ms-MY/winget.resw index b4c9e17871..d72568c168 100644 --- a/Localization/Resources/ms-MY/winget.resw +++ b/Localization/Resources/ms-MY/winget.resw @@ -476,6 +476,9 @@ Ia boleh dikonfigurasikan menerusi fail seting 'winget settings'. Saluran + + Menunjukkan maklumat tentang pakej tertentu. Secara lalai, pertanyaan mesti sepadan dengan ID, nama atau moniker pakej. Medan lain boleh digunakan dengan memberikan pilihan yang sesuai. + Menunjukkan maklumat tentang pakej @@ -650,7 +653,7 @@ Ia boleh dikonfigurasikan menerusi fail seting 'winget settings'. Perintah tidak dikenali - Kemas kini semua pakej yang dipasang ke yang terkini jika tersedia + Kemas kini semua pakej yang dipasang kepada versi yang terkini jika tersedia Tiada kemas kini yang berkaitan ditemui. @@ -719,4 +722,96 @@ Ia boleh dikonfigurasikan menerusi fail seting 'winget settings'. Log Diagnostic files containing information about application use. + + Pemasang ini disekat oleh dasar + + + Pemasang gagal melepasi semakan keselamatan + + + Produk anti virus melaporkan jangkitan dalam pemasang + + + Gagal mengemas kini sumber: + + + Memadam pakej yang dipilih, sama ada ditemui dengan mencari di senarai pakej yang dipasang atau terus daripada manifes. Secara lalai, pertanyaan mesti menepati ID, nama atau moniker pakej tanpa sensitif huruf. Medan lain boleh digunakan dengan lulus pilihan yang sesuai. + + + Memasang pakej yang diberikan + + + Memulakan nyahpasang pakej... + + + Berjaya dinyahpasang + + + winget tidak dapat mencari perintah nyahpasang untuk pakej ini. Sila hubungi penerbit pakej untuk sokongan. + {Locked="winget"} + + + Nyahpasang dibatalkan + + + Nyahpasang gagal dengan kod keluar: + + + Eksport senarai pakej yang dipasang + + + Pasang semua pakej yang disenaraikan dalam fail. + + + Pasang semua pakej dalam fail. + + + Fail yang mana keputusan akan ditulis + + + Fail menerangkan pakej untuk dipasang + + + Pakej eksport daripada sumber yang ditentukan + + + Menulis senarai pakej yang dipasang kepada fail. Kemudian, pakej boleh dipasang dengan perintah import. + {Locked="import"} + + + Satu atau lebih banyak pakej yang diimport gagal dipasang + + + Pakej tidak ditemui untuk Import: + + + Sumber yang diperlukan untuk import tidak dipasang: + + + Versi pakej yang dipasang tidak tersedia daripada mana-mana sumber: + + + Versi pakej yang dipasang tidak tersedia daripada mana-mana sumber: + + + Tiada pakej ditemui dalam fail import + + + Fail JSON tidak sah + + + Pakej telah dipasang: + + + Abaikan pakej yang tidak tersedia + + + Sertakan versi pakej dalam fail yang dihasilkan + + + Abaikan versi pakej daripada fail import + + + Laluan tidak wujud: + \ No newline at end of file diff --git a/Localization/Resources/nb-NO/winget.resw b/Localization/Resources/nb-NO/winget.resw index 9cff7d3da4..9aef77ccd2 100644 --- a/Localization/Resources/nb-NO/winget.resw +++ b/Localization/Resources/nb-NO/winget.resw @@ -476,6 +476,9 @@ De kan konfigureres via innstillings filen 'winget settings'. Kanal + + Viser informasjon om en bestemt pakke. Spørringen må som standard skille mellom små og store bokstaver mellom ID, navn eller kalle navn for pakken. Andre felt kan brukes ved å sende riktig alternativ. + Viser informasjon om en pakke @@ -719,4 +722,96 @@ De kan konfigureres via innstillings filen 'winget settings'. Logger Diagnostic files containing information about application use. + + Installasjonsprogrammet er blokkert av policy + + + Installasjonsprogrammet passerte ikke sikkerhetskontrollen + + + Et antivirus-produkt rapporterer en infeksjon i installasjonsprogrammet + + + Feil under forsøk på å oppdatere kilden: + + + Avinstallerer den valgte pakken, enten funnet ved å søke i listen installerte pakker eller direkte fra et manifest. Spørringen må som standard skille mellom små og store bokstaver mellom ID, navn eller kalle navn for pakken. Andre felt kan brukes ved å sende riktig alternativ. + + + Avinstallerer angitt pakke + + + Starter avinstallasjon av pakke... + + + Avinstallasjon vellykket + + + winget ikke finne Avinstaller-kommandoen for denne pakken. Ta kontakt med Package-utgiveren for å få hjelp. + {Locked="winget"} + + + Avinstallasjonen er avbrutt + + + Avinstallasjon mislyktes med avslutningskode: + + + Eksporterer en liste over installerte pakker + + + Installerer alle pakkene som er oppført i en fil. + + + Installerer alle pakkene i en fil + + + Fil der resultatet skal skrives + + + Fil som beskriver pakkene som skal installeres + + + Eksporter pakker fra den angitte kilden + + + Skriver en liste over de installerte pakkene til en fil. Pakkene kan installeres med kommandoen import. + {Locked="import"} + + + Kan ikke installere en eller flere av de importerte pakkene + + + Finner ikke pakken for import: + + + Kilden som kreves for import er ikke installert: + + + Installert pakke er ikke tilgjengelig fra noen kilde: + + + Installert versjon av pakken er ikke tilgjengelig fra noen kilde: + + + Finner ingen pakker i importfilen + + + JSON-filen er ikke gyldig + + + Pakken er allerede installert: + + + Ignorer utilgjengelige pakker + + + Inkluder pakkeversjoner i produsert fil + + + Ignorerer pakkeversjoner fra importfil + + + Banen finnes ikke: + \ No newline at end of file diff --git a/Localization/Resources/nl-NL/winget.resw b/Localization/Resources/nl-NL/winget.resw index a8fb495116..333aaa9745 100644 --- a/Localization/Resources/nl-NL/winget.resw +++ b/Localization/Resources/nl-NL/winget.resw @@ -389,7 +389,7 @@ Ze kunnen worden geconfigureerd via het instellingenbestand ' winget settings '. Er zijn geen installatieprogramma's van toepassing op het huidige systeem. - Er zijn momenteel geen Exprimental-functies beschikbaar. + Er zijn momenteel geen experimentele functies beschikbaar. Er is geen geïnstalleerd pakket gevonden dat overeenkomt met de invoercriteria. @@ -476,6 +476,9 @@ Ze kunnen worden geconfigureerd via het instellingenbestand ' winget settings '. Kanaal + + Informatie over een specifiek pakket weergeven. Standaard moet de query hoofdlettergevoelig overeenkomen met de id of naam van het pakket. Andere velden kunnen worden gebruikt door de juiste optie door te geven. + Toont informatie over een pakket @@ -719,4 +722,96 @@ Ze kunnen worden geconfigureerd via het instellingenbestand ' winget settings '. Logboeken Diagnostic files containing information about application use. + + Het installatieprogramma is geblokkeerd op basis van beleid + + + De beveiligingscontrole van het installatieprogramma is mislukt + + + Een antivirusproduct meldt een besmetting in het installatieprogramma + + + Er is een fout opgetreden tijdens het bijwerken van de bron: + + + Installatie van het geselecteerde pakket ongedaan maken door in de lijst met geïnstalleerde pakketten te zoeken of rechtstreeks vanuit een manifest te zoeken. De query moet standaard niet-hoofdlettergevoelig zijn voor de id, naam of moniker van het pakket. Andere velden kunnen worden gebruikt door de juiste optie door te geven. + + + Installatie van het opgegeven pakket ongedaan maken + + + Installatie ongedaan maken van het pakket wordt gestart... + + + Installatie ongedaan gemaakt + + + winget kan de uninstall-opdracht voor dit pakket niet vinden. Neem contact op met de uitgever van het pakket voor ondersteuning. + {Locked="winget"} + + + Installatie ongedaan maken is afgebroken + + + Installatie ongedaan maken is mislukt met afsluitcode: + + + Exporteert een lijst met geïnstalleerde pakketten + + + Installeert alle pakketten die in een bestand worden vermeld. + + + Installeert alle pakketten in een bestand + + + Bestand waarin het resultaat moet worden geschreven + + + Bestand met een beschrijving van de pakketten die moeten worden geïnstalleerd + + + Pakketten exporteren uit de opgegeven bron + + + Schrijft een lijst met de geïnstalleerde pakketten naar een bestand. De pakketten kunnen vervolgens worden geïnstalleerd met de importopdracht. + {Locked="import"} + + + De installatie van een of meer geïmporteerde pakketten is mislukt + + + Pakket niet gevonden voor importeren: + + + De bron die nodig is voor het importeren is niet geïnstalleerd: + + + Geïnstalleerde pakket is vanuit geen enkele bron beschikbaar: + + + Geïnstalleerde versie van het pakket is vanuit geen enkele bron beschikbaar: + + + Er zijn geen pakketten gevonden in het importbestand + + + JSON-bestand is niet geldig + + + Pakket is al geïnstalleerd: + + + Niet-beschikbare pakketten negeren + + + Pakketversies opnemen in een geproduceerd bestand + + + Pakketversies negeren vanuit importbestand + + + Pad bestaat niet: + \ No newline at end of file diff --git a/Localization/Resources/pl-PL/winget.resw b/Localization/Resources/pl-PL/winget.resw index 48922972de..9cf15c2bb9 100644 --- a/Localization/Resources/pl-PL/winget.resw +++ b/Localization/Resources/pl-PL/winget.resw @@ -389,7 +389,7 @@ Można je skonfigurować za pomocą pliku ustawień "winget settings". Żadni instalatorzy nie mają zastosowania w bieżącym systemie. - Obecnie nie są dostępne żadne funkcje ekaperymentalne. + Obecnie nie są dostępne żadne funkcje eksperymentalne. Nie znaleziono żadnego zainstalowanego pakietu pasującego do wprowadzonych kryteriów. @@ -476,6 +476,9 @@ Można je skonfigurować za pomocą pliku ustawień "winget settings". Kanał + + Pokaż informacje o określonym pakiecie. Domyślnie kwerenda musi być zgodna z wielkością liter, nazwą lub monikerem pakietu. Inne pola mogą być używane przez przekazanie odpowiedniej opcji. + Wyświetla informacje o pakiecie @@ -719,4 +722,96 @@ Można je skonfigurować za pomocą pliku ustawień "winget settings". Dzienniki Diagnostic files containing information about application use. + + Instalator jest blokowany przez zasady + + + Sprawdzanie zabezpieczeń w instalatorze nie powiodło się + + + Oprogramowanie antywirusowe zgłasza infekcję w instalatorze + + + Próba zaktualizowania źródła nie powiodła się: + + + Odinstalowuje wybrany pakiet, który został znaleziony przez przeszukiwanie listy zainstalowanych pakietów lub bezpośrednio z manifestu. Domyślnie kwerenda musi być zgodna z wielkością liter, nazwą lub monikerem pakietu. Inne pola mogą być używane przez przekazanie odpowiedniej opcji. + + + Odinstalowuje dany pakiet + + + Rozpoczynanie dezinstalacji pakietu + + + Pomyślnie odinstalowano + + + winget nie może zlokalizować polecenia Uninstall dla tego pakietu. Skontaktuj się z wydawcą pakietu, aby uzyskać pomoc techniczną. + {Locked="winget"} + + + Dezinstalacja została porzucona + + + Dezinstalacja nie powiodła się. Kod zakończenia: + + + Eksportuje listę zainstalowanych pakietów + + + Instaluje wszystkie pakiety wymienione w pliku. + + + Instaluje wszystkie pakiety w pliku + + + Plik, w którym ma zostać zapisany wynik + + + Plik opisujący pakiety do zainstalowania + + + Eksportuj pakiety z określonego źródła + + + Zapisuje listę zainstalowanych pakietów w pliku. Pakiety można następnie zainstalować za pomocą polecenia import. + {Locked="import"} + + + Nie można zainstalować co najmniej jednego zaimportowanego pakietu + + + Nie znaleziono pakietu do importu: + + + Nie zainstalowano źródła wymaganego do importu: + + + Zainstalowany pakiet nie jest dostępny z żadnego źródła: + + + Zainstalowana wersja pakietu nie jest dostępna z żadnego źródła: + + + Nie znaleziono żadnych pakietów w pliku importu + + + Plik JSON jest nieprawidłowy + + + Pakiet jest już zainstalowany: + + + Ignoruj niedostępne pakiety + + + Uwzględnij wersje pakietu w wytworzonym pliku + + + Ignoruj wersje pakietu z pliku importu + + + Ścieżka nie istnieje: + \ No newline at end of file diff --git a/Localization/Resources/pt-BR/winget.resw b/Localization/Resources/pt-BR/winget.resw index da1266df6c..489fef9b8f 100644 --- a/Localization/Resources/pt-BR/winget.resw +++ b/Localization/Resources/pt-BR/winget.resw @@ -389,7 +389,7 @@ Eles podem ser configurados por meio do arquivo de configurações ' winget sett Nenhum instalador se aplica ao sistema atual. - No momento, não há recursos experimentais disponíveis. + Atualmente não há recursos experimentais disponíveis. Nenhum pacote instalado foi encontrado que corresponda aos critérios de entrada. @@ -476,6 +476,9 @@ Eles podem ser configurados por meio do arquivo de configurações ' winget sett Canal + + Mostra informações sobre um pacote específico. Por padrão, a consulta deve corresponder de forma insensível à ID, ao nome ou ao moniker do pacote. Outros campos podem ser usados passando a opção apropriada. + Mostra informações sobre um pacote @@ -719,4 +722,96 @@ Eles podem ser configurados por meio do arquivo de configurações ' winget sett Registros Diagnostic files containing information about application use. + + O instalador foi bloqueado pela política + + + O instalador falhou na verificação de segurança + + + Um produto antivírus relata uma infecção no instalador + + + Falha ao tentar atualizar a fonte: + + + Desinstala o pacote selecionado, encontrado pesquisando a lista de pacotes instalados ou diretamente de um manifesto. Por padrão, a consulta deve corresponder de forma insensível à ID, ao nome ou ao moniker do pacote. Outros campos podem ser usados passando a opção apropriada. + + + Instala um determinado pacote + + + Iniciando a desinstalação do pacote... + + + Desinstalado com êxito + + + winget não é possível localizar o comando Uninstall deste pacote. Entre no fornecedor do pacote para obter suporte. + {Locked="winget"} + + + Desinstalação abandonada + + + Falha na desinstalação com o código de saída: + + + Exporta uma lista dos pacotes instalados + + + Instala todos os pacotes listados em um arquivo. + + + Instala todos os pacotes em um arquivo + + + Arquivo em que o resultado deve ser gravado + + + Arquivo descrevendo os pacotes que serão instalados + + + Exportar pacotes a partir da fonte especificada + + + Grava uma lista de pacotes instalados em um arquivo. Os pacotes podem ser instalados com o comando import. + {Locked="import"} + + + Um ou mais pacotes importados não foram instalados + + + Pacote não localizado para importação: + + + A fonte necessária para importação não está instalada: + + + Pacote instalado não disponível em nenhuma fonte: + + + Versão instalada do pacote não disponível em nenhuma fonte: + + + Não foram encontrados pacotes no arquivo de importação + + + Arquivo JSON não é válido + + + O pacote já está instalado: + + + Ignorar os pacotes indisponíveis + + + Incluir as versões do pacote no arquivo produzido + + + Ignorar as versões de pacote do arquivo de importação + + + O caminho não existe: + \ No newline at end of file diff --git a/Localization/Resources/pt-PT/winget.resw b/Localization/Resources/pt-PT/winget.resw index ae9b9e2fb4..5972045a35 100644 --- a/Localization/Resources/pt-PT/winget.resw +++ b/Localization/Resources/pt-PT/winget.resw @@ -389,7 +389,7 @@ Podem ser configurados através do ficheiro de definições ' winget settings '. Não existem instaladores aplicáveis ao sistema atual. - Não existem actualmente características exprimentares disponíveis. + Atualmente, não existem funcionalidades experimentais disponíveis. Não foi encontrado nenhum pacote instalado que corresponda aos critérios de entrada. @@ -476,6 +476,9 @@ Podem ser configurados através do ficheiro de definições ' winget settings '. Canal + + Mostra informação sobre um pacote específico. Por predefinição, a consulta tem de corresponder, sem distinção de maiúsculas e minúsculas, ao id, nome, ou caminho de ligação do pacote. Outros campos podem ser utilizados, passando a sua opção apropriada. + Mostra informações acerca de um pacote @@ -719,4 +722,96 @@ Podem ser configurados através do ficheiro de definições ' winget settings '. Registos Diagnostic files containing information about application use. + + O instalador está bloqueado por uma política + + + A verificação de segurança do instalador falhou + + + Um produto antivírus relata um infeção no instalador + + + Ocorreu uma falha ao tentar atualizar a origem: + + + Desinstala o pacote selecionado, quer seja encontrado através de pesquisa na lista de pacotes instalados ou diretamente a partir de um manifesto. Por defeito, a consulta tem de corresponder, sem distinção de maiúsculas e minúsculas, ao ID, nome, ou caminho da ligação do pacote. Outros campos podem ser utilizados, passando a opção apropriada. + + + Desinstala o pacote fornecido + + + A iniciar a desinstalação do pacote… + + + Desinstalado com êxito + + + Não winget possível localizar o comando desinstalar para este pacote. Contacte o fabricante do pacote para obter suporte. + {Locked="winget"} + + + Desinstalação abandonada + + + Ocorreu uma falha na desinstalação com o código de saída: + + + Exporta uma lista dos pacotes instalados + + + Instala todos os pacotes listados num ficheiro. + + + Instala todos os pacotes num ficheiro + + + Ficheiro onde o resultado deve ser escrito + + + Ficheiro que descreve os pacotes a instalar + + + Pacotes de exportação a partir da fonte especificada + + + Escreve uma lista dos pacotes instalados para um ficheiro. Os pacotes podem então ser instalados com o comando de importação. + {Locked="import"} + + + Falha ao instalar um ou mais pacotes importados + + + Embalagem não encontrada para importação: + + + A fonte necessária para a importação não está instalada: + + + O pacote instalado não está disponível a partir de qualquer fonte: + + + A versão instalada do pacote não está disponível a partir de qualquer fonte: + + + Nenhum pacote encontrado no ficheiro de importação + + + O ficheiro JSON não é válido + + + O pacote já está instalado: + + + Ignorar pacotes indisponíveis + + + Incluir versões de pacotes em ficheiro produzido + + + Ignorar as versões dos pacotes do ficheiro de importação + + + O caminho não existe: + \ No newline at end of file diff --git a/Localization/Resources/ro-RO/winget.resw b/Localization/Resources/ro-RO/winget.resw index 24f68b5606..0e9bec2e48 100644 --- a/Localization/Resources/ro-RO/winget.resw +++ b/Localization/Resources/ro-RO/winget.resw @@ -476,6 +476,9 @@ Ele pot fi configurate prin fișierul de setări „winget settings”. Canal + + Afișează informații despre un anumit pachet. În mod implicit, interogarea trebuie să insensitively cu ID-ul, numele sau moniker pachetului. Alte câmpuri pot fi utilizate prin transmiterea opțiunii corespunzătoare. + Afișează informații despre un pachet @@ -650,7 +653,7 @@ Ele pot fi configurate prin fișierul de setări „winget settings”. Comandă nerecunoscută - Se actualizează toate pachetele instalate la cea mai recentă dacă sunt disponibile + Se actualizează toate pachetele instalate la cea mai recentă versiune, dacă sunt disponibile Nu s-au găsit actualizări aplicabile. @@ -719,4 +722,96 @@ Ele pot fi configurate prin fișierul de setări „winget settings”. Jurnale Diagnostic files containing information about application use. + + Programul de instalare este blocat de politică + + + Programul de instalare nu a trecut de verificarea de securitate + + + Un produs anti-virus raportează o infectare în programul de instalare + + + Încercarea de actualizare a sursei nu a reușit: + + + Dezinstalează pachetul selectat, fie s-a găsit prin căutarea în lista de pachete instalate sau direct dintr-un manifest. În mod implicit, interogarea trebuie să insensitively cu ID-ul, numele sau moniker pachetului. Alte câmpuri pot fi utilizate prin transmiterea opțiunii corespunzătoare. + + + Dezinstalează pachetul dat + + + Se începe dezinstalarea pachetului... + + + Dezinstalare reușită + + + winget nu poate găsi comanda de dezinstalare pentru acest pachet. Luați legătura cu editorul pachetului pentru asistență. + {Locked="winget"} + + + Dezinstalare abandonată + + + Dezinstalarea nu a reușit, cu codul de ieșire: + + + Exportă o listă cu pachetele instalate + + + Instalează toate pachetele listate într-un fișier. + + + Instalează toate pachetele dintr-un fișier + + + Fișier unde se va scrie rezultatul + + + Fișierul care descrie pachetele de instalat + + + Export pachete din sursa specificată + + + Scrie o listă a pachetelor instalate într-un fișier. Pachetele pot fi instalate apoi cu comanda import. + {Locked="import"} + + + Unul sau mai multe pachete importate nu s-au instalat + + + Pachetul nu a fost găsit pentru import: + + + Sursa necesară pentru import nu este instalată: + + + Versiunea de pachet nu este disponibilă din nicio sursă: + + + Versiunea de pachet instalată nu este disponibilă din nicio sursă: + + + Nu s-a găsit niciun pachet în fișierul de import + + + Fișierul JSON nu este valid + + + Pachetul este deja instalat: + + + Ignorare pachete indisponibile + + + Se includ versiunile de pachet în fișierul generat + + + Ignorare versiuni pachet din fișierul de import + + + Calea nu există: + \ No newline at end of file diff --git a/Localization/Resources/ru-RU/winget.resw b/Localization/Resources/ru-RU/winget.resw index 0ec2a70aa8..63ab29edb4 100644 --- a/Localization/Resources/ru-RU/winget.resw +++ b/Localization/Resources/ru-RU/winget.resw @@ -476,6 +476,9 @@ Канал + + Отображает сведения о конкретном пакете. По умолчанию запрос должен сравнить идентификатор, имя или моникер пакета (без учета регистра). Можно использовать и другие поля, указав соответствующий параметр. + Отображение сведений о пакете @@ -719,4 +722,96 @@ Журналы Diagnostic files containing information about application use. + + Установщик заблокирован политикой + + + Установщик не прошел проверку безопасности + + + Антивирусная программа сообщает о заражении в установщике + + + Не удалось обновить источник: + + + Удаление выбранного пакета, обнаруженного путем поиска в списке установленных пакетов либо непосредственно из манифеста. По умолчанию запрос должен соответствовать идентификатору, имени или моникеру пакета (без учета регистра). Можно использовать и другие поля, указав соответствующий параметр. + + + Удаление указанного пакета + + + Запуск удаления пакета... + + + Удалено + + + winget не может найти команду удаления для пакета. Обратитесь за помощью к издателю пакетов. + {Locked="winget"} + + + Удаление прекращено + + + Сбой удаления с кодом выхода: + + + Экспортирует список установленных пакетов + + + Установка всех пакетов, перечисленных в файле. + + + Устанавливает все пакеты в файле + + + Файл, в который будет записан результат + + + Файл с описанием устанавливаемых пакетов + + + Экспорт пакетов из указанного источника + + + Записывает список установленных пакетов в файл. После этого пакеты можно установить командой import. + {Locked="import"} + + + Не удалось установить один или несколько импортированных пакетов + + + Не найден пакет для импорта: + + + Источник, необходимый для импорта, не установлен: + + + Установленный пакет недоступен из любого источника: + + + Установленная версия пакета недоступна из любого источника: + + + Пакеты в файле импорта не найдены + + + Файл JSON не является допустимым + + + Пакет уже установлен: + + + Игнорировать недоступные пакеты + + + Включить версии пакетов в созданный файл + + + Игнорировать версии пакета из файла импорта + + + Путь не существует: + \ No newline at end of file diff --git a/Localization/Resources/sk-SK/winget.resw b/Localization/Resources/sk-SK/winget.resw index ae36e33038..fe5ea86359 100644 --- a/Localization/Resources/sk-SK/winget.resw +++ b/Localization/Resources/sk-SK/winget.resw @@ -389,7 +389,7 @@ Môžu byť nakonfigurované prostredníctvom súboru „winget settings“.Na aktuálny systém nie sú použiteľné žiadne inštalačné súbory. - Momentálne nie sú k dispozícii žiadne exprimentálne funkcie. + Momentálne nie sú k dispozícii žiadne exprimentálne funkcie. Nenašiel sa žiaden nainštalovaný balík zodpovedajúci zadaným kritériám. @@ -476,6 +476,9 @@ Môžu byť nakonfigurované prostredníctvom súboru „winget settings“. Kanál + + Zobrazuje informácie o konkrétnych balíkoch. V predvolenom nastavení sa dotaz musí zhodovať (bez ohľadu na veľké a malé písmená) s ID, menom alebo monikerom balíka. Iné polia je možné použiť zadaním vhodnej možnosti. + Zobrazuje informácie o balíku @@ -719,4 +722,96 @@ Môžu byť nakonfigurované prostredníctvom súboru „winget settings“.Denníky Diagnostic files containing information about application use. + + Inštalačný program je zablokovaný politikou + + + Kontrola zabezpečenia inštalačného programu zlyhala + + + Antivírus ohlásil infekciu v inštalačnom programe + + + Nepodarilo sa vykonať aktualizáciu zdroja: + + + Odinštaluje vybratý balík, a to buď pomocou vyhľadávania v zozname nainštalovaných balíkov, alebo priamo z manifestu. Predvolene dotaz musí bez rozlišovania malých a veľkých písmen obsahovať ID, meno alebo moniker balíka. Iné polia je možné použiť zadaním vhodnej možnosti. + + + Odinštaluje daný balík + + + Spúšťa sa odinštalovanie balíka... + + + Úspešne odinštalované + + + Nástroj winget nemôže nájsť príkaz na odinštalovanie pre tento balík. Ak chcete získať podporu, obráťte sa na vydavateľa balíka. + {Locked="winget"} + + + Odinštalovanie bolo zrušené + + + Odinštalovanie zlyhalo s kódom ukončenia: + + + Exportuje zoznam nainštalovaných balíkov + + + Nainštaluje všetky balíky uvedené v súbore. + + + Nainštaluje všetky balíky v súbore. + + + Súbor, do ktorého sa výsledok zapíše + + + Súbor popisujúci balíky na inštaláciu + + + Exportovať balíky zo zadaného zdroja + + + Zapisuje zoznam nainštalovaných balíkov do súboru. Balíky sa môžu nainštalovať pomocou príkazu import. + {Locked="import"} + + + Inštalácia jedného alebo viacerých importovaných balíkov zlyhala + + + Na import sa nenašiel balík: + + + Zdroj vyžadovaný na import nie je nainštalovaný: + + + Nainštalovaný balík nie je k dispozícii zo žiadneho zdroja: + + + Nainštalovaná verzia balíka nie je k dispozícii zo žiadneho zdroja: + + + V súbore na import sa nenašli žiadne balíky + + + Súbor JSON je neplatný + + + Balík je už nainštalovaný: + + + Ignorovať nedostupné balíky + + + Zahrnúť verzie balíka do vytvoreného súboru + + + Ignorovať verzie balíka zo súboru na import + + + Cesta neexistuje: + \ No newline at end of file diff --git a/Localization/Resources/sl-SI/winget.resw b/Localization/Resources/sl-SI/winget.resw index 0caf109b30..66625f3298 100644 --- a/Localization/Resources/sl-SI/winget.resw +++ b/Localization/Resources/sl-SI/winget.resw @@ -476,6 +476,9 @@ jih je mogoče konfigurirati v datoteki z nastavitvami »winget settings«. Kanal + + Prikaže informacije o določenem paketu. Privzeto se mora poizvedba, ki ne razlikuje med velikimi in malimi črkami, ujemati z ID-jem, imenom ali vzdevkom paketa. Druga polja lahko uporabite z vnosom ustreznih možnosti. + Iskanje in prikaz osnovnih informacij paketov @@ -719,4 +722,96 @@ jih je mogoče konfigurirati v datoteki z nastavitvami »winget settings«.Dnevniki Diagnostic files containing information about application use. + + Pravilnik je blokiral namestitveni program + + + Varnostno preverjanje namestitvenega programa ni uspelo + + + Izdelek za zaščito pred virusi je zaznal okužbo v namestitvenem programu + + + Poskus posodobitve vira ni uspel: + + + Odstrani izbrani paket, ki ga najdete tako, da poiščete seznam nameščenih paketov ali neposredno iz manifesta. Poizvedba mora privzeto insensitively ujemanje z ID-jem, imenom ali vzdevek paketa. Druge polja lahko uporabite tako, da vnesete ustrezno možnost. + + + Odstrani dani paket + + + Začetek odstranjevanja paketa ... + + + Odstranjevanje je bilo uspešno + + + winget ne more najti ukaza za odstranitev za ta paket. Za podporo se obrnite na izdajatelja paketa. + {Locked="winget"} + + + Odstranjevanje je bilo opuščeno + + + Postopek odstranjevanja ni uspel s kodo izhoda: + + + Izvozi seznam nameščenih paketov + + + Namesti vse pakete, navedene v datoteki. + + + Namesti vse pakete, navedene v datoteki + + + Datoteka, v katero bo mogoče pisati rezultat + + + Datoteka opisuje pakete za namestitev + + + Izvozite pakete iz navedenega vira + + + Zapiše seznam nameščenih paketov v datoteko. Pakete lahko nato namestite z ukazom za import. + {Locked="import"} + + + Enega ali več uvoženih paketov ni bilo mogoče namestiti + + + Paketa za uvoz ni bilo mogoče najti: + + + Vir, potreben za uvoz, ni nameščen: + + + Nameščena različica paketa ni na voljo iz poljubnega vira: + + + Nameščena različica paketa ni na voljo iz poljubnega vira: + + + V datoteki za uvoz ni bil najden noben paket + + + Datoteka JSON ni veljavna + + + Paket je že nameščen: + + + Prezrite pakete, ki niso na voljo + + + Vključi različice paketa v ustvarjene datoteke + + + Prezrite različice paketa iz datoteke za uvoz + + + Pot ne obstaja: + \ No newline at end of file diff --git a/Localization/Resources/sq-AL/winget.resw b/Localization/Resources/sq-AL/winget.resw index 9b7b47c465..7f3cd81af9 100644 --- a/Localization/Resources/sq-AL/winget.resw +++ b/Localization/Resources/sq-AL/winget.resw @@ -389,7 +389,7 @@ Ato mund të konfigurohen përmes skedarit të parametrave ''winget settings''.< Asnjë instalues nuk është i aplikueshëm për sistemin aktual. - Nuk ka aktualisht asnjë tipar eksperimental në dispozicion. + Nuk ka aktualisht asnjë tipar eksperimental në dispozicion. Nuk u gjet asnjë paketë e instaluar që përputhet me kriterin e hyrjes. @@ -476,6 +476,9 @@ Ato mund të konfigurohen përmes skedarit të parametrave ''winget settings''.< Kanal + + Shfaq informacionin për një paketë specifike. Sipas parazgjedhjes, pyetja duhet të përputhet me ID-në edhe në madhësinë e shkronjave, emrin ose pseudonimin e paketës. Fushat e tjera mund të përdoren duke kaluar opsionin e tyre të përshtatshëm. + Shfaq informacione rreth një pakete @@ -719,4 +722,96 @@ Ato mund të konfigurohen përmes skedarit të parametrave ''winget settings''.< Evidencat Diagnostic files containing information about application use. + + Instaluesi është bllokuar nga politika + + + Instaluesi dështoi gjatë kontrollit të sigurisë + + + Një produkt antivirus raporton për një infeksion në instaluesin + + + Dështoi në përpjekjen për të përditësuar burimin: + + + Çinstalon paketën e përzgjedhur, që është gjetur qoftë duke kërkuar listën e paketave të instaluara ose drejtpërdrejt nga një manifest. Sipas parazgjedhjes, pyetja duhet të përputhet pa ndjeshmëri ndaj kapitaleve me ID-në, emrin ose pseudonimin e paketës. Fushat e tjera mund të përdoren duke kaluar opsionin e tyre përkatës. + + + Çinstalon paketën e dhënë + + + Duke nisur çinstalimin e paketës... + + + U çinstalua me sukses + + + winget nuk mund të gjejë komandën e çinstalimit për këtë paketë. Kontakto me publikuesin e paketës për mbështetje. + {Locked="winget"} + + + Çinstalimi u braktis + + + Çinstalimi dështoi me kodin e daljes: + + + Eksporton një listë të paketave të instaluara + + + Instalon të gjitha paketat e listuara në një skedar. + + + Instalon të gjitha paketat në një skedar + + + Skedari ku do të shkruhet rezultati + + + Skedari që përshkruan paketat për t’u instaluar + + + Eksporto paketat nga burimi i specifikuar + + + Shkruan një listë të paketave të instaluara në një skedar. Paketat më pas mund të instalohen me komandën e importimit. + {Locked="import"} + + + Instalimi i një ose më shumë paketave të importuara dështoi + + + Paketa nuk u gjet për importim: + + + Burimi që kërkohet për importim nuk është instaluar: + + + Paketa e instaluar nuk është në dispozicion nga asnjë burim: + + + Versioni i instaluar i paketës nuk është në dispozicion nga asnjë burim: + + + Nuk u gjet asnjë paketë në skedarin për importim + + + Skedari JSON nuk është i vlefshëm + + + Paketa është instaluar tashmë: + + + Injoro paketat që nuk janë në dispozicion + + + Përfshi versionet e paketës në skedarin e prodhuar + + + Injoro versionet e paketës nga skedari për importim + + + Shtegu nuk ekziston: + \ No newline at end of file diff --git a/Localization/Resources/sr-Latn-RS/winget.resw b/Localization/Resources/sr-Latn-RS/winget.resw index 8d651ded1f..23adebdefc 100644 --- a/Localization/Resources/sr-Latn-RS/winget.resw +++ b/Localization/Resources/sr-Latn-RS/winget.resw @@ -476,6 +476,9 @@ One se mogu konfigurisati putem „winget settings“ datoteke sa postavkama. Kanal + + Prikazuje informacije o određenom paketu. Veličina slova upita podrazumevano mora da se podudara sa ID-om, imenom ili monikerom paketa. Druga polja mogu da se koriste izborom odgovarajuće opcije. + Prikazuje informacije o paketu @@ -650,7 +653,7 @@ One se mogu konfigurisati putem „winget settings“ datoteke sa postavkama.Neprepoznata komanda - Ažuriraj sve instalirane pakete na najnoviji koji je dostupan + Ažuriraj sve instalirane pakete na najnovije ako su dostupni Nije pronađena nijedna primenljiva ispravka. @@ -719,4 +722,96 @@ One se mogu konfigurisati putem „winget settings“ datoteke sa postavkama.Datoteke evidencije Diagnostic files containing information about application use. + + Smernice blokiraju instalacioni program + + + Nije uspela provera bezbednosti instalacionog programa + + + Antivirusni proizvod prijavljuje zaraženost u instalacionom programu + + + Pokušaj ažuriranja izvora nije uspeo: + + + Deinstalira izabrani paket, koji se pronalazi ili pretraživanjem liste instaliranih paketa ili direktno iz manifesta. Veličina slova upita podrazumevano mora da se podudari sa ID-om, imenom ili monikerom paketa. Druga polja mogu da se koriste izborom odgovarajuće opcije. + + + Deinstalira dati paket + + + Pokretanje deinstalacije paketa... + + + Uspešno je deinstalirano + + + winget ne može da pronađe komandu za deinstalaciju za ovaj paket. Obratite se izdavaču paketa za podršku. + {Locked="winget"} + + + Deinstalacija je napuštena + + + Deinstalacija nije uspela uz izlazni kôd: + + + Izvozi listu instaliranih paketa + + + Instalira sve pakete navedene u datoteci. + + + Instalira sve pakete u datoteci + + + Datoteka u koju treba upisati rezultat + + + Datoteka koja opisuje pakete za instaliranje + + + Izvezi pakete iz navedenog izvora + + + Zapisuje listu instaliranih paketa u datoteku. Paketi se zatim mogu instalirati pomoću komande ‚‚import‘‘. + {Locked="import"} + + + Instalacija jednog ili više uvezenih paketa nije uspela + + + Paket nije pronađen za uvoz: + + + Izvor potreban za uvoz nije instaliran: + + + Instalirani paket nije dostupan ni iz jednog izvora: + + + Instalirana verzija paketa nije dostupna ni iz jednog izvora: + + + U datoteci za uvoz nije pronađen nijedan paket + + + JSON datoteka nije važeća + + + Paket je već instaliran: + + + Zanemari nedostupne pakete + + + Uključi verzije paketa u proizvedenu datoteku + + + Zanemari verzije paketa iz datoteke za uvoz + + + Putanja ne postoji: + \ No newline at end of file diff --git a/Localization/Resources/sv-SE/winget.resw b/Localization/Resources/sv-SE/winget.resw index d33d3df354..75f834e614 100644 --- a/Localization/Resources/sv-SE/winget.resw +++ b/Localization/Resources/sv-SE/winget.resw @@ -142,7 +142,7 @@ Nested commands that can be run in context of the selected command - Använd den angivna kanalen. standard är allmän målgrupp + Använd den angivna kanalen. Standard är allmän målgrupp kommando @@ -210,7 +210,7 @@ Följande experimentella funktioner är under utveckling. -De kan konfigureras via inställnings filen "winget settings". +De kan konfigureras via inställningsfilen "winget settings". {Locked="winget settings"} @@ -247,7 +247,7 @@ De kan konfigureras via inställnings filen "winget settings". Detta program är licensierat till dig av ägaren. - Microsoft är inte ansvariga för och tillhandahåller inte heller några licenser förnågra paket från tredje part. + Microsoft är inte ansvariga för och tillhandahåller inte heller några licenser för några paket från tredje part. Det här paketet tillhandahålls via Microsoft Store. winget kan behöva hämta paketet från Microsoft Store för den aktuella användarens räkning. @@ -257,7 +257,7 @@ De kan konfigureras via inställnings filen "winget settings". Det går inte att installera paketet eftersom en högre version av Windows krävs: - Installerar det markerade paketet, antingen genom att söka i en konfigurerad källa eller direkt från ett manifest. Som standard måste frågan matcha ID, namn eller länksökväg för paketet (inte skiftlägeskänsligt). Du kan använda andra fält genom att ange lämpliga alternativ. + Installerar det markerade paketet, antingen genom att söka i en konfigurerad källa eller direkt från ett manifest. Som standard måste sökningen matcha ID, namn eller länksökväg för paketet (inte skiftlägeskänsligt). Du kan använda andra fält genom att ange lämpliga alternativ. id, name, and moniker are all named values in our context, and may benefit from not being translated. @@ -476,6 +476,9 @@ De kan konfigureras via inställnings filen "winget settings". Kanal + + Visar information om ett specifikt paket. Som standard måste frågan matcha ID, namn eller länksökväg för paketet. Andra fält kan användas genom att välja lämpliga alternativ. + Visar information om ett paket @@ -682,7 +685,7 @@ De kan konfigureras via inställnings filen "winget settings". Kontrollera att indatafilen är en giltig, signerad MSIX. - Använd den angivna versionen. standard är den senaste versionen + Använd den angivna versionen. Standard är den senaste versionen Visa tillgängliga versioner av paketet @@ -719,4 +722,96 @@ De kan konfigureras via inställnings filen "winget settings". Loggar Diagnostic files containing information about application use. + + Installationsprogrammet blockeras av en princip + + + Säkerhetskontrollen för installationsprogrammet misslyckades + + + En antivirusprodukt rapporterar ett angrepp i installationsprogrammet + + + Det gick inte att uppdatera källan: + + + Avinstallerar det valda paketet, som antingen hittas genom att söka i listan över installerade paket eller direkt från ett manifest. Som standard måste frågan skiftlägesokänsligt matcha paketets id, namn eller länksökväg. Andra fält kan användas genom att lämna lämpligt alternativ. + + + Avinstallerar angivet paket + + + Startar avinstallationen av paketet... + + + Avinstallerad + + + winget kan inte hitta avinstallations kommandot för det här paketet. Kontakta paket utgivaren för support. + {Locked="winget"} + + + Avinstallation har avbrutits + + + Avinstallationen misslyckades med slutkod: + + + Exporterar en lista över installerade paket + + + Installerar alla paket som finns i en fil. + + + Installerar alla paket i en fil + + + Fil där resultatet ska skrivas + + + Fil som beskriver paketen som ska installeras + + + Exportera paket från den angivna källan + + + Skriver en lista över installerade paket till en fil. Paketen kan installeras med kommandot import. + {Locked="import"} + + + Det gick inte att installera ett eller flera importerade paket + + + Det var inte möjligt att hitta paketet för importering: + + + Den källa som krävs för import är inte installerad: + + + Det installerade paketet är inte tillgängligt från någon källa: + + + Den installerade versionen av paketet är inte tillgänglig från någon källa: + + + Inga paket hittades i importfilen + + + JSON-filen är ogiltig. + + + Packet är redan installerat: + + + Ignorera inaktiverade paket + + + Inkludera paketversioner i genererad fil + + + Ignorera paketversioner från importfilen + + + Sökvägen finns inte: + \ No newline at end of file diff --git a/Localization/Resources/ta-IN/winget.resw b/Localization/Resources/ta-IN/winget.resw index b96bb93061..10df47eab3 100644 --- a/Localization/Resources/ta-IN/winget.resw +++ b/Localization/Resources/ta-IN/winget.resw @@ -476,6 +476,9 @@ சேனல் + + ஒரு குறிப்பிட்ட தொகுப்பைப் பற்றிய தகவலை காண்பிக்கும். இயல்புநிலையில், ID, பெயர் அல்லது தொகுப்பின் மோனிக்கர் ஆகியவற்றுடன் வினவலின் எழுத்துமாறுபாடு உணர்திறன் அற்றது பொருந்த வேண்டும். மற்ற புலங்களை அவற்றின் பொருத்தமான விருப்பத்தைக் கடப்பதன் மூலம் பயன்படுத்தலாம். + தொகுப்பைப் பற்றிய தகவலைக் காட்டுகிறது @@ -720,4 +723,96 @@ பதிவுகள் Diagnostic files containing information about application use. + + கொள்கையால் நிறுவி தடுக்கப்பட்டது + + + நிறுவி பாதுகாப்புச் சரிபார்ப்பில் தோல்வியுற்றது + + + நிறுவியில் தொற்று உள்ளதாக ஒரு வைரஸ் எதிர்ப்புத் தயாரிப்பு அறிக்கையிடுகிறது + + + மூலத்தைப் புதுப்பிக்க முயலும் செயல்பாட்டில் தோல்வியடைந்தது: + + + நிறுவிய தொகுப்புகளின் பட்டியல் அல்லது வெளியிலிருந்து நேரடியாகத் தேடுவதன் மூலம் கண்டுபிடிக்கப்பட்ட, தேர்ந்தெடுத்த தொகுப்பை நிறுவல்நீக்குகிறது. இயல்புநிலையில், ID, பெயர் அல்லது தொகுப்பின் இணைப்புப் பாதை ஆகியவற்றுடன் வினவலின் எழுத்துமாறுபாடு உணர்திறன் அற்றதாகப் பொருந்த வேண்டும். மற்ற புலங்களை அவற்றின் பொருத்தமான விருப்பத்தைக் கடப்பதன் மூலம் பயன்படுத்தலாம். + + + வழங்கப்பட்ட தொகுப்பை நிறுவல்நீக்கவும் + + + தொகுப்பை நிறுவல்நீக்கத் தொடங்குகிறது... + + + வெற்றிகரமாக நிறுவல்நீக்கம் செய்யப்பட்டது + + + winget இந்தத் தொகுப்புக்கான நிறுவல்நீக்குக் கட்டளையை கண்டுபிடிக்க இயலவில்லை. ஆதரவு பெற தொகுப்பு வெளியீட்டாளரை தொடர்பு கொள்ளவும். + {Locked="winget"} + + + நிறுவல்நீக்கம் கைவிடப்பட்டது + + + அழி குறியீட்டுடன் நிறுவல்நீக்குதல் தோல்வியடைந்தது: + + + நிறுவப்பட்ட தொகுப்புகளின் பட்டியலை ஏற்றுமதிச் செய்கிறது + + + ஒரு கோப்பில் பட்டியலிடப்பட்டுள்ள அனைத்துத் தொகுப்புகளையும் நிறுவுகிறது. + + + கோப்பில் உள்ள அனைத்துத் தொகுப்புகளையும் நிறுவுகிறது + + + முடிவு எழுதப்பட வேண்டிய கோப்பு + + + நிறுவுவதற்கான தொகுப்புகளை விவரிக்கும் கோப்பு + + + குறிப்பிட்ட மூலத்திலிருந்து தொகுப்புகளை ஏற்றுமதிசெய் + + + நிறுவப்பட்ட தொகுப்புகளின் பட்டியலை ஒரு கோப்பிற்கு எழுதுகிறது. பிறகு தொகுப்புகளை import கட்டளையின் மூலம் நிறுவலாம். + {Locked="import"} + + + நிறுவ முடியாத ஒன்று அல்லது அதற்கு மேற்பட்ட இறக்குமதிச் செய்யப்பட்ட தொகுப்புகள் + + + இறக்குமதிக்குத் தொகுப்பு கிடைக்கவில்லை: + + + இறக்குமதிக்குத் தேவையான மூலம் நிறுவப்படவில்லை: + + + நிறுவப்பட்ட தொகுப்பு எந்த மூலத்திலிருந்தும் கிடைக்கப்பெறவில்லை: + + + தொகுப்பின் நிறுவப்பட்ட பதிப்பு எந்த மூலத்திலிருந்தும் கிடைக்கவில்லை: + + + இறக்குமதிக் கோப்பில் தொகுப்புகள் எதுவும் இல்லை + + + JSON கோப்பு செல்லுபடியாகாது + + + தொகுப்பு ஏற்கனவே நிறுவப்பட்டுள்ளது: + + + கிடைக்கப்பெறாத தொகுப்புகளைப் புறக்கணி + + + தயாரிக்கப்பட்ட கோப்பில் தொகுப்புப் பதிப்புகளைச் சேர் + + + இறக்குமதிக் கோப்பிலிருந்து தொகுப்புப் பதிப்புகளைப் புறக்கணி + + + இல்லாத பாதை: + \ No newline at end of file diff --git a/Localization/Resources/te-IN/winget.resw b/Localization/Resources/te-IN/winget.resw index 08a9d963f2..1545270dff 100644 --- a/Localization/Resources/te-IN/winget.resw +++ b/Localization/Resources/te-IN/winget.resw @@ -389,7 +389,7 @@ ప్రస్తుత వ్యవస్థకు ఇన్‌స్టాలర్లు వర్తించవు. - ప్రస్తుతం ఎటువంటి ప్రయోగాత్మక ఫీచర్‌లు అందుబాటులో లేవు. + ప్రస్తుతం ఎటువంటి ప్రయోగాత్మక ఫీచర్లు అందుబాటులో లేవు. ఇన్‌పుట్ ప్రమాణానికి సరిపోయే ఇన్‌స్టాల్ చేయబడిన ప్యాకేజీ కనుగొనబడలేదు. @@ -476,6 +476,9 @@ ఛానెల్ + + నిర్దిష్ట ప్యాకేజీపై సమాచారాన్ని చూపుతుంది. అప్రమేయంగా, ప్రశ్న తప్పనిసరిగా ప్యాకేజీ యొక్క ఐడీ, పేరు లేదా మోనికేర్‌తో సరిపోలాలి. ఇతర ఫీల్డ్‌లు వాటి తగిన ఎంపికను దాటడం ద్వారా ఉపయోగించవచ్చు. + ప్యాకేజి గురించి సమాచారాన్ని చూపుతుంది @@ -650,7 +653,7 @@ గుర్తించబడని ఆదేశం - వ్యవస్థాపించిన అన్ని ప్యాకేజీలు అందుబాటులో ఉంటే తాజా దానికి నవీకరించండి + అందుబాటులో ఉంటే వ్యవస్థాపించిన అన్ని ప్యాకేజీలను తాజా వాటికి నవీకరించండి వర్తించే నవీకరణ కనుగొనబడలేదు. @@ -719,4 +722,96 @@ లాగ్‌లు Diagnostic files containing information about application use. + + విధానం ద్వారా ఇన్‌స్టాలర్ బ్లాక్ చేయబడింది + + + ఇన్‌స్టాలర్ భద్రతా తనిఖీలో విఫలమైంది + + + యాంటీ-వైరస్ ఉత్పత్తి ఇన్‌స్టాలర్ సంక్రమణను నివేదిస్తుంది + + + మూలాన్ని నవీకరించే ప్రయత్నంలో విఫలమైంది: + + + ఎంచుకున్న ప్యాకేజీని అన్‌ఇన్‌స్టాల్ చేస్తుంది, వ్యవస్థాపించిన ప్యాకేజీల జాబితాను శోధించడం ద్వారా లేదా మానిఫెస్ట్ నుండి నేరుగా కనుగొనబడుతుంది. డిఫాల్ట్‌గా, ప్రశ్న తప్పనిసరిగా ప్యాకేజీ యొక్క ఐడీ, పేరు లేదా మోనికేర్‌తో సరిపోలాలి. ఇతర ఫీల్డ్‌లు వాటి తగిన ఎంపికను దాటడం ద్వారా ఉపయోగించవచ్చు. + + + ఇచ్చిన ప్యాకేజీని అన్‌ఇన్‌స్టాల్ చేస్తుంది + + + ప్యాకేజీని అన్‌ఇన్‌స్టాల్ చేయడం ప్రారంభిస్తోంది... + + + విజయవంతంగా అన్ఇన్‌స్టాల్ చేయబడింది + + + ఈ ప్యాకేజీ కోసం అన్‌ఇన్‌స్టాల్ ఆదేశాన్ని winget గుర్తించలేదు. మద్దతు కోసం దయచేసి ప్యాకేజీ ప్రచురణకర్తను సంప్రదించండి. + {Locked="winget"} + + + అన్‌ఇన్‌స్టాల్ చేయడం రద్దు చేయబడింది + + + నిష్క్రమణ కోడ్‌తో అన్‌ఇన్‌స్టాల్ చేయడం విఫలమైంది: + + + వ్యవస్థాపించిన ప్యాకేజీల జాబితాను ఎగుమతి చేస్తుంది + + + ఫైల్‌లో జాబితా చేయబడిన అన్ని ప్యాకేజీలను వ్యవస్థాపిస్తుంది. + + + ఫైల్‌లో అన్ని ప్యాకేజీలను వ్యవస్థాపిస్తుంది + + + ఫలితం రాయాల్సిన ఫైల్ + + + వ్యవస్థాపించాల్సిన ప్యాకేజీలను ఫైల్ వివరిస్తోంది + + + నిర్ధిష్ట మూలం నుంచి ప్యాకేజీలను ఎగుమతి చేయండి + + + వ్యవస్థాపించబడిన ప్యాకేజీల జాబితాను ఫైల్‌కు రాస్తుంది. తర్వాత import ఆదేశంతో ప్యాకేజీలు వ్యవస్థాపించబడవచ్చు. + {Locked="import"} + + + ఒకటి లేదా అంతకంటే ఎక్కువ దిగుమతి చేసిన ప్యాకేజీలు వ్యవస్థాపించడంలో విఫలమయ్యాయి + + + దిగుమతి చేసుకునేందుకు ప్యాకేజీ కనుగొనబడలేదు: + + + దిగుమతి కోసం అవసరమైన మూలం వ్యవస్థాపించబడలేదు: + + + వ్యవస్థాపించిన ప్యాకేజీ ఏ మూలం నుంచి అందుబాటులో లేదు: + + + ప్యాకేజీ యొక్క వ్యవస్థాపించిన వెర్షన్ ఏ మూలం నుంచి అందుబాటులో లేదు: + + + దిగుమతి ఫైల్‌లో ఏ ప్యాకేజీ కనుగొనబడలేదు + + + JSON ఫైల్ చెల్లదు + + + ప్యాకేజీ ఇప్పటికే వ్యవస్థాపించబడింది: + + + అందుబాటులో లేని ప్యాకేజీలను విస్మరించండి + + + ప్యాకేజీ వెర్షన్‌లను ఉత్పత్తి చేసిన ఫైల్‌లో చేర్చండి + + + దిగుమతి ఫైల్ నుంచి ప్యాకేజీ వెర్షన్‌లను విస్మరించండి + + + పథం ఉనికిలో లేదు: + \ No newline at end of file diff --git a/Localization/Resources/th-TH/winget.resw b/Localization/Resources/th-TH/winget.resw index c1be052952..e9786e15df 100644 --- a/Localization/Resources/th-TH/winget.resw +++ b/Localization/Resources/th-TH/winget.resw @@ -476,6 +476,9 @@ แชนเนล + + แสดงข้อมูลเกี่ยวกับแพคเกจเฉพาะ ตามค่าเริ่มต้น คิวรีต้องมีตัวอักษรใหญ่-เล็กตรงกับรหัส ชื่อ หรือเส้นทางการเชื่อมโยงของแพคเกจ เขตข้อมูลอื่นๆ สามารถใช้ได้โดยการส่งผ่านตัวเลือกที่เหมาะสม + แสดงข้อมูลเกี่ยวกับแพคเกจ @@ -719,4 +722,96 @@ รายการบันทึก Diagnostic files containing information about application use. + + ตัวติดตั้งถูกบล็อคโดยนโยบาย + + + ตัวติดตั้งล้มเหลวในการตรวจสอบความปลอดภัย + + + ผลิตภัณฑ์ป้องกันไวรัสรายงานการติดไวรัสในตัวติดตั้ง + + + ล้มเหลวในการพยายามอัปเดตแหล่งข้อมูล: + + + ถอนการติดตั้งแพคเกจที่เลือก ซึ่งจะพบได้ทั้งจากรายการแพคเกจที่ติดตั้งหรือจากรายการโดยตรง โดยคิวรีจะต้องมีตัวอักษรใหญ่เล็กที่ตรงกับ ID ชื่อ หรือเส้นทางการเชื่อมโยงของแพคเกจตามค่าเริ่มต้น สำหรับเขตข้อมูลอื่นสามารถใช้ได้ผ่านตัวเลือกที่เหมาะสม + + + ถอนการติดตั้งแพคเกจที่ระบุ + + + กำลังเริ่มการถอนการติดตั้งแพคเกจ... + + + ถอนการติดตั้งเรียบร้อยแล้ว + + + winget ไม่สามารถค้นหาคำสั่งถอนการติดตั้งสำหรับแพคเกจนี้ได้ โปรดติดต่อกับผู้เผยแพร่แพคเกจเพื่อรับการสนับสนุน + {Locked="winget"} + + + การถอนการติดตั้งถูกยกเลิก + + + การถอนการติดตั้งล้มเหลวด้วยโค้ดสั่งให้ออก: + + + ส่งออกรายการของแพคเกจที่ติดตั้งไว้ + + + ติดตั้งแพคเกจทั้งหมดที่แสดงในไฟล์ + + + ติดตั้งแพคเกจทั้งหมดในไฟล์ + + + ไฟล์ที่มีการเขียนผลลัพธ์ + + + ไฟล์ที่อธิบายแพคเกจที่จะติดตั้ง + + + ส่งออกแพคเกจจากแหล่งที่ระบุ + + + เขียนรายการของแพคเกจที่ติดตั้งไปยังไฟล์ แพคเกจสามารถติดตั้งได้โดยใช้คำสั่ง import + {Locked="import"} + + + แพคเกจที่นำเข้าอย่างน้อยหนึ่งรายการไม่สามารถติดตั้งได้ + + + ไม่พบแพคเกจสําหรับการนําเข้า: + + + ไม่ได้ติดตั้งแหล่งข้อมูลที่ต้องการสําหรับการนําเข้า: + + + แพคเกจที่ติดตั้งไว้ไม่พร้อมใช้งานจากแหล่งใดๆ: + + + เวอร์ชันที่ติดตั้งของแพคเกจไม่พร้อมใช้งานจากแหล่งใดๆ: + + + ไม่พบแพคเกจในไฟล์นำเข้า + + + ไฟล์ JSON ไม่ถูกต้อง + + + แพคเกจถูกติดตั้งไว้แล้ว: + + + ละเว้นแพคเกจที่ไม่พร้อมใช้งาน + + + รวมเวอร์ชันแพคเกจไว้ในไฟล์ที่สร้างแล้ว + + + ละเว้นเวอร์ชันแพคเกจจากไฟล์นําเข้า + + + ไม่มีเส้นทางนี้อยู่: + \ No newline at end of file diff --git a/Localization/Resources/tr-TR/winget.resw b/Localization/Resources/tr-TR/winget.resw index 88549a4f31..693909f688 100644 --- a/Localization/Resources/tr-TR/winget.resw +++ b/Localization/Resources/tr-TR/winget.resw @@ -476,6 +476,9 @@ Bunlar 'winget settings' ayarlar dosyası aracılığıyla yapılandırılabilir Kanal + + Belirli bir paketteki bilgileri gösterir. Varsayılan olarak, sorgunun büyük/küçük harf insensitively 'i paketin kimliği, adı veya adıyla eşleşmesi gerekir. Diğer alanlar, uygun seçenekleri geçirerek kullanılabilir. + Bir paketle ilgili bilgileri gösterir @@ -719,4 +722,96 @@ Bunlar 'winget settings' ayarlar dosyası aracılığıyla yapılandırılabilir Günlükler Diagnostic files containing information about application use. + + Yükleyici ilke tarafından engellendi + + + Yükleyici güvenlik kontrolünden geçemedi + + + Bir virüsten koruma ürünü yükleyicide bulaşma olduğunu bildiriyor + + + Kaynağı güncelleştirme denemesi başarısız oldu: + + + Yüklü paketler listesi aranarak bulunan veya doğrudan bir bildirimdeki seçili paketi kaldırır. Varsayılan olarak, sorgu, paketin kimliğiyle, adıyla veya bilinen adıyla büyük/küçük harfe duyarlı olmadan eşleşmelidir. Diğer alanlar uygun seçenekleri aktarılarak kullanılabilir. + + + Belirtilen paketi kaldırır + + + Paket kaldırma başlatılıyor... + + + Başarıyla kaldırıldı + + + winget bu paket için kaldırma komutunu bulamıyor. Lütfen destek için paket yayımcısı ile iletişim kurun. + {Locked="winget"} + + + Kaldırma yarıda bırakıldı + + + Kaldırma şu çıkış koduyla başarısız oldu: + + + Yüklü paketlerin listesini dışarı aktarır + + + Bir dosyada listelenen tüm paketleri yükler. + + + Bir dosyadaki tüm paketleri yükler + + + Sonucun yazılacağı dosya + + + Yüklenecek paketleri açıklayan dosya + + + Paketleri belirtilen kaynaktan dışarı aktarın + + + Yüklü paketlerin listesini bir dosyaya yazar. Bunun üzerine paketler import komutuyla yüklenebilir. + {Locked="import"} + + + Bir veya daha fazla içeri aktarılan paket yüklenemedi + + + İçeri aktarılacak paket bulunamadı: + + + İçeri aktarma için gerekli kaynak yüklü değil: + + + Yüklü paket hiçbir kaynaktan alınamıyor: + + + Yüklü paket sürümü hiçbir kaynaktan alınamıyor: + + + İçeri aktarma dosyasında paket bulunamadı + + + JSON dosyası geçerli değil + + + Paket zaten yüklü: + + + Kullanılamayan paketleri yoksayın + + + Paket sürümlerini oluşturulan dosyaya ekleyin + + + İçeri aktarma dosyasındaki paket sürümlerini yoksayın + + + Yol yok: + \ No newline at end of file diff --git a/Localization/Resources/uk-UA/winget.resw b/Localization/Resources/uk-UA/winget.resw index 555ecf0fb1..63b39f97b0 100644 --- a/Localization/Resources/uk-UA/winget.resw +++ b/Localization/Resources/uk-UA/winget.resw @@ -476,6 +476,9 @@ Канал + + Відображення відомостей для певного пакета. За замовчуванням запит має insensitively збігається з ідентифікатором, іменем або монікер пакета. Інші поля можна використовувати, достатня відповідний параметр. + Відображає відомості про пакет @@ -650,7 +653,7 @@ Нерозпізнана команда - Оновіть усі інстальовані пакети до найновішого, якщо доступно + Оновіть усі інстальовані пакети до найновіших, якщо доступно Не знайдено застосовних оновлень. @@ -719,4 +722,96 @@ Журнали Diagnostic files containing information about application use. + + Інсталятор заблоковано політикою + + + Перевірку безпеки інсталятора не пройдено + + + Антивірусний продукт повідомляє про інфекцію в інсталяторі + + + Помилка під час спроби оновити джерело: + + + Видаляє вибраний пакет, знайдений за допомогою пошуку списку встановлених пакетів або безпосередньо з маніфесту. За замовчуванням запит має без урахування регістра збігатися з ідентифікатором, іменем або монікером пакета. Інші поля можна використовувати, передаючи його відповідні параметри. + + + Видаляє вказаний пакет + + + Запуск видалення пакета... + + + Видалено + + + winget не вдалося знайти команду видалення для цього пакета. Зверніться до видавця пакета, щоб отримати підтримку. + {Locked="winget"} + + + Видалення припинено + + + Помилка видалення з кодом виходу: + + + Експортує список інстальованих пакетів + + + Інсталює всі пакети, перелічені у файлі. + + + Інсталює всі пакети у файлі + + + Файл, у якому має бути записаний результат + + + Файл з описом пакетів, які потрібно інсталювати + + + Експортувати пакети із вказаного джерела + + + Записує список інстальованих пакетів у файл. Пакети можна інсталювати за допомогою команди import. + {Locked="import"} + + + Не вдалося інсталювати один або кілька імпортованих пакетів + + + Пакет не знайдено для імпорту: + + + Джерело, необхідне для імпорту, не інстальовано: + + + Інстальований пакет недоступний з будь-якого джерела: + + + Інстальована версія пакета недоступна з будь-якого джерела: + + + У файлі імпорту не знайдено жодного пакета + + + Неприпустимий файл JSON + + + Пакет вже інстальовано: + + + Ігнорувати недоступні пакети + + + Включення версій пакетів до отриманого файлу + + + Ігнорувати версії пакета з файлу імпорту + + + Шлях не існує: + \ No newline at end of file diff --git a/Localization/Resources/vi-VN/winget.resw b/Localization/Resources/vi-VN/winget.resw index d2bb92a59f..a16597cbb0 100644 --- a/Localization/Resources/vi-VN/winget.resw +++ b/Localization/Resources/vi-VN/winget.resw @@ -389,7 +389,7 @@ Chúng có thể được cấu hình thông qua tệp cài đặt 'winget set Không có trình cài đặt nào được áp dụng cho hệ thống hiện tại. - Hiện tại không có các tính năng thử nghiệm có sẵn. + Hiện tại không có tính năng thử nghiệm nào sẵn có. Không tìm thấy gói đã cài đặt nào phù hợp với tiêu chí đầu vào. @@ -476,6 +476,9 @@ Chúng có thể được cấu hình thông qua tệp cài đặt 'winget set Kênh + + Hiển thị thông tin trên một gói cụ thể. Theo mặc định, truy vấn phải là chữ hoa-insensitively khớp với ID, tên hoặc moniker của gói. Các trường khác có thể được sử dụng bằng cách chuyển tùy chọn phù hợp của chúng. + Hiển thị thông tin về gói @@ -650,7 +653,7 @@ Chúng có thể được cấu hình thông qua tệp cài đặt 'winget set Lệnh không nhận dạng được: - Cập nhật tất cả các gói đã cài đặt lên mới nhất nếu có + Cập nhật tất cả các gói đã cài đặt thành phiên bản mới nhất nếu có Không tìm thấy bản cập nhật thích hợp. @@ -719,4 +722,96 @@ Chúng có thể được cấu hình thông qua tệp cài đặt 'winget set Nhật ký Diagnostic files containing information about application use. + + Trình cài đặt bị chặn bởi chính sách + + + Trình cài đặt không kiểm tra được bảo mật + + + Sản phẩm chống vi-rút báo cáo sự lây nhiễm trong trình cài đặt + + + Không cập nhật được nguồn: + + + Uninstalls gói đã chọn, được tìm thấy bằng cách tìm kiếm danh sách gói đã cài đặt hoặc trực tiếp từ một tệp kê khai. Theo mặc định, truy vấn phải là chữ hoa-insensitively khớp với ID, tên hoặc moniker của gói. Các trường khác có thể được sử dụng bằng cách chuyển tùy chọn phù hợp của chúng. + + + Gỡ cài đặt gói đã cho + + + Đang bắt đầu gỡ cài đặt gói... + + + Đã gỡ cài đặt thành công + + + winget không thể định vị lệnh gỡ cài đặt cho gói này. Vui lòng liên hệ với nhà xuất bản gói để được hỗ trợ. + {Locked="winget"} + + + Gỡ cài đặt đã hủy bỏ + + + Gỡ cài đặt không thành công với mã thoát: + + + Xuất danh sách các gói đã cài đặt + + + Cài đặt tất cả các gói được liệt kê trong một tệp. + + + Cài đặt tất cả gói trong một tệp + + + Tệp ghi kết quả + + + Tệp mô tả các gói để cài đặt + + + Xuất gói từ nguồn được chỉ định + + + Ghi danh sách các gói đã cài đặt thành tệp. Gói này có thể được cài đặt với lệnh import. + {Locked="import"} + + + Một hoặc nhiều gói đã nhập không thể cài đặt + + + Không tìm thấy gói để nhập: + + + Nguồn cần thiết để nhập chưa được cài đặt: + + + Phiên bản đã cài đặt không sẵn dùng từ mọi nguồn: + + + Phiên bản gói đã cài đặt không sẵn dùng từ mọi nguồn: + + + Không tìm thấy gói trong tệp đã nhập + + + Tệp JSON không hợp lệ + + + Gói đã được cài đặt: + + + Bỏ qua các gói không sẵn dùng + + + Bao gồm phiên bản gói trong tệp được tạo + + + Bỏ qua phiên bản gói từ tệp đã nhập + + + Đường dẫn không tồn tại: + \ No newline at end of file diff --git a/Localization/Resources/zh-CN/winget.resw b/Localization/Resources/zh-CN/winget.resw index 3f758a38a6..194fcb07bd 100644 --- a/Localization/Resources/zh-CN/winget.resw +++ b/Localization/Resources/zh-CN/winget.resw @@ -476,6 +476,9 @@ 频道 + + 显示有关特定程序包的信息。默认情况下,查询必须以不区分大小写的方式匹配程序包的 ID、名称或名字对象。可通过传递相应的选项来使用其他字段。 + 显示有关程序包的信息 @@ -719,4 +722,96 @@ 日志 Diagnostic files containing information about application use. + + 安装程序已被策略阻止 + + + 安装程序未通过安全性检查 + + + 防病毒产品报告安装程序受感染 + + + 尝试更新源时失败: + + + 通过搜索已安装的程序包列表或直接从清单中卸载选择的程序包。默认情况下,查询必须 insensitively 匹配程序包的 id、名称或名字对象。可通过传递适当的选项来使用其他字段。 + + + 卸载给定的程序包 + + + 正在启动程序包卸载... + + + 已成功卸载 + + + winget 找不到此程序包的卸载命令。请与程序包发布者联系以获取支持。 + {Locked="winget"} + + + 卸载已放弃 + + + 卸载失败,退出代码为: + + + 导出已安装程序包的列表 + + + 安装文件中列出的所有程序包。 + + + 安装文件中的所有程序包 + + + 将在其中写入结果的文件 + + + 描述要安装的程序包的文件 + + + 从指定源导出程序包 + + + 将已安装程序包的列表写入文件。然后可以通过 import 命令安装这些包。 + {Locked="import"} + + + 无法安装一个或多个导入的程序包 + + + 找不到要导入的程序包: + + + 未安装导入所需的源: + + + 无法从任何源获得已安装的程序包: + + + 无法从任何源获得已安装的程序包版本: + + + 在导入文件中找不到程序包 + + + JSON 文件无效 + + + 已安装程序包: + + + 忽略不可用的程序包 + + + 在生成的文件中包括程序包版本 + + + 忽略导入文件中的程序包版本 + + + 路径不存在: + \ No newline at end of file diff --git a/Localization/Resources/zh-TW/winget.resw b/Localization/Resources/zh-TW/winget.resw index f07d18d1c2..dfd4d6b976 100644 --- a/Localization/Resources/zh-TW/winget.resw +++ b/Localization/Resources/zh-TW/winget.resw @@ -476,6 +476,9 @@ 頻道 + + 顯示特定封裝的資訊。根據預設,查詢必須 insensitively 符合封裝的識別碼、名稱或名字物件。您可以透過傳遞適當的選項來使用其他欄位。 + 顯示與套件的相關資訊 @@ -719,4 +722,96 @@ 記錄檔 Diagnostic files containing information about application use. + + 安裝程式受到政策封鎖 + + + 安裝程式的安全性檢查失敗 + + + 防毒程式產品報告安裝程式中有感染 + + + 嘗試更新資料來源失敗: + + + 可透過搜尋已安裝的套件或直接從資訊清單中,解除安裝已選取的套件。根據預設,查詢必須不區分大小寫地符合該套件的識別碼、名稱或連結路徑。您可以透過傳遞適當的選項,來使用其他欄位。 + + + 解除安裝指定的套件 + + + 正在啟動套件解除安裝... + + + 已成功解除安裝 + + + winget 找不到此套件的解除安裝命令。請向套件發行者尋求支援。 + {Locked="winget"} + + + 已放棄解除安裝 + + + 解除安裝失敗,結束代碼: + + + 匯出已安裝套件的清單 + + + 安裝檔案中列出的所有套件。 + + + 安裝檔案中的所有套件。 + + + 欲寫入結果的檔案 + + + 描述要安裝之套件的檔案 + + + 從指定來源匯出套件 + + + 將已安裝套件的清單寫入檔案。然後可以使用 import 命令來安裝封裝。 + {Locked="import"} + + + 無法安裝一或多個匯入的套件 + + + 找不到要匯入的套件: + + + 尚未安裝匯入所需的來源: + + + 已安裝套件無法從任何來源取得: + + + 已安裝的套件版本無法從任一來源取得: + + + 在匯入檔案中找不到套件 + + + JSON 檔案無效。 + + + 已安裝的套件: + + + 略過無法使用的套件 + + + 在製作的檔案中包含套件版本 + + + 略過匯入檔案的套件版本 + + + 路徑不存在: + \ No newline at end of file diff --git a/NOTICE b/NOTICE index c5905ea1e2..f6bff6ffbc 100644 --- a/NOTICE +++ b/NOTICE @@ -23,103 +23,214 @@ NuGet.Frameworks 5.0.0 - Apache-2.0 (c) 2008 VeriSign, Inc. (c) Microsoft Corporation. -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - - - "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - - - - "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - - - - "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - - - "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - - - - "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - - - - "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - - - - "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - - - - "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - - - - "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - - - - "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); - -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. -Unless required by applicable law or agreed to in writing, software +--------------------------------------------------------- -distributed under the License is distributed on an "AS IS" BASIS, +--------------------------------------------------------- -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +tristanpenman/valijson 7a52fc88cdffd6678c009ca2fad700151f7363c6 - BSD-2-Clause AND MIT + + +(c) 2008 Google Inc. +Copyright 2003 Google Inc. +Copyright 2005 Google Inc. +Copyright 2008 Google Inc. +Copyright 2009 Google Inc. +Copyright 2010 Google Inc. +Copyright (c) 2002 JSON.org +Copyright 2003, Google Inc. +Copyright 2005, Google Inc. +Copyright 2006, Google Inc. +Copyright 2007, Google Inc. +Copyright 2008, Google Inc. +Copyright 2009, Google Inc. +Copyright 2010, Google Inc. +Copyright 2013, Google Inc. +Copyright (c) 2010 IETF Trust +Copyright (c) 2012 IETF Trust +Copyright (c) 2013 IETF Trust +Copyright 2011-2014 Kazuho Oku +Copyright (c) 1994 X Consortium +Copyright (c) 2012 Julian Berman +Copyright (c) 2013 Dropbox, Inc. +Copyright (c) 2014 DeNA Co., Ltd. +Copyright 2011 Baptiste Lepilleur +Copyright (c) 2011-2015 Kazuho Oku +Copyright (c) 2016, Tristan Penman +Copyright (c) 2009 Florian Loitsch. +Copyright (c) 2015 THL A29 Limited. +Copyright 2009-2010 Cybozu Labs, Inc. +copyright (c) 2013-2017 Niels Lohmann +Copyright (c) 2016 Akamai Technologies +Copyright 2007-2010 Baptiste Lepilleur +Copyright 2007-2011 Baptiste Lepilleur +Copyright (c) 2009-2010 Cybozu Labs, Inc. +Copyright (c) 2006-2013 Alexander Chemeris +Copyright (c) 2007-2010 Baptiste Lepilleur +Copyright (c) The Internet Society (2005). +Copyright (c) The Internet Society (2006). +copyright 2009-2013 Christopher M. Kohlhoff +Copyright (c) 2016, Akamai Technolgies, Inc. +Copyright (c) 2007-2010 by Baptiste Lepilleur +Copyright (c) 2011 - 2012 Andrzej Krzemienski. +Copyright (c) 2003-2013 Christopher M. Kohlhoff +Copyright (c) 2009-2013 Christopher M. Kohlhoff +Copyright (c) 2004 Free Software Foundation, Inc. +Copyright (c) 2010 Free Software Foundation, Inc. +Copyright (c) 2011 Free Software Foundation, Inc. +Copyright (c) 2003, 2005 Free Software Foundation, Inc. +Copyright (c) 2013-2018 Niels Lohmann +Copyright (c) 2004, 2005, 2012 Free Software Foundation, Inc. +Copyright (c) 2006, 2008, 2010 Free Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo +Copyright (c) 2001, 2003, 2005, 2011 Free Software Foundation, Inc. +Copyright (c) 2004, 2005, 2007, 2008 Free Software Foundation, Inc. +Copyright (c) 2004, 2005, 2007, 2009 Free Software Foundation, Inc. +Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip. +Copyright 2011-2014 Kazuho Oku, Yasuhiro Matsumoto, Shigeo Mitsunari +Copyright (c) 2001, 2002, 2003, 2005, 2009 Free Software Foundation, Inc. +Copyright (c) 2001, 2003, 2005, 2008, 2011 Free Software Foundation, Inc. +Copyright (c) 2003, 2004, 2005, 2006, 2011 Free Software Foundation, Inc. +Copyright (c) 2004, 2005, 2007, 2008, 2009 Free Software Foundation, Inc. +Copyright (c) 2001, 2002, 2003, 2005, 2008, 2010 Free Software Foundation, Inc. +Copyright (c) 1996, 1997, 2000, 2001, 2003, 2005, 2008 Free Software Foundation, Inc. +Copyright (c) 2002, 2003, 2005, 2006, 2007, 2008, 2011 Free Software Foundation, Inc. +Copyright (c) 1997, 1999, 2000, 2001, 2003, 2004, 2005, 2008 Free Software Foundation, Inc. +Copyright (c) 1997, 2000, 2001, 2003, 2004, 2005, 2006, 2008 Free Software Foundation, Inc. +Copyright (c) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2008 Free Software Foundation, Inc. +Copyright (c) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2008, 2009, 2011 Free Software Foundation, Inc. +Copyright (c) 1999, 2000, 2003, 2004, 2005, 2006, 2007, 2009, 2010, 2011 Free Software Foundation, Inc. +Copyright (c) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2009, 2010, 2011 Free Software Foundation, Inc. +Copyright (c) 2008-2009 Bjoern Hoehrmann sa http://bjoern.hoehrmann.de/utf-8/decoder/dfa +Copyright (c) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2008, 2009 Free Software Foundation, Inc. +Copyright (c) 1996, 1997, 1999, 2000, 2002, 2003, 2004, 2005, 2006, 2008, 2009, 2010, 2011, 2012 Free Software Foundation, Inc. +Copyright (c) 1996, 1997, 1998, 1999, 2000, 2001, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +Copyright (c) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +Copyright (c) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc. +Copyright (c) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +Copyright (c) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 Free Software Foundation, Inc. + +Copyright (c) 2016, Tristan Penman +Copyright (c) 2016, Akamai Technolgies, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -See the License for the specific language governing permissions and - -limitations under the License. --------------------------------------------------------- @@ -149,212 +260,89 @@ Copyright (c) 2019 Two Blue Cubes Ltd. Copyright.txt or https://cmake.org/licensing Copyright (c) 2015-2017 RWTH Aachen University, Federal Republic of Germany -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -Microsoft.NETCore.Platforms 3.1.0 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.Win32.Registry 4.7.0 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.Windows.CppWinRT 2.0.191111.2 - MIT +Microsoft.Msix.Utils 1.0.200812001 - MIT (c) 2008 VeriSign, Inc. +Copyright (c) Microsoft. (c) Microsoft Corporation. -Copyright (c) Microsoft Corporation. - - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - +copyrightYear Clear AltDirectorySeparatorChar + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- --------------------------------------------------------- -Microsoft.Windows.CppWinRT 2.0.191202.6 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) Microsoft Corporation. - - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE +Microsoft.Windows.CppWinRT 2.0.200729.8 - MIT + + + + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE --------------------------------------------------------- --------------------------------------------------------- -Microsoft.Windows.ImplementationLibrary 1.0.191107.2 - MIT +Microsoft.Windows.ImplementationLibrary 1.0.200519.2 - MIT (c) 2008 VeriSign, Inc. @@ -363,61 +351,27 @@ Copyright (c) Microsoft. Copyright (c) Microsoft Corporation. Copyright (c) 2009-2014 by the contributors - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - - ---------------------------------------------------------- - ---------------------------------------------------------- - -NETStandard.Library 2.0.0 - MIT - - -(c) 2008 VeriSign, Inc. -copyright Unmanaged32Bit Required32Bit -Copyright (c) .NET Foundation and Contributors - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE --------------------------------------------------------- @@ -430,14 +384,14 @@ Newtonsoft.Json 9.0.1 - MIT (c) 2008 VeriSign, Inc. Copyright James Newton-King 2008 -MIT License - -Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- @@ -508,114 +462,6 @@ THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -System.Security.AccessControl 4.7.0 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -System.Security.Principal.Windows 4.7.0 - MIT - - -(c) 2008 VeriSign, Inc. -(c) Microsoft Corporation. -Copyright (c) .NET Foundation. -Copyright (c) 2011, Google Inc. -(c) 1997-2005 Sean Eron Anderson. -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2017 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Portions (c) International Organization -Copyright (c) 2015 The Chromium Authors. -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) .NET Foundation Contributors -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 1995-2017 Jean-loup Gailly and Mark Adler -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang). Disclaimers THIS WORK IS PROVIDED AS - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --------------------------------------------------------- --------------------------------------------------------- @@ -626,26 +472,102 @@ yaml/libyaml 2c891fc7a770e8ba2fec34fc6b545c672beb37e6 - MIT Copyright (c) 2006-2016 Kirill Simonov Copyright (c) 2006-2016 Kirill Simonov -Copyright (c) 2017-2020 Ingy döt Net -Copyright (c) 2006-2016 Kirill Simonov +Copyright (c) 2017-2020 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +--------------------------------------------------------- -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +--------------------------------------------------------- + +open-source-parsers/jsoncpp 9be589598595963f94ba264d7b416d0533421106 - MIT OR OTHER + + +Copyright (c) 2016 InfoTeCS JSC. +Copyright 2007-2010 The JsonCpp Authors +Copyright 2007-2019 The JsonCpp Authors +Copyright 2007 Baptiste Lepilleur and The JsonCpp Authors +Copyright 2009 Baptiste Lepilleur and The JsonCpp Authors +Copyright 2010 Baptiste Lepilleur and The JsonCpp Authors +Copyright 2011 Baptiste Lepilleur and The JsonCpp Authors +Copyright 2007-2010 Baptiste Lepilleur and The JsonCpp Authors +Copyright 2007-2011 Baptiste Lepilleur and The JsonCpp Authors +Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors +Copyright (c) 2007-2010 by Baptiste Lepilleur and The JsonCpp Authors + +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... + +Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. + +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and +The JsonCpp Authors, and is released under the terms of the MIT License (see below). + +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. + +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: + + http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +======================================================================== +Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +======================================================================== +(END LICENSE TEXT) + +The MIT license is compatible with both the GPL and commercial +software, affording one all of the rights of Public Domain with the +minor nuisance of being required to keep the above copyright notice +and license text in the source code. Note also that by accepting the +Public Domain "license" you can re-license your copy using whatever +license you like. --------------------------------------------------------- diff --git a/WinGetUtil.nuspec b/WinGetUtil.nuspec new file mode 100644 index 0000000000..a7eadecb0a --- /dev/null +++ b/WinGetUtil.nuspec @@ -0,0 +1,22 @@ + + + + Microsoft.WindowsPackageManager.Utils + $version$ + + Microsoft + + https://github.com/microsoft/winget-cli + MIT + true + The utility binary for use with the WinGet CLI. + © Microsoft Corporation. All rights reserved. + winget + + + + + + + + \ No newline at end of file diff --git a/azure-pipelines.loc.yml b/azure-pipelines.loc.yml index 79b89fd2ed..7d3eeaa815 100644 --- a/azure-pipelines.loc.yml +++ b/azure-pipelines.loc.yml @@ -23,7 +23,7 @@ jobs: displayName: Send resources to Touchdown Build inputs: teamId: 8343 - authId: d3dd8113-65b3-4526-bdca-a00a7d1c37ba + authId: 2796a411-f030-46c1-ae3e-ab56f60ea523 authKey: $(LocServiceKey) isPreview: false relativePathRoot: src\AppInstallerCLIPackage\Shared\Strings\en-us diff --git a/azure-pipelines.nuget.yml b/azure-pipelines.nuget.yml new file mode 100644 index 0000000000..71a90f13a8 --- /dev/null +++ b/azure-pipelines.nuget.yml @@ -0,0 +1,143 @@ +trigger: none + +parameters: + - name: version + displayName: Version to stamp on binaries and nuget package. Should be in form of "major.minor", for example "1.0" + type: string + +pool: + vmImage: "windows-latest" + +variables: + solution: "src/AppInstallerCLI.sln" + buildConfiguration: "Release" + packageName: Microsoft.WindowsPackageManager.Utils + +jobs: + - job: "Build" + variables: + BuildVer: $[counter(${{ parameters.version }}, 1)] + version: ${{ parameters.version }}.$(BuildVer) + steps: + - script: echo $(version) + + - task: NuGetToolInstaller@1 + displayName: Install Nuget + + # Restores all projects, including native (vcxproj) projects + - task: NuGetCommand@2 + displayName: Restore Packages + inputs: + restoreSolution: "$(solution)" + + # Restores only .NET core projects, but is still necessary, as without this the IndexCreationTool and LocalhostWebServer projects fail to build + - task: DotNetCoreCLI@2 + displayName: DotNet Restore + inputs: + command: "restore" + projects: "**/*.csproj" + + - task: PowerShell@2 + displayName: Update Binary Version + condition: not(eq(variables['Build.Reason'], 'PullRequest')) + inputs: + filePath: 'src\binver\Update-BinVer.ps1' + arguments: '-TargetFile binver\binver\version.h -BuildVersion $(BuildVer) -MajorMinorOverride ${{ parameters.version }}' + workingDirectory: "src" + + - task: VSBuild@1 + displayName: Build Solution x86 + inputs: + platform: "x86" + solution: "$(solution)" + configuration: "$(buildConfiguration)" + + - task: VSBuild@1 + displayName: Build Solution x64 + inputs: + platform: "x64" + solution: "$(solution)" + configuration: "$(buildConfiguration)" + + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + displayName: "ESRP CodeSigning - Package contents" + inputs: + ConnectedServiceName: "WindowsPackageManager ESRP CodeSigning" + FolderPath: src + Pattern: | + *\$(buildConfiguration)\WinGetUtil\WinGetUtil.dll + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode" : "CP-230012", + "OperationCode" : "SigntoolSign", + "Parameters" : { + "OpusName" : "Microsoft", + "OpusInfo" : "http://www.microsoft.com", + "FileDigest" : "/fd \"SHA256\"", + "PageHash" : "/NPH", + "TimeStamp" : "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName" : "sign", + "ToolVersion" : "1.0" + }, + { + "KeyCode" : "CP-230012", + "OperationCode" : "SigntoolVerify", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + } + ] + + - task: NuGetCommand@2 + displayName: Pack WingetUtil nuget package + inputs: + command: pack + packagesToPack: WinGetUtil.nuspec + versioningScheme: byEnvVar + versionEnvVar: version + packDestination: "$(Build.ArtifactStagingDirectory)" + + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + displayName: "ESRP CodeSigning - NuGet package" + inputs: + ConnectedServiceName: "WindowsPackageManager ESRP CodeSigning" + FolderPath: "$(Build.ArtifactStagingDirectory)" + Pattern: "Microsoft.Packaging.WinGetUtil.$(version).nupkg" + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode" : "CP-401405", + "OperationCode" : "NuGetSign", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + }, + { + "KeyCode" : "CP-401405", + "OperationCode" : "NuGetVerify", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + } + ] + + - task: PublishBuildArtifacts@1 + displayName: Publish nuget package to artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)\$(packageName).$(version).nupkg' + ArtifactName: $(packageName) + publishLocation: Container + + - task: NuGetCommand@2 + displayName: Push WingetUtil nuget package to nuget.org + inputs: + command: push + nuGetFeedType: external + includeNugetOrg: true + packagesToPush: '$(Build.ArtifactStagingDirectory)\$(packageName).$(version).nupkg' + publishFeedCredentials: "WindowsPackageManagerAzurePipelineNuget - NuGet.org" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0d304bbd24..944d2f31bd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,11 +47,13 @@ jobs: - task: NuGetToolInstaller@1 displayName: Install Nuget + # Restores all projects, including native (vcxproj) projects - task: NuGetCommand@2 displayName: Restore Packages inputs: restoreSolution: '$(solution)' + # Restores only .NET core projects, but is still necessary, as without this the IndexCreationTool and LocalhostWebServer projects fail to build - task: DotNetCoreCLI@2 displayName: DotNet Restore inputs: @@ -201,13 +203,13 @@ jobs: name: AppInstallerTest displayName: 'Download Source Package Certificate' inputs: - secureFile: 'AppInstallerTest.pfx' + secureFile: 'AppInstallerTest.pfx' - task: DownloadSecureFile@1 name: HTTPSDevCert displayName: 'Download Kestrel Certificate' inputs: - secureFile: 'HTTPSDevCert.pfx' + secureFile: 'HTTPSDevCert.pfx' - task: MSBuild@1 displayName: Build MSIX Test Installer File @@ -238,6 +240,7 @@ jobs: - task: VSTest@2 displayName: Run E2E Tests Packaged x64 inputs: + testRunTitle: 'E2E Packaged x64' testSelector: 'testAssemblies' testAssemblyVer2: 'src\x64\Release\AppInstallerCLIE2ETests\AppInstallerCLIE2ETests.dll' runSettingsFile: 'src\x64\Release\AppInstallerCLIE2ETests\Test.runsettings' @@ -263,6 +266,7 @@ jobs: - task: VSTest@2 displayName: Run E2E Tests Packaged x86 inputs: + testRunTitle: 'E2E Packaged x86' testSelector: 'testAssemblies' testAssemblyVer2: 'src\x86\Release\AppInstallerCLIE2ETests\AppInstallerCLIE2ETests.dll' runSettingsFile: 'src\x86\Release\AppInstallerCLIE2ETests\Test.runsettings' diff --git a/cgmanifest.json b/cgmanifest.json index 12896aedda..0401fd998b 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -26,6 +26,24 @@ "commitHash": "9be589598595963f94ba264d7b416d0533421106" } } + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/tristanpenman/valijson.git", + "commitHash": "7a52fc88cdffd6678c009ca2fad700151f7363c6" + } + } + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/microsoft/cpprestsdk.git", + "commitHash": "122d09549201da5383321d870bed45ecb9e168c5" + } + } } ], "Version": 1 diff --git a/doc/ManifestSpecv0.1.md b/doc/ManifestSpecv0.1.md index 026b9543eb..beefe4974c 100644 --- a/doc/ManifestSpecv0.1.md +++ b/doc/ManifestSpecv0.1.md @@ -108,7 +108,8 @@ Protocols: "ms-winget" # Restrictions: [min: 1, max:40] Commands: "code" -# InstallerType is a required field. Supported types are inno, wix, msi, nullsoft, zip, appx, msix and exe. +# InstallerType is a required field in the root or for each installer entry. +# Supported types are inno, wix, msi, nullsoft, zip, appx, msix and exe. # The winget command tool uses this value to assist in installing this application. # If the value is an exe, you will need to provide the quiet switches. # zip is not supported in this preview (5/24/2020) @@ -239,9 +240,8 @@ Localization: Homepage: https://github.com/microsoft/msix-packaging/es-MX LicenseUrl: https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX -# ManifestVersion: 0.1.0 # ManifestVersion is a required field. ManifestVersion will allow the client to detect updated manifests and treat it differently. - +ManifestVersion: 0.1.0 ``` ## Minimal YAML file example @@ -252,11 +252,13 @@ Id: Microsoft.VisualStudioCode Version: 1.41.1 Name: Visual Studio Code Publisher: Microsoft Corporation +License: MIT License Installers: - Arch: x64 Url: https://aka.ms/win32-x64-user-stable Installertype: Inno Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestVersion: 0.1.0 ``` ## Best Practices The Id must be unique. You cannot have multiple submissions with the same Id. @@ -282,3 +284,4 @@ History: | .05 | 4/1/2020 | Added restrictions. Added SystemAppId | | .06 | 4/23/2020 | Renamed client. Updated License to required. | | .07 | 5/15/2020 | Add ManifestVersion. | +| .08 | 2/17/2021 | Fixed minimal example based on required fields documentation | diff --git a/doc/Settings.md b/doc/Settings.md index 875cb20d1e..7e113a8e08 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -87,3 +87,33 @@ While work is in progress on upgrade, the command is hidden behind a feature tog "upgrade": true }, ``` + +### uninstall + +While work is in progress on uninstall, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "uninstall": true + }, +``` + +### import + +While work is in progress for import, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "import": true + }, +``` + +### restSource + +While work is in progress for rest source support, the feature is hidden behind a feature toggle. Enabling this will not change how client works currently and will allow testing any additional rest sources added. One can enable it as below: + +``` + "experimentalFeatures": { + "restSource": true + }, +``` diff --git a/doc/windows-package-manager-v1-roadmap.md b/doc/windows-package-manager-v1-roadmap.md index 10f3c97d61..c3d16dbe06 100644 --- a/doc/windows-package-manager-v1-roadmap.md +++ b/doc/windows-package-manager-v1-roadmap.md @@ -45,15 +45,17 @@ Ultimately, we're aiming for Windows Package Manager v1.0 to be released in Spri | July 2020 | | | | August 2020 | | | | September 2020 | [v0.2](https://github.com/microsoft/winget-cli/milestone/4) | Support for Microsoft Store (curated list of developer tools in experimental feature)| -| October 2020 | [v0.3](https://github.com/microsoft/winget-cli/milestone/5), [v0.4](https://github.com/microsoft/winget-cli/milestone/6), [v0.5](https://github.com/microsoft/winget-cli/milestone/7) | List, Upgrade, and Uninstall (includes Apps in Control Panel/Add Remove Programs) | -| November 2020 | [v0.6](https://github.com/microsoft/winget-cli/milestone/8), [v0.7](https://github.com/microsoft/winget-cli/milestone/9) | Import / Export and Dependency Support| -| December 2020 | [v0.8](https://github.com/microsoft/winget-cli/milestone/10) | Multiple Architectures | -| January 2021 | [v0.9](https://github.com/microsoft/winget-cli/milestone/11) | Multiple Languages | -| February 2021 | [v0.10](https://github.com/microsoft/winget-cli/milestone/12) | User vs. System installation | -| March 2021 | [v0.11](https://github.com/microsoft/winget-cli/milestone/13) | Third party REST source| -| April 2021 | [v0.12](https://github.com/microsoft/winget-cli/milestone/14) | Group Policy| +| October 2020 | [v0.3](https://github.com/microsoft/winget-cli/milestone/5) | List (includes Apps in Control Panel/Add Remove Programs) | +| November 2020 | [v0.4](https://github.com/microsoft/winget-cli/milestone/6) | Upgrade | +| December 2020 | [v0.5](https://github.com/microsoft/winget-cli/milestone/7) | Uninstall | +| January 2021 | [v0.6](https://github.com/microsoft/winget-cli/milestone/8) | Import / Export | +| February 2021 | , [v0.7](https://github.com/microsoft/winget-cli/milestone/9), [v0.8](https://github.com/microsoft/winget-cli/milestone/10), [v0.9](https://github.com/microsoft/winget-cli/milestone/11), [v0.10](https://github.com/microsoft/winget-cli/milestone/12) | Dependency Support, Multiple Architectures, Multiple Languages, and User vs. System installation | +| March 2021 | [v0.11](https://github.com/microsoft/winget-cli/milestone/13), [v0.12](https://github.com/microsoft/winget-cli/milestone/14), [v0.13](https://github.com/microsoft/winget-cli/milestone/15), [v0.14](https://github.com/microsoft/winget-cli/milestone/16), [v0.16](https://github.com/microsoft/winget-cli/milestone/18), [v0.17](https://github.com/microsoft/winget-cli/milestone/19) | Third party REST source, Group Policy, Delivery Optimization, Metered Networks, .zip, and .exe| +| April 2021 ||| | May 2021 | [v1.0](https://github.com/microsoft/winget-cli/milestone/1) | Windows Package Manager v1.0 Release | +Note: Many of the features have been implemented in experimental mode. If you execute `winget features` a list of experimental features and their status is displayed. You may modify your settings file with `winget settings` to enable or disable them. The experimental "list" feature is a prerequisite for "upgrade", "uninstall", and other features in development. Once the "list" feature has been fully implemented, the other stable features depending on it will also be migrated from experimental to default. + ## GitHub Milestones Each Release above is/will be reflected in our [GitHub milestones](https://github.com/microsoft/winget-cli/milestones): @@ -74,7 +76,7 @@ Each Release above is/will be reflected in our [GitHub milestones](https://githu | [v0.12](https://github.com/microsoft/winget-cli/milestone/14) | Group Policy | | [v0.13](https://github.com/microsoft/winget-cli/milestone/15) | Delivery Optimization | | [v0.14](https://github.com/microsoft/winget-cli/milestone/16) | Metered Networks | -| [v0.15](https://github.com/microsoft/winget-cli/milestone/17) | App Config Files | +| ~~[v0.15](https://github.com/microsoft/winget-cli/milestone/17)~~ | App Config Files* | | [v0.16](https://github.com/microsoft/winget-cli/milestone/18) | .zip | | [v0.17](https://github.com/microsoft/winget-cli/milestone/19) | .exe | | [v0.18](https://github.com/microsoft/winget-cli/milestone/20) | Portable/Standalone Apps | @@ -93,6 +95,8 @@ Each Release above is/will be reflected in our [GitHub milestones](https://githu | [v0.31](https://github.com/microsoft/winget-cli/milestone/33) | Auto Upgrade Apps | | [Backlog](https://github.com/microsoft/winget-cli/milestone/2) | Work not yet assigned to a milestone or release | +* Versions with strikethrough have been pushed post v1.0. + ## Issue Triage & Prioritization Incoming issues/asks/etc. are triaged several times a week, labelled appropriately, and assigned to a milestone in priority order: @@ -113,39 +117,40 @@ The following are a list of the key scenarios we're aiming to deliver for Window | [v0.1.41821-preview](https://github.com/microsoft/winget-cli/releases/tag/v0.1.41821-preview) | Configurability & Customization | The client will have a modern, flexible settings mechanism that persists settings to/from a JSON file stored in the user's app data folders, and/or in files synchronized between machines via OneDrive, etc. | | [v0.1.41821-preview](https://github.com/microsoft/winget-cli/releases/tag/v0.1.41821-preview) | Color Theming & Styling | The client will honor the user's Windows dark/light theme settings, and/or color accent settings. | | [v0.1.42241-preview](https://github.com/microsoft/winget-cli/releases/tag/v0.1.42241-preview) | Autocomplete | The client will support autocomplete for all commands and packages in the local cache. | -| V1 | #119 `winget list` | The client should be able to tell you what Apps are installed including the Control Panel. | -| V1 | #120 `winget upgrade` | The client should be able to update one or "all" installed Apps. | -| V1 | #121 `winget uninstall` | The client should be able to uninstall Apps. | -| V1 | #220 Export/Import | The client should be able to export the list of installed Apps and import the exported list. | -| V1 | #163 Dependencies | The client should be able to install package dependencies. | -| V1 | #132 Multiple Architectures | The client should support multiple architectures in the same manifest. | -| V1 | #149 User vs. System | Applications may be installed for the local user or for the system. | -| V1 | #124 Multiple Languages | The client will support installation for almost every language for which there is a fixed-width font including East Asian languages. Bonus points for RTL languages/scripts. | -| V1 | #226 REST Repository | Support for a REST based repository. | -| V1 | #154 Group Policy | Support for Group Policy control. | -| V1 | #151 Delivery Optimization | Delivery Optimization should be leveraged for large Apps. | -| V1 | #150 Metered Networks | The client should download responsibly when on metered networks. | -| V1 | #158 App Config Files | Support for silent installers that require a configuration file. | -| V1 | #140 Install .zip | The client should be able to install programs in a .zip file. | -| V1 | #194 Install .exe | The client should be able to install a static .exe file. | -| V1 | #182 Install portable app | The client should be able to install portable Apps. | -| V1 | #201 Specify install directory | The client should be able to install to an alternate directory. | -| V1 | #137 Non-Zero Exit Codes | The client should support applications with non-zero exit codes as success. | -| V1 | #279 Opt-Out of Telemetry | The client should be able to Opt-Out of Telemetry. | -| V1 | #161 Client Verbosity Settings | The client should support different verbosity settings. | -| V1 | #147 Release Channels | Some applications have different release channels and we should support them. | -| V1 | #221 Native PowerShell | Native PowerShell support for the client. | -| V1 | #164 Install PWA | Support installing Progressive Web Applications. | -| V1 | #219 Install Multiple Apps | The client should allow a user to specify multiple apps to install. | -| V1 | #229 Suppress reboot | The client should allow a user to suppress reboot as a default setting. | -| V1 | #227 Version specification | The client should allow more variation to specifying package versions for installation. | -| V1 | #225 Parallel download | The client should support multiple connections per package for download. | -| V1 | #166 Fonts | The client should support installing fonts. | -| V1 | #212 Auto Upgrade | The client should be able to auto upgrade installed apps if configured to do so. | -| V1 | #157 Manifest Wizard | Help a user generate a manifest. | -| V1 | #161 Verbosity | Client Verbosity Settings. | -| V1 | #117 Microsoft Store | Support for installing Apps from the Microsoft Store. | +| V1 | [#119](https://github.com/microsoft/winget-cli/issues/119) `winget list` | The client should be able to tell you what Apps are installed including the Control Panel. | +| V1 | [#120](https://github.com/microsoft/winget-cli/issues/120) `winget upgrade` | The client should be able to update one or "all" installed Apps. | +| V1 | [#121](https://github.com/microsoft/winget-cli/issues/121) `winget uninstall` | The client should be able to uninstall Apps. | +| V1 | [#220](https://github.com/microsoft/winget-cli/issues/220) Export/Import | The client should be able to export the list of installed Apps and import the exported list. | +| V1 | [#163](https://github.com/microsoft/winget-cli/issues/163) Dependencies | The client should be able to install package dependencies. | +| V1 | [#132](https://github.com/microsoft/winget-cli/issues/132) Multiple Architectures | The client should support multiple architectures in the same manifest. | +| V1 | [#149](https://github.com/microsoft/winget-cli/issues/149) User vs. System | Applications may be installed for the local user or for the system. | +| V1 | [#124](https://github.com/microsoft/winget-cli/issues/124) Multiple Languages | The client will support installation for almost every language for which there is a fixed-width font including East Asian languages. Bonus points for RTL languages/scripts. | +| V1 | [#226](https://github.com/microsoft/winget-cli/issues/226) REST Repository | Support for a REST based repository. | +| V1 | [#154](https://github.com/microsoft/winget-cli/issues/154) Group Policy | Support for Group Policy control. | +| V1 | [#151](https://github.com/microsoft/winget-cli/issues/151) Delivery Optimization | Delivery Optimization should be leveraged for large Apps. | +| V1 | [#150](https://github.com/microsoft/winget-cli/issues/150) Metered Networks | The client should download responsibly when on metered networks. | +| V1 | [#140](https://github.com/microsoft/winget-cli/issues/140) Install .zip | The client should be able to install programs in a .zip file. | +| V1 | [#194](https://github.com/microsoft/winget-cli/issues/194) Install .exe | The client should be able to install a static .exe file. | +| V1 | [#182](https://github.com/microsoft/winget-cli/issues/182) Install portable app | The client should be able to install portable Apps. | +| V1 | [#201](https://github.com/microsoft/winget-cli/issues/201) Specify install directory | The client should be able to install to an alternate directory. | +| V1 | [#137](https://github.com/microsoft/winget-cli/issues/137) Non-Zero Exit Codes | The client should support applications with non-zero exit codes as success. | +| V1 | [#279](https://github.com/microsoft/winget-cli/issues/279) Opt-Out of Telemetry | The client should be able to Opt-Out of Telemetry. | +| V1 | [#161](https://github.com/microsoft/winget-cli/issues/161) Client Verbosity Settings | The client should support different verbosity settings. | +| V1 | [#147](https://github.com/microsoft/winget-cli/issues/147) Release Channels | Some applications have different release channels and we should support them. | +| V1 | [#221](https://github.com/microsoft/winget-cli/issues/221) Native PowerShell | Native PowerShell support for the client. | +| V1 | [#164](https://github.com/microsoft/winget-cli/issues/164) Install PWA | Support installing Progressive Web Applications. | +| V1 | [#219](https://github.com/microsoft/winget-cli/issues/219) Install Multiple Apps | The client should allow a user to specify multiple apps to install. | +| V1 | [#229](https://github.com/microsoft/winget-cli/issues/229) Suppress reboot | The client should allow a user to suppress reboot as a default setting. | +| V1 | [#227](https://github.com/microsoft/winget-cli/issues/227) Version specification | The client should allow more variation to specifying package versions for installation. | +| V1 | [#225](https://github.com/microsoft/winget-cli/issues/225) Parallel download | The client should support multiple connections per package for download. | +| V1 | [#166](https://github.com/microsoft/winget-cli/issues/166) Fonts | The client should support installing fonts. | +| V1 | [#212](https://github.com/microsoft/winget-cli/issues/212) Auto Upgrade | The client should be able to auto upgrade installed apps if configured to do so. | +| V1 | [#157](https://github.com/microsoft/winget-cli/issues/157) Manifest Wizard | Help a user generate a manifest. | +| V1 | [#161](https://github.com/microsoft/winget-cli/issues/161) Verbosity | Client Verbosity Settings. | +| V1 | [#117](https://github.com/microsoft/winget-cli/issues/117) Microsoft Store | Support for installing Apps from the Microsoft Store. | | V1 | Accessibility (A11y) | The client will be highly accessible and inclusive. It will expose its contents via [UIA](https://docs.microsoft.com/en-us/dotnet/framework/ui-automation/ui-automation-overview) to support tools such as [Windows Narrator](https://support.microsoft.com/en-us/help/22798/windows-10-complete-guide-to-narrator), and UI automation tools including [WinAppDriver](https://github.com/Microsoft/WinAppDriver). | +| V1.x | [#158](https://github.com/microsoft/winget-cli/issues/158) App Config Files | Support for silent installers that require a configuration file. | + Feature Notes: \* Feature Priorities will be influenced by community feedback on issues. diff --git a/schemas/JSON/manifests/preview/manifest.0.1.0.json b/schemas/JSON/manifests/preview/manifest.0.1.0.json new file mode 100644 index 0000000000..53298ecc4c --- /dev/null +++ b/schemas/JSON/manifests/preview/manifest.0.1.0.json @@ -0,0 +1,313 @@ +{ + "$id": "https://aka.ms/winget-manifest.0.1.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A single-file manifest representing a package in winget community repo. v0.1.0 Preview", + "definitions": { + "InstallerType": { + "type": [ "string", "null" ], + "pattern": "^(([Ee][Xx][Ee])|([Mm][Ss][Ii])|([Mm][Ss][Ii][Xx])|([Ii][Nn][Nn][Oo])|([Ww][Ii][Xx])|([Nn][Uu][Ll][Ll][Ss][Oo][Ff][Tt])|([Aa][Pp][Pp][Xx])|([Zz][Ii][Pp])|([Bb][Uu][Rr][Nn]))$", + "description": "InstallerType is required under Installer node if it's not defined in root" + }, + "UpdateBehavior": { + "type": [ "string", "null" ], + "pattern": "^(([Ii][Nn][Ss][Tt][Aa][Ll][Ll])|([Uu][Nn][Ii][Nn][Ss][Tt][Aa][Ll][Ll][Pp][Rr][Ee][Vv][Ii][Oo][Uu][Ss]))$", + "description": "UpdateBehavior is used to specify desired action during package upgrade" + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 10000, + "description": "Description of the package" + }, + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2000 + }, + "Homepage": { + "$ref": "#/definitions/Url", + "description": "Homepage is a Url where the user can find more information about the package" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "LicenseUrl provides a link to the license for the user to read" + }, + "InstallerSwitches": { + "type": [ "object", "null" ], + "properties": { + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the installer by winget" + }, + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "Language": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Some installers include all localized resources. By specifying a Language switch, winget will pass the value of Language to the installer. This is not yet supported in Preview releases" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Update": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Update is the value that should be passed to the installer when user chooses an upgrade" + } + } + }, + "Installer": { + "type": "object", + "properties": { + "Arch": { + "type": "string", + "pattern": "^(([Aa][Rr][Mm])|([Xx]86)|([Xx]64)|([Aa][Rr][Mm]64)|([Nn][Ee][Uu][Tt][Rr][Aa][Ll]))$", + "description": "Arch is required. The installer architecture" + }, + "Url": { + "type": "string", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2000, + "description": "Url is required. The path to the installer." + }, + "Sha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "Language": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 20, + "description": "Language is the specific language of the installer. Language must follow IETF language tag guidelines" + }, + "Scope": { + "type": [ "string", "null" ], + "pattern": "^(([Uu][Ss][Ee][Rr])|([Mm][Aa][Cc][Hh][Ii][Nn][Ee]))$", + "description": "Scope indicates if the installer is per user or per machine. Scope is not yet supported in Preview releases" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "UpdateBehavior": { + "$ref": "#/definitions/UpdateBehavior" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Switches": { + "$ref": "#/definitions/InstallerSwitches" + } + }, + "required": [ + "Arch", + "Url", + "Sha256" + ] + }, + "ManifestLocalization": { + "type": "object", + "properties": { + "Language": { + "type": "string", + "minLength": 2, + "maxLength": 20, + "description": "Language is the specific language of the localization. Language must follow IETF language tag guidelines" + }, + "Description": { + "$ref": "#/definitions/Description" + }, + "Homepage": { + "$ref": "#/definitions/Homepage" + }, + "LicenseUrl": { + "$ref": "#/definitions/LicenseUrl" + } + }, + "required": [ + "Language" + ] + } + }, + "type": "object", + "properties": { + "ManifestVersion": { + "type": "string", + "default": "0.1.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + }, + "Id": { + "type": "string", + "pattern": "^[^\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+\\.[^\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 255, + "description": "Id is a required field. It MUST include the publisher name and package name separated by a period. For example: Publisher.Package" + }, + "Name": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Name is a required field. The name of the package" + }, + "Version": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "minLength": 1, + "description": "Version is a required field. The version of the package" + }, + "Publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Publisher is a required field. The legal publisher name" + }, + "AppMoniker": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "AppMoniker is the common name someone may use to search for the package" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Channel a string representing the flight ring. For example: stable, beta, canary" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 100, + "description": "The person or company responsible for authoring the package" + }, + "License": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "description": "License is a required field. License provides the type of license the package is provided under" + }, + "MinOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "MinOSVersion uses the Windows version to limit installations on unsupported platforms" + }, + "Tags": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Tags is a comma separated list. They represent strings that user may use to search for the package" + }, + "Commands": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Commands is a comma separated list. They are the common executable or alias that user might type trying to run the package" + }, + "Protocols": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "Protocols is a comma separated list. Protocols provides the list of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 1000, + "description": "FileExtensions is a comma separated list. FileExtensions provides the list of extensions the package can support" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "UpdateBehavior": { + "$ref": "#/definitions/UpdateBehavior" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Description": { + "$ref": "#/definitions/Description" + }, + "Homepage": { + "$ref": "#/definitions/Homepage" + }, + "LicenseUrl": { + "$ref": "#/definitions/LicenseUrl" + }, + "Switches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "uniqueItems": true + }, + "Localization": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/ManifestLocalization" + } + } + }, + "required": [ + "Id", + "Name", + "Version", + "Publisher", + "License", + "Installers" + ] +} \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.defaultLocale.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.defaultLocale.1.0.0.json new file mode 100644 index 0000000000..b1a2dc1f2a --- /dev/null +++ b/schemas/JSON/manifests/v1.0.0/manifest.defaultLocale.1.0.0.json @@ -0,0 +1,142 @@ +{ + "$id": "https://aka.ms/winget-manifest.defaultlocale.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing a default app metadata in the OWC. v1.0.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "ManifestType": { + "type": "string", + "default": "defaultLocale", + "const": "defaultLocale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "ManifestType", + "ManifestVersion" + ] +} \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json new file mode 100644 index 0000000000..b27d2fe165 --- /dev/null +++ b/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json @@ -0,0 +1,454 @@ +{ + "$id": "https://aka.ms/winget-manifest.installer.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app installers in the OWC. v1.0.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "interactive", + "silent", + "silentWithProgress" + ] + }, + "maxItems": 3, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero installer success exit codes other than known default values by winget" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 2048 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 40 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 128 + }, + "ManifestType": { + "type": "string", + "default": "installer", + "const": "installer", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "Installers", + "ManifestType", + "ManifestVersion" + ] +} \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.locale.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.locale.1.0.0.json new file mode 100644 index 0000000000..241040462b --- /dev/null +++ b/schemas/JSON/manifests/v1.0.0/manifest.locale.1.0.0.json @@ -0,0 +1,138 @@ +{ + "$id": "https://aka.ms/winget-manifest.locale.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multiple-file manifest representing app metadata in other locale in the OWC. v1.0.0", + "definitions": { + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "ManifestType": { + "type": "string", + "default": "locale", + "const": "locale", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "ManifestType", + "ManifestVersion" + ] +} \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json new file mode 100644 index 0000000000..991bf5a39c --- /dev/null +++ b/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json @@ -0,0 +1,555 @@ +{ + "$id": "https://aka.ms/winget-manifest.singleton.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a single-file manifest representing an app in the OWC. v1.0.0", + "definitions": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "Locale": { + "type": [ "string", "null" ], + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Url": { + "type": [ "string", "null" ], + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "maxLength": 2000, + "description": "Optional Url type" + }, + "Tag": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 40, + "description": "Package moniker or tag" + }, + "Channel": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 16, + "description": "The distribution channel" + }, + "Platform": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "Windows.Desktop", + "Windows.Universal" + ] + }, + "maxItems": 2, + "uniqueItems": true, + "description": "The installer supported operating system" + }, + "MinimumOSVersion": { + "type": [ "string", "null" ], + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$", + "description": "The installer minimum operating system version" + }, + "InstallerType": { + "type": [ "string", "null" ], + "enum": [ + "msix", + "msi", + "appx", + "exe", + "zip", + "inno", + "nullsoft", + "wix", + "burn", + "pwa" + ], + "description": "Enumeration of supported installer types" + }, + "Scope": { + "type": [ "string", "null" ], + "enum": [ + "user", + "machine" + ], + "description": "Scope indicates if the installer is per user or per machine" + }, + "InstallModes": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "enum": [ + "interactive", + "silent", + "silentWithProgress" + ] + }, + "maxItems": 3, + "uniqueItems": true, + "description": "List of supported installer modes" + }, + "InstallerSwitches": { + "type": "object", + "properties": { + "Silent": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Silent is the value that should be passed to the installer when user chooses a silent or quiet install" + }, + "SilentWithProgress": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "SilentWithProgress is the value that should be passed to the installer when user chooses a non-interactive install" + }, + "Interactive": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Interactive is the value that should be passed to the installer when user chooses an interactive install" + }, + "InstallLocation": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "InstallLocation is the value passed to the installer for custom install location. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Log": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Log is the value passed to the installer for custom log file path. token can be included in the switch value so that winget will replace the token with user provided path" + }, + "Upgrade": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 512, + "description": "Upgrade is the value that should be passed to the installer when user chooses an upgrade" + }, + "Custom": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 2048, + "description": "Custom switches will be passed directly to the installer by winget" + } + } + }, + "InstallerSuccessCodes": { + "type": [ "array", "null" ], + "items": { + "type": "integer", + "not": { + "enum": [ 0 ] + } + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional non-zero installer success exit codes other than known default values by winget" + }, + "UpgradeBehavior": { + "type": [ "string", "null" ], + "enum": [ + "install", + "uninstallPrevious" + ], + "description": "The upgrade method" + }, + "Commands": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of commands or aliases to run the package" + }, + "Protocols": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[a-z][-a-z0-9\\.\\+]*$", + "maxLength": 2048 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of protocols the package provides a handler for" + }, + "FileExtensions": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 40 + }, + "maxItems": 256, + "uniqueItems": true, + "description": "List of file extensions the package could support" + }, + "Dependencies": { + "type": [ "object", "null" ], + "properties": { + "WindowsFeatures": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows feature dependencies" + }, + "WindowsLibraries": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of Windows library dependencies" + }, + "PackageDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "MinimumVersion": { + "$ref": "#/definitions/PackageVersion" + } + }, + "required": [ "PackageIdentifier" ] + }, + "maxItems": 16, + "description": "List of package dependencies from current source" + }, + "ExternalDependencies": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of external package dependencies" + } + } + }, + "PackageFamilyName": { + "type": [ "string", "null" ], + "pattern": "^[A-Za-z0-9][-\\.A-Za-z0-9]+_[A-Za-z0-9]{13}$", + "maxLength": 255, + "description": "PackageFamilyName for appx or msix installer. Could be used for correlation of packages across sources" + }, + "ProductCode": { + "type": [ "string", "null" ], + "minLength": 1, + "maxLength": 255, + "description": "ProductCode could be used for correlation of packages across sources" + }, + "Capabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer capabilities" + }, + "RestrictedCapabilities": { + "type": [ "array", "null" ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "maxItems": 1000, + "uniqueItems": true, + "description": "List of appx or msix installer restricted capabilities" + }, + "Installer": { + "type": "object", + "properties": { + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "Architecture": { + "type": "string", + "enum": [ + "x86", + "x64", + "arm", + "arm64", + "neutral" + ], + "description": "The installer target architecture" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallerUrl": { + "type": "string", + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "description": "The installer Url" + }, + "InstallerSha256": { + "type": "string", + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Sha256 is required. Sha256 of the installer" + }, + "SignatureSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "SignatureSha256 is recommended for appx or msix. It is the sha256 of signature file inside appx or msix. Could be used during streaming install if applicable" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + } + }, + "required": [ + "Architecture", + "InstallerUrl", + "InstallerSha256" + ] + } + }, + "type": "object", + "properties": { + "PackageIdentifier": { + "$ref": "#/definitions/PackageIdentifier" + }, + "PackageVersion": { + "$ref": "#/definitions/PackageVersion" + }, + "PackageLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The package meta-data locale" + }, + "Publisher": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The publisher name" + }, + "PublisherUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher home page" + }, + "PublisherSupportUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher support page" + }, + "PrivacyUrl": { + "$ref": "#/definitions/Url", + "description": "The publisher privacy page or the package privacy page" + }, + "Author": { + "type": [ "string", "null" ], + "minLength": 2, + "maxLength": 256, + "description": "The package author" + }, + "PackageName": { + "type": "string", + "minLength": 2, + "maxLength": 256, + "description": "The package name" + }, + "PackageUrl": { + "$ref": "#/definitions/Url", + "description": "The package home page" + }, + "License": { + "type": "string", + "minLength": 3, + "maxLength": 512, + "description": "The package license" + }, + "LicenseUrl": { + "$ref": "#/definitions/Url", + "description": "The license page" + }, + "Copyright": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 512, + "description": "The package copyright" + }, + "CopyrightUrl": { + "$ref": "#/definitions/Url", + "description": "The package copyright page" + }, + "ShortDescription": { + "type": "string", + "minLength": 3, + "maxLength": 256, + "description": "The short package description" + }, + "Description": { + "type": [ "string", "null" ], + "minLength": 3, + "maxLength": 10000, + "description": "The full package description" + }, + "Moniker": { + "$ref": "#/definitions/Tag", + "description": "The most common package term" + }, + "Tags": { + "type": [ "array", "null" ], + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 16, + "uniqueItems": true, + "description": "List of additional package search terms" + }, + "Channel": { + "$ref": "#/definitions/Channel" + }, + "InstallerLocale": { + "$ref": "#/definitions/Locale" + }, + "Platform": { + "$ref": "#/definitions/Platform" + }, + "MinimumOSVersion": { + "$ref": "#/definitions/MinimumOSVersion" + }, + "InstallerType": { + "$ref": "#/definitions/InstallerType" + }, + "Scope": { + "$ref": "#/definitions/Scope" + }, + "InstallModes": { + "$ref": "#/definitions/InstallModes" + }, + "InstallerSwitches": { + "$ref": "#/definitions/InstallerSwitches" + }, + "InstallerSuccessCodes": { + "$ref": "#/definitions/InstallerSuccessCodes" + }, + "UpgradeBehavior": { + "$ref": "#/definitions/UpgradeBehavior" + }, + "Commands": { + "$ref": "#/definitions/Commands" + }, + "Protocols": { + "$ref": "#/definitions/Protocols" + }, + "FileExtensions": { + "$ref": "#/definitions/FileExtensions" + }, + "Dependencies": { + "$ref": "#/definitions/Dependencies" + }, + "PackageFamilyName": { + "$ref": "#/definitions/PackageFamilyName" + }, + "ProductCode": { + "$ref": "#/definitions/ProductCode" + }, + "Capabilities": { + "$ref": "#/definitions/Capabilities" + }, + "RestrictedCapabilities": { + "$ref": "#/definitions/RestrictedCapabilities" + }, + "Installers": { + "type": "array", + "items": { + "$ref": "#/definitions/Installer" + }, + "minItems": 1, + "maxItems": 1 + }, + "ManifestType": { + "type": "string", + "default": "singleton", + "const": "singleton", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "PackageLocale", + "Publisher", + "PackageName", + "License", + "ShortDescription", + "Installers", + "ManifestType", + "ManifestVersion" + ] +} \ No newline at end of file diff --git a/schemas/JSON/manifests/v1.0.0/manifest.version.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.version.1.0.0.json new file mode 100644 index 0000000000..cfb4a89c2a --- /dev/null +++ b/schemas/JSON/manifests/v1.0.0/manifest.version.1.0.0.json @@ -0,0 +1,45 @@ +{ + "$id": "https://aka.ms/winget-manifest.version.1.0.0.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "A representation of a multi-file manifest representing an app version in the OWC. v1.0.0", + "type": "object", + "properties": { + "PackageIdentifier": { + "type": "string", + "pattern": "^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,3}$", + "maxLength": 128, + "description": "The package unique identifier" + }, + "PackageVersion": { + "type": "string", + "pattern": "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$", + "maxLength": 128, + "description": "The package version" + }, + "DefaultLocale": { + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20, + "description": "The default package meta-data locale" + }, + "ManifestType": { + "type": "string", + "default": "version", + "const": "version", + "description": "The manifest type" + }, + "ManifestVersion": { + "type": "string", + "default": "1.0.0", + "pattern": "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$", + "description": "The manifest syntax version" + } + }, + "required": [ + "PackageIdentifier", + "PackageVersion", + "DefaultLocale", + "ManifestType", + "ManifestVersion" + ] +} \ No newline at end of file diff --git a/schemas/JSON/packages/packages.schema.1.0.json b/schemas/JSON/packages/packages.schema.1.0.json new file mode 100644 index 0000000000..dcaac9387a --- /dev/null +++ b/schemas/JSON/packages/packages.schema.1.0.json @@ -0,0 +1,97 @@ +{ + "$id": "https://aka.ms/winget-packages.schema.json", + "$schema": "https://json-schema.org/draft/2019-09/schema#", + + "title": "winget Packages List Schema", + "description": "Describes a list of packages for batch installs", + + "type": "object", + "required": [ "WinGetVersion", "Sources" ], + "additionalProperties": true, + + "properties": { + "WinGetVersion": { + "description": "Version of winget that generated this file", + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + + "CreationDate": { + "description": "Date when this list was generated", + "type": "string", + "format": "date-time" + }, + + "Sources": { + "description": "Sources from which each package comes from", + "type": "array", + + "items": { + "description": "A source and the list of packages to install from it", + "type": "object", + "required": [ "SourceDetails", "Packages" ], + "additionalProperties": true, + + "properties": { + "SourceDetails": { + "description": "Details about this source", + "type": "object", + "required": [ "Name", "Identifier", "Argument", "Type" ], + "additionalProperties": true, + + "properties": { + "Name": { + "description": "Name of the source", + "type": "string" + }, + + "Identifier": { + "description": "Identifier for the source", + "type": "string" + }, + + "Argument": { + "description": "Argument used to install the source", + "type": "string" + }, + + "Type": { + "description": "Type of the source", + "type": "string" + } + } + }, + + "Packages": { + "description": "Packages installed from this source", + "type": "array", + "required": [ "Id" ], + "minItems": 1, + + "items": { + "description": "A package to be installed from this source", + "type": "object", + "additionalProperties": true, + "properties": { + "Id": { + "description": "Package ID", + "type": "string" + }, + + "Version": { + "description": "Package version", + "type": "string" + }, + + "Channel": { + "description": "Package channel", + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/doc/settings.schema.json b/schemas/JSON/settings/settings.schema.0.2.json similarity index 81% rename from doc/settings.schema.json rename to schemas/JSON/settings/settings.schema.0.2.json index de30f6685a..8cef34e33e 100644 --- a/doc/settings.schema.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -57,11 +57,26 @@ "description": "Enable the upgrade command while it is in development", "type": "boolean", "default": false + }, + "uninstall": { + "description": "Enable the uninstall command while it is in development", + "type": "boolean", + "default": false + }, + "import": { + "description": "Enable the import command while it is in development", + "type": "boolean", + "default": false + }, + "restSource": { + "description": "Enable the rest source support while it is in development", + "type": "boolean", + "default": false } } } }, - "allOf": [ + "allOf": [ { "properties": { "visual": { "$ref": "#/definitions/Visual" } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index ee39aea80c..6f699945ef 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -19,7 +19,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project", "Project", "{8D53 ..\cgmanifest.json = ..\cgmanifest.json ..\README.md = ..\README.md ..\doc\Settings.md = ..\doc\Settings.md - ..\doc\settings.schema.json = ..\doc\settings.schema.json EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "catch2", "catch2\catch2.vcxitems", "{5295E21E-9868-4DE2-A177-FBB97B36579B}" @@ -63,13 +62,42 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinGetYamlFuzzing", "WinGet EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalhostWebServer", "LocalhostWebServer\LocalhostWebServer.csproj", "{3BAF989F-7F65-465B-ACE8-BAFE42D1017E}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Valijson", "Valijson\Valijson.vcxitems", "{358BC478-0624-4AD1-A933-0422B5292AF8}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ManifestSchema", "ManifestSchema\ManifestSchema.vcxitems", "{7D05F64D-CE5A-42AA-A2C1-E91458F061CF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinGetSchemas", "WinGetSchemas\WinGetSchemas.vcxitems", "{952B513F-8A00-4D74-9271-925AFB3C6252}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "spelling", "spelling", "{2ACDE176-F13F-42FA-8159-C34FA3D37837}" + ProjectSection(SolutionItems) = preProject + ..\.github\actions\spelling\allow.txt = ..\.github\actions\spelling\allow.txt + ..\.github\actions\spelling\excludes.txt = ..\.github\actions\spelling\excludes.txt + ..\.github\actions\spelling\expect.txt = ..\.github\actions\spelling\expect.txt + ..\.github\actions\spelling\patterns.txt = ..\.github\actions\spelling\patterns.txt + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cpprestsdk", "cpprestsdk\cpprestsdk.vcxproj", "{866C3F06-636F-4BE8-BC24-5F86ECC606A1}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + ManifestSchema\ManifestSchema.vcxitems*{1622da16-914f-4f57-a259-d5169003cc8c}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{1c6e0108-2860-4b17-9f7e-fa5c6c1f3d3d}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{1c6e0108-2860-4b17-9f7e-fa5c6c1f3d3d}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{358bc478-0624-4ad1-a933-0422b5292af8}*SharedItemsImports = 9 catch2\catch2.vcxitems*{5295e21e-9868-4de2-a177-fbb97b36579b}*SharedItemsImports = 9 + ManifestSchema\ManifestSchema.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 binver\binver.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 binver\binver.vcxitems*{6e36ddd7-1602-474e-b1d7-d0a7e1d5ad86}*SharedItemsImports = 9 + ManifestSchema\ManifestSchema.vcxitems*{7d05f64d-ce5a-42aa-a2c1-e91458f061cf}*SharedItemsImports = 9 catch2\catch2.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{952b513f-8a00-4d74-9271-925afb3c6252}*SharedItemsImports = 9 binver\binver.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 + ManifestSchema\ManifestSchema.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM @@ -379,29 +407,41 @@ Global {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|x64.ActiveCfg = Fuzzing|x64 {1622DA16-914F-4F57-A259-D5169003CC8C}.Release|x86.ActiveCfg = Fuzzing|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM.ActiveCfg = Debug|x86 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM.Build.0 = Debug|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM64.ActiveCfg = Debug|x86 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|ARM64.Build.0 = Debug|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x64.ActiveCfg = Debug|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x64.Build.0 = Debug|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x86.ActiveCfg = Debug|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Debug|x86.Build.0 = Debug|x86 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM.ActiveCfg = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM.Build.0 = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM64.ActiveCfg = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM64.Build.0 = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x64.ActiveCfg = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x64.Build.0 = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x86.ActiveCfg = Debug|Any CPU - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x86.Build.0 = Debug|Any CPU + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM.ActiveCfg = Release|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|ARM64.ActiveCfg = Release|x86 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x64.ActiveCfg = Release|x64 + {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Fuzzing|x86.ActiveCfg = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM.ActiveCfg = Release|x86 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM.Build.0 = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM64.ActiveCfg = Release|x86 - {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|ARM64.Build.0 = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x64.ActiveCfg = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x64.Build.0 = Release|x64 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x86.ActiveCfg = Release|x86 {3BAF989F-7F65-465B-ACE8-BAFE42D1017E}.Release|x86.Build.0 = Release|x86 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|ARM.ActiveCfg = Debug|ARM + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|ARM.Build.0 = Debug|ARM + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|ARM64.Build.0 = Debug|ARM64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|x64.ActiveCfg = Debug|x64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|x64.Build.0 = Debug|x64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|x86.ActiveCfg = Debug|Win32 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Debug|x86.Build.0 = Debug|Win32 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Fuzzing|ARM.ActiveCfg = Release|ARM + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Fuzzing|ARM64.ActiveCfg = Release|ARM64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Fuzzing|x64.ActiveCfg = Release|x64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Fuzzing|x86.ActiveCfg = Release|Win32 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|ARM.ActiveCfg = Release|ARM + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|ARM.Build.0 = Release|ARM + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|ARM64.ActiveCfg = Release|ARM64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|ARM64.Build.0 = Release|ARM64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|x64.ActiveCfg = Release|x64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|x64.Build.0 = Release|x64 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|x86.ActiveCfg = Release|Win32 + {866C3F06-636F-4BE8-BC24-5F86ECC606A1}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -414,6 +454,11 @@ Global {82B39FDA-E86B-4713-A873-9D56DE00247A} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {1622DA16-914F-4F57-A259-D5169003CC8C} = {6D7776A8-42FE-46DD-B0F8-712F35EA0C79} {3BAF989F-7F65-465B-ACE8-BAFE42D1017E} = {EA8CD934-0702-4911-A2C5-A40600E616DE} + {358BC478-0624-4AD1-A933-0422B5292AF8} = {60618CAC-2995-4DF9-9914-45C6FC02C995} + {7D05F64D-CE5A-42AA-A2C1-E91458F061CF} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {952B513F-8A00-4D74-9271-925AFB3C6252} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {2ACDE176-F13F-42FA-8159-C34FA3D37837} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {866C3F06-636F-4BE8-BC24-5F86ECC606A1} = {60618CAC-2995-4DF9-9914-45C6FC02C995} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B6FDB70C-A751-422C-ACD1-E35419495857} diff --git a/src/AppInstallerCLI/AppInstallerCLI.vcxproj b/src/AppInstallerCLI/AppInstallerCLI.vcxproj index 43c670c710..541c8cd1eb 100644 --- a/src/AppInstallerCLI/AppInstallerCLI.vcxproj +++ b/src/AppInstallerCLI/AppInstallerCLI.vcxproj @@ -70,6 +70,8 @@ + + @@ -140,7 +142,7 @@ Console false - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) $(ProjectDir)..\manifest\shared.manifest %(AdditionalManifestFiles) @@ -192,7 +194,7 @@ true true false - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) $(ProjectDir)..\manifest\shared.manifest %(AdditionalManifestFiles) @@ -225,6 +227,9 @@ {5eb88068-5fb9-4e69-89b2-72dbc5e068f9} + + {866c3f06-636f-4be8-bc24-5f86ecc606a1} + {82b39fda-e86b-4713-a873-9d56de00247a} diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index a4ae59e9c1..d99b7b790f 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -68,7 +68,10 @@ - + + + + @@ -122,9 +125,9 @@ Disabled _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -139,7 +142,7 @@ WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true @@ -152,10 +155,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -177,6 +180,8 @@ + + @@ -185,11 +190,13 @@ + + @@ -200,22 +207,28 @@ + + + + + + @@ -224,6 +237,7 @@ + @@ -238,12 +252,14 @@ + + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 20d1285f43..784c9210aa 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -9,10 +9,6 @@ {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;xsd - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - {4b0dcf8b-b4a1-47e5-9c28-e8a3440178e6} @@ -72,9 +68,6 @@ Header Files - - Header Files - Header Files @@ -135,6 +128,30 @@ Commands + + Commands + + + Workflows + + + Commands + + + Header Files + + + Workflows + + + Commands + + + Header Files + + + Header Files + @@ -236,6 +253,24 @@ Commands + + Commands + + + Workflows + + + Commands + + + Source Files + + + Workflows + + + Commands + diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp index d737e39abb..9dc2013a80 100644 --- a/src/AppInstallerCLICore/ChannelStreams.cpp +++ b/src/AppInstallerCLICore/ChannelStreams.cpp @@ -85,6 +85,13 @@ namespace AppInstaller::CLI::Execution return *this; } + OutputStream& OutputStream::operator<<(const std::filesystem::path& path) + { + ApplyFormat(); + m_out << path.u8string(); + return *this; + } + NoVTStream::NoVTStream(std::ostream& out, bool enabled) : m_out(out, enabled, false) {} diff --git a/src/AppInstallerCLICore/ChannelStreams.h b/src/AppInstallerCLICore/ChannelStreams.h index 22a234442a..d3b7df2315 100644 --- a/src/AppInstallerCLICore/ChannelStreams.h +++ b/src/AppInstallerCLICore/ChannelStreams.h @@ -100,6 +100,7 @@ namespace AppInstaller::CLI::Execution OutputStream& operator<<(std::ostream& (__cdecl* f)(std::ostream&)); OutputStream& operator<<(const VirtualTerminal::Sequence& sequence); OutputStream& operator<<(const VirtualTerminal::ConstructedSequence& sequence); + OutputStream& operator<<(const std::filesystem::path& path); private: // Applies the format for the stream. diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.cpp b/src/AppInstallerCLICore/Commands/ExportCommand.cpp new file mode 100644 index 0000000000..15d0923281 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ExportCommand.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ExportCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/ImportExportFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + std::vector ExportCommand::GetArguments() const + { + return { + Argument{ "output", 'o', Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "source", 's', Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, + Argument{ "include-versions", Argument::NoAlias, Execution::Args::Type::IncludeVersions, Resource::String::ExportIncludeVersionsArgumentDescription, ArgumentType::Flag }, + }; + } + + Resource::LocString ExportCommand::ShortDescription() const + { + return { Resource::String::ExportCommandShortDescription }; + } + + Resource::LocString ExportCommand::LongDescription() const + { + return { Resource::String::ExportCommandLongDescription }; + } + + void ExportCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + { + if (valueType == Execution::Args::Type::OutputFile) + { + // Intentionally output nothing to allow pass through to filesystem. + return; + } + + if (valueType == Execution::Args::Type::Source) + { + context << Workflow::CompleteSourceName; + return; + } + } + + std::string ExportCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-export"; + } + + void ExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) << + Workflow::SearchSourceForMany << + Workflow::EnsureMatchesFromSearchResult(true) << + Workflow::SelectVersionsToExport << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << + Workflow::WriteImportFile; + } +} diff --git a/src/AppInstallerCLICore/Commands/ExportCommand.h b/src/AppInstallerCLICore/Commands/ExportCommand.h new file mode 100644 index 0000000000..0dcbb8fcbe --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ExportCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + // Command to get the set of installed packages on the system. + struct ExportCommand final : public Command + { + ExportCommand(std::string_view parent) : Command("export", parent, Settings::ExperimentalFeature::Feature::ExperimentalExport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp new file mode 100644 index 0000000000..7d7c9c2a19 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ImportCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/ImportExportFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace std::string_view_literals; + + std::vector ImportCommand::GetArguments() const + { + return { + Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, + Argument{ "ignore-versions", Argument::NoAlias, Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag }, + }; + } + + Resource::LocString ImportCommand::ShortDescription() const + { + return { Resource::String::ImportCommandShortDescription }; + } + + Resource::LocString ImportCommand::LongDescription() const + { + return { Resource::String::ImportCommandLongDescription }; + } + + std::string ImportCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-import"; + } + + void ImportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) << + Workflow::VerifyFile(Execution::Args::Type::ImportFile) << + Workflow::ReadImportFile << + Workflow::OpenSourcesForImport << + Workflow::OpenPredefinedSource(Repository::PredefinedSource::Installed) << + Workflow::SearchPackagesForImport << + Workflow::ReportExecutionStage(Workflow::ExecutionStage::Execution) << + Workflow::InstallMultiple; + } +} diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.h b/src/AppInstallerCLICore/Commands/ImportCommand.h new file mode 100644 index 0000000000..d9c75c3446 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ImportCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + // Command to install a set of packages from a list. + struct ImportCommand final : public Command + { + ImportCommand(std::string_view parent) : Command("import", parent, Settings::ExperimentalFeature::Feature::ExperimentalImport) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + std::string HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 157f9676bc..a900ac1ce7 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -99,15 +99,6 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::GetManifest << - Workflow::EnsureMinOSVersion << - Workflow::SelectInstaller << - Workflow::EnsureApplicableInstaller << - Workflow::ShowInstallationDisclaimer << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - Workflow::DownloadInstaller << - Workflow::ReportExecutionStage(ExecutionStage::Execution) << - Workflow::ExecuteInstaller << - Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << - Workflow::RemoveInstaller; + Workflow::InstallPackageVersion; } } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp index b84d79f291..aa34ba601e 100644 --- a/src/AppInstallerCLICore/Commands/RootCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -9,12 +9,15 @@ #include "SearchCommand.h" #include "ListCommand.h" #include "UpgradeCommand.h" +#include "UninstallCommand.h" #include "HashCommand.h" #include "ValidateCommand.h" #include "SettingsCommand.h" #include "FeaturesCommand.h" #include "ExperimentalCommand.h" #include "CompleteCommand.h" +#include "ExportCommand.h" +#include "ImportCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -32,12 +35,15 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), + std::make_unique(FullName()), }); } diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp index 632706c88a..8fbd4f5b24 100644 --- a/src/AppInstallerCLICore/Commands/ShowCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -71,6 +71,7 @@ namespace AppInstaller::CLI { context << Workflow::GetManifest << + Workflow::ReportManifestIdentity << Workflow::SelectInstaller << Workflow::ShowManifestInfo; } diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp new file mode 100644 index 0000000000..df84be4b78 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "UninstallCommand.h" +#include "Workflows/UninstallFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +using AppInstaller::CLI::Execution::Args; +using AppInstaller::CLI::Workflow::ExecutionStage; + +namespace AppInstaller::CLI +{ + std::vector UninstallCommand::GetArguments() const + { + // TODO: determine exact arguments needed + return + { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::Manifest), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Moniker), + Argument::ForType(Args::Type::Version), + Argument::ForType(Args::Type::Channel), + Argument::ForType(Args::Type::Source), + Argument::ForType(Args::Type::Exact), + Argument::ForType(Args::Type::Interactive), + Argument::ForType(Args::Type::Silent), + Argument::ForType(Args::Type::Log), + }; + } + + Resource::LocString UninstallCommand::ShortDescription() const + { + return { Resource::String::UninstallCommandShortDescription }; + } + + Resource::LocString UninstallCommand::LongDescription() const + { + return { Resource::String::UninstallCommandLongDescription }; + } + + void UninstallCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + { + if (valueType == Execution::Args::Type::Manifest || + valueType == Execution::Args::Type::Log) + { + // Intentionally output nothing to allow pass through to filesystem. + return; + } + + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + switch (valueType) + { + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Version: + case Execution::Args::Type::Channel: + case Execution::Args::Type::Source: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); + break; + } + } + + std::string UninstallCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-uninstall"; + } + + void UninstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::Manifest) && + (execArgs.Contains(Execution::Args::Type::Query) || + execArgs.Contains(Execution::Args::Type::Id) || + execArgs.Contains(Execution::Args::Type::Name) || + execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::Version) || + execArgs.Contains(Execution::Args::Type::Channel) || + execArgs.Contains(Execution::Args::Type::Source) || + execArgs.Contains(Execution::Args::Type::Exact))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + } + } + + void UninstallCommand::ExecuteInternal(Execution::Context& context) const + { + // open the sources where to search for the package + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + // find the uninstaller + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + // --manifest case where new manifest is provided + context << + Workflow::GetManifestFromArg << + Workflow::ReportManifestIdentity << + Workflow::SearchSourceUsingManifest << + Workflow::EnsureOneMatchFromSearchResult(true); + } + else + { + // search for a single package to uninstall + context << + Workflow::SearchSourceForSingle << + Workflow::EnsureOneMatchFromSearchResult(true) << + Workflow::ReportPackageIdentity; + } + + context << + Workflow::GetInstalledPackageVersion << + Workflow::GetUninstallInfo << + Workflow::ReportExecutionStage(ExecutionStage::Execution) << + Workflow::ExecuteUninstaller << + Workflow::ReportExecutionStage(ExecutionStage::PostExecution); + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.h b/src/AppInstallerCLICore/Commands/UninstallCommand.h new file mode 100644 index 0000000000..29ad34246a --- /dev/null +++ b/src/AppInstallerCLICore/Commands/UninstallCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct UninstallCommand final : public Command + { + UninstallCommand(std::string_view parent) : Command("uninstall", parent, Settings::ExperimentalFeature::Feature::ExperimentalUninstall) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp index d2fcbd58bd..a42353a5f8 100644 --- a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -126,8 +126,8 @@ namespace AppInstaller::CLI context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << - OpenSource << - OpenCompositeSource(Repository::PredefinedSource::Installed); + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); if (ShouldListUpgrade(context)) { @@ -150,21 +150,13 @@ namespace AppInstaller::CLI // --manifest case where new manifest is provided context << GetManifestFromArg << - ReportManifestIdentity << SearchSourceUsingManifest << EnsureOneMatchFromSearchResult(true) << GetInstalledPackageVersion << EnsureUpdateVersionApplicable << - EnsureMinOSVersion << SelectInstaller << EnsureApplicableInstaller << - ShowInstallationDisclaimer << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - DownloadInstaller << - Workflow::ReportExecutionStage(ExecutionStage::Execution) << - ExecuteInstaller << - Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << - RemoveInstaller; + InstallPackageInstaller; } else { @@ -172,7 +164,6 @@ namespace AppInstaller::CLI context << SearchSourceForSingle << EnsureOneMatchFromSearchResult(true) << - ReportPackageIdentity << GetInstalledPackageVersion; if (context.Args.Contains(Execution::Args::Type::Version)) @@ -181,7 +172,6 @@ namespace AppInstaller::CLI context << GetManifestFromPackage << EnsureUpdateVersionApplicable << - EnsureMinOSVersion << SelectInstaller << EnsureApplicableInstaller; } @@ -192,14 +182,7 @@ namespace AppInstaller::CLI context << SelectLatestApplicableUpdate(true); } - context << - ShowInstallationDisclaimer << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - DownloadInstaller << - Workflow::ReportExecutionStage(ExecutionStage::Execution) << - ExecuteInstaller << - Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << - RemoveInstaller; + context << InstallPackageInstaller; } } } diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp index 39b27e64ca..8955e13de2 100644 --- a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp @@ -34,7 +34,7 @@ namespace AppInstaller::CLI void ValidateCommand::ExecuteInternal(Execution::Context& context) const { context << - Workflow::VerifyFile(Execution::Args::Type::ValidateManifest) << + Workflow::VerifyPath(Execution::Args::Type::ValidateManifest) << [](Execution::Context& context) { auto inputFile = Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::ValidateManifest)); diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 0d0191187f..eb0e67fcea 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -55,6 +55,15 @@ namespace AppInstaller::CLI::Execution CommandLine, Position, + // Export Command + OutputFile, + IncludeVersions, + + // Import Command + ImportFile, + IgnoreUnavailable, + IgnoreVersions, + // Other All, // Used in Update command to update all installed packages to latest Force, // Generic flag to enable a command to skip some check diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index ac04c51495..f442a8189f 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -2,33 +2,30 @@ // Licensed under the MIT License. #pragma once #include -#include -#include -#include #include "ExecutionReporter.h" #include "ExecutionArgs.h" +#include "ExecutionContextData.h" #include "CompletionData.h" -#include -#include -#include -#include -#include -#include +#include // Terminates the Context with some logging to indicate the location. // Also returns from the current function. -#define AICLI_TERMINATE_CONTEXT_ARGS(_context_,_hr_) \ +#define AICLI_TERMINATE_CONTEXT_ARGS(_context_,_hr_,_ret_) \ do { \ HRESULT AICLI_TERMINATE_CONTEXT_ARGS_hr = _hr_; \ _context_.Terminate(AICLI_TERMINATE_CONTEXT_ARGS_hr, __FILE__, __LINE__); \ - return; \ + return _ret_; \ } while(0,0) // Terminates the Context named 'context' with some logging to indicate the location. // Also returns from the current function. -#define AICLI_TERMINATE_CONTEXT(_hr_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_) +#define AICLI_TERMINATE_CONTEXT(_hr_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_,) + +// Terminates the Context named 'context' with some logging to indicate the location. +// Also returns the specified value from the current function. +#define AICLI_TERMINATE_CONTEXT_RETURN(_hr_,_ret_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_,_ret_) namespace AppInstaller::CLI::Workflow { @@ -38,28 +35,6 @@ namespace AppInstaller::CLI::Workflow namespace AppInstaller::CLI::Execution { - // Names a piece of data stored in the context by a workflow step. - // Must start at 0 to enable direct access to variant in Context. - // Max must be last and unused. - enum class Data : size_t - { - Source, - SearchResult, - SourceList, - Package, - Manifest, - PackageVersion, - Installer, - HashPair, - InstallerPath, - LogPath, - InstallerArgs, - CompletionData, - InstalledPackageVersion, - ExecutionStage, - Max - }; - // bit masks used as Context flags enum class ContextFlag : int { @@ -71,113 +46,10 @@ namespace AppInstaller::CLI::Execution DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); - namespace details - { - template - struct DataMapping - { - // value_t type specifies the type of this data - }; - - template <> - struct DataMapping - { - using value_t = std::shared_ptr; - }; - - template <> - struct DataMapping - { - using value_t = Repository::SearchResult; - }; - - template <> - struct DataMapping - { - using value_t = std::vector; - }; - - template <> - struct DataMapping - { - using value_t = std::shared_ptr; - }; - - template <> - struct DataMapping - { - using value_t = Manifest::Manifest; - }; - - template <> - struct DataMapping - { - using value_t = std::shared_ptr; - }; - - template <> - struct DataMapping - { - using value_t = std::optional; - }; - - template <> - struct DataMapping - { - using value_t = std::pair, std::vector>; - }; - - template <> - struct DataMapping - { - using value_t = std::filesystem::path; - }; - - template <> - struct DataMapping - { - using value_t = std::filesystem::path; - }; - - template <> - struct DataMapping - { - using value_t = std::string; - }; - - template <> - struct DataMapping - { - using value_t = CLI::CompletionData; - }; - - template <> - struct DataMapping - { - using value_t = std::shared_ptr; - }; - - template <> - struct DataMapping - { - using value_t = Workflow::ExecutionStage; - }; - - // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. - template - inline auto Deduce(std::index_sequence) { return std::variant(I)>::value_t...>{}; } - - // Holds data of any type listed in a DataMapping. - using DataVariant = decltype(Deduce(std::make_index_sequence(Data::Max)>())); - - // Gets the index into the variant for the given Data. - constexpr inline size_t DataIndex(Data d) { return static_cast(d) + 1; } - } - // The context within which all commands execute. // Contains input/output via Execution::Reporter and // arguments via Execution::Args. - struct Context + struct Context : EnumBasedVariantMap { Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} @@ -211,31 +83,6 @@ namespace AppInstaller::CLI::Execution // Set the context to the terminated state. void Terminate(HRESULT hr, std::string_view file = {}, size_t line = {}); - // Adds a value to the context data, or overwrites an existing entry. - // This must be used to create the initial data entry, but Get can be used to modify. - template - void Add(typename details::DataMapping::value_t&& v) - { - m_data[D].emplace(std::forward::value_t>(v)); - } - template - void Add(const typename details::DataMapping::value_t& v) - { - m_data[D].emplace(v); - } - - // Return a value indicating whether the given data type is stored in the context. - bool Contains(Data d) { return (m_data.find(d) != m_data.end()); } - - // Gets context data; which can be modified in place. - template - typename details::DataMapping::value_t& Get() - { - auto itr = m_data.find(D); - THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), itr == m_data.end(), "Get(%d)", D); - return std::get(itr->second); - } - // Gets context flags ContextFlag GetFlags() const { @@ -263,7 +110,6 @@ namespace AppInstaller::CLI::Execution DestructionToken m_disableCtrlHandlerOnExit = false; bool m_isTerminated = false; HRESULT m_terminationHR = S_OK; - std::map m_data; size_t m_CtrlSignalCount = 0; ContextFlag m_flags = ContextFlag::None; }; diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h new file mode 100644 index 0000000000..7349c6fe16 --- /dev/null +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include +#include "CompletionData.h" +#include "PackageCollection.h" +#include "Workflows/WorkflowBase.h" + +#include +#include +#include +#include +#include +#include + + +namespace AppInstaller::CLI::Execution +{ + // Names a piece of data stored in the context by a workflow step. + // Must start at 0 to enable direct access to variant in Context. + // Max must be last and unused. + enum class Data : size_t + { + Source, + SearchResult, + SourceList, + Package, + Manifest, + PackageVersion, + Installer, + HashPair, + InstallerPath, + LogPath, + InstallerArgs, + CompletionData, + InstalledPackageVersion, + ExecutionStage, + UninstallString, + PackageFamilyNames, + ProductCodes, + // On export: A collection of packages to be exported to a file + // On import: A collection of packages read from a file + PackageCollection, + // On import: A collection of specific package versions to install + PackagesToInstall, + // On import: Sources for the imported packages + Sources, + ARPSnapshot, + Max + }; + + namespace details + { + template + struct DataMapping + { + // value_t type specifies the type of this data + }; + + template <> + struct DataMapping + { + using value_t = std::shared_ptr; + }; + + template <> + struct DataMapping + { + using value_t = Repository::SearchResult; + }; + + template <> + struct DataMapping + { + using value_t = std::vector; + }; + + template <> + struct DataMapping + { + using value_t = std::shared_ptr; + }; + + template <> + struct DataMapping + { + using value_t = Manifest::Manifest; + }; + + template <> + struct DataMapping + { + using value_t = std::shared_ptr; + }; + + template <> + struct DataMapping + { + using value_t = std::optional; + }; + + template <> + struct DataMapping + { + using value_t = std::pair, std::vector>; + }; + + template <> + struct DataMapping + { + using value_t = std::filesystem::path; + }; + + template <> + struct DataMapping + { + using value_t = std::filesystem::path; + }; + + template <> + struct DataMapping + { + using value_t = std::string; + }; + + template <> + struct DataMapping + { + using value_t = CLI::CompletionData; + }; + + template <> + struct DataMapping + { + using value_t = std::shared_ptr; + }; + + template <> + struct DataMapping + { + using value_t = Workflow::ExecutionStage; + }; + + template <> + struct DataMapping + { + using value_t = std::string; + }; + + template <> + struct DataMapping + { + using value_t = std::vector; + }; + + template <> + struct DataMapping + { + using value_t = std::vector; + }; + + template <> + struct DataMapping + { + using value_t = CLI::PackageCollection; + }; + + template <> + struct DataMapping + { + using value_t = std::vector>; + }; + + template <> + struct DataMapping + { + using value_t = std::vector>; + }; + + template <> + struct DataMapping + { + // Contains the { Id, Version, Channel } + using value_t = std::vector>; + }; + } +} diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index 609f04db54..fef6d891f3 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -133,11 +133,13 @@ namespace AppInstaller::CLI::Execution { void ProgressVisualizerBase::ApplyStyle(size_t i, size_t max, bool enabled) { + if (!UseVT()) + { + // Either no style set or VT disabled + return; + } switch (m_style) { - case VisualStyle::NoVT: - // No VT means no style set - break; case VisualStyle::Retro: if (enabled) { diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp index 184ba2cb58..4e4bc298aa 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.cpp +++ b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -32,6 +32,10 @@ namespace AppInstaller::CLI::Execution Reporter::Reporter(const Reporter& other, clone_t) : Reporter(other.m_out, other.m_in) { + if (other.m_style.has_value()) + { + SetStyle(*other.m_style); + } } OutputStream Reporter::GetOutputStream(Level level) @@ -78,6 +82,7 @@ namespace AppInstaller::CLI::Execution void Reporter::SetStyle(VisualStyle style) { + m_style = style; if (m_spinner) { m_spinner->SetStyle(style); diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h index 559fdc3979..16ae7ba327 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.h +++ b/src/AppInstallerCLICore/ExecutionReporter.h @@ -131,6 +131,7 @@ namespace AppInstaller::CLI::Execution std::ostream& m_out; std::istream& m_in; bool m_isVTEnabled = true; + std::optional m_style; std::optional m_spinner; std::optional m_progressBar; wil::srwlock m_progressCallbackLock; diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp new file mode 100644 index 0000000000..072bb05ffd --- /dev/null +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" + +#include "PackageCollection.h" + +#include "AppInstallerRuntime.h" +#include "winget/JsonSchemaValidation.h" + +#include "PackagesSchema.h" + +#include +#include + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::Repository; + + namespace + { + // Strings used in the Packages JSON file. + // Most will be used to access a JSON value, so they need to be std::string + const std::string s_PackagesJson_Schema = "$schema"; + const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; + const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; + const std::string s_PackagesJson_CreationDate = "CreationDate"; + + const std::string s_PackagesJson_Sources = "Sources"; + const std::string s_PackagesJson_Source_Details = "SourceDetails"; + const std::string s_PackagesJson_Source_Name = "Name"; + const std::string s_PackagesJson_Source_Identifier = "Identifier"; + const std::string s_PackagesJson_Source_Argument = "Argument"; + const std::string s_PackagesJson_Source_Type = "Type"; + + const std::string s_PackagesJson_Packages = "Packages"; + const std::string s_PackagesJson_Package_Id = "Id"; + const std::string s_PackagesJson_Package_Version = "Version"; + const std::string s_PackagesJson_Package_Channel = "Channel"; + + // Gets or creates a property of a JSON object by its name. + Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName, Json::ValueType valueType) + { + if (!node.isMember(propertyName)) + { + node[propertyName] = Json::Value{ valueType }; + } + else + { + THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != valueType); + } + + return node[propertyName]; + } + + // Reads the description of a package from a Package node in the JSON. + PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) + { + std::string id = packageNode[s_PackagesJson_Package_Id].asString(); + std::string version = packageNode.isMember(s_PackagesJson_Package_Version) ? packageNode[s_PackagesJson_Package_Version].asString() : ""; + std::string channel = packageNode.isMember(s_PackagesJson_Package_Channel) ? packageNode[s_PackagesJson_Package_Channel].asString() : ""; + + PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } }; + + return package; + } + + // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON. + PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode) + { + SourceDetails sourceDetails; + auto& detailsNode = sourceNode[s_PackagesJson_Source_Details]; + sourceDetails.Identifier = Utility::LocIndString{ detailsNode[s_PackagesJson_Source_Identifier].asString() }; + sourceDetails.Name = detailsNode[s_PackagesJson_Source_Name].asString(); + sourceDetails.Arg = detailsNode[s_PackagesJson_Source_Argument].asString(); + sourceDetails.Type = detailsNode[s_PackagesJson_Source_Type].asString(); + + PackageCollection::Source source{ std::move(sourceDetails) }; + for (const auto& packageNode : sourceNode[s_PackagesJson_Packages]) + { + source.Packages.emplace_back(ParsePackageNode(packageNode)); + } + + return source; + } + + // Creates a minimal root object of a Packages JSON file. + Json::Value CreateRoot(const std::string& wingetVersion) + { + Json::Value root{ Json::ValueType::objectValue }; + root[s_PackagesJson_WinGetVersion] = wingetVersion; + root[s_PackagesJson_Schema] = s_PackagesJson_SchemaUri_v1_0; + + // TODO: This uses localtime. Do we want to use UTC or add time zone? + std::stringstream currentTimeStream; + Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now()); + root[s_PackagesJson_CreationDate] = currentTimeStream.str(); + + return root; + } + + // Adds a new Package node to a Source node in the Json file, and returns it. + Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageCollection::Package& package) + { + Json::Value packageNode{ Json::ValueType::objectValue }; + packageNode[s_PackagesJson_Package_Id] = package.Id.get(); + + // Only add version and channel if present. + // Packages may not have a channel, or versions may not have been requested. + const std::string& version = package.VersionAndChannel.GetVersion().ToString(); + if (!version.empty()) + { + packageNode[s_PackagesJson_Package_Version] = version; + } + + const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); + if (!channel.empty()) + { + packageNode[s_PackagesJson_Package_Channel] = channel; + } + + return sourceNode[s_PackagesJson_Packages].append(std::move(packageNode)); + } + + // Adds a new Source node to the JSON, and returns it. + Json::Value& AddSourceNode(Json::Value& root, const PackageCollection::Source& source) + { + Json::Value sourceNode{ Json::ValueType::objectValue }; + + Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; + sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; + sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; + sourceDetailsNode[s_PackagesJson_Source_Identifier] = source.Details.Identifier; + sourceDetailsNode[s_PackagesJson_Source_Type] = source.Details.Type; + sourceNode[s_PackagesJson_Source_Details] = std::move(sourceDetailsNode); + + sourceNode[s_PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue }; + + auto& sourcesNode = GetJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); + for (const auto& package : source.Packages) + { + AddPackageToSource(sourceNode, package); + } + + return sourcesNode.append(std::move(sourceNode)); + } + } + + namespace PackagesJson + { + Json::Value CreateJson(const PackageCollection& packages) + { + Json::Value root = CreateRoot(packages.ClientVersion); + for (const auto& source : packages.Sources) + { + AddSourceNode(root, source); + } + + return root; + } + + ParseResult TryParseJson(const Json::Value& root) + { + // Find the schema used for the JSON + if (!(root.isObject() && root.isMember(s_PackagesJson_Schema) && root[s_PackagesJson_Schema].isString())) + { + AICLI_LOG(CLI, Error, << "Import file is missing \"" << s_PackagesJson_Schema << "\" property"); + return ParseResult{ ParseResult::Type::MissingSchema }; + } + + const auto& schemaUri = root[s_PackagesJson_Schema].asString(); + Json::Value schemaJson; + if (schemaUri == s_PackagesJson_SchemaUri_v1_0) + { + schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V1), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE)); + } + else + { + AICLI_LOG(CLI, Error, << "Unrecognized schema for import file: " << schemaUri); + return ParseResult{ ParseResult::Type::UnrecognizedSchema }; + } + + // Validate the JSON against the schema. + valijson::Schema schema; + JsonSchema::PopulateSchema(schemaJson, schema); + + valijson::ValidationResults results; + if (!JsonSchema::Validate(schema, root, results)) + { + return ParseResult{ ParseResult::Type::SchemaValidationFailed, JsonSchema::GetErrorStringFromResults(results) }; + } + + // Extract the data from the JSON. + PackageCollection packages; + packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); + for (const auto& sourceNode : root[s_PackagesJson_Sources]) + { + auto newSource = ParseSourceNode(sourceNode); + auto existingSource = std::find_if(packages.Sources.begin(), packages.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; }); + if (existingSource == packages.Sources.end()) + { + packages.Sources.push_back(std::move(newSource)); + } + else + { + existingSource->Packages.insert(existingSource->Packages.end(), newSource.Packages.begin(), newSource.Packages.end()); + } + } + + return ParseResult{ std::move(packages) }; + } + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h new file mode 100644 index 0000000000..3dd53e7161 --- /dev/null +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#include "AppInstallerDateTime.h" +#include "AppInstallerLanguageUtilities.h" +#include "AppInstallerRepositorySource.h" + +#include + +#include + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::Repository; + + // Container for data to identify multiple packages to be installed from multiple sources. + struct PackageCollection + { + // Description of a package. + // Does not represent the actual package, just enough to find and install it. + struct Package + { + Package() = default; + Package(Utility::LocIndString&& id) : + Id(std::move(id)) {} + Package(Utility::LocIndString&& id, Utility::Version&& version, Utility::Channel&& channel) : + Id(std::move(id)), VersionAndChannel(std::move(version), std::move(channel)) {} + Package(Utility::LocIndString&& id, Utility::VersionAndChannel&& versionAndChannel) : + Id(std::move(id)), VersionAndChannel(std::move(versionAndChannel)) {} + + Utility::LocIndString Id; + Utility::VersionAndChannel VersionAndChannel; + }; + + // A source along with a set of packages available from it. + struct Source + { + Source() = default; + Source(const SourceDetails& sourceDetails) : Details(sourceDetails) {} + Source(SourceDetails&& sourceDetails) : Details(std::move(sourceDetails)) {} + + SourceDetails Details; + std::vector Packages; + }; + + // Version of the WinGet client that produced this collection. + std::string ClientVersion; + + // Requests from each individual source. + std::vector Sources; + }; + + namespace PackagesJson + { + struct ParseResult + { + enum class Type + { + MissingSchema, + UnrecognizedSchema, + SchemaValidationFailed, + Success, + }; + + ParseResult(Type result) : Result(result) {} + ParseResult(Type result, std::string_view errors) : Result(result), Errors(errors) {} + ParseResult(PackageCollection&& packages) : Result(Type::Success), Packages(std::move(packages)) {} + + Type Result; + PackageCollection Packages; + std::string Errors; + }; + + // Converts a collection of packages to its JSON representation for exporting. + Json::Value CreateJson(const PackageCollection& packages); + + // Tries to parse a JSON into a collection of packages. + ParseResult TryParseJson(const Json::Value& root); + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 3258173277..7e63e21377 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -48,6 +48,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportIncludeVersionsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ExportSourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExtraPositionalError); WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription); @@ -68,12 +72,27 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(HelpForDetails); WINGET_DEFINE_RESOURCE_STRINGID(HelpLinkPreamble); WINGET_DEFINE_RESOURCE_STRINGID(IdArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportFileHasInvalidSchema); + WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnorePackageVersionsArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ImportPackageAlreadyInstalled); + WINGET_DEFINE_RESOURCE_STRINGID(ImportSearchFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ImportSourceNotInstalled); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2); WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimerMSStore); WINGET_DEFINE_RESOURCE_STRINGID(InstallationRequiresHigherWindows); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageNotAvailable); + WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageVersionNotAvailable); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerBlockedByPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedVirusScan); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchAdminBlock); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverridden); WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashMismatchOverrideRequired); @@ -84,6 +103,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InvalidAliasError); WINGET_DEFINE_RESOURCE_STRINGID(InvalidArgumentSpecifierError); + WINGET_DEFINE_RESOURCE_STRINGID(InvalidJsonFile); WINGET_DEFINE_RESOURCE_STRINGID(InvalidNameError); WINGET_DEFINE_RESOURCE_STRINGID(LanguageArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LicenseAgreement); @@ -104,13 +124,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreAppBlocked); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNetworkError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNoStoreAccount); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementServerError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementSuccess); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreStoreClientBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallTryGetEntitlement); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreStoreClientBlocked); WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); @@ -118,11 +138,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); + WINGET_DEFINE_RESOURCE_STRINGID(NoPackagesFoundInImportFile); + WINGET_DEFINE_RESOURCE_STRINGID(NoUninstallInfoFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoSourceDefined); WINGET_DEFINE_RESOURCE_STRINGID(Options); + WINGET_DEFINE_RESOURCE_STRINGID(OutputFileArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Package); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); @@ -177,6 +200,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceNameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenFailedSuggestion); WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenPredefinedFailedSuggestion); + WINGET_DEFINE_RESOURCE_STRINGID(SourceOpenWithFailedUpdate); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); @@ -201,6 +225,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(TooManyArgError); WINGET_DEFINE_RESOURCE_STRINGID(TooManyBehaviorsError); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(UnrecognizedCommand); WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); @@ -214,6 +244,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedIsDirectory); WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VerifyFileSignedMsix); + WINGET_DEFINE_RESOURCE_STRINGID(VerifyPathFailedNotExist); WINGET_DEFINE_RESOURCE_STRINGID(VersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(VersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(WordArgumentDescription); diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index f1d5dda616..436e8616b9 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -149,6 +149,13 @@ namespace AppInstaller::CLI::VirtualTerminal { } + + ConstructedSequence Hyperlink(const std::string& text, const std::string& ref) + { + std::ostringstream result; + result << AICLI_VT_OSC "8;;" << ref << AICLI_VT_ESCAPE << "\\" << text << AICLI_VT_OSC << "8;;" << AICLI_VT_ESCAPE << "\\"; + return ConstructedSequence{ result.str() }; + } } namespace TextModification diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index 5cb1a052af..ecb56d2b8d 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -123,6 +123,8 @@ namespace AppInstaller::CLI::VirtualTerminal { } + + ConstructedSequence Hyperlink(const std::string& text, const std::string& ref); } namespace TextModification diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp new file mode 100644 index 0000000000..bf04ded8e2 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "InstallFlow.h" +#include "ImportExportFlow.h" +#include "UpdateFlow.h" +#include "PackageCollection.h" +#include "WorkflowBase.h" +#include "AppInstallerRepositorySearch.h" + +namespace AppInstaller::CLI::Workflow +{ + using namespace AppInstaller::Repository; + + namespace + { + SourceDetails GetSourceDetails(const SourceDetails& source) + { + return source; + } + + SourceDetails GetSourceDetails(const PackageCollection::Source& source) + { + return source.Details; + } + + SourceDetails GetSourceDetails(const std::shared_ptr& source) + { + return source->GetDetails(); + } + + // Creates a predicate that determines whether a source matches a description in a SourceDetails. + template + std::function GetSourceDetailsEquivalencePredicate(const SourceDetails& details) + { + return [&](const T& source) + { + SourceDetails sourceDetails = GetSourceDetails(source); + return sourceDetails.Type == details.Type && sourceDetails.Identifier == details.Identifier; + }; + } + + // Finds a source equivalent to the one specified. + template + typename std::vector::const_iterator FindSource(const std::vector& sources, const SourceDetails& details) + { + return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate(details)); + } + + // Finds a source equivalent to the one specified. + template + typename std::vector::iterator FindSource(std::vector& sources, const SourceDetails& details) + { + return std::find_if(sources.begin(), sources.end(), GetSourceDetailsEquivalencePredicate(details)); + } + + // Gets the available version of an installed package. + // If requested, checks that the installed version is available and reports a warning if it is not. + std::shared_ptr GetAvailableVersionForInstalledPackage( + Execution::Context& context, + std::shared_ptr package, + std::string_view version, + std::string_view channel, + bool checkVersion) + { + if (!checkVersion) + { + return package->GetLatestAvailableVersion(); + } + + auto availablePackageVersion = package->GetAvailableVersion({ "", version, channel }); + if (!availablePackageVersion) + { + availablePackageVersion = package->GetLatestAvailableVersion(); + if (availablePackageVersion) + { + // Warn installed version is not available. + AICLI_LOG( + CLI, + Info, + << "Installed package version is not available." + << " Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Version [" << version << "], Channel [" << channel << "]" + << ". Found Version [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "], Channel [" << availablePackageVersion->GetProperty(PackageVersionProperty::Version) << "]"); + context.Reporter.Warn() + << Resource::String::InstalledPackageVersionNotAvailable + << ' ' << availablePackageVersion->GetProperty(PackageVersionProperty::Id) + << ' ' << version << ' ' << channel << std::endl; + } + } + + return availablePackageVersion; + } + } + + void SelectVersionsToExport(Execution::Context& context) + { + const auto& searchResult = context.Get(); + const bool includeVersions = context.Args.Contains(Execution::Args::Type::IncludeVersions); + PackageCollection exportedPackages; + exportedPackages.ClientVersion = Runtime::GetClientVersion().get(); + auto& exportedSources = exportedPackages.Sources; + for (const auto& packageMatch : searchResult.Matches) + { + auto installedPackageVersion = packageMatch.Package->GetInstalledVersion(); + auto version = installedPackageVersion->GetProperty(PackageVersionProperty::Version); + auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); + + // Find an available version of this package to determine its source. + auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, version, channel, includeVersions); + if (!availablePackageVersion) + { + // Report package not found and move to next package. + AICLI_LOG(CLI, Warning, << "No available version of package [" << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << "] was found to export"); + context.Reporter.Warn() << Resource::String::InstalledPackageNotAvailable << ' ' << installedPackageVersion->GetProperty(PackageVersionProperty::Name) << std::endl; + continue; + } + + const auto& sourceDetails = availablePackageVersion->GetSource()->GetDetails(); + AICLI_LOG(CLI, Info, + << "Installed package is available. Package Id [" << availablePackageVersion->GetProperty(PackageVersionProperty::Id) << "], Source [" << sourceDetails.Identifier << "]"); + + // Find the exported source for this package + auto sourceItr = FindSource(exportedSources, sourceDetails); + if (sourceItr == exportedSources.end()) + { + exportedSources.emplace_back(sourceDetails); + sourceItr = std::prev(exportedSources.end()); + } + + // Take the Id from the available package because that is the one used in the source, + // but take the exported version from the installed package if needed. + if (includeVersions) + { + sourceItr->Packages.emplace_back( + availablePackageVersion->GetProperty(PackageVersionProperty::Id), + version.get(), + channel.get()); + } + else + { + sourceItr->Packages.emplace_back(availablePackageVersion->GetProperty(PackageVersionProperty::Id)); + } + } + + context.Add(std::move(exportedPackages)); + } + + void WriteImportFile(Execution::Context& context) + { + auto packages = PackagesJson::CreateJson(context.Get()); + + std::filesystem::path outputFilePath{ context.Args.GetArg(Execution::Args::Type::OutputFile) }; + std::ofstream outputFileStream{ outputFilePath }; + outputFileStream << packages; + } + + void ReadImportFile(Execution::Context& context) + { + std::ifstream importFile{ context.Args.GetArg(Execution::Args::Type::ImportFile) }; + THROW_LAST_ERROR_IF(importFile.fail()); + + Json::Value jsonRoot; + Json::CharReaderBuilder builder; + Json::String errors; + if (!Json::parseFromStream(builder, importFile, &jsonRoot, &errors)) + { + AICLI_LOG(CLI, Error, << "Failed to read JSON: " << errors); + context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); + } + + PackagesJson::ParseResult parseResult = PackagesJson::TryParseJson(jsonRoot); + if (parseResult.Result != PackagesJson::ParseResult::Type::Success) + { + context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; + if (parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema || + parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema) + { + context.Reporter.Error() << Resource::String::ImportFileHasInvalidSchema << std::endl; + } + else if (parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed) + { + context.Reporter.Error() << parseResult.Errors << std::endl; + } + + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); + } + + PackageCollection& packages = parseResult.Packages; + if (packages.Sources.empty()) + { + AICLI_LOG(CLI, Warning, << "No packages to install"); + context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); + } + + if (context.Args.Contains(Execution::Args::Type::IgnoreVersions)) + { + // Strip out all the version information as we don't need it. + for (auto& source : packages.Sources) + { + for (auto& package : source.Packages) + { + package.VersionAndChannel = {}; + } + } + } + + context.Add(std::move(packages)); + } + + void OpenSourcesForImport(Execution::Context& context) + { + auto availableSources = Repository::GetSources(); + for (auto& requiredSource : context.Get().Sources) + { + // Find the installed source matching the one described in the collection. + AICLI_LOG(CLI, Info, << "Looking for source [" << requiredSource.Details.Identifier << "]"); + auto matchingSource = FindSource(availableSources, requiredSource.Details); + if (matchingSource != availableSources.end()) + { + requiredSource.Details.Name = matchingSource->Name; + } + else + { + AICLI_LOG(CLI, Error, << "Missing required source: " << requiredSource.Details.Name); + context.Reporter.Warn() << Resource::String::ImportSourceNotInstalled << ' ' << requiredSource.Details.Name << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } + + context << Workflow::OpenNamedSourceForSources(requiredSource.Details.Name); + if (context.IsTerminated()) + { + return; + } + } + } + + void SearchPackagesForImport(Execution::Context& context) + { + const auto& sources = context.Get(); + std::vector> packagesToInstall = {}; + bool foundAll = true; + + // Look for the packages needed from each source independently. + // If a package is available from multiple sources, this ensures we will get it from the right one. + for (auto& requiredSource : context.Get().Sources) + { + // Find the required source among the open sources. This must exist as we already found them. + auto sourceItr = FindSource(sources, requiredSource.Details); + if (sourceItr == sources.end()) + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR); + } + + // Search for all the packages in the source. + // Each search is done in a sub context to search everything regardless of previous failures. + auto source = Repository::CreateCompositeSource(context.Get(), *sourceItr, CompositeSearchBehavior::AllPackages); + AICLI_LOG(CLI, Info, << "Searching for packages requested from source [" << requiredSource.Details.Identifier << "]"); + for (const auto& packageRequest : requiredSource.Packages) + { + Logging::SubExecutionTelemetryScope subExecution; + AICLI_LOG(CLI, Info, << "Searching for package [" << packageRequest.Id << "]"); + + // Search for the current package + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, packageRequest.Id.get())); + + auto searchContextPtr = context.Clone(); + Execution::Context& searchContext = *searchContextPtr; + searchContext.Add(source); + searchContext.Add(source->Search(searchRequest)); + + // Find the single version we want is available + searchContext << + Workflow::EnsureOneMatchFromSearchResult(false) << + Workflow::GetManifestWithVersionFromPackage(packageRequest.VersionAndChannel) << + Workflow::GetInstalledPackageVersion; + + if (searchContext.Contains(Execution::Data::InstalledPackageVersion) && searchContext.Get()) + { + searchContext << Workflow::EnsureUpdateVersionApplicable; + } + + if (searchContext.IsTerminated()) + { + if (searchContext.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) + { + AICLI_LOG(CLI, Info, << "Package is already installed: [" << packageRequest.Id << "]"); + context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled << ' ' << packageRequest.Id << std::endl; + continue; + } + else + { + AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << packageRequest.VersionAndChannel.ToString()); + context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; + + // Keep searching for the remaining packages and only fail at the end. + foundAll = false; + continue; + } + } + + packagesToInstall.push_back(std::move(searchContext.Get())); + } + } + + if (!foundAll) + { + AICLI_LOG(CLI, Info, << "Could not find one or more packages for import"); + if (context.Args.Contains(Execution::Args::Type::IgnoreUnavailable)) + { + AICLI_LOG(CLI, Info, << "Ignoring unavailable packages due to command line argument"); + } + else + { + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); + } + } + + context.Add(std::move(packagesToInstall)); + } +} diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.h b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h new file mode 100644 index 0000000000..f89381a955 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Selects the package versions to list on the exported file + // Required Args: None + // Inputs: SearchResult + // Outputs: PackageCollection + void SelectVersionsToExport(Execution::Context& context); + + // Exports a collection of packages to a JSON import file + // Required Args: OutputFile + // Inputs: PackageCollection + // Outputs: None + void WriteImportFile(Execution::Context& context); + + // Reads the contents of an import file + // Required Args: ImportFile + // Inputs: None + // Outputs: PackageCollection + void ReadImportFile(Execution::Context& context); + + // Opens the sources specified in an import file + // Required Args: None + // Inputs: PackageCollection + // Outputs: Sources + void OpenSourcesForImport(Execution::Context& context); + + // Finds the package versions to install matching their descriptions + // Needs the sources for all packages and the installed source + // Required Args: None + // Inputs: PackageCollection, Sources, Source + // Outputs: PackagesToInstall + void SearchPackagesForImport(Execution::Context& context); +} diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 6494d866d6..50a93e0625 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "InstallFlow.h" +#include "UninstallFlow.h" #include "Resources.h" #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" @@ -17,15 +18,22 @@ namespace AppInstaller::CLI::Workflow using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; - void EnsureMinOSVersion(Execution::Context& context) + namespace { - const auto& manifest = context.Get(); - - if (!manifest.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version(manifest.MinOSVersion))) + bool MightWriteToARP(InstallerTypeEnum type) { - context.Reporter.Error() << Resource::String::InstallationRequiresHigherWindows << ' ' << manifest.MinOSVersion << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_OLD_WIN_VERSION)); + switch (type) + { + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: + return true; + default: + return false; + } } } @@ -44,7 +52,7 @@ namespace AppInstaller::CLI::Workflow { auto installerType = context.Get().value().InstallerType; - if (installerType == ManifestInstaller::InstallerTypeEnum::MSStore) + if (installerType == InstallerTypeEnum::MSStore) { context.Reporter.Info() << Resource::String::InstallationDisclaimerMSStore << std::endl; } @@ -62,15 +70,15 @@ namespace AppInstaller::CLI::Workflow switch (installer.InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; break; - case ManifestInstaller::InstallerTypeEnum::Msix: + case InstallerTypeEnum::Msix: if (installer.SignatureSha256.empty()) { context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; @@ -81,7 +89,7 @@ namespace AppInstaller::CLI::Workflow context << GetMsixSignatureHash << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; } break; - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::MSStore: // Nothing to do here break; default: @@ -101,11 +109,40 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << "Downloading " << Execution::UrlEmphasis << installer.Url << std::endl; - auto hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, - installer.Url, - tempInstallerPath, - std::placeholders::_1, - true)); + std::optional> hash; + + const int MaxRetryCount = 2; + for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) + { + bool success = false; + try + { + hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, + installer.Url, + tempInstallerPath, + std::placeholders::_1, + true)); + + success = true; + } + catch (...) + { + if (retryCount < MaxRetryCount - 1) + { + AICLI_LOG(CLI, Info, << "Failed to download, waiting a bit and retry. Url: " << installer.Url); + Sleep(500); + } + else + { + throw; + } + } + + if (success) + { + break; + } + } if (!hash) { @@ -135,9 +172,10 @@ namespace AppInstaller::CLI::Workflow } catch (const winrt::hresult_error& e) { - if (e.code() == HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED)) + if (e.code() == HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED) || + HRESULT_FACILITY(e.code()) == FACILITY_HTTP) { - // Server does not support range request, use download + // Failed to get signature hash through HttpStream, use download downloadInstead = true; } else @@ -210,7 +248,26 @@ namespace AppInstaller::CLI::Workflow else if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerHashMatched)) { const auto& installer = context.Get(); - Utility::ApplyMotwUsingIAttachmentExecuteIfApplicable(context.Get(), installer.value().Url); + HRESULT hr = Utility::ApplyMotwUsingIAttachmentExecuteIfApplicable(context.Get(), installer.value().Url); + + // Not using SUCCEEDED(hr) to check since there are cases file is missing after a successful scan + if (hr != S_OK) + { + switch (hr) + { + case INET_E_SECURITY_PROBLEM: + context.Reporter.Error() << Resource::String::InstallerBlockedByPolicy << std::endl; + break; + case E_FAIL: + context.Reporter.Error() << Resource::String::InstallerFailedVirusScan << std::endl; + break; + default: + context.Reporter.Error() << Resource::String::InstallerFailedSecurityCheck << std::endl; + } + + AICLI_LOG(Fail, Error, << "Installer failed security check. Url: " << installer.value().Url << " Result: " << WINGET_OSTREAM_FORMAT_HRESULT(hr)); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED); + } } } } @@ -223,24 +280,25 @@ namespace AppInstaller::CLI::Workflow switch (installer.InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Wix: - if (isUpdate && installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious) + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: + if (isUpdate && installer.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious) { - // TODO: hook up with uninstall when uninstall is implemented + context << + GetUninstallInfo << + ExecuteUninstaller; context.ClearFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } context << ShellExecuteInstall; break; - case ManifestInstaller::InstallerTypeEnum::Msix: + case InstallerTypeEnum::Msix: context << MsixInstall; break; - case ManifestInstaller::InstallerTypeEnum::MSStore: + case InstallerTypeEnum::MSStore: context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ExperimentalMSStore) << EnsureStorePolicySatisfied << @@ -317,4 +375,266 @@ namespace AppInstaller::CLI::Workflow } } } + + void InstallPackageInstaller(Execution::Context& context) + { + context << + Workflow::ReportManifestIdentity << + Workflow::ShowInstallationDisclaimer << + Workflow::ReportExecutionStage(ExecutionStage::Download) << + Workflow::DownloadInstaller << + Workflow::ReportExecutionStage(ExecutionStage::PreExecution) << + Workflow::SnapshotARPEntries << + Workflow::ReportExecutionStage(ExecutionStage::Execution) << + Workflow::ExecuteInstaller << + Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << + Workflow::ReportARPChanges << + Workflow::RemoveInstaller; + } + + void InstallPackageVersion(Execution::Context& context) + { + context << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::InstallPackageInstaller; + } + + void InstallMultiple(Execution::Context& context) + { + bool allSucceeded = true; + for (auto package : context.Get()) + { + Logging::SubExecutionTelemetryScope subExecution; + + // We want to do best effort to install all packages regardless of previous failures + auto installContextPtr = context.Clone(); + Execution::Context& installContext = *installContextPtr; + + // Extract the data needed for installing + installContext.Add(package); + installContext.Add(package->GetManifest()); + + installContext << InstallPackageVersion; + if (installContext.IsTerminated()) + { + allSucceeded = false; + } + } + + if (!allSucceeded) + { + context.Reporter.Error() << Resource::String::ImportInstallFailed << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED); + } + } + + void SnapshotARPEntries(Execution::Context& context) try + { + // Ensure that installer type might actually write to ARP, otherwise this is a waste of time + auto installer = context.Get(); + + if (installer && MightWriteToARP(installer->InstallerType)) + { + std::shared_ptr arpSource = context.Reporter.ExecuteWithProgress( + [](IProgressCallback& progress) + { + return Repository::OpenPredefinedSource(PredefinedSource::ARP, progress); + }, true); + + std::vector> entries; + + for (const auto& entry : arpSource->Search({}).Matches) + { + auto installed = entry.Package->GetInstalledVersion(); + entries.emplace_back(std::make_tuple( + entry.Package->GetProperty(PackageProperty::Id), + installed->GetProperty(PackageVersionProperty::Version), + installed->GetProperty(PackageVersionProperty::Channel))); + } + + std::sort(entries.begin(), entries.end()); + + context.Add(std::move(entries)); + } + } + CATCH_LOG() + + void ReportARPChanges(Execution::Context& context) try + { + if (context.Contains(Execution::Data::ARPSnapshot)) + { + const auto& entries = context.Get(); + + // Open it again to get the (potentially) changed ARP entries + std::shared_ptr arpSource = context.Reporter.ExecuteWithProgress( + [](IProgressCallback& progress) + { + return Repository::OpenPredefinedSource(PredefinedSource::ARP, progress); + }, true); + + std::vector changes; + + for (auto& entry : arpSource->Search({}).Matches) + { + auto installed = entry.Package->GetInstalledVersion(); + auto entryKey = std::make_tuple( + entry.Package->GetProperty(PackageProperty::Id), + installed->GetProperty(PackageVersionProperty::Version), + installed->GetProperty(PackageVersionProperty::Channel)); + + auto itr = std::lower_bound(entries.begin(), entries.end(), entryKey); + if (itr == entries.end() || *itr != entryKey) + { + changes.emplace_back(std::move(entry)); + } + } + + // Also attempt to find the entry based on the manifest data + const auto& manifest = context.Get(); + + SearchRequest nameAndPublisherRequest; + + // The default localization must contain the name or we cannot do this lookup + if (manifest.DefaultLocalization.Contains(Localization::PackageName)) + { + AppInstaller::Manifest::Manifest::string_t defaultName = manifest.DefaultLocalization.Get(); + AppInstaller::Manifest::Manifest::string_t defaultPublisher; + if (manifest.DefaultLocalization.Contains(Localization::Publisher)) + { + defaultPublisher = manifest.DefaultLocalization.Get(); + } + + nameAndPublisherRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, defaultName, defaultPublisher)); + + for (const auto& loc : manifest.Localizations) + { + if (loc.Contains(Localization::PackageName) || loc.Contains(Localization::Publisher)) + { + nameAndPublisherRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, + loc.Contains(Localization::PackageName) ? loc.Get() : defaultName, + loc.Contains(Localization::Publisher) ? loc.Get() : defaultPublisher)); + } + } + } + + std::vector productCodes; + for (const auto& installer : manifest.Installers) + { + if (!installer.ProductCode.empty()) + { + if (std::find(productCodes.begin(), productCodes.end(), installer.ProductCode) == productCodes.end()) + { + nameAndPublisherRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode)); + productCodes.emplace_back(installer.ProductCode); + } + } + } + + SearchResult findByManifest; + + // Don't execute this search if it would just find everything + if (!nameAndPublisherRequest.IsForEverything()) + { + findByManifest = arpSource->Search(nameAndPublisherRequest); + } + + // Cross reference the changes with the search results + std::vector> packagesInBoth; + + for (const auto& change : changes) + { + for (const auto& byManifest : findByManifest.Matches) + { + if (change.Package->IsSame(byManifest.Package.get())) + { + packagesInBoth.emplace_back(change.Package); + break; + } + } + } + + // We now have all of the package changes; time to report them. + // The set of cases we could have for changes to ARP: + // 0 packages :: No changes were detected to ARP, which could mean that the installer + // did not write an entry. It could also be a forced reinstall. + // 1 package :: Golden path; this should be what we installed. + // 2+ packages :: We need to determine which package actually matches the one that we + // were installing. + // + // The set of cases we could have for finding packages based on the manifest: + // 0 packages :: The manifest data does not match the ARP information. + // 1 package :: Golden path; this should be what we installed. + // 2+ packages :: The data in the manifest is either too broad or we have + // a problem with our name normalization. + // + // ARP Package changes + // 0 1 N + // +------------------+--------------------+--------------------+ + // M | | | | + // a | Package does not | Manifest data does | Manifest data does | + // n 0 | write to ARP | not match ARP | not match ARP | + // i | Log this fact | Log for fixup | Log for fixup | + // f | | | | + // e +------------------+--------------------+--------------------+ + // s | | | | + // t | Reinstall of | Golden Path! | Treat manifest as | + // 1 | existing version | (assuming match) | main if common | + // r | | | | + // e +------------------+--------------------+--------------------+ + // s | | | | + // u | Not expected | Treat ARP as main | Not expected | + // l N | Log this for | | Log this for | + // t | investigation | | investigation | + // s | | | | + // +------------------+--------------------+--------------------+ + + // Find the package that we are going to log + std::shared_ptr toLog; + + // If no changes found, only log if a single matching package was found by the manifest + if (changes.empty() && findByManifest.Matches.size() == 1) + { + toLog = findByManifest.Matches[0].Package->GetInstalledVersion(); + } + // If only a single ARP entry was changed, always log that + else if (changes.size() == 1) + { + toLog = changes[0].Package->GetInstalledVersion(); + } + // Finally, if there is only a single common package, log that one + else if (packagesInBoth.size() == 1) + { + toLog = packagesInBoth[0]->GetInstalledVersion(); + } + + IPackageVersion::Metadata toLogMetadata; + if (toLog) + { + toLogMetadata = toLog->GetMetadata(); + } + + // We can only get the source identifier from an active source + std::string sourceIdentifier; + if (context.Contains(Execution::Data::PackageVersion)) + { + sourceIdentifier = context.Get()->GetProperty(PackageVersionProperty::SourceIdentifier); + } + + Logging::Telemetry().LogSuccessfulInstallARPChange( + sourceIdentifier, + manifest.Id, + manifest.Version, + manifest.Channel, + changes.size(), + findByManifest.Matches.size(), + packagesInBoth.size(), + toLog ? static_cast(toLog->GetProperty(PackageVersionProperty::Name)) : "", + toLog ? static_cast(toLog->GetProperty(PackageVersionProperty::Version)) : "", + toLog ? static_cast(toLogMetadata[PackageVersionMetadata::Publisher]) : "", + toLog ? static_cast(toLogMetadata[PackageVersionMetadata::Locale]) : "" + ); + } + } + CATCH_LOG() } diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h index 8e6351fecc..30837e856c 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -11,12 +11,6 @@ namespace AppInstaller::CLI::Workflow static constexpr std::string_view ARG_TOKEN_LOGPATH = ""sv; static constexpr std::string_view ARG_TOKEN_INSTALLPATH = ""sv; - // Ensures that the current OS version is greater than or equal to the one in the manifest. - // Required Args: None - // Inputs: Manifest - // Outputs: None - void EnsureMinOSVersion(Execution::Context& context); - // Ensures that there is an applicable installer. // Required Args: None // Inputs: Installer @@ -82,4 +76,34 @@ namespace AppInstaller::CLI::Workflow // Inputs: InstallerPath // Outputs: None void RemoveInstaller(Execution::Context& context); + + // Installs a specific package installer. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + void InstallPackageInstaller(Execution::Context& context); + + // Installs a specific package version. + // Required Args: None + // Inputs: Manifest, PackageVersion, Source + // Outputs: None + void InstallPackageVersion(Execution::Context& context); + + // Installs multiple packages. + // Required Args: None + // Inputs: Manifests + // Outputs: None + void InstallMultiple(Execution::Context& context); + + // Stores the existing set of packages in ARP. + // Required Args: None + // Inputs: Installer + // Outputs: ARPSnapshot + void SnapshotARPEntries(Execution::Context& context); + + // Reports on the changes between the stored ARPSnapshot and the current values. + // Required Args: None + // Inputs: ARPSnapshot?, Manifest, PackageVersion + // Outputs: None + void ReportARPChanges(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp index be4e2e5d83..97ff49ff77 100644 --- a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp +++ b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -12,15 +12,23 @@ namespace AppInstaller::CLI::Workflow namespace { // Determine if the installer is applicable. - bool IsInstallerApplicable(const Manifest::ManifestInstaller& installer, Manifest::ManifestInstaller::InstallerTypeEnum installedType) + // TODO: Implement a mechanism for better error messaging for no applicable installer scenario + bool IsInstallerApplicable(const Manifest::ManifestInstaller& installer, Manifest::InstallerTypeEnum installedType) { + // Check MinOSVersion + if (!installer.MinOSVersion.empty() && + !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(installer.MinOSVersion))) + { + return false; + } + if (Utility::IsApplicableArchitecture(installer.Arch) == Utility::InapplicableArchitecture) { return false; } - if (installedType != Manifest::ManifestInstaller::InstallerTypeEnum::Unknown && - !Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer.InstallerType, installedType)) + if (installedType != Manifest::InstallerTypeEnum::Unknown && + !Manifest::IsInstallerTypeCompatible(installer.InstallerType, installedType)) { return false; } @@ -33,33 +41,21 @@ namespace AppInstaller::CLI::Workflow bool IsInstallerBetterMatch( const Manifest::ManifestInstaller& installer1, const Manifest::ManifestInstaller& installer2, - Manifest::ManifestInstaller::InstallerTypeEnum installedType) + Manifest::InstallerTypeEnum installedType) { - auto arch1 = Utility::IsApplicableArchitecture(installer1.Arch); - auto arch2 = Utility::IsApplicableArchitecture(installer2.Arch); - - // Applicable architecture should always come before inapplicable architecture - if (arch1 != Utility::InapplicableArchitecture && - arch2 == Utility::InapplicableArchitecture) - { - return true; - } - // If there's installation metadata, pick the preferred one or compatible one - if (installedType != Manifest::ManifestInstaller::InstallerTypeEnum::Unknown) + if (installedType != Manifest::InstallerTypeEnum::Unknown) { if (installer1.InstallerType == installedType && installer2.InstallerType != installedType) { return true; } - if (Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer1.InstallerType, installedType) && - !Manifest::ManifestInstaller::IsInstallerTypeCompatible(installer2.InstallerType, installedType)) - { - return true; - } } // Todo: Compare only architecture for now. Need more work and spec. + auto arch1 = Utility::IsApplicableArchitecture(installer1.Arch); + auto arch2 = Utility::IsApplicableArchitecture(installer2.Arch); + if (arch1 > arch2) { return true; @@ -67,28 +63,6 @@ namespace AppInstaller::CLI::Workflow return false; } - - // This is used in sorting the list of available localizations to get the best match. - struct LocalizationComparator - { - bool operator() ( - const Manifest::ManifestLocalization& loc1, - const Manifest::ManifestLocalization& loc2) - { - // Todo: Compare simple language for now. Need more work and spec. - std::string userPreferredLocale = std::locale("").name(); - - auto foundLoc1 = userPreferredLocale.find(loc1.Language); - auto foundLoc2 = userPreferredLocale.find(loc2.Language); - - if (foundLoc1 != std::string::npos && foundLoc2 == std::string::npos) - { - return true; - } - - return false; - } - }; } std::optional ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) @@ -96,11 +70,11 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Starting installer selection."); // Get the currently installed package's type (if present) - Manifest::ManifestInstaller::InstallerTypeEnum installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Unknown; + Manifest::InstallerTypeEnum installedType = Manifest::InstallerTypeEnum::Unknown; auto installerTypeItr = m_installationMetadata.find(Repository::PackageVersionMetadata::InstalledType); if (installerTypeItr != m_installationMetadata.end()) { - installedType = Manifest::ManifestInstaller::ConvertToInstallerTypeEnum(installerTypeItr->second); + installedType = Manifest::ConvertToInstallerTypeEnum(installerTypeItr->second); } const Manifest::ManifestInstaller* result = nullptr; @@ -114,7 +88,7 @@ namespace AppInstaller::CLI::Workflow result = &installer; } } - else if (IsInstallerBetterMatch(installer, *result, installedType)) + else if (IsInstallerApplicable(installer, installedType) && IsInstallerBetterMatch(installer, *result, installedType)) { result = &installer; } @@ -128,39 +102,10 @@ namespace AppInstaller::CLI::Workflow Logging::Telemetry().LogSelectedInstaller( static_cast(result->Arch), result->Url, - Manifest::ManifestInstaller::InstallerTypeToString(result->InstallerType), - Manifest::ManifestInstaller::ScopeToString(result->Scope), - result->Language); + Manifest::InstallerTypeToString(result->InstallerType), + Manifest::ScopeToString(result->Scope), + result->Locale); return *result; } - - Manifest::ManifestLocalization ManifestComparator::GetPreferredLocalization(const Manifest::Manifest& manifest) - { - AICLI_LOG(CLI, Info, << "Starting localization selection."); - - ManifestLocalization selectedLocalization; - - // Sorting the list of available localizations according to rules defined in LocalizationComparator. - if (!manifest.Localization.empty()) - { - auto localization = manifest.Localization; - std::sort(localization.begin(), localization.end(), LocalizationComparator()); - - // TODO: needs to check language applicability here - - selectedLocalization = localization[0]; - } - else - { - // Populate default from package manifest - selectedLocalization.Description = manifest.Description; - selectedLocalization.Homepage = manifest.Homepage; - selectedLocalization.LicenseUrl = manifest.LicenseUrl; - } - - AICLI_LOG(CLI, Info, << "Completed localization selection. Selected localization language: " << selectedLocalization.Language); - - return selectedLocalization; - } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h index e4c896cf54..fa366f7380 100644 --- a/src/AppInstallerCLICore/Workflows/ManifestComparator.h +++ b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -15,7 +15,6 @@ namespace AppInstaller::CLI::Workflow m_installationMetadata(std::move(installationMetadata)) {} std::optional GetPreferredInstaller(const Manifest::Manifest& manifest); - Manifest::ManifestLocalization GetPreferredLocalization(const Manifest::Manifest& manifest); private: // TODO: Handle args to change how we select. diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp index 188e370976..b8c22610e0 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -7,6 +7,7 @@ using namespace AppInstaller::CLI; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; namespace AppInstaller::CLI::Workflow { @@ -15,7 +16,7 @@ namespace AppInstaller::CLI::Workflow // ShellExecutes the given path. std::optional InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) { - AICLI_LOG(CLI, Info, << "Starting installer: '" << filePath.u8string() << "' with arguments '" << args << '\''); + AICLI_LOG(CLI, Info, << "Starting: '" << filePath.u8string() << "' with arguments '" << args << '\''); SHELLEXECUTEINFOW execInfo = { 0 }; execInfo.cbSize = sizeof(execInfo); @@ -26,10 +27,8 @@ namespace AppInstaller::CLI::Workflow // Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete. // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. execInfo.nShow = SW_SHOW; - if (!ShellExecuteExW(&execInfo) || !execInfo.hProcess) - { - return GetLastError(); - } + + THROW_LAST_ERROR_IF(!ShellExecuteExW(&execInfo) || !execInfo.hProcess); wil::unique_process_handle process{ execInfo.hProcess }; @@ -70,19 +69,19 @@ namespace AppInstaller::CLI::Workflow // Construct install experience arg. // SilentWithProgress is default, so look for it first. - auto experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress); + auto experienceArgsItr = installerSwitches.find(InstallerSwitchType::SilentWithProgress); if (context.Args.Contains(Execution::Args::Type::Interactive)) { // If interactive requested, always use Interactive (or nothing). If the installer supports // interactive it is usually the default, and thus it is cumbersome to put a blank entry in // the manifest. - experienceArgsItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive); + experienceArgsItr = installerSwitches.find(InstallerSwitchType::Interactive); } // If no SilentWithProgress exists, or Silent requested, try to find Silent. else if (experienceArgsItr == installerSwitches.end() || context.Args.Contains(Execution::Args::Type::Silent)) { - auto silentItr = installerSwitches.find(ManifestInstaller::InstallerSwitchType::Silent); + auto silentItr = installerSwitches.find(InstallerSwitchType::Silent); // If Silent requested, but doesn't exist, then continue using SilentWithProgress. if (silentItr != installerSwitches.end()) { @@ -96,35 +95,35 @@ namespace AppInstaller::CLI::Workflow } // Construct language arg if necessary. - if (context.Args.Contains(Execution::Args::Type::Language) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Language) != installerSwitches.end()) + if (context.Args.Contains(Execution::Args::Type::Language) && installerSwitches.find(InstallerSwitchType::Language) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Language); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Language); } // Construct log path arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Log) != installerSwitches.end()) + if (installerSwitches.find(InstallerSwitchType::Log) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Log); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Log); } // Construct custom arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Custom) != installerSwitches.end()) + if (installerSwitches.find(InstallerSwitchType::Custom) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Custom); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Custom); } // Construct update arg if applicable - if (isUpdate && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Update) != installerSwitches.end()) + if (isUpdate && installerSwitches.find(InstallerSwitchType::Update) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Update); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Update); } // Construct install location arg if necessary. if (!isUpdate && context.Args.Contains(Execution::Args::Type::InstallLocation) && - installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) + installerSwitches.find(InstallerSwitchType::InstallLocation) != installerSwitches.end()) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); + installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::InstallLocation); } return installerArgs; @@ -166,6 +165,27 @@ namespace AppInstaller::CLI::Workflow // Todo: language token support will be implemented later } + + // Gets the arguments for uninstalling an MSI with MsiExec + std::string GetMsiExecUninstallArgs(Execution::Context& context, const Utility::LocIndString& productCode) + { + std::string args = "/x" + productCode.get(); + + // Set UI level for MsiExec with the /q flag. + // If interactive is requested, use the default instead of Reduced or Full as the installer may not use them. + if (context.Args.Contains(Execution::Args::Type::Silent)) + { + // n = None = silent + args += " /qn"; + } + else if (!context.Args.Contains(Execution::Args::Type::Interactive)) + { + // b = Basic = only progress bar + args += " /qb"; + } + + return args; + } } void ShellExecuteInstallImpl(Execution::Context& context) @@ -173,6 +193,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; const std::string& installerArgs = context.Get(); + const auto& additionalSuccessCodes = context.Get()->InstallerSuccessCodes; auto installResult = context.Reporter.ExecuteWithProgress( std::bind(InvokeShellExecute, @@ -185,7 +206,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Warn() << "Installation abandoned" << std::endl; AICLI_TERMINATE_CONTEXT(E_ABORT); } - else if (installResult.value() != 0) + else if (installResult.value() != 0 && (std::find(additionalSuccessCodes.begin(), additionalSuccessCodes.end(), installResult.value()) == additionalSuccessCodes.end())) { const auto& manifest = context.Get(); Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, "ShellExecute", installResult.value()); @@ -229,14 +250,14 @@ namespace AppInstaller::CLI::Workflow switch(context.Get()->InstallerType) { - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: renamedDownloadedInstaller += L".exe"; break; - case ManifestInstaller::InstallerTypeEnum::Msi: - case ManifestInstaller::InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: renamedDownloadedInstaller += L".msi"; break; } @@ -247,4 +268,82 @@ namespace AppInstaller::CLI::Workflow installerPath.assign(renamedDownloadedInstaller); AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); } + + void ShellExecuteUninstallImpl(Execution::Context& context) + { + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + std::wstring commandUtf16 = Utility::ConvertToUTF16(context.Get()); + + // Parse the command string as application and command line for CreateProcess + wil::unique_cotaskmem_string app = nullptr; + wil::unique_cotaskmem_string args = nullptr; + THROW_IF_FAILED(SHEvaluateSystemCommandTemplate(commandUtf16.c_str(), &app, NULL, &args)); + + auto uninstallResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecute, + std::filesystem::path(app.get()), + Utility::ConvertToUTF8(args.get()), + std::placeholders::_1)); + + if (!uninstallResult) + { + context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + else if (uninstallResult.value() != 0) + { + const auto installedPackageVersion = context.Get(); + Logging::Telemetry().LogUninstallerFailure( + installedPackageVersion->GetProperty(PackageVersionProperty::Id), + installedPackageVersion->GetProperty(PackageVersionProperty::Version), + "UninstallString", + uninstallResult.value()); + + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + } + else + { + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } + } + + void ShellExecuteMsiExecUninstall(Execution::Context& context) + { + const auto& productCodes = context.Get(); + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + const std::filesystem::path msiexecPath{ ExpandEnvironmentVariables(L"%windir%\\system32\\msiexec.exe") }; + + for (const auto& productCode : productCodes) + { + AICLI_LOG(CLI, Info, << "Removing: " << productCode); + auto uninstallResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecute, + msiexecPath, + GetMsiExecUninstallArgs(context, productCode), + std::placeholders::_1)); + + if (!uninstallResult) + { + context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + else if (uninstallResult.value() != 0) + { + // TODO: Check for other success codes + const auto installedPackageVersion = context.Get(); + Logging::Telemetry().LogUninstallerFailure( + installedPackageVersion->GetProperty(PackageVersionProperty::Id), + installedPackageVersion->GetProperty(PackageVersionProperty::Version), + "MsiExec", + uninstallResult.value()); + + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + } + } + + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h index 7d5c6eec5d..227ac22f5f 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -17,6 +17,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ShellExecuteInstallImpl(Execution::Context& context); + // Uninstall is done through invoking ShellExecute on uninstall string. + // Required Args: None + // Inputs: UninstallString + // Outputs: None + void ShellExecuteUninstallImpl(Execution::Context& context); + + // Removes the MSI + // Required Args: None + // Inputs: ProductCodes + // Output: None + void ShellExecuteMsiExecUninstall(Execution::Context& context); + // Gets the installer args from the context. // Required Args: None // Inputs: Manifest?, Installer, InstallerPath diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp index 35976084af..c6da146fd0 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -15,44 +15,47 @@ namespace AppInstaller::CLI::Workflow const auto& manifest = context.Get(); const auto& installer = context.Get(); - ManifestComparator manifestComparator(context.Args); - auto selectedLocalization = manifestComparator.GetPreferredLocalization(manifest); - // TODO: Come up with a prettier format context.Reporter.Info() << "Version: " << manifest.Version << std::endl; - context.Reporter.Info() << "Publisher: " << manifest.Publisher << std::endl; - if (!manifest.Author.empty()) + context.Reporter.Info() << "Publisher: " << manifest.CurrentLocalization.Get() << std::endl; + auto author = manifest.CurrentLocalization.Get(); + if (!author.empty()) { - context.Reporter.Info() << "Author: " << manifest.Author << std::endl; + context.Reporter.Info() << "Author: " << author << std::endl; } - if (!manifest.AppMoniker.empty()) + if (!manifest.Moniker.empty()) { - context.Reporter.Info() << "AppMoniker: " << manifest.AppMoniker << std::endl; + context.Reporter.Info() << "Moniker: " << manifest.Moniker << std::endl; } - if (!selectedLocalization.Description.empty()) + auto description = manifest.CurrentLocalization.Get(); + if (description.empty()) { - context.Reporter.Info() << "Description: " << selectedLocalization.Description << std::endl; + // Fall back to short description + description = manifest.CurrentLocalization.Get(); } - if (!selectedLocalization.Homepage.empty()) + if (!description.empty()) { - context.Reporter.Info() << "Homepage: " << selectedLocalization.Homepage << std::endl; + context.Reporter.Info() << "Description: " << description << std::endl; } - if (!manifest.License.empty()) + auto homepage = manifest.CurrentLocalization.Get(); + if (!homepage.empty()) { - context.Reporter.Info() << "License: " << manifest.License << std::endl; + context.Reporter.Info() << "Homepage: " << homepage << std::endl; } - if (!selectedLocalization.LicenseUrl.empty()) + context.Reporter.Info() << "License: " << manifest.CurrentLocalization.Get() << std::endl; + auto licenseUrl = manifest.CurrentLocalization.Get(); + if (!licenseUrl.empty()) { - context.Reporter.Info() << "License Url: " << selectedLocalization.LicenseUrl << std::endl; + context.Reporter.Info() << "License Url: " << licenseUrl << std::endl; } context.Reporter.Info() << "Installer:" << std::endl; if (installer) { - context.Reporter.Info() << " Type: " << Manifest::ManifestInstaller::InstallerTypeToString(installer->InstallerType) << std::endl; - if (!installer->Language.empty()) + context.Reporter.Info() << " Type: " << Manifest::InstallerTypeToString(installer->InstallerType) << std::endl; + if (!installer->Locale.empty()) { - context.Reporter.Info() << " Language: " << installer->Language << std::endl; + context.Reporter.Info() << " Locale: " << installer->Locale << std::endl; } if (!installer->Url.empty()) { @@ -76,7 +79,6 @@ namespace AppInstaller::CLI::Workflow void ShowManifestVersion(Execution::Context& context) { const auto& manifest = context.Get(); - Execution::TableOutput<2> table(context.Reporter, { Resource::String::ShowVersion, Resource::String::ShowChannel }); table.OutputLine({ manifest.Version, manifest.Channel }); table.Complete(); diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp new file mode 100644 index 0000000000..b5fa852316 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "UninstallFlow.h" +#include "WorkflowBase.h" +#include "ShellExecuteInstallerHandler.h" +#include "AppInstallerMsixInfo.h" + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Msix; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI::Workflow +{ + void GetUninstallInfo(Execution::Context& context) + { + auto installedPackageVersion = context.Get(); + const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; + switch (ConvertToInstallerTypeEnum(installedTypeString)) + { + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: + { + IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); + + // Default to silent unless it is not present or interactivity is requested + auto uninstallCommandItr = packageMetadata.find(PackageVersionMetadata::SilentUninstallCommand); + if (uninstallCommandItr == packageMetadata.end() || context.Args.Contains(Execution::Args::Type::Interactive)) + { + auto interactiveItr = packageMetadata.find(PackageVersionMetadata::StandardUninstallCommand); + if (interactiveItr != packageMetadata.end()) + { + uninstallCommandItr = interactiveItr; + } + } + + if (uninstallCommandItr == packageMetadata.end()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add(uninstallCommandItr->second); + break; + } + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: + { + // Uninstall strings for MSI don't include UI level (/q) needed to avoid interactivity, + // so we handle them differently. + auto productCodes = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + if (productCodes.empty()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add(std::move(productCodes)); + break; + } + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: + { + auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); + if (packageFamilyNames.empty()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add(packageFamilyNames); + break; + } + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + void ExecuteUninstaller(Execution::Context& context) + { + const std::string installedTypeString = context.Get()->GetMetadata()[PackageVersionMetadata::InstalledType]; + switch (ConvertToInstallerTypeEnum(installedTypeString)) + { + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: + context << Workflow::ShellExecuteUninstallImpl; + break; + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: + context << Workflow::ShellExecuteMsiExecUninstall; + break; + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: + context << Workflow::MsixUninstall; + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + void MsixUninstall(Execution::Context& context) + { + const auto& packageFamilyNames = context.Get(); + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + for (const auto& packageFamilyName : packageFamilyNames) + { + auto packageFullName = Msix::GetPackageFullNameFromFamilyName(packageFamilyName); + if (!packageFullName.has_value()) + { + AICLI_LOG(CLI, Warning, << "No package found with family name: " << packageFamilyName); + continue; + } + + AICLI_LOG(CLI, Info, << "Removing MSIX package: " << packageFullName.value()); + context.Reporter.ExecuteWithProgress(std::bind(Deployment::RemovePackage, packageFullName.value(), std::placeholders::_1)); + } + + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h new file mode 100644 index 0000000000..8a71a23040 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Gets the command string or package family names used to uninstall the package. + // Required Args: None + // Inputs: InstalledPackageVersion + // Output: UninstallString?, PackageFamilyNames? + void GetUninstallInfo(Execution::Context& context); + + // Uninstalls the package according to its type. + // Required Args: None + // Inputs: InstalledPackageVersion, UninstallString?, PackageFamilyNames? + // Output: None + void ExecuteUninstaller(Execution::Context& context); + + // Removes the MSIX. + // Required Args: None + // Inputs: PackageFamilyNames + // Outputs: None + void MsixUninstall(Execution::Context& context); +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp index 89951de070..a591be56fc 100644 --- a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -37,13 +37,6 @@ namespace AppInstaller::CLI::Workflow auto packageVersion = package->GetAvailableVersion(key); auto manifest = packageVersion->GetManifest(); - // Check MinOSVersion - if (!manifest.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Utility::Version(manifest.MinOSVersion))) - { - continue; - } - // Check applicable Installer auto installer = manifestComparator.GetPreferredInstaller(manifest); if (!installer.has_value()) @@ -52,11 +45,13 @@ namespace AppInstaller::CLI::Workflow } // Since we already did installer selection, just populate the context Data + manifest.ApplyLocale(); context.Add(std::move(manifest)); context.Add(std::move(packageVersion)); context.Add(std::move(installer)); updateFound = true; + break; } else { @@ -116,15 +111,7 @@ namespace AppInstaller::CLI::Workflow updateAllFoundUpdate = true; - updateContext << - ReportManifestIdentity << - ShowInstallationDisclaimer << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - DownloadInstaller << - Workflow::ReportExecutionStage(ExecutionStage::Execution) << - ExecuteInstaller << - Workflow::ReportExecutionStage(ExecutionStage::PostExecution) << - RemoveInstaller; + updateContext << InstallPackageInstaller; updateContext.Reporter.Info() << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 51d585df8d..3eab876c0b 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -36,6 +36,53 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::ReportIdentityFound << ' ' << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; } + std::shared_ptr OpenNamedSource(Execution::Context& context, std::string_view sourceName) + { + std::shared_ptr source; + try + { + auto result = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1), true); + source = result.Source; + + // We'll only report the source update failure as warning and continue + for (const auto& s : result.SourcesWithUpdateFailure) + { + context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate << ' ' << s.Name << std::endl; + } + } + catch (...) + { + context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; + throw; + } + + if (!source) + { + std::vector sources = GetSources(); + + if (!sourceName.empty() && !sources.empty()) + { + // A bad name was given, try to help. + context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << sourceName << std::endl; + context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; + for (const auto& details : sources) + { + context.Reporter.Info() << " "_liv << details.Name << std::endl; + } + + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, {}); + } + else + { + // Even if a name was given, there are no sources + context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED, {}); + } + } + + return source; + } + void SearchSourceApplyFilters(Execution::Context& context, SearchRequest& searchRequest, MatchType matchType) { const auto& args = context.Args; @@ -102,43 +149,30 @@ namespace AppInstaller::CLI::Workflow sourceName = context.Args.GetArg(Execution::Args::Type::Source); } - std::shared_ptr source; - try + auto source = OpenNamedSource(context, sourceName); + if (context.IsTerminated()) { - source = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1), true); + return; } - catch (...) + + context.Add(std::move(source)); + } + + void OpenNamedSourceForSources::operator()(Execution::Context& context) const + { + auto source = OpenNamedSource(context, m_sourceName); + if (context.IsTerminated()) { - context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; - throw; + return; } - if (!source) + if (!context.Contains(Execution::Data::Sources)) { - std::vector sources = GetSources(); - - if (context.Args.Contains(Execution::Args::Type::Source) && !sources.empty()) - { - // A bad name was given, try to help. - context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch << ' ' << context.Args.GetArg(Execution::Args::Type::Source) << std::endl; - context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; - for (const auto& details : sources) - { - context.Reporter.Info() << " "_liv << details.Name << std::endl; - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); - } - else - { - // Even if a name was given, there are no sources - context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED); - } + context.Add({ std::move(source) }); } else { - context.Add(std::move(source)); + context.Get().emplace_back(std::move(source)); } } @@ -465,12 +499,9 @@ namespace AppInstaller::CLI::Workflow }; } - void GetManifestFromPackage(Execution::Context& context) + void GetManifestWithVersionFromPackage::operator()(Execution::Context& context) const { - std::string_view version = context.Args.GetArg(Execution::Args::Type::Version); - std::string_view channel = context.Args.GetArg(Execution::Args::Type::Channel); - - PackageVersionKey key("", version, channel); + PackageVersionKey key("", m_version, m_channel); auto requestedVersion = context.Get()->GetAvailableVersion(key); std::optional manifest; @@ -481,25 +512,32 @@ namespace AppInstaller::CLI::Workflow if (!manifest) { - context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound << ' '; - if (!version.empty()) + auto errorStream = context.Reporter.Error(); + errorStream << Resource::String::GetManifestResultVersionNotFound << ' '; + if (!m_version.empty()) { - context.Reporter.Error() << version; + errorStream << m_version; } - if (!channel.empty()) + if (!m_channel.empty()) { - context.Reporter.Error() << '[' << channel << ']'; + errorStream << '[' << m_channel << ']'; } - context.Reporter.Error() << std::endl; + errorStream << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); } - Logging::Telemetry().LogManifestFields(manifest->Id, manifest->Name, manifest->Version); + Logging::Telemetry().LogManifestFields(manifest->Id, manifest->DefaultLocalization.Get(), manifest->Version); + manifest->ApplyLocale(); context.Add(std::move(manifest.value())); context.Add(std::move(requestedVersion)); } + void GetManifestFromPackage(Execution::Context& context) + { + context << GetManifestWithVersionFromPackage(context.Args.GetArg(Execution::Args::Type::Version), context.Args.GetArg(Execution::Args::Type::Channel)); + } + void VerifyFile::operator()(Execution::Context& context) const { std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); @@ -517,16 +555,28 @@ namespace AppInstaller::CLI::Workflow } } + void VerifyPath::operator()(Execution::Context& context) const + { + std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); + + if (!std::filesystem::exists(path)) + { + context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist << ' ' << path.u8string() << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); + } + } + void GetManifestFromArg(Execution::Context& context) { Logging::Telemetry().LogIsManifestLocal(true); context << - VerifyFile(Execution::Args::Type::Manifest) << + VerifyPath(Execution::Args::Type::Manifest) << [](Execution::Context& context) { Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Manifest))); - Logging::Telemetry().LogManifestFields(manifest.Id, manifest.Name, manifest.Version); + Logging::Telemetry().LogManifestFields(manifest.Id, manifest.DefaultLocalization.Get(), manifest.Version); + manifest.ApplyLocale(); context.Add(std::move(manifest)); }; } @@ -540,7 +590,7 @@ namespace AppInstaller::CLI::Workflow void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get(); - ReportIdentity(context, manifest.Name, manifest.Id); + ReportIdentity(context, manifest.CurrentLocalization.Get(), manifest.Id); } void GetManifest(Execution::Context& context) @@ -548,8 +598,7 @@ namespace AppInstaller::CLI::Workflow if (context.Args.Contains(Execution::Args::Type::Manifest)) { context << - GetManifestFromArg << - ReportManifestIdentity; + GetManifestFromArg; } else { @@ -557,7 +606,6 @@ namespace AppInstaller::CLI::Workflow OpenSource << SearchSourceForSingle << EnsureOneMatchFromSearchResult(false) << - ReportPackageIdentity << GetManifestFromPackage; } } @@ -630,7 +678,7 @@ namespace AppInstaller::CLI::Workflow SearchRequest searchRequest; searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); // In case there're same Ids from different sources, filter the result using package name - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.Name)); + searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.DefaultLocalization.Get())); context.Add(source->Search(searchRequest)); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index add4ec792b..51fdb10c7b 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -22,6 +22,7 @@ namespace AppInstaller::CLI::Workflow ParseArgs = 1000, Discovery = 2000, Download = 3000, + PreExecution = 3500, Execution = 4000, PostExecution = 5000, }; @@ -60,6 +61,20 @@ namespace AppInstaller::CLI::Workflow // Outputs: Source void OpenSource(Execution::Context& context); + // Creates a source object for a source specified by name, and adds it to the list of open sources. + // Required Args: None + // Inputs: Sources? + // Outputs: Sources + struct OpenNamedSourceForSources : public WorkflowTask + { + OpenNamedSourceForSources(std::string_view sourceName) : WorkflowTask("OpenNamedSourceForSources"), m_sourceName(sourceName) {} + + void operator()(Execution::Context& context) const override; + + private: + std::string_view m_sourceName; + }; + // Creates a source object for a predefined source. // Required Args: None // Inputs: None @@ -155,7 +170,7 @@ namespace AppInstaller::CLI::Workflow void ReportMultiplePackageFoundResult(Execution::Context& context); // Ensures that there is at least one result in the search. - // Required Args: bool indicating id the search result is from installed source + // Required Args: bool indicating if the search result is from installed source // Inputs: SearchResult // Outputs: None struct EnsureMatchesFromSearchResult : public WorkflowTask @@ -170,7 +185,7 @@ namespace AppInstaller::CLI::Workflow }; // Ensures that there is only one result in the search. - // Required Args: bool indicating id the search result is from installed source + // Required Args: bool indicating if the search result is from installed source // Inputs: SearchResult // Outputs: None struct EnsureOneMatchFromSearchResult : public WorkflowTask @@ -184,13 +199,32 @@ namespace AppInstaller::CLI::Workflow bool m_isFromInstalledSource; }; + // Gets the manifest from package. + // Required Args: Version and channel; can be empty + // Inputs: Package + // Outputs: Manifest, PackageVersion + struct GetManifestWithVersionFromPackage : public WorkflowTask + { + GetManifestWithVersionFromPackage(const Utility::VersionAndChannel& versionAndChannel) : + WorkflowTask("GetManifestWithVersionFromPackage"), m_version(versionAndChannel.GetVersion().ToString()), m_channel(versionAndChannel.GetChannel().ToString()) {} + + GetManifestWithVersionFromPackage(std::string_view version, std::string_view channel) : + WorkflowTask("GetManifestWithVersionFromPackage"), m_version(version), m_channel(channel) {} + + void operator()(Execution::Context& context) const override; + + private: + std::string_view m_version; + std::string_view m_channel; + }; + // Gets the manifest from package. // Required Args: None // Inputs: Package // Outputs: Manifest, PackageVersion void GetManifestFromPackage(Execution::Context& context); - // Ensures the the file exists and is not a directory. + // Ensures the file exists and is not a directory. // Required Args: the one given // Inputs: None // Outputs: None @@ -204,6 +238,20 @@ namespace AppInstaller::CLI::Workflow Execution::Args::Type m_arg; }; + // Ensures the path exists. + // Required Args: the one given + // Inputs: None + // Outputs: None + struct VerifyPath : public WorkflowTask + { + VerifyPath(Execution::Args::Type arg) : WorkflowTask("VerifyPath"), m_arg(arg) {} + + void operator()(Execution::Context& context) const override; + + private: + Execution::Args::Type m_arg; + }; + // Opens the manifest file provided on the command line. // Required Args: Manifest // Inputs: None diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index 73ed67aec1..e62d38f92a 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -7,6 +7,14 @@ #include #include +#pragma warning( push ) +#pragma warning ( disable : 4458 4100 4702 ) +#include +#include +#include +#include +#pragma warning( pop ) + #include #include #include @@ -16,6 +24,7 @@ #include #include #include +#include #include #include #include diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj index 40833adb02..c01d6d4bbc 100644 --- a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj +++ b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -22,6 +22,14 @@ + + + + + + + + diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs index a1f90ecf28..77c284701a 100644 --- a/src/AppInstallerCLIE2ETests/BaseCommand.cs +++ b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -12,7 +12,9 @@ namespace AppInstallerCLIE2ETests public class BaseCommand { - public readonly string SettingsJsonFilePath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json"; + public string SettingsJsonFilePath => TestCommon.PackagedContext ? + @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json" : + @"Microsoft\WinGet\Settings\settings.json"; public readonly string LocalAppData = "LocalAppData"; [OneTimeSetUp] @@ -57,7 +59,8 @@ public void InitializeAllFeatures(bool status) experimentalCmd = status, experimentalMSStore = status, list = status, - upgrade = status + upgrade = status, + uninstall = status, } }; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 91a4500154..426e174e0a 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -45,13 +45,26 @@ public class Constants public const string WinGetUtil = "WinGetUtil"; public const string E2ETestLogsPath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir"; + // Test installers' package IDs + public const string ExeInstallerPackageId = "AppInstallerTest.TestExeInstaller"; + public const string MsiInstallerPackageId = "AppInstallerTest.TestMsiInstaller"; + public const string MsixInstallerPackageId = "AppInstallerTest.TestMsixInstaller"; + + public const string MsiInstallerProductCode = "{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; + public const string MsixInstallerPackageFamilyName = "6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e_8wekyb3d8bbwe"; + + public const string TestExeInstalledFileName = "TestExeInstalled.txt"; + public const string TestExeUninstallerFileName = "UninstallTestExe.bat"; + public class ErrorCode { public const int S_OK = 0; public const int ERROR_FILE_NOT_FOUND = unchecked((int)0x80070002); + public const int ERROR_PATH_NOT_FOUND = unchecked((int)0x80070003); public const int ERROR_NO_RANGES_PROCESSED = unchecked((int)0x80070138); public const int OPC_E_ZIP_MISSING_END_OF_CENTRAL_DIRECTORY = unchecked((int)0x8051100f); public const int ERROR_OLD_WIN_VERSION = unchecked((int)0x8007047e); + public const int HTTP_E_STATUS_NOT_FOUND = unchecked((int)0x80190194); // AICLI custom HRESULTs public const int ERROR_INTERNAL_ERROR = unchecked((int)0x8A150001); @@ -95,6 +108,19 @@ public class ErrorCode public const int ERROR_LIBYAML_ERROR = unchecked((int)0x8A150027); public const int ERROR_MANIFEST_VALIDATION_WARNING = unchecked((int)0x8A150028); public const int ERROR_MANIFEST_VALIDATION_FAILURE = unchecked((int)0x8A150029); + public const int APPINSTALLER_CLI_ERROR_INVALID_MANIFEST = unchecked((int)0x8A15002A); + public const int APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE = unchecked((int)0x8A15002B); + public const int APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE = unchecked((int)0x8A15002C); + public const int APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED = unchecked((int)0x8A15002D); + public const int APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH = unchecked((int)0x8A15002E); + public const int APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND = unchecked((int)0x8a15002F); + public const int APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED = unchecked((int)0x8a150030); + public const int APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR = unchecked((int)0x8A150031); + public const int APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR = unchecked((int)0x8A150032); + public const int APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR = unchecked((int)0x8A150033); + public const int APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150034); + public const int APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150035); + public const int APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150036); } } } diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs new file mode 100644 index 0000000000..913ca631ac --- /dev/null +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using NUnit.Framework; + + public class ImportCommand : BaseCommand + { + [SetUp] + public void Setup() + { + InitializeAllFeatures(false); + ConfigureFeature("import", true); + ConfigureFeature("export", true); + CleanupTestExe(); + } + + [TearDown] + public void TearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void ImportSuccessful() + { + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Good.json")); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + // Ignore while we don't have schema validation + [Test] + public void ImportInvalidFile() + { + // Verify failure when trying to import with an invalid file + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-Invalid.json")); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, result.ExitCode); + Assert.True(result.StdOut.Contains("JSON file is not valid")); + } + + [Test] + public void ImportUnknownSource() + { + // Verify failure when trying to import from an unknown source + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownSource.json")); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + Assert.True(result.StdOut.Contains("Source required for import is not installed")); + } + + [Test] + public void ImportUnavailablePackage() + { + // Verify failure when trying to import an unavailable package + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackage.json")); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Package not found for import")); + } + + [Test] + public void ImportUnavailableVersion() + { + // Verify failure when trying to import an unavailable package + var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackageVersion.json")); + Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Package not found for import")); + } + + [Test] + public void ImportAlreadyInstalled() + { + // Verify success with message when trying to import a package that is already installed + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller -l {installDir}"); + var result = TestCommon.RunAICLICommand("import", $"{GetTestImportFile("ImportFile-Good.json")}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Package is already installed")); + Assert.False(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + [Test] + public void ImportExportedFile() + { + // Verify success when importing an exported list of packages. + // First install the test package to ensure it is exported. + TestCommon.RunAICLICommand("install", Constants.ExeInstallerPackageId); + + var jsonFile = TestCommon.GetRandomTestFile(".json"); + TestCommon.RunAICLICommand("export", $"{jsonFile} -s TestSource"); + + // Uninstall the package to ensure we can install it again + UninstallTestExe(); + + // Import the file + var result = TestCommon.RunAICLICommand("import", jsonFile); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(VerifyTestExeInstalled()); + UninstallTestExe(); + } + + private string GetTestImportFile(string importFileName) + { + return TestCommon.GetTestDataFile(Path.Combine("ImportFiles", importFileName)); + } + + private bool VerifyTestExeInstalled(string installDir = null) + { + if (string.IsNullOrEmpty(installDir)) + { + // Default location used by installer + installDir = Path.GetTempPath(); + } + + return File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName)); + } + + private void UninstallTestExe() + { + ConfigureFeature("uninstall", true); + TestCommon.RunAICLICommand("uninstall", Constants.ExeInstallerPackageId); + } + + private void CleanupTestExe() + { + UninstallTestExe(); + File.Delete(Path.Combine(Path.GetTempPath(), Constants.TestExeInstalledFileName)); + File.Delete(Path.Combine(Path.GetTempPath(), Constants.TestExeUninstallerFileName)); + } + } +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 17c22ca103..9e9265d503 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -8,8 +8,7 @@ namespace AppInstallerCLIE2ETests public class InstallCommand : BaseCommand { - private const string InstallTestExeInstalledFile = @"TestExeInstalled.txt"; - private const string InstallTestMsiInstalledFile = @"AppInstallerTestMsiInstaller.msi"; + private const string InstallTestMsiInstalledFile = @"AppInstallerTestExeInstaller.exe"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; @@ -44,8 +43,8 @@ public void InstallExeWithInsufficientMinOsVersion() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"InapplicableOsVersion --silent -l {installDir}"); - Assert.AreEqual(Constants.ErrorCode.ERROR_OLD_WIN_VERSION, result.ExitCode); - Assert.True(result.StdOut.Contains("Cannot install package, as it requires a higher version of Windows")); + // MinOSVersion is moved to installer level, the check is performed during installer selection + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICABLE_INSTALLER, result.ExitCode); Assert.False(VerifyTestExeInstalled(installDir)); } @@ -92,9 +91,14 @@ public void InstallNullSoft() Assert.True(VerifyTestExeInstalled(installDir, "/S")); } - //[Test] + [Test] public void InstallMSI() { + if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + Assert.Ignore("MSI installer not available"); + } + var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"TestMsiInstaller --silent -l {installDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); @@ -132,17 +136,18 @@ public void InstallMSIXWithSignatureHashMismatch() private bool VerifyTestExeInstalled(string installDir, string expectedContent = null) { - if (!File.Exists(Path.Combine(installDir, InstallTestExeInstalledFile))) + if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) { return false; } if (!string.IsNullOrEmpty(expectedContent)) { - string content = File.ReadAllText(Path.Combine(installDir, InstallTestExeInstalledFile)); + string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeInstalledFileName)); return content.Contains(expectedContent); } + TestCommon.RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); return true; } diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs index 39c3c234f8..9c20bbb1c0 100644 --- a/src/AppInstallerCLIE2ETests/SetUpFixture.cs +++ b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -64,7 +64,7 @@ public void Setup() ShouldDisableDevModeOnExit = EnableDevMode(true); - ShouldRevertDefaultFileTypeRiskOnExit = DecreaseFileTypeRisk(".exe", false); + ShouldRevertDefaultFileTypeRiskOnExit = DecreaseFileTypeRisk(".exe;.msi", false); Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"TRUSTEDPEOPLE\" " + TestCommon.GetTestDataFile(Constants.AppInstallerTestCert)), "Add AppInstallerTestCert"); Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"ROOT\" " + TestCommon.GetTestDataFile(Constants.IndexPackageRootCert)), "Add IndexPackageRootCert"); diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs index 5d8e97b6f7..f69dc3622d 100644 --- a/src/AppInstallerCLIE2ETests/SourceCommand.cs +++ b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -29,8 +29,8 @@ public void SourceAddWithDuplicateName() public void SourceAddWithInvalidURL() { // Add source with invalid url should fail - var result = TestCommon.RunAICLICommand("source add", "AnotherSource https://microsoft.com"); - Assert.AreEqual(Constants.ErrorCode.ERROR_NO_RANGES_PROCESSED, result.ExitCode); + var result = TestCommon.RunAICLICommand("source add", $"AnotherSource {Constants.TestSourceUrl}/Invalid/Directory/Dont/Add/Me"); + Assert.AreEqual(Constants.ErrorCode.HTTP_E_STATUS_NOT_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("An unexpected error occurred while executing the command")); } @@ -50,7 +50,7 @@ public void SourceListWithNoArgs() // List with no args should list all available sources var result = TestCommon.RunAICLICommand("source list", ""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); } [Test] @@ -58,8 +58,8 @@ public void SourceListWithName() { var result = TestCommon.RunAICLICommand("source list", $"-n {Constants.TestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("TestSource")); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceName)); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); Assert.True(result.StdOut.Contains("Microsoft.PreIndexed.Package")); Assert.True(result.StdOut.Contains("Updated")); } @@ -110,8 +110,8 @@ public void SourceReset() { var result = TestCommon.RunAICLICommand("source reset", ""); Assert.True(result.StdOut.Contains("The following sources will be reset if the --force option is given:")); - Assert.True(result.StdOut.Contains("TestSource")); - Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains(Constants.TestSourceName)); + Assert.True(result.StdOut.Contains(Constants.TestSourceUrl)); } [Test] @@ -126,8 +126,8 @@ public void SourceForceReset() result = TestCommon.RunAICLICommand("source list", ""); Assert.True(result.StdOut.Contains("winget")); Assert.True(result.StdOut.Contains("https://winget.azureedge.net/cache")); - Assert.False(result.StdOut.Contains($"{Constants.TestSourceName}")); - Assert.False(result.StdOut.Contains($"{Constants.TestSourceUrl}")); + Assert.False(result.StdOut.Contains(Constants.TestSourceName)); + Assert.False(result.StdOut.Contains(Constants.TestSourceUrl)); ResetTestSource(); } } diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs index 6697735d0e..9b1d84f67b 100644 --- a/src/AppInstallerCLIE2ETests/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -190,7 +190,7 @@ public static RunCommandResult RunAICLICommandViaInvokeCommandInDesktopPackage(s return result; } - public static bool RunCommand(string fileName, string args, int timeOut = 60000) + public static bool RunCommand(string fileName, string args = "", int timeOut = 60000) { return RunCommandWithResult(fileName, args, timeOut).ExitCode == 0; } @@ -233,13 +233,25 @@ public static string GetTestDataFile(string fileName) return GetTestFile(Path.Combine("TestData", fileName)); } + public static string GetTestWorkDir() + { + string workDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "WorkDirectory"); + Directory.CreateDirectory(workDir); + return workDir; + } + public static string GetRandomTestDir() { - string randDir = Path.Combine(TestContext.CurrentContext.TestDirectory, Path.Combine("WorkDirectory", Path.GetRandomFileName())); + string randDir = Path.Combine(GetTestWorkDir(), Path.GetRandomFileName()); Directory.CreateDirectory(randDir); return randDir; } + public static string GetRandomTestFile(string extension) + { + return Path.Combine(GetTestWorkDir(), Path.GetRandomFileName() + extension); + } + public static bool InstallMsix(string file) { return RunCommand("powershell", $"Add-AppxPackage \"{file}\""); diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json new file mode 100644 index 0000000000..6bfedac997 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-Invalid.json @@ -0,0 +1 @@ +"A valid JSON file that does not conform to the schema" \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json new file mode 100644 index 0000000000..317c9f299b --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackage.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "MissingPackage", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json new file mode 100644 index 0000000000..5ba77b275b --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownPackageVersion.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "4.3.2.1" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json new file mode 100644 index 0000000000..13c63b2ce1 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Bad-UnknownSource.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost", + "Identifier": "WingetE2E.UnknownTestSource_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json new file mode 100644 index 0000000000..f513feaa9a --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/ImportFiles/ImportFile-Good.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Name": "TestSource", + "Argument": "https://localhost:5001/TestKit", + "Identifier": "WingetE2E.Tests_8wekyb3d8bbwe", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml index c17eef567e..f2d6af8541 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml @@ -8,8 +8,9 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' Switches: - Custom: /execustom + Custom: /execustom /Version 2.0.0.0 SilentWithProgress: /exeswp Silent: /exesilent Interactive: /exeinteractive diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml index 60bb28f482..85e3be9e8e 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml @@ -8,12 +8,13 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' Switches: Custom: /execustom SilentWithProgress: /exeswp Silent: /exesilent Interactive: /exeinteractive Language: /exeenus - Log: /exelog + Log: /LogFile InstallLocation: /InstallDir ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml index 8d2876825a..6133191be3 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml @@ -8,4 +8,5 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi Sha256: InstallerType: msi + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml index f1f9fccdf8..932a2a5de9 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml @@ -8,4 +8,5 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix Sha256: InstallerType: msix + PackageFamilyName: 6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs index 02c5773616..d74a69cc96 100644 --- a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs +++ b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -31,6 +31,11 @@ public static void GenerateTestDirectory() CopyExeInstallerToTestDirectory(); } + if (!string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + CopyMsiInstallerToTestDirectory(); + } + if (!string.IsNullOrEmpty(TestCommon.MsixInstallerPath)) { CopyMsixInstallerToTestDirectory(); @@ -95,6 +100,22 @@ private static void CopyExeInstallerToTestDirectory() SignFile(TestCommon.ExeInstallerPath); } + private static void CopyMsiInstallerToTestDirectory() + { + // Set MSI Test Installer Path + string msiInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.MsiInstaller); + DirectoryInfo msiInstallerDestDir = Directory.CreateDirectory(msiInstallerDestPath); + + // Copy MSI Test Installer to Destination Path + string msiInstallerFullName = Path.Combine(msiInstallerDestDir.FullName, "AppInstallerTestMsiInstaller.msi"); + + File.Copy(TestCommon.MsiInstallerPath, msiInstallerFullName, true); + TestCommon.MsiInstallerPath = msiInstallerFullName; + + // Sign MSI Installer File + SignFile(TestCommon.MsiInstallerPath); + } + private static void CopyMsixInstallerToTestDirectory() { // Set Msix Test Installer Path diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs index e102a51bf2..37fb4f4e43 100644 --- a/src/AppInstallerCLIE2ETests/UninstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -1,30 +1,115 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace AppInstallerCLIE2ETests -{ - using NUnit.Framework; - using System.IO; - - public class UninstallCommand : BaseCommand - { - private const string UninstallTestExeInstalledFile = @"TestExeUninstalled.txt"; - - //[Test] - public void UninstallTestExe() - { - // Example Uninstall Command Test - var installDir = TestCommon.GetRandomTestDir(); - TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); - var result = TestCommon.RunAICLICommand("uninstall", $"AppInstallerTest.TextExeInstaller"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Successfully uninstalled")); - Assert.True(VerifyTestExeUninstalled(installDir)); - } - - private bool VerifyTestExeUninstalled(string uninstallDir) - { - return File.Exists(Path.Combine(uninstallDir, UninstallTestExeInstalledFile)); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + using System.IO; + + public class UninstallCommand : BaseCommand + { + // Custom product code for overriding the default in the test exe + private const string CustomProductCode = "{f08fc03c-0b7e-4fca-9b3c-3a384d18a9f3}"; + + // File written when uninstalling the test exe + private const string UninstallTestExeUninstalledFile = "TestExeUninstalled.txt"; + + // Name of a file installed by the MSI that will be removed during uninstall + private const string UninstallTestMsiInstalledFile = "AppInstallerTestExeInstaller.exe"; + + // Package name of the test MSIX package + private const string UninstallTestMsixName = "6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + InitializeAllFeatures(false); + ConfigureFeature("uninstall", true); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void UninstallTestExe() + { + // Uninstall an Exe + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.ExeInstallerPackageId} --silent -l {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", Constants.ExeInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestExeUninstalled(installDir)); + } + + [Test] + public void UninstallTestMsi() + { + if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + Assert.Ignore("MSI installer not available"); + } + + // Uninstall an MSI + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.MsiInstallerPackageId} -l {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", Constants.MsiInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestMsiUninstalled(installDir)); + } + + [Test] + public void UninstallTestMsix() + { + // Uninstall an MSIX + TestCommon.RunAICLICommand("install", Constants.MsixInstallerPackageId); + var result = TestCommon.RunAICLICommand("uninstall", Constants.MsixInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestMsixUninstalled()); + } + + [Test] + public void UninstallNotIndexed() + { + // Uninstalls a package found with ARP not matching any known manifest. + // Install the test EXE providing a custom Product Code so that it cannot be mapped + // back to its manifest, then uninstall it using its Product Code + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.ExeInstallerPackageId} --override \"/ProductID {CustomProductCode} /InstallDir {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", CustomProductCode); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestExeUninstalled(installDir)); + } + + [Test] + public void UninstallAppNotInstalled() + { + // Verify failure when trying to uninstall an app that is not installed. + var result = TestCommon.RunAICLICommand("uninstall", $"TestMsixInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No installed package found matching input criteria.")); + } + + private bool VerifyTestExeUninstalled(string installDir) + { + return File.Exists(Path.Combine(installDir, UninstallTestExeUninstalledFile)); + } + + private bool VerifyTestMsiUninstalled(string installDir) + { + return !File.Exists(Path.Combine(installDir, UninstallTestMsiInstalledFile)); + } + + private bool VerifyTestMsixUninstalled() + { + var result = TestCommon.RunCommandWithResult("powershell", $"Get-AppxPackage {UninstallTestMsixName}"); + return string.IsNullOrWhiteSpace(result.StdOut); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/ValidateCommand.cs b/src/AppInstallerCLIE2ETests/ValidateCommand.cs index 59d4abc218..1a8e45e54d 100644 --- a/src/AppInstallerCLIE2ETests/ValidateCommand.cs +++ b/src/AppInstallerCLIE2ETests/ValidateCommand.cs @@ -35,8 +35,8 @@ public void ValidateInvalidManifest() public void ValidateManifestDoesNotExist() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\DoesNotExist")); - Assert.AreEqual(Constants.ErrorCode.ERROR_FILE_NOT_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("File does not exist")); + Assert.AreEqual(Constants.ErrorCode.ERROR_PATH_NOT_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("Path does not exist")); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 64bd3f6ed3..933e2d9097 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -722,4 +722,99 @@ They can be configured through the settings file 'winget settings'. Logs Diagnostic files containing information about application use. + + The installer is blocked by policy + + + The installer failed security check + + + An anti-virus product reports an infection in the installer + + + Failed in attempting to update the source: + + + Uninstalls the selected package, either found by searching the installed packages list or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option. + + + Uninstalls the given package + + + Starting package uninstall... + + + Successfully uninstalled + + + winget cannot locate the uninstall command for this package. Please reach out to the package publisher for support. + {Locked="winget"} + + + Uninstallation abandoned + + + Uninstall failed with exit code: + + + Exports a list of the installed packages + + + Installs all the packages listed in a file. + + + Installs all the packages in a file + + + File where the result is to be written + + + File describing the packages to install + + + Export packages from the specified source + + + Writes a list of the installed packages to a file. The packages can then be installed with the import command. + {Locked="import"} + + + One or more imported packages failed to install + + + Package not found for import: + + + Source required for import is not installed: + + + Installed package is not available from any source: + + + Installed version of package is not available from any source: + + + No packages found in import file + + + JSON file is not valid + + + Package is already installed: + + + Ignore unavailable packages + + + Include package versions in produced file + + + Ignore package versions from import file + + + Path does not exist: + + + The JSON file does not specify a recognized schema. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/ARPChanges.cpp b/src/AppInstallerCLITests/ARPChanges.cpp new file mode 100644 index 0000000000..2c308e2900 --- /dev/null +++ b/src/AppInstallerCLITests/ARPChanges.cpp @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestSource.h" +#include "TestHooks.h" +#include +#include +#include +#include + +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::CLI::Workflow; +using namespace AppInstaller::Logging; + +struct TestTelemetry : public TelemetryTraceLogger +{ + void LogSuccessfulInstallARPChange( + std::string_view sourceIdentifier, + std::string_view packageIdentifier, + std::string_view packageVersion, + std::string_view packageChannel, + size_t changesToARP, + size_t matchesInARP, + size_t countOfIntersectionOfChangesAndMatches, + std::string_view arpName, + std::string_view arpVersion, + std::string_view arpPublisher, + std::string_view arpLanguage) const noexcept override + { + WasLogSuccessfulInstallARPChangeCalled = true; + if (OnLogSuccessfulInstallARPChange) + { + OnLogSuccessfulInstallARPChange( + sourceIdentifier, packageIdentifier, packageVersion, packageChannel, + changesToARP, matchesInARP, countOfIntersectionOfChangesAndMatches, + arpName, arpVersion, arpPublisher, arpLanguage); + } + } + + std::function OnLogSuccessfulInstallARPChange; + + mutable bool WasLogSuccessfulInstallARPChangeCalled = false; +}; + +struct TestContext : public Context +{ + TestContext(Manifest::InstallerTypeEnum installerType = Manifest::InstallerTypeEnum::Exe) : + Context(OStream, IStream), SourceFactory([this](const SourceDetails&) { return Source; }) + { + // Put installer in to control whether arp change code cares to run + Manifest::ManifestInstaller installer; + installer.InstallerType = installerType; + Add(std::move(installer)); + + // Put in an empty manifest by default + Manifest::Manifest manifest; + manifest.Id = "Installing.Id"; + manifest.Version = "Installing.Version"; + manifest.Channel = "Installing.Channel"; + manifest.DefaultLocalization.Add("Installing.Name"); + Add(std::move(manifest)); + + // Set up logger to intercept event + Logger = std::make_shared(); + TestHook_SetTelemetryOverride(Logger); + + Logger->OnLogSuccessfulInstallARPChange = [this]( + std::string_view sourceIdentifier, + std::string_view packageIdentifier, + std::string_view packageVersion, + std::string_view packageChannel, + size_t changesToARP, + size_t matchesInARP, + size_t countOfIntersectionOfChangesAndMatches, + std::string_view arpName, + std::string_view arpVersion, + std::string_view arpPublisher, + std::string_view arpLanguage) + { + SourceIdentifier = sourceIdentifier; + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + PackageChannel = packageChannel; + ChangesToARP = changesToARP; + MatchesInARP = matchesInARP; + CountOfIntersectionOfChangesAndMatches = countOfIntersectionOfChangesAndMatches; + ARPName = arpName; + ARPVersion = arpVersion; + ARPPublisher = arpPublisher; + ARPLanguage = arpLanguage; + }; + + // Inject our source + TestHook_SetSourceFactoryOverride(std::string{ Repository::Microsoft::PredefinedInstalledSourceFactory::Type() }, SourceFactory); + + Source = std::make_shared(); + Source->SearchFunction = [&](const SearchRequest& request) + { + return request.IsForEverything() ? EverythingResult : MatchResult; + }; + + // The package version is used to get the source identifier + Add(TestPackageVersion::Make(Get(), Source)); + + // Populate everything result with a few items + AddEverythingResult("Id1", "Name1", "Publisher1", "1.0"); + AddEverythingResult("Id2", "Name2", "Publisher2", "2.0"); + } + + ~TestContext() + { + TestHook_ClearSourceFactoryOverrides(); + TestHook_SetTelemetryOverride({}); + } + + void AddEverythingResult(std::string_view id, std::string_view name, std::string_view publisher, std::string_view version) + { + AddResult(EverythingResult, id, name, publisher, version); + } + + void AddMatchResult(std::string_view id, std::string_view name, std::string_view publisher, std::string_view version) + { + AddResult(MatchResult, id, name, publisher, version); + } + + void ExpectEvent(size_t arpChanges, size_t matches, size_t overlap, IPackage* arpEntry = nullptr) + { + REQUIRE(Logger->WasLogSuccessfulInstallARPChangeCalled); + + const auto& manifest = Get(); + + REQUIRE(Source->GetIdentifier() == SourceIdentifier); + REQUIRE(manifest.Id == PackageIdentifier); + REQUIRE(manifest.Version == PackageVersion); + REQUIRE(manifest.Channel == PackageChannel); + REQUIRE(arpChanges == ChangesToARP); + REQUIRE(matches == MatchesInARP); + REQUIRE(overlap == CountOfIntersectionOfChangesAndMatches); + + if (arpEntry) + { + auto version = arpEntry->GetInstalledVersion(); + REQUIRE(version->GetProperty(PackageVersionProperty::Name) == ARPName); + REQUIRE(version->GetProperty(PackageVersionProperty::Version) == ARPVersion); + + auto metadata = version->GetMetadata(); + REQUIRE(metadata[PackageVersionMetadata::Publisher] == ARPPublisher); + REQUIRE(metadata[PackageVersionMetadata::Locale] == ARPLanguage); + } + else + { + REQUIRE(ARPName.empty()); + REQUIRE(ARPVersion.empty()); + REQUIRE(ARPPublisher.empty()); + REQUIRE(ARPLanguage.empty()); + } + } + + std::ostringstream OStream; + std::istringstream IStream; + std::shared_ptr Logger; + TestSourceFactory SourceFactory; + std::shared_ptr Source; + SearchResult EverythingResult; + SearchResult MatchResult; + + // EventData + std::string SourceIdentifier; + std::string PackageIdentifier; + std::string PackageVersion; + std::string PackageChannel; + size_t ChangesToARP; + size_t MatchesInARP; + size_t CountOfIntersectionOfChangesAndMatches; + std::string ARPName; + std::string ARPVersion; + std::string ARPPublisher; + std::string ARPLanguage; + + private: + void AddResult(SearchResult& result, std::string_view id, std::string_view name, std::string_view publisher, std::string_view version) + { + PackageMatchFilter defaultFilter{ PackageMatchField::Id, MatchType::Exact }; + Manifest::Manifest manifest; + + manifest.Id = id; + manifest.DefaultLocalization.Add(name); + manifest.DefaultLocalization.Add(publisher); + manifest.Version = version; + manifest.Installers.push_back({}); + + TestPackage::MetadataMap metadata; + metadata[PackageVersionMetadata::Publisher] = publisher; + + result.Matches.emplace_back(TestPackage::Make(manifest, std::move(metadata), std::vector{}, Source), defaultFilter); + } +}; + + +TEST_CASE("ARPChanges_MSIX_Ignored", "[ARPChanges][workflow]") +{ + TestContext context(Manifest::InstallerTypeEnum::Msix); + + context << SnapshotARPEntries; + + REQUIRE(!context.Contains(Data::ARPSnapshot)); + + context << ReportARPChanges; + + REQUIRE(!context.Logger->WasLogSuccessfulInstallARPChangeCalled); +} + +TEST_CASE("ARPChanges_CheckSnapshot", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + + REQUIRE(context.Contains(Data::ARPSnapshot)); + + auto snapshot = context.Get(); + + REQUIRE(context.EverythingResult.Matches.size() == snapshot.size()); + + // Destructively match + for (const auto& match : context.EverythingResult.Matches) + { + bool found = false; + for (auto itr = snapshot.begin(); itr != snapshot.end(); ++itr) + { + if (match.Package->GetProperty(PackageProperty::Id) == std::get<0>(*itr)) + { + REQUIRE(match.Package->GetInstalledVersion()->GetProperty(PackageVersionProperty::Version) == std::get<1>(*itr)); + REQUIRE(match.Package->GetInstalledVersion()->GetProperty(PackageVersionProperty::Channel) == std::get<2>(*itr)); + + snapshot.erase(itr); + found = true; + break; + } + } + REQUIRE(found); + } + + REQUIRE(snapshot.empty()); +} + +TEST_CASE("ARPChanges_NoChange_NoMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context << ReportARPChanges; + context.ExpectEvent(0, 0, 0); +} + +TEST_CASE("ARPChanges_NoChange_SingleMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + + context << ReportARPChanges; + context.ExpectEvent(0, 1, 0, context.MatchResult.Matches[0].Package.get()); +} + +TEST_CASE("ARPChanges_NoChange_MultiMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + context.AddMatchResult("MatchId2", "MatchName2", "MatchPublisher2", "MatchVersion2"); + + context << ReportARPChanges; + context.ExpectEvent(0, 2, 0); +} + +TEST_CASE("ARPChanges_SingleChange_NoMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + + context << ReportARPChanges; + context.ExpectEvent(1, 0, 0, context.EverythingResult.Matches.back().Package.get()); +} + +TEST_CASE("ARPChanges_SingleChange_SingleMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + + context << ReportARPChanges; + context.ExpectEvent(1, 1, 0, context.EverythingResult.Matches.back().Package.get()); +} + +TEST_CASE("ARPChanges_SingleChange_MultiMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + context.MatchResult.Matches.emplace_back(context.EverythingResult.Matches.back()); + + context << ReportARPChanges; + context.ExpectEvent(1, 2, 1, context.EverythingResult.Matches.back().Package.get()); +} + +TEST_CASE("ARPChanges_MultiChange_NoMatch", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + + context << ReportARPChanges; + context.ExpectEvent(2, 0, 0); +} + +TEST_CASE("ARPChanges_MultiChange_SingleMatch_NoOverlap", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + + context << ReportARPChanges; + context.ExpectEvent(2, 1, 0); +} + +TEST_CASE("ARPChanges_MultiChange_SingleMatch_Overlap", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + context.MatchResult.Matches.emplace_back(context.EverythingResult.Matches.back()); + + context << ReportARPChanges; + context.ExpectEvent(2, 1, 1, context.MatchResult.Matches.back().Package.get()); +} + +TEST_CASE("ARPChanges_MultiChange_MultiMatch_NoOverlap", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + context.AddMatchResult("MatchId2", "MatchName2", "MatchPublisher2", "MatchVersion2"); + + context << ReportARPChanges; + context.ExpectEvent(2, 2, 0); +} + +TEST_CASE("ARPChanges_MultiChange_MultiMatch_SingleOverlap", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + context.AddMatchResult("MatchId1", "MatchName1", "MatchPublisher1", "MatchVersion1"); + context.MatchResult.Matches.emplace_back(context.EverythingResult.Matches.back()); + + context << ReportARPChanges; + context.ExpectEvent(2, 2, 1, context.MatchResult.Matches.back().Package.get()); +} + +TEST_CASE("ARPChanges_MultiChange_MultiMatch_MultiOverlap", "[ARPChanges][workflow]") +{ + TestContext context; + + context << SnapshotARPEntries; + REQUIRE(context.Contains(Data::ARPSnapshot)); + + context.AddEverythingResult("EverythingId1", "EverythingName1", "EverythingPublisher1", "EverythingVersion1"); + context.MatchResult.Matches.emplace_back(context.EverythingResult.Matches.back()); + context.AddEverythingResult("EverythingId2", "EverythingName2", "EverythingPublisher2", "EverythingVersion2"); + context.MatchResult.Matches.emplace_back(context.EverythingResult.Matches.back()); + + context << ReportARPChanges; + context.ExpectEvent(2, 2, 2); +} diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index fc733cf476..548286e8c9 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -54,6 +54,8 @@ + + @@ -114,7 +116,7 @@ Console false - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) $(ProjectDir)..\manifest\shared.manifest @@ -130,7 +132,7 @@ true - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) $(ProjectDir)..\manifest\shared.manifest @@ -155,8 +157,8 @@ true true false - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) - wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) + wininet.lib;shell32.lib;winsqlite3.lib;shlwapi.lib;icuuc.lib;icuin.lib;urlmon.lib;Advapi32.lib;winhttp.lib;%(AdditionalDependencies) $(ProjectDir)..\manifest\shared.manifest @@ -178,6 +180,7 @@ + @@ -185,8 +188,11 @@ + + + @@ -231,6 +237,30 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + @@ -261,9 +291,6 @@ true - - true - true @@ -429,9 +456,39 @@ true + + true + true + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + @@ -443,6 +500,9 @@ {5eb88068-5fb9-4e69-89b2-72dbc5e068f9} + + {866c3f06-636f-4be8-bc24-5f86ecc606a1} + {82b39fda-e86b-4713-a873-9d56de00247a} diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index 64bc5e2ff0..d566fe8e51 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -16,6 +16,9 @@ {d5cac203-3846-4b39-a1cd-8de9303757b4} + + {69fcd25c-e737-4d28-a6d1-39ce491bf293} + @@ -110,6 +113,18 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -123,9 +138,6 @@ TestData - - TestData - TestData @@ -267,6 +279,9 @@ TestData + + TestData + TestData @@ -330,8 +345,59 @@ TestData + + TestData + TestData + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData + + + TestData\MultiFileManifestV1 + + + TestData\MultiFileManifestV1 + + + TestData\MultiFileManifestV1 + + + TestData\MultiFileManifestV1 + + + TestData + \ No newline at end of file diff --git a/src/AppInstallerCLITests/CompositeSource.cpp b/src/AppInstallerCLITests/CompositeSource.cpp index ffb9aab73e..2ec3dcbd3b 100644 --- a/src/AppInstallerCLITests/CompositeSource.cpp +++ b/src/AppInstallerCLITests/CompositeSource.cpp @@ -27,13 +27,9 @@ struct ComponentTestSource : public TestSource { return Everything; } - else if (SearchFunction) - { - return SearchFunction(request); - } else { - return {}; + return TestSource::Search(request); } } @@ -74,8 +70,8 @@ Manifest::Manifest MakeDefaultManifest() Manifest::Manifest result; result.Id = "Id"; - result.Name = "Name"; - result.Publisher = "Publisher"; + result.DefaultLocalization.Add("Name"); + result.DefaultLocalization.Add("Publisher"); result.Version = "1.0"; result.Installers.push_back({}); @@ -191,7 +187,7 @@ TEST_CASE("CompositeSource_MultiMatch_FindsId", "[CompositeSource]") { SearchResult result; result.Matches.emplace_back(MakeAvailable([](Manifest::Manifest& m) { m.Id = "A different ID"; }), Criteria()); - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = name; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add(name); }), Criteria()); return result; }; @@ -482,7 +478,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = firstName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add(firstName); }), Criteria()); return result; }; @@ -492,7 +488,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchFirst", "[CompositeSour REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add(secondName); }), Criteria()); return result; }; @@ -522,7 +518,7 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_MatchSecond", "[CompositeSou REQUIRE(request.Inclusions[0].Value == pfn); SearchResult result; - result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.Name = secondName; }), Criteria()); + result.Matches.emplace_back(MakeAvailable([&](Manifest::Manifest& m) { m.DefaultLocalization.Add(secondName); }), Criteria()); return result; }; @@ -561,3 +557,18 @@ TEST_CASE("CompositeSource_MultipleAvailableSources_ReverseMatchBoth", "[Composi REQUIRE(result.Matches[0].Package->GetInstalledVersion()); REQUIRE(result.Matches[0].Package->GetAvailableVersionKeys().size() == 1); } + +TEST_CASE("CompositeSource_IsSame", "[CompositeSource]") +{ + CompositeTestSetup setup; + setup.Installed->Everything.Matches.emplace_back(MakeInstalled(WithPFN("sortof_apfn")), Criteria()); + setup.Available->Everything.Matches.emplace_back(MakeAvailable(WithPFN("sortof_apfn")), Criteria()); + + SearchResult result1 = setup.Search(); + REQUIRE(result1.Matches.size() == 1); + + SearchResult result2 = setup.Search(); + REQUIRE(result2.Matches.size() == 1); + + REQUIRE(result1.Matches[0].Package->IsSame(result2.Matches[0].Package.get())); +} diff --git a/src/AppInstallerCLITests/NameNormalization.cpp b/src/AppInstallerCLITests/NameNormalization.cpp new file mode 100644 index 0000000000..5e77185003 --- /dev/null +++ b/src/AppInstallerCLITests/NameNormalization.cpp @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include + +using namespace std::string_view_literals; +using namespace AppInstaller::Utility; + + +// This skipped test case can be used to update the test file. +// It writes back to the output content location, so you must manually +// copy the file(s) back to the git managed location to update. +TEST_CASE("NameNorm_Update_Database_Initial", "[.]") +{ + std::ifstream namesStream(TestCommon::TestDataFile("InputNames.txt")); + REQUIRE(namesStream); + std::ifstream publishersStream(TestCommon::TestDataFile("InputPublishers.txt")); + REQUIRE(publishersStream); + std::ofstream resultsStream(TestCommon::TestDataFile("NormalizationInitialIdsUpdate.txt"), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); + REQUIRE(resultsStream); + + // Far larger than any one value; hopefully + char name[4096]{}; + char publisher[4096]{}; + + NameNormalizer normer(NormalizationVersion::Initial); + + for (;;) + { + namesStream.getline(name, ARRAYSIZE(name)); + publishersStream.getline(publisher, ARRAYSIZE(publisher)); + + if (namesStream || publishersStream) + { + REQUIRE(namesStream); + REQUIRE(publishersStream); + + INFO("Name[" << name << "], Publisher[" << publisher << "]"); + + auto normalized = normer.Normalize(name, publisher); + + std::string normalizedId = normalized.Publisher(); + normalizedId += '.'; + normalizedId += normalized.Name(); + + resultsStream << normalizedId << std::endl; + REQUIRE(resultsStream); + } + else + { + break; + } + } +} + +// If this test is failing, either changes to winget code or the ICU binaries have caused it. +// This will impact the functionality of the PreIndexedPackageSource, as it is the primary +// mechanism used to cross reference packages installed outside of winget with those in the +// source. +TEST_CASE("NameNorm_Database_Initial", "[name_norm]") +{ + std::ifstream namesStream(TestCommon::TestDataFile("InputNames.txt")); + REQUIRE(namesStream); + std::ifstream publishersStream(TestCommon::TestDataFile("InputPublishers.txt")); + REQUIRE(publishersStream); + std::ifstream resultsStream(TestCommon::TestDataFile("NormalizationInitialIds.txt")); + REQUIRE(resultsStream); + + // Far larger than any one value; hopefully + char name[4096]{}; + char publisher[4096]{}; + char expectedId[4096]{}; + + NameNormalizer normer(NormalizationVersion::Initial); + + for (;;) + { + namesStream.getline(name, ARRAYSIZE(name)); + publishersStream.getline(publisher, ARRAYSIZE(publisher)); + resultsStream.getline(expectedId, ARRAYSIZE(expectedId)); + + if (namesStream || publishersStream || resultsStream) + { + REQUIRE(namesStream); + REQUIRE(publishersStream); + REQUIRE(resultsStream); + + INFO("Name[" << name << "], Publisher[" << publisher << "]"); + + auto normalized = normer.Normalize(name, publisher); + + std::string normalizedId = normalized.Publisher(); + normalizedId += '.'; + normalizedId += normalized.Name(); + + REQUIRE(expectedId == normalizedId); + } + else + { + break; + } + } +} + +TEST_CASE("NameNorm_Architecture", "[name_norm]") +{ + NameNormalizer normer(NormalizationVersion::Initial); + + REQUIRE(normer.Normalize("Name", {}).Architecture() == Architecture::Unknown); + REQUIRE(normer.Normalize("Name x86", {}).Architecture() == Architecture::X86); + REQUIRE(normer.Normalize("Name x86_64", {}).Architecture() == Architecture::X64); + REQUIRE(normer.Normalize("Name (64 bit)", {}).Architecture() == Architecture::X64); + REQUIRE(normer.Normalize("Name 32/64 bit", {}).Architecture() == Architecture::Unknown); + REQUIRE(normer.Normalize("Fox86", {}).Architecture() == Architecture::Unknown); +} + +TEST_CASE("NameNorm_Locale", "[name_norm]") +{ + NameNormalizer normer(NormalizationVersion::Initial); + + REQUIRE(normer.Normalize("Name", {}).Locale() == ""); + REQUIRE(normer.Normalize("Name en-US", {}).Locale() == "en-us"); + REQUIRE(normer.Normalize("Name (es-mx)", {}).Locale() == "es-mx"); + REQUIRE(normer.Normalize("Names-mx", {}).Locale() == ""); +} + +TEST_CASE("NameNorm_KBNumbers", "[name_norm]") +{ + NameNormalizer normer(NormalizationVersion::Initial); + + REQUIRE(normer.Normalize("Fix for (KB42)", {}).Name() == "FixforKB42"); +} diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp new file mode 100644 index 0000000000..32614bfd3a --- /dev/null +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -0,0 +1,456 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" + +#include + +// Duplicating here because a change to these values in the product *REALLY* needs to be thought through. +using namespace std::string_literals; +using namespace std::string_view_literals; + +using namespace AppInstaller::CLI; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Utility; + +const std::string s_PackagesJson_Schema = "$schema"; +const std::string s_PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json"; +const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; +const std::string s_PackagesJson_CreationDate = "CreationDate"; + +const std::string s_PackagesJson_Sources = "Sources"; +const std::string s_PackagesJson_Source_Details = "SourceDetails"; +const std::string s_PackagesJson_Source_Name = "Name"; +const std::string s_PackagesJson_Source_Identifier = "Identifier"; +const std::string s_PackagesJson_Source_Argument = "Argument"; +const std::string s_PackagesJson_Source_Type = "Type"; + +const std::string s_PackagesJson_Packages = "Packages"; +const std::string s_PackagesJson_Package_Id = "Id"; +const std::string s_PackagesJson_Package_Version = "Version"; +const std::string s_PackagesJson_Package_Channel = "Channel"; + +namespace +{ + + Json::Value ParseJsonString(const std::string& jsonString) + { + Json::Value root; + std::stringstream{ jsonString } >> root; + return root; + } + + void ValidateJsonStringProperty(const Json::Value& node, const std::string& propertyName, std::string_view expectedValue, bool allowMissing = false) + { + if (allowMissing && expectedValue.empty() && !node.isMember(propertyName)) + { + return; + } + + REQUIRE(node.isMember(propertyName)); + REQUIRE(node[propertyName].isString()); + REQUIRE(node[propertyName].asString() == expectedValue); + } + + const Json::Value& GetAndValidateJsonProperty(const Json::Value& node, const std::string& propertyName, Json::ValueType valueType) + { + REQUIRE(node.isMember(propertyName)); + REQUIRE(node[propertyName].type() == valueType); + return node[propertyName]; + } + + void ValidateJsonWithCollection(const Json::Value& root, const PackageCollection& collection) + { + ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_v1_0); + ValidateJsonStringProperty(root, s_PackagesJson_WinGetVersion, collection.ClientVersion); + REQUIRE(root.isMember(s_PackagesJson_CreationDate)); + + const auto& jsonSources = GetAndValidateJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); + REQUIRE(jsonSources.size() == collection.Sources.size()); + + // Expect the order to be the same. Not really needed, but it makes things easier. + auto jsonSourceItr = jsonSources.begin(); + auto sourceItr = collection.Sources.begin(); + for (; jsonSourceItr != jsonSources.end(); ++jsonSourceItr, ++sourceItr) + { + REQUIRE(jsonSourceItr->isObject()); + const auto& jsonSourceDetails = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Source_Details, Json::ValueType::objectValue); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Name, sourceItr->Details.Name); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Argument, sourceItr->Details.Arg); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Type, sourceItr->Details.Type); + ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Identifier, sourceItr->Details.Identifier); + + const auto& jsonPackages = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Packages, Json::ValueType::arrayValue); + REQUIRE(jsonPackages.size() == sourceItr->Packages.size()); + + auto jsonPackageItr = jsonPackages.begin(); + auto packageItr = sourceItr->Packages.begin(); + for (; jsonPackageItr != jsonPackages.end(); ++jsonPackageItr, ++packageItr) + { + REQUIRE(jsonPackageItr->isObject()); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Id, packageItr->Id); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Version, packageItr->VersionAndChannel.GetVersion().ToString(), true); + ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Channel, packageItr->VersionAndChannel.GetChannel().ToString(), true); + } + } + } + + void ValidateEqualCollections(const PackageCollection& first, const PackageCollection& second) + { + REQUIRE(first.ClientVersion == second.ClientVersion); + REQUIRE(first.Sources.size() == second.Sources.size()); + + auto firstSourceItr = first.Sources.begin(); + auto secondSourceItr = second.Sources.begin(); + for (; firstSourceItr != first.Sources.end(); ++firstSourceItr, ++secondSourceItr) + { + REQUIRE(firstSourceItr->Details.Name == secondSourceItr->Details.Name); + REQUIRE(firstSourceItr->Details.Arg == secondSourceItr->Details.Arg); + REQUIRE(firstSourceItr->Details.Type == secondSourceItr->Details.Type); + REQUIRE(firstSourceItr->Details.Identifier == secondSourceItr->Details.Identifier); + + REQUIRE(firstSourceItr->Packages.size() == secondSourceItr->Packages.size()); + auto firstPackageItr = firstSourceItr->Packages.begin(); + auto secondPackageItr = secondSourceItr->Packages.begin(); + for (; firstPackageItr != firstSourceItr->Packages.end(); ++firstPackageItr, ++secondPackageItr) + { + REQUIRE(firstPackageItr->Id == secondPackageItr->Id); + REQUIRE(firstPackageItr->VersionAndChannel.ToString() == secondPackageItr->VersionAndChannel.ToString()); + } + } + } +} + +TEST_CASE("PackageCollection_Write_SingleSource", "[PackageCollection]") +{ + PackageCollection::Source source; + source.Details.Name = "TestSource"; + source.Details.Arg = "https://aka.ms/winget"; + source.Details.Type = "Microsoft.PreIndexed.Package"; + source.Details.Identifier = "TestSourceId"; + + source.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); + source.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2" }, Channel{ "Public" }); + + PackageCollection pc + { + "0.1.2.3", + std::vector{ source } + }; + + ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); +} + +TEST_CASE("PackageCollection_Write_MultipleSources", "[PackageCollection]") +{ + PackageCollection::Source source1; + source1.Details.Name = "TestSource"; + source1.Details.Arg = "https://aka.ms/winget"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "TestSourceId"; + source1.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "TestSource2"; + source2.Details.Arg = "https://aka.ms/winget"; + source2.Details.Type = "*Test"; + source2.Details.Identifier = "SecondId"; + source2.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2.1.0" }, Channel{ "Beta" }); + + PackageCollection pc + { + "1.0.0.0", + std::vector{ source1, source2 } + }; + + ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); +} + +TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.WithVersion", + "Version": "0.1", + "Channel": "Preview" + }, + { + "Id": "test.NoVersion" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); + + PackageCollection::Source source; + source.Details.Name = "TestSource"; + source.Details.Arg = "https://aka.ms/winget"; + source.Details.Type = "Microsoft.PreIndexed.Package"; + source.Details.Identifier = "TestSourceId"; + + source.Packages.emplace_back(LocIndString{ "test.WithVersion"sv }, Version{ "0.1" }, Channel{ "Preview" }); + source.Packages.emplace_back(LocIndString{ "test.NoVersion"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source } + }; + + ValidateEqualCollections(parseResult.Packages, expected); +} + +TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "WinGetVersion": "1.0.0", + "Sources": [ + { + "SourceDetails": { + "Argument": "//firstSource", + "Identifier": "Id1", + "Name": "First", + "Type": "Microsoft.PreIndexed.Package" + }, + "Packages": [ + { + "Id": "test" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test2", + "Version": "1.0" + } + ] + } + ] + })"); + + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); + + PackageCollection::Source source1; + source1.Details.Name = "First"; + source1.Details.Arg = "//firstSource"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "Id1"; + source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "Second"; + source2.Details.Arg = "//secondSource"; + source2.Details.Type = "*TestSource"; + source2.Details.Identifier = "Id2"; + source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source1, source2 } + }; + + ValidateEqualCollections(parseResult.Packages, expected); +} + +TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "WinGetVersion": "1.0.0", + "Sources": [ + { + "SourceDetails": { + "Argument": "//firstSource", + "Identifier": "Id1", + "Name": "First", + "Type": "Microsoft.PreIndexed.Package" + }, + "Packages": [ + { + "Id": "test" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test2", + "Version": "1.0" + } + ] + }, + { + "SourceDetails": { + "Argument": "//secondSource", + "Identifier": "Id2", + "Name": "Second", + "Type": "*TestSource" + }, + "Packages": [ + { + "Id": "test3", + "Version": "1.2" + } + ] + } + ] + })"); + + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); + + PackageCollection::Source source1; + source1.Details.Name = "First"; + source1.Details.Arg = "//firstSource"; + source1.Details.Type = "Microsoft.PreIndexed.Package"; + source1.Details.Identifier = "Id1"; + source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); + + PackageCollection::Source source2; + source2.Details.Name = "Second"; + source2.Details.Arg = "//secondSource"; + source2.Details.Type = "*TestSource"; + source2.Details.Identifier = "Id2"; + source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); + source2.Packages.emplace_back(LocIndString{ "test3"sv }, Version{ "1.2" }, Channel{ "" }); + + PackageCollection expected + { + "1.0.0", + std::vector{ source1, source2 } + }; + + ValidateEqualCollections(parseResult.Packages, expected); +} + +TEST_CASE("PackageCollection_Read_MissingSchema", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); + + json = ParseJsonString("\"Not even a JSON object\""); + + parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); +} + +TEST_CASE("PackageCollection_Read_WrongSchema", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-settings.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema); +} + +TEST_CASE("PackageCollection_Read_SchemaValidationFail", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "NotSources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + INFO(parseResult.Errors); + + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); + REQUIRE(parseResult.Errors.find("Missing required property 'Sources'.") != std::string::npos); +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp index 7eb35cfdd6..0fba5541a5 100644 --- a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp +++ b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -85,11 +85,11 @@ SQLiteIndex::MetadataResult::const_iterator Find(const SQLiteIndex::MetadataResu return std::find_if(metadata.begin(), metadata.end(), [value](const auto& m) { return m.first == value; }); } -void VerifyInstalledType(const SQLiteIndex::MetadataResult& metadata, ManifestInstaller::InstallerTypeEnum type) +void VerifyInstalledType(const SQLiteIndex::MetadataResult& metadata, InstallerTypeEnum type) { auto itr = Find(metadata, PackageVersionMetadata::InstalledType); REQUIRE(itr != metadata.end()); - REQUIRE(ManifestInstaller::ConvertToInstallerTypeEnum(itr->second) == type); + REQUIRE(ConvertToInstallerTypeEnum(itr->second) == type); } void VerifyTestScope(const SQLiteIndex::MetadataResult& metadata) @@ -115,7 +115,6 @@ void VerifyMetadataString(const SQLiteIndex::MetadataResult& metadata, PackageVe void VerifyEntryAgainstIndex(const SQLiteIndex& index, SQLiteIndex::IdType manifestId, const ARPEntry& entry) { - REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Id) == entry.EntryName); REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Name) == entry.DisplayName); REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Version) == entry.DisplayVersion); @@ -126,8 +125,9 @@ void VerifyEntryAgainstIndex(const SQLiteIndex& index, SQLiteIndex::IdType manif auto metadata = index.GetMetadataByManifestId(manifestId); - VerifyInstalledType(metadata, entry.WindowsInstaller.value_or(false) ? ManifestInstaller::InstallerTypeEnum::Msi : ManifestInstaller::InstallerTypeEnum::Exe); + VerifyInstalledType(metadata, entry.WindowsInstaller.value_or(false) ? InstallerTypeEnum::Msi : InstallerTypeEnum::Exe); VerifyTestScope(metadata); + VerifyMetadataString(metadata, PackageVersionMetadata::Publisher, entry.Publisher); VerifyMetadataString(metadata, PackageVersionMetadata::InstalledLocation, entry.InstallLocation); VerifyMetadataString(metadata, PackageVersionMetadata::StandardUninstallCommand, entry.UninstallString); VerifyMetadataString(metadata, PackageVersionMetadata::SilentUninstallCommand, entry.QuietUninstallString); @@ -151,7 +151,7 @@ TEST_CASE("ARPHelper_GetARPForArchitecture", "[arphelper][list]") ARPHelper helper; - auto nativeMachineKey = helper.GetARPKey(ManifestInstaller::ScopeEnum::Machine, systemArch); + auto nativeMachineKey = helper.GetARPKey(ScopeEnum::Machine, systemArch); REQUIRE(nativeMachineKey); } @@ -246,7 +246,7 @@ TEST_CASE("ARPHelper_DetermineVersion_Version", "[arphelper][list]") SetRegistryValue(root.get(), helper.VersionMinor, 14); auto result = helper.DetermineVersion(key); - REQUIRE(result == "2.7.42"); + REQUIRE(result == "3.14"); } TEST_CASE("ARPHelper_DetermineVersion_VersionMajorMinor", "[arphelper][list]") diff --git a/src/AppInstallerCLITests/Regex.cpp b/src/AppInstallerCLITests/Regex.cpp new file mode 100644 index 0000000000..974c800682 --- /dev/null +++ b/src/AppInstallerCLITests/Regex.cpp @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include + +using namespace std::string_view_literals; +using namespace AppInstaller::Regex; + + +TEST_CASE("Regex_Construction", "[regex]") +{ + Expression empty; + REQUIRE(!empty); + + Expression lowerVowels("(a|e|i|o|u)"); + REQUIRE(lowerVowels); + + // Ensure functionality to later verify against copy + REQUIRE(lowerVowels.IsMatch(L"a")); + REQUIRE(!lowerVowels.IsMatch(L"b")); + + Expression copy = lowerVowels; + REQUIRE(copy); + + // Ensure that the copy can also work + REQUIRE(lowerVowels.IsMatch(L"a")); + REQUIRE(copy.IsMatch(L"a")); + + REQUIRE(!lowerVowels.IsMatch(L"b")); + REQUIRE(!copy.IsMatch(L"b")); + + Expression moved = std::move(copy); + REQUIRE(moved); + + REQUIRE(moved.IsMatch(L"a")); + REQUIRE(!moved.IsMatch(L"b")); +} + +TEST_CASE("Regex_IsMatch", "[regex]") +{ + Expression ArchitectureX32{ R"((X32|X86)(?=\P{Nd}|$)(?:\sEDITION)?)", Options::CaseInsensitive }; + + REQUIRE(ArchitectureX32.IsMatch(L"X32")); + REQUIRE(ArchitectureX32.IsMatch(L"X86 edition")); + + REQUIRE(!ArchitectureX32.IsMatch(L"Not a match")); + REQUIRE(!ArchitectureX32.IsMatch(L"X86 editions")); +} + +TEST_CASE("Regex_Replace", "[regex]") +{ + Expression test{ R"((b|d\s|vy))" }; + REQUIRE(test.Replace(L"The bright and swervy", {}) == std::wstring{ L"The right answer" }); + + Expression vowels{ "(a|e|i|o|u)", Options::CaseInsensitive }; + REQUIRE(vowels.Replace(L"The QUICK brown fox jumped over the lazy dog.", L"[$0]") == std::wstring{ L"Th[e] Q[U][I]CK br[o]wn f[o]x j[u]mp[e]d [o]v[e]r th[e] l[a]zy d[o]g." }); +} + +TEST_CASE("Regex_ForEach", "[regex]") +{ + std::wstring input = L"The words in the Sentence but no more"; + std::vector expected = { L"The", L"words", L"in", L"the", L"Sentence" }; + + Expression test{ R"(\S+)" }; + + size_t i = 0; + test.ForEach(input, + [&](bool isMatch, std::wstring_view text) + { + if (!isMatch) + { + REQUIRE(text == L" "); + return true; + } + else + { + REQUIRE(i < expected.size()); + REQUIRE(text == expected[i]); + ++i; + + return i < expected.size(); + } + }); +} diff --git a/src/AppInstallerCLITests/Registry.cpp b/src/AppInstallerCLITests/Registry.cpp index 7cf1dc1152..ad5f5cf950 100644 --- a/src/AppInstallerCLITests/Registry.cpp +++ b/src/AppInstallerCLITests/Registry.cpp @@ -80,6 +80,30 @@ TEST_CASE("Values_String", "[registry]") REQUIRE(value->GetValue() == ConvertToUTF8(valueValue)); } +TEST_CASE("Values_WideStringWithNarrowNull", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + std::wstring valueValue = L"TestValueValue"; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + + // Copy the bytes from the string value into a byte vector + std::vector valueBytes; + valueBytes.resize((valueValue.length() + 1) * sizeof(wchar_t)); + memcpy_s(valueBytes.data(), valueBytes.size(), valueValue.c_str(), (valueValue.length() + 1) * sizeof(wchar_t)); + // Remove the last byte to make a narrow null + valueBytes.resize(valueBytes.size() - 1); + + SetRegistryValue(root.get(), valueName, valueBytes, REG_SZ); + + Key key{ root.get(), L"" }; + + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::String); + REQUIRE(value->GetValue() == ConvertToUTF8(valueValue)); +} + TEST_CASE("Values_ExpandString", "[registry]") { std::wstring valueName = L"TestValueName"; diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index 1ec5aee142..27ad7f5aec 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -32,7 +32,7 @@ SQLiteIndex CreateTestIndex(const std::string& filePath, std::optional("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; relativePath = "test/id/1.0.0.yaml"; @@ -125,8 +136,35 @@ struct IndexFields ProductCodes(std::move(productCodes)) {} + IndexFields( + std::string id, + std::string name, + std::string publisher, + std::string moniker, + std::string version, + std::string channel, + std::vector tags, + std::vector commands, + std::string path, + std::vector packageFamilyNames, + std::vector productCodes + ) : + Id(std::move(id)), + Name(std::move(name)), + Publisher(std::move(publisher)), + Moniker(std::move(moniker)), + Version(std::move(version)), + Channel(std::move(channel)), + Tags(std::move(tags)), + Commands(std::move(commands)), + Path(std::move(path)), + PackageFamilyNames(std::move(packageFamilyNames)), + ProductCodes(std::move(productCodes)) + {} + std::string Id; std::string Name; + std::string Publisher; std::string Moniker; std::string Version; std::string Channel; @@ -143,17 +181,25 @@ SQLiteIndex SearchTestSetup(const std::string& filePath, std::initializer_list(d.Name); + manifest.DefaultLocalization.Add(d.Publisher); + manifest.Moniker = d.Moniker; manifest.Version = d.Version; - manifest.Channel = d.Channel; - manifest.Tags = d.Tags; - manifest.Commands = d.Commands; + manifest.DefaultLocalization.Add(d.Tags); manifest.Installers.resize(std::max(d.PackageFamilyNames.size(), d.ProductCodes.size())); + if (manifest.Installers.size() == 0) + { + manifest.Installers.push_back({}); + } + + manifest.Channel = d.Channel; + manifest.Installers[0].Commands = d.Commands; + for (size_t i = 0; i < d.PackageFamilyNames.size(); ++i) { manifest.Installers[i].PackageFamilyName = d.PackageFamilyNames[i]; @@ -181,6 +227,12 @@ bool ArePackageFamilyNameAndProductCodeSupported(const SQLiteIndex& index, const return (index.GetVersion() >= Schema::Version{ 1, 1 } && testVersion >= Schema::Version{ 1, 1 }); } +bool AreNormalizedNameAndPublisherSupported(const SQLiteIndex& index, const Schema::Version& testVersion) +{ + UNSCOPED_INFO("Index " << index.GetVersion() << " | Test " << testVersion); + return (index.GetVersion() >= Schema::Version{ 1, 2 } && testVersion >= Schema::Version{ 1, 2 }); +} + bool IsManifestMetadataSupported(const SQLiteIndex& index, const Schema::Version& testVersion) { UNSCOPED_INFO("Index " << index.GetVersion() << " | Test " << testVersion); @@ -319,24 +371,26 @@ TEST_CASE("SQLiteIndex_RemoveManifest", "[sqliteindex][V1_0]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; - + manifest2.DefaultLocalization.Add({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; + { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -391,23 +445,25 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; + manifest2.DefaultLocalization.Add({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; SQLiteIndex index = CreateTestIndex(tempFile); @@ -442,7 +498,7 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") REQUIRE(rowId.value() == manifest2RowId); REQUIRE(manifest2.Id == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Id)); - REQUIRE(manifest2.Name == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Name)); + REQUIRE(manifest2.DefaultLocalization.Get() == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Name)); REQUIRE(manifest2.Version == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Version)); REQUIRE(manifest2.Channel == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::Channel)); REQUIRE(manifest2Path == index.GetPropertyByManifestId(manifest2RowId, PackageVersionProperty::RelativePath)); @@ -486,13 +542,14 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add < Localization::PackageName>("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -521,16 +578,16 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_0]") // Update with no updates should return false REQUIRE(!index.UpdateManifest(manifest, manifestPath)); - manifest.Description = "description2"; + manifest.DefaultLocalization.Add("description2"); // Update with no indexed updates should return false REQUIRE(!index.UpdateManifest(manifest, manifestPath)); // Update with indexed changes - manifest.Name = "Test Name2"; - manifest.AppMoniker = "testmoniker2"; - manifest.Tags = { "t1", "t2", "t3" }; - manifest.Commands = {}; + manifest.DefaultLocalization.Add("Test Name2"); + manifest.Moniker = "testmoniker2"; + manifest.DefaultLocalization.Add({ "t1", "t2", "t3" }); + manifest.Installers[0].Commands = {}; REQUIRE(index.UpdateManifest(manifest, manifestPath)); } @@ -579,13 +636,14 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangePath", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -660,13 +718,14 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]") std::string manifestPath = "test/id/test.id-1.0.0.yaml"; Manifest manifest; + manifest.Installers.push_back({}); manifest.Id = "test.id"; - manifest.Name = "Test Name"; - manifest.AppMoniker = "testmoniker"; + manifest.DefaultLocalization.Add("Test Name"); + manifest.Moniker = "testmoniker"; manifest.Version = "1.0.0-test"; manifest.Channel = "test"; - manifest.Tags = { "t1", "t2" }; - manifest.Commands = { "test1", "test2" }; + manifest.DefaultLocalization.Add({ "t1", "t2" }); + manifest.Installers[0].Commands = { "test1", "test2" }; { SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 0 }); @@ -704,7 +763,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]") { SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite); - manifest.Name = "test name"; + manifest.DefaultLocalization.Add("test name"); // Update with path update should indicate change REQUIRE(index.UpdateManifest(manifest, manifestPath)); @@ -738,12 +797,13 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/id/test.id-2.0.0.yaml"; Manifest manifest2 = manifest1; @@ -999,7 +1059,7 @@ TEST_CASE("SQLiteIndex_NameString", "[sqliteindex]") REQUIRE(results.Matches.size() == 1); auto result = GetNameStringById(index, results.Matches[0].first); - REQUIRE(result == manifest.Name); + REQUIRE(result == manifest.DefaultLocalization.Get()); } TEST_CASE("SQLiteIndex_PathString", "[sqliteindex]") @@ -1199,11 +1259,16 @@ TEST_CASE("SQLiteIndex_SearchResultsTableSearches", "[sqliteindex][V1_0]") std::string value = "test"; // Perform every type of field and match search + PackageMatchFilter filter(PackageMatchField::Id, MatchType::Exact, value); + for (auto field : { PackageMatchField::Id, PackageMatchField::Name, PackageMatchField::Moniker, PackageMatchField::Tag, PackageMatchField::Command }) { + filter.Field = field; + for (auto match : { MatchType::Exact, MatchType::Fuzzy, MatchType::FuzzySubstring, MatchType::Substring, MatchType::Wildcard }) { - search.SearchOnField(field, match, value); + filter.Type = match; + search.SearchOnField(filter); } } } @@ -1821,23 +1886,25 @@ TEST_CASE("SQLiteIndex_CheckConsistency_Failure", "[sqliteindex][V1_1]") std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; Manifest manifest1; + manifest1.Installers.push_back({}); manifest1.Id = "test.id"; - manifest1.Name = "Test Name"; - manifest1.AppMoniker = "testmoniker"; + manifest1.DefaultLocalization.Add("Test Name"); + manifest1.Moniker = "testmoniker"; manifest1.Version = "1.0.0"; manifest1.Channel = "test"; - manifest1.Tags = { "t1", "t2" }; - manifest1.Commands = { "test1", "test2" }; + manifest1.DefaultLocalization.Add({ "t1", "t2" }); + manifest1.Installers[0].Commands = { "test1", "test2" }; std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; Manifest manifest2; + manifest2.Installers.push_back({}); manifest2.Id = "test.woah"; - manifest2.Name = "Test Name WOAH"; - manifest2.AppMoniker = "testmoniker"; + manifest2.DefaultLocalization.Add("Test Name WOAH"); + manifest2.Moniker = "testmoniker"; manifest2.Version = "1.0.0"; manifest2.Channel = "test"; - manifest2.Tags = {}; - manifest2.Commands = { "test1", "test2", "test3" }; + manifest2.DefaultLocalization.Add({}); + manifest2.Installers[0].Commands = { "test1", "test2", "test3" }; SQLite::rowid_t manifestRowId = 0; @@ -1975,3 +2042,91 @@ TEST_CASE("SQLiteIndex_ManifestMetadata", "[sqliteindex]") REQUIRE(index.GetMetadataByManifestId(manifestId2).empty()); } + +TEST_CASE("SQLiteIndex_NormNameAndPublisher_Exact", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string testName = "Name"; + std::string testPublisher = "Publisher"; + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Id1", testName, testPublisher, "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1", {}, { "PC1", "PC2" } }, + }); + + Schema::Version testVersion = TestPrepareForRead(index); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, testName, testPublisher)); + + auto results = index.Search(request); + + if (AreNormalizedNameAndPublisherSupported(index, testVersion)) + { + REQUIRE(results.Matches.size() == 1); + } + else + { + REQUIRE(results.Matches.empty()); + } +} + +TEST_CASE("SQLiteIndex_NormNameAndPublisher_Simple", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string testName = "Name"; + std::string testPublisher = "Publisher"; + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Id1", testName, testPublisher, "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1", {}, { "PC1", "PC2" } }, + }); + + Schema::Version testVersion = TestPrepareForRead(index); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, testName + " 1.0", testPublisher + " Corporation")); + + auto results = index.Search(request); + + if (AreNormalizedNameAndPublisherSupported(index, testVersion)) + { + REQUIRE(results.Matches.size() == 1); + } + else + { + REQUIRE(results.Matches.empty()); + } +} + +TEST_CASE("SQLiteIndex_NormNameAndPublisher_Complex", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string testName = "Name"; + std::string testPublisher = "Publisher"; + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Id1", testName, testPublisher, "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1", {}, { "PC1", "PC2" } }, + { "Id2", testName, "Different Publisher", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2", {}, { "PC1", "PC2" } }, + }); + + Schema::Version testVersion = TestPrepareForRead(index); + + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::NormalizedNameAndPublisher, MatchType::Exact, testName + " 1.0", testPublisher)); + + auto results = index.Search(request); + + if (AreNormalizedNameAndPublisherSupported(index, testVersion)) + { + REQUIRE(results.Matches.size() == 1); + } + else + { + REQUIRE(results.Matches.empty()); + } +} diff --git a/src/AppInstallerCLITests/SQLiteIndexSource.cpp b/src/AppInstallerCLITests/SQLiteIndexSource.cpp index 9d9d3b85d0..adfbe812bc 100644 --- a/src/AppInstallerCLITests/SQLiteIndexSource.cpp +++ b/src/AppInstallerCLITests/SQLiteIndexSource.cpp @@ -108,7 +108,7 @@ TEST_CASE("SQLiteIndexSource_Name", "[sqliteindexsource]") REQUIRE(results.Matches[0].Package); auto latestVersion = results.Matches[0].Package->GetLatestAvailableVersion(); - REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Name).get() == manifest.Name); + REQUIRE(latestVersion->GetProperty(PackageVersionProperty::Name).get() == manifest.DefaultLocalization.Get()); } TEST_CASE("SQLiteIndexSource_Versions", "[sqliteindexsource]") @@ -156,7 +156,7 @@ TEST_CASE("SQLiteIndexSource_GetManifest", "[sqliteindexsource]") REQUIRE(specificResultVersion); auto specificResult = specificResultVersion->GetManifest(); REQUIRE(specificResult.Id == manifest.Id); - REQUIRE(specificResult.Name == manifest.Name); + REQUIRE(specificResult.DefaultLocalization.Get() == manifest.DefaultLocalization.Get()); REQUIRE(specificResult.Version == manifest.Version); REQUIRE(specificResult.Channel == manifest.Channel); @@ -164,10 +164,32 @@ TEST_CASE("SQLiteIndexSource_GetManifest", "[sqliteindexsource]") REQUIRE(latestResultVersion); auto latestResult = latestResultVersion->GetManifest(); REQUIRE(latestResult.Id == manifest.Id); - REQUIRE(latestResult.Name == manifest.Name); + REQUIRE(latestResult.DefaultLocalization.Get() == manifest.DefaultLocalization.Get()); REQUIRE(latestResult.Version == manifest.Version); REQUIRE(latestResult.Channel == manifest.Channel); auto noResultVersion = package->GetAvailableVersion(PackageVersionKey("", "blargle", "flargle")); REQUIRE(!noResultVersion); } + +TEST_CASE("SQLiteIndexSource_IsSame", "[sqliteindexsource]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SourceDetails details; + Manifest manifest; + std::string relativePath; + std::shared_ptr source = SimpleTestSetup(tempFile, details, manifest, relativePath); + + SearchRequest request; + request.Query = RequestMatch(MatchType::Exact, manifest.Id); + + auto result1 = source->Search(request); + REQUIRE(result1.Matches.size() == 1); + + auto result2 = source->Search(request); + REQUIRE(result2.Matches.size() == 1); + + REQUIRE(result1.Matches[0].Package->IsSame(result2.Matches[0].Package.get())); +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp index ecd70cbbba..e3c85cf77e 100644 --- a/src/AppInstallerCLITests/Sources.cpp +++ b/src/AppInstallerCLITests/Sources.cpp @@ -12,6 +12,7 @@ #include #include +using namespace TestCommon; using namespace AppInstaller; using namespace AppInstaller::Runtime; using namespace AppInstaller::Repository; @@ -110,7 +111,7 @@ constexpr std::string_view s_TwoSource_AggregateSourceTest = R"( namespace { // Helper to create a simple source. - struct SourcesTestSource : public TestCommon::TestSource + struct SourcesTestSource : public TestSource { SourcesTestSource() = default; SourcesTestSource(const SourceDetails& details) @@ -136,50 +137,6 @@ namespace return result; } }; - - // Helper that allows some lambdas to be wrapped into a source factory. - struct TestSourceFactory : public ISourceFactory - { - using CreateFunctor = std::function(const SourceDetails&)>; - using AddFunctor = std::function; - using UpdateFunctor = std::function; - using RemoveFunctor = std::function; - - TestSourceFactory() : - m_Create(SourcesTestSource::Create), m_Add([](SourceDetails&) {}), m_Update([](const SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} - - // ISourceFactory - std::shared_ptr Create(const SourceDetails& details, IProgressCallback&) override - { - return m_Create(details); - } - - void Add(SourceDetails& details, IProgressCallback&) override - { - m_Add(details); - } - - void Update(const SourceDetails& details, IProgressCallback&) override - { - m_Update(details); - } - - void Remove(const SourceDetails& details, IProgressCallback&) override - { - m_Remove(details); - } - - // Make copies of self when requested. - operator std::function()>() - { - return [this]() { return std::make_unique(*this); }; - } - - CreateFunctor m_Create; - AddFunctor m_Add; - UpdateFunctor m_Update; - RemoveFunctor m_Remove; - }; } @@ -273,8 +230,8 @@ TEST_CASE("RepoSources_AddSource", "[sources]") std::string data = "thisIsTheData"; bool addCalledOnFactory = false; - TestSourceFactory factory; - factory.m_Add = [&](SourceDetails& sd) { addCalledOnFactory = true; sd.Data = data; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnAdd = [&](SourceDetails& sd) { addCalledOnFactory = true; sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory); ProgressCallback progress; @@ -306,8 +263,8 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") const char* suffix[2] = { "", "2" }; - TestSourceFactory factory1; - factory1.m_Add = [&](SourceDetails& sd) { sd.Data = data; }; + TestSourceFactory factory1{ SourcesTestSource::Create }; + factory1.OnAdd = [&](SourceDetails& sd) { sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory1); ProgressCallback progress; @@ -325,8 +282,8 @@ TEST_CASE("RepoSources_AddMultipleSources", "[sources]") REQUIRE(sources[1].Origin == SourceOrigin::Default); - TestSourceFactory factory2; - factory2.m_Add = [&](SourceDetails& sd) { sd.Data = data + suffix[1]; }; + TestSourceFactory factory2{ SourcesTestSource::Create }; + factory2.OnAdd = [&](SourceDetails& sd) { sd.Data = data + suffix[1]; }; TestHook_SetSourceFactoryOverride(type + suffix[1], factory2); AddSource(name + suffix[1], type + suffix[1], arg + suffix[1], progress); @@ -361,8 +318,8 @@ TEST_CASE("RepoSources_UpdateSource", "[sources]") std::string data = "thisIsTheData"; bool addCalledOnFactory = false; - TestSourceFactory factory; - factory.m_Add = [&](SourceDetails& sd) { addCalledOnFactory = true; sd.Data = data; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnAdd = [&](SourceDetails& sd) { addCalledOnFactory = true; sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory); ProgressCallback progress; @@ -385,7 +342,7 @@ TEST_CASE("RepoSources_UpdateSource", "[sources]") // Reset for a call to update bool updateCalledOnFactory = false; auto now = std::chrono::system_clock::now(); - factory.m_Update = [&](const SourceDetails&) { updateCalledOnFactory = true; }; + factory.OnUpdate = [&](const SourceDetails&) { updateCalledOnFactory = true; }; UpdateSource(name, progress); @@ -413,8 +370,8 @@ TEST_CASE("RepoSources_UpdateSourceRetries", "[sources]") std::string arg = "thisIsTheArg"; std::string data = "thisIsTheData"; - TestSourceFactory factory; - factory.m_Add = [&](SourceDetails& sd) { sd.Data = data; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnAdd = [&](SourceDetails& sd) { sd.Data = data; }; TestHook_SetSourceFactoryOverride(type, factory); ProgressCallback progress; @@ -423,7 +380,7 @@ TEST_CASE("RepoSources_UpdateSourceRetries", "[sources]") // Reset for a call to update bool updateShouldThrow = false; bool updateCalledOnFactoryAgain = false; - factory.m_Update = [&](const SourceDetails&) + factory.OnUpdate = [&](const SourceDetails&) { if (updateShouldThrow) { @@ -449,8 +406,8 @@ TEST_CASE("RepoSources_RemoveSource", "[sources]") std::string data = "thisIsTheData"; bool removeCalledOnFactory = false; - TestSourceFactory factory; - factory.m_Remove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnRemove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; TestHook_SetSourceFactoryOverride(type, factory); ProgressCallback progress; @@ -477,8 +434,8 @@ TEST_CASE("RepoSources_RemoveDefaultSource", "[sources]") REQUIRE(sources[0].Origin == SourceOrigin::Default); bool removeCalledOnFactory = false; - TestSourceFactory factory; - factory.m_Remove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnRemove = [&](const SourceDetails&) { removeCalledOnFactory = true; }; TestHook_SetSourceFactoryOverride(sources[0].Type, factory); ProgressCallback progress; @@ -503,14 +460,14 @@ TEST_CASE("RepoSources_UpdateOnOpen", "[sources]") std::string data = "testData"; bool updateCalledOnFactory = false; - TestSourceFactory factory; - factory.m_Update = [&](const SourceDetails&) { updateCalledOnFactory = true; }; + TestSourceFactory factory{ SourcesTestSource::Create }; + factory.OnUpdate = [&](const SourceDetails&) { updateCalledOnFactory = true; }; TestHook_SetSourceFactoryOverride(type, factory); SetSetting(Streams::UserSources, s_SingleSource); ProgressCallback progress; - auto source = OpenSource(name, progress); + auto source = OpenSource(name, progress).Source; REQUIRE(updateCalledOnFactory); @@ -568,13 +525,13 @@ TEST_CASE("RepoSources_DropAllSources", "[sources]") TEST_CASE("RepoSources_SearchAcrossMultipleSources", "[sources]") { TestHook_ClearSourceFactoryOverrides(); - TestSourceFactory factory; + TestSourceFactory factory{ SourcesTestSource::Create }; TestHook_SetSourceFactoryOverride("testType", factory); SetSetting(Streams::UserSources, s_TwoSource_AggregateSourceTest); ProgressCallback progress; - auto source = OpenSource("", progress); + auto source = OpenSource("", progress).Source; SearchRequest request; auto result = source->Search(request); diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index a4cbea78f8..02ea9c49fb 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "TestCommon.h" #include +#include using namespace std::string_view_literals; using namespace AppInstaller::Utility; @@ -147,3 +148,19 @@ TEST_CASE("ExpandEnvironmentVariables", "[strings]") REQUIRE(ExpandEnvironmentVariables(L"%TEMP%") == tempPath); } + +TEST_CASE("PathOutput", "[strings]") +{ + std::string original = "\xe6\xb5\x8b\xe8\xaf\x95"; + std::filesystem::path path = ConvertToUTF16(original); + AICLI_LOG(Test, Info, << path); + + std::istringstream in; + std::ostringstream out; + AppInstaller::CLI::Execution::Reporter reporter{ out, in }; + + reporter.Info() << path; + + std::string output = out.str(); + REQUIRE(output.substr(output.size() - original.size()) == original); +} diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp index 0a0de9dc68..e6ba3b3b00 100644 --- a/src/AppInstallerCLITests/TestCommon.cpp +++ b/src/AppInstallerCLITests/TestCommon.cpp @@ -179,9 +179,9 @@ namespace TestCommon THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, type, reinterpret_cast(value.c_str()), static_cast(sizeof(wchar_t) * (value.size() + 1)))); } - void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector& value) + void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector& value, DWORD type) { - THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, REG_BINARY, reinterpret_cast(value.data()), static_cast(value.size()))); + THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, type, reinterpret_cast(value.data()), static_cast(value.size()))); } void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value) diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h index 2f8314e129..aab515acf3 100644 --- a/src/AppInstallerCLITests/TestCommon.h +++ b/src/AppInstallerCLITests/TestCommon.h @@ -106,7 +106,7 @@ namespace TestCommon // Set registry values. void SetRegistryValue(HKEY key, const std::wstring& name, const std::wstring& value, DWORD type = REG_SZ); - void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector& value); + void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector& value, DWORD type = REG_BINARY); void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value); } diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json new file mode 100644 index 0000000000..6bfedac997 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Invalid.json @@ -0,0 +1 @@ +"A valid JSON file that does not conform to the schema" \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json new file mode 100644 index 0000000000..7e50697753 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-Malformed.json @@ -0,0 +1 @@ +This is not a valid JSON file. \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json new file mode 100644 index 0000000000..4ea305296a --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackage.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "2.0.0.0" + }, + { + "Id": "MissingPackage", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json new file mode 100644 index 0000000000..4d2b076e5a --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownPackageVersion.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "4.3.2.1" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json new file mode 100644 index 0000000000..213cc6eff2 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Bad-UnknownSource.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//bad", + "Identifier": "*BadSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json b/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json new file mode 100644 index 0000000000..4463587609 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good-AlreadyInstalled.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "1.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/ImportFile-Good.json b/src/AppInstallerCLITests/TestData/ImportFile-Good.json new file mode 100644 index 0000000000..42c46904d6 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ImportFile-Good.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "AppInstallerCliTest.TestExeInstaller", + "Version": "2.0.0.0" + }, + { + "Id": "AppInstallerCliTest.TestMsixInstaller", + "Version": "2.0.0.0" + } + ], + "SourceDetails": { + "Argument": "//arg", + "Identifier": "*TestSource", + "Name": "TestSource", + "Type": "Microsoft.TestSource" + } + } + ], + "WinGetVersion": "1.0.0" +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InputNames.txt b/src/AppInstallerCLITests/TestData/InputNames.txt new file mode 100644 index 0000000000..3d33d50fef --- /dev/null +++ b/src/AppInstallerCLITests/TestData/InputNames.txt @@ -0,0 +1,1137 @@ +0 A.D. +010 Editor 11.0 (64-bit) +360安全卫士 +360杀毒 +4K Slideshow Maker 1.8 +4K Stogram +4K Video Downloader +4K Video Downloader 4.12 +4K Video to MP3 +4K YouTube to MP3 3.12 +7-Zip 16.04 (x64 edition) +7-Zip 19.00 (x64 edition) +7-Zip 20.02 alpha (x64) +7-Zip ZS 19.00 ZS v1.4.5 R2 (x64) +AWS Command Line Interface +AWS Command Line Interface v2 +AWS SAM Command Line Interface +AXIS Camera Station 5.33 +AbaClient +Accessibility Insights For Windows v1.1 +Active Directory Authentication Library for SQL Server +Adobe Acrobat Reader DC - Czech +Adobe Acrobat Reader DC +Adobe Acrobat Reader DC MUI +AdoptOpenJDK JDK with Hotspot 11.0.7.10 (x64) +AdoptOpenJDK JDK with Hotspot 11.0.8.10 (x64) +AdoptOpenJDK JDK with Hotspot 14.0.1.7 (x64) +AdoptOpenJDK JDK with Hotspot 14.0.2.12 (x64) +AdoptOpenJDK JDK with Hotspot 15.0.0.36 (x64) +AdoptOpenJDK JDK with Hotspot 8.0.252.09 (x64) +AdoptOpenJDK JDK with Hotspot 8.0.265.01 (x64) +Advanced IP Scanner 2.5 +Advanced Log Viewer 8.1.0 +Advanced Port Scanner 2.5 +AdvancedRestClient 15.0.5 +Aegisub 8975-master-8d77da3 +Aegisub r8942 +AeroZoom 4.0 beta 2 +Alacritty +Alchemy Beta x64 +Algodoo v2.1.0 +AltDrag +Amazon Chime +Amazon Corretto (x64) +Amazon Corretto 8 (x64) +Amazon WorkSpaces +Anaconda3 2020.07 (Python 3.8.3 64-bit) +Angry IP Scanner +AntiMicro +AppGet +Appium 1.15.1 +Appium 1.18.3 +Arduino +Armagetron Advanced 0.2.8.3.5 +Artha 1.0.3.0 +AssaultCube 1.2.0.2 +Audacity 2.4.2 +AuthPass version 1.7.9_1605 +Auto Dark Mode +AutoHotkey 1.1.32.00 +AutoHotkey 1.1.33.02 +Autopsy +AviSynth 2.6 +Avro Keyboard 5.6.0 +Aya +Azure Cosmos DB Emulator +Azure Data Studio (User) +Azure Functions Core Tools - 3.0.2534 (x64) +Azure IoT Explorer (preview) +Azure IoT explorer +BCUninstaller +BITS Manager +BKChem-0.13.0 +BOINC +BPBible 0.5.3.1 +Backup and Sync from Google +Barrier 2.3.2-snapshot +Barrier 2.3.3-release +Beaker Browser 0.8.10 +Beats winlogbeat 7.7.0 (x86_64) +Beats winlogbeat 7.9.2 (x86_64) +BeeBEEP 5.8.2 +Beeftext +Betaflight Configurator +Beyond Compare 4 +Beyond Compare 4.3.6 +Beyond Compare 4.3.7 +BiglyBT +BitPay version 4.8.1 +Bitwarden +BleachBit 4.0.0.1628 (current user) +Blender +BlueJeans +Bob the Hamster VGA 2008-01-23 +Bonjour +Borderless Gaming +Bot Framework Composer 1.0.0 +Bot Framework Composer 1.0.1 +Bot Framework Composer 1.0.2 +Bot Framework Composer 1.1.1 +Bot Framework Emulator 4.10.0 +Bot Framework Emulator 4.8.1 +Bot Framework Emulator 4.9.0 +Brackets +Brave +Brave Nightly +Bulk Rename Utility 3.3.1.0 (64-bit) +Buttercup 1.19.0 +Buttercup 1.20.0 +C-Dogs SDL +CCEnhancer version 4.5.6 +CCleaner +CDBurnerXP (64 bit) +CMake +CORSAIR iCUE Software +CPUID CPU-Z 1.92 +CPUID CPU-Z 1.93 +CPUID CPU-Z 1.94 +CPUID HWMonitor 1.41 +CPUID HWMonitor 1.42 +Cacher +CaesiumPH 0.9.5 +Camtasia 2019 +Camtasia 2020 +Caprine 2.47.0 +Caprine 2.48.0 +Caption 2.0.1 (only current user) +Captura v8.0.0 +CemUI 2.3.3 (only current user) +Cerebro 0.3.2 +CertAid for Windows +CertDump +Certify The Web version 5.1.5 +ChMac 2.0 +ChefDK v4.11.0 +ChemAxon ChemCurator +ChemAxon Markush Editor +ChemAxon Marvin Suite 20.13.0 +ChemAxon Marvin Suite 20.19.0 +Chromium +Circuit Diagram version 3.0 +Circuit Diagram version 3.1 +Cisco Webex Meetings +Citycraft Launcher 1.9.9 +ClamWin Free Antivirus 0.99.4 +Clash for Windows 0.11.1 +Clash for Windows 0.11.6 +Clash for Windows 0.11.8 +Clash for Windows 0.9.11 +ClassIn +Clementine +Clink v0.4.9 +Cloudflare WARP +CodeBlocks +CodeLite +Coffee +Colobot: Gold Edition alpha-0.1.12 +Color Cop 5.4.3 +Colorpicker 2.0.3 +ConEmu 201011.x64 +Concept2 Utility +ConfigMgr 2012 Toolkit R2 +Contasimple Desktop 3.1.0 +Core Temp 1.16 +Couchbase Server 6.5.1-6299 Community Edition +Couchbase Server 6.5.1-6299 Enterprise Edition +Cozy Drive 3.20.0 +Cppcheck x64 2.0 +Crypter 4.0.0 +Cryptomator +CrystalDiskInfo 8.6.1 +CrystalDiskInfo 8.6.2 +CrystalDiskInfo 8.6.2 Kurei Kei Edition +CrystalDiskInfo 8.6.2 Shizuku Edition +CrystalDiskInfo 8.7.0 +CrystalDiskInfo 8.8.1 +CrystalDiskInfo 8.8.1 Kurei Kei Edition +CrystalDiskInfo 8.8.1 Shizuku Edition +CrystalDiskInfo 8.8.5 +CrystalDiskInfo 8.8.9 +CrystalDiskInfo 8.8.9 Kurei Kei Edition +CrystalDiskInfo 8.8.9 Shizuku Edition +CrystalDiskInfo 8_8_2 +CrystalDiskInfo 8_8_2 Kurei Kei Edition +CrystalDiskInfo 8_8_2 Shizuku Edition +CrystalDiskMark 7.0.0h +CrystalDiskMark 7.0.0h Shizuku Edition +CubicSDR 0.2.5 Installer +CutePDF Writer +Cyberduck +DB Browser for SQLite +DBeaver 7.0.5 (current user) +DBeaver 7.1.0 (current user) +DBeaver 7.1.2 (current user) +DBeaver 7.1.3 (current user) +DBeaver 7.1.4 (current user) +DBeaver 7.2.0 (current user) +DBeaver 7.2.1 (current user) +DBeaver 7.2.2 (current user) +DBeaver 7.2.3 (current user) +DJI Assistant 2 (DJI FPV series) version V2.0.2.11 +DJI Assistant 2 For Aeroscope version V2.0.1.3 +DJI Assistant 2 For Autopilot version V2.0.3.7 +DJI Assistant 2 For Battery Station version V2.0.1.9 +DJI Assistant 2 For MG version V2.0.18.1 +DJI Assistant 2 For Matrice version V2.0.13.2 +DJI Assistant 2 For Mavic version V2.0.14.1 +DJI Assistant 2 For Phantom version V2.0.10.4 +Dave Gnukem +DeepVocalToolBox_beta_1.1.6 version beta_1.1.6 +DeepVocal_beta_1.1.6 version beta_1.1.6 +Deezer 4.19.20 +DefaultAudio +Defraggler +Dell Command | Update +Dell Update +Dev-C++ +Dimension 4 v5.31 +Discord Media Loader version 1.3.0.0 +Discord Media Loader version 1.4.0.0 +Ditto +Dixa +DjVuLibre DjView 3.5.27+4.11 +DockStation 1.5.1 +Docker Desktop +Dokan Library 1.3.0.1000 (x64) +Dokan Library 1.4.0.1000 (x64) +Dolphin +Doomsday 2.2.2.3313 +Dopamine +Doxie 2.12.2 +Dropbox +EGR-SafenetActivation +EGR-ShellExtension +EagleGet version 2.1.5.10 +EagleGet version 2.1.6.70 +EasyConnect +EditPlus (64 bit) +EduMIPS64 +Elasticsearch 7.9.2 +Elgato Stream Deck +Empoche 0.4.0 +Empoche 0.4.3 +Encrypto version 1.0.1 +EnglishizeCmd 2.0 +Enpass +Eraser 6.2.0.2986 +Eraser 6.2.0.2988 +Eraser 6.2.0.2989 +Eraser 6.2.0.2990 +Esteem 2.2.7 +Ethereum - Geth - Official Go implementation of the Ethereum protocol +Evernote v. 6.21.2 +Evernote v. 6.24.2 +Everything 1.4.1.969 (x64) +Everything 1.4.1.986 (x64) +Everything 1.4.1.988 (x64) +Everything 1.4.1.988 Lite (x64) +Everything 1.4.1.992 (x64) +ExpressVPN +Expresso +Extreme TuxRacer 0.8 (x64) +Far Manager 3 x64 +FastCopy +FastStone Capture 9.3 +FastStone Image Viewer 7.5 +Fedora Media Writer +Ferdi 5.5.0 +Fiddler Everywhere 1.0.2 +Fiddler Everywhere 1.1.0 +Fiddler Everywhere 1.1.0-insiders +Fiddler Everywhere 1.1.0-internal +Fiddler Everywhere 1.1.1 +Fiddler Everywhere 1.1.1-insiders +Fiddler Everywhere 1.1.1-internal +File Converter (64 bit) +FileSeek 6.4 +FileZilla Client 3.47.0 +FileZilla Client 3.48.1 +FileZilla Client 3.49.1 +FileZilla Client 3.50.0 +FileZilla Client 3.51.0 +Firefox Developer Edition 77.0 (x64 en-US) +Firefox Developer Edition 78.0 (x64 en-US) +Firefox Developer Edition 80.0 (x64 en-US) +Firefox Developer Edition 82.0 (x64 en-US) +FlashFXP 5 +FlightGear v2018.3.5 +FontBase 2.11.3 +FontForge version 14-03-2020 +FormatFactory 5.3.0.1 +FormatFactory 5.4.5.0 +Foxit PhantomPDF +Foxit Reader +Franz 5.5.0 +FreeCommander XE +FreeMat +GIMP 2.10.0 +GIMP 2.10.10 +GIMP 2.10.14 +GIMP 2.10.16 +GIMP 2.10.18 +GIMP 2.10.20 +GIMP 2.10.6 +GIMP 2.10.8 +GNU Arm Embedded Toolchain 9-2020-q2-update 9 2020 (remove only) +GNU Midnight Commander version 4.8.24 (build: 20200521-217) +GNU Privacy Guard +GNURadio-3.7 +GOG GALAXY +GPL Ghostscript +GSview 5.0 +Garmin Express +Gauge 1.0.6 +Gauge 1.1.1 +Geany 1.36 +Geekbench 5 +Gephi 0.9.2 +GetDiz +Git Extensions 3.3.1.7897 +Git Extensions 3.4.1.9675 +Git Extensions 3.4.3.9999 +Git LFS version 2.11.0 +Git version 2.24.1.2 +Git version 2.25.1 +Git version 2.26.2 +Git version 2.27.0 +Git version 2.28.0 +Git version 2.29.0 +GitHub CLI +GitHub Desktop Machine-Wide Installer +GitHubReleaseNotes +Gitter +Glimpse 0.1.2 +Glimpse 0.2.0 (64-bit) +Glimpse 0.2.0 +GnuCash 3.9 +GnuCash 4.1 +GnuWin32: Grep-2.5.4 +GnuWin32: Make-3.81 +GnuWin32: Wget-1.11.4-1 +GnuWin32: Zip-3.0 +Go Programming Language amd64 go1.13.12 +Go Programming Language amd64 go1.13.13 +Go Programming Language amd64 go1.13.14 +Go Programming Language amd64 go1.13.15 +Go Programming Language amd64 go1.14.10 +Go Programming Language amd64 go1.14.3 +Go Programming Language amd64 go1.14.4 +Go Programming Language amd64 go1.14.5 +Go Programming Language amd64 go1.14.6 +Go Programming Language amd64 go1.14.7 +Go Programming Language amd64 go1.14.8 +Go Programming Language amd64 go1.14.9 +Go Programming Language amd64 go1.15 +Go Programming Language amd64 go1.15.1 +Go Programming Language amd64 go1.15.2 +Go Programming Language amd64 go1.15.3 +Go Programming Language amd64 go1.15beta1 +GoldWave v6.52 +Google Chrome +Google Cloud SDK +Google Earth Pro +Gpg4win (3.1.11) +Gpg4win (3.1.13) +GrafanaEnterprise +GrafanaOSS +Grammarly for Microsoft® Office Suite +GrampsAIO64 +GraphQL Playground 1.8.10 +GraphiQL 0.7.2 +Graphviz +Greenshot 1.2.10.6 +Grid 1.6.2 +Grindstone 4 +HHD Software Free Hex Editor Neo 6.44 +HM NIS Edit 2.0.3 +HP Cloud Recovery Tool +HUAWEI Cloud +HWiNFO64 Version 6.26 +HWiNFO64 Version 6.28 +HWiNFO64 Version 6.30 +HWiNFO64 Version 6.32 +HandyWinGet +Harmony 0.9.1 (only current user) +HashTab 5.2.0.14 +HashTab 6.0.0.34 +HeavyLoad V3.6 (64 bit) +Hedgewars +HeidiSQL 11.0.0.5919 +HeidiSQL 11.0.0.5995 +HeidiSQL 11.0.0.5997 +HeidiSQL 11.0.0.6000 +HeidiSQL 11.0.0.6057 +Helix Core Apps +HelpNDoc 6.9.0.577 Personal Edition +HexChat +Hosts File Editor +Hover +HttpMaster Express Edition 4.7.0 +HttpMaster Express Edition 4.7.1 +HttpMaster Express Edition 4.7.2 +HttpMaster Express Edition 4.7.3 +HttpMaster Professional Edition 4.7.1 +HttpMaster Professional Edition 4.7.2 +HttpMaster Professional Edition 4.7.3 +Huawei QuickApp IDE +Hyne Timber Design 7.5.14.0 +Hyperspace Desktop 1.1.3 +IAP Desktop +IDA Freeware 7.0 +IO Ninja 3 +IPFilter 3.0.2.9-beta +IRCCloud 0.15.0 +IZArc 4.4 +ImageGlass +Inkscape +Inno Setup version 6.0.4 +Inno Setup version 6.0.5 +InternetOff 3.0, 32\64 bit edition +Intuiter 0.5.0 +IrfanView 4.54 (64-bit) +IronPython 2.7.10 +IronPython 2.7.9 +IsWiX +IsoBuster 4.6 +JChem .NET API 20.19.0.482 +JabRef +Jackett +Jami +Java 8 Update 251 (64-bit) +Java 8 Update 261 (64-bit) +JetBrains Toolbox +Jitsi Meet 2.3.1 +Jitsi Meet 2.4.1 +Joplin 1.0.201 +Joplin 1.0.216 +Joplin 1.0.233 +Julia 1.4.1 +Julia 1.4.2 +Julia 1.5.1 +K-Lite Codec Pack 15.7.0 Standard +K-Lite Mega Codec Pack 15.7.0 +KKBOX +Kaku 2.0.2 +KeePass Password Safe 2.44 +KeePass Password Safe 2.45 +KeePass Password Safe 2.46 +KeePassXC +KeeWeb +Keybase +KiCad 5.1.5_1 +KiCad 5.1.6_1 +KiCad 5.1.7_1 +Krisp +Krita (x64) 4.3.0 +L'Math version r1.6 +LBRY 0.45.1 +LBRY 0.45.2 +LBRY 0.46.2 +LBRY 0.47.0 +LBRY 0.47.1 +LINE +LINQPad 6 +LLVM +LMMS 1.2.1 +LMMS 1.2.2 +LOVE 11.3 +Laragon 4.0.15 +LastPass (uninstall only) +Lazarus 2.0.8 +League of Legends +Lenovo Migration Assistant +Lenovo System Update +Lens 3.5.1 +Leonflix 0.7.0 +Liberica JDK 11 (64-bit) +Liberica JDK 11 Full (64-bit) +Liberica JDK 14 (64-bit) +Liberica JDK 14 Full (64-bit) +Liberica JDK 15 (64-bit) +Liberica JDK 15 Full (64-bit) +Liberica JDK 8 (64-bit) +Liberica JDK 8 Full (64-bit) +LibreCAD +LibreOffice 7.0.1.2 +LibreOffice 7.0.2.2 +Lidarr version 0.7.1 +LightBulb 2.0 +LightBulb 2.2 +Lightscreen version 2.4 +Linrad-04.14a version 04.14a +Lisk Hub 1.22.0 +Listen1 2.13.0 +Listen1 2.5.2 +Local 5.5.3 +LockHunter 3.3, 32/64 bit +LogFusion 6.4 +LogFusion 6.4.1 +Logitech Gaming Software 9.02 +Loom 0.37.2 +LyX 2.3.4.4 +LyX 2.3.5.2 +MCX Studio version nightlybuild +MCX Studio version v2020 +MKVToolNix 46.0.0 (64-bit) +MKVToolNix 47.0.0 (64-bit) +MKVToolNix 48.0.0 (64-bit) +MKVToolNix 49.0.0 (64-bit) +MKVToolNix 50.0.0 (64-bit) +MPC-HC 1.7.13 (64-bit) +MPC-HC 1.9.5 (64-bit) +MPC-HC 1.9.6 (64-bit) +MQTT Explorer 0.3.5 +MSIX Core +MX5 +MY.GAMES GameCenter +MacType +MailWasher +MailWasherPro +Majsoul Plus 2.0.0 +MakeMKV v1.15.3 +Malwarebytes version 4.2.1.89 +Marble version 2.2.0 +MariaDB 10.5 (x64) +Mark Text 0.16.2 +Markdown Monster 1.22.8.0 +Markdown Monster 1.23.0.0 +Markdown Monster 1.23.12.0 +Markdown Monster 1.23.14.0 +Markdown Monster 1.24.12.0 +Markdown Outlook +Master Packager +Mattermost +MediaInfo 20.09 +MediaInfo-CLI 20.09 +MediaMonkey 4.1 +Meld +Memurai Developer +Microsoft .NET Core SDK 3.1.202 (x64) +Microsoft .NET Core SDK 3.1.300 (x64) +Microsoft .NET Core SDK 3.1.301 (x64) +Microsoft .NET Core SDK 3.1.302 (x64) +Microsoft .NET Core SDK 3.1.401 (x64) +Microsoft .NET Core SDK 3.1.402 (x64) +Microsoft .NET Framework 4.5 Multi-Targeting Pack +Microsoft .NET Framework 4.5.1 Multi-Targeting Pack (ENU) +Microsoft .NET Framework 4.5.1 Multi-Targeting Pack +Microsoft .NET Framework 4.5.1 SDK +Microsoft .NET Framework 4.5.2 Multi-Targeting Pack (ENU) +Microsoft .NET Framework 4.5.2 Multi-Targeting Pack +Microsoft .NET Framework 4.7.2 SDK +Microsoft .NET Framework 4.7.2 Targeting Pack +Microsoft .NET Framework 4.8 SDK +Microsoft .NET Framework 4.8 Targeting Pack +Microsoft .NET SDK 5.0.100-preview.5.20279.10 (x64) +Microsoft .NET SDK 5.0.100-preview.8.20417.9 (x64) +Microsoft .NET SDK 5.0.100-rc.1.20452.10 (x64) +Microsoft .NET SDK 5.0.100-rc.2.20479.15 (x64) +Microsoft Azure CLI +Microsoft Azure Storage Emulator - v5.10 +Microsoft Azure Storage Explorer version 1.14.0 +Microsoft Azure Storage Explorer version 1.15.1 +Microsoft Deployment Toolkit (6.3.8456.1000) +Microsoft Edge +Microsoft Edge Beta +Microsoft Edge Dev +Microsoft Garage Mouse without Borders +Microsoft Help Viewer 2.2 +Microsoft Help Viewer 2.3 +Microsoft MPI (10.1.12498.16) +Microsoft MPI (10.1.12498.18) +Microsoft MPI SDK (10.1.12498.18) +Microsoft ODBC Driver 13 for SQL Server +Microsoft ODBC Driver 17 for SQL Server +Microsoft OLE DB Driver for SQL Server +Microsoft R Open 3.5.3 +Microsoft R Open 4.0.2 +Microsoft SQL Server 2012 Native Client +Microsoft SQL Server 2014 Management Objects +Microsoft SQL Server 2016 +Microsoft SQL Server 2016 Policies +Microsoft SQL Server 2016 T-SQL Language Service +Microsoft SQL Server 2016 T-SQL ScriptDom +Microsoft SQL Server 2017 +Microsoft SQL Server 2017 Policies +Microsoft SQL Server 2017 T-SQL Language Service +Microsoft SQL Server Data-Tier Application Framework (x86) +Microsoft SQL Server Management Studio - 16.5.3 +Microsoft SQL Server Management Studio - 17.9.1 +Microsoft SQL Server Management Studio - 18.5 +Microsoft SQL Server Management Studio - 18.5.1 +Microsoft SQL Server Management Studio - 18.6 +Microsoft Small Basic v1.2 +Microsoft System CLR Types for SQL Server 2014 +Microsoft System CLR Types for SQL Server 2016 +Microsoft System CLR Types for SQL Server 2017 +Microsoft Visio Viewer 2016 +Microsoft Visual Studio 2010 Tools for Office Runtime (x64) +Microsoft Visual Studio 2015 Shell (Isolated) +Microsoft Visual Studio Code (User) +Microsoft Visual Studio Code Insiders (User) +Microsoft Visual Studio Tools for Applications 2015 +Microsoft Visual Studio Tools for Applications 2015 Language Support +Microsoft Visual Studio Tools for Applications 2017 +Microsoft Web Platform Installer 5.1 +Miniconda3 4.7.12 (Python 3.7.4 64-bit) +Miniconda3 py37_4.8.3 (Python 3.7.7 64-bit) +MongoDB 4.2.8 2008R2Plus SSL (64 bit) +Mono for Windows (x64) +MonoGame SDK +Moonlight Game Streaming Client +Motrix 1.5.10 +Motrix 1.5.15 +Mozilla Firefox 68.8.0 ESR (x64 en-US) +Mozilla Firefox 68.9.0 ESR (x64 en-US) +Mozilla Firefox 76.0.1 (x86 en-US) +Mozilla Firefox 77.0 (x64 en-US) +Mozilla Firefox 77.0.1 (x64 en-US) +Mozilla Firefox 78.0 ESR (x64 en-US) +Mozilla Firefox 78.0.1 (x64 en-US) +Mozilla Firefox 78.0.2 (x64 cs) +Mozilla Firefox 78.0.2 (x64 en-US) +Mozilla Firefox 78.1.0 ESR (x64 en-US) +Mozilla Firefox 78.4.0 ESR (x64 en-US) +Mozilla Firefox 79.0 (x64 en-US) +Mozilla Firefox 80.0 (x64 en-US) +Mozilla Firefox 80.0.1 (x64 en-US) +Mozilla Firefox 81.0 (x64 en-US) +Mozilla Firefox 81.0.1 (x64 en-US) +Mozilla Firefox 81.0.2 (x64 en-US) +Mozilla Firefox 82.0 (x64 en-US) +Mozilla Firefox 82.0.1 (x64 en-US) +Mozilla Firefox 82.0.1 (x64 es-MX) +Mozilla Firefox 82.0.2 (x64 en-US) +Mozilla Maintenance Service +Mozilla Thunderbird 68.10.0 (x64 en-US) +Mozilla Thunderbird 68.8.0 (x86 en-US) +Mozilla Thunderbird 68.9.0 (x64 en-US) +Mozilla Thunderbird 77.0 (x64 en-US) +Mozilla Thunderbird 78.0 (x64 cs) +Mozilla Thunderbird 78.0 (x64 en-US) +Mozilla Thunderbird 78.0.1 (x64 en-US) +Mozilla Thunderbird 78.1.0 (x64 en-US) +Mozilla Thunderbird 78.1.1 (x64 en-US) +Mozilla Thunderbird 78.3.2 (x64 en-US) +Mu +Mullvad VPN 2020.5.0 +Mullvad VPN 2020.6.0 +Multilingual App Toolkit 4.0 +Multipass +Mumble 1.3.1 +Mumble 1.3.2 +Mumble 1.3.3 +MuseScore 3 +Muta 2.1.02 +MyHarmony +MyPaint +MySQL Installer - Community +Mypal 28.14.2 (x64 en-US) +NBTExplorer +NSwagStudio +NVDA +NVIDIA NVIDIA RTX Voice Driver 1.0.0.2 +NVIDIA RTX Voice Application +NZXT CAM 4.10.1 +NZXT CAM 4.11.0 +NZXT CAM 4.12.0 +NZXT CAM 4.13.0 +NZXT CAM 4.8.0 +NZXT CAM 4.9.2 +Nelson-0.4.8.2662 (64 bits) +NeoLoad 7.3.0 +Netron 4.5.9 +Nitro Pro +Nmap 7.80 +NoSQLBooster for MongoDB 4.7.5 (only current user) +NoSQLBooster for MongoDB 5.2.12 +NoSQLBooster for MongoDB 6.1.8 +Node.js +Nodist +NordVPN +NordVPN +NordVPN network TAP +NordVPN network TUN +NoteHighlight2016 +Notepad2-mod 4.2.25.998 +Notion 2.0.8 +Notion 2.0.9 +Npcap 0.9982 +NullpoMino version 7.5 +Nullsoft Install System +OBS Studio +OHRRPGCE gorgonzola 20200502 +ONLYOFFICE Desktop Editors 5.6 (x64) +Octave 5.2.0 +OneNote Tagging Kit +Open Shop Channel Downloader version 1.2.9 +Open-Shell +OpenHashTab version 2.2.0 +OpenHashTab version 2.3.0 +OpenJDK 1.8.0_252-2-ojdkbuild +OpenJDK 11.0.7-1-ojdkbuild +OpenJDK 13.0.3-1-ojdkbuild +OpenJDK 14.0.1-1-ojdkbuild +OpenMPT 1.29 (64-Bit) +OpenOffice 4.1.7 +OpenRA +OpenSCAD (remove only) +OpenSSL (64-bit) +OpenShot Video Editor version 2.5.1 +OpenTTD 1.10.1 +OpenTTD 1.10.3 +OpenVPN 2.4.9-I601-Win10 +OpenVPN Configuration Generator x64 +OpenVPN Connect +Opera GX Stable 68.0.3618.142 +Opera GX Stable 68.0.3618.197 +Opera Stable 68.0.3618.63 +Opera Stable 69.0.3686.36 +Opera Stable 69.0.3686.77 +Opera Stable 70.0.3728.144 +Opera Stable 70.0.3728.95 +Opera Stable 71.0.3770.198 +Oracle VM VirtualBox 6.1.10 +Oracle VM VirtualBox 6.1.12 +Oracle VM VirtualBox 6.1.14 +Oracle VM VirtualBox 6.1.16 +OutSystems Development Environment 11 +PDF reDirect (remove only) +PDFsam Basic +PKU_Gateway 0.9.8 +PSPad editor +Packet Sender x64 +Pale Moon 28.10.0 (x64 en-US) +Pale Moon 28.9.3 (x64 en-US) +Pandoc 2.10.1 +Pandoc 2.11.0.2 +Pandoc 2.9.2.1 +Paradox Launcher +Paragon Backup & Recoveryâ„¢ 17 CE +Parsec +PasteIntoFile version 2.0 +PeaZip 7.2.1 (WIN64) +PeaZip 7.3.1 (WIN64) +Persepolis Download Manager version 3.2.0.0 +PhonerLite 2.84 +PhotoSync +PicPick +PicoTorrent +Planet Blupi +PlayStationâ„¢Now +Playnite +Plex +Plex Media Player +Plex Media Server +Plexamp 3.0.3 +Plexamp 3.1.0 +Plexamp 3.1.1 +PokerTH +Postbox 7.0.18 (x86 en-US) +PostgreSQL 12 +PostgreSQL 13 +PowerShell 7-preview-x64 +PowerShell 7-x64 +PowerShell Universal +PowerToys (Preview) +Primesieve version 7.5 +Private Internet Access +Progress Telerik Fiddler +Project My Screen App +ProtonVPN +ProtonVPNTap +PuTTY release 0.74 (64-bit) +Puppet +Puppet Agent (64-bit) +Puppet Bolt +Puppet Development Kit +Pure Data (64-bit) 0.50-2 +PyMODA version 1.1.0 +Python 2.7.18 +Python 3.7 PyAudio-0.2.11 +Python 3.7.7 (64-bit) +Python 3.8.1 (64-bit) +Python 3.8.3 (64-bit) +Python 3.8.4 (64-bit) +Python 3.8.5 (64-bit) +Python 3.8.6 (64-bit) +Python 3.9.0 (64-bit) +Python Launcher +QGIS 3.10.6 'A Coru +QGIS 3.12.3 'Bucuresti' +QGIS 3.14.0 'Pi' +QTextPad version 1.4 +Qalculate! +QtSpim +Quick Picture Viewer +QuickLook +Quicken +R for Windows 4.0.0 +R for Windows 4.0.2 +R for Windows 4.0.3 +RStudio +Rambox 0.7.5 +Rapid Environment Editor version 9.2.0.937 +Raspberry Pi Imager +RawTherapee version 5.8 +Reddit Wallpaper Changer +Reko decompiler for x86-64 +Remote Desktop Manager +Remote Desktop Manager Free +Remote Mouse version 3.015 +RenderDoc +Renode +ResponsivelyApp 0.1.5 +RetroShare +Revo Uninstaller 2.1.7 +Revo Uninstaller Pro 4.3.3 +Robo 3T 1.3.1 +Robo 3T 1.4.1 +Robo 3T 1.4.2 +Rocket.Chat 2.17.9 +RocketDock 1.3.5 +Rocks'n'Diamonds 4.1.4.1 +Rosi +Royal TS 5.02.60420.0 +Royal TS 5.03.60925.0 +Rtools 4.0 (4.0.0.28) +Ruby 2.7.1-1-x64 +Ruby 2.7.1-1-x64 with MSYS2 +Ruby 2.7.2-1-x64 +RunJS 1.10.1 +RunJS 1.11.0 +Rust 1.43 (MSVC 64-bit) +Rust 1.44 (GNU 64-bit) +Rust 1.44 (GNU) +Rust 1.44 (MSVC 64-bit) +Rust 1.44 (MSVC) +Rust 1.45 (GNU 64-bit) +Rust 1.45 (GNU) +Rust 1.45 (MSVC 64-bit) +Rust 1.45 (MSVC) +SIW 2020 v10.6.0915a Trial +SMPlayer 20.4.2 (x64) +SSHFS-Win 2020 (x64) +SVG Explorer Extension 0.1.1 +Samsung DeX +Satisfactory Mod Launcher 1.0.17 +Scratch Desktop 3.11.1 +ScreenToGif +Scribus 1.4.8 (64bit) +ScummVM 2.2.0 +Search Deflector +Sejda PDF Desktop +Seq +SharePoint Online Management Shell +ShareX +SharpKeys +Shotcut +Sigil 1.3.0 +Signal 1.34.1 +Signal 1.34.3 +Signal 1.34.4 +Signal 1.34.5 +Signal 1.36.1 +Signal 1.36.3 +Signal 1.37.2 +Simply Fortran 3 +SitdownMW +Skype version 8.60 +Skype version 8.64 +Skype version 8.65 +Slack Machine-Wide +Snagit 2020 +SnakeTail 64-bit v1.9.6.0 +Snoop +SoapUI 5.5.0 +Sonic Pi +Sonos +Sonos Controller +SoundSwitch 5.0.4.31153 +Sourcetree +SpeedCrunch +Spek +Standard Notes 3.4.1 +Steam +Steel Bank Common Lisp 2.0.0 (X86-64) +SteelSeries Engine 3.17.9 +Stellarium 0.20.1 +Stellarium 0.20.2 +Stellarium 0.20.3 +Strawberry Perl +Streamlabs OBS +Streamlabs OBS 0.23.2 +Streamlabs OBS 0.24.0 +Streamlink +Streamlink Twitch GUI +Stretchly 1.0.0 +Stretchly 1.1.0 +Stretchly 1.2.0 +Stride +Studio 2.0 version 2.0 +Sublime Merge +Sublime Text 3 +SumatraPDF +SuperCollider Version 3.11.0 +SuperTuxKart 1.1.0 - 3D open-source arcade racer with a variety characters, tracks, and modes to play +Surface Duo Emulator version 2020.806.2 +SyncTrayzor (x64) version 1.1.24.0 +System Explorer 7.0.0 +TAP-Windows 9.24.2 +Taiga +Tailscale +Tailscale IPN +Taisei Project +Taskade 3.1.1 +Taskade 3.2.0 +Td-agent v3.8.0 +Td-agent v4.0.1 +TeXstudio - TeXstudio is a fully featured LaTeX editor. +TeXworks 0.6.5 +TeamSpeak 3 Client +TechPowerUp GPU-Z +Telegram Desktop version 2.1.13 +Telegram Desktop version 2.1.20 +Telegram Desktop version 2.1.6 +Telegram Desktop version 2.2 +Telegram Desktop version 2.3 +Telegram Desktop version 2.3.1 +Telegram Desktop version 2.4.1 +Telegram Desktop version 2.4.3 +Telegram Desktop version 2.4.4 +Tera Term 4.105 +Terminus 1.0.117 +Terminus 1.0.119 +Terminus 1.0.120 +Termite +Tesseract-OCR - open source OCR engine +Texmaker 5.0.4 (64-bit) +Texnomic SecureDNS Terminal +Textify v1.8.2 +TickTick version 3.7.1.1 +TightVNC +TikzEdt 0.2.3 +Tiled +TmNationsForever +Toggl Desktop +Toggl Track +TortoiseGit 2.10.0.2 (64 bit) +TortoiseSVN 1.13.1.28686 (64 bit) +TortoiseSVN 1.14.0.28885 (64 bit) +TranslucentTB +Transmission 3.00 (bb6b5a062e) (x64) +Transmission Remote GUI 5.18 +TrayStatus 4.3 +TrayStatus 4.4 +TreeSize Free V4.4.2 +TreeSize V8.0.2 (64 bit) +Trelby +Trillian +TunnelBear +Tux Paint 0.9.23 +Tweeten +Twinkle Tray 1.12.2 +Twitch +TypeRefHasher +USB Safely Remove 6.3 +UXL Launcher Version 3.3.1.0 +UXL Launcher Version 4.0.0.0 +Ultimaker Cura +Ultimaker Cura 4.5 +Ultimaker Cura 4.6 +UltraVnc +Unchecky v1.2 +Unified Remote +Unity Hub 2.4.2 +Update for (KB2504637) +Update for Microsoft Visual Studio 2015 (KB3095681) +Uplay +Ut Video Codec Suite +VCV Rack +VLC media player 3.0.10 (64-bit) +VLC media player 3.0.11 (64-bit) +VLC media player 4.0.0 (64-bit) +VMware Horizon Client +VMware Player +VMware Workstation +VNC Server 6.0.0 +VNC Viewer 6.0.0 +VSCodium (User) +VSCodium +VcXsrv +Vim 8.2 (x64) +VirtViewer 9.0-256 (64-bit) +Vivaldi +Vortex +Vrew 0.4.18 +VulkanSDK 1.2.135.0 +Warzone 2100-3.4.0 +Waterfox Current 2020.05 (x64 en-US) +Waterfox Current 2020.09 (x64 en-US) +Waterfox Current 2020.10 (x64 en-US) +Wayk Now +WeakAuras Companion 3.0.1 +WeakAuras Companion 3.0.2 +WeakAuras Companion 3.0.3 +WeakAuras Companion 3.0.6 +Weka 3.8.4 +Win32DiskImager version 1.0.0 +WinCompose version 0.9.4 +WinDynamicDesktop version 3.4.1.0 +WinDynamicDesktop version 4.2.0.0 +WinDynamicDesktop version 4.3.1.0 +WinFsp 2020 +WinHTTrack Website Copier 3.49-2 (x64) +WinMerge 2.16.6.0 +WinRAR 5.90 (64-bit) +WinRAR 5.91 (64-bit) +WinSCP 5.17.7 +WinZip 24.0 +Winamp +Windows 10 Update Assistant +Windows Admin Center +Windows Assessment and Deployment Kit - Windows 10 +Windows Assessment and Deployment Kit Windows Preinstallation Environment Add-ons - Windows 10 +Windows Driver Kit - Windows 10.0.19041.1 +Windows Driver Package - Dynastream Innovations, Inc. ANT LibUSB Drivers (04/11/2012 1.2.40.201) +Windows Driver Package - Silicon Labs Software (DSI_SiUSBXp_3_1) USB (02/06/2007 3.1) +Windows SDK AddOn +Windows Software Development Kit - Windows 10.0.17763.132 +Windows Software Development Kit - Windows 10.0.18362.1 +Windows Software Development Kit - Windows 10.0.19041.1 +WireGuard +Wireshark 3.2.2 32-bit +Wireshark 3.2.4 64-bit +Wireshark 3.2.5 64-bit +Wireshark 3.2.7 64-bit +WizFile v2.06 +WizKey v1.5.0.8 +WizMouse v1.7.0.3 +WizTree v3.33 +WizTree v3.35 +WordPress.com 5.2.0 +WordPress.com 6.0.0 +WordPress.com 6.0.1 +WordPress.com 6.0.2 +Workrave 1.10.44 +Writage +X2Go Client for Windows +XAMPP +XCA +XMake build utility +XMind 10.2.1 +XnView 2.49.4 +XnViewMP 0.97.1 +Yarn +Yinxiang Biji v. 6.20.16 +Yinxiang Biji v. 6.21.10 +Yinxiang Biji v. 6.21.3 +Yinxiang Biji v. 6.21.4 +Yinxiang Biji v. 6.21.9 +YouTube Music Desktop App 1.11.0 +YubiKey Manager +Zentimo PRO 2.3 +ZeroTier One +Zettlr +Zint +Zoom +Zoom Outlook Plugin +Zotero +Zulip +Zulu 3.5.1 +Zygor Client Uninstaller +balenaEtcher 1.5.100 +balenaEtcher 1.5.101 +balenaEtcher 1.5.102 +balenaEtcher 1.5.106 +balenaEtcher 1.5.107 +balenaEtcher 1.5.109 +balenaEtcher 1.5.88 +balenaEtcher 1.5.95 +beatdrop 2.6.2 +bottom +butterflow-ui +calibre 64bit +copytranslator 9.1.0 +darktable +dnGREP 2.9.270 (x64) +draw.io 13.0.3 +dupeGuru 4.0.4 +eM Client +ebbflow +f.lux +ffftp +ghostwriter version 1.7.1 +gnuplot 5.2 patchlevel 8 +grepWin x64 +guinget version 0.1.0.1 +guinget version 0.1.1 +guinget version 0.1.2 +hide.me VPN 3.4.0 +hide.me VPN 3.4.1 +i2pd +iSEEK AnswerWorks English Runtime +iTunes +kdenlive +kdiff3 +mRemoteNG +maxima-5.43.2 +mpv.net version 5.4.8.0 +ndm 1.2.0 (only current user) +nexusfont 2.6 (ver 2.6.2.1870) +ownCloud +pCon.planner PRO +pandoc-plot 0.7.1.0 +pgAdmin 4 version 4.22 +pgAdmin 4 version 4.23 +pgAdmin 4 version 4.26 +pgAdmin 4 version 4.27 +qBittorrent 4.2.5 +qBittorrent 4.3.0 +qBittorrent 4.3.0.1 +remoteit 2.5.32 +remoteit 2.6.2 +sbt 1.3.8 +scilab-6.1.0 (64-bit) +sqlectron 1.31.0 +stretchly 0.21.1 +ueli 8.7.0 +ueli 8.8.1 +ueli 8.9.0 +xmoto 0.6.0 +xmoto 0.6.1 +微软设备健康助手 +支付宝安全控件 5.3.0.3807 +百度网盘 +腾讯QQ diff --git a/src/AppInstallerCLITests/TestData/InputPublishers.txt b/src/AppInstallerCLITests/TestData/InputPublishers.txt new file mode 100644 index 0000000000..8086c5f7bc --- /dev/null +++ b/src/AppInstallerCLITests/TestData/InputPublishers.txt @@ -0,0 +1,1137 @@ +Wildfire Games +SweetScape Software +360安全中心 +360安全中心 +Open Media LLC +Open Media LLC +Open Media LLC +Open Media LLC +Open Media LLC +Open Media LLC +Igor Pavlov +Igor Pavlov +Igor Pavlov +Igor Pavlov, Tino Reichardt +Amazon Web Services Developer Relations +Amazon Web Services +AWS Serverless Applications +Axis Communications AB +Abacus Research AG +Microsoft +Microsoft Corporation +Adobe Systems Incorporated +Adobe Systems Incorporated +Adobe Systems Incorporated +AdoptOpenJDK +AdoptOpenJDK +AdoptOpenJDK +AdoptOpenJDK +AdoptOpenJDK +AdoptOpenJDK +AdoptOpenJDK +Famatech +Ondrej Salplachta +Famatech +Pawel Psztyc +Aegisub Team +Aegisub Team +a wandersick +Alacritty +Alchemy Development Group +Algoryx +Stefan Sundin +Amazon.com, Inc. +Amazon +Amazon +Amazon Web Services, Inc +Anaconda, Inc. +Angry IP Scanner +AntiMicro +AppGet +Appium Developers +Appium Developers +Arduino LLC +Armagetron Advanced Team +Sundaram Ramaswamy +Rabid Viper Productions +Audacity Team +CodeUX.design e.U. +Armin Osaj +Lexikos +Lexikos +The Sleuth Kit +GPL Public release. +OmicronLab +7room +Microsoft® Corporation +Microsoft Corporation +Microsoft +Microsoft +Microsoft +Marcin Szeniak +Contoso.com +Beda Kosata +Space Sciences Laboratory, U.C. Berkeley +BPBible Development Team +Google, Inc. +Debauchee Open Source Group +Debauchee Open Source Group +Paul Frazee +Elastic +Elastic +Marco Mastroddi Software +beeftext.org +The Betaflight open source project. +Scooter Software, Inc. +Scooter Software +Scooter Software +Bigly Software +BitPay +Bitwarden Inc. +BleachBit +Blender Foundation +BlueJeans Network, Inc. +Hamster Republic Productions +Apple Inc. +Andrew Sampson +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +brackets.io +Brave Software Inc +Brave Software Inc +TGRMN Software +Buttercup +Buttercup +C-Dogs SDL Team +SingularLabs +Piriform +Canneverbe Limited +Kitware +Corsair +CPUID, Inc. +CPUID, Inc. +CPUID, Inc. +CPUID, Inc. +CPUID, Inc. +Penguin Labs, LLC +SaeraSoft +TechSmith Corporation +TechSmith Corporation +Sindre Sorhus +Sindre Sorhus +Giel Cobben +Mathew Sachin +RedDucks +Alexandr Subbotin +MIT IS&T +secana +Webprofusion Pty Ltd +a wandersick +Chef Software, Inc. +ChemAxon +ChemAxon +ChemAxon +ChemAxon +The Chromium Authors +Circuit Diagram +Circuit Diagram +Cisco Webex LLC +Daniel Scalzi +alch +Fndroid +Fndroid +Fndroid +Fndroid +Beijing EEO Education Technology Co., Ltd. +Clementine +Martin Ridgers +Cloudflare, Inc. +The Code::Blocks Team +Eran Ifrah +Steven Cole +TerranovaTeam +Jay Prall +Toinane +ConEmu-Maximus5 +Concept2 Inc. +Microsoft Corporation +Contasimple S.L. +ALCPU +Couchbase Inc. +Couchbase Inc. +Cozy Cloud +The Cppcheck team +Habib Rehman +cryptomator.org +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +Crystal Dew World +cubicsdr.com +Acro Software Inc. +iterate GmbH +DB Browser for SQLite Team +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DBeaver Corp +DJI +DJI +DJI +DJI +DJI +DJI +DJI +DJI +TshwaneDJe +Boxstar +Boxstar +Deezer +Ashley Stone +Piriform +Dell, Inc. +Dell Inc. +Bloodshed Software +Thinking Man Software +Serraniel +Serraniel +Scott Brogden +Dixa +DjVuZone +DockStation +Docker Inc. +Dokany Project +Dokany Project +Dolphin Team +dengine.net +Digimezzo +Doxie & Co. LLC +Dropbox, Inc. +EasternGraphics +EasternGraphics +EagleGet +EagleGet +Luke Stratman +ES-Computing +EduMIPS64 Development Team +Elastic +Elgato Systems GmbH +Empoche.com +Empoche.com +MacPaw, Inc. +a wandersick +Sinew Software Systems Private Limited +The Eraser Project +The Eraser Project +The Eraser Project +The Eraser Project +Esteem +Ethereum +Evernote Corp. +Evernote Corp. +David Carpenter +David Carpenter +David Carpenter +David Carpenter +voidtools +ExpressVPN +Ultrapico +The ExtremeTuxRacer team +Eugene Roshal & Far Group +H.Shirouzu +FastStone Soft +FastStone Soft +Fedora Project +Amine Mouafik +Progress Software Corporation +Progress Software Corporation +Progress Software Corporation +Progress Software Corporation +Progress Software Corporation +Progress Software Corporation +Progress Software Corporation +Adrien Allard +Binary Fortress Software +Tim Kosse +Tim Kosse +Tim Kosse +Tim Kosse +Tim Kosse +Mozilla +Mozilla +Mozilla +Mozilla +OpenSight Software LLC +The FlightGear Team +Dominik Levitsky Studio, LLC +FontForgeBuilds +Free Time +Free Time +Foxit Software Inc. +Foxit Software Inc. +Stefan Malzner +Marek Jasinski - www.FreeCommander.com +Humanity +The GIMP Team +The GIMP Team +The GIMP Team +The GIMP Team +The GIMP Team +The GIMP Team +The GIMP Team +The GIMP Team +ARM Holdings +The Free Software Foundation, Inc. +The GnuPG Project +GCN Development +GOG.com +Artifex Software Inc. +Ghostgum Software Pty Ltd +Garmin Ltd or its subsidiaries +ThoughtWorks Inc. +ThoughtWorks Inc. +The Geany developer team +Primate Labs Inc. +Gephi +Outertech +Git Extensions Team +Git Extensions Team +Git Extensions Team +GitHub, Inc. +The Git Development Community +The Git Development Community +The Git Development Community +The Git Development Community +The Git Development Community +The Git Development Community +GitHub, Inc. +GitHub, Inc. +Stef Heyenrath +Troupe Technology Limited +Glimpse Project +Glimpse Project +Glimpse Project +GnuCash Development Team +GnuCash Development Team +GnuWin32 +GnuWin32 +GnuWin32 +GnuWin32 +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +https://golang.org +GoldWave Inc. +Google LLC +Google Inc. +Google +The Gpg4win Project +The Gpg4win Project +Grafana Labs +Grafana Labs +Grammarly +The Gramps project +Graphcool +Adam Miskiewicz +AT&T Research Labs. +Greenshot +Grid Team +Epiforge Software, LLC +HHD Software, Ltd. +Hector Maurcio Rodriguez Segura +HP Inc. +Huawei Software Technologies Co., Ltd. +Martin Malik - REALiX +Martin Malik - REALiX +Martin Malik - REALiX +Martin Malik - REALiX +HandyOrg +Vincent L +Implbits Software +Implbits Software +JAM Software +Hedgewars Project +Ansgar Becker +Ansgar Becker +Ansgar Becker +Ansgar Becker +Ansgar Becker +Perforce Software, Inc. +IBE Software +HexChat +Scott Lerch +Caphyon +Borvid +Borvid +Borvid +Borvid +Borvid +Borvid +Borvid +Huawei Corporation +Hyne & Son Pty Ltd +Marquis Kurt +Google Inc +Hex-Rays SA +Tibbo Technology Inc +David Moore +IRCCloud Ltd. +Ivan Zahariev +Duong Dieu Phap +Inkscape +jrsoftware.org +jrsoftware.org +Crystal Rich, Ltd +seonglae +Irfan Skiljan +IronPython Team +IronPython Team +ISWIX LLC +Smart Projects +ChemAxon +JabRef +Jackett +Savoir-Faire Linux +Oracle Corporation +Oracle Corporation +JetBrains +Jitsi Team +Jitsi Team +Laurent Cozic +Laurent Cozic +Laurent Cozic +Julia Language +Julia Language +Julia Language +KLCP +KLCP +KKBOX Taiwan Co., Ltd. +Chia-Lung, Chen +Dominik Reichl +Dominik Reichl +Dominik Reichl +KeePassXC Team +KeeWeb +Keybase, Inc. +KiCad +KiCad +KiCad +Krisp Technologies, Inc +Krita Foundation +Roni Lehto +LBRY Inc. +LBRY Inc. +LBRY Inc. +LBRY Inc. +LBRY Inc. +LINE Corporation +Joseph Albahari +LLVM +LMMS Developers +LMMS Developers +love2d.org +leokhoa +LastPass +Lazarus Team +Riot Games, Inc +Lenovo +Lenovo +Lakend Labs, Inc. +Leonflix +BellSoft +BellSoft +BellSoft +BellSoft +BellSoft +BellSoft +BellSoft +BellSoft +LibreCAD Team +The Document Foundation +The Document Foundation +Team Lidarr +Alexey 'Tyrrrz' Golub +Alexey 'Tyrrrz' Golub +Christian Kaiser +Leif Asbrink SM5BSZ +Lisk Foundation +Listen 1 +Listen 1 +Flywheel +Crystal Rich Ltd +Binary Fortress Software +Binary Fortress Software +Logitech Inc. +Loom, Inc. +LyX Team +LyX Team +COTILab +COTILab +Moritz Bunkus +Moritz Bunkus +Moritz Bunkus +Moritz Bunkus +Moritz Bunkus +MPC-HC Team +MPC-HC Team +MPC-HC Team +Thomas Nordquist +Microsoft +Maxthon International Limited +MY.COM B.V. +FlyingSnow, Samantha Glocker +Firetrust +Firetrust +Majsoul Plus Team +GuinpinSoft inc +Malwarebytes +KDE +MariaDB Corporation Ab +Jocs +West Wind Technologies +West Wind Technologies +West Wind Technologies +West Wind Technologies +West Wind Technologies +Markdown Outlook +Master Packager Ltd. +Mattermost, Inc. +MediaArea.net +MediaArea.net +Ventis Media Inc. +The Meld project +Janea Systems +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Garage +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft +Microsoft +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Anaconda, Inc. +Anaconda, Inc. +MongoDB Inc. +Xamarin, Inc. +The MonoGame Team +Moonlight Game Streaming Project +AGALWOOD +AGALWOOD +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Mozilla +Nicholas H.Tollervey +Mullvad VPN +Mullvad VPN +Microsoft Corporation +canonical +The Mumble Developers +The Mumble Developers +The Mumble Developers +Werner Schweer and Others +Youta Tec +Logitech +Martin Renold and the MyPaint Development Team +Oracle Corporation +Feodor2 +Justin Aquadro +Rico Suter +NV Access +NVIDIA Corporation +NVIDIA Corporation +NZXT, Inc. +NZXT, Inc. +NZXT, Inc. +NZXT, Inc. +NZXT, Inc. +NZXT, Inc. +Allan CORNET +Neotys +Lutz Roeder +Nitro +Nmap Project +qinghai +qinghai +qinghai +Node.js Foundation +Nodist +NordVPN +TEFINCOM S.A. +NordVPN +NordVPN +CodingRoad +XhmikosR +Notion Labs, Incorporated +Notion Labs, Incorporated +Nmap Project +NullNoname +Nullsoft and Contributors +OBS Project +Hamster Republic Productions +Ascensio System SIA +GNU Octave +WetHat Lab +Open Shop Channel +The Open-Shell Team +namazso +namazso +ojdkbuild open-source project +ojdkbuild open-source project +ojdkbuild open-source project +ojdkbuild open-source project +OpenMPT Devs +Apache Software Foundation +OpenRA developers +The OpenSCAD Developers +Shining Light Productions +OpenShot Studios, LLC +OpenTTD +OpenTTD +OpenVPN Technologies, Inc. +SparkLabs Pty Ltd +OpenVPN Technologies +Opera Software +Opera Software +Opera Software +Opera Software +Opera Software +Opera Software +Opera Software +Opera Software +Oracle Corporation +Oracle Corporation +Oracle Corporation +Oracle Corporation +OutSystems +EXP Systems LLC +Sober Lemur S.a.s. di Vacondio Andrea +CCPKU +Jan Fiala +NagleCode, LLC +Moonchild Productions +Moonchild Productions +John MacFarlane +John MacFarlane +John MacFarlane +Paradox Interactive +Paragon Software GmbH +Parsec Cloud Inc. +Francesco Sorge +Giorgio Tani +Giorgio Tani +Persepolis Team +Heiko Sommerfeldt +touchbyte GmbH +NGWIN +PicoTorrent contributors. +blupi.org +Sony Interactive Entertainment Network America LLC +Josef Nemec +Plex, Inc. +Plex +Plex, Inc. +Plex, Inc. +Plex, Inc. +Plex, Inc. +www.pokerth.net +Postbox, Inc. +PostgreSQL Global Development Group +PostgreSQL Global Development Group +Microsoft Corporation +Microsoft Corporation +Ironman Software, LLC +Microsoft +Kim Walisch +Private Internet Access, Inc. +Progress Software Corporation +Microsoft Corporation +Proton Technologies AG +Proton Technologies AG +Simon Tatham +Puppet Labs +Puppet Inc +Puppet, Inc. +Puppet Inc +Miller Puckette +Lancaster University Physics +Python Software Foundation +Hubert Pham +Python Software Foundation +Python Software Foundation +Python Software Foundation +Python Software Foundation +Python Software Foundation +Python Software Foundation +Python Software Foundation +Python Software Foundation +QGIS Development Team +QGIS Development Team +QGIS Development Team +Michael Hansen +Hanna Knutsson +LarusStone +Module Art +Paddy Xu +Quicken +R Core Team +R Core Team +R Core Team +RStudio +Rambox +Oleg Danilov +Raspberry Pi +rawtherapee.com +Paul Rawnsley +jklSoft +Devolutions inc. +Devolutions inc. +Remote Mouse +Baldur Karlsson +Antmicro +Responsively +RetroShare Team +VS Revo Group, Ltd. +VS Revo Group, Ltd. +3T Software Labs Ltd +3T Software Labs Ltd +3T Software Labs Ltd +Rocket.Chat Support +Punk Software +Artsoft Entertainment +MarkoBL +code4ward GmbH +Royal Apps GmbH +The R Foundation +RubyInstaller Team +RubyInstaller Team +RubyInstaller Team +Luke Haas +Luke Haas +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +The Rust Project Developers +Topala Software Solutions +Ricardo Villalba +Navimatics LLC +Dotz Softwares +Samsung Electronics Co., Ltd. +mircearoata +Scratch Foundation +Nicke Manarin +The Scribus Team +The ScummVM Team +spikespaz +Sejda BV +Datalust Pty Ltd +Microsoft Corporation +ShareX Team +RandyRants.com +Meltytech, LLC +Sigil-Ebook +Open Whisper Systems +Open Whisper Systems +Open Whisper Systems +Open Whisper Systems +Open Whisper Systems +Open Whisper Systems +Open Whisper Systems +Approximatrix, LLC +Ashley Stone +Skype Technologies S.A. +Skype Technologies S.A. +Skype Technologies S.A. +Slack Technologies +TechSmith Corporation +SnakeNest.com +Cory Plotts +SmartBear Software +Sonic Pi +Sonos, Inc. +Sonos, Inc. +Antoine Aflalo +Atlassian +SpeedCrunch +Spek Project +Standard Notes +Valve Corporation +http://www.sbcl.org +SteelSeries ApS +Stellarium team +Stellarium team +Stellarium team +strawberryperl.com project +General Workings, Inc. +General Workings, Inc. +General Workings, Inc. +Streamlink +Sebastian Meyer +Jan Hovancik +Jan Hovancik +Jan Hovancik +Stride +BrickLink Corporation +Sublime HQ Pty Ltd +Sublime HQ Pty Ltd +Krzysztof Kowalczyk +SuperCollider Community +SuperTuxKart +Microsoft +SyncTrayzor +Mister Group +OpenVPN Technologies, Inc. +erengy +Tailscale Inc. +Tailscale Inc. +Taisei Project +Taskcade Inc. +Taskcade Inc. +"Treasure Data, Inc" +Treasure Data, Inc +Benito van der Zander +TeX Users Group +TeamSpeak Systems GmbH +TechPowerUp +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +Telegram FZ-LLC +TeraTerm Project +Eugene Pankov +Eugene Pankov +Eugene Pankov +CompuPhase +Tesseract-OCR community +Texmaker +Texnomic +RaMMicHaeL +Appest.com +GlavSoft LLC. +TikzEdt +mapeditor.org +Nadeo +Toggl +Toggl +TortoiseGit +TortoiseSVN +TortoiseSVN +TranslucentTB Open Source Developers +Transmission Project +Yury Sidorov & Transmission Remote GUI working group +Binary Fortress Software +Binary Fortress Software +JAM Software +JAM Software +Trelby.org +Cerulean Studios, LLC +TunnelBear +New Breed Software +Inspect Element Inc. +Xander Frangos +Twitch Interactive, Inc. +G DATA CyberDefense AG +SafelyRemove.com +Drew Naylor +Drew Naylor +Ultimaker B.V. +Ultimaker B.V. +Ultimaker B.V. +uvnc bvba +Reason Software Company Inc. +Unified Intents AB +Unity Technologies Inc. +Microsoft Corporation +Microsoft Corporation +Ubisoft +UMEZAWA Takeshi +VCV +VideoLAN +VideoLAN +VideoLAN +VMware, Inc. +VMware, Inc. +VMware, Inc. +RealVNC Ltd +RealVNC Ltd +Microsoft Corporation +Microsoft Corporation +marha@users.sourceforge.net +Bram Moolenaar et al. +Virt Manager Project +Vivaldi Technologies AS. +Black Tree Gaming Ltd. +VoyagerX, Inc. +LunarG, Inc. +Warzone 2100 Project +Waterfox +Waterfox +Waterfox +Devolutions Inc. +Buds +Buds +Buds +Buds +Machine Learning Group, University of Waikato, Hamilton, NZ +ImageWriter Developers +Sam Hocevar +Timothy Johnson +Timothy Johnson +Timothy Johnson +Navimatics LLC +HTTrack +Thingamahoochie Software +win.rar GmbH +win.rar GmbH +Martin Prikryl +Corel Corporation +Winamp SA +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Dynastream Innovations, Inc. +Silicon Labs Software +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +Microsoft Corporation +WireGuard LLC +The Wireshark developer community, https://www.wireshark.org +The Wireshark developer community, https://www.wireshark.org +The Wireshark developer community, https://www.wireshark.org +The Wireshark developer community, https://www.wireshark.org +Antibody Software +Antibody Software +Antibody Software +Antibody Software +Antibody Software +Automattic Inc. +Automattic Inc. +Automattic Inc. +Automattic Inc. +Rob Caelers & Raymond Penners +Writage +X2Go Project +Bitnami +Christian Hohnstaedt +The TBOOX Open Source Group +XMind Ltd. +Gougelet Pierre-e +Gougelet Pierre-e +Yarn Contributors +Beijing Yinxiang Biji Technologies Co., Ltd. +Beijing Yinxiang Biji Technologies Co., Ltd. +Beijing Yinxiang Biji Technologies Co., Ltd. +Beijing Yinxiang Biji Technologies Co., Ltd. +Beijing Yinxiang Biji Technologies Co., Ltd. +Adler Luiz +Yubico AB +Zentimo.com +ZeroTier, Inc. +Hendrik Erz +Robin Stuart & BogDan Vatra +Zoom +Zoom +Corporation for Digital Scholarship +Kandra Labs, Inc. +Sangoma Technologies Corp. +Zygor Guides +Balena Inc. +Balena Inc. +Balena Inc. +Balena Inc. +Balena Inc. +Balena Inc. +Balena Inc. +Balena Inc. +Nathaniel Johns +Clement Tsang +butterflow-ui @ github +Kovid Goyal +Elliott Zheng +the darktable project +dnGrep Community Contributors +JGraph +Hardcoded Software +eM Client Inc. +Ebbflow.io +f.lux Software LLC +Kurata Sayuri +wereturtle +gnuplot development team +Stefans Tools +Drew Naylor +Drew Naylor +Drew Naylor +eVenture Limited +eVenture Limited +PurpleI2P +Vantage Linguistics +Apple Inc. +KDE e.V. +KDE e.V. +Next Generation Software +Maxima Team +Frank Skare (stax76) +720kb +xiles +ownCloud GmbH +EasternGraphics +Laurent P. Ren© de Cotret +The pgAdmin Development Team +The pgAdmin Development Team +The pgAdmin Development Team +The pgAdmin Development Team +The qBittorrent project +The qBittorrent project +The qBittorrent project +remote.it +remote.it +Lightbend, Inc. +Scilab Enterprises +The Sqlectron Team +Jan Hovancik +Oliver Schwendener +Oliver Schwendener +Oliver Schwendener +Humanity +Humanity +Microsoft Corporation +Alipay.com Co., Ltd. +百度在线网络技术(北京)有限公司 +腾讯科技(深圳)有限公司 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml index 49385cc4d3..7b1232e04d 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_Exe.yaml @@ -1,6 +1,6 @@ Id: AppInstallerCliTest.TestExeInstaller Version: 1.0.0.0 -Name: AppInstaller Test Installer +Name: AppInstaller Test Exe Installer Publisher: Microsoft Corporation AppMoniker: AICLITestExe License: Test diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml index 090a8ed85c..abf1ddc81d 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml @@ -9,4 +9,5 @@ Installers: Url: https://ThisIsNotUsed InstallerType: MSStore ProductId: 9WZDNCRFJ364 + PackageFamilyName: Microsoft.SkypeApp_kzf8qxf38zg5c ManifestVersion: 0.2.0-msstore diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml index a2bf26d00c..23a9996ebf 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml @@ -9,4 +9,5 @@ Installers: Url: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml index 36f158c36c..b9ada47afd 100644 --- a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml @@ -10,4 +10,5 @@ Installers: InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml new file mode 100644 index 0000000000..f1b3f02b69 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml @@ -0,0 +1,20 @@ +PackageIdentifier: AppInstallerCliTest.TestInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +InstallerSwitches: + Custom: /ExitCode 0x80070005 + SilentWithProgress: /silentwithprogress + Silent: /silence +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSuccessCodes: + - -2147024891 +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml index ccf36be366..ca0bc77c93 100644 --- a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml +++ b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml @@ -9,4 +9,5 @@ Installers: Url: https://ThisIsNotUsed InstallerType: msi Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml index e311400003..a7e4630b39 100644 --- a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml +++ b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml @@ -9,6 +9,7 @@ Installers: Url: https://ThisIsNotUsed InstallerType: msi Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' Switches: Custom: /mycustom SilentWithProgress: /mysilentwithprogress diff --git a/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml b/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml index da6232b0ae..3cdf47cda6 100644 --- a/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml +++ b/src/AppInstallerCLITests/TestData/Manifest-Bad-Channel-NotSupported.yaml @@ -1,4 +1,3 @@ -# Minimum required Id: microsoft.msixsdk Name: MSIX SDK Version: 1.7.32 diff --git a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml index fb921858d9..fdb0d28c02 100644 --- a/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml +++ b/src/AppInstallerCLITests/TestData/Manifest-Good-InstallerUniqueness-DiffScope.yaml @@ -13,5 +13,5 @@ Installers: - Arch: x86 Url: https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdk-x86.zip Sha256: 98B67758CEAFFCBB3FE47838FD0A8D7BD581C2650842D6B2B0E0D49A23270CCD - Scope: system + Scope: machine ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/ManifestV1-Singleton.yaml b/src/AppInstallerCLITests/TestData/ManifestV1-Singleton.yaml new file mode 100644 index 0000000000..b082d532b6 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/ManifestV1-Singleton.yaml @@ -0,0 +1,121 @@ +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" +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 +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}" + +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 + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-DefaultLocale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-DefaultLocale.yaml new file mode 100644 index 0000000000..dfd1c3e69a --- /dev/null +++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-DefaultLocale.yaml @@ -0,0 +1,23 @@ +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" + +ManifestType: defaultLocale +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Installer.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Installer.yaml new file mode 100644 index 0000000000..5e6ca8a097 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Installer.yaml @@ -0,0 +1,108 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +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 +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}" + +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 + UpgradeBehavior: install + Commands: + - makemsixPreview + - makeappxPreview + Protocols: + - protocol1preview + - protocol2preview + FileExtensions: + - appxbundle + - msixbundle + - appx + - msix + Dependencies: + WindowsFeatures: + - PreviewIIS + WindowsLibraries: + - Preview VC Runtime + PackageDependencies: + - PackageIdentifier: Microsoft.MsixSdkDepPreview + ExternalDependencies: + - Preview Outside dependencies + PackageFamilyName: Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe + Capabilities: + - internetClientPreview + RestrictedCapabilities: + - runFullTrustPreview + - Architecture: x64 + InstallerType: exe + InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe + InstallerSha256: 69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82 + ProductCode: "{Bar}" + +ManifestType: installer +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Locale.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Locale.yaml new file mode 100644 index 0000000000..9fe96f8a44 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Locale.yaml @@ -0,0 +1,22 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +PackageLocale: en-GB +Publisher: Microsoft UK +PublisherUrl: https://www.microsoft.com/UK +PublisherSupportUrl: https://www.microsoft.com/support/UK +PrivacyUrl: https://www.microsoft.com/privacy/UK +Author: Microsoft UK +PackageName: MSIX SDK UK +PackageUrl: https://www.microsoft.com/msixsdk/home/UK +License: MIT License UK +LicenseUrl: https://www.microsoft.com/msixsdk/license/UK +Copyright: Copyright Microsoft Corporation UK +CopyrightUrl: https://www.microsoft.com/msixsdk/copyright/UK +ShortDescription: This is MSIX SDK UK +Description: The MSIX SDK project is an effort to enable developers UK +Tags: + - "appxsdkUK" + - "msixsdkUK" + +ManifestType: locale +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Version.yaml b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Version.yaml new file mode 100644 index 0000000000..34bd203939 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/MultiFileManifestV1/ManifestV1-MultiFile-Version.yaml @@ -0,0 +1,6 @@ +PackageIdentifier: microsoft.msixsdk +PackageVersion: 1.7.32 +DefaultLocale: en-US + +ManifestType: version +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/TestData/NormalizationInitialIds.txt b/src/AppInstallerCLITests/TestData/NormalizationInitialIds.txt new file mode 100644 index 0000000000..97b681ff41 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/NormalizationInitialIds.txt @@ -0,0 +1,1137 @@ +WildfireGames.0AD +SweetScapeSoftware.010Editor +360安全中心.360安全卫士 +360安全中心.360杀毒 +OpenMedia.4KSlideshowMaker +OpenMedia.4KStogram +OpenMedia.4KVideoDownloader +OpenMedia.4KVideoDownloader +OpenMedia.4KVideotoMP3 +OpenMedia.4KYouTubetoMP3 +IgorPavlov.7Zip +IgorPavlov.7Zip +IgorPavlov.7Zipalpha +IgorPavlovTinoReichardt.7ZipZSZS +AmazonWebServicesDeveloperRelations.AWSCommandLineInterface +AmazonWebServices.AWSCommandLineInterface +AWSServerlessApplications.AWSSAMCommandLineInterface +AxisCommunications.AXISCameraStation +AbacusResearch.AbaClient +Microsoft.AccessibilityInsightsForWindows +Microsoft.ActiveDirectoryAuthenticationLibraryforSQLServer +AdobeSystems.AdobeAcrobatReaderDCCzech +AdobeSystems.AdobeAcrobatReaderDC +AdobeSystems.AdobeAcrobatReaderDCMUI +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +AdoptOpenJDK.AdoptOpenJDKJDKwithHotspot +Famatech.AdvancedIPScanner +OndrejSalplachta.AdvancedLogViewer +Famatech.AdvancedPortScanner +PawelPsztyc.AdvancedRestClient +AegisubTeam.Aegisub8975master8d77da3 +AegisubTeam.Aegisub +awandersick.AeroZoombeta2 +Alacritty.Alacritty +AlchemyDevelopmentGroup.AlchemyBeta +Algoryx.Algodoo +StefanSundin.AltDrag +Amazoncom.AmazonChime +Amazon.AmazonCorretto +Amazon.AmazonCorretto8 +AmazonWebServices.AmazonWorkSpaces +Anaconda.Anaconda3 +AngryIPScanner.AngryIPScanner +AntiMicro.AntiMicro +AppGet.AppGet +AppiumDevelopers.Appium +AppiumDevelopers.Appium +Arduino.Arduino +ArmagetronAdvancedTeam.ArmagetronAdvanced +SundaramRamaswamy.Artha +RabidViperProductions.AssaultCube +AudacityTeam.Audacity +CodeUXdesigneU.AuthPass +ArminOsaj.AutoDarkMode +Lexikos.AutoHotkey +Lexikos.AutoHotkey +TheSleuthKit.Autopsy +GPLPublicrelease.AviSynth +OmicronLab.AvroKeyboard +7room.Aya +Microsoft.AzureCosmosDBEmulator +Microsoft.AzureDataStudio +Microsoft.AzureFunctionsCoreTools +Microsoft.AzureIoTExplorer +Microsoft.AzureIoTexplorer +MarcinSzeniak.BCUninstaller +Contosocom.BITSManager +BedaKosata.BKChem +SpaceSciencesLaboratoryUCBerkeley.BOINC +BPBibleDevelopmentTeam.BPBible +Google.BackupandSyncfromGoogle +DebaucheeOpenSourceGroup.Barrier +DebaucheeOpenSourceGroup.Barrier +PaulFrazee.BeakerBrowser +Elastic.Beatswinlogbeat +Elastic.Beatswinlogbeat +MarcoMastroddiSoftware.BeeBEEP +beeftextorg.Beeftext +TheBetaflightopensourceproject.BetaflightConfigurator +ScooterSoftware.BeyondCompare4 +ScooterSoftware.BeyondCompare +ScooterSoftware.BeyondCompare +BiglySoftware.BiglyBT +BitPay.BitPay +Bitwarden.Bitwarden +BleachBit.BleachBit +BlenderFoundation.Blender +BlueJeansNetwork.BlueJeans +HamsterRepublicProductions.BobtheHamsterVGA +Apple.Bonjour +AndrewSampson.BorderlessGaming +Microsoft.BotFrameworkComposer +Microsoft.BotFrameworkComposer +Microsoft.BotFrameworkComposer +Microsoft.BotFrameworkComposer +Microsoft.BotFrameworkEmulator +Microsoft.BotFrameworkEmulator +Microsoft.BotFrameworkEmulator +bracketsio.Brackets +BraveSoftware.Brave +BraveSoftware.BraveNightly +TGRMNSoftware.BulkRenameUtility +Buttercup.Buttercup +Buttercup.Buttercup +CDogsSDLTeam.CDogsSDL +SingularLabs.CCEnhancer +Piriform.CCleaner +Canneverbe.CDBurnerXP +Kitware.CMake +Corsair.CORSAIRiCUESoftware +CPUID.CPUIDCPUZ +CPUID.CPUIDCPUZ +CPUID.CPUIDCPUZ +CPUID.CPUIDHWMonitor +CPUID.CPUIDHWMonitor +PenguinLabs.Cacher +SaeraSoft.CaesiumPH +TechSmith.Camtasia2019 +TechSmith.Camtasia2020 +SindreSorhus.Caprine +SindreSorhus.Caprine +GielCobben.Caption +MathewSachin.Captura +RedDucks.CemUI +AlexandrSubbotin.Cerebro +MITIST.CertAidforWindows +secana.CertDump +Webprofusion.CertifyTheWeb +awandersick.ChMac +ChefSoftware.ChefDK +ChemAxon.ChemAxonChemCurator +ChemAxon.ChemAxonMarkushEditor +ChemAxon.ChemAxonMarvinSuite +ChemAxon.ChemAxonMarvinSuite +TheChromiumAuthors.Chromium +CircuitDiagram.CircuitDiagram +CircuitDiagram.CircuitDiagram +CiscoWebex.CiscoWebexMeetings +DanielScalzi.CitycraftLauncher +alch.ClamWinFreeAntivirus +Fndroid.ClashforWindows +Fndroid.ClashforWindows +Fndroid.ClashforWindows +Fndroid.ClashforWindows +BeijingEEOEducationTechnology.ClassIn +Clementine.Clementine +MartinRidgers.Clink +Cloudflare.CloudflareWARP +TheCodeBlocksTeam.CodeBlocks +EranIfrah.CodeLite +StevenCole.Coffee +TerranovaTeam.ColobotGoldEditionalpha +JayPrall.ColorCop +Toinane.Colorpicker +ConEmuMaximus.ConEmu201011 +Concept2.Concept2Utility +Microsoft.ConfigMgr2012Toolkit +Contasimple.ContasimpleDesktop +ALCPU.CoreTemp +Couchbase.CouchbaseServerCommunityEdition +Couchbase.CouchbaseServerEnterpriseEdition +CozyCloud.CozyDrive +TheCppcheckteam.Cppcheck +HabibRehman.Crypter +cryptomatororg.Cryptomator +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfoKureiKeiEdition +CrystalDewWorld.CrystalDiskInfoShizukuEdition +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfoKureiKeiEdition +CrystalDewWorld.CrystalDiskInfoShizukuEdition +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfoKureiKeiEdition +CrystalDewWorld.CrystalDiskInfoShizukuEdition +CrystalDewWorld.CrystalDiskInfo +CrystalDewWorld.CrystalDiskInfoKureiKeiEdition +CrystalDewWorld.CrystalDiskInfoShizukuEdition +CrystalDewWorld.CrystalDiskMark +CrystalDewWorld.CrystalDiskMarkShizukuEdition +cubicsdrcom.CubicSDRInstaller +AcroSoftware.CutePDFWriter +iterate.Cyberduck +DBBrowserforSQLiteTeam.DBBrowserforSQLite +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DBeaver.DBeaver +DJI.DJIAssistant2 +DJI.DJIAssistant2ForAeroscope +DJI.DJIAssistant2ForAutopilot +DJI.DJIAssistant2ForBatteryStation +DJI.DJIAssistant2ForMG +DJI.DJIAssistant2ForMatrice +DJI.DJIAssistant2ForMavic +DJI.DJIAssistant2ForPhantom +TshwaneDJe.DaveGnukem +Boxstar.DeepVocalToolBoxbetaversionbeta +Boxstar.DeepVocalbetaversionbeta +Deezer.Deezer +AshleyStone.DefaultAudio +Piriform.Defraggler +Dell.DellCommandUpdate +Dell.DellUpdate +BloodshedSoftware.DevC +ThinkingManSoftware.Dimension4 +Serraniel.DiscordMediaLoader +Serraniel.DiscordMediaLoader +ScottBrogden.Ditto +Dixa.Dixa +DjVuZone.DjVuLibreDjView +DockStation.DockStation +Docker.DockerDesktop +DokanyProject.DokanLibrary +DokanyProject.DokanLibrary +DolphinTeam.Dolphin +denginenet.Doomsday +Digimezzo.Dopamine +Doxie.Doxie +Dropbox.Dropbox +EasternGraphics.EGRSafenetActivation +EasternGraphics.EGRShellExtension +EagleGet.EagleGet +EagleGet.EagleGet +LukeStratman.EasyConnect +ESComputing.EditPlus +EduMIPS64DevelopmentTeam.EduMIPS64 +Elastic.Elasticsearch +ElgatoSystems.ElgatoStreamDeck +Empochecom.Empoche +Empochecom.Empoche +MacPaw.Encrypto +awandersick.EnglishizeCmd +SinewSoftwareSystemsPrivate.Enpass +TheEraserProject.Eraser +TheEraserProject.Eraser +TheEraserProject.Eraser +TheEraserProject.Eraser +Esteem.Esteem +Ethereum.EthereumGethOfficialGoimplementationoftheEthereumprotocol +Evernote.Evernotev +Evernote.Evernotev +DavidCarpenter.Everything +DavidCarpenter.Everything +DavidCarpenter.Everything +DavidCarpenter.EverythingLite +voidtools.Everything +ExpressVPN.ExpressVPN +Ultrapico.Expresso +TheExtremeTuxRacerteam.ExtremeTuxRacer +EugeneRoshalFarGroup.FarManager3 +HShirouzu.FastCopy +FastStoneSoft.FastStoneCapture +FastStoneSoft.FastStoneImageViewer +FedoraProject.FedoraMediaWriter +AmineMouafik.Ferdi +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +ProgressSoftware.FiddlerEverywhere +AdrienAllard.FileConverter +BinaryFortressSoftware.FileSeek +TimKosse.FileZillaClient +TimKosse.FileZillaClient +TimKosse.FileZillaClient +TimKosse.FileZillaClient +TimKosse.FileZillaClient +Mozilla.FirefoxDeveloperEdition +Mozilla.FirefoxDeveloperEdition +Mozilla.FirefoxDeveloperEdition +Mozilla.FirefoxDeveloperEdition +OpenSightSoftware.FlashFXP5 +TheFlightGearTeam.FlightGear +DominikLevitskyStudio.FontBase +FontForgeBuilds.FontForge +FreeTime.FormatFactory +FreeTime.FormatFactory +FoxitSoftware.FoxitPhantomPDF +FoxitSoftware.FoxitReader +StefanMalzner.Franz +MarekJasinskiwwwFreeCommandercom.FreeCommanderXE +Humanity.FreeMat +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +TheGIMPTeam.GIMP +ARM.GNUArmEmbeddedToolchain92020 +TheFreeSoftwareFoundation.GNUMidnightCommander +TheGnuPGProject.GNUPrivacyGuard +GCNDevelopment.GNURadio +GOGcom.GOGGALAXY +ArtifexSoftware.GPLGhostscript +GhostgumSoftware.GSview +Garmin.GarminExpress +ThoughtWorks.Gauge +ThoughtWorks.Gauge +TheGeanydeveloperteam.Geany +PrimateLabs.Geekbench5 +Gephi.Gephi +Outertech.GetDiz +GitExtensionsTeam.GitExtensions +GitExtensionsTeam.GitExtensions +GitExtensionsTeam.GitExtensions +GitHub.GitLFS +TheGitDevelopmentCommunity.Git +TheGitDevelopmentCommunity.Git +TheGitDevelopmentCommunity.Git +TheGitDevelopmentCommunity.Git +TheGitDevelopmentCommunity.Git +TheGitDevelopmentCommunity.Git +GitHub.GitHubCLI +GitHub.GitHubDesktopMachineWideInstaller +StefHeyenrath.GitHubReleaseNotes +TroupeTechnology.Gitter +GlimpseProject.Glimpse +GlimpseProject.Glimpse +GlimpseProject.Glimpse +GnuCashDevelopmentTeam.GnuCash +GnuCashDevelopmentTeam.GnuCash +GnuWin.GnuWin32Grep +GnuWin.GnuWin32Make +GnuWin.GnuWin32Wget +GnuWin.GnuWin32Zip +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +golangorg.GoProgrammingLanguagego +GoldWave.GoldWave +Google.GoogleChrome +Google.GoogleCloudSDK +Google.GoogleEarthPro +TheGpg4winProject.Gpg4win +TheGpg4winProject.Gpg4win +GrafanaLabs.GrafanaEnterprise +GrafanaLabs.GrafanaOSS +Grammarly.GrammarlyforMicrosoftOfficeSuite +TheGrampsproject.GrampsAIO64 +Graphcool.GraphQLPlayground +AdamMiskiewicz.GraphiQL +ATTResearchLabs.Graphviz +Greenshot.Greenshot +GridTeam.Grid +EpiforgeSoftware.Grindstone4 +HHDSoftware.HHDSoftwareFreeHexEditorNeo +HectorMaurcioRodriguezSegura.HMNISEdit +HP.HPCloudRecoveryTool +HuaweiSoftwareTechnologies.HUAWEICloud +MartinMalikREALiX.HWiNFO64 +MartinMalikREALiX.HWiNFO64 +MartinMalikREALiX.HWiNFO64 +MartinMalikREALiX.HWiNFO64 +HandyOrg.HandyWinGet +VincentL.Harmony +ImplbitsSoftware.HashTab +ImplbitsSoftware.HashTab +JAMSoftware.HeavyLoad +HedgewarsProject.Hedgewars +AnsgarBecker.HeidiSQL +AnsgarBecker.HeidiSQL +AnsgarBecker.HeidiSQL +AnsgarBecker.HeidiSQL +AnsgarBecker.HeidiSQL +PerforceSoftware.HelixCoreApps +IBESoftware.HelpNDocPersonalEdition +HexChat.HexChat +ScottLerch.HostsFileEditor +Caphyon.Hover +Borvid.HttpMasterExpressEdition +Borvid.HttpMasterExpressEdition +Borvid.HttpMasterExpressEdition +Borvid.HttpMasterExpressEdition +Borvid.HttpMasterProfessionalEdition +Borvid.HttpMasterProfessionalEdition +Borvid.HttpMasterProfessionalEdition +Huawei.HuaweiQuickAppIDE +HyneSon.HyneTimberDesign +MarquisKurt.HyperspaceDesktop +Google.IAPDesktop +HexRays.IDAFreeware +TibboTechnology.IONinja3 +DavidMoore.IPFilter +IRCCloud.IRCCloud +IvanZahariev.IZArc +DuongDieuPhap.ImageGlass +Inkscape.Inkscape +jrsoftwareorg.InnoSetup +jrsoftwareorg.InnoSetup +CrystalRich.InternetOff +seonglae.Intuiter +IrfanSkiljan.IrfanView +IronPythonTeam.IronPython +IronPythonTeam.IronPython +ISWIX.IsWiX +SmartProjects.IsoBuster +ChemAxon.JChemNETAPI +JabRef.JabRef +Jackett.Jackett +SavoirFaireLinux.Jami +Oracle.Java8Update251 +Oracle.Java8Update261 +JetBrains.JetBrainsToolbox +JitsiTeam.JitsiMeet +JitsiTeam.JitsiMeet +LaurentCozic.Joplin +LaurentCozic.Joplin +LaurentCozic.Joplin +JuliaLanguage.Julia +JuliaLanguage.Julia +JuliaLanguage.Julia +KLCP.KLiteCodecPackStandard +KLCP.KLiteMegaCodecPack +KKBOXTaiwan.KKBOX +ChiaLungChen.Kaku +DominikReichl.KeePassPasswordSafe +DominikReichl.KeePassPasswordSafe +DominikReichl.KeePassPasswordSafe +KeePassXCTeam.KeePassXC +KeeWeb.KeeWeb +Keybase.Keybase +KiCad.KiCad +KiCad.KiCad +KiCad.KiCad +KrispTechnologies.Krisp +KritaFoundation.Krita +RoniLehto.LMath +LBRY.LBRY +LBRY.LBRY +LBRY.LBRY +LBRY.LBRY +LBRY.LBRY +LINE.LINE +JosephAlbahari.LINQPad6 +LLVM.LLVM +LMMSDevelopers.LMMS +LMMSDevelopers.LMMS +love2dorg.LOVE +leokhoa.Laragon +LastPass.LastPass +LazarusTeam.Lazarus +RiotGames.LeagueofLegends +Lenovo.LenovoMigrationAssistant +Lenovo.LenovoSystemUpdate +LakendLabs.Lens +Leonflix.Leonflix +BellSoft.LibericaJDK11 +BellSoft.LibericaJDK11Full +BellSoft.LibericaJDK14 +BellSoft.LibericaJDK14Full +BellSoft.LibericaJDK15 +BellSoft.LibericaJDK15Full +BellSoft.LibericaJDK8 +BellSoft.LibericaJDK8Full +LibreCADTeam.LibreCAD +TheDocumentFoundation.LibreOffice +TheDocumentFoundation.LibreOffice +TeamLidarr.Lidarr +AlexeyTyrrrzGolub.LightBulb +AlexeyTyrrrzGolub.LightBulb +ChristianKaiser.Lightscreen +LeifAsbrinkSM5BSZ.Linrad +LiskFoundation.LiskHub +Listen.Listen1 +Listen.Listen1 +Flywheel.Local +CrystalRich.LockHunter +BinaryFortressSoftware.LogFusion +BinaryFortressSoftware.LogFusion +Logitech.LogitechGamingSoftware +Loom.Loom +LyXTeam.LyX +LyXTeam.LyX +COTILab.MCXStudioversionnightlybuild +COTILab.MCXStudio +MoritzBunkus.MKVToolNix +MoritzBunkus.MKVToolNix +MoritzBunkus.MKVToolNix +MoritzBunkus.MKVToolNix +MoritzBunkus.MKVToolNix +MPCHCTeam.MPCHC +MPCHCTeam.MPCHC +MPCHCTeam.MPCHC +ThomasNordquist.MQTTExplorer +Microsoft.MSIXCore +MaxthonInternational.MX5 +MYCOM.MYGAMESGameCenter +FlyingSnowSamanthaGlocker.MacType +Firetrust.MailWasher +Firetrust.MailWasherPro +MajsoulPlusTeam.MajsoulPlus +GuinpinSoft.MakeMKV +Malwarebytes.Malwarebytes +KDE.Marble +MariaDB.MariaDB +Jocs.MarkText +WestWindTechnologies.MarkdownMonster +WestWindTechnologies.MarkdownMonster +WestWindTechnologies.MarkdownMonster +WestWindTechnologies.MarkdownMonster +WestWindTechnologies.MarkdownMonster +MarkdownOutlook.MarkdownOutlook +MasterPackager.MasterPackager +Mattermost.Mattermost +MediaAreanet.MediaInfo +MediaAreanet.MediaInfoCLI +VentisMedia.MediaMonkey +TheMeldproject.Meld +JaneaSystems.MemuraiDeveloper +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETCoreSDK +Microsoft.MicrosoftNETFrameworkMultiTargetingPack +Microsoft.MicrosoftNETFrameworkMultiTargetingPack +Microsoft.MicrosoftNETFrameworkMultiTargetingPack +Microsoft.MicrosoftNETFrameworkSDK +Microsoft.MicrosoftNETFrameworkMultiTargetingPack +Microsoft.MicrosoftNETFrameworkMultiTargetingPack +Microsoft.MicrosoftNETFrameworkSDK +Microsoft.MicrosoftNETFrameworkTargetingPack +Microsoft.MicrosoftNETFrameworkSDK +Microsoft.MicrosoftNETFrameworkTargetingPack +Microsoft.MicrosoftNETSDK +Microsoft.MicrosoftNETSDK +Microsoft.MicrosoftNETSDK +Microsoft.MicrosoftNETSDK +Microsoft.MicrosoftAzureCLI +Microsoft.MicrosoftAzureStorageEmulator +Microsoft.MicrosoftAzureStorageExplorer +Microsoft.MicrosoftAzureStorageExplorer +Microsoft.MicrosoftDeploymentToolkit +Microsoft.MicrosoftEdge +Microsoft.MicrosoftEdgeBeta +Microsoft.MicrosoftEdgeDev +MicrosoftGarage.MicrosoftGarageMousewithoutBorders +Microsoft.MicrosoftHelpViewer +Microsoft.MicrosoftHelpViewer +Microsoft.MicrosoftMPI +Microsoft.MicrosoftMPI +Microsoft.MicrosoftMPISDK +Microsoft.MicrosoftODBCDriver13forSQLServer +Microsoft.MicrosoftODBCDriver17forSQLServer +Microsoft.MicrosoftOLEDBDriverforSQLServer +Microsoft.MicrosoftROpen +Microsoft.MicrosoftROpen +Microsoft.MicrosoftSQLServer2012NativeClient +Microsoft.MicrosoftSQLServer2014ManagementObjects +Microsoft.MicrosoftSQLServer2016 +Microsoft.MicrosoftSQLServer2016Policies +Microsoft.MicrosoftSQLServer2016TSQLLanguageService +Microsoft.MicrosoftSQLServer2016TSQLScriptDom +Microsoft.MicrosoftSQLServer2017 +Microsoft.MicrosoftSQLServer2017Policies +Microsoft.MicrosoftSQLServer2017TSQLLanguageService +Microsoft.MicrosoftSQLServerDataTierApplicationFramework +Microsoft.MicrosoftSQLServerManagementStudio +Microsoft.MicrosoftSQLServerManagementStudio +Microsoft.MicrosoftSQLServerManagementStudio +Microsoft.MicrosoftSQLServerManagementStudio +Microsoft.MicrosoftSQLServerManagementStudio +Microsoft.MicrosoftSmallBasic +Microsoft.MicrosoftSystemCLRTypesforSQLServer2014 +Microsoft.MicrosoftSystemCLRTypesforSQLServer2016 +Microsoft.MicrosoftSystemCLRTypesforSQLServer2017 +Microsoft.MicrosoftVisioViewer2016 +Microsoft.MicrosoftVisualStudio2010ToolsforOfficeRuntime +Microsoft.MicrosoftVisualStudio2015Shell +Microsoft.MicrosoftVisualStudioCode +Microsoft.MicrosoftVisualStudioCodeInsiders +Microsoft.MicrosoftVisualStudioToolsforApplications2015 +Microsoft.MicrosoftVisualStudioToolsforApplications2015LanguageSupport +Microsoft.MicrosoftVisualStudioToolsforApplications2017 +Microsoft.MicrosoftWebPlatformInstaller +Anaconda.Miniconda3 +Anaconda.Miniconda3py +MongoDB.MongoDB2008PlusSSL +Xamarin.MonoforWindows +TheMonoGameTeam.MonoGameSDK +MoonlightGameStreamingProject.MoonlightGameStreamingClient +AGALWOOD.Motrix +AGALWOOD.Motrix +Mozilla.MozillaFirefoxESR +Mozilla.MozillaFirefoxESR +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefoxESR +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefoxESR +Mozilla.MozillaFirefoxESR +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaFirefox +Mozilla.MozillaMaintenanceService +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +Mozilla.MozillaThunderbird +NicholasHTollervey.Mu +MullvadVPN.MullvadVPN +MullvadVPN.MullvadVPN +Microsoft.MultilingualAppToolkit +canonical.Multipass +TheMumbleDevelopers.Mumble +TheMumbleDevelopers.Mumble +TheMumbleDevelopers.Mumble +WernerSchweerandOthers.MuseScore3 +YoutaTec.Muta +Logitech.MyHarmony +MartinRenoldandtheMyPaintDevelopmentTeam.MyPaint +Oracle.MySQLInstallerCommunity +Feodor.Mypal +JustinAquadro.NBTExplorer +RicoSuter.NSwagStudio +NVAccess.NVDA +NVIDIA.NVIDIANVIDIARTXVoiceDriver +NVIDIA.NVIDIARTXVoiceApplication +NZXT.NZXTCAM +NZXT.NZXTCAM +NZXT.NZXTCAM +NZXT.NZXTCAM +NZXT.NZXTCAM +NZXT.NZXTCAM +AllanCORNET.Nelson +Neotys.NeoLoad +LutzRoeder.Netron +Nitro.NitroPro +NmapProject.Nmap +qinghai.NoSQLBoosterforMongoDB +qinghai.NoSQLBoosterforMongoDB +qinghai.NoSQLBoosterforMongoDB +NodejsFoundation.Nodejs +Nodist.Nodist +NordVPN.NordVPN +TEFINCOM.NordVPN +NordVPN.NordVPNnetworkTAP +NordVPN.NordVPNnetworkTUN +CodingRoad.NoteHighlight2016 +XhmikosR.Notepad2mod +NotionLabs.Notion +NotionLabs.Notion +NmapProject.Npcap +NullNoname.NullpoMino +NullsoftandContributors.NullsoftInstallSystem +OBSProject.OBSStudio +HamsterRepublicProductions.OHRRPGCEgorgonzola20200502 +AscensioSystemSIA.ONLYOFFICEDesktopEditors +GNUOctave.Octave +WetHatLab.OneNoteTaggingKit +OpenShopChannel.OpenShopChannelDownloader +TheOpenShellTeam.OpenShell +namazso.OpenHashTab +namazso.OpenHashTab +ojdkbuildopensourceproject.OpenJDK +ojdkbuildopensourceproject.OpenJDK +ojdkbuildopensourceproject.OpenJDK +ojdkbuildopensourceproject.OpenJDK +OpenMPTDevs.OpenMPT +ApacheSoftwareFoundation.OpenOffice +OpenRAdevelopers.OpenRA +TheOpenSCADDevelopers.OpenSCAD +ShiningLightProductions.OpenSSL +OpenShotStudios.OpenShotVideoEditor +OpenTTD.OpenTTD +OpenTTD.OpenTTD +OpenVPNTechnologies.OpenVPN +SparkLabs.OpenVPNConfigurationGenerator +OpenVPNTechnologies.OpenVPNConnect +OperaSoftware.OperaGXStable +OperaSoftware.OperaGXStable +OperaSoftware.OperaStable +OperaSoftware.OperaStable +OperaSoftware.OperaStable +OperaSoftware.OperaStable +OperaSoftware.OperaStable +OperaSoftware.OperaStable +Oracle.OracleVMVirtualBox +Oracle.OracleVMVirtualBox +Oracle.OracleVMVirtualBox +Oracle.OracleVMVirtualBox +OutSystems.OutSystemsDevelopmentEnvironment11 +EXPSystems.PDFreDirect +SoberLemurSasdiVacondioAndrea.PDFsamBasic +CCPKU.PKUGateway +JanFiala.PSPadeditor +NagleCode.PacketSender +MoonchildProductions.PaleMoon +MoonchildProductions.PaleMoon +JohnMacFarlane.Pandoc +JohnMacFarlane.Pandoc +JohnMacFarlane.Pandoc +ParadoxInteractive.ParadoxLauncher +ParagonSoftware.ParagonBackupRecoveryÃâžÂ17CE +ParsecCloud.Parsec +FrancescoSorge.PasteIntoFile +GiorgioTani.PeaZip +GiorgioTani.PeaZip +PersepolisTeam.PersepolisDownloadManager +HeikoSommerfeldt.PhonerLite +touchbyte.PhotoSync +NGWIN.PicPick +PicoTorrentcontributors.PicoTorrent +blupiorg.PlanetBlupi +SonyInteractiveEntertainmentNetworkAmerica.PlayStationÃâžÂNow +JosefNemec.Playnite +Plex.Plex +Plex.PlexMediaPlayer +Plex.PlexMediaServer +Plex.Plexamp +Plex.Plexamp +Plex.Plexamp +wwwpokerthnet.PokerTH +Postbox.Postbox +PostgreSQLGlobalDevelopmentGroup.PostgreSQL12 +PostgreSQLGlobalDevelopmentGroup.PostgreSQL13 +Microsoft.PowerShell7preview +Microsoft.PowerShell7 +IronmanSoftware.PowerShellUniversal +Microsoft.PowerToys +KimWalisch.Primesieve +PrivateInternetAccess.PrivateInternetAccess +ProgressSoftware.ProgressTelerikFiddler +Microsoft.ProjectMyScreenApp +ProtonTechnologies.ProtonVPN +ProtonTechnologies.ProtonVPNTap +SimonTatham.PuTTY +PuppetLabs.Puppet +Puppet.PuppetAgent +Puppet.PuppetBolt +Puppet.PuppetDevelopmentKit +MillerPuckette.PureData +LancasterUniversityPhysics.PyMODA +PythonSoftwareFoundation.Python +HubertPham.PythonPyAudio +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.Python +PythonSoftwareFoundation.PythonLauncher +QGISDevelopmentTeam.QGISACoru +QGISDevelopmentTeam.QGISBucuresti +QGISDevelopmentTeam.QGISPi +MichaelHansen.QTextPad +HannaKnutsson.Qalculate +LarusStone.QtSpim +ModuleArt.QuickPictureViewer +PaddyXu.QuickLook +Quicken.Quicken +RCoreTeam.RforWindows +RCoreTeam.RforWindows +RCoreTeam.RforWindows +RStudio.RStudio +Rambox.Rambox +OlegDanilov.RapidEnvironmentEditor +RaspberryPi.RaspberryPiImager +rawtherapeecom.RawTherapee +PaulRawnsley.RedditWallpaperChanger +jklSoft.Rekodecompilerfor +Devolutions.RemoteDesktopManager +Devolutions.RemoteDesktopManagerFree +RemoteMouse.RemoteMouse +BaldurKarlsson.RenderDoc +Antmicro.Renode +Responsively.ResponsivelyApp +RetroShareTeam.RetroShare +VSRevoGroup.RevoUninstaller +VSRevoGroup.RevoUninstallerPro +3TSoftwareLabs.Robo3T +3TSoftwareLabs.Robo3T +3TSoftwareLabs.Robo3T +RocketChatSupport.RocketChat +PunkSoftware.RocketDock +ArtsoftEntertainment.RocksnDiamonds +MarkoBL.Rosi +code4ward.RoyalTS +RoyalApps.RoyalTS +TheRFoundation.Rtools +RubyInstallerTeam.Ruby +RubyInstallerTeam.RubywithMSYS2 +RubyInstallerTeam.Ruby +LukeHaas.RunJS +LukeHaas.RunJS +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TheRustProjectDevelopers.Rust +TopalaSoftwareSolutions.SIW2020aTrial +RicardoVillalba.SMPlayer +Navimatics.SSHFSWin2020 +DotzSoftwares.SVGExplorerExtension +SamsungElectronics.SamsungDeX +mircearoata.SatisfactoryModLauncher +ScratchFoundation.ScratchDesktop +NickeManarin.ScreenToGif +TheScribusTeam.Scribus +TheScummVMTeam.ScummVM +spikespaz.SearchDeflector +Sejda.SejdaPDFDesktop +Datalust.Seq +Microsoft.SharePointOnlineManagementShell +ShareXTeam.ShareX +RandyRantscom.SharpKeys +Meltytech.Shotcut +SigilEbook.Sigil +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +OpenWhisperSystems.Signal +Approximatrix.SimplyFortran3 +AshleyStone.SitdownMW +SkypeTechnologies.Skype +SkypeTechnologies.Skype +SkypeTechnologies.Skype +SlackTechnologies.SlackMachineWide +TechSmith.Snagit2020 +SnakeNestcom.SnakeTail +CoryPlotts.Snoop +SmartBearSoftware.SoapUI +SonicPi.SonicPi +Sonos.Sonos +Sonos.SonosController +AntoineAflalo.SoundSwitch +Atlassian.Sourcetree +SpeedCrunch.SpeedCrunch +SpekProject.Spek +StandardNotes.StandardNotes +Valve.Steam +wwwsbclorg.SteelBankCommonLisp +SteelSeries.SteelSeriesEngine +Stellariumteam.Stellarium +Stellariumteam.Stellarium +Stellariumteam.Stellarium +strawberryperlcomproject.StrawberryPerl +GeneralWorkings.StreamlabsOBS +GeneralWorkings.StreamlabsOBS +GeneralWorkings.StreamlabsOBS +Streamlink.Streamlink +SebastianMeyer.StreamlinkTwitchGUI +JanHovancik.Stretchly +JanHovancik.Stretchly +JanHovancik.Stretchly +Stride.Stride +BrickLink.Studio +SublimeHQ.SublimeMerge +SublimeHQ.SublimeText3 +KrzysztofKowalczyk.SumatraPDF +SuperColliderCommunity.SuperCollider +SuperTuxKart.SuperTuxKart3Dopensourcearcaderacerwithavarietycharacterstracksandmodestoplay +Microsoft.SurfaceDuoEmulator +SyncTrayzor.SyncTrayzor +MisterGroup.SystemExplorer +OpenVPNTechnologies.TAPWindows +erengy.Taiga +Tailscale.Tailscale +Tailscale.TailscaleIPN +TaiseiProject.TaiseiProject +Taskcade.Taskade +Taskcade.Taskade +TreasureData.Tdagent +TreasureData.Tdagent +BenitovanderZander.TeXstudioTeXstudioisafullyfeaturedLaTeXeditor +TeXUsersGroup.TeXworks +TeamSpeakSystems.TeamSpeak3Client +TechPowerUp.TechPowerUpGPUZ +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TelegramFZ.TelegramDesktop +TeraTermProject.TeraTerm +EugenePankov.Terminus +EugenePankov.Terminus +EugenePankov.Terminus +CompuPhase.Termite +TesseractOCRcommunity.TesseractOCRopensourceOCRengine +Texmaker.Texmaker +Texnomic.TexnomicSecureDNSTerminal +RaMMicHaeL.Textify +Appestcom.TickTick +GlavSoft.TightVNC +TikzEdt.TikzEdt +mapeditororg.Tiled +Nadeo.TmNationsForever +Toggl.TogglDesktop +Toggl.TogglTrack +TortoiseGit.TortoiseGit +TortoiseSVN.TortoiseSVN +TortoiseSVN.TortoiseSVN +TranslucentTBOpenSourceDevelopers.TranslucentTB +TransmissionProject.Transmission +YurySidorovTransmissionRemoteGUIworkinggroup.TransmissionRemoteGUI +BinaryFortressSoftware.TrayStatus +BinaryFortressSoftware.TrayStatus +JAMSoftware.TreeSizeFree +JAMSoftware.TreeSize +Trelbyorg.Trelby +CeruleanStudios.Trillian +TunnelBear.TunnelBear +NewBreedSoftware.TuxPaint +InspectElement.Tweeten +XanderFrangos.TwinkleTray +TwitchInteractive.Twitch +GDATACyberDefense.TypeRefHasher +SafelyRemovecom.USBSafelyRemove +DrewNaylor.UXLLauncher +DrewNaylor.UXLLauncher +Ultimaker.UltimakerCura +Ultimaker.UltimakerCura +Ultimaker.UltimakerCura +uvncbvba.UltraVnc +ReasonSoftware.Unchecky +UnifiedIntents.UnifiedRemote +UnityTechnologies.UnityHub +Microsoft.UpdateforKB2504637 +Microsoft.UpdateforMicrosoftVisualStudio2015KB3095681 +Ubisoft.Uplay +UMEZAWATakeshi.UtVideoCodecSuite +VCV.VCVRack +VideoLAN.VLCmediaplayer +VideoLAN.VLCmediaplayer +VideoLAN.VLCmediaplayer +VMware.VMwareHorizonClient +VMware.VMwarePlayer +VMware.VMwareWorkstation +RealVNC.VNCServer +RealVNC.VNCViewer +Microsoft.VSCodium +Microsoft.VSCodium +marhauserssourceforgenet.VcXsrv +BramMoolenaaretal.Vim +VirtManagerProject.VirtViewer +VivaldiTechnologies.Vivaldi +BlackTreeGaming.Vortex +VoyagerX.Vrew +LunarG.VulkanSDK +WarzoneProject.Warzone +Waterfox.WaterfoxCurrent +Waterfox.WaterfoxCurrent +Waterfox.WaterfoxCurrent +Devolutions.WaykNow +Buds.WeakAurasCompanion +Buds.WeakAurasCompanion +Buds.WeakAurasCompanion +Buds.WeakAurasCompanion +MachineLearningGroupUniversityofWaikatoHamiltonNZ.Weka +ImageWriterDevelopers.Win32DiskImager +SamHocevar.WinCompose +TimothyJohnson.WinDynamicDesktop +TimothyJohnson.WinDynamicDesktop +TimothyJohnson.WinDynamicDesktop +Navimatics.WinFsp2020 +HTTrack.WinHTTrackWebsiteCopier +ThingamahoochieSoftware.WinMerge +winrar.WinRAR +winrar.WinRAR +MartinPrikryl.WinSCP +Corel.WinZip +Winamp.Winamp +Microsoft.Windows10UpdateAssistant +Microsoft.WindowsAdminCenter +Microsoft.WindowsAssessmentandDeploymentKitWindows10 +Microsoft.WindowsAssessmentandDeploymentKitWindowsPreinstallationEnvironmentAddonsWindows10 +Microsoft.WindowsDriverKitWindows +DynastreamInnovations.WindowsDriverPackageDynastreamInnovationsANTLibUSBDrivers +SiliconLabsSoftware.WindowsDriverPackageSiliconLabsSoftwareUSB +Microsoft.WindowsSDKAddOn +Microsoft.WindowsSoftwareDevelopmentKitWindows +Microsoft.WindowsSoftwareDevelopmentKitWindows +Microsoft.WindowsSoftwareDevelopmentKitWindows +WireGuard.WireGuard +TheWiresharkdevelopercommunitywwwwiresharkorg.Wireshark +TheWiresharkdevelopercommunitywwwwiresharkorg.Wireshark +TheWiresharkdevelopercommunitywwwwiresharkorg.Wireshark +TheWiresharkdevelopercommunitywwwwiresharkorg.Wireshark +AntibodySoftware.WizFile +AntibodySoftware.WizKey +AntibodySoftware.WizMouse +AntibodySoftware.WizTree +AntibodySoftware.WizTree +Automattic.WordPresscom +Automattic.WordPresscom +Automattic.WordPresscom +Automattic.WordPresscom +RobCaelersRaymondPenners.Workrave +Writage.Writage +X2GoProject.X2GoClientforWindows +Bitnami.XAMPP +ChristianHohnstaedt.XCA +TheTBOOXOpenSourceGroup.XMakebuildutility +XMind.XMind +GougeletPierree.XnView +GougeletPierree.XnViewMP +YarnContributors.Yarn +BeijingYinxiangBijiTechnologies.YinxiangBijiv +BeijingYinxiangBijiTechnologies.YinxiangBijiv +BeijingYinxiangBijiTechnologies.YinxiangBijiv +BeijingYinxiangBijiTechnologies.YinxiangBijiv +BeijingYinxiangBijiTechnologies.YinxiangBijiv +AdlerLuiz.YouTubeMusicDesktopApp +Yubico.YubiKeyManager +Zentimocom.ZentimoPRO +ZeroTier.ZeroTierOne +HendrikErz.Zettlr +RobinStuartBogDanVatra.Zint +Zoom.Zoom +Zoom.ZoomOutlookPlugin +CorporationforDigitalScholarship.Zotero +KandraLabs.Zulip +SangomaTechnologies.Zulu +ZygorGuides.ZygorClientUninstaller +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +Balena.balenaEtcher +NathanielJohns.beatdrop +ClementTsang.bottom +butterflowuigithub.butterflowui +KovidGoyal.calibre +ElliottZheng.copytranslator +thedarktableproject.darktable +dnGrepCommunityContributors.dnGREP +JGraph.drawio +HardcodedSoftware.dupeGuru +eMClient.eMClient +Ebbflowio.ebbflow +fluxSoftware.flux +KurataSayuri.ffftp +wereturtle.ghostwriter +gnuplotdevelopmentteam.gnuplotpatchlevel8 +StefansTools.grepWin +DrewNaylor.guinget +DrewNaylor.guinget +DrewNaylor.guinget +eVenture.hidemeVPN +eVenture.hidemeVPN +PurpleI2P.i2pd +VantageLinguistics.iSEEKAnswerWorksEnglishRuntime +Apple.iTunes +KDE.kdenlive +KDE.kdiff3 +NextGenerationSoftware.mRemoteNG +MaximaTeam.maxima +FrankSkare.mpvnet +720kb.ndm +xiles.nexusfont +ownCloud.ownCloud +EasternGraphics.pConplannerPRO +LaurentPRendeCotret.pandocplot +ThepgAdminDevelopmentTeam.pgAdmin4 +ThepgAdminDevelopmentTeam.pgAdmin4 +ThepgAdminDevelopmentTeam.pgAdmin4 +ThepgAdminDevelopmentTeam.pgAdmin4 +TheqBittorrentproject.qBittorrent +TheqBittorrentproject.qBittorrent +TheqBittorrentproject.qBittorrent +remoteit.remoteit +remoteit.remoteit +Lightbend.sbt +ScilabEnterprises.scilab +TheSqlectronTeam.sqlectron +JanHovancik.stretchly +OliverSchwendener.ueli +OliverSchwendener.ueli +OliverSchwendener.ueli +Humanity.xmoto +Humanity.xmoto +Microsoft.微软设备健康助手 +Alipaycom.支付宝安全控件 +百度在线网络技术有限公司.百度网盘 +腾讯科技有限公司.腾讯QQ diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_2.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_2.yaml new file mode 100644 index 0000000000..8674ba6f04 --- /dev/null +++ b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Exe_2.yaml @@ -0,0 +1,18 @@ +# Same content with UpdateFlowTest_Exe.yaml but with higher version +Id: AppInstallerCliTest.TestExeInstaller +Version: 3.0.0.0 +Name: AppInstaller Test Installer +Publisher: Microsoft Corporation +AppMoniker: AICLITestExe +License: Test +Switches: + Custom: /custom /ver3.0.0.0 + SilentWithProgress: /silentwithprogress + Silent: /silence + Update: /update +Installers: + - Arch: x64 + Url: https://ThisIsNotUsed + InstallerType: exe + Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml index 46b7cc7c13..8fa39c20b3 100644 --- a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml +++ b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml @@ -11,4 +11,5 @@ Installers: InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h index 959467cdbc..2a03ef9e14 100644 --- a/src/AppInstallerCLITests/TestHooks.h +++ b/src/AppInstallerCLITests/TestHooks.h @@ -7,6 +7,7 @@ #include #include +#include #include #ifdef AICLI_DISABLE_TEST_HOOKS @@ -26,4 +27,9 @@ namespace AppInstaller void TestHook_SetSourceFactoryOverride(const std::string& type, std::function()>&& factory); void TestHook_ClearSourceFactoryOverrides(); } + + namespace Logging + { + void TestHook_SetTelemetryOverride(std::shared_ptr ttl); + } } diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp index 5fc1ca3513..46de9c6f3c 100644 --- a/src/AppInstallerCLITests/TestSource.cpp +++ b/src/AppInstallerCLITests/TestSource.cpp @@ -9,8 +9,11 @@ using namespace AppInstaller::Repository; namespace TestCommon { - TestPackageVersion::TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata) : - VersionManifest(manifest), Metadata(std::move(installationMetadata)) {} + TestPackageVersion::TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata, std::weak_ptr source) : + VersionManifest(manifest), Metadata(std::move(installationMetadata)), Source(source) {} + + TestPackageVersion::TestPackageVersion(const Manifest& manifest, std::weak_ptr source) : + VersionManifest(manifest), Source(source) {} TestPackageVersion::LocIndString TestPackageVersion::GetProperty(PackageVersionProperty property) const { @@ -19,11 +22,13 @@ namespace TestCommon case PackageVersionProperty::Id: return LocIndString{ VersionManifest.Id }; case PackageVersionProperty::Name: - return LocIndString{ VersionManifest.Name }; + return LocIndString{ VersionManifest.DefaultLocalization.Get() }; case PackageVersionProperty::Version: return LocIndString{ VersionManifest.Version }; case PackageVersionProperty::Channel: return LocIndString{ VersionManifest.Channel }; + case PackageVersionProperty::SourceIdentifier: + return LocIndString{ Source.lock()->GetIdentifier() }; default: return {}; } @@ -52,14 +57,14 @@ namespace TestCommon return result; } - TestPackageVersion::Manifest TestPackageVersion::GetManifest() const + TestPackageVersion::Manifest TestPackageVersion::GetManifest() { return VersionManifest; } std::shared_ptr TestPackageVersion::GetSource() const { - return Source; + return Source.lock(); } TestPackageVersion::MetadataMap TestPackageVersion::GetMetadata() const @@ -80,20 +85,20 @@ namespace TestCommon } } - TestPackage::TestPackage(const std::vector& available) + TestPackage::TestPackage(const std::vector& available, std::weak_ptr source) { for (const auto& manifest : available) { - AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest, source)); } } - TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available) : - InstalledVersion(TestPackageVersion::Make(installed, std::move(installationMetadata))) + TestPackage::TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available, std::weak_ptr source) : + InstalledVersion(TestPackageVersion::Make(installed, std::move(installationMetadata), source)) { for (const auto& manifest : available) { - AvailableVersions.emplace_back(TestPackageVersion::Make(manifest)); + AvailableVersions.emplace_back(TestPackageVersion::Make(manifest, source)); } } @@ -178,6 +183,28 @@ namespace TestCommon return false; } + bool TestPackage::IsSame(const IPackage* other) const + { + const TestPackage* otherAvailable = dynamic_cast(other); + + if (!otherAvailable || + InstalledVersion.get() != otherAvailable->InstalledVersion.get() || + AvailableVersions.size() != otherAvailable->AvailableVersions.size()) + { + return false; + } + + for (size_t i = 0; i < AvailableVersions.size(); ++i) + { + if (AvailableVersions[i].get() != otherAvailable->AvailableVersions[i].get()) + { + return false; + } + } + + return true; + } + const SourceDetails& TestSource::GetDetails() const { return Details; @@ -185,7 +212,7 @@ namespace TestCommon const std::string& TestSource::GetIdentifier() const { - return Identifier; + return Details.Identifier; } SearchResult TestSource::Search(const SearchRequest& request) const @@ -204,4 +231,39 @@ namespace TestCommon { return Composite; } + + std::shared_ptr TestSourceFactory::Create(const SourceDetails& details, IProgressCallback&) + { + return OnCreate(details); + } + + void TestSourceFactory::Add(SourceDetails& details, IProgressCallback&) + { + if (OnAdd) + { + OnAdd(details); + } + } + + void TestSourceFactory::Update(const SourceDetails& details, IProgressCallback&) + { + if (OnUpdate) + { + OnUpdate(details); + } + } + + void TestSourceFactory::Remove(const SourceDetails& details, IProgressCallback&) + { + if (OnRemove) + { + OnRemove(details); + } + } + + // Make copies of self when requested. + TestSourceFactory::operator std::function()>() + { + return [this]() { return std::make_unique(*this); }; + } } diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h index daee38ca28..6f614d5faa 100644 --- a/src/AppInstallerCLITests/TestSource.h +++ b/src/AppInstallerCLITests/TestSource.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include @@ -17,7 +18,8 @@ namespace TestCommon using LocIndString = AppInstaller::Utility::LocIndString; using MetadataMap = AppInstaller::Repository::IPackageVersion::Metadata; - TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata = {}); + TestPackageVersion(const Manifest& manifest, std::weak_ptr source = {}); + TestPackageVersion(const Manifest& manifest, MetadataMap installationMetadata, std::weak_ptr source = {}); template static std::shared_ptr Make(Args&&... args) @@ -27,13 +29,13 @@ namespace TestCommon LocIndString GetProperty(AppInstaller::Repository::PackageVersionProperty property) const override; std::vector GetMultiProperty(AppInstaller::Repository::PackageVersionMultiProperty property) const override; - Manifest GetManifest() const override; + Manifest GetManifest() override; std::shared_ptr GetSource() const override; MetadataMap GetMetadata() const override; Manifest VersionManifest; MetadataMap Metadata; - std::shared_ptr Source; + std::weak_ptr Source; protected: static void AddFoldedIfHasValueAndNotPresent(const AppInstaller::Utility::NormalizedString& value, std::vector& target); @@ -43,14 +45,15 @@ namespace TestCommon struct TestPackage : public AppInstaller::Repository::IPackage { using Manifest = AppInstaller::Manifest::Manifest; + using ISource = AppInstaller::Repository::ISource; using LocIndString = AppInstaller::Utility::LocIndString; using MetadataMap = TestPackageVersion::MetadataMap; // Create a package with only available versions using these manifests. - TestPackage(const std::vector& available); + TestPackage(const std::vector& available, std::weak_ptr source = {}); // Create a package with an installed version, metadata, and optionally available versions. - TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available = {}); + TestPackage(const Manifest& installed, MetadataMap installationMetadata, const std::vector& available = {}, std::weak_ptr source = {}); template static std::shared_ptr Make(Args&&... args) @@ -64,22 +67,47 @@ namespace TestCommon std::shared_ptr GetLatestAvailableVersion() const override; std::shared_ptr GetAvailableVersion(const AppInstaller::Repository::PackageVersionKey& versionKey) const override; bool IsUpdateAvailable() const override; + bool IsSame(const IPackage* other) const override; std::shared_ptr InstalledVersion; std::vector> AvailableVersions; }; // An ISource implementation for use across the test code. - struct TestSource : public AppInstaller::Repository::ISource + struct TestSource : public AppInstaller::Repository::ISource, public std::enable_shared_from_this { const AppInstaller::Repository::SourceDetails& GetDetails() const override; const std::string& GetIdentifier() const override; AppInstaller::Repository::SearchResult Search(const AppInstaller::Repository::SearchRequest& request) const override; bool IsComposite() const override; - AppInstaller::Repository::SourceDetails Details; - std::string Identifier = "*TestSource"; + AppInstaller::Repository::SourceDetails Details = { "TestSource", "Microsoft.TestSource", "//arg", "", "*TestSource" }; std::function SearchFunction; bool Composite = false; }; + + // An ISourceFactory implementation for use across the test code. + struct TestSourceFactory : public AppInstaller::Repository::ISourceFactory + { + using CreateFunctor = std::function(const AppInstaller::Repository::SourceDetails&)>; + using AddFunctor = std::function; + using UpdateFunctor = std::function; + using RemoveFunctor = std::function; + + TestSourceFactory(CreateFunctor create) : OnCreate(std::move(create)) {} + + // ISourceFactory + std::shared_ptr Create(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override; + void Add(AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override; + void Update(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override; + void Remove(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override; + + // Make copies of self when requested. + operator std::function()>(); + + CreateFunctor OnCreate; + AddFunctor OnAdd; + UpdateFunctor OnUpdate; + RemoveFunctor OnRemove; + }; } diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index d5104d5a82..805a185452 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -15,8 +17,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -63,7 +68,7 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest }), + TestPackage::Make(std::vector{ manifest }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } else if (input == "TestQueryReturnTwo") @@ -71,13 +76,13 @@ namespace auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest }), + TestPackage::Make(std::vector{ manifest }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); result.Matches.emplace_back( ResultMatch( - TestPackage::Make(std::vector{ manifest2 }), + TestPackage::Make(std::vector{ manifest2 }, this->shared_from_this()), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); } @@ -107,12 +112,19 @@ namespace { auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe.yaml")); + auto manifest3 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_Exe_2.yaml")); result.Matches.emplace_back( ResultMatch( TestPackage::Make( manifest, - TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, - std::vector{ manifest2, manifest } + TestPackage::MetadataMap + { + { PackageVersionMetadata::InstalledType, "Exe" }, + { PackageVersionMetadata::StandardUninstallCommand, "C:\\uninstall.exe" }, + { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, + }, + std::vector{ manifest3, manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -126,7 +138,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMsixInstaller"))); } @@ -139,7 +152,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "MSStore" } }, - std::vector{ manifest } + std::vector{ manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); } @@ -153,7 +167,8 @@ namespace TestPackage::Make( manifest2, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -167,7 +182,8 @@ namespace TestPackage::Make( manifest, TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Msix" } }, - std::vector{ manifest2, manifest } + std::vector{ manifest2, manifest }, + this->shared_from_this() ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); } @@ -287,6 +303,19 @@ void OverrideForCompositeInstalledSource(TestContext& context) } }); } +void OverrideForImportSource(TestContext& context) +{ + context.Override({ "OpenPredefinedSource", [](TestContext& context) + { + context.Add({}); + } }); + + context.Override({ Workflow::OpenSourcesForImport, [](TestContext& context) + { + context.Add(std::vector>{ std::make_shared() }); + } }); +} + void OverrideForUpdateInstallerMotw(TestContext& context) { context.Override({ UpdateInstallerFileMotwIfApplicable, [](TestContext&) @@ -309,6 +338,19 @@ void OverrideForShellExecute(TestContext& context) OverrideForUpdateInstallerMotw(context); } +void OverrideForExeUninstall(TestContext& context) +{ + context.Override({ ShellExecuteUninstallImpl, [](TestContext& context) + { + // Write out the uninstall command + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestExeUninstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file << context.Get(); + file.close(); + } }); +} + void OverrideForMSIX(TestContext& context) { context.Override({ MsixInstall, [](TestContext& context) @@ -330,6 +372,23 @@ void OverrideForMSIX(TestContext& context) } }); } +void OverrideForMSIXUninstall(TestContext& context) +{ + context.Override({ MsixUninstall, [](TestContext& context) + { + // Write out the package full name + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMsixUninstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + for (const auto& packageFamilyName : context.Get()) + { + file << packageFamilyName << std::endl; + } + + file.close(); + } }); +} + void OverrideForMSStore(TestContext& context, bool isUpdate) { if (isUpdate) @@ -387,6 +446,30 @@ TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } +TEST_CASE("InstallFlowNonZeroExitCode", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_NonZeroExitCode.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(context.GetTerminationHR() == S_OK); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/ExitCode 0x80070005") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); +} + TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -651,7 +734,7 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow][workflow]") REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::MultiplePackagesFound).get()) != std::string::npos); } -TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") +TEST_CASE("ShowFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") { std::ostringstream showOutput; TestContext context{ showOutput, std::cin }; @@ -664,12 +747,12 @@ TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow][workflow]") // Verify AppInfo is printed REQUIRE(showOutput.str().find("AppInstallerCliTest.TestExeInstaller") != std::string::npos); - REQUIRE(showOutput.str().find("AppInstaller Test Installer") != std::string::npos); + REQUIRE(showOutput.str().find("AppInstaller Test Exe Installer") != std::string::npos); REQUIRE(showOutput.str().find("1.0.0.0") != std::string::npos); REQUIRE(showOutput.str().find("https://ThisIsNotUsed") != std::string::npos); } -TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow][workflow]") +TEST_CASE("ShowFlow_SearchAndShowAppVersion", "[ShowFlow][workflow]") { std::ostringstream showOutput; TestContext context{ showOutput, std::cin }; @@ -795,6 +878,7 @@ TEST_CASE("UpdateFlow_UpdateExe", "[UpdateFlow][workflow]") std::getline(updateResultFile, updateResultStr); REQUIRE(updateResultStr.find("/update") != std::string::npos); REQUIRE(updateResultStr.find("/silence") != std::string::npos); + REQUIRE(updateResultStr.find("/ver3.0.0.0") != std::string::npos); } TEST_CASE("UpdateFlow_UpdateMsix", "[UpdateFlow][workflow]") @@ -940,6 +1024,328 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); } +TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForExeUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called and parameters are passed in. + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("uninstall.exe") != std::string::npos); + REQUIRE(uninstallResultStr.find("/silence") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallMsix", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestMsixUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSIXUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMsixInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called with the package full name. + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallMSStore", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestMsixUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSIXUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMSStoreInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called with the package full name + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("microsoft.skypeapp_kzf8qxf38zg5c") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallExeNotFound", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.MissingApp"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is not called. + REQUIRE(!std::filesystem::exists(uninstallResultPath.GetPath())); + REQUIRE(uninstallOutput.str().find(Resource::LocString(Resource::String::NoInstalledPackageFound).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); +} + +TEST_CASE("ExportFlow_ExportAll", "[ExportFlow][workflow]") +{ + TestCommon::TempFile exportResultPath("TestExport.json"); + + std::ostringstream exportOutput; + TestContext context{ exportOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::OutputFile, exportResultPath); + + ExportCommand exportCommand({}); + exportCommand.Execute(context); + INFO(exportOutput.str()); + + // Verify contents of exported collection + const auto& exportedCollection = context.Get(); + REQUIRE(exportedCollection.Sources.size() == 1); + REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); + + const auto& exportedPackages = exportedCollection.Sources[0].Packages; + REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMsixInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString().empty(); + })); +} + +TEST_CASE("ExportFlow_ExportAll_WithVersions", "[ExportFlow][workflow]") +{ + TestCommon::TempFile exportResultPath("TestExport.json"); + + std::ostringstream exportOutput; + TestContext context{ exportOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::OutputFile, exportResultPath); + context.Args.AddArg(Execution::Args::Type::IncludeVersions); + + ExportCommand exportCommand({}); + exportCommand.Execute(context); + INFO(exportOutput.str()); + + // Verify contents of exported collection + const auto& exportedCollection = context.Get(); + REQUIRE(exportedCollection.Sources.size() == 1); + REQUIRE(exportedCollection.Sources[0].Details.Identifier == "*TestSource"); + + const auto& exportedPackages = exportedCollection.Sources[0].Packages; + REQUIRE(exportedPackages.size() == 3); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestExeInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMsixInstaller" && p.VersionAndChannel.GetVersion().ToString() == "1.0.0.0"; + })); + REQUIRE(exportedPackages.end() != std::find_if(exportedPackages.begin(), exportedPackages.end(), [](const auto& p) + { + return p.Id == "AppInstallerCliTest.TestMSStoreInstaller" && p.VersionAndChannel.GetVersion().ToString() == "Latest"; + })); +} + +TEST_CASE("ImportFlow_Successful", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + TestCommon::TempFile msixInstallResultPath("TestMsixInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForMSIX(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify all packages were installed + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(std::filesystem::exists(msixInstallResultPath.GetPath())); +} + +TEST_CASE("ImportFlow_PackageAlreadyInstalled", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-AlreadyInstalled.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Exe should not have been installed again + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportPackageAlreadyInstalled).get()) != std::string::npos); +} + +TEST_CASE("ImportFlow_IgnoreVersions", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Good-AlreadyInstalled.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::IgnoreVersions); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Specified version is already installed. It should have been updated since we ignored the version. + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); +} + +TEST_CASE("ImportFlow_MissingSource", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownSource.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSourceNotInstalled).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); +} + +TEST_CASE("ImportFlow_MissingPackage", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackage.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); +} + +TEST_CASE("ImportFlow_IgnoreMissingPackage", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackage.json").GetPath().string()); + context.Args.AddArg(Execution::Args::Type::IgnoreUnavailable); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Verify installer was called for the package that was available. + REQUIRE(std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); +} + +TEST_CASE("ImportFlow_MissingVersion", "[ImportFlow][workflow]") +{ + TestCommon::TempFile exeInstallResultPath("TestExeInstalled.txt"); + + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + OverrideForImportSource(context); + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-UnknownPackageVersion.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Installer should not be called + REQUIRE(!std::filesystem::exists(exeInstallResultPath.GetPath())); + REQUIRE(importOutput.str().find(Resource::LocString(Resource::String::ImportSearchFailed).get()) != std::string::npos); + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND); +} + +TEST_CASE("ImportFlow_MalformedJsonFile", "[ImportFlow][workflow]") +{ + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Malformed.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Command should have failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); +} + +TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") +{ + std::ostringstream importOutput; + TestContext context{ importOutput, std::cin }; + context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Invalid.json").GetPath().string()); + + ImportCommand importCommand({}); + importCommand.Execute(context); + INFO(importOutput.str()); + + // Command should have failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); @@ -987,4 +1393,4 @@ TEST_CASE("VerifyInstallerTrustLevelAndUpdateInstallerFileMotw", "[DownloadInsta VerifyMotw(testInstallerPath, 3); INFO(updateMotwOutput.str()); -} \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp index aa340c01b3..7ee6f33cdf 100644 --- a/src/AppInstallerCLITests/YamlManifest.cpp +++ b/src/AppInstallerCLITests/YamlManifest.cpp @@ -4,10 +4,13 @@ #include "TestCommon.h" #include #include +#include using namespace TestCommon; using namespace AppInstaller::Manifest; +using namespace AppInstaller::Manifest::YamlParser; using namespace AppInstaller::Utility; +using namespace AppInstaller::YAML; using MultiValue = std::vector; bool operator==(const MultiValue& a, const MultiValue& b) @@ -28,41 +31,41 @@ bool operator==(const MultiValue& a, const MultiValue& b) return true; } -TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") +TEST_CASE("ReadPreviewGoodManifestAndVerifyContents", "[ManifestValidation]") { Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); - REQUIRE(manifest.Name == "MSIX SDK"); - REQUIRE(manifest.AppMoniker == "msixsdk"); + REQUIRE(manifest.DefaultLocalization.Get() == "MSIX SDK"); + REQUIRE(manifest.Moniker == "msixsdk"); REQUIRE(manifest.Version == "1.7.32"); - REQUIRE(manifest.Publisher == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get() == "Microsoft"); REQUIRE(manifest.Channel == "release"); - REQUIRE(manifest.Author == "Microsoft"); - REQUIRE(manifest.License == "MIT License"); - REQUIRE(manifest.LicenseUrl == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE"); - REQUIRE(manifest.MinOSVersion == "0.0.0.0"); - REQUIRE(manifest.Description == "The MSIX SDK project is an effort to enable developers"); - REQUIRE(manifest.Homepage == "https://github.com/microsoft/msix-packaging"); - REQUIRE(manifest.Tags == MultiValue{ "msix", "appx" }); - REQUIRE(manifest.Commands == MultiValue{ "makemsix", "makeappx" }); - REQUIRE(manifest.Protocols == MultiValue{ "protocol1", "protocol2" }); - REQUIRE(manifest.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); - REQUIRE(manifest.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(manifest.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); - REQUIRE(manifest.ProductCode == "{Foo}"); - REQUIRE(manifest.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(manifest.DefaultLocalization.Get() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get() == "MIT License"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE"); + REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "0.0.0.0"); + REQUIRE(manifest.DefaultLocalization.Get() == "The MSIX SDK project is an effort to enable developers"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://github.com/microsoft/msix-packaging"); + REQUIRE(manifest.DefaultLocalization.Get() == MultiValue{ "msix", "appx" }); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); + REQUIRE(manifest.DefaultInstallerInfo.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(manifest.DefaultInstallerInfo.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); + REQUIRE(manifest.DefaultInstallerInfo.ProductCode == "{Foo}"); + REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); // default switches - auto switches = manifest.Switches; - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/custom"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/silence"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/interactive"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log="); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir="); - REQUIRE(switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); + auto switches = manifest.DefaultInstallerInfo.Switches; + REQUIRE(switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(switches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(switches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(switches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(switches.at(InstallerSwitchType::Language) == "/en-us"); + REQUIRE(switches.at(InstallerSwitchType::Log) == "/log="); + REQUIRE(switches.at(InstallerSwitchType::InstallLocation) == "/dir="); + REQUIRE(switches.at(InstallerSwitchType::Update) == "/update"); // installers REQUIRE(manifest.Installers.size() == 2); @@ -70,52 +73,52 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1.Arch == Architecture::X86); REQUIRE(installer1.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx86.zip"); REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); - REQUIRE(installer1.Language == "en-US"); - REQUIRE(installer1.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer1.Scope == ManifestInstaller::ScopeEnum::User); + REQUIRE(installer1.Locale == "en-US"); + REQUIRE(installer1.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(installer1.Scope == ScopeEnum::User); REQUIRE(installer1.PackageFamilyName == ""); REQUIRE(installer1.ProductCode == ""); - REQUIRE(installer1.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Install); + REQUIRE(installer1.UpdateBehavior == UpdateBehaviorEnum::Install); auto installer1Switches = installer1.Switches; - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/c"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/sp"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/s"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/i"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en"); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/l="); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/d="); - REQUIRE(installer1Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/u"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Custom) == "/c"); + REQUIRE(installer1Switches.at(InstallerSwitchType::SilentWithProgress) == "/sp"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Language) == "/en"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Log) == "/l="); + REQUIRE(installer1Switches.at(InstallerSwitchType::InstallLocation) == "/d="); + REQUIRE(installer1Switches.at(InstallerSwitchType::Update) == "/u"); ManifestInstaller installer2 = manifest.Installers.at(1); REQUIRE(installer2.Arch == Architecture::X64); REQUIRE(installer2.Url == "https://rubengustorage.blob.core.windows.net/publiccontainer/msixsdkx64.zip"); REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF0000")); - REQUIRE(installer2.Language == "en-US"); - REQUIRE(installer2.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer2.Scope == ManifestInstaller::ScopeEnum::User); + REQUIRE(installer2.Locale == "en-US"); + REQUIRE(installer2.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(installer2.Scope == ScopeEnum::User); REQUIRE(installer2.PackageFamilyName == ""); REQUIRE(installer2.ProductCode == ""); - REQUIRE(installer2.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(installer2.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); // Installer2 does not declare switches, it inherits switches from package default. auto installer2Switches = installer2.Switches; - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Custom) == "/custom"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Silent) == "/silence"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Interactive) == "/interactive"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Language) == "/en-us"); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Log) == "/log="); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::InstallLocation) == "/dir="); - REQUIRE(installer2Switches.at(ManifestInstaller::InstallerSwitchType::Update) == "/update"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(installer2Switches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Language) == "/en-us"); + REQUIRE(installer2Switches.at(InstallerSwitchType::Log) == "/log="); + REQUIRE(installer2Switches.at(InstallerSwitchType::InstallLocation) == "/dir="); + REQUIRE(installer2Switches.at(InstallerSwitchType::Update) == "/update"); // Localization - REQUIRE(manifest.Localization.size() == 1); - ManifestLocalization localization1 = manifest.Localization.at(0); - REQUIRE(localization1.Language == "es-MX"); - REQUIRE(localization1.Description == "El proyecto MSIX SDK es habilita desarrolladores de diferentes"); - REQUIRE(localization1.Homepage == "https://github.com/microsoft/msix-packaging/es-MX"); - REQUIRE(localization1.LicenseUrl == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX"); + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization localization1 = manifest.Localizations.at(0); + REQUIRE(localization1.Locale == "es-MX"); + REQUIRE(localization1.Get() == "El proyecto MSIX SDK es habilita desarrolladores de diferentes"); + REQUIRE(localization1.Get() == "https://github.com/microsoft/msix-packaging/es-MX"); + REQUIRE(localization1.Get() == "https://github.com/microsoft/msix-packaging/blob/master/LICENSE-es-MX"); } TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") @@ -123,20 +126,20 @@ TEST_CASE("ReadGoodManifestWithSpaces", "[ManifestValidation]") Manifest manifest = YamlParser::CreateFromPath(TestDataFile("Manifest-Good-Spaces.yaml")); REQUIRE(manifest.Id == "microsoft.msixsdk"); - REQUIRE(manifest.Name == "MSIX SDK"); - REQUIRE(manifest.AppMoniker == "msixsdk"); + REQUIRE(manifest.DefaultLocalization.Get() == "MSIX SDK"); + REQUIRE(manifest.Moniker == "msixsdk"); REQUIRE(manifest.Version == "1.7.32"); REQUIRE(manifest.Channel == "release"); - REQUIRE(manifest.MinOSVersion == "0.0.0.0"); - REQUIRE(manifest.Tags == MultiValue{ "msix", "appx" }); - REQUIRE(manifest.Commands == MultiValue{ "makemsix", "makeappx" }); - REQUIRE(manifest.Protocols == MultiValue{ "protocol1", "protocol2" }); - REQUIRE(manifest.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); + REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "0.0.0.0"); + REQUIRE(manifest.DefaultLocalization.Get() == MultiValue{ "msix", "appx" }); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "appxbundle", "msix", "msixbundle" }); } struct ManifestExceptionMatcher : public Catch::MatcherBase { - ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly) : + ManifestExceptionMatcher(std::string expectedMessage, bool expectedWarningOnly = false) : m_expectedMessage(expectedMessage), m_expectedWarningOnly(expectedWarningOnly) {} // Performs the test for this matcher @@ -148,7 +151,7 @@ struct ManifestExceptionMatcher : public Catch::MatcherBase virtual std::string describe() const override { std::ostringstream ss; - ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << "Expected IsWarningOnly: " << m_expectedWarningOnly; + ss << std::boolalpha << "Expected exception message: " << m_expectedMessage << " Expected IsWarningOnly: " << m_expectedWarningOnly; return ss.str(); } @@ -204,7 +207,7 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") ManifestTestCase TestCases[] = { { "Manifest-Bad-ArchInvalid.yaml", "Invalid field value. Field: Arch" }, - { "Manifest-Bad-ArchMissing.yaml", "Required field missing. Field: Arch" }, + { "Manifest-Bad-ArchMissing.yaml", "Missing required property 'Arch'" }, { "Manifest-Bad-Channel-NotSupported.yaml", "Field is not supported. Field: Channel" }, { "Manifest-Bad-DifferentCase-camelCase.yaml", "All field names should be PascalCased. Field: installerType" }, { "Manifest-Bad-DifferentCase-lower.yaml", "All field names should be PascalCased. Field: installertype" }, @@ -212,9 +215,9 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-DuplicateKey.yaml", "Duplicate field found in the manifest." }, { "Manifest-Bad-DuplicateKey-DifferentCase.yaml", "Duplicate field found in the manifest." }, { "Manifest-Bad-DuplicateKey-DifferentCase-lower.yaml", "Duplicate field found in the manifest." }, - { "Manifest-Bad-IdInvalid.yaml", "Invalid field value. Field: Id" }, - { "Manifest-Bad-IdMissing.yaml", "Required field missing. Field: Id" }, - { "Manifest-Bad-InstallersMissing.yaml", "Required field missing. Field: Installers" }, + { "Manifest-Bad-IdInvalid.yaml", "Failed to validate against schema associated with property name 'Id'" }, + { "Manifest-Bad-IdMissing.yaml", "Missing required property 'Id'" }, + { "Manifest-Bad-InstallersMissing.yaml", "Missing required property 'Installers'" }, { "Manifest-Bad-InstallerTypeExe-NoSilent.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, { "Manifest-Bad-InstallerTypeExe-NoSilentRoot.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, { "Manifest-Bad-InstallerTypeExeRoot-NoSilent.yaml", "Silent and SilentWithProgress switches are not specified for InstallerType exe.", true }, @@ -225,19 +228,19 @@ TEST_CASE("ReadBadManifests", "[ManifestValidation]") { "Manifest-Bad-InstallerUniqueness-DefaultScope.yaml", "Duplicate installer entry found." }, { "Manifest-Bad-InstallerUniqueness-DefaultValues.yaml", "Duplicate installer entry found." }, { "Manifest-Bad-InstallerUniqueness-SameLang.yaml", "Duplicate installer entry found." }, - { "Manifest-Bad-LicenseMissing.yaml", "Required field missing. Field: License" }, - { "Manifest-Bad-NameMissing.yaml", "Required field missing. Field: Name" }, - { "Manifest-Bad-PublisherMissing.yaml", "Required field missing. Field: Publisher" }, - { "Manifest-Bad-Sha256Invalid.yaml", "Invalid field value. Field: Sha256" }, + { "Manifest-Bad-LicenseMissing.yaml", "Missing required property 'License'" }, + { "Manifest-Bad-NameMissing.yaml", "Missing required property 'Name'" }, + { "Manifest-Bad-PublisherMissing.yaml", "Missing required property 'Publisher'" }, + { "Manifest-Bad-Sha256Invalid.yaml", "Failed to validate against schema associated with property name 'Sha256'" }, { "Manifest-Bad-Sha256Missing.yaml", "Required field missing. Field: Sha256" }, { "Manifest-Bad-SwitchInvalid.yaml", "Unknown field. Field: NotASwitch", true }, { "Manifest-Bad-UnknownProperty.yaml", "Unknown field. Field: Fake", true }, { "Manifest-Bad-UnsupportedVersion.yaml", "Unsupported ManifestVersion" }, { "Manifest-Bad-UrlInvalid.yaml", "Invalid field value. Field: Url" }, { "Manifest-Bad-UrlMissing.yaml", "Required field missing. Field: Url" }, - { "Manifest-Bad-VersionInvalid.yaml", "Invalid field value. Field: Version" }, - { "Manifest-Bad-VersionMissing.yaml", "Required field missing. Field: Version" }, - { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Invalid field value. Field: ManifestVersion" }, + { "Manifest-Bad-VersionInvalid.yaml", "Failed to validate against schema associated with property name 'Version'" }, + { "Manifest-Bad-VersionMissing.yaml", "Missing required property 'Version'" }, + { "Manifest-Bad-InvalidManifestVersionValue.yaml", "Failed to validate against schema associated with property name 'ManifestVersion'" }, { "InstallFlowTest_MSStore.yaml", "Field value is not supported. Field: InstallerType Value: MSStore" }, { "Manifest-Bad-PackageFamilyNameOnMSI.yaml", "The specified installer type does not support PackageFamilyName. Field: InstallerType Value: Msi" }, { "Manifest-Bad-ProductCodeOnMSIX.yaml", "The specified installer type does not support ProductCode. Field: InstallerType Value: Msix" }, @@ -267,7 +270,7 @@ TEST_CASE("ManifestEncoding", "[ManifestValidation]") { INFO(testCase.TestFile); Manifest manifest = YamlParser::CreateFromPath(TestDataFile(testCase.TestFile), true, true); - REQUIRE(manifest.Name == u8"MSIX SDK\xA9"); + REQUIRE(manifest.DefaultLocalization.Get() == u8"MSIX SDK\xA9"); } } @@ -278,30 +281,30 @@ TEST_CASE("ComplexSystemReference", "[ManifestValidation]") REQUIRE(manifest.Installers.size() == 5); // Zip installer does not inherit - REQUIRE(manifest.Installers[0].InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); + REQUIRE(manifest.Installers[0].InstallerType == InstallerTypeEnum::Zip); REQUIRE(manifest.Installers[0].PackageFamilyName == ""); REQUIRE(manifest.Installers[0].ProductCode == ""); // MSIX installer does inherit - REQUIRE(manifest.Installers[1].InstallerType == ManifestInstaller::InstallerTypeEnum::Msix); + REQUIRE(manifest.Installers[1].InstallerType == InstallerTypeEnum::Msix); REQUIRE(manifest.Installers[1].Arch == Architecture::X86); REQUIRE(manifest.Installers[1].PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); REQUIRE(manifest.Installers[1].ProductCode == ""); // MSI installer does inherit - REQUIRE(manifest.Installers[2].InstallerType == ManifestInstaller::InstallerTypeEnum::Msi); + REQUIRE(manifest.Installers[2].InstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.Installers[2].Arch == Architecture::X86); REQUIRE(manifest.Installers[2].PackageFamilyName == ""); REQUIRE(manifest.Installers[2].ProductCode == "{Foo}"); // MSIX installer with override - REQUIRE(manifest.Installers[3].InstallerType == ManifestInstaller::InstallerTypeEnum::Msix); + REQUIRE(manifest.Installers[3].InstallerType == InstallerTypeEnum::Msix); REQUIRE(manifest.Installers[3].Arch == Architecture::X64); REQUIRE(manifest.Installers[3].PackageFamilyName == "Override_8wekyb3d8bbwe"); REQUIRE(manifest.Installers[3].ProductCode == ""); // MSI installer with override - REQUIRE(manifest.Installers[4].InstallerType == ManifestInstaller::InstallerTypeEnum::Msi); + REQUIRE(manifest.Installers[4].InstallerType == InstallerTypeEnum::Msi); REQUIRE(manifest.Installers[4].Arch == Architecture::X64); REQUIRE(manifest.Installers[4].PackageFamilyName == ""); REQUIRE(manifest.Installers[4].ProductCode == "Override"); @@ -318,3 +321,255 @@ TEST_CASE("ManifestVersionExtensions", "[ManifestValidation]") REQUIRE(ManifestVer("1.0.0-other-msstore.2"sv).HasExtension("msstore")); REQUIRE(ManifestVer("1.0.0-msstore.2-other"sv).HasExtension("msstore")); } + +void CopyTestDataFilesToFolder(const std::vector& testDataFiles, const std::filesystem::path& dest) +{ + for (const auto& fileName : testDataFiles) + { + std::filesystem::copy(TestDataFile(fileName), dest); + } +} + +void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) +{ + REQUIRE(manifest.Id == "microsoft.msixsdk"); + REQUIRE(manifest.Version == "1.7.32"); + REQUIRE(manifest.DefaultLocalization.Locale == "en-US"); + REQUIRE(manifest.DefaultLocalization.Get() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com/support"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com/privacy"); + REQUIRE(manifest.DefaultLocalization.Get() == "Microsoft"); + REQUIRE(manifest.DefaultLocalization.Get() == "MSIX SDK"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com/msixsdk/home"); + REQUIRE(manifest.DefaultLocalization.Get() == "MIT License"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com/msixsdk/license"); + REQUIRE(manifest.DefaultLocalization.Get() == "Copyright Microsoft Corporation"); + REQUIRE(manifest.DefaultLocalization.Get() == "https://www.microsoft.com/msixsdk/copyright"); + REQUIRE(manifest.DefaultLocalization.Get() == "This is MSIX SDK"); + REQUIRE(manifest.DefaultLocalization.Get() == "The MSIX SDK project is an effort to enable developers"); + REQUIRE(manifest.Moniker == "msixsdk"); + REQUIRE(manifest.DefaultLocalization.Get() == MultiValue{ "appxsdk", "msixsdk" }); + REQUIRE(manifest.DefaultInstallerInfo.Locale == "en-US"); + REQUIRE(manifest.DefaultInstallerInfo.Platform == std::vector{ PlatformEnum::Desktop, PlatformEnum::Universal }); + REQUIRE(manifest.DefaultInstallerInfo.MinOSVersion == "10.0.0.0"); + REQUIRE(manifest.DefaultInstallerInfo.InstallerType == InstallerTypeEnum::Zip); + REQUIRE(manifest.DefaultInstallerInfo.Scope == ScopeEnum::Machine); + REQUIRE(manifest.DefaultInstallerInfo.InstallModes == std::vector{ InstallModeEnum::Interactive, InstallModeEnum::Silent, InstallModeEnum::SilentWithProgress }); + + auto defaultSwitches = manifest.DefaultInstallerInfo.Switches; + REQUIRE(defaultSwitches.at(InstallerSwitchType::Custom) == "/custom"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::SilentWithProgress) == "/silentwithprogress"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Silent) == "/silence"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Interactive) == "/interactive"); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Log) == "/log="); + REQUIRE(defaultSwitches.at(InstallerSwitchType::InstallLocation) == "/dir="); + REQUIRE(defaultSwitches.at(InstallerSwitchType::Update) == "/upgrade"); + + REQUIRE(manifest.DefaultInstallerInfo.InstallerSuccessCodes == std::vector{ 1, static_cast(0x80070005) }); + REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); + REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); + REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); + REQUIRE(manifest.DefaultInstallerInfo.FileExtensions == MultiValue{ "appx", "msix", "appxbundle", "msixbundle" }); + + auto dependencies = manifest.DefaultInstallerInfo.Dependencies; + REQUIRE(dependencies.WindowsFeatures == MultiValue{ "IIS" }); + REQUIRE(dependencies.WindowsLibraries == MultiValue{ "VC Runtime" }); + REQUIRE(dependencies.PackageDependencies.size() == 1); + REQUIRE(dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDep"); + REQUIRE(dependencies.PackageDependencies[0].MinVersion == "1.0.0"); + REQUIRE(dependencies.ExternalDependencies == MultiValue{ "Outside dependencies" }); + + REQUIRE(manifest.DefaultInstallerInfo.Capabilities == MultiValue{ "internetClient" }); + REQUIRE(manifest.DefaultInstallerInfo.RestrictedCapabilities == MultiValue{ "runFullTrust" }); + REQUIRE(manifest.DefaultInstallerInfo.PackageFamilyName == "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"); + REQUIRE(manifest.DefaultInstallerInfo.ProductCode == "{Foo}"); + + if (isSingleton) + { + REQUIRE(manifest.Installers.size() == 1); + } + else + { + REQUIRE(manifest.Installers.size() == 2); + } + + ManifestInstaller installer1 = manifest.Installers.at(0); + REQUIRE(installer1.Arch == Architecture::X86); + REQUIRE(installer1.Locale == "en-GB"); + REQUIRE(installer1.Platform == std::vector{ PlatformEnum::Desktop }); + REQUIRE(installer1.MinOSVersion == "10.0.1.0"); + REQUIRE(installer1.InstallerType == InstallerTypeEnum::Msix); + REQUIRE(installer1.Url == "https://www.microsoft.com/msixsdk/msixsdkx86.msix"); + REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer1.SignatureSha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer1.Scope == ScopeEnum::User); + REQUIRE(installer1.InstallModes == std::vector{ InstallModeEnum::Interactive }); + + auto installer1Switches = installer1.Switches; + REQUIRE(installer1Switches.at(InstallerSwitchType::Custom) == "/c"); + REQUIRE(installer1Switches.at(InstallerSwitchType::SilentWithProgress) == "/sp"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Silent) == "/s"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Interactive) == "/i"); + REQUIRE(installer1Switches.at(InstallerSwitchType::Log) == "/l="); + REQUIRE(installer1Switches.at(InstallerSwitchType::InstallLocation) == "/d="); + REQUIRE(installer1Switches.at(InstallerSwitchType::Update) == "/u"); + + REQUIRE(installer1.UpdateBehavior == UpdateBehaviorEnum::Install); + REQUIRE(installer1.Commands == MultiValue{ "makemsixPreview", "makeappxPreview" }); + REQUIRE(installer1.Protocols == MultiValue{ "protocol1preview", "protocol2preview" }); + REQUIRE(installer1.FileExtensions == MultiValue{ "appxbundle", "msixbundle", "appx", "msix" }); + + auto installer1Dependencies = installer1.Dependencies; + REQUIRE(installer1Dependencies.WindowsFeatures == MultiValue{ "PreviewIIS" }); + REQUIRE(installer1Dependencies.WindowsLibraries == MultiValue{ "Preview VC Runtime" }); + REQUIRE(installer1Dependencies.PackageDependencies.size() == 1); + REQUIRE(installer1Dependencies.PackageDependencies[0].Id == "Microsoft.MsixSdkDepPreview"); + REQUIRE(installer1Dependencies.ExternalDependencies == MultiValue{ "Preview Outside dependencies" }); + + REQUIRE(installer1.Capabilities == MultiValue{ "internetClientPreview" }); + REQUIRE(installer1.RestrictedCapabilities == MultiValue{ "runFullTrustPreview" }); + REQUIRE(installer1.PackageFamilyName == "Microsoft.DesktopAppInstallerPreview_8wekyb3d8bbwe"); + + if (!isSingleton) + { + ManifestInstaller installer2 = manifest.Installers.at(1); + REQUIRE(installer2.Arch == Architecture::X64); + REQUIRE(installer2.InstallerType == InstallerTypeEnum::Exe); + REQUIRE(installer2.Url == "https://www.microsoft.com/msixsdk/msixsdkx64.exe"); + REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); + REQUIRE(installer2.ProductCode == "{Bar}"); + + // Localization + REQUIRE(manifest.Localizations.size() == 1); + ManifestLocalization localization1 = manifest.Localizations.at(0); + REQUIRE(localization1.Locale == "en-GB"); + REQUIRE(localization1.Get() == "Microsoft UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/support/UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/privacy/UK"); + REQUIRE(localization1.Get() == "Microsoft UK"); + REQUIRE(localization1.Get() == "MSIX SDK UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/msixsdk/home/UK"); + REQUIRE(localization1.Get() == "MIT License UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/msixsdk/license/UK"); + REQUIRE(localization1.Get() == "Copyright Microsoft Corporation UK"); + REQUIRE(localization1.Get() == "https://www.microsoft.com/msixsdk/copyright/UK"); + REQUIRE(localization1.Get() == "This is MSIX SDK UK"); + REQUIRE(localization1.Get() == "The MSIX SDK project is an effort to enable developers UK"); + REQUIRE(localization1.Get() == MultiValue{ "appxsdkUK", "msixsdkUK" }); + } +} + +TEST_CASE("ValidateV1GoodManifestAndVerifyContents", "[ManifestValidation]") +{ + TempDirectory singletonDirectory{ "SingletonManifest" }; + CopyTestDataFilesToFolder({ "ManifestV1-Singleton.yaml" }, singletonDirectory); + Manifest singletonManifest = YamlParser::CreateFromPath(singletonDirectory, true, true); + VerifyV1ManifestContent(singletonManifest, true); + + TempDirectory multiFileDirectory{ "MultiFileManifest" }; + CopyTestDataFilesToFolder({ + "ManifestV1-MultiFile-Version.yaml", + "ManifestV1-MultiFile-Installer.yaml", + "ManifestV1-MultiFile-DefaultLocale.yaml", + "ManifestV1-MultiFile-Locale.yaml" }, multiFileDirectory); + + TempFile mergedManifestFile{ "merged.yaml" }; + Manifest multiFileManifest = YamlParser::CreateFromPath(multiFileDirectory, true, true, mergedManifestFile); + VerifyV1ManifestContent(multiFileManifest, false); + + // Read from merged manifest should have the same content as multi file manifest + Manifest mergedManifest = YamlParser::CreateFromPath(mergedManifestFile); + VerifyV1ManifestContent(mergedManifest, false); +} + +YamlManifestInfo CreateYamlManifestInfo(std::string testDataFile) +{ + YamlManifestInfo result; + result.Root = AppInstaller::YAML::Load(TestDataFile(testDataFile)); + result.FileName = testDataFile; + return result; +} + +TEST_CASE("MultifileManifestInputValidation", "[ManifestValidation]") +{ + auto previewManifest = CreateYamlManifestInfo("Manifest-Good.yaml"); + auto v1SingletonManifest = CreateYamlManifestInfo("ManifestV1-Singleton.yaml"); + auto v1VersionManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Version.yaml"); + auto v1InstallerManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Installer.yaml"); + auto v1DefaultLocaleManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-DefaultLocale.yaml"); + auto v1LocaleManifest = CreateYamlManifestInfo("ManifestV1-MultiFile-Locale.yaml"); + + { + // Preview and multi file manifest together + std::vector input = { previewManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("Preview manifest does not support multi file manifest format")); + } + + { + // Singleton and multi file manifest together + std::vector input = { v1SingletonManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should not contain file with the particular ManifestType. Field: ManifestType Value: singleton")); + } + + { + // More than 1 version manifest + std::vector input = { v1VersionManifest, v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: version")); + } + + { + // More than 1 installer manifest + std::vector input = { v1VersionManifest, v1InstallerManifest, v1InstallerManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: installer")); + } + + { + // More than 1 default locale manifest + std::vector input = { v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest should contain only one file with the particular ManifestType. Field: ManifestType Value: defaultLocale")); + } + + { + // Duplicate locales + std::vector input = { v1VersionManifest, v1InstallerManifest, v1DefaultLocaleManifest, v1LocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest contains duplicate PackageLocale. Field: PackageLocale Value: en-GB")); + } + + { + // default locale not match + auto defaultLocaleManifestCopy = v1DefaultLocaleManifest; + defaultLocaleManifestCopy.Root["PackageLocale"].SetScalar("fr-fr"); + std::vector input = { v1VersionManifest, v1InstallerManifest, defaultLocaleManifestCopy, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("DefaultLocale value in version manifest does not match PackageLocale value in defaultLocale manifest")); + } + + { + // Package Id does not match + auto installerManifestCopy = v1InstallerManifest; + installerManifestCopy.Root["PackageIdentifier"].SetScalar("Another.Identifier"); + std::vector input = { v1VersionManifest, installerManifestCopy, v1DefaultLocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest has inconsistent field values. Field: PackageIdentifier Value: Another.Identifier")); + } + + { + // Package Version does not match + auto installerManifestCopy = v1InstallerManifest; + installerManifestCopy.Root["PackageVersion"].SetScalar("Another.Version"); + std::vector input = { v1VersionManifest, installerManifestCopy, v1DefaultLocaleManifest, v1LocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest has inconsistent field values. Field: PackageVersion Value: Another.Version")); + } + + { + // Incomplete multi file manifest, missing installer + std::vector input = { v1VersionManifest, v1DefaultLocaleManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest is incomplete")); + } + + { + // Incomplete multi file manifest, missing default locale + std::vector input = { v1VersionManifest, v1InstallerManifest }; + REQUIRE_THROWS_MATCHES(YamlParser::ParseManifest(input), ManifestException, ManifestExceptionMatcher("The multi file manifest is incomplete")); + } +} \ No newline at end of file diff --git a/src/AppInstallerCLITests/main.cpp b/src/AppInstallerCLITests/main.cpp index 20f5aaff85..9d192bd916 100644 --- a/src/AppInstallerCLITests/main.cpp +++ b/src/AppInstallerCLITests/main.cpp @@ -52,6 +52,7 @@ int main(int argc, char** argv) bool hasSetTestDataBasePath = false; bool waitBeforeReturn = false; + bool keepSQLLogging = false; std::vector args; for (int i = 0; i < argc; ++i) @@ -86,6 +87,10 @@ int main(int argc, char** argv) { waitBeforeReturn = true; } + else if ("-logsql"s == argv[i]) + { + keepSQLLogging = true; + } else { args.push_back(argv[i]); @@ -105,9 +110,15 @@ int main(int argc, char** argv) } } - // Enable all logging, to force log string building to run. + // Enable logging, to force log string building to run. + // Disable SQL by default, as it generates 10s of MBs of log file and + // increases the the full test run time by 60% or more. // By not creating a log target, it will all be thrown away. Logging::Log().EnableChannel(Logging::Channel::All); + if (!keepSQLLogging) + { + Logging::Log().DisableChannel(Logging::Channel::SQL); + } Logging::Log().SetLevel(Logging::Level::Verbose); Logging::EnableWilFailureTelemetry(); diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 28da9287c3..c298f0310e 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -90,7 +90,10 @@ - + + + + @@ -160,9 +163,9 @@ Disabled _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -177,7 +180,7 @@ WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true @@ -190,10 +193,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -215,10 +218,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD;WINGET_DISABLE_FOR_FUZZING - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)Public;$(ProjectDir)Telemetry;$(ProjectDir)..\binver;$(ProjectDir)..\YamlCppLib\libyaml\include;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -264,13 +267,19 @@ + + + + + + @@ -302,14 +311,19 @@ true + - + + + true + + diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 1f5a663e35..c3ebb155ce 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -123,9 +123,6 @@ Public\winget - - Public\winget - Public\winget @@ -141,6 +138,27 @@ Public\winget + + Public\winget + + + Public\winget + + + Public\winget + + + Public\winget + + + Public\winget + + + Public\winget + + + Public\winget + @@ -221,19 +239,34 @@ Source Files - + Manifest - + Manifest - + + Source Files + + + Source Files + + + Source Files + + Manifest - + Manifest - + + Manifest + + + Manifest + + Source Files diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp index 9a57111b4c..f659957433 100644 --- a/src/AppInstallerCommonCore/AppInstallerStrings.cpp +++ b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "Public/AppInstallerLogging.h" #include "Public/AppInstallerStrings.h" +#include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerLogging.h" #include "icu.h" namespace AppInstaller::Utility @@ -27,28 +28,28 @@ namespace AppInstaller::Utility if (U_FAILURE(err)) { AICLI_LOG(Core, Error, << "utext_openUTF8 returned " << err); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR); } m_brk.reset(ubrk_open(type, nullptr, nullptr, 0, &err)); if (U_FAILURE(err)) { AICLI_LOG(Core, Error, << "ubrk_open returned " << err); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR); } ubrk_setUText(m_brk.get(), m_text.get(), &err); if (U_FAILURE(err)) { AICLI_LOG(Core, Error, << "ubrk_setUText returned " << err); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR); } int32_t i = ubrk_first(m_brk.get()); if (i != 0) { AICLI_LOG(Core, Error, << "ubrk_first returned " << i); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR); } } @@ -58,7 +59,7 @@ namespace AppInstaller::Utility // Gets the current byte offset, throwing if the value is UBRK_DONE or negative. size_t CurrentOffset() const { - THROW_HR_IF(E_UNEXPECTED, m_currentBrk < 0); + THROW_HR_IF(E_NOT_VALID_STATE, m_currentBrk < 0); return static_cast(m_currentBrk); } @@ -318,14 +319,14 @@ namespace AppInstaller::Utility if (U_FAILURE(errorCode)) { AICLI_LOG(Core, Error, << "ucasemap_open returned " << errorCode); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR); } int32_t cch = ucasemap_utf8FoldCase(caseMap.get(), nullptr, 0, input.data(), static_cast(input.size()), &errorCode); if (errorCode != U_BUFFER_OVERFLOW_ERROR) { AICLI_LOG(Core, Error, << "ucasemap_utf8FoldCase returned " << errorCode); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR); } errorCode = UErrorCode::U_ZERO_ERROR; @@ -335,7 +336,7 @@ namespace AppInstaller::Utility if (U_FAILURE(errorCode)) { AICLI_LOG(Core, Error, << "ucasemap_utf8FoldCase returned " << errorCode); - THROW_HR(E_UNEXPECTED); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR); } while (result.back() == '\0') @@ -353,17 +354,24 @@ namespace AppInstaller::Utility return result; } - bool IsEmptyOrWhitespace(std::wstring_view str) + bool IsEmptyOrWhitespace(std::string_view str) { if (str.empty()) { return true; } - std::wstring inputAsWStr(str.data()); - bool nonWhitespaceNotFound = inputAsWStr.find_last_not_of(s_WideSpaceChars) == std::wstring::npos; + return str.find_last_not_of(s_SpaceChars) == std::string_view::npos; + } + + bool IsEmptyOrWhitespace(std::wstring_view str) + { + if (str.empty()) + { + return true; + } - return nonWhitespaceNotFound; + return str.find_last_not_of(s_WideSpaceChars) == std::wstring_view::npos; } bool FindAndReplace(std::string& inputStr, std::string_view token, std::string_view value) @@ -381,21 +389,51 @@ namespace AppInstaller::Utility std::string& Trim(std::string& str) { - size_t begin = str.find_first_not_of(s_SpaceChars); - size_t end = str.find_last_not_of(s_SpaceChars); - - if (begin == std::string_view::npos || end == std::string_view::npos) + if (!str.empty()) { - str.clear(); + size_t begin = str.find_first_not_of(s_SpaceChars); + size_t end = str.find_last_not_of(s_SpaceChars); + + if (begin == std::string_view::npos || end == std::string_view::npos) + { + str.clear(); + } + else if (begin != 0 || end != str.length() - 1) + { + str = str.substr(begin, (end - begin) + 1); + } } - else + + return str; + } + + std::wstring& Trim(std::wstring& str) + { + if (!str.empty()) { - str = str.substr(begin, (end - begin) + 1); + size_t begin = str.find_first_not_of(s_WideSpaceChars); + size_t end = str.find_last_not_of(s_WideSpaceChars); + + if (begin == std::string_view::npos || end == std::string_view::npos) + { + str.clear(); + } + else if (begin != 0 || end != str.length() - 1) + { + str = str.substr(begin, (end - begin) + 1); + } } return str; } + std::string Trim(std::string&& str) + { + std::string result = std::move(str); + Utility::Trim(result); + return result; + } + std::string ReadEntireStream(std::istream& stream) { std::streampos currentPos = stream.tellg(); diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp index adbb26997a..2168bc4e8e 100644 --- a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp +++ b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -6,6 +6,7 @@ #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerSHA256.h" #include "Public/AppInstallerStrings.h" +#include "winget/UserSettings.h" #define AICLI_TraceLoggingStringView(_sv_,_name_) TraceLoggingCountedUtf8String(_sv_.data(), static_cast(_sv_.size()), _name_) @@ -34,18 +35,10 @@ namespace AppInstaller::Logging { static const uint32_t s_RootExecutionId = 0; - // Used to disable telemetry on the fly. - std::atomic_bool s_isTelemetryEnabled{ true }; - std::atomic_uint32_t s_executionStage{ 0 }; std::atomic_uint32_t s_subExecutionId{ s_RootExecutionId }; - bool IsTelemetryEnabled() - { - return g_IsTelemetryProviderEnabled && s_isTelemetryEnabled; - } - void __stdcall wilResultLoggingCallback(const wil::FailureInfo& info) noexcept { Telemetry().LogFailure(info); @@ -67,7 +60,10 @@ namespace AppInstaller::Logging TelemetryTraceLogger::TelemetryTraceLogger() { + // TODO: Needs to be made a singleton registration/removal in the future RegisterTraceLogging(); + + m_isSettingEnabled = !Settings::User().Get(); } TelemetryTraceLogger::~TelemetryTraceLogger() @@ -81,7 +77,17 @@ namespace AppInstaller::Logging return instance; } - void TelemetryTraceLogger::LogFailure(const wil::FailureInfo& failure) noexcept + bool TelemetryTraceLogger::DisableRuntime() + { + return m_isRuntimeEnabled.exchange(false); + } + + void TelemetryTraceLogger::EnableRuntime() + { + m_isRuntimeEnabled = true; + } + + void TelemetryTraceLogger::LogFailure(const wil::FailureInfo& failure) const noexcept { if (IsTelemetryEnabled()) { @@ -110,7 +116,7 @@ namespace AppInstaller::Logging }()); } - void TelemetryTraceLogger::LogStartup() noexcept + void TelemetryTraceLogger::LogStartup() const noexcept { LocIndString version = Runtime::GetClientVersion(); LocIndString packageVersion; @@ -127,7 +133,7 @@ namespace AppInstaller::Logging nullptr, TraceLoggingCountedString(version->c_str(), static_cast(version->size()), "Version"), TraceLoggingCountedString(packageVersion->c_str(), static_cast(packageVersion->size()), "PackageVersion"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance|PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } @@ -140,7 +146,7 @@ namespace AppInstaller::Logging } } - void TelemetryTraceLogger::LogCommand(std::string_view commandName) noexcept + void TelemetryTraceLogger::LogCommand(std::string_view commandName) const noexcept { if (IsTelemetryEnabled()) { @@ -149,14 +155,14 @@ namespace AppInstaller::Logging GetActivityId(), nullptr, AICLI_TraceLoggingStringView(commandName, "Command"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Info, << "Leaf command to execute: " << commandName); } - void TelemetryTraceLogger::LogCommandSuccess(std::string_view commandName) noexcept + void TelemetryTraceLogger::LogCommandSuccess(std::string_view commandName) const noexcept { if (IsTelemetryEnabled()) { @@ -172,7 +178,7 @@ namespace AppInstaller::Logging AICLI_LOG(CLI, Info, << "Leaf command succeeded: " << commandName); } - void TelemetryTraceLogger::LogCommandTermination(HRESULT hr, std::string_view file, size_t line) noexcept + void TelemetryTraceLogger::LogCommandTermination(HRESULT hr, std::string_view file, size_t line) const noexcept { if (IsTelemetryEnabled()) { @@ -192,7 +198,7 @@ namespace AppInstaller::Logging AICLI_LOG(CLI, Error, << "Terminating context: 0x" << SetHRFormat << hr << " at " << file << ":" << line); } - void TelemetryTraceLogger::LogException(std::string_view commandName, std::string_view type, std::string_view message) noexcept + void TelemetryTraceLogger::LogException(std::string_view commandName, std::string_view type, std::string_view message) const noexcept { if (IsTelemetryEnabled()) { @@ -212,7 +218,7 @@ namespace AppInstaller::Logging AICLI_LOG(CLI, Error, << "Caught " << type << ": " << message); } - void TelemetryTraceLogger::LogIsManifestLocal(bool isLocalManifest) noexcept + void TelemetryTraceLogger::LogIsManifestLocal(bool isLocalManifest) const noexcept { if (IsTelemetryEnabled()) { @@ -222,12 +228,12 @@ namespace AppInstaller::Logging nullptr, TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), TraceLoggingBool(isLocalManifest, "IsManifestLocal"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } } - void TelemetryTraceLogger::LogManifestFields(std::string_view id, std::string_view name, std::string_view version) noexcept + void TelemetryTraceLogger::LogManifestFields(std::string_view id, std::string_view name, std::string_view version) const noexcept { if (IsTelemetryEnabled()) { @@ -239,14 +245,14 @@ namespace AppInstaller::Logging AICLI_TraceLoggingStringView(id, "Id"), AICLI_TraceLoggingStringView(name,"Name"), AICLI_TraceLoggingStringView(version, "Version"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance|PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Info, << "Manifest fields: Name [" << name << "], Version [" << version << ']'); } - void TelemetryTraceLogger::LogNoAppMatch() noexcept + void TelemetryTraceLogger::LogNoAppMatch() const noexcept { if (IsTelemetryEnabled()) { @@ -255,14 +261,14 @@ namespace AppInstaller::Logging GetActivityId(), nullptr, TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Info, << "No app found matching input criteria"); } - void TelemetryTraceLogger::LogMultiAppMatch() noexcept + void TelemetryTraceLogger::LogMultiAppMatch() const noexcept { if (IsTelemetryEnabled()) { @@ -271,14 +277,14 @@ namespace AppInstaller::Logging GetActivityId(), nullptr, TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Info, << "Multiple apps found matching input criteria"); } - void TelemetryTraceLogger::LogAppFound(std::string_view name, std::string_view id) noexcept + void TelemetryTraceLogger::LogAppFound(std::string_view name, std::string_view id) const noexcept { if (IsTelemetryEnabled()) { @@ -289,14 +295,14 @@ namespace AppInstaller::Logging TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), AICLI_TraceLoggingStringView(name, "Name"), AICLI_TraceLoggingStringView(id, "Id"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Info, << "Found one app. App id: " << id << " App name: " << name); } - void TelemetryTraceLogger::LogSelectedInstaller(int arch, std::string_view url, std::string_view installerType, std::string_view scope, std::string_view language) noexcept + void TelemetryTraceLogger::LogSelectedInstaller(int arch, std::string_view url, std::string_view installerType, std::string_view scope, std::string_view language) const noexcept { if (IsTelemetryEnabled()) { @@ -310,7 +316,7 @@ namespace AppInstaller::Logging AICLI_TraceLoggingStringView(installerType, "InstallerType"), AICLI_TraceLoggingStringView(scope, "Scope"), AICLI_TraceLoggingStringView(language, "Language"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } @@ -331,7 +337,7 @@ namespace AppInstaller::Logging std::string_view tag, std::string_view command, size_t maximum, - std::string_view request) + std::string_view request) const noexcept { if (IsTelemetryEnabled()) { @@ -349,12 +355,12 @@ namespace AppInstaller::Logging AICLI_TraceLoggingStringView(command, "Command"), TraceLoggingUInt64(static_cast(maximum), "Maximum"), AICLI_TraceLoggingStringView(request, "Request"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } } - void TelemetryTraceLogger::LogSearchResultCount(uint64_t resultCount) noexcept + void TelemetryTraceLogger::LogSearchResultCount(uint64_t resultCount) const noexcept { if (IsTelemetryEnabled()) { @@ -364,7 +370,7 @@ namespace AppInstaller::Logging nullptr, TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), TraceLoggingUInt64(resultCount, "ResultCount"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } } @@ -375,7 +381,7 @@ namespace AppInstaller::Logging std::string_view channel, const std::vector& expected, const std::vector& actual, - bool overrideHashMismatch) + bool overrideHashMismatch) const noexcept { if (IsTelemetryEnabled()) { @@ -390,7 +396,7 @@ namespace AppInstaller::Logging TraceLoggingBinary(expected.data(), static_cast(expected.size()), "Expected"), TraceLoggingBinary(actual.data(), static_cast(actual.size()), "Actual"), TraceLoggingValue(overrideHashMismatch, "Override"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } @@ -402,7 +408,7 @@ namespace AppInstaller::Logging << ']'); } - void TelemetryTraceLogger::LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) + void TelemetryTraceLogger::LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) const noexcept { if (IsTelemetryEnabled()) { @@ -416,32 +422,109 @@ namespace AppInstaller::Logging AICLI_TraceLoggingStringView(channel, "Channel"), AICLI_TraceLoggingStringView(type, "Type"), TraceLoggingUInt32(errorCode, "ErrorCode"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } AICLI_LOG(CLI, Error, << type << " installer failed: " << errorCode); - } - - void TelemetryTraceLogger::LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name) + } + + void TelemetryTraceLogger::LogUninstallerFailure(std::string_view id, std::string_view version, std::string_view type, uint32_t errorCode) const noexcept { if (IsTelemetryEnabled()) { TraceLoggingWriteActivity(g_hTelemetryProvider, - "DuplicateARPEntry", + "UninstallerFailure", GetActivityId(), nullptr, TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), - TraceLoggingHResult(hr, "HResult"), - AICLI_TraceLoggingStringView(scope, "Scope"), - AICLI_TraceLoggingStringView(architecture, "Architecture"), - AICLI_TraceLoggingStringView(productCode, "ProductCode"), - AICLI_TraceLoggingStringView(name, "Name"), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + AICLI_TraceLoggingStringView(id, "Id"), + AICLI_TraceLoggingStringView(version, "Version"), + AICLI_TraceLoggingStringView(type, "Type"), + TraceLoggingUInt32(errorCode, "ErrorCode"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << type << " uninstaller failed: " << errorCode); + } + + void TelemetryTraceLogger::LogSuccessfulInstallARPChange( + std::string_view sourceIdentifier, + std::string_view packageIdentifier, + std::string_view packageVersion, + std::string_view packageChannel, + size_t changesToARP, + size_t matchesInARP, + size_t countOfIntersectionOfChangesAndMatches, + std::string_view arpName, + std::string_view arpVersion, + std::string_view arpPublisher, + std::string_view arpLanguage) const noexcept + { + if (IsTelemetryEnabled()) + { + size_t languageNumber = 0xFFFF; + + try + { + std::istringstream languageConversion{ std::string{ arpLanguage } }; + languageConversion >> languageNumber; + } + catch (...) {} + + TraceLoggingWriteActivity(g_hTelemetryProvider, + "InstallARPChange", + GetActivityId(), + nullptr, + TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), + AICLI_TraceLoggingStringView(sourceIdentifier, "SourceIdentifier"), + AICLI_TraceLoggingStringView(packageIdentifier, "PackageIdentifier"), + AICLI_TraceLoggingStringView(packageVersion, "PackageVersion"), + AICLI_TraceLoggingStringView(packageChannel, "PackageChannel"), + TraceLoggingUInt64(static_cast(changesToARP), "ChangesToARP"), + TraceLoggingUInt64(static_cast(matchesInARP), "MatchesInARP"), + TraceLoggingUInt64(static_cast(countOfIntersectionOfChangesAndMatches), "ChangesThatMatch"), + AICLI_TraceLoggingStringView(arpName, "ARPName"), + AICLI_TraceLoggingStringView(arpVersion, "ARPVersion"), + AICLI_TraceLoggingStringView(arpPublisher, "ARPPublisher"), + TraceLoggingUInt64(static_cast(languageNumber), "ARPLanguage"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage | PDT_SoftwareSetupAndInventory), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } - AICLI_LOG(CLI, Error, << "Ignoring duplicate ARP entry " << scope << '|' << architecture << '|' << productCode << " [" << name << "]"); + AICLI_LOG(CLI, Info, << "During package install, " << changesToARP << " changes to ARP were observed, " + << matchesInARP << " matches were found for the package, and " << countOfIntersectionOfChangesAndMatches << " packages were in both"); + + if (arpName.empty()) + { + AICLI_LOG(CLI, Info, << "No single entry was determined to be associated with the package"); + } + else + { + AICLI_LOG(CLI, Info, << "The entry determined to be associated with the package is '" << arpName << "', with publisher '" << arpPublisher << "'"); + } + } + + bool TelemetryTraceLogger::IsTelemetryEnabled() const noexcept + { + return g_IsTelemetryProviderEnabled && m_isSettingEnabled && m_isRuntimeEnabled; + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static std::shared_ptr s_TelemetryTraceLogger_TestOverride; +#endif + + TelemetryTraceLogger& Telemetry() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_TelemetryTraceLogger_TestOverride) + { + return *s_TelemetryTraceLogger_TestOverride.get(); + } +#endif + + return TelemetryTraceLogger::GetInstance(); } void EnableWilFailureTelemetry() @@ -451,14 +534,14 @@ namespace AppInstaller::Logging DisableTelemetryScope::DisableTelemetryScope() { - m_token = s_isTelemetryEnabled.exchange(false); + m_token = Telemetry().DisableRuntime(); } DisableTelemetryScope::~DisableTelemetryScope() { if (m_token) { - s_isTelemetryEnabled = true; + Telemetry().EnableRuntime(); } } @@ -469,15 +552,23 @@ namespace AppInstaller::Logging std::atomic_uint32_t SubExecutionTelemetryScope::m_sessionId{ s_RootExecutionId }; - SubExecutionTelemetryScope::SubExecutionTelemetryScope() - { - auto expected = s_RootExecutionId; - THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !s_subExecutionId.compare_exchange_strong(expected, ++m_sessionId), - "Cannot create a sub execution telemetry session when a previous session exists."); - } - - SubExecutionTelemetryScope::~SubExecutionTelemetryScope() - { - s_subExecutionId = s_RootExecutionId; - } + SubExecutionTelemetryScope::SubExecutionTelemetryScope() + { + auto expected = s_RootExecutionId; + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !s_subExecutionId.compare_exchange_strong(expected, ++m_sessionId), + "Cannot create a sub execution telemetry session when a previous session exists."); + } + + SubExecutionTelemetryScope::~SubExecutionTelemetryScope() + { + s_subExecutionId = s_RootExecutionId; + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + // Replace this test hook with context telemetry when it gets moved over + void TestHook_SetTelemetryOverride(std::shared_ptr ttl) + { + s_TelemetryTraceLogger_TestOverride = std::move(ttl); + } +#endif } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/DateTime.cpp b/src/AppInstallerCommonCore/DateTime.cpp index 238ffc462e..06ca51455b 100644 --- a/src/AppInstallerCommonCore/DateTime.cpp +++ b/src/AppInstallerCommonCore/DateTime.cpp @@ -6,7 +6,7 @@ namespace AppInstaller::Utility { // If moved to C++20, this can be replaced with standard library implementations. - void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time) + void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339) { using namespace std::chrono; @@ -14,12 +14,14 @@ namespace AppInstaller::Utility auto tt = system_clock::to_time_t(time); _localtime64_s(&localTime, &tt); - // Don't bother with fill chars for dates, as most of the time this won't be an issue. - stream << (1900 + localTime.tm_year) << '-' << (1 + localTime.tm_mon) << '-' << localTime.tm_mday << ' ' + stream + << std::setw(4) << (1900 + localTime.tm_year) << '-' + << std::setw(2) << (1 + localTime.tm_mon) << '-' + << std::setw(2) << localTime.tm_mday << (useRFC3339 ? 'T' : ' ') << std::setw(2) << std::setfill('0') << localTime.tm_hour << ':' << std::setw(2) << std::setfill('0') << localTime.tm_min << ':' << std::setw(2) << std::setfill('0') << localTime.tm_sec << '.'; - + // Get partial seconds auto sinceEpoch = time.time_since_epoch(); auto leftoverMillis = duration_cast(sinceEpoch) - duration_cast(sinceEpoch); diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index e1ad55432f..5593ff8998 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "Public/AppInstallerErrors.h" #include "Public/AppInstallerRuntime.h" #include "Public/AppInstallerDownloader.h" #include "Public/AppInstallerSHA256.h" @@ -109,6 +110,12 @@ namespace AppInstaller::Utility dest.flush(); + // Check download size matches if content length is provided in response header + if (contentLength > 0) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH, bytesDownloaded != contentLength); + } + std::vector result; if (computeHash) { @@ -192,24 +199,26 @@ namespace AppInstaller::Utility AICLI_LOG(Core, Info, << "Finished applying motw"); } - void ApplyMotwUsingIAttachmentExecuteIfApplicable(const std::filesystem::path& filePath, const std::string& source) + HRESULT ApplyMotwUsingIAttachmentExecuteIfApplicable(const std::filesystem::path& filePath, const std::string& source) { AICLI_LOG(Core, Info, << "Started applying motw using IAttachmentExecute to " << filePath); if (!IsNTFS(filePath)) { AICLI_LOG(Core, Info, << "File system is not NTFS. Skipped applying motw"); - return; + return S_OK; } // Attachment execution service needs STA to succeed, so we'll create a new thread and CoInitialize with STA. + HRESULT aesSaveResult = S_OK; auto updateMotw = [&]() -> HRESULT { Microsoft::WRL::ComPtr attachmentExecute; RETURN_IF_FAILED(CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&attachmentExecute))); RETURN_IF_FAILED(attachmentExecute->SetLocalPath(filePath.c_str())); RETURN_IF_FAILED(attachmentExecute->SetSource(Utility::ConvertToUTF16(source).c_str())); - RETURN_IF_FAILED(attachmentExecute->Save()); + aesSaveResult = attachmentExecute->Save(); + RETURN_IF_FAILED(aesSaveResult); return S_OK; }; @@ -229,6 +238,8 @@ namespace AppInstaller::Utility aesThread.join(); - AICLI_LOG(Core, Info, << "Finished applying motw using IAttachmentExecute. Result: " << hr); + AICLI_LOG(Core, Info, << "Finished applying motw using IAttachmentExecute. Result: " << hr << " IAttachmentExecute::Save() result: " << aesSaveResult); + + return aesSaveResult; } } diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index 6f83155dca..791140bf67 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -103,6 +103,32 @@ namespace AppInstaller return "No applicable update found"; case APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE: return "winget upgrade --all completed with failures"; + case APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED: + return "Installer failed security check"; + case APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH: + return "Download size does not match expected content length"; + case APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND: + return "Uninstall command not found"; + case APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED: + return "Running uninstall command failed"; + case APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR: + return "ICU break iterator error"; + case APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR: + return "ICU casemap error"; + case APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR: + return "ICU regex error"; + case APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED: + return "Failed to install one or more imported packages"; + case APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND: + return "Could not find one or more requested packages"; + case APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE: + return "Json file is invalid"; + case APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE: + return "The source location is not remote"; + case APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE: + return "The configured rest source is not supported"; + case APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA: + return "Invalid data returned by rest source"; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index bc20d3648d..223f31ab3c 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -25,6 +25,14 @@ namespace AppInstaller::Settings return User().Get(); case Feature::ExperimentalUpgrade: return User().Get(); + case Feature::ExperimentalUninstall: + return User().Get(); + case Feature::ExperimentalImport: + return User().Get(); + case Feature::ExperimentalExport: + return User().Get(); + case Feature::ExperimentalRestSource: + return User().Get(); default: THROW_HR(E_UNEXPECTED); } @@ -44,6 +52,14 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "List Command", "list", "https://aka.ms/winget-settings", Feature::ExperimentalList }; case Feature::ExperimentalUpgrade: return ExperimentalFeature{ "Upgrade Command", "upgrade", "https://aka.ms/winget-settings", Feature::ExperimentalUpgrade }; + case Feature::ExperimentalUninstall: + return ExperimentalFeature{ "Uninstall Command", "uninstall", "https://aka.ms/winget-settings", Feature::ExperimentalUninstall }; + case Feature::ExperimentalImport: + return ExperimentalFeature{ "Import Command", "import", "https://aka.ms/winget-settings", Feature::ExperimentalImport }; + case Feature::ExperimentalExport: + return ExperimentalFeature{ "Export Command", "export", "https://aka.ms/winget-settings", Feature::ExperimentalExport }; + case Feature::ExperimentalRestSource: + return ExperimentalFeature{ "Rest Source Support", "restSource", "https://aka.ms/winget-settings", Feature::ExperimentalRestSource }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp index a5ce68e188..cb5ea7843b 100644 --- a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp +++ b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp @@ -45,7 +45,9 @@ namespace AppInstaller::Utility::HttpStream HttpResponseMessage response = co_await m_httpClient.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED), response.StatusCode() != HttpStatusCode::Ok); + THROW_HR_IF( + MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode()), + response.StatusCode() != HttpStatusCode::Ok); // Get the length from the response if (response.Content().Headers().HasKey(L"Content-Length")) diff --git a/src/AppInstallerCommonCore/JsonSchemaValidation.cpp b/src/AppInstallerCommonCore/JsonSchemaValidation.cpp new file mode 100644 index 0000000000..f0a57ac7a6 --- /dev/null +++ b/src/AppInstallerCommonCore/JsonSchemaValidation.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/JsonSchemaValidation.h" + +namespace AppInstaller::JsonSchema +{ + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType) + { + HMODULE resourceModule = NULL; + GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (PCWSTR)LoadResourceAsString, + &resourceModule); + THROW_LAST_ERROR_IF_NULL(resourceModule); + + HRSRC resourceInfoHandle = FindResource(resourceModule, resourceName, resourceType); + THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); + + HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); + + ULONG resourceSize = 0; + char* resourceContent = NULL; + resourceSize = SizeofResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF(resourceSize == 0); + + resourceContent = reinterpret_cast(LockResource(resourceMemoryHandle)); + THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); + + std::string resourceStr; + resourceStr.assign(resourceContent, resourceSize); + + return resourceStr; + } + + Json::Value LoadSchemaDoc(const std::string& schemaStr) + { + Json::Value schemaJson; + int schemaLength = static_cast(schemaStr.length()); + Json::CharReaderBuilder charReaderBuilder; + const std::unique_ptr jsonReader(charReaderBuilder.newCharReader()); + std::string errorMsg; + if (!jsonReader->parse(schemaStr.c_str(), schemaStr.c_str() + schemaLength, &schemaJson, &errorMsg)) { + THROW_HR_MSG(E_UNEXPECTED, "Jsoncpp parser failed to parse the schema doc. Reason: %s", errorMsg.c_str()); + } + + return schemaJson; + } + + Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType) + { + return LoadSchemaDoc(LoadResourceAsString(resourceName, resourceType)); + } + + void PopulateSchema(const Json::Value& schemaJson, valijson::Schema& schema) + { + valijson::SchemaParser schemaParser; + valijson::adapters::JsonCppAdapter jsonSchemaAdapter(schemaJson); + schemaParser.populateSchema(jsonSchemaAdapter, schema); + } + + bool Validate(const valijson::Schema& schema, const Json::Value& json, valijson::ValidationResults& results) + { + valijson::Validator schemaValidator; + valijson::adapters::JsonCppAdapter jsonAdapter(json); + return schemaValidator.validate(schema, jsonAdapter, &results); + } + + std::string GetErrorStringFromResults(valijson::ValidationResults& results) + { + valijson::ValidationResults::Error error; + std::stringstream ss; + + ss << "Schema validation failed." << std::endl; + while (results.popError(error)) + { + std::string context; + for (auto itr = error.context.begin(); itr != error.context.end(); itr++) + { + context += *itr; + } + + ss << "Error context: " << context << " Description: " << error.description << std::endl; + } + + return ss.str(); + } +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/Manifest.cpp b/src/AppInstallerCommonCore/Manifest/Manifest.cpp index 9ceee4cc83..4e52c0a7c2 100644 --- a/src/AppInstallerCommonCore/Manifest/Manifest.cpp +++ b/src/AppInstallerCommonCore/Manifest/Manifest.cpp @@ -2,87 +2,49 @@ // Licensed under the MIT License. #include "pch.h" #include "winget/Manifest.h" -#include "winget/ManifestValidation.h" namespace AppInstaller::Manifest { - ManifestVer::ManifestVer(std::string_view version) + void Manifest::ApplyLocale(const std::string&) { - bool validationSuccess = true; - - // Separate the extensions out - size_t hyphenPos = version.find_first_of('-'); - if (hyphenPos != std::string_view::npos) - { - // The first part is the main version - Assign(std::string{ version.substr(0, hyphenPos) }, "."); - - // The second part is the extensions - hyphenPos += 1; - while (hyphenPos < version.length()) - { - size_t newPos = version.find_first_of('-', hyphenPos); - - size_t length = (newPos == std::string::npos ? version.length() : newPos) - hyphenPos; - m_extensions.emplace_back(std::string{ version.substr(hyphenPos, length) }, "."); + // TODO: need more work in locale processing + CurrentLocalization = DefaultLocalization; + } - hyphenPos += length + 1; - } - } - else - { - Assign(std::string{ version }, "."); - } + std::vector Manifest::GetAggregatedTags() const + { + std::vector resultTags = DefaultLocalization.Get(); - if (m_parts.size() > 3) - { - validationSuccess = false; - } - else + for (const auto& locale : Localizations) { - for (size_t i = 0; i < m_parts.size(); i++) - { - if (!m_parts[i].Other.empty()) - { - validationSuccess = false; - break; - } - } - - for (const Version& ext : m_extensions) + auto tags = locale.Get(); + for (const auto& tag : tags) { - if (ext.GetParts().empty() || ext.GetParts()[0].Integer != 0) + if (std::find(resultTags.begin(), resultTags.end(), tag) == resultTags.end()) { - validationSuccess = false; - break; + resultTags.emplace_back(tag); } } } - if (!validationSuccess) - { - std::vector errors; - errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", std::string{ version }); - THROW_EXCEPTION(ManifestException(std::move(errors))); - } + return resultTags; } - bool ManifestVer::HasExtension() const + std::vector Manifest::GetAggregatedCommands() const { - return !m_extensions.empty(); - } + std::vector resultCommands; - bool ManifestVer::HasExtension(std::string_view extension) const - { - for (const Version& ext : m_extensions) + for (const auto& installer : Installers) { - const auto& parts = ext.GetParts(); - if (!parts.empty() && parts[0].Integer == 0 && parts[0].Other == extension) + for (const auto& command : installer.Commands) { - return true; + if (std::find(resultCommands.begin(), resultCommands.end(), command) == resultCommands.end()) + { + resultCommands.emplace_back(command); + } } } - return false; + return resultCommands; } -} +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp new file mode 100644 index 0000000000..7a42090e06 --- /dev/null +++ b/src/AppInstallerCommonCore/Manifest/ManifestCommon.cpp @@ -0,0 +1,382 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/ManifestCommon.h" +#include "winget/ManifestValidation.h" + +namespace AppInstaller::Manifest +{ + namespace + { + enum class CompatibilitySet + { + None, + Exe, + Msi, + Msix, + }; + + CompatibilitySet GetCompatibilitySet(InstallerTypeEnum type) + { + switch (type) + { + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + return CompatibilitySet::Exe; + case InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + return CompatibilitySet::Msi; + case InstallerTypeEnum::Msix: + case InstallerTypeEnum::MSStore: + return CompatibilitySet::Msix; + default: + return CompatibilitySet::None; + } + } + } + + ManifestVer::ManifestVer(std::string_view version) + { + bool validationSuccess = true; + + // Separate the extensions out + size_t hyphenPos = version.find_first_of('-'); + if (hyphenPos != std::string_view::npos) + { + // The first part is the main version + Assign(std::string{ version.substr(0, hyphenPos) }, "."); + + // The second part is the extensions + hyphenPos += 1; + while (hyphenPos < version.length()) + { + size_t newPos = version.find_first_of('-', hyphenPos); + + size_t length = (newPos == std::string::npos ? version.length() : newPos) - hyphenPos; + m_extensions.emplace_back(std::string{ version.substr(hyphenPos, length) }, "."); + + hyphenPos += length + 1; + } + } + else + { + Assign(std::string{ version }, "."); + } + + if (m_parts.size() > 3) + { + validationSuccess = false; + } + else + { + for (size_t i = 0; i < m_parts.size(); i++) + { + if (!m_parts[i].Other.empty()) + { + validationSuccess = false; + break; + } + } + + for (const Version& ext : m_extensions) + { + if (ext.GetParts().empty() || ext.GetParts()[0].Integer != 0) + { + validationSuccess = false; + break; + } + } + } + + if (!validationSuccess) + { + std::vector errors; + errors.emplace_back(ManifestError::InvalidFieldValue, "ManifestVersion", std::string{ version }); + THROW_EXCEPTION(ManifestException(std::move(errors))); + } + } + + bool ManifestVer::HasExtension() const + { + return !m_extensions.empty(); + } + + bool ManifestVer::HasExtension(std::string_view extension) const + { + for (const Version& ext : m_extensions) + { + const auto& parts = ext.GetParts(); + if (!parts.empty() && parts[0].Integer == 0 && parts[0].Other == extension) + { + return true; + } + } + + return false; + } + + InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in) + { + std::string inStrLower = Utility::ToLower(in); + InstallerTypeEnum result = InstallerTypeEnum::Unknown; + + if (inStrLower == "inno") + { + result = InstallerTypeEnum::Inno; + } + else if (inStrLower == "wix") + { + result = InstallerTypeEnum::Wix; + } + else if (inStrLower == "msi") + { + result = InstallerTypeEnum::Msi; + } + else if (inStrLower == "nullsoft") + { + result = InstallerTypeEnum::Nullsoft; + } + else if (inStrLower == "zip") + { + result = InstallerTypeEnum::Zip; + } + else if (inStrLower == "appx" || inStrLower == "msix") + { + result = InstallerTypeEnum::Msix; + } + else if (inStrLower == "exe") + { + result = InstallerTypeEnum::Exe; + } + else if (inStrLower == "burn") + { + result = InstallerTypeEnum::Burn; + } + else if (inStrLower == "msstore") + { + result = InstallerTypeEnum::MSStore; + } + + return result; + } + + UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in) + { + UpdateBehaviorEnum result = UpdateBehaviorEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "install")) + { + result = UpdateBehaviorEnum::Install; + } + else if (Utility::CaseInsensitiveEquals(in, "uninstallprevious")) + { + result = UpdateBehaviorEnum::UninstallPrevious; + } + + return result; + } + + ScopeEnum ConvertToScopeEnum(const std::string& in) + { + ScopeEnum result = ScopeEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "user")) + { + result = ScopeEnum::User; + } + else if (Utility::CaseInsensitiveEquals(in, "machine")) + { + result = ScopeEnum::Machine; + } + + return result; + } + + InstallModeEnum ConvertToInstallModeEnum(const std::string& in) + { + InstallModeEnum result = InstallModeEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "interactive")) + { + result = InstallModeEnum::Interactive; + } + else if (Utility::CaseInsensitiveEquals(in, "silent")) + { + result = InstallModeEnum::Silent; + } + else if (Utility::CaseInsensitiveEquals(in, "silentWithProgress")) + { + result = InstallModeEnum::SilentWithProgress; + } + + return result; + } + + PlatformEnum ConvertToPlatformEnum(const std::string& in) + { + PlatformEnum result = PlatformEnum::Unknown; + + if (Utility::CaseInsensitiveEquals(in, "windows.desktop")) + { + result = PlatformEnum::Desktop; + } + else if (Utility::CaseInsensitiveEquals(in, "windows.universal")) + { + result = PlatformEnum::Universal; + } + + return result; + } + + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in) + { + if (in == "singleton") + { + return ManifestTypeEnum::Singleton; + } + else if (in == "version") + { + return ManifestTypeEnum::Version; + } + else if (in == "installer") + { + return ManifestTypeEnum::Installer; + } + else if (in == "defaultLocale") + { + return ManifestTypeEnum::DefaultLocale; + } + else if (in == "locale") + { + return ManifestTypeEnum::Locale; + } + else if (in == "merged") + { + return ManifestTypeEnum::Merged; + } + else + { + THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported ManifestType: %s", in.c_str()); + } + } + + std::string_view InstallerTypeToString(InstallerTypeEnum installerType) + { + switch (installerType) + { + case InstallerTypeEnum::Exe: + return "Exe"sv; + case InstallerTypeEnum::Inno: + return "Inno"sv; + case InstallerTypeEnum::Msi: + return "Msi"sv; + case InstallerTypeEnum::Msix: + return "Msix"sv; + case InstallerTypeEnum::Nullsoft: + return "Nullsoft"sv; + case InstallerTypeEnum::Wix: + return "Wix"sv; + case InstallerTypeEnum::Zip: + return "Zip"sv; + case InstallerTypeEnum::Burn: + return "Burn"sv; + case InstallerTypeEnum::MSStore: + return "MSStore"sv; + } + + return "Unknown"sv; + } + + std::string_view ScopeToString(ScopeEnum scope) + { + switch (scope) + { + case ScopeEnum::User: + return "User"sv; + case ScopeEnum::Machine: + return "Machine"sv; + } + + return "Unknown"sv; + } + + bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) + { + return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); + } + + bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType) + { + return ( + installerType == InstallerTypeEnum::Exe || + installerType == InstallerTypeEnum::Inno || + installerType == InstallerTypeEnum::Msi || + installerType == InstallerTypeEnum::Nullsoft || + installerType == InstallerTypeEnum::Wix || + installerType == InstallerTypeEnum::Burn + ); + } + + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) + { + // Unknown type cannot be compatible with any other + if (type1 == InstallerTypeEnum::Unknown || type2 == InstallerTypeEnum::Unknown) + { + return false; + } + + // Not unknown, so must be compatible + if (type1 == type2) + { + return true; + } + + CompatibilitySet set1 = GetCompatibilitySet(type1); + CompatibilitySet set2 = GetCompatibilitySet(type2); + + // If either is none, they can't be compatible + if (set1 == CompatibilitySet::None || set2 == CompatibilitySet::None) + { + return false; + } + + return set1 == set2; + } + + std::map GetDefaultKnownSwitches(InstallerTypeEnum installerType) + { + switch (installerType) + { + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Wix: + case InstallerTypeEnum::Msi: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, + {InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")}, + {InstallerSwitchType::Update, ManifestInstaller::string_t("REINSTALL=ALL REINSTALLMODE=vamus")} + }; + case InstallerTypeEnum::Nullsoft: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/S")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/S")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/D=" + std::string(ARG_TOKEN_INSTALLPATH))} + }; + case InstallerTypeEnum::Inno: + return + { + {InstallerSwitchType::Silent, ManifestInstaller::string_t("/VERYSILENT")}, + {InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/SILENT")}, + {InstallerSwitchType::Log, ManifestInstaller::string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, + {InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} + }; + default: + return {}; + } + } +} diff --git a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp deleted file mode 100644 index 28ff6efb48..0000000000 --- a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "winget/ManifestInstaller.h" - -namespace AppInstaller::Manifest -{ - namespace - { - enum class CompatibilitySet - { - None, - Exe, - Msi, - Msix, - }; - - CompatibilitySet GetCompatibilitySet(ManifestInstaller::InstallerTypeEnum type) - { - switch (type) - { - case ManifestInstaller::InstallerTypeEnum::Inno: - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - case ManifestInstaller::InstallerTypeEnum::Exe: - case ManifestInstaller::InstallerTypeEnum::Burn: - return CompatibilitySet::Exe; - case ManifestInstaller::InstallerTypeEnum::Wix: - case ManifestInstaller::InstallerTypeEnum::Msi: - return CompatibilitySet::Msi; - case ManifestInstaller::InstallerTypeEnum::Msix: - case ManifestInstaller::InstallerTypeEnum::MSStore: - return CompatibilitySet::Msix; - default: - return CompatibilitySet::None; - } - } - } - - ManifestInstaller::InstallerTypeEnum ManifestInstaller::ConvertToInstallerTypeEnum(const std::string& in) - { - std::string inStrLower = Utility::ToLower(in); - InstallerTypeEnum result = InstallerTypeEnum::Unknown; - - if (inStrLower == "inno") - { - result = InstallerTypeEnum::Inno; - } - else if (inStrLower == "wix") - { - result = InstallerTypeEnum::Wix; - } - else if (inStrLower == "msi") - { - result = InstallerTypeEnum::Msi; - } - else if (inStrLower == "nullsoft") - { - result = InstallerTypeEnum::Nullsoft; - } - else if (inStrLower == "zip") - { - result = InstallerTypeEnum::Zip; - } - else if (inStrLower == "appx" || inStrLower == "msix") - { - result = InstallerTypeEnum::Msix; - } - else if (inStrLower == "exe") - { - result = InstallerTypeEnum::Exe; - } - else if (inStrLower == "burn") - { - result = InstallerTypeEnum::Burn; - } - else if (inStrLower == "msstore") - { - result = InstallerTypeEnum::MSStore; - } - - return result; - } - - ManifestInstaller::UpdateBehaviorEnum ManifestInstaller::ConvertToUpdateBehaviorEnum(const std::string& in) - { - UpdateBehaviorEnum result = UpdateBehaviorEnum::Unknown; - - if (Utility::CaseInsensitiveEquals(in, "install")) - { - result = UpdateBehaviorEnum::Install; - } - else if (Utility::CaseInsensitiveEquals(in, "uninstallprevious")) - { - result = UpdateBehaviorEnum::UninstallPrevious; - } - - return result; - } - - ManifestInstaller::ScopeEnum ManifestInstaller::ConvertToScopeEnum(const std::string& in) - { - ScopeEnum result = ScopeEnum::Unknown; - - if (Utility::CaseInsensitiveEquals(in, "user")) - { - result = ScopeEnum::User; - } - else if (Utility::CaseInsensitiveEquals(in, "machine")) - { - result = ScopeEnum::Machine; - } - - return result; - } - - std::string_view ManifestInstaller::InstallerTypeToString(ManifestInstaller::InstallerTypeEnum installerType) - { - switch (installerType) - { - case InstallerTypeEnum::Exe: - return "Exe"sv; - case InstallerTypeEnum::Inno: - return "Inno"sv; - case InstallerTypeEnum::Msi: - return "Msi"sv; - case InstallerTypeEnum::Msix: - return "Msix"sv; - case InstallerTypeEnum::Nullsoft: - return "Nullsoft"sv; - case InstallerTypeEnum::Wix: - return "Wix"sv; - case InstallerTypeEnum::Zip: - return "Zip"sv; - case InstallerTypeEnum::Burn: - return "Burn"sv; - case InstallerTypeEnum::MSStore: - return "MSStore"sv; - } - - return "Unknown"sv; - } - - std::string_view ManifestInstaller::ScopeToString(ScopeEnum scope) - { - switch (scope) - { - case ScopeEnum::User: - return "User"sv; - case ScopeEnum::Machine: - return "Machine"sv; - } - - return "Unknown"sv; - } - - bool ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) - { - return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); - } - - bool ManifestInstaller::DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType) - { - return ( - installerType == InstallerTypeEnum::Exe || - installerType == InstallerTypeEnum::Inno || - installerType == InstallerTypeEnum::Msi || - installerType == InstallerTypeEnum::Nullsoft || - installerType == InstallerTypeEnum::Wix || - installerType == InstallerTypeEnum::Burn - ); - } - - bool ManifestInstaller::IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2) - { - // Unknown type cannot be compatible with any other - if (type1 == InstallerTypeEnum::Unknown || type2 == InstallerTypeEnum::Unknown) - { - return false; - } - - // Not unknown, so must be compatible - if (type1 == type2) - { - return true; - } - - CompatibilitySet set1 = GetCompatibilitySet(type1); - CompatibilitySet set2 = GetCompatibilitySet(type2); - - // If either is none, they can't be compatible - if (set1 == CompatibilitySet::None || set2 == CompatibilitySet::None) - { - return false; - } - - return set1 == set2; - } -} diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp new file mode 100644 index 0000000000..80c7c05d6f --- /dev/null +++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/Yaml.h" +#include "winget/JsonSchemaValidation.h" +#include "winget/ManifestCommon.h" +#include "winget/ManifestSchemaValidation.h" +#include "winget/ManifestYamlParser.h" + +#include + +namespace AppInstaller::Manifest::YamlParser +{ + using namespace std::string_view_literals; + + namespace + { + enum class YamlScalarType + { + String, + Int + }; + + // List of fields that use non string scalar types + const std::map ManifestFieldTypes= + { + { "InstallerSuccessCodes"sv, YamlScalarType::Int } + }; + + YamlScalarType GetManifestScalarValueType(const std::string& key) + { + auto iter = ManifestFieldTypes.find(key); + if (iter != ManifestFieldTypes.end()) + { + return iter->second; + } + + return YamlScalarType::String; + } + + Json::Value YamlScalarNodeToJson(const YAML::Node& scalarNode, YamlScalarType scalarType) + { + if (scalarType == YamlScalarType::Int) + { + return Json::Value(scalarNode.as()); + } + else + { + return Json::Value(scalarNode.as()); + } + } + + Json::Value ManifestYamlNodeToJson(const YAML::Node& rootNode, YamlScalarType scalarType = YamlScalarType::String) + { + Json::Value result; + + if (rootNode.IsNull()) + { + result = Json::Value::nullSingleton(); + } + else if (rootNode.IsMap()) + { + for (auto const& keyValuePair : rootNode.Mapping()) + { + // We only support string type as key in our manifest + auto key = keyValuePair.first.as(); + result[keyValuePair.first.as()] = ManifestYamlNodeToJson(keyValuePair.second, GetManifestScalarValueType(key)); + } + } + else if (rootNode.IsSequence()) + { + for (auto const& value : rootNode.Sequence()) + { + result.append(ManifestYamlNodeToJson(value, scalarType)); + } + } + else if (rootNode.IsScalar()) + { + result = YamlScalarNodeToJson(rootNode, scalarType); + } + else + { + THROW_HR(E_UNEXPECTED); + } + + return result; + } + } + + Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType) + { + std::string schemaStr; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + switch (manifestType) + { + case AppInstaller::Manifest::ManifestTypeEnum::Singleton: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_SINGLETON), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Version: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_VERSION), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Installer: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_INSTALLER), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::DefaultLocale: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + case AppInstaller::Manifest::ManifestTypeEnum::Locale: + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_LOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + else + { + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_PREVIEW), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + } + + return JsonSchema::LoadSchemaDoc(schemaStr); + } + + std::vector ValidateAgainstSchema(const std::vector& manifestList, const ManifestVer& manifestVersion) + { + std::vector errors; + // A list of schema validator to avoid multiple loadings of same schema + std::map schemaList; + + for (const auto& entry : manifestList) + { + if (schemaList.find(entry.ManifestType) == schemaList.end()) + { + // Copy constructor of valijson::Schema was private + valijson::Schema& newSchema = schemaList.emplace( + std::piecewise_construct, std::make_tuple(entry.ManifestType), std::make_tuple()).first->second; + Json::Value schemaJson = LoadSchemaDoc(manifestVersion, entry.ManifestType); + JsonSchema::PopulateSchema(schemaJson, newSchema); + } + + const auto& schema = schemaList.find(entry.ManifestType)->second; + Json::Value manifestJson = ManifestYamlNodeToJson(entry.Root); + valijson::ValidationResults results; + + if (!JsonSchema::Validate(schema, manifestJson, results)) + { + errors.emplace_back(ValidationError::MessageWithFile(JsonSchema::GetErrorStringFromResults(results), entry.FileName)); + } + } + + return errors; + } +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index 9cb9e6100c..69cdcc8eaf 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -25,12 +25,6 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Version", manifest.Version); } - // License field is required - if (manifest.License.empty()) - { - resultErrors.emplace_back(ManifestError::RequiredFieldMissing, "License"); - } - // Comparison function to check duplicate installer entry. {installerType, arch, language and scope} combination is the key. // Todo: use the comparator from ManifestComparator when that one is fully implemented. auto installerCmp = [](const ManifestInstaller& in1, const ManifestInstaller& in2) @@ -45,9 +39,9 @@ namespace AppInstaller::Manifest return in1.Arch < in2.Arch; } - if (in1.Language != in2.Language) + if (in1.Locale != in2.Locale) { - return in1.Language < in2.Language; + return in1.Locale < in2.Locale; } if (in1.Scope != in2.Scope) @@ -75,33 +69,33 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::InvalidFieldValue, "Arch"); } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Unknown) + if (installer.InstallerType == InstallerTypeEnum::Unknown) { resultErrors.emplace_back(ManifestError::InvalidFieldValue, "InstallerType"); } - if (installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Unknown) + if (installer.UpdateBehavior == UpdateBehaviorEnum::Unknown) { resultErrors.emplace_back(ManifestError::InvalidFieldValue, "UpdateBehavior"); } // Validate system reference strings if they are set at the installer level - if (!installer.PackageFamilyName.empty() && !ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + if (!installer.PackageFamilyName.empty() && !DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) { - resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportPackageFamilyName, "InstallerType", InstallerTypeToString(installer.InstallerType)); } - if (!installer.ProductCode.empty() && !ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + if (!installer.ProductCode.empty() && !DoesInstallerTypeUseProductCode(installer.InstallerType)) { - resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + resultErrors.emplace_back(ManifestError::InstallerTypeDoesNotSupportProductCode, "InstallerType", InstallerTypeToString(installer.InstallerType)); } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::MSStore) + if (installer.InstallerType == InstallerTypeEnum::MSStore) { // MSStore type is not supported in community repo resultErrors.emplace_back( ManifestError::FieldValueNotSupported, "InstallerType", - ManifestInstaller::InstallerTypeToString(installer.InstallerType)); + InstallerTypeToString(installer.InstallerType)); if (installer.ProductId.empty()) { @@ -126,9 +120,9 @@ namespace AppInstaller::Manifest } } - if (installer.InstallerType == ManifestInstaller::InstallerTypeEnum::Exe && - (installer.Switches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress) == installer.Switches.end() || - installer.Switches.find(ManifestInstaller::InstallerSwitchType::Silent) == installer.Switches.end())) + if (installer.InstallerType == InstallerTypeEnum::Exe && + (installer.Switches.find(InstallerSwitchType::SilentWithProgress) == installer.Switches.end() || + installer.Switches.find(InstallerSwitchType::Silent) == installer.Switches.end())) { resultErrors.emplace_back(ManifestError::ExeInstallerMissingSilentSwitches, ValidationError::Level::Warning); } diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp new file mode 100644 index 0000000000..e4638ec742 --- /dev/null +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -0,0 +1,555 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AppInstallerSHA256.h" +#include "winget/ManifestYamlPopulator.h" + +namespace AppInstaller::Manifest +{ + using ValidationErrors = std::vector; + + namespace + { + // Only used in preview manifest + std::vector SplitMultiValueField(const std::string& input) + { + if (input.empty()) + { + return {}; + } + + std::vector result; + size_t currentPos = 0; + while (currentPos < input.size()) + { + size_t splitPos = input.find(',', currentPos); + if (splitPos == std::string::npos) + { + splitPos = input.size(); + } + + std::string splitVal = input.substr(currentPos, splitPos - currentPos); + Utility::Trim(splitVal); + if (!splitVal.empty()) + { + result.emplace_back(std::move(splitVal)); + } + currentPos = splitPos + 1; + } + + return result; + } + + std::vector ProcessStringSequenceNode(const YAML::Node& node, bool trim = true) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector result; + + for (auto const& entry : node.Sequence()) + { + std::string value = entry.as(); + if (trim) + { + Utility::Trim(value); + } + + result.emplace_back(std::move(value)); + } + + return result; + } + + std::vector ProcessInstallerSuccessCodeSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(static_cast(entry.as())); + } + + return result; + } + + std::vector ProcessPlatformSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(ConvertToPlatformEnum(entry.as())); + } + + return result; + } + + std::vector ProcessInstallModeSequenceNode(const YAML::Node& node) + { + THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); + + std::vector result; + + for (auto const& entry : node.Sequence()) + { + result.emplace_back(ConvertToInstallModeEnum(entry.as())); + } + + return result; + } + } + + std::vector ManifestYamlPopulator::GetRootFieldProcessInfo(const ManifestVer& manifestVersion) + { + // Common fields across versions + std::vector result = + { + { "ManifestVersion", [](const YAML::Node&)->ValidationErrors { /* ManifestVersion already populated. Field listed here for duplicate and PascalCase check */ return {}; } }, + { "Installers", [this](const YAML::Node& value)->ValidationErrors { m_p_installersNode = &value; return {}; } }, + { "Localization", [this](const YAML::Node& value)->ValidationErrors { m_p_localizationsNode = &value; return {}; } }, + { "Channel", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Channel = Utility::Trim(value.as()); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + std::vector previewRootFields + { + { "Id", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Id = Utility::Trim(value.as()); return {}; } }, + { "Version", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Version = Utility::Trim(value.as()); return {}; } }, + { "AppMoniker", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Moniker = Utility::Trim(value.as()); return {}; } }, + }; + + std::move(previewRootFields.begin(), previewRootFields.end(), std::inserter(result, result.end())); + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + std::vector v1RootFields + { + { "PackageIdentifier", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Id = Utility::Trim(value.as()); return {}; } }, + { "PackageVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Version = Utility::Trim(value.as()); return {}; } }, + { "Moniker", [this](const YAML::Node& value)->ValidationErrors { m_p_manifest->Moniker = Utility::Trim(value.as()); return {}; } }, + { "ManifestType", [](const YAML::Node&)->ValidationErrors { /* ManifestType already checked. Field listed here for duplicate and PascalCase check */ return {}; } }, + }; + + std::move(v1RootFields.begin(), v1RootFields.end(), std::inserter(result, result.end())); + } + } + + // Root fields mapped as Installer and Localization values + auto rootInstallerFields = GetInstallerFieldProcessInfo(manifestVersion, true); + std::move(rootInstallerFields.begin(), rootInstallerFields.end(), std::inserter(result, result.end())); + + auto rootLocalizationFields = GetLocalizationFieldProcessInfo(manifestVersion, true); + std::move(rootLocalizationFields.begin(), rootLocalizationFields.end(), std::inserter(result, result.end())); + + return result; + } + + std::vector ManifestYamlPopulator::GetInstallerFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields) + { + // Common fields across versions + std::vector result = + { + { "InstallerType", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerType = ConvertToInstallerTypeEnum(value.as()); return {}; } }, + { "PackageFamilyName", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->PackageFamilyName = value.as(); return {}; } }, + { "ProductCode", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ProductCode = value.as(); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + // Root level and Localization node level + std::vector previewCommonFields = + { + { "UpdateBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UpdateBehavior = ConvertToUpdateBehaviorEnum(value.as()); return {}; } }, + { "Switches", [this](const YAML::Node& value)->ValidationErrors { m_p_switches = &(m_p_installer->Switches); return ValidateAndProcessFields(value, SwitchesFieldInfos); } }, + }; + + std::move(previewCommonFields.begin(), previewCommonFields.end(), std::inserter(result, result.end())); + + if (!forRootFields) + { + // Installer node only + std::vector installerOnlyFields = + { + { "Arch", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as()); return {}; } }, + { "Url", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Url = value.as(); return {}; } }, + { "Sha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, + { "SignatureSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, + { "Language", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Locale = value.as(); return {}; } }, + { "Scope", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Scope = ConvertToScopeEnum(value.as()); return {}; } }, + }; + + if (manifestVersion.HasExtension(s_MSStoreExtension)) + { + installerOnlyFields.emplace_back("ProductId", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->ProductId = value.as(); return {}; }); + } + + std::move(installerOnlyFields.begin(), installerOnlyFields.end(), std::inserter(result, result.end())); + } + else + { + // Root node only + std::vector rootOnlyFields = + { + { "MinOSVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->MinOSVersion = value.as(); return {}; } }, + { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = SplitMultiValueField(value.as()); return {}; } }, + { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = SplitMultiValueField(value.as()); return {}; } }, + { "FileExtensions", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->FileExtensions = SplitMultiValueField(value.as()); return {}; } }, + }; + + std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); + } + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + // Root level and Installer node level + std::vector v1CommonFields = + { + { "InstallerLocale", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Locale = value.as(); return {}; } }, + { "Platform", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Platform = ProcessPlatformSequenceNode(value); return {}; } }, + { "MinimumOSVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->MinOSVersion = value.as(); return {}; } }, + { "Scope", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Scope = ConvertToScopeEnum(value.as()); return {}; } }, + { "InstallModes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallModes = ProcessInstallModeSequenceNode(value); return {}; } }, + { "InstallerSwitches", [this](const YAML::Node& value)->ValidationErrors { m_p_switches = &(m_p_installer->Switches); return ValidateAndProcessFields(value, SwitchesFieldInfos); } }, + { "InstallerSuccessCodes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerSuccessCodes = ProcessInstallerSuccessCodeSequenceNode(value); return {}; } }, + { "UpgradeBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UpdateBehavior = ConvertToUpdateBehaviorEnum(value.as()); return {}; } }, + { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = ProcessStringSequenceNode(value); return {}; } }, + { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = ProcessStringSequenceNode(value); return {}; } }, + { "FileExtensions", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->FileExtensions = ProcessStringSequenceNode(value); return {}; } }, + { "Dependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency = &(m_p_installer->Dependencies); return ValidateAndProcessFields(value, DependenciesFieldInfos); } }, + { "Capabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Capabilities = ProcessStringSequenceNode(value); return {}; } }, + { "RestrictedCapabilities", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->RestrictedCapabilities = ProcessStringSequenceNode(value); return {}; } }, + }; + + std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); + + if (!forRootFields) + { + // Installer level only fields + std::vector v1InstallerFields = + { + { "Architecture", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as()); return {}; } }, + { "InstallerUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Url = value.as(); return {}; } }, + { "InstallerSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, + { "SignatureSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, + }; + + std::move(v1InstallerFields.begin(), v1InstallerFields.end(), std::inserter(result, result.end())); + } + } + } + + return result; + } + + std::vector ManifestYamlPopulator::GetSwitchesFieldProcessInfo(const ManifestVer& manifestVersion) + { + // Common fields across versions + std::vector result = + { + { "Custom", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Custom] = value.as(); return{}; } }, + { "Silent", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Silent] = value.as(); return{}; } }, + { "SilentWithProgress", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::SilentWithProgress] = value.as(); return{}; } }, + { "Interactive", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Interactive] = value.as(); return{}; } }, + { "Log", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Log] = value.as(); return{}; } }, + { "InstallLocation", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::InstallLocation] = value.as(); return{}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + // Language only exists in preview manifests. Though we don't use it in our code yet, keep it here to be consistent with schema. + result.emplace_back("Language", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Language] = value.as(); return{}; }); + result.emplace_back("Update", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Update] = value.as(); return{}; }); + } + else if (manifestVersion.Major() == 1) + { + result.emplace_back("Upgrade", [this](const YAML::Node& value)->ValidationErrors { (*m_p_switches)[InstallerSwitchType::Update] = value.as(); return{}; }); + } + + return result; + } + + std::vector ManifestYamlPopulator::GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields) + { + // Common fields across versions + std::vector result = + { + { "Description", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "LicenseUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + }; + + // Additional version specific fields + if (manifestVersion.Major() == 0) + { + // Root level and Localization node level + result.emplace_back("Homepage", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; }); + + if (!forRootFields) + { + // Localization node only + result.emplace_back("Language", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Locale = value.as(); return {}; }); + } + else + { + // Root node only + std::vector rootOnlyFields = + { + { "Name", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(Utility::Trim(value.as())); return {}; } }, + { "Publisher", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "Author", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "License", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "Tags", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(SplitMultiValueField(value.as())); return {}; } }, + }; + + std::move(rootOnlyFields.begin(), rootOnlyFields.end(), std::inserter(result, result.end())); + } + } + else if (manifestVersion.Major() == 1) + { + // Starting v1, we should be only adding new fields for each minor version increase + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + // Root level and Localization node level + std::vector v1CommonFields = + { + { "PackageLocale", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Locale = value.as(); return {}; } }, + { "Publisher", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "PublisherUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "PublisherSupportUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "PrivacyUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "Author", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "PackageName", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(Utility::Trim(value.as())); return {}; } }, + { "PackageUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "License", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "Copyright", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "CopyrightUrl", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "ShortDescription", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(value.as()); return {}; } }, + { "Tags", [this](const YAML::Node& value)->ValidationErrors { m_p_localization->Add(ProcessStringSequenceNode(value)); return {}; } }, + }; + + std::move(v1CommonFields.begin(), v1CommonFields.end(), std::inserter(result, result.end())); + } + } + + return result; + } + + std::vector ManifestYamlPopulator::GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + result = + { + { "WindowsFeatures", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsFeatures = ProcessStringSequenceNode(value); return {}; } }, + { "WindowsLibraries", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->WindowsLibraries = ProcessStringSequenceNode(value); return {}; } }, + { "PackageDependencies", [this](const YAML::Node& value)->ValidationErrors { return ProcessPackageDependenciesNode(value, m_p_dependency->PackageDependencies); } }, + { "ExternalDependencies", [this](const YAML::Node& value)->ValidationErrors { m_p_dependency->ExternalDependencies = ProcessStringSequenceNode(value); return {}; } }, + }; + } + + return result; + } + + std::vector ManifestYamlPopulator::GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion) + { + std::vector result = {}; + + if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) + { + result = + { + { "PackageIdentifier", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->Id = Utility::Trim(value.as()); return {}; } }, + { "MinimumVersion", [this](const YAML::Node& value)->ValidationErrors { m_p_packageDependency->MinVersion = Utility::Trim(value.as()); return {}; } }, + }; + } + + return result; + } + + ValidationErrors ManifestYamlPopulator::ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector& fieldInfos) + { + ValidationErrors resultErrors; + + if (!rootNode.IsMap() || rootNode.size() == 0) + { + resultErrors.emplace_back(ManifestError::InvalidRootNode, "", "", m_isMergedManifest ? 0 : rootNode.Mark().line, m_isMergedManifest ? 0 : rootNode.Mark().column); + return resultErrors; + } + + // Keeps track of already processed fields. Used to check duplicate fields. + std::set processedFields; + + for (auto const& keyValuePair : rootNode.Mapping()) + { + std::string key = keyValuePair.first.as(); + const YAML::Node& valueNode = keyValuePair.second; + + // We'll do case insensitive search first and validate correct case later. + auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), + [&](auto const& s) + { + return Utility::CaseInsensitiveEquals(s.Name, key); + }); + + if (fieldIter != fieldInfos.end()) + { + const FieldProcessInfo& fieldInfo = *fieldIter; + + // Make sure the found key is in Pascal Case + if (key != fieldInfo.Name) + { + resultErrors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column); + } + + // Make sure it's not a duplicate key + if (!processedFields.insert(fieldInfo.Name).second) + { + resultErrors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column); + } + + if (!valueNode.IsNull()) + { + try + { + auto errors = fieldInfo.ProcessFunc(valueNode); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } + catch (const std::exception&) + { + resultErrors.emplace_back(ManifestError::FieldFailedToProcess, fieldInfo.Name); + } + } + } + else + { + // For full validation, also reports unrecognized fields as warning + if (m_fullValidation) + { + resultErrors.emplace_back(ManifestError::FieldUnknown, key, "", m_isMergedManifest ? 0 : keyValuePair.first.Mark().line, m_isMergedManifest ? 0 : keyValuePair.first.Mark().column, ValidationError::Level::Warning); + } + } + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector& packageDependencies) + { + ValidationErrors resultErrors; + + packageDependencies.clear(); + for (auto const& entry : rootNode.Sequence()) + { + PackageDependency packageDependency; + m_p_packageDependency = &packageDependency; + auto errors = ValidateAndProcessFields(entry, PackageDependenciesFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + packageDependencies.emplace_back(std::move(std::move(packageDependency))); + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) + { + m_fullValidation = fullValidation; + m_isMergedManifest = !rootNode["ManifestType"sv].IsNull() && rootNode["ManifestType"sv].as() == "merged"; + + ValidationErrors resultErrors; + manifest.ManifestVersion = manifestVersion; + + // Prepare field infos + RootFieldInfos = GetRootFieldProcessInfo(manifestVersion); + InstallerFieldInfos = GetInstallerFieldProcessInfo(manifestVersion); + SwitchesFieldInfos = GetSwitchesFieldProcessInfo(manifestVersion); + DependenciesFieldInfos = GetDependenciesFieldProcessInfo(manifestVersion); + PackageDependenciesFieldInfos = GetPackageDependenciesFieldProcessInfo(manifestVersion); + LocalizationFieldInfos = GetLocalizationFieldProcessInfo(manifestVersion); + + // Populate root + m_p_manifest = &manifest; + m_p_installer = &(manifest.DefaultInstallerInfo); + m_p_localization = &(manifest.DefaultLocalization); + resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos); + + if (!m_p_installersNode) + { + return resultErrors; + } + + // Populate installers + for (auto const& entry : m_p_installersNode->Sequence()) + { + ManifestInstaller installer = manifest.DefaultInstallerInfo; + + // Clear these defaults as PackageFamilyName and ProductCode needs to be copied based on InstallerType + installer.PackageFamilyName.clear(); + installer.ProductCode.clear(); + + m_p_installer = &installer; + auto errors = ValidateAndProcessFields(entry, InstallerFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + + // Copy in system reference strings from the root if not set in the installer and appropriate + if (installer.PackageFamilyName.empty() && DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + { + installer.PackageFamilyName = manifest.DefaultInstallerInfo.PackageFamilyName; + } + + if (installer.ProductCode.empty() && DoesInstallerTypeUseProductCode(installer.InstallerType)) + { + installer.ProductCode = manifest.DefaultInstallerInfo.ProductCode; + } + + // Populate installer default switches if not exists + auto defaultSwitches = GetDefaultKnownSwitches(installer.InstallerType); + for (auto const& defaultSwitch : defaultSwitches) + { + if (installer.Switches.find(defaultSwitch.first) == installer.Switches.end()) + { + installer.Switches[defaultSwitch.first] = defaultSwitch.second; + } + } + + manifest.Installers.emplace_back(std::move(installer)); + } + + // Populate additional localizations + if (m_p_localizationsNode && m_p_localizationsNode->IsSequence()) + { + for (auto const& entry : m_p_localizationsNode->Sequence()) + { + ManifestLocalization localization; + m_p_localization = &localization; + auto errors = ValidateAndProcessFields(entry, LocalizationFieldInfos); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + manifest.Localizations.emplace_back(std::move(std::move(localization))); + } + } + + return resultErrors; + } + + ValidationErrors ManifestYamlPopulator::PopulateManifest(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation) + { + ManifestYamlPopulator manifestPopulator; + return manifestPopulator.PopulateManifestInternal(rootNode, manifest, manifestVersion, fullValidation); + } +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp index 47925ea534..1b75ab9467 100644 --- a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp +++ b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -3,448 +3,549 @@ #include "pch.h" #include "AppInstallerSHA256.h" #include "winget/Yaml.h" +#include "winget/ManifestSchemaValidation.h" +#include "winget/ManifestYamlPopulator.h" #include "winget/ManifestYamlParser.h" -namespace AppInstaller::Manifest +namespace AppInstaller::Manifest::YamlParser { namespace { - // The maximum supported major version known about by this code. - constexpr uint64_t s_MaxSupportedMajorVersion = 0; + // Basic V1 manifest required fields check for later manifest consistency check + void ValidateV1ManifestInput(const YamlManifestInfo& entry) + { + std::vector errors; - // The default manifest version assigned to manifests without a ManifestVersion field. - constexpr std::string_view s_DefaultManifestVersion = "0.1.0"sv; + if (!entry.Root.IsMap()) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root. File: %S", entry.FileName.c_str()); + } - // The manifest extension for the MS Store - constexpr std::string_view s_MSStoreExtension = "msstore"sv; + if (!entry.Root["PackageIdentifier"]) + { + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::RequiredFieldMissing, "PackageIdentifier", entry.FileName)); + } - std::vector SplitMultiValueField(const std::string& input) - { - if (input.empty()) + if (!entry.Root["PackageVersion"]) { - return {}; + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::RequiredFieldMissing, "PackageVersion", entry.FileName)); } - std::vector result; - size_t currentPos = 0; - while (currentPos < input.size()) + if (!entry.Root["ManifestVersion"]) { - size_t splitPos = input.find(',', currentPos); - if (splitPos == std::string::npos) - { - splitPos = input.size(); - } + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::RequiredFieldMissing, "ManifestVersion", entry.FileName)); + } - std::string splitVal = input.substr(currentPos, splitPos - currentPos); - Utility::Trim(splitVal); - if (!splitVal.empty()) + if (!entry.Root["ManifestType"]) + { + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "ManifestType", entry.FileName)); + } + else + { + auto manifestType = ConvertToManifestTypeEnum(entry.Root["ManifestType"].as()); + + switch (manifestType) { - result.emplace_back(std::move(splitVal)); + case ManifestTypeEnum::Version: + if (!entry.Root["DefaultLocale"]) + { + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::RequiredFieldMissing, "DefaultLocale", entry.FileName)); + } + break; + case ManifestTypeEnum::Singleton: + case ManifestTypeEnum::Locale: + case ManifestTypeEnum::DefaultLocale: + if (!entry.Root["PackageLocale"]) + { + errors.emplace_back(ValidationError::MessageFieldWithFile( + ManifestError::RequiredFieldMissing, "PackageLocale", entry.FileName)); + } + break; } - currentPos = splitPos + 1; } - return result; + if (!errors.empty()) + { + ManifestException ex{ std::move(errors) }; + THROW_EXCEPTION(ex); + } } - } - void YamlParser::PrepareManifestFieldInfos(const ManifestVer& manifestVer) - { - // Initially supported fields - RootFieldInfos = - { - { "ManifestVersion", [](const YAML::Node&) { /* ManifestVersion already processed */ }, false, - // Regex here is to prevent leading 0s in the version, this also keeps consistent with other versions in the manifest - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){2}$" }, - { "Id", [this](const YAML::Node& value) { m_p_manifest->Id = value.as(); Utility::Trim(m_p_manifest->Id); }, true, "^[\\S]+\\.[\\S]+$" }, - { "Name", [this](const YAML::Node& value) { m_p_manifest->Name = value.as(); Utility::Trim(m_p_manifest->Name); }, true }, - { "Version", [this](const YAML::Node& value) { m_p_manifest->Version = value.as(); Utility::Trim(m_p_manifest->Version); }, true, - /* File name chars not allowed */ "^[^\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]+$" }, - { "Publisher", [this](const YAML::Node& value) { m_p_manifest->Publisher = value.as(); }, true }, - { "AppMoniker", [this](const YAML::Node& value) { m_p_manifest->AppMoniker = value.as(); Utility::Trim(m_p_manifest->AppMoniker); } }, - { "Channel", [this](const YAML::Node& value) { m_p_manifest->Channel = value.as(); Utility::Trim(m_p_manifest->Channel); } }, - { "Author", [this](const YAML::Node& value) { m_p_manifest->Author = value.as(); } }, - { "License", [this](const YAML::Node& value) { m_p_manifest->License = value.as(); } }, - { "MinOSVersion", [this](const YAML::Node& value) { m_p_manifest->MinOSVersion = value.as(); Utility::Trim(m_p_manifest->MinOSVersion); }, false, - "^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){0,3}$" }, - { "Tags", [this](const YAML::Node& value) { m_p_manifest->Tags = SplitMultiValueField(value.as()); } }, - { "Commands", [this](const YAML::Node& value) { m_p_manifest->Commands = SplitMultiValueField(value.as()); } }, - { "Protocols", [this](const YAML::Node& value) { m_p_manifest->Protocols = SplitMultiValueField(value.as()); } }, - { "FileExtensions", [this](const YAML::Node& value) { m_p_manifest->FileExtensions = SplitMultiValueField(value.as()); } }, - { "InstallerType", [this](const YAML::Node& value) { m_p_manifest->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as()); } }, - { "UpdateBehavior", [this](const YAML::Node& value) { m_p_manifest->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as()); } }, - { "PackageFamilyName", [this](const YAML::Node& value) { m_p_manifest->PackageFamilyName = value.as(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, - { "ProductCode", [this](const YAML::Node& value) { m_p_manifest->ProductCode = value.as(); } }, - { "Description", [this](const YAML::Node& value) { m_p_manifest->Description = value.as(); } }, - { "Homepage", [this](const YAML::Node& value) { m_p_manifest->Homepage = value.as(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { m_p_manifest->LicenseUrl = value.as(); } }, - { "Switches", [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, - { "Installers", [this](const YAML::Node& value) { *m_p_installersNode = value; }, true }, - { "Localization", [this](const YAML::Node& value) { *m_p_localizationsNode = value; } }, - }; - - InstallerFieldInfos = - { - { "Arch", [this](const YAML::Node& value) { m_p_installer->Arch = Utility::ConvertToArchitectureEnum(value.as()); }, true }, - { "Url", [this](const YAML::Node& value) { m_p_installer->Url = value.as(); } }, - { "Sha256", [this](const YAML::Node& value) { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "SignatureSha256", [this](const YAML::Node& value) { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as()); }, false, "^[A-Fa-f0-9]{64}$" }, - { "Language", [this](const YAML::Node& value) { m_p_installer->Language = value.as(); } }, - { "Scope", [this](const YAML::Node& value) { m_p_installer->Scope = ManifestInstaller::ConvertToScopeEnum(value.as()); } }, - { "InstallerType", [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as()); } }, - { "UpdateBehavior", [this](const YAML::Node& value) { m_p_installer->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as()); } }, - { "PackageFamilyName", [this](const YAML::Node& value) { m_p_installer->PackageFamilyName = value.as(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, - { "ProductCode", [this](const YAML::Node& value) { m_p_installer->ProductCode = value.as(); } }, - { "Switches", [this](const YAML::Node& value) { *m_p_switchesNode = value; } }, - }; - - SwitchesFieldInfos = + // Input validations: + // - Determine manifest version + // - Check multi file manifest input integrity + // - All manifests use same PackageIdentifier, PackageVersion, ManifestVersion + // - All required types exist and exist only once. i.e. version, installer, defaultLocale + // - No duplicate locales across manifests + // - DefaultLocale matches in version manifest and defaultLocale manifest + // - Validate manifest type correctness + // - Allowed file type in multi file manifest: version, installer, defaultLocale, locale + // - Allowed file type in single file manifest: preview manifest, merged and singleton + ManifestVer ValidateInput(std::vector& input, bool fullValidation, bool schemaValidationOnly) { - { "Custom", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Custom] = value.as(); } }, - { "Silent", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Silent] = value.as(); } }, - { "SilentWithProgress", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::SilentWithProgress] = value.as(); } }, - { "Interactive", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Interactive] = value.as(); } }, - { "Language", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Language] = value.as(); } }, - { "Log", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Log] = value.as(); } }, - { "InstallLocation", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::InstallLocation] = value.as(); } }, - { "Update", [this](const YAML::Node& value) { (*m_p_switches)[ManifestInstaller::InstallerSwitchType::Update] = value.as(); } }, - }; - - LocalizationFieldInfos = - { - { "Language", [this](const YAML::Node& value) { m_p_localization->Language = value.as(); }, true }, - { "Description", [this](const YAML::Node& value) { m_p_localization->Description = value.as(); } }, - { "Homepage", [this](const YAML::Node& value) { m_p_localization->Homepage = value.as(); } }, - { "LicenseUrl", [this](const YAML::Node& value) { m_p_localization->LicenseUrl = value.as(); } }, - }; - - // Store extension - if (manifestVer.HasExtension(s_MSStoreExtension)) - { - InstallerFieldInfos.emplace_back("ProductId", [this](const YAML::Node& value) { m_p_installer->ProductId = value.as(); }); - } - } + std::vector errors; - Manifest YamlParser::CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation, bool throwOnWarning) - { - Manifest manifest; - std::vector errors; + std::string manifestVersionStr; + ManifestVer manifestVersion; + ManifestVer ManifestVersionV1{ s_ManifestVersionV1 }; + bool isMultifileManifest = input.size() > 1; - try - { - YAML::Node rootNode = YAML::Load(inputFile); - YamlParser parser; - errors = parser.ParseManifest(rootNode, manifest, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); - } + // Use the first manifest doc to determine ManifestVersion, there'll be checks for manifest version consistency later + auto& firstYamlManifest = input[0]; + if (!firstYamlManifest.Root.IsMap()) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root. File: %S", firstYamlManifest.FileName.c_str()); + } - if (!errors.empty()) - { - ManifestException ex{ std::move(errors) }; + if (firstYamlManifest.Root["ManifestVersion"sv]) + { + manifestVersionStr = firstYamlManifest.Root["ManifestVersion"sv].as(); + } + else + { + manifestVersionStr = s_DefaultManifestVersion; + } + manifestVersion = ManifestVer{ manifestVersionStr }; - if (throwOnWarning || !ex.IsWarningOnly()) + // Check max supported version + if (manifestVersion.Major() > s_MaxSupportedMajorVersion) { - THROW_EXCEPTION(ex); + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifestVersion.ToString().c_str()); } - } - return manifest; - } + // Preview manifest validations + if (manifestVersion < ManifestVersionV1) + { + // multi file manifest is only supported starting ManifestVersion 1.0.0 + if (isMultifileManifest) + { + THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "Preview manifest does not support multi file manifest format."); + } - Manifest YamlParser::Create(const std::string& input, bool fullValidation, bool throwOnWarning) - { - Manifest manifest; - std::vector errors; + firstYamlManifest.ManifestType = ManifestTypeEnum::Preview; + } + // V1 manifest validations + else + { + // Check required fields used by later consistency check for better error message instead of + // Field Type Not Match error. + for (auto const& entry : input) + { + ValidateV1ManifestInput(entry); + } - try - { - YAML::Node rootNode = YAML::Load(input); - YamlParser parser; - errors = parser.ParseManifest(rootNode, manifest, fullValidation); - } - catch (const ManifestException&) - { - // Prevent ManifestException from being wrapped in another ManifestException - throw; - } - catch (const std::exception& e) - { - THROW_EXCEPTION_MSG(ManifestException(), e.what()); - } + if (isMultifileManifest) + { + // Populates the PackageIdentifier and PackageVersion from first doc for later consistency check + std::string packageId = firstYamlManifest.Root["PackageIdentifier"].as(); + std::string packageVersion = firstYamlManifest.Root["PackageVersion"].as(); - if (!errors.empty()) - { - ManifestException ex{ std::move(errors) }; + std::set localesSet; - if (throwOnWarning || !ex.IsWarningOnly()) + bool isVersionManifestFound = false; + bool isInstallerManifestFound = false; + bool isDefaultLocaleManifestFound = false; + std::string defaultLocaleFromVersionManifest; + std::string defaultLocaleFromDefaultLocaleManifest; + + for (auto& entry : input) + { + std::string localPackageId = entry.Root["PackageIdentifier"].as(); + if (localPackageId != packageId) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "PackageIdentifier", localPackageId, entry.FileName)); + } + + std::string localPackageVersion = entry.Root["PackageVersion"].as(); + if (localPackageVersion != packageVersion) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "PackageVersion", localPackageVersion, entry.FileName)); + } + + std::string localManifestVersion = entry.Root["ManifestVersion"].as(); + if (localManifestVersion != manifestVersionStr) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::InconsistentMultiFileManifestFieldValue, "ManifestVersion", localManifestVersion, entry.FileName)); + } + + std::string manifestTypeStr = entry.Root["ManifestType"sv].as(); + ManifestTypeEnum manifestType = ConvertToManifestTypeEnum(manifestTypeStr); + entry.ManifestType = manifestType; + + switch (manifestType) + { + case ManifestTypeEnum::Version: + if (isVersionManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isVersionManifestFound = true; + defaultLocaleFromVersionManifest = entry.Root["DefaultLocale"sv].as(); + } + break; + case ManifestTypeEnum::Installer: + if (isInstallerManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isInstallerManifestFound = true; + } + break; + case ManifestTypeEnum::DefaultLocale: + if (isDefaultLocaleManifestFound) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + else + { + isDefaultLocaleManifestFound = true; + auto packageLocale = entry.Root["PackageLocale"sv].as(); + defaultLocaleFromDefaultLocaleManifest = packageLocale; + + if (localesSet.find(packageLocale) != localesSet.end()) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestLocale, "PackageLocale", packageLocale, entry.FileName)); + } + else + { + localesSet.insert(packageLocale); + } + } + break; + case ManifestTypeEnum::Locale: + { + auto packageLocale = entry.Root["PackageLocale"sv].as(); + if (localesSet.find(packageLocale) != localesSet.end()) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::DuplicateMultiFileManifestLocale, "PackageLocale", packageLocale, entry.FileName)); + } + else + { + localesSet.insert(packageLocale); + } + } + break; + default: + errors.emplace_back(ValidationError::MessageFieldValueWithFile( + ManifestError::UnsupportedMultiFileManifestType, "ManifestType", manifestTypeStr, entry.FileName)); + } + } + + if (isVersionManifestFound && isDefaultLocaleManifestFound && defaultLocaleFromDefaultLocaleManifest != defaultLocaleFromVersionManifest) + { + errors.emplace_back(ManifestError::InconsistentMultiFileManifestDefaultLocale); + } + + if (!schemaValidationOnly && !(isVersionManifestFound && isInstallerManifestFound && isDefaultLocaleManifestFound)) + { + errors.emplace_back(ManifestError::IncompleteMultiFileManifest); + } + } + else + { + std::string manifestTypeStr = firstYamlManifest.Root["ManifestType"sv].as(); + ManifestTypeEnum manifestType = ConvertToManifestTypeEnum(manifestTypeStr); + firstYamlManifest.ManifestType = manifestType; + + if (fullValidation && manifestType == ManifestTypeEnum::Merged) + { + errors.emplace_back(ValidationError::MessageFieldValueWithFile(ManifestError::FieldValueNotSupported, "ManifestType", manifestTypeStr, firstYamlManifest.FileName)); + } + + if (!schemaValidationOnly && manifestType != ManifestTypeEnum::Merged && manifestType != ManifestTypeEnum::Singleton) + { + errors.emplace_back(ValidationError::MessageWithFile(ManifestError::IncompleteMultiFileManifest, firstYamlManifest.FileName)); + } + } + } + + if (!errors.empty()) { + ManifestException ex{ std::move(errors) }; THROW_EXCEPTION(ex); } - } - return manifest; - } - - std::vector YamlParser::ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation) - { - // Detects empty files with a better error. - if (!rootNode.IsMap()) - { - THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST), "The manifest does not contain a valid root."); + return manifestVersion; } - // Detect manifest version first to determine expected fields - // Use index to access ManifestVersion directly. If there're duplicates or other general errors, it'll be detected in later - // processing of iterating the whole manifest. - if (rootNode["ManifestVersion"sv]) - { - auto manifestVersionValue = rootNode["ManifestVersion"sv].as(); - manifest.ManifestVersion = ManifestVer(manifestVersionValue); - } - else + // Find a unique required manifest from the input in multi manifest case + const YAML::Node& FindUniqueRequiredDocFromMultiFileManifest(const std::vector& input, ManifestTypeEnum manifestType) { - manifest.ManifestVersion = ManifestVer(s_DefaultManifestVersion); - } + auto iter = std::find_if(input.begin(), input.end(), + [=](auto const& s) + { + return s.ManifestType == manifestType; + }); - // Check manifest version is supported - if (manifest.ManifestVersion.Major() > s_MaxSupportedMajorVersion) - { - THROW_EXCEPTION_MSG(ManifestException(APPINSTALLER_CLI_ERROR_UNSUPPORTED_MANIFESTVERSION), "Unsupported ManifestVersion: %S", manifest.ManifestVersion.ToString().c_str()); + THROW_HR_IF(E_UNEXPECTED, iter == input.end()); + + return iter->Root; } - PrepareManifestFieldInfos(manifest.ManifestVersion); + // Merge one manifest file to the final merged manifest, basically copying the mapping but excluding certain common fields + void MergeOneManifestToMultiFileManifest(const YAML::Node& input, YAML::Node& destination) + { + THROW_HR_IF(E_UNEXPECTED, !input.IsMap()); + THROW_HR_IF(E_UNEXPECTED, !destination.IsMap()); - // Populate root fields - YAML::Node switchesNode; - YAML::Node installersNode; - YAML::Node localizationsNode; - m_p_switchesNode = &switchesNode; - m_p_installersNode = &installersNode; - m_p_localizationsNode = &localizationsNode; - m_p_manifest = &manifest; - auto resultErrors = ValidateAndProcessFields(rootNode, RootFieldInfos, fullValidation); + const std::vector FieldsToIgnore = { "PackageIdentifier", "PackageVersion", "ManifestType", "ManifestVersion" }; - // Populate root switches - if (!switchesNode.IsNull()) - { - m_p_switches = &manifest.Switches; - auto errors = ValidateAndProcessFields(switchesNode, SwitchesFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + for (auto const& keyValuePair : input.Mapping()) + { + // We only support string type as key in our manifest + if (std::find(FieldsToIgnore.begin(), FieldsToIgnore.end(), keyValuePair.first.as()) == FieldsToIgnore.end()) + { + YAML::Node key = keyValuePair.first; + YAML::Node value = keyValuePair.second; + destination.AddMappingNode(std::move(key), std::move(value)); + } + } } - // Populate installers - for (std::size_t i = 0; i < installersNode.size(); i++) + YAML::Node MergeMultiFileManifest(const std::vector& input) { - YAML::Node installerNode = installersNode[i]; - ManifestInstaller installer; - YAML::Node installerSwitchesNode; - - // Populate defaults - installer.InstallerType = manifest.InstallerType; - installer.UpdateBehavior = manifest.UpdateBehavior; - installer.Scope = ManifestInstaller::ScopeEnum::User; - - m_p_installer = &installer; - m_p_switchesNode = &installerSwitchesNode; - auto errors = ValidateAndProcessFields(installerNode, InstallerFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + // Starts with installer manifest + YAML::Node result = FindUniqueRequiredDocFromMultiFileManifest(input, ManifestTypeEnum::Installer); + + // Copy default locale manifest content into manifest root + YAML::Node defaultLocaleManifest = FindUniqueRequiredDocFromMultiFileManifest(input, ManifestTypeEnum::DefaultLocale); + MergeOneManifestToMultiFileManifest(defaultLocaleManifest, result); - // Copy in system reference strings from the root if not set in the installer and appropriate - if (installer.PackageFamilyName.empty() && ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(installer.InstallerType)) + // Copy additional locale manifests + YAML::Node localizations{ YAML::Node::Type::Sequence, "", YAML::Mark() }; + for (const auto& entry : input) { - installer.PackageFamilyName = manifest.PackageFamilyName; + if (entry.ManifestType == ManifestTypeEnum::Locale) + { + YAML::Node localization{ YAML::Node::Type::Mapping, "", YAML::Mark() }; + MergeOneManifestToMultiFileManifest(entry.Root, localization); + localizations.AddSequenceNode(std::move(localization)); + } } - if (installer.ProductCode.empty() && ManifestInstaller::DoesInstallerTypeUseProductCode(installer.InstallerType)) + if (localizations.size() > 0) { - installer.ProductCode = manifest.ProductCode; + YAML::Node key{ YAML::Node::Type::Scalar, "", YAML::Mark() }; + key.SetScalar("Localization"); + result.AddMappingNode(std::move(key), std::move(localizations)); } - // Populate default known switches - installer.Switches = GetDefaultKnownSwitches(installer.InstallerType); + result["ManifestType"sv].SetScalar("merged"); + + return result; + } - // Override with switches from manifest root if applicable - for (auto const& keyValuePair : manifest.Switches) + void EmitYamlNode(const YAML::Node& input, YAML::Emitter& emitter) + { + if (input.IsMap()) + { + emitter << YAML::BeginMap; + for (auto const& keyValuePair : input.Mapping()) + { + emitter << YAML::Key; + EmitYamlNode(keyValuePair.first, emitter); + emitter << YAML::Value; + EmitYamlNode(keyValuePair.second, emitter); + } + emitter << YAML::EndMap; + } + else if (input.IsSequence()) { - installer.Switches[keyValuePair.first] = keyValuePair.second; + emitter << YAML::BeginSeq; + for (auto const& value : input.Sequence()) + { + EmitYamlNode(value, emitter); + } + emitter << YAML::EndSeq; } - - // Override with switches from installer declaration if applicable - if (!installerSwitchesNode.IsNull()) + else if (input.IsScalar()) { - m_p_switches = &installer.Switches; - auto switchesErrors = ValidateAndProcessFields(installerSwitchesNode, SwitchesFieldInfos, fullValidation); - std::move(switchesErrors.begin(), switchesErrors.end(), std::inserter(resultErrors, resultErrors.end())); + emitter << input.as(); + } + else if (input.IsNull()) + { + emitter << ""; + } + else + { + THROW_HR(E_UNEXPECTED); } - - manifest.Installers.emplace_back(std::move(installer)); } - // Populate localization fields - if (!localizationsNode.IsNull()) + void OutputYamlDoc(const YAML::Node& input, const std::filesystem::path& out) { - for (std::size_t i = 0; i < localizationsNode.size(); i++) - { - YAML::Node localizationNode = localizationsNode[i]; - ManifestLocalization localization; + THROW_HR_IF(E_UNEXPECTED, !input.IsMap()); - // Populates default values from root first - localization.Description = manifest.Description; - localization.Homepage = manifest.Homepage; - localization.LicenseUrl = manifest.LicenseUrl; + YAML::Emitter emitter; + EmitYamlNode(input, emitter); - m_p_localization = &localization; - auto errors = ValidateAndProcessFields(localizationNode, LocalizationFieldInfos, fullValidation); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - manifest.Localization.emplace_back(std::move(localization)); - } + std::filesystem::create_directories(out.parent_path()); + std::ofstream outFileStream(out); + emitter.Emit(outFileStream); + outFileStream.close(); } - // Extra semantic validations after basic validation and field population - if (fullValidation) + std::vector ParseManifestImpl( + std::vector& input, + Manifest& manifest, + bool fullValidation, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) { - auto errors = ValidateManifest(manifest); - std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - } + THROW_HR_IF_MSG(E_INVALIDARG, input.size() == 0, "No manifest file found"); + THROW_HR_IF_MSG(E_INVALIDARG, schemaValidationOnly && !mergedManifestPath.empty(), "Manifest cannot be merged if only schema validation is performed"); + THROW_HR_IF_MSG(E_INVALIDARG, input.size() == 1 && !mergedManifestPath.empty(), "Manifest cannot be merged from a single manifest"); - return resultErrors; - } + auto manifestVersion = ValidateInput(input, fullValidation, schemaValidationOnly); - std::vector YamlParser::ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector& fieldInfos, - bool fullValidation) - { - std::vector errors; + std::vector resultErrors; - if (rootNode.size() == 0 || !rootNode.IsMap()) - { - errors.emplace_back(ManifestError::InvalidRootNode, "", "", rootNode.Mark().line, rootNode.Mark().column); - return errors; - } + if (fullValidation || schemaValidationOnly) + { + resultErrors = ValidateAgainstSchema(input, manifestVersion); + } - // Keeps track of already processed fields. Used to check duplicate fields or missing required fields. - std::set processedFields; + if (schemaValidationOnly) + { + return resultErrors; + } - for (auto const& keyValuePair : rootNode.Mapping()) - { - std::string key = keyValuePair.first.as(); - const YAML::Node& valueNode = keyValuePair.second; + // Merge manifests in multi file manifest case + const YAML::Node& manifestDoc = (input.size() > 1) ? MergeMultiFileManifest(input) : input[0].Root; - // We'll do case insensitive search first and validate correct case later. - auto fieldIter = std::find_if(fieldInfos.begin(), fieldInfos.end(), - [&](auto const& s) - { - return Utility::CaseInsensitiveEquals(s.Name, key); - }); + auto errors = ManifestYamlPopulator::PopulateManifest(manifestDoc, manifest, manifestVersion, fullValidation); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); - if (fieldIter != fieldInfos.end()) + // Extra semantic validations after basic validation and field population + if (fullValidation) { - const ManifestFieldInfo& fieldInfo = *fieldIter; + errors = ValidateManifest(manifest); + std::move(errors.begin(), errors.end(), std::inserter(resultErrors, resultErrors.end())); + } - // Make sure the found key is in Pascal Case - if (key != fieldInfo.Name) - { - errors.emplace_back(ManifestError::FieldIsNotPascalCase, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + // Output merged manifest if requested + if (!mergedManifestPath.empty()) + { + OutputYamlDoc(manifestDoc, mergedManifestPath); + } - // Make sure it's not a duplicate key - if (!processedFields.insert(fieldInfo.Name).second) - { - errors.emplace_back(ManifestError::FieldDuplicate, fieldInfo.Name, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column); - } + return resultErrors; + } + } - // Validate non empty value is provided for required fields - if (fieldInfo.Required) - { - if (!valueNode.IsDefined() || valueNode.IsNull() || // Should be defined and not null - (valueNode.IsScalar() && valueNode.as().empty()) || // Scalar type should have content - ((valueNode.IsMap() || valueNode.IsSequence()) && valueNode.size() == 0)) // Map or sequence type should have size greater than 0 - { - errors.emplace_back(ManifestError::RequiredFieldEmpty, fieldInfo.Name, "", valueNode.Mark().line, valueNode.Mark().column); - } - } + Manifest CreateFromPath( + const std::filesystem::path& inputPath, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) + { + std::vector docList; - // Validate value against regex if applicable - if (fullValidation && !fieldInfo.RegEx.empty()) + try + { + if (std::filesystem::is_directory(inputPath)) + { + for (const auto& file : std::filesystem::directory_iterator(inputPath)) { - std::string value = valueNode.as(); - std::regex pattern{ fieldInfo.RegEx }; - if (!std::regex_match(value, pattern)) - { - errors.emplace_back(ManifestError::InvalidFieldValue, fieldInfo.Name, value, valueNode.Mark().line, valueNode.Mark().column); - continue; - } - } + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED), std::filesystem::is_directory(file.path()), "Subdirectory not supported in manifest path"); - if (!valueNode.IsNull()) - { - fieldInfo.ProcessFunc(valueNode); + YamlManifestInfo doc; + doc.Root = YAML::Load(file.path()); + doc.FileName = file.path().filename().u8string(); + docList.emplace_back(std::move(doc)); } } else { - // For full validation, also reports unrecognized fields as warning - if (fullValidation) - { - errors.emplace_back(ManifestError::FieldUnknown, key, "", keyValuePair.first.Mark().line, keyValuePair.first.Mark().column, ValidationError::Level::Warning); - } + YamlManifestInfo doc; + doc.Root = YAML::Load(inputPath); + doc.FileName = inputPath.filename().u8string(); + docList.emplace_back(std::move(doc)); } } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + return ParseManifest(docList, fullValidation, throwOnWarning, mergedManifestPath, schemaValidationOnly); + } + + Manifest Create( + const std::string& input, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) + { + std::vector docList; - // Make sure required fields are provided - for (auto const& fieldInfo : fieldInfos) + try { - if (fieldInfo.Required && processedFields.find(fieldInfo.Name) == processedFields.end()) - { - errors.emplace_back(ManifestError::RequiredFieldMissing, fieldInfo.Name); - } + YamlManifestInfo doc; + doc.Root = YAML::Load(input); + docList.emplace_back(std::move(doc)); + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); } - return errors; + return ParseManifest(docList, fullValidation, throwOnWarning, mergedManifestPath, schemaValidationOnly); } - std::map YamlParser::GetDefaultKnownSwitches( - ManifestInstaller::InstallerTypeEnum installerType) + Manifest ParseManifest( + std::vector& input, + bool fullValidation, + bool throwOnWarning, + const std::filesystem::path& mergedManifestPath, + bool schemaValidationOnly) { - switch (installerType) + Manifest manifest; + std::vector errors; + + try { - case ManifestInstaller::InstallerTypeEnum::Burn: - case ManifestInstaller::InstallerTypeEnum::Wix: - case ManifestInstaller::InstallerTypeEnum::Msi: - return - { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/quiet")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/passive")}, - {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/log \"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("TARGETDIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::Update, ManifestInstaller::string_t("REINSTALL=ALL REINSTALLMODE=vamus")} - }; - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - return - { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/S")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/S")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/D=" + std::string(ARG_TOKEN_INSTALLPATH))} - }; - case ManifestInstaller::InstallerTypeEnum::Inno: - return + errors = ParseManifestImpl(input, manifest, fullValidation, mergedManifestPath, schemaValidationOnly); + } + catch (const ManifestException&) + { + // Prevent ManifestException from being wrapped in another ManifestException + throw; + } + catch (const std::exception& e) + { + THROW_EXCEPTION_MSG(ManifestException(), e.what()); + } + + if (!errors.empty()) + { + ManifestException ex{ std::move(errors) }; + + if (throwOnWarning || !ex.IsWarningOnly()) { - {ManifestInstaller::InstallerSwitchType::Silent, ManifestInstaller::string_t("/VERYSILENT")}, - {ManifestInstaller::InstallerSwitchType::SilentWithProgress, ManifestInstaller::string_t("/SILENT")}, - {ManifestInstaller::InstallerSwitchType::Log, ManifestInstaller::string_t("/LOG=\"" + std::string(ARG_TOKEN_LOGPATH) + "\"")}, - {ManifestInstaller::InstallerSwitchType::InstallLocation, ManifestInstaller::string_t("/DIR=\"" + std::string(ARG_TOKEN_INSTALLPATH) + "\"")} - }; - default: - return {}; + THROW_EXCEPTION(ex); + } } + + return manifest; } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/NameNormalization.cpp b/src/AppInstallerCommonCore/NameNormalization.cpp new file mode 100644 index 0000000000..892f87eca3 --- /dev/null +++ b/src/AppInstallerCommonCore/NameNormalization.cpp @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/NameNormalization.h" +#include "Public/AppInstallerStrings.h" +#include "Public/winget/Regex.h" + + +namespace AppInstaller::Utility +{ + namespace + { + struct InterimNameNormalizationResult + { + std::wstring Name; + Architecture Architecture; + std::wstring Locale; + }; + + struct InterimPublisherNormalizationResult + { + std::wstring Publisher; + }; + + // To maintain consistency, changes that result in different output must be done in a new version. + // This can potentially be ignored (if thought through) when the changes will only increase the + // number of matches being made, with no impact to existing matches. For instance, removing an + // arbitrary new processor architecture from names would hopefully only affect existing packages + // that were not matching properly. Fixing a bug that was causing bad strings to be produced would + // be impactful, and thus should likely result in a new iteration. + class NormalizationInitial : public details::INameNormalizer + { + static std::wstring PrepareForValidation(std::string_view value) + { + std::wstring result = Utility::Normalize(ConvertToUTF16(value)); + Trim(result); + size_t atPos = result.find(L"@@", 3); + if (atPos != std::wstring::npos) + { + result = result.substr(0, atPos); + } + return result; + } + + // If the string is wrapped with some character groups, remove them. + // Returns true if string was wrapped; false if not. + static bool Unwrap(std::wstring& value) + { + if (value.length() >= 2) + { + bool unwrap = false; + + switch (value[0]) + { + case L'"': + unwrap = value.back() == L'"'; + break; + + case L'(': + unwrap = value.back() == L')'; + break; + } + + if (unwrap) + { + value = value.substr(1, value.length() - 2); + return true; + } + } + + return false; + } + + // Removes all matches from the input string. + static bool Remove(const Regex::Expression& re, std::wstring& input) + { + std::wstring output = re.Replace(input, {}); + bool result = (output != input); + input = std::move(output); + return result; + } + + // Removes the architecture and returns the value, if any + Architecture RemoveArchitecture(std::wstring& value) const + { + Architecture result = Architecture::Unknown; + + // Must detect this first because "32/64 bit" is a superstring of "64 bit" + if (Remove(Architecture32Or64Bit, value)) + { + // If the program is 32 and 64 bit in the same installer, leave as unknown. + } + // Must detect 64 bit before 32 bit because of "x86-64" being a superstring of "x86" + else if (Remove(ArchitectureX64, value) || Remove(Architecture64Bit, value)) + { + result = Architecture::X64; + } + else if (Remove(ArchitectureX32, value) || Remove(Architecture32Bit, value)) + { + result = Architecture::X86; + } + + return result; + } + + // Removes all matches for the given regular expressions + static bool RemoveAll(const std::vector& regexes, std::wstring& value) + { + bool result = false; + + for (const auto& re : regexes) + { + result = Remove(*re, value) || result; + } + + return result; + } + + // Removes all locales and returns the common value, if any + std::wstring RemoveLocale(std::wstring& value) const + { + bool localeFound = false; + std::wstring result; + + std::wstring newValue; + auto newValueInserter = std::back_inserter(newValue); + + Locale.ForEach(value, + [&](bool isMatch, std::wstring_view text) + { + bool copy = !isMatch; + + if (isMatch) + { + std::wstring foldedText = ConvertToUTF16(FoldCase(text)); + + // Ensure that the value is in the locale list + auto bound = std::lower_bound(Locales.begin(), Locales.end(), foldedText); + + if (bound == Locales.end() || *bound != foldedText) + { + // Match was not a locale in our list, so copy it out + copy = true; + } + else if (!localeFound) + { + // First/only match, just extract the value + result = foldedText; + localeFound = true; + } + else if (!result.empty()) + { + // For some reason, there are multiple locales listed. + // See if they have anything in common. + if (result != foldedText) + { + // Not completely the same (expected), see if they are at least the same language + result.erase(result.find(L'-')); + foldedText.erase(foldedText.find(L'-')); + + if (result != foldedText) + { + // Not the same language, abandon having a locale and just clean them + result.clear(); + } + } + } + } + + if (copy) + { + std::copy(text.begin(), text.end(), newValueInserter); + } + + return true; + }); + + value = std::move(newValue); + + return result; + } + + // Splits the string based on the regex matches, excluding empty/whitespace strings + // and any values found in the exclusions. + static std::vector Split(const Regex::Expression& re, const std::wstring& value, const std::vector& exclusions, bool stopOnExclusion = false) + { + std::vector result; + + re.ForEach(value, + [&](bool, std::wstring_view text) + { + if (IsEmptyOrWhitespace(text)) + { + return true; + } + + // Do not stop for an exclusion if it is the first word found + if (!result.empty()) + { + std::wstring foldedText = ConvertToUTF16(FoldCase(text)); + + auto bound = std::lower_bound(exclusions.begin(), exclusions.end(), foldedText); + + if (bound != exclusions.end() && *bound == foldedText) + { + return !stopOnExclusion; + } + } + + result.emplace_back(std::wstring{ text }); + return true; + }); + + return result; + } + + // Joins all of the given strings into a single value + static std::wstring Join(const std::vector& values) + { + std::wstring result; + + for (const auto& v : values) + { + result += v; + } + + return result; + } + + static constexpr Regex::Options reOptions = Regex::Options::CaseInsensitive; + + // Architecture + Regex::Expression ArchitectureX32{ R"((?<=^|[^\p{L}\p{Nd}])(X32|X86)(?=\P{Nd}|$)(?:\sEDITION)?)", reOptions }; + Regex::Expression ArchitectureX64{ R"((?<=^|[^\p{L}\p{Nd}])(X64|AMD64|X86([\p{Pd}\p{Pc}]64))(?=\P{Nd}|$)(?:\sEDITION)?)", reOptions }; + Regex::Expression Architecture32Bit{ R"((?<=^|[^\p{L}\p{Nd}])(32[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; + Regex::Expression Architecture64Bit{ R"((?<=^|[^\p{L}\p{Nd}])(64[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; + Regex::Expression Architecture32Or64Bit{ R"((?<=^|[^\p{L}\p{Nd}])((64[\\\/]32|32[\\\/]64)[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; + + // Locale + Regex::Expression Locale{ R"((? ProgramNameRegexes + { + &Roblox, + &Bomgar, + &PrefixParens, + &EmptyParens, + &FilePathGHS, + &FilePathParens, + &FilePathQuotes, + &FilePath, + &VersionLetter, + &VersionDelimited, + &Version, + &EN, + &NonNestedBracket, + &BracketEnclosed, + &URIProtocol, + &LeadingSymbols, + &TrailingSymbols + }; + + const std::vector PublisherNameRegexes + { + &VersionDelimited, + &Version, + &NonNestedBracket, + &BracketEnclosed, + &URIProtocol, + &NonLetters, + &TrailingNonLetters, + &AcronymSeparators + }; + + // Add values here but use Locales in code. + const std::vector LocaleViews + { + L"AF-ZA", L"AM-ET", L"AR-AE", L"AR-BH", L"AR-DZ", L"AR-EG", L"AR-IQ", L"AR-JO", L"AR-KW", L"AR-LB", L"AR-LY", + L"AR-MA", L"ARN-CL", L"AR-OM", L"AR-QA", L"AR-SA", L"AR-SY", L"AR-TN", L"AR-YE", L"AS-IN", L"BA-RU", L"BE-BY", + L"BG-BG", L"BN-BD", L"BN-IN", L"BO-CN", L"BR-FR", L"CA-ES", L"CA-ES-VALENCIA", + L"CO-FR", L"CS-CZ", L"CY-GB", L"DA-DK", L"DE-AT", + L"DE-CH", L"DE-DE", L"DE-LI", L"DE-LU", L"DSB-DE", L"DV-MV", L"EL-GR", L"EN-AU", L"EN-BZ", L"EN-CA", L"EN-GB", + L"EN-IE", L"EN-IN", L"EN-JM", L"EN-MY", L"EN-NZ", L"EN-PH", L"EN-SG", L"EN-TT", L"EN-US", L"EN-ZA", L"EN-ZW", + L"ES-AR", L"ES-BO", L"ES-CL", L"ES-CO", L"ES-CR", L"ES-DO", L"ES-EC", L"ES-ES", L"ES-GT", L"ES-HN", L"ES-MX", + L"ES-NI", L"ES-PA", L"ES-PE", L"ES-PR", L"ES-PY", L"ES-SV", L"ES-US", L"ES-UY", L"ES-VE", L"ET-EE", L"EU-ES", + L"FA-IR", L"FI-FI", L"FIL-PH", L"FO-FO", L"FR-BE", L"FR-CA", L"FR-CH", L"FR-FR", L"FR-LU", L"FR-MC", L"FY-NL", + L"GA-IE", L"GD-DB", L"GL-ES", L"GSW-FR", L"GU-IN", L"HE-IL", L"HI-IN", L"HR-BA", L"HR-HR", L"HSB-DE", L"HU-HU", + L"HY-AM", L"ID-ID", L"IG-NG", L"II-CN", L"IS-IS", L"IT-CH", L"IT-IT", L"JA-JP", L"KA-GE", L"KK-KZ", L"KL-GL", + L"KM-KH", L"KN-IN", L"KOK-IN", L"KO-KR", L"KY-KG", L"LB-LU", L"LO-LA", L"LT-LT", L"LV-LV", L"MI-NZ", L"MK-MK", + L"ML-IN", L"MN-MN", L"MOH-CA", L"MR-IN", L"MS-BN", L"MS-MY", L"MT-MT", L"NB-NO", L"NE-NP", L"NL-BE", L"NL-NL", + L"NN-NO", L"NSO-ZA", L"OC-FR", L"OR-IN", L"PA-IN", L"PL-PL", L"PRS-AF", L"PS-AF", L"PT-BR", L"PT-PT", L"QUT-GT", + L"QUZ-BO", L"QUZ-EC", L"QUZ-PE", L"RM-CH", L"RO-RO", L"RU-RU", L"RW-RW", L"SAH-RU", L"SA-IN", L"SE-FI", L"SE-NO", + L"SE-SE", L"SI-LK", L"SK-SK", L"SL-SI", L"SMA-NO", L"SMA-SE", L"SMJ-NO", L"SMJ-SE", L"SMN-FI", L"SMS-FI", L"SQ-AL", + L"SV-FI", L"SV-SE", L"SW-KE", L"SYR-SY", L"TA-IN", L"TE-IN", L"TH-TH", L"TK-TM", L"TN-ZA", L"TR-TR", L"TT-RU", + L"UG-CN", L"UK-UA", L"UR-PK", L"VI-VN", L"WO-SN", L"XH-ZA", L"YO-NG", L"ZH-CN", L"ZH-HK", L"ZH-MO", L"ZH-SG", + L"ZH-TW", L"ZU-ZA", L"AZ-CYRL-AZ", L"AZ-LATN-AZ", L"BS-CYRL-BA", L"BS-LATN-BA", L"HA-LATN-NG", L"IU-CANS-CA", + L"IU-LATN-CA", L"MN-MONG-CN", L"SR-CYRL-BA", L"SR-CYRL-CS", L"SR-CYRL-ME", L"SR-CYRL-RS", L"SR-LATN-BA", + L"SR-LATN-CS", L"SR-LATN-ME", L"SR-LATN-RS", L"TG-CYRL-TJ", L"TZM-LATN-DZ", L"UZ-CYRL-UZ", L"UZ-LATN-UZ", + }; + + // The folded and sorted version of LocaleViews. + const std::vector Locales; + + // Add values here but use LegalEntitySuffixes in code. + const std::vector LegalEntitySuffixViews + { + // Acronyms + L"AB", L"AD", L"AG", L"APS", L"AS", L"ASA", L"BV", L"CO", L"CV", L"DOO", L"eV", L"GES", L"GESMBH", L"GMBH", L"INC", L"KG", + L"KS", L"PS", L"LLC", L"LP", L"LTD", L"LTDA", L"MBH", L"NV", L"PLC", L"SL", L"PTY", L"PVT", L"SA", L"SARL", + L"SC", L"SCA", L"SL", L"SP", L"SPA", L"SRL", L"SRO", + + // Words + L"COMPANY", L"CORP", L"CORPORATION", L"HOLDING", L"HOLDINGS", L"INCORPORATED", L"LIMITED", L"SUBSIDIARY" + }; + + // The folded and sorted version of LocaleViews. + const std::vector LegalEntitySuffixes; + + static std::vector FoldAndSort(const std::vector& input) + { + std::vector result; + std::transform(input.begin(), input.end(), std::back_inserter(result), [](const std::wstring_view wsv) { return Utility::ConvertToUTF16(Utility::FoldCase(wsv)); }); + std::sort(result.begin(), result.end()); + return result; + } + + InterimNameNormalizationResult NormalizeNameInternal(std::string_view name) const + { + InterimNameNormalizationResult result; + result.Name = PrepareForValidation(name); + while (Unwrap(result.Name)); // remove wrappers + + // handle (large majority of) SAP Business Object programs + if (SAPPackage.IsMatch(result.Name)) + { + return result; + } + + result.Architecture = RemoveArchitecture(result.Name); + result.Locale = RemoveLocale(result.Name); + + // Extract KB numbers from their parens and preserve them + result.Name = KBNumbers.Replace(result.Name, L"$1"); + + // Repeatedly remove matches for the regexes to create the minimum name + while (RemoveAll(ProgramNameRegexes, result.Name)); + + auto tokens = Split(ProgramNameSplit, result.Name, LegalEntitySuffixes); + result.Name = Join(tokens); + + // Drop all undesired characters + Remove(NonLettersAndDigits, result.Name); + + return result; + } + + InterimPublisherNormalizationResult NormalizePublisherInternal(std::string_view publisher) const + { + InterimPublisherNormalizationResult result; + + result.Publisher = PrepareForValidation(publisher); + while (Unwrap(result.Publisher)); // remove wrappers + + while (RemoveAll(PublisherNameRegexes, result.Publisher)); + + auto tokens = Split(PublisherNameSplit, result.Publisher, LegalEntitySuffixes, true); + result.Publisher = Join(tokens); + + // Drop all undesired characters + Remove(NonLettersAndDigits, result.Publisher); + + return result; + } + + public: + NormalizationInitial() : Locales(FoldAndSort(LocaleViews)), LegalEntitySuffixes(FoldAndSort(LegalEntitySuffixViews)) + { + } + + NormalizedName Normalize(std::string_view name, std::string_view publisher) const override + { + InterimNameNormalizationResult nameResult = NormalizeNameInternal(name); + InterimPublisherNormalizationResult pubResult = NormalizePublisherInternal(publisher); + + NormalizedName result; + result.Name(ConvertToUTF8(nameResult.Name)); + result.Architecture(nameResult.Architecture); + result.Locale(ConvertToUTF8(nameResult.Locale)); + result.Publisher(ConvertToUTF8(pubResult.Publisher)); + + return result; + } + + NormalizedName NormalizeName(std::string_view name) const override + { + InterimNameNormalizationResult nameResult = NormalizeNameInternal(name); + + NormalizedName result; + result.Name(ConvertToUTF8(nameResult.Name)); + result.Architecture(nameResult.Architecture); + result.Locale(ConvertToUTF8(nameResult.Locale)); + + return result; + } + + std::string NormalizePublisher(std::string_view publisher) const override + { + InterimPublisherNormalizationResult pubResult = NormalizePublisherInternal(publisher); + + return ConvertToUTF8(pubResult.Publisher); + } + }; + } + + NameNormalizer::NameNormalizer(NormalizationVersion version) + { + switch (version) + { + case AppInstaller::Utility::NormalizationVersion::Initial: + m_normalizer = std::make_unique(); + break; + default: + THROW_HR(E_INVALIDARG); + } + } + + NormalizedName NameNormalizer::Normalize(std::string_view name, std::string_view publisher) const + { + return m_normalizer->Normalize(name, publisher); + } + + NormalizedName NameNormalizer::NormalizeName(std::string_view name) const + { + return m_normalizer->NormalizeName(name); + } + + std::string NameNormalizer::NormalizePublisher(std::string_view publisher) const + { + return m_normalizer->NormalizePublisher(publisher); + } +} diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h index b4999801ce..f84be6d4dd 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDateTime.h @@ -10,7 +10,7 @@ namespace AppInstaller::Utility // Writes the given time to the given stream. // Assumes that system_clock uses Linux epoch (as required by C++20 standard). // Time is also assumed to be after the epoch. - void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time); + void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339 = false); // Gets the current time as a string. Can be used as a file name. std::string GetCurrentTimeForFilename(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h index 10252c44d8..0d55061bd2 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -45,5 +45,6 @@ namespace AppInstaller::Utility // Apply Mark of the web using IAttachmentExecute::Save if the target file is on NTFS, otherwise does nothing. // This method only does a best effort since Attachment Execution Service may be disabled. - void ApplyMotwUsingIAttachmentExecuteIfApplicable(const std::filesystem::path& filePath, const std::string& source); + // If IAttachmentExecute::Save is successfully invoked and the scan failed, the failure HRESULT is returned. + HRESULT ApplyMotwUsingIAttachmentExecuteIfApplicable(const std::filesystem::path& filePath, const std::string& source); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 66a8d7e25e..cf31a0bf9d 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -57,6 +57,19 @@ #define APPINSTALLER_CLI_ERROR_INVALID_MANIFEST ((HRESULT)0x8A15002A) #define APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE ((HRESULT)0x8A15002B) #define APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE ((HRESULT)0x8A15002C) +#define APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED ((HRESULT)0x8A15002D) +#define APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH ((HRESULT)0x8A15002E) +#define APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND ((HRESULT)0x8A15002F) +#define APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED ((HRESULT)0x8A150030) +#define APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR ((HRESULT)0x8A150031) +#define APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR ((HRESULT)0x8A150032) +#define APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR ((HRESULT)0x8A150033) +#define APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED ((HRESULT)0x8a150034) +#define APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND ((HRESULT)0x8a150035) +#define APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE ((HRESULT)0x8a150036) +#define APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE ((HRESULT)0x8A150037) +#define APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE ((HRESULT)0x8A150038) +#define APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA ((HRESULT)0x8A150039) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h index 64f4190630..165d08b465 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include #include +#include #include #include #include #include #include +#include namespace AppInstaller { @@ -66,6 +69,82 @@ namespace AppInstaller { return static_cast(ut); } + + // Enum based variant helper. + // Enum must be an enum whose first member has the value 0, each subsequent member increases by 1, and the final member is named Max. + // Mapping is a template type that takes one template parameter of type Enum, and whose members define value_t as the type for that enum value. + template typename Mapping> + struct EnumBasedVariant + { + private: + // Used to deduce the variant type; making a variant that includes std::monostate and all Mapping types. + template + static inline auto Deduce(std::index_sequence) { return std::variant(I)>::value_t...>{}; } + + public: + // Holds data of any type listed in Mapping. + using variant_t = decltype(Deduce(std::make_index_sequence(Enum::Max)>())); + + // Gets the index into the variant for the given Data. + static constexpr inline size_t Index(Enum e) { return static_cast(e) + 1; } + }; + + // Provides a map of the Enum to the mapped types. + template typename Mapping> + struct EnumBasedVariantMap + { + using Variant = EnumBasedVariant; + + template + using mapping_t = typename Mapping::value_t; + + // Adds a value to the map, or overwrites an existing entry. + // This must be used to create the initial data entry, but Get can be used to modify. + template + void Add(mapping_t&& v) + { + m_data[E].emplace(std::move(std::forward>(v))); + } + + template + void Add(const mapping_t& v) + { + m_data[E].emplace(v); + } + + // Return a value indicating whether the given enum is stored in the map. + bool Contains(Enum e) { return (m_data.find(e) != m_data.end()); } + + // Gets the value. + template + mapping_t& Get() + { + return std::get(GetVariant(E)); + } + + template + const mapping_t& Get() const + { + return std::get(GetVariant(E)); + } + + private: + typename Variant::variant_t& GetVariant(Enum e) + { + auto itr = m_data.find(e); + THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.end(), "GetVariant(%d)", e); + return itr->second; + } + + const typename Variant::variant_t& GetVariant(Enum e) const + { + auto itr = m_data.find(e); + THROW_HR_IF_MSG(E_NOT_SET, itr == m_data.cend(), "GetVariant(%d)", e); + return itr->second; + } + + std::map m_data; + }; } // Enable enums to be output generically (as their integral value). diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLogging.h b/src/AppInstallerCommonCore/Public/AppInstallerLogging.h index cea60aa3fa..e6b7163e09 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerLogging.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerLogging.h @@ -17,7 +17,7 @@ auto& _aicli_log_log = AppInstaller::Logging::Log(); \ if (_aicli_log_log.IsEnabled(_aicli_log_channel, _aicli_log_level)) \ { \ - std::stringstream _aicli_log_strstr; \ + AppInstaller::Logging::LoggingStream _aicli_log_strstr; \ _aicli_log_strstr _outstream_; \ _aicli_log_log.Write(_aicli_log_channel, _aicli_log_level, _aicli_log_strstr.str()); \ } \ @@ -140,6 +140,37 @@ namespace AppInstaller::Logging // Calls the various stream format functions to produce an 8 character hexadecimal output. std::ostream& SetHRFormat(std::ostream& out); + + // This type allows us to override the default behavior of output operators for logging. + struct LoggingStream + { + // Force use of the UTF-8 string from a file path. + // This should not be necessary when we move to C++20 and convert to using u8string. + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, std::filesystem::path& path) + { + out.m_out << path.u8string(); + return out; + } + + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, const std::filesystem::path& path) + { + out.m_out << path.u8string(); + return out; + } + + // Everything else. + template + friend AppInstaller::Logging::LoggingStream& operator<<(AppInstaller::Logging::LoggingStream& out, T&& t) + { + out.m_out << std::forward(t); + return out; + } + + std::string str() const { return m_out.str(); } + + private: + std::stringstream m_out; + }; } // Enable output of system_clock time_points. diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h index 4772e8c8f0..dc0d338968 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -32,6 +32,7 @@ namespace AppInstaller::Utility NormalizedUTF8(std::string_view sv) : std::string(Normalize(sv, Form)) {} + NormalizedUTF8(std::string& s) : std::string(Normalize(s, Form)) {} NormalizedUTF8(const std::string& s) : std::string(Normalize(s, Form)) {} NormalizedUTF8(std::string&& s) : std::string(Normalize(s, Form)) {} @@ -113,6 +114,7 @@ namespace AppInstaller::Utility NormalizedString FoldCase(const NormalizedString& input); // Checks if the input string is empty or whitespace + bool IsEmptyOrWhitespace(std::string_view str); bool IsEmptyOrWhitespace(std::wstring_view str); // Find token in the input string and replace with value. @@ -121,6 +123,10 @@ namespace AppInstaller::Utility // Removes whitespace from the beginning and end of the string. std::string& Trim(std::string& str); + std::string Trim(std::string&& str); + + // Removes whitespace from the beginning and end of the string. + std::wstring& Trim(std::wstring& str); // Reads the entire stream into a string. std::string ReadEntireStream(std::istream& stream); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerSynchronization.h b/src/AppInstallerCommonCore/Public/AppInstallerSynchronization.h index c3a328ff0c..3b5774cfe2 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerSynchronization.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerSynchronization.h @@ -33,11 +33,14 @@ namespace AppInstaller::Synchronization static CrossProcessReaderWriteLock LockForWrite(std::string_view name); + bool WasAbandoned() { return m_wasAbandoned; } + private: CrossProcessReaderWriteLock(std::string_view name); wil::unique_mutex m_mutex; wil::unique_semaphore m_semaphore; ResetWhenMovedFrom m_semaphoreReleases{ 0 }; + bool m_wasAbandoned = false; }; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h index 852cc4c7eb..4564819481 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -15,52 +15,56 @@ namespace AppInstaller::Logging // this should not become a burden. struct TelemetryTraceLogger { - ~TelemetryTraceLogger(); + virtual ~TelemetryTraceLogger(); - TelemetryTraceLogger(const TelemetryTraceLogger&) = delete; - TelemetryTraceLogger& operator=(const TelemetryTraceLogger&) = delete; + TelemetryTraceLogger(const TelemetryTraceLogger&) = default; + TelemetryTraceLogger& operator=(const TelemetryTraceLogger&) = default; - TelemetryTraceLogger(TelemetryTraceLogger&&) = delete; - TelemetryTraceLogger& operator=(TelemetryTraceLogger&&) = delete; + TelemetryTraceLogger(TelemetryTraceLogger&&) = default; + TelemetryTraceLogger& operator=(TelemetryTraceLogger&&) = default; // Gets the singleton instance of this type. static TelemetryTraceLogger& GetInstance(); + // Control whether this trace logger is enabled at runtime. + bool DisableRuntime(); + void EnableRuntime(); + // Logs the failure info. - void LogFailure(const wil::FailureInfo& failure) noexcept; + void LogFailure(const wil::FailureInfo& failure) const noexcept; // Logs the initial process startup. - void LogStartup() noexcept; + void LogStartup() const noexcept; // Logs the invoked command. - void LogCommand(std::string_view commandName) noexcept; + void LogCommand(std::string_view commandName) const noexcept; // Logs the invoked command success. - void LogCommandSuccess(std::string_view commandName) noexcept; + void LogCommandSuccess(std::string_view commandName) const noexcept; // Logs the invoked command termination. - void LogCommandTermination(HRESULT hr, std::string_view file, size_t line) noexcept; + void LogCommandTermination(HRESULT hr, std::string_view file, size_t line) const noexcept; // Logs the invoked command termination. - void LogException(std::string_view commandName, std::string_view type, std::string_view message) noexcept; + void LogException(std::string_view commandName, std::string_view type, std::string_view message) const noexcept; // Logs whether the manifest used in workflow is local - void LogIsManifestLocal(bool isLocalManifest) noexcept; + void LogIsManifestLocal(bool isLocalManifest) const noexcept; // Logs the Manifest fields. - void LogManifestFields(std::string_view id, std::string_view name, std::string_view version) noexcept; + void LogManifestFields(std::string_view id, std::string_view name, std::string_view version) const noexcept; // Logs when there is no matching App found for search - void LogNoAppMatch() noexcept; + void LogNoAppMatch() const noexcept; // Logs when there is multiple matching Apps found for search - void LogMultiAppMatch() noexcept; + void LogMultiAppMatch() const noexcept; // Logs the name and Id of app found - void LogAppFound(std::string_view name, std::string_view id) noexcept; + void LogAppFound(std::string_view name, std::string_view id) const noexcept; // Logs the selected installer details - void LogSelectedInstaller(int arch, std::string_view url, std::string_view installerType, std::string_view scope, std::string_view language) noexcept; + void LogSelectedInstaller(int arch, std::string_view url, std::string_view installerType, std::string_view scope, std::string_view language) const noexcept; // Logs details of a search request. void LogSearchRequest( @@ -72,10 +76,10 @@ namespace AppInstaller::Logging std::string_view tag, std::string_view command, size_t maximum, - std::string_view request); + std::string_view request) const noexcept; // Logs the Search Result - void LogSearchResultCount(uint64_t resultCount) noexcept; + void LogSearchResultCount(uint64_t resultCount) const noexcept; // Logs a mismatch between the expected and actual hash values. void LogInstallerHashMismatch( @@ -84,24 +88,43 @@ namespace AppInstaller::Logging std::string_view channel, const std::vector& expected, const std::vector& actual, - bool overrideHashMismatch); + bool overrideHashMismatch) const noexcept; // Logs a failed installation attempt. - void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode); + void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) const noexcept; + + // Logs a failed uninstallation attempt. + void LogUninstallerFailure(std::string_view id, std::string_view version, std::string_view type, uint32_t errorCode) const noexcept; + + // Logs data about the changes that ocurred in the ARP entries based on an install. + // First 4 arguments are well known values for the package that we installed. + // The next 3 are counts of the number of packages in each category. + // The last 4 are the fields directly from the ARP entry that has been determined to be related to the package that + // was installed, or they will be empty if there is no data or ambiguity about which entry should be logged. + virtual void LogSuccessfulInstallARPChange( + std::string_view sourceIdentifier, + std::string_view packageIdentifier, + std::string_view packageVersion, + std::string_view packageChannel, + size_t changesToARP, + size_t matchesInARP, + size_t countOfIntersectionOfChangesAndMatches, + std::string_view arpName, + std::string_view arpVersion, + std::string_view arpPublisher, + std::string_view arpLanguage) const noexcept; + + protected: + TelemetryTraceLogger(); - // Logs a failure to insert a value into the in-memory cache of installed system packages. - // The most likely reason is due to the same key name being used under multiple ARP scope/architecture locations. - void LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name); + bool IsTelemetryEnabled() const noexcept; - private: - TelemetryTraceLogger(); + bool m_isSettingEnabled = true; + std::atomic_bool m_isRuntimeEnabled{ true }; }; // Helper to make the call sites look clean. - inline TelemetryTraceLogger& Telemetry() - { - return TelemetryTraceLogger::GetInstance(); - } + TelemetryTraceLogger& Telemetry(); // Turns on wil failure telemetry and logging. void EnableWilFailureTelemetry(); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 658262e3c0..ab9df6c37e 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -24,7 +24,14 @@ namespace AppInstaller::Settings ExperimentalMSStore = 0x4, ExperimentalList = 0x8, ExperimentalUpgrade = 0x10, - Max = 0x11, // This MUST always be last + ExperimentalUninstall = 0x20, + ExperimentalImport = 0x40, + ExperimentalRestSource = 0x80, + Max, // This MUST always be after all experimental features + + // Features listed after Max will not be shown with the features command + // This can be used to hide highly experimental features + ExperimentalExport = 0x100 }; using Feature_t = std::underlying_type_t; diff --git a/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h new file mode 100644 index 0000000000..1c921f8921 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +namespace AppInstaller::JsonSchema +{ + // Load an embedded resource from binary and return as std::string + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType); + + // Load schema as parsed json doc + Json::Value LoadSchemaDoc(const std::string& schemaStr); + + // Load an embedded resource from binary and return as Json::Value + Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType); + + // Populate a valijson Schema object from a json value + void PopulateSchema(const Json::Value& schemaJson, valijson::Schema& schema); + + // Validate a json doc with a schema + // Returns whether it was successful and fills the results object + bool Validate(const valijson::Schema& schema, const Json::Value& json, valijson::ValidationResults& results); + + // Extracts the error messages from a result into a single non-localized string + std::string GetErrorStringFromResults(valijson::ValidationResults& results); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/LocIndependent.h b/src/AppInstallerCommonCore/Public/winget/LocIndependent.h index 5774ab8d10..6a629982de 100644 --- a/src/AppInstallerCommonCore/Public/winget/LocIndependent.h +++ b/src/AppInstallerCommonCore/Public/winget/LocIndependent.h @@ -53,12 +53,12 @@ namespace AppInstaller::Utility bool operator<(const LocIndString& other) const { return m_value < other.m_value; } + friend std::ostream& operator<<(std::ostream& out, const AppInstaller::Utility::LocIndString& lis) + { + return (out << lis.get()); + } + private: std::string m_value; }; } - -inline std::ostream& operator<<(std::ostream& out, const AppInstaller::Utility::LocIndString& lis) -{ - return (out << lis.get()); -} diff --git a/src/AppInstallerCommonCore/Public/winget/Manifest.h b/src/AppInstallerCommonCore/Public/winget/Manifest.h index c9a1486750..1b2c59e6ef 100644 --- a/src/AppInstallerCommonCore/Public/winget/Manifest.h +++ b/src/AppInstallerCommonCore/Public/winget/Manifest.h @@ -2,7 +2,6 @@ // Licensed under the MIT License. #pragma once #include -#include #include #include @@ -10,90 +9,39 @@ namespace AppInstaller::Manifest { - // ManifestVer is inherited from Utility::Version and is a more restricted version. - // ManifestVer is used to specify the version of app manifest itself. - // ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] - // and optionally a following tag in the format of -[SomeString] for experimental purpose. - struct ManifestVer : public Utility::Version - { - ManifestVer() = default; - - ManifestVer(std::string_view version); - - uint64_t Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } - uint64_t Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } - uint64_t Patch() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } - - bool HasExtension() const; - - bool HasExtension(std::string_view extension) const; - - private: - std::vector m_extensions; - }; - // Representation of the parsed manifest file. struct Manifest { using string_t = Utility::NormalizedString; - // Required string_t Id; - // Required - string_t Name; - - // Required string_t Version; - // Required - string_t Publisher; - - string_t AppMoniker; - string_t Channel; - string_t Author; - - string_t License; + string_t Moniker; - string_t MinOSVersion; - - // Comma separated values - std::vector Tags; - - // Comma separated values - std::vector Commands; - - // Comma separated values - std::vector Protocols; - - // Comma separated values - std::vector FileExtensions; - - ManifestInstaller::InstallerTypeEnum InstallerType = ManifestInstaller::InstallerTypeEnum::Unknown; - - // Default is Install if not specified - ManifestInstaller::UpdateBehaviorEnum UpdateBehavior = ManifestInstaller::UpdateBehaviorEnum::Install; + ManifestVer ManifestVersion; - // Package family name for MSIX packaged installers. - string_t PackageFamilyName; + ManifestInstaller DefaultInstallerInfo; - // Product code for ARP (Add/Remove Programs) installers. - string_t ProductCode; + std::vector Installers; - string_t Description; + ManifestLocalization DefaultLocalization; - string_t Homepage; + std::vector Localizations; - string_t LicenseUrl; + ManifestLocalization CurrentLocalization; - ManifestVer ManifestVersion; + // ApplyLocale will update the CurrentLocalization according to the specified locale + // If locale is empty, user setting locale will be used + void ApplyLocale(const std::string& locale = {}); - std::map Switches; - - std::vector Installers; + // Get all tags across localizations + std::vector GetAggregatedTags() const; - std::vector Localization; + // Get all commands across installers + std::vector GetAggregatedCommands() const; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h new file mode 100644 index 0000000000..6e1ce22c47 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include + +#include + +namespace AppInstaller::Manifest +{ + using string_t = Utility::NormalizedString; + using namespace std::string_view_literals; + + // The maximum supported major version known about by this code. + constexpr uint64_t s_MaxSupportedMajorVersion = 1; + + // The default manifest version assigned to manifests without a ManifestVersion field. + constexpr std::string_view s_DefaultManifestVersion = "0.1.0"sv; + + // V1 manifest version for GA + constexpr std::string_view s_ManifestVersionV1 = "1.0.0"sv; + + // The manifest extension for the MS Store + constexpr std::string_view s_MSStoreExtension = "msstore"sv; + + // ManifestVer is inherited from Utility::Version and is a more restricted version. + // ManifestVer is used to specify the version of app manifest itself. + // ManifestVer is a 3 part version in the format of [0-65535].[0-65535].[0-65535] + // and optionally a following tag in the format of -[SomeString] for experimental purpose. + struct ManifestVer : public Utility::Version + { + ManifestVer() = default; + + ManifestVer(std::string_view version); + + uint64_t Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } + uint64_t Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } + uint64_t Patch() const { return m_parts.size() > 2 ? m_parts[2].Integer : 0; } + + bool HasExtension() const; + + bool HasExtension(std::string_view extension) const; + + private: + std::vector m_extensions; + }; + + enum class InstallerTypeEnum + { + Unknown, + Inno, + Wix, + Msi, + Nullsoft, + Zip, + Msix, + Exe, + Burn, + MSStore, + }; + + enum class UpdateBehaviorEnum + { + Unknown, + Install, + UninstallPrevious, + }; + + enum class InstallerSwitchType + { + Custom, + Silent, + SilentWithProgress, + Interactive, + Language, + Log, + InstallLocation, + Update, + }; + + enum class ScopeEnum + { + Unknown, + User, + Machine, + }; + + enum class InstallModeEnum + { + Unknown, + Interactive, + Silent, + SilentWithProgress, + }; + + enum class PlatformEnum + { + Unknown, + Universal, + Desktop, + }; + + enum class ManifestTypeEnum + { + Singleton, + Version, + Installer, + DefaultLocale, + Locale, + Merged, + Preview, + }; + + struct PackageDependency + { + string_t Id; + string_t MinVersion; + }; + + struct Dependency + { + std::vector WindowsFeatures; + std::vector WindowsLibraries; + std::vector PackageDependencies; + std::vector ExternalDependencies; + }; + + + InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + + UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); + + ScopeEnum ConvertToScopeEnum(const std::string& in); + + InstallModeEnum ConvertToInstallModeEnum(const std::string& in); + + PlatformEnum ConvertToPlatformEnum(const std::string& in); + + ManifestTypeEnum ConvertToManifestTypeEnum(const std::string& in); + + std::string_view InstallerTypeToString(InstallerTypeEnum installerType); + + std::string_view ScopeToString(ScopeEnum scope); + + // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. + bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + + // Gets a value indicating whether the given installer type uses the ProductCode system reference. + bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + + // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible + bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); + + // Get a list of default switches for known installer types + std::map GetDefaultKnownSwitches(InstallerTypeEnum installerType); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h index eff350dd34..50f07d6266 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -3,6 +3,7 @@ #pragma once #include #include +#include #include #include @@ -19,100 +20,57 @@ namespace AppInstaller::Manifest { using string_t = Utility::NormalizedString; - enum class InstallerTypeEnum - { - Unknown, - Inno, - Wix, - Msi, - Nullsoft, - Zip, - Msix, - Exe, - Burn, - MSStore, - }; - - enum class UpdateBehaviorEnum - { - Unknown, - Install, - UninstallPrevious, - }; - - enum class InstallerSwitchType - { - Custom, - Silent, - SilentWithProgress, - Interactive, - Language, - Log, - InstallLocation, - Update - }; - - enum class ScopeEnum - { - Unknown, - User, - Machine, - }; - - // Required. Values: x86, x64, arm, arm64, all. AppInstaller::Utility::Architecture Arch; - // Required string_t Url; - // Required std::vector Sha256; // Optional. Only used by appx/msix type. If provided, Appinstaller will // validate appx/msix signature and perform streaming install. std::vector SignatureSha256; - // Empty means default - string_t Language; - - // Name TBD - ScopeEnum Scope; - // Store Product Id string_t ProductId; - // Package family name for MSIX packaged installers. - string_t PackageFamilyName; + string_t Locale; - // Product code for ARP (Add/Remove Programs) installers. - string_t ProductCode; + std::vector Platform; + + string_t MinOSVersion; // If present, has more precedence than root - InstallerTypeEnum InstallerType; + InstallerTypeEnum InstallerType = InstallerTypeEnum::Unknown; - // Default is Install if not specified - UpdateBehaviorEnum UpdateBehavior; + ScopeEnum Scope = ScopeEnum::User; + + std::vector InstallModes; // If present, has more precedence than root std::map Switches; - static InstallerTypeEnum ConvertToInstallerTypeEnum(const std::string& in); + std::vector InstallerSuccessCodes; + + UpdateBehaviorEnum UpdateBehavior = UpdateBehaviorEnum::Install; - static UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); + std::vector Commands; - static ScopeEnum ConvertToScopeEnum(const std::string& in); + std::vector Protocols; - static std::string_view InstallerTypeToString(InstallerTypeEnum installerType); + std::vector FileExtensions; - static std::string_view ScopeToString(ScopeEnum scope); + // Package family name for MSIX packaged installers. + string_t PackageFamilyName; + + // Product code for ARP (Add/Remove Programs) installers. + string_t ProductCode; - // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. - static bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); + // For msix only + std::vector Capabilities; - // Gets a value indicating whether the given installer type uses the ProductCode system reference. - static bool DoesInstallerTypeUseProductCode(InstallerTypeEnum installerType); + // For msix only + std::vector RestrictedCapabilities; - // Checks whether 2 installer types are compatible. E.g. inno and exe are update compatible - static bool IsInstallerTypeCompatible(InstallerTypeEnum type1, InstallerTypeEnum type2); + Dependency Dependencies; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h index e430d68058..732f6b2886 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h @@ -3,20 +3,91 @@ #pragma once #include +#include + namespace AppInstaller::Manifest { - class ManifestLocalization + using string_t = Utility::NormalizedString; + + enum class Localization : size_t + { + Publisher, + PublisherUrl, + PublisherSupportUrl, + PrivacyUrl, + Author, + PackageName, + PackageUrl, + License, + LicenseUrl, + Copyright, + CopyrightUrl, + ShortDescription, + Description, + Tags, + Max + }; + + namespace details + { + template + struct LocalizationMapping + { + using value_t = string_t; + }; + + template <> + struct LocalizationMapping + { + using value_t = std::vector; + }; + + // Used to deduce the LocalizationVariant type; making a variant that includes std::monostate and all LocalizationMapping types. + template + inline auto Deduce(std::index_sequence) { return std::variant(I)>::value_t...>{}; } + + // Holds data of any type listed in a LocalizationMapping. + using LocalizationVariant = decltype(Deduce(std::make_index_sequence(Localization::Max)>())); + + // Gets the index into the variant for the given Localization. + constexpr inline size_t LocalizationIndex(Localization l) { return static_cast(l) + 1; } + } + + struct ManifestLocalization { - public: - using string_t = Utility::NormalizedString; + string_t Locale; - // Required - string_t Language; + // Adds a value to the Localization data, or overwrites an existing entry. + template + void Add(typename details::LocalizationMapping::value_t&& v) + { + m_data[L].emplace(std::forward::value_t>(v)); + } + template + void Add(const typename details::LocalizationMapping::value_t& v) + { + m_data[L].emplace(v); + } - string_t Description; + // Return a value indicating whether the given localization type exists. + bool Contains(Localization l) const { return (m_data.find(l) != m_data.end()); } - string_t Homepage; + // Gets the localization value if exists, otherwise empty for easier access + template + typename details::LocalizationMapping::value_t Get() const + { + auto itr = m_data.find(L); + if (itr == m_data.end()) + { + return {}; + } + else + { + return std::get(itr->second); + } + } - string_t LicenseUrl; + private: + std::map m_data; }; } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h new file mode 100644 index 0000000000..35488bae24 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ManifestCommon.h" +#include "ManifestValidation.h" + +#include + +namespace AppInstaller::Manifest::YamlParser +{ + // Forward declarations + struct YamlManifestInfo; + + // Load manifest schema as parsed json doc + Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType); + + // Validate a list of individual manifests against schema + std::vector ValidateAgainstSchema( + const std::vector& manifestList, + const ManifestVer& manifestVersion); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index 9022718288..aef2281409 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -31,6 +31,13 @@ namespace AppInstaller::Manifest const char* const DuplicateInstallerEntry = "Duplicate installer entry found."; const char* const InstallerTypeDoesNotSupportPackageFamilyName = "The specified installer type does not support PackageFamilyName."; const char* const InstallerTypeDoesNotSupportProductCode = "The specified installer type does not support ProductCode."; + const char* const IncompleteMultiFileManifest = "The multi file manifest is incomplete. A multi file manifest must contain at least version, installer and defaultLocale manifest."; + const char* const InconsistentMultiFileManifestFieldValue = "The multi file manifest has inconsistent field values."; + const char* const DuplicateMultiFileManifestType = "The multi file manifest should contain only one file with the particular ManifestType."; + const char* const DuplicateMultiFileManifestLocale = "The multi file manifest contains duplicate PackageLocale."; + const char* const UnsupportedMultiFileManifestType = "The multi file manifest should not contain file with the particular ManifestType."; + const char* const InconsistentMultiFileManifestDefaultLocale = "DefaultLocale value in version manifest does not match PackageLocale value in defaultLocale manifest."; + const char* const FieldFailedToProcess = "Failed to process field."; } struct ValidationError @@ -48,6 +55,7 @@ namespace AppInstaller::Manifest size_t Line = 0; size_t Column = 0; Level ErrorLevel = Level::Error; + std::string FileName; ValidationError(std::string message) : Message(std::move(message)) {} @@ -69,6 +77,27 @@ namespace AppInstaller::Manifest ValidationError(std::string message, std::string field, std::string value, size_t line, size_t column, Level level) : Message(std::move(message)), Field(std::move(field)), Value(std::move(value)), Line(line), Column(column), ErrorLevel(level) {} + + static ValidationError MessageWithFile(std::string message, std::string file) + { + ValidationError error{ message }; + error.FileName = file; + return error; + } + + static ValidationError MessageFieldWithFile(std::string message, std::string field, std::string file) + { + ValidationError error{ message, field }; + error.FileName = file; + return error; + } + + static ValidationError MessageFieldValueWithFile(std::string message, std::string field, std::string value, std::string file) + { + ValidationError error{ message, field, value }; + error.FileName = file; + return error; + } }; struct ManifestException : public wil::ResultException @@ -93,7 +122,10 @@ namespace AppInstaller::Manifest if (m_errors.empty()) { // Syntax error, yaml parser error is stored in FailureInfo - m_manifestErrorMessage = Utility::ConvertToUTF8(GetFailureInfo().pszMessage); + if (GetFailureInfo().pszMessage) + { + m_manifestErrorMessage = Utility::ConvertToUTF8(GetFailureInfo().pszMessage); + } } else { @@ -121,6 +153,10 @@ namespace AppInstaller::Manifest { m_manifestErrorMessage += " Line: " + std::to_string(error.Line) + ", Column: " + std::to_string(error.Column); } + if (!error.FileName.empty()) + { + m_manifestErrorMessage += " File: " + error.FileName; + } m_manifestErrorMessage += '\n'; } } diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h index 60a7861673..02d96d712f 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestYamlParser.h @@ -3,61 +3,46 @@ #pragma once #include #include +#include #include -namespace AppInstaller::Manifest +namespace AppInstaller::Manifest::YamlParser { - struct YamlParser + struct YamlManifestInfo { - // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. - // e.g. Channel should be null. Client code does not need this check to work properly. - // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. - static Manifest CreateFromPath(const std::filesystem::path& inputFile, bool fullValidation = false, bool throwOnWarning = false); + // Root node of a yaml manifest file + YAML::Node Root; - static Manifest Create(const std::string& input, bool fullValidation = false, bool throwOnWarning = false); + // File name of the manifest file if applicable for error reporting + std::string FileName; - private: - // These pointers are referenced in the processing functions in manifest field info table. - YAML::Node* m_p_installersNode = nullptr; - YAML::Node* m_p_switchesNode = nullptr; - YAML::Node* m_p_localizationsNode = nullptr; - AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; - AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; - std::map* m_p_switches = nullptr; - AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; - - // This struct contains individual app manifest field info - struct ManifestFieldInfo - { - ManifestFieldInfo(std::string name, std::function func, bool required = false, std::string regex = {}) : - Name(std::move(name)), ProcessFunc(func), Required(required), RegEx(std::move(regex)) {} - - std::string Name; - std::function ProcessFunc; - bool Required = false; - std::string RegEx = {}; - }; - - std::vector RootFieldInfos; - std::vector InstallerFieldInfos; - std::vector SwitchesFieldInfos; - std::vector LocalizationFieldInfos; - - std::vector ParseManifest(const YAML::Node& rootNode, Manifest& manifest, bool fullValidation); - - static std::map GetDefaultKnownSwitches( - ManifestInstaller::InstallerTypeEnum installerType); - - // This method takes YAML root node and list of manifest field info. - // Yaml-cpp does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, - // the value is undefined. So in this method, we will iterate through the node map and process each individual - // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. - static std::vector ValidateAndProcessFields( - const YAML::Node& rootNode, - const std::vector& fieldInfos, - bool fullValidation); - - void PrepareManifestFieldInfos(const ManifestVer& manifestVer); + ManifestTypeEnum ManifestType; }; + + // fullValidation: Bool to set if manifest creation should perform extra validation that client does not need. + // e.g. Channel should be null. Client code does not need this check to work properly. + // throwOnWarning: Bool to indicate if an exception should be thrown with only warnings detected in the manifest. + // mergedManifestPath: Output file for merged manifest after processing a multi file manifest + // schemaValidationOnly: Bool to indicate if only schema validation should be performed + Manifest CreateFromPath( + const std::filesystem::path& inputPath, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); + + Manifest Create( + const std::string& input, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); + + Manifest ParseManifest( + std::vector& input, + bool fullValidation = false, + bool throwOnWarning = false, + const std::filesystem::path& mergedManifestPath = {}, + bool schemaValidationOnly = false); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h new file mode 100644 index 0000000000..886cf2184c --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/ManifestYamlPopulator.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include + +namespace AppInstaller::Manifest +{ + struct ManifestYamlPopulator + { + static std::vector PopulateManifest(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); + + private: + + bool m_fullValidation = false; + bool m_isMergedManifest = false; + + // Struct mapping a manifest field to its population logic + struct FieldProcessInfo + { + FieldProcessInfo(std::string name, std::function(const YAML::Node&)> func) : + Name(std::move(name)), ProcessFunc(func) {} + + std::string Name; + std::function(const YAML::Node&)> ProcessFunc; + }; + + std::vector RootFieldInfos; + std::vector InstallerFieldInfos; + std::vector SwitchesFieldInfos; + std::vector DependenciesFieldInfos; + std::vector PackageDependenciesFieldInfos; + std::vector LocalizationFieldInfos; + + // These pointers are referenced in the processing functions in manifest field process info table. + AppInstaller::Manifest::Manifest* m_p_manifest = nullptr; + AppInstaller::Manifest::ManifestInstaller* m_p_installer = nullptr; + std::map* m_p_switches = nullptr; + AppInstaller::Manifest::Dependency* m_p_dependency = nullptr; + AppInstaller::Manifest::PackageDependency* m_p_packageDependency = nullptr; + AppInstaller::Manifest::ManifestLocalization* m_p_localization = nullptr; + + // Cache of Installers node and Localization node + YAML::Node const* m_p_installersNode = nullptr; + YAML::Node const* m_p_localizationsNode = nullptr; + + std::vector GetRootFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector GetInstallerFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); + std::vector GetSwitchesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector GetDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector GetPackageDependenciesFieldProcessInfo(const ManifestVer& manifestVersion); + std::vector GetLocalizationFieldProcessInfo(const ManifestVer& manifestVersion, bool forRootFields = false); + + // This method takes YAML root node and list of manifest field info. + // Yaml lib does not support case insensitive search and it allows duplicate keys. If duplicate keys exist, + // the value is undefined. So in this method, we will iterate through the node map and process each individual + // pair ourselves. This also helps with generating aggregated error rather than throwing on first failure. + std::vector ValidateAndProcessFields( + const YAML::Node& rootNode, + const std::vector& fieldInfos); + + std::vector ProcessPackageDependenciesNode(const YAML::Node& rootNode, std::vector& packageDependencies); + + std::vector PopulateManifestInternal(const YAML::Node& rootNode, Manifest& manifest, const ManifestVer& manifestVersion, bool fullValidation); + }; +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/NameNormalization.h b/src/AppInstallerCommonCore/Public/winget/NameNormalization.h new file mode 100644 index 0000000000..2c2c4cf8dd --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/NameNormalization.h @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +#include +#include + + +namespace AppInstaller::Utility +{ + // The specific version of normalization being used. + enum class NormalizationVersion + { + Initial, + }; + + struct NameNormalizer; + + // A package publisher and name that has been normalized, allowing direct + // comparison across versions and many other facet. Also allows use in + // generating and Id for local packages. + struct NormalizedName + { + NormalizedName() = default; + + const std::string& Name() const { return m_name; } + void Name(std::string&& name) { m_name = std::move(name); } + void Name(std::string_view name) { m_name = name; } + + Utility::Architecture Architecture() const { return m_arch; } + void Architecture(Utility::Architecture arch) { m_arch = arch; } + + const std::string& Locale() const { return m_locale; } + void Locale(std::string&& locale) { m_locale = std::move(locale); } + + const std::string& Publisher() const { return m_publisher; } + void Publisher(std::string&& publisher) { m_publisher = std::move(publisher); } + void Publisher(std::string_view publisher) { m_publisher = publisher; } + + private: + std::string m_name; + Utility::Architecture m_arch = Utility::Architecture::Unknown; + std::string m_locale; + std::string m_publisher; + }; + + namespace details + { + // NameNormalizer interface to allow different versions. + struct INameNormalizer + { + virtual ~INameNormalizer() = default; + + // Normalize both the name and publisher at the same time. + virtual NormalizedName Normalize(std::string_view name, std::string_view publisher) const = 0; + + // Normalize only the name. + virtual NormalizedName NormalizeName(std::string_view name) const = 0; + + // Normalize only the publisher. + virtual std::string NormalizePublisher(std::string_view publisher) const = 0; + }; + } + + // Helper that manages the lifetime of the internals required to + // execute the name normalization. + struct NameNormalizer + { + NameNormalizer(NormalizationVersion version); + + NormalizedName Normalize(std::string_view name, std::string_view publisher) const; + NormalizedName NormalizeName(std::string_view name) const; + std::string NormalizePublisher(std::string_view publisher) const; + + private: + std::unique_ptr m_normalizer; + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/Regex.h b/src/AppInstallerCommonCore/Public/winget/Regex.h new file mode 100644 index 0000000000..8f824e71b3 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/Regex.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include + + +namespace AppInstaller::Regex +{ + // Options for regular expression use. + enum class Options + { + None = 0, + CaseInsensitive, + }; + + // Stores the compiled regular expression. + // All pattern strings are considered UTF-8. + // All input strings are considered UTF-16, as this is what ICU operates on internally. + struct Expression + { + Expression(); + Expression(std::string_view pattern, Options options = Options::None); + + Expression(const Expression&); + Expression& operator=(const Expression&); + + Expression(Expression&&); + Expression& operator=(Expression&&); + + ~Expression(); + + // Determines if the expression contains a value. + operator bool() const; + + // Returns a value indicating whether the *entire* input matches the expression. + bool IsMatch(std::wstring_view input) const; + + // Replaces all matches in the input with the replacement. + std::wstring Replace(std::wstring_view input, std::wstring_view replacement) const; + + // For each section of the input, invoke the given functor. This allows the caller + // to iterate over the entire string, taking action as appropriate for each part. + // The parameters are: + // bool :: indicates whether this section was a match + // string_view :: the text for the section + // The functor should return true to continue the loop, or false to break it. + void ForEach(std::wstring_view input, const std::function& f) const; + + private: + struct impl; + std::unique_ptr pImpl; + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index ee79c6df43..c2bc8e63b6 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -10,11 +10,11 @@ #include #include +using namespace std::chrono_literals; +using namespace std::string_view_literals; + namespace AppInstaller::Settings { - using namespace std::chrono_literals; - using namespace std::string_view_literals; - // The type of argument. enum class UserSettingsType { @@ -51,6 +51,11 @@ namespace AppInstaller::Settings EFExperimentalMSStore, EFList, EFExperimentalUpgrade, + EFUninstall, + EFImport, + EFExport, + TelemetryDisable, + EFRestSource, Max }; @@ -85,6 +90,11 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFList, bool, bool, false, ".experimentalFeatures.list"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.upgrade"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFUninstall, bool, bool, false, ".experimentalFeatures.uninstall"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFImport, bool, bool, false, ".experimentalFeatures.import"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFExport, bool, bool, false, ".experimentalFeatures.export"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFRestSource, bool, bool, false, ".experimentalFeatures.restSource"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template diff --git a/src/AppInstallerCommonCore/Public/winget/Yaml.h b/src/AppInstallerCommonCore/Public/winget/Yaml.h index 9a15687ca5..e7b847b0cb 100644 --- a/src/AppInstallerCommonCore/Public/winget/Yaml.h +++ b/src/AppInstallerCommonCore/Public/winget/Yaml.h @@ -140,6 +140,7 @@ namespace AppInstaller::YAML // The workers for the as function. std::string as_dispatch(std::string*) const; int64_t as_dispatch(int64_t*) const; + int as_dispatch(int*) const; bool as_dispatch(bool*) const; Type m_type; @@ -195,6 +196,9 @@ namespace AppInstaller::YAML // Gets the result of the emitter; can only be retrieved once. std::string str(); + // Gets the result of the emitter to out stream; can only be retrieved once. + void Emit(std::ostream& out); + private: // Appends the given node to the current container if applicable. void AppendNode(int id); diff --git a/src/AppInstallerCommonCore/Regex.cpp b/src/AppInstallerCommonCore/Regex.cpp new file mode 100644 index 0000000000..d0ce534b89 --- /dev/null +++ b/src/AppInstallerCommonCore/Regex.cpp @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/Regex.h" +#include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerLogging.h" + +#define WINGET_THROW_REGEX_ERROR_IF_FAILED(_err_,_func_) \ + if (U_FAILURE(_err_)) \ + { \ + AICLI_LOG(Core, Error, << #_func_ " returned " << _err_); \ + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR); \ + } + + +namespace AppInstaller::Regex +{ + struct Expression::impl + { + using uregex_ptr = wil::unique_any; + using utext_ptr = wil::unique_any; + + impl(std::string_view pattern, Options options) + { + UErrorCode uec = U_ZERO_ERROR; + + utext_ptr patternUtext{ utext_openUTF8(nullptr, pattern.data(), pattern.length(), &uec) }; + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, utext_openUTF8); + + // For now, just handle the one option + uint32_t flags = 0; + + if (options == Options::CaseInsensitive) + { + flags = UREGEX_CASE_INSENSITIVE; + } + + UParseError parseError{}; + + m_regex.reset(uregex_openUText(patternUtext.get(), flags, &parseError, &uec)); + + if (U_FAILURE(uec)) + { + AICLI_LOG(Core, Error, << "uregex_openUText failed with error [" << uec << "] at line " << parseError.line << ", position " << parseError.offset << '\n' << pattern); + THROW_HR(APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR); + } + } + + impl(const impl& other) + { + UErrorCode uec = U_ZERO_ERROR; + + m_regex.reset(uregex_clone(other.m_regex.get(), &uec)); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_clone); + } + + impl& operator=(const impl& other) + { + *this = impl{ other }; + } + + impl(impl&&) = default; + impl& operator=(impl&&) = default; + + ~impl() = default; + + bool IsMatch(std::wstring_view input) const + { + UErrorCode uec = U_ZERO_ERROR; + + SetText(input); + + UBool result = uregex_matches(m_regex.get(), -1, &uec); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_matches); + + return !!result; + } + + std::wstring Replace(std::wstring_view input, std::wstring_view replacement) const + { + UErrorCode uec = U_ZERO_ERROR; + + SetText(input); + + std::u16string_view u16replacement = Convert(replacement); + utext_ptr replacementUtext{ utext_openUChars(nullptr, u16replacement.data(), u16replacement.length(), &uec) }; + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, utext_openUTF8); + + utext_ptr resultUText{ uregex_replaceAllUText(m_regex.get(), replacementUtext.get(), nullptr, &uec) }; + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_replaceAllUText); + + int64_t cch = utext_nativeLength(resultUText.get()); + std::wstring result(static_cast(cch), '\0'); + + utext_extract(resultUText.get(), 0, std::numeric_limits::max(), reinterpret_cast(&result[0]), static_cast(result.size()), &uec); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, utext_extract); + + return result; + } + + void ForEach(std::wstring_view input, const std::function&f) const + { + UErrorCode uec = U_ZERO_ERROR; + + SetText(input); + int32_t startPos = 0; + + while (uregex_findNext(m_regex.get(), &uec)) + { + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_findNext); + + int32_t pos = uregex_start(m_regex.get(), 0, &uec); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_start); + THROW_HR_IF(E_UNEXPECTED, pos == -1); + + // First, send off the unmatched part before the match + if (pos > startPos) + { + if (!f(false, input.substr(startPos, pos - startPos))) + { + return; + } + } + + // Now send the matched part + int32_t end = uregex_end(m_regex.get(), 0, &uec); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_end); + THROW_HR_IF(E_UNEXPECTED, end == -1); + + if (!f(true, input.substr(pos, end - pos))) + { + return; + } + + startPos = end; + } + + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_findNext); + + // Finally, send any remaining part + if (input.length() > static_cast(startPos)) + { + f(false, input.substr(startPos)); + } + } + + private: + static std::u16string_view Convert(std::wstring_view input) + { + static_assert(sizeof(wchar_t) == sizeof(char16_t), "wchar_t and char16_t must be the same size"); + return { reinterpret_cast(input.data()), input.size() }; + } + + void SetText(std::wstring_view input) const + { + UErrorCode uec = U_ZERO_ERROR; + + std::u16string_view u16 = Convert(input); + + uregex_setText(m_regex.get(), u16.data(), static_cast(u16.length()), &uec); + WINGET_THROW_REGEX_ERROR_IF_FAILED(uec, uregex_setText); + } + + uregex_ptr m_regex; + }; + + Expression::Expression() = default; + + Expression::Expression(std::string_view pattern, Options options) : pImpl(std::make_unique(pattern, options)) {} + + Expression::Expression(const Expression& other) + { + if (other.pImpl) + { + pImpl = std::make_unique(*other.pImpl); + } + } + + Expression& Expression::operator=(const Expression& other) + { + return *this = Expression{ other }; + } + + Expression::Expression(Expression&&) = default; + Expression& Expression::operator=(Expression&&) = default; + + Expression::~Expression() = default; + + Expression::operator bool() const + { + return static_cast(pImpl); + } + + bool Expression::IsMatch(std::wstring_view input) const + { + THROW_HR_IF(E_NOT_VALID_STATE, !pImpl); + return pImpl->IsMatch(input); + } + + std::wstring Expression::Replace(std::wstring_view input, std::wstring_view replacement) const + { + THROW_HR_IF(E_NOT_VALID_STATE, !pImpl); + return pImpl->Replace(input, replacement); + } + + void Expression::ForEach(std::wstring_view input, const std::function& f) const + { + THROW_HR_IF(E_NOT_VALID_STATE, !pImpl); + return pImpl->ForEach(input, f); + } +} diff --git a/src/AppInstallerCommonCore/Registry.cpp b/src/AppInstallerCommonCore/Registry.cpp index cebd2d6009..08eecb2ab3 100644 --- a/src/AppInstallerCommonCore/Registry.cpp +++ b/src/AppInstallerCommonCore/Registry.cpp @@ -231,7 +231,7 @@ namespace AppInstaller::Registry while (data.size() < (64 << 20)) { byteCount = wil::safe_cast(data.size()); - status = RegQueryValueExW(m_key.get(), name.c_str(), nullptr, &type, data.data(), &byteCount); + status = RegGetValueW(m_key.get(), nullptr, name.c_str(), RRF_RT_ANY | RRF_NOEXPAND, &type, data.data(), &byteCount); if (status == ERROR_MORE_DATA && byteCount > data.size()) { diff --git a/src/AppInstallerCommonCore/Synchronization.cpp b/src/AppInstallerCommonCore/Synchronization.cpp index 926fd41349..bb1bc865dc 100644 --- a/src/AppInstallerCommonCore/Synchronization.cpp +++ b/src/AppInstallerCommonCore/Synchronization.cpp @@ -46,7 +46,8 @@ namespace AppInstaller::Synchronization DWORD status = 0; auto lock = result.m_mutex.acquire(&status); - THROW_HR_IF(E_UNEXPECTED, status != WAIT_OBJECT_0); + THROW_HR_IF_NULL(E_UNEXPECTED, lock); + result.m_wasAbandoned = (status == WAIT_ABANDONED); for (LONG i = 0; i < s_CrossProcessReaderWriteLock_MaxReaders; ++i) { diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 7ebe947fa5..b43f013bea 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -126,7 +126,7 @@ namespace AppInstaller::Settings } else { - AICLI_LOG(Core, Info, << "Setting " << path <<" not found. Using default"); + AICLI_LOG(Core, Info, << "Setting " << path << " not found. Using default"); } } @@ -151,6 +151,14 @@ namespace AppInstaller::Settings namespace details { + // Stamps out a validate function that simply returns the input value. +#define WINGET_VALIDATE_PASS_THROUGH(_setting_) \ + std::optional::value_t> \ + SettingMapping::Validate(const SettingMapping::json_t& value) \ + { \ + return value; \ + } + std::optional::value_t> SettingMapping::Validate(const SettingMapping::json_t& value) { @@ -181,35 +189,16 @@ namespace AppInstaller::Settings return {}; } - std::optional::value_t> - SettingMapping::Validate(const SettingMapping::json_t& value) - { - return value; - } - - std::optional::value_t> - SettingMapping::Validate(const SettingMapping::json_t& value) - { - return value; - } - - std::optional::value_t> - SettingMapping::Validate(const SettingMapping::json_t& value) - { - return value; - } - - std::optional::value_t> - SettingMapping::Validate(const SettingMapping::json_t& value) - { - return value; - } - - std::optional::value_t> - SettingMapping::Validate(const SettingMapping::json_t& value) - { - return value; - } + WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) + WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) + WINGET_VALIDATE_PASS_THROUGH(EFExperimentalMSStore) + WINGET_VALIDATE_PASS_THROUGH(EFList) + WINGET_VALIDATE_PASS_THROUGH(EFExperimentalUpgrade) + WINGET_VALIDATE_PASS_THROUGH(EFUninstall) + WINGET_VALIDATE_PASS_THROUGH(EFImport) + WINGET_VALIDATE_PASS_THROUGH(EFExport) + WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) + WINGET_VALIDATE_PASS_THROUGH(EFRestSource) } UserSettings::UserSettings() : m_type(UserSettingsType::Default) diff --git a/src/AppInstallerCommonCore/Yaml.cpp b/src/AppInstallerCommonCore/Yaml.cpp index d6105b0405..74f4b5d773 100644 --- a/src/AppInstallerCommonCore/Yaml.cpp +++ b/src/AppInstallerCommonCore/Yaml.cpp @@ -244,6 +244,12 @@ namespace AppInstaller::YAML return std::stoll(m_scalar); } + int Node::as_dispatch(int*) const + { + // To allow HResult representation + return static_cast(std::stoll(m_scalar, 0, 0)); + } + bool Node::as_dispatch(bool*) const { if (Utility::CaseInsensitiveEquals(m_scalar, "true")) @@ -418,6 +424,14 @@ namespace AppInstaller::YAML return stream.str(); } + void Emitter::Emit(std::ostream& out) + { + Wrapper::Emitter emitter(out); + + emitter.Dump(*m_document); + emitter.Flush(); + } + void Emitter::AppendNode(int id) { if (!m_containers.empty()) diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h index a5dad48fc7..870cc8740d 100644 --- a/src/AppInstallerCommonCore/pch.h +++ b/src/AppInstallerCommonCore/pch.h @@ -10,12 +10,23 @@ #include #include #include +#include #include "TraceLogging.h" #define YAML_DECLARE_STATIC #include +#include + +#pragma warning( push ) +#pragma warning ( disable : 4458 4100 4702 ) +#include +#include +#include +#include +#pragma warning( pop ) + #include #include #include diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index c998b6533b..d9bfa7eafe 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -121,10 +121,10 @@ Disabled - _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + _NO_ASYNCRTIMP;_DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) true true true @@ -138,8 +138,8 @@ - WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + _NO_ASYNCRTIMP;WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) true @@ -151,11 +151,11 @@ MaxSpeed true true - NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + _NO_ASYNCRTIMP;NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\cpprestsdk\cpprestsdk\Release\include;%(AdditionalIncludeDirectories) true true true @@ -195,12 +195,27 @@ + + + + + + + + + + + + + + + @@ -232,6 +247,8 @@ + + @@ -240,6 +257,15 @@ Create + + + + + + + + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index f09efbd432..3210bf6c2c 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -31,6 +31,21 @@ {aef8989c-44fd-4848-ae2c-c81d3f2e2c72} + + {dc8b6163-e9b5-4d05-87bc-d6098afe0f92} + + + {7f16f6e0-12c6-4bb9-885e-3f8b666d5f74} + + + {15a1ee83-4118-40fe-ba43-c54d7185d505} + + + {8a93b62f-d065-4048-b43a-a2b14b70c330} + + + {6bcbaf7a-289f-4d0b-b128-67bef903745c} + @@ -138,6 +153,51 @@ Microsoft + + Microsoft\Schema\1_2 + + + Microsoft\Schema\1_2 + + + Microsoft\Schema\1_2 + + + Microsoft\Schema\1_2 + + + Rest\Schema\1_0 + + + Rest\Schema + + + Rest + + + Rest + + + Rest + + + Rest + + + Rest\Schema\Json + + + Rest\Schema\Json + + + Rest\Schema\Json + + + Rest\Schema\Json + + + Rest\Schema\Json + @@ -209,6 +269,39 @@ Microsoft + + Microsoft\Schema\1_2 + + + Microsoft\Schema\1_2 + + + Rest\Schema\1_0 + + + Rest + + + Rest + + + Rest + + + Rest + + + Rest\Schema\Json + + + Rest\Schema\Json + + + Rest\Schema\Json + + + Rest\Schema\Json + diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index a48d4de937..79a3f2310b 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -106,6 +106,22 @@ namespace AppInstaller::Repository return (latest && (GetVACFromVersion(installed.get()).IsUpdatedBy(GetVACFromVersion(latest.get())))); } + bool IsSame(const IPackage* other) const override + { + const CompositePackage* otherComposite = dynamic_cast(other); + + if (!otherComposite || + static_cast(m_installedPackage) != static_cast(otherComposite->m_installedPackage) || + m_installedPackage && !m_installedPackage->IsSame(otherComposite->m_installedPackage.get()) || + static_cast(m_availablePackage) != static_cast(otherComposite->m_availablePackage) || + m_availablePackage && !m_availablePackage->IsSame(otherComposite->m_availablePackage.get())) + { + return false; + } + + return true; + } + void SetAvailablePackage(std::shared_ptr availablePackage) { m_availablePackage = std::move(availablePackage); @@ -140,7 +156,7 @@ namespace AppInstaller::Repository return {}; }; - Manifest::Manifest GetManifest() const override + Manifest::Manifest GetManifest() override { return {}; } @@ -186,6 +202,18 @@ namespace AppInstaller::Repository // Lie here so that list and upgrade will carry on to be able to output the diagnostic information. return true; } + + bool IsSame(const IPackage* other) const override + { + const UnknownAvailablePackage* otherUnknown = dynamic_cast(other); + + if (otherUnknown) + { + return true; + } + + return false; + } }; // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. @@ -364,10 +392,10 @@ namespace AppInstaller::Repository }; } - CompositeSource::CompositeSource(std::string identifier) : - m_identifier(identifier) + CompositeSource::CompositeSource(std::string identifier) { m_details.Name = "CompositeSource"; + m_details.Identifier = std::move(identifier); } const SourceDetails& CompositeSource::GetDetails() const @@ -377,7 +405,7 @@ namespace AppInstaller::Repository const std::string& CompositeSource::GetIdentifier() const { - return m_identifier; + return m_details.Identifier; } // The composite search needs to take several steps to get results, and due to the @@ -404,9 +432,10 @@ namespace AppInstaller::Repository m_availableSources.emplace_back(std::move(source)); } - void CompositeSource::SetInstalledSource(std::shared_ptr source) + void CompositeSource::SetInstalledSource(std::shared_ptr source, CompositeSearchBehavior searchBehavior) { m_installedSource = std::move(source); + m_searchBehavior = searchBehavior; } // An installed search first finds all installed packages that match the request, then correlates with available sources. @@ -436,7 +465,7 @@ namespace AppInstaller::Repository { for (const auto& srs : installedPackageData.SystemReferenceStrings) { - systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String)); + systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String.get())); } std::shared_ptr availablePackage; @@ -466,7 +495,7 @@ namespace AppInstaller::Repository { auto id = installedVersion->GetProperty(PackageVersionProperty::Id); - AICLI_LOG(Repo, Info, + AICLI_LOG(Repo, Info, << "Found multiple matches for installed package [" << id << "] in source [" << source->GetIdentifier() << "] when searching for [" << systemReferenceSearch.ToString() << "]"); // More than one match found for the system reference; run some heuristics to check for a match @@ -515,13 +544,14 @@ namespace AppInstaller::Repository // If no package was found that was already in the results, do a correlation lookup with the installed // source to create a new composite package entry if we find any packages there. + bool foundInstalledMatch = false; if (packageData && !packageData->SystemReferenceStrings.empty()) { // Create a search request to run against the installed source SearchRequest systemReferenceSearch; for (const auto& srs : packageData->SystemReferenceStrings) { - systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String)); + systemReferenceSearch.Inclusions.emplace_back(PackageMatchFilter(srs.Field, MatchType::Exact, srs.String.get())); } SearchResult installedCrossRef = m_installedSource->Search(systemReferenceSearch); @@ -532,9 +562,16 @@ namespace AppInstaller::Repository auto installedVersion = crossRef.Package->GetInstalledVersion(); auto installedPackageData = result.ReserveInstalledPackageSlot(installedVersion.get()); + foundInstalledMatch = true; result.Matches.emplace_back(std::make_shared(std::move(crossRef.Package), std::move(match.Package)), match.MatchCriteria); } } + + // If there was no correlation for this package, add it without one. + if (m_searchBehavior == CompositeSearchBehavior::AllPackages && !foundInstalledMatch) + { + result.Matches.push_back(std::move(match)); + } } SortResultMatches(result.Matches); diff --git a/src/AppInstallerRepositoryCore/CompositeSource.h b/src/AppInstallerRepositoryCore/CompositeSource.h index 905c89a98a..1b5dc1e92e 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.h +++ b/src/AppInstallerRepositoryCore/CompositeSource.h @@ -41,7 +41,7 @@ namespace AppInstaller::Repository void AddAvailableSource(std::shared_ptr source); // Sets the installed source to be composited. - void SetInstalledSource(std::shared_ptr source); + void SetInstalledSource(std::shared_ptr source, CompositeSearchBehavior searchBehavior = CompositeSearchBehavior::Installed); private: // Performs a search when an installed source is present. @@ -57,7 +57,7 @@ namespace AppInstaller::Repository std::shared_ptr m_installedSource; std::vector> m_availableSources; SourceDetails m_details; - std::string m_identifier; + CompositeSearchBehavior m_searchBehavior; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp index 8d1b470579..b55240ac5c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -5,16 +5,16 @@ namespace AppInstaller::Repository::Microsoft { - Registry::Key ARPHelper::GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const + Registry::Key ARPHelper::GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const { HKEY rootKey = NULL; switch (scope) { - case Manifest::ManifestInstaller::ScopeEnum::User: + case Manifest::ScopeEnum::User: rootKey = HKEY_CURRENT_USER; break; - case Manifest::ManifestInstaller::ScopeEnum::Machine: + case Manifest::ScopeEnum::Machine: rootKey = HKEY_LOCAL_MACHINE; break; default: @@ -38,7 +38,7 @@ namespace AppInstaller::Repository::Microsoft switch (architecture) { case Utility::Architecture::X86: - if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + if (scope == Manifest::ScopeEnum::Machine) { access |= KEY_WOW64_32KEY; isValid = true; @@ -62,7 +62,7 @@ namespace AppInstaller::Repository::Microsoft switch (architecture) { case Utility::Architecture::X86: - if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + if (scope == Manifest::ScopeEnum::Machine) { #ifdef _ARM_ // Not accessible if this is an ARM process @@ -99,6 +99,7 @@ namespace AppInstaller::Repository::Microsoft std::string ARPHelper::DetermineVersion(const Registry::Key& arpKey) const { + // First check DisplayVersion for a complete version string auto displayVersion = arpKey[DisplayVersion]; if (displayVersion && displayVersion->GetType() == Registry::Value::Type::String) { @@ -109,6 +110,36 @@ namespace AppInstaller::Repository::Microsoft } } + // Next attempt VersionMajor.VersionMinor, then MajorVersion.MinorVersion + for (const auto& names : { std::make_pair(std::ref(VersionMajor), std::ref(VersionMinor)), std::make_pair(std::ref(MajorVersion), std::ref(MinorVersion)) }) + { + auto majorVersion = arpKey[names.first]; + auto minorVersion = arpKey[names.second]; + if (majorVersion || minorVersion) + { + uint32_t majorVersionInt = 0; + uint32_t minorVersionInt = 0; + + if (majorVersion && majorVersion->GetType() == Registry::Value::Type::DWord) + { + majorVersionInt = majorVersion->GetValue(); + } + + if (minorVersion && minorVersion->GetType() == Registry::Value::Type::DWord) + { + minorVersionInt = minorVersion->GetValue(); + } + + if (majorVersionInt || minorVersionInt) + { + std::ostringstream strstr; + strstr << majorVersionInt << '.' << minorVersionInt; + return strstr.str(); + } + } + } + + // Finally attempt to turn the Version DWORD into a version string auto version = arpKey[Version]; if (version && version->GetType() == Registry::Value::Type::DWord) { @@ -121,40 +152,31 @@ namespace AppInstaller::Repository::Microsoft } } - auto majorVersion = arpKey[VersionMajor]; - auto minorVersion = arpKey[VersionMinor]; - if (majorVersion || minorVersion) + return Utility::Version::CreateUnknown().ToString(); + } + + void ARPHelper::AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata) + { + auto value = key[name]; + if (value) { - uint32_t majorVersionInt = 0; - uint32_t minorVersionInt = 0; + std::string valueString; - if (majorVersion && majorVersion->GetType() == Registry::Value::Type::DWord) + if (value->GetType() == Registry::Value::Type::String) { - majorVersionInt = majorVersion->GetValue(); + valueString = value->GetValue(); } - - if (minorVersion && minorVersion->GetType() == Registry::Value::Type::DWord) + else if (value->GetType() == Registry::Value::Type::ExpandString) { - minorVersionInt = minorVersion->GetValue(); + valueString = value->GetValue(); } - - if (majorVersionInt || minorVersionInt) + else if (value->GetType() == Registry::Value::Type::DWord) { std::ostringstream strstr; - strstr << majorVersionInt << '.' << minorVersionInt; - return strstr.str(); + strstr << value->GetValue(); + valueString = strstr.str(); } - } - return Utility::Version::CreateUnknown().ToString(); - } - - void ARPHelper::AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata) - { - auto value = key[name]; - if (value && value->GetType() == Registry::Value::Type::String) - { - auto valueString = value->GetValue(); if (!valueString.empty()) { index.SetMetadataByManifestId(manifestId, metadata, valueString); @@ -162,7 +184,7 @@ namespace AppInstaller::Repository::Microsoft } } - void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const + void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ScopeEnum scope) const { for (auto architecture : Utility::GetApplicableArchitectures()) { @@ -170,7 +192,7 @@ namespace AppInstaller::Repository::Microsoft if (arpRootKey) { - PopulateIndexFromKey(index, arpRootKey, Manifest::ManifestInstaller::ScopeToString(scope), Utility::ToString(architecture)); + PopulateIndexFromKey(index, arpRootKey, Manifest::ScopeToString(scope), Utility::ToString(architecture)); } } } @@ -184,7 +206,7 @@ namespace AppInstaller::Repository::Microsoft std::string productCode = arpEntry.Name(); Manifest::Manifest manifest; - manifest.Tags = { "ARP" }; + manifest.DefaultLocalization.Add({ "ARP" }); // Use the key name as the Id, as it is supposed to be unique. // TODO: We probably want something better here, like constructing the value as @@ -216,8 +238,9 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is not a REG_SZ value"); continue; } - manifest.Name = displayName->GetValue(); - if (manifest.Name.empty()) + auto displayNameValue = displayName->GetValue(); + manifest.DefaultLocalization.Add(displayNameValue); + if (displayNameValue.empty()) { AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is empty"); continue; @@ -234,37 +257,39 @@ namespace AppInstaller::Repository::Microsoft auto publisher = arpKey[Publisher]; if (publisher && publisher->GetType() == Registry::Value::Type::String) { - manifest.Publisher = publisher->GetValue(); + manifest.DefaultLocalization.Add(publisher->GetValue()); + + // If Publisher is set, change the Id using name normalization + // TODO: Figure out how to actually make this work since there are often instances of the same + // data in x64 and x86 entries that will collide. + //auto normalizedName = index.NormalizeName( + // manifest.DefaultLocalization.Get(), + // manifest.DefaultLocalization.Get()); + //manifest.Id = normalizedName.Publisher() + '.' + normalizedName.Name(); } // TODO: If we want to keep the constructed manifest around to allow for `show` type commands // against installed packages, we should use URLInfoAbout/HelpLink for the Homepage. - // TODO: Pick up Language/InnoSetupLanguage to enable proper selection of language for upgrade. - - // TODO: Determine the best way to handle duplicates, which may very well happen. - // For now, we will attempt to insert and catch, then send failure telemetry. - // In a future where we cache these entries + // TODO: Determine the best way to handle duplicates; sometimes the same package will be listed under + // both x64 and x86 locations for ARP. + // For now, we will attempt to insert and catch. std::optional manifestIdOpt; - HRESULT addHr = S_OK; try { // Use the ProductCode as a unique key for the path manifestIdOpt = index.AddManifest(manifest, Utility::ConvertToUTF16(manifest.Installers[0].ProductCode)); } - catch (wil::ResultException& re) - { - addHr = re.GetErrorCode(); - } catch (...) { - addHr = E_FAIL; + // Ignore errors if they occur, they are most likely a duplicate value } if (!manifestIdOpt) { - Logging::Telemetry().LogDuplicateARPEntry(addHr, scope, architecture, productCode, manifest.Name); + AICLI_LOG(Repo, Warning, + << "Ignoring duplicate ARP entry " << scope << '|' << architecture << '|' << productCode << " [" << manifest.DefaultLocalization.Get() << "]"); continue; } @@ -277,6 +302,14 @@ namespace AppInstaller::Repository::Microsoft // is from it's ARP location, despite it very clearly being a specific architecture. And note that user // scope does not have separate ARP locations, so every architecture would appear as native. + // Publisher is needed for certain scenarios but we don't store it from the manifest + if (manifest.DefaultLocalization.Contains(Manifest::Localization::Publisher)) + { + index.SetMetadataByManifestId( + manifestId, PackageVersionMetadata::Publisher, + manifest.DefaultLocalization.Get()); + } + // Pick up InstallLocation when upgrade supports remove/install to enable this location // to survive across the removal. AddMetadataIfPresent(arpKey, InstallLocation, index, manifestId, PackageVersionMetadata::InstalledLocation); @@ -285,16 +318,20 @@ namespace AppInstaller::Repository::Microsoft AddMetadataIfPresent(arpKey, UninstallString, index, manifestId, PackageVersionMetadata::StandardUninstallCommand); AddMetadataIfPresent(arpKey, QuietUninstallString, index, manifestId, PackageVersionMetadata::SilentUninstallCommand); + // Pick up Language to enable proper selection of language for upgrade. + // TODO: Determine if InnoSetupLanguage represents the same concept and pick it up if language is not present. + AddMetadataIfPresent(arpKey, Language, index, manifestId, PackageVersionMetadata::Locale); + // Pick up WindowsInstaller to determine if this is an MSI install. // TODO: Could also determine Inno (and maybe other types) through detecting other keys here. - auto installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Exe; + auto installedType = Manifest::InstallerTypeEnum::Exe; if (GetBoolValue(arpKey, WindowsInstaller)) { - installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Msi; + installedType = Manifest::InstallerTypeEnum::Msi; } - index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::ManifestInstaller::InstallerTypeToString(installedType)); + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::InstallerTypeToString(installedType)); } } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h index 6074158902..f0ff77a530 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h @@ -29,6 +29,10 @@ namespace AppInstaller::Repository::Microsoft std::wstring VersionMajor{ L"VersionMajor" }; // REG_DWORD std::wstring VersionMinor{ L"VersionMinor" }; + // REG_DWORD + std::wstring MajorVersion{ L"MajorVersion" }; + // REG_DWORD + std::wstring MinorVersion{ L"MinorVersion" }; // REG_SZ std::wstring URLInfoAbout{ L"URLInfoAbout" }; // REG_SZ @@ -50,7 +54,7 @@ namespace AppInstaller::Repository::Microsoft // Gets the registry key associated with the given scope and architecture on this platform. // May return an empty key if there is no valid location (bad combination or not found). - Registry::Key GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const; + Registry::Key GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const; // Returns true IFF the value exists and contains a non-zero DWORD. static bool GetBoolValue(const Registry::Key& arpKey, const std::wstring& name); @@ -67,7 +71,7 @@ namespace AppInstaller::Repository::Microsoft // Populates the index with the ARP entries from the given scope (machine/user). // Handles all of the architectures for the given scope. - void PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const; + void PopulateIndexFromARP(SQLiteIndex& index, Manifest::ScopeEnum scope) const; // Populates the index with the ARP entries from the given key. // This entry point is primarily to allow unit tests to operate of arbitrary keys; diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index cd6646cc82..0948198217 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -86,6 +86,7 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Found package full name: " << details.Name << " => " << fullName); details.Data = Msix::GetPackageFamilyNameFromFullName(fullName); + details.Identifier = Msix::GetPackageFamilyNameFromFullName(fullName); auto lock = Synchronization::CrossProcessReaderWriteLock::LockForWrite(CreateNameForCPRWL(details)); @@ -146,6 +147,8 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::Open(indexLocation.u8string(), SQLiteIndex::OpenDisposition::Immutable); + // We didn't use to store the source identifier, so we compute it here in case it's + // missing from the details. return std::make_shared(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } @@ -252,6 +255,8 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::Open(packageLocation.u8string(), SQLiteIndex::OpenDisposition::Read); + // We didn't use to store the source identifier, so we compute it here in case it's + // missing from the details. return std::make_shared(details, GetPackageFamilyNameFromDetails(details), std::move(index), std::move(lock)); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp index 72c7588c18..d03ab265da 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -33,7 +33,7 @@ namespace AppInstaller::Repository::Microsoft // Add one installer for storing the package family name. manifest.Installers.emplace_back(); // Every package will have the same tags currently. - manifest.Tags = { "msix" }; + manifest.DefaultLocalization.Add({ "msix" }); // Fields in the index but not populated: // AppMoniker - Not sure what we would put. @@ -55,11 +55,17 @@ namespace AppInstaller::Repository::Microsoft manifest.Id = familyName; + bool isPackageNameSet = false; // Attempt to get the DisplayName. Since this will retrieve the localized value, it has a chance to fail. // Rather than completely skip this package in that case, we will simply fall back to using the package name below. try { - manifest.Name = Utility::ConvertToUTF8(package.DisplayName()); + auto displayName = Utility::ConvertToUTF8(package.DisplayName()); + if (!displayName.empty()) + { + manifest.DefaultLocalization.Add(displayName); + isPackageNameSet = true; + } } catch (const winrt::hresult_error& hre) { @@ -71,9 +77,9 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Unknown exception thrown when getting DisplayName for " << familyName); } - if (manifest.Name.empty()) + if (!isPackageNameSet) { - manifest.Name = Utility::ConvertToUTF8(packageId.Name()); + manifest.DefaultLocalization.Add(Utility::ConvertToUTF8(packageId.Name())); } std::ostringstream strstr; @@ -88,7 +94,7 @@ namespace AppInstaller::Repository::Microsoft auto manifestId = index.AddManifest(manifest, std::filesystem::path{ packageId.FamilyName().c_str() }); index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, - Manifest::ManifestInstaller::InstallerTypeToString(Manifest::ManifestInstaller::InstallerTypeEnum::Msix)); + Manifest::InstallerTypeToString(Manifest::InstallerTypeEnum::Msix)); } } @@ -110,26 +116,11 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, Schema::Version::Latest()); // Put installed packages into the index - std::optional arpHelper; - - if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_System) - { - if (!arpHelper) - { - arpHelper = ARPHelper(); - } - - arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::Machine); - } - - if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_User) + if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP) { - if (!arpHelper) - { - arpHelper = ARPHelper(); - } - - arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::User); + ARPHelper arpHelper; + arpHelper.PopulateIndexFromARP(index, Manifest::ScopeEnum::Machine); + arpHelper.PopulateIndexFromARP(index, Manifest::ScopeEnum::User); } if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::MSIX) @@ -166,10 +157,8 @@ namespace AppInstaller::Repository::Microsoft { case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::None: return "None"sv; - case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_System: - return "ARP_System"sv; - case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_User: - return "ARP_User"sv; + case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::ARP: + return "ARP"sv; case AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory::Filter::MSIX: return "MSIX"sv; default: @@ -179,13 +168,9 @@ namespace AppInstaller::Repository::Microsoft PredefinedInstalledSourceFactory::Filter PredefinedInstalledSourceFactory::StringToFilter(std::string_view filter) { - if (filter == FilterToString(Filter::ARP_System)) - { - return Filter::ARP_System; - } - else if (filter == FilterToString(Filter::ARP_User)) + if (filter == FilterToString(Filter::ARP)) { - return Filter::ARP_User; + return Filter::ARP; } else if (filter == FilterToString(Filter::MSIX)) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h index 1b816e1e69..30c3b0174a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.h @@ -25,8 +25,7 @@ namespace AppInstaller::Repository::Microsoft enum class Filter { None, - ARP_System, - ARP_User, + ARP, MSIX, }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index ca4bcba8fb..459f3e3576 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -133,7 +133,7 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex::IdType SQLiteIndex::AddManifest(const std::filesystem::path& manifestPath, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Adding manifest from file [" << manifestPath << "]"); + AICLI_LOG(Repo, Verbose, << "Adding manifest from file [" << manifestPath << "]"); Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); return AddManifest(manifest, relativePath); @@ -141,7 +141,7 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex::IdType SQLiteIndex::AddManifest(const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Adding manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); + AICLI_LOG(Repo, Verbose, << "Adding manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "sqliteindex_addmanifest"); @@ -156,7 +156,7 @@ namespace AppInstaller::Repository::Microsoft bool SQLiteIndex::UpdateManifest(const std::filesystem::path& manifestPath, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Updating manifest from file [" << manifestPath << "]"); + AICLI_LOG(Repo, Verbose, << "Updating manifest from file [" << manifestPath << "]"); Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); return UpdateManifest(manifest, relativePath); @@ -164,7 +164,7 @@ namespace AppInstaller::Repository::Microsoft bool SQLiteIndex::UpdateManifest(const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Updating manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); + AICLI_LOG(Repo, Verbose, << "Updating manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "sqliteindex_updatemanifest"); @@ -182,7 +182,7 @@ namespace AppInstaller::Repository::Microsoft void SQLiteIndex::RemoveManifest(const std::filesystem::path& manifestPath, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Removing manifest from file [" << manifestPath << "]"); + AICLI_LOG(Repo, Verbose, << "Removing manifest from file [" << manifestPath << "]"); Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(manifestPath); RemoveManifest(manifest, relativePath); @@ -190,7 +190,7 @@ namespace AppInstaller::Repository::Microsoft void SQLiteIndex::RemoveManifest(const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) { - AICLI_LOG(Repo, Info, << "Removing manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); + AICLI_LOG(Repo, Verbose, << "Removing manifest for [" << manifest.Id << ", " << manifest.Version << "] at relative path [" << relativePath << "]"); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "sqliteindex_removemanifest"); @@ -221,7 +221,7 @@ namespace AppInstaller::Repository::Microsoft Schema::ISQLiteIndex::SearchResult SQLiteIndex::Search(const SearchRequest& request) const { - AICLI_LOG(Repo, Info, << "Performing search: " << request.ToString()); + AICLI_LOG(Repo, Verbose, << "Performing search: " << request.ToString()); return m_interface->Search(m_dbconn, request); } @@ -256,6 +256,11 @@ namespace AppInstaller::Repository::Microsoft m_interface->SetMetadataByManifestId(m_dbconn, manifestId, metadata, value); } + Utility::NormalizedName SQLiteIndex::NormalizeName(std::string_view name, std::string_view publisher) const + { + return m_interface->NormalizeName(name, publisher); + } + // Recording last write time based on MSDN documentation stating that time returns a POSIX epoch time and thus // should be consistent across systems. void SQLiteIndex::SetLastWriteTime() diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index 54a1da4081..297253c6c7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -121,6 +122,10 @@ namespace AppInstaller::Repository::Microsoft // Sets the string for the given metadata and manifest id. void SetMetadataByManifestId(IdType manifestId, PackageVersionMetadata metadata, std::string_view value); + // Normalizes a name using the internal rules used by the index. + // Largely a utility function; should not be used to do work on behalf of the index by the caller. + Utility::NormalizedName NormalizeName(std::string_view name, std::string_view publisher) const; + private: // Constructor used to open an existing index. SQLiteIndex(const std::string& target, SQLite::Connection::OpenDisposition disposition, SQLite::Connection::OpenFlags flags); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp index f5d46d3794..cc67074ba9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -64,7 +64,7 @@ namespace AppInstaller::Repository::Microsoft return result; } - Manifest::Manifest GetManifest() const override + Manifest::Manifest GetManifest() override { std::shared_ptr source = GetReferenceSource(); std::optional relativePathOpt = source->GetIndex().GetPropertyByManifestId(m_manifestId, PackageVersionProperty::RelativePath); @@ -106,7 +106,34 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Downloading manifest"); ProgressCallback emptyCallback; - (void)Utility::DownloadToStream(fullPath, manifestStream, emptyCallback); + + const int MaxRetryCount = 2; + for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) + { + bool success = false; + try + { + (void)Utility::DownloadToStream(fullPath, manifestStream, emptyCallback); + success = true; + } + catch (...) + { + if (retryCount < MaxRetryCount - 1) + { + AICLI_LOG(Repo, Info, << "Downloading manifest failed, waiting a bit and retrying: " << fullPath); + Sleep(500); + } + else + { + throw; + } + } + + if (success) + { + break; + } + } std::string manifestContents = manifestStream.str(); AICLI_LOG(Repo, Verbose, << "Manifest contents: " << manifestContents); @@ -150,6 +177,11 @@ namespace AppInstaller::Repository::Microsoft return result; } + bool IsSame(const PackageBase& other) const + { + return m_idId == other.m_idId; + } + protected: std::shared_ptr GetLatestVersionInternal() const { @@ -225,6 +257,18 @@ namespace AppInstaller::Repository::Microsoft { return false; } + + bool IsSame(const IPackage* other) const override + { + const AvailablePackage* otherAvailable = dynamic_cast(other); + + if (otherAvailable) + { + return PackageBase::IsSame(*otherAvailable); + } + + return false; + } }; // The IPackage impl for SQLiteIndexSource of Installed packages. @@ -262,12 +306,25 @@ namespace AppInstaller::Repository::Microsoft { return false; } + + bool IsSame(const IPackage* other) const override + { + const InstalledPackage* otherInstalled = dynamic_cast(other); + + if (otherInstalled) + { + return PackageBase::IsSame(*otherInstalled); + } + + return false; + } }; } SQLiteIndexSource::SQLiteIndexSource(const SourceDetails& details, std::string identifier, SQLiteIndex&& index, Synchronization::CrossProcessReaderWriteLock&& lock, bool isInstalledSource) : - m_details(details), m_identifier(std::move(identifier)), m_lock(std::move(lock)), m_isInstalled(isInstalledSource), m_index(std::move(index)) + m_details(details), m_lock(std::move(lock)), m_isInstalled(isInstalledSource), m_index(std::move(index)) { + m_details.Identifier = std::move(identifier); } const SourceDetails& SQLiteIndexSource::GetDetails() const @@ -277,7 +334,7 @@ namespace AppInstaller::Repository::Microsoft const std::string& SQLiteIndexSource::GetIdentifier() const { - return m_identifier; + return m_details.Identifier; } SearchResult SQLiteIndexSource::Search(const SearchRequest& request) const diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h index 69f854d043..71b7821a2d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.h @@ -39,7 +39,6 @@ namespace AppInstaller::Repository::Microsoft private: SourceDetails m_details; - std::string m_identifier; Synchronization::CrossProcessReaderWriteLock m_lock; bool m_isInstalled; SQLiteIndex m_index; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h index 3e29963320..1a1fa07a62 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h @@ -31,6 +31,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 MetadataResult GetMetadataByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId) const override; void SetMetadataByManifestId(SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMetadata metadata, std::string_view value) override; + // Version 1.2 + Utility::NormalizedName NormalizeName(std::string_view name, std::string_view publisher) const override; + protected: // Creates the search results table. virtual std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const; @@ -38,7 +41,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Gets the ordering of matches to execute, with more specific matches coming first. virtual std::vector GetMatchTypeOrder(MatchType type) const; - // Executes all relevant searchs for the query. + // Executes all relevant searches for the query. virtual void PerformQuerySearch(SearchResultsTable& resultsTable, const RequestMatch& query) const; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp index 8bcf1ec77b..68be0076a0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp @@ -29,21 +29,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 std::optional idId = IdTable::SelectIdByValue(connection, manifest.Id, true); if (!idId) { - AICLI_LOG(Repo, Info, << "Did not find an Id { " << manifest.Id << " }"); + AICLI_LOG(Repo, Verbose, << "Did not find an Id { " << manifest.Id << " }"); return {}; } std::optional versionId = VersionTable::SelectIdByValue(connection, manifest.Version, true); if (!versionId) { - AICLI_LOG(Repo, Info, << "Did not find a Version { " << manifest.Version << " }"); + AICLI_LOG(Repo, Verbose, << "Did not find a Version { " << manifest.Version << " }"); return {}; } std::optional channelId = ChannelTable::SelectIdByValue(connection, manifest.Channel, true); if (!channelId) { - AICLI_LOG(Repo, Info, << "Did not find a Channel { " << manifest.Channel << " }"); + AICLI_LOG(Repo, Verbose, << "Did not find a Channel { " << manifest.Channel << " }"); return {}; } @@ -51,7 +51,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 if (!result) { - AICLI_LOG(Repo, Info, << "Did not find a manifest row for { " << manifest.Id << ", " << manifest.Version << ", " << manifest.Channel << " }"); + AICLI_LOG(Repo, Verbose, << "Did not find a manifest row for { " << manifest.Id << ", " << manifest.Version << ", " << manifest.Channel << " }"); } return result; @@ -185,8 +185,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Ensure that all of the 1:1 data exists. SQLite::rowid_t idId = IdTable::EnsureExists(connection, manifest.Id, true); - SQLite::rowid_t nameId = NameTable::EnsureExists(connection, manifest.Name); - SQLite::rowid_t monikerId = MonikerTable::EnsureExists(connection, manifest.AppMoniker); + SQLite::rowid_t nameId = NameTable::EnsureExists(connection, manifest.DefaultLocalization.Get()); + SQLite::rowid_t monikerId = MonikerTable::EnsureExists(connection, manifest.Moniker); SQLite::rowid_t versionId = VersionTable::EnsureExists(connection, manifest.Version); SQLite::rowid_t channelId = ChannelTable::EnsureExists(connection, manifest.Channel); @@ -201,8 +201,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 }); // Add all of the 1:N data. - TagsTable::EnsureExistsAndInsert(connection, manifest.Tags, manifestId); - CommandsTable::EnsureExistsAndInsert(connection, manifest.Commands, manifestId); + TagsTable::EnsureExistsAndInsert(connection, manifest.GetAggregatedTags(), manifestId); + CommandsTable::EnsureExistsAndInsert(connection, manifest.GetAggregatedCommands(), manifestId); savepoint.Commit(); @@ -243,15 +243,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 indexModified = true; } - if (nameInIndex != manifest.Name) + auto packageName = manifest.DefaultLocalization.Get(); + if (nameInIndex != packageName) { - UpdateManifestValueById(connection, manifest.Name, manifestId); + UpdateManifestValueById(connection, packageName, manifestId); indexModified = true; } - if (monikerInIndex != manifest.AppMoniker) + if (monikerInIndex != manifest.Moniker) { - UpdateManifestValueById(connection, manifest.AppMoniker, manifestId); + UpdateManifestValueById(connection, manifest.Moniker, manifestId); indexModified = true; } @@ -273,8 +274,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // Update all 1:N tables as necessary - indexModified = TagsTable::UpdateIfNeededByManifestId(connection, manifest.Tags, manifestId) || indexModified; - indexModified = CommandsTable::UpdateIfNeededByManifestId(connection, manifest.Commands, manifestId) || indexModified; + indexModified = TagsTable::UpdateIfNeededByManifestId(connection, manifest.GetAggregatedTags(), manifestId) || indexModified; + indexModified = CommandsTable::UpdateIfNeededByManifestId(connection, manifest.GetAggregatedCommands(), manifestId) || indexModified; savepoint.Commit(); @@ -412,7 +413,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SearchResult result; for (SQLite::rowid_t id : ids) { - result.Matches.emplace_back(std::make_pair(id, PackageMatchFilter(PackageMatchField::Id, MatchType::Wildcard, {}))); + result.Matches.emplace_back(std::make_pair(id, PackageMatchFilter(PackageMatchField::Id, MatchType::Wildcard))); } result.Truncated = (request.MaximumResults && IdTable::GetCount(connection) > request.MaximumResults); @@ -437,11 +438,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 if (!request.Inclusions.empty()) { - for (const auto& include : request.Inclusions) + for (auto include : request.Inclusions) { for (MatchType match : GetMatchTypeOrder(include.Type)) { - resultsTable->SearchOnField(include.Field, match, include.Value); + include.Type = match; + resultsTable->SearchOnField(include); } } @@ -454,11 +456,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 THROW_HR_IF(E_UNEXPECTED, request.Filters.empty()); // Perform search for just the field matching the first filter - const PackageMatchFilter& filter = request.Filters[0]; + PackageMatchFilter filter = request.Filters[0]; for (MatchType match : GetMatchTypeOrder(filter.Type)) { - resultsTable->SearchOnField(filter.Field, match, filter.Value); + filter.Type = match; + resultsTable->SearchOnField(filter); } // Skip the filter as we already know everything matches @@ -471,13 +474,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Second phase, for remaining filters, flag matching search results, then remove unflagged values. for (size_t i = filterIndex; i < request.Filters.size(); ++i) { - const PackageMatchFilter& filter = request.Filters[i]; + PackageMatchFilter filter = request.Filters[i]; resultsTable->PrepareToFilter(); for (MatchType match : GetMatchTypeOrder(filter.Type)) { - resultsTable->FilterOnField(filter.Field, match, filter.Value); + filter.Type = match; + resultsTable->FilterOnField(filter); } resultsTable->CompleteFilter(); @@ -546,6 +550,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { } + Utility::NormalizedName Interface::NormalizeName(std::string_view name, std::string_view publisher) const + { + Utility::NormalizedName result; + result.Name(name); + result.Publisher(publisher); + return result; + } + std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const { return std::make_unique(connection); @@ -576,13 +588,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void Interface::PerformQuerySearch(SearchResultsTable& resultsTable, const RequestMatch& query) const { + // Arbitrary values to create a reusable filter with the given value. + PackageMatchFilter filter(PackageMatchField::Id, MatchType::Exact, query.Value); + for (MatchType match : GetMatchTypeOrder(query.Type)) { - resultsTable.SearchOnField(PackageMatchField::Id, match, query.Value); - resultsTable.SearchOnField(PackageMatchField::Name, match, query.Value); - resultsTable.SearchOnField(PackageMatchField::Moniker, match, query.Value); - resultsTable.SearchOnField(PackageMatchField::Command, match, query.Value); - resultsTable.SearchOnField(PackageMatchField::Tag, match, query.Value); + filter.Type = match; + + for (auto field : { PackageMatchField::Id, PackageMatchField::Name, PackageMatchField::Moniker, PackageMatchField::Command, PackageMatchField::Tag }) + { + filter.Field = field; + resultsTable.SearchOnField(filter); + } } } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp index 51c6b543d0..960dfd5c7a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp @@ -165,10 +165,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return result; } - int ManifestTableBuildSearchStatement( + std::vector ManifestTableBuildSearchStatement( SQLite::Builder::StatementBuilder& builder, - const SQLite::Builder::QualifiedColumn& column, - bool isOneToOne, + std::initializer_list columns, + std::initializer_list isOneToOnes, std::string_view manifestAlias, std::string_view valueAlias, bool useLike) @@ -176,40 +176,78 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 using QCol = SQLite::Builder::QualifiedColumn; // Build a statement like: - // SELECT manifest.rowid as m, ids.id as v from manifest join ids on manifest.id = ids.rowid where ids.id = + // SELECT manifest.rowid as m, ids.id as v from manifest + // join ids on manifest.id = ids.rowid + // where ids.id = // OR - // SELECT manifest.rowid as m, tags.tag as v from manifest join tags_map on manifest.rowid = tags_map.manifest - // join tags on tags_map.tag = tags.rowid where tags.tag = + // SELECT manifest.rowid as m, tags.tag as v from manifest + // join tags_map on manifest.rowid = tags_map.manifest + // join tags on tags_map.tag = tags.rowid + // where tags.tag = + // Where the joins and where portions are repeated for each table in question. builder.Select(). - Column(QCol(s_ManifestTable_Table_Name, SQLite::RowIDName)).As(manifestAlias). - Column(column).As(valueAlias); + Column(QCol(s_ManifestTable_Table_Name, SQLite::RowIDName)).As(manifestAlias); - if (isOneToOne) + // Value will be captured for single tables references, and left empty for multi-tables + if (columns.size() == 1) { - builder.From(s_ManifestTable_Table_Name). - Join(column.Table).On(QCol(s_ManifestTable_Table_Name, column.Column), QCol(column.Table, SQLite::RowIDName)). - Where(column); + builder.Column(*columns.begin()); } else { - std::string mapTableName = details::OneToManyTableGetMapTableName(column.Table); - builder.From(s_ManifestTable_Table_Name). - Join(mapTableName).On(QCol(s_ManifestTable_Table_Name, SQLite::RowIDName), QCol(mapTableName, details::OneToManyTableGetManifestColumnName())). - Join(column.Table).On(QCol(mapTableName, column.Column), QCol(column.Table, SQLite::RowIDName)). - Where(column); + builder.LiteralColumn(""); } - int result = 0; - if (useLike) + builder.As(valueAlias).From(s_ManifestTable_Table_Name); + + // Create join clauses + THROW_HR_IF(E_INVALIDARG, columns.size() != isOneToOnes.size()); + auto columnItr = columns.begin(); + auto isOneToOneItr = isOneToOnes.begin(); + + for (; columnItr != columns.end(); ++columnItr, ++isOneToOneItr) { - builder.Like(SQLite::Builder::Unbound); - result = builder.GetLastBindIndex(); - builder.Escape(SQLite::EscapeCharForLike); + const SQLite::Builder::QualifiedColumn& column = *columnItr; + + if (*isOneToOneItr) + { + builder. + Join(column.Table).On(QCol(s_ManifestTable_Table_Name, column.Column), QCol(column.Table, SQLite::RowIDName)); + } + else + { + std::string mapTableName = details::OneToManyTableGetMapTableName(column.Table); + builder. + Join(mapTableName).On(QCol(s_ManifestTable_Table_Name, SQLite::RowIDName), QCol(mapTableName, details::OneToManyTableGetManifestColumnName())). + Join(column.Table).On(QCol(mapTableName, column.Column), QCol(column.Table, SQLite::RowIDName)); + } } - else + + std::vector result; + + // Create where clause + for (const SQLite::Builder::QualifiedColumn& column : columns) { - builder.Equals(SQLite::Builder::Unbound); - result = builder.GetLastBindIndex(); + if (result.empty()) + { + builder.Where(column); + } + else + { + builder.And(column); + } + + if (useLike) + { + builder.Like(SQLite::Builder::Unbound); + result.push_back(builder.GetLastBindIndex()); + builder.Escape(SQLite::EscapeCharForLike); + } + else + { + builder.Equals(SQLite::Builder::Unbound); + result.push_back(builder.GetLastBindIndex()); + } } return result; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h index b7841f8932..b2c8c62284 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h @@ -46,10 +46,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 std::initializer_list ids); // Builds the search select statement base on the given values. - int ManifestTableBuildSearchStatement( + std::vector ManifestTableBuildSearchStatement( SQLite::Builder::StatementBuilder& builder, - const SQLite::Builder::QualifiedColumn& column, - bool isOneToOne, + std::initializer_list columns, + std::initializer_list isOneToOnes, std::string_view manifestAlias, std::string_view valueAlias, bool useLike); @@ -138,10 +138,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // Builds the search select statement base on the given values. - template - static int BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, std::string_view manifestAlias, std::string_view valueAlias, bool useLike) + // If more than one table is provided, no value will be captured. + // The return value is the bind indices of the values to match against. + template + static std::vector BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, std::string_view manifestAlias, std::string_view valueAlias, bool useLike) { - return details::ManifestTableBuildSearchStatement(builder, SQLite::Builder::QualifiedColumn{ Table::TableName(), Table::ValueName() }, Table::IsOneToOne(), manifestAlias, valueAlias, useLike); + return details::ManifestTableBuildSearchStatement(builder, { SQLite::Builder::QualifiedColumn{ Table::TableName(), Table::ValueName() }... }, { Table::IsOneToOne()... }, manifestAlias, valueAlias, useLike); } // Update the value of a single column for the manifest with the given rowid. diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h index 7011826274..571ceb331c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/NameTable.h @@ -18,5 +18,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // The table for Name. + // TODO: Currently only indexing name from default locale, might need to be OneToMany table using NameTable = OneToOneTable; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h index 6ed63e7fa1..87f7abcce2 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h @@ -25,7 +25,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SearchResultsTable& operator=(SearchResultsTable&&) = default; // Performs the requested search type on the requested field. - void SearchOnField(PackageMatchField field, MatchType match, std::string_view value); + void SearchOnField(const PackageMatchFilter& filter); // Removes rows with manifest ids whose sort order is below the highest one. void RemoveDuplicateManifestRows(); @@ -34,7 +34,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void PrepareToFilter(); // Performs the requested filter type on the requested field. - void FilterOnField(PackageMatchField field, MatchType match, std::string_view value); + void FilterOnField(const PackageMatchFilter& filter); // Completes a filtering pass, removing filtered rows. void CompleteFilter(); @@ -44,15 +44,20 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 protected: // Builds the search statement for the specified field and match type. - std::optional BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, PackageMatchField field, MatchType match) const; + std::vector BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, PackageMatchField field, MatchType match) const; - virtual std::optional BuildSearchStatement( + virtual std::vector BuildSearchStatement( SQLite::Builder::StatementBuilder& builder, PackageMatchField field, std::string_view manifestAlias, std::string_view valueAlias, bool useLike) const; + static bool MatchUsesLike(MatchType match); + void BindStatementForMatchType(SQLite::Statement& statement, MatchType match, int bindIndex, std::string_view value); + + virtual void BindStatementForMatchType(SQLite::Statement& statement, const PackageMatchFilter& filter, const std::vector& bindIndex); + private: const SQLite::Connection& m_connection; int m_sortOrdinalValue = 0; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp index 0e251c9cad..10386b7ee0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp @@ -32,49 +32,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 constexpr std::string_view s_SearchResultsTable_SubSelect_TableAlias = "valueTable"sv; constexpr std::string_view s_SearchResultsTable_SubSelect_ManifestAlias = "m"sv; constexpr std::string_view s_SearchResultsTable_SubSelect_ValueAlias = "v"sv; - - bool MatchUsesLike(MatchType match) - { - return (match != MatchType::Exact); - } - - void ExecuteStatementForMatchType(SQLite::Statement& statement, MatchType match, int bindIndex, bool escapeValueForLike, std::string_view value) - { - // TODO: Implement these more complex match types - if (match == MatchType::Wildcard || match == MatchType::Fuzzy || match == MatchType::FuzzySubstring) - { - AICLI_LOG(Repo, Verbose, << "Specific match type not implemented, skipping: " << MatchTypeToString(match)); - return; - } - - std::string valueToUse; - - if (escapeValueForLike) - { - valueToUse = SQLite::EscapeStringForLike(value); - } - else - { - valueToUse = value; - } - - switch (match) - { - case AppInstaller::Repository::MatchType::StartsWith: - valueToUse += '%'; - break; - case AppInstaller::Repository::MatchType::Substring: - valueToUse = "%"s + valueToUse + '%'; - break; - default: - // No changes required for others. - break; - } - - statement.Bind(bindIndex, valueToUse); - - statement.Execute(); - } } SearchResultsTable::SearchResultsTable(const SQLite::Connection& connection) : @@ -113,7 +70,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } } - void SearchResultsTable::SearchOnField(PackageMatchField field, MatchType match, std::string_view value) + void SearchResultsTable::SearchOnField(const PackageMatchFilter& filter) { using namespace SQLite::Builder; @@ -128,26 +85,27 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 StatementBuilder builder; builder.InsertInto(GetQualifiedName()).Select(). Column(QualifiedColumn(s_SearchResultsTable_SubSelect_TableAlias, s_SearchResultsTable_SubSelect_ManifestAlias)). - Value(field). - Value(match). + Value(filter.Field). + Value(filter.Type). Column(QualifiedColumn(s_SearchResultsTable_SubSelect_TableAlias, s_SearchResultsTable_SubSelect_ValueAlias)). Value(sortOrdinal). Value(false). From().BeginParenthetical(); // Add the field specific portion - std::optional bindIndex = BuildSearchStatement(builder, field, match); + std::vector bindIndex = BuildSearchStatement(builder, filter.Field, filter.Type); - if (!bindIndex) + if (bindIndex.empty()) { - AICLI_LOG(Repo, Verbose, << "PackageMatchField not supported in this version: " << PackageMatchFieldToString(field)); + AICLI_LOG(Repo, Verbose, << "PackageMatchField not supported in this version: " << PackageMatchFieldToString(filter.Field)); return; } builder.EndParenthetical().As(s_SearchResultsTable_SubSelect_TableAlias); SQLite::Statement statement = builder.Prepare(m_connection); - ExecuteStatementForMatchType(statement, match, bindIndex.value(), MatchUsesLike(match), value); + BindStatementForMatchType(statement, filter, bindIndex); + statement.Execute(); AICLI_LOG(Repo, Verbose, << "Search found " << m_connection.GetChanges() << " rows"); } @@ -183,7 +141,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.Execute(m_connection); } - void SearchResultsTable::FilterOnField(PackageMatchField field, MatchType match, std::string_view value) + void SearchResultsTable::FilterOnField(const PackageMatchFilter& filter) { using namespace SQLite::Builder; @@ -200,18 +158,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 Select(s_SearchResultsTable_SubSelect_ManifestAlias).From().BeginParenthetical(); // Add the field specific portion - std::optional bindIndex = BuildSearchStatement(builder, field, match); + std::vector bindIndex = BuildSearchStatement(builder, filter.Field, filter.Type); - if (!bindIndex) + if (bindIndex.empty()) { - AICLI_LOG(Repo, Verbose, << "PackageMatchField not supported in this version: " << PackageMatchFieldToString(field)); + AICLI_LOG(Repo, Verbose, << "PackageMatchField not supported in this version: " << PackageMatchFieldToString(filter.Field)); return; } builder.EndParenthetical().EndParenthetical(); SQLite::Statement statement = builder.Prepare(m_connection); - ExecuteStatementForMatchType(statement, match, bindIndex.value(), MatchUsesLike(match), value); + BindStatementForMatchType(statement, filter, bindIndex); + statement.Execute(); AICLI_LOG(Repo, Verbose, << "Filter kept " << m_connection.GetChanges() << " rows"); } @@ -267,12 +226,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return result; } - std::optional SearchResultsTable::BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, PackageMatchField field, MatchType match) const + std::vector SearchResultsTable::BuildSearchStatement(SQLite::Builder::StatementBuilder& builder, PackageMatchField field, MatchType match) const { return BuildSearchStatement(builder, field, s_SearchResultsTable_SubSelect_ManifestAlias, s_SearchResultsTable_SubSelect_ValueAlias, MatchUsesLike(match)); } - std::optional SearchResultsTable::BuildSearchStatement( + std::vector SearchResultsTable::BuildSearchStatement( SQLite::Builder::StatementBuilder& builder, PackageMatchField field, std::string_view manifestAlias, @@ -295,4 +254,50 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return {}; } } + + bool SearchResultsTable::MatchUsesLike(MatchType match) + { + return (match != MatchType::Exact); + } + + void SearchResultsTable::BindStatementForMatchType(SQLite::Statement& statement, MatchType match, int bindIndex, std::string_view value) + { + std::string valueToUse; + + if (MatchUsesLike(match)) + { + valueToUse = SQLite::EscapeStringForLike(value); + } + else + { + valueToUse = value; + } + + switch (match) + { + case AppInstaller::Repository::MatchType::StartsWith: + valueToUse += '%'; + break; + case AppInstaller::Repository::MatchType::Substring: + valueToUse = "%"s + valueToUse + '%'; + break; + default: + // No changes required for others. + break; + } + + statement.Bind(bindIndex, valueToUse); + } + + void SearchResultsTable::BindStatementForMatchType(SQLite::Statement& statement, const PackageMatchFilter& filter, const std::vector& bindIndex) + { + // TODO: Implement these more complex match types + if (filter.Type == MatchType::Wildcard || filter.Type == MatchType::Fuzzy || filter.Type == MatchType::FuzzySubstring) + { + AICLI_LOG(Repo, Verbose, << "Specific match type not implemented, skipping: " << MatchTypeToString(filter.Type)); + return; + } + + BindStatementForMatchType(statement, filter.Type, bindIndex[0], filter.Value); + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h index 70f1e2c821..b48d6da5a0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h @@ -28,5 +28,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 protected: std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const override; void PerformQuerySearch(V1_0::SearchResultsTable& resultsTable, const RequestMatch& query) const override; + virtual SearchResult SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const; + virtual void PrepareForPackaging(SQLite::Connection& connection, bool vacuum); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp index 8672fd2ab8..a899c5b0e7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp @@ -150,34 +150,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 void Interface::PrepareForPackaging(SQLite::Connection& connection) { - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v1_1"); - - V1_0::IdTable::PrepareForPackaging(connection); - V1_0::NameTable::PrepareForPackaging(connection); - V1_0::MonikerTable::PrepareForPackaging(connection); - V1_0::VersionTable::PrepareForPackaging(connection); - V1_0::ChannelTable::PrepareForPackaging(connection); - - V1_0::PathPartTable::PrepareForPackaging(connection); - - V1_0::ManifestTable::PrepareForPackaging(connection, { - V1_0::VersionTable::ValueName(), - V1_0::ChannelTable::ValueName(), - V1_0::PathPartTable::ValueName(), - }); - - V1_0::TagsTable::PrepareForPackaging(connection, false); - V1_0::CommandsTable::PrepareForPackaging(connection, false); - PackageFamilyNameTable::PrepareForPackaging(connection, true, true); - ProductCodeTable::PrepareForPackaging(connection, true, true); - - savepoint.Commit(); - - // Force the database to actually shrink the file size. - // This *must* be done outside of an active transaction. - SQLite::Builder::StatementBuilder builder; - builder.Vacuum(); - builder.Execute(connection); + PrepareForPackaging(connection, true); } bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const @@ -200,29 +173,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const { - // Update any system reference strings to be folded - SearchRequest foldedRequest = request; - - auto foldIfNeeded = [](PackageMatchFilter& filter) - { - if ((filter.Field == PackageMatchField::PackageFamilyName || filter.Field == PackageMatchField::ProductCode) && - filter.Type == MatchType::Exact) - { - filter.Value = Utility::FoldCase(filter.Value); - } - }; - - for (auto& inclusion : foldedRequest.Inclusions) - { - foldIfNeeded(inclusion); - } - - for (auto& filter : foldedRequest.Filters) - { - foldIfNeeded(filter); - } - - return V1_0::Interface::Search(connection, foldedRequest); + SearchRequest updatedRequest = request; + return SearchInternal(connection, updatedRequest); } std::vector Interface::GetMultiPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMultiProperty property) const @@ -273,11 +225,73 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 { // First, do an exact match search for the folded system reference strings // We do this first because it is exact, and likely won't match anything else if it matches this. - std::string foldedQuery = Utility::FoldCase(query.Value); - resultsTable.SearchOnField(PackageMatchField::PackageFamilyName, MatchType::Exact, foldedQuery); - resultsTable.SearchOnField(PackageMatchField::ProductCode, MatchType::Exact, foldedQuery); + PackageMatchFilter filter(PackageMatchField::PackageFamilyName, MatchType::Exact, Utility::FoldCase(query.Value)); + resultsTable.SearchOnField(filter); + + filter.Field = PackageMatchField::ProductCode; + resultsTable.SearchOnField(filter); // Then do the 1.0 search V1_0::Interface::PerformQuerySearch(resultsTable, query); } + + ISQLiteIndex::SearchResult Interface::SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const + { + // Update any system reference strings to be folded + auto foldIfNeeded = [](PackageMatchFilter& filter) + { + if ((filter.Field == PackageMatchField::PackageFamilyName || filter.Field == PackageMatchField::ProductCode) && + filter.Type == MatchType::Exact) + { + filter.Value = Utility::FoldCase(filter.Value); + } + }; + + for (auto& inclusion : request.Inclusions) + { + foldIfNeeded(inclusion); + } + + for (auto& filter : request.Filters) + { + foldIfNeeded(filter); + } + + return V1_0::Interface::Search(connection, request); + } + + void Interface::PrepareForPackaging(SQLite::Connection& connection, bool vacuum) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v1_1"); + + V1_0::IdTable::PrepareForPackaging(connection); + V1_0::NameTable::PrepareForPackaging(connection); + V1_0::MonikerTable::PrepareForPackaging(connection); + V1_0::VersionTable::PrepareForPackaging(connection); + V1_0::ChannelTable::PrepareForPackaging(connection); + + V1_0::PathPartTable::PrepareForPackaging(connection); + + V1_0::ManifestTable::PrepareForPackaging(connection, { + V1_0::VersionTable::ValueName(), + V1_0::ChannelTable::ValueName(), + V1_0::PathPartTable::ValueName(), + }); + + V1_0::TagsTable::PrepareForPackaging(connection, false); + V1_0::CommandsTable::PrepareForPackaging(connection, false); + PackageFamilyNameTable::PrepareForPackaging(connection, true, true); + ProductCodeTable::PrepareForPackaging(connection, true, true); + + savepoint.Commit(); + + if (vacuum) + { + // Force the database to actually shrink the file size. + // This *must* be done outside of an active transaction. + SQLite::Builder::StatementBuilder builder; + builder.Vacuum(); + builder.Execute(connection); + } + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable.h index 041938d69b..ae1f39c639 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable.h @@ -18,7 +18,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 SearchResultsTable& operator=(SearchResultsTable&&) = default; protected: - std::optional BuildSearchStatement( + std::vector BuildSearchStatement( SQLite::Builder::StatementBuilder& builder, PackageMatchField field, std::string_view manifestAlias, diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable_1_1.cpp index 530f06c84a..7023b5c7c8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/SearchResultsTable_1_1.cpp @@ -11,7 +11,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 { - std::optional SearchResultsTable::BuildSearchStatement( + std::vector SearchResultsTable::BuildSearchStatement( SQLite::Builder::StatementBuilder& builder, PackageMatchField field, std::string_view manifestAlias, diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface.h new file mode 100644 index 0000000000..e0a07542a8 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/ISQLiteIndex.h" +#include "Microsoft/Schema/1_1/Interface.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + // Interface to this schema version exposed through ISQLiteIndex. + struct Interface : public V1_1::Interface + { + Interface(Utility::NormalizationVersion normVersion = Utility::NormalizationVersion::Initial); + + // Version 1.0 + Schema::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + SQLite::rowid_t AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; + std::pair UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; + SQLite::rowid_t RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; + bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; + + // Version 1.2 + Utility::NormalizedName NormalizeName(std::string_view name, std::string_view publisher) const override; + + protected: + std::unique_ptr CreateSearchResultsTable(const SQLite::Connection& connection) const override; + SearchResult SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const override; + void PrepareForPackaging(SQLite::Connection& connection, bool vacuum) override; + + // The name normalization utility + Utility::NameNormalizer m_normalizer; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface_1_2.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface_1_2.cpp new file mode 100644 index 0000000000..753265559e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/Interface_1_2.cpp @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/1_2/Interface.h" + +#include "Microsoft/Schema/1_2/NormalizedPackageNameTable.h" +#include "Microsoft/Schema/1_2/NormalizedPackagePublisherTable.h" + +#include "Microsoft/Schema/1_2/SearchResultsTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + namespace + { + void AddLocalizationNormalizedName(const Utility::NameNormalizer& normalizer, const Manifest::ManifestLocalization& localization, std::vector& out) + { + if (localization.Contains(Manifest::Localization::PackageName)) + { + Utility::NormalizedString value = normalizer.NormalizeName(Utility::FoldCase(localization.Get())).Name(); + if (std::find(out.begin(), out.end(), value) == out.end()) + { + out.emplace_back(std::move(value)); + } + } + } + + void AddLocalizationNormalizedPublisher(const Utility::NameNormalizer& normalizer, const Manifest::ManifestLocalization& localization, std::vector& out) + { + if (localization.Contains(Manifest::Localization::Publisher)) + { + Utility::NormalizedString value = normalizer.NormalizePublisher(Utility::FoldCase(localization.Get())); + if (std::find(out.begin(), out.end(), value) == out.end()) + { + out.emplace_back(std::move(value)); + } + } + } + + std::vector GetNormalizedNames(const Utility::NameNormalizer& normalizer, const Manifest::Manifest& manifest) + { + std::vector result; + + AddLocalizationNormalizedName(normalizer, manifest.DefaultLocalization, result); + for (const auto& loc : manifest.Localizations) + { + AddLocalizationNormalizedName(normalizer, loc, result); + } + + return result; + } + + std::vector GetNormalizedPublishers(const Utility::NameNormalizer& normalizer, const Manifest::Manifest& manifest) + { + std::vector result; + + AddLocalizationNormalizedPublisher(normalizer, manifest.DefaultLocalization, result); + for (const auto& loc : manifest.Localizations) + { + AddLocalizationNormalizedPublisher(normalizer, loc, result); + } + + return result; + } + } + + Interface::Interface(Utility::NormalizationVersion normVersion) : m_normalizer(normVersion) + { + } + + Schema::Version Interface::GetVersion() const + { + return { 1, 2 }; + } + + void Interface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createtables_v1_2"); + + V1_1::Interface::CreateTables(connection); + + // While the name and publisher should be linked per-locale, we are not implementing that here. + // This will mean that one can match cross locale name and publisher, but the chance that this + // leads to a confusion between packages is very small. More likely would be intentional attempts + // to confuse the correlation, which could be fairly easily carried out even with linked values. + NormalizedPackageNameTable::Create(connection); + NormalizedPackagePublisherTable::Create(connection); + + savepoint.Commit(); + } + + SQLite::rowid_t Interface::AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addmanifest_v1_2"); + + SQLite::rowid_t manifestId = V1_1::Interface::AddManifest(connection, manifest, relativePath); + + // Add the new 1.2 data + // These normalized strings are all stored with their cases folded so that they can be + // looked up ordinally; enabling the index to provide efficient searches. + NormalizedPackageNameTable::EnsureExistsAndInsert(connection, GetNormalizedNames(m_normalizer, manifest), manifestId); + NormalizedPackagePublisherTable::EnsureExistsAndInsert(connection, GetNormalizedPublishers(m_normalizer, manifest), manifestId); + + savepoint.Commit(); + + return manifestId; + } + + std::pair Interface::UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatemanifest_v1_2"); + + auto [indexModified, manifestId] = V1_1::Interface::UpdateManifest(connection, manifest, relativePath); + + // Update new 1.2 tables as necessary + indexModified = NormalizedPackageNameTable::UpdateIfNeededByManifestId(connection, GetNormalizedNames(m_normalizer, manifest), manifestId) || indexModified; + indexModified = NormalizedPackagePublisherTable::UpdateIfNeededByManifestId(connection, GetNormalizedPublishers(m_normalizer, manifest), manifestId) || indexModified; + + savepoint.Commit(); + + return { indexModified, manifestId }; + } + + SQLite::rowid_t Interface::RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "removemanifest_v1_2"); + + SQLite::rowid_t manifestId = V1_1::Interface::RemoveManifest(connection, manifest, relativePath); + + // Remove all of the new 1.2 data that is no longer referenced. + NormalizedPackageNameTable::DeleteIfNotNeededByManifestId(connection, manifestId); + NormalizedPackagePublisherTable::DeleteIfNotNeededByManifestId(connection, manifestId); + + savepoint.Commit(); + + return manifestId; + } + + bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const + { + bool result = V1_1::Interface::CheckConsistency(connection, log); + + // If the v1.1 index was consistent, or if full logging of inconsistency was requested, check the v1.2 data. + if (result || log) + { + result = NormalizedPackageNameTable::CheckConsistency(connection, log) && result; + } + + if (result || log) + { + result = NormalizedPackagePublisherTable::CheckConsistency(connection, log) && result; + } + + return result; + } + + Utility::NormalizedName Interface::NormalizeName(std::string_view name, std::string_view publisher) const + { + return m_normalizer.Normalize(name, publisher); + } + + std::unique_ptr Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const + { + return std::make_unique(connection); + } + + ISQLiteIndex::SearchResult Interface::SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const + { + // Update NormalizedNameAndPublisher with normalization and folding + auto updateIfNeeded = [&](PackageMatchFilter& filter) + { + if (filter.Field == PackageMatchField::NormalizedNameAndPublisher && filter.Type == MatchType::Exact) + { + Utility::NormalizedName normalized = m_normalizer.Normalize(Utility::FoldCase(filter.Value), Utility::FoldCase(filter.Additional.value())); + filter.Value = normalized.Name(); + filter.Additional = normalized.Publisher(); + } + }; + + for (auto& inclusion : request.Inclusions) + { + updateIfNeeded(inclusion); + } + + for (auto& filter : request.Filters) + { + updateIfNeeded(filter); + } + + return V1_1::Interface::SearchInternal(connection, request); + } + + void Interface::PrepareForPackaging(SQLite::Connection& connection, bool vacuum) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareforpackaging_v1_2"); + + V1_1::Interface::PrepareForPackaging(connection, false); + + NormalizedPackageNameTable::PrepareForPackaging(connection, true, true); + NormalizedPackagePublisherTable::PrepareForPackaging(connection, true, true); + + savepoint.Commit(); + + if (vacuum) + { + // Force the database to actually shrink the file size. + // This *must* be done outside of an active transaction. + SQLite::Builder::StatementBuilder builder; + builder.Vacuum(); + builder.Execute(connection); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackageNameTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackageNameTable.h new file mode 100644 index 0000000000..799d2f0071 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackageNameTable.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/1_0/OneToManyTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + namespace details + { + using namespace std::string_view_literals; + + struct NormalizedPackageNameTableInfo + { + inline static constexpr std::string_view TableName() { return "norm_names"sv; } + inline static constexpr std::string_view ValueName() { return "norm_name"sv; } + }; + } + + // The table for NormalizedPackageName. + using NormalizedPackageNameTable = V1_0::OneToManyTable; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackagePublisherTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackagePublisherTable.h new file mode 100644 index 0000000000..46f08742ff --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/NormalizedPackagePublisherTable.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/1_0/OneToManyTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + namespace details + { + using namespace std::string_view_literals; + + struct NormalizedPackagePublisherTableInfo + { + inline static constexpr std::string_view TableName() { return "norm_publishers"sv; } + inline static constexpr std::string_view ValueName() { return "norm_publisher"sv; } + }; + } + + // The table for NormalizedPackagePublisher. + using NormalizedPackagePublisherTable = V1_0::OneToManyTable; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable.h new file mode 100644 index 0000000000..331868aa6e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/1_1/SearchResultsTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + // Table for holding temporary search results. + struct SearchResultsTable : public V1_1::SearchResultsTable + { + SearchResultsTable(const SQLite::Connection& connection) : V1_1::SearchResultsTable(connection) {} + + SearchResultsTable(const SearchResultsTable&) = delete; + SearchResultsTable& operator=(const SearchResultsTable&) = delete; + + SearchResultsTable(SearchResultsTable&&) = default; + SearchResultsTable& operator=(SearchResultsTable&&) = default; + + protected: + std::vector BuildSearchStatement( + SQLite::Builder::StatementBuilder& builder, + PackageMatchField field, + std::string_view manifestAlias, + std::string_view valueAlias, + bool useLike) const override; + + // Import all overrides of this function + using V1_0::SearchResultsTable::BindStatementForMatchType; + + void BindStatementForMatchType(SQLite::Statement& statement, const PackageMatchFilter& filter, const std::vector& bindIndex) override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable_1_2.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable_1_2.cpp new file mode 100644 index 0000000000..edb97cba4a --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_2/SearchResultsTable_1_2.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "SearchResultsTable.h" + +#include "Microsoft/Schema/1_0/ManifestTable.h" +#include "Microsoft/Schema/1_2/NormalizedPackageNameTable.h" +#include "Microsoft/Schema/1_2/NormalizedPackagePublisherTable.h" + + +namespace AppInstaller::Repository::Microsoft::Schema::V1_2 +{ + std::vector SearchResultsTable::BuildSearchStatement( + SQLite::Builder::StatementBuilder& builder, + PackageMatchField field, + std::string_view manifestAlias, + std::string_view valueAlias, + bool useLike) const + { + switch (field) + { + case PackageMatchField::NormalizedNameAndPublisher: + return V1_0::ManifestTable::BuildSearchStatement(builder, manifestAlias, valueAlias, useLike); + default: + return V1_1::SearchResultsTable::BuildSearchStatement(builder, field, manifestAlias, valueAlias, useLike); + } + } + + void SearchResultsTable::BindStatementForMatchType(SQLite::Statement& statement, const PackageMatchFilter& filter, const std::vector& bindIndex) + { + V1_0::SearchResultsTable::BindStatementForMatchType(statement, filter, bindIndex); + + if (filter.Field == PackageMatchField::NormalizedNameAndPublisher) + { + BindStatementForMatchType(statement, filter.Type, bindIndex[1], filter.Additional.value()); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 024c68c27a..0a0ef69efa 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -6,6 +6,7 @@ #include "Public/AppInstallerRepositorySearch.h" #include #include +#include #include @@ -80,5 +81,9 @@ namespace AppInstaller::Repository::Microsoft::Schema // Sets the string for the given metadata and manifest id. virtual void SetMetadataByManifestId(SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionMetadata metadata, std::string_view value) = 0; + + // Normalizes a name using the internal rules used by the index. + // Largely a utility function; should not be used to do work on behalf of the index by the caller. + virtual Utility::NormalizedName NormalizeName(std::string_view name, std::string_view publisher) const = 0; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp index 66612bb14d..6731ee7156 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp @@ -6,6 +6,7 @@ #include "1_0/Interface.h" #include "1_1/Interface.h" +#include "1_2/Interface.h" namespace AppInstaller::Repository::Microsoft::Schema { @@ -34,11 +35,15 @@ namespace AppInstaller::Repository::Microsoft::Schema { return std::make_unique(); } - else if (*this == Version{ 1, 1 } || + else if (*this == Version{ 1, 1 }) + { + return std::make_unique(); + } + else if (*this == Version{ 1, 2 } || this->MajorVersion == 1 || this->IsLatest()) { - return std::make_unique(); + return std::make_unique(); } // We do not have the capacity to operate on this schema version diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h index ae9815fcec..d6c3e09b4b 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -42,6 +42,7 @@ namespace AppInstaller::Repository Tag, PackageFamilyName, ProductCode, + NormalizedNameAndPublisher, }; // A single match to be performed during a search. @@ -49,8 +50,13 @@ namespace AppInstaller::Repository { MatchType Type; Utility::NormalizedString Value; + std::optional Additional; - RequestMatch(MatchType t, std::string_view v) : Type(t), Value(v) {} + RequestMatch(MatchType t) : Type(t) {} + RequestMatch(MatchType t, Utility::NormalizedString& v) : Type(t), Value(v) {} + RequestMatch(MatchType t, const Utility::NormalizedString& v) : Type(t), Value(v) {} + RequestMatch(MatchType t, Utility::NormalizedString&& v) : Type(t), Value(std::move(v)) {} + RequestMatch(MatchType t, std::string_view v1, std::string_view v2) : Type(t), Value(v1), Additional(Utility::NormalizedString{ v2 }) {} }; // A match on a specific field to be performed during a search. @@ -58,7 +64,21 @@ namespace AppInstaller::Repository { PackageMatchField Field; - PackageMatchFilter(PackageMatchField f, MatchType t, std::string_view v) : RequestMatch(t, v), Field(f) {} + PackageMatchFilter(PackageMatchField f, MatchType t) : RequestMatch(t), Field(f) { EnsureRequiredValues(); } + PackageMatchFilter(PackageMatchField f, MatchType t, Utility::NormalizedString& v) : RequestMatch(t, v), Field(f) { EnsureRequiredValues(); } + PackageMatchFilter(PackageMatchField f, MatchType t, const Utility::NormalizedString& v) : RequestMatch(t, v), Field(f) { EnsureRequiredValues(); } + PackageMatchFilter(PackageMatchField f, MatchType t, Utility::NormalizedString&& v) : RequestMatch(t, std::move(v)), Field(f) { EnsureRequiredValues(); } + PackageMatchFilter(PackageMatchField f, MatchType t, std::string_view v1, std::string_view v2) : RequestMatch(t, v1, v2), Field(f) { EnsureRequiredValues(); } + + protected: + void EnsureRequiredValues() + { + // Ensure that the second value always exists when it should + if (Field == PackageMatchField::NormalizedNameAndPublisher && !Additional) + { + Additional = Utility::NormalizedString{}; + } + } }; // Container for data used to filter the available manifests in a source. @@ -122,6 +142,10 @@ namespace AppInstaller::Repository StandardUninstallCommand, // An uninstall command that should be non-interactive SilentUninstallCommand, + // The publisher of the package + Publisher, + // The locale of the package + Locale, }; // Convert a PackageVersionMetadata to a string. @@ -141,7 +165,7 @@ namespace AppInstaller::Repository virtual std::vector GetMultiProperty(PackageVersionMultiProperty property) const = 0; // Gets the manifest of this package version. - virtual Manifest::Manifest GetManifest() const = 0; + virtual Manifest::Manifest GetManifest() = 0; // Gets the source where this package version is from. virtual std::shared_ptr GetSource() const = 0; @@ -207,6 +231,9 @@ namespace AppInstaller::Repository // Gets a value indicating whether an available version is newer than the installed version. virtual bool IsUpdateAvailable() const = 0; + + // Determines if the given IPackage refers to the same package as this one. + virtual bool IsSame(const IPackage*) const = 0; }; // A single result from the search. @@ -276,6 +303,8 @@ namespace AppInstaller::Repository return "PackageFamilyName"sv; case PackageMatchField::ProductCode: return "ProductCode"sv; + case PackageMatchField::NormalizedNameAndPublisher: + return "NormalizedNameAndPublisher"sv; } return "UnknownMatchField"sv; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h index 77b08f1e81..e0761bede6 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -44,9 +44,12 @@ namespace AppInstaller::Repository // The argument used when adding the source. std::string Arg; - // The sources extra data string. + // The source's extra data string. std::string Data; + // The source's unique identifier. + std::string Identifier; + // The last time that this source was updated. std::chrono::system_clock::time_point LastUpdateTime = {}; @@ -97,17 +100,25 @@ namespace AppInstaller::Repository // Adds a new source for the user. void AddSource(std::string_view name, std::string_view type, std::string_view arg, IProgressCallback& progress); + struct OpenSourceResult + { + // The ISource returned by OpenSource + std::shared_ptr Source; + + // List of SourceDetails that failed to update + std::vector SourcesWithUpdateFailure; + }; + // Opens an existing source. // Passing an empty string as the name of the source will return a source that aggregates all others. - std::shared_ptr OpenSource(std::string_view name, IProgressCallback& progress); + OpenSourceResult OpenSource(std::string_view name, IProgressCallback& progress); // A predefined source. // These sources are not under the direct control of the user, such as packages installed on the system. enum class PredefinedSource { Installed, - ARP_System, - ARP_User, + ARP, MSIX, }; @@ -115,8 +126,23 @@ namespace AppInstaller::Repository // These sources are not under the direct control of the user, such as packages installed on the system. std::shared_ptr OpenPredefinedSource(PredefinedSource source, IProgressCallback& progress); + // Search behavior for composite sources. + // Only relevant for composite sources with an installed source, not for aggregates of multiple available sources. + // Installed and available packages in the result are always correlated when possible. + enum class CompositeSearchBehavior + { + // Search only installed packages. + Installed, + // Search both installed and available packages. + AllPackages, + }; + // Creates a source that merges the installed packages with the given available packages. - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource); + // The source can search for installed packages only, or also include non-installed available packages. + std::shared_ptr CreateCompositeSource( + const std::shared_ptr& installedSource, + const std::shared_ptr& availableSource, + CompositeSearchBehavior searchBehavior = CompositeSearchBehavior::Installed); // Updates an existing source. // Return value indicates whether the named source was found. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index f9eb7693ca..fe290380b6 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -7,6 +7,7 @@ #include "SourceFactory.h" #include "Microsoft/PredefinedInstalledSourceFactory.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" +#include "Rest/RestSourceFactory.h" namespace AppInstaller::Repository { @@ -20,6 +21,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_SourcesYaml_Source_Type = "Type"sv; constexpr std::string_view s_SourcesYaml_Source_Arg = "Arg"sv; constexpr std::string_view s_SourcesYaml_Source_Data = "Data"sv; + constexpr std::string_view s_SourcesYaml_Source_Identifier = "Identifier"sv; constexpr std::string_view s_SourcesYaml_Source_IsTombstone = "IsTombstone"sv; constexpr std::string_view s_MetadataYaml_Sources = "Sources"sv; @@ -29,10 +31,12 @@ namespace AppInstaller::Repository constexpr std::string_view s_Source_WingetCommunityDefault_Name = "winget"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Arg = "https://winget.azureedge.net/cache"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Data = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; + constexpr std::string_view s_Source_WingetCommunityDefault_Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Name = "msstore"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Arg = "https://winget.azureedge.net/msstore"sv; constexpr std::string_view s_Source_WingetMSStoreDefault_Data = "Microsoft.Winget.MSStore.Source_8wekyb3d8bbwe"sv; + constexpr std::string_view s_Source_WingetMSStoreDefault_Identifier = "Microsoft.Winget.MSStore.Source_8wekyb3d8bbwe"sv; namespace { @@ -187,6 +191,7 @@ namespace AppInstaller::Repository details.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); details.Arg = s_Source_WingetCommunityDefault_Arg; details.Data = s_Source_WingetCommunityDefault_Data; + details.Identifier = s_Source_WingetCommunityDefault_Identifier; details.TrustLevel = SourceTrustLevel::Trusted; result.emplace_back(std::move(details)); @@ -197,13 +202,15 @@ namespace AppInstaller::Repository storeDetails.Type = Microsoft::PreIndexedPackageSourceFactory::Type(); storeDetails.Arg = s_Source_WingetMSStoreDefault_Arg; storeDetails.Data = s_Source_WingetMSStoreDefault_Data; + storeDetails.Identifier = s_Source_WingetMSStoreDefault_Identifier; storeDetails.TrustLevel = SourceTrustLevel::Trusted; result.emplace_back(std::move(storeDetails)); } } - break; + break; case SourceOrigin::User: - result = GetSourcesFromSetting( + { + std::vector userSources = GetSourcesFromSetting( Settings::Streams::UserSources, s_SourcesYaml_Sources, [&](SourceDetailsInternal& details, const std::string& settingValue, const YAML::Node& source) @@ -214,9 +221,22 @@ namespace AppInstaller::Repository if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Arg, details.Arg)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Data, details.Data)) { return false; } if (!TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_IsTombstone, details.IsTombstone)) { return false; } + TryReadScalar(name, settingValue, source, s_SourcesYaml_Source_Identifier, details.Identifier); return true; }); - break; + + for (auto& source : userSources) + { + if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), source.Type) + && !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + continue; + } + + result.emplace_back(std::move(source)); + } + } + break; default: THROW_HR(E_UNEXPECTED); } @@ -246,6 +266,7 @@ namespace AppInstaller::Repository out << YAML::Key << s_SourcesYaml_Source_Type << YAML::Value << details.Type; out << YAML::Key << s_SourcesYaml_Source_Arg << YAML::Value << details.Arg; out << YAML::Key << s_SourcesYaml_Source_Data << YAML::Value << details.Data; + out << YAML::Key << s_SourcesYaml_Source_Identifier << YAML::Value << details.Identifier; out << YAML::Key << s_SourcesYaml_Source_IsTombstone << YAML::Value << details.IsTombstone; out << YAML::EndMap; } @@ -320,6 +341,11 @@ namespace AppInstaller::Repository { return Microsoft::PredefinedInstalledSourceFactory::Create(); } + else if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), type) + && Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + return Rest::RestSourceFactory::Create(); + } THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE); } @@ -374,7 +400,7 @@ namespace AppInstaller::Repository constexpr static auto s_ZeroMins = 0min; auto autoUpdateTime = User().Get(); - // A value of zero means no auto update, to get update the source run `winget update` + // A value of zero means no auto update, to get update the source run `winget update` if (autoUpdateTime != s_ZeroMins) { auto autoUpdateTimeMins = std::chrono::minutes(autoUpdateTime); @@ -600,14 +626,24 @@ namespace AppInstaller::Repository details.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); details.Origin = SourceOrigin::User; + // Check feature flag enablement for rest source. + if (Utility::CaseInsensitiveEquals(Rest::RestSourceFactory::Type(), type) + && !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ExperimentalRestSource)) + { + AICLI_LOG(Repo, Error, << Settings::ExperimentalFeature::GetFeature(Settings::ExperimentalFeature::Feature::ExperimentalRestSource).Name() + << " feature is disabled. Execution cancelled."); + THROW_HR(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + AddSourceFromDetails(details, progress); AICLI_LOG(Repo, Info, << "Source created with extra data: " << details.Data); + AICLI_LOG(Repo, Info, << "Source created with identifier: " << details.Identifier); sourceList.AddSource(details); } - std::shared_ptr OpenSource(std::string_view name, IProgressCallback& progress) + OpenSourceResult OpenSource(std::string_view name, IProgressCallback& progress) { SourceListInternal sourceList; auto currentSources = sourceList.GetCurrentSourceRefs(); @@ -619,7 +655,7 @@ namespace AppInstaller::Repository AICLI_LOG(Repo, Info, << "Default source requested, but no sources configured"); return {}; } - else if(currentSources.size() == 1) + else if (currentSources.size() == 1) { AICLI_LOG(Repo, Info, << "Default source requested, only 1 source available, using the only source: " << currentSources[0].get().Name); return OpenSource(currentSources[0].get().Name, progress); @@ -628,6 +664,7 @@ namespace AppInstaller::Repository { AICLI_LOG(Repo, Info, << "Default source requested, multiple sources available, creating aggregated source."); auto aggregatedSource = std::make_shared("*DefaultSource"); + OpenSourceResult result; bool sourceUpdated = false; for (auto& source : currentSources) @@ -636,10 +673,18 @@ namespace AppInstaller::Repository if (ShouldUpdateBeforeOpen(source)) { - // TODO: Consider adding a context callback to indicate we are doing the same action - // to avoid the progress bar fill up multiple times. - UpdateSourceFromDetails(source, progress); - sourceUpdated = true; + try + { + // TODO: Consider adding a context callback to indicate we are doing the same action + // to avoid the progress bar fill up multiple times. + UpdateSourceFromDetails(source, progress); + sourceUpdated = true; + } + catch (...) + { + AICLI_LOG(Repo, Warning, << "Failed to update source: " << source.get().Name); + result.SourcesWithUpdateFailure.emplace_back(source); + } } aggregatedSource->AddAvailableSource(CreateSourceFromDetails(source, progress)); } @@ -649,7 +694,8 @@ namespace AppInstaller::Repository sourceList.SaveMetadata(); } - return aggregatedSource; + result.Source = aggregatedSource; + return result; } } else @@ -664,12 +710,24 @@ namespace AppInstaller::Repository { AICLI_LOG(Repo, Info, << "Named source requested, found: " << source->Name); + OpenSourceResult result; + if (ShouldUpdateBeforeOpen(*source)) { - UpdateSourceFromDetails(*source, progress); - sourceList.SaveMetadata(); + try + { + UpdateSourceFromDetails(*source, progress); + sourceList.SaveMetadata(); + } + catch (...) + { + AICLI_LOG(Repo, Warning, << "Failed to update source: " << (*source).Name); + result.SourcesWithUpdateFailure.emplace_back(*source); + } } - return CreateSourceFromDetails(*source, progress); + + result.Source = CreateSourceFromDetails(*source, progress); + return result; } } } @@ -685,13 +743,9 @@ namespace AppInstaller::Repository details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::None); return CreateSourceFromDetails(details, progress); - case PredefinedSource::ARP_System: - details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); - details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_System); - return CreateSourceFromDetails(details, progress); - case PredefinedSource::ARP_User: + case PredefinedSource::ARP: details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); - details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::ARP_User); + details.Arg = Microsoft::PredefinedInstalledSourceFactory::FilterToString(Microsoft::PredefinedInstalledSourceFactory::Filter::ARP); return CreateSourceFromDetails(details, progress); case PredefinedSource::MSIX: details.Type = Microsoft::PredefinedInstalledSourceFactory::Type(); @@ -702,7 +756,7 @@ namespace AppInstaller::Repository THROW_HR(E_UNEXPECTED); } - std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource) + std::shared_ptr CreateCompositeSource(const std::shared_ptr& installedSource, const std::shared_ptr& availableSource, CompositeSearchBehavior searchBehavior) { std::shared_ptr result = std::dynamic_pointer_cast(availableSource); @@ -712,7 +766,7 @@ namespace AppInstaller::Repository result->AddAvailableSource(availableSource); } - result->SetInstalledSource(installedSource); + result->SetInstalledSource(installedSource, searchBehavior); return result; } diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp new file mode 100644 index 0000000000..b5a2da4e98 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" +#include "HttpClientHelper.h" + +namespace AppInstaller::Repository::Rest +{ + HttpClientHelper::HttpClientHelper(const utility::string_t& url) : m_client(url), m_url(url) {} + + pplx::task HttpClientHelper::Post( + const web::json::value& body, const std::vector>& headers) + { + AICLI_LOG(Repo, Verbose, << "Sending http POST request to: " << utility::conversions::to_utf8string(m_url)); + web::http::http_request request{ web::http::methods::POST }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + request.set_body(body.serialize()); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + + return MakeRequest(request); + } + + web::json::value HttpClientHelper::HandlePost( + const web::json::value& body, const std::vector>& headers) + { + web::http::http_response httpResponse; + HttpClientHelper::Post(body, headers).then([&httpResponse](const web::http::http_response& response) + { + AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); + httpResponse = response; + }).wait(); + + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); + } + + pplx::task HttpClientHelper::Get(const std::vector>& headers) + { + AICLI_LOG(Repo, Verbose, << "Sending http GET request to: " << utility::conversions::to_utf8string(m_url)); + web::http::http_request request{ web::http::methods::GET }; + request.headers().set_content_type(web::http::details::mime_types::application_json); + + // Add headers + for (auto& pair : headers) + { + request.headers().add(pair.first, pair.second); + } + + return MakeRequest(request); + } + + web::json::value HttpClientHelper::HandleGet(const std::vector>& headers) + { + web::http::http_response httpResponse; + Get(headers).then([&httpResponse](const web::http::http_response& response) + { + AICLI_LOG(Repo, Verbose, << "Response status: " << response.status_code()); + httpResponse = response; + }).wait(); + + THROW_HR_IF(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, httpResponse.status_code()), httpResponse.status_code() != web::http::status_codes::OK); + return httpResponse.extract_json().get(); + } + + pplx::task HttpClientHelper::MakeRequest(web::http::http_request req) + { + return m_client.request(req); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h new file mode 100644 index 0000000000..69dfaf17cb --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/HttpClientHelper.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" + +namespace AppInstaller::Repository::Rest +{ + struct HttpClientHelper + { + HttpClientHelper(const utility::string_t& url); + + pplx::task Post(const web::json::value& body, const std::vector>& headers = {}); + + web::json::value HandlePost(const web::json::value& body, const std::vector>& headers = {}); + + pplx::task Get(const std::vector>& headers = {}); + + web::json::value HandleGet(const std::vector>& headers = {}); + + protected: + pplx::task MakeRequest(web::http::http_request req); + + private: + web::http::client::http_client m_client; + utility::string_t m_url; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp new file mode 100644 index 0000000000..0a7fa35eda --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestClient.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/HttpClientHelper.h" +#include "Rest/Schema/Json/InformationResponseDeserializer.h" +#include "Rest/Schema/Json/JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" + +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::Json; + +namespace AppInstaller::Repository::Rest +{ + RestClient::RestClient(std::unique_ptr supportedInterface) + : m_interface(std::move(supportedInterface)) + { + } + + std::optional RestClient::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + return m_interface->GetManifestByVersion(packageId, version, channel); + } + + RestClient::SearchResult RestClient::Search(const SearchRequest& request) const + { + return m_interface->Search(request); + } + + utility::string_t RestClient::GetInformationEndpoint(const std::string& restApiUri) + { + std::string informationApi = restApiUri; + return utility::conversions::to_string_t(informationApi.append(InformationGetEndpoint)); + } + + std::string RestClient::GetSupportedVersion(const std::string& restApi) + { + // Call information endpoint + HttpClientHelper httpClientHelper{ GetInformationEndpoint(restApi) }; + web::json::value response = httpClientHelper.HandleGet(); + + Json::InformationResponseDeserializer responseDeserializer; + IRestClient::Information info = responseDeserializer.Deserialize(response); + + // TODO: Get a version that winget client and rest source both support. Using first version given for now. + IRestClient::Information information{ std::move(info) }; + return information.ServerSupportedVersions[0]; + } + + std::unique_ptr RestClient::GetSupportedInterface(const std::string& api, const std::string& version) + { + // TODO: Add supported version logic. Use V1_0 for now. + UNREFERENCED_PARAMETER(version); + return std::make_unique(api); + } + + RestClient RestClient::Create(const std::string& restApi) + { + std::string version = GetSupportedVersion(restApi); + std::unique_ptr supportedInterface = GetSupportedInterface(restApi, version); + return RestClient{ std::move(supportedInterface) }; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h new file mode 100644 index 0000000000..49e1ce57a3 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/IRestClient.h" +#include "cpprest/json.h" + +namespace AppInstaller::Repository::Rest +{ + struct RestClient + { + RestClient(std::unique_ptr supportedInterface); + + // The return type of Search + using SearchResult = Rest::Schema::IRestClient::SearchResult; + + RestClient(const RestClient&) = delete; + RestClient& operator=(const RestClient&) = delete; + + RestClient(RestClient&&) = default; + RestClient& operator=(RestClient&&) = default; + + // Performs a search based on the given criteria. + Schema::IRestClient::SearchResult Search(const SearchRequest& request) const; + + std::optional GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const; + + static utility::string_t GetInformationEndpoint(const std::string& restApiUri); + + static std::string GetSupportedVersion(const std::string& restApi); + + static std::unique_ptr GetSupportedInterface(const std::string& restApi, const std::string& version); + + static RestClient Create(const std::string& restApi); + + private: + std::unique_ptr m_interface; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.cpp b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp new file mode 100644 index 0000000000..985f2209e6 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestSource.cpp @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestSource.h" + +using namespace AppInstaller::Utility; + +namespace AppInstaller::Repository::Rest +{ + namespace + { + using namespace AppInstaller::Repository::Rest::Schema; + + // The source reference used by package objects. + struct SourceReference + { + SourceReference(const std::shared_ptr& source) : + m_source(source) {} + + protected: + std::shared_ptr GetReferenceSource() const + { + std::shared_ptr source = m_source.lock(); + THROW_HR_IF(E_NOT_VALID_STATE, !source); + return source; + } + + private: + std::weak_ptr m_source; + }; + + // The IPackageVersion impl for RestSource. + struct PackageVersion : public SourceReference, public IPackageVersion + { + PackageVersion( + const std::shared_ptr& source, IRestClient::PackageInfo packageInfo, IRestClient::VersionInfo versionInfo) + : SourceReference(source), m_packageInfo(packageInfo), m_versionInfo(versionInfo) {} + + // Inherited via IPackageVersion + Utility::LocIndString GetProperty(PackageVersionProperty property) const override + { + switch (property) + { + case PackageVersionProperty::SourceIdentifier: + return Utility::LocIndString{ GetReferenceSource()->GetIdentifier() }; + case PackageVersionProperty::SourceName: + return Utility::LocIndString{ GetReferenceSource()->GetDetails().Name }; + case PackageVersionProperty::Id: + return Utility::LocIndString{ m_packageInfo.PackageIdentifier }; + case PackageVersionProperty::Name: + return Utility::LocIndString{ m_packageInfo.PackageName }; + case PackageVersionProperty::Version: + return Utility::LocIndString{ m_versionInfo.VersionAndChannel.GetVersion().ToString() }; + case PackageVersionProperty::Channel: + return Utility::LocIndString{ m_versionInfo.VersionAndChannel.GetChannel().ToString() }; + default: + return Utility::LocIndString{}; + } + } + + // TODO + std::vector GetMultiProperty(PackageVersionMultiProperty property) const override + { + UNREFERENCED_PARAMETER(property); + std::vector result; + return result; + } + + Manifest::Manifest GetManifest() override + { + AICLI_LOG(Repo, Verbose, << "Getting manifest"); + + if (m_versionInfo.Manifest) + { + return m_versionInfo.Manifest.value(); + } + + std::optional manifest = GetReferenceSource()->GetRestClient().GetManifestByVersion( + m_packageInfo.PackageIdentifier, m_versionInfo.VersionAndChannel.GetVersion().ToString(), m_versionInfo.VersionAndChannel.GetChannel().ToString()); + + if (!manifest) + { + AICLI_LOG(Repo, Verbose, << "Valid manifest not found for package: " << m_packageInfo.PackageIdentifier); + return {}; + } + + m_versionInfo.Manifest = std::move(manifest.value()); + return m_versionInfo.Manifest.value(); + } + + std::shared_ptr GetSource() const override + { + return GetReferenceSource(); + } + + IPackageVersion::Metadata GetMetadata() const override + { + IPackageVersion::Metadata result; + return result; + } + + private: + IRestClient::PackageInfo m_packageInfo; + IRestClient::VersionInfo m_versionInfo; + }; + + // The base for IPackage implementations here. + struct PackageBase : public SourceReference + { + PackageBase(const std::shared_ptr& source, IRestClient::Package&& package) : + SourceReference(source), m_package(std::move(package)) + { + // Sort the versions + std::sort(m_package.Versions.begin(), m_package.Versions.end(), + [](const IRestClient::VersionInfo& a, const IRestClient::VersionInfo& b) + { + return a.VersionAndChannel < b.VersionAndChannel; + }); + } + + Utility::LocIndString GetProperty(PackageProperty property) const + { + switch (property) + { + case PackageProperty::Id: + return Utility::LocIndString{ m_package.PackageInformation.PackageIdentifier }; + case PackageProperty::Name: + return Utility::LocIndString{ m_package.PackageInformation.PackageName }; + default: + THROW_HR(E_UNEXPECTED); + } + } + + protected: + std::shared_ptr GetLatestVersionInternal() const + { + IRestClient::VersionInfo latestVersion = m_package.Versions.front(); + return std::make_shared(GetReferenceSource(), m_package.PackageInformation, latestVersion); + } + + IRestClient::Package m_package; + }; + + // The IPackage impl for Available packages from RestSource. + struct AvailablePackage : public PackageBase, public IPackage + { + using PackageBase::PackageBase; + + // Inherited via IPackage + Utility::LocIndString GetProperty(PackageProperty property) const override + { + return PackageBase::GetProperty(property); + } + + std::shared_ptr GetInstalledVersion() const override + { + return {}; + } + + std::vector GetAvailableVersionKeys() const override + { + std::shared_ptr source = GetReferenceSource(); + + std::vector result; + for (const auto& versionInfo : m_package.Versions) + { + result.emplace_back( + source->GetIdentifier(), versionInfo.VersionAndChannel.GetVersion().ToString(), versionInfo.VersionAndChannel.GetChannel().ToString()); + } + + return result; + } + + std::shared_ptr GetLatestAvailableVersion() const override + { + return GetLatestVersionInternal(); + } + + std::shared_ptr GetAvailableVersion(const PackageVersionKey& versionKey) const override + { + std::shared_ptr source = GetReferenceSource(); + + // Ensure that this key targets this (or any) source + if (!versionKey.SourceId.empty() && versionKey.SourceId != source->GetIdentifier()) + { + return {}; + } + + std::shared_ptr packageVersion; + if (!versionKey.Version.empty() && !versionKey.Channel.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetVersion().ToString(), versionKey.Version) + && CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetChannel().ToString(), versionKey.Channel)) + { + packageVersion = std::make_shared(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + else if (versionKey.Version.empty() && versionKey.Channel.empty()) + { + packageVersion = GetLatestAvailableVersion(); + } + else if (versionKey.Version.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetChannel().ToString(), versionKey.Channel)) + { + packageVersion = std::make_shared(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + else if (versionKey.Channel.empty()) + { + for (const auto& versionInfo : m_package.Versions) + { + if (CaseInsensitiveEquals(versionInfo.VersionAndChannel.GetVersion().ToString(), versionKey.Version)) + { + packageVersion = std::make_shared(source, m_package.PackageInformation, versionInfo); + break; + } + } + } + + return packageVersion; + } + + bool IsUpdateAvailable() const override + { + return false; + } + + bool IsSame(const IPackage* other) const override + { + const AvailablePackage* otherAvailablePackage = dynamic_cast(other); + + if (otherAvailablePackage) + { + return Utility::CaseInsensitiveEquals(m_package.PackageInformation.PackageIdentifier, otherAvailablePackage->m_package.PackageInformation.PackageIdentifier); + } + + return false; + } + }; + } + + RestSource::RestSource(const SourceDetails& details, std::string identifier, RestClient&& restClient) + : m_details(details), m_identifier(std::move(identifier)), m_restClient(std::move(restClient)) + { + } + + const SourceDetails& RestSource::GetDetails() const + { + return m_details; + } + + const RestClient& RestSource::GetRestClient() const + { + return m_restClient; + } + + const std::string& RestSource::GetIdentifier() const + { + return m_identifier; + } + + SearchResult RestSource::Search(const SearchRequest& request) const + { + // Note: Basic search functionality to fetch everything. + RestClient::SearchResult results = m_restClient.Search(request); + SearchResult searchResult; + + std::shared_ptr sharedThis = shared_from_this(); + for (auto& result : results.Matches) + { + std::unique_ptr package = std::make_unique(sharedThis, std::move(result)); + + // TODO: Improvise to use Package match filter to return relevant search results. + PackageMatchFilter packageFilter{ {}, {}, {} }; + + searchResult.Matches.emplace_back(std::move(package), std::move(packageFilter)); + } + + return searchResult; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSource.h b/src/AppInstallerRepositoryCore/Rest/RestSource.h new file mode 100644 index 0000000000..b56b19515f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestSource.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "RestClient.h" + +namespace AppInstaller::Repository::Rest +{ + // A source that holds a RestSource. + struct RestSource : public std::enable_shared_from_this, public ISource + { + RestSource(const SourceDetails& details, std::string identifier, RestClient&& m_restClient); + + RestSource(const RestSource&) = delete; + RestSource& operator=(const RestSource&) = delete; + + RestSource(RestSource&&) = default; + RestSource& operator=(RestSource&&) = default; + + ~RestSource() = default; + + // Get the source's details. + const SourceDetails& GetDetails() const override; + + // Gets the source's identifier; a unique identifier independent of the name + // that will not change between a remove/add or between additional adds. + // Must be suitable for filesystem names. + const std::string& GetIdentifier() const override; + + // Gets the rest client. + const RestClient& GetRestClient() const; + + // Execute a search on the source. + SearchResult Search(const SearchRequest& request) const override; + + private: + SourceDetails m_details; + std::string m_identifier; + RestClient m_restClient; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp new file mode 100644 index 0000000000..b19fdee425 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "RestSourceFactory.h" +#include "RestClient.h" +#include "RestSource.h" + +using namespace std::string_literals; +using namespace std::string_view_literals; + +namespace AppInstaller::Repository::Rest +{ + namespace + { + // The base class for data that comes from a rest based source. + struct RestSourceFactoryBase : public ISourceFactory + { + std::shared_ptr Create(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + + RestClient restClient = RestClient::Create(details.Arg); + + // TODO: Change identifier if required. + return std::make_shared(details, details.Arg, std::move(restClient)); + } + + void Add(SourceDetails& details, IProgressCallback&) override final + { + if (details.Type.empty()) + { + details.Type = RestSourceFactory::Type(); + } + else + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + + // Check if URL is remote and secure + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_REMOTE, !Utility::IsUrlRemote(details.Arg)); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_SECURE, !Utility::IsUrlSecure(details.Arg)); + } + + void Update(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + + void Remove(const SourceDetails& details, IProgressCallback&) override final + { + THROW_HR_IF(E_INVALIDARG, !Utility::CaseInsensitiveEquals(details.Type, RestSourceFactory::Type())); + } + }; + } + + std::unique_ptr RestSourceFactory::Create() + { + return std::make_unique(); + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.h b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.h new file mode 100644 index 0000000000..af52bd7f4f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Public/AppInstallerRepositorySource.h" +#include "SourceFactory.h" +#include + +namespace AppInstaller::Repository::Rest +{ + using namespace std::string_view_literals; + + // A source where the information is stored on a REST based server. + // In addition, the manifest information is also available on the server. + // Arg :: Expected to be a API which supports querying functionality. + struct RestSourceFactory + { + // Get the type string for this source. + static constexpr std::string_view Type() + { + using namespace std::string_view_literals; + return "Microsoft.Rest"sv; + } + + // Creates a source factory for this type. + static std::unique_ptr Create(); + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp new file mode 100644 index 0000000000..cb7ef38800 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.cpp @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/HttpClientHelper.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" +#include "Rest/Schema/Json/JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" +#include "Rest/Schema/Json/ManifestDeserializer.h" +#include "Rest/Schema/Json/SearchResponseDeserializer.h" +#include "winget/ManifestValidation.h" + +using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Rest::Schema::Json; + +namespace AppInstaller::Repository::Rest::Schema::V1_0 +{ + // Endpoint constants + constexpr std::string_view ManifestSearchPostEndpoint = "/manifestSearch"sv; + constexpr std::string_view ManifestByVersionAndChannelGetEndpoint = "/packageManifests/"sv; + + // Search body constants + constexpr std::string_view FetchAllManifests = "fetchAllManifests"sv; + + namespace + { + web::json::value GetSearchBody(const SearchRequest& searchRequest) + { + // TODO: Use search request to construct search body. + UNREFERENCED_PARAMETER(searchRequest); + + web::json::value json_body; + json_body[JsonHelper::GetUtilityString(FetchAllManifests)] = web::json::value::string(L"true"); + + return json_body; + } + + std::string GetRestAPIBaseUri(std::string restApiUri) + { + if (!restApiUri.empty() && restApiUri.back() == '/') + { + restApiUri.pop_back(); + } + + return restApiUri; + } + + utility::string_t GetSearchEndpoint(const std::string& restApiUri) + { + std::string fullSearchAPI = restApiUri; + return utility::conversions::to_string_t(fullSearchAPI.append(ManifestSearchPostEndpoint)); + } + + utility::string_t GetManifestByVersionEndpoint( + const std::string& restApiUri, const std::string& packageId, const std::string& version, const std::string& channel) + { + std::string versionEndpoint = restApiUri; + versionEndpoint.append(ManifestByVersionAndChannelGetEndpoint).append(packageId); + + // Add Version Query param + // TODO: Encode the URL. + if (!version.empty()) + { + versionEndpoint.append("?version=").append(version); + } + + // Add Channel Query param + if (version.empty() && !channel.empty()) + { + versionEndpoint.append("?channel=").append(channel); + } + else if (!channel.empty()) + { + versionEndpoint.append("&channel=").append(channel); + } + + return utility::conversions::to_string_t(versionEndpoint); + } + } + + Interface::Interface(const std::string& restApi) + { + m_restApiUri = GetRestAPIBaseUri(restApi); + m_searchEndpoint = GetSearchEndpoint(m_restApiUri); + m_requiredRestApiHeaders.emplace_back( + std::pair(JsonHelper::GetUtilityString(ContractVersion), JsonHelper::GetUtilityString(GetVersion()))); + } + + std::string Interface::GetVersion() const + { + // TODO: Change type to Version if necessary. + return "1.0.0"; + } + + IRestClient::SearchResult Interface::Search(const SearchRequest& request) const + { + // Optimization + if (MeetsOptimizedSearchCriteria(request)) + { + return OptimizedSearch(request); + } + + // TODO: Handle continuation token + HttpClientHelper clientHelper{ m_searchEndpoint }; + web::json::value jsonObject = clientHelper.HandlePost(GetSearchBody(request), m_requiredRestApiHeaders); + + SearchResponseDeserializer searchResponseDeserializer; + return searchResponseDeserializer.Deserialize(jsonObject); + } + + std::optional Interface::GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const + { + std::vector manifests = GetManifests(packageId, version, channel); + + // TODO: Handle multiple manifest selection. + if (manifests.size() > 0) + { + return manifests.at(0); + } + + return {}; + } + + bool Interface::MeetsOptimizedSearchCriteria(const SearchRequest& request) const + { + if (!request.Query && request.Inclusions.size() == 0 && + request.Filters.size() == 1 && request.Filters[0].Field == PackageMatchField::Id && + request.Filters[0].Type == MatchType::Exact) + { + AICLI_LOG(Repo, Verbose, << "Search request meets optimized search criteria."); + return true; + } + + return false; + } + + IRestClient::SearchResult Interface::OptimizedSearch(const SearchRequest& request) const + { + // TODO: Send in VersionLatest = true query param. + SearchResult searchResult; + std::vector manifests = GetManifests(request.Filters[0].Value, {}, {}); + + if (manifests.size() > 0) + { + // TODO: After adding the VersionLatest query param, we should be expecting one or no version. Using the first one until then. + Manifest::Manifest manifest = manifests.at(0); + PackageInfo packageInfo = PackageInfo{ + manifest.Id, + manifest.DefaultLocalization.Get(), + manifest.DefaultLocalization.Get() }; + + std::vector versions; + versions.emplace_back( + VersionInfo{ AppInstaller::Utility::VersionAndChannel {manifest.Version, manifest.Channel}, std::move(manifest) }); + + Package package = Package{ std::move(packageInfo), std::move(versions) }; + searchResult.Matches.emplace_back(std::move(package)); + } + + return searchResult; + } + + std::vector Interface::GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const + { + // TODO: Make a list of query params supported instead of using function parameters. + std::vector results; + HttpClientHelper clientHelper{ GetManifestByVersionEndpoint(m_restApiUri, packageId, version, channel) }; + web::json::value jsonObject = clientHelper.HandleGet(m_requiredRestApiHeaders); + + // Parse json and return Manifests + ManifestDeserializer manifestDeserializer; + std::vector manifests = manifestDeserializer.Deserialize(jsonObject); + + // Manifest validation + for (auto& manifestItem : manifests) + { + Manifest::Manifest manifest = manifestItem; + std::vector validationErrors = + AppInstaller::Manifest::ValidateManifest(manifest); + + int errors = 0; + for (auto& error : validationErrors) + { + if (error.ErrorLevel == Manifest::ValidationError::Level::Error) + { + AICLI_LOG(Repo, Error, << "Received manifest contains validation error: " << error.Message); + errors++; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, errors > 0); + + results.emplace_back(manifest); + } + + return results; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h new file mode 100644 index 0000000000..8ea16261cc --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/IRestClient.h" +#include "cpprest/json.h" +#include + +namespace AppInstaller::Repository::Rest::Schema::V1_0 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public IRestClient + { + Interface(const std::string& restApi); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + std::string GetVersion() const override; + IRestClient::SearchResult Search(const SearchRequest& request) const override; + std::optional GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const override; + std::vector GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const override; + + protected: + bool MeetsOptimizedSearchCriteria(const SearchRequest& request) const; + IRestClient::SearchResult OptimizedSearch(const SearchRequest& request) const; + + private: + std::string m_restApiUri; + utility::string_t m_searchEndpoint; + std::vector> m_requiredRestApiHeaders; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h new file mode 100644 index 0000000000..b34bc106e7 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Microsoft/Schema/Version.h" +#include +#include + +namespace AppInstaller::Repository::Rest::Schema +{ + // The common interface used to interact with RestAPI responses. + struct IRestClient + { + virtual ~IRestClient() = default; + + struct PackageInfo + { + std::string PackageIdentifier; + std::string PackageName; + std::string Publisher; + + PackageInfo(std::string packageIdentifier, std::string packageName, std::string publisher) + : PackageIdentifier(packageIdentifier), PackageName(packageName), Publisher(publisher) {} + }; + + struct VersionInfo + { + AppInstaller::Utility::VersionAndChannel VersionAndChannel; + std::optional Manifest; + + VersionInfo(AppInstaller::Utility::VersionAndChannel versionAndChannel, std::optional manifest) + : VersionAndChannel(versionAndChannel), Manifest(manifest) {} + }; + + // Minimal information retrieved for any search request. + struct Package + { + PackageInfo PackageInformation; + std::vector Versions; + + Package(PackageInfo packageInfo, std::vector versions) + : PackageInformation(packageInfo), Versions(versions) {} + }; + + struct SearchResult + { + std::vector Matches; + bool Truncated = false; + }; + + // Information endpoint models + struct Information + { + std::string SourceIdentifier; + std::vector ServerSupportedVersions; + + Information(std::string sourceId, std::vector versions) + : SourceIdentifier(sourceId), ServerSupportedVersions(versions) {} + }; + + // Get interface version. + virtual std::string GetVersion() const = 0; + + // Performs a search based on the given criteria. + virtual SearchResult Search(const SearchRequest& request) const = 0; + + // Gets the manifest for given version + virtual std::optional GetManifestByVersion(const std::string& packageId, const std::string& version, const std::string& channel) const = 0; + + // Gets the manifests for given query parameters + virtual std::vector GetManifests(const std::string& packageId, const std::string& version, const std::string& channel) const = 0; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h new file mode 100644 index 0000000000..608fa14c23 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/CommonRestConstants.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // General API response constants + constexpr std::string_view Data = "Data"sv; + constexpr std::string_view ContractVersion = "Version"sv; // TODO: Finalize name + + // General endpoint constants + constexpr std::string_view InformationGetEndpoint = "/information"sv; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp new file mode 100644 index 0000000000..a636bd6243 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.cpp @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include +#include "JsonHelper.h" +#include "InformationResponseDeserializer.h" +#include "CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Information response constants + constexpr std::string_view SourceIdentifier = "SourceIdentifier"sv; + constexpr std::string_view ServerSupportedVersions = "ServerSupportedVersions"sv; + } + + IRestClient::Information InformationResponseDeserializer::Deserialize(const web::json::value& dataObject) const + { + // Get information result from json output. + std::optional information = DeserializeInformation(dataObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !information); + + return information.value(); + } + + std::optional InformationResponseDeserializer::DeserializeInformation(const web::json::value& dataObject) const + { + try + { + std::optional> data = JsonHelper::GetJsonValueFromNode(dataObject, JsonHelper::GetUtilityString(Data)); + if (!data) + { + AICLI_LOG(Repo, Error, << "Missing data"); + return {}; + } + + auto& dataValue = data.value().get(); + std::optional sourceId = JsonHelper::GetRawStringValueFromJsonNode(dataValue, JsonHelper::GetUtilityString(SourceIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(sourceId)) + { + AICLI_LOG(Repo, Error, << "Missing source identifier"); + return {}; + } + + std::optional> versions = JsonHelper::GetRawJsonArrayFromJsonNode(dataValue, JsonHelper::GetUtilityString(ServerSupportedVersions)); + + if (!versions || versions.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing supported versions"); + return {}; + } + + std::vector allVersions; + for (auto& versionItem : versions.value().get()) + { + std::optional sp = JsonHelper::GetRawStringValueFromJsonValue(versionItem); + if (sp) + { + allVersions.emplace_back(std::move(sp.value())); + } + } + + if (allVersions.size() == 0) + { + AICLI_LOG(Repo, Error, << "Received incomplete information."); + return {}; + } + + IRestClient::Information info{ std::move(sourceId.value()), std::move(allVersions) }; + return info; + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Received invalid information."); + } + + return {}; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h new file mode 100644 index 0000000000..2d029364a6 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/InformationResponseDeserializer.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Information response Deserializer. + struct InformationResponseDeserializer + { + // Gets the information model for given response + IRestClient::Information Deserialize(const web::json::value& dataObject) const; + + protected: + std::optional DeserializeInformation(const web::json::value& dataObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp new file mode 100644 index 0000000000..76b05fa447 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "JsonHelper.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + utility::string_t JsonHelper::GetUtilityString(std::string_view nodeName) + { + return utility::conversions::to_string_t(nodeName.data()); + } + + std::optional> JsonHelper::GetJsonValueFromNode(const web::json::value& node, const utility::string_t& keyName) + { + if (node.is_null() || !node.has_field(keyName)) + { + return {}; + } + + return node.at(keyName); + } + + std::optional JsonHelper::GetRawStringValueFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_string()) + { + return {}; + } + + return utility::conversions::to_utf8string(value.as_string()); + } + + std::optional JsonHelper::GetRawStringValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName) + { + std::optional> jsonValue = GetJsonValueFromNode(node, keyName); + + if (jsonValue) + { + return GetRawStringValueFromJsonValue(jsonValue.value().get()); + } + + return {}; + } + + std::optional JsonHelper::GetRawIntValueFromJsonValue(const web::json::value& value) + { + if (value.is_null() || !value.is_integer()) + { + return {}; + } + + return value.as_integer(); + } + + std::optional> JsonHelper::GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName) + { + std::optional> jsonValue = GetJsonValueFromNode(node, keyName); + + if (!jsonValue || !jsonValue.value().get().is_array()) + { + return {}; + } + + return jsonValue.value().get().as_array(); + } + + std::vector JsonHelper::GetRawStringArrayFromJsonNode( + const web::json::value& node, const utility::string_t& keyName) + { + std::optional> arrayValue = GetRawJsonArrayFromJsonNode(node, keyName); + + std::vector result; + if (!arrayValue) + { + return result; + } + + for (auto& value : arrayValue.value().get()) + { + std::optional item = JsonHelper::GetRawStringValueFromJsonValue(value); + if (item) + { + result.emplace_back(std::move(item.value())); + } + } + + return result; + } + + bool JsonHelper::IsValidNonEmptyStringValue(std::optional& value) + { + if (Utility::IsEmptyOrWhitespace(value.value_or(""))) + { + return false; + } + + return true; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h new file mode 100644 index 0000000000..bf0ad7a176 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/JsonHelper.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include +#include "winget/Manifest.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Json helper. + struct JsonHelper + { + static std::optional> GetJsonValueFromNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional GetRawStringValueFromJsonValue(const web::json::value& value); + + static std::optional GetRawStringValueFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional> GetRawJsonArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + + static std::optional GetRawIntValueFromJsonValue(const web::json::value& node); + + static utility::string_t GetUtilityString(std::string_view nodeName); + + static std::vector GetRawStringArrayFromJsonNode(const web::json::value& node, const utility::string_t& keyName); + + static bool IsValidNonEmptyStringValue(std::optional& value); + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp new file mode 100644 index 0000000000..c49c6109cb --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.cpp @@ -0,0 +1,417 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_0/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/HttpClientHelper.h" +#include "cpprest/http_client.h" +#include "cpprest/json.h" +#include "ManifestDeserializer.h" +#include "JsonHelper.h" +#include "Rest/Schema/Json/CommonRestConstants.h" + +using namespace AppInstaller::Manifest; + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Manifest response constants specific to this deserializer + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "PackageVersion"sv; + constexpr std::string_view Channel = "Channel"sv; + + // Locale + constexpr std::string_view DefaultLocale = "DefaultLocale"sv; + constexpr std::string_view Locales = "Locales"sv; + constexpr std::string_view PackageLocale = "PackageLocale"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PublisherUrl = "PublisherUrl"sv; + constexpr std::string_view PublisherSupportUrl = "PublisherSupportUrl"sv; + constexpr std::string_view PrivacyUrl = "PrivacyUrl"sv; + constexpr std::string_view Author = "Author"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view PackageUrl = "PackageUrl"sv; + constexpr std::string_view License = "License"sv; + constexpr std::string_view LicenseUrl = "LicenseUrl"sv; + constexpr std::string_view Copyright = "Copyright"sv; + constexpr std::string_view CopyrightUrl = "CopyrightUrl"sv; + constexpr std::string_view ShortDescription = "ShortDescription"sv; + constexpr std::string_view Description = "Description"sv; + constexpr std::string_view Tags = "Tags"sv; + constexpr std::string_view Moniker = "Moniker"sv; + + // Installer + constexpr std::string_view Installers = "Installers"sv; + constexpr std::string_view InstallerIdentifier = "InstallerIdentifier"sv; + constexpr std::string_view InstallerSha256 = "InstallerSha256"sv; + constexpr std::string_view InstallerUrl = "InstallerUrl"sv; + constexpr std::string_view Architecture = "Architecture"sv; + constexpr std::string_view InstallerLocale = "InstallerLocale"sv; + constexpr std::string_view Platform = "Platform"sv; + constexpr std::string_view MinimumOSVersion = "MinimumOSVersion"sv; + constexpr std::string_view InstallerType = "InstallerType"sv; + constexpr std::string_view Scope = "Scope"sv; + constexpr std::string_view SignatureSha256 = "SignatureSha256"sv; + constexpr std::string_view InstallModes = "InstallModes"sv; + + // Installer switches + constexpr std::string_view InstallerSwitches = "InstallerSwitches"sv; + constexpr std::string_view Silent = "Silent"sv; + constexpr std::string_view SilentWithProgress = "SilentWithProgress"sv; + constexpr std::string_view Interactive = "Interactive"sv; + constexpr std::string_view InstallLocation = "InstallLocation"sv; + constexpr std::string_view Log = "Log"sv; + constexpr std::string_view Upgrade = "Upgrade"sv; + constexpr std::string_view Custom = "Custom"sv; + + constexpr std::string_view InstallerSuccessCodes = "InstallerSuccessCodes"sv; + constexpr std::string_view UpgradeBehavior = "UpgradeBehavior"sv; + constexpr std::string_view Commands = "Commands"sv; + constexpr std::string_view Protocols = "Protocols"sv; + constexpr std::string_view FileExtensions = "FileExtensions"sv; + + // Dependencies + constexpr std::string_view Dependencies = "Dependencies"sv; + constexpr std::string_view WindowsFeatures = "WindowsFeatures"sv; + constexpr std::string_view WindowsLibraries = "WindowsLibraries"sv; + constexpr std::string_view PackageDependencies = "PackageDependencies"sv; + constexpr std::string_view MinimumVersion = "MinimumVersion"sv; + constexpr std::string_view ExternalDependencies = "ExternalDependencies"sv; + + constexpr std::string_view Capabilities = "Capabilities"sv; + constexpr std::string_view RestrictedCapabilities = "RestrictedCapabilities"sv; + } + + std::vector ManifestDeserializer::Deserialize(const web::json::value& dataJsonObject) const + { + // Get manifest from json output. + std::optional> manifests = DeserializeVersion(dataJsonObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !manifests); + + return manifests.value(); + } + + std::optional> ManifestDeserializer::DeserializeVersion(const web::json::value& dataJsonObject) const + { + std::vector manifests; + try + { + std::optional> manifestObject = + JsonHelper::GetJsonValueFromNode(dataJsonObject, JsonHelper::GetUtilityString(Data)); + + if (!manifestObject || manifestObject.value().get().is_null()) + { + AICLI_LOG(Repo, Verbose, << "No manifest results returned."); + return manifests; + } + + auto& manifestJsonObject = manifestObject.value().get(); + std::optional id = JsonHelper::GetRawStringValueFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(PackageIdentifier)); + if (!JsonHelper::IsValidNonEmptyStringValue(id)) + { + AICLI_LOG(Repo, Error, << "Missing package identifier."); + return {}; + } + + std::optional> versions = JsonHelper::GetRawJsonArrayFromJsonNode(manifestJsonObject, JsonHelper::GetUtilityString(Versions)); + if (!versions || versions.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing versions in package: " << id.value()); + return {}; + } + + const web::json::array versionNodes = versions.value().get(); + for (auto& versionItem : versionNodes) + { + Manifest::Manifest manifest; + manifest.Id = id.value(); + + std::optional packageVersion = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageVersion)) + { + AICLI_LOG(Repo, Error, << "Missing package version in package: " << manifest.Id); + return {}; + } + manifest.Version = std::move(packageVersion.value()); + + manifest.Channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + + // Default locale + auto& defaultLocale = versionItem.at(JsonHelper::GetUtilityString(DefaultLocale)); + std::optional defaultLocaleObject = DeserializeLocale(defaultLocale); + if (!defaultLocaleObject) + { + AICLI_LOG(Repo, Error, << "Missing default locale in package: " << manifest.Id); + return {}; + } + manifest.DefaultLocalization = std::move(defaultLocaleObject.value()); + + // Moniker is in Default locale + manifest.Moniker = JsonHelper::GetRawStringValueFromJsonNode(defaultLocale, JsonHelper::GetUtilityString(Moniker)).value_or(""); + + // Installers + std::optional> installers = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Installers)); + if (!installers || installers.value().get().size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing installers in package: " << manifest.Id); + return {}; + } + + for (auto& installer : installers.value().get()) + { + std::optional installerObject = DeserializeInstaller(installer); + if (installerObject) + { + manifest.Installers.emplace_back(std::move(installerObject.value())); + } + } + + if (manifest.Installers.size() == 0) + { + AICLI_LOG(Repo, Error, << "Missing valid installers in package: " << manifest.Id); + return {}; + } + + // Other locales + std::optional> locales = JsonHelper::GetRawJsonArrayFromJsonNode(versionItem, JsonHelper::GetUtilityString(Locales)); + if (locales) + { + for (auto& locale : locales.value().get()) + { + std::optional localeObject = DeserializeLocale(locale); + if (localeObject) + { + manifest.Localizations.emplace_back(std::move(localeObject.value())); + } + } + } + + manifests.emplace_back(std::move(manifest)); + } + } + catch (...) + { + AICLI_LOG(Repo, Error, << "Error encountered while deserializing manifest..."); + return {}; + } + + return manifests; + } + + std::optional ManifestDeserializer::DeserializeLocale(const web::json::value& localeJsonObject) const + { + if (localeJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestLocalization locale; + std::optional packageLocale = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageLocale)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageLocale)) + { + AICLI_LOG(Repo, Error, << "Missing package locale."); + return {}; + } + locale.Locale = std::move(packageLocale.value()); + + std::optional packageName = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageName)); + if (!JsonHelper::IsValidNonEmptyStringValue(packageName)) + { + AICLI_LOG(Repo, Error, << "Missing package name."); + return {}; + } + locale.Add(std::move(packageName.value())); + + std::optional publisher = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Publisher)); + if (!JsonHelper::IsValidNonEmptyStringValue(publisher)) + { + AICLI_LOG(Repo, Error, << "Missing publisher."); + return {}; + } + locale.Add(std::move(publisher.value())); + + std::optional shortDescription = JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(ShortDescription)); + if (!JsonHelper::IsValidNonEmptyStringValue(shortDescription)) + { + AICLI_LOG(Repo, Error, << "Missing short description."); + return {}; + } + locale.Add(std::move(shortDescription.value())); + + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PublisherSupportUrl)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PrivacyUrl)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Author)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(PackageUrl)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(License)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(LicenseUrl)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Copyright)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(CopyrightUrl)).value_or("")); + locale.Add(JsonHelper::GetRawStringValueFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Description)).value_or("")); + locale.Add(JsonHelper::GetRawStringArrayFromJsonNode(localeJsonObject, JsonHelper::GetUtilityString(Tags))); + + return locale; + } + + std::optional ManifestDeserializer::DeserializeInstaller(const web::json::value& installerJsonObject) const + { + if (installerJsonObject.is_null()) + { + return {}; + } + + Manifest::ManifestInstaller installer; + std::optional url = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerUrl)); + if (!JsonHelper::IsValidNonEmptyStringValue(url)) + { + AICLI_LOG(Repo, Error, << "Missing installer url."); + return {}; + } + installer.Url = std::move(url.value()); + + std::optional sha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSha256)); + if (!JsonHelper::IsValidNonEmptyStringValue(sha256)) + { + AICLI_LOG(Repo, Error, << "Missing installer SHA256."); + return {}; + } + installer.Sha256 = Utility::SHA256::ConvertToBytes(sha256.value()); + + std::optional arch = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Architecture)); + if (!JsonHelper::IsValidNonEmptyStringValue(arch)) + { + AICLI_LOG(Repo, Error, << "Missing installer architecture."); + return {}; + } + installer.Arch = Utility::ConvertToArchitectureEnum(arch.value()); + + installer.InstallerType = Manifest::ConvertToInstallerTypeEnum( + JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerType)).value_or("")); + installer.Locale = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerLocale)).value_or(""); + + // platform + std::optional> platforms = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Platform)); + if (platforms) + { + for (auto& platform : platforms.value().get()) + { + std::optional platformValue = JsonHelper::GetRawStringValueFromJsonValue(platform); + if (platformValue) + { + installer.Platform.emplace_back(Manifest::ConvertToPlatformEnum(platformValue.value())); + } + } + } + + installer.MinOSVersion = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(MinimumOSVersion)).value_or(""); + std::optional scope = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Scope)); + if (scope) + { + installer.Scope = Manifest::ConvertToScopeEnum(scope.value()); + } + + std::optional signatureSha256 = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(SignatureSha256)); + if (signatureSha256) + { + installer.SignatureSha256 = Utility::SHA256::ConvertToBytes(signatureSha256.value()); + } + + // Install modes + std::optional> installModes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallModes)); + if (installModes) + { + for (auto& mode : installModes.value().get()) + { + std::optional modeObject = JsonHelper::GetRawStringValueFromJsonValue(mode); + if (modeObject) + { + installer.InstallModes.emplace_back(Manifest::ConvertToInstallModeEnum(modeObject.value())); + } + } + } + + // Installer Switches + auto& installerSwitches = installerJsonObject.at(JsonHelper::GetUtilityString(InstallerSwitches)); + installer.Switches[InstallerSwitchType::Silent] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Silent)).value_or(""); + installer.Switches[InstallerSwitchType::SilentWithProgress] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(SilentWithProgress)).value_or(""); + installer.Switches[InstallerSwitchType::Interactive] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Interactive)).value_or(""); + installer.Switches[InstallerSwitchType::InstallLocation] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(InstallLocation)).value_or(""); + installer.Switches[InstallerSwitchType::Log] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Log)).value_or(""); + installer.Switches[InstallerSwitchType::Update] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Upgrade)).value_or(""); + installer.Switches[InstallerSwitchType::Custom] = JsonHelper::GetRawStringValueFromJsonNode(installerSwitches, JsonHelper::GetUtilityString(Custom)).value_or(""); + + // Installer SuccessCodes + std::optional> installSuccessCodes = JsonHelper::GetRawJsonArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(InstallerSuccessCodes)); + if (installSuccessCodes) + { + for (auto& code : installSuccessCodes.value().get()) + { + std::optional codeValue = JsonHelper::GetRawIntValueFromJsonValue(code); + if (codeValue) + { + installer.InstallerSuccessCodes.emplace_back(std::move(codeValue.value())); + } + } + } + + std::optional updateBehavior = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(UpgradeBehavior)); + if (updateBehavior) + { + installer.UpdateBehavior = Manifest::ConvertToUpdateBehaviorEnum(updateBehavior.value()); + } + + installer.Commands = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Commands)); + installer.Protocols = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Protocols)); + installer.FileExtensions = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(FileExtensions)); + + // Dependencies + auto& dependenciesObject = installerJsonObject.at(JsonHelper::GetUtilityString(Dependencies)); + std::optional dependency = DeserializeDependency(dependenciesObject); + if (dependency) + { + installer.Dependencies = std::move(dependency.value()); + } + + installer.PackageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); + installer.ProductCode = JsonHelper::GetRawStringValueFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(ProductCode)).value_or(""); + installer.Capabilities = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(Capabilities)); + installer.RestrictedCapabilities = JsonHelper::GetRawStringArrayFromJsonNode(installerJsonObject, JsonHelper::GetUtilityString(RestrictedCapabilities)); + + return installer; + } + + std::optional ManifestDeserializer::DeserializeDependency(const web::json::value& dependenciesObject) const + { + if (dependenciesObject.is_null()) + { + return {}; + } + + Manifest::Dependency dependency; + + dependency.WindowsFeatures = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsFeatures)); + dependency.WindowsLibraries = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(WindowsLibraries)); + dependency.ExternalDependencies = JsonHelper::GetRawStringArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(ExternalDependencies)); + + // Package Dependencies + std::optional> packageDependencies = JsonHelper::GetRawJsonArrayFromJsonNode(dependenciesObject, JsonHelper::GetUtilityString(PackageDependencies)); + if (packageDependencies) + { + for (auto& packageDependency : packageDependencies.value().get()) + { + std::optional id = JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(PackageIdentifier)); + if (id) + { + PackageDependency pkg{ std::move(id.value()) , JsonHelper::GetRawStringValueFromJsonNode(packageDependency, JsonHelper::GetUtilityString(MinimumVersion)).value_or("") }; + dependency.PackageDependencies.emplace_back(std::move(pkg)); + } + } + } + + return dependency; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h new file mode 100644 index 0000000000..0a177b04bb --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/ManifestDeserializer.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Manifest Deserializer. + struct ManifestDeserializer + { + // Gets the manifest from the given json object + std::vector Deserialize(const web::json::value& dataJsonObject) const; + + protected: + std::optional> DeserializeVersion(const web::json::value& dataJsonObject) const; + + std::optional DeserializeLocale(const web::json::value& localeJsonObject) const; + + std::optional DeserializeInstaller(const web::json::value& installerJsonObject) const; + + std::optional DeserializeDependency(const web::json::value& dependenciesJsonObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp new file mode 100644 index 0000000000..240b88c987 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/IRestClient.h" +#include "SearchResponseDeserializer.h" +#include +#include "JsonHelper.h" +#include "CommonRestConstants.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + namespace + { + // Search response constants + constexpr std::string_view PackageIdentifier = "PackageIdentifier"sv; + constexpr std::string_view PackageName = "PackageName"sv; + constexpr std::string_view Publisher = "Publisher"sv; + constexpr std::string_view PackageFamilyName = "PackageFamilyName"sv; + constexpr std::string_view ProductCode = "ProductCode"sv; + constexpr std::string_view Versions = "Versions"sv; + constexpr std::string_view PackageVersion = "Version"sv; + constexpr std::string_view Channel = "Channel"sv; + } + + IRestClient::SearchResult SearchResponseDeserializer::Deserialize(const web::json::value& searchResponseObject) const + { + std::optional response = DeserializeSearchResult(searchResponseObject); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_DATA, !response); + + return response.value(); + } + + std::optional SearchResponseDeserializer::DeserializeSearchResult(const web::json::value& searchResponseObject) const + { + // Make search result from json output. + if (searchResponseObject.is_null()) + { + return {}; + } + + IRestClient::SearchResult result; + try + { + std::optional> dataArray = JsonHelper::GetRawJsonArrayFromJsonNode(searchResponseObject, JsonHelper::GetUtilityString(Data)); + if (!dataArray || dataArray.value().get().size() == 0) + { + AICLI_LOG(Repo, Verbose, << "No search results returned."); + return result; + } + + for (auto& manifestItem : dataArray.value().get()) + { + std::optional packageId = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageIdentifier)); + std::optional packageName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageName)); + std::optional publisher = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Publisher)); + + if (!JsonHelper::IsValidNonEmptyStringValue(packageId) || !JsonHelper::IsValidNonEmptyStringValue(packageName) || !JsonHelper::IsValidNonEmptyStringValue(packageName)) + { + AICLI_LOG(Repo, Error, << "Missing required package fields in manifest search results."); + return {}; + } + + std::string packageFamilyName = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(PackageFamilyName)).value_or(""); + std::string productCode = JsonHelper::GetRawStringValueFromJsonNode(manifestItem, JsonHelper::GetUtilityString(ProductCode)).value_or(""); + std::optional> versionValue = JsonHelper::GetRawJsonArrayFromJsonNode(manifestItem, JsonHelper::GetUtilityString(Versions)); + std::vector versionList; + + if (versionValue) + { + for (auto& versionItem : versionValue.value().get()) + { + std::optional version = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(PackageVersion)); + if (!JsonHelper::IsValidNonEmptyStringValue(version)) + { + AICLI_LOG(Repo, Error, << "Received incomplete package version in package: " << packageId.value()); + return {}; + } + + std::string channel = JsonHelper::GetRawStringValueFromJsonNode(versionItem, JsonHelper::GetUtilityString(Channel)).value_or(""); + versionList.emplace_back(IRestClient::VersionInfo{ + AppInstaller::Utility::VersionAndChannel{std::move(version.value()), std::move(channel)}, {} }); + } + } + + if (versionList.size() == 0) + { + AICLI_LOG(Repo, Error, << "Received no versions in package: " << packageId.value()); + return {}; + } + + IRestClient::PackageInfo packageInfo{ + std::move(packageId.value()), std::move(packageName.value()), std::move(publisher.value()) }; + IRestClient::Package package{ std::move(packageInfo), std::move(versionList) }; + result.Matches.emplace_back(std::move(package)); + } + } + catch (...) + { + // TODO: Catch known types and log error information from them + AICLI_LOG(Repo, Error, << "Error encountered while deserializing search result..."); + return {}; + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h new file mode 100644 index 0000000000..a5e946d293 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Rest/Schema/Json/SearchResponseDeserializer.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "cpprest/json.h" +#include "Rest/Schema/IRestClient.h" + +namespace AppInstaller::Repository::Rest::Schema::Json +{ + // Search Result Deserializer. + struct SearchResponseDeserializer + { + // Gets the search result for given version + IRestClient::SearchResult Deserialize(const web::json::value& searchResultJsonObject) const; + + protected: + std::optional DeserializeSearchResult(const web::json::value& searchResultJsonObject) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp index 71cf1cd21e..f99520c3a7 100644 --- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp @@ -323,6 +323,17 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::LiteralColumn(std::string_view value) + { + if (m_needsComma) + { + m_stream << ", "; + } + AddBindFunctor(AppendOpAndBinder(Op::Literal), value); + m_needsComma = true; + return *this; + } + StatementBuilder& StatementBuilder::Escape(std::string_view escapeChar) { THROW_HR_IF(E_INVALIDARG, escapeChar.length() != 1); @@ -773,6 +784,9 @@ namespace AppInstaller::Repository::SQLite::Builder case Op::Escape: m_stream << " ESCAPE ?"; break; + case Op::Literal: + m_stream << " ?"; + break; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h index 35cf236b27..a5f5453737 100644 --- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h +++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h @@ -239,6 +239,8 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& LikeWithEscape(std::string_view value); StatementBuilder& Like(details::unbound_t); + StatementBuilder& LiteralColumn(std::string_view value); + StatementBuilder& Escape(std::string_view escapeChar); StatementBuilder& Not(); @@ -394,7 +396,8 @@ namespace AppInstaller::Repository::SQLite::Builder { Equals, Like, - Escape + Escape, + Literal, }; // Appends given the operation. diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index 306265b2bb..5a97641e40 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -11,51 +11,63 @@ using namespace std::filesystem; -std::string_view registrySubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; -std::string_view defaultProductID = "{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view registrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::wstring_view defaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::wstring_view defaultVersion = L"1.0.0.0"; -std::wstring GenerateUninstaller(std::ostream& out, const path& installDirectory) { +path GenerateUninstaller(std::wostream& out, const path& installDirectory, const std::wstring& productID) +{ path uninstallerPath = installDirectory; uninstallerPath /= "UninstallTestExe.bat"; - out << "Uninstaller located at path: " << uninstallerPath << '\n'; + out << "Uninstaller located at path: " << uninstallerPath << std::endl; path uninstallerOutputTextFilePath = installDirectory; uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; - // TODO: Needs to re-invoke the installer and remove the Uninstall key that it added - std::ofstream uninstallerScript(uninstallerPath); + std::wstring registryKey{ L"HKEY_CURRENT_USER\\" }; + registryKey += registrySubkey; + if (!productID.empty()) + { + registryKey += productID; + } + else + { + registryKey += defaultProductID; + } + + std::wofstream uninstallerScript(uninstallerPath); uninstallerScript << "@echo off\n"; uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; + uninstallerScript << "REG DELETE " << registryKey << " /f\n"; uninstallerScript.close(); - return uninstallerPath.wstring(); + return uninstallerPath; } -void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, const std::wstring& uninstallerPath) +void WriteToUninstallRegistry(std::wostream& out, const std::wstring& productID, const path& uninstallerPath, const std::wstring& displayVersion) { HKEY hkey; LONG lReg; // String inputs to registry must be of wide char type const wchar_t* displayName = L"AppInstallerTestExeInstaller"; - const wchar_t* displayVersion = L"1.0.0.0"; const wchar_t* publisher = L"Microsoft Corporation"; - const wchar_t* uninstallString = uninstallerPath.c_str(); + std::wstring uninstallString = uninstallerPath.wstring(); DWORD version = 1; - path registryKey{ registrySubkey }; + std::wstring registryKey{ registrySubkey }; if (!productID.empty()) { - registryKey /= productID; - out << "Product Code overridden to: " << registryKey << "\n"; + registryKey += productID; + out << "Product Code overridden to: " << registryKey << std::endl; } else { - registryKey /= defaultProductID; - out << "Default Product Code used: " << registryKey << "\n"; + registryKey += defaultProductID; + out << "Default Product Code used: " << registryKey << std::endl; } lReg = RegCreateKeyEx( @@ -69,104 +81,131 @@ void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, &hkey, NULL); - if (lReg == ERROR_SUCCESS) { - - out << "Successfully opened registry key \n"; + if (lReg == ERROR_SUCCESS) + { + out << "Successfully opened registry key" << std::endl; // Set Display Name Property Value if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName, (DWORD)(wcslen(displayName) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - out << "Failed to write DisplayName value. Error Code: " << res << "\n"; + out << "Failed to write DisplayName value. Error Code: " << res << std::endl; } // Set Display Version Property Value - if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion, (DWORD)(wcslen(displayVersion) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion.c_str(), (DWORD)(displayVersion.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - out << "Failed to write DisplayVersion value. Error Code: " << res << "\n"; + out << "Failed to write DisplayVersion value. Error Code: " << res << std::endl; } // Set Publisher Property Value if (LONG res = RegSetValueEx(hkey, L"Publisher", NULL, REG_SZ, (LPBYTE)publisher, (DWORD)(wcslen(publisher) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - out << "Failed to write Publisher value. Error Code: " << res << "\n"; + out << "Failed to write Publisher value. Error Code: " << res << std::endl; } // Set UninstallString Property Value - if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString, (DWORD)wcslen(uninstallString + 1) * sizeof(wchar_t*)) != ERROR_SUCCESS) + if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString.c_str(), (DWORD)(uninstallString.length() + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - out << "Failed to write UninstallString value. Error Code: " << res << "\n"; + out << "Failed to write UninstallString value. Error Code: " << res << std::endl; } // Set Version Property Value if (LONG res = RegSetValueEx(hkey, L"Version", NULL, REG_DWORD, (LPBYTE)&version, sizeof(version)) != ERROR_SUCCESS) { - out << "Failed to write Version value. Error Code: " << res << "\n"; + out << "Failed to write Version value. Error Code: " << res << std::endl; } - out << "Write to registry key completed \n"; + out << "Write to registry key completed" << std::endl; } else { - out << "Key Creation Failed\n"; + out << "Key Creation Failed" << std::endl; } RegCloseKey(hkey); } // The installer prints all args to an output file and writes to the Uninstall registry key -int main(int argc, const char** argv) +int wmain(int argc, const wchar_t** argv) { path installDirectory = temp_directory_path(); - std::wstringstream productCodeStream; - std::stringstream outContent; + std::wstringstream outContent; std::wstring productCode; + std::wstring version; + int exitCode = 0; // Output to cout by default, but swap to a file if requested - std::ostream* out = &std::cout; - std::ofstream logFile; + std::wostream* out = &std::wcout; + std::wofstream logFile; for (int i = 1; i < argc; i++) { outContent << argv[i] << ' '; // Supports custom install path. - if (_stricmp(argv[i], "/InstallDir") == 0 && ++i < argc) + if (_wcsicmp(argv[i], L"/InstallDir") == 0) + { + if (++i < argc) + { + installDirectory = argv[i]; + outContent << argv[i] << ' '; + } + } + + // Supports custom exit code + else if (_wcsicmp(argv[i], L"/ExitCode") == 0) { - installDirectory = argv[i]; - outContent << argv[i] << ' '; + if (++i < argc) + { + exitCode = static_cast(std::stoll(argv[i], 0, 0)); + outContent << argv[i] << ' '; + } } - + // Supports custom product code ID - if (_stricmp(argv[i], "/ProductID") == 0 && ++i < argc) + else if (_wcsicmp(argv[i], L"/ProductID") == 0) { - productCodeStream << argv[i]; + if (++i < argc) + { + productCode = argv[i]; + } + } + + // Supports custom version + else if (_wcsicmp(argv[i], L"/Version") == 0) + { + if (++i < argc) + { + version = argv[i]; + } } // Supports log file - if (_stricmp(argv[i], "/LogFile") == 0 && ++i < argc) + else if (_wcsicmp(argv[i], L"/LogFile") == 0) { - logFile = std::ofstream(argv[i], std::ofstream::out | std::ofstream::trunc); - out = &logFile; + if (++i < argc) + { + logFile = std::wofstream(argv[i], std::wofstream::out | std::wofstream::trunc); + out = &logFile; + } } } + if (version.empty()) + { + version = defaultVersion; + } + path outFilePath = installDirectory; outFilePath /= "TestExeInstalled.txt"; - std::ofstream file(outFilePath, std::ofstream::out); + std::wofstream file(outFilePath, std::ofstream::out); file << outContent.str(); file.close(); - if (!productCodeStream.str().empty()) - { - productCode = productCodeStream.str(); - } - - std::wstring uninstallerPath = GenerateUninstaller(*out, installDirectory); - - WriteToUninstallRegistry(*out, productCode, uninstallerPath); - - return 0; -} + path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode); + WriteToUninstallRegistry(*out, productCode, uninstallerPath, version); + return exitCode; +} diff --git a/src/ManifestSchema/ManifestSchema.h b/src/ManifestSchema/ManifestSchema.h new file mode 100644 index 0000000000..136661d352 --- /dev/null +++ b/src/ManifestSchema/ManifestSchema.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define MANIFESTSCHEMA_RESOURCE_TYPE 200 + +#define IDX_MANIFEST_SCHEMA_PREVIEW 201 +#define IDX_MANIFEST_SCHEMA_V1_SINGLETON 202 +#define IDX_MANIFEST_SCHEMA_V1_VERSION 203 +#define IDX_MANIFEST_SCHEMA_V1_INSTALLER 204 +#define IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE 205 +#define IDX_MANIFEST_SCHEMA_V1_LOCALE 206 diff --git a/src/ManifestSchema/ManifestSchema.rc b/src/ManifestSchema/ManifestSchema.rc new file mode 100644 index 0000000000..b624ed6561 --- /dev/null +++ b/src/ManifestSchema/ManifestSchema.rc @@ -0,0 +1,71 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "ManifestSchema.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE 9, 1 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + +///////////////////////////////////////////////////////////////////////////// +// +// Manifest schema +// +IDX_MANIFEST_SCHEMA_PREVIEW MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\preview\\manifest.0.1.0.json" +IDX_MANIFEST_SCHEMA_V1_SINGLETON MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.singleton.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_VERSION MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.version.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_INSTALLER MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.installer.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.defaultLocale.1.0.0.json" +IDX_MANIFEST_SCHEMA_V1_LOCALE MANIFESTSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\manifests\\v1.0.0\\manifest.locale.1.0.0.json" diff --git a/src/ManifestSchema/ManifestSchema.vcxitems b/src/ManifestSchema/ManifestSchema.vcxitems new file mode 100644 index 0000000000..09f45ae722 --- /dev/null +++ b/src/ManifestSchema/ManifestSchema.vcxitems @@ -0,0 +1,31 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + {7d05f64d-ce5a-42aa-a2c1-e91458f061cf} + + + + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ManifestSchema/ManifestSchema.vcxitems.filters b/src/ManifestSchema/ManifestSchema.vcxitems.filters new file mode 100644 index 0000000000..31bafd420b --- /dev/null +++ b/src/ManifestSchema/ManifestSchema.vcxitems.filters @@ -0,0 +1,35 @@ + + + + + {d6998b42-8ce1-440a-ad12-8b505a284d7b} + + + + + + + + + + + + schema + + + schema + + + schema + + + schema + + + schema + + + schema + + + \ No newline at end of file diff --git a/src/ManifestSchema/resource.h b/src/ManifestSchema/resource.h new file mode 100644 index 0000000000..1f8b4abe6d --- /dev/null +++ b/src/ManifestSchema/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by ManifestSchema.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/Valijson/Valijson.vcxitems b/src/Valijson/Valijson.vcxitems new file mode 100644 index 0000000000..2da44c84c7 --- /dev/null +++ b/src/Valijson/Valijson.vcxitems @@ -0,0 +1,57 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + {358bc478-0624-4ad1-a933-0422b5292af8} + + + + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory)valijson\include + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Valijson/Valijson.vcxitems.filters b/src/Valijson/Valijson.vcxitems.filters new file mode 100644 index 0000000000..185d926459 --- /dev/null +++ b/src/Valijson/Valijson.vcxitems.filters @@ -0,0 +1,122 @@ + + + + + {7a0239af-b4b5-48ba-a16c-4759e2fe7b43} + + + {f503d3b0-5fd9-4ef4-9db9-5e48fe68db82} + + + {bce3512d-8612-4a2e-a2ad-7156b0d2f607} + + + {e6b3c438-739c-4dbc-bfd0-f0dd8ff97467} + + + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + adapters + + + constraints + + + constraints + + + constraints + + + constraints + + + internal + + + internal + + + internal + + + internal + + + internal + + + internal + + + utils + + + utils + + + utils + + + utils + + + utils + + + utils + + + utils + + + utils + + + utils + + + utils + + + + + + + + + + \ No newline at end of file diff --git a/src/Valijson/valijson/.gitignore b/src/Valijson/valijson/.gitignore new file mode 100644 index 0000000000..086e53c609 --- /dev/null +++ b/src/Valijson/valijson/.gitignore @@ -0,0 +1,6 @@ +build +bin +xcode/valijson.xcodeproj/project.xcworkspace +xcode/valijson.xcodeproj/project.xcworkspace/xcuserdata +xcode/valijson.xcodeproj/xcuserdata +doc/html diff --git a/src/Valijson/valijson/.gitmodules b/src/Valijson/valijson/.gitmodules new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Valijson/valijson/.travis.yml b/src/Valijson/valijson/.travis.yml new file mode 100644 index 0000000000..d425e247fe --- /dev/null +++ b/src/Valijson/valijson/.travis.yml @@ -0,0 +1,74 @@ +language: cpp +sudo: required +dist: trusty + +matrix: + include: + - os: linux + compiler: gcc + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-5', 'qtbase5-dev', 'libboost1.55-dev', 'libssl-dev', 'libcurl4-openssl-dev'] + env: + - CXX_COMPILER=g++-5 + - C_COMPILER=gcc-5 + + - os: linux + compiler: clang + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.6'] + packages: ['clang-3.6', 'qtbase5-dev', 'libboost1.55-dev', 'libssl-dev', 'libcurl4-openssl-dev'] + env: + - CXX_COMPILER=clang++-3.6 + - C_COMPILER=clang-3.6 + + - os: linux + compiler: clang + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.7'] + packages: ['clang-3.7', 'qtbase5-dev', 'libboost1.55-dev', 'libssl-dev', 'libcurl4-openssl-dev'] + env: + - CXX_COMPILER=clang++-3.7 + - C_COMPILER=clang-3.7 + + - os: linux + compiler: clang + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.8'] + packages: ['clang-3.8', 'libc++-dev', 'libc++abi-dev', 'qtbase5-dev', 'libboost1.55-dev', 'libssl-dev', 'libcurl4-openssl-dev'] + env: + - CXX_COMPILER=clang++-3.8 + - C_COMPILER=clang-3.8 + - CMAKE_CXX_FLAGS="-stdlib=libc++" + - CMAKE_EXE_LINKER_FLAGS="-lc++" + +before_install: + - pushd ~ + - wget https://github.com/pocoproject/poco/archive/poco-1.7.8p2-release.tar.gz + - tar -xf poco-1.7.8p2-release.tar.gz + - cd poco-poco-1.7.8p2-release + - mkdir cmake_build + - cd cmake_build + - export POCO_OPTS="-DENABLE_CRYPTO=off -DENABLE_DATA=off -DENABLE_DATA_MYSQL=off -DENABLE_DATA_ODBC=off -DENABLE_DATA_SQLITE=off" + - export POCO_OPTS="$POCO_OPTS -DENABLE_MONGODB=off -DENABLE_NET=off -DENABLE_NETSSL=off -DENABLE_PAGECOMPILER=off" + - export POCO_OPTS="$POCO_OPTS -DENABLE_PAGECOMPILER_FILE2PAGE=off -DENABLE_PDF=off -DENABLE_UTIL=off -DENABLE_XML=off -DENABLE_ZIP=off" + - cmake -D CMAKE_CXX_COMPILER=`which ${CXX_COMPILER}` -DCMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS" -DCMAKE_EXE_LINKER_FLAGS="$CMAKE_EXE_LINKER_FLAGS" $POCO_OPTS .. + - sudo make install + - wget -O curlpp-0.8.1.tar.gz https://github.com/jpbarrette/curlpp/archive/v0.8.1.tar.gz + - tar -xf curlpp-0.8.1.tar.gz + - cd curlpp-0.8.1 + - mkdir cmake_build + - cd cmake_build + - cmake -D CMAKE_CXX_COMPILER=`which ${CXX_COMPILER}` -DCMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS" -DCMAKE_EXE_LINKER_FLAGS="$CMAKE_EXE_LINKER_FLAGS" .. + - sudo make install + - popd + +script: + - mkdir build && cd build + - cmake -D CMAKE_CXX_FLAGS="$CMAKE_CXX_FLAGS" -D CMAKE_EXE_LINKER_FLAGS="$CMAKE_EXE_LINKER_FLAGS" -D CMAKE_CXX_COMPILER="$CXX_COMPILER" -D CMAKE_C_COMPILER="$C_COMPILER" .. + - VERBOSE=1 make + - ./test_suite diff --git a/src/Valijson/valijson/Authors b/src/Valijson/valijson/Authors new file mode 100644 index 0000000000..a6b025b96a --- /dev/null +++ b/src/Valijson/valijson/Authors @@ -0,0 +1,35 @@ +Tristan Penman, tristan@tristanpenman.com + Core library development + +James Jensen, jjensen@akamai.com, changes Copyright (c) 2016 Akamai Technologies + Polymorphic constraint support + +Tengiz Sharafiev, btolfa+github@gmail.com + Adapter for nlohmann/json parser library + +hotwatermorning (github username), hotwatermorning@gmail.com + Adapter for json11 parser library + +shsfre09 (github username), + Adapter for picojson parser library + +Michael Smith, michael.smith@puppetlabs.com + Memory management improvements for RapidJson parser library + +Richard Clamp, richardc@unixbeard.net + Boost-related fixes + +Lars Immisch, lars@ibp.de + noboost branch + +Nicolas Witczak, n_witczak@yahoo.com + C++11 improvements + +Martin Robinson, 0x4d52@gmail.com + Bug fixes + +Pierre Lamot, pierre.lamot@yahoo.fr + Adapter for Qt JSON parser + +drewxa (github username), bo.ro.da@mail.ru + Adapter for Poco JSON parser diff --git a/src/Valijson/valijson/CMakeLists.txt b/src/Valijson/valijson/CMakeLists.txt new file mode 100644 index 0000000000..7a0ba3645d --- /dev/null +++ b/src/Valijson/valijson/CMakeLists.txt @@ -0,0 +1,163 @@ +cmake_minimum_required(VERSION 3.1) +project(valijson) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +option(valijson_INSTALL_HEADERS "Install valijson headers." FALSE) +option(valijson_BUILD_EXAMPLES "Build valijson examples." FALSE) +option(valijson_BUILD_TESTS "Build valijson test suite." TRUE) +option(valijson_EXCLUDE_BOOST "Exclude Boost when building test suite." FALSE) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + +if(valijson_INSTALL_HEADERS) + install(DIRECTORY include/ DESTINATION include) +endif() + +if(NOT valijson_BUILD_TESTS AND NOT valijson_BUILD_EXAMPLES) + return() +endif() + +SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0") + +include(CheckCXXCompilerFlag) +CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11) +if(COMPILER_SUPPORTS_CXX11) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +else() + message(FATAL_ERROR "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.") +endif() + +find_package(curlpp) +find_package(Poco OPTIONAL_COMPONENTS JSON) +find_package(Qt5Core) + +# jsoncpp library +add_library(jsoncpp + thirdparty/jsoncpp-0.9.4/src/lib_json/json_reader.cpp + thirdparty/jsoncpp-0.9.4/src/lib_json/json_value.cpp + thirdparty/jsoncpp-0.9.4/src/lib_json/json_writer.cpp +) + +target_include_directories(jsoncpp SYSTEM PRIVATE thirdparty/jsoncpp-0.9.4/include) +set_target_properties(jsoncpp PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/thirdparty/jsoncpp-0.9.4) + +add_library(json11 + thirdparty/json11-ec4e452/json11.cpp +) + +target_include_directories(json11 SYSTEM PRIVATE thirdparty/json11-ec4e452) +set_target_properties(json11 PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/thirdparty/json11-ec4e452) + +# Not all of these are required for examples build it doesn't hurt to include them +include_directories(include SYSTEM + thirdparty/gtest-1.7.0/include + thirdparty/json11-ec4e452 + thirdparty/jsoncpp-0.9.4/include + thirdparty/rapidjson-1.1.0/include + thirdparty/picojson-1.3.0 + thirdparty/nlohmann-json-3.1.2 + ) + +if(valijson_BUILD_TESTS) + if(NOT valijson_EXCLUDE_BOOST) + find_package(Boost) + endif() + + # Build local gtest + set(gtest_force_shared_crt ON) + add_subdirectory(thirdparty/gtest-1.7.0) + + set(TEST_SOURCES + tests/test_adapter_comparison.cpp + tests/test_fetch_document_callback.cpp + tests/test_json_pointer.cpp + tests/test_json11_adapter.cpp + tests/test_jsoncpp_adapter.cpp + tests/test_nlohmann_json_adapter.cpp + tests/test_rapidjson_adapter.cpp + tests/test_picojson_adapter.cpp + tests/test_poly_constraint.cpp + tests/test_validation_errors.cpp + tests/test_validator.cpp + ) + + # Unit tests executable + add_executable(test_suite ${TEST_SOURCES}) + + # Definition for using picojson + set_target_properties(test_suite PROPERTIES COMPILE_DEFINITIONS "PICOJSON_USE_INT64") + + set(TEST_LIBS gtest gtest_main jsoncpp json11) + + if(Boost_FOUND) + include_directories(${Boost_INCLUDE_DIRS}) + list(APPEND TEST_SOURCES tests/test_property_tree_adapter.cpp) + add_definitions(-DBOOST_ALL_DYN_LINK) + set(Boost_USE_STATIC_LIBS OFF) + set(Boost_USE_MULTITHREADED ON) + set(Boost_USE_STATIC_RUNTIME OFF) + target_compile_definitions(test_suite PRIVATE "VALIJSON_BUILD_PROPERTY_TREE_ADAPTER") + endif() + + if(Poco_FOUND) + include_directories(${Poco_INCLUDE_DIRS}) + list(APPEND TEST_SOURCES tests/test_poco_json_adapter.cpp) + list(APPEND TEST_LIBS ${Poco_Foundation_LIBRARIES} ${Poco_JSON_LIBRARIES}) + target_compile_definitions(test_suite PRIVATE "VALIJSON_BUILD_POCO_ADAPTER") + endif() + + if(Qt5Core_FOUND) + include_directories(${Qt5Core_INCLUDE_DIRS}) + list(APPEND TEST_SOURCES tests/test_qtjson_adapter.cpp) + list(APPEND TEST_LIBS Qt5::Core) + target_compile_definitions(test_suite PRIVATE "VALIJSON_BUILD_QT_ADAPTER") + endif() + + target_link_libraries(test_suite ${TEST_LIBS} ${Boost_LIBRARIES}) +endif() + +if(valijson_BUILD_EXAMPLES) + include_directories(SYSTEM) + + add_executable(custom_schema + examples/custom_schema.cpp + ) + + add_executable(external_schema + examples/external_schema.cpp + ) + + add_executable(array_iteration_basics + examples/array_iteration_basics.cpp + ) + + add_executable(array_iteration_template_fn + examples/array_iteration_template_fn.cpp + ) + + add_executable(object_iteration + examples/object_iteration.cpp + ) + + add_executable(json_pointers + examples/json_pointers.cpp + ) + + if(curlpp_FOUND) + include_directories(${curlpp_INCLUDE_DIR}) + + add_executable(remote_resolution + examples/remote_resolution.cpp + ) + + target_link_libraries(remote_resolution curl ${curlpp_LIBRARIES}) + endif() + + target_link_libraries(custom_schema ${Boost_LIBRARIES}) + target_link_libraries(external_schema ${Boost_LIBRARIES}) + target_link_libraries(array_iteration_basics jsoncpp) + target_link_libraries(array_iteration_template_fn jsoncpp) + target_link_libraries(object_iteration jsoncpp) + target_link_libraries(json_pointers) +endif() diff --git a/src/Valijson/valijson/Doxyfile b/src/Valijson/valijson/Doxyfile new file mode 100644 index 0000000000..88996c5b1e --- /dev/null +++ b/src/Valijson/valijson/Doxyfile @@ -0,0 +1,2362 @@ +# Doxyfile 1.8.9.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Valijson" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = NO + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = YES + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = YES + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = YES + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = YES + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. + +INPUT = README.md include + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = README.md + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /