LeviLamina
Loading...
Searching...
No Matches
VersionRequirement.h
1#pragma once
2
3#include <algorithm>
4#include <compare>
5#include <concepts>
6#include <cstddef>
7#include <cstdint>
8#include <limits>
9#include <optional>
10#include <ranges>
11#include <string>
12#include <string_view>
13#include <type_traits>
14#include <utility>
15#include <vector>
16
17#include "ll/api/data/Version.h"
18#include "ll/api/reflection/ReflectionError.h"
19#include "ll/api/utils/HashUtils.h"
20
21namespace ll::data {
22
23struct VersionRequirement {
24 enum class Operator : std::uint8_t {
25 Equal,
26 Greater,
27 GreaterEqual,
28 Less,
29 LessEqual,
30 };
31
32 struct Comparator {
33 Operator operation;
34 Version version;
35
36 [[nodiscard]] constexpr bool operator==(Comparator const&) const noexcept = default;
37 };
38
39 using ComparatorSet = std::vector<Comparator>;
40
41private:
42 struct PartialVersion {
43 std::optional<std::uint16_t> major;
44 std::optional<std::uint16_t> minor;
45 std::optional<std::uint16_t> patch;
46 std::optional<Version> exact;
47 std::uint8_t precision{};
48 };
49
50 std::vector<ComparatorSet> alternatives{{}};
51
52 static constexpr bool isWhitespace(char c) noexcept { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
53
54 static constexpr bool isWildcard(std::string_view value) noexcept {
55 return value == "*" || value == "x" || value == "X";
56 }
57
58 static constexpr std::string_view trim(std::string_view value) noexcept {
59 while (!value.empty() && isWhitespace(value.front())) {
60 value.remove_prefix(1);
61 }
62 while (!value.empty() && isWhitespace(value.back())) {
63 value.remove_suffix(1);
64 }
65 return value;
66 }
67
68 static constexpr bool parseNumber(std::string_view value, std::uint16_t& result) noexcept {
69 if (value.empty() || (value.size() > 1 && value.front() == '0')) {
70 return false;
71 }
72 auto parsed = detail::from_chars(value.data(), value.data() + value.size(), result);
73 return parsed && parsed.ptr == value.data() + value.size();
74 }
75
76 static constexpr bool parsePartial(std::string_view value, PartialVersion& result) noexcept {
77 value = trim(value);
78 if (value.empty()) {
79 return false;
80 }
81
82 auto suffix = value.find_first_of("-+");
83 if (suffix != std::string_view::npos) {
84 if (!Version::valid(value)) {
85 return false;
86 }
87 Version version{value};
88 result.major = version.major;
89 result.minor = version.minor;
90 result.patch = version.patch;
91 result.exact = std::move(version);
92 result.precision = 3;
93 return true;
94 }
95
96 std::vector<std::string_view> parts;
97 size_t begin = 0;
98 while (begin <= value.size()) {
99 auto end = value.find('.', begin);
100 if (end == std::string_view::npos) {
101 end = value.size();
102 }
103 parts.emplace_back(value.substr(begin, end - begin));
104 if (end == value.size()) {
105 break;
106 }
107 begin = end + 1;
108 }
109 if (parts.empty() || parts.size() > 3) {
110 return false;
111 }
112
113 bool wildcardFound = false;
114 for (size_t i = 0; i < parts.size(); ++i) {
115 if (isWildcard(parts[i])) {
116 wildcardFound = true;
117 continue;
118 }
119 if (wildcardFound) {
120 return false;
121 }
122
123 std::uint16_t number{};
124 if (!parseNumber(parts[i], number)) {
125 return false;
126 }
127 if (i == 0) {
128 result.major = number;
129 } else if (i == 1) {
130 result.minor = number;
131 } else {
132 result.patch = number;
133 }
134 result.precision = static_cast<std::uint8_t>(i + 1);
135 }
136
137 if (result.precision == 3) {
138 result.exact = Version{*result.major, *result.minor, *result.patch};
139 }
140 return true;
141 }
142
143 static constexpr Version floor(PartialVersion const& partial) noexcept {
144 return Version{partial.major.value_or(0), partial.minor.value_or(0), partial.patch.value_or(0)};
145 }
146
147 static constexpr std::optional<Version> nextMajor(Version const& version) noexcept {
148 if (version.major == (std::numeric_limits<std::uint16_t>::max)()) {
149 return std::nullopt;
150 }
151 return Version{static_cast<std::uint16_t>(version.major + 1), 0, 0};
152 }
153
154 static constexpr std::optional<Version> nextMinor(Version const& version) noexcept {
155 if (version.minor != (std::numeric_limits<std::uint16_t>::max)()) {
156 return Version{version.major, static_cast<std::uint16_t>(version.minor + 1), 0};
157 }
158 return nextMajor(version);
159 }
160
161 static constexpr std::optional<Version> nextPatch(Version const& version) noexcept {
162 if (version.patch != (std::numeric_limits<std::uint16_t>::max)()) {
163 return Version{version.major, version.minor, static_cast<std::uint16_t>(version.patch + 1)};
164 }
165 return nextMinor(version);
166 }
167
168 static constexpr void addComparator(ComparatorSet& set, Operator operation, Version version) {
169 version.build.reset();
170 set.emplace_back(operation, std::move(version));
171 }
172
173 static constexpr void addUpperBound(ComparatorSet& set, std::optional<Version> upper) {
174 if (upper) {
175 addComparator(set, Operator::Less, std::move(*upper));
176 }
177 }
178
179 static constexpr bool expandPartial(ComparatorSet& set, PartialVersion const& partial) {
180 if (!partial.major) {
181 return true;
182 }
183 auto lower = floor(partial);
184 if (partial.exact) {
185 addComparator(set, Operator::Equal, *partial.exact);
186 } else {
187 addComparator(set, Operator::GreaterEqual, lower);
188 if (!partial.minor) {
189 addUpperBound(set, nextMajor(lower));
190 } else {
191 addUpperBound(set, nextMinor(lower));
192 }
193 }
194 return true;
195 }
196
197 static constexpr bool expandComparator(ComparatorSet& set, std::string_view token) noexcept try {
198 enum class Prefix {
199 Bare,
200 Equal,
201 Greater,
202 GreaterEqual,
203 Less,
204 LessEqual,
205 Caret,
206 Tilde,
207 };
208
209 Prefix prefix = Prefix::Bare;
210 if (token.starts_with(">=")) {
211 prefix = Prefix::GreaterEqual;
212 token.remove_prefix(2);
213 } else if (token.starts_with("<=")) {
214 prefix = Prefix::LessEqual;
215 token.remove_prefix(2);
216 } else if (token.starts_with('>')) {
217 prefix = Prefix::Greater;
218 token.remove_prefix(1);
219 } else if (token.starts_with('<')) {
220 prefix = Prefix::Less;
221 token.remove_prefix(1);
222 } else if (token.starts_with('=')) {
223 prefix = Prefix::Equal;
224 token.remove_prefix(1);
225 } else if (token.starts_with('^')) {
226 prefix = Prefix::Caret;
227 token.remove_prefix(1);
228 } else if (token.starts_with('~')) {
229 prefix = Prefix::Tilde;
230 token.remove_prefix(1);
231 }
232 if (token.empty()) {
233 return false;
234 }
235
236 PartialVersion partial;
237 if (!parsePartial(token, partial)) {
238 return false;
239 }
240 if (!partial.major) {
241 return prefix == Prefix::Bare || prefix == Prefix::Equal;
242 }
243
244 auto lower = floor(partial);
245 switch (prefix) {
246 case Prefix::Bare:
247 if (partial.exact) {
248 // TODO: Treat bare full versions as exact matches in the next breaking release.
249 addComparator(set, Operator::GreaterEqual, *partial.exact);
250 addUpperBound(set, nextMajor(*partial.exact));
251 return true;
252 }
253 return expandPartial(set, partial);
254 case Prefix::Equal:
255 return expandPartial(set, partial);
256 case Prefix::GreaterEqual:
257 addComparator(set, Operator::GreaterEqual, partial.exact.value_or(lower));
258 return true;
259 case Prefix::Greater:
260 if (partial.exact) {
261 addComparator(set, Operator::Greater, *partial.exact);
262 } else {
263 auto upper = !partial.minor ? nextMajor(lower) : nextMinor(lower);
264 if (!upper) {
265 return false;
266 }
267 addComparator(set, Operator::GreaterEqual, *upper);
268 }
269 return true;
270 case Prefix::Less:
271 addComparator(set, Operator::Less, partial.exact.value_or(lower));
272 return true;
273 case Prefix::LessEqual:
274 if (partial.exact) {
275 addComparator(set, Operator::LessEqual, *partial.exact);
276 } else {
277 auto upper = !partial.minor ? nextMajor(lower) : nextMinor(lower);
278 if (!upper) {
279 return false;
280 }
281 addComparator(set, Operator::Less, *upper);
282 }
283 return true;
284 case Prefix::Caret: {
285 addComparator(set, Operator::GreaterEqual, partial.exact.value_or(lower));
286 if (!partial.minor || lower.major != 0) {
287 addUpperBound(set, nextMajor(lower));
288 } else if (!partial.patch || lower.minor != 0) {
289 addUpperBound(set, nextMinor(lower));
290 } else {
291 addUpperBound(set, nextPatch(lower));
292 }
293 return true;
294 }
295 case Prefix::Tilde:
296 addComparator(set, Operator::GreaterEqual, partial.exact.value_or(lower));
297 if (!partial.minor) {
298 addUpperBound(set, nextMajor(lower));
299 } else {
300 addUpperBound(set, nextMinor(lower));
301 }
302 return true;
303 }
304 return false;
305 } catch (...) {
306 return false;
307 }
308
309 static constexpr bool
310 expandHyphen(ComparatorSet& set, std::string_view lowerText, std::string_view upperText) noexcept try {
311 PartialVersion lowerPartial;
312 PartialVersion upperPartial;
313 if (!parsePartial(lowerText, lowerPartial) || !parsePartial(upperText, upperPartial) || !lowerPartial.major
314 || !upperPartial.major) {
315 return false;
316 }
317
318 addComparator(set, Operator::GreaterEqual, lowerPartial.exact.value_or(floor(lowerPartial)));
319 auto upper = floor(upperPartial);
320 if (upperPartial.exact) {
321 addComparator(set, Operator::LessEqual, *upperPartial.exact);
322 } else if (!upperPartial.minor) {
323 addUpperBound(set, nextMajor(upper));
324 } else {
325 addUpperBound(set, nextMinor(upper));
326 }
327 return true;
328 } catch (...) {
329 return false;
330 }
331
332 static constexpr std::vector<std::string_view> tokenize(std::string_view value) {
333 std::vector<std::string_view> result;
334 size_t current = 0;
335 while (current < value.size()) {
336 while (current < value.size() && isWhitespace(value[current])) {
337 ++current;
338 }
339 if (current == value.size()) {
340 break;
341 }
342 auto begin = current;
343 while (current < value.size() && !isWhitespace(value[current])) {
344 ++current;
345 }
346 result.emplace_back(value.substr(begin, current - begin));
347 }
348 return result;
349 }
350
351 static constexpr bool comparatorLess(Comparator const& lhs, Comparator const& rhs) noexcept {
352 if (lhs.operation != rhs.operation) {
353 return lhs.operation < rhs.operation;
354 }
355 return lhs.version < rhs.version;
356 }
357
358 constexpr void normalize() {
359 for (auto& set : alternatives) {
360 std::ranges::sort(set, comparatorLess);
361 set.erase(std::unique(set.begin(), set.end()), set.end());
362 }
363 std::ranges::sort(alternatives, [](ComparatorSet const& lhs, ComparatorSet const& rhs) {
364 return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), comparatorLess);
365 });
366 alternatives.erase(std::unique(alternatives.begin(), alternatives.end()), alternatives.end());
367 }
368
369 [[nodiscard]] static constexpr bool compare(Version const& candidate, Comparator const& comparator) noexcept {
370 switch (comparator.operation) {
371 case Operator::Equal:
372 return candidate == comparator.version;
373 case Operator::Greater:
374 return candidate > comparator.version;
375 case Operator::GreaterEqual:
376 return candidate >= comparator.version;
377 case Operator::Less:
378 return candidate < comparator.version;
379 case Operator::LessEqual:
380 return candidate <= comparator.version;
381 }
382 return false;
383 }
384
385 [[nodiscard]] static constexpr bool allowsPreRelease(ComparatorSet const& set, Version const& candidate) noexcept {
386 if (!candidate.preRelease) {
387 return true;
388 }
389 return std::ranges::any_of(set, [&](Comparator const& comparator) {
390 auto const& version = comparator.version;
391 return version.preRelease && version.major == candidate.major && version.minor == candidate.minor
392 && version.patch == candidate.patch;
393 });
394 }
395
396public:
397 constexpr VersionRequirement() = default;
398 constexpr ~VersionRequirement() = default;
399 explicit constexpr VersionRequirement(std::string_view str) { from_string(str); }
400
401 [[nodiscard]] constexpr detail::from_chars_result from_chars(char const* first, char const* last) noexcept {
402 if (first == nullptr || last == nullptr || first > last) {
403 return {first, std::errc::invalid_argument};
404 }
405 try {
406 std::string_view input{first, static_cast<size_t>(last - first)};
407 std::vector<ComparatorSet> parsedAlternatives;
408
409 if (trim(input).empty()) {
410 parsedAlternatives.emplace_back();
411 } else {
412 size_t begin = 0;
413 while (begin <= input.size()) {
414 auto end = input.find("||", begin);
415 if (end == std::string_view::npos) {
416 end = input.size();
417 }
418 auto group = trim(input.substr(begin, end - begin));
419 if (group.empty()) {
420 return {first + begin, std::errc::invalid_argument};
421 }
422
423 auto tokens = tokenize(group);
424 ComparatorSet comparators;
425 for (size_t i = 0; i < tokens.size();) {
426 if (i + 2 < tokens.size() && tokens[i + 1] == "-") {
427 if (!expandHyphen(comparators, tokens[i], tokens[i + 2])) {
428 return {first + begin, std::errc::invalid_argument};
429 }
430 i += 3;
431 continue;
432 }
433
434 std::string combined;
435 auto token = tokens[i];
436 if ((token == ">" || token == ">=" || token == "<" || token == "<=" || token == "="
437 || token == "^" || token == "~")
438 && i + 1 < tokens.size()) {
439 combined = std::string{token} + std::string{tokens[++i]};
440 token = combined;
441 }
442 if (!expandComparator(comparators, token)) {
443 return {first + begin, std::errc::invalid_argument};
444 }
445 ++i;
446 }
447 parsedAlternatives.emplace_back(std::move(comparators));
448 if (end == input.size()) {
449 break;
450 }
451 begin = end + 2;
452 }
453 }
454
455 alternatives = std::move(parsedAlternatives);
456 normalize();
457 return {last, std::errc{}};
458 } catch (...) {
459 return {first, std::errc::not_enough_memory};
460 }
461 }
462
463 [[nodiscard]] constexpr detail::from_chars_result from_string_noexcept(std::string_view str) noexcept {
464 return from_chars(str.data(), str.data() + str.size());
465 }
466
467 constexpr VersionRequirement& from_string(std::string_view str) {
468 from_string_noexcept(str).value();
469 return *this;
470 }
471
472 [[nodiscard]] constexpr bool matches(Version const& version) const noexcept {
473 return std::ranges::any_of(alternatives, [&](ComparatorSet const& set) {
474 return allowsPreRelease(set, version)
475 && std::ranges::all_of(set, [&](Comparator const& comparator) { return compare(version, comparator); });
476 });
477 }
478
479 [[nodiscard]] constexpr std::vector<ComparatorSet> const& comparatorSets() const noexcept { return alternatives; }
480
481 [[nodiscard]] constexpr std::string to_string() const {
482 auto operatorString = [](Operator operation) -> std::string_view {
483 switch (operation) {
484 case Operator::Equal:
485 return "=";
486 case Operator::Greater:
487 return ">";
488 case Operator::GreaterEqual:
489 return ">=";
490 case Operator::Less:
491 return "<";
492 case Operator::LessEqual:
493 return "<=";
494 }
495 return {};
496 };
497
498 std::string result;
499 for (auto const& set : alternatives) {
500 if (!result.empty()) {
501 result += " || ";
502 }
503 if (set.empty()) {
504 result += '*';
505 continue;
506 }
507 for (auto const& comparator : set) {
508 if (!result.empty() && !result.ends_with(" || ")) {
509 result += ' ';
510 }
511 result += operatorString(comparator.operation);
512 result += comparator.version.to_string();
513 }
514 }
515 return result;
516 }
517
518 [[nodiscard]] constexpr bool operator==(VersionRequirement const& other) const noexcept {
519 return alternatives == other.alternatives;
520 }
521
522 [[nodiscard]] static constexpr bool valid(std::string_view str) noexcept {
523 return VersionRequirement{}.from_string_noexcept(str);
524 }
525};
526
527template <class J, class T>
528[[nodiscard]] inline Expected<J> serialize(T&& requirement) noexcept
529 requires(std::same_as<std::remove_cvref_t<T>, VersionRequirement>)
530try {
531 return requirement.to_string();
532} catch (...) {
533 return makeExceptionError();
534}
535
536template <class T, class J>
537[[nodiscard]] inline Expected<> deserialize(T& requirement, J const& j) noexcept
538 requires(std::same_as<T, VersionRequirement>)
539{
540 if (!j.is_string()) {
541 return reflection::makeDeserStringTypeError();
542 }
543 if (auto result = requirement.from_string_noexcept((std::string const&)j); result) {
544 return {};
545 } else {
546 return makeErrorCodeError(result.ec);
547 }
548}
549
550} // namespace ll::data
551
552namespace std {
553template <>
554struct hash<ll::data::VersionRequirement::Comparator> {
555 size_t operator()(ll::data::VersionRequirement::Comparator const& comparator) const noexcept {
556 return ll::hash_utils::HashCombiner{}
557 .add(static_cast<std::uint8_t>(comparator.operation))
558 .add(comparator.version);
559 }
560};
561
562template <>
563struct hash<ll::data::VersionRequirement> {
564 size_t operator()(ll::data::VersionRequirement const& requirement) const noexcept {
565 ll::hash_utils::HashCombiner result;
566 for (auto const& set : requirement.comparatorSets()) {
567 result.add(set.size()).addRange(set);
568 }
569 return result;
570 }
571};
572} // namespace std
STL namespace.
Definition VersionRequirement.h:32
Definition VersionRequirement.h:23
Definition Version.h:214