|
| 1 | +#include "extensions/versionconstraints.h" |
| 2 | + |
| 3 | +#include "formatters.h" |
| 4 | + |
| 5 | +using VersionCompareFunction = bool (*)(MOBase::Version const& lhs, |
| 6 | + MOBase::Version const& rhs); |
| 7 | + |
| 8 | +// official semver regex |
| 9 | +static const QRegularExpression s_ConstraintStrictRegEx{ |
| 10 | + R"(^(?P<constraint>>=|<=|<|>|!=|==|\^|~)?\s*(?P<major>0|[1-9*]\d*)(?:\.(?P<minor>0|[1-9*]\d*)(?:\.(?P<patch>0|[1-9*]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?)?)?$)"}; |
| 11 | + |
| 12 | +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX |
| 13 | +static const QRegularExpression s_ConstraintMO2RegEx{ |
| 14 | + R"(^(?P<constraint>>=|<=|<|>|!=|\^|~)?\s*(?P<major>0|[1-9*]\d*)(?:\.(?P<minor>0|[1-9*]\d*)(?:\.(?P<patch>0|[1-9*]\d*)(?:\.(?P<subpatch>0|[1-9*]\d*))?(?:(?P<type>dev|a|alpha|b|beta|rc)(?P<prerelease>0|[1-9](?:[.0-9])*))?)?)?$)"}; |
| 15 | + |
| 16 | +// match from value to release type |
| 17 | +static const std::unordered_map<QString, MOBase::Version::ReleaseType> |
| 18 | + s_StringToRelease{{"dev", MOBase::Version::Development}, |
| 19 | + {"alpha", MOBase::Version::Alpha}, |
| 20 | + {"a", MOBase::Version::Alpha}, |
| 21 | + {"beta", MOBase::Version::Beta}, |
| 22 | + {"b", MOBase::Version::Beta}, |
| 23 | + {"rc", MOBase::Version::ReleaseCandidate}}; |
| 24 | + |
| 25 | +#define _COMPARE_PAIR(OP) \ |
| 26 | + { \ |
| 27 | + #OP, +[](MOBase::Version const& lhs, MOBase::Version const& rhs) { \ |
| 28 | + return lhs OP rhs; \ |
| 29 | + } \ |
| 30 | + } |
| 31 | + |
| 32 | +static const std::unordered_map<QString, VersionCompareFunction> s_CompareToFunction{ |
| 33 | + _COMPARE_PAIR(>), _COMPARE_PAIR(>=), _COMPARE_PAIR(<), |
| 34 | + _COMPARE_PAIR(<=), _COMPARE_PAIR(!=), _COMPARE_PAIR(==)}; |
| 35 | + |
| 36 | +#undef _COMPARE_PAIR |
| 37 | + |
| 38 | +namespace MOBase |
| 39 | +{ |
| 40 | + |
| 41 | +class VersionConstraintImpl |
| 42 | +{ |
| 43 | +public: |
| 44 | + virtual bool matches(Version const& version) const = 0; |
| 45 | + virtual ~VersionConstraintImpl() = default; |
| 46 | +}; |
| 47 | + |
| 48 | +// version constraint for a range with lower bound included and upper bound excluded, |
| 49 | +// typically used for tilde, caret and wilcard constraints |
| 50 | +// |
| 51 | +class RangeVersionConstraint : public VersionConstraintImpl |
| 52 | +{ |
| 53 | +public: |
| 54 | + RangeVersionConstraint(Version const& min, Version const& max) |
| 55 | + : m_Min{min}, m_Max{max} |
| 56 | + {} |
| 57 | + |
| 58 | + bool matches(Version const& version) const override |
| 59 | + { |
| 60 | + return m_Min <= version && version < m_Max; |
| 61 | + } |
| 62 | + |
| 63 | +private: |
| 64 | + Version m_Min, m_Max; |
| 65 | +}; |
| 66 | + |
| 67 | +// version constraint for inequality and equality constraint |
| 68 | +// |
| 69 | +class InequalityVersionConstraint : public VersionConstraintImpl |
| 70 | +{ |
| 71 | + |
| 72 | +public: |
| 73 | + InequalityVersionConstraint(Version const& target, VersionCompareFunction compare) |
| 74 | + : m_Target{target}, m_Compare{compare} |
| 75 | + {} |
| 76 | + |
| 77 | + bool matches(Version const& version) const override |
| 78 | + { |
| 79 | + return m_Compare(version, m_Target); |
| 80 | + } |
| 81 | + |
| 82 | +private: |
| 83 | + Version m_Target; |
| 84 | + VersionCompareFunction m_Compare; |
| 85 | +}; |
| 86 | + |
| 87 | +VersionConstraint VersionConstraint::parse(QString const& value, |
| 88 | + Version::ParseMode mode) |
| 89 | +{ |
| 90 | + const auto& regex = mode == Version::ParseMode::SemVer ? s_ConstraintStrictRegEx |
| 91 | + : s_ConstraintMO2RegEx; |
| 92 | + |
| 93 | + const auto match = regex.match(value); |
| 94 | + if (!match.hasMatch()) { |
| 95 | + throw InvalidConstraintException( |
| 96 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 97 | + } |
| 98 | + |
| 99 | + const auto constraint = match.captured("constraint"); |
| 100 | + |
| 101 | + const auto major_s = match.captured("major"); |
| 102 | + const auto minor_s = match.captured("minor"); |
| 103 | + const auto patch_s = match.captured("patch"); |
| 104 | + const auto subpatch_s = match.captured("subpatch"); |
| 105 | + |
| 106 | + const auto wildcard = |
| 107 | + major_s == "*" || minor_s == "*" || patch_s == "*" || subpatch_s == "*"; |
| 108 | + const auto tilde = match.captured("constraint") == "~"; |
| 109 | + const auto caret = match.captured("constraint") == "^"; |
| 110 | + |
| 111 | + // cannot use wildcard with a constraint |
| 112 | + if (wildcard && !constraint.isEmpty()) { |
| 113 | + throw InvalidConstraintException( |
| 114 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 115 | + } |
| 116 | + |
| 117 | + // cannot use pre-release with wilcard, tilde or caret constraint |
| 118 | + if ((wildcard || tilde || caret) && match.hasCaptured("prerelease")) { |
| 119 | + throw InvalidConstraintException( |
| 120 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 121 | + } |
| 122 | + |
| 123 | + // if a part has a wildcard, lower part should be missing or wildcard (e.g., 2.*.3 |
| 124 | + // is invalid) |
| 125 | + if (major_s == "*" && !minor_s.isEmpty() && minor_s != "*") { |
| 126 | + throw InvalidConstraintException( |
| 127 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 128 | + } |
| 129 | + if (minor_s == "*" && !patch_s.isEmpty() && patch_s != "*") { |
| 130 | + throw InvalidConstraintException( |
| 131 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 132 | + } |
| 133 | + if (patch_s == "*" && !subpatch_s.isEmpty() && subpatch_s != "*") { |
| 134 | + throw InvalidConstraintException( |
| 135 | + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); |
| 136 | + } |
| 137 | + |
| 138 | + std::vector<std::variant<int, Version::ReleaseType>> prereleases; |
| 139 | + if (mode == Version::ParseMode::SemVer) { |
| 140 | + for (auto& part : match.captured("prerelease") |
| 141 | + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { |
| 142 | + // try to extract an int |
| 143 | + bool ok = true; |
| 144 | + const auto intValue = part.toInt(&ok); |
| 145 | + if (ok) { |
| 146 | + prereleases.push_back(intValue); |
| 147 | + continue; |
| 148 | + } |
| 149 | + |
| 150 | + // check if we have a valid prerelease type |
| 151 | + const auto it = s_StringToRelease.find(part.toLower()); |
| 152 | + if (it == s_StringToRelease.end()) { |
| 153 | + throw InvalidVersionException( |
| 154 | + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); |
| 155 | + } |
| 156 | + |
| 157 | + prereleases.push_back(it->second); |
| 158 | + } |
| 159 | + } else { |
| 160 | + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); |
| 161 | + |
| 162 | + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into |
| 163 | + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, |
| 164 | + // 1, 1} |
| 165 | + // |
| 166 | + for (const auto& preVersion : |
| 167 | + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { |
| 168 | + prereleases.push_back(preVersion.toInt()); |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + constexpr auto min_int = std::numeric_limits<int>::min(); |
| 173 | + constexpr auto max_int = std::numeric_limits<int>::max(); |
| 174 | + |
| 175 | + std::shared_ptr<VersionConstraintImpl> impl; |
| 176 | + |
| 177 | + if (wildcard || caret || tilde) { |
| 178 | + |
| 179 | + // you can get more information at |
| 180 | + // https://python-poetry.org/docs/dependency-specification/ |
| 181 | + |
| 182 | + // note that the only case where all 4 xxxOk is false is for '*' |
| 183 | + // |
| 184 | + bool majorOk, minorOk, patchOk, subpatchOk; |
| 185 | + auto major = major_s.toInt(&majorOk), minor = minor_s.toInt(&minorOk), |
| 186 | + patch = patch_s.toInt(&patchOk), subpatch = subpatch_s.toInt(&subpatchOk); |
| 187 | + |
| 188 | + // the lower bound is always the actual version with missing or wildcard components |
| 189 | + // set to 0, e.g. |
| 190 | + // - 2.3.* -> >= 2.3.0 |
| 191 | + // - ^1 -> >= 1.0.0 |
| 192 | + // - ^0.3 -> >= 0.3.0 |
| 193 | + // - ~1.2 -> >= 1.2.0 |
| 194 | + const Version min = Version(major, minor, patch, subpatch); |
| 195 | + |
| 196 | + // the upper bound is a bit more complicated to compute |
| 197 | + Version max = Version(max_int, max_int, max_int, max_int); |
| 198 | + |
| 199 | + if (wildcard) { |
| 200 | + // for wildcard, we increment the last non-wildcard character by one |
| 201 | + // |
| 202 | + if (minorOk && patchOk) { |
| 203 | + max = Version(major, minor, patch + 1); |
| 204 | + } else if (minorOk) { |
| 205 | + max = Version(major, minor + 1, 0); |
| 206 | + } else { |
| 207 | + max = Version(major + 1, 0, 0); |
| 208 | + } |
| 209 | + } else if (caret) { |
| 210 | + // TODO: clean this... |
| 211 | + |
| 212 | + if (!minorOk && !patchOk && !subpatchOk) { |
| 213 | + max = Version(major + 1, 0, 0); |
| 214 | + } else if (!patchOk && !subpatchOk) { |
| 215 | + if (major == 0) { |
| 216 | + max = Version(major, minor + 1, 0); |
| 217 | + } else { |
| 218 | + max = Version(major + 1, 0, 0); |
| 219 | + } |
| 220 | + } else if (!subpatchOk) { |
| 221 | + if (major == 0 && minor == 0) { |
| 222 | + max = Version(major, minor, patch + 1); |
| 223 | + } else if (major == 0) { |
| 224 | + max = Version(major, minor + 1, 0); |
| 225 | + } else { |
| 226 | + max = Version(major + 1, 0, 0); |
| 227 | + } |
| 228 | + } else { |
| 229 | + if (major == 0 && minor == 0 && patch == 0 && subpatch == 0) { |
| 230 | + max = min; // this creates an impossible range (>= 0, < 0), but is expected |
| 231 | + } else if (major == 0 && minor == 0 && patch == 0) { |
| 232 | + max = Version(major, minor, patch, subpatch + 1); |
| 233 | + } else if (major == 0 && minor == 0) { |
| 234 | + max = Version(major, minor, patch + 1, 0); |
| 235 | + } else if (major == 0) { |
| 236 | + max = Version(major, minor + 1, 0); |
| 237 | + } else { |
| 238 | + max = Version(major + 1, 0, 0); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + } else if (tilde) { |
| 243 | + if (minorOk && patchOk && subpatchOk) { |
| 244 | + max = Version(major, minor, patch, subpatch + 1); |
| 245 | + } else if (minorOk && patchOk) { |
| 246 | + max = Version(major, minor, patch + 1); |
| 247 | + } else if (minorOk) { |
| 248 | + max = Version(major, minor + 1, 0); |
| 249 | + } else { |
| 250 | + max = Version(major + 1, 0, 0); |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + impl = std::make_shared<RangeVersionConstraint>(min, max); |
| 255 | + |
| 256 | + } else { |
| 257 | + auto op = match.captured("constraint"); |
| 258 | + if (op.isEmpty()) { |
| 259 | + op = "=="; |
| 260 | + } |
| 261 | + impl = std::make_shared<InequalityVersionConstraint>( |
| 262 | + Version(major_s.toInt(), minor_s.toInt(), patch_s.toInt(), subpatch_s.toInt(), |
| 263 | + std::move(prereleases)), |
| 264 | + s_CompareToFunction.at(op)); |
| 265 | + } |
| 266 | + |
| 267 | + return VersionConstraint(std::move(impl)); |
| 268 | +} |
| 269 | + |
| 270 | +VersionConstraint::VersionConstraint(std::shared_ptr<VersionConstraintImpl> impl) |
| 271 | + : m_Impl{std::move(impl)} |
| 272 | +{} |
| 273 | + |
| 274 | +VersionConstraint::~VersionConstraint() = default; |
| 275 | + |
| 276 | +bool VersionConstraint::matches(Version const& version) const |
| 277 | +{ |
| 278 | + return m_Impl->matches(version); |
| 279 | +} |
| 280 | + |
| 281 | +VersionConstraints VersionConstraints::parse(QString const& value, |
| 282 | + Version::ParseMode mode) |
| 283 | +{ |
| 284 | + std::vector<VersionConstraint> constraints; |
| 285 | + for (const auto& part : value.split(",")) { |
| 286 | + constraints.push_back(VersionConstraint::parse(part.trimmed(), mode)); |
| 287 | + } |
| 288 | + return VersionConstraints(std::move(constraints)); |
| 289 | +} |
| 290 | + |
| 291 | +bool VersionConstraints::matches(Version const& version) const |
| 292 | +{ |
| 293 | + return std::all_of(m_Constraints.begin(), m_Constraints.end(), |
| 294 | + [version](const auto& constraint) { |
| 295 | + return constraint.matches(version); |
| 296 | + }); |
| 297 | +} |
| 298 | + |
| 299 | +VersionConstraints::VersionConstraints(std::vector<VersionConstraint> checkers) |
| 300 | + : m_Constraints{std::move(checkers)} |
| 301 | +{} |
| 302 | + |
| 303 | +} // namespace MOBase |
0 commit comments