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..e21bee3ec6 100644 --- a/src/AppInstallerCLITests/DateTime.cpp +++ b/src/AppInstallerCLITests/DateTime.cpp @@ -91,3 +91,57 @@ 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-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); + + // 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..a96550bc24 100644 --- a/src/AppInstallerSharedLib/DateTime.cpp +++ b/src/AppInstallerSharedLib/DateTime.cpp @@ -194,4 +194,29 @@ namespace AppInstaller::Utility return std::chrono::system_clock::from_time_t(_mkgmtime64(&versionTime)); } + + std::optional TryGetTimePointFromDate(std::string_view date) + { + tm parsed{}; + std::istringstream stream{ std::string{ date } }; + stream >> std::get_time(&parsed, "%Y-%m-%d"); + + // 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; + } + + // 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; + } + + return std::chrono::system_clock::from_time_t(epochTime); + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h index 089a7bca24..671df68cad 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 ISO 8601 date (YYYY-MM-DD) into a time_point at midnight UTC of that date. + // 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); } 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.