aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libbutl/standard-version.cxx319
-rw-r--r--libbutl/standard-version.mxx11
-rw-r--r--tests/standard-version/driver.cxx26
-rw-r--r--tests/standard-version/testscript93
4 files changed, 368 insertions, 81 deletions
diff --git a/libbutl/standard-version.cxx b/libbutl/standard-version.cxx
index b7678ac..4e5e9f6 100644
--- a/libbutl/standard-version.cxx
+++ b/libbutl/standard-version.cxx
@@ -44,31 +44,37 @@ namespace butl
{
// Utility functions
//
- static uint64_t
+ static bool
parse_uint64 (const string& s, size_t& p,
- const char* m,
+ uint64_t& r,
uint64_t min, uint64_t max)
{
if (s[p] == '-' || s[p] == '+') // strtoull() allows these.
- throw invalid_argument (m);
+ return false;
const char* b (s.c_str () + p);
char* e (nullptr);
- uint64_t r (strtoull (b, &e, 10));
+ uint64_t v (strtoull (b, &e, 10)); // Can't throw.
- if (b == e || r < min || r > max)
- throw invalid_argument (m);
+ if (errno == ERANGE || b == e || v < min || v > max)
+ return false;
p = e - s.c_str ();
- return static_cast<uint64_t> (r);
+ r = static_cast<uint64_t> (v);
+ return true;
}
- static uint16_t
+ static bool
parse_uint16 (const string& s, size_t& p,
- const char* m,
+ uint16_t& r,
uint16_t min = 0, uint16_t max = 999)
{
- return static_cast<uint16_t> (parse_uint64 (s, p, m, min, max));
+ uint64_t v;
+ if (!parse_uint64 (s, p, v, min, max))
+ return false;
+
+ r = static_cast<uint16_t> (v);
+ return true;
}
static void
@@ -130,12 +136,63 @@ namespace butl
throw invalid_argument ("invalid project version");
}
- // standard_version
- //
- standard_version::
- standard_version (const std::string& s, flags f)
+ static bool
+ parse_snapshot (const std::string& s,
+ size_t& p,
+ standard_version& r,
+ std::string& failure_reason)
{
- auto bail = [] (const char* m) {throw invalid_argument (m);};
+ // Note that snapshot id must be empty for 'z' snapshot number.
+ //
+ if (s[p] == 'z')
+ {
+ r.snapshot_sn = standard_version::latest_sn;
+ r.snapshot_id = "";
+ ++p;
+ return true;
+ }
+
+ uint64_t sn;
+ if (!parse_uint64 (s, p, sn, 1, standard_version::latest_sn - 1))
+ {
+ failure_reason = "invalid snapshot number";
+ return false;
+ }
+
+ std::string id;
+ if (s[p] == '.')
+ {
+ char c;
+ for (++p; alnum (c = s[p]); ++p)
+ id += c;
+
+ if (id.empty () || id.size () > 16)
+ {
+ failure_reason = "invalid snapshot id";
+ return false;
+ }
+ }
+
+ r.snapshot_sn = sn;
+ r.snapshot_id = move (id);
+ return true;
+ }
+
+ struct parse_result
+ {
+ optional<standard_version> version;
+ string failure_reason;
+ };
+
+ static parse_result
+ parse_version (const std::string& s, standard_version::flags f)
+ {
+ auto bail = [] (string m) -> parse_result
+ {
+ return parse_result {nullopt, move (m)};
+ };
+
+ standard_version r;
// Note that here and below p is less or equal n, and so s[p] is always
// valid.
@@ -146,47 +203,51 @@ namespace butl
if (ep)
{
- epoch = parse_uint16 (s, ++p, "invalid epoch", 1, uint16_t (~0));
+ if (!parse_uint16 (s, ++p, r.epoch, 1, uint16_t (~0)))
+ return bail ("invalid epoch");
// Skip the terminating character if it is '-', otherwise fail.
//
if (s[p++] != '-')
- bail ("'-' expected after epoch");
+ return bail ("'-' expected after epoch");
}
uint16_t ma, mi, bf, ab (0);
bool earliest (false);
- ma = parse_uint16 (s, p, "invalid major version");
+ if (!parse_uint16 (s, p, ma))
+ return bail ("invalid major version");
// The only valid version that has no epoch, contains only the major
// version being equal to zero, that is optionally followed by the plus
// character, is the stub version, unless forbidden.
//
- bool stub ((f & allow_stub) != 0 && !ep && ma == 0 &&
+ bool stub ((f & standard_version::allow_stub) != 0 && !ep && ma == 0 &&
(p == n || s[p] == '+'));
if (stub)
- version = uint64_t (~0);
+ r.version = uint64_t (~0);
else
{
if (s[p] != '.')
- bail ("'.' expected after major version");
+ return bail ("'.' expected after major version");
- mi = parse_uint16 (s, ++p, "invalid minor version");
+ if (!parse_uint16 (s, ++p, mi))
+ return bail ("invalid minor version");
if (s[p] != '.')
- bail ("'.' expected after minor version");
+ return bail ("'.' expected after minor version");
- bf = parse_uint16 (s, ++p, "invalid patch version");
+ if (!parse_uint16 (s, ++p, bf))
+ return bail ("invalid patch version");
- // AAABBBCCCDDDE
- version = ma * 10000000000ULL +
- mi * 10000000ULL +
- bf * 10000ULL;
+ // AAABBBCCCDDDE
+ r.version = ma * 10000000000ULL +
+ mi * 10000000ULL +
+ bf * 10000ULL;
- if (version == 0)
- bail ("0.0.0 version");
+ if (r.version == 0)
+ return bail ("0.0.0 version");
// Parse the pre-release component if present.
//
@@ -197,17 +258,18 @@ namespace butl
// If the last character in the string is dash, then this is the
// earliest version pre-release, unless forbidden.
//
- if (k == '\0' && (f & allow_earliest) != 0)
+ if (k == '\0' && (f & standard_version::allow_earliest) != 0)
earliest = true;
else
{
if (k != 'a' && k != 'b')
- bail ("'a' or 'b' expected in pre-release");
+ return bail ("'a' or 'b' expected in pre-release");
if (s[++p] != '.')
- bail ("'.' expected after pre-release letter");
+ return bail ("'.' expected after pre-release letter");
- ab = parse_uint16 (s, ++p, "invalid pre-release", 0, 499);
+ if (!parse_uint16 (s, ++p, ab, 0, 499))
+ return bail ("invalid pre-release");
if (k == 'b')
ab += 500;
@@ -216,9 +278,13 @@ namespace butl
// number can't be zero for the final pre-release.
//
if (s[p] == '.')
- parse_snapshot (s, ++p);
+ {
+ string e;
+ if (!parse_snapshot (s, ++p, r, e))
+ return bail (move (e));
+ }
else if (ab == 0 || ab == 500)
- bail ("invalid final pre-release");
+ return bail ("invalid final pre-release");
}
}
}
@@ -227,17 +293,39 @@ namespace butl
{
assert (!earliest); // Would bail out earlier (a or b expected after -).
- revision = parse_uint16 (s, ++p, "invalid revision", 1, uint16_t (~0));
+ if (!parse_uint16 (s, ++p, r.revision, 1, uint16_t (~0)))
+ return bail ("invalid revision");
}
if (p != n)
- bail ("junk after version");
+ return bail ("junk after version");
+
+ if (ab != 0 || r.snapshot_sn != 0 || earliest)
+ r.version -= 10000 - ab * 10;
+
+ if (r.snapshot_sn != 0 || earliest)
+ r.version += 1;
- if (ab != 0 || snapshot_sn != 0 || earliest)
- version -= 10000 - ab * 10;
+ return parse_result {move (r), string () /* failure_reason */};
+ }
+
+ optional<standard_version>
+ parse_standard_version (const std::string& s, standard_version::flags f)
+ {
+ return parse_version (s, f).version;
+ }
+
+ // standard_version
+ //
+ standard_version::
+ standard_version (const std::string& s, flags f)
+ {
+ parse_result r (parse_version (s, f));
- if (snapshot_sn != 0 || earliest)
- version += 1;
+ if (r.version)
+ *this = move (*r.version);
+ else
+ throw invalid_argument (r.failure_reason);
}
standard_version::
@@ -257,7 +345,9 @@ namespace butl
if (snapshot)
{
size_t p (0);
- parse_snapshot (s, p);
+ std::string e;
+ if (!parse_snapshot (s, p, *this, e))
+ throw invalid_argument (e);
if (p != s.size ())
throw invalid_argument ("junk after snapshot");
@@ -311,37 +401,6 @@ namespace butl
throw invalid_argument ("invalid snapshot");
}
- void standard_version::
- parse_snapshot (const std::string& s, size_t& p)
- {
- // Note that snapshot id must be empty for 'z' snapshot number.
- //
- if (s[p] == 'z')
- {
- snapshot_sn = latest_sn;
- ++p;
- return;
- }
-
- uint64_t sn (parse_uint64 (s,
- p,
- "invalid snapshot number",
- 1, latest_sn - 1));
- std::string id;
- if (s[p] == '.')
- {
- char c;
- for (++p; alnum (c = s[p]); ++p)
- id += c;
-
- if (id.empty () || id.size () > 16)
- throw invalid_argument ("invalid snapshot id");
- }
-
- snapshot_sn = sn;
- snapshot_id = move (id);
- }
-
string standard_version::
string_pre_release () const
{
@@ -460,6 +519,70 @@ namespace butl
// standard_version_constraint
//
+ // Return the maximum version (right hand side) of the range the shortcut
+ // operator translates to:
+ //
+ // ~X.Y.Z -> [X.Y.Z X.Y+1.0-)
+ // ^X.Y.Z -> [X.Y.Z X+1.0.0-)
+ // ^0.Y.Z -> [0.Y.Z 0.Y+1.0-)
+ //
+ // If it is impossible to construct such a version due to overflow (see
+ // below) then throw std::invalid_argument, unless requested to ignore in
+ // which case return an empty version.
+ //
+ static standard_version
+ shortcut_max_version (char c,
+ const standard_version& version,
+ bool ignore_overflow)
+ {
+ assert (c == '~' || c == '^');
+
+ // Advance major/minor version number by one and make the version earliest
+ // (see standard_version() ctor for details).
+ //
+ uint64_t v;
+
+ if (c == '~' || (c == '^' && version.major () == 0))
+ {
+ // If for ~X.Y.Z Y is 999, then we cannot produce a valid X.Y+1.0-
+ // version (due to an overflow).
+ //
+ if (version.minor () == 999)
+ {
+ if (ignore_overflow)
+ return standard_version ();
+
+ throw invalid_argument ("invalid minor version");
+ }
+
+ // AAABBBCCCDDDE
+ v = version.major () * 10000000000ULL +
+ (version.minor () + 1) * 10000000ULL;
+ }
+ else
+ {
+ // If for ^X.Y.Z X is 999, then we cannot produce a valid X+1.0.0-
+ // version (due to an overflow).
+ //
+ if (version.major () == 999)
+ {
+ if (ignore_overflow)
+ return standard_version ();
+
+ throw invalid_argument ("invalid major version");
+ }
+
+ // AAABBBCCCDDDE
+ v = (version.major () + 1) * 10000000000ULL;
+ }
+
+ return standard_version (version.epoch,
+ v - 10000 /* no alpha/beta */ + 1 /* earliest */,
+ string () /* snapshot */,
+ 0 /* revision */,
+ standard_version::allow_earliest);
+ }
+
standard_version_constraint::
standard_version_constraint (const std::string& s)
{
@@ -522,8 +645,38 @@ namespace butl
// Verify and copy the constraint.
//
- *this = standard_version_constraint (min_version, min_open,
- max_version, max_open);
+ *this = standard_version_constraint (move (min_version), min_open,
+ move (max_version), max_open);
+ }
+ else if (c == '~' || c == '^')
+ {
+ p = s.find_first_not_of (spaces, ++p);
+ if (p == string::npos)
+ bail ("no version");
+
+ // Can throw.
+ //
+ standard_version min_version (
+ standard_version (s.substr (p), standard_version::allow_earliest));
+
+ // Can throw.
+ //
+ standard_version max_version (
+ shortcut_max_version (c, min_version, false));
+
+ try
+ {
+ *this = standard_version_constraint (
+ move (min_version), false /* min_open */,
+ move (max_version), true /* max_open */);
+ }
+ catch (const invalid_argument&)
+ {
+ // There shouldn't be a reason for standard_version_constraint()
+ // to throw.
+ //
+ assert (false);
+ }
}
else
{
@@ -637,6 +790,20 @@ namespace butl
if (*min_version == *max_version)
return "== " + min_version->string ();
+ if (!min_open && max_open)
+ {
+ // We try the '^' shortcut first as prefer ^0.2.3 syntax over ~0.2.3.
+ //
+ // In the case of overflow shortcut_max_version() function will return
+ // an empty version, that will never match the max_version.
+ //
+ if (shortcut_max_version ('^', *min_version, true) == *max_version)
+ return '^' + min_version->string ();
+
+ if (shortcut_max_version ('~', *min_version, true) == *max_version)
+ return '~' + min_version->string ();
+ }
+
return (min_open ? '(' : '[') + min_version->string () + ' ' +
max_version->string () + (max_open ? ')' : ']');
}
diff --git a/libbutl/standard-version.mxx b/libbutl/standard-version.mxx
index b161063..fa74e01 100644
--- a/libbutl/standard-version.mxx
+++ b/libbutl/standard-version.mxx
@@ -157,12 +157,14 @@ LIBBUTL_MODEXPORT namespace butl
// Create empty version.
//
standard_version () {} // = default; @@ MOD VC
-
- private:
- void
- parse_snapshot (const std::string&, std::size_t&);
};
+ // Try to parse a string as a standard version returning nullopt if invalid.
+ //
+ LIBBUTL_SYMEXPORT optional<standard_version>
+ parse_standard_version (const std::string&,
+ standard_version::flags = standard_version::none);
+
inline bool
operator< (const standard_version& x, const standard_version& y) noexcept
{
@@ -220,6 +222,7 @@ LIBBUTL_MODEXPORT namespace butl
// The build2 "standard version" constraint:
//
// ('==' | '>' | '<' | '>=' | '<=') <version>
+ // ('^' | '~') <version>
// ('(' | '[') <version> <version> (')' | ']')
//
struct LIBBUTL_SYMEXPORT standard_version_constraint
diff --git a/tests/standard-version/driver.cxx b/tests/standard-version/driver.cxx
index c6f9c11..1eb1bc8 100644
--- a/tests/standard-version/driver.cxx
+++ b/tests/standard-version/driver.cxx
@@ -76,6 +76,32 @@ version (const string& s,
assert (r == v);
}
+ if (!r.stub ())
+ {
+ auto max_ver = [&v] (char c) -> string
+ {
+ string e (v.epoch != 0 ? '+' + to_string (v.epoch) + '-' : string ());
+
+ return c == '~' || v.major () == 0
+ ? e + to_string (v.major ()) + '.' + to_string (v.minor () + 1) + ".0-"
+ : e + to_string (v.major () + 1) + ".0.0-";
+ };
+
+ if (v.minor () != 999)
+ {
+ standard_version_constraint c1 ("~" + s);
+ standard_version_constraint c2 ('[' + s + ' ' + max_ver ('~') + ')');
+ assert (c1 == c2);
+ }
+
+ if ((v.major () == 0 && v.minor () != 999) ||
+ (v.major () != 0 && v.major () != 999))
+ {
+ standard_version_constraint c1 ("^" + s);
+ standard_version_constraint c2 ('[' + s + ' ' + max_ver ('^') + ')');
+ assert (c1 == c2);
+ }
+ }
}
catch (const invalid_argument& e)
{
diff --git a/tests/standard-version/testscript b/tests/standard-version/testscript
index f06b178..65c446b 100644
--- a/tests/standard-version/testscript
+++ b/tests/standard-version/testscript
@@ -66,6 +66,14 @@
$* <<EOF >>EOF
EOF
+
+ : max
+ :
+ $* <<EOF >>EOF
+ 1.2.999
+ 1.999.999
+ 999.999.999
+ EOF
}
: invalid
@@ -236,10 +244,12 @@
[1.2.3 1.2.4]
(1.2.3 1.2.4)
[ 1.2.3- 1.2.4- ]
+ [1.999.0 2.0.0)
EOI
[1.2.3 1.2.4]
(1.2.3 1.2.4)
[1.2.3- 1.2.4-]
+ [1.999.0 2.0.0)
EOE
: invalid
@@ -262,7 +272,7 @@
: min-gt-max
:
- $* <'[1.2.4 1.2.3]' 2>'min version is greater than max version' == 1
+ $* <'[999.0.0 1.0.0)' 2>'min version is greater than max version' == 1
: open-end
:
@@ -319,6 +329,53 @@
$* <'>= 1.2.3-a.1.1.ads@' 2>'invalid version: junk after version' == 1
}
}
+
+ : shortcut
+ :
+ {
+ : valid
+ :
+ $* <<EOI >>EOO
+ ~1.2.3
+ ^1.2.3
+ ^0.2.3
+ ~1.2.3-
+ ^1.2.3-
+ ^0.2.3-
+ ~1.2.3-a.1
+ ^1.2.3-a.1
+ ^0.2.3-a.1
+ ~1.2.3-a.1.123
+ ^1.2.3-a.1.123
+ ^0.2.3-a.1.123
+ ~ 1.2.3
+ EOI
+ ~1.2.3
+ ^1.2.3
+ ^0.2.3
+ ~1.2.3-
+ ^1.2.3-
+ ^0.2.3-
+ ~1.2.3-a.1
+ ^1.2.3-a.1
+ ^0.2.3-a.1
+ ~1.2.3-a.1.123
+ ^1.2.3-a.1.123
+ ^0.2.3-a.1.123
+ ~1.2.3
+ EOO
+
+ : invalid
+ :
+ {
+ $* <'-1.2.3' 2>'invalid constraint' == 1 : bad-char
+ $* <'~' 2>'no version' == 1 : no-version
+ $* <'~1.2' 2>"'.' expected after minor version" == 1 : bad-ver
+ $* <'~1.999.0' 2>"invalid minor version" == 1 : bad-min-tilde
+ $* <'^0.999.0' 2>"invalid minor version" == 1 : bad-min-caret
+ $* <'^999.0.0' 2>"invalid major version" == 1 : bad-maj-caret
+ }
+ }
}
: satisfaction
@@ -401,4 +458,38 @@
$* '1.2.3' '[1.2.1 1.2.2)' >n : open
}
}
+
+ : shortcut
+ :
+ {
+ : tilde
+ :
+ {
+ $* '1.2.3-b.499' '~1.2.3' >n : out-left
+ $* '1.2.3' '~1.2.3' >y : in-left
+ $* '1.2.4' '~1.2.3' >y : in
+ $* '1.2.999' '~1.2.3' >y : in-right
+ $* '1.3.0-' '~1.2.3' >n : out-right
+ }
+
+ : caret
+ :
+ {
+ $* '1.2.3-b.499' '^1.2.3' >n : out-left
+ $* '1.2.3' '^1.2.3' >y : in-left
+ $* '1.3.0' '^1.2.3' >y : in
+ $* '1.999.999' '^1.2.3' >y : in-right
+ $* '2.0.0-' '^1.2.3' >n : out-right
+ }
+
+ : caret-zero-major
+ :
+ {
+ $* '0.2.3-b.499' '^0.2.3' >n : out-left
+ $* '0.2.3' '^0.2.3' >y : in-left
+ $* '0.2.4' '^0.2.3' >y : in
+ $* '0.2.999' '^0.2.3' >y : in-right
+ $* '0.3.0-' '^0.2.3' >n : out-right
+ }
+ }
}