Skip to content

Instantly share code, notes, and snippets.

@anatawa12
Created July 10, 2024 04:45
Show Gist options
  • Save anatawa12/82e0bbeedd811941e524b741dab1552e to your computer and use it in GitHub Desktop.
Save anatawa12/82e0bbeedd811941e524b741dab1552e to your computer and use it in GitHub Desktop.
Winget-cli version comparator and MPS Versions on winget-pkgs test
#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