NAH 2.0.16
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_semver.h
Go to the documentation of this file.
1
22#ifndef NAH_SEMVER_H
23#define NAH_SEMVER_H
24
25#ifdef __cplusplus
26
27#include <cstdint>
28#include <optional>
29#include <string>
30#include <vector>
31#include <cctype>
32#include <sstream>
33#include <algorithm>
34#include <tuple>
35
36namespace nah {
37namespace semver {
38
39// ============================================================================
40// Version Class
41// ============================================================================
42
53class Version {
54public:
55 Version() : major_(0), minor_(0), patch_(0) {}
56 Version(uint64_t major, uint64_t minor, uint64_t patch)
57 : major_(major), minor_(minor), patch_(patch) {}
58 Version(uint64_t major, uint64_t minor, uint64_t patch,
59 const std::string& prerelease, const std::string& build = "")
60 : major_(major), minor_(minor), patch_(patch),
61 prerelease_(prerelease), build_(build) {}
62
63 uint64_t major() const { return major_; }
64 uint64_t minor() const { return minor_; }
65 uint64_t patch() const { return patch_; }
66 const std::string& prerelease() const { return prerelease_; }
67 const std::string& build() const { return build_; }
68
69 std::string str() const {
70 std::string s = std::to_string(major_) + "." +
71 std::to_string(minor_) + "." +
72 std::to_string(patch_);
73 if (!prerelease_.empty()) s += "-" + prerelease_;
74 if (!build_.empty()) s += "+" + build_;
75 return s;
76 }
77
78 // Comparison operators (SemVer 2.0.0 precedence rules)
79 bool operator==(const Version& o) const {
80 return major_ == o.major_ && minor_ == o.minor_ &&
81 patch_ == o.patch_ && prerelease_ == o.prerelease_;
82 }
83 bool operator!=(const Version& o) const { return !(*this == o); }
84 bool operator<(const Version& o) const { return compare(o) < 0; }
85 bool operator<=(const Version& o) const { return compare(o) <= 0; }
86 bool operator>(const Version& o) const { return compare(o) > 0; }
87 bool operator>=(const Version& o) const { return compare(o) >= 0; }
88
89private:
90 uint64_t major_, minor_, patch_;
91 std::string prerelease_;
92 std::string build_; // Ignored in comparisons per SemVer spec
93
94 int compare(const Version& o) const {
95 // Compare major.minor.patch
96 if (major_ != o.major_) return major_ < o.major_ ? -1 : 1;
97 if (minor_ != o.minor_) return minor_ < o.minor_ ? -1 : 1;
98 if (patch_ != o.patch_) return patch_ < o.patch_ ? -1 : 1;
99
100 // Prerelease comparison
101 // A version without prerelease has higher precedence
102 if (prerelease_.empty() && !o.prerelease_.empty()) return 1;
103 if (!prerelease_.empty() && o.prerelease_.empty()) return -1;
104 if (prerelease_.empty() && o.prerelease_.empty()) return 0;
105
106 // Compare prerelease identifiers
107 return compare_prerelease(prerelease_, o.prerelease_);
108 }
109
110 static std::vector<std::string> split_prerelease(const std::string& s) {
111 std::vector<std::string> parts;
112 std::string::size_type start = 0, pos;
113 while ((pos = s.find('.', start)) != std::string::npos) {
114 parts.push_back(s.substr(start, pos - start));
115 start = pos + 1;
116 }
117 parts.push_back(s.substr(start));
118 return parts;
119 }
120
121 static bool is_numeric(const std::string& s) {
122 if (s.empty()) return false;
123 for (char c : s) if (!std::isdigit(static_cast<unsigned char>(c))) return false;
124 return true;
125 }
126
127 static int compare_prerelease(const std::string& a, const std::string& b) {
128 auto pa = split_prerelease(a);
129 auto pb = split_prerelease(b);
130
131 for (size_t i = 0; i < pa.size() && i < pb.size(); ++i) {
132 bool a_num = is_numeric(pa[i]);
133 bool b_num = is_numeric(pb[i]);
134
135 if (a_num && b_num) {
136 // Both numeric: compare as integers
137 uint64_t na = std::stoull(pa[i]);
138 uint64_t nb = std::stoull(pb[i]);
139 if (na != nb) return na < nb ? -1 : 1;
140 } else if (a_num != b_num) {
141 // Numeric has lower precedence than alphanumeric
142 return a_num ? -1 : 1;
143 } else {
144 // Both alphanumeric: compare as strings
145 int cmp = pa[i].compare(pb[i]);
146 if (cmp != 0) return cmp < 0 ? -1 : 1;
147 }
148 }
149
150 // Shorter prerelease has lower precedence
151 if (pa.size() != pb.size()) return pa.size() < pb.size() ? -1 : 1;
152 return 0;
153 }
154};
155
156// ============================================================================
157// Range Types
158// ============================================================================
159
161enum class Comparator {
162 Eq,
163 Lt,
164 Le,
165 Gt,
166 Ge
167};
168
170struct Constraint {
171 Comparator op;
172 Version version;
173};
174
177using ComparatorSet = std::vector<Constraint>;
178
183struct VersionRange {
184 std::vector<ComparatorSet> sets;
185
187 std::optional<Version> min_version() const;
188
190 std::string selection_key() const;
191};
192
193// ============================================================================
194// API Functions
195// ============================================================================
196
202inline std::optional<Version> parse_version(const std::string& str);
203
217inline std::optional<VersionRange> parse_range(const std::string& str);
218
220inline bool satisfies(const Version& version, const Constraint& constraint);
221
223inline bool satisfies(const Version& version, const ComparatorSet& set);
224
226inline bool satisfies(const Version& version, const VersionRange& range);
227
234inline std::optional<Version> select_best(
235 const std::vector<Version>& versions,
236 const VersionRange& range);
237
238// ============================================================================
239// Implementation - Parsing Helpers
240// ============================================================================
241
242namespace detail {
243
244inline std::string trim(const std::string& in) {
245 size_t start = 0;
246 while (start < in.size() && std::isspace(static_cast<unsigned char>(in[start]))) ++start;
247 size_t end = in.size();
248 while (end > start && std::isspace(static_cast<unsigned char>(in[end - 1]))) --end;
249 return in.substr(start, end - start);
250}
251
252inline std::vector<std::string> split(const std::string& s, const std::string& delim) {
253 std::vector<std::string> parts;
254 size_t start = 0;
255 size_t pos;
256 while ((pos = s.find(delim, start)) != std::string::npos) {
257 parts.push_back(s.substr(start, pos - start));
258 start = pos + delim.length();
259 }
260 parts.push_back(s.substr(start));
261 return parts;
262}
263
264inline std::vector<std::string> tokenize(const std::string& s) {
265 std::vector<std::string> tokens;
266 std::istringstream iss(s);
267 std::string token;
268 while (iss >> token) {
269 tokens.push_back(token);
270 }
271 return tokens;
272}
273
274// Parse version core: MAJOR.MINOR.PATCH
275inline std::optional<Version> parse_version_impl(const std::string& str) {
276 std::string s = trim(str);
277 if (s.empty()) return std::nullopt;
278
279 // Split off build metadata (+...)
280 std::string build;
281 auto plus_pos = s.find('+');
282 if (plus_pos != std::string::npos) {
283 build = s.substr(plus_pos + 1);
284 s = s.substr(0, plus_pos);
285 }
286
287 // Split off prerelease (-...)
288 std::string prerelease;
289 auto dash_pos = s.find('-');
290 if (dash_pos != std::string::npos) {
291 prerelease = s.substr(dash_pos + 1);
292 s = s.substr(0, dash_pos);
293 }
294
295 // Parse MAJOR.MINOR.PATCH
296 auto parts = split(s, ".");
297 if (parts.size() != 3) return std::nullopt;
298
299 try {
300 for (const auto& p : parts) {
301 if (p.empty()) return std::nullopt;
302 for (char c : p) {
303 if (!std::isdigit(static_cast<unsigned char>(c))) return std::nullopt;
304 }
305 }
306 uint64_t major = std::stoull(parts[0]);
307 uint64_t minor = std::stoull(parts[1]);
308 uint64_t patch = std::stoull(parts[2]);
309 return Version(major, minor, patch, prerelease, build);
310 } catch (...) {
311 return std::nullopt;
312 }
313}
314
315// Expand caret range: ^1.2.3 -> >=1.2.3 <2.0.0
316inline std::optional<ComparatorSet> expand_caret(const std::string& version_str) {
317 auto v = parse_version_impl(version_str);
318 if (!v) return std::nullopt;
319
320 ComparatorSet set;
321 set.push_back({Comparator::Ge, *v});
322
323 // ^0.0.x -> =0.0.x (exact)
324 // ^0.x -> >=0.x.0 <0.(x+1).0
325 // ^x.y.z -> >=x.y.z <(x+1).0.0
326 Version upper;
327 if (v->major() == 0) {
328 if (v->minor() == 0) {
329 // ^0.0.x means exactly 0.0.x
330 set.clear();
331 set.push_back({Comparator::Eq, *v});
332 return set;
333 }
334 // ^0.x means <0.(x+1).0
335 upper = Version(0, v->minor() + 1, 0);
336 } else {
337 upper = Version(v->major() + 1, 0, 0);
338 }
339 set.push_back({Comparator::Lt, upper});
340 return set;
341}
342
343// Expand tilde range: ~1.2.3 -> >=1.2.3 <1.3.0
344inline std::optional<ComparatorSet> expand_tilde(const std::string& version_str) {
345 auto v = parse_version_impl(version_str);
346 if (!v) return std::nullopt;
347
348 ComparatorSet set;
349 set.push_back({Comparator::Ge, *v});
350 set.push_back({Comparator::Lt, Version(v->major(), v->minor() + 1, 0)});
351 return set;
352}
353
354// Parse X-range: 1.x, 1.2.x, *
355inline std::optional<ComparatorSet> expand_x_range(const std::string& str) {
356 std::string s = trim(str);
357
358 // * or x means any version
359 if (s == "*" || s == "x" || s == "X") {
360 return ComparatorSet{}; // Empty set = match anything
361 }
362
363 // Check for x/X in version
364 auto parts = split(s, ".");
365 if (parts.empty()) return std::nullopt;
366
367 try {
368 if (parts.size() == 1 || (parts.size() >= 2 && (parts[1] == "x" || parts[1] == "X" || parts[1] == "*"))) {
369 // 1.x or 1.* -> >=1.0.0 <2.0.0
370 uint64_t major = std::stoull(parts[0]);
371 ComparatorSet set;
372 set.push_back({Comparator::Ge, Version(major, 0, 0)});
373 set.push_back({Comparator::Lt, Version(major + 1, 0, 0)});
374 return set;
375 }
376
377 if (parts.size() >= 3 && (parts[2] == "x" || parts[2] == "X" || parts[2] == "*")) {
378 // 1.2.x or 1.2.* -> >=1.2.0 <1.3.0
379 uint64_t major = std::stoull(parts[0]);
380 uint64_t minor = std::stoull(parts[1]);
381 ComparatorSet set;
382 set.push_back({Comparator::Ge, Version(major, minor, 0)});
383 set.push_back({Comparator::Lt, Version(major, minor + 1, 0)});
384 return set;
385 }
386 } catch (...) {
387 return std::nullopt;
388 }
389
390 return std::nullopt;
391}
392
393inline std::optional<Constraint> parse_constraint(const std::string& str) {
394 std::string s = trim(str);
395 if (s.empty()) return std::nullopt;
396
397 Comparator op = Comparator::Eq;
398 std::string version_str;
399
400 if (s.rfind(">=", 0) == 0) {
401 op = Comparator::Ge;
402 version_str = s.substr(2);
403 } else if (s.rfind("<=", 0) == 0) {
404 op = Comparator::Le;
405 version_str = s.substr(2);
406 } else if (s.rfind(">", 0) == 0) {
407 op = Comparator::Gt;
408 version_str = s.substr(1);
409 } else if (s.rfind("<", 0) == 0) {
410 op = Comparator::Lt;
411 version_str = s.substr(1);
412 } else if (s.rfind("=", 0) == 0) {
413 op = Comparator::Eq;
414 version_str = s.substr(1);
415 } else {
416 op = Comparator::Eq;
417 version_str = s;
418 }
419
420 version_str = trim(version_str);
421 if (version_str.empty()) return std::nullopt;
422
423 auto version = parse_version_impl(version_str);
424 if (!version) return std::nullopt;
425 return Constraint{op, *version};
426}
427
428inline std::optional<ComparatorSet> parse_comparator_set(const std::string& str) {
429 std::string s = trim(str);
430 if (s.empty()) return ComparatorSet{}; // Empty = match all
431
432 // Check for caret range
433 if (s[0] == '^') {
434 return expand_caret(s.substr(1));
435 }
436
437 // Check for tilde range
438 if (s[0] == '~') {
439 return expand_tilde(s.substr(1));
440 }
441
442 // Check for X-range
443 if (s.find('x') != std::string::npos || s.find('X') != std::string::npos ||
444 s.find('*') != std::string::npos || s == "*") {
445 auto expanded = expand_x_range(s);
446 if (expanded) return expanded;
447 }
448
449 // Parse as space-separated constraints
450 auto tokens = tokenize(s);
451 if (tokens.empty()) return ComparatorSet{};
452
453 ComparatorSet set;
454 for (const auto& token : tokens) {
455 // Handle caret/tilde within compound ranges
456 if (!token.empty() && token[0] == '^') {
457 auto expanded = expand_caret(token.substr(1));
458 if (!expanded) return std::nullopt;
459 for (const auto& c : *expanded) set.push_back(c);
460 } else if (!token.empty() && token[0] == '~') {
461 auto expanded = expand_tilde(token.substr(1));
462 if (!expanded) return std::nullopt;
463 for (const auto& c : *expanded) set.push_back(c);
464 } else {
465 auto constraint = parse_constraint(token);
466 if (!constraint) return std::nullopt;
467 set.push_back(*constraint);
468 }
469 }
470 return set;
471}
472
473} // namespace detail
474
475// ============================================================================
476// VersionRange methods
477// ============================================================================
478
479inline std::optional<Version> VersionRange::min_version() const {
480 std::optional<Version> min;
481
482 for (const auto& set : sets) {
483 for (const auto& constraint : set) {
484 if (constraint.op == Comparator::Ge || constraint.op == Comparator::Eq ||
485 constraint.op == Comparator::Gt) {
486 if (!min || constraint.version < *min) {
487 min = constraint.version;
488 }
489 }
490 }
491 }
492
493 return min;
494}
495
496inline std::string VersionRange::selection_key() const {
497 auto min = min_version();
498 if (!min) return "";
499 return std::to_string(min->major()) + "." + std::to_string(min->minor());
500}
501
502// ============================================================================
503// API Implementation
504// ============================================================================
505
506inline std::optional<Version> parse_version(const std::string& str) {
507 return detail::parse_version_impl(str);
508}
509
510inline std::optional<VersionRange> parse_range(const std::string& str) {
511 std::string s = detail::trim(str);
512 if (s.empty()) return std::nullopt;
513
514 // Split by || for OR
515 auto or_parts = detail::split(s, "||");
516
517 VersionRange range;
518 for (const auto& part : or_parts) {
519 auto set = detail::parse_comparator_set(detail::trim(part));
520 if (!set) return std::nullopt;
521 range.sets.push_back(*set);
522 }
523
524 if (range.sets.empty()) return std::nullopt;
525 return range;
526}
527
528inline bool satisfies(const Version& version, const Constraint& constraint) {
529 switch (constraint.op) {
530 case Comparator::Eq:
531 return version == constraint.version;
532 case Comparator::Lt:
533 return version < constraint.version;
534 case Comparator::Le:
535 return version <= constraint.version;
536 case Comparator::Gt:
537 return version > constraint.version;
538 case Comparator::Ge:
539 return version >= constraint.version;
540 }
541 return false;
542}
543
544inline bool satisfies(const Version& version, const ComparatorSet& set) {
545 // Empty set matches everything
546 if (set.empty()) return true;
547
548 // All constraints in a set must be satisfied (AND)
549 for (const auto& constraint : set) {
550 if (!satisfies(version, constraint)) {
551 return false;
552 }
553 }
554 return true;
555}
556
557inline bool satisfies(const Version& version, const VersionRange& range) {
558 // Empty range matches nothing
559 if (range.sets.empty()) return false;
560
561 // Any set in the range must be satisfied (OR)
562 for (const auto& set : range.sets) {
563 if (satisfies(version, set)) {
564 return true;
565 }
566 }
567 return false;
568}
569
570inline std::optional<Version> select_best(
571 const std::vector<Version>& versions,
572 const VersionRange& range)
573{
574 std::optional<Version> best;
575
576 for (const auto& v : versions) {
577 if (satisfies(v, range)) {
578 if (!best || v > *best) {
579 best = v;
580 }
581 }
582 }
583
584 return best;
585}
586
587// ============================================================================
588// NAK Selection Helper
589// ============================================================================
590
610struct NakSelectionResult {
611 bool found = false;
612 std::string nak_id;
613 std::string nak_version;
614 std::string record_ref;
615 std::string selection_reason;
616 std::vector<std::string> candidates;
617 std::string error;
618};
619
625template<typename RuntimeMap>
626inline NakSelectionResult select_nak_from_inventory(
627 const RuntimeMap& runtimes,
628 const std::string& nak_id,
629 const std::string& version_req)
630{
631 NakSelectionResult result;
632 result.nak_id = nak_id;
633
634 // Parse version requirement
635 auto range = parse_range(version_req);
636 if (!range) {
637 result.error = "Invalid version requirement: " + version_req;
638 return result;
639 }
640
641 // Find all matching versions
642 std::vector<std::pair<Version, std::string>> matches; // (version, record_ref)
643
644 for (const auto& [record_ref, runtime] : runtimes) {
645 // Check if this runtime matches the NAK ID
646 if (runtime.nak.id != nak_id) {
647 continue;
648 }
649
650 // Parse the runtime version
651 auto version = parse_version(runtime.nak.version);
652 if (!version) {
653 continue; // Skip invalid versions
654 }
655
656 // Check if it satisfies the requirement
657 if (satisfies(*version, *range)) {
658 matches.push_back({*version, record_ref});
659 result.candidates.push_back(runtime.nak.version);
660 }
661 }
662
663 if (matches.empty()) {
664 result.error = "No NAK found matching " + nak_id + " " + version_req;
665 return result;
666 }
667
668 // Select the highest matching version
669 auto best = std::max_element(matches.begin(), matches.end(),
670 [](const auto& a, const auto& b) { return a.first < b.first; });
671
672 result.found = true;
673 result.nak_version = best->first.str();
674 result.record_ref = best->second;
675 result.selection_reason = "highest_matching_version";
676
677 return result;
678}
679
680} // namespace semver
681} // namespace nah
682
683#endif // __cplusplus
684
685#endif // NAH_SEMVER_H