Created
July 10, 2024 04:45
-
-
Save anatawa12/82e0bbeedd811941e524b741dab1552e to your computer and use it in GitHub Desktop.
Winget-cli version comparator and MPS Versions on winget-pkgs test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <iostream> | |
using namespace std::string_view_literals; | |
#define THROW_HR_IF(error, condition) if (condition) throw std::runtime_error(#error); | |
// region Utility shim | |
namespace Utility { | |
constexpr std::string_view s_SpaceChars = " \f\n\r\t\v"sv; | |
// original foldCase will make lowercase chars | |
std::string FoldCase(std::string_view input) | |
{ | |
std::string result(input); | |
std::transform(result.begin(), result.end(), result.begin(), | |
[](unsigned char c) { return static_cast<char>(std::tolower(c)); }); | |
return result; | |
} | |
std::string& Trim(std::string& str) | |
{ | |
if (!str.empty()) | |
{ | |
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); | |
} | |
} | |
return str; | |
} | |
std::string Trim(std::string&& str) | |
{ | |
std::string result = std::move(str); | |
Utility::Trim(result); | |
return result; | |
} | |
} | |
// endregion | |
namespace Utility { | |
// region https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerSharedLib/AppInstallerStrings.cpp#L393-L399 | |
std::string ToLower(std::string_view in) { | |
std::string result(in); | |
std::transform(result.begin(), result.end(), result.begin(), | |
[](unsigned char c) { return static_cast<char>(std::tolower(c)); }); | |
return result; | |
} | |
// endregion | |
// region https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerSharedLib/AppInstallerStrings.cpp#L118-L121 | |
bool CaseInsensitiveEquals(std::string_view a, std::string_view b) { | |
return ToLower(a) == ToLower(b); | |
} | |
bool CaseInsensitiveStartsWith(std::string_view a, std::string_view b) { | |
return a.length() >= b.length() && CaseInsensitiveEquals(a.substr(0, b.length()), b); | |
} | |
// endregion | |
} | |
using namespace Utility; | |
// region https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Public/AppInstallerVersions.h#L12-L127 | |
// Creates a comparable version object from a string. | |
// Versions are parsed by: | |
// 1. Parse approximate comparator sign if applicable | |
// 2. Splitting the string based on the given splitChars (or DefaultSplitChars) | |
// 3. Parsing a leading, positive integer from each split part | |
// 4. Saving any remaining, non-digits as a supplemental value | |
// | |
// Versions are compared by: | |
// for each part in each version | |
// if both sides have no more parts, return equal | |
// else if one side has no more parts, it is less | |
// else if integers not equal, return comparison of integers | |
// else if only one side has a non-empty string part, it is less | |
// else if string parts not equal, return comparison of strings | |
// if all parts are same, use approximate comparator if applicable | |
// | |
// Note: approximate to another approximate version is invalid. | |
// approximate to Unknown is invalid. | |
struct Version | |
{ | |
// Used in approximate version to indicate the relation to the base version. | |
enum class ApproximateComparator | |
{ | |
None, | |
LessThan, | |
GreaterThan, | |
}; | |
// The default characters to split a version string on. | |
constexpr static std::string_view DefaultSplitChars = "."sv; | |
Version() = default; | |
Version(const std::string& version, std::string_view splitChars = DefaultSplitChars) : | |
Version(std::string(version), splitChars) {} | |
Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); | |
// Constructing an approximate version from a base version. | |
Version(Version baseVersion, ApproximateComparator approximateComparator); | |
// Resets the version's value to the input. | |
virtual void Assign(std::string version, std::string_view splitChars = DefaultSplitChars); | |
// Gets the full version string used to construct the Version. | |
const std::string& ToString() const { return m_version; } | |
bool operator<(const Version& other) const; | |
bool operator>(const Version& other) const; | |
bool operator<=(const Version& other) const; | |
bool operator>=(const Version& other) const; | |
bool operator==(const Version& other) const; | |
bool operator!=(const Version& other) const; | |
// Determines if this version is the sentinel value defining the 'Latest' version | |
bool IsLatest() const; | |
// Returns a Version that will return true for IsLatest | |
static Version CreateLatest(); | |
// Determines if this version is the sentinel value defining an 'Unknown' version | |
bool IsUnknown() const; | |
// Returns a Version that will return true for IsUnknown | |
static Version CreateUnknown(); | |
// Gets a bool indicating whether the full version string is empty. | |
// Does not indicate that Parts is empty; for instance when "0.0" is given, | |
// this will be false while GetParts().empty() would be true. | |
bool IsEmpty() const { return m_version.empty(); } | |
// An individual version part in between split characters. | |
struct Part | |
{ | |
Part() = default; | |
Part(uint64_t integer) : Integer(integer) {} | |
Part(const std::string& part); | |
Part(uint64_t integer, std::string other); | |
bool operator<(const Part& other) const; | |
bool operator==(const Part& other) const; | |
bool operator!=(const Part& other) const; | |
uint64_t Integer = 0; | |
std::string Other; | |
private: | |
std::string m_foldedOther; | |
}; | |
// Gets the part breakdown for a given version. | |
const std::vector<Part>& GetParts() const { return m_parts; } | |
// Gets the part at the given index; or the implied zero part if past the end. | |
const Part& PartAt(size_t index) const; | |
// Returns if the version is an approximate version. | |
bool IsApproximate() const { return m_approximateComparator != ApproximateComparator::None; } | |
// Get the base version from approximate version, or return a copy if the version is not approximate. | |
Version GetBaseVersion() const; | |
protected: | |
bool IsBaseVersionLatest() const; | |
bool IsBaseVersionUnknown() const; | |
// Called by overloaded less than operator implementation when base version already compared and equal, less than determined by approximate comparator. | |
bool ApproximateCompareLessThan(const Version& other) const; | |
std::string m_version; | |
std::vector<Part> m_parts; | |
bool m_trimPrefix = true; | |
ApproximateComparator m_approximateComparator = ApproximateComparator::None; | |
// Remove trailing empty parts (0 or empty) | |
void Trim(); | |
}; | |
// endregion | |
// region https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L9-L362 | |
using namespace std::string_view_literals; | |
static constexpr std::string_view s_Digit_Characters = "0123456789"sv; | |
static constexpr std::string_view s_Version_Part_Latest = "Latest"sv; | |
static constexpr std::string_view s_Version_Part_Unknown = "Unknown"sv; | |
static constexpr std::string_view s_Approximate_Less_Than = "< "sv; | |
static constexpr std::string_view s_Approximate_Greater_Than = "> "sv; | |
Version::Version(std::string&& version, std::string_view splitChars) | |
{ | |
Assign(std::move(version), splitChars); | |
} | |
/* | |
RawVersion::RawVersion(std::string version, std::string_view splitChars) | |
{ | |
m_trimPrefix = false; | |
Assign(std::move(version), splitChars); | |
} | |
Version::Version(Version baseVersion, ApproximateComparator approximateComparator) : Version(std::move(baseVersion)) | |
{ | |
if (approximateComparator == ApproximateComparator::None) | |
{ | |
return; | |
} | |
THROW_HR_IF(E_INVALIDARG, this->IsApproximate() || this->IsUnknown()); | |
m_approximateComparator = approximateComparator; | |
if (approximateComparator == ApproximateComparator::LessThan) | |
{ | |
m_version = std::string{ s_Approximate_Less_Than } + m_version; | |
} | |
else if (approximateComparator == ApproximateComparator::GreaterThan) | |
{ | |
m_version = std::string{ s_Approximate_Greater_Than } + m_version; | |
} | |
} | |
*/ | |
void Version::Assign(std::string version, std::string_view splitChars) | |
{ | |
m_version = std::move(Utility::Trim(version)); | |
// Process approximate comparator if applicable | |
std::string baseVersion = m_version; | |
if (CaseInsensitiveStartsWith(m_version, s_Approximate_Less_Than)) | |
{ | |
m_approximateComparator = ApproximateComparator::LessThan; | |
baseVersion = m_version.substr(s_Approximate_Less_Than.length(), m_version.length() - s_Approximate_Less_Than.length()); | |
} | |
else if (CaseInsensitiveStartsWith(m_version, s_Approximate_Greater_Than)) | |
{ | |
m_approximateComparator = ApproximateComparator::GreaterThan; | |
baseVersion = m_version.substr(s_Approximate_Greater_Than.length(), m_version.length() - s_Approximate_Greater_Than.length()); | |
} | |
// If there is a digit before the split character, or no split characters exist, trim off all leading non-digit characters | |
size_t digitPos = baseVersion.find_first_of(s_Digit_Characters); | |
size_t splitPos = baseVersion.find_first_of(splitChars); | |
if (m_trimPrefix && digitPos != std::string::npos && (splitPos == std::string::npos || digitPos < splitPos)) | |
{ | |
baseVersion.erase(0, digitPos); | |
} | |
// Then parse the base version | |
size_t pos = 0; | |
while (pos < baseVersion.length()) | |
{ | |
size_t newPos = baseVersion.find_first_of(splitChars, pos); | |
size_t length = (newPos == std::string::npos ? baseVersion.length() : newPos) - pos; | |
m_parts.emplace_back(baseVersion.substr(pos, length)); | |
pos += length + 1; | |
} | |
// Trim version parts | |
Trim(); | |
THROW_HR_IF(E_INVALIDARG, m_approximateComparator != ApproximateComparator::None && IsBaseVersionUnknown()); | |
} | |
void Version::Trim() | |
{ | |
while (!m_parts.empty()) | |
{ | |
const Part& part = m_parts.back(); | |
if (part.Integer == 0 && part.Other.empty()) | |
{ | |
m_parts.pop_back(); | |
} | |
else | |
{ | |
return; | |
} | |
} | |
} | |
bool Version::operator<(const Version& other) const | |
{ | |
// Sort Latest higher than any other values | |
bool thisIsLatest = IsBaseVersionLatest(); | |
bool otherIsLatest = other.IsBaseVersionLatest(); | |
if (thisIsLatest && otherIsLatest) | |
{ | |
return ApproximateCompareLessThan(other); | |
} | |
else if (thisIsLatest || otherIsLatest) | |
{ | |
// If only one is latest, this can only be less than if the other is and this is not. | |
return (otherIsLatest && !thisIsLatest); | |
} | |
// Sort Unknown lower than any known values | |
bool thisIsUnknown = IsBaseVersionUnknown(); | |
bool otherIsUnknown = other.IsBaseVersionUnknown(); | |
if (thisIsUnknown && otherIsUnknown) | |
{ | |
// This code path should always return false as we disable approximate version for Unknown for now | |
return ApproximateCompareLessThan(other); | |
} | |
else if (thisIsUnknown || otherIsUnknown) | |
{ | |
// If at least one is unknown, this can only be less than if it is and the other is not. | |
return (thisIsUnknown && !otherIsUnknown); | |
} | |
for (size_t i = 0; i < m_parts.size(); ++i) | |
{ | |
if (i >= other.m_parts.size()) | |
{ | |
// All parts equal to this point | |
break; | |
} | |
const Part& partA = m_parts[i]; | |
const Part& partB = other.m_parts[i]; | |
if (partA < partB) | |
{ | |
return true; | |
} | |
else if (partB < partA) | |
{ | |
return false; | |
} | |
// else parts are equal, so continue to next part | |
} | |
// All parts tested were equal | |
if (m_parts.size() == other.m_parts.size()) | |
{ | |
return ApproximateCompareLessThan(other); | |
} | |
else | |
{ | |
// Else this is only less if there are more parts in other. | |
return m_parts.size() < other.m_parts.size(); | |
} | |
} | |
bool Version::operator>(const Version& other) const | |
{ | |
return other < *this; | |
} | |
bool Version::operator<=(const Version& other) const | |
{ | |
return !(*this > other); | |
} | |
bool Version::operator>=(const Version& other) const | |
{ | |
return !(*this < other); | |
} | |
bool Version::operator==(const Version& other) const | |
{ | |
if (m_approximateComparator != other.m_approximateComparator) | |
{ | |
return false; | |
} | |
if ((IsBaseVersionLatest() && other.IsBaseVersionLatest()) || | |
(IsBaseVersionUnknown() && other.IsBaseVersionUnknown())) | |
{ | |
return true; | |
} | |
if (m_parts.size() != other.m_parts.size()) | |
{ | |
return false; | |
} | |
for (size_t i = 0; i < m_parts.size(); ++i) | |
{ | |
if (m_parts[i] != other.m_parts[i]) | |
{ | |
return false; | |
} | |
} | |
return true; | |
} | |
bool Version::operator!=(const Version& other) const | |
{ | |
return !(*this == other); | |
} | |
bool Version::IsLatest() const | |
{ | |
return (m_approximateComparator != ApproximateComparator::LessThan && IsBaseVersionLatest()); | |
} | |
Version Version::CreateLatest() | |
{ | |
Version result; | |
result.m_version = s_Version_Part_Latest; | |
result.m_parts.emplace_back(0, std::string{ s_Version_Part_Latest }); | |
return result; | |
} | |
bool Version::IsUnknown() const | |
{ | |
return IsBaseVersionUnknown(); | |
} | |
Version Version::CreateUnknown() | |
{ | |
Version result; | |
result.m_version = s_Version_Part_Unknown; | |
result.m_parts.emplace_back(0, std::string{ s_Version_Part_Unknown }); | |
return result; | |
} | |
const Version::Part& Version::PartAt(size_t index) const | |
{ | |
static Part s_zero{}; | |
if (index < m_parts.size()) | |
{ | |
return m_parts[index]; | |
} | |
else | |
{ | |
return s_zero; | |
} | |
} | |
Version Version::GetBaseVersion() const | |
{ | |
Version baseVersion = *this; | |
baseVersion.m_approximateComparator = ApproximateComparator::None; | |
if (m_approximateComparator == ApproximateComparator::LessThan) | |
{ | |
baseVersion.m_version = m_version.substr(s_Approximate_Less_Than.size()); | |
} | |
else if (m_approximateComparator == ApproximateComparator::GreaterThan) | |
{ | |
baseVersion.m_version = m_version.substr(s_Approximate_Greater_Than.size()); | |
} | |
return baseVersion; | |
} | |
bool Version::IsBaseVersionLatest() const | |
{ | |
return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Latest)); | |
} | |
bool Version::IsBaseVersionUnknown() const | |
{ | |
return (m_parts.size() == 1 && m_parts[0].Integer == 0 && Utility::CaseInsensitiveEquals(m_parts[0].Other, s_Version_Part_Unknown)); | |
} | |
bool Version::ApproximateCompareLessThan(const Version& other) const | |
{ | |
// Only true if this is less than, other is not, OR this is none, other is greater than | |
return (m_approximateComparator == ApproximateComparator::LessThan && other.m_approximateComparator != ApproximateComparator::LessThan) || | |
(m_approximateComparator == ApproximateComparator::None && other.m_approximateComparator == ApproximateComparator::GreaterThan); | |
} | |
Version::Part::Part(const std::string& part) | |
{ | |
std::string interimPart = Utility::Trim(part.c_str()); | |
const char* begin = interimPart.c_str(); | |
char* end = nullptr; | |
errno = 0; | |
Integer = strtoull(begin, &end, 10); | |
if (errno == ERANGE) | |
{ | |
Integer = 0; | |
Other = interimPart; | |
} | |
else if (static_cast<size_t>(end - begin) != interimPart.length()) | |
{ | |
Other = end; | |
} | |
m_foldedOther = Utility::FoldCase(static_cast<std::string_view>(Other)); | |
} | |
Version::Part::Part(uint64_t integer, std::string other) : | |
Integer(integer), Other(std::move(Utility::Trim(other))) | |
{ | |
m_foldedOther = Utility::FoldCase(static_cast<std::string_view>(Other)); | |
} | |
bool Version::Part::operator<(const Part& other) const | |
{ | |
if (Integer < other.Integer) | |
{ | |
return true; | |
} | |
else if (Integer > other.Integer) | |
{ | |
return false; | |
} | |
else if (Other.empty()) | |
{ | |
// If this Other is empty, it is at least >= | |
return false; | |
} | |
else if (!Other.empty() && other.Other.empty()) | |
{ | |
// If the other Other is empty and this is not, this is less. | |
return true; | |
} | |
else if (m_foldedOther < other.m_foldedOther) | |
{ | |
// Compare the folded versions | |
return true; | |
} | |
// else Other >= other.Other | |
return false; | |
} | |
bool Version::Part::operator==(const Part& other) const | |
{ | |
return Integer == other.Integer && m_foldedOther == other.m_foldedOther; | |
} | |
bool Version::Part::operator!=(const Part& other) const | |
{ | |
return !(*this == other); | |
} | |
// endregion | |
// https://github.com/badges/shields/pull/10245#discussion_r1670870831 | |
// https://github.com/microsoft/winget-pkgs/tree/c939ca136ff33f217db1de99f1a1839790287a16/manifests/j/JetBrains/MPS/EAP | |
int main() { | |
Version v_213_6461_785 {"213.6461.785"}; | |
Version v_mps_241_18034_990 {"MPS-241.18034.990"}; | |
std::cout << "213.6461.785 < MPS-241.18034.990: " << (v_213_6461_785 < v_mps_241_18034_990) << std::endl; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment