From e7ac2887aba47561ffb8e54e8c62df016b29df79 Mon Sep 17 00:00:00 2001 From: dishahpatel <9454898+dishahpatel@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:10:54 -0700 Subject: [PATCH 1/3] Project installer ReleaseDate in COM PackageInstallerInfo --- .../Interop/CheckInstalledStatusInterop.cs | 3 + .../Interop/InstallInterop.cs | 1 + ...TestZipInstaller_PackageInstallerInfo.yaml | 1 + src/AppInstallerCLITests/DateTime.cpp | 57 +++++++++++++++++++ src/AppInstallerSharedLib/DateTime.cpp | 57 +++++++++++++++++++ .../Public/AppInstallerDateTime.h | 7 +++ .../PackageInstallerInfo.cpp | 11 ++++ .../PackageInstallerInfo.h | 2 + .../PackageManager.idl | 9 ++- 9 files changed, 147 insertions(+), 1 deletion(-) diff --git a/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs b/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs index d1aa5379e0..8e82233f9f 100644 --- a/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/CheckInstalledStatusInterop.cs @@ -110,6 +110,9 @@ public async Task CheckInstalledStatusArpVersionMatched() Assert.That(installerInstalledStatus.InstallerInfo.NestedInstallerType, Is.EqualTo(PackageInstallerType.Unknown)); Assert.That(installerInstalledStatus.InstallerInfo.Scope, Is.EqualTo(PackageInstallerScope.Unknown)); + // The test manifest does not declare the optional ReleaseDate. + Assert.That(installerInstalledStatus.InstallerInfo.ReleaseDate, Is.Null); + // Installer status Assert.That(installerInstalledStatus.InstallerInstalledStatus[0].Type, Is.EqualTo(InstalledStatusType.AppsAndFeaturesEntry)); Assert.That(GetHResultFromInstalledStatus(installerInstalledStatus.InstallerInstalledStatus[0]), Is.EqualTo(Constants.ErrorCode.S_OK)); diff --git a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs index 37c30418b1..bd49dc7891 100644 --- a/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs +++ b/src/AppInstallerCLIE2ETests/Interop/InstallInterop.cs @@ -710,6 +710,7 @@ public void GetApplicableInstaller() Assert.That(packageInstallerInfo.NestedInstallerType, Is.EqualTo(PackageInstallerType.Exe)); Assert.That(packageInstallerInfo.Scope, Is.EqualTo(PackageInstallerScope.User)); Assert.That(packageInstallerInfo.Locale, Is.EqualTo("en-US")); + Assert.That(packageInstallerInfo.ReleaseDate, Is.EqualTo(new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero))); } /// diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_PackageInstallerInfo.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_PackageInstallerInfo.yaml index da7ddc96eb..1fc33a8045 100644 --- a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_PackageInstallerInfo.yaml +++ b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestZipInstaller_PackageInstallerInfo.yaml @@ -19,5 +19,6 @@ Installers: - RelativeFilePath: AppInstallerTestExeInstaller.exe ElevationRequirement: elevationRequired InstallerLocale: en-US + ReleaseDate: 2024-06-15 ManifestType: singleton ManifestVersion: 1.4.0 diff --git a/src/AppInstallerCLITests/DateTime.cpp b/src/AppInstallerCLITests/DateTime.cpp index 258836f95b..89ec533317 100644 --- a/src/AppInstallerCLITests/DateTime.cpp +++ b/src/AppInstallerCLITests/DateTime.cpp @@ -91,3 +91,60 @@ TEST_CASE("ShortFileTime", "[datetime]") INFO(longTime); REQUIRE(shortTime.length() < longTime.length()); } + +void VerifyTryGetTimePointFromDate(std::string_view date, int year, int month, int day) +{ + INFO(date); + auto result = TryGetTimePointFromDate(date); + REQUIRE(result.has_value()); + + tm time{}; + auto tt = system_clock::to_time_t(result.value()); + _gmtime64_s(&time, &tt); + + REQUIRE(year == time.tm_year + 1900); + REQUIRE(month == time.tm_mon + 1); + REQUIRE(day == time.tm_mday); + REQUIRE(0 == time.tm_hour); + REQUIRE(0 == time.tm_min); + REQUIRE(0 == time.tm_sec); +} + +TEST_CASE("TryGetTimePointFromDate", "[datetime]") +{ + // Not a YYYY-MM-DD date + REQUIRE_FALSE(TryGetTimePointFromDate("").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("06/15/2024").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2024-6-15").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-1a").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-150").has_value()); + + // Years out of range + REQUIRE_FALSE(TryGetTimePointFromDate("1969-12-31").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("3001-01-01").has_value()); + + // Months out of range + REQUIRE_FALSE(TryGetTimePointFromDate("2024-00-15").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2024-13-15").has_value()); + + // Days out of range + REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-00").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-31").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2023-02-29").has_value()); + REQUIRE_FALSE(TryGetTimePointFromDate("2100-02-29").has_value()); + + // In range baseline + VerifyTryGetTimePointFromDate("2024-06-15", 2024, 6, 15); + + // Leap days + VerifyTryGetTimePointFromDate("2024-02-29", 2024, 2, 29); + VerifyTryGetTimePointFromDate("2000-02-29", 2000, 2, 29); + + // Time portion is ignored + VerifyTryGetTimePointFromDate("2024-06-15T00:00:00", 2024, 6, 15); + VerifyTryGetTimePointFromDate("2024-06-15t23:59:59-08:00", 2024, 6, 15); + VerifyTryGetTimePointFromDate("2024-06-15 12:00:00Z", 2024, 6, 15); + + // Epoch time + REQUIRE(TryGetTimePointFromDate("1970-01-01").value() == system_clock::time_point{}); +} diff --git a/src/AppInstallerSharedLib/DateTime.cpp b/src/AppInstallerSharedLib/DateTime.cpp index 40fecd73b2..4c6ab43bdd 100644 --- a/src/AppInstallerSharedLib/DateTime.cpp +++ b/src/AppInstallerSharedLib/DateTime.cpp @@ -30,6 +30,31 @@ namespace AppInstaller::Utility char FollowingSeparator; void (*Action)(const OutputTimePointContext&); }; + + // Returns -1 if any character is not an ASCII digit. + int ParseDigits(std::string_view value) + { + int result = 0; + + for (char c : value) + { + if (c < '0' || c > '9') + { + return -1; + } + + result = result * 10 + (c - '0'); + } + + return result; + } + + int DaysInMonth(int year, int month) + { + constexpr int s_daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + return (month == 2 && isLeapYear) ? 29 : s_daysInMonth[month - 1]; + } } void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339) @@ -194,4 +219,36 @@ namespace AppInstaller::Utility return std::chrono::system_clock::from_time_t(_mkgmtime64(&versionTime)); } + + std::optional TryGetTimePointFromDate(std::string_view date) + { + constexpr size_t c_dateLength = 10; + if (date.length() < c_dateLength || date[4] != '-' || date[7] != '-') + { + return std::nullopt; + } + + // Some sources provide a full timestamp; only the calendar date is used. + if (date.length() > c_dateLength && date[c_dateLength] != 'T' && date[c_dateLength] != 't' && date[c_dateLength] != ' ') + { + return std::nullopt; + } + + int year = ParseDigits(date.substr(0, 4)); + int month = ParseDigits(date.substr(5, 2)); + int day = ParseDigits(date.substr(8, 2)); + + // Limit to the range supported by _mkgmtime64, which is 1970 to 3000 + if (year < 1970 || year > 3000 || month < 1 || month > 12 || day < 1 || day > DaysInMonth(year, month)) + { + return std::nullopt; + } + + tm dateTime{}; + dateTime.tm_year = year - 1900; + dateTime.tm_mon = month - 1; + dateTime.tm_mday = day; + + return std::chrono::system_clock::from_time_t(_mkgmtime64(&dateTime)); + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h index 089a7bca24..5df212cc60 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h @@ -3,7 +3,9 @@ #pragma once #include #include +#include #include +#include namespace AppInstaller::Utility { @@ -67,4 +69,9 @@ namespace AppInstaller::Utility // Ensure that the package is expected to use this format, or you may get strange times. // If the version is not convertable, the minimum time is returned. std::chrono::system_clock::time_point GetTimePointFromVersion(const UInt64Version& version); + + // Converts the given date (YYYY-MM-DD) into a time_point at midnight UTC of that date. + // Any time portion following the date (separated by 'T', 't' or a space) is ignored. + // If the value is not a valid date between 1970 and 3000, std::nullopt is returned. + std::optional TryGetTimePointFromDate(std::string_view date); } diff --git a/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp b/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp index ad13947b4b..0ad3e4e5be 100644 --- a/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp +++ b/src/Microsoft.Management.Deployment/PackageInstallerInfo.cpp @@ -5,6 +5,7 @@ #include "PackageInstallerInfo.g.cpp" #include "AuthenticationInfo.h" #include "Converters.h" +#include #include namespace winrt::Microsoft::Management::Deployment::implementation @@ -49,4 +50,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation }); return m_authenticationInfo; } + winrt::Windows::Foundation::IReference PackageInstallerInfo::ReleaseDate() + { + const auto releaseDate = ::AppInstaller::Utility::TryGetTimePointFromDate(m_manifestInstaller.ReleaseDate); + if (!releaseDate) + { + return nullptr; + } + + return winrt::clock::from_sys(releaseDate.value()); + } } diff --git a/src/Microsoft.Management.Deployment/PackageInstallerInfo.h b/src/Microsoft.Management.Deployment/PackageInstallerInfo.h index 3240753855..713e945aa7 100644 --- a/src/Microsoft.Management.Deployment/PackageInstallerInfo.h +++ b/src/Microsoft.Management.Deployment/PackageInstallerInfo.h @@ -23,6 +23,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::ElevationRequirement ElevationRequirement(); // Contract 12.0 winrt::Microsoft::Management::Deployment::AuthenticationInfo AuthenticationInfo(); + // Contract 31.0 + winrt::Windows::Foundation::IReference ReleaseDate(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index f5b207741a..c4f1fcb64c 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(30)] // For version 1.30 + [contractversion(31)] // For version 1.31 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -532,6 +532,13 @@ namespace Microsoft.Management.Deployment /// Authentication info from the package installer. AuthenticationInfo AuthenticationInfo { get; }; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 31)] + { + /// The package installer release date declared in the manifest, as midnight UTC of that date. + /// Null if the manifest does not declare a release date or the value is not a valid date. + Windows.Foundation.IReference ReleaseDate { get; }; + } }; /// The installed status type. The values need to match InstalledStatusType from winget/RepositorySearch.h. From 09c97614a8029f496fc5bd689d0d2d5379233a52 Mon Sep 17 00:00:00 2001 From: dishahpatel <9454898+dishahpatel@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:43:51 -0700 Subject: [PATCH 2/3] Parse ReleaseDate as an ISO 8601 date with std::get_time --- src/AppInstallerCLITests/DateTime.cpp | 3 - src/AppInstallerSharedLib/DateTime.cpp | 56 ++++--------------- .../Public/AppInstallerDateTime.h | 4 +- 3 files changed, 14 insertions(+), 49 deletions(-) diff --git a/src/AppInstallerCLITests/DateTime.cpp b/src/AppInstallerCLITests/DateTime.cpp index 89ec533317..e21bee3ec6 100644 --- a/src/AppInstallerCLITests/DateTime.cpp +++ b/src/AppInstallerCLITests/DateTime.cpp @@ -115,7 +115,6 @@ TEST_CASE("TryGetTimePointFromDate", "[datetime]") // Not a YYYY-MM-DD date REQUIRE_FALSE(TryGetTimePointFromDate("").has_value()); REQUIRE_FALSE(TryGetTimePointFromDate("06/15/2024").has_value()); - REQUIRE_FALSE(TryGetTimePointFromDate("2024-6-15").has_value()); REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-1a").has_value()); REQUIRE_FALSE(TryGetTimePointFromDate("2024-06-150").has_value()); @@ -142,8 +141,6 @@ TEST_CASE("TryGetTimePointFromDate", "[datetime]") // Time portion is ignored VerifyTryGetTimePointFromDate("2024-06-15T00:00:00", 2024, 6, 15); - VerifyTryGetTimePointFromDate("2024-06-15t23:59:59-08:00", 2024, 6, 15); - VerifyTryGetTimePointFromDate("2024-06-15 12:00:00Z", 2024, 6, 15); // Epoch time REQUIRE(TryGetTimePointFromDate("1970-01-01").value() == system_clock::time_point{}); diff --git a/src/AppInstallerSharedLib/DateTime.cpp b/src/AppInstallerSharedLib/DateTime.cpp index 4c6ab43bdd..a96550bc24 100644 --- a/src/AppInstallerSharedLib/DateTime.cpp +++ b/src/AppInstallerSharedLib/DateTime.cpp @@ -30,31 +30,6 @@ namespace AppInstaller::Utility char FollowingSeparator; void (*Action)(const OutputTimePointContext&); }; - - // Returns -1 if any character is not an ASCII digit. - int ParseDigits(std::string_view value) - { - int result = 0; - - for (char c : value) - { - if (c < '0' || c > '9') - { - return -1; - } - - result = result * 10 + (c - '0'); - } - - return result; - } - - int DaysInMonth(int year, int month) - { - constexpr int s_daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; - bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; - return (month == 2 && isLeapYear) ? 29 : s_daysInMonth[month - 1]; - } } void OutputTimePoint(std::ostream& stream, const std::chrono::system_clock::time_point& time, bool useRFC3339) @@ -222,33 +197,26 @@ namespace AppInstaller::Utility std::optional TryGetTimePointFromDate(std::string_view date) { - constexpr size_t c_dateLength = 10; - if (date.length() < c_dateLength || date[4] != '-' || date[7] != '-') - { - return std::nullopt; - } + tm parsed{}; + std::istringstream stream{ std::string{ date } }; + stream >> std::get_time(&parsed, "%Y-%m-%d"); - // Some sources provide a full timestamp; only the calendar date is used. - if (date.length() > c_dateLength && date[c_dateLength] != 'T' && date[c_dateLength] != 't' && date[c_dateLength] != ' ') + // An ISO 8601 date and time is also accepted, but only the date is used. + if (stream.fail() || (stream.peek() != EOF && stream.peek() != 'T')) { return std::nullopt; } - int year = ParseDigits(date.substr(0, 4)); - int month = ParseDigits(date.substr(5, 2)); - int day = ParseDigits(date.substr(8, 2)); - - // Limit to the range supported by _mkgmtime64, which is 1970 to 3000 - if (year < 1970 || year > 3000 || month < 1 || month > 12 || day < 1 || day > DaysInMonth(year, month)) + // Limit to the range supported by _mkgmtime64, which is 1970 to 3000. + // _mkgmtime64 rolls days past the end of the month into the next month (2023-02-29 becomes 2023-03-01), so reject those. + int year = parsed.tm_year + 1900; + tm normalized = parsed; + __time64_t epochTime = _mkgmtime64(&normalized); + if (year < 1970 || year > 3000 || normalized.tm_mday != parsed.tm_mday) { return std::nullopt; } - tm dateTime{}; - dateTime.tm_year = year - 1900; - dateTime.tm_mon = month - 1; - dateTime.tm_mday = day; - - return std::chrono::system_clock::from_time_t(_mkgmtime64(&dateTime)); + return std::chrono::system_clock::from_time_t(epochTime); } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h index 5df212cc60..b7c44d88c3 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h @@ -70,8 +70,8 @@ namespace AppInstaller::Utility // If the version is not convertable, the minimum time is returned. std::chrono::system_clock::time_point GetTimePointFromVersion(const UInt64Version& version); - // Converts the given date (YYYY-MM-DD) into a time_point at midnight UTC of that date. - // Any time portion following the date (separated by 'T', 't' or a space) is ignored. + // Converts the given ISO 8601 date (YYYY-MM-DD) into a time_point at midnight UTC of that date. + // An ISO 8601 date and time (YYYY-MM-DDThh:mm:ss) is also accepted, but the time is ignored. // If the value is not a valid date between 1970 and 3000, std::nullopt is returned. std::optional TryGetTimePointFromDate(std::string_view date); } From 1dea48949c6c1426b2400621827f684c584cd3d2 Mon Sep 17 00:00:00 2001 From: dishahpatel <9454898+dishahpatel@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:08:34 -0700 Subject: [PATCH 3/3] Fix spelling check in ReleaseDate comment --- src/AppInstallerSharedLib/Public/AppInstallerDateTime.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h index b7c44d88c3..671df68cad 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h @@ -71,7 +71,7 @@ namespace AppInstaller::Utility std::chrono::system_clock::time_point GetTimePointFromVersion(const UInt64Version& version); // Converts the given ISO 8601 date (YYYY-MM-DD) into a time_point at midnight UTC of that date. - // An ISO 8601 date and time (YYYY-MM-DDThh:mm:ss) is also accepted, but the time is ignored. + // An ISO 8601 date and time (such as 2024-06-15T12:00:00Z) is also accepted, but the time is ignored. // If the value is not a valid date between 1970 and 3000, std::nullopt is returned. std::optional TryGetTimePointFromDate(std::string_view date); }