FlightGear next
AddonVersion.cxx
Go to the documentation of this file.
1// -*- coding: utf-8 -*-
2//
3// AddonVersion.cxx --- Version class for FlightGear add-ons
4// Copyright (C) 2017 Florent Rougon
5//
6// This program is free software; you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation; either version 2 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along
17// with this program; if not, write to the Free Software Foundation, Inc.,
18// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20#include <numeric> // std::accumulate()
21#include <ostream>
22#include <regex>
23#include <string>
24#include <tuple>
25#include <type_traits>
26#include <utility>
27#include <vector>
28
29#include <cassert>
30
31#include <simgear/misc/strutils.hxx>
32#include <simgear/nasal/cppbind/Ghost.hxx>
33#include <simgear/nasal/cppbind/NasalCallContext.hxx>
34#include <simgear/nasal/cppbind/NasalHash.hxx>
35#include <simgear/sg_inlines.h>
36#include <simgear/structure/exception.hxx>
37
38#include "addon_fwd.hxx"
39#include "AddonVersion.hxx"
40
41using std::string;
42using std::vector;
43using simgear::enumValue;
44
45namespace strutils = simgear::strutils;
46
47namespace flightgear
48{
49
50namespace addons
51{
52
53// ***************************************************************************
54// * AddonVersionSuffix *
55// ***************************************************************************
56
58 AddonVersionSuffixPrereleaseType preReleaseType, int preReleaseNum,
59 bool developmental, int devNum)
60 : _preReleaseType(preReleaseType),
61 _preReleaseNum(preReleaseNum),
62 _developmental(developmental),
63 _devNum(devNum)
64{ }
65
66// Construct an AddonVersionSuffix instance from a tuple (preReleaseType,
67// preReleaseNum, developmental, devNum). This would be nicer with
68// std::apply(), but it requires C++17.
70 const std::tuple<AddonVersionSuffixPrereleaseType, int, bool, int>& t)
71 : AddonVersionSuffix(std::get<0>(t), std::get<1>(t), std::get<2>(t),
72 std::get<3>(t))
73{ }
74
76 : AddonVersionSuffix(suffixStringToTuple(suffix))
77{ }
78
80 : AddonVersionSuffix(string(suffix))
81{ }
82
83// Static method
84string
85AddonVersionSuffix::releaseTypeStr(AddonVersionSuffixPrereleaseType releaseType)
86{
87 switch (releaseType) {
89 return string("a");
91 return string("b");
93 return string("rc");
95 return string();
96 default:
97 throw sg_error("unexpected value for member of "
98 "flightgear::addons::AddonVersionSuffixPrereleaseType: " +
99 std::to_string(enumValue(releaseType)));
100 }
101}
102
103string
105{
106 string res = releaseTypeStr(_preReleaseType);
107
108 if (!res.empty()) {
109 res += std::to_string(_preReleaseNum);
110 }
111
112 if (_developmental) {
113 res += ".dev" + std::to_string(_devNum);
114 }
115
116 return res;
117}
118
119// Static method
120std::tuple<AddonVersionSuffixPrereleaseType, int, bool, int>
121AddonVersionSuffix::suffixStringToTuple(const std::string& suffix)
122{
123#ifdef HAVE_WORKING_STD_REGEX
124 // Use a simplified variant of the syntax described in PEP 440
125 // <https://www.python.org/dev/peps/pep-0440/>: for the version suffix, only
126 // allow a pre-release segment and a development release segment, but no
127 // post-release segment.
128 std::regex versionSuffixRegexp(R"((?:(a|b|rc)(\d+))?(?:\.dev(\d+))?)");
129 std::smatch results;
130
131 if (std::regex_match(suffix, results, versionSuffixRegexp)) {
132 const string preReleaseType_s = results.str(1);
133 const string preReleaseNum_s = results.str(2);
134 const string devNum_s = results.str(3);
135
137 int preReleaseNum = 0;
138 int devNum = 0;
139
140 if (preReleaseType_s.empty()) {
142 } else {
143 if (preReleaseType_s == "a") {
145 } else if (preReleaseType_s == "b") {
147 } else if (preReleaseType_s == "rc") {
149 } else {
150 assert(false); // the regexp should prevent this
151 }
152
153 assert(!preReleaseNum_s.empty());
154 preReleaseNum = strutils::readNonNegativeInt<int>(preReleaseNum_s);
155
156 if (preReleaseNum < 1) {
157 string msg = "invalid add-on version suffix: '" + suffix + "' "
158 "(prerelease number must be greater than or equal to 1, but got " +
159 preReleaseNum_s + ")";
160 throw sg_format_exception(msg, suffix);
161 }
162 }
163
164 if (!devNum_s.empty()) {
165 devNum = strutils::readNonNegativeInt<int>(devNum_s);
166
167 if (devNum < 1) {
168 string msg = "invalid add-on version suffix: '" + suffix + "' "
169 "(development release number must be greater than or equal to 1, "
170 "but got " + devNum_s + ")";
171 throw sg_format_exception(msg, suffix);
172 }
173 }
174
175 return std::make_tuple(preReleaseType, preReleaseNum, !devNum_s.empty(),
176 devNum);
177#else // all this 'else' clause should be removed once we actually require C++11
178 bool isMatch;
180 int preReleaseNum;
181 bool developmental;
182 int devNum;
183
184 std::tie(isMatch, preReleaseType, preReleaseNum, developmental, devNum) =
185 parseVersionSuffixString_noRegexp(suffix);
186
187 if (isMatch) {
188 return std::make_tuple(preReleaseType, preReleaseNum, developmental,
189 devNum);
190#endif // HAVE_WORKING_STD_REGEX
191 } else { // the regexp didn't match
192 string msg = "invalid add-on version suffix: '" + suffix + "' "
193 "(expected form is [{a|b|rc}N1][.devN2] where N1 and N2 are positive "
194 "integers)";
195 throw sg_format_exception(msg, suffix);
196 }
197}
198
199// Static method, only needed for compilers that are not C++11-compliant
200// (gcc 4.8 pretends to support <regex> as required by C++11 but doesn't, see
201// <https://stackoverflow.com/a/12665408/4756009>).
202std::tuple<bool, AddonVersionSuffixPrereleaseType, int, bool, int>
203AddonVersionSuffix::parseVersionSuffixString_noRegexp(const string& suffix)
204{
206 string rest;
207 int preReleaseNum = 0; // alpha, beta or release candidate number, or
208 // 0 when absent
209 bool developmental = false; // whether 'suffix' has a .devN2 part
210 int devNum = 0; // the N2 in question, or 0 when absent
211
212 std::tie(preReleaseType, rest) = popPrereleaseTypeFromBeginning(suffix);
213
214 if (preReleaseType != AddonVersionSuffixPrereleaseType::none) {
215 std::size_t startPrerelNum = rest.find_first_of("0123456789");
216 if (startPrerelNum != 0) { // no prerelease num -> no match
217 return std::make_tuple(false, preReleaseType, preReleaseNum, false,
218 devNum);
219 }
220
221 std::size_t endPrerelNum = rest.find_first_not_of("0123456789", 1);
222 // Works whether endPrerelNum is string::npos or not
223 string preReleaseNum_s = rest.substr(0, endPrerelNum);
224 preReleaseNum = strutils::readNonNegativeInt<int>(preReleaseNum_s);
225
226 if (preReleaseNum < 1) {
227 string msg = "invalid add-on version suffix: '" + suffix + "' "
228 "(prerelease number must be greater than or equal to 1, but got " +
229 preReleaseNum_s + ")";
230 throw sg_format_exception(msg, suffix);
231 }
232
233 rest = (endPrerelNum == string::npos) ? "" : rest.substr(endPrerelNum);
234 }
235
236 if (strutils::starts_with(rest, ".dev")) {
237 rest = rest.substr(4);
238 std::size_t startDevNum = rest.find_first_of("0123456789");
239 if (startDevNum != 0) { // no dev num -> no match
240 return std::make_tuple(false, preReleaseType, preReleaseNum, false,
241 devNum);
242 }
243
244 std::size_t endDevNum = rest.find_first_not_of("0123456789", 1);
245 if (endDevNum != string::npos) {
246 // There is trailing garbage after the development release number
247 // -> no match
248 return std::make_tuple(false, preReleaseType, preReleaseNum, false,
249 devNum);
250 }
251
252 devNum = strutils::readNonNegativeInt<int>(rest);
253 if (devNum < 1) {
254 string msg = "invalid add-on version suffix: '" + suffix + "' "
255 "(development release number must be greater than or equal to 1, "
256 "but got " + rest + ")";
257 throw sg_format_exception(msg, suffix);
258 }
259
260 developmental = true;
261 }
262
263 return std::make_tuple(true, preReleaseType, preReleaseNum, developmental,
264 devNum);
265}
266
267// Static method
268std::tuple<AddonVersionSuffixPrereleaseType, string>
269AddonVersionSuffix::popPrereleaseTypeFromBeginning(const string& s)
270{
271 if (s.empty()) {
272 return std::make_tuple(AddonVersionSuffixPrereleaseType::none, s);
273 } else if (s[0] == 'a') {
274 return std::make_tuple(AddonVersionSuffixPrereleaseType::alpha,
275 s.substr(1));
276 } else if (s[0] == 'b') {
277 return std::make_tuple(AddonVersionSuffixPrereleaseType::beta, s.substr(1));
278 } else if (strutils::starts_with(s, "rc")) {
279 return std::make_tuple(AddonVersionSuffixPrereleaseType::candidate,
280 s.substr(2));
281 }
282
283 return std::make_tuple(AddonVersionSuffixPrereleaseType::none, s);
284}
285
286// Beware, this is not suitable for sorting! cf. genSortKey() below.
287std::tuple<AddonVersionSuffixPrereleaseType, int, bool, int>
289{
290 return std::make_tuple(_preReleaseType, _preReleaseNum, _developmental,
291 _devNum);
292}
293
294std::tuple<int,
295 std::underlying_type<AddonVersionSuffixPrereleaseType>::type,
296 int, int, int>
297AddonVersionSuffix::genSortKey() const
298{
299 using AddonRelType = AddonVersionSuffixPrereleaseType;
300
301 // The first element means that a plain .devN is lower than everything else,
302 // except .devM with M <= N (namely: all dev and non-dev alpha, beta,
303 // candidates, as well as the empty suffix).
304 return std::make_tuple(
305 ((_developmental && _preReleaseType == AddonRelType::none) ? 0 : 1),
306 enumValue(_preReleaseType),
307 _preReleaseNum,
308 (_developmental ? 0 : 1), // e.g., 1.0.3a2.devN < 1.0.3a2 for all N
309 _devNum);
310}
311
313{ return lhs.genSortKey() == rhs.genSortKey(); }
314
316{ return !operator==(lhs, rhs); }
317
319{ return lhs.genSortKey() < rhs.genSortKey(); }
320
322{ return operator<(rhs, lhs); }
323
325{ return !operator>(lhs, rhs); }
326
328{ return !operator<(lhs, rhs); }
329
330std::ostream& operator<<(std::ostream& os,
331 const AddonVersionSuffix& addonVersionSuffix)
332{ return os << addonVersionSuffix.str(); }
333
334// ***************************************************************************
335// * AddonVersion *
336// ***************************************************************************
337
338AddonVersion::AddonVersion(int major, int minor, int patchLevel,
340 : _major(major),
341 _minor(minor),
342 _patchLevel(patchLevel),
343 _suffix(std::move(suffix))
344 { }
345
346// Construct an AddonVersion instance from a tuple (major, minor, patchLevel,
347// suffix). This would be nicer with std::apply(), but it requires C++17.
349 const std::tuple<int, int, int, AddonVersionSuffix>& t)
350 : AddonVersion(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t))
351{ }
352
353AddonVersion::AddonVersion(const std::string& versionStr)
354 : AddonVersion(versionStringToTuple(versionStr))
355{ }
356
357AddonVersion::AddonVersion(const char* versionStr)
358 : AddonVersion(string(versionStr))
359{ }
360
361// Static method
362std::tuple<int, int, int, AddonVersionSuffix>
363AddonVersion::versionStringToTuple(const std::string& versionStr)
364{
365#ifdef HAVE_WORKING_STD_REGEX
366 // Use a simplified variant of the syntax described in PEP 440
367 // <https://www.python.org/dev/peps/pep-0440/> (always 3 components in the
368 // release segment, pre-release segment + development release segment; no
369 // post-release segment allowed).
370 std::regex versionRegexp(R"((\d+)\.(\d+).(\d+)(.*))");
371 std::smatch results;
372
373 if (std::regex_match(versionStr, results, versionRegexp)) {
374 const string majorNumber_s = results.str(1);
375 const string minorNumber_s = results.str(2);
376 const string patchLevel_s = results.str(3);
377 const string suffix_s = results.str(4);
378
379 int major = strutils::readNonNegativeInt<int>(majorNumber_s);
380 int minor = strutils::readNonNegativeInt<int>(minorNumber_s);
381 int patchLevel = strutils::readNonNegativeInt<int>(patchLevel_s);
382
383 return std::make_tuple(major, minor, patchLevel,
384 AddonVersionSuffix(suffix_s));
385#else // all this 'else' clause should be removed once we actually require C++11
386 bool isMatch;
387 int major, minor, patchLevel;
389
390 std::tie(isMatch, major, minor, patchLevel, suffix) =
391 parseVersionString_noRegexp(versionStr);
392
393 if (isMatch) {
394 return std::make_tuple(major, minor, patchLevel, suffix);
395#endif // HAVE_WORKING_STD_REGEX
396 } else { // the regexp didn't match
397 string msg = "invalid add-on version number: '" + versionStr + "' "
398 "(expected form is MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where "
399 "N1 and N2 are positive integers)";
400 throw sg_format_exception(msg, versionStr);
401 }
402}
403
404// Static method, only needed for compilers that are not C++11-compliant
405// (gcc 4.8 pretends to support <regex> as required by C++11 but doesn't, see
406// <https://stackoverflow.com/a/12665408/4756009>).
407std::tuple<bool, int, int, int, AddonVersionSuffix>
408AddonVersion::parseVersionString_noRegexp(const string& versionStr)
409{
410 int major = 0, minor = 0, patchLevel = 0;
411 AddonVersionSuffix suffix{};
412
413 // Major version number
414 std::size_t endMajor = versionStr.find_first_not_of("0123456789");
415 if (endMajor == 0 || endMajor == string::npos) { // no match
416 return std::make_tuple(false, major, minor, patchLevel, suffix);
417 }
418 major = strutils::readNonNegativeInt<int>(versionStr.substr(0, endMajor));
419
420 // Dot separating the major and minor version numbers
421 if (versionStr.size() < endMajor + 1 || versionStr[endMajor] != '.') {
422 return std::make_tuple(false, major, minor, patchLevel, suffix);
423 }
424 string rest = versionStr.substr(endMajor + 1);
425
426 // Minor version number
427 std::size_t endMinor = rest.find_first_not_of("0123456789");
428 if (endMinor == 0 || endMinor == string::npos) { // no match
429 return std::make_tuple(false, major, minor, patchLevel, suffix);
430 }
431 minor = strutils::readNonNegativeInt<int>(rest.substr(0, endMinor));
432
433 // Dot separating the minor version number and the patch level
434 if (rest.size() < endMinor + 1 || rest[endMinor] != '.') {
435 return std::make_tuple(false, major, minor, patchLevel, suffix);
436 }
437 rest = rest.substr(endMinor + 1);
438
439 // Patch level
440 std::size_t endPatchLevel = rest.find_first_not_of("0123456789");
441 if (endPatchLevel == 0) { // no patch level, therefore no match
442 return std::make_tuple(false, major, minor, patchLevel, suffix);
443 }
444 patchLevel = strutils::readNonNegativeInt<int>(rest.substr(0, endPatchLevel));
445
446 if (endPatchLevel != string::npos) { // there is a version suffix, parse it
447 suffix = AddonVersionSuffix(rest.substr(endPatchLevel));
448 }
449
450 return std::make_tuple(true, major, minor, patchLevel, suffix);
451}
452
454{ return _major; }
455
457{ return _minor; }
458
460{ return _patchLevel; }
461
463{ return _suffix; }
464
465std::string AddonVersion::suffixStr() const
466{ return suffix().str(); }
467
468std::tuple<int, int, int, AddonVersionSuffix> AddonVersion::makeTuple() const
469{
470 return std::make_tuple(majorNumber(), minorNumber(), patchLevel(), suffix());
471}
472
473string AddonVersion::str() const
474{
475 // Assemble the major.minor.patchLevel string
476 vector<int> v({majorNumber(), minorNumber(), patchLevel()});
477 string relSeg = std::accumulate(std::next(v.begin()), v.end(),
478 std::to_string(v[0]),
479 [](string s, int num) {
480 return s + '.' + std::to_string(num);
481 });
482
483 // Concatenate with the suffix string
484 return relSeg + suffixStr();
485}
486
487
488bool operator==(const AddonVersion& lhs, const AddonVersion& rhs)
489{ return lhs.makeTuple() == rhs.makeTuple(); }
490
491bool operator!=(const AddonVersion& lhs, const AddonVersion& rhs)
492{ return !operator==(lhs, rhs); }
493
494bool operator< (const AddonVersion& lhs, const AddonVersion& rhs)
495{ return lhs.makeTuple() < rhs.makeTuple(); }
496
497bool operator> (const AddonVersion& lhs, const AddonVersion& rhs)
498{ return operator<(rhs, lhs); }
499
500bool operator<=(const AddonVersion& lhs, const AddonVersion& rhs)
501{ return !operator>(lhs, rhs); }
502
503bool operator>=(const AddonVersion& lhs, const AddonVersion& rhs)
504{ return !operator<(lhs, rhs); }
505
506std::ostream& operator<<(std::ostream& os, const AddonVersion& addonVersion)
507{ return os << addonVersion.str(); }
508
509
510// ***************************************************************************
511// * For the Nasal bindings *
512// ***************************************************************************
513
514bool AddonVersion::equal(const nasal::CallContext& ctx) const
515{
516 auto other = ctx.requireArg<AddonVersionRef>(0);
517 return *this == *other;
518}
519
520bool AddonVersion::nonEqual(const nasal::CallContext& ctx) const
521{
522 auto other = ctx.requireArg<AddonVersionRef>(0);
523 return *this != *other;
524}
525
526bool AddonVersion::lowerThan(const nasal::CallContext& ctx) const
527{
528 auto other = ctx.requireArg<AddonVersionRef>(0);
529 return *this < *other;
530}
531
532bool AddonVersion::lowerThanOrEqual(const nasal::CallContext& ctx) const
533{
534 auto other = ctx.requireArg<AddonVersionRef>(0);
535 return *this <= *other;
536}
537
538bool AddonVersion::greaterThan(const nasal::CallContext& ctx) const
539{
540 auto other = ctx.requireArg<AddonVersionRef>(0);
541 return *this > *other;
542}
543
544bool AddonVersion::greaterThanOrEqual(const nasal::CallContext& ctx) const
545{
546 auto other = ctx.requireArg<AddonVersionRef>(0);
547 return *this >= *other;
548}
549
550// Static method
551void AddonVersion::setupGhost(nasal::Hash& addonsModule)
552{
553 nasal::Ghost<AddonVersionRef>::init("addons.AddonVersion")
554 .member("majorNumber", &AddonVersion::majorNumber)
555 .member("minorNumber", &AddonVersion::minorNumber)
556 .member("patchLevel", &AddonVersion::patchLevel)
557 .member("suffix", &AddonVersion::suffixStr)
558 .method("str", &AddonVersion::str)
559 .method("equal", &AddonVersion::equal)
560 .method("nonEqual", &AddonVersion::nonEqual)
561 .method("lowerThan", &AddonVersion::lowerThan)
562 .method("lowerThanOrEqual", &AddonVersion::lowerThanOrEqual)
563 .method("greaterThan", &AddonVersion::greaterThan)
564 .method("greaterThanOrEqual", &AddonVersion::greaterThanOrEqual);
565}
566
567} // of namespace addons
568
569} // of namespace flightgear
friend bool operator<(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
AddonVersionSuffix(AddonVersionSuffixPrereleaseType _preReleaseType=AddonVersionSuffixPrereleaseType::none, int preReleaseNum=0, bool developmental=false, int devNum=0)
std::tuple< AddonVersionSuffixPrereleaseType, int, bool, int > makeTuple() const
friend bool operator==(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
bool lowerThan(const nasal::CallContext &ctx) const
bool nonEqual(const nasal::CallContext &ctx) const
friend bool operator==(const AddonVersion &lhs, const AddonVersion &rhs)
bool equal(const nasal::CallContext &ctx) const
bool lowerThanOrEqual(const nasal::CallContext &ctx) const
AddonVersionSuffix suffix() const
static void setupGhost(nasal::Hash &addonsModule)
bool greaterThan(const nasal::CallContext &ctx) const
bool greaterThanOrEqual(const nasal::CallContext &ctx) const
friend bool operator<(const AddonVersion &lhs, const AddonVersion &rhs)
AddonVersion(int major=0, int minor=0, int patchLevel=0, AddonVersionSuffix suffix=AddonVersionSuffix())
bool operator>(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
bool operator<=(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
bool operator>=(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
std::ostream & operator<<(std::ostream &os, const Addon &addon)
Definition Addon.cxx:509
bool operator!=(const AddonVersionSuffix &lhs, const AddonVersionSuffix &rhs)
SGSharedPtr< AddonVersion > AddonVersionRef
Definition addon_fwd.hxx:47
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
Definition Addon.cxx:53