aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--butl/buildfile1
-rw-r--r--butl/standard-version111
-rw-r--r--butl/standard-version.cxx333
-rw-r--r--butl/standard-version.ixx94
-rw-r--r--tests/standard-version/buildfile7
-rw-r--r--tests/standard-version/driver.cxx136
-rw-r--r--tests/standard-version/testscript190
7 files changed, 872 insertions, 0 deletions
diff --git a/butl/buildfile b/butl/buildfile
index 75217fa..04e6e09 100644
--- a/butl/buildfile
+++ b/butl/buildfile
@@ -28,6 +28,7 @@ lib{butl}: \
{hxx ixx cxx}{ sendmail } \
{hxx cxx}{ sha256 } \
{hxx }{ small-vector } \
+ {hxx ixx cxx}{ standard-version } \
{hxx cxx}{ string-parser } \
{hxx txx }{ string-table } \
{hxx cxx}{ tab-parser } \
diff --git a/butl/standard-version b/butl/standard-version
new file mode 100644
index 0000000..0888900
--- /dev/null
+++ b/butl/standard-version
@@ -0,0 +1,111 @@
+// file : butl/standard-version -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BUTL_STANDARD_VERSION
+#define BUTL_STANDARD_VERSION
+
+#include <string>
+#include <cstdint> // uint*_t
+#include <cstddef> // size_t
+#include <ostream>
+
+#include <butl/export>
+
+namespace butl
+{
+ // The build2 "standard version":
+ //
+ // [<epoch>~]<maj>.<min>.<patch>[-(a|b).<num>[.<snapsn>[.<snapid>]]][+<rev>]
+ //
+ struct LIBBUTL_EXPORT standard_version
+ {
+ // Invariants:
+ //
+ // 1. (E == 0) == (snapshot_sn == 0 && snapshot_id.empty ())
+ //
+ // 2. snapshot_sn != latest_sn || snapshot_id.empty ()
+ //
+ static const std::uint64_t latest_sn = std::uint64_t (~0);
+
+ std::uint16_t epoch = 0; // 0 if not specified.
+ std::uint64_t version = 0; // AAABBBCCCDDDE
+ std::uint64_t snapshot_sn = 0; // 0 if not specifed, latest_sn if 'z'.
+ std::string snapshot_id; // Empty if not specified.
+ std::uint16_t revision = 0; // 0 if not specified.
+
+ std::uint16_t major () const;
+ std::uint16_t minor () const;
+ std::uint16_t patch () const;
+ std::uint16_t pre_release () const; // Note: 0 is ambiguous (-a.0.z).
+
+ // Note: return empty if the corresponding component is unspecified.
+ //
+ std::string string () const; // Package version.
+ std::string string_project () const; // Project version (no epoch/rev).
+ std::string string_version () const; // Version only (no snapshot).
+ std::string string_pre_release () const; // Pre-release part only (a.1).
+ std::string string_snapshot () const; // Snapshot part only (1234.1f23).
+
+ bool empty () const {return version == 0;}
+
+ bool alpha () const;
+ bool beta () const;
+ bool snapshot () const {return snapshot_sn != 0;}
+
+ int
+ compare (const standard_version&) const;
+
+ // Parse the version. Throw std::invalid_argument if the format is not
+ // recognizable or components are invalid.
+ //
+ explicit
+ standard_version (const std::string&);
+
+ explicit
+ standard_version (std::uint64_t version);
+
+ standard_version (std::uint64_t version, const std::string& snapshot);
+
+ standard_version (std::uint16_t epoch,
+ std::uint64_t version,
+ const std::string& snapshot,
+ std::uint16_t revision);
+
+ standard_version (std::uint16_t epoch,
+ std::uint64_t version,
+ std::uint64_t snapshot_sn,
+ std::string snapshot_id,
+ std::uint16_t revision);
+
+ // Create empty version.
+ //
+ standard_version () = default;
+
+ private:
+ void
+ parse_snapshot (const std::string&, std::size_t&);
+ };
+
+ inline bool
+ operator== (const standard_version& x, const standard_version& y)
+ {
+ return x.compare (y) == 0;
+ }
+
+ inline bool
+ operator!= (const standard_version& x, const standard_version& y)
+ {
+ return !(x == y);
+ }
+
+ inline std::ostream&
+ operator<< (std::ostream& o, const standard_version& x)
+ {
+ return o << x.string ();
+ }
+}
+
+#include <butl/standard-version.ixx>
+
+#endif // BUTL_STANDARD_VERSION
diff --git a/butl/standard-version.cxx b/butl/standard-version.cxx
new file mode 100644
index 0000000..7e144a6
--- /dev/null
+++ b/butl/standard-version.cxx
@@ -0,0 +1,333 @@
+// file : butl/standard-version.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <butl/standard-version>
+
+#include <cstdlib> // strtoull()
+#include <cstddef> // size_t
+#include <stdexcept> // invalid_argument
+
+#include <butl/utility> // alnum()
+
+using namespace std;
+
+namespace butl
+{
+ // Utility functions
+ //
+ static uint64_t
+ parse_num (const string& s, size_t& p,
+ const char* m,
+ uint64_t min = 0, uint64_t max = 999)
+ {
+ if (s[p] == '-' || s[p] == '+') // strtoull() allows these.
+ throw invalid_argument (m);
+
+ const char* b (s.c_str () + p);
+ char* e (nullptr);
+ uint64_t r (strtoull (b, &e, 10));
+
+ if (b == e || r < min || r > max)
+ throw invalid_argument (m);
+
+ p = e - s.c_str ();
+ return static_cast<uint64_t> (r);
+ }
+
+ static void
+ check_version (uint64_t version, bool snapshot)
+ {
+ // Check that the version isn't too large.
+ //
+ // AAABBBCCCDDDE
+ bool r (version < 10000000000000ULL);
+
+ // Check that E version component is consistent with the snapshot flag.
+ //
+ if (r)
+ r = (version % 10) == (snapshot ? 1 : 0);
+
+ // Check that pre-release number is consistent with the snapshot flag.
+ //
+ if (r)
+ {
+ uint64_t ab (version / 10 % 1000);
+
+ // Note that if ab is 0, it can either mean non-pre-release version in
+ // the absence of snapshot number, or 'a.0' pre-release otherwise. If ab
+ // is 500, it can only mean 'b.0', that must be followed by a snapshot
+ // number.
+ //
+ if (ab != 0)
+ r = ab != 500 || snapshot;
+ }
+
+ // Check that the major, the minor and the bugfix versions are not
+ // simultaneously zeros.
+ //
+ if (r)
+ r = (version / 10000) != 0;
+
+ if (!r)
+ throw invalid_argument ("invalid project version");
+ }
+
+ // standard_version
+ //
+ standard_version::
+ standard_version (const std::string& s)
+ {
+ auto bail = [] (const char* m) {throw invalid_argument (m);};
+
+ // Pre-parse the first component to see if the version starts with epoch,
+ // to keep the subsequent parsing straightforward.
+ //
+ bool ep (false);
+ {
+ char* e (nullptr);
+ strtoull (s.c_str (), &e, 10);
+ ep = *e == '~';
+ }
+
+ size_t p (0), n (s.size ());
+
+ if (ep)
+ {
+ epoch = parse_num (s, p, "invalid epoch", 1, uint16_t (~0));
+ ++p; // Skip '~'.
+ }
+
+ uint16_t ma, mi, bf, ab (0);
+
+ ma = parse_num (s, p, "invalid major version");
+
+ // Note that here and below p is less or equal n, and so s[p] is always
+ // valid.
+ //
+ if (s[p] != '.')
+ bail ("'.' expected after major version");
+
+ mi = parse_num (s, ++p, "invalid minor version");
+
+ if (s[p] != '.')
+ bail ("'.' expected after minor version");
+
+ bf = parse_num (s, ++p, "invalid bugfix version");
+
+ // AAABBBCCCDDDE
+ version = ma * 10000000000ULL +
+ mi * 10000000ULL +
+ bf * 10000ULL;
+
+ if (version == 0)
+ bail ("0.0.0 version");
+
+ // Parse the pre-release component if present.
+ //
+ if (s[p] == '-')
+ {
+ char k (s[++p]);
+
+ if (k != 'a' && k != 'b')
+ bail ("'a' or 'b' expected in pre-release");
+
+ if (s[++p] != '.')
+ bail ("'.' expected after pre-release letter");
+
+ ab = parse_num (s, ++p, "invalid pre-release", 0, 499);
+
+ if (k == 'b')
+ ab += 500;
+
+ // Parse the snapshot components if present. Note that pre-release number
+ // can't be zero for the final pre-release.
+ //
+ if (s[p] == '.')
+ parse_snapshot (s, ++p);
+ else if (ab == 0 || ab == 500)
+ bail ("invalid final pre-release");
+ }
+
+ if (s[p] == '+')
+ revision = parse_num (s, ++p, "invalid revision", 1, uint16_t (~0));
+
+ if (p != n)
+ bail ("junk after version");
+
+ if (ab != 0)
+ version -= 10000 - ab * 10;
+
+ if (snapshot_sn != 0)
+ version += 1;
+ }
+
+ standard_version::
+ standard_version (uint64_t v)
+ : version (v)
+ {
+ check_version (v, false);
+ }
+
+ standard_version::
+ standard_version (uint64_t v, const std::string& s)
+ : version (v)
+ {
+ bool snapshot (!s.empty ());
+ check_version (version, snapshot);
+
+ if (snapshot)
+ {
+ size_t p (0);
+ parse_snapshot (s, p);
+
+ if (p != s.size ())
+ throw invalid_argument ("junk after snapshot");
+ }
+ }
+
+ standard_version::
+ standard_version (uint16_t ep,
+ uint64_t vr,
+ uint64_t sn,
+ std::string si,
+ uint16_t rv)
+ : epoch (ep),
+ version (vr),
+ snapshot_sn (sn),
+ snapshot_id (move (si)),
+ revision (rv)
+ {
+ check_version (vr, true);
+
+ if (!snapshot_id.empty () && (snapshot_id.size () > 16 ||
+ snapshot_sn == 0 ||
+ snapshot_sn == latest_sn))
+ 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_num (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
+ {
+ std::string r;
+
+ if (alpha () || beta ())
+ {
+ uint64_t ab (version / 10 % 1000);
+
+ if (ab < 500)
+ {
+ r += "a.";
+ r += to_string (ab);
+ }
+ else
+ {
+ r += "b.";
+ r += to_string (ab - 500);
+ }
+ }
+
+ return r;
+ }
+
+ string standard_version::
+ string_version () const
+ {
+ std::string r (to_string (major ()) + '.' + to_string (minor ()) + '.' +
+ to_string (patch ()));
+
+ if (alpha () || beta ())
+ {
+ r += '-';
+ r += string_pre_release ();
+
+ if (snapshot ())
+ r += '.';
+ }
+
+ return r;
+ }
+
+ string standard_version::
+ string_snapshot () const
+ {
+ std::string r;
+
+ if (snapshot ())
+ {
+ r = snapshot_sn == latest_sn ? "z" : to_string (snapshot_sn);
+
+ if (!snapshot_id.empty ())
+ {
+ r += '.';
+ r += snapshot_id;
+ }
+ }
+
+ return r;
+ }
+
+ string standard_version::
+ string_project () const
+ {
+ std::string r (string_version ());
+
+ if (snapshot ())
+ r += string_snapshot (); // string_version() includes trailing dot.
+
+ return r;
+ }
+
+ string standard_version::
+ string () const
+ {
+ std::string r;
+
+ if (epoch != 0)
+ {
+ r = to_string (epoch);
+ r += '~';
+ }
+
+ r += string_project ();
+
+ if (revision != 0)
+ {
+ r += '+';
+ r += to_string (revision);
+ }
+
+ return r;
+ }
+}
diff --git a/butl/standard-version.ixx b/butl/standard-version.ixx
new file mode 100644
index 0000000..e414deb
--- /dev/null
+++ b/butl/standard-version.ixx
@@ -0,0 +1,94 @@
+// file : butl/standard-version.ixx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+namespace butl
+{
+ inline standard_version::
+ standard_version ( std::uint16_t e,
+ std::uint64_t v,
+ const std::string& s,
+ std::uint16_t r)
+ : standard_version (v, s)
+ {
+ // Can't initialize above due to ctor delegating.
+ //
+ epoch = e;
+ revision = r;
+ }
+
+ inline std::uint16_t standard_version::
+ major () const
+ {
+ std::uint64_t v (version / 10);
+ std::uint64_t ab (v % 1000);
+ if (ab != 0)
+ v += 1000 - ab;
+
+ return static_cast<std::uint16_t> (v / 1000000000 % 1000);
+ }
+
+ inline std::uint16_t standard_version::
+ minor () const
+ {
+ std::uint64_t v (version / 10);
+ std::uint64_t ab (v % 1000);
+ if (ab != 0)
+ v += 1000 - ab;
+
+ return static_cast<std::uint16_t> (v / 1000000 % 1000);
+ }
+
+ inline std::uint16_t standard_version::
+ patch () const
+ {
+ std::uint64_t v (version / 10);
+ std::uint64_t ab (v % 1000);
+ if (ab != 0)
+ v += 1000 - ab;
+
+ return static_cast<std::uint16_t> (v / 1000 % 1000);
+ }
+
+ inline std::uint16_t standard_version::
+ pre_release () const
+ {
+ std::uint64_t ab (version / 10 % 1000);
+ if (ab > 500)
+ ab -= 500;
+
+ return static_cast<std::uint16_t> (ab);
+ }
+
+ inline bool standard_version::
+ alpha () const
+ {
+ std::uint64_t abe (version % 10000);
+ return abe > 0 && abe < 5000;
+ }
+
+ inline bool standard_version::
+ beta () const
+ {
+ std::uint64_t abe (version % 10000);
+ return abe > 5000;
+ }
+
+ inline int standard_version::
+ compare (const standard_version& v) const
+ {
+ if (epoch != v.epoch)
+ return epoch < v.epoch ? -1 : 1;
+
+ if (version != v.version)
+ return version < v.version ? -1 : 1;
+
+ if (snapshot_sn != v.snapshot_sn)
+ return snapshot_sn < v.snapshot_sn ? -1 : 1;
+
+ if (revision != v.revision)
+ return revision < v.revision ? -1 : 1;
+
+ return 0;
+ }
+}
diff --git a/tests/standard-version/buildfile b/tests/standard-version/buildfile
new file mode 100644
index 0000000..4afb691
--- /dev/null
+++ b/tests/standard-version/buildfile
@@ -0,0 +1,7 @@
+# file : tests/tab-parser/buildfile
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+exe{driver}: cxx{driver} ../../butl/lib{butl} test{testscript}
+
+include ../../butl/
diff --git a/tests/standard-version/driver.cxx b/tests/standard-version/driver.cxx
new file mode 100644
index 0000000..1299090
--- /dev/null
+++ b/tests/standard-version/driver.cxx
@@ -0,0 +1,136 @@
+// file : tests/standard-version/driver.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <ios> // ios::failbit, ios::badbit
+#include <string>
+#include <cassert>
+#include <iostream>
+#include <stdexcept> // invalid_argument
+
+#include <butl/utility> // operator<<(ostream,exception)
+#include <butl/standard-version>
+
+using namespace std;
+using namespace butl;
+
+// Create standard version from string, and also test another ctors.
+//
+static standard_version
+version (const string& s)
+{
+ standard_version r (s);
+
+ try
+ {
+ standard_version v (r.epoch,
+ r.version,
+ r.snapshot ()
+ ? r.string_snapshot ()
+ : string (),
+ r.revision);
+
+ assert (r == v);
+
+ if (r.epoch == 0 && r.revision == 0)
+ {
+ standard_version v (r.version,
+ r.snapshot ()
+ ? r.string_snapshot ()
+ : string ());
+ assert (r == v);
+
+ if (!r.snapshot ())
+ {
+ standard_version v (r.version);
+ assert (r == v);
+ }
+ }
+
+ if (r.snapshot ())
+ {
+ standard_version v (r.epoch,
+ r.version,
+ r.snapshot_sn,
+ r.snapshot_id,
+ r.revision);
+ assert (r == v);
+ }
+
+ }
+ catch (const invalid_argument& e)
+ {
+ cerr << e << endl;
+ assert (false);
+ }
+
+ return r;
+}
+
+// Usages:
+//
+// argv[0] -a <version>
+// argv[0] -b <version>
+// argv[0] -c <version> <version>
+// argv[0]
+//
+// -a output 'y' for alpha-version, 'n' otherwise
+// -b output 'y' for beta-version, 'n' otherwise
+// -c output 0 if versions are equal, -1 if the first one is less, 1 otherwise
+//
+// If no options are specified, then create versions from STDIN lines, and
+// print them to STDOUT.
+//
+int
+main (int argc, char* argv[])
+try
+{
+ cin.exceptions (ios::badbit);
+ cout.exceptions (ios::failbit | ios::badbit);
+
+ if (argc > 1)
+ {
+ string o (argv[1]);
+
+ if (o == "-a")
+ {
+ assert (argc == 3);
+ char r (version (argv[2]).alpha ()
+ ? 'y'
+ : 'n');
+
+ cout << r << endl;
+ }
+ else if (o == "-b")
+ {
+ assert (argc == 3);
+ char r (version (argv[2]).beta ()
+ ? 'y'
+ : 'n');
+
+ cout << r << endl;
+ }
+ else if (o == "-c")
+ {
+ assert (argc == 4);
+
+ int r (version (argv[2]).compare (version (argv[3])));
+ cout << r << endl;
+ }
+ else
+ assert (false);
+
+ return 0;
+ }
+
+ string s;
+ while (getline (cin, s))
+ cout << version (s) << endl;
+
+ return 0;
+}
+catch (const invalid_argument& e)
+{
+ cerr << e << endl;
+ return 1;
+}
diff --git a/tests/standard-version/testscript b/tests/standard-version/testscript
new file mode 100644
index 0000000..335bed9
--- /dev/null
+++ b/tests/standard-version/testscript
@@ -0,0 +1,190 @@
+# file : tests/standard-version/testscript
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+: valid
+:
+: Roundtrip version.
+:
+{
+ : non-prerelease
+ :
+ $* <<EOF >>EOF
+ 1.2.3
+ EOF
+
+ : prerelease
+ :
+ {
+ : final
+ :
+ $* <<EOF >>EOF
+ 1.2.3-a.1
+ 1.2.3-b.1
+ EOF
+
+ : snapshot
+ :
+ $* <<EOF >>EOF
+ 1.2.3-a.1.z
+ 1.2.3-a.0.456
+ 1.2.3-a.1.456.340c0a26a5ef
+ EOF
+ }
+
+ : revision
+ :
+ $* <<EOF >>EOF
+ 1.2.3+4
+ 1.2.3-a.4+5
+ 1.2.3-a.4.z+5
+ 1.2.3-a.4.567+8
+ 1.2.3-a.4.567.340c0a26a5ef+8
+ EOF
+
+ : epoch
+ :
+ $* <<EOF >>EOF
+ 4~1.2.3
+ EOF
+}
+
+: invalid
+:
+{
+ : major
+ :
+ $* <'a' 2>'invalid major version' == 1
+
+ : no-major-dot
+ :
+ $* <'1' 2>"'.' expected after major version" == 1
+
+ : minor
+ :
+ $* <'1.a' 2>'invalid minor version' == 1
+
+ : no-minor-dot
+ :
+ $* <'1.2' 2>"'.' expected after minor version" == 1
+
+ : bugfix
+ :
+ $* <'1.2.a' 2>'invalid bugfix version' == 1
+
+ : zero-version
+ :
+ $* <'1~0.0.0' 2>'0.0.0 version' == 1
+
+ : a-b-expected1
+ :
+ $* <'1.2.3-' 2>"'a' or 'b' expected in pre-release" == 1
+
+ : a-b-expected2
+ :
+ $* <'1.2.3-k' 2>"'a' or 'b' expected in pre-release" == 1
+
+ : prerelease-dot-expected
+ :
+ $* <'1.2.3-a' 2>"'.' expected after pre-release letter" == 1
+
+ : prerelease
+ :
+ $* <'1.2.3-a.b' 2>'invalid pre-release' == 1
+
+ : final-prerelease
+ :
+ $* <'1.2.3-b.0' 2>'invalid final pre-release' == 1
+
+ : snapshot-num
+ :
+ $* <'1.2.3-a.1.0' 2>'invalid snapshot number' == 1
+
+ : snapshot-id
+ :
+ $* <'1.2.3-a.1.1.@' 2>'invalid snapshot id' == 1
+
+ : revision
+ :
+ {
+ : non-prerelease
+ :
+ $* <'1.2.3+a' 2>'invalid revision' == 1
+
+ : prerelease
+ :
+ $* <'1.2.3-a.1+a' 2>'invalid revision' == 1
+
+ : snapshot-num
+ :
+ $* <'1.2.3-a.0.1+a' 2>'invalid revision' == 1
+
+ : snapshot-id
+ :
+ $* <'1.2.3-a.0.1.83jdgsf+0' 2>'invalid revision' == 1
+ }
+
+ : trailing-junk-after
+ :
+ {
+ : snapshot-num
+ :
+ $* <'1.2.3-a.1.z.a' 2>'junk after version' == 1
+
+ : revision
+ :
+ $* <'1.2.3-a.1.z+1a' 2>'junk after version' == 1
+ }
+}
+
+: alpha
+:
+{
+ test.options += -a
+
+ $* '1.2.3' >n: non-prerelease
+ $* '1.2.3-b.1' >n: beta
+ $* '1.2.3-a.1' >y: final
+ $* '1.2.3-a.0.1' >y: snapshot
+}
+
+: beta
+:
+{
+ test.options += -b
+
+ $* '1.2.3' >n: non-prerelease
+ $* '1.2.3-a.1' >n: alpha
+ $* '1.2.3-b.1' >y: final
+ $* '1.2.3-b.0.1' >y: snapshot
+}
+
+: compare
+:
+{
+ test.options += -c
+
+ : epoch
+ :
+ {
+ $* '4~1.2.3' '4~1.2.3' >'0' : equal
+ $* '1.2.4' '4~1.2.3' >'-1': less
+ }
+
+ : non-prerelease
+ :
+ {
+ $* '1.2.3' '1.2.3' >'0' : equal
+ $* '1.2.3' '1.2.4' >'-1' : less
+ }
+
+ : prerelease
+ :
+ {
+ $* '1.2.3-a.1' '1.2.3-a.1' >'0' : equal
+ $* '1.2.3' '1.2.3-a.1' >'1' : release-gt-prereleas
+ $* '1.2.3-a.2' '1.2.3-b.1' >'-1' : a-lt-b
+ $* '1.2.3-a.1' '1.2.3-a.1.2' >'-1' : final-lt-snapshot
+ $* '1.2.3-a.1.2.xy' '1.2.3-a.1.2' >'0' : ignore-snapshot-id
+ }
+}